From 35a95b613ab453f1d52da1b39d99855447e45e37 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 1 Nov 2026 16:33:59 +0200 Subject: [PATCH] Run the font tests as layers in layer mode Layer mode only made layers of files a test opens, so most font tests, which build their PDFs in memory, never ran on a layer. EmbedderTest::OpenDocumentFromMemory() and embedpdf_test::LoadMemDocumentForTest() take in-memory documents through the same path, and the font, ActualText and last-resort tests use them. New layer tests cover a font written, saved and reopened over its base, and a write to a font's widths. A display program only a changed font dictionary read is now forgotten, so its hash finds nothing. --- core/fpdfdoc/cpdf_documentfonts.cpp | 19 +- core/fpdfdoc/cpdf_documentfonts.h | 3 +- docs/embedpdf/fork-ledger.md | 11 +- fpdfsdk/epdf_annot_embeddertest.cpp | 14 +- fpdfsdk/epdf_edit_test_util.cpp | 7 + fpdfsdk/epdf_edit_test_util.h | 6 + fpdfsdk/epdf_font_embeddertest.cpp | 371 ++++++++++++++++++++---- fpdfsdk/epdf_text_embeddertest.cpp | 7 +- testing/embedder_test.cpp | 24 ++ testing/embedder_test.h | 3 + testing/embedpdf_layer_fixture.h | 19 ++ testing/embedpdf_layer_mode.cpp | 39 ++- testing/embedpdf_layer_mode.h | 19 ++ testing/embedpdf_layer_mode_writers.txt | 14 + 14 files changed, 472 insertions(+), 84 deletions(-) diff --git a/core/fpdfdoc/cpdf_documentfonts.cpp b/core/fpdfdoc/cpdf_documentfonts.cpp index 10fd372d68..0cdcc5eefd 100644 --- a/core/fpdfdoc/cpdf_documentfonts.cpp +++ b/core/fpdfdoc/cpdf_documentfonts.cpp @@ -463,9 +463,26 @@ void CPDF_DocumentFonts::Forget(const std::set& objnums) { inspected_.erase(objnum); } for (auto& [objnum, inspected] : inspected_) { + std::set> read_by_changed; std::erase_if(inspected.display_of_dict, [&](const auto& entry) { - return objnums.contains(entry.first); + if (!objnums.contains(entry.first)) { + return false; + } + if (entry.second.has_value()) { + read_by_changed.insert(*entry.second); + } + return true; }); + // A display only a changed dictionary read is no longer the document's, + // so its hash finds nothing. + for (const DataVector& sha256 : read_by_changed) { + const bool still_read = std::ranges::any_of( + inspected.display_of_dict, + [&](const auto& entry) { return entry.second == sha256; }); + if (!still_read) { + inspected.displays.erase(sha256); + } + } } } diff --git a/core/fpdfdoc/cpdf_documentfonts.h b/core/fpdfdoc/cpdf_documentfonts.h index 5c31dee4f5..85fb16fc51 100644 --- a/core/fpdfdoc/cpdf_documentfonts.h +++ b/core/fpdfdoc/cpdf_documentfonts.h @@ -200,7 +200,8 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { std::optional aligned; // Display programs by their SHA-256. Any other program is read through // each font dictionary's encoding, widths and flags, so one stream can - // be drawn as more than one. + // be drawn as more than one. A display goes when the last dictionary that + // read it changes. std::map, Display> displays; // The display each font dictionary (by object number) reads the stream // as; nullopt when it builds none. Forgotten when the dictionary diff --git a/docs/embedpdf/fork-ledger.md b/docs/embedpdf/fork-ledger.md index e89de2b696..d3bdcafa87 100644 --- a/docs/embedpdf/fork-ledger.md +++ b/docs/embedpdf/fork-ledger.md @@ -491,12 +491,15 @@ race. ### layer-mode **Rule.** With `EPDF_EMBEDDER_LAYER_MODE=1`, the embedder test harness opens every file as a fresh layer over a frozen base. Like the engine, it sends the form-fill environment no page events and -doesn't draw with it. +doesn't draw with it. A document a test builds in memory goes through the layer too when the test +opens it with `EmbedderTest::OpenDocumentFromMemory()`, or with +`embedpdf_test::LoadMemDocumentForTest()` when it keeps the document itself; a test that writes +into a new document starts from `EmptyPdf()`, since `CreateEmptyDocument()` never makes a layer. **Why.** Upstream's embedder tests then run through the layer's read and write paths, so a new upstream feature is tested on a layer without anyone writing a layer test. -**Where.** `testing/embedpdf_layer_mode.{h,cpp}`, four hooks in `testing/embedder_test.cpp`, -`testing/tools/run_layer_mode.py` with `testing/embedpdf_layer_mode_expectations.txt` and -`testing/embedpdf_layer_mode_writers.txt`. +**Where.** `testing/embedpdf_layer_mode.{h,cpp}`, the `EmbedPDF(layer-mode)` hooks in +`testing/embedder_test.{h,cpp}`, `testing/tools/run_layer_mode.py` with +`testing/embedpdf_layer_mode_expectations.txt` and `testing/embedpdf_layer_mode_writers.txt`. **Guarded by.** `run_layer_mode.py` fails when no test leaves an object in a layer, which is what happens when a hook stops working. **When upstream touches this.** `EmbedderTest`'s document opening, page loading and page diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp index 271d915e43..7580a9db8d 100644 --- a/fpdfsdk/epdf_annot_embeddertest.cpp +++ b/fpdfsdk/epdf_annot_embeddertest.cpp @@ -54,6 +54,7 @@ #include "core/fxge/cfx_font.h" #include "core/fxge/cfx_fontregistry.h" #include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_edit_test_util.h" #include "public/cpp/fpdf_scopers.h" #include "public/epdf_edit.h" #include "public/epdf_flatten.h" @@ -76,6 +77,7 @@ #include "testing/embedder_test_constants.h" #include "testing/embedpdf_form_writes.h" #include "testing/embedpdf_layer_fixture.h" +#include "testing/embedpdf_layer_mode.h" #include "testing/fx_string_testhelpers.h" #include "testing/gmock/include/gmock/gmock-matchers.h" #include "testing/utils/file_util.h" @@ -4303,7 +4305,9 @@ TEST_F(EPDFAnnotEmbedderTest, RichTextLastResortDrawsWhatNoFontHas) { std::string saved; { - ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + const std::string empty = EmptyPdf(); + embedpdf_test::ScopedTestDocument doc(embedpdf_test::LoadMemDocumentForTest( + empty.data(), empty.size(), nullptr)); ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); ScopedFPDFAnnotation annot( FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); @@ -4337,8 +4341,8 @@ TEST_F(EPDFAnnotEmbedderTest, RichTextLastResortDrawsWhatNoFontHas) { saved.assign(static_cast(saved_buffer), saved_size); EPDF_FreeBuffer(saved_buffer); } - ScopedFPDFDocument doc(FPDF_LoadMemDocument( - saved.data(), static_cast(saved.size()), nullptr)); + embedpdf_test::ScopedTestDocument doc(embedpdf_test::LoadMemDocumentForTest( + saved.data(), saved.size(), nullptr)); ASSERT_TRUE(doc); ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); ASSERT_TRUE(page); @@ -4367,7 +4371,9 @@ TEST_F(EPDFAnnotEmbedderTest, RichTextPreviewAndPrintLastResortWaits) { ASSERT_NE(0u, box_id); ASSERT_TRUE(EPDFFont_SetLastResortFont(box_id)); - ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + const std::string empty = EmptyPdf(); + embedpdf_test::ScopedTestDocument doc(embedpdf_test::LoadMemDocumentForTest( + empty.data(), empty.size(), nullptr)); ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); const char kJson[] = "{\"body\":{\"family\":\"Roboto\",\"size\":18}," diff --git a/fpdfsdk/epdf_edit_test_util.cpp b/fpdfsdk/epdf_edit_test_util.cpp index 610e1fc539..f8575a19ac 100644 --- a/fpdfsdk/epdf_edit_test_util.cpp +++ b/fpdfsdk/epdf_edit_test_util.cpp @@ -102,6 +102,13 @@ std::string MakePdf(const std::vector& objects) { return pdf; } +std::string EmptyPdf() { + return MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [] /Count 0 >>", + }); +} + std::string Stream(const std::string& data) { return Stream("", data); } diff --git a/fpdfsdk/epdf_edit_test_util.h b/fpdfsdk/epdf_edit_test_util.h index 75ef0b1d72..b9e1fc523d 100644 --- a/fpdfsdk/epdf_edit_test_util.h +++ b/fpdfsdk/epdf_edit_test_util.h @@ -42,6 +42,12 @@ inline constexpr uint8_t kBluePng[] = { // A PDF of the given object bodies, numbered from 1, with a classic xref. std::string MakePdf(const std::vector& objects); +// A PDF with no pages, for a test that writes into a document of its own. +// Opened with EmbedderTest::OpenDocumentFromMemory() or +// embedpdf_test::LoadMemDocumentForTest(), it is a layer in layer mode, as +// every document the engine opens is; CreateEmptyDocument() never is. +std::string EmptyPdf(); + // A stream object body holding `data`. std::string Stream(const std::string& data); diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp index 6dd7910be6..cfaef1d45c 100644 --- a/fpdfsdk/epdf_font_embeddertest.cpp +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -19,6 +19,7 @@ #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_reference.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/parser/cpdf_stream_acc.h" @@ -40,6 +41,8 @@ #include "public/fpdf_edit.h" #include "testing/embedder_test.h" #include "testing/embedpdf_form_writes.h" +#include "testing/embedpdf_layer_fixture.h" +#include "testing/embedpdf_layer_mode.h" #include "testing/fx_string_testhelpers.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/utils/file_util.h" @@ -306,13 +309,16 @@ class EPDFFontEmbedderTest : public EmbedderTest { uint32_t AttachAndFill(FPDF_ANNOTATION annot, const wchar_t* name, const std::wstring& value) { + // The annotation's own document: the harness's, or a layer a test opened. + FPDF_DOCUMENT document = FPDFDocumentFromCPDFDocument( + CPDFAnnotContextFromFPDFAnnotation(annot)->GetPage()->GetDocument()); ScopedFPDFWideString field_name = GetFPDFWideString(name); const uint32_t field = EPDFForm_CreateField( - document(), 4 /* EPDF_FORMFIELD_FAMILY_TEXT */, field_name.get(), 0); + document, 4 /* EPDF_FORMFIELD_FAMILY_TEXT */, field_name.get(), 0); EXPECT_GT(field, 0u); EXPECT_TRUE(EPDFForm_AttachWidget( - document(), field, EPDFAnnot_GetObjectNumber(annot), nullptr, 0)); - EXPECT_TRUE(embedpdf_test::FillTextField(document(), field, value)); + document, field, EPDFAnnot_GetObjectNumber(annot), nullptr, 0)); + EXPECT_TRUE(embedpdf_test::FillTextField(document, field, value)); EXPECT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot)); return field; } @@ -427,7 +433,7 @@ TEST_F(EPDFFontEmbedderTest, ListsADocumentProgramAndHandsOutItsBytes) { "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); ASSERT_NE(0u, roboto_id); - CreateEmptyDocument(); + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); ASSERT_TRUE(page); AddRegisteredFontField(page.get(), roboto_id, L"name", 320.0f, L"hello"); @@ -503,7 +509,7 @@ TEST_F(EPDFFontEmbedderTest, RealignsAProgramABrowserWouldRefuse) { misaligned.data(), misaligned.size()); ASSERT_NE(0u, font_id); - CreateEmptyDocument(); + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); ASSERT_TRUE(page); AddRegisteredFontField(page.get(), font_id, L"name", 320.0f, L"hello"); @@ -585,7 +591,7 @@ TEST_F(EPDFFontEmbedderTest, WidgetWritesWithADocumentProgram) { "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); ASSERT_NE(0u, roboto_id); - CreateEmptyDocument(); + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); ASSERT_TRUE(page); AddRegisteredFontField(page.get(), roboto_id, L"first", 320.0f, L"hello"); @@ -676,7 +682,7 @@ TEST_F(EPDFFontEmbedderTest, ResolvesTheFontsAWriteWouldUse) { ASSERT_NE(0u, droid_id); ASSERT_TRUE(EPDFFont_AddFallbackFont(droid_id, /*tier=*/0)); - CreateEmptyDocument(); + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); ScopedFPDFWideString text0 = GetFPDFWideString(L"Hello \x4E2D"); ScopedFPDFWideString text1 = GetFPDFWideString(L"Roboto here"); ScopedFPDFWideString text2 = GetFPDFWideString(L"X"); @@ -738,7 +744,7 @@ TEST_F(EPDFFontEmbedderTest, ResolvesADocumentProgram) { const EPDF_FONT_ID roboto_id = EPDFFont_RegisterMemFont64( "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); ASSERT_NE(0u, roboto_id); - CreateEmptyDocument(); + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); ASSERT_TRUE(page); AddRegisteredFontField(page.get(), roboto_id, L"name", 320.0f, L"hello"); @@ -802,7 +808,8 @@ TEST_F(EPDFFontEmbedderTest, FindsFontsWhereverADrawingUsesThem) { "/LastChar 0 /Widths [0] /Resources << /Font << /FG 15 0 R >> >> >>", "<< /Type /Font /Subtype /TrueType /BaseFont /Georgia >>", }); - ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + embedpdf_test::ScopedTestDocument doc( + embedpdf_test::LoadMemDocumentForTest(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(doc); EPDF_FONT_FACES faces = EPDFFontFaces_Load(doc.get()); ASSERT_TRUE(faces); @@ -982,6 +989,7 @@ TEST_F(EPDFFontEmbedderTest, AChangeOutsideATransactionIsSeen) { // time. TEST_F(EPDFFontEmbedderTest, APlainDocumentIsWalkedWholeEachTime) { const std::string pdf = ThreePagesOfFonts(); + // Plain in layer mode too: a layer would say what changed. ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(doc); const CPDF_DocumentFonts* index = @@ -997,7 +1005,7 @@ TEST_F(EPDFFontEmbedderTest, APlainDocumentIsWalkedWholeEachTime) { // line breaks and zero width characters are never reported. TEST_F(EPDFFontEmbedderTest, FreeTextReportsCharactersNoFontHas) { ScopedRegisteredFonts scoped_fonts; - CreateEmptyDocument(); + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); ASSERT_TRUE(page); ScopedFPDFAnnotation annot( @@ -1030,7 +1038,7 @@ TEST_F(EPDFFontEmbedderTest, UnauthorizedRegisteredFontDoesNotTakeOverABox) { EPDFFont_GetEmbeddingPermission(font_id)); ASSERT_TRUE(EPDFFont_AuthorizeEditing(font_id)); - CreateEmptyDocument(); + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); ASSERT_TRUE(page); ScopedFPDFAnnotation annot( @@ -1162,7 +1170,8 @@ bool Covers(const std::vector& ranges, uint32_t scalar) { // advances it lays them out with. void ExpectDisplayProgramDrawsTheKeptFont(const std::string& pdf, int expected_format) { - ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + embedpdf_test::ScopedTestDocument doc( + embedpdf_test::LoadMemDocumentForTest(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(doc); EPDF_FONT_FACES faces = EPDFFontFaces_Load(doc.get()); ASSERT_TRUE(faces); @@ -1209,13 +1218,16 @@ void ExpectDisplayProgramDrawsTheKeptFont(const std::string& pdf, // The engine's view of the same font. CPDF_Document* pdf_doc = CPDFDocumentFromFPDFDocument(doc.get()); - RetainPtr font_dict = pdf_doc->GetMutableRoot() - ->GetMutableDictFor("AcroForm") - ->GetMutableDictFor("DR") - ->GetMutableDictFor("Font") - ->GetMutableDictFor("FKept"); - RetainPtr font = - CPDF_DocPageData::FromDocument(pdf_doc)->GetFont(font_dict); + RetainPtr font_dict = pdf_doc->GetRoot() + ->GetDictFor("AcroForm") + ->GetDictFor("DR") + ->GetDictFor("Font") + ->GetDictFor("FKept"); + ASSERT_TRUE(font_dict); + // Read as the engine reads it: GetFont() takes a mutable dictionary but + // doesn't write it, and a mutable getter would copy it into a layer. + RetainPtr font = CPDF_DocPageData::FromDocument(pdf_doc)->GetFont( + pdfium::WrapRetain(const_cast(font_dict.Get()))); ASSERT_TRUE(font); RetainPtr source_face = font->GetFont()->GetFace(); @@ -1265,8 +1277,8 @@ void ExpectDisplayProgramDrawsTheKeptFont(const std::string& pdf, // The same bytes from another opening of the same file, by the hash alone: // the document is walked to find it. - ScopedFPDFDocument again( - FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + embedpdf_test::ScopedTestDocument again( + embedpdf_test::LoadMemDocumentForTest(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(again); std::vector rebuilt(length); ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData(again.get(), display_hash, @@ -1284,7 +1296,8 @@ TEST_F(EPDFFontEmbedderTest, SimpleFieldFontWritesThroughItsProgram) { ASSERT_FALSE(roboto.empty()); const std::string pdf = FormWithFieldFont(roboto, "FontFile2", nullptr, "Roboto-Regular", "TrueType"); - ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + embedpdf_test::ScopedTestDocument doc( + embedpdf_test::LoadMemDocumentForTest(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(doc); ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); ASSERT_TRUE(page); @@ -1322,6 +1335,56 @@ TEST_F(EPDFFontEmbedderTest, BuildsADisplayProgramForAType1Font) { EPDF_FONT_FORMAT_TYPE1); } +namespace { + +// A simple font over FoxitFixed's bare CFF program, object 4: every character +// |width| wide, described by font descriptor |descriptor|. +std::string FoxitFixedFont(int width, int descriptor) { + std::string widths; + for (int code = 32; code <= 126; ++code) { + widths += std::to_string(width) + " "; + } + return "<< /Type /Font /Subtype /Type1 /BaseFont /FoxitFixed /FirstChar 32 " + "/LastChar 126 /Widths [" + + widths + "] /Encoding /WinAnsiEncoding /FontDescriptor " + + std::to_string(descriptor) + " 0 R >>"; +} + +constexpr char kFoxitFixedDescriptor[] = + "<< /Type /FontDescriptor /FontName /FoxitFixed /Flags 32 /FontBBox [0 " + "-250 1000 900] /ItalicAngle 0 /Ascent 900 /Descent -250 /CapHeight 700 " + "/StemV 80 /FontFile3 4 0 R >>"; + +// A text field on page 3 whose /DA names |font_alias|. +std::string FoxitFixedField(const char* name, + const char* font_alias, + int bottom) { + return std::string("<< /Type /Annot /Subtype /Widget /FT /Tx /T (") + name + + ") /Rect [10 " + std::to_string(bottom) + " 200 " + + std::to_string(bottom + 30) + "] /DA (/" + font_alias + + " 12 Tf 0 g) /P 3 0 R >>"; +} + +// The advance of |c| in the OpenType font |program|, in font units, or -1. +int AdvanceOf(pdfium::span program, wchar_t c) { + auto font = std::make_unique(); + if (!font->LoadFaceZeroFromSpan(program, /*force_vertical=*/false, + /*object_tag=*/0)) { + return -1; + } + RetainPtr face = font->GetFace(); + if (!face->SelectCharMap(fxge::FontEncoding::kUnicode)) { + return -1; + } + const int glyph = face->GetCharIndex(c); + if (glyph <= 0 || face->LoadGlyph(glyph, /*scale=*/false) != 0) { + return -1; + } + return face->GetGlyphTTWidth(glyph); +} + +} // namespace + // Two font dictionaries that share one bare CFF program but give it other // widths are two display programs: each draws at its own dictionary's // advances, under a hash of its own. The program is still one stream. A field @@ -1330,26 +1393,6 @@ TEST_F(EPDFFontEmbedderTest, ADisplayProgramIsTheProgramAsItsDictionaryReadsIt) { std::vector cff = LoadTestFont("fonts/foxit_fixed.cff"); ASSERT_FALSE(cff.empty()); - auto font = [](int width, int descriptor) { - std::string widths; - for (int code = 32; code <= 126; ++code) { - widths += std::to_string(width) + " "; - } - return "<< /Type /Font /Subtype /Type1 /BaseFont /FoxitFixed /FirstChar 32 " - "/LastChar 126 /Widths [" + - widths + "] /Encoding /WinAnsiEncoding /FontDescriptor " + - std::to_string(descriptor) + " 0 R >>"; - }; - const char kDescriptor[] = - "<< /Type /FontDescriptor /FontName /FoxitFixed /Flags 32 /FontBBox [0 " - "-250 1000 900] /ItalicAngle 0 /Ascent 900 /Descent -250 /CapHeight 700 " - "/StemV 80 /FontFile3 4 0 R >>"; - auto field = [](const char* name, const char* font_alias, int bottom) { - return std::string("<< /Type /Annot /Subtype /Widget /FT /Tx /T (") + name + - ") /Rect [10 " + std::to_string(bottom) + " 200 " + - std::to_string(bottom + 30) + "] /DA (/" + font_alias + - " 12 Tf 0 g) /P 3 0 R >>"; - }; const std::string pdf = MakePdf({ "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [9 0 R 10 0 R] " "/DR << /Font << /FNarrow 5 0 R /FWide 6 0 R >> >> >> >>", @@ -1357,14 +1400,15 @@ TEST_F(EPDFFontEmbedderTest, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Annots [9 0 R 10 " "0 R] >>", Stream("/Subtype /Type1C", std::string(cff.begin(), cff.end())), - font(600, 7), - font(900, 8), - kDescriptor, - kDescriptor, - field("narrow", "FNarrow", 200), - field("wide", "FWide", 100), + FoxitFixedFont(600, 7), + FoxitFixedFont(900, 8), + kFoxitFixedDescriptor, + kFoxitFixedDescriptor, + FoxitFixedField("narrow", "FNarrow", 200), + FoxitFixedField("wide", "FWide", 100), }); - ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + embedpdf_test::ScopedTestDocument doc( + embedpdf_test::LoadMemDocumentForTest(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(doc); EPDF_FONT_FACES faces = EPDFFontFaces_Load(doc.get()); @@ -1405,15 +1449,7 @@ TEST_F(EPDFFontEmbedderTest, const DataVector hashed = CryptSha256Generate(display); EXPECT_EQ(display_hashes[i], std::vector(hashed.begin(), hashed.end())); - auto display_font = std::make_unique(); - ASSERT_TRUE(display_font->LoadFaceZeroFromSpan( - display, /*force_vertical=*/false, /*object_tag=*/0)); - RetainPtr display_face = display_font->GetFace(); - ASSERT_TRUE(display_face->SelectCharMap(fxge::FontEncoding::kUnicode)); - const int glyph = display_face->GetCharIndex('A'); - ASSERT_GT(glyph, 0); - ASSERT_EQ(0, display_face->LoadGlyph(glyph, /*scale=*/false)); - EXPECT_EQ(expected_advance[i], display_face->GetGlyphTTWidth(glyph)) << i; + EXPECT_EQ(expected_advance[i], AdvanceOf(display, 'A')) << i; EXPECT_GT(EPDFDoc_GetFontDisplayProgramCoverage( doc.get(), display_hashes[i].data(), nullptr, 0), 0u); @@ -1453,6 +1489,213 @@ TEST_F(EPDFFontEmbedderTest, } } +namespace { + +// What the font list says of a family's first program. +struct ListedProgram { + unsigned long objnum = 0; + std::vector hash; + std::vector display_hash; + + bool operator==(const ListedProgram&) const = default; +}; + +ListedProgram ListedProgramOf(FPDF_DOCUMENT document, + const std::wstring& family) { + ListedProgram listed; + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document); + std::optional found = FindProgram(faces, family); + if (found.has_value()) { + listed.objnum = + EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + unsigned char hash[32] = {}; + if (EPDFFontFaces_GetProgramHash(faces, found->face, found->program, + hash)) { + listed.hash.assign(hash, hash + 32); + } + if (EPDFFontFaces_GetProgramDisplayHash(faces, found->face, found->program, + hash)) { + listed.display_hash.assign(hash, hash + 32); + } + } + EPDFFontFaces_Close(faces); + return listed; +} + +// The display program |display_hash| of |document|, or nothing. +std::vector DisplayProgram(FPDF_DOCUMENT document, + const std::vector& display_hash) { + const unsigned long length = EPDFDoc_GetFontDisplayProgramData( + document, display_hash.data(), nullptr, 0); + std::vector program(length); + if (length > 0) { + EPDFDoc_GetFontDisplayProgramData(document, display_hash.data(), + program.data(), length); + } + return program; +} + +// What resolve says the first field on page 0 would draw |family| with. +struct ResolvedProgram { + int source = -1; + unsigned long objnum = 0; + std::vector display_hash; + + bool operator==(const ResolvedProgram&) const = default; +}; + +ResolvedProgram ResolveFirstField(FPDF_DOCUMENT document, + const wchar_t* family_name) { + ResolvedProgram resolved; + ScopedFPDFPage page(FPDF_LoadPage(document, 0)); + ScopedFPDFAnnotation annot(page ? FPDFPage_GetAnnot(page.get(), 0) : nullptr); + if (!annot) { + return resolved; + } + ScopedFPDFWideString text = GetFPDFWideString(L"Hello"); + ScopedFPDFWideString family = GetFPDFWideString(family_name); + FPDF_WIDESTRING texts[] = {text.get()}; + FPDF_WIDESTRING families[] = {family.get()}; + const int weights[] = {400}; + const FPDF_BOOL italics[] = {false}; + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + document, annot.get(), 1, texts, families, weights, italics, nullptr); + if (!resolution) { + return resolved; + } + resolved.source = EPDFFontResolution_GetSource(resolution, 0); + resolved.objnum = EPDFFontResolution_GetProgramObjNum(resolution, 0); + unsigned char hash[32] = {}; + if (EPDFFontResolution_GetProgramDisplayHash(resolution, 0, hash)) { + resolved.display_hash.assign(hash, hash + 32); + } + EPDFFontResolution_Close(resolution); + return resolved; +} + +// Gives font |objnum| of |document| the width |width| for every character. +void SetWidths(FPDF_DOCUMENT document, uint32_t objnum, int width) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + RetainPtr font = + ToDictionary(doc->GetMutableIndirectObject(objnum)); + ASSERT_TRUE(font); + auto widths = font->SetNewFor("Widths"); + for (int code = 32; code <= 126; ++code) { + widths->AppendNew(width); + } +} + +} // namespace + +// A font a write embeds on a layer reads the same once the layer is saved and +// reopened over its base, as a session reopens its saved changes: the same +// program, bytes and display program, and the field written in it resolves +// as it did. Without the registered font, the field resolves to the program +// the layer holds. Reading it there copies nothing into the layer. +TEST_F(EPDFFontEmbedderTest, AFontWrittenOnALayerReadsTheSameReopened) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const EPDF_FONT_ID roboto_id = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + ASSERT_NE(0u, roboto_id); + + const std::string pdf = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] >>", + }); + embedpdf_test::LayerFixture doc; + ASSERT_TRUE(doc.OpenBytes(std::vector(pdf.begin(), pdf.end()))); + ASSERT_TRUE(EPDFLayer_BeginTransaction(doc.layer)); + { + ScopedFPDFPage page(FPDF_LoadPage(doc.layer, 0)); + ASSERT_TRUE(page); + AddRegisteredFontField(page.get(), roboto_id, L"name", 320.0f, L"hello"); + } + ASSERT_TRUE(EPDFLayer_CommitTransaction(doc.layer)); + const ListedProgram written = ListedProgramOf(doc.layer, L"Roboto"); + ASSERT_NE(0u, written.objnum); + ASSERT_FALSE(written.display_hash.empty()); + const ResolvedProgram resolved = ResolveFirstField(doc.layer, L"Roboto"); + ASSERT_NE(-1, resolved.source); + + const std::vector delta = doc.SaveDelta(); + ASSERT_FALSE(delta.empty()); + ASSERT_TRUE(doc.Reopen(delta)); + const unsigned long ingested = EPDFLayer_GetPromotedObjectCount(doc.layer); + + EXPECT_EQ(written, ListedProgramOf(doc.layer, L"Roboto")); + std::vector bytes(roboto.size()); + ASSERT_EQ(roboto.size(), + EPDFDoc_GetFontProgramData(doc.layer, written.objnum, bytes.data(), + bytes.size())); + EXPECT_EQ(roboto, bytes); + EXPECT_EQ(roboto, DisplayProgram(doc.layer, written.display_hash)); + EXPECT_EQ(resolved, ResolveFirstField(doc.layer, L"Roboto")); + + EPDFFont_ClearRegisteredFonts(); + const ResolvedProgram kept = ResolveFirstField(doc.layer, L"Roboto"); + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, kept.source); + EXPECT_EQ(written.objnum, kept.objnum); + EXPECT_EQ(written.display_hash, kept.display_hash); + EXPECT_EQ(ingested, EPDFLayer_GetPromotedObjectCount(doc.layer)); +} + +// A write to a font's dictionary on a layer gives the font the display +// program its new dictionary reads, from a list during the write on; rolled +// back, the old one is back. Once no dictionary reads a display program, its +// hash finds nothing. Reading the fonts copies nothing into the layer. +TEST_F(EPDFFontEmbedderTest, AWriteToAFontsWidthsGivesItANewDisplayProgram) { + std::vector cff = LoadTestFont("fonts/foxit_fixed.cff"); + ASSERT_FALSE(cff.empty()); + const std::string pdf = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [7 0 R] /DR << " + "/Font << /FFixed 5 0 R >> >> >> >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Annots [7 0 R] " + ">>", + Stream("/Subtype /Type1C", std::string(cff.begin(), cff.end())), + FoxitFixedFont(600, 6), + kFoxitFixedDescriptor, + FoxitFixedField("fixed", "FFixed", 200), + }); + ScopedFPDFDocument layer = OpenLayerOver(pdf); + ASSERT_TRUE(layer); + + const ListedProgram narrow = ListedProgramOf(layer.get(), L"FoxitFixed"); + ASSERT_EQ(4u, narrow.objnum); + ASSERT_FALSE(narrow.display_hash.empty()); + EXPECT_EQ(600, + AdvanceOf(DisplayProgram(layer.get(), narrow.display_hash), 'A')); + EXPECT_EQ(narrow.display_hash, + ResolveFirstField(layer.get(), L"FoxitFixed").display_hash); + EXPECT_EQ(0ul, EPDFLayer_GetPromotedObjectCount(layer.get())); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + SetWidths(layer.get(), 5, 900); + const ListedProgram during = ListedProgramOf(layer.get(), L"FoxitFixed"); + EXPECT_NE(narrow.display_hash, during.display_hash); + ASSERT_TRUE(EPDFLayer_AbortTransaction(layer.get())); + EXPECT_EQ(narrow, ListedProgramOf(layer.get(), L"FoxitFixed")); + EXPECT_TRUE(DisplayProgram(layer.get(), during.display_hash).empty()); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + SetWidths(layer.get(), 5, 900); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + const ListedProgram wide = ListedProgramOf(layer.get(), L"FoxitFixed"); + EXPECT_EQ(narrow.objnum, wide.objnum); + EXPECT_EQ(narrow.hash, wide.hash); + ASSERT_FALSE(wide.display_hash.empty()); + EXPECT_NE(narrow.display_hash, wide.display_hash); + EXPECT_EQ(900, + AdvanceOf(DisplayProgram(layer.get(), wide.display_hash), 'A')); + EXPECT_EQ(wide.display_hash, + ResolveFirstField(layer.get(), L"FoxitFixed").display_hash); + // No dictionary reads the program 600 wide any more. + EXPECT_TRUE(DisplayProgram(layer.get(), narrow.display_hash).empty()); +} + // A form field draws each cluster in the first font that has it: Hebrew // typed into a Helvetica field comes from the fallback, in bidi order, under // ActualText, and nothing is reported missing. @@ -1466,7 +1709,7 @@ TEST_F(EPDFFontEmbedderTest, WidgetDrawsEachClusterInAFontThatHasIt) { ASSERT_NE(0u, arimo_id); ASSERT_TRUE(EPDFFont_AddFallbackFont(arimo_id, /*tier=*/0)); - CreateEmptyDocument(); + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); ScopedFPDFAnnotation annot( EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); @@ -1506,7 +1749,7 @@ TEST_F(EPDFFontEmbedderTest, WidgetDrawsWhatNoFontHasWithTheLastResort) { ASSERT_NE(0u, box_id); ASSERT_TRUE(EPDFFont_SetLastResortFont(box_id)); - CreateEmptyDocument(); + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); ScopedFPDFAnnotation annot( EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); @@ -1534,7 +1777,7 @@ TEST_F(EPDFFontEmbedderTest, WidgetDrawsWhatNoFontHasWithTheLastResort) { // A password field writes stars and nothing else: not the value, and no // ActualText that would carry it. TEST_F(EPDFFontEmbedderTest, PasswordFieldWritesOnlyStars) { - CreateEmptyDocument(); + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); ScopedFPDFAnnotation annot( EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); @@ -1635,7 +1878,8 @@ std::vector AppearanceFontPrograms(FPDF_ANNOTATION annot) { TEST_F(EPDFFontEmbedderTest, ResolvesTheProgramARunNames) { ScopedRegisteredFonts scoped_fonts; const std::string pdf = TwoProgramsDescribedAlike(); - ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + embedpdf_test::ScopedTestDocument doc( + embedpdf_test::LoadMemDocumentForTest(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(doc); ScopedFPDFWideString text = GetFPDFWideString(L"Hello"); ScopedFPDFWideString family = GetFPDFWideString(L"Mono"); @@ -1665,7 +1909,8 @@ TEST_F(EPDFFontEmbedderTest, ResolvesTheProgramARunNames) { TEST_F(EPDFFontEmbedderTest, RichTextDrawsTheProgramItsStyleNames) { ScopedRegisteredFonts scoped_fonts; const std::string pdf = TwoProgramsDescribedAlike(); - ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + embedpdf_test::ScopedTestDocument doc( + embedpdf_test::LoadMemDocumentForTest(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(doc); ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); ASSERT_TRUE(page); diff --git a/fpdfsdk/epdf_text_embeddertest.cpp b/fpdfsdk/epdf_text_embeddertest.cpp index 86da683286..8b64c96f0e 100644 --- a/fpdfsdk/epdf_text_embeddertest.cpp +++ b/fpdfsdk/epdf_text_embeddertest.cpp @@ -17,6 +17,7 @@ #include "public/cpp/fpdf_scopers.h" #include "public/fpdf_text.h" #include "testing/embedder_test.h" +#include "testing/embedpdf_layer_mode.h" #include "testing/fx_string_testhelpers.h" #include "testing/gmock/include/gmock/gmock.h" @@ -183,7 +184,8 @@ TEST_F(EPDFTextEmbedderTest, ActualTextInAFormIsInPageSpace) { "/Matrix [2 0 0 2 0 0] /Resources << /Font << /F1 5 0 R >> >>", std::string("BT /F1 12 Tf 10 25 Td ") + kSpan + " ET"), }); - ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + embedpdf_test::ScopedTestDocument doc( + embedpdf_test::LoadMemDocumentForTest(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(doc); ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); ASSERT_TRUE(page); @@ -267,7 +269,8 @@ TEST_F(EPDFTextEmbedderTest, ActualTextReadsAsWritten) { Stream(content), "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", }); - ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + embedpdf_test::ScopedTestDocument doc( + embedpdf_test::LoadMemDocumentForTest(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(doc); ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); ASSERT_TRUE(page); diff --git a/testing/embedder_test.cpp b/testing/embedder_test.cpp index ab4fcda176..9677c6a93d 100644 --- a/testing/embedder_test.cpp +++ b/testing/embedder_test.cpp @@ -685,6 +685,30 @@ bool EmbedderTest::OpenDocumentWithOptions(const std::string& filename, &form_handle_); } +// EmbedPDF(layer-mode): what OpenDocumentWithOptions() does with a file's +// contents, with `contents`. +bool EmbedderTest::OpenDocumentFromMemory(std::string_view contents) { + if (contents.empty()) { + return false; + } + file_contents_.assign(contents.begin(), contents.end()); + + EXPECT_TRUE(!loader_); + loader_ = std::make_unique(file_contents_); + + file_access_ = { + .m_FileLen = pdfium::checked_cast(file_contents_.size()), + .m_GetBlock = TestLoader::GetBlock, + .m_Param = loader_.get(), + }; + + fake_file_access_ = std::make_unique(&file_access_); + return OpenDocumentHelper("", LinearizeOption::kDefaultLinearize, + JavaScriptOption::kEnableJavaScript, + fake_file_access_.get(), &document_, &avail_, + &form_handle_); +} + bool EmbedderTest::OpenDocumentHelper(const ByteString& password, LinearizeOption linearize_option, JavaScriptOption javascript_option, diff --git a/testing/embedder_test.h b/testing/embedder_test.h index 19ebdfc03c..7d7402798b 100644 --- a/testing/embedder_test.h +++ b/testing/embedder_test.h @@ -215,6 +215,9 @@ class EmbedderTest : public ::testing::Test, bool OpenDocumentWithPassword(const std::string& filename, const ByteString& password); bool OpenDocumentWithoutJavaScript(const std::string& filename); + // EmbedPDF(layer-mode): OpenDocument() for a document a test builds in + // memory, so that in layer mode it opens as a layer, as a file does. + bool OpenDocumentFromMemory(std::string_view contents); // Close the document from a previous OpenDocument() call. This happens // automatically at tear-down, and is usually not explicitly required, diff --git a/testing/embedpdf_layer_fixture.h b/testing/embedpdf_layer_fixture.h index 08a4a34ed7..356aff52e7 100644 --- a/testing/embedpdf_layer_fixture.h +++ b/testing/embedpdf_layer_fixture.h @@ -8,6 +8,7 @@ #include #include #include "public/epdf_layer.h" +#include "public/epdf_save.h" #include "public/fpdf_save.h" #include "public/fpdfview.h" #include "testing/utils/file_util.h" @@ -79,6 +80,24 @@ struct LayerFixture { return layer && status == EPDFLayerOpenStatus_kSuccess; } + // The layer's changes as a session saves them, for Reopen(); empty when it + // has none or the save fails. + std::vector SaveDelta() const { + unsigned long size = 0; + EPDFLayerSaveStatus status = EPDFLayerSaveStatus_kSaveFailed; + void* buffer = EPDFLayer_SaveDeltaToOwnedBuffer(layer, &size, &status); + std::vector delta; + if (!buffer) { + return delta; + } + if (status == EPDFLayerSaveStatus_kSuccess) { + const uint8_t* data = static_cast(buffer); + delta.assign(data, data + size); + } + EPDF_FreeBuffer(buffer); + return delta; + } + // Close the layer and open a new one over the same base with |delta| as // the loaded delta: what a session reopening its saved layer does. bool Reopen(std::vector delta) { diff --git a/testing/embedpdf_layer_mode.cpp b/testing/embedpdf_layer_mode.cpp index 9f1b8244ac..cbe3a0c530 100644 --- a/testing/embedpdf_layer_mode.cpp +++ b/testing/embedpdf_layer_mode.cpp @@ -14,6 +14,22 @@ namespace embedpdf_test { +namespace { + +// A fresh layer over `base`, which keeps the base alive. +FPDF_DOCUMENT OpenLayerOver(EPDF_BASE_DOCUMENT base) { + if (!base) { + return nullptr; + } + FPDF_DOCUMENT layer = EPDFLayer_OpenLayer(base, /*pFileAccess=*/nullptr, + /*password=*/nullptr, + /*out_status=*/nullptr); + EPDF_ReleaseBaseDocument(base); + return layer; +} + +} // namespace + bool IsLayerModeEnabled() { static const bool enabled = [] { const char* value = getenv("EPDF_EMBEDDER_LAYER_MODE"); @@ -27,16 +43,16 @@ FPDF_DOCUMENT LoadCustomDocumentForTest(FPDF_FILEACCESS* file_access, if (!IsLayerModeEnabled()) { return FPDF_LoadCustomDocument(file_access, password); } - EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(file_access, password); - if (!base) { - return nullptr; + return OpenLayerOver(EPDF_LoadBaseDocument(file_access, password)); +} + +FPDF_DOCUMENT LoadMemDocumentForTest(const void* data, + size_t size, + FPDF_BYTESTRING password) { + if (!IsLayerModeEnabled()) { + return FPDF_LoadMemDocument64(data, size, password); } - FPDF_DOCUMENT layer = EPDFLayer_OpenLayer(base, /*pFileAccess=*/nullptr, - /*password=*/nullptr, - /*out_status=*/nullptr); - // The layer holds its own reference to the base. - EPDF_ReleaseBaseDocument(base); - return layer; + return OpenLayerOver(EPDF_LoadMemBaseDocument64(data, size, password)); } void NoteClosingDocument(FPDF_DOCUMENT document) { @@ -60,4 +76,9 @@ void NoteClosingDocument(FPDF_DOCUMENT document) { fclose(file); } +void TestDocumentCloser::operator()(FPDF_DOCUMENT document) const { + NoteClosingDocument(document); + FPDF_CloseDocument(document); +} + } // namespace embedpdf_test diff --git a/testing/embedpdf_layer_mode.h b/testing/embedpdf_layer_mode.h index 170b974e79..498ea8c585 100644 --- a/testing/embedpdf_layer_mode.h +++ b/testing/embedpdf_layer_mode.h @@ -4,6 +4,11 @@ #ifndef TESTING_EMBEDPDF_LAYER_MODE_H_ #define TESTING_EMBEDPDF_LAYER_MODE_H_ +#include + +#include +#include + #include "public/fpdfview.h" namespace embedpdf_test { @@ -23,6 +28,13 @@ bool IsLayerModeEnabled(); FPDF_DOCUMENT LoadCustomDocumentForTest(FPDF_FILEACCESS* file_access, FPDF_BYTESTRING password); +// FPDF_LoadMemDocument64(), or in layer mode a fresh layer over a base loaded +// from `data`: a document a test builds in memory goes through the layer as +// one it opens from a file does. `data` must outlive the document either way. +FPDF_DOCUMENT LoadMemDocumentForTest(const void* data, + size_t size, + FPDF_BYTESTRING password); + // Call just before closing a document a test opened. In layer mode, with // EPDF_EMBEDDER_LAYER_MODE_REPORT= set, appends ` ` to // that file: how many objects the layer holds. A test that only reads must @@ -30,6 +42,13 @@ FPDF_DOCUMENT LoadCustomDocumentForTest(FPDF_FILEACCESS* file_access, // known to write. void NoteClosingDocument(FPDF_DOCUMENT document); +// Closes a document a test opened itself, NoteClosingDocument() first. +struct TestDocumentCloser { + void operator()(FPDF_DOCUMENT document) const; +}; +using ScopedTestDocument = + std::unique_ptr, TestDocumentCloser>; + } // namespace embedpdf_test #endif // TESTING_EMBEDPDF_LAYER_MODE_H_ diff --git a/testing/embedpdf_layer_mode_writers.txt b/testing/embedpdf_layer_mode_writers.txt index 6358af3088..3ef2323d36 100644 --- a/testing/embedpdf_layer_mode_writers.txt +++ b/testing/embedpdf_layer_mode_writers.txt @@ -62,6 +62,8 @@ EPDFAnnotEmbedderTest.RedactVerticalTextMiddleGlyphsKeepsTailInPlace 3 EPDFAnnotEmbedderTest.RemoveKey 1 EPDFAnnotEmbedderTest.RichTextCoversSupplementaryCharactersByScalar 10 EPDFAnnotEmbedderTest.RichTextEmptyBoxNeverNamesACmaplessSubset 5 +EPDFAnnotEmbedderTest.RichTextLastResortDrawsWhatNoFontHas 20 +EPDFAnnotEmbedderTest.RichTextPreviewAndPrintLastResortWaits 25 EPDFAnnotEmbedderTest.RichTextSkipsDocumentSubsetWithoutCmap 6 EPDFAnnotEmbedderTest.RotatedCaretAppearanceCarriesTransform 3 EPDFAnnotEmbedderTest.TextFieldDrawsItsBorderStyle 2 @@ -77,6 +79,17 @@ EPDFFlattenEmbedderTest.FlattenMergedWidgetRemovesFieldTreeEntry 12 EPDFFlattenEmbedderTest.FlattenPageHonorsPrintUsage 10 EPDFFlattenEmbedderTest.FlattenPagePreservesUnpaintedAnnotationsAndDetachesWidget 12 EPDFFlattenEmbedderTest.FlattenSpecificAnnotationByHandle 7 +EPDFFontEmbedderTest.FreeTextReportsCharactersNoFontHas 6 +EPDFFontEmbedderTest.ListsADocumentProgramAndHandsOutItsBytes 25 +EPDFFontEmbedderTest.PasswordFieldWritesOnlyStars 8 +EPDFFontEmbedderTest.RealignsAProgramABrowserWouldRefuse 25 +EPDFFontEmbedderTest.ResolvesADocumentProgram 25 +EPDFFontEmbedderTest.RichTextDrawsTheProgramItsStyleNames 22 +EPDFFontEmbedderTest.SimpleFieldFontWritesThroughItsProgram 13 +EPDFFontEmbedderTest.UnauthorizedRegisteredFontDoesNotTakeOverABox 25 +EPDFFontEmbedderTest.WidgetDrawsEachClusterInAFontThatHasIt 20 +EPDFFontEmbedderTest.WidgetDrawsWhatNoFontHasWithTheLastResort 20 +EPDFFontEmbedderTest.WidgetWritesWithADocumentProgram 52 EPDFFormEmbedderTest.AttachToLegacyMergedFieldKeepsFieldId 6 EPDFFormEmbedderTest.AttachWidgetsFormsARadioGroup 11 EPDFFormEmbedderTest.CalculationOrderSetReplaceAndRemove 1 @@ -97,6 +110,7 @@ EPDFFormEmbedderTest.FieldSettersValidateAndApply 3 EPDFFormEmbedderTest.ListBoxTopIndexIsRead 1 EPDFFormEmbedderTest.ListBoxValuesAreAnArray 1 EPDFFormEmbedderTest.MalformedDefaultReadsAsUnsupported 1 +EPDFFormEmbedderTest.NewFieldAppearanceIsAFormXObject 7 EPDFFormEmbedderTest.NoneShadowsAParentFieldsValue 2 EPDFFormEmbedderTest.PushButtonIsAuthorableAndShowsItsCaption 7 EPDFFormEmbedderTest.RepairBakeClearsNeedAppearances 24