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;