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