From 8bdd5c097be2c29f72a70dd13e0a1e98ce28f65c Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Wed, 4 Nov 2026 14:00:31 +0200 Subject: [PATCH] Read a comment's rich text over the reader's defaults A markup annotation's /RC is its comment, which draws nothing, so it states its style over the reader's defaults, as Acrobat writes one with no body style. The parser read every annotation's /RC over its /DA and /DS. Among the comment kinds only a redact has a /DA, and it styles the overlay text: a redact's comment that stated no font read in the overlay's font, size and color, and one written with the default body read back in the overlay's style. DefaultsFromAnnotation now takes /DA, /DS, /Q and /TextColor only for a free text and a widget, whose rich text is the text they draw. EPDFAnnot_GetRichTextJSON, SetRichTextJSON and SetRichTextXHTML all go through it. --- core/fpdfdoc/cpdf_richtextparser.cpp | 7 +++- core/fpdfdoc/cpdf_richtextparser.h | 13 +++++-- fpdfsdk/epdf_annot_embeddertest.cpp | 49 +++++++++++++++++++++++++ public/epdf_annot.h | 7 ++-- testing/embedpdf_layer_mode_writers.txt | 1 + 5 files changed, 69 insertions(+), 8 deletions(-) diff --git a/core/fpdfdoc/cpdf_richtextparser.cpp b/core/fpdfdoc/cpdf_richtextparser.cpp index bca8d00b81..42ae7be2d3 100644 --- a/core/fpdfdoc/cpdf_richtextparser.cpp +++ b/core/fpdfdoc/cpdf_richtextparser.cpp @@ -1273,6 +1273,11 @@ void CPDF_RichTextParser::DefaultsFromAnnotation( if (!annot_dict) { return; } + // A comment draws nothing: the reader's defaults stand. + const ByteString subtype = annot_dict->GetNameFor("Subtype"); + if (subtype != "FreeText" && subtype != "Widget") { + return; + } // /DA: font name (→ face through /DR), size, colour. CPDF_DefaultAppearance da(annot_dict, acroform_dict); @@ -1321,7 +1326,7 @@ void CPDF_RichTextParser::DefaultsFromAnnotation( } defaults = ApplyRichTextStyleDelta(defaults, ds_delta); } - if (defaults.size <= 0 && annot_dict->GetNameFor("Subtype") == "FreeText") { + if (defaults.size <= 0 && subtype == "FreeText") { defaults.size = 12.0f; // DA size 0 (auto) has no meaning for rich text } } diff --git a/core/fpdfdoc/cpdf_richtextparser.h b/core/fpdfdoc/cpdf_richtextparser.h index ec14747240..5f84253cd7 100644 --- a/core/fpdfdoc/cpdf_richtextparser.h +++ b/core/fpdfdoc/cpdf_richtextparser.h @@ -53,15 +53,20 @@ class CPDF_RichTextParser { const CPDF_RichTextStyle& defaults, const CPDF_RichTextParagraphProps& paragraph_defaults); - // The document an annotation holds: /RC (or /RV for widgets) over - // DA ∪ DS defaults, else its /Contents (or /V) as plain text. + // The document an annotation holds: /RC (or /RV for widgets) over its + // defaults (DefaultsFromAnnotation()), else its /Contents (or /V) as plain + // text. static CPDF_RichTextDocument FromAnnotation( const CPDF_Dictionary* annot_dict, const CPDF_Dictionary* acroform_dict, const CPDF_Document* doc = nullptr); - // DA ∪ DS alone (no /RC): the defaults an imported XHTML body style is - // applied on top of. + // The defaults an annotation's rich text is read over, and an imported + // XHTML body style is applied on top of. A free text's and a widget's + // rich text is the text they draw, so its defaults are DA ∪ DS (and /Q, + // /TextColor). Any other annotation's rich text is its comment, which + // draws nothing: its defaults are the reader's (CPDF_RichTextStyle{}), + // whatever a /DA says. A redact's /DA styles its overlay text. static void DefaultsFromAnnotation( const CPDF_Dictionary* annot_dict, const CPDF_Dictionary* acroform_dict, diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp index 475b1cf5b8..c67ccef714 100644 --- a/fpdfsdk/epdf_annot_embeddertest.cpp +++ b/fpdfsdk/epdf_annot_embeddertest.cpp @@ -4288,6 +4288,55 @@ TEST_F(EPDFAnnotEmbedderTest, SetRichTextJSONRefusesKindsThatArentMarkup) { } } +// A redact's /DA styles its overlay text, not its comment: a comment that +// states no style reads in the reader's defaults, written by JSON or +// imported from XHTML, and the overlay keeps its /DA. +TEST_F(EPDFAnnotEmbedderTest, RedactCommentReadsOverTheReadersDefaults) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_REDACT)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 320.0f, 200.0f, 280.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_COURIER, + 10.0f, 255, 0, 0)); + const std::string reader_body = + "\"body\":{\"family\":\"\",\"weight\":400,\"italic\":false," + "\"size\":12,\"color\":\"#000000\","; + + ASSERT_TRUE(EPDFAnnot_SetRichTextJSON( + annot.get(), + "{\"paragraphs\":[{\"runs\":[{\"text\":\"Approved by \"}," + "{\"text\":\"legal\",\"style\":{\"weight\":700}}]}]}")); + const CPDF_Dictionary* dict = + CPDFAnnotContextFromFPDFAnnotation(annot.get())->GetAnnotDict(); + EXPECT_TRUE(dict->GetUnicodeTextFor("RC").Contains( + L"xfa:spec=\"2.0.2\">

Approved by ")); + std::string json = GetRichTextJson(annot.get()); + EXPECT_NE(std::string::npos, json.find("\"source\":\"rc\"")) << json; + EXPECT_NE(std::string::npos, json.find(reader_body)) << json; + + ScopedFPDFWideString xhtml = GetFPDFWideString( + L"

Imported

"); + ASSERT_TRUE(EPDFAnnot_SetRichTextXHTML(annot.get(), xhtml.get())); + EXPECT_TRUE(dict->GetUnicodeTextFor("RC").Contains( + L"xfa:spec=\"2.0.2\">

Imported

")); + json = GetRichTextJson(annot.get()); + EXPECT_NE(std::string::npos, json.find(reader_body)) << json; + + FPDF_STANDARD_FONT font = FPDF_FONT_UNKNOWN; + float font_size = 0; + unsigned int r = 0; + unsigned int g = 0; + unsigned int b = 0; + ASSERT_TRUE(EPDFAnnot_GetDefaultAppearance(annot.get(), &font, &font_size, &r, + &g, &b)); + EXPECT_EQ(FPDF_FONT_COURIER, font); + EXPECT_EQ(10.0f, font_size); + EXPECT_EQ(255u, r); +} + TEST_F(EPDFAnnotEmbedderTest, SetRichTextJSONWithRegisteredBodyFont) { ScopedRegisteredFonts scoped_fonts; std::vector roboto = LoadRobotoFontData(); diff --git a/public/epdf_annot.h b/public/epdf_annot.h index bf87aa6fb5..fb411207c6 100644 --- a/public/epdf_annot.h +++ b/public/epdf_annot.h @@ -494,7 +494,8 @@ EPDFAnnot_GetRichContent(FPDF_ANNOTATION annot, // Experimental EmbedPDF Extension API. // The rich text of a FreeText annotation (/RC over /DS and /DA), of another -// markup annotation's comment (/RC), or of a rich text field (/RV), as UTF-8 +// markup annotation's comment (/RC over the reader's defaults, whatever its +// /DA says), or of a rich text field (/RV over /DA), as UTF-8 // JSON: {"source":"rc"|"contents","body":{...}, // "paragraphs":[{"align","dir","runs":[{"text","style":{deltas}}]}], // "diagnostics":[{"code","detail"}]}. When there is no rich text the document @@ -535,8 +536,8 @@ EPDFAnnot_SetRichTextJSON(FPDF_ANNOTATION annot, FPDF_BYTESTRING json_utf8); // Experimental EmbedPDF Extension API (rich text, Phase D). // Import raw XHTML (the XFA rich text subset, e.g. another producer's /RC) -// over the annotation's DA and DS defaults; same effects and failures as -// EPDFAnnot_SetRichTextJSON(). Malformed XML returns false. +// over the defaults EPDFAnnot_GetRichTextJSON() reads it over; same effects +// and failures as EPDFAnnot_SetRichTextJSON(). Malformed XML returns false. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetRichTextXHTML(FPDF_ANNOTATION annot, FPDF_WIDESTRING xhtml); diff --git a/testing/embedpdf_layer_mode_writers.txt b/testing/embedpdf_layer_mode_writers.txt index 2a455f68c9..660ed98467 100644 --- a/testing/embedpdf_layer_mode_writers.txt +++ b/testing/embedpdf_layer_mode_writers.txt @@ -124,6 +124,7 @@ EPDFAnnotEmbedderTest.OpacityIsStoredAsGiven 3 EPDFAnnotEmbedderTest.PolylineKneePastTheMiterLimitIsBeveled 4 EPDFAnnotEmbedderTest.PreviewAndPrintFallbackDoesNotAuthorUntilAuthorized 25 EPDFAnnotEmbedderTest.ReadPurityRenderMarkupAnnotWithContents 1 +EPDFAnnotEmbedderTest.RedactCommentReadsOverTheReadersDefaults 6 EPDFAnnotEmbedderTest.RedactInQuadsMultipleRegionsOnOneTextObject 3 EPDFAnnotEmbedderTest.RedactInQuadsRemovesTextInsideFormXObject 5 EPDFAnnotEmbedderTest.RedactLabelRegisteredFontInstallsRealDrEntry 12