Create FDPFPageObj_AddExistingMark()

CPDF_PageContentGenerator::ProcessContentMarks() already operates in
such a way that consecutive page objects with the same
CPDF_ContentMarkItem pointer get merged into a single BMC/EMC group.
So this new API allows embedders to insert marks that span multiple page
objects.

Bug: 409021827, 408926609
Change-Id: I139d279f0fd59ccd6ed05d0c1c79c560bfebb153
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/148370
Reviewed-by: Andy Phan <andyphan@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
diff --git a/core/fpdfapi/page/cpdf_contentmarks.cpp b/core/fpdfapi/page/cpdf_contentmarks.cpp
index fa81a18..77580f2 100644
--- a/core/fpdfapi/page/cpdf_contentmarks.cpp
+++ b/core/fpdfapi/page/cpdf_contentmarks.cpp
@@ -49,6 +49,11 @@
   mark_data_->AddMark(std::move(name));
 }
 
+void CPDF_ContentMarks::AddExistingMark(RetainPtr<CPDF_ContentMarkItem> mark) {
+  EnsureMarkDataExists();
+  mark_data_->AddExistingMark(std::move(mark));
+}
+
 void CPDF_ContentMarks::AddMarkWithDirectDict(ByteString name,
                                               RetainPtr<CPDF_Dictionary> dict) {
   EnsureMarkDataExists();
@@ -136,6 +141,11 @@
   marks_.push_back(pItem);
 }
 
+void CPDF_ContentMarks::MarkData::AddExistingMark(
+    RetainPtr<CPDF_ContentMarkItem> mark) {
+  marks_.push_back(std::move(mark));
+}
+
 void CPDF_ContentMarks::MarkData::AddMarkWithDirectDict(
     ByteString name,
     RetainPtr<CPDF_Dictionary> dict) {
diff --git a/core/fpdfapi/page/cpdf_contentmarks.h b/core/fpdfapi/page/cpdf_contentmarks.h
index 7a9ca10..10a7c23 100644
--- a/core/fpdfapi/page/cpdf_contentmarks.h
+++ b/core/fpdfapi/page/cpdf_contentmarks.h
@@ -32,6 +32,7 @@
   const CPDF_ContentMarkItem* GetItem(size_t index) const;
 
   void AddMark(ByteString name);
+  void AddExistingMark(RetainPtr<CPDF_ContentMarkItem> mark);
   void AddMarkWithDirectDict(ByteString name, RetainPtr<CPDF_Dictionary> dict);
   void AddMarkWithPropertiesHolder(const ByteString& name,
                                    RetainPtr<CPDF_Dictionary> dict,
@@ -51,6 +52,7 @@
 
     int GetMarkedContentID() const;
     void AddMark(ByteString name);
+    void AddExistingMark(RetainPtr<CPDF_ContentMarkItem> mark);
     void AddMarkWithDirectDict(ByteString name,
                                RetainPtr<CPDF_Dictionary> dict);
     void AddMarkWithPropertiesHolder(const ByteString& name,
diff --git a/fpdfsdk/fpdf_edit_embeddertest.cpp b/fpdfsdk/fpdf_edit_embeddertest.cpp
index 8bf352c..6e66cf7 100644
--- a/fpdfsdk/fpdf_edit_embeddertest.cpp
+++ b/fpdfsdk/fpdf_edit_embeddertest.cpp
@@ -4134,6 +4134,81 @@
   CheckMarkCounts(saved_page.get(), 0, 2, 0, 0, 0, 1);
 }
 
+TEST_F(FPDFEditEmbedderTest, AddExistingMarkBadInputs) {
+  EXPECT_FALSE(FPDFPageObj_AddExistingMark(nullptr, nullptr));
+
+  ASSERT_TRUE(OpenDocument("hello_world.pdf"));
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+  FPDF_PAGEOBJECT page_object1 = FPDFPage_GetObject(page.get(), 0);
+
+  // Null mark should error
+  EXPECT_FALSE(FPDFPageObj_AddExistingMark(page_object1, nullptr));
+
+  FPDF_PAGEOBJECTMARK mark = FPDFPageObj_AddMark(page_object1, "Prime");
+  EXPECT_TRUE(mark);
+  EXPECT_TRUE(FPDFPageObjMark_SetStringParam(document(), page_object1, mark,
+                                             "Test", "Hello"));
+
+  // Null object valid mark should error
+  EXPECT_FALSE(FPDFPageObj_AddExistingMark(nullptr, mark));
+}
+
+TEST_F(FPDFEditEmbedderTest, AddExistingMarkCompressedStream) {
+  // Load document with some text in a compressed stream.
+  ASSERT_TRUE(OpenDocument("hello_world_compressed_stream.pdf"));
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+
+  // Render and check there are no marks.
+  {
+    ScopedFPDFBitmap page_bitmap = RenderPage(page.get());
+    CompareBitmapWithExpectationSuffix(page_bitmap.get(), kHelloWorldPng);
+  }
+  CheckMarkCounts(page.get(), 0, 2, 0, 0, 0, 0);
+
+  // Add a mark to the first page object
+  FPDF_PAGEOBJECT page_object1 = FPDFPage_GetObject(page.get(), 0);
+  FPDF_PAGEOBJECTMARK mark = FPDFPageObj_AddMark(page_object1, "Prime");
+  EXPECT_TRUE(mark);
+  EXPECT_TRUE(FPDFPageObjMark_SetStringParam(document(), page_object1, mark,
+                                             "Test", "Hello"));
+
+  // Render and check there is 1 mark.
+  {
+    ScopedFPDFBitmap page_bitmap = RenderPage(page.get());
+    CompareBitmapWithExpectationSuffix(page_bitmap.get(), kHelloWorldPng);
+  }
+  CheckMarkCounts(page.get(), 0, 2, 1, 0, 0, 0);
+
+  // Add the same bounds mark to the second object.
+  FPDF_PAGEOBJECT page_object2 = FPDFPage_GetObject(page.get(), 1);
+  EXPECT_TRUE(FPDFPageObj_AddExistingMark(page_object2, mark));
+
+  // Render and check there are 2 marks.
+  {
+    ScopedFPDFBitmap page_bitmap = RenderPage(page.get());
+    CompareBitmapWithExpectationSuffix(page_bitmap.get(), kHelloWorldPng);
+  }
+  CheckMarkCounts(page.get(), 0, 2, 2, 0, 0, 0);
+
+  // Save the file.
+  EXPECT_TRUE(FPDFPage_GenerateContent(page.get()));
+  EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+
+  // Re-open the file and check the new mark is present.
+  ScopedSavedDoc saved_document = OpenScopedSavedDocument();
+  ASSERT_TRUE(saved_document);
+  ScopedSavedPage saved_page = LoadScopedSavedPage(0);
+  ASSERT_TRUE(saved_page);
+
+  {
+    ScopedFPDFBitmap page_bitmap = RenderPage(saved_page.get());
+    CompareBitmapWithExpectationSuffix(page_bitmap.get(), kHelloWorldPng);
+  }
+  CheckMarkCounts(saved_page.get(), 0, 2, 2, 0, 0, 0);
+}
+
 TEST_F(FPDFEditEmbedderTest, SetMarkParam) {
   // Load document with some text.
   ASSERT_TRUE(OpenDocument("text_in_page_marked.pdf"));
diff --git a/fpdfsdk/fpdf_editpage.cpp b/fpdfsdk/fpdf_editpage.cpp
index 0a0d115..7b5613e 100644
--- a/fpdfsdk/fpdf_editpage.cpp
+++ b/fpdfsdk/fpdf_editpage.cpp
@@ -36,6 +36,7 @@
 #include "core/fxcrt/fx_extension.h"
 #include "core/fxcrt/notreached.h"
 #include "core/fxcrt/numerics/safe_conversions.h"
+#include "core/fxcrt/retain_ptr.h"
 #include "core/fxcrt/span.h"
 #include "core/fxcrt/span_util.h"
 #include "core/fxcrt/stl_util.h"
@@ -395,6 +396,22 @@
   return FPDFPageObjectMarkFromCPDFContentMarkItem(pMarks->GetItem(index));
 }
 
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFPageObj_AddExistingMark(FPDF_PAGEOBJECT page_object,
+                            FPDF_PAGEOBJECTMARK mark) {
+  CPDF_PageObject* page = CPDFPageObjectFromFPDFPageObject(page_object);
+  CPDF_ContentMarkItem* mark_item =
+      CPDFContentMarkItemFromFPDFPageObjectMark(mark);
+  if (!page || !mark_item) {
+    return false;
+  }
+
+  CPDF_ContentMarks* marks = page->GetContentMarks();
+  marks->AddExistingMark(pdfium::WrapRetain(mark_item));
+  page->SetDirty(true);
+  return true;
+}
+
 FPDF_EXPORT FPDF_PAGEOBJECTMARK FPDF_CALLCONV
 FPDFPageObj_AddMark(FPDF_PAGEOBJECT page_object, FPDF_BYTESTRING name) {
   CPDF_PageObject* pPageObj = CPDFPageObjectFromFPDFPageObject(page_object);
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index 85b6219..0de97ff 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -209,6 +209,7 @@
     CHK(FPDFPageObjMark_SetFloatParam);
     CHK(FPDFPageObjMark_SetIntParam);
     CHK(FPDFPageObjMark_SetStringParam);
+    CHK(FPDFPageObj_AddExistingMark);
     CHK(FPDFPageObj_AddMark);
     CHK(FPDFPageObj_CountMarks);
     CHK(FPDFPageObj_CreateNewPath);
diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h
index eb89d01..8fac863 100644
--- a/public/fpdf_edit.h
+++ b/public/fpdf_edit.h
@@ -465,6 +465,22 @@
 FPDFPageObj_AddMark(FPDF_PAGEOBJECT page_object, FPDF_BYTESTRING name);
 
 // Experimental API.
+// Add an existing content mark to a |page_object|. If consecutive page objects
+// have the same |mark|, the generated PDF will contain a single mark that spans
+// all of them. If the page objects are not consecutive, multiple copies of the
+// |mark| are inserted in the PDF.
+//
+//   page_object - handle to a page object.
+//   mark        - handle to a mark object.
+//
+// Returns true on success, or false on failure. The handles are all owned by
+// the library. The |page_object| and |mark| params must be associated with the
+// same document.
+FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
+FPDFPageObj_AddExistingMark(FPDF_PAGEOBJECT page_object,
+                            FPDF_PAGEOBJECTMARK mark);
+
+// Experimental API.
 // Removes a content |mark| from a |page_object|.
 // The mark handle will be invalid after the removal.
 //