All notable changes are listed here. The format follows Keep a Changelog and versions follow Semantic Versioning.
[Unreleased]
1.11.0 - 2026-10-26
Added
- Tagging of existing untagged PDFs, in two layers. The happy path,
PdfDocument.Tag(text, block => role, options)(andPlanTagging, which returns the plan without applying it), takes the role a caller gives each block of an extraction (PdfBlockRole: headings, paragraphs, list items, tables, continued tables, figures, artifacts) and builds the structure by documented rules: list items grouped into oneLwithLblandLBody, table rows and cells from the lines and their cells with a header row, headers and footers as pagination artifacts, link annotations asLinkelements with object references, figures by area, everything else an artifact. Each default is aPdfTagOptionssetting, andCustomizecan change every generated element. The plan API underneath,PdfDocument.CreateTaggingPlan()returns a plan in which a caller (typically an accessibility remediation tool) gives blocks, lines, words, table cells, graphics areas and link annotations of a text extraction a structure role;PdfDocument.ApplyTagging(plan)then writes the marked content, the structure tree, the parent tree,/StructParents,/MarkInfo, the language and the title into the editable document, and marks everything the plan leaves out as an artifact. Pages look and extract the same afterwards: only marked-content operators are added, text-showing operators are split at glyph boundaries without moving a glyph, and Form XObjects are wrapped whole or, when their content belongs to several elements, copied for that use. OnePDF does not choose roles or invent alternate text. Files that already have a structure tree, declare/MarkInfo /Marked trueor contain marked content with an/MCIDare refused withPdfTaggingException, as are references that come from another document, content referenced twice, impossible nesting and pages that cannot be rewritten safely. A link annotation without/Contentsgets the link's alternate text or its words. Seedocs/tagging.md.
Changed
CheckPdfUAfollows a Form XObject drawn by aDooutside marked content: it reports the form only when it paints something unmarked, and checks the form's own marked content againstMCRentries with/Stmin the structure tree and its/StructParents. Before, any suchDowas reported as untagged content.
1.10.2 - 2026-10-26
Fixed
- Text extraction read columns of running text row by row when the gutter between them was narrower than 1.25 em (typically 0.5 to 1.25 em on tightly set pages). Words closer than that are joined into one fragment, so the gutter was never seen and each line came out as the left and right column text joined together. A region with no wide gutter and no gap between blocks is now also cut at an empty stripe of at least 0.5 em that runs clear through at least 10 lines, when every column it leaves has at least 5 lines and its typical line is at least 12 em wide (running text, not labels, bullets or table cells). The columns are read one after another. Regions that already had a wide gutter or a gap between blocks are cut exactly as before.
1.10.1 - 2026-10-24
Added
CheckPdfUA(PdfUAPart.Part2)and the part-2 save check the structure rules forRuby(ISO 14289-2 8.2.5.23),Warichu(8.2.5.24),Captionposition (8.2.5.27), MathML placement and associated files (8.2.5.29), and the parent/child structure type table (Table 5), which keepsEmandStronginline. The MathML namespace is accepted. Part 1 is unchanged. Seedocs/pdfua.mdfor what stays unchecked.
Fixed
- Flate decoding could fail or lose its place in some valid streams. The decoder's fast loop reads up to three symbols after one refill of its bit buffer, and assumed that 56 bits were enough. A literal followed by a length/distance pair with long Huffman codes can need up to 63, so the bit count went negative and the stream desynchronised. Depending on the data this raised a spurious "invalid code" or "distance" error, or produced wrong output that only a trailing Adler-32 would reveal. The decoder now refills before such a pair. Streams are checked against the platform's inflate at every split point and under random chunking.
- PNG images that are decoded (interlaced images and images with an alpha channel, and embedded ICC profiles) now fail when the zlib checksum at the end of the data does not match, instead of accepting damaged pixels. Flate streams in PDF files stay lenient: the data is kept and a mismatch is recorded as a diagnostic, as before.
Changed
- Less memory when interpreting and extracting large documents; output is unchanged (display-list text, snapshots and extraction JSON are byte-identical). A
PdfDisplayListstores glyph runs more compactly: glyphs keep only their own fields (vertical-writing fields and outlines live in side tables, and repeated library-owned outlines are shared), and the font, size, spacing and mode of a run are stored once and shared between runs. A large Flate stream with no known size (typically a content stream) is measured by a first inflate pass and then decoded into one array of exactly that size, instead of growing segments that were joined and discarded, so it allocates about once its size. Compressed input under 128 KiB is unaffected. On two large corpus files, whole-document extraction plus interpretation of every page into a display list allocated 12 % and 22 % less, and each phase's peak working set fell by 14 to 19 %.
1.10.0 - 2026-10-24
Fixed
Redaction reaches content that is not drawn by the page's own operators. Text, paths and image samples in the group of a soft mask are edited like page content, in the mask's own coordinate system, on copies of the graphics state and group. A tiling pattern's cell cannot be cut, so a path that meets an area and is painted with a tiling pattern is removed whole even with
PdfRedactionVectorMode.ClipOut, and a pattern that no remaining paint uses is replaced by an empty cell. A Type 3 glyph is removed when anything its procedure paints meets an area, wherever the glyph stands, and the font is copied without the procedures of glyphs the page no longer shows (and the resources only they used). On a page something was removed from, fonts, XObjects, graphics states, patterns and shadings that the rewritten content no longer refers to are dropped from the page's resource dictionary and from the copies it makes; a font nothing remaining uses becomes a bare Helvetica reference, so its program is unreachable. Shared dictionaries and objects are copied, never changed. The Limits section ofdocs/redaction.mdshrinks accordingly. A resource category stored as its own object (/Font 7 0 R) used to be dropped when redaction added an entry to it; it is now copied entry by entry.A PDF/A or PDF/UA save never drops content. A PDF/UA-1 save of a damaged file used to pass the check and then write
nullfor an embedded font program that did not exist (and keep unreadable streams without saying so).CheckPdfA,CheckPdfUA,PreparePdfA,PreparePdfUAand the conformance saves now share one scan that names every object a save would lose, by number, generation and kind (font program or other font part, ICC profile, image, content stream, XMP stream, embedded file, annotation, structure node): streams whose bytes cannot be read, cannot be decrypted cleanly, or whose filter data is truncated or invalid, and references to needed parts that lead to no object. Check and save agree, a save that would lose content throwsPdfAException/PdfUAExceptionwith the finding and writes nothing, and an unreadable XMP packet is no longer replaced by one built from Info. Saves withoutPdfA/PdfUAare byte-identical to before. Seedocs/pdfa.md.A stream's dictionary now reports the object identity of its stream:
PdfStream.Dictionary.ReferenceandSourceOffsetequal the stream's. Until now they were null and -1, so code that tells "the same object" from "a copy" through the dictionary (GetDictionary(key)on a stream-valued entry, or a stream's.Dictionarystored in a container) treated a shared stream's dictionary as a direct one: storing it copied the dictionary instead of referring to the stream, and identity checks keyed onReferencemissed it. Saves, incremental saves, merge, split, import, redaction and flattening already shared streams through the stream's own reference and are unchanged; plain saves of the test files are byte-identical.Structure destinations (ISO 32000-2 12.3.2.3) report a page.
PdfDestination.PageIndexandPagewere -1 and the structure element; they are now the page of the element's content (its/Pg, else the first page found in its marked-content and object references and descendants, else the nearest ancestor's/Pg), and stay -1 when none is a page of the document. NewPdfDestination.IsStructureDestinationandStructureElement;Pageis now the page dictionary for these destinations. A GoTo action's/SDis preferred over/D(PDF 2.0 GoTo actions).A PDF/A or PDF/UA check and save treat a string damaged by decryption (bad padding, partial last block) as lost content, like a damaged stream: the finding names the key and object and the save refuses. Saves without
PdfA/PdfUAare unchanged. Seedocs/pdfa.md.
Added
- SVG pictures in layout, as vector PDF content.
SvgImage(Parsefrom bytes or text,Width,Height,Title,Description,Drawinto anyPdfContentBuilder),SvgDrawOptions,SvgLimits, theSvgPictureelement, the fluentSlot.Svg(...)withSvgDescriptor(fit modes, DPI, alignment,AltText,Artifact) andCanvasSurface.DrawSvg. Output is paths, strokes with dashes, caps and joins, linear and radial gradients as PDF shadings (spread methods unrolled into stops), gradients and patterns on fills and strokes, clip paths, masks as soft masks, group opacity, blend modes,use/symbol/nestedsvg, markers,viewBoxandpreserveAspectRatio, a CSS subset (presentation attributes,style,<style>sheets with selectors), PNG and JPEG data URIs, and text shaped through OneFont with the layout's fonts, so it extracts as text. A tagged document writes the picture as aFigurewhose alternate text is the author's, else the SVG<title>, else its<desc>; PDF/UA-1 output with an SVG passes veraPDF. Scripts, animation,foreignObject, external references and filters are dropped (filters draw the unfiltered content), each with a diagnostic; nothing is fetched or run. The XML reader refuses entity expansion and every size, depth, instance, path, stop, CSS, data-URI and text bound is aSvgLimitssetting that raisesPdfLimitException. There is ansvgfuzz target. Seedocs/layout-svg.md. - Core: transparency groups and soft masks on the content builder.
PdfFormContentBuilder.SetTransparencyGroup(isolated, knockout)marks a form as a transparency group,PdfContentBuilder.SetSoftMask(form, PdfSoftMaskKind)andClearSoftMask()apply and remove a soft mask made from a form, andPdfSoftMaskKindchooses alpha or luminosity. The SVG renderer needs them for group opacity and masks; existing output does not change. - PDF/UA-2 (ISO 14289-2, on PDF 2.0) output and checking.
PdfUAOptions.Part(PdfUAPart.Part1, the default and unchanged, orPart2),PdfDocument.CheckPdfUA(PdfUAPart, CancellationToken),PdfUAViolation.Part,XmpMetadata.PdfUARevision. A part-2 save writes PDF 2.0, lists the PDF 2.0 and PDF 1.7 structure namespaces on the structure tree root and names one in/NSon every element (NotebecomesFENote), fills/Refon table-of-contents items from the link inside them, and identifies the file withpdfuaid:part2 andpdfuaid:rev2024. It refuses what a person has to supply or what OnePDF cannot choose, with the clause of ISO 14289-2 for each finding: the unnumberedH, a list with labels and noListNumbering, irregular tables, figures without alternate text, page destinations inside the document (PDF/UA-2 needs structure destinations), custom or unnamespaced structure types, and the part-1 findings that still apply. Combining it with PDF/A-2 or PDF/A-3 (PDF 1.7 formats) throwsPdfAException.PdfDocumentWriter.SetBookmarkElementandSetDestinationElementwrite structure destinations in a PDF 2.0 writer; layout uses them so bookmarks, anchors and table-of-contents links work out of the box. Layout gainsStructureGroupandSlot.Aside(),Footnote(),Title()andGroup(type)for the 2.0 structure types. Checked with veraPDF'sua2flavour as a black box. Seedocs/pdfua.md. - Colour glyphs in layout: OpenType
COLRversion 0 glyphs withCPALpalettes are drawn as their layers in palette colours, the foreground entry in the text colour.ColorPalette(Default,Light,Dark,FromIndex) chosen byTextStyle.Palette,Text.Palette, the fluentPalette(...)orLayoutOptions.ColorPalette. Each glyph's layers sit in one/ActualTextspan, so extraction returns the character once; PDF/A-2b and PDF/UA-1 output with colour glyphs passes veraPDF. Tables that are not valid, and glyphs described only inCOLRversion 1, are drawn in one colour with one diagnostic per font. Seedocs/layout-text.md. - Inline objects in layout text: an icon, a small picture, a badge with its own text or a drawn checkbox sits in a line of text and flows with it.
TextSpan.Inline/TextSpan.ForInline,InlineObject(InlineBoxfor a drawn or picture box,InlineElementfor an element subtree in a layoutText),InlineAlignment(baseline with an offset, top, middle, bottom, text top, text bottom),InnerBaseline,GrowsLine,KeepWithNeighbours,AltText,ArtifactandLink; the fluentTextDescriptor.Inline,InlineImageandInlineCanvaswithInlineDescriptor. An object is one unbreakable character (U+FFFC: line break class CB, bidi class ON) with breaks around it by first fit and Knuth-Plass, justification spreads the spaces and leaves it alone, the line grows to hold it unless the line height is fixed, and right-to-left lines place it with its neighbours. In tagged output it is aFigureinside the paragraph with/Alt(or an artifact), the text extractor returns its description through/ActualTextand never a U+FFFC, and links and anchors work on it; PDF/A-2a/3a and PDF/UA-1 output stays compliant. A literal U+FFFC in span text is now dropped. Documents without inline objects are byte-identical to before. Seedocs/layout.mdanddocs/layout-text.md. - Per-page values in layout:
Accumulateadds adecimalto a named account andMarkerrecords a text under a key, both wrapping content (Slot.Accumulate,Slot.Marker);DynamicText(Slot.DynamicText, styled byDynamicTextDescriptor) is a line of text computed for each page by a callback fromPageValues:Sum,Total,BroughtForward,First,Last,Current,Entering, page index, number, label and section. Values come from what actually landed on the page after page breaking, so "Carried forward" and "Brought forward" lines, page subtotals and running headers move with the rows that keep rules push to the next page. They work in headers, footers and the body, in tables, multi-column flow, sections, right-to-left documents and documents with a table of contents (no double counting), are artifacts in headers and footers and tagged text in the body, and are deterministic. Seedocs/layout.md.
1.9.0 - 2026-10-24
Added
Knuth–Plass line breaking in layout, opt in per paragraph and as a document default.
LineBreakMode(Greedy, the default, andKnuthPlass),KnuthPlassOptions(tolerance, pre-tolerance, line, hyphen and explicit-hyphen penalties, consecutive-hyphen, final-hyphen and fitness demerits, looseness, emergency stretch),TextParagraph.LineBreakMode/KnuthPlass,Text.LineBreakMode/KnuthPlass,LayoutOptions.LineBreakMode/KnuthPlass,TextDescriptor.OptimalLineBreaks/GreedyLineBreaksandDocumentComposer.LineBreaking. After Knuth and Plass, 1981: the breaks of a whole paragraph are chosen together, with hyphenation points, mixed fonts and sizes and bidi text; a paragraph with no setting in tolerance is retried with emergency stretch and then accepts any line, and one beyond its search bounds is broken greedily with a warning. Greedy output is unchanged. Seedocs/layout-text.md.RecursiveAI.OnePdf.Hyphenation, a new package:PatternHyphenator(Liang's pattern algorithm with exception lists, minimum word length and left/right minimums) andHyphenators(For,Auto,Resolve,IsSupported,Languages), with bundled patterns for 26 language variants loaded lazily from compressed resources: English (US, GB), German (1996, 1901, Swiss 1901), French, Spanish, Italian, Dutch, Portuguese, Polish, Danish, Norwegian (no, nb, nn), Bulgarian, Slovak, Slovenian, Lithuanian, Estonian, Ukrainian, Galician, Greek (monotonic), Latin, Welsh and Irish. Plug it into layout withTextDescriptor.Hyphenator(Hyphenators.Auto()); layout itself does not depend on it and hyphenation stays opt-in. Pattern data is generated bytools/hyphenation/generate.pyfrom pinned upstream files, each under its own permissive licence (notices in the package). Swedish, Finnish, Icelandic, Catalan, Croatian, Russian, Turkish, Esperanto, Czech, Hungarian, Latvian and Romanian are not bundled because their pattern files are copyleft, LPPL-only or have unclear terms. Seedocs/layout-text.md.Gradient backgrounds in layout:
LayoutGradient(Linearat any angle,Horizontal,Vertical,Circlewith a centre andRadialExtent) andLayoutGradientStop, taken bySlot.Background,Box.BackgroundGradient, table rows and cells (TableRowDescriptor.Background,CellBackground,TableRow.BackgroundGradient,TableCell.BackgroundGradient) and page backgrounds. Clipped to rounded corners, faded by opacity, carried by transforms, drawn as artifacts. A box split over pages paints the whole gradient in each piece. Seedocs/layout.md.Multi-column flow in layout:
MultiColumn(element) andSlot.MultiColumn(cols => ...)pour paragraphs, lists, tables and pictures through 1 to 32 equal columns, down each column, then across, then onto the next page. Column count, gap, an optional rule between columns, balanced (default) or sequential fill (ColumnFill),Spacingbetween items, andColumnBreak. Content breaks between columns as it breaks between pages: widows and orphans, keep-together, keep-with-next, ensure-space, headings never ending a column, table rows splitting with repeated header rows. Balancing the last region is a bisection of at most 12 trial layouts, so it is bounded whatever the content, and it never breaks a rule to even the columns out. Right-to-left documents fill columns right to left; the structure tree and text extraction follow the flow. Seedocs/layout.md.Content builder:
PdfGradient(Axial,Radial),PdfGradientStop,PdfGradientColor(DeviceGray, DeviceRGB, DeviceCMYK),PdfContentBuilder.PaintGradientandFillRectangle. Written as axial and radial shadings with exponential and stitching functions. Seedocs/content-creation.md.Generated table of contents in layout:
TableOfContents(levels, leader, indent, gap, spacing, per-levelTocLevelStyle),TocEntryfor explicit entries,Heading.InTocandHeading.TocTitle,LayoutOptions.TableOfContents, and the fluentSlot.TableOfContents,Slot.TocEntry,Slot.Heading(level, inToc, tocTitle)andTocDescriptor. Entries show the page label of their target (roman and prefixed numbering included), wrap long titles with the number on the last line, link to their heading and are taggedTOC/TOCI/Reference/Link. Page numbers are settled by laying the document out without drawing until the entries stop changing, then once more for real; documents without a table are unaffected. Seedocs/layout.md.
Changed
- Faster first document in a managed process (JIT included): about 16 ms less for the layout invoice (about 130 to 114 ms on the benchmark host).
PdfDocument.CreateEmpty(and with it everyPdfDocumentWriter) builds its cross-reference data directly instead of reading a generated file back, and the first document no longer compiles struct-keyed concurrent and sorted collections: the editable document's object table, the layout font-chain and shaping-option caches, the writer's information, destination and role maps, and the TrueType subsetter's table set and composite check now use plain dictionaries, arrays and snapshots. Output is byte-identical. Peak memory of the first document is about 1 MB lower. - Much less memory when recording large pages into a
PdfDisplayList. The recording now grows in fixed-size chunks instead of doubling and copying its arrays, stores each distinct graphics state once, and shares the outline of a glyph between all its uses instead of copying it for every occurrence (PdfPath.ToOwnedreturns a library-owned, never-modified path as it is). Replay output and the text format are unchanged. On two large real-world files (open, extract text, interpret every page): allocation 2.3 GiB to 0.9 GiB and peak working set about 1.07 GiB to 0.43 GiB for a 137-page, 135,000-item-per-page file; 3.3 GiB to 1.5 GiB and 1.0 GiB to 0.47 GiB for a 154-page file with 300,000 items on a page. A text-heavy page into a display list: 15 ms to 2.2 ms and 32 MB to 4.8 MB allocated. - PDF/A saves embed the standard-14 substitutes (Liberation) as subsets of the characters the document actually shows, not of the whole encoding. The pages, form XObjects, patterns, soft-mask groups, Type 3 glyph procedures and annotation appearances are read to find the codes in use; a font that is never shown, is listed in the form's default resources, or sits where the scan cannot follow keeps its whole encoding. A two-font PDF/A-2b document went from 63,030 to 14,856 bytes.
- CFF subsets (
AddFontwith an OpenType-CFF or bare CFF font) no longer copy the source String INDEX; the subset's own two ROS strings are all it holds. - A glyph that ends in the old accented-character form of
endchar(adx ady bchar achar, Type 2 charstring appendix C) can now be subset. Its composed outline and width are written as a plain path charstring, because a CID-keyed subset has no glyph names to resolve the two StandardEncoding parts with, so the base and accent glyphs are not needed in the subset. A base or accent that is itself composed is still refused with aFontProgramException. - Signing: a time-stamp token returned with indefinite lengths or constructed OCTET STRINGs is re-encoded to DER before it is embedded (ETSI EN 319 142-1 wants a DER SignedData in
/Contents). Only the length framing changes; certificates, signed attributes and every value are copied byte for byte.
Fixed
- Flattening a form in a tagged document left
OBJRentries in the structure tree pointing at the removed widgets, and their entries in the parent tree. Both are removed now, and a structure element that held nothing else (aFormelement, say) goes with them, so the tree stays valid and PDF/UA-1 output stays compliant. The baked-in appearance is marked as an artifact in a tagged document. - Overlays and stamps count the page's open graphics states with the content-stream lexer instead of a scan of the raw bytes, so
qandQinside strings, comments, names, dictionaries and inline image data are no longer counted, and a damaged or unbalanced stream cannot throw the count off. - Text stamps (and any text) set on a diagonal came out of extraction in reversed letter order, and at 135 degrees as single letters. Text rotated by more than 12 degrees from its quantized reading direction is now measured along its own baseline, so its glyphs share one line. Axis-aligned text is unchanged.
1.8.0 - 2026-10-24
Added
- PDF/UA-1 (ISO 14289-1):
PdfSaveOptions.PdfUA(PdfUAOptions),PdfDocument.PreparePdfUAandCheckPdfUA,PdfUAExceptionandPdfUAViolation(Matterhorn checkpoint and ISO clause per finding). A save repairs/MarkInfo,/Lang(fromPdfUAOptions.Language),DisplayDocTitle,/Tabs /S, link/Contentsand the XMP identification, and refuses with every violation listed: missing title or language, figures without alternate text, untagged content, skipped heading levels, tables without header cells or with irregular rows, malformed lists, unmapped structure types, links and widgets outside their structure elements, unembedded fonts and encryption that blocks assistive technology. Only for documents that are already tagged; a clean result is not proof of conformance. Seedocs/pdfua.md. - PDF/A level A:
PdfAConformance.Afor parts 2 and 3 (PDF/A-2a, 3a), alone or combined with PDF/UA-1 (thepdfuaidschema is declared as an XMP extension schema);PdfAOptions.Language.XmpMetadata.PdfUAPart. - Layout:
LayoutDocument.Author,SubjectandKeywords(and the same on the fluentDocumentComposer);LayoutDocument.Save(stream, PdfSaveOptions)takesPdfUAand level A.
1.7.0 - 2026-10-24
Added
PdfFontSource: parses a font once and shares it between documents (PdfDocument.AddFontandPdfDocumentWriter.AddFontoverloads), so producing many documents with the same face does not parse and validate it each time.PdfDocumentWriterpacks dictionaries and arrays into compressed object streams (the catalog stays plain) unless the cross-reference format is a table. A tagged 250-page table went from 8.0 MB to 2.0 MB.PdfDocument.AddPngImage: a minimal PNG reader for image XObjects. Plain non-interlaced PNGs (gray, RGB, palette, 1 to 16 bits) are passed through with their IDAT data unchanged (FlateDecode,/Predictor 15); interlaced PNGs and PNGs with alpha are decoded, alpha becoming a soft mask;tRNSbecomes a colour-key mask where it can, and a matchingiCCPprofile becomes anICCBasedcolour space. Apngfuzz target covers it. Seedocs/content-creation.md.PdfContentBuilder.DrawGlyphRunandPdfShapedGlyph: draw glyphs chosen by a shaping engine, by glyph index with advances, offsets and clusters, from an embedded font. Each cluster maps to text through the font's/ToUnicodemap, or through/ActualTextwhere one glyph cannot carry it (reordered or split clusters, a glyph reused for other text), so extraction returns the logical text.Marked content on the content builder:
BeginMarkedContent,BeginSpan,BeginArtifact,EndMarkedContent,MarkedContentDepth. Marked content andSave/Restoremust nest.PdfDocumentWriter: streaming creation. Each page is written when committed, so a 10,000-page document needs a few hundred bytes of memory per page. It bringsPdfStreamPageBuilder, reserved forms for "page X of Y" (ReserveForm), links, bookmarks and named destinations, and a tagged structure tree (BeginStructureElement,PdfStructureElement,PdfStructureElementOptions,PdfStructureAttributes,MapRole) with marked-content ids,OBJRentries for links, a parent tree and/Tabs /S.PdfDocumentWriter.AddPageLabelRange: page labels (ISO 32000-2 12.4.2) for streamed documents: decimal, roman and letter styles, prefixes and start numbers.PdfDocumentWriter.BeginStructureElement(parent, type, options)andEndStructureElement(element): begin a structure element under an explicit parent and end any open element once its own children have ended, so a producer whose content continues on later pages (a table row split over pages) can keep several branches open at once. The earlier overloads are unchanged.RecursiveAI.OnePdf.Layout, a new package that depends onRecursiveAI.OnePdf,RecursiveAI.OnePdf.FontsandRecursiveAI.OneFont1.1.0: a fluent layout layer.LayoutDocument.Composetakes nested delegates for pages, header, content and footer slots, decorators (padding, border, background, rounded corners, size limits, alignment, rotate/scale/translate), columns, rows, layers, lists, tables with spans and repeated header and footer rows, images, canvas, shaped rich text with page numbers and totals, links, bookmarks, tagging and right-to-left documents;IComponentfor reuse;Savestreams page by page,Save(stream, PdfSaveOptions)adds PDF/A and encryption,ToPdfDocumenthands the result to the core API. The element objects stay public as the lower layer. Seedocs/layout.mdanddocs/migrating-from-questpdf.md.layoutfuzz target and a set of layout reference documents with page images (testdata/layout-references,tools/OnePdf.Layout.References).
Changed
- Layout: shaping is done once per text and released after its last line; fonts are parsed once per face;
Table.MeasuredRowsisint?(null: every row when a column is automatic, otherwise the first 100 body rows). Producing documents is about twice as fast and allocates about half as much. - Layout: page totals ("page X of Y") are shaped and drawn with the embedded font, so documents with totals pass PDF/A-2b and PDF/UA-1 font checks.
Row.Compactmakes a row as wide as its items.RotateandScalethrowPdfLayoutExceptionwhen their child would need a page break, instead of dropping the content after it.
1.6.0 - 2026-10-23
Highlights
- True redaction: text, paths, images, annotations and metadata under given areas are removed from the file, not covered.
- Reading order on multi-column pages: the reading-order error on the real-world multi-column category drops from 16.8 % to 7.6 % (PdfPig, best of its pipelines: 16.8 %); overall from 15.2 % to 14.0 %.
Added
- True redaction.
PdfDocument.Redactremoves glyphs, vector paths, shadings and image samples inside given areas by rewriting page, form and annotation-appearance content (remaining glyphs keep their places), blanks and re-encodes images as Flate, removes or redacts annotations and form values under the areas, strips alternate text and structure text of the removed content, and clears metadata (Info, XMP, outline titles) that matches given terms.ApplyRedactAnnotationsapplies the/Redactannotations in a file. A page that cannot be read completely fails the call withPdfRedactionExceptionand leaves the document unchanged. Aredactfuzz target covers it. Seedocs/redaction.md. PdfSoftMaskInfo.TransferTable: the soft mask's/TRtransfer function (ISO 32000-2 11.6.5.2) evaluated by OnePDF into 256 bytes (entryiis the output fori / 255, rounded half up and clamped), null for no/TRor/Identity. Function types 0, 2, 3 and 4 are supported; an unusable function is ignored with a warning. The display-list text format is version 6:begin-softmaskgainstrtable=.
Changed
- Documented that viewers disagree on whether
/NeedAppearancesregenerates check box, radio button and push button art, and pinned OnePDF's behaviour (stored art is kept) with a test. No behaviour change.
Fixed
- Reading order: columns whose paragraph gaps fall at the same heights are no longer read band by band when something spanning both columns (a full-width note or caption) stops the vertical cut. The horizontal cut now keeps successive bands together when each holds multi-line columns and they share a gutter, and the columns are cut apart afterwards. On the committed and local real-world set the reading-order error is 13.8 % overall (was 15.2 %): multi-column 7.6 % (was 16.8 %), table 0.6 % (3.0 %), form 4.2 % (6.5 %), rotated 9.3 % (12.2 %); math moves from 11.9 % to 12.5 %. The generated set is unchanged. See
docs/text-layout.md. - Editable documents: a new object never takes the number of a reference that points at an undefined object, so a dangling reference no longer starts to resolve to unrelated content added later.
- Encrypted files: the
/Contentsstring of a signature dictionary (/Type /Sigor/DocTimeStamp, or any dictionary with/ByteRange) is no longer decrypted on read (ISO 32000-2 7.6.2), so the raw bytes,PdfSignature.Contentsand a full save of a signed encrypted file all carry the stored signature. - CFF subsets (CID-keyed) write the leading ROS operands of the Top DICT in their shortest integer form, which strict readers need to recognise the Top DICT as CID-keyed (veraPDF warned "The Top DICT does not begin with ROS operator").
- The TrueType subsetter can write a (3,1) format 4
cmap, and PDF/A production now subsets the Liberation faces it embeds for standard fonts (the four-font test file went from 834 KB to 112 KB) instead of embedding them whole.
1.5.0 - 2026-10-23
Added
- PDF/A production.
PdfSaveOptions.PdfA(PdfAOptions: part 2 or 3, level B or U) writes ISO 19005-2/-3 files from a full save: it first adds an sRGB output intent (a version 2 ICC profile OnePDF generates from the published sRGB parameters) and an XMP packet built from the Info dictionary, sets the Print flag and stores generated appearances on annotations, clears/NeedAppearancesonce every widget has an appearance, and embeds Liberation faces (fromRecursiveAI.OnePdf.Fonts) for standard fonts OnePDF itself added. What is still wrong is refused withPdfAException, whoseViolationslist every problem with its clause, object and page: non-embedded fonts, CMYK, encryption, JavaScript and other forbidden actions, LZW, embedded files in part 2, missing appearances, level U fonts without Unicode mapping and more.PdfDocument.CheckPdfAreports without changing anything,PreparePdfAmakes the repairs, andPdfDocument.AttachFileadds attachments for part 3. Output is deterministic withPdfSaveOptions.DocumentId. Apdfafuzz target covers it. Seedocs/pdfa.md. XmpMetadata.PdfAPartandPdfAConformanceread the PDF/A identification of a packet.
Fixed
- Importing pages from a document whose fonts were added with
AddFontcopied the font before its subset had been built; the imported text drew with an empty font program. The source's fonts are now finished before pages are copied.
1.4.0 - 2026-10-23
Added
- Signature verification, read-only.
PdfDocument.GetSignatures()lists signature fields and document time-stamps with signer name, reason, location, contact,/M, sub-filter,/ByteRange, the CMS bytes, the/DocMDPcertification level and the field lock;PdfFormField.GetSignature()returns one field's signature.PdfSignature.VerifyandPdfDocument.VerifySignaturescheck, with the platform's hashes, RSA (PKCS#1 v1.5 and PSS) and ECDSA and its X.509 chain builder (noSystem.Security.Cryptography.Pkcs):- the byte range (starts at 0, leaves out exactly
/Contents, no gaps or overlaps, ends at a revision boundary) and which revision it covers; - the digest, the signed attributes and the signature, for
adbe.pkcs7.detached,ETSI.CAdES.detached(with the signing-certificate attribute),ETSI.RFC3161document time-stamps, and the legacyadbe.pkcs7.sha1(flagged weak);adbe.x509.rsa_sha1is reported as unsupported and weak; - an RFC 3161 signature time-stamp, itself verified, and the certificate chain against
TrustAnchors(or, if asked, the system store) at a validation time chosen from the time-stamp, the signing time or now, or given explicitly; - revocation, which is never fetched: reported as not checked unless a
RevocationCheckcallback answers from data the caller holds; - changes after signing, object by object and classified (form field value, new signature, annotation, page content, validation data, other) against the DocMDP level and FieldMDP locks.
The result (
PdfSignatureVerification) keeps every check separate;IsValidis a convenience. Seedocs/signatures.md.
- the byte range (starts at 0, leaves out exactly
- A
signaturefuzz target (ASN.1/CMS parsing, certificates, chain building, byte ranges and change analysis; also time-stamp responses). - Signing.
PdfDocument.Signwrites an incremental update holding an approval or certification signature (PdfCertificationLevel1 to 3, an optional field/Lock), andPdfDocument.AddDocumentTimestampadds anETSI.RFC3161document time-stamp; both need a document opened withAllowEditing, keep the original bytes as an exact prefix and keep encryption (the/Contentsstring is never encrypted, ISO 32000-2 7.6.2).- Formats: PAdES baseline B-B (
ETSI.CAdES.detachedwithcontentType,messageDigestandsigningCertificateV2, nosigningTime, and the ESIC extensions entry for pre-2.0 files), B-T whenPdfSigningOptions.Timestampis set, oradbe.pkcs7.detached. Digests SHA-256, SHA-384, SHA-512; RSA PKCS#1 or PSS, and ECDSA. - Keys:
PdfSigningCredential.FromCertificate(a certificate with a private key) orFromExternalSigner, a callback that receives the digest of the signed attributes and returns the signature value, for HSMs and remote signing. Every signature value is checked with the certificate's public key before it is embedded. - Time-stamps only through a
PdfTimestampCallbackthat receives the RFC 3161 request and returns a response or token; OnePDF opens no connection. The token's status, imprint and nonce are checked before it is embedded. - Visible signatures get an appearance built with the content builder (name, date, reason, location, optional image). Failures throw
PdfSigningExceptionwith aPdfSigningErrorreason and write nothing. Our own DER writer builds the CMS; noSystem.Security.Cryptography.Pkcs. PdfSigningResultreports the reserved and used/Contentssize. Seedocs/signatures.md.
- Formats: PAdES baseline B-B (
Fixed
- Signature verification checked too little in several attack cases found by crafting the published PDF signature attacks (see
docs/signatures.md, "Known attacks"):- Data appended after the last
%%EOF(objects with no cross-reference section, or a bare trailer) is now an unsigned change; before, a file whose signature covered its last revision still counted as the whole document. - An update whose trailer points
/Rootat a different catalog is a disallowed change. - The certification level is read from the signed revision, not from the newest dictionary.
- An indirect
/ByteRangeor/Contentsis refused. - A signer certificate whose key usage lacks
digitalSignature/nonRepudiation, or whose extended key usage is only for other purposes, now failsIsValid(PdfSignatureVerification.KeyUsage;PdfSignatureVerifyOptions.RequireSigningKeyUsage = falseallows it). A time-stamp signed by a certificate without the time-stamping purpose is invalid.
- Data appended after the last
- Signature verification no longer throws on a damaged embedded certificate on macOS, where the platform accepts certificate bytes and fails only when a property is read. Every certificate from the file, the extra certificates and the trust anchors is read once on load; one that fails is left out with a warning (the signer then counts as missing). Signing checks the credential's certificates the same way and reports
PdfSigningExceptioninstead of a platform exception.
1.3.0 - 2026-10-23
Added
- Form filling.
PdfFormField.SetValue,SetValues,SetCheckedandReset, andPdfAcroForm.SetValue,SetValues,SetChecked,ResetAllandGenerateMissingAppearances, for documents opened withAllowEditing: text (with/MaxLen), combo and list boxes (export or display values, multi-select), check boxes and radio groups./Vand/ASare updated, generated normal appearances are stored in/AP, and/NeedAppearancesis cleared once every widget has one. Refused values throwPdfFormValueException(anArgumentException) with aPdfFormValueErrorreason;PdfFillOptionscontrols read-only fields, truncation and appearance generation. Scripts never run. Seedocs/forms.md. - Flattening.
PdfDocument.Flatten(PdfFlattenOptions?)bakes widget appearances (and optionally other annotations) into page content as Form XObjects, removes the annotations and fields, and drops an empty/AcroForm; it returns aPdfFlattenResult. - The
appearancefuzz target also fills, flattens, saves and reopens. - Page operations on editable documents:
RemovePage/RemovePages,MovePage,ReorderPages,RotatePage/SetPageRotation,InsertBlankPage,DuplicatePage, andPdfDocument.CreateEmpty. Deleting a page repairs the bookmarks, links, named destinations, open action, form fields and page labels that referred to it. - Pages across documents:
PdfDocument.ImportPagescopies pages with their whole dependency graph (resources, fonts, images, annotations with appearances; inherited/Resources,/MediaBox,/CropBoxand/Rotatewritten into the page), each shared object once. Bookmarks, named destinations used by links, form fields (clashing names renamed), optional content, page labels and the structure tree of tagged files come along (PdfImportOptions).PdfDocument.Mergejoins documents;PdfDocument.Splitcuts one into several (PdfPageRange, or a number of pages per part). Seedocs/page-operations.md. - Content creation.
PdfPage.BeginContentreturns aPdfPageContentBuilderthat adds content over or under a page's own (the existing content is wrapped so its graphics state cannot leak in);PdfDocument.BeginFormbuilds a reusable Form XObject.PdfContentBuilderwrites paths (fill, stroke, clip, dashes, caps, joins), DeviceGray/RGB/CMYK colours, opacity and blend modes (ExtGState), a transform stack, text (character and word spacing, render modes,MeasureText,WrapText,DrawTextAligned,DrawTextBox), JPEG bytes embedded as they are, raw 8-bit images with an alpha plane, and images and forms already in the document. Fonts:PdfDocument.AddStandardFont(WinAnsi, Adobe metrics) andAddFontfor TrueType and OpenType (CFF) files, written as Type 0 fonts with Identity-H, a/Warray and a/ToUnicodemap, subset to the glyphs used (ownglyf/locaand CFF subsetters; ISO 32000-2 9.6.4 tags; theOS/2fsTypeembedding and no-subsetting bits are honoured, withPdfFontEmbeddingExceptionand an explicit override).AddTextStampandAddImageStampwatermark chosen pages through one shared form.PdfSaveOptions.DocumentIdfixes the trailer/IDso saves are reproducible. Seedocs/content-creation.md. - A
fontsubsetfuzz target. docs/migrating-from-pdfsharp.md, a guide for moving from PDFsharp.- The
savefuzz target also runs page operations, merge and split. PdfImageDecodeRequest.TargetWidthandTargetHeight: the size in device pixels the image covers on the page (the unit square under the CTM and output transform, rounded up, never above the image's own size), or 0 when unknown. A JPEG 2000 decoder can use it to decode at a reduced resolution.PdfImage.TargetWidth/TargetHeightexpose the same values to devices. Decoding outside a page leaves them 0; existing decoders can ignore them.- A decoder may return fewer pixels than the dictionary declares. OnePDF passes the reduced size through (
PdfImageData.Width/Height) without resampling; placement is unchanged because the image fills the unit square. No warning is recorded when a target size was given and the result is not larger than declared.
Fixed
- TrueType instructions (fonts whose composites need their programs): ISECT now uses exact 128-bit arithmetic instead of treating a discriminant of -1 as degenerate. A glyph program that fails on its own now leaves only its own component unhinted; the whole glyph is drawn from its plain outline only when the shared per-glyph instruction budget is spent.
1.2.0 - 2026-10-23
Added
- Knockout data for devices.
PdfGraphicsState.TextKnockoutcarries the ExtGState/TKflag (default true; saved and restored withq/Q).IPdfDevice.FillStrokePath(a default interface method, also virtual onPdfDevice) receivesB,B*,bandb*as one call; its default callsFillPaththenStrokePath, so existing devices behave as before.PdfDisplayListrecords it as one item and replays it asFillStrokePath. The display-list text format is version 5 (fillstrokerecord,tk=0ings). - Writing.
PdfOpenOptions.AllowEditingopens a document for changing;PdfDictionary.SetEntry/RemoveEntry,PdfArray.Append/Insert/SetItem/RemoveAt/Clear,PdfStream.SetData/SetStoredBytes,PdfDocument.AddObject/SetObject/RemoveObject/CreateStream/SetInfo/SetXmp, and public constructors for the simple COS types. Seedocs/writing.md. PdfDocument.Savewrites a complete new file (classic table, or cross-reference and object streams; optional Flate for unfiltered streams; unreachable objects dropped, numbers compact) and can keep, remove or set encryption (AES-128 revision 4, AES-256 revision 6).PdfDocument.SaveIncrementalappends an update that leaves the original bytes untouched. Options are inPdfSaveOptionsandPdfEncryptionOptions.- A
savefuzz target (open, change, save, reopen).
Fixed
PdfImageencoded data of an encrypted document (GetEncodedData) was the ciphertext; it is now the stored bytes with the encryption removed.- A
startxrefthat pointed exactly at a cross-reference section was passed over for a reading that landed on white space in front of another object, in files with junk before the header; an exact hit now wins. - Composite glyphs of MingLiU, PMingLiU and DFKai-SB fonts (and subsets of them) were drawn with displaced or missing strokes, because their component placement is done by the font's own instructions. For fonts recognised from their names or program-table checksums, the glyph instructions now run (at one pixel per font unit, without grid fitting), in a budgeted interpreter; a failing program falls back to the plain outline with a diagnostic. All other fonts are unchanged and never execute font code. See
docs/pdf-fonts.md.
1.1.0 - 2026-10-23
Added
- Appearance generation. An annotation with no usable normal appearance, and every text or choice field of a form that sets
/NeedAppearances true, is now drawn from its own data instead of being skipped: widgets (text, multi-line, comb, password, combo box, list box, check box, radio button, push button, with/MKand/BSborders and rotation) and the Square, Circle, Line, Polygon, PolyLine, Ink, Highlight, Underline, StrikeOut, Squiggly, FreeText and Text annotations. Seedocs/appearance-generation.md. PdfAnnotation.GenerateAppearance()returns the synthesised appearance as data (PdfGeneratedAppearance: form dictionary, content bytes, rectangle) without touching the document.PdfInterpretOptions.GenerateAppearances(default true); set it to false for the 1.0 behaviour. Each generated appearance is recorded as an Info diagnostic.PdfLimits.MaxAppearanceTextandPdfLimits.MaxAppearanceItemsbound the text and the item counts of a generated appearance.
Changed
- Pages whose annotations lacked an appearance now interpret to more content than before; pages with stored appearances (and no
/NeedAppearances) are unchanged. Text extraction is unchanged.
1.0.1 - 2026-10-23
Fixed
- The package README's links to documentation, benchmarks, licenses and notices are absolute, so they work on the nuget.org package page.
1.0.0 - 2026-10-23
First release. Two packages, RecursiveAI.OnePdf (the library, net10.0, no dependencies beyond .NET itself) and RecursiveAI.OnePdf.Fonts (substitute fonts for the standard 14), tested on Windows x64, Linux x64 and arm64, and macOS arm64, with NativeAOT and trimming supported.
Added
- Reading: cross-reference tables and streams, object streams, hybrid and incremental files, and a documented repair chain for damaged files (trust the xref, fix offsets and lengths by scanning, rebuild by scanning for object headers). Repairs are reported as diagnostics. Lenient by default,
PdfOpenOptions.Strictfor errors. - Filters: Flate, LZW, ASCIIHex, ASCII85, RunLength, PNG and TIFF predictors, CCITT fax and Crypt; DCT, JPX and JBIG2 through the
IPdfImageDecoderregistry. - Encryption: the standard security handler, revisions 2 to 6 (RC4 40/128, AES-128, AES-256), with user or owner password.
- Document model: public COS objects and typed wrappers for pages, Info and XMP metadata, outlines, page labels, links, annotations, embedded files, AcroForm field values and permissions. Objects parse lazily into a byte-budgeted cache; any number of threads can read one document.
- Fonts: Type 1, CFF and CID-keyed CFF, TrueType, OpenType-CFF and Type 3, encodings and
/Differences, ToUnicode, predefined CMaps for the Adobe CJK collections, the standard 14 metrics and a font-provider interface. - Text extraction: glyphs, words, lines and blocks in reading order (structure tree for tagged files, geometry otherwise), right-to-left text, vertical text, three plain-text modes and a versioned JSON form.
- Content interpreter:
page.Interpret(device)drivesIPdfDevicewith paths, glyph runs, images, shadings, patterns, transparency groups, soft masks, optional content and annotation appearances;PdfDisplayListrecords and replays calls, with a versioned text format. - Limits on by default (
PdfLimits) and fuzzed parsers, filters and font readers; nounsafecode. OnePDF never runs code or touches the network. RecursiveAI.OnePdf.Fonts: Liberation and Noto substitute outlines under the SIL Open Font License 1.1, registered withFontsPackage.Register().