Add experimental FPDFAnnot_SetFontColor() API
Add an API to mirror FPDFAnnot_GetFontColor(). For now, the
implementation stays consistent with FPDFAnnot_SetBorder() and removes
the existing appearance stream, if any.
Bug: 397836729
Change-Id: I675a4f35c8b481dbb3a64713017c9f55106d121a
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/131830
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/core/fpdfdoc/cpdf_generateap.cpp b/core/fpdfdoc/cpdf_generateap.cpp
index 03077e2..a5ef72c 100644
--- a/core/fpdfdoc/cpdf_generateap.cpp
+++ b/core/fpdfdoc/cpdf_generateap.cpp
@@ -92,18 +92,24 @@
return PDF_EncodeString(words) + " Tj\n";
}
+ByteString StringFromFontNameAndSize(const ByteString& font_name,
+ float font_size) {
+ fxcrt::ostringstream font_stream;
+ if (font_name.GetLength() > 0 && font_size > 0) {
+ font_stream << "/" << font_name << " ";
+ WriteFloat(font_stream, font_size) << " Tf\n";
+ }
+ return ByteString(font_stream);
+}
+
ByteString GetFontSetString(IPVT_FontMap* font_map,
int32_t font_index,
float font_size) {
- fxcrt::ostringstream font_stream;
- if (font_map) {
- ByteString font_alias = font_map->GetPDFFontAlias(font_index);
- if (font_alias.GetLength() > 0 && font_size > 0) {
- font_stream << "/" << font_alias << " ";
- WriteFloat(font_stream, font_size) << " Tf\n";
- }
+ if (!font_map) {
+ return ByteString();
}
- return ByteString(font_stream);
+ return StringFromFontNameAndSize(font_map->GetPDFFontAlias(font_index),
+ font_size);
}
void SetVtFontSize(float font_size, CPVT_VariableText& vt) {
@@ -1601,3 +1607,44 @@
return false;
}
}
+
+// static
+bool CPDF_GenerateAP::GenerateDefaultAppearanceWithColor(
+ CPDF_Document* doc,
+ CPDF_Dictionary* annot_dict,
+ const CFX_Color& color) {
+ RetainPtr<CPDF_Dictionary> root_dict = doc->GetMutableRoot();
+ if (!root_dict) {
+ return false;
+ }
+
+ RetainPtr<CPDF_Dictionary> acroform_dict =
+ root_dict->GetMutableDictFor("AcroForm");
+ if (!acroform_dict) {
+ acroform_dict = CPDF_InteractiveForm::InitAcroFormDict(doc);
+ CHECK(acroform_dict);
+ }
+
+ CPDF_DefaultAppearance default_appearance(annot_dict, acroform_dict);
+ auto maybe_font_name_and_size = default_appearance.GetFont();
+ if (!maybe_font_name_and_size.has_value()) {
+ return false;
+ }
+
+ ByteString new_default_appearance_font_name_and_size =
+ StringFromFontNameAndSize(maybe_font_name_and_size.value().name,
+ maybe_font_name_and_size.value().size);
+ if (new_default_appearance_font_name_and_size.IsEmpty()) {
+ return false;
+ }
+
+ ByteString new_default_appearance_color =
+ GenerateColorAP(color, PaintOperation::kFill);
+ CHECK(!new_default_appearance_color.IsEmpty());
+ annot_dict->SetNewFor<CPDF_String>(
+ "DA",
+ new_default_appearance_font_name_and_size + new_default_appearance_color);
+
+ // TODO(thestig): Call GenerateAnnotAP();
+ return true;
+}
diff --git a/core/fpdfdoc/cpdf_generateap.h b/core/fpdfdoc/cpdf_generateap.h
index 520ce4f..60b158b 100644
--- a/core/fpdfdoc/cpdf_generateap.h
+++ b/core/fpdfdoc/cpdf_generateap.h
@@ -11,6 +11,7 @@
class CPDF_Dictionary;
class CPDF_Document;
+struct CFX_Color;
class CPDF_GenerateAP {
public:
@@ -26,6 +27,10 @@
CPDF_Dictionary* pAnnotDict,
CPDF_Annot::Subtype subtype);
+ static bool GenerateDefaultAppearanceWithColor(CPDF_Document* doc,
+ CPDF_Dictionary* annot_dict,
+ const CFX_Color& color);
+
CPDF_GenerateAP() = delete;
CPDF_GenerateAP(const CPDF_GenerateAP&) = delete;
CPDF_GenerateAP& operator=(const CPDF_GenerateAP&) = delete;
diff --git a/core/fpdfdoc/cpdf_interactiveform.h b/core/fpdfdoc/cpdf_interactiveform.h
index ea3a07a..33c0917 100644
--- a/core/fpdfdoc/cpdf_interactiveform.h
+++ b/core/fpdfdoc/cpdf_interactiveform.h
@@ -105,6 +105,8 @@
const std::vector<UnownedPtr<CPDF_FormControl>>& GetControlsForField(
const CPDF_FormField* field);
+ CPDF_Document* document() { return document_; }
+
private:
void LoadField(RetainPtr<CPDF_Dictionary> field_dict, int nLevel);
void AddTerminalField(RetainPtr<CPDF_Dictionary> field_dict);
diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp
index d259908..2ead581 100644
--- a/fpdfsdk/fpdf_annot.cpp
+++ b/fpdfsdk/fpdf_annot.cpp
@@ -1484,6 +1484,46 @@
}
FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFAnnot_SetFontColor(FPDF_FORMHANDLE handle,
+ FPDF_ANNOTATION annot,
+ unsigned int R,
+ unsigned int G,
+ unsigned int B) {
+ RetainPtr<CPDF_Dictionary> annot_dict =
+ GetMutableAnnotDictFromFPDFAnnotation(annot);
+ if (!annot_dict || R > 255 || G > 255 || B > 255) {
+ return false;
+ }
+
+ const CPDF_Annot::Subtype subtype = CPDF_Annot::StringToAnnotSubtype(
+ annot_dict->GetNameFor(pdfium::annotation::kSubtype));
+ if (subtype != CPDF_Annot::Subtype::FREETEXT) {
+ // TODO(thestig): Consider adding widget support to mirror
+ // FPDFAnnot_GetFontColor().
+ return false;
+ }
+
+ CPDFSDK_InteractiveForm* form = FormHandleToInteractiveForm(handle);
+ if (!form) {
+ return false;
+ }
+
+ bool generated = CPDF_GenerateAP::GenerateDefaultAppearanceWithColor(
+ form->GetInteractiveForm()->document(), annot_dict, CFX_Color(R, G, B));
+ if (!generated) {
+ return false;
+ }
+
+ // Remove the appearance stream. Otherwise PDF viewers will render that and
+ // not use the new color.
+ //
+ // TODO(thestig) When GenerateDefaultAppearanceWithColor() properly updates
+ // the annotation's appearance stream, remove this.
+ annot_dict->RemoveFor(pdfium::annotation::kAP);
+ return true;
+}
+
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
FPDFAnnot_GetFontColor(FPDF_FORMHANDLE hHandle,
FPDF_ANNOTATION annot,
unsigned int* R,
diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp
index d666122..0417f74 100644
--- a/fpdfsdk/fpdf_annot_embeddertest.cpp
+++ b/fpdfsdk/fpdf_annot_embeddertest.cpp
@@ -2745,6 +2745,98 @@
}
}
+TEST_F(FPDFAnnotEmbedderTest, SetFontColor) {
+ static constexpr int kDimension = 200;
+ const char* original_checksum = []() {
+ if (CFX_DefaultRenderDevice::UseSkiaRenderer()) {
+#if BUILDFLAG(IS_WIN)
+ return "7e5b28c095c794fad32ab6d42a2f872f";
+#elif BUILDFLAG(IS_APPLE)
+ return "13349bf30b80250e1b2fa1f410cfdf02";
+#else
+ return "cb504dd6465c780887ec051df19912bb";
+#endif
+ }
+#if BUILDFLAG(IS_APPLE)
+ return "d00b5e669e922f2e1e9b442c8b896056";
+#else
+ return "7f2e777d88a8c4d914cf4bd38e9fdf0d";
+#endif
+ }();
+ const char* modified_checksum = []() {
+ if (CFX_DefaultRenderDevice::UseSkiaRenderer()) {
+#if BUILDFLAG(IS_WIN)
+ return "056eef1ffcbf522e64142ee99c50d6ec";
+#elif BUILDFLAG(IS_APPLE)
+ return "c736793c4c9f89c9c192d400d84f6979";
+#else
+ return "1407e39fd5ee2d999c62e642821a33ab";
+#endif
+ }
+#if BUILDFLAG(IS_APPLE)
+ return "1977b3820460c3a01f1047d30a0da25f";
+#else
+ return "5b339051f56d48dd7314c84e106a7c82";
+#endif
+ }();
+
+ ASSERT_TRUE(OpenDocument("freetext_annotation_without_da.pdf"));
+ ScopedPage page = LoadScopedPage(0);
+ ASSERT_TRUE(page);
+
+ {
+ ScopedFPDFBitmap bitmap = RenderLoadedPageWithFlags(page.get(), FPDF_ANNOT);
+ CompareBitmap(bitmap.get(), kDimension, kDimension, original_checksum);
+
+ // Obtain the only annotation and set its text color.
+ ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0));
+ ASSERT_TRUE(annot);
+
+ // TODO(thestig): Check FPDFAnnot_GetFontColor() results before and after,
+ // when the API supports freetext annotations.
+ ASSERT_TRUE(
+ FPDFAnnot_SetFontColor(form_handle(), annot.get(), 60, 120, 180));
+ bitmap = RenderLoadedPageWithFlags(page.get(), FPDF_ANNOT);
+ CompareBitmap(bitmap.get(), kDimension, kDimension, modified_checksum);
+ }
+
+ EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+
+ ASSERT_TRUE(OpenSavedDocument());
+ FPDF_PAGE saved_page = LoadSavedPage(0);
+ ASSERT_TRUE(saved_page);
+ VerifySavedRendering(saved_page, kDimension, kDimension, modified_checksum);
+
+ CloseSavedPage(saved_page);
+ CloseSavedDocument();
+}
+
+TEST_F(FPDFAnnotEmbedderTest, SetFontColorNegative) {
+ ASSERT_TRUE(OpenDocument("text_form_color.pdf"));
+ ScopedPage page = LoadScopedPage(0);
+ ASSERT_TRUE(page);
+
+ {
+ // Obtain the first annotation, a text field with orange color.
+ ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0));
+ ASSERT_TRUE(annot);
+
+ // Negative testing with invalid parameters.
+ ASSERT_FALSE(FPDFAnnot_SetFontColor(nullptr, nullptr, 256, 256, 256));
+ ASSERT_FALSE(FPDFAnnot_SetFontColor(form_handle(), nullptr, 0, 0, 0));
+ ASSERT_FALSE(FPDFAnnot_SetFontColor(nullptr, annot.get(), 0, 0, 0));
+ ASSERT_FALSE(FPDFAnnot_SetFontColor(nullptr, nullptr, 256, 0, 0));
+ ASSERT_FALSE(FPDFAnnot_SetFontColor(nullptr, nullptr, 0, 256, 0));
+ ASSERT_FALSE(FPDFAnnot_SetFontColor(nullptr, nullptr, 0, 0, 256));
+
+ // The text field widget in the PDF is not supported yet.
+ // TODO(thestig): Move out of this test case and make sure this succeeds
+ // after adding support.
+ ASSERT_FALSE(
+ FPDFAnnot_SetFontColor(form_handle(), annot.get(), 60, 120, 180));
+ }
+}
+
TEST_F(FPDFAnnotEmbedderTest, GetFontColor) {
// Open a file with textfield annotations and load its first page.
ASSERT_TRUE(OpenDocument("text_form_color.pdf"));
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index 6048a94..d2ec92e 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -94,6 +94,7 @@
CHK(FPDFAnnot_SetColor);
CHK(FPDFAnnot_SetFlags);
CHK(FPDFAnnot_SetFocusableSubtypes);
+ CHK(FPDFAnnot_SetFontColor);
CHK(FPDFAnnot_SetFormFieldFlags);
CHK(FPDFAnnot_SetRect);
CHK(FPDFAnnot_SetStringValue);
diff --git a/public/fpdf_annot.h b/public/fpdf_annot.h
index 525a9f9..0aa9f68 100644
--- a/public/fpdf_annot.h
+++ b/public/fpdf_annot.h
@@ -860,6 +860,27 @@
float* value);
// Experimental API.
+// Set the text color of an annotation.
+//
+// handle - handle to the form fill module, returned by
+// FPDFDOC_InitFormFillEnvironment.
+// annot - handle to an annotation.
+// R - the red component for the text color.
+// G - the green component for the text color.
+// B - the blue component for the text color.
+//
+// Returns true if successful.
+//
+// Currently supported subtypes: freetext.
+// The range for the color components is 0 to 255.
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFAnnot_SetFontColor(FPDF_FORMHANDLE handle,
+ FPDF_ANNOTATION annot,
+ unsigned int R,
+ unsigned int G,
+ unsigned int B);
+
+// Experimental API.
// Get the RGB value of the font color for an |annot| with variable text.
//
// hHandle - handle to the form fill module, returned by
diff --git a/testing/resources/freetext_annotation_without_da.pdf b/testing/resources/freetext_annotation_without_da.pdf
new file mode 100644
index 0000000..e68f344
--- /dev/null
+++ b/testing/resources/freetext_annotation_without_da.pdf
@@ -0,0 +1,29 @@
+%PDF-1.7
+%¡³Å×
+1 0 obj
+<</Pages 2 0 R /Type/Catalog>>
+endobj
+2 0 obj
+<</Count 1/Kids[ 4 0 R ]/Type/Pages>>
+endobj
+3 0 obj
+<</CreationDate(D:20241122110120)/Creator(PDFium)>>
+endobj
+4 0 obj
+<</Annots[<</C[ 1 0 0]/CA 1/Contents(Hello!)/Rect[ 100 50 150 75]/Subtype/FreeText/Type/Annot>>]/MediaBox[ 0 0 200 200]/Parent 2 0 R /Resources<<>>/Rotate 0/Type/Page>>
+endobj
+xref
+0 5
+0000000000 65535 f
+0000000017 00000 n
+0000000066 00000 n
+0000000122 00000 n
+0000000192 00000 n
+trailer
+<<
+/Root 1 0 R
+/Info 3 0 R
+/Size 5/ID[<05093BA87ACB734AE02FD2A979A55577><05093BA87ACB734AE02FD2A979A55577>]>>
+startxref
+379
+%%EOF