diff --git a/DEPS b/DEPS index 785b0915e9..99c5b5d917 100644 --- a/DEPS +++ b/DEPS @@ -118,7 +118,7 @@ vars = { # Three lines of non-changing comments so that # the commit queue can handle CLs rolling code_coverage # and whatever else without interference from each other. - 'code_coverage_revision': 'a81032f8d6f57774af865e502c5661cf24780ca2', + 'code_coverage_revision': '084f00efb99dc9a1726ed68cc2e05d25bc52802b', # Three lines of non-changing comments so that # the commit queue can handle CLs rolling cpu_features # and whatever else without interference from each other. @@ -228,7 +228,7 @@ vars = { # Three lines of non-changing comments so that # the commit queue can handle CLs rolling pdfium_tests # and whatever else without interference from each other. - 'pdfium_tests_revision': '86219637fec25a23f5f87252b3bd156ff9ca848e', + 'pdfium_tests_revision': 'f7a1d03ba45e874b7d3cdf722959a2b8ba4dbfa5', # Three lines of non-changing comments so that # the commit queue can handle CLs rolling result_adapter_revision # and whatever else without interference from each other. diff --git a/core/fpdfapi/page/cpdf_page.h b/core/fpdfapi/page/cpdf_page.h index 5cb6eb25cb..70990827aa 100644 --- a/core/fpdfapi/page/cpdf_page.h +++ b/core/fpdfapi/page/cpdf_page.h @@ -52,9 +52,10 @@ class CPDF_Page final : public IPDF_Page, public CPDF_PageObjectHolder { virtual ~RenderContextIface() = default; }; - class RenderContextClearer { + class [[maybe_unused, nodiscard]] RenderContextClearer { public: FX_STACK_ALLOCATED(); + explicit RenderContextClearer(CPDF_Page* pPage); ~RenderContextClearer(); diff --git a/core/fpdfapi/parser/cpdf_document.h b/core/fpdfapi/parser/cpdf_document.h index 346705d960..497737e5fc 100644 --- a/core/fpdfapi/parser/cpdf_document.h +++ b/core/fpdfapi/parser/cpdf_document.h @@ -357,7 +357,7 @@ class CPDF_Document : public Observable, private: // EmbedPDF(measurement): see GetMeasureStorage(). std::unique_ptr measure_storage_; - class StockFontClearer { + class [[maybe_unused, nodiscard]] StockFontClearer { public: FX_STACK_ALLOCATED(); diff --git a/core/fpdfapi/parser/cpdf_read_validator.h b/core/fpdfapi/parser/cpdf_read_validator.h index 86f41a770a..42370cbbb4 100644 --- a/core/fpdfapi/parser/cpdf_read_validator.h +++ b/core/fpdfapi/parser/cpdf_read_validator.h @@ -13,7 +13,7 @@ class CPDF_ReadValidator final : public IFX_SeekableReadStream { public: - class ScopedSession { + class [[maybe_unused, nodiscard]] ScopedSession { public: FX_STACK_ALLOCATED(); diff --git a/core/fpdfapi/render/cpdf_rendershading.cpp b/core/fpdfapi/render/cpdf_rendershading.cpp index 900481dbb9..11a8406dad 100644 --- a/core/fpdfapi/render/cpdf_rendershading.cpp +++ b/core/fpdfapi/render/cpdf_rendershading.cpp @@ -139,24 +139,30 @@ void DrawAxialShading(const RetainPtr& pBitmap, float x_span = end_x - start_x; float y_span = end_y - start_y; float axis_len_square = (x_span * x_span) + (y_span * y_span); + if (axis_len_square == 0.0f) { + return; + } std::array shading_steps; if (!GetShadingSteps(t_min, t_max, funcs, pCS, alpha, &shading_steps)) { return; } - CFX_Matrix matrix = mtObject2Bitmap.GetInverse(); + const CFX_Matrix matrix = mtObject2Bitmap.GetInverse(); + const float delta_num = (matrix.a * x_span) + (matrix.b * y_span); + const float delta_scale = delta_num / axis_len_square; for (int row = 0; row < height; row++) { auto dest_buf = pBitmap->GetWritableScanlineAs(row).first( static_cast(width)); - size_t column_counter = 0; + const CFX_PointF pos = + matrix.Transform(CFX_PointF(0.0f, static_cast(row))); + const float num_0 = + ((pos.x - start_x) * x_span) + ((pos.y - start_y) * y_span); + const float scale_0 = num_0 / axis_len_square; + float column = 0.0f; for (auto& pix : dest_buf) { - const float column = static_cast(column_counter++); - const CFX_PointF pos = - matrix.Transform(CFX_PointF(column, static_cast(row))); - float scale = - (((pos.x - start_x) * x_span) + ((pos.y - start_y) * y_span)) / - axis_len_square; + const float scale = scale_0 + (column * delta_scale); + ++column; int index = static_cast(scale * (kShadingSteps - 1)); if (index < 0) { if (!bStartExtend) { diff --git a/core/fpdfdoc/cpdf_formfield_unittest.cpp b/core/fpdfdoc/cpdf_formfield_unittest.cpp index 91562b6f30..8e26959045 100644 --- a/core/fpdfdoc/cpdf_formfield_unittest.cpp +++ b/core/fpdfdoc/cpdf_formfield_unittest.cpp @@ -27,7 +27,7 @@ namespace { // Create and destroys the page module that is necessary when instantiating a // CPDF_Document. -class ScopedCPDF_PageModule { +class [[maybe_unused, nodiscard]] ScopedCPDF_PageModule { public: FX_STACK_ALLOCATED(); diff --git a/core/fxcrt/autonuller.h b/core/fxcrt/autonuller.h index 01c24d49dc..335bbcf6eb 100644 --- a/core/fxcrt/autonuller.h +++ b/core/fxcrt/autonuller.h @@ -11,7 +11,7 @@ namespace fxcrt { template -class AutoNuller { +class [[maybe_unused, nodiscard]] AutoNuller { public: FX_STACK_ALLOCATED(); diff --git a/core/fxcrt/autorestorer.h b/core/fxcrt/autorestorer.h index 367b9bf825..69217ed2f6 100644 --- a/core/fxcrt/autorestorer.h +++ b/core/fxcrt/autorestorer.h @@ -5,27 +5,31 @@ #ifndef CORE_FXCRT_AUTORESTORER_H_ #define CORE_FXCRT_AUTORESTORER_H_ +#include +#include + #include "core/fxcrt/fx_memory.h" #include "core/fxcrt/unowned_ptr.h" namespace fxcrt { template -class AutoRestorer { +class [[maybe_unused, nodiscard]] AutoRestorer { public: FX_STACK_ALLOCATED(); explicit AutoRestorer(T* location) : location_(location), old_value_(*location) {} - AutoRestorer(T* location, const T& new_value) - : location_(location), old_value_(*location) { - *location = new_value; - } + template + requires std::is_assignable_v + AutoRestorer(T* location, U&& new_value) + : location_(location), + old_value_(std::exchange(*location, std::forward(new_value))) {} ~AutoRestorer() { if (location_) { - *location_ = old_value_; + *location_ = std::move(old_value_); } } @@ -33,7 +37,7 @@ class AutoRestorer { private: UnownedPtr location_; - const T old_value_; + T old_value_; }; } // namespace fxcrt diff --git a/core/fxcrt/autorestorer_unittest.cpp b/core/fxcrt/autorestorer_unittest.cpp index dfa6e849e7..a5de9993a9 100644 --- a/core/fxcrt/autorestorer_unittest.cpp +++ b/core/fxcrt/autorestorer_unittest.cpp @@ -3,6 +3,10 @@ // found in the LICENSE file. #include "core/fxcrt/autorestorer.h" + +#include +#include + #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/unowned_ptr.h" #include "testing/gtest/include/gtest/gtest.h" @@ -52,6 +56,11 @@ TEST(fxcrt, AutoRestorerUnownedPtr) { EXPECT_EQ(&y, ptr); } EXPECT_EQ(&x, ptr); + { + AutoRestorer> restorer(&ptr, &y); + EXPECT_EQ(&y, ptr); + } + EXPECT_EQ(&x, ptr); } TEST(fxcrt, AutoRestorerUnownedNoOp) { @@ -80,35 +89,140 @@ TEST(fxcrt, AutoRestorerUnownedPtrAbandon) { TEST(fxcrt, AutoRestorerRetainPtr) { PseudoRetainable obj1; PseudoRetainable obj2; - RetainPtr ptr(&obj1); - EXPECT_EQ(1, obj1.retain_count()); - EXPECT_EQ(0, obj1.release_count()); { - AutoRestorer> restorer(&ptr); - // |obj1| is kept alive by restorer in case it need to be restored. - EXPECT_EQ(2, obj1.retain_count()); + RetainPtr ptr(&obj1); + EXPECT_EQ(1, obj1.retain_count()); EXPECT_EQ(0, obj1.release_count()); - ptr.Reset(&obj2); - EXPECT_EQ(&obj2, ptr.Get()); + { + AutoRestorer> restorer(&ptr); + // `obj1` is kept alive by restorer in case it need to be restored. + EXPECT_EQ(2, obj1.retain_count()); + EXPECT_EQ(0, obj1.release_count()); + ptr.Reset(&obj2); + EXPECT_EQ(&obj2, ptr.Get()); + + // `obj1` released by `ptr` assignment, but kept alive by `AutoRestorer`'s + // `old_value_`. + EXPECT_TRUE(obj1.alive()); + EXPECT_EQ(2, obj1.retain_count()); + EXPECT_EQ(1, obj1.release_count()); + + // `obj2` now kept alive by `ptr`. + EXPECT_TRUE(obj1.alive()); + EXPECT_EQ(1, obj2.retain_count()); + EXPECT_EQ(0, obj2.release_count()); + } + EXPECT_EQ(&obj1, ptr.Get()); - // |obj1| released by |ptr| assignment. + // Moving from `AutoRestorer`'s `old_value_` into `ptr` avoided ref churn. EXPECT_TRUE(obj1.alive()); EXPECT_EQ(2, obj1.retain_count()); EXPECT_EQ(1, obj1.release_count()); - // |obj2| now kept alive by |ptr|. - EXPECT_TRUE(obj1.alive()); + // `obj2` dead. + EXPECT_FALSE(obj2.alive()); + EXPECT_EQ(1, obj2.retain_count()); + EXPECT_EQ(1, obj2.release_count()); + } + EXPECT_FALSE(obj1.alive()); + EXPECT_EQ(2, obj1.retain_count()); + EXPECT_EQ(2, obj1.release_count()); + EXPECT_FALSE(obj2.alive()); + EXPECT_EQ(1, obj2.retain_count()); + EXPECT_EQ(1, obj2.release_count()); +} + +TEST(fxcrt, AutoRestorerRetainPtrCopy) { + PseudoRetainable obj1; + PseudoRetainable obj2; + { + RetainPtr ptr1(&obj1); + RetainPtr ptr2(&obj2); + EXPECT_EQ(1, obj1.retain_count()); + EXPECT_EQ(0, obj1.release_count()); EXPECT_EQ(1, obj2.retain_count()); EXPECT_EQ(0, obj2.release_count()); + { + AutoRestorer> restorer(&ptr1, ptr2); + EXPECT_EQ(&obj2, ptr1.Get()); + EXPECT_EQ(&obj2, ptr2.Get()); + + // `obj1` was moved into `restorer`'s `old_value_` without ref churn, + // where it is kept alive until restoration. + EXPECT_TRUE(obj1.alive()); + EXPECT_EQ(1, obj1.retain_count()); + EXPECT_EQ(0, obj1.release_count()); + + // `obj2` was copied into `ptr1`, incrementing its retain count. + EXPECT_TRUE(obj2.alive()); + EXPECT_EQ(2, obj2.retain_count()); + EXPECT_EQ(0, obj2.release_count()); + } + EXPECT_EQ(&obj1, ptr1.Get()); + EXPECT_EQ(&obj2, ptr2.Get()); + + // `obj1` moved back to `ptr1` on destruction, with zero ref churn. + EXPECT_TRUE(obj1.alive()); + EXPECT_EQ(1, obj1.retain_count()); + EXPECT_EQ(0, obj1.release_count()); + + // `ptr1`'s copy of `obj2` was released when overwritten by restoration. + EXPECT_TRUE(obj2.alive()); + EXPECT_EQ(2, obj2.retain_count()); + EXPECT_EQ(1, obj2.release_count()); } - EXPECT_EQ(&obj1, ptr.Get()); + EXPECT_FALSE(obj1.alive()); + EXPECT_EQ(1, obj1.retain_count()); + EXPECT_EQ(1, obj1.release_count()); + EXPECT_FALSE(obj2.alive()); + EXPECT_EQ(2, obj2.retain_count()); + EXPECT_EQ(2, obj2.release_count()); +} - // |obj1| now kept alive again by |ptr|. - EXPECT_TRUE(obj1.alive()); - EXPECT_EQ(3, obj1.retain_count()); - EXPECT_EQ(2, obj1.release_count()); +TEST(fxcrt, AutoRestorerRetainPtrMove) { + PseudoRetainable obj1; + PseudoRetainable obj2; + { + RetainPtr ptr1(&obj1); + RetainPtr ptr2(&obj2); + EXPECT_EQ(1, obj1.retain_count()); + EXPECT_EQ(0, obj1.release_count()); + EXPECT_EQ(1, obj2.retain_count()); + EXPECT_EQ(0, obj2.release_count()); + { + AutoRestorer> restorer(&ptr1, + std::move(ptr2)); + EXPECT_EQ(&obj2, ptr1.Get()); + EXPECT_FALSE(ptr2); + + // `obj1` was moved into `restorer`'s `old_value_` without ref churn, + // where it is kept alive until restoration. + EXPECT_TRUE(obj1.alive()); + EXPECT_EQ(1, obj1.retain_count()); + EXPECT_EQ(0, obj1.release_count()); + + // `ptr2` was moved into `ptr1` without ref churn. + EXPECT_TRUE(obj2.alive()); + EXPECT_EQ(1, obj2.retain_count()); + EXPECT_EQ(0, obj2.release_count()); + } + EXPECT_EQ(&obj1, ptr1.Get()); + EXPECT_FALSE(ptr2); + + // `obj1` moved back to `ptr1` on destruction, with zero ref churn. + EXPECT_TRUE(obj1.alive()); + EXPECT_EQ(1, obj1.retain_count()); + EXPECT_EQ(0, obj1.release_count()); - // |obj2| dead. + // `ptr1`'s sole reference to `obj2` was released on restoration, so `obj2` + // dies. + EXPECT_FALSE(obj2.alive()); + EXPECT_EQ(1, obj2.retain_count()); + EXPECT_EQ(1, obj2.release_count()); + } + EXPECT_FALSE(obj1.alive()); + EXPECT_EQ(1, obj1.retain_count()); + EXPECT_EQ(1, obj1.release_count()); EXPECT_FALSE(obj2.alive()); EXPECT_EQ(1, obj2.retain_count()); EXPECT_EQ(1, obj2.release_count()); @@ -116,55 +230,91 @@ TEST(fxcrt, AutoRestorerRetainPtr) { TEST(fxcrt, AutoRestorerRetainPtrNoOp) { PseudoRetainable obj1; - RetainPtr ptr(&obj1); - EXPECT_EQ(1, obj1.retain_count()); - EXPECT_EQ(0, obj1.release_count()); { - AutoRestorer> restorer(&ptr); - EXPECT_EQ(2, obj1.retain_count()); + RetainPtr ptr(&obj1); + EXPECT_EQ(1, obj1.retain_count()); EXPECT_EQ(0, obj1.release_count()); - } - EXPECT_EQ(&obj1, ptr.Get()); + { + AutoRestorer> restorer(&ptr); + EXPECT_EQ(2, obj1.retain_count()); + EXPECT_EQ(0, obj1.release_count()); + } + EXPECT_EQ(&obj1, ptr.Get()); - // Self-reassignement avoided ref churn. - EXPECT_TRUE(obj1.alive()); + // Self-reassignement avoided ref churn. + EXPECT_TRUE(obj1.alive()); + EXPECT_EQ(2, obj1.retain_count()); + EXPECT_EQ(1, obj1.release_count()); + } + EXPECT_FALSE(obj1.alive()); EXPECT_EQ(2, obj1.retain_count()); - EXPECT_EQ(1, obj1.release_count()); + EXPECT_EQ(2, obj1.release_count()); } TEST(fxcrt, AutoRestorerRetainPtrAbandon) { PseudoRetainable obj1; PseudoRetainable obj2; - RetainPtr ptr(&obj1); - EXPECT_EQ(1, obj1.retain_count()); - EXPECT_EQ(0, obj1.release_count()); { - AutoRestorer> restorer(&ptr); - // |obj1| is kept alive by restorer in case it need to be restored. - EXPECT_EQ(2, obj1.retain_count()); + RetainPtr ptr(&obj1); + EXPECT_EQ(1, obj1.retain_count()); EXPECT_EQ(0, obj1.release_count()); - ptr.Reset(&obj2); + { + AutoRestorer> restorer(&ptr); + // `obj1` is kept alive by restorer in case it need to be restored. + EXPECT_EQ(2, obj1.retain_count()); + EXPECT_EQ(0, obj1.release_count()); + ptr.Reset(&obj2); + EXPECT_EQ(&obj2, ptr.Get()); + + // `obj1` released by `ptr` assignment. + EXPECT_EQ(2, obj1.retain_count()); + EXPECT_EQ(1, obj1.release_count()); + + // `obj2` now kept alive by `ptr`. + EXPECT_EQ(1, obj2.retain_count()); + EXPECT_EQ(0, obj2.release_count()); + + restorer.AbandonRestoration(); + } EXPECT_EQ(&obj2, ptr.Get()); - // |obj1| released by |ptr| assignment. + // `obj1` now dead as a result of abandonment. + EXPECT_FALSE(obj1.alive()); EXPECT_EQ(2, obj1.retain_count()); - EXPECT_EQ(1, obj1.release_count()); + EXPECT_EQ(2, obj1.release_count()); - // |obj2| now kept alive by |ptr|. + // `obj2` kept alive by `ptr`. + EXPECT_TRUE(obj2.alive()); EXPECT_EQ(1, obj2.retain_count()); EXPECT_EQ(0, obj2.release_count()); - - restorer.AbandonRestoration(); } - EXPECT_EQ(&obj2, ptr.Get()); - - // |obj1| now dead as a result of abandonment. EXPECT_FALSE(obj1.alive()); EXPECT_EQ(2, obj1.retain_count()); EXPECT_EQ(2, obj1.release_count()); - - // |obj2| kept alive by |ptr|. - EXPECT_TRUE(obj2.alive()); + EXPECT_FALSE(obj2.alive()); EXPECT_EQ(1, obj2.retain_count()); - EXPECT_EQ(0, obj2.release_count()); + EXPECT_EQ(1, obj2.release_count()); +} + +TEST(fxcrt, AutoRestorerMoveOnlyType) { + auto ptr1 = std::make_unique(5); + auto ptr2 = std::make_unique(6); + { + AutoRestorer> restorer(&ptr1, std::move(ptr2)); + EXPECT_FALSE(ptr2); + EXPECT_EQ(6, *ptr1); + } + EXPECT_EQ(5, *ptr1); +} + +TEST(fxcrt, AutoRestorerMoveOnlyTypeAbandon) { + auto ptr1 = std::make_unique(5); + auto ptr2 = std::make_unique(6); + { + AutoRestorer> restorer(&ptr1, std::move(ptr2)); + EXPECT_FALSE(ptr2); + EXPECT_EQ(6, *ptr1); + restorer.AbandonRestoration(); + } + EXPECT_EQ(6, *ptr1); } diff --git a/core/fxge/BUILD.gn b/core/fxge/BUILD.gn index 2b555f4f52..d0b9e2029f 100644 --- a/core/fxge/BUILD.gn +++ b/core/fxge/BUILD.gn @@ -340,6 +340,10 @@ pdfium_unittest_source_set("unittests") { sources += [ "win32/cfx_psrenderer_unittest.cpp" ] } + if (pdf_use_agg) { + sources += [ "agg/cfx_agg_cliprgn_unittest.cpp" ] + } + if (pdf_enable_fontations) { sources += [ "skrifa/fx_skrifa_unittest.cpp" ] deps += [ diff --git a/core/fxge/agg/cfx_agg_cliprgn.cpp b/core/fxge/agg/cfx_agg_cliprgn.cpp index 931f5dbbcb..dcb66343da 100644 --- a/core/fxge/agg/cfx_agg_cliprgn.cpp +++ b/core/fxge/agg/cfx_agg_cliprgn.cpp @@ -12,6 +12,7 @@ #include "core/fxcrt/check_op.h" #include "core/fxcrt/stl_util.h" +#include "core/fxcrt/zip.h" #include "core/fxge/dib/cfx_dibitmap.h" CFX_AggClipRgn::CFX_AggClipRgn(int width, int height) @@ -51,16 +52,22 @@ void CFX_AggClipRgn::IntersectMask(int left, box_ = new_box; return; } + const int width = new_box.Width(); auto new_dib = pdfium::MakeRetain(); - CHECK(new_dib->Create(new_box.Width(), new_box.Height(), - FXDIB_Format::k8bppMask)); + CHECK(new_dib->Create(width, new_box.Height(), FXDIB_Format::k8bppMask)); + const size_t width_size = static_cast(width); + const size_t old_offset = static_cast(new_box.left - box_.left); + const size_t mask_offset = static_cast(new_box.left - left); for (int row = new_box.top; row < new_box.bottom; row++) { - pdfium::span old_scan = mask_->GetScanline(row - box_.top); - pdfium::span mask_scan = pMask->GetScanline(row - top); - auto new_scan = new_dib->GetWritableScanline(row - new_box.top); - for (int col = new_box.left; col < new_box.right; col++) { - new_scan[col - new_box.left] = - old_scan[col - box_.left] * mask_scan[col - left] / 255; + pdfium::span old_scan = + mask_->GetScanline(row - box_.top).subspan(old_offset, width_size); + pdfium::span mask_scan = + pMask->GetScanline(row - top).subspan(mask_offset); + pdfium::span new_scan = + new_dib->GetWritableScanline(row - new_box.top); + for (auto [old_val, mask_val, new_val] : + fxcrt::Zip(old_scan, mask_scan, new_scan)) { + new_val = old_val * mask_val / 255; } } box_ = new_box; diff --git a/core/fxge/agg/cfx_agg_cliprgn_unittest.cpp b/core/fxge/agg/cfx_agg_cliprgn_unittest.cpp new file mode 100644 index 0000000000..1b5c82bba1 --- /dev/null +++ b/core/fxge/agg/cfx_agg_cliprgn_unittest.cpp @@ -0,0 +1,174 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "core/fxge/agg/cfx_agg_cliprgn.h" + +#include + +#include +#include + +#include "core/fxcrt/fx_coordinates.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "core/fxge/dib/cfx_dibitmap.h" +#include "core/fxge/dib/fx_dib.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +RetainPtr CreateMask(int width, int height, uint8_t fill_val) { + auto bitmap = pdfium::MakeRetain(); + if (!bitmap->Create(width, height, FXDIB_Format::k8bppMask)) { + return nullptr; + } + std::ranges::fill(bitmap->GetWritableBuffer(), fill_val); + return bitmap; +} + +RetainPtr CreateGradientMask(int width, int height) { + auto bitmap = pdfium::MakeRetain(); + if (!bitmap->Create(width, height, FXDIB_Format::k8bppMask)) { + return nullptr; + } + for (int row = 0; row < height; ++row) { + auto scan = bitmap->GetWritableScanline(row); + for (int col = 0; col < width; ++col) { + // Give each pixel a unique value based on its row and column (for masks + // up to 10x10) so offset bugs are caught, starting at 100 so multiplying + // two masks and dividing by 255 does not round down to 0. + scan[col] = static_cast(100 + row * 10 + col); + } + } + return bitmap; +} + +} // namespace + +// Intersecting disjoint rects produces an empty clip box, and subsequent +// IntersectMask() calls on an empty clip leave GetMask() null. +TEST(CFX_AggClipRgnTest, EmptyClip) { + CFX_AggClipRgn clip(10, 10); + EXPECT_EQ(FX_RECT(0, 0, 10, 10), clip.GetBox()); + EXPECT_FALSE(clip.GetMask()); + + clip.IntersectRect(FX_RECT(20, 20, 30, 30)); + EXPECT_TRUE(clip.GetBox().IsEmpty()); + EXPECT_FALSE(clip.GetMask()); + + auto mask = CreateMask(5, 5, 255); + clip.IntersectMask(0, 0, mask); + EXPECT_TRUE(clip.GetBox().IsEmpty()); + EXPECT_FALSE(clip.GetMask()); +} + +// When no mask exists yet and the incoming mask lies completely within +// GetBox(), IntersectMask() adopts the bitmap directly without copying. +TEST(CFX_AggClipRgnTest, InitialMaskMatchingBox) { + CFX_AggClipRgn clip(5, 5); + auto mask = CreateMask(5, 5, 128); + clip.IntersectMask(0, 0, mask); + + EXPECT_EQ(FX_RECT(0, 0, 5, 5), clip.GetBox()); + ASSERT_TRUE(clip.GetMask()); + EXPECT_EQ(mask, clip.GetMask()); + EXPECT_EQ(128, clip.GetMask()->GetScanline(0)[0]); +} + +// When no mask exists yet and the incoming mask extends outside GetBox(), +// IntersectMask() allocates a new bitmap cropped to the intersection. +TEST(CFX_AggClipRgnTest, InitialMaskSubBox) { + CFX_AggClipRgn clip(4, 4); + clip.IntersectRect(FX_RECT(1, 2, 4, 4)); + auto mask = CreateGradientMask(4, 4); + clip.IntersectMask(0, 0, mask); + + EXPECT_EQ(FX_RECT(1, 2, 4, 4), clip.GetBox()); + ASSERT_TRUE(clip.GetMask()); + EXPECT_NE(mask, clip.GetMask()); + EXPECT_EQ(3, clip.GetMask()->GetWidth()); + EXPECT_EQ(2, clip.GetMask()->GetHeight()); + for (int row = 0; row < 2; ++row) { + auto actual_scan = clip.GetMask()->GetScanline(row); + auto expected_scan = mask->GetScanline(row + 2); + for (int col = 0; col < 3; ++col) { + EXPECT_EQ(expected_scan[col + 1], actual_scan[col]); + } + } +} + +// Intersecting two masks with identical bounds multiplies pixel values as +// (old_val * new_val) / 255. +TEST(CFX_AggClipRgnTest, MatchingBoxMaskIntersection) { + CFX_AggClipRgn clip(4, 4); + auto mask1 = CreateMask(4, 4, 200); + clip.IntersectMask(0, 0, std::move(mask1)); + + ASSERT_TRUE(clip.GetMask()); + EXPECT_EQ(200, clip.GetMask()->GetScanline(0)[0]); + + auto mask2 = CreateMask(4, 4, 100); + clip.IntersectMask(0, 0, mask2); + + // 200 * 100 / 255 = 78. + ASSERT_TRUE(clip.GetMask()); + for (int row = 0; row < 4; ++row) { + auto scan = clip.GetMask()->GetScanline(row); + for (int col = 0; col < 4; ++col) { + EXPECT_EQ(78, scan[col]); + } + } +} + +// Copying a CFX_AggClipRgn shares the underlying mask bitmap, so a subsequent +// IntersectMask() call allocates a new bitmap without mutating the copy. +TEST(CFX_AggClipRgnTest, SavedClipPreserved) { + CFX_AggClipRgn clip(4, 4); + auto mask1 = CreateMask(4, 4, 255); + clip.IntersectMask(0, 0, mask1); + + // Copy clip (simulating SaveState). + CFX_AggClipRgn saved_clip = clip; + + auto mask2 = CreateMask(4, 4, 128); + clip.IntersectMask(0, 0, mask2); + + // The saved clip must not be modified. + ASSERT_TRUE(saved_clip.GetMask()); + EXPECT_EQ(255, saved_clip.GetMask()->GetScanline(0)[0]); + + // The modified clip must have the multiplied result. + ASSERT_TRUE(clip.GetMask()); + EXPECT_EQ(128, clip.GetMask()->GetScanline(0)[0]); + EXPECT_NE(saved_clip.GetMask(), clip.GetMask()); +} + +// Intersecting partially overlapping gradient masks exercises non-zero row and +// column offsets into both the existing mask and the incoming mask. +TEST(CFX_AggClipRgnTest, ShrinkingMaskIntersection) { + CFX_AggClipRgn clip(4, 4); + auto mask1 = CreateGradientMask(3, 3); + clip.IntersectMask(0, 1, mask1); + + // Overlaps (0, 1, 3, 4) with (1, 0, 4, 3) -> (1, 1, 3, 3). + auto mask2 = CreateGradientMask(3, 3); + clip.IntersectMask(1, 0, mask2); + + EXPECT_EQ(FX_RECT(1, 1, 3, 3), clip.GetBox()); + ASSERT_TRUE(clip.GetMask()); + EXPECT_EQ(2, clip.GetMask()->GetWidth()); + EXPECT_EQ(2, clip.GetMask()->GetHeight()); + + // The intersection starts at (1, 1), which is offset by col=1 in `mask1` at + // (0, 1) and by row=1 in `mask2` at (1, 0). + for (int row = 0; row < 2; ++row) { + auto scan = clip.GetMask()->GetScanline(row); + auto scan1 = mask1->GetScanline(row); + auto scan2 = mask2->GetScanline(row + 1); + for (int col = 0; col < 2; ++col) { + uint8_t expected = (scan1[col + 1] * scan2[col]) / 255; + EXPECT_EQ(expected, scan[col]); + } + } +} diff --git a/core/fxge/cfx_face.cpp b/core/fxge/cfx_face.cpp index eca14f58ea..fcf6160db0 100644 --- a/core/fxge/cfx_face.cpp +++ b/core/fxge/cfx_face.cpp @@ -455,7 +455,7 @@ FT_Render_Mode FtRenderModeFromFontAntiAliasingMode( // Sets the given transform on the FaceRec, and resets it to the identity when // it goes out of scope. -class ScopedFaceTransform { +class [[maybe_unused, nodiscard]] ScopedFaceTransform { public: FX_STACK_ALLOCATED(); diff --git a/core/fxge/cfx_fontmapper.cpp b/core/fxge/cfx_fontmapper.cpp index 2477d8d0a6..cd422d03b6 100644 --- a/core/fxge/cfx_fontmapper.cpp +++ b/core/fxge/cfx_fontmapper.cpp @@ -313,7 +313,7 @@ uint32_t GetChecksumFromTT(pdfium::span font_data) { return checksum; } -class ScopedFontDeleter { +class [[maybe_unused, nodiscard]] ScopedFontDeleter { public: FX_STACK_ALLOCATED(); diff --git a/core/fxge/cfx_path_unittest.cpp b/core/fxge/cfx_path_unittest.cpp index 688fab13e8..799889d8b2 100644 --- a/core/fxge/cfx_path_unittest.cpp +++ b/core/fxge/cfx_path_unittest.cpp @@ -77,6 +77,31 @@ TEST(CFXPath, ShearTransform) { EXPECT_EQ(CFX_FloatRect(1, 2, 3, 5), path.GetBoundingBox()); } +TEST(CFXPath, MicroShearTransformTolerance) { + CFX_Path path; + path.AppendRect(/*left=*/1, /*bottom=*/2, /*right=*/3, /*top=*/5); + + const CFX_Matrix kMicroDeviationMatrix(1.0f, 0.0f, -4.69486423e-08f, 1.0f, + 0.0f, 0.0f); + EXPECT_TRUE(path.IsRect()); + // TODO(crbug.com/550515869): Update expectation to CFX_FloatRect(1, 2, 3, 5) + // once micro-shear tolerance is supported. + EXPECT_FALSE(path.GetRect(&kMicroDeviationMatrix).has_value()); +} + +TEST(CFXPath, FivePointRectFuzzyEndpoints) { + CFX_Path path; + path.AppendPoint({0, 0}, CFX_Path::Point::Type::kMove); + path.AppendPoint({2, 0}, CFX_Path::Point::Type::kLine); + path.AppendPoint({2, 1}, CFX_Path::Point::Type::kLine); + path.AppendPoint({0, 1}, CFX_Path::Point::Type::kLine); + path.AppendPoint({1e-7f, -1e-7f}, CFX_Path::Point::Type::kLine); + // TODO(crbug.com/550515869): Update expectations to true and + // CFX_FloatRect(0, 0, 2, 1) once fuzzy endpoint matching is supported. + EXPECT_FALSE(path.IsRect()); + EXPECT_FALSE(path.GetRect(nullptr).has_value()); +} + TEST(CFXPath, Hexagon) { CFX_Path path; path.AppendPoint({1, 0}, CFX_Path::Point::Type::kMove); diff --git a/docs/embedpdf/syncs/2026-10-28-2b2d56c79.md b/docs/embedpdf/syncs/2026-10-28-2b2d56c79.md new file mode 100644 index 0000000000..82b363fd3b --- /dev/null +++ b/docs/embedpdf/syncs/2026-10-28-2b2d56c79.md @@ -0,0 +1,117 @@ +# Sync 2026-10-28: upstream 7b3f6f936..2b2d56c79 (8 commits) + +## Summary + +Upstream from 2026-10-25 to 2026-10-26, merged into `4987bc930`. + +Upstream sped up axial shadings (incremental stepping, 4-6x on gradient-heavy pages, with +slightly different pixels) and AGG clip masks, guarded a zero-length gradient axis, and marked +RAII helpers `[[maybe_unused, nodiscard]]`. One conflict, next to our measurement storage. + +## Likely security fixes + +None. + +## Conflicts (1 files, 1 blocks) + +A block with an `EPDF_TLS` count is never resolved by taking upstream's side without putting `EPDF_TLS` back (rule thread-confined-globals). + +| File | Blocks | Rules in the blocks | EPDF_TLS | Upstream commits | Resolution | +| :-- | --: | :-- | --: | :-- | :-- | +| `core/fpdfapi/parser/cpdf_document.h` | 1 | layer-save, layer-transaction, measurement | | `60c86bb41` Mark several RAII classes as [[maybe_unused, nodiscard]]. | Both: our `measure_storage_` member (measurement), then upstream's attributed `StockFontClearer`. | + +## Write audit (0 lines) + +None. + +## Upstream changes outside the conflicts + +- **Axial shadings (`2f8880782`)** step the gradient coordinate once per pixel instead of + transforming each pixel: 4-6x faster on gradient-heavy pages. The exact math moves some gradient + pixels slightly; upstream updated its corpus goldens (`shade_expected*.png`, via the + `pdfium_tests` roll). Engine render expectations with axial shadings may move the same way when + the engine adopts this runtime. +- **Zero-length gradient axis (`93ba3fc5e`)** returns early instead of dividing by zero. +- **AGG clip masks (`4a72657f7`)** intersect row by row, 40% less render CPU on upstream's test + page; same output. New unit tests (`19b828e9a`, `2b2d56c79`) pass. +- **`[[maybe_unused, nodiscard]]` on RAII helpers (`60c86bb41`)** and move support in + `AutoRestorer` (`97f8540a9`); merged into our `cpdf_document.h`, `cpdf_page.h`, + `cpdf_read_validator.h`, `cfx_face.cpp` and `cfx_fontmapper.cpp`. +- None of these files had render-speedup changes of ours: upstream's `cpdf_rendershading.cpp` and + `cfx_agg_cliprgn.cpp` are taken as they are. + +## Upstream appearance generator + +None. + +## Globals + +No new globals by the heuristic. `check-globals.py` on the darwin-arm64 release build: ok (263 shared +writable, 25 thread-local). + +## Build and patches + +- `DEPS` changed in 2 commits: code coverage tooling (`09c6ccf13`) and the `pdfium_tests` corpus + (`2f8880782`); neither is in our builds. Synced with `gclient sync --no-history --shallow`. +- All of `patches/embedpdf-runtime/` apply: every target's patches dry-run against clean sources, + and darwin-arm64 and wasm32 build. + +## API diff + +None. The darwin-arm64 exports match the public headers, as in S13. + +## Tests + +- [x] fork unit + embedder tests (darwin-arm64): 1303 unit tests pass, upstream's new clip-region, + path and `AutoRestorer` tests included. Embedder: 1690 tests, 9 skipped, 30 failing: exactly + S13's list, every one "Pixels do not match" against a Mac PNG golden. Linux runs in the PR's CI. + The 30, as the baseline for the next sync: + - `EPDFMeasureEmbedderTest.CaptionAppearanceGolden` + - `FPDFAnnotEmbedderTest.AddAndModifyImage` + - `FPDFAnnotEmbedderTest.AddAndModifyPath` + - `FPDFAnnotEmbedderTest.AddAndModifyText` + - `FPDFAnnotEmbedderTest.AnnotationBorderRendering` + - `FPDFAnnotEmbedderTest.GetSetStringValue` + - `FPDFAnnotEmbedderTest.ModifyRectQuadpointsWithAP` + - `FPDFAnnotEmbedderTest.SetFontColor` + - `FPDFEditEmbedderTest.AddExistingMarkCompressedStream` + - `FPDFEditEmbedderTest.AddMarkCompressedStream` + - `FPDFEditEmbedderTest.Bug1574` + - `FPDFEditEmbedderTest.Bug1893` + - `FPDFEditEmbedderTest.Bug461845674` + - `FPDFEditEmbedderTest.GetAndSetMatrixForFormWithText` + - `FPDFEditEmbedderTest.ModifyFormObject` + - `FPDFEditEmbedderTest.PathOnTopOfText` + - `FPDFEditEmbedderTest.RemoveMarkedObjectsPrime` + - `FPDFEditEmbedderTest.SetText` + - `FPDFEditEmbedderTest.SetTextKeepClippingPath` + - `FPDFEditEmbedderTest.SetTextRenderMode` + - `FPDFProgressiveRenderEmbedderTest.RenderAnnotWithPause` + - `FPDFProgressiveRenderEmbedderTest.RenderFormsWithPause` + - `FPDFProgressiveRenderEmbedderTest.RenderHighlightWithColorScheme` + - `FPDFProgressiveRenderEmbedderTest.RenderHighlightWithColorSchemeAndConvertFillToStroke` + - `FPDFProgressiveRenderEmbedderTest.RenderStampWithColorScheme` + - `FPDFProgressiveRenderEmbedderTest.RenderTextWithColorScheme` + - `FPDFProgressiveRenderEmbedderTest.RenderWithPause` + - `FPDFProgressiveRenderEmbedderTest.RenderWithoutPause` + - `FPDFViewEmbedderTest.RenderBug664284WithNoNativeText` + - `FPDFViewEmbedderTest.RenderHelloWorldWithFlags` +- [x] layer mode: exit 0, no warnings; 213 layer-only failures, all expected; 185 crashes and 58 + failures, as in S13; 274 writers, the four new ones below. (A Mac recording drops the Linux-only + `TransFormWithClipAndSaveWithLocale`; its line stays.) +- [x] tag check (`--base 2b2d56c79`): 992 blocks, 0 untagged. +- [x] C API check: ok, 534 functions in 24 headers. +- [x] globals check: ok. +- [x] wasm build: builds locally. +- [ ] ThreadSanitizer soak: in the PR's CI. +- [ ] engine tests and engine-conformance with the local runtime: not run for this sync. +- changed expectations, each with its reason: + - Layer-mode writers: added `EPDFPpoEmbedderTest.AddsToAFormAndRenamesAFontItUses` (1 object), + `RenamesAFontWithOtherWidths` (2), `RenamesTheFontOfAnAppearanceWithAComment` (2) and + `SharesAFontDefinedTheSameWay` (1). They came with #89 (import-acroform), which didn't record + them; they import pages with form fields, so they write. Without them the release workflow's + layer-mode gate fails. + +## Ledger changes proposed + +None. diff --git a/fxjs/xfa/cfxjse_isolatetracker.h b/fxjs/xfa/cfxjse_isolatetracker.h index 0ee8418fac..89db8d7044 100644 --- a/fxjs/xfa/cfxjse_isolatetracker.h +++ b/fxjs/xfa/cfxjse_isolatetracker.h @@ -13,7 +13,7 @@ class CFXJSE_Context; -class CFXJSE_ScopeUtil_IsolateHandle { +class [[maybe_unused, nodiscard]] CFXJSE_ScopeUtil_IsolateHandle { public: FX_STACK_ALLOCATED(); @@ -29,7 +29,7 @@ class CFXJSE_ScopeUtil_IsolateHandle { v8::HandleScope handle_scope_; }; -class CFXJSE_ScopeUtil_Context { +class [[maybe_unused, nodiscard]] CFXJSE_ScopeUtil_Context { public: FX_STACK_ALLOCATED(); @@ -42,7 +42,7 @@ class CFXJSE_ScopeUtil_Context { v8::Context::Scope context_scope_; }; -class CFXJSE_ScopeUtil_IsolateHandleContext { +class [[maybe_unused, nodiscard]] CFXJSE_ScopeUtil_IsolateHandleContext { public: FX_STACK_ALLOCATED(); @@ -58,7 +58,7 @@ class CFXJSE_ScopeUtil_IsolateHandleContext { CFXJSE_ScopeUtil_Context context_; }; -class CFXJSE_ScopeUtil_RootContext { +class [[maybe_unused, nodiscard]] CFXJSE_ScopeUtil_RootContext { public: FX_STACK_ALLOCATED(); @@ -72,7 +72,7 @@ class CFXJSE_ScopeUtil_RootContext { v8::Context::Scope context_scope_; }; -class CFXJSE_ScopeUtil_IsolateHandleRootContext { +class [[maybe_unused, nodiscard]] CFXJSE_ScopeUtil_IsolateHandleRootContext { public: FX_STACK_ALLOCATED(); diff --git a/testing/embedpdf_layer_mode_writers.txt b/testing/embedpdf_layer_mode_writers.txt index 256e94d8e0..6358af3088 100644 --- a/testing/embedpdf_layer_mode_writers.txt +++ b/testing/embedpdf_layer_mode_writers.txt @@ -117,6 +117,10 @@ EPDFMeasureEmbedderTest.NumberFormatsRoundTripAndAbsentVersusEmpty 1 EPDFNamedPagesEmbedderTest.DeletingAPageDropsItsRegistrations 9 EPDFNamedPagesEmbedderTest.RemoveByKeyKeepsThePage 8 EPDFNamedPagesEmbedderTest.SetReplacesRejectsAndRoundTrips 8 +EPDFPpoEmbedderTest.AddsToAFormAndRenamesAFontItUses 1 +EPDFPpoEmbedderTest.RenamesAFontWithOtherWidths 2 +EPDFPpoEmbedderTest.RenamesTheFontOfAnAppearanceWithAComment 2 +EPDFPpoEmbedderTest.SharesAFontDefinedTheSameWay 1 EPDFRedactEmbedderTest.DirectReplacementTextIsRemovedFromSiblingObjects 3 EPDFRedactEmbedderTest.InheritedPropertiesLeaveNoUnusedOriginalOnPage 5 EPDFRedactEmbedderTest.NestedFormsUseTheRenderersTransformOrder 7 diff --git a/testing/resources/pixel/axial_shading_zero_axis.in b/testing/resources/pixel/axial_shading_zero_axis.in new file mode 100644 index 0000000000..273b0777e2 --- /dev/null +++ b/testing/resources/pixel/axial_shading_zero_axis.in @@ -0,0 +1,53 @@ +{{header}} +{{object 1 0}} << + /Pages 2 0 R +>> +endobj +{{object 2 0}} << + /Type /Pages + /Count 1 + /Kids [3 0 R] +>> +endobj +{{object 3 0}} << + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 300 400] + /Contents 4 0 R + /Resources << + /Shading << + /R9 5 0 R + >> + >> +>> +endobj +{{object 4 0}} << + {{streamlen}} +>> +stream +50 0 0 50 150 100 cm +10 10 280 380 re f +/R9 sh +endstream +endobj +{{object 5 0}} << + /ShadingType 2 + /ColorSpace /DeviceCMYK + /Coords [0.0 0.0 0.0 0.0] + /Function 6 0 R + /Extend [true true] +>> +endobj +{{object 6 0}} << + /FunctionType 2 + /Domain [0.0 1.0] + /C0 [0.1 0.2 0.8 0.0] + /C1 [0.05 0.01 0.4 0.1] + /N 1 +>> +endobj + +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/pixel/axial_shading_zero_axis_expected.pdf.0.png b/testing/resources/pixel/axial_shading_zero_axis_expected.pdf.0.png new file mode 100644 index 0000000000..cd7c65f21f Binary files /dev/null and b/testing/resources/pixel/axial_shading_zero_axis_expected.pdf.0.png differ diff --git a/testing/resources/pixel/shade_expected.pdf.0.png b/testing/resources/pixel/shade_expected.pdf.0.png index 0007a40433..0f1f755804 100644 Binary files a/testing/resources/pixel/shade_expected.pdf.0.png and b/testing/resources/pixel/shade_expected.pdf.0.png differ diff --git a/testing/resources/pixel/shade_expected_skia.pdf.0.png b/testing/resources/pixel/shade_expected_skia.pdf.0.png index 94909533c9..0b8b2fac25 100644 Binary files a/testing/resources/pixel/shade_expected_skia.pdf.0.png and b/testing/resources/pixel/shade_expected_skia.pdf.0.png differ diff --git a/testing/resources/pixel/shade_expected_skia_gdi.pdf.0.png b/testing/resources/pixel/shade_expected_skia_gdi.pdf.0.png index 745be5ca7b..9fb6b07a07 100644 Binary files a/testing/resources/pixel/shade_expected_skia_gdi.pdf.0.png and b/testing/resources/pixel/shade_expected_skia_gdi.pdf.0.png differ diff --git a/testing/scoped_set_tz.h b/testing/scoped_set_tz.h index 6b1d284eaf..d6ecd12878 100644 --- a/testing/scoped_set_tz.h +++ b/testing/scoped_set_tz.h @@ -10,7 +10,7 @@ #include "core/fxcrt/fx_memory.h" -class ScopedSetTZ { +class [[maybe_unused, nodiscard]] ScopedSetTZ { public: FX_STACK_ALLOCATED(); diff --git a/testing/utils/scoped_time_overrides.h b/testing/utils/scoped_time_overrides.h index 994a741e26..e656e68527 100644 --- a/testing/utils/scoped_time_overrides.h +++ b/testing/utils/scoped_time_overrides.h @@ -8,7 +8,7 @@ #include "core/fxcrt/fx_extension.h" #include "core/fxcrt/fx_memory.h" -class ScopedTimeFunction { +class [[maybe_unused, nodiscard]] ScopedTimeFunction { public: FX_STACK_ALLOCATED(); @@ -21,7 +21,7 @@ class ScopedTimeFunction { const TimeFunction old_func_; }; -class ScopedLocaltimeFunction { +class [[maybe_unused, nodiscard]] ScopedLocaltimeFunction { public: FX_STACK_ALLOCATED(); diff --git a/xfa/fgas/graphics/cfgas_gegraphics.h b/xfa/fgas/graphics/cfgas_gegraphics.h index 1426f202a5..bb1a7226d0 100644 --- a/xfa/fgas/graphics/cfgas_gegraphics.h +++ b/xfa/fgas/graphics/cfgas_gegraphics.h @@ -24,7 +24,7 @@ class CFX_RenderDevice; class CFGAS_GEGraphics { public: - class StateRestorer { + class [[maybe_unused, nodiscard]] StateRestorer { public: FX_STACK_ALLOCATED();