Add support in FPDFAnnot_SetAP() for transparent appearance streams

Annotation's appearance stream data only supports RGB and CMYK color
schemes and not RGBA. This makes it impossible to support transparency
through color values in the appearance stream. Transparency in
annotation is supported through modifying Graphics State (/GS) for the
entire object.

This CL adds support for specifying transparency in FPDFAnnot_SetAP()
for annotations by populating appropriate entries (e.g., /CA) in the
Graphics State dictionary.

This CL also includes test cases to validate creation of graphics state
dictionary in case alpha value specified in annotation's color is not
opaque and vice versa.

Bug: pdfium:1434
Change-Id: I2f14f6d14e49f1397e14bf6c57d56b7e49a6b473
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/63610
Commit-Queue: Shikha Walia <shwali@microsoft.com>
Reviewed-by: Lei Zhang <thestig@chromium.org>
diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp
index d77cee6..2ef525b 100644
--- a/fpdfsdk/fpdf_annot.cpp
+++ b/fpdfsdk/fpdf_annot.cpp
@@ -14,6 +14,7 @@
 #include "core/fpdfapi/page/cpdf_page.h"
 #include "core/fpdfapi/page/cpdf_pageobject.h"
 #include "core/fpdfapi/parser/cpdf_array.h"
+#include "core/fpdfapi/parser/cpdf_boolean.h"
 #include "core/fpdfapi/parser/cpdf_dictionary.h"
 #include "core/fpdfapi/parser/cpdf_document.h"
 #include "core/fpdfapi/parser/cpdf_name.h"
@@ -217,6 +218,44 @@
   return context ? context->GetAnnotDict() : nullptr;
 }
 
+RetainPtr<CPDF_Dictionary> SetExtGStateInResourceDict(
+    CPDF_Document* pDoc,
+    const CPDF_Dictionary* pAnnotDict,
+    const ByteString& sBlendMode) {
+  auto pGSDict =
+      pdfium::MakeRetain<CPDF_Dictionary>(pAnnotDict->GetByteStringPool());
+
+  // ExtGState represents a graphics state parameter dictionary.
+  pGSDict->SetNewFor<CPDF_Name>("Type", "ExtGState");
+
+  // CA respresents current stroking alpha specifying constant opacity
+  // value that should be used in transparent imaging model.
+  float fOpacity = pAnnotDict->GetNumberFor("CA");
+
+  pGSDict->SetNewFor<CPDF_Number>("CA", fOpacity);
+
+  // ca represents fill color alpha specifying constant opacity
+  // value that should be used in transparent imaging model.
+  pGSDict->SetNewFor<CPDF_Number>("ca", fOpacity);
+
+  // AIS represents alpha source flag specifying whether current alpha
+  // constant shall be interpreted as shape value (true) or opacity value
+  // (false).
+  pGSDict->SetNewFor<CPDF_Boolean>("AIS", false);
+
+  // BM represents Blend Mode
+  pGSDict->SetNewFor<CPDF_Name>("BM", sBlendMode);
+
+  auto pExtGStateDict =
+      pdfium::MakeRetain<CPDF_Dictionary>(pAnnotDict->GetByteStringPool());
+
+  pExtGStateDict->SetFor("GS", pGSDict);
+
+  auto pResourceDict = pDoc->New<CPDF_Dictionary>();
+  pResourceDict->SetFor("ExtGState", pExtGStateDict);
+  return pResourceDict;
+}
+
 }  // namespace
 
 FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
@@ -794,6 +833,15 @@
     pStreamDict->SetNewFor<CPDF_Name>(pdfium::annotation::kType, "XObject");
     pStreamDict->SetNewFor<CPDF_Name>(pdfium::annotation::kSubtype, "Form");
     pStreamDict->SetRectFor("BBox", rect);
+    // Transparency values are specified in range [0.0f, 1.0f]. We are strictly
+    // checking for value < 1 and not <= 1 so that the output PDF size does not
+    // unnecessarily bloat up by creating a new dictionary in case of solid
+    // color.
+    if (pAnnotDict->KeyExist("CA") && pAnnotDict->GetNumberFor("CA") < 1.0f) {
+      RetainPtr<CPDF_Dictionary> pResourceDict =
+          SetExtGStateInResourceDict(pDoc, pAnnotDict, "Normal");
+      pStreamDict->SetFor("Resources", pResourceDict);
+    }
 
     // Storing reference to indirect object in annotation's AP
     if (!pApDict)
diff --git a/fpdfsdk/fpdf_annot_unittest.cpp b/fpdfsdk/fpdf_annot_unittest.cpp
index 8acae3d..a5f619e 100644
--- a/fpdfsdk/fpdf_annot_unittest.cpp
+++ b/fpdfsdk/fpdf_annot_unittest.cpp
@@ -16,6 +16,17 @@
 #include "testing/fx_string_testhelpers.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+namespace {
+
+const wchar_t kStreamData[] =
+    L"/GS gs 0.0 0.0 0.0 RG 4 w 211.8 747.6 m 211.8 744.8 "
+    L"212.6 743.0 214.2 740.8 "
+    L"c 215.4 739.0 216.8 737.1 218.9 736.1 c 220.8 735.1 221.4 733.0 "
+    L"223.7 732.4 c 232.6 729.9 242.0 730.8 251.2 730.8 c 257.5 730.8 "
+    L"263.0 732.9 269.0 734.4 c S";
+
+}  // namespace
+
 class PDFAnnotTest : public testing::Test {
  protected:
   PDFAnnotTest() = default;
@@ -27,14 +38,11 @@
 
 TEST_F(PDFAnnotTest, SetAP) {
   ScopedFPDFDocument doc(FPDF_CreateNewDocument());
+  ASSERT_TRUE(doc);
   ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 100, 100));
-  const std::wstring kStreamData =
-      L"0.0 0.0 0.0 RG 4 w 211.8 747.6 m 211.8 744.8 "
-      L"212.6 743.0 214.2 740.8 "
-      L"c 215.4 739.0 216.8 737.1 218.9 736.1 c 220.8 735.1 221.4 733.0 "
-      L"223.7 732.4 c 232.6 729.9 242.0 730.8 251.2 730.8 c 257.5 730.8 "
-      L"263.0 732.9 269.0 734.4 c S";
+  ASSERT_TRUE(page);
   ScopedFPDFWideString ap_stream = GetFPDFWideString(kStreamData);
+  ASSERT_TRUE(ap_stream);
 
   ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_INK));
   ASSERT_TRUE(annot);
@@ -47,6 +55,9 @@
   const FS_RECTF bounding_rect{206.0f, 753.0f, 339.0f, 709.0f};
   EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &bounding_rect));
 
+  ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color,
+                                 /*R=*/255, /*G=*/0, /*B=*/0, /*A=*/255));
+
   EXPECT_TRUE(FPDFAnnot_SetAP(annot.get(), FPDF_ANNOT_APPEARANCEMODE_NORMAL,
                               ap_stream.get()));
 
@@ -59,6 +70,9 @@
   ASSERT_TRUE(ap_dict);
   CPDF_Dictionary* stream_dict = ap_dict->GetDictFor("N");
   ASSERT_TRUE(stream_dict);
+  // Check for non-existence of resources dictionary in case of opaque color
+  CPDF_Dictionary* resources_dict = stream_dict->GetDictFor("Resources");
+  ASSERT_FALSE(resources_dict);
   ByteString type = stream_dict->GetStringFor(pdfium::annotation::kType);
   EXPECT_EQ("XObject", type);
   ByteString sub_type = stream_dict->GetStringFor(pdfium::annotation::kSubtype);
@@ -66,7 +80,7 @@
 
   // Check that the appearance stream is same as we just set.
   const uint32_t kStreamDataSize =
-      (kStreamData.size() + 1) * sizeof(FPDF_WCHAR);
+      FX_ArraySize(kStreamData) * sizeof(FPDF_WCHAR);
   unsigned long normal_length_bytes = FPDFAnnot_GetAP(
       annot.get(), FPDF_ANNOT_APPEARANCEMODE_NORMAL, nullptr, 0);
   ASSERT_EQ(kStreamDataSize, normal_length_bytes);
@@ -76,3 +90,48 @@
                             buf.data(), normal_length_bytes));
   EXPECT_EQ(kStreamData, GetPlatformWString(buf.data()));
 }
+
+TEST_F(PDFAnnotTest, SetAPWithOpacity) {
+  ScopedFPDFDocument doc(FPDF_CreateNewDocument());
+  ASSERT_TRUE(doc);
+  ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 100, 100));
+  ASSERT_TRUE(page);
+  ScopedFPDFWideString ap_stream = GetFPDFWideString(kStreamData);
+  ASSERT_TRUE(ap_stream);
+
+  ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_INK));
+  ASSERT_TRUE(annot);
+
+  ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color,
+                                 /*R=*/255, /*G=*/0, /*B=*/0, /*A=*/102));
+
+  const FS_RECTF bounding_rect{206.0f, 753.0f, 339.0f, 709.0f};
+  EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &bounding_rect));
+
+  EXPECT_TRUE(FPDFAnnot_SetAP(annot.get(), FPDF_ANNOT_APPEARANCEMODE_NORMAL,
+                              ap_stream.get()));
+
+  CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot.get());
+  ASSERT_TRUE(context);
+  CPDF_Dictionary* annot_dict = context->GetAnnotDict();
+  ASSERT_TRUE(annot_dict);
+  CPDF_Dictionary* ap_dict = annot_dict->GetDictFor(pdfium::annotation::kAP);
+  ASSERT_TRUE(ap_dict);
+  CPDF_Dictionary* stream_dict = ap_dict->GetDictFor("N");
+  ASSERT_TRUE(stream_dict);
+  CPDF_Dictionary* resources_dict = stream_dict->GetDictFor("Resources");
+  ASSERT_TRUE(stream_dict);
+  CPDF_Dictionary* extGState_dict = resources_dict->GetDictFor("ExtGState");
+  ASSERT_TRUE(extGState_dict);
+  CPDF_Dictionary* gs_dict = extGState_dict->GetDictFor("GS");
+  ASSERT_TRUE(gs_dict);
+  ByteString type = gs_dict->GetStringFor(pdfium::annotation::kType);
+  EXPECT_EQ("ExtGState", type);
+  float opacity = gs_dict->GetNumberFor("CA");
+  // Opacity value of 102 is represented as 0.4f (=104/255) in /CA entry.
+  EXPECT_FLOAT_EQ(0.4f, opacity);
+  ByteString blend_mode = gs_dict->GetStringFor("BM");
+  EXPECT_EQ("Normal", blend_mode);
+  bool alpha_source_flag = gs_dict->GetBooleanFor("AIS", true);
+  EXPECT_FALSE(alpha_source_flag);
+}