From 55cc10b2077dbb79b62c2835c6a47ab3528fa21b Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 8 Nov 2026 12:15:26 +0200 Subject: [PATCH] Read and write a page's viewports without loading the page A page's measurement viewports (/VP) could only be reached through a loaded FPDF_PAGE, and loading a page parses its whole content stream. The engine reads every page's viewports, so a 1,000-page document parsed 1,000 pages to read one dictionary entry each. The viewport functions now have raw twins, as annotations have EPDFPage_GetAnnotRaw: EPDFPage_CountViewportsRaw, GetViewportRaw, AddViewportRaw and RemoveViewportRaw take a document and a page index and never parse the page. A raw viewport is hosted by a page built from its dictionary, and every viewport, measure and number-format accessor works on it unchanged. EPDFViewport_Close releases the host and every handle obtained through it; one left open is freed with the document. Raw and page handles share their generations, so a removal through either makes the other side's handles stale. Each page function and its raw twin share one body. --- fpdfsdk/epdf_c_api_test.c | 5 + fpdfsdk/epdf_measure.cpp | 186 +++++++++++++++++++----- fpdfsdk/epdf_measure_embeddertest.cpp | 124 +++++++++++++++- public/epdf_measure.h | 47 ++++++ testing/embedpdf_layer_mode_writers.txt | 3 + 5 files changed, 319 insertions(+), 46 deletions(-) diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 93c3d3bca5..deb0909969 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -482,11 +482,16 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFNumberFormat_SetText); CHK(EPDFNumberFormat_SetUnit); CHK(EPDFPage_AddViewport); + CHK(EPDFPage_AddViewportRaw); CHK(EPDFPage_CountViewports); + CHK(EPDFPage_CountViewportsRaw); CHK(EPDFPage_FindViewport); CHK(EPDFPage_GetViewport); + CHK(EPDFPage_GetViewportRaw); CHK(EPDFPage_RemoveViewport); + CHK(EPDFPage_RemoveViewportRaw); CHK(EPDFViewport_AddMeasure); + CHK(EPDFViewport_Close); CHK(EPDFViewport_GetBBox); CHK(EPDFViewport_GetMeasure); CHK(EPDFViewport_GetName); diff --git a/fpdfsdk/epdf_measure.cpp b/fpdfsdk/epdf_measure.cpp index acf6198a39..b88597b56f 100644 --- a/fpdfsdk/epdf_measure.cpp +++ b/fpdfsdk/epdf_measure.cpp @@ -41,8 +41,13 @@ struct OwnerEpochs { // the same owner must invalidate one another. Direct annotations use // page/index. using OwnerKey = std::tuple; +struct Path; struct Registry final : CPDF_MeasureStorage { std::map owners; + // The page behind each open raw viewport handle (EPDFPage_GetViewportRaw): + // built from the page dictionary and never parsed. Kept until + // EPDFViewport_Close, or until the document closes. + std::map> raw_hosts; }; struct Path { Kind kind; @@ -85,12 +90,14 @@ Arena* ArenaFor(Host* host) { } return static_cast(host->GetMeasureStorage()); } -OwnerEpochs* EpochsFor(CPDF_Page* page, CPDF_AnnotContext* annot) { - auto* doc = page->GetDocument(); +Registry* RegistryFor(CPDF_Document* doc) { if (!doc->GetMeasureStorage()) { doc->SetMeasureStorage(std::make_unique()); } - auto* registry = static_cast(doc->GetMeasureStorage()); + return static_cast(doc->GetMeasureStorage()); +} +OwnerEpochs* EpochsFor(CPDF_Page* page, CPDF_AnnotContext* annot) { + auto* registry = RegistryFor(page->GetDocument()); uint32_t obj = annot ? annot->GetAnnotDict()->GetObjNum() : page->GetDict()->GetObjNum(); int direct_index = -1; @@ -340,6 +347,79 @@ bool IsShape(const CPDF_AnnotContext* context) { auto type = context->GetAnnotDict()->GetNameFor("Subtype"); return type == "Polygon" || type == "PolyLine"; } + +// The page-level viewport functions, for a loaded page and for a raw host +// alike: each reads and writes only the page dictionary. +unsigned long CountViewports(const CPDF_Page* page) { + auto array = page->GetDict()->GetArrayFor("VP"); + return array ? array->size() : 0; +} +Path* GetViewport(CPDF_Page* page, unsigned long index) { + auto array = page->GetDict()->GetArrayFor("VP"); + if (!array || !array->GetDictAt(index)) { + return nullptr; + } + return ViewportPath(page, index); +} +Path* AddViewport(CPDF_Page* page, const FS_RECTF* bbox) { + if (!Finite(bbox)) { + return nullptr; + } + auto current = page->GetDict(); + if (current->KeyExist("VP") && !current->GetArrayFor("VP")) { + return nullptr; + } + auto dict = page->GetMutableDict(); + CPDF_Measure::DetachIndirect(dict.Get(), "VP"); + auto array = dict->GetOrCreateArrayFor("VP"); + auto viewport = array->AppendNew(); + viewport->SetNewFor("Type", "Viewport"); + CFX_FloatRect rect(bbox->left, bbox->bottom, bbox->right, bbox->top); + rect.Normalize(); + viewport->SetRectFor("BBox", rect); + return ViewportPath(page, array->size() - 1); +} +bool RemoveViewport(CPDF_Page* page, unsigned long index) { + auto current = page->GetDict()->GetArrayFor("VP"); + if (!current || index >= current->size()) { + return false; + } + auto dict = page->GetMutableDict(); + CPDF_Measure::DetachIndirect(dict.Get(), "VP"); + auto array = dict->GetMutableArrayFor("VP"); + array->RemoveAt(index); + if (array->IsEmpty()) { + dict->RemoveFor("VP"); + } + auto* epochs = EpochsFor(page, nullptr); + ++epochs->viewports; + epochs->measures.clear(); + return true; +} + +// A page built from its dictionary and never parsed, as EPDFPage_GetAnnotRaw +// builds one: what a raw viewport reads and writes through. Call it inside the +// document's view scope, so a layer resolves the page in the layer. +RetainPtr RawHost(CPDF_Document* pdf, int page_index) { + if (!pdf || page_index < 0 || page_index >= pdf->GetPageCount()) { + return nullptr; + } + RetainPtr dict = pdf->GetPageDictionary(page_index); + if (!dict || !CPDF_Document::IsPageTreeLeaf(dict.Get())) { + return nullptr; + } + return pdfium::MakeRetain( + pdf, pdfium::WrapRetain(const_cast(dict.Get()))); +} +// A raw viewport handle keeps its host until EPDFViewport_Close. The host's +// arena holds the handle and every handle obtained through it. +EPDF_VIEWPORT KeepHost(RetainPtr host, Path* path) { + if (!path) { + return nullptr; + } + RegistryFor(host->GetDocument())->raw_hosts[path] = std::move(host); + return reinterpret_cast(path); +} } // namespace FPDF_EXPORT EPDF_MEASURE FPDF_CALLCONV @@ -365,17 +445,13 @@ EPDFAnnot_RemoveMeasure(FPDF_ANNOTATION annot) { FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFPage_CountViewports(FPDF_PAGE page) { ScopedFPDFPageView view(page); - auto array = view ? view.Get()->GetDict()->GetArrayFor("VP") : nullptr; - return array ? array->size() : 0; + return view ? CountViewports(view.Get()) : 0; } FPDF_EXPORT EPDF_VIEWPORT FPDF_CALLCONV EPDFPage_GetViewport(FPDF_PAGE page, unsigned long index) { ScopedFPDFPageView view(page); - auto array = view ? view.Get()->GetDict()->GetArrayFor("VP") : nullptr; - if (!array || !array->GetDictAt(index)) { - return nullptr; - } - return reinterpret_cast(ViewportPath(view.Get(), index)); + return view ? reinterpret_cast(GetViewport(view.Get(), index)) + : nullptr; } FPDF_EXPORT EPDF_VIEWPORT FPDF_CALLCONV EPDFPage_AddViewport(FPDF_PAGE page, const FS_RECTF* bbox) { @@ -383,42 +459,76 @@ EPDFPage_AddViewport(FPDF_PAGE page, const FS_RECTF* bbox) { return nullptr; } ScopedFPDFPageView view(page); - if (!view) { + return view ? reinterpret_cast(AddViewport(view.Get(), bbox)) + : nullptr; +} +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPage_RemoveViewport(FPDF_PAGE page, unsigned long index) { + ScopedFPDFPageView view(page); + return view && RemoveViewport(view.Get(), index); +} +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFPage_CountViewportsRaw(FPDF_DOCUMENT document, int page_index) { + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(document); + if (!pdf || page_index < 0 || page_index >= pdf->GetPageCount()) { + return 0; + } + CPDF_DocumentViewScope document_view(pdf); + RetainPtr dict = pdf->GetPageDictionary(page_index); + auto array = dict ? dict->GetArrayFor("VP") : nullptr; + return array ? array->size() : 0; +} +FPDF_EXPORT EPDF_VIEWPORT FPDF_CALLCONV +EPDFPage_GetViewportRaw(FPDF_DOCUMENT document, + int page_index, + unsigned long index) { + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(document); + CPDF_DocumentViewScope document_view(pdf); + auto host = RawHost(pdf, page_index); + if (!host) { return nullptr; } - auto current = view.Get()->GetDict(); - if (current->KeyExist("VP") && !current->GetArrayFor("VP")) { + Path* path = GetViewport(host.Get(), index); + return KeepHost(std::move(host), path); +} +FPDF_EXPORT EPDF_VIEWPORT FPDF_CALLCONV +EPDFPage_AddViewportRaw(FPDF_DOCUMENT document, + int page_index, + const FS_RECTF* bbox) { + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(document); + CPDF_DocumentViewScope document_view(pdf); + auto host = RawHost(pdf, page_index); + if (!host) { return nullptr; } - auto dict = view.Get()->GetMutableDict(); - CPDF_Measure::DetachIndirect(dict.Get(), "VP"); - auto array = dict->GetOrCreateArrayFor("VP"); - auto viewport = array->AppendNew(); - viewport->SetNewFor("Type", "Viewport"); - CFX_FloatRect rect(bbox->left, bbox->bottom, bbox->right, bbox->top); - rect.Normalize(); - viewport->SetRectFor("BBox", rect); - return reinterpret_cast( - ViewportPath(view.Get(), array->size() - 1)); + Path* path = AddViewport(host.Get(), bbox); + return KeepHost(std::move(host), path); } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_RemoveViewport(FPDF_PAGE page, unsigned long index) { - ScopedFPDFPageView view(page); - auto current = view ? view.Get()->GetDict()->GetArrayFor("VP") : nullptr; - if (!current || index >= current->size()) { - return false; +EPDFPage_RemoveViewportRaw(FPDF_DOCUMENT document, + int page_index, + unsigned long index) { + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(document); + CPDF_DocumentViewScope document_view(pdf); + auto host = RawHost(pdf, page_index); + return host && RemoveViewport(host.Get(), index); +} +FPDF_EXPORT void FPDF_CALLCONV EPDFViewport_Close(EPDF_VIEWPORT viewport) { + const auto* path = reinterpret_cast(viewport); + auto* storage = + path ? path->page->GetDocument()->GetMeasureStorage() : nullptr; + if (!storage) { + return; } - auto dict = view.Get()->GetMutableDict(); - CPDF_Measure::DetachIndirect(dict.Get(), "VP"); - auto array = dict->GetMutableArrayFor("VP"); - array->RemoveAt(index); - if (array->IsEmpty()) { - dict->RemoveFor("VP"); + auto& hosts = static_cast(storage)->raw_hosts; + auto it = hosts.find(path); + // A loaded page's viewport: the page owns it. + if (it == hosts.end()) { + return; } - auto* epochs = EpochsFor(view.Get(), nullptr); - ++epochs->viewports; - epochs->measures.clear(); - return true; + // The host goes when this returns, with its arena and every path in it. + RetainPtr host = std::move(it->second); + hosts.erase(it); } FPDF_EXPORT int FPDF_CALLCONV EPDFPage_FindViewport(FPDF_PAGE page, const FS_POINTF* point) { diff --git a/fpdfsdk/epdf_measure_embeddertest.cpp b/fpdfsdk/epdf_measure_embeddertest.cpp index 404d76e7a6..29bc234c76 100644 --- a/fpdfsdk/epdf_measure_embeddertest.cpp +++ b/fpdfsdk/epdf_measure_embeddertest.cpp @@ -148,6 +148,11 @@ TEST_F(EPDFMeasureEmbedderTest, BadArgumentsAndRejectedWrites) { EXPECT_EQ(nullptr, EPDFPage_AddViewport(nullptr, &kBox)); EXPECT_FALSE(EPDFPage_RemoveViewport(nullptr, 0)); EXPECT_EQ(-1, EPDFPage_FindViewport(nullptr, nullptr)); + EXPECT_EQ(0u, EPDFPage_CountViewportsRaw(nullptr, 0)); + EXPECT_EQ(nullptr, EPDFPage_GetViewportRaw(nullptr, 0, 0)); + EXPECT_EQ(nullptr, EPDFPage_AddViewportRaw(nullptr, 0, &kBox)); + EXPECT_FALSE(EPDFPage_RemoveViewportRaw(nullptr, 0, 0)); + EPDFViewport_Close(nullptr); EXPECT_FALSE(EPDFViewport_GetBBox(nullptr, nullptr)); EXPECT_FALSE(EPDFViewport_SetBBox(nullptr, &kBox)); EXPECT_FALSE(EPDFViewport_SetName(nullptr, nullptr)); @@ -348,6 +353,110 @@ TEST_F(EPDFMeasureEmbedderTest, ViewportsFindLastAndInvalidateOnRemoval) { EXPECT_FALSE(CPDFPageFromFPDFPage(page.get())->GetDict()->KeyExist("VP")); } +TEST_F(EPDFMeasureEmbedderTest, RawViewportsReadWhatTheLoadedPageReads) { + auto bytes = Base(); + embedpdf_test::ScopedTestDocument doc(embedpdf_test::LoadMemDocumentForTest( + bytes.data(), bytes.size(), nullptr)); + EXPECT_EQ(1u, EPDFPage_CountViewportsRaw(doc.get(), 0)); + EXPECT_EQ(0u, EPDFPage_CountViewportsRaw(doc.get(), 1)); + EXPECT_EQ(0u, EPDFPage_CountViewportsRaw(doc.get(), -1)); + EXPECT_EQ(nullptr, EPDFPage_GetViewportRaw(doc.get(), 0, 1)); + EXPECT_EQ(nullptr, EPDFPage_GetViewportRaw(doc.get(), 1, 0)); + auto raw = EPDFPage_GetViewportRaw(doc.get(), 0, 0); + ASSERT_TRUE(raw); + FS_RECTF box; + ASSERT_TRUE(EPDFViewport_GetBBox(raw, &box)); + EXPECT_FLOAT_EQ(612, box.right); + EXPECT_FLOAT_EQ(792, box.top); + EXPECT_EQ(0u, EPDFViewport_GetName(raw, nullptr, 0)); + EXPECT_EQ(L"page scale", Ratio(EPDFViewport_GetMeasure(raw))); + // A loaded page reads the same viewport; closing its handle does nothing. + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + EXPECT_EQ(1u, EPDFPage_CountViewports(page.get())); + auto loaded = EPDFPage_GetViewport(page.get(), 0); + EPDFViewport_Close(loaded); + EXPECT_EQ(L"page scale", Ratio(EPDFViewport_GetMeasure(loaded))); + EPDFViewport_Close(raw); + // One left open goes with the document. + EXPECT_TRUE(EPDFPage_GetViewportRaw(doc.get(), 0, 0)); +} + +TEST_F(EPDFMeasureEmbedderTest, + RawViewportWritesShareGenerationsWithLoadedPages) { + ASSERT_TRUE(OpenDocumentFromMemory(EmptyPdf())); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 612, 792)); + auto a = EPDFPage_AddViewportRaw(document(), 0, &kBox); + const FS_RECTF box{50, 300, 300, 50}; + auto b = EPDFPage_AddViewportRaw(document(), 0, &box); + ASSERT_TRUE(a); + ASSERT_TRUE(b); + SetRatio(EPDFViewport_AddMeasure(a), L"first"); + auto mb = EPDFViewport_AddMeasure(b); + SetRatio(mb, L"second"); + auto fb = AddFormat(mb); + ASSERT_TRUE(fb); + ASSERT_TRUE(EPDFViewport_SetName(b, GetFPDFWideString(L"EmbedPDF").get())); + // The loaded page sees what the raw handles wrote. + ASSERT_EQ(2u, EPDFPage_CountViewports(page.get())); + EXPECT_EQ(2u, EPDFPage_CountViewportsRaw(document(), 0)); + auto loaded_b = EPDFPage_GetViewport(page.get(), 1); + EXPECT_EQ(L"second", Ratio(EPDFViewport_GetMeasure(loaded_b))); + FS_POINTF point{100, 100}; + EXPECT_EQ(1, EPDFPage_FindViewport(page.get(), &point)); + // A raw removal makes every earlier handle stale, raw or loaded. + ASSERT_TRUE(EPDFPage_RemoveViewportRaw(document(), 0, 0)); + EXPECT_FALSE(EPDFPage_RemoveViewportRaw(document(), 0, 1)); + FS_RECTF out; + EXPECT_FALSE(EPDFViewport_GetBBox(a, &out)); + EXPECT_FALSE(EPDFViewport_GetBBox(b, &out)); + EXPECT_FALSE(EPDFViewport_GetBBox(loaded_b, &out)); + EXPECT_EQ(EPDF_MEASURE_SUBTYPE_UNKNOWN, EPDFMeasure_GetSubtype(mb)); + EXPECT_FALSE(EPDFNumberFormat_SetPrecision(fb, 10)); + auto remaining = EPDFPage_GetViewportRaw(document(), 0, 0); + EXPECT_EQ(L"second", Ratio(EPDFViewport_GetMeasure(remaining))); + EXPECT_EQ(18u, EPDFViewport_GetName(remaining, nullptr, 0)); + // A loaded page's removal makes the raw handles stale too. + ASSERT_TRUE(EPDFPage_RemoveViewport(page.get(), 0)); + EXPECT_FALSE(EPDFViewport_GetBBox(remaining, &out)); + EXPECT_FALSE(CPDFPageFromFPDFPage(page.get())->GetDict()->KeyExist("VP")); + EXPECT_EQ(0u, EPDFPage_CountViewportsRaw(document(), 0)); + EPDFViewport_Close(remaining); + EPDFViewport_Close(b); + EPDFViewport_Close(a); +} + +TEST_F(EPDFMeasureEmbedderTest, RawLayerReadsPromoteNothingAndWritesOnlyPage) { + embedpdf_test::LayerFixture layer; + ASSERT_TRUE(layer.OpenBytes(Base())); + EXPECT_EQ(1u, EPDFPage_CountViewportsRaw(layer.layer, 0)); + auto first = EPDFPage_GetViewportRaw(layer.layer, 0, 0); + ASSERT_TRUE(first); + FS_RECTF box; + EXPECT_TRUE(EPDFViewport_GetBBox(first, &box)); + EXPECT_EQ(L"page scale", Ratio(EPDFViewport_GetMeasure(first))); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.layer)); + const FS_RECTF detail{50, 300, 300, 50}; + auto second = EPDFPage_AddViewportRaw(layer.layer, 0, &detail); + ASSERT_TRUE(second); + EXPECT_EQ(1u, EPDFLayer_GetPromotedObjectCount(layer.layer)); + EXPECT_TRUE( + EPDFViewport_SetName(second, GetFPDFWideString(L"EmbedPDF").get())); + // The first handle follows the page into the layer. + EXPECT_EQ(L"page scale", Ratio(EPDFViewport_GetMeasure(first))); + EXPECT_EQ(1u, EPDFLayer_GetPromotedObjectCount(layer.layer)); + EPDFViewport_Close(second); + EPDFViewport_Close(first); + auto delta = SaveDelta(layer.layer); + ASSERT_TRUE(delta.changed); + ASSERT_TRUE(layer.Reopen(std::move(delta.bytes))); + const auto loaded_count = EPDFLayer_GetPromotedObjectCount(layer.layer); + ASSERT_EQ(2u, EPDFPage_CountViewportsRaw(layer.layer, 0)); + auto reopened = EPDFPage_GetViewportRaw(layer.layer, 0, 1); + EXPECT_EQ(18u, EPDFViewport_GetName(reopened, nullptr, 0)); + EXPECT_EQ(loaded_count, EPDFLayer_GetPromotedObjectCount(layer.layer)); + EPDFViewport_Close(reopened); +} + TEST_F(EPDFMeasureEmbedderTest, UnknownMeasureIsPreservedAndGeoSettersRejectBeforePromotion) { auto bytes = Base(); @@ -562,9 +671,9 @@ TEST_F(EPDFMeasureEmbedderTest, ShortDistanceOmitsShaftAndBoundsCanShrink) { ASSERT_TRUE(EPDFAnnot_SetLine(annot.get(), &start, &end)); ASSERT_TRUE(EPDFAnnot_SetLineLeader(annot.get(), -15, 5, 0)); ASSERT_TRUE(EPDFAnnot_SetLineEndings(annot.get(), FPDF_ANNOT_LE_ClosedArrow, - FPDF_ANNOT_LE_ClosedArrow)); + FPDF_ANNOT_LE_ClosedArrow)); ASSERT_TRUE(EPDFAnnot_SetLineCaption(annot.get(), true, EPDF_CAPTION_INLINE, - nullptr)); + nullptr)); ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); const auto appearance = Appearance(annot.get()); @@ -579,7 +688,7 @@ TEST_F(EPDFMeasureEmbedderTest, ShortDistanceOmitsShaftAndBoundsCanShrink) { EPDF_CAPTION_OFFSET offset{100, -150}; ASSERT_TRUE(EPDFAnnot_SetLineCaption(annot.get(), true, EPDF_CAPTION_INLINE, - &offset)); + &offset)); ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); FS_RECTF expanded; ASSERT_TRUE(FPDFAnnot_GetRect(annot.get(), &expanded)); @@ -587,7 +696,7 @@ TEST_F(EPDFMeasureEmbedderTest, ShortDistanceOmitsShaftAndBoundsCanShrink) { EXPECT_LT(expanded.bottom, original.bottom); ASSERT_TRUE(EPDFAnnot_SetLineCaption(annot.get(), true, EPDF_CAPTION_INLINE, - nullptr)); + nullptr)); ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); FS_RECTF restored; ASSERT_TRUE(FPDFAnnot_GetRect(annot.get(), &restored)); @@ -964,10 +1073,9 @@ TEST_F(EPDFMeasureEmbedderTest, FontScope() { EPDFFont_ClearRegisteredFonts(); } ~FontScope() { EPDFFont_ClearRegisteredFonts(); } } fonts; - auto bytes = - GetFileContents(PathService::GetThirdPartyFilePath( - "harfbuzz/src/perf/fonts/Roboto-Regular.ttf") - .c_str()); + auto bytes = GetFileContents(PathService::GetThirdPartyFilePath( + "harfbuzz/src/perf/fonts/Roboto-Regular.ttf") + .c_str()); ASSERT_FALSE(bytes.empty()); auto id = EPDFFont_RegisterMemFont64("Roboto", 400, false, bytes.data(), bytes.size()); diff --git a/public/epdf_measure.h b/public/epdf_measure.h index f18c75e12f..74377ce7a4 100644 --- a/public/epdf_measure.h +++ b/public/epdf_measure.h @@ -19,6 +19,9 @@ extern "C" { // PDF objects in a layer. Generic raw dictionary edits must not be mixed with // retained measurement handles: reacquire after such edits. // +// A raw viewport (EPDFPage_GetViewportRaw) is its own origin: it is closed with +// EPDFViewport_Close. +// // A /Measure dictionary (ISO 32000-2 Table 266), owned by an annotation or a // viewport. typedef struct epdf_measure_t__* EPDF_MEASURE; @@ -161,6 +164,50 @@ EPDFViewport_AddMeasure(EPDF_VIEWPORT viewport); // as EPDFAnnot_AddMeasure FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFViewport_RemoveMeasure(EPDF_VIEWPORT viewport); +// ── 2b. Viewports by page index, without loading the page +// ────────────────────────────────────────────────── + +// Experimental EmbedPDF Extension API. +// The page functions above for a page that isn't loaded: they read and write +// the page dictionary and never parse the page's content, as +// EPDFPage_GetAnnotRaw does. In a layer they resolve the page in the layer. +// Generations are shared with loaded pages: a removal through either side +// makes the other side's handles stale. +// +// Number of entries in the page's /VP array; 0 for an invalid page index. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFPage_CountViewportsRaw(FPDF_DOCUMENT document, int page_index); + +// Experimental EmbedPDF Extension API. +// The viewport at |index|, or NULL when out of range. The handle owns what it +// reads through: close it with EPDFViewport_Close, which also ends every +// measure and format handle obtained through it. One left open is freed with +// the document. +FPDF_EXPORT EPDF_VIEWPORT FPDF_CALLCONV +EPDFPage_GetViewportRaw(FPDF_DOCUMENT document, + int page_index, + unsigned long index); + +// Experimental EmbedPDF Extension API. +// As EPDFPage_AddViewport. The handle is closed as EPDFPage_GetViewportRaw's. +FPDF_EXPORT EPDF_VIEWPORT FPDF_CALLCONV +EPDFPage_AddViewportRaw(FPDF_DOCUMENT document, + int page_index, + const FS_RECTF* bbox); + +// Experimental EmbedPDF Extension API. +// As EPDFPage_RemoveViewport, including which handles become stale. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPage_RemoveViewportRaw(FPDF_DOCUMENT document, + int page_index, + unsigned long index); + +// Experimental EmbedPDF Extension API. +// Close a viewport handle from EPDFPage_GetViewportRaw or +// EPDFPage_AddViewportRaw, and every handle obtained through it. A viewport of +// a loaded page belongs to the page: closing one does nothing. NULL is ignored. +FPDF_EXPORT void FPDF_CALLCONV EPDFViewport_Close(EPDF_VIEWPORT viewport); + // ── 3. The measure dictionary (host-agnostic) // ─────────────────────────────────────────────── diff --git a/testing/embedpdf_layer_mode_writers.txt b/testing/embedpdf_layer_mode_writers.txt index 2c2c129b3f..6eb4b5718f 100644 --- a/testing/embedpdf_layer_mode_writers.txt +++ b/testing/embedpdf_layer_mode_writers.txt @@ -269,6 +269,9 @@ EPDFMeasureEmbedderTest.LineAndShapeCaptionEntriesStaySeparate 5 EPDFMeasureEmbedderTest.LineAppearanceLeadersInlineGapAndStableBounds 7 EPDFMeasureEmbedderTest.MissingOrMalformedConversionNeverBecomesOne 4 EPDFMeasureEmbedderTest.NumberFormatsRoundTripAndAbsentVersusEmpty 4 +EPDFMeasureEmbedderTest.RawLayerReadsPromoteNothingAndWritesOnlyPage 5 +EPDFMeasureEmbedderTest.RawViewportWritesShareGenerationsWithLoadedPages 3 +EPDFMeasureEmbedderTest.RawViewportsReadWhatTheLoadedPageReads 5 EPDFMeasureEmbedderTest.RegisteredCaptionFontsStayOutOfCatalogAndEphemeralWrites 11 EPDFMeasureEmbedderTest.ShapeCaptionBoundsShrinkWhenMovedInward 6 EPDFMeasureEmbedderTest.ShapeCaptionCenterRoundTripsAndOnlyChangesVisualBounds 7