diff --git a/core/fpdfdoc/cpdf_richtextwriter.cpp b/core/fpdfdoc/cpdf_richtextwriter.cpp
index c9fec0bcf..f3b0f9ac5 100644
--- a/core/fpdfdoc/cpdf_richtextwriter.cpp
+++ b/core/fpdfdoc/cpdf_richtextwriter.cpp
@@ -224,6 +224,45 @@ WideString SpanStyle(const CPDF_RichTextStyleDelta& delta) {
return css;
}
+// Acrobat's XHTML envelope around the paragraphs, the body styled with
+// |body_css| when it says anything.
+WideString RichContent(const WideString& body_css,
+ const CPDF_RichTextDocument& document) {
+ WideString xml =
+ L"
";
+ for (const CPDF_RichTextParagraph& paragraph : document.paragraphs) {
+ xml += L"";
+ for (const CPDF_RichTextRun& run : paragraph.runs) {
+ const WideString span_css = SpanStyle(run.style);
+ if (span_css.IsEmpty()) {
+ xml += EscapeXml(run.text);
+ } else {
+ xml += L"" +
+ EscapeXml(run.text) + L"";
+ }
+ }
+ xml += L"
";
+ }
+ xml += L"";
+ return xml;
+}
+
} // namespace
CPDF_RichTextWriter::Prepared::Prepared() = default;
@@ -277,36 +316,61 @@ WideString CPDF_RichTextWriter::SerializeRC(
if (!extra_paragraph.IsEmpty()) {
body_css += L";" + extra_paragraph;
}
+ return RichContent(body_css, document);
+}
- WideString xml =
- L"";
- for (const CPDF_RichTextParagraph& paragraph : document.paragraphs) {
- xml += L"";
- for (const CPDF_RichTextRun& run : paragraph.runs) {
- const WideString span_css = SpanStyle(run.style);
- if (span_css.IsEmpty()) {
- xml += EscapeXml(run.text);
- } else {
- xml += L"" +
- EscapeXml(run.text) + L"";
- }
+// static
+WideString CPDF_RichTextWriter::SerializeCommentRC(
+ const CPDF_RichTextDocument& document) {
+ // What the parser reads from a comment that states nothing.
+ const CPDF_RichTextStyle assumed;
+ const CPDF_RichTextParagraphProps assumed_paragraph;
+ const CPDF_RichTextStyle& body = document.body;
+ WideString body_css;
+ if (body.size != assumed.size) {
+ AppendDeclaration(&body_css, L"font-size", Points(body.size));
+ }
+ if (document.body_paragraph.align != assumed_paragraph.align) {
+ AppendDeclaration(&body_css, L"text-align",
+ AlignName(document.body_paragraph.align));
+ }
+ if (body.color != assumed.color) {
+ AppendDeclaration(&body_css, L"color", HexColor(body.color));
+ }
+ if (body.weight != assumed.weight) {
+ AppendDeclaration(&body_css, L"font-weight", WeightName(body.weight));
+ }
+ if (body.italic != assumed.italic) {
+ AppendDeclaration(&body_css, L"font-style", L"italic");
+ }
+ if (body.family != assumed.family) {
+ AppendDeclaration(&body_css, L"font-family", FamilyValue(body.family));
+ }
+ if (body.decoration) {
+ AppendDeclaration(&body_css, L"text-decoration",
+ DecorationValue(body.decoration));
+ }
+ if (body.letter_spacing != assumed.letter_spacing) {
+ AppendDeclaration(&body_css, L"letter-spacing",
+ Points(body.letter_spacing));
+ }
+ if (body.horz_scale != assumed.horz_scale) {
+ AppendDeclaration(
+ &body_css, L"xfa-font-horizontal-scale",
+ WideString::Format(L"%d",
+ static_cast(body.horz_scale * 100 + 0.5f)));
+ }
+ CPDF_RichTextParagraphProps without_align = document.body_paragraph;
+ without_align.align = assumed_paragraph.align;
+ const WideString extra_paragraph =
+ ParagraphStyle(without_align, assumed_paragraph);
+ if (!extra_paragraph.IsEmpty()) {
+ if (!body_css.IsEmpty()) {
+ body_css += L";";
}
- xml += L"
";
+ body_css += extra_paragraph;
}
- xml += L"";
- return xml;
+ return RichContent(body_css, document);
}
// static
@@ -444,3 +508,15 @@ bool CPDF_RichTextWriter::Apply(CPDF_Document* doc,
Publish(doc, annot_dict, std::move(prepared));
return true;
}
+
+// static
+void CPDF_RichTextWriter::ApplyComment(CPDF_Dictionary* annot_dict,
+ const CPDF_RichTextDocument& document) {
+ if (!annot_dict) {
+ return;
+ }
+ annot_dict->SetNewFor(
+ "RC", SerializeCommentRC(document).AsStringView());
+ annot_dict->SetNewFor("Contents",
+ ProjectPlainText(document).AsStringView());
+}
diff --git a/core/fpdfdoc/cpdf_richtextwriter.h b/core/fpdfdoc/cpdf_richtextwriter.h
index 50eaa6db6..7b86aaf2a 100644
--- a/core/fpdfdoc/cpdf_richtextwriter.h
+++ b/core/fpdfdoc/cpdf_richtextwriter.h
@@ -5,6 +5,8 @@
// (Acrobat's XHTML envelope), /DS, /DA, /Contents and the appearance, in the
// two halves of the Phase C note §6. Prepare builds everything off to the
// side and touches nothing; Publish writes it all, or nothing was written.
+// Any other markup annotation's rich text is its comment (ISO 32000-2
+// §12.5.6.2), which draws nothing: ApplyComment writes /RC and /Contents.
#ifndef CORE_FPDFDOC_CPDF_RICHTEXTWRITER_H_
#define CORE_FPDFDOC_CPDF_RICHTEXTWRITER_H_
@@ -54,6 +56,10 @@ class CPDF_RichTextWriter {
CPDF_Dictionary* annot_dict,
const CPDF_RichTextDocument& document,
bool* degraded);
+ // A markup annotation's comment: /RC (SerializeCommentRC) and its plain
+ // projection in /Contents. The appearance stays: a comment draws nothing.
+ static void ApplyComment(CPDF_Dictionary* annot_dict,
+ const CPDF_RichTextDocument& document);
// Test-only: forwards to CPDF_AnnotFontMap::FailAfterStagedFontsForTesting.
static void FailAfterStagedFontsForTesting(int staged_count);
@@ -61,6 +67,11 @@ class CPDF_RichTextWriter {
// The four forms, exposed for tests. |da_color| is the annotation's DA
// (border) colour, which DS repeats by Acrobat's convention.
static WideString SerializeRC(const CPDF_RichTextDocument& document);
+ // A comment's /RC, as Acrobat writes a note's: the body states only what
+ // differs from what a reader assumes when it states nothing (its own face,
+ // 12 pt, black, left-aligned), so a comment written with the body it was
+ // read with reads back the same.
+ static WideString SerializeCommentRC(const CPDF_RichTextDocument& document);
static WideString BuildDefaultStyle(const CPDF_RichTextDocument& document,
FX_ARGB da_color);
static WideString ProjectPlainText(const CPDF_RichTextDocument& document);
diff --git a/fpdfsdk/epdf_annot.cpp b/fpdfsdk/epdf_annot.cpp
index 0e51d4e16..1a92680e2 100644
--- a/fpdfsdk/epdf_annot.cpp
+++ b/fpdfsdk/epdf_annot.cpp
@@ -1777,8 +1777,26 @@ EPDFAnnot_GetRichTextJSON(FPDF_ANNOTATION annot,
namespace {
-// The rich text setters share one path: build the document, then write
-// all four forms and the appearance through CPDF_RichTextWriter, or nothing.
+// The kinds ISO 32000-2 Table 171 marks as markup: the ones /RC is an entry
+// of (§12.5.6.2).
+bool IsMarkupSubtype(const ByteString& subtype) {
+ static constexpr const char* kMarkup[] = {
+ "Text", "FreeText", "Line", "Square", "Circle",
+ "Polygon", "PolyLine", "Highlight", "Underline", "Squiggly",
+ "StrikeOut", "Caret", "Stamp", "Ink", "FileAttachment",
+ "Sound", "Redact", "Projection"};
+ for (const char* name : kMarkup) {
+ if (subtype == name) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// The rich text setters share one path. A free text draws its rich text:
+// all four forms and the appearance through CPDF_RichTextWriter, or
+// nothing. Another markup annotation's is its comment: /RC and /Contents.
+// Every other kind has none.
bool ApplyRichTextDocument(FPDF_ANNOTATION annot,
const CPDF_RichTextDocument& document) {
CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot);
@@ -1786,9 +1804,17 @@ bool ApplyRichTextDocument(FPDF_ANNOTATION annot,
return false;
}
RetainPtr annot_dict = context->GetMutableAnnotDict();
- if (!annot_dict || annot_dict->GetNameFor("Subtype") != "FreeText") {
+ if (!annot_dict) {
return false;
}
+ const ByteString subtype = annot_dict->GetNameFor("Subtype");
+ if (subtype != "FreeText") {
+ if (!IsMarkupSubtype(subtype)) {
+ return false;
+ }
+ CPDF_RichTextWriter::ApplyComment(annot_dict.Get(), document);
+ return true;
+ }
CPDF_Document* doc = context->GetPage()->GetDocument();
if (!doc) {
return false;
diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp
index 52ea54d9d..475b1cf5b 100644
--- a/fpdfsdk/epdf_annot_embeddertest.cpp
+++ b/fpdfsdk/epdf_annot_embeddertest.cpp
@@ -4164,6 +4164,130 @@ TEST_F(EPDFAnnotEmbedderTest, SetRichTextXHTMLImportsOverDefaults) {
EXPECT_NE(std::string::npos, json.find("\"size\":16"));
}
+// /RC is every markup annotation's comment (ISO 32000-2 §12.5.6.2). Acrobat
+// 26 writes a note's with no body style, Enter as "
" inside a span, and
+// "font-size:-14.0pt" on every span, a size nothing can be: the body's
+// stays.
+TEST_F(EPDFAnnotEmbedderTest, AcrobatNoteCommentReadsAsRichText) {
+ ASSERT_TRUE(OpenDocument("text_rich_comment_acrobat.pdf"));
+ ScopedPage page = LoadScopedPage(0);
+ ASSERT_TRUE(page);
+ ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0));
+ ASSERT_EQ(FPDF_ANNOT_TEXT, FPDFAnnot_GetSubtype(annot.get()));
+ const std::string json = GetRichTextJson(annot.get());
+ EXPECT_NE(std::string::npos, json.find("\"source\":\"rc\"")) << json;
+ EXPECT_NE(std::string::npos,
+ json.find("\"body\":{\"family\":\"\",\"weight\":400,"
+ "\"italic\":false,\"size\":12,"))
+ << json;
+ EXPECT_NE(std::string::npos,
+ json.find("{\"text\":\"test\",\"style\":{\"weight\":700,"
+ "\"italic\":false}}"))
+ << json;
+ EXPECT_NE(std::string::npos,
+ json.find("{\"text\":\"you\",\"style\":{\"weight\":400,"
+ "\"italic\":false,\"decoration\":[\"underline\"]}}"))
+ << json;
+ EXPECT_NE(std::string::npos, json.find("{\"text\":\"?\\r\\r\",")) << json;
+ EXPECT_NE(std::string::npos,
+ json.find("{\"text\":\"Test\",\"style\":{\"weight\":400,"
+ "\"italic\":true}}"))
+ << json;
+}
+
+// Any other markup annotation's rich text is its comment, which draws
+// nothing: /RC and /Contents, as Acrobat writes a note's, and no /DS, /DA or
+// appearance. The body states only what differs from a comment that states
+// nothing, so the one it was read with is written as nothing.
+TEST_F(EPDFAnnotEmbedderTest, SetRichTextJSONWritesACommentAsAcrobatDoes) {
+ embedpdf_test::ScopedTestDocument doc(NewTestDocument());
+ ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400));
+ ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT));
+ ASSERT_TRUE(annot);
+ const FS_RECTF rect{50.0f, 320.0f, 70.0f, 300.0f};
+ ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect));
+
+ ASSERT_TRUE(EPDFAnnot_SetRichTextJSON(
+ annot.get(),
+ "{\"paragraphs\":[{\"runs\":[{\"text\":\"Hello \"},"
+ "{\"text\":\"bold\",\"style\":{\"weight\":700}}]},"
+ "{\"runs\":[{\"text\":\"two\\rlines\"}]}]}"));
+ CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot.get());
+ const CPDF_Dictionary* dict = context->GetAnnotDict();
+ EXPECT_EQ(
+ L""
+ L"Hello bold
"
+ L"two
lines
",
+ dict->GetUnicodeTextFor("RC"));
+ EXPECT_EQ(L"Hello bold\rtwo\rlines", dict->GetUnicodeTextFor("Contents"));
+ EXPECT_FALSE(dict->KeyExist("DS"));
+ EXPECT_FALSE(dict->KeyExist("DA"));
+ EXPECT_FALSE(dict->KeyExist("AP"));
+
+ const std::string json = GetRichTextJson(annot.get());
+ EXPECT_NE(std::string::npos, json.find("\"source\":\"rc\"")) << json;
+ EXPECT_NE(std::string::npos, json.find("\"family\":\"\",")) << json;
+ EXPECT_NE(std::string::npos,
+ json.find("{\"text\":\"bold\",\"style\":{\"weight\":700}}"))
+ << json;
+
+ // A body states what differs from that: here its size and colour.
+ ScopedFPDFAnnotation square(
+ FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_SQUARE));
+ ASSERT_TRUE(square);
+ ASSERT_TRUE(EPDFAnnot_SetRichTextJSON(
+ square.get(),
+ "{\"body\":{\"size\":10,\"color\":\"#FF0000\"},"
+ "\"paragraphs\":[{\"runs\":[{\"text\":\"Note\"}]}]}"));
+ EXPECT_TRUE(CPDFAnnotContextFromFPDFAnnotation(square.get())
+ ->GetAnnotDict()
+ ->GetUnicodeTextFor("RC")
+ .Contains(L"xfa:spec=\"2.0.2\" "
+ L"style=\"font-size:10.0pt;color:#FF0000\">"
+ L"Note