Repository navigation
feat: read and convert a selection of a PDF's pages - #1690
Merged
Merged
Conversation
…Version A LayoutDocument from a page-selected read carries pageCount, so the pageCount property no longer narrows pdf_inspect's result to its summary shape. formatVersion is on every LayoutDocument and on no summary.
readPdf takes a pages option of zero-based page-tree indices, negative indices counting from the end. An unselected page's content stream is never fetched, decoded or interpreted, so the fonts and images only unselected pages use are never resolved, and a page-selected read decodes no embedded file, leaving attachments out of its result. Structure ownership and its parent-tree diagnostics cover selected pages only; the structure element tree stays document-level. The selected pages come back in document order, each carrying its source position as index, with the page tree's length as pageCount; a full read sets neither. Document-level page references keep naming source indices, and sourcePageIndex resolves either shape. An empty selection reads no page. An entry naming no page throws PdfPageSelectionError before any page is read. writePdf refuses a document carrying pageCount with PdfPartialDocumentError, since it places pages by position and the selection's references name source pages. Refs #1688
The pdfTo* conversions, convertDocument with a pdf source and readNativeDocumentTree/readContentDocument take a pages option, passed to readPdf. The plan runner hands it only to the first hop of a plan whose source is pdf, so a route that renders an intermediate pdf (docx to ods) never selects pages of a document the caller did not supply. readDocumentMetadata reads a pdf with an empty selection, and openPdf always reads every page. Every reconstruction now stamps frames, comment anchor keys and sheet names with the page's source index (sourcePageIndex) rather than its position, so they agree with the destinations and form widgets the layout already names by source index. A page-selected conversion's DocumentTree carries no pages envelope, since that array is indexed by frame page index and a selection cannot fill it. Markdown output of a page-selected conversion opens each page with its original one-based page number, written as <!-- page N --> beside the existing page-break marker. buildMarkdownText and renderContentDocumentToMarkdown take the same labels as pageNumbers for callers composing the stages themselves. The label is output-only: reading markdown keeps it as ordinary content. Refs #1688
The /ParentTree number tree joins the structure element tree in the list of what a selected read still reads whole, and the README tells a caller to check pageCount before taking an absent attachments list to mean the document has none. The LAYOUT_FORMAT_VERSION comment records why index and pageCount do not bump the version. Tests cover a selected non-first page keeping its structure ownership and the page-boxes residue holding only the selected pages' rows. Refs #1688
…ome pdf buildDocumentBytes(tree, "pdf") said every pdf-to-format dump carries pages, which a page-selected conversion's dump does not. The message and the README now say so, and readDocumentMetadata's comment states what an empty selection skips rather than implying a catalog-only read. Refs #1688
readDocumentMetadata dispatches a content format through CONTENT_READERS and reads pdf with readPdf directly, not through readDocumentLayout or the registry. The codec module comments, the README and the abort-signal test name now say so. Refs #1688
… stale format counts The metadata directory holds only the write side. readDocumentMetadata lives with the pdf conversions and never goes through the codec registry. Comments no longer state how many content formats there are.
Mearman
marked this pull request as ready for review
October 17, 2026 03:05
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
This security review couldn’t start because the paying account has exhausted its Codex allowance and has insufficient available credits. The payer can check their balance and add credits in the Codex usage dashboard, then try again. If you do not manage the paying account, contact this repository's admins. |
Contributor
|
🎉 This PR is included in version 14.5.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
readPdf(bytes, { pages: [0, 1, 2, -1] })now reads only the listed pages. Indices are zero-based into the page tree and a negative index counts from the end. Duplicates collapse, and the result comes back in document order regardless of the order you asked in. Each returned page carries its source position asindex, and the document carries the page tree's length aspageCount. A full read sets neither, so its output is unchanged. An index that names no page throwsPdfPageSelectionErrorbefore any page is read: out of range givespdf/page-out-of-range, and anything that isn't an integer (NaN and the infinities included) givespdf/page-index-not-integer.-0reads as page 0. An empty selection is valid and reads no page, so it's the cheap way to get the page count and metadata.A page-selected read skips three things. An unselected page's content stream is never fetched, decoded or interpreted. The fonts and images that only unselected pages use are never resolved. No embedded file is decoded, so
attachmentsis absent from a page-selected result. An attachment is document-level payload, and its size has nothing to do with which pages you asked for. An absentattachmentstherefore means "not read" wheneverpageCountis set, and the READMEs tell callers to checkpageCountbefore concluding a document has no attachments.Some things are still read whole, either because page references resolve against them or because they're document-level and hold no page content stream. These are the page tree, the outline, named destinations, layers, the AcroForm, the structure element tree and the
/ParentTreenumber tree (both of which scale with the document), metadata and residue.Two surfaces come from page content and so cover selected pages only. One is structure ownership, together with its parent-tree diagnostics. The other is the
page-boxesresidue. Direct destination arrays on links are minted asdestNonly when their page is interpreted. So an unselected page's direct link destinations are missing, and the minted names depend on which pages were read.I haven't measured heap. The claim here is structural: what is and isn't read.
writePdfnow refuses any document carryingpageCountand throwsPdfPartialDocumentError. It places pages by position, while a selection's references name source pages, so writing one would point links at the wrong pages.In documents.js,
pagesis threaded throughPdfToDocumentOptions,UnifiedConversionOptionsandReadNativeDocumentTreeOptions. The plan runner passes it only to the first hop of a plan whose source is pdf. A docx to ods route renders an intermediate pdf internally, and that pdf is never page-selected.readDocumentMetadatareads a pdf with an empty selection, andopenPdf(which now takesOmit<ReadPdfOptions, "pages">) always reads every page.Reconstructions used each page's position in
layout.pagesas its page index. They now use the source index for frames, comment anchor keys and sheet names. That keeps them consistent with the form widgets and destinations, which already use source indices. A page-selected conversion'sDocumentTreeleaves out thepagesenvelope, because that array is indexed by frame page index. Such a tree can't be rebuilt to pdf, andbuildDocumentBytes(tree, "pdf")now names the page-selected case in its error rather than claiming every pdf-to-format dump carries pages. Reconstruction only sees the selected pages, so a cover page read on its own can lose its title heading: the body size is taken from the selection. This is documented on the option and in the README; the algorithm is unchanged.Page-selected Markdown opens each page with
<!-- page N -->, where N is the original page number. It sits beside the existing<!-- page break -->. Callers who composereadPdf,reconstructWordprocessingand the writer themselves get the same labels throughpageNumbers. The labels are output-only, and a markdown read keeps them as ordinary comments.The tests spy on
interpretContentStreamand ondecodeStream, and the fixture streams carry a marker name. With that setup, a selection interprets one content stream per selected page. It decodes neither the unselected page's content stream nor the font program only that page uses, and on any selection, empty included, it decodes no embedded file. A full read still decodes and returns the attachment. An image used only by an unselected page never reachesimages. A broken/StructParentson an unselected page produces no diagnostic, while a full read still produces one. A selected non-first page of a tagged document keeps its structure ownership, equal to the same page in a full read. Thepage-boxesresidue holds only the selected pages' rows.The selected pages equal the same pages of a full read, plus
index. Further tests cover negative indices, duplicates, empty selections, out-of-range and non-integer indices, andwritePdfrefusing a selection. On the documents.js side there are tests for:buildDocumentBytes(tree, "pdf")on a page-selected tree throwing the page-selected messagepages, which converts the whole document and doesn't throw for an index past its renderingonDocumentstill firing once on a multi-hop routereadDocumentMetadataasking for no pageopenPdfreading every page<!-- page 12 -->surviving a markdown readFor every new piece of logic I broke it on purpose (reading every page, skipping no attachments, structure for every page, dropping the write guard, passing
pagesto every hop, forwardingpagesfromopenPdf, full-reading for metadata, and the rest) and confirmed a test fails.This supersedes #1689, rebuilt as commits scoped to one package each so releases attribute them correctly. The document-operations change is comment and test only: a page-selected
LayoutDocumentalso carriespageCount, so a caller tellingpdf_inspect's two result shapes apart should testformatVersion, which only the layout has, rather than"pageCount" in result.Refs #1688