Add FPDFAnnot_SetAP to public API.

Change-Id: I6de3e4e158a8b0276775c0915cbe53417135eec3
Reviewed-on: https://pdfium-review.googlesource.com/22570
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Henrique Nakashima <hnakashima@chromium.org>
diff --git a/fpdfsdk/fpdfannot.cpp b/fpdfsdk/fpdfannot.cpp
index e40e219..9b0cb39 100644
--- a/fpdfsdk/fpdfannot.cpp
+++ b/fpdfsdk/fpdfannot.cpp
@@ -747,6 +747,51 @@
                                              buffer, buflen);
 }
 
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFAnnot_SetAP(FPDF_ANNOTATION annot,
+                FPDF_ANNOT_APPEARANCEMODE appearanceMode,
+                FPDF_WIDESTRING value) {
+  if (appearanceMode < 0 || appearanceMode >= FPDF_ANNOT_APPEARANCEMODE_COUNT)
+    return false;
+
+  if (!annot)
+    return false;
+
+  CPDF_Dictionary* pAnnotDict =
+      CPDFAnnotContextFromFPDFAnnotation(annot)->GetAnnotDict();
+  if (!pAnnotDict)
+    return false;
+
+  constexpr const char* modeKeyForMode[] = {"N", "R", "D"};
+  static_assert(FX_ArraySize(modeKeyForMode) == FPDF_ANNOT_APPEARANCEMODE_COUNT,
+                "length of modeKeyForMode should be equal to "
+                "FPDF_ANNOT_APPEARANCEMODE_COUNT");
+  const char* modeKey = modeKeyForMode[appearanceMode];
+
+  CPDF_Dictionary* pApDict = pAnnotDict->GetDictFor("AP");
+
+  // If value is null, we're in remove mode. Otherwise, we're in add/update
+  // mode.
+  if (value) {
+    if (!pApDict)
+      pApDict = pAnnotDict->SetNewFor<CPDF_Dictionary>("AP");
+
+    ByteString newValue = CFXByteStringFromFPDFWideString(value);
+    auto pNewApStream = pdfium::MakeUnique<CPDF_Stream>();
+    pNewApStream->SetData(newValue.raw_str(), newValue.GetLength());
+    pApDict->SetFor(modeKey, std::move(pNewApStream));
+  } else {
+    if (pApDict) {
+      if (appearanceMode == FPDF_ANNOT_APPEARANCEMODE_NORMAL)
+        pAnnotDict->RemoveFor("AP");
+      else
+        pApDict->RemoveFor(modeKey);
+    }
+  }
+
+  return true;
+}
+
 FPDF_EXPORT unsigned long FPDF_CALLCONV
 FPDFAnnot_GetAP(FPDF_ANNOTATION annot,
                 FPDF_ANNOT_APPEARANCEMODE appearanceMode,
diff --git a/fpdfsdk/fpdfannot_embeddertest.cpp b/fpdfsdk/fpdfannot_embeddertest.cpp
index f07070a..b38bd87 100644
--- a/fpdfsdk/fpdfannot_embeddertest.cpp
+++ b/fpdfsdk/fpdfannot_embeddertest.cpp
@@ -889,7 +889,7 @@
   CloseSavedDocument();
 }
 
-TEST_F(FPDFAnnotEmbeddertest, GetAP) {
+TEST_F(FPDFAnnotEmbeddertest, GetSetAP) {
   // Open a file with four annotations and load its first page.
   ASSERT_TRUE(OpenDocument("annotation_stamp_with_ap.pdf"));
   FPDF_PAGE page = FPDF_LoadPage(document(), 0);
@@ -900,26 +900,26 @@
   ASSERT_TRUE(annot);
 
   // Check that the string value of an AP returns the expected length.
-  unsigned long len =
+  unsigned long normal_len =
       FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL, nullptr, 0);
-  EXPECT_EQ(73970u, len);
+  EXPECT_EQ(73970u, normal_len);
 
   // Check that the string value of an AP is not returned if the buffer is too
   // small. The result buffer should be overwritten with an empty string.
-  std::vector<char> buf(len - 1);
+  std::vector<char> buf(normal_len - 1);
   // Write L"z" in the buffer to verify it's not overwritten.
   wcscpy(reinterpret_cast<wchar_t*>(buf.data()), L"z");
   EXPECT_EQ(73970u, FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL,
-                                    buf.data(), len - 1));
+                                    buf.data(), buf.size()));
   std::string ap = BufferToString(buf);
   EXPECT_STREQ("z", ap.c_str());
 
   // Check that the string value of an AP is returned through a buffer that is
   // the right size.
   buf.clear();
-  buf.resize(len);
+  buf.resize(normal_len);
   EXPECT_EQ(73970u, FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL,
-                                    buf.data(), len));
+                                    buf.data(), buf.size()));
   ap = BufferToString(buf);
   EXPECT_THAT(ap, testing::StartsWith("q Q q 7.442786 w 2 J"));
   EXPECT_THAT(ap, testing::EndsWith("c 716.5381 327.7156 l S Q Q"));
@@ -927,21 +927,142 @@
   // Check that the string value of an AP is returned through a buffer that is
   // larger than necessary.
   buf.clear();
-  buf.resize(len + 1);
+  buf.resize(normal_len + 1);
   EXPECT_EQ(73970u, FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL,
-                                    buf.data(), len + 1));
+                                    buf.data(), buf.size()));
   ap = BufferToString(buf);
   EXPECT_THAT(ap, testing::StartsWith("q Q q 7.442786 w 2 J"));
   EXPECT_THAT(ap, testing::EndsWith("c 716.5381 327.7156 l S Q Q"));
 
   // Check that getting an AP for a mode that does not have an AP returns an
   // empty string.
+  unsigned long rollover_len =
+      FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_ROLLOVER, nullptr, 0);
+  EXPECT_EQ(2u, rollover_len);
+
   buf.clear();
-  buf.resize(len);
+  buf.resize(1000);
   EXPECT_EQ(2u, FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_ROLLOVER,
-                                buf.data(), len));
+                                buf.data(), buf.size()));
+  EXPECT_STREQ("", BufferToString(buf).c_str());
+
+  // Check that setting the AP for an invalid appearance mode fails.
+  std::unique_ptr<unsigned short, pdfium::FreeDeleter> apText =
+      GetFPDFWideString(L"new test ap");
+  EXPECT_FALSE(FPDFAnnot_SetAP(annot, -1, apText.get()));
+  EXPECT_FALSE(
+      FPDFAnnot_SetAP(annot, FPDF_ANNOT_APPEARANCEMODE_COUNT, apText.get()));
+  EXPECT_FALSE(FPDFAnnot_SetAP(annot, FPDF_ANNOT_APPEARANCEMODE_COUNT + 1,
+                               apText.get()));
+
+  // Set the AP correctly now.
+  EXPECT_TRUE(
+      FPDFAnnot_SetAP(annot, FPDF_ANNOT_APPEARANCEMODE_ROLLOVER, apText.get()));
+
+  // Check that the new annotation value is equal to the value we just set.
+  rollover_len =
+      FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_ROLLOVER, nullptr, 0);
+  EXPECT_EQ(24u, rollover_len);
+  buf.clear();
+  buf.resize(rollover_len);
+  EXPECT_EQ(24u, FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_ROLLOVER,
+                                 buf.data(), buf.size()));
+  EXPECT_STREQ(L"new test ap", BufferToWString(buf).c_str());
+
+  // Check that the Normal AP was not touched when the Rollover AP was set.
+  buf.clear();
+  buf.resize(normal_len);
+  EXPECT_EQ(73970u, FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL,
+                                    buf.data(), buf.size()));
   ap = BufferToString(buf);
-  EXPECT_STREQ("", ap.c_str());
+  EXPECT_THAT(ap, testing::StartsWith("q Q q 7.442786 w 2 J"));
+  EXPECT_THAT(ap, testing::EndsWith("c 716.5381 327.7156 l S Q Q"));
+
+  // Save the modified document, then reopen it.
+  FPDFPage_CloseAnnot(annot);
+  EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+  FPDF_ClosePage(page);
+
+  OpenSavedDocument();
+  page = LoadSavedPage(0);
+  FPDF_ANNOTATION new_annot = FPDFPage_GetAnnot(page, 0);
+
+  // Check that the new annotation value is equal to the value we set before
+  // saving.
+  rollover_len = FPDFAnnot_GetAP(new_annot, FPDF_ANNOT_APPEARANCEMODE_ROLLOVER,
+                                 nullptr, 0);
+  EXPECT_EQ(24u, rollover_len);
+  buf.clear();
+  buf.resize(rollover_len);
+  EXPECT_EQ(24u, FPDFAnnot_GetAP(new_annot, FPDF_ANNOT_APPEARANCEMODE_ROLLOVER,
+                                 buf.data(), buf.size()));
+  EXPECT_STREQ(L"new test ap", BufferToWString(buf).c_str());
+
+  // Close saved document.
+  FPDFPage_CloseAnnot(new_annot);
+  CloseSavedPage(page);
+  CloseSavedDocument();
+}
+
+TEST_F(FPDFAnnotEmbeddertest, RemoveOptionalAP) {
+  // Open a file with four annotations and load its first page.
+  ASSERT_TRUE(OpenDocument("annotation_stamp_with_ap.pdf"));
+  FPDF_PAGE page = FPDF_LoadPage(document(), 0);
+  ASSERT_TRUE(page);
+
+  // Retrieve the first annotation.
+  FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page, 0);
+  ASSERT_TRUE(annot);
+
+  // Set Down AP. Normal AP is already set.
+  std::unique_ptr<unsigned short, pdfium::FreeDeleter> apText =
+      GetFPDFWideString(L"new test ap");
+  EXPECT_TRUE(
+      FPDFAnnot_SetAP(annot, FPDF_ANNOT_APPEARANCEMODE_DOWN, apText.get()));
+  EXPECT_EQ(73970u, FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL,
+                                    nullptr, 0));
+  EXPECT_EQ(24u,
+            FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_DOWN, nullptr, 0));
+
+  // Check that setting the Down AP to null removes the Down entry but keeps
+  // Normal intact.
+  EXPECT_TRUE(FPDFAnnot_SetAP(annot, FPDF_ANNOT_APPEARANCEMODE_DOWN, nullptr));
+  EXPECT_EQ(73970u, FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL,
+                                    nullptr, 0));
+  EXPECT_EQ(2u,
+            FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_DOWN, nullptr, 0));
+
+  FPDFPage_CloseAnnot(annot);
+  FPDF_ClosePage(page);
+}
+
+TEST_F(FPDFAnnotEmbeddertest, RemoveRequiredAP) {
+  // Open a file with four annotations and load its first page.
+  ASSERT_TRUE(OpenDocument("annotation_stamp_with_ap.pdf"));
+  FPDF_PAGE page = FPDF_LoadPage(document(), 0);
+  ASSERT_TRUE(page);
+
+  // Retrieve the first annotation.
+  FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page, 0);
+  ASSERT_TRUE(annot);
+
+  // Set Down AP. Normal AP is already set.
+  std::unique_ptr<unsigned short, pdfium::FreeDeleter> apText =
+      GetFPDFWideString(L"new test ap");
+  EXPECT_TRUE(
+      FPDFAnnot_SetAP(annot, FPDF_ANNOT_APPEARANCEMODE_DOWN, apText.get()));
+  EXPECT_EQ(73970u, FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL,
+                                    nullptr, 0));
+  EXPECT_EQ(24u,
+            FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_DOWN, nullptr, 0));
+
+  // Check that setting the Normal AP to null removes the whole AP dictionary.
+  EXPECT_TRUE(
+      FPDFAnnot_SetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL, nullptr));
+  EXPECT_EQ(
+      2u, FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL, nullptr, 0));
+  EXPECT_EQ(2u,
+            FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_DOWN, nullptr, 0));
 
   FPDFPage_CloseAnnot(annot);
   FPDF_ClosePage(page);
diff --git a/fpdfsdk/fpdfview_c_api_test.c b/fpdfsdk/fpdfview_c_api_test.c
index 08696c2..ad1d8a7 100644
--- a/fpdfsdk/fpdfview_c_api_test.c
+++ b/fpdfsdk/fpdfview_c_api_test.c
@@ -62,6 +62,7 @@
     CHK(FPDFAnnot_GetValueType);
     CHK(FPDFAnnot_SetStringValue);
     CHK(FPDFAnnot_GetStringValue);
+    CHK(FPDFAnnot_SetAP);
     CHK(FPDFAnnot_GetAP);
     CHK(FPDFAnnot_GetLinkedAnnot);
     CHK(FPDFAnnot_GetFlags);
diff --git a/public/fpdf_annot.h b/public/fpdf_annot.h
index 8841963..d5ef54c 100644
--- a/public/fpdf_annot.h
+++ b/public/fpdf_annot.h
@@ -401,6 +401,23 @@
                          unsigned long buflen);
 
 // Experimental API.
+// Set the AP (appearance string) in |annot|'s dictionary for a given
+// |appearanceMode|.
+//
+//   annot          - handle to an annotation.
+//   appearanceMode - the appearance mode (normal, rollover or down) for which
+//                    to get the AP.
+//   value          - the string value to be set, encoded in UTF16-LE. If
+//                    nullptr is passed, the AP is cleared for that mode. If the
+//                    mode is Normal, APs for all modes are cleared.
+//
+// Returns true if successful.
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFAnnot_SetAP(FPDF_ANNOTATION annot,
+                FPDF_ANNOT_APPEARANCEMODE appearanceMode,
+                FPDF_WIDESTRING value);
+
+// Experimental API.
 // Get the AP (appearance string) from |annot|'s dictionary for a given
 // |appearanceMode|.
 // |buffer| is only modified if |buflen| is large enough to hold the whole AP