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/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(