Repository navigation
ResizePage: destinations and annotation measures move along (part of #205) - #233
Merged
Merged
Conversation
The /BBox of an annotation's appearance stream is mapped onto its /Rect, and the border of a field lies on the edge of that box. With the box off the pixels, the exact clip and the border's edge, both antialiased, lightened the edge pixels twice: a field Editor.SetFields wrote was drawn with other edges than the same value shown from a FormState, which is not clipped. The clip of an annotation's appearance is now rounded outward to whole device pixels when it stays on the axes, as PDFium, MuPDF, Poppler and Ghostscript clip rectangles. The accuracy report against PDFium is unchanged. overlayFile puts its fields off the pixels, which the test held them to. Fixes #222.
The first fix of #222 rounded the clip of every annotation's appearance outward to whole pixels. That made the render of an appearance stream differ from the page it is flattened to, whose content clips exactly: the corpus API run reported 41 files of pdf.js whose flattened first page differed, by up to 4.9 % of its pixels. The other way round: the appearance cera generates for a widget is clipped to the widget's box exactly, as the stream Editor.SetFields writes for it and the form Editor.Flatten draws are. The render with a FormState, the saved file and the flattened page are drawn alike at any scale; the border of a field off the pixels is lighter on its outer edge in all three, as the appearance streams of documents are. The corpus API report and the accuracy report against PDFium are the same as on main.
ImportOptions.Structure brings the elements of the source's structure tree whose content is on imported pages, with the elements above them; an element left without content is dropped. Where the result has a tree, the elements follow its own in a Part under its Document element (or its root): the source's Document becomes the Part, or one is made around its elements, with the source's /Lang where it differs. A result without a tree takes the first one brought as its own. The keys of the parent tree are numbered anew after the result's: pages (/StructParents), their annotations (/StructParent) and the form XObjects they draw. A page imported twice is tagged once, its first copy; a form only pages that did not come draw as tagged content loses its keys. Marked content and object references are mapped to the copies. Role maps are merged, a type the result maps otherwise written as what the source means by it; a class the result defines otherwise is renamed Cls_2, an identifier the result has id_2; namespaces are joined. /MarkInfo says /Marked. The corpus API run gets a step: the pages looked at, imported with their structure into the document itself, keep their marked content in the tree. Over the pdf.js corpus it finds no difference. Fixes #183.
PDF 2.0 lets a page carry /OutputIntents of its own (14.11.5), which take precedence over the document's on it. Such a page is now drawn with a view of the document (Page.cmykDoc) whose DeviceCMYK converts through the profile of the page's intent, read as the catalogue's is: the first intent of kind GTS_PDFX, GTS_PDFA1 or ISO_PDFE1 with a CMYK profile. Document is split into what all views share (docShared: the reader, fonts, the form, the profiles and their budget, transfer functions) and what stands on DeviceCMYK (cmykView: colour spaces, shadings and patterns by reference, decoded images under an id of their own). Pages with one profile share a view; forms and patterns drawn on a page take its view. NaiveCMYK, CMYKProfile and IgnoreOutputIntent decide for the pages as for the document. A page profile counts against maxCMYKProfiles, and one cera cannot read is counted as icc-lut, the page then converting as the document does. The corpus API report and the accuracy report against PDFium are the same as before. Fixes #186.
SignOptions.Timestamp takes a TimestampFunc that gets a token (RFC 3161) from a timestamp authority for the SHA-256 digest of the signature value; the token goes into the signer info as the unsigned attribute signature-time-stamp (PAdES B-T). cera does no network I/O: the function makes the round trip, and may answer with the token or with the whole TimeStampResp (cms.Token). A token that does not stamp the digest, or whose authority did not sign it, is an error (cms.Stamps). The room for /Contents grows by 16 KB; Editor.SignContext passes a context. Editor.Timestamp writes a document timestamp (/Type /DocTimeStamp, ETSI.RFC3161) over the update, in an invisible field. Editor.AddValidation records certificates, OCSP responses and CRLs the caller fetched, written into the document security store (/DSS with /Certs, /OCSPs, /CRLs and a /VRI entry per signature, keyed by the SHA-1 of its /Contents) by Update, Sign and Timestamp; what the store has stays and is not stored twice. With a document timestamp after it the signatures are B-LTA. Both are no change by the rules of a certification (PDF 2.0, 12.8.2.2.2), so a document certified to allow none may get them, and Verify does not count a document timestamp as a change. Two faults of Verify's comparison of revisions, found on the way: a reference to a page was compared by the page's content, so that a link to a page whose /Annots a later signature changed made the signature report annotations changed; and fields were matched by name, so that a form with two fields of one name reported a value filled in. Pages are now compared by their place, fields by their objects. The corpus API step "sign" signs with a timestamp, adds validation data and a document timestamp, and checks both signatures. Fixes #193. Revocation from /DSS when verifying, and the smaller points #143 left, are #228.
A path that reached into an area was clipped, and its numbers stayed in the file: a curve half under the area could be read back whole. Now its segments are split where they cross the edges of the area (lines and cubic curves against the lines of a convex quad), and each run of pieces in the area is replaced by the straight line across it; the clip stays. The area is convex, so what the path fills outside it is the same. A stroke is cut against the area narrowed by how far it reaches (half its width, the mitre limit with mitre joins), so that its new joins stay in the area; a dashed stroke is only clipped, since a shorter path moves its dashes. A filled subpath is cut as a loop; a stroked open one loses the runs at its ends. s, b and b* are written as S, B and B* with the path closed itself, and a line there and back is stroked as the line, as the renderer draws it. A clip in an area becomes an empty one, a clip that reaches into one is cut too. ApplyRedactions writes /OverlayText over the rectangle of a redaction annotation, in Helvetica (fallback fonts for what it lacks) at the size and in the colour of /DA, aligned by /Q, with /Repeat over and over. Part of #198: the other points of its list stay open.
SaveOptions.Images takes ImageOptions: MaxPPI and TargetPPI resample images drawn finer than asked, JPEGQuality stores photographs in grey or RGB as JPEG, Bilevel stores images of one bit a pixel as CCITT Group 4. The resolution of an image is measured where it is drawn largest: the pages of the result are run with a device the interpreter tells of the images it draws (imageSeer) instead of decoding them, in forms, patterns and annotation appearances, all layers shown; masks are measured where their image is drawn. An image is decoded to its samples in its own colour space and resampled by averaging (palette indices by the middle pixel; two and four bits become eight); one-bit images, stencil masks among them, are not resampled. It keeps its colour space, /Decode and masks, and is replaced only by an encoding that is at least 64 bytes smaller. Images under DCT, JPX or JBIG2 and with colour key masks are left as they are. Photographs are told from line art by the colours on a grid over them. The Group 4 encoder (internal/pdf/ccittenc.go) codes T.6 with the tables of the decoder and is checked against it. pdfedit.Doc.Replace writes objects of a source document in place of what it has. The corpus API run gets the step "images": the file opens, is no larger and the pages looked at are as bright as they were. Fixes #201.
SaveOptions.SubsetFonts writes the TrueType and name-keyed CFF programs of the pages with only the glyphs they show. The pages are run with a TextDevice that collects, by font, the glyphs and codes shown — in forms, patterns, Type 3 glyphs and annotation appearances, all layers shown — so the glyphs are those the renderer takes. Glyphs not shown are emptied and keep their numbers: glyf/loca rebuilt with the components of composite glyphs kept, CFF charstrings replaced by endchar (opentype's SubsetCFF; a bare program is wrapped in the smallest OpenType file for it). Layout and variation tables, which a PDF is not drawn with, go; a vertical font keeps GSUB for its vertical forms. /BaseFont and /FontName get a subset tag, /CIDSet goes. A program stays whole where that is not safe: fonts of the form (/DR), fonts cera draws with a stand-in or could not read, CID-keyed CFF and Type 1 programs, documents whose pages did not run cleanly. The subset is read back: every glyph shown must have its outline, and every code of a simple font its glyph and its text, or the program stays whole — the corpus showed two files where a subset program found other glyphs for its codes. The corpus API run gets the step "subset fonts": pages look and read as they did. Fixes #202.
Destinations that lead to a page whose geometry moves are moved with it: the coordinates of /XYZ, /FitH, /FitV, /FitR, /FitBH and /FitBV, by the page's matrix (a single coordinate where the matrix keeps the axes apart); the zoom stays. In the edited document a bounded walk from the catalogue (/OpenAction, /Dests, the name tree, the outline, the form) and from the annotations of the pages written finds every object that holds one and changes it, so an update moves them too. Destinations imported pages bring — links, outline entries, named destinations — move as they are made. What an annotation measures in its own terms is scaled with the page, by the mean of the two directions: the width and dashes of its border (/Border, /BS), the leader lines of a line (/LL, /LLE, /LLO) and the font size of its own /DA. Part of #205: the editor's own coordinates, tagged pages with patterns, rotation as geometry and /UserUnit stay open.
This was referenced Oct 14, 2026
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.
Part of #205 — its first two points ("the first is the one a reader notices"); the issue keeps the rest. Stacked on #232 (on #231 … #225).
Destinations (
internal/pdfedit/geometry.go)Geometry.destmoves an explicit destination by the page's matrix:/XYZ left top(one coordinate alone where the matrix keeps the axes apart),/FitH//FitBHtop,/FitV//FitBVleft,/FitRas the box of its moved corners; the zoom stays;/Fit,/FitBneed nothing.builder.pageGeomaps the output pages that move to their geometry;source.moveDestlooks the target up throughpageTarget(the first copy of a page).moveDests, right aftermovePages, before patches are written): a bounded walk (2¹⁸ objects) from the catalogue's/OpenAction,/Dests,/Names,/Outlines,/AcroForm,/AAand from the annotations of the pages written; it does not follow page dictionaries, content, resources, appearance streams, the structure tree. Each object that holds a destination to a moved page is patched (b.patched, orchangefor direct catalogue entries), so an update moves them as well.link), outline entries (outlineItems) and named destinations (mergeDests, also the edited document's own direct arrays there) move as they are made.Annotation measures (
Geometry.annot)Scaled by
√|det M|:/Borderradii, width and dashes;/BS /Wand/D(directBS);/LL,/LLE,/LLO; the font size of the annotation's own/DA(scaleDA; size 0 stays, a/DAinherited from the field stays). This replaces the comment that said these stay.Tests
TestResizeDestinations(save and update):/OpenActionXYZ, outline FitR and GoTo FitH, old/DestsFitV, name tree FitBH (and an entry of another page that stays), a link on the page itself (null zoom kept) and one on another page.TestResizeDestinationsImported: the same brought by imported pages withOutline/Destinations.TestResizeAnnotationMeasures: border, dashes, leader lines,/DAsize at half scale.go test ./...green.Left in #205: the editor's own coordinates after
ResizePage, tagged pages with patterns, writing/Rotateas geometry,/UserUnit.