Add public APIs to create and use Form XObjects.

Add FPDF_NewXObjectFromPage() and FPDF_CloseXObject() to make and
destroy FPDF_XOBJECT handles. FPDF_XOBJECT handles can be used with
FPDF_NewFormObjectFromXObject() to create new FPDF_PAGEOBJECT handles of
type FPDF_PAGEOBJ_FORM.

This is based a prototype by feinberg@google.com.

Bug: pdfium:977
Change-Id: I3dd27e28121defef483321750c9a43ccbb750bc7
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/82072
Commit-Queue: Lei Zhang <thestig@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
diff --git a/fpdfsdk/cpdfsdk_helpers.h b/fpdfsdk/cpdfsdk_helpers.h
index baaf411..a037b7a 100644
--- a/fpdfsdk/cpdfsdk_helpers.h
+++ b/fpdfsdk/cpdfsdk_helpers.h
@@ -40,6 +40,7 @@
 class CPDFSDK_FormFillEnvironment;
 class CPDFSDK_InteractiveForm;
 struct CPDF_JavaScript;
+struct XObjectContext;
 
 // Conversions to/from underlying types.
 IPDF_Page* IPDFPageFromFPDFPage(FPDF_PAGE page);
@@ -218,6 +219,14 @@
   return reinterpret_cast<CPDF_Dictionary*>(signature);
 }
 
+inline FPDF_XOBJECT FPDFXObjectFromXObjectContext(XObjectContext* xobject) {
+  return reinterpret_cast<FPDF_XOBJECT>(xobject);
+}
+
+inline XObjectContext* XObjectContextFromFPDFXObject(FPDF_XOBJECT xobject) {
+  return reinterpret_cast<XObjectContext*>(xobject);
+}
+
 CPDFSDK_InteractiveForm* FormHandleToInteractiveForm(FPDF_FORMHANDLE hHandle);
 
 ByteString ByteStringFromFPDFWideString(FPDF_WIDESTRING wide_string);
diff --git a/fpdfsdk/fpdf_ppo.cpp b/fpdfsdk/fpdf_ppo.cpp
index 614e5ab..c3ffb9d 100644
--- a/fpdfsdk/fpdf_ppo.cpp
+++ b/fpdfsdk/fpdf_ppo.cpp
@@ -15,6 +15,8 @@
 #include <vector>
 
 #include "constants/page_object.h"
+#include "core/fpdfapi/page/cpdf_form.h"
+#include "core/fpdfapi/page/cpdf_formobject.h"
 #include "core/fpdfapi/page/cpdf_page.h"
 #include "core/fpdfapi/page/cpdf_pageobject.h"
 #include "core/fpdfapi/parser/cpdf_array.h"
@@ -36,6 +38,11 @@
 #include "third_party/base/check.h"
 #include "third_party/base/span.h"
 
+struct XObjectContext {
+  CPDF_Document* dest_doc;
+  RetainPtr<CPDF_Stream> xobject;
+};
+
 namespace {
 
 // Struct that stores sub page origin and scale information.  When importing
@@ -485,6 +492,9 @@
                          size_t nPagesOnXAxis,
                          size_t nPagesOnYAxis);
 
+  std::unique_ptr<XObjectContext> CreateXObjectContextFromPage(
+      int src_page_index);
+
  private:
   // Map page object number to XObject object name.
   using PageXObjectMap = std::map<uint32_t, ByteString>;
@@ -499,6 +509,7 @@
   // Creates an XObject from |pSrcPageDict|. Updates mapping as needed.
   // Returns the name of the newly created XObject.
   ByteString MakeXObjectFromPage(const CPDF_Dictionary* pSrcPageDict);
+  CPDF_Stream* MakeXObjectFromPageRaw(const CPDF_Dictionary* pSrcPageDict);
 
   // Adds |bsContent| as the Contents key in |pDestPageDict|.
   // Adds the objects in |m_XObjectNameToNumberMap| to the XObject dictionary in
@@ -599,7 +610,7 @@
   return ByteString(contentStream);
 }
 
-ByteString CPDF_NPageToOneExporter::MakeXObjectFromPage(
+CPDF_Stream* CPDF_NPageToOneExporter::MakeXObjectFromPageRaw(
     const CPDF_Dictionary* pSrcPageDict) {
   DCHECK(pSrcPageDict);
 
@@ -644,6 +655,12 @@
     }
     pNewXObject->SetDataAndRemoveFilter(bsSrcContentStream.raw_span());
   }
+  return pNewXObject;
+}
+
+ByteString CPDF_NPageToOneExporter::MakeXObjectFromPage(
+    const CPDF_Dictionary* pSrcPageDict) {
+  CPDF_Stream* pNewXObject = MakeXObjectFromPageRaw(pSrcPageDict);
 
   // TODO(xlou): A better name schema to avoid possible object name collision.
   ByteString bsXObjectName = ByteString::Format("X%d", ++m_nObjectNumber);
@@ -652,6 +669,18 @@
   return bsXObjectName;
 }
 
+std::unique_ptr<XObjectContext>
+CPDF_NPageToOneExporter::CreateXObjectContextFromPage(int src_page_index) {
+  CPDF_Dictionary* src_page = src()->GetPageDictionary(src_page_index);
+  if (!src_page)
+    return nullptr;
+
+  auto xobject = std::make_unique<XObjectContext>();
+  xobject->dest_doc = dest();
+  xobject->xobject = MakeXObjectFromPageRaw(src_page);
+  return xobject;
+}
+
 void CPDF_NPageToOneExporter::FinishPage(CPDF_Dictionary* pDestPageDict,
                                          const ByteString& bsContent) {
   DCHECK(pDestPageDict);
@@ -773,6 +802,45 @@
   return output_doc.release();
 }
 
+FPDF_EXPORT FPDF_XOBJECT FPDF_CALLCONV
+FPDF_NewXObjectFromPage(FPDF_DOCUMENT dest_doc,
+                        FPDF_DOCUMENT src_doc,
+                        int src_page_index) {
+  CPDF_Document* dest = CPDFDocumentFromFPDFDocument(dest_doc);
+  if (!dest)
+    return nullptr;
+
+  CPDF_Document* src = CPDFDocumentFromFPDFDocument(src_doc);
+  if (!src)
+    return nullptr;
+
+  CPDF_NPageToOneExporter exporter(dest, src);
+  std::unique_ptr<XObjectContext> xobject =
+      exporter.CreateXObjectContextFromPage(src_page_index);
+  return FPDFXObjectFromXObjectContext(xobject.release());
+}
+
+FPDF_EXPORT void FPDF_CALLCONV FPDF_CloseXObject(FPDF_XOBJECT xobject) {
+  std::unique_ptr<XObjectContext> xobject_deleter(
+      XObjectContextFromFPDFXObject(xobject));
+}
+
+FPDF_EXPORT FPDF_PAGEOBJECT FPDF_CALLCONV
+FPDF_NewFormObjectFromXObject(FPDF_XOBJECT xobject) {
+  XObjectContext* xobj = XObjectContextFromFPDFXObject(xobject);
+  if (!xobj)
+    return nullptr;
+
+  // If used directly with std::make_unique(), linking fails.
+  // Build toolchain bug?
+  constexpr int kNoContentStream = CPDF_PageObject::kNoContentStream;
+  auto form = std::make_unique<CPDF_Form>(xobj->dest_doc, nullptr,
+                                          xobj->xobject.Get(), nullptr);
+  auto form_object = std::make_unique<CPDF_FormObject>(
+      kNoContentStream, std::move(form), CFX_Matrix());
+  return FPDFPageObjectFromCPDFPageObject(form_object.release());
+}
+
 FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV
 FPDF_CopyViewerPreferences(FPDF_DOCUMENT dest_doc, FPDF_DOCUMENT src_doc) {
   CPDF_Document* pDstDoc = CPDFDocumentFromFPDFDocument(dest_doc);
diff --git a/fpdfsdk/fpdf_ppo_embeddertest.cpp b/fpdfsdk/fpdf_ppo_embeddertest.cpp
index 85751c1..dc5130c 100644
--- a/fpdfsdk/fpdf_ppo_embeddertest.cpp
+++ b/fpdfsdk/fpdf_ppo_embeddertest.cpp
@@ -5,12 +5,16 @@
 #include <memory>
 #include <string>
 
+#include "core/fpdfapi/page/cpdf_form.h"
+#include "core/fpdfapi/page/cpdf_formobject.h"
+#include "fpdfsdk/cpdfsdk_helpers.h"
 #include "public/cpp/fpdf_scopers.h"
 #include "public/fpdf_edit.h"
 #include "public/fpdf_ppo.h"
 #include "public/fpdf_save.h"
 #include "public/fpdfview.h"
 #include "testing/embedder_test.h"
+#include "testing/embedder_test_constants.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/base/cxx17_backports.h"
 
@@ -143,6 +147,160 @@
   }
 }
 
+TEST_F(FPDFPPOEmbedderTest, ImportPageToXObject) {
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+  static const char kChecksum[] = "d6ebc0a8afc22fe0137f54ce54e1a19c";
+#else
+  static const char kChecksum[] = "2d88d180af7109eb346439f7c855bb29";
+#endif
+
+  ASSERT_TRUE(OpenDocument("rectangles.pdf"));
+
+  {
+    ScopedFPDFDocument output_doc(FPDF_CreateNewDocument());
+    ASSERT_TRUE(output_doc);
+
+    FPDF_XOBJECT xobject =
+        FPDF_NewXObjectFromPage(output_doc.get(), document(), 0);
+    ASSERT_TRUE(xobject);
+
+    for (int i = 0; i < 2; ++i) {
+      ScopedFPDFPage page(FPDFPage_New(output_doc.get(), 0, 612, 792));
+      ASSERT_TRUE(page);
+
+      FPDF_PAGEOBJECT page_object = FPDF_NewFormObjectFromXObject(xobject);
+      ASSERT_TRUE(page_object);
+      EXPECT_EQ(FPDF_PAGEOBJ_FORM, FPDFPageObj_GetType(page_object));
+      FPDFPage_InsertObject(page.get(), page_object);
+      EXPECT_TRUE(FPDFPage_GenerateContent(page.get()));
+
+      // TODO(thestig): This should have `kChecksum`.
+      ScopedFPDFBitmap page_bitmap = RenderPage(page.get());
+      CompareBitmap(page_bitmap.get(), 612, 792,
+                    pdfium::kBlankPage612By792Checksum);
+    }
+
+    EXPECT_TRUE(FPDF_SaveAsCopy(output_doc.get(), this, 0));
+
+    FPDF_CloseXObject(xobject);
+  }
+
+  constexpr int kExpectedPageCount = 2;
+  ASSERT_TRUE(OpenSavedDocument());
+
+  FPDF_PAGE saved_pages[kExpectedPageCount];
+  FPDF_PAGEOBJECT xobjects[kExpectedPageCount];
+  for (int i = 0; i < kExpectedPageCount; ++i) {
+    saved_pages[i] = LoadSavedPage(i);
+    ASSERT_TRUE(saved_pages[i]);
+
+    EXPECT_EQ(1, FPDFPage_CountObjects(saved_pages[i]));
+    xobjects[i] = FPDFPage_GetObject(saved_pages[i], 0);
+    ASSERT_TRUE(xobjects[i]);
+    ASSERT_EQ(FPDF_PAGEOBJ_FORM, FPDFPageObj_GetType(xobjects[i]));
+    EXPECT_EQ(8, FPDFFormObj_CountObjects(xobjects[i]));
+
+    {
+      ScopedFPDFBitmap page_bitmap = RenderPage(saved_pages[i]);
+      CompareBitmap(page_bitmap.get(), 612, 792, kChecksum);
+    }
+  }
+
+  // Peek at object internals to make sure the two XObjects use the same stream.
+  EXPECT_NE(xobjects[0], xobjects[1]);
+  CPDF_PageObject* obj1 = CPDFPageObjectFromFPDFPageObject(xobjects[0]);
+  ASSERT_TRUE(obj1->AsForm());
+  ASSERT_TRUE(obj1->AsForm()->form());
+  ASSERT_TRUE(obj1->AsForm()->form()->GetStream());
+  CPDF_PageObject* obj2 = CPDFPageObjectFromFPDFPageObject(xobjects[1]);
+  ASSERT_TRUE(obj2->AsForm());
+  ASSERT_TRUE(obj2->AsForm()->form());
+  ASSERT_TRUE(obj2->AsForm()->form()->GetStream());
+  EXPECT_EQ(obj1->AsForm()->form()->GetStream(),
+            obj2->AsForm()->form()->GetStream());
+
+  for (FPDF_PAGE saved_page : saved_pages)
+    CloseSavedPage(saved_page);
+
+  CloseSavedDocument();
+}
+
+TEST_F(FPDFPPOEmbedderTest, ImportPageToXObjectWithSameDoc) {
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+  static const char kChecksum[] = "8e7d672f49f9ca98fb9157824cefc204";
+#else
+  static const char kChecksum[] = "4d5ca14827b7707f8283e639b33c121a";
+#endif
+
+  ASSERT_TRUE(OpenDocument("rectangles.pdf"));
+
+  FPDF_XOBJECT xobject = FPDF_NewXObjectFromPage(document(), document(), 0);
+  ASSERT_TRUE(xobject);
+
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+
+  {
+    ScopedFPDFBitmap bitmap = RenderLoadedPage(page);
+    CompareBitmap(bitmap.get(), 200, 300, pdfium::kRectanglesChecksum);
+  }
+
+  FPDF_PAGEOBJECT page_object = FPDF_NewFormObjectFromXObject(xobject);
+  ASSERT_TRUE(page_object);
+  ASSERT_EQ(FPDF_PAGEOBJ_FORM, FPDFPageObj_GetType(page_object));
+
+  // Access the CPDF_FormObject underneath, as there is no public API to set
+  // the matrix for form objects. (yet)
+  static constexpr FS_MATRIX kMatrix = {0.5f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f};
+  CPDF_FormObject* pFormObj =
+      CPDFPageObjectFromFPDFPageObject(page_object)->AsForm();
+  pFormObj->Transform(CFXMatrixFromFSMatrix(kMatrix));
+  pFormObj->SetDirty(true);
+
+  FPDFPage_InsertObject(page, page_object);
+  EXPECT_TRUE(FPDFPage_GenerateContent(page));
+
+  {
+    // TODO(thestig): This should have `kChecksum`.
+    ScopedFPDFBitmap bitmap = RenderLoadedPage(page);
+    CompareBitmap(bitmap.get(), 200, 300, pdfium::kRectanglesChecksum);
+  }
+
+  FPDF_CloseXObject(xobject);
+
+  EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+  VerifySavedDocument(200, 300, kChecksum);
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFPPOEmbedderTest, XObjectNullParams) {
+  ASSERT_TRUE(OpenDocument("rectangles.pdf"));
+  ASSERT_EQ(1, FPDF_GetPageCount(document()));
+
+  EXPECT_FALSE(FPDF_NewXObjectFromPage(nullptr, nullptr, -1));
+  EXPECT_FALSE(FPDF_NewXObjectFromPage(nullptr, nullptr, 0));
+  EXPECT_FALSE(FPDF_NewXObjectFromPage(nullptr, nullptr, 1));
+  EXPECT_FALSE(FPDF_NewXObjectFromPage(document(), nullptr, -1));
+  EXPECT_FALSE(FPDF_NewXObjectFromPage(document(), nullptr, 0));
+  EXPECT_FALSE(FPDF_NewXObjectFromPage(document(), nullptr, 1));
+  EXPECT_FALSE(FPDF_NewXObjectFromPage(nullptr, document(), -1));
+  EXPECT_FALSE(FPDF_NewXObjectFromPage(nullptr, document(), 0));
+  EXPECT_FALSE(FPDF_NewXObjectFromPage(nullptr, document(), 1));
+
+  {
+    ScopedFPDFDocument output_doc(FPDF_CreateNewDocument());
+    ASSERT_TRUE(output_doc);
+    EXPECT_FALSE(FPDF_NewXObjectFromPage(output_doc.get(), document(), -1));
+    EXPECT_FALSE(FPDF_NewXObjectFromPage(output_doc.get(), document(), 1));
+  }
+
+  // Should be a no-op.
+  FPDF_CloseXObject(nullptr);
+
+  EXPECT_FALSE(FPDF_NewFormObjectFromXObject(nullptr));
+}
+
 TEST_F(FPDFPPOEmbedderTest, BUG_925981) {
   ASSERT_TRUE(OpenDocument("bug_925981.pdf"));
   ScopedFPDFDocument output_doc_2up(
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index 7297b61..3924088 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -306,10 +306,13 @@
     CHK(FPDFJavaScriptAction_GetScript);
 
     // fpdf_ppo.h
+    CHK(FPDF_CloseXObject);
     CHK(FPDF_CopyViewerPreferences);
     CHK(FPDF_ImportNPagesToOne);
     CHK(FPDF_ImportPages);
     CHK(FPDF_ImportPagesByIndex);
+    CHK(FPDF_NewFormObjectFromXObject);
+    CHK(FPDF_NewXObjectFromPage);
 
     // fpdf_progressive.h
     CHK(FPDF_RenderPageBitmapWithColorScheme_Start);
diff --git a/public/fpdf_ppo.h b/public/fpdf_ppo.h
index d27b788..6421837 100644
--- a/public/fpdf_ppo.h
+++ b/public/fpdf_ppo.h
@@ -75,6 +75,30 @@
                        size_t num_pages_on_x_axis,
                        size_t num_pages_on_y_axis);
 
+// Experimental API.
+// Create a template to generate form xobjects from |src_doc|'s page at
+// |src_page_index|, for use in |dest_doc|.
+//
+// Returns a handle on success, or NULL on failure. Caller owns the newly
+// created object.
+FPDF_EXPORT FPDF_XOBJECT FPDF_CALLCONV
+FPDF_NewXObjectFromPage(FPDF_DOCUMENT dest_doc,
+                        FPDF_DOCUMENT src_doc,
+                        int src_page_index);
+
+// Experimental API.
+// Close an FPDF_XOBJECT handle created by FPDF_NewXObjectFromPage().
+// FPDF_PAGEOBJECTs created from the FPDF_XOBJECT handle are not affected.
+FPDF_EXPORT void FPDF_CALLCONV FPDF_CloseXObject(FPDF_XOBJECT xobject);
+
+// Experimental API.
+// Create a new form object from an FPDF_XOBJECT object.
+//
+// Returns a new form object on success, or NULL on failure. Caller owns the
+// newly created object.
+FPDF_EXPORT FPDF_PAGEOBJECT FPDF_CALLCONV
+FPDF_NewFormObjectFromXObject(FPDF_XOBJECT xobject);
+
 // Copy the viewer preferences from |src_doc| into |dest_doc|.
 //
 //   dest_doc - Document to write the viewer preferences into.
diff --git a/public/fpdfview.h b/public/fpdfview.h
index e37f9f4..1cc7bd9 100644
--- a/public/fpdfview.h
+++ b/public/fpdfview.h
@@ -77,6 +77,7 @@
 typedef struct fpdf_structtree_t__* FPDF_STRUCTTREE;
 typedef struct fpdf_textpage_t__* FPDF_TEXTPAGE;
 typedef struct fpdf_widget_t__* FPDF_WIDGET;
+typedef struct fpdf_xobject_t__* FPDF_XOBJECT;
 
 // Basic data types
 typedef int FPDF_BOOL;