Add experimental FPDFAnnot_SetURI() API.

Allow the creation of link annots. FPDFAnnot_SetURI() can then set the
URI for a newly created link annot.

Change FPDFAnnot_IsSupportedSubtype() to return true for link annots.

Change-Id: I0298d26e6b99f8f3df97d88814e99fd419935b99
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/79233
Commit-Queue: Lei Zhang <thestig@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Reviewed-by: Daniel Hosseinian <dhoss@chromium.org>
diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp
index 338ea96..ddedcf1 100644
--- a/fpdfsdk/fpdf_annot.cpp
+++ b/fpdfsdk/fpdf_annot.cpp
@@ -316,10 +316,10 @@
   // The supported subtypes must also be communicated in the user doc.
   return subtype == FPDF_ANNOT_CIRCLE || subtype == FPDF_ANNOT_FREETEXT ||
          subtype == FPDF_ANNOT_HIGHLIGHT || subtype == FPDF_ANNOT_INK ||
-         subtype == FPDF_ANNOT_POPUP || subtype == FPDF_ANNOT_SQUARE ||
-         subtype == FPDF_ANNOT_SQUIGGLY || subtype == FPDF_ANNOT_STAMP ||
-         subtype == FPDF_ANNOT_STRIKEOUT || subtype == FPDF_ANNOT_TEXT ||
-         subtype == FPDF_ANNOT_UNDERLINE;
+         subtype == FPDF_ANNOT_LINK || subtype == FPDF_ANNOT_POPUP ||
+         subtype == FPDF_ANNOT_SQUARE || subtype == FPDF_ANNOT_SQUIGGLY ||
+         subtype == FPDF_ANNOT_STAMP || subtype == FPDF_ANNOT_STRIKEOUT ||
+         subtype == FPDF_ANNOT_TEXT || subtype == FPDF_ANNOT_UNDERLINE;
 }
 
 FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV
@@ -1380,3 +1380,16 @@
   return Utf16EncodeMaybeCopyAndReturnLength(pWidget->GetExportValue(), buffer,
                                              buflen);
 }
+
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFAnnot_SetURI(FPDF_ANNOTATION annot,
+                                                     const char* uri) {
+  if (!uri || FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_LINK)
+    return false;
+
+  CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot);
+  CPDF_Dictionary* action = annot_dict->SetNewFor<CPDF_Dictionary>("A");
+  action->SetNewFor<CPDF_Name>("Type", "Action");
+  action->SetNewFor<CPDF_Name>("S", "URI");
+  action->SetNewFor<CPDF_String>("URI", uri, /*bHex=*/false);
+  return true;
+}
diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp
index 03e6773..c5fef24 100644
--- a/fpdfsdk/fpdf_annot_embeddertest.cpp
+++ b/fpdfsdk/fpdf_annot_embeddertest.cpp
@@ -659,6 +659,84 @@
   UnloadPage(page);
 }
 
+TEST_F(FPDFAnnotEmbedderTest, AddAndSaveLinkAnnotation) {
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+  {
+    ScopedFPDFBitmap bitmap = RenderLoadedPageWithFlags(page, FPDF_ANNOT);
+    CompareBitmap(bitmap.get(), 200, 200, pdfium::kHelloWorldChecksum);
+  }
+  EXPECT_EQ(0, FPDFPage_GetAnnotCount(page));
+
+  constexpr char kUri[] = "https://pdfium.org/";
+
+  {
+    // Add a link annotation to the page and set its URI.
+    ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page, FPDF_ANNOT_LINK));
+    ASSERT_TRUE(annot);
+    EXPECT_EQ(1, FPDFPage_GetAnnotCount(page));
+    EXPECT_EQ(FPDF_ANNOT_LINK, FPDFAnnot_GetSubtype(annot.get()));
+    EXPECT_TRUE(FPDFAnnot_SetURI(annot.get(), kUri));
+    VerifyUriActionInLink(document(), FPDFAnnot_GetLink(annot.get()), kUri);
+
+    // Negative tests:
+    EXPECT_FALSE(FPDFAnnot_SetURI(nullptr, nullptr));
+    VerifyUriActionInLink(document(), FPDFAnnot_GetLink(annot.get()), kUri);
+    EXPECT_FALSE(FPDFAnnot_SetURI(annot.get(), nullptr));
+    VerifyUriActionInLink(document(), FPDFAnnot_GetLink(annot.get()), kUri);
+    EXPECT_FALSE(FPDFAnnot_SetURI(nullptr, kUri));
+    VerifyUriActionInLink(document(), FPDFAnnot_GetLink(annot.get()), kUri);
+
+    // Position the link on top of "Hello, world!" without a border.
+    const FS_RECTF kRect = {19.0f, 48.0f, 85.0f, 60.0f};
+    EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &kRect));
+    EXPECT_TRUE(FPDFAnnot_SetBorder(annot.get(), /*horizontal_radius=*/0.0f,
+                                    /*vertical_radius=*/0.0f,
+                                    /*border_width=*/0.0f));
+
+    VerifyUriActionInLink(document(), FPDFLink_GetLinkAtPoint(page, 40.0, 50.0),
+                          kUri);
+  }
+
+  {
+    // Add an ink annotation to the page. Trying to add a link to it fails.
+    ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page, FPDF_ANNOT_INK));
+    ASSERT_TRUE(annot);
+    EXPECT_EQ(2, FPDFPage_GetAnnotCount(page));
+    EXPECT_EQ(FPDF_ANNOT_INK, FPDFAnnot_GetSubtype(annot.get()));
+    EXPECT_FALSE(FPDFAnnot_SetURI(annot.get(), kUri));
+  }
+
+  // Remove the ink annotation added above for negative testing.
+  EXPECT_TRUE(FPDFPage_RemoveAnnot(page, 1));
+  EXPECT_EQ(1, FPDFPage_GetAnnotCount(page));
+
+  // Save the document, closing the page.
+  EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+  UnloadPage(page);
+
+  // Reopen the document and make sure it still renders the same. Since the link
+  // does not have a border, it does not affect the rendering.
+  ASSERT_TRUE(OpenSavedDocument());
+  page = LoadSavedPage(0);
+  ASSERT_TRUE(page);
+  VerifySavedRendering(page, 200, 200, pdfium::kHelloWorldChecksum);
+  EXPECT_EQ(1, FPDFPage_GetAnnotCount(page));
+
+  {
+    ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page, 0));
+    ASSERT_TRUE(annot);
+    EXPECT_EQ(FPDF_ANNOT_LINK, FPDFAnnot_GetSubtype(annot.get()));
+    VerifyUriActionInLink(document(), FPDFAnnot_GetLink(annot.get()), kUri);
+    VerifyUriActionInLink(document(), FPDFLink_GetLinkAtPoint(page, 40.0, 50.0),
+                          kUri);
+  }
+
+  CloseSavedPage(page);
+  CloseSavedDocument();
+}
+
 TEST_F(FPDFAnnotEmbedderTest, AddAndSaveUnderlineAnnotation) {
   // Open a file with one annotation and load its first page.
   ASSERT_TRUE(OpenDocument("annotation_highlight_long_content.pdf"));
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index bb92322..2a68e11 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -91,6 +91,7 @@
     CHK(FPDFAnnot_SetFocusableSubtypes);
     CHK(FPDFAnnot_SetRect);
     CHK(FPDFAnnot_SetStringValue);
+    CHK(FPDFAnnot_SetURI);
     CHK(FPDFAnnot_UpdateObject);
     CHK(FPDFPage_CloseAnnot);
     CHK(FPDFPage_CreateAnnot);
diff --git a/public/fpdf_annot.h b/public/fpdf_annot.h
index 06b1de3..e4729ce 100644
--- a/public/fpdf_annot.h
+++ b/public/fpdf_annot.h
@@ -91,7 +91,7 @@
 
 // Experimental API.
 // Check if an annotation subtype is currently supported for creation.
-// Currently supported subtypes: circle, highlight, ink, popup, square,
+// Currently supported subtypes: circle, highlight, ink, link, popup, square,
 // squiggly, stamp, strikeout, text, and underline.
 //
 //   subtype   - the subtype to be checked.
@@ -891,6 +891,16 @@
                                   FPDF_WCHAR* buffer,
                                   unsigned long buflen);
 
+// Experimental API.
+// Add a URI action to |annot|, overwriting the existing action, if any.
+//
+//   annot  - handle to a link annotation.
+//   uri    - the URI to be set, encoded in 7-bit ASCII.
+//
+// Returns true if successful.
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFAnnot_SetURI(FPDF_ANNOTATION annot,
+                                                     const char* uri);
+
 #ifdef __cplusplus
 }  // extern "C"
 #endif  // __cplusplus