diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index e9a433962..45b7eb74f 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -24,6 +24,7 @@ source_set("fpdfsdk") { "epdf_capture.cpp", # EmbedPDF: the catalog's metadata stream (public/epdf_catalog.h). "epdf_catalog.cpp", + "epdf_pageobject.cpp", # EmbedPDF: implements EPDFFont_* runtime font registration APIs. "epdf_font.cpp", # EmbedPDF: session-free AcroForm model snapshot (EPDFForm_* APIs). @@ -257,6 +258,7 @@ pdfium_embeddertest_source_set("embeddertests") { "epdf_capture_embeddertest.cpp", # EmbedPDF: covers the catalog's metadata stream (EPDFCatalog_*Metadata). "epdf_catalog_embeddertest.cpp", + "epdf_pageobject_embeddertest.cpp", # EmbedPDF: covers appearance states and modes, widgets in page renders, # and the check box and radio button symbols. "epdf_appearance_state_embeddertest.cpp", diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 249c8d018..a8ea4ca13 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -267,8 +267,14 @@ int CheckEmbedPDFCApi(void) { CHK(EPDF_SetMetaTrapped); // epdf_edit.h + CHK(EPDFFormObj_GetTransparencyGroup); CHK(EPDFImageObj_SetJpeg); CHK(EPDFImageObj_SetPng); + CHK(EPDFPageObj_GetBlendMode); + CHK(EPDFPageObj_GetFillAlpha); + CHK(EPDFPageObj_GetMiterLimit); + CHK(EPDFPageObj_GetStrokeAlpha); + CHK(EPDFPageObj_HasSoftMask); CHK(EPDFPage_InsertBlankRaw); CHK(EPDFText_RedactInQuads); CHK(EPDFText_RedactInRect); diff --git a/fpdfsdk/epdf_pageobject.cpp b/fpdfsdk/epdf_pageobject.cpp new file mode 100644 index 000000000..2c00fb8b3 --- /dev/null +++ b/fpdfsdk/epdf_pageobject.cpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "public/epdf_edit.h" + +#include "core/fpdfapi/page/cpdf_form.h" +#include "core/fpdfapi/page/cpdf_formobject.h" +#include "core/fpdfapi/page/cpdf_pageobject.h" +#include "core/fxcrt/compiler_specific.h" +#include "core/fxcrt/span.h" +#include "fpdfsdk/cpdfsdk_helpers.h" + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPageObj_GetFillAlpha(FPDF_PAGEOBJECT page_object, float* alpha) { + const auto* object = CPDFPageObjectFromFPDFPageObject(page_object); + if (!object || !alpha) { + return false; + } + *alpha = object->general_state().GetFillAlpha(); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPageObj_GetStrokeAlpha(FPDF_PAGEOBJECT page_object, float* alpha) { + const auto* object = CPDFPageObjectFromFPDFPageObject(page_object); + if (!object || !alpha) { + return false; + } + *alpha = object->general_state().GetStrokeAlpha(); + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFPageObj_GetBlendMode(FPDF_PAGEOBJECT page_object, + char* buffer, + unsigned long buflen) { + const auto* object = CPDFPageObjectFromFPDFPageObject(page_object); + if (!object) { + return 0; + } + if (!buffer) { + buflen = 0; + } + // SAFETY: the caller provides a writable buffer of buflen bytes. + return NulTerminateMaybeCopyAndReturnLength( + object->general_state().GetBlendMode(), + UNSAFE_BUFFERS(pdfium::span(buffer, static_cast(buflen)))); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPageObj_HasSoftMask(FPDF_PAGEOBJECT page_object) { + const auto* object = CPDFPageObjectFromFPDFPageObject(page_object); + return object && object->general_state().GetSoftMask(); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFormObj_GetTransparencyGroup(FPDF_PAGEOBJECT form_object, + FPDF_BOOL* is_group, + FPDF_BOOL* is_isolated) { + const auto* object = CPDFPageObjectFromFPDFPageObject(form_object); + if (!object || !object->IsForm() || !is_group || !is_isolated) { + return false; + } + const auto& transparency = object->AsForm()->form()->GetTransparency(); + *is_group = transparency.IsGroup(); + *is_isolated = transparency.IsGroup() && transparency.IsIsolated(); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPageObj_GetMiterLimit(FPDF_PAGEOBJECT page_object, float* miter_limit) { + const auto* object = CPDFPageObjectFromFPDFPageObject(page_object); + if (!object || !miter_limit) { + return false; + } + *miter_limit = object->graph_state().GetMiterLimit(); + return true; +} diff --git a/fpdfsdk/epdf_pageobject_embeddertest.cpp b/fpdfsdk/epdf_pageobject_embeddertest.cpp new file mode 100644 index 000000000..e7ac582e7 --- /dev/null +++ b/fpdfsdk/epdf_pageobject_embeddertest.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "public/epdf_edit.h" + +#include +#include + +#include "public/cpp/fpdf_scopers.h" +#include "testing/embedder_test.h" + +class EPDFPageObjectEmbedderTest : public EmbedderTest {}; + +TEST_F(EPDFPageObjectEmbedderTest, DefaultsAndInvalidArguments) { + ScopedFPDFPageObject path(FPDFPageObj_CreateNewRect(0, 0, 10, 10)); + ASSERT_TRUE(path); + float value = -1; + EXPECT_TRUE(EPDFPageObj_GetFillAlpha(path.get(), &value)); + EXPECT_FLOAT_EQ(1, value); + EXPECT_TRUE(EPDFPageObj_GetStrokeAlpha(path.get(), &value)); + EXPECT_FLOAT_EQ(1, value); + EXPECT_TRUE(EPDFPageObj_GetMiterLimit(path.get(), &value)); + EXPECT_FLOAT_EQ(10, value); + EXPECT_FALSE(EPDFPageObj_HasSoftMask(path.get())); + char mode[7] = {}; + EXPECT_EQ(7u, EPDFPageObj_GetBlendMode(path.get(), mode, sizeof(mode))); + EXPECT_STREQ("Normal", mode); + + value = -1; + EXPECT_FALSE(EPDFPageObj_GetFillAlpha(nullptr, &value)); + EXPECT_FALSE(EPDFPageObj_GetStrokeAlpha(nullptr, &value)); + EXPECT_FALSE(EPDFPageObj_GetMiterLimit(nullptr, &value)); + EXPECT_FLOAT_EQ(-1, value); + EXPECT_FALSE(EPDFPageObj_GetFillAlpha(path.get(), nullptr)); + EXPECT_FALSE(EPDFPageObj_GetStrokeAlpha(path.get(), nullptr)); + EXPECT_FALSE(EPDFPageObj_GetMiterLimit(path.get(), nullptr)); + EXPECT_FALSE(EPDFPageObj_HasSoftMask(nullptr)); + EXPECT_EQ(0u, EPDFPageObj_GetBlendMode(nullptr, mode, sizeof(mode))); + EXPECT_STREQ("Normal", mode); + FPDF_BOOL group = -1; + FPDF_BOOL isolated = -1; + EXPECT_FALSE(EPDFFormObj_GetTransparencyGroup(nullptr, &group, &isolated)); + EXPECT_FALSE(EPDFFormObj_GetTransparencyGroup(path.get(), &group, &isolated)); + EXPECT_EQ(-1, group); + EXPECT_EQ(-1, isolated); +} + +TEST_F(EPDFPageObjectEmbedderTest, ReadsGraphicsStateWithoutChangingRendering) { + ASSERT_TRUE(OpenDocument("epdf_pageobject_state.pdf")); + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(7, FPDFPage_CountObjects(page)); + const auto before = HashBitmap(RenderPage(page).get()); + + // Images and shadings carry alpha without a color state. + const std::array types = {FPDF_PAGEOBJ_PATH, FPDF_PAGEOBJ_IMAGE, + FPDF_PAGEOBJ_SHADING}; + for (int i = 0; i < 3; ++i) { + FPDF_PAGEOBJECT object = FPDFPage_GetObject(page, i); + EXPECT_EQ(types[i], FPDFPageObj_GetType(object)); + float alpha = -1; + EXPECT_TRUE(EPDFPageObj_GetFillAlpha(object, &alpha)); + EXPECT_FLOAT_EQ(0.25f, alpha); + EXPECT_TRUE(EPDFPageObj_GetStrokeAlpha(object, &alpha)); + EXPECT_FLOAT_EQ(0.75f, alpha); + char mode[9] = {}; + EXPECT_EQ(9u, EPDFPageObj_GetBlendMode(object, nullptr, 0)); + EXPECT_EQ(9u, EPDFPageObj_GetBlendMode(object, nullptr, sizeof(mode))); + EXPECT_EQ(9u, EPDFPageObj_GetBlendMode(object, mode, sizeof(mode))); + EXPECT_STREQ("Multiply", mode); + char small[4] = {'k', 'e', 'e', 'p'}; + EXPECT_EQ(9u, EPDFPageObj_GetBlendMode(object, small, sizeof(small))); + EXPECT_EQ(0, std::memcmp(small, "keep", sizeof(small))); + EXPECT_FALSE(EPDFPageObj_HasSoftMask(object)); + if (i != 0) { + unsigned int r = 0, g = 0, b = 0, a = 0; + EXPECT_FALSE(FPDFPageObj_GetFillColor(object, &r, &g, &b, &a)); + } + } + float miter = 0; + EXPECT_TRUE(EPDFPageObj_GetMiterLimit(FPDFPage_GetObject(page, 0), &miter)); + EXPECT_FLOAT_EQ(7, miter); + EXPECT_EQ(FPDF_LINEJOIN_ROUND, + FPDFPageObj_GetLineJoin(FPDFPage_GetObject(page, 0))); + + for (int i = 3; i < 6; ++i) { + FPDF_PAGEOBJECT object = FPDFPage_GetObject(page, i); + FPDF_BOOL group = -1; + FPDF_BOOL isolated = -1; + EXPECT_TRUE(EPDFFormObj_GetTransparencyGroup(object, &group, &isolated)); + EXPECT_EQ(i != 3, group); + EXPECT_EQ(i == 5, isolated); + group = isolated = -1; + EXPECT_FALSE(EPDFFormObj_GetTransparencyGroup(object, nullptr, &isolated)); + EXPECT_FALSE(EPDFFormObj_GetTransparencyGroup(object, &group, nullptr)); + EXPECT_EQ(-1, group); + EXPECT_EQ(-1, isolated); + } + EXPECT_TRUE(EPDFPageObj_HasSoftMask(FPDFPage_GetObject(page, 6))); + EXPECT_EQ(before, HashBitmap(RenderPage(page).get())); + UnloadPage(page); +} diff --git a/public/epdf_edit.h b/public/epdf_edit.h index 25ede6598..9a1fda4d0 100644 --- a/public/epdf_edit.h +++ b/public/epdf_edit.h @@ -12,7 +12,77 @@ extern "C" { #endif // __cplusplus // EmbedPDF's additions to public/fpdf_edit.h: blank pages without loading the -// page, PNG and JPEG image objects, and text redaction. +// page, PNG and JPEG image objects, text redaction, and page-object state. + +// Experimental EmbedPDF Extension API. +// Read the object's non-stroking alpha, including for images and shadings +// that have no color state. This is the object's graphics-state value, not +// a composited opacity after soft masks or enclosing transparency groups. +// +// page_object - handle to a page object. +// alpha - receives the alpha (0 = transparent, 1 = opaque). +// +// Returns TRUE on success. Returns FALSE for a NULL handle or output pointer, +// leaving the output unchanged. Does not modify the object. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPageObj_GetFillAlpha(FPDF_PAGEOBJECT page_object, float* alpha); + +// Experimental EmbedPDF Extension API. +// Read the object's stroking alpha. The output, failure, and ownership +// semantics are the same as EPDFPageObj_GetFillAlpha(). The value does not +// imply that the object actually strokes any content. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPageObj_GetStrokeAlpha(FPDF_PAGEOBJECT page_object, float* alpha); + +// Experimental EmbedPDF Extension API. +// Read the object's resolved blend-mode name, using the names accepted by +// FPDFPageObj_SetBlendMode(). The default is "Normal". +// +// page_object - handle to a page object. +// buffer - caller-owned buffer, or NULL to query the required size. +// buflen - size of |buffer| in bytes. +// +// Returns the required byte count including the terminating NUL, or 0 for a +// NULL object. Copies only when |buffer| is non-NULL and large enough; +// otherwise leaves it unchanged. Does not modify the object. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFPageObj_GetBlendMode(FPDF_PAGEOBJECT page_object, + char* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Return TRUE if the object's graphics state contains a soft mask, FALSE +// otherwise (including a NULL object). This does not report an image's own +// /SMask or masks applied by enclosing objects. Does not modify the object. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPageObj_HasSoftMask(FPDF_PAGEOBJECT page_object); + +// Experimental EmbedPDF Extension API. +// Read a Form XObject's transparency-group state. +// +// form_object - handle to a form page object. +// is_group - receives TRUE for a transparency group, FALSE otherwise. +// is_isolated - receives TRUE for an isolated transparency group, FALSE +// otherwise, including an ordinary form. +// +// Returns TRUE on success. Returns FALSE for a NULL/non-form object or NULL +// output pointer, leaving both outputs unchanged. Does not modify the form. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFormObj_GetTransparencyGroup(FPDF_PAGEOBJECT form_object, + FPDF_BOOL* is_group, + FPDF_BOOL* is_isolated); + +// Experimental EmbedPDF Extension API. +// Read the object's miter limit: the maximum ratio of miter length to stroke +// width. The PDF default is 10. The value does not imply a miter join is used. +// +// page_object - handle to a page object. +// miter_limit - receives the graphics-state miter limit. +// +// Returns TRUE on success. Returns FALSE for a NULL handle or output pointer, +// leaving the output unchanged. Does not modify the object. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPageObj_GetMiterLimit(FPDF_PAGEOBJECT page_object, float* miter_limit); // Experimental EmbedPDF Extension API. // Insert a blank page without loading it: FPDFPage_New() for a caller that diff --git a/testing/resources/epdf_pageobject_state.in b/testing/resources/epdf_pageobject_state.in new file mode 100644 index 000000000..31501d75b --- /dev/null +++ b/testing/resources/epdf_pageobject_state.in @@ -0,0 +1,77 @@ +{{header}} +{{object 1 0}} << /Type /Catalog /Pages 2 0 R >> +endobj +{{object 2 0}} << /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +{{object 3 0}} << + /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] + /Resources << + /ExtGState << /Half 5 0 R /Mask 11 0 R >> + /XObject << /Im 6 0 R /Plain 8 0 R /Group 9 0 R /Isolated 10 0 R >> + /Shading << /Sh 7 0 R >> + >> + /Contents 4 0 R +>> +endobj +{{object 4 0}} << {{streamlen}} >> +stream +q /Half gs 4 w 1 j 7 M 10 10 20 20 re B Q +q /Half gs 10 0 0 10 40 10 cm /Im Do Q +q /Half gs /Sh sh Q +/Plain Do +/Group Do +/Isolated Do +q /Mask gs 10 60 20 20 re f Q +endstream +endobj +{{object 5 0}} << /Type /ExtGState /ca 0.25 /CA 0.75 /BM /Multiply >> +endobj +{{object 6 0}} << + /Type /XObject /Subtype /Image /Width 1 /Height 1 + /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /ASCIIHexDecode + {{streamlen}} +>> +stream +FF0000> +endstream +endobj +{{object 7 0}} << + /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] + /Function << /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >> + /Extend [true true] +>> +endobj +{{object 8 0}} << + /Type /XObject /Subtype /Form /BBox [0 0 100 100] /Resources << >> + {{streamlen}} +>> +stream +10 40 20 20 re f +endstream +endobj +{{object 9 0}} << + /Type /XObject /Subtype /Form /BBox [0 0 100 100] /Resources << >> + /Group << /S /Transparency /I false >> + {{streamlen}} +>> +stream +40 40 20 20 re f +endstream +endobj +{{object 10 0}} << + /Type /XObject /Subtype /Form /BBox [0 0 100 100] /Resources << >> + /Group << /S /Transparency /I true >> + {{streamlen}} +>> +stream +70 40 20 20 re f +endstream +endobj +{{object 11 0}} << + /Type /ExtGState /SMask << /S /Alpha /G 10 0 R >> +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF