From 4639ac974e93d732b354524f6ed5cdee93ba412f Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Wed, 21 Oct 2026 11:40:30 +0300 Subject: [PATCH] Draw images inside patterns and Type3 glyphs on a layer A layer took tiling patterns and Type3 fonts from its base. Their content (a pattern's cell, a glyph's procedure) was parsed for the base, so its images carried the base document, and the layer page's image cache refuses an image of another document. CPDF_ImageLoader then read the cache's current entry: null on a page with no cached image yet (the native runtime crashed, WASM drew nothing), else the previous image's (nothing drawn). Chrome prints every rasterized CSS background as a tiling pattern with one image, and every engine open() is a layer. - layer-seam: tiling patterns and Type3 fonts never use the base's entry, as images already didn't. GetCachedType3() drops its base fallback, which can no longer see a base Type3 font. - bug-fix: CPDF_PageImageCache::Accepts() holds the same-document rule, and the image loader loads an image the cache refuses without the cache. Co-Authored-By: Claude Opus 5.5 --- core/fpdfapi/page/cpdf_docpagedata.cpp | 18 ++- core/fpdfapi/page/cpdf_imageloader.cpp | 7 +- core/fpdfapi/page/cpdf_pageimagecache.cpp | 8 +- core/fpdfapi/page/cpdf_pageimagecache.h | 4 + core/fpdfapi/render/cpdf_docrenderdata.cpp | 6 - fpdfsdk/epdf_layer_embeddertest.cpp | 37 +++++ fpdfsdk/epdf_view_embeddertest.cpp | 26 ++++ testing/resources/layer_content_images.in | 151 +++++++++++++++++++++ testing/resources/layer_content_images.pdf | Bin 0 -> 2508 bytes 9 files changed, 244 insertions(+), 13 deletions(-) create mode 100644 testing/resources/layer_content_images.in create mode 100644 testing/resources/layer_content_images.pdf diff --git a/core/fpdfapi/page/cpdf_docpagedata.cpp b/core/fpdfapi/page/cpdf_docpagedata.cpp index 22fd0d49fd..6c8bd3c237 100644 --- a/core/fpdfapi/page/cpdf_docpagedata.cpp +++ b/core/fpdfapi/page/cpdf_docpagedata.cpp @@ -226,8 +226,10 @@ RetainPtr CPDF_DocPageData::GetFont( return pdfium::WrapRetain(it->second.Get()); } - // EmbedPDF(layer-seam): an unpromoted base object uses the base's entry. - if (fallback_ && CanUseFallbackForObject(font_dict.Get())) { + // EmbedPDF(layer-seam): an unpromoted base object uses the base's entry, + // except a Type3 font: its glyphs are content, see GetImage(). + if (fallback_ && CanUseFallbackForObject(font_dict.Get()) && + font_dict->GetByteStringFor("Subtype") != "Type3") { return fallback_->GetFont(font_dict); } @@ -432,13 +434,16 @@ RetainPtr CPDF_DocPageData::GetPattern( return pdfium::WrapRetain(it->second.Get()); } - // EmbedPDF(layer-seam): an unpromoted base object uses the base's entry. - if (fallback_ && CanUseFallbackForObject(pPatternObj.Get())) { + // EmbedPDF(layer-seam): an unpromoted base object uses the base's entry, + // except a tiling pattern: its cell is content, see GetImage(). + const int pattern_type = pPatternObj->GetDict()->GetIntegerFor("PatternType"); + if (fallback_ && CanUseFallbackForObject(pPatternObj.Get()) && + pattern_type != CPDF_Pattern::kTiling) { return fallback_->GetPattern(pPatternObj, matrix); } RetainPtr pattern; - switch (pPatternObj->GetDict()->GetIntegerFor("PatternType")) { + switch (pattern_type) { case CPDF_Pattern::kTiling: pattern = pdfium::MakeRetain(GetDocument(), pPatternObj, matrix); @@ -491,6 +496,9 @@ RetainPtr CPDF_DocPageData::GetImage(uint32_t dwStreamObjNum) { // CPDF_Image carries a document back-pointer and CPDF_PageImageCache rejects // cross-document images. Build a document-local CPDF_Image even when the // underlying stream falls through to a shared base document. + // Content makes images of the document that parses it, so what holds content + // of its own (a tiling pattern's cell, a Type3 font's glyphs) never uses the + // base's entry either: GetPattern(), GetFont(). auto pImage = pdfium::MakeRetain(GetDocument(), dwStreamObjNum); image_map_[dwStreamObjNum] = pImage; return pImage; diff --git a/core/fpdfapi/page/cpdf_imageloader.cpp b/core/fpdfapi/page/cpdf_imageloader.cpp index a8387d9f90..f49ed99f6b 100644 --- a/core/fpdfapi/page/cpdf_imageloader.cpp +++ b/core/fpdfapi/page/cpdf_imageloader.cpp @@ -28,7 +28,12 @@ bool CPDF_ImageLoader::Start(const CPDF_ImageObject* pImage, CPDF_ColorSpace::Family eFamily, bool bLoadMask, const CFX_Size& max_size_required) { - cache_ = pPageImageCache; + // EmbedPDF(bug-fix): the page image cache refuses an image of another + // document, and Finish() then read the cache's current entry: null (a crash) + // or the previous image's (nothing drawn). Such an image loads uncached. + cache_ = pPageImageCache && pPageImageCache->Accepts(*pImage->GetImage()) + ? pPageImageCache + : nullptr; image_object_ = pImage; bool should_continue; if (cache_) { diff --git a/core/fpdfapi/page/cpdf_pageimagecache.cpp b/core/fpdfapi/page/cpdf_pageimagecache.cpp index 5a59e69c9b..90c984398d 100644 --- a/core/fpdfapi/page/cpdf_pageimagecache.cpp +++ b/core/fpdfapi/page/cpdf_pageimagecache.cpp @@ -166,6 +166,11 @@ void CPDF_PageImageCache::ClearImageCacheEntry(const CPDF_Stream* pStream) { image_cache_.erase(it); } +// EmbedPDF(bug-fix): see the header. +bool CPDF_PageImageCache::Accepts(const CPDF_Image& image) const { + return page_->GetDocument() == image.GetDocument(); +} + bool CPDF_PageImageCache::StartGetCachedBitmap( RetainPtr pImage, const CPDF_Dictionary* pFormResources, @@ -175,7 +180,8 @@ bool CPDF_PageImageCache::StartGetCachedBitmap( bool bLoadMask, const CFX_Size& max_size_required) { // A cross-document image may have come from the embedder. - if (page_->GetDocument() != pImage->GetDocument()) { + // EmbedPDF(bug-fix): the rule lives in Accepts(); the loader asks it first. + if (!Accepts(*pImage)) { return false; } diff --git a/core/fpdfapi/page/cpdf_pageimagecache.h b/core/fpdfapi/page/cpdf_pageimagecache.h index 886333cf32..44ded0bce8 100644 --- a/core/fpdfapi/page/cpdf_pageimagecache.h +++ b/core/fpdfapi/page/cpdf_pageimagecache.h @@ -36,6 +36,10 @@ class CPDF_PageImageCache { uint32_t GetTimeCount() const { return time_count_; } CPDF_Page* GetPage() const { return page_; } + // EmbedPDF(bug-fix): whether StartGetCachedBitmap() loads |image|. An image + // of another document is not the cache's: see CPDF_ImageLoader::Start(). + bool Accepts(const CPDF_Image& image) const; + bool StartGetCachedBitmap(RetainPtr pImage, const CPDF_Dictionary* pFormResources, const CPDF_Dictionary* pPageResources, diff --git a/core/fpdfapi/render/cpdf_docrenderdata.cpp b/core/fpdfapi/render/cpdf_docrenderdata.cpp index 90cabd1e28..0de957d51d 100644 --- a/core/fpdfapi/render/cpdf_docrenderdata.cpp +++ b/core/fpdfapi/render/cpdf_docrenderdata.cpp @@ -55,12 +55,6 @@ RetainPtr CPDF_DocRenderData::GetCachedType3( return pdfium::WrapRetain(it->second.Get()); } - // EmbedPDF(layer-seam): an unpromoted base font uses the base's cache. - if (fallback_ && font->GetFontDictObjNum() != 0 && - !GetDocument()->IsObjectPromoted(font->GetFontDictObjNum())) { - return fallback_->GetCachedType3(font); - } - auto cache = pdfium::MakeRetain(font); type3_face_map_[font].Reset(cache.Get()); return cache; diff --git a/fpdfsdk/epdf_layer_embeddertest.cpp b/fpdfsdk/epdf_layer_embeddertest.cpp index ca706e150b..e11c87e920 100644 --- a/fpdfsdk/epdf_layer_embeddertest.cpp +++ b/fpdfsdk/epdf_layer_embeddertest.cpp @@ -2013,3 +2013,40 @@ TEST_F(EPDFLayerEmbedderTest, LayerDeltaReplaysRemoveAllAnnots) { EPDF_ReleaseBaseDocument(base); } + +// A layer draws the images inside content: a tiling pattern's one large cell, +// a pattern of small cells, a Type3 glyph. Each page renders as the file does +// on its own. The layer took that content from its base, parsed for the base, +// so its images were the base's, which the layer page's image cache refuses: +// the render crashed. +TEST_F(EPDFLayerEmbedderTest, LayerDrawsImagesInPatternsAndType3Glyphs) { + static constexpr char kFile[] = "layer_content_images.pdf"; + static constexpr int kPageCount = 3; + std::vector expected; + ASSERT_TRUE(OpenDocument(kFile)); + for (int page_index = 0; page_index < kPageCount; ++page_index) { + ScopedPage page = LoadScopedPage(page_index); + ASSERT_TRUE(page); + ScopedFPDFBitmap bitmap = RenderPage(page.get()); + expected.push_back(HashBitmap(bitmap.get())); + } + + FileAccessForTesting base_access(kFile); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + { + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status)); + ASSERT_TRUE(layer); + for (int page_index = 0; page_index < kPageCount; ++page_index) { + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), page_index)); + ASSERT_TRUE(page); + ScopedFPDFBitmap bitmap = RenderPage(page.get()); + EXPECT_EQ(expected[page_index], HashBitmap(bitmap.get())) + << "page " << page_index; + } + } + + EPDF_ReleaseBaseDocument(base); +} diff --git a/fpdfsdk/epdf_view_embeddertest.cpp b/fpdfsdk/epdf_view_embeddertest.cpp index 355c40fe2a..d3e76b1794 100644 --- a/fpdfsdk/epdf_view_embeddertest.cpp +++ b/fpdfsdk/epdf_view_embeddertest.cpp @@ -961,3 +961,29 @@ TEST_F(EPDFViewEmbedderTest, StandardFontsDrawWithBuiltInFaces) { EXPECT_EQ((std::vector{"Tinos", "Arimo"}), HelloWorldFontFamilies(page.get())); } + +// An image object of another document draws as one of the page's own (rule +// bug-fix). The page image cache refuses it, and the image loader then read +// the cache's current entry: with none yet, a crash. +TEST_F(EPDFViewEmbedderTest, ImageOfAnotherDocumentDraws) { + ScopedFPDFBitmap red(FPDFBitmap_Create(2, 2, 0)); + ASSERT_TRUE(red); + ASSERT_TRUE(FPDFBitmap_FillRect(red.get(), 0, 0, 2, 2, 0xFFFF0000)); + // Draws an image of |image_document| on a new page of |document|. + auto render = [&red](FPDF_DOCUMENT document, FPDF_DOCUMENT image_document) { + ScopedFPDFPage page(FPDFPage_New(document, 0, 200, 200)); + FPDF_PAGEOBJECT image = FPDFPageObj_NewImageObj(image_document); + EXPECT_TRUE(FPDFImageObj_SetBitmap(nullptr, 0, image, red.get())); + EXPECT_TRUE(FPDFImageObj_SetMatrix(image, 100, 0, 0, 100, 50, 50)); + FPDFPage_InsertObject(page.get(), image); + ScopedFPDFBitmap bitmap = RenderPage(page.get()); + return HashBitmap(bitmap.get()); + }; + + ScopedFPDFDocument document(FPDF_CreateNewDocument()); + ScopedFPDFDocument other(FPDF_CreateNewDocument()); + ASSERT_TRUE(document); + ASSERT_TRUE(other); + EXPECT_EQ(render(document.get(), document.get()), + render(document.get(), other.get())); +} diff --git a/testing/resources/layer_content_images.in b/testing/resources/layer_content_images.in new file mode 100644 index 0000000000..b09f5899f8 --- /dev/null +++ b/testing/resources/layer_content_images.in @@ -0,0 +1,151 @@ +{{header}} +{{object 1 0}} << + /Type /Catalog + /Pages 2 0 R +>> +endobj +{{object 2 0}} << + /Type /Pages + /MediaBox [0 0 200 200] + /Count 3 + /Kids [3 0 R 4 0 R 5 0 R] +>> +endobj +% A tiling pattern whose one cell is larger than the area it fills, the way +% Chrome prints a CSS background. +{{object 3 0}} << + /Type /Page + /Parent 2 0 R + /Resources << + /Pattern << /P1 10 0 R >> + >> + /Contents 6 0 R +>> +endobj +% A tiling pattern of small cells. +{{object 4 0}} << + /Type /Page + /Parent 2 0 R + /Resources << + /Pattern << /P2 11 0 R >> + >> + /Contents 7 0 R +>> +endobj +% A Type3 glyph that draws a rectangle and the image. +{{object 5 0}} << + /Type /Page + /Parent 2 0 R + /Resources << + /Font << /F1 12 0 R >> + >> + /Contents 8 0 R +>> +endobj +{{object 6 0}} << + {{streamlen}} +>> +stream +/Pattern cs /P1 scn +20 20 160 160 re f +endstream +endobj +{{object 7 0}} << + {{streamlen}} +>> +stream +/Pattern cs /P2 scn +0 0 200 200 re f +endstream +endobj +{{object 8 0}} << + {{streamlen}} +>> +stream +BT +/F1 100 Tf +50 50 Td +(a) Tj +ET +endstream +endobj +% Red and green on top, blue and yellow below. +{{object 9 0}} << + /Type /XObject + /Subtype /Image + /Width 2 + /Height 2 + /ColorSpace /DeviceRGB + /BitsPerComponent 8 + /Filter /ASCIIHexDecode + {{streamlen}} +>> +stream +FF0000 00FF00 0000FF FFFF00> +endstream +endobj +{{object 10 0}} << + /Type /Pattern + /PatternType 1 + /PaintType 1 + /TilingType 1 + /BBox [0 0 200 200] + /XStep 202 + /YStep 202 + /Resources << + /XObject << /Im1 9 0 R >> + >> + {{streamlen}} +>> +stream +q 200 0 0 200 0 0 cm /Im1 Do Q +endstream +endobj +{{object 11 0}} << + /Type /Pattern + /PatternType 1 + /PaintType 1 + /TilingType 1 + /BBox [0 0 20 20] + /XStep 20 + /YStep 20 + /Resources << + /XObject << /Im1 9 0 R >> + >> + {{streamlen}} +>> +stream +q 20 0 0 20 0 0 cm /Im1 Do Q +endstream +endobj +{{object 12 0}} << + /Type /Font + /Subtype /Type3 + /FontBBox [0 0 1000 1000] + /FontMatrix [0.001 0 0 0.001 0 0] + /FirstChar 97 + /LastChar 97 + /Widths [1000] + /Encoding << + /Type /Encoding + /Differences [97 /a] + >> + /CharProcs << /a 13 0 R >> + /Resources << + /XObject << /Im1 9 0 R >> + >> +>> +endobj +{{object 13 0}} << + {{streamlen}} +>> +stream +1000 0 d0 +0 0 500 1000 re f +q 500 0 0 1000 500 0 cm /Im1 Do Q +endstream +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/layer_content_images.pdf b/testing/resources/layer_content_images.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e0d274ecbf737bb24a98dc427439ec7fd8826d0e GIT binary patch literal 2508 zcmcIm+m72d5Pj!Y%!`2*NNrNS1B(E$y>1pw5XUyq6pK925-oEpiw?!j?)Gc?&@U|V z0i7Ai>xPi5#mQOnat+5U| zb14aCIyD7E_|jawd?`$x;LC#6=ZE-BI`Y;eR*&2x+)8{clKlBdJ7agB2WZpuCz}*- z+a|rx;jv5O$D>Oc@Ct&>Y@WhShhTgT4;xn)aJhllWEt23GVN32A#8Mxg8}W01{+{) zv#fZ*q6hsX8Zh2?w>7ZyHV*}87%vvE(($jUN28LaAX6^6RD$3Q8eXD+#?MXR?tP5F zFls27RR&&N;aq_t&*y-EK{=r7c|aWn^l0bnUb;0DTaEsalSNYyKL3$v5hx1yWz+f> zOd~?HA`Ro@NMno-odKi(F|6isQFiJ4KujRcK8SMF#vLii0y# z3Dv`;Z%m$s4YUV@a}kgdTk#yCxZn^KaV{dFCa7Nd@dnmJ@ajNCbpoZXx%ao4BOuL3 z%txdy=_L>9dZQz~qorUJFoBk9(UpMz%S3#qzlY_WxL($6ZNS_l9FNo+lVhSlaJv_< z%I?cBJt3jo1FQ^=O))$%p-lY`?^bshSz`U-eigVJ+)!Xi^0Q44)`+IBjZHV05;PyX z%=yJm$5=Wh=Ch5>{KpY(jBF@o#*f{0hdGOZ9guWtGfZY^y;_WKZeE+miHTjp`m4A_ z$8noZB|ab}P4J5*FvUkoE=Q-);>0_^TKZHaC*565S*}U})`P=x$@S*29$lYgo5Jsw`rk zummwUj9#_)Ix`==S)(S^5tO>+1HEpy-Gwzg%M>;ZdLTc@dRQUPWKkcp=SI{*VXJ`BEn!ZSOj%*HI+YIYr`2%e3TmWv- a%w+qUA?&z~X&)HPNOcFI(YSs$6@LLhZIuB4 literal 0 HcmV?d00001