# onepdf docs, all pages (OnePDF 1.11.0) Every page of https://recursiveai.org/onepdf/docs/ and https://recursiveai.org/onepdf/cookbook/ as markdown, getting started first. Index: https://recursiveai.org/onepdf/llms.txt # Getting started with OnePDF Install OnePDF for .NET 10, read text with word positions, edit pages, fill a form, sign a PDF, and generate an invoice with OnePdf.Layout. OnePDF is a PDF library written in C# with no dependency beyond .NET itself. Every sample on this page was compiled and run against the 1.11.0 packages from NuGet. ## Install OnePDF targets .NET 10 (`net10.0`), so you need the .NET 10 SDK. ```sh dotnet new console -o PdfStart cd PdfStart dotnet add package RecursiveAI.OnePdf --version 1.11.0 dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0 # only for generating documents ``` The namespace is `OnePdf`. The core package has no dependencies: no native binaries and no other NuGet packages. It contains no `unsafe` code, and NativeAOT and trimming are supported. The license is MIT or Apache-2.0, at your choice. The samples read and write files in the current directory. Put your own PDFs there under the names the code uses. ## Read the text ```csharp using OnePdf; using OnePdf.Text; using PdfDocument doc = PdfDocument.Open("report.pdf"); Console.WriteLine($"{doc.PageCount} pages, PDF {doc.Version}, title: {doc.Info.Title}"); // The text of every page, in reading order. IReadOnlyList pages = doc.ExtractText(); Console.WriteLine(pages[0].Text); // Words with their boxes, in PDF points with the origin at the lower left of the page. PdfPageText first = doc.Pages[0].ExtractText(); foreach (PdfTextWord word in first.Words.Take(5)) Console.WriteLine($"{word.Text,-10} left={word.Bounds.Left:F1} bottom={word.Bounds.Bottom:F1} width={word.Bounds.Width:F1}"); ``` The start of the output, for a six-page report with a two-column section: ```text 6 pages, PDF 1.7, title: Harbor trial report Harbor trial report Week 1 Prepared by Jane Doe, account 4471-2290. The harbor trial ran for six weeks. Each crew logged its ... Left column: crews on the north quay. Left column: crews on the north quay. Left column: crews on the ... Right column: crews on the south quay. Right column: ... Harbor left=54.0 bottom=727.7 width=27.5 trial left=84.0 bottom=727.7 width=14.5 ``` - `PdfDocument` holds the file open and reads it as needed, so dispose it. It opens a path, a `Stream` or a `ReadOnlyMemory`. Page indexes are zero-based. - `doc.ExtractText()` reads the pages in parallel. Reading order comes from the structure tree when the file is tagged, and from the geometry of the page otherwise, so the two columns above come out one after the other instead of line by line. Right-to-left text comes out in logical order. - `Text` is one of three plain-text modes: `ReadingOrder` (the default), `ContentOrder`, and `Layout`, which keeps the arrangement of the page like `pdftotext -layout`. See [Extract text from a PDF](https://recursiveai.org/onepdf/cookbook/extract-text/). - A word’s `Bounds` is in points (1/72 inch), with the origin at the lower left of the page and y going up, after the page’s rotation is applied. `Blocks` hold `Lines`, which hold `Words`. - OnePDF does no OCR. A scanned page without a text layer gives no text. ## Merge, split, rotate and delete pages ```csharp using OnePdf; // Merge two files into a new document. using (PdfDocument a = PdfDocument.Open("chapter-1.pdf")) using (PdfDocument b = PdfDocument.Open("chapter-2.pdf")) using (PdfDocument merged = PdfDocument.Merge([a, b])) { merged.SetInfo("Title", "Chapters 1 and 2"); merged.Save("chapters.pdf"); } // Rotate and delete pages of an existing file. Page indexes are zero-based. using (PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true })) { doc.RotatePage(0, 90); // clockwise, added to the current rotation doc.RemovePages([4, 5]); // the last two pages doc.Save("report-edited.pdf"); } // Split into files of two pages each. using (PdfDocument doc = PdfDocument.Open("report.pdf")) { int n = 1; foreach (PdfDocument part in PdfDocument.Split(doc, pagesPerPart: 2)) { part.Save($"report-part-{n++}.pdf"); part.Dispose(); } } ``` - Documents open read-only. Pass `AllowEditing = true` to change one. - `Merge` and `Split` return new documents held in memory. They don’t refer to their sources, so the sources can be disposed straight away. Bookmarks, links, form fields and the tagged structure of the copied pages come along. `Merge` copies no title or other metadata, which is why the sample sets one. - After a page operation, read `doc.Pages` again. `PdfPage` objects taken before it are out of date. - `Save` writes a clean new file. `SaveIncremental` appends only the changes to the original bytes. In this run `chapters.pdf` has 4 pages and all four bookmarks of its sources, `report-edited.pdf` has 4 pages with the first turned 90 degrees, and the report became three files of two pages. ## Fill a form ```csharp using OnePdf; using PdfDocument doc = PdfDocument.Open("of345.pdf", new PdfOpenOptions { AllowEditing = true }); PdfAcroForm form = doc.Form ?? throw new InvalidOperationException("The file has no form."); foreach (PdfFormField field in form.TerminalFields.Take(6)) Console.WriteLine($"{field.FullName} ({field.Kind})"); form.SetValue("name", "Ada Lovelace"); form.SetValue("title", "Analyst"); form.SetChecked("xyes1", true); doc.Save("of345-filled.pdf"); ``` ```text name (Text) dob (Text) title (Text) homeaddr (Text) agcy (Text) xyes1 (CheckBox) ``` The form here is OF 345, a US government form. Fields are addressed by their fully qualified names, which `TerminalFields` lists. Every value is checked before it is stored: an unknown field, a value that isn’t one of a list’s options, or text longer than the field allows throws `PdfFormValueException` and changes nothing. OnePDF also draws each filled field’s appearance, so the value shows in every viewer, not only in viewers that redraw fields themselves. Field scripts never run, so calculated fields aren’t recomputed. XFA form data is read as raw XML and never filled. To fill a form and then flatten it into the page, see [Fill and flatten form fields](https://recursiveai.org/onepdf/cookbook/fill-flatten-forms/). ## Sign and verify ```csharp using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using OnePdf; // A self-signed certificate for trying this out. In production, load your own certificate with its private key. using RSA key = RSA.Create(3072); var request = new CertificateRequest("CN=Example Signer", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation, critical: true)); using X509Certificate2 certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1)); using (PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true })) { doc.Sign("report-signed.pdf", PdfSigningCredential.FromCertificate(certificate), new PdfSigningOptions { FieldName = "Approval", Reason = "Approved for release", PageIndex = 5, Rectangle = new PdfRectangle(54, 400, 254, 460), // omit for an invisible signature }); } using (PdfDocument signed = PdfDocument.Open("report-signed.pdf")) { PdfSignatureReport report = signed.VerifySignatures(new PdfSignatureVerifyOptions { TrustAnchors = [certificate] }); foreach (PdfSignatureVerification v in report.Signatures) Console.WriteLine($"{v.Signature.FieldName}: valid={v.IsValid}, chain={v.Chain}, coverage={v.Coverage}"); } ``` ```text Approval: valid=True, chain=Trusted, coverage=WholeDocument ``` - Signing is always an incremental update, so the original bytes stay an exact prefix of the output and earlier signatures stay valid. - The default format is PAdES baseline B-B (`ETSI.CAdES.detached`). Give `PdfSigningOptions.Timestamp` a callback that posts the request to your time-stamp authority and the signature becomes B-T. OnePDF doesn’t write long-term validation data (PAdES B-LT and B-LTA). - Verification never touches the network. A chain counts as trusted only when it leads to a certificate in `TrustAnchors` (or to the system store, with `UseSystemTrustStore`). The report also lists every change made after each signature. In production, load your certificate with `X509CertificateLoader.LoadPkcs12FromFile`, or keep the key in a hardware module and sign through `PdfSigningCredential.FromExternalSigner`. More in [Sign a PDF and verify its signatures](https://recursiveai.org/onepdf/cookbook/sign-and-verify/). ## Generate a document `RecursiveAI.OnePdf.Layout` builds new documents from code. This one is a one-page invoice: ```csharp using System.Globalization; using OnePdf.Layout; var us = CultureInfo.GetCultureInfo("en-US"); var lines = new (string Item, int Quantity, decimal Price)[] { ("Design review", 2, 450.00m), ("Implementation, hours", 12, 120.00m), ("Hosting, October", 1, 80.00m), }; decimal total = lines.Sum(l => l.Quantity * l.Price); LayoutDocument.Compose(doc => { doc.Title("Invoice 2026-104").Language("en-US"); doc.Page(page => { page.Size(PageSize.Letter); page.Margin(54); page.Content().Column(col => { col.Spacing(14); col.Item().Row(row => { row.RelativeItem().Heading(1).Text("Invoice").FontSize(26).Bold(); row.RelativeItem().AlignEnd().Text("No. 2026-104\nOctober 26, 2026"); }); col.Item().Text("Bill to: Example Co., 100 Main Street, Springfield"); col.Item().Table(table => { table.Columns(c => { c.Relative(3); c.Constant(60); c.Constant(90); c.Constant(90); }); table.CellPadding(5); table.CellBorder(0.5, Colors.LightGrey); table.HeaderRow(r => { r.Cell().Text("Item").Bold(); r.Cell().AlignEnd().Text("Qty").Bold(); r.Cell().AlignEnd().Text("Price").Bold(); r.Cell().AlignEnd().Text("Amount").Bold(); }); foreach (var l in lines) { table.Row(r => { r.Cell().Text(l.Item); r.Cell().AlignEnd().Text(l.Quantity.ToString(us)); r.Cell().AlignEnd().Text(l.Price.ToString("C", us)); r.Cell().AlignEnd().Text((l.Quantity * l.Price).ToString("C", us)); }); } table.FooterRow(r => { r.Cell().Span(3).AlignEnd().Text("Total").Bold(); r.Cell().AlignEnd().Text(total.ToString("C", us)).Bold(); }); }); }); page.Footer().AlignCenter().Text("Payment due within 30 days.").FontSize(9); }); }).Save("invoice.pdf"); ``` `pdftotext -layout` reads the result back as: ```text No. 2026-104 Invoice October 26, 2026 Bill to: Example Co., 100 Main Street, Springfield Item Qty Price Amount Design review 2 $450.00 $900.00 Implementation, hours 12 $120.00 $1,440.00 Hosting, October 1 $80.00 $80.00 Total $2,420.00 ``` - Pages are described with nested delegates. Nothing is laid out until `Save`, which writes the file page by page. - Text is shaped (kerning, ligatures, Arabic, Indic and other complex scripts) and set in Liberation Sans by default, embedded as a subset. System fonts are never used unless you add them. - Output is tagged by default: the heading is an `H1` and the table’s header cells are `TH`. That is what makes a [PDF/UA save](https://recursiveai.org/onepdf/cookbook/create-pdf-ua/) possible. - Header and footer rows repeat on every page a table reaches. See [Generate a PDF with tables](https://recursiveai.org/onepdf/cookbook/generate-tables/). ## The packages All four packages share one version number. | Package | What it is | License | |---|---|---| | `RecursiveAI.OnePdf` | The library: open and repair, decrypt, extract text, edit pages, fill forms, draw on pages, redact, sign and verify, and save as PDF/A or PDF/UA. Namespace `OnePdf`. | MIT or Apache-2.0 | | `RecursiveAI.OnePdf.Layout` | Generates documents from code: shaped text, tables, lists, images, SVG, headers and footers, page numbers, tagged output. Namespace `OnePdf.Layout`. Depends on `RecursiveAI.OnePdf`, `RecursiveAI.OnePdf.Fonts` and `RecursiveAI.OneFont`. | MIT or Apache-2.0 | | `RecursiveAI.OnePdf.Fonts` | Fonts for documents that use the standard 14 fonts without embedding them: Liberation Sans, Serif and Mono, Noto Sans Math and Noto Sans Symbols. Layout uses them as its default fonts, and PDF/A and PDF/UA saves embed them for standard fonts OnePDF added. Namespace `OnePdf.Fonts`. | MIT or Apache-2.0, fonts under the SIL Open Font License | | `RecursiveAI.OnePdf.Hyphenation` | Hyphenation patterns for Layout in 26 language variants, loaded on first use. Opt in with `Hyphenator(Hyphenators.Auto())`. Namespace `OnePdf.Hyphenation`. | MIT or Apache-2.0, with the patterns' own MIT, BSD-3-Clause and Unlicense terms | ```sh dotnet add package RecursiveAI.OnePdf.Fonts --version 1.11.0 dotnet add package RecursiveAI.OnePdf.Hyphenation --version 1.11.0 ``` Layout brings in the Fonts package itself. Files you open that use the standard fonts without embedding them get their outlines from the package once you call `OnePdf.Fonts.FontsPackage.Register()` at startup, before opening documents. ## What OnePDF doesn’t do - **HTML to PDF.** There is no HTML engine. Build documents with OnePdf.Layout instead. - **Rendering pages to images.** OnePDF turns a page into drawing calls for a device you supply. To get PNG or JPEG pages, use OneImage.Pdf: see [PDF pages as images](https://recursiveai.org/oneimage/docs/pdf/). - **OCR.** Scanned pages without a text layer give no text. ## Where next Cookbook recipes, each with a complete program: - [Extract text from a PDF](https://recursiveai.org/onepdf/cookbook/extract-text/) and [get words with their positions](https://recursiveai.org/onepdf/cookbook/word-positions/) - [Merge PDFs](https://recursiveai.org/onepdf/cookbook/merge-pdfs/), [split a PDF](https://recursiveai.org/onepdf/cookbook/split-pdf/), [rotate, delete and reorder pages](https://recursiveai.org/onepdf/cookbook/rotate-delete-pages/) - [Fill and flatten form fields](https://recursiveai.org/onepdf/cookbook/fill-flatten-forms/), [add an image or a watermark](https://recursiveai.org/onepdf/cookbook/add-image-watermark/), [redact text](https://recursiveai.org/onepdf/cookbook/redact/) - [Sign a PDF and verify its signatures](https://recursiveai.org/onepdf/cookbook/sign-and-verify/), [decrypt or encrypt a PDF](https://recursiveai.org/onepdf/cookbook/decrypt-pdf/), [repair a damaged PDF](https://recursiveai.org/onepdf/cookbook/repair-damaged-pdf/) - [Create PDF/A](https://recursiveai.org/onepdf/cookbook/create-pdf-a/), [create PDF/UA](https://recursiveai.org/onepdf/cookbook/create-pdf-ua/), [generate a PDF with tables](https://recursiveai.org/onepdf/cookbook/generate-tables/), [Arabic and Hindi text](https://recursiveai.org/onepdf/cookbook/arabic-hindi-text/) Articles: - [Choosing a C# PDF library](https://recursiveai.org/onepdf/articles/csharp-pdf-library/): license, price and native code compared - [Extract text from PDF in C#](https://recursiveai.org/onepdf/articles/extract-text-from-pdf-csharp/) and [create PDF invoices and reports in C#](https://recursiveai.org/onepdf/articles/create-pdf-csharp/) - [Moving from iText](https://recursiveai.org/onepdf/articles/migrate-from-itext/): the same five jobs side by side - Full migration guides from [PDFsharp and MigraDoc](https://recursiveai.org/onepdf/articles/migrate-from-pdfsharp/) and [PdfPig](https://recursiveai.org/onepdf/articles/migrate-from-pdfpig/), and [extracting images from a PDF](https://recursiveai.org/onepdf/articles/extract-images-from-pdf-csharp/) The reference docs: - [Text layout and reading order](https://recursiveai.org/onepdf/docs/text-layout/) and the [JSON form of extracted text](https://recursiveai.org/onepdf/docs/extraction-json/) - [Writing](https://recursiveai.org/onepdf/docs/writing/), [page operations](https://recursiveai.org/onepdf/docs/page-operations/), [forms](https://recursiveai.org/onepdf/docs/forms/), [content creation](https://recursiveai.org/onepdf/docs/content-creation/), [redaction](https://recursiveai.org/onepdf/docs/redaction/) - [Signatures](https://recursiveai.org/onepdf/docs/signatures/), [repair rules](https://recursiveai.org/onepdf/docs/repair/), [PDF/A](https://recursiveai.org/onepdf/docs/pdfa/), [PDF/UA](https://recursiveai.org/onepdf/docs/pdfua/), [tagging existing files](https://recursiveai.org/onepdf/docs/tagging/) - [Layout](https://recursiveai.org/onepdf/docs/layout/), [layout text](https://recursiveai.org/onepdf/docs/layout-text/), [SVG in layout](https://recursiveai.org/onepdf/docs/layout-svg/) - Moving from another library: [PdfPig](https://recursiveai.org/onepdf/docs/migrating-from-pdfpig/), [PDFsharp](https://recursiveai.org/onepdf/docs/migrating-from-pdfsharp/), [QuestPDF](https://recursiveai.org/onepdf/docs/migrating-from-questpdf/) # Text layout and reading order How glyphs become words, lines and blocks, and the parameters that decide it. The code is in `src/OnePdf/Text/Extraction/`; every number below is a constant in `LayoutParameters`. Distances are in em of the glyphs concerned, so the analysis does not depend on the page scale. The analysis is deterministic: no randomness and no dependence on thread count; the same glyphs in the same order give the same result. ## Input A producer supplies, per page, glyphs in content order in *upright page space* (points, y up, `/Rotate` applied). Each glyph has its Unicode text, a matrix from em space to the page, an advance, ascent and descent, a vertical-writing flag, fill colour, render mode, visible/clipped flags, a marked-content id and, for `/ActualText` spans, a span number plus the string on the span's first glyph. See `PdfTextGlyph` for the glyph box definition. ## The producer `PdfTextExtractor` interprets each page with `GlyphOutlines` off and `Annotations` none, into a device that turns every shown character into a glyph: - The matrix is the glyph matrix of the run (font size, horizontal scale, rise, text matrix, CTM) followed by the flip from output space (y down) to upright page space. In vertical writing the box hangs from the text position (origin 1), which is the glyph origin plus the position vector v (ISO 32000-2 9.7.4.3); `Advance` is |w1|. - Text is the font's Unicode, or U+FFFD when no source knew it. Glyphs with no text, no glyph and no width are dropped, as are glyphs whose font size is zero. - `Ascent` and `Descent` keep their defaults (0.9, -0.2): descriptor values are too often wrong to steer line formation. - Colour is 0xRRGGBB for gray, RGB and CMYK (naive conversion) and spaces with the same component count; -1 for patterns and Lab. `IsVisible` is false in render modes 3 and 7 and when the opacity of everything the mode paints is 0. - `IsClipped` is set when the glyph box misses the intersection of the clip paths' bounding boxes (a conservative test: a rotated or curved clip never removes more than it should). Text clips do not narrow it. - `MarkedContentId` is the innermost `/MCID`. The outermost marked-content sequence with `/ActualText` forms a span; its first glyph carries the string. - Text inside annotation appearances, soft masks and Type 3 glyph procedures is not page text: a Type 3 run already carries the text. ## Clean-up (before layout) 1. Glyphs with a non-finite matrix or metrics are dropped. 2. Unless `IncludeClippedAndOffPage`: glyphs the clip removes (`IsClipped`) and glyphs whose centre is more than 1 pt outside the page are dropped. 3. `/ActualText`: in a span that has the string, the first glyph carries it and the rest carry empty text (they keep their geometry). 4. Ligatures U+FB00–U+FB06 are replaced by their letters (`DecomposeLigatures`). Then presentation forms and compatibility characters are folded to the characters they stand for (`FoldCompatibilityForms`): Kangxi and CJK radicals (U+2E80–2FDF), CJK compatibility ideographs (U+F900–FAFF, U+2F800–2FA1F), Hebrew presentation forms (U+FB1D–FB4F) and Arabic presentation forms (U+FB50–FDFF, U+FE70–FEFF). The table (`FoldData.g.cs`) holds the full compatibility decomposition from Unicode 17.0.0 `UnicodeData.txt`, generated by `tools/fold/generate.py` because `string.Normalize` does nothing when globalization is invariant. A decomposition that begins with a space before a combining mark (the isolated Arabic diacritics) loses the space. 5. Unpaired surrogates become U+FFFD. 6. Duplicates (`CollapseDuplicates`): a glyph with the same text, direction and size (within 5%) as an earlier one, whose origin is within 0.1 em on both axes, is dropped (fake bold, drop shadows). Candidate neighbours are looked up by x; at most 32 are compared. ## Reading direction Each glyph's reading direction is its baseline direction (the matrix x axis; for vertical writing the downward glyph axis), snapped to the nearest of the four axis directions. Glyphs are laid out per direction: the direction with most glyphs first, then the others (right, up, left, down order). All geometry below is done in a frame where *x runs along the text* and *y towards the next line*, so rotated pages and vertical writing use the same code. Text at other angles is analyzed by its nearest axis direction. **Ideographs stacked by a horizontal producer.** Some producers (browsers printing `vertical-rl` text) set each upright ideograph with its own horizontal matrix, so the glyphs look like horizontal text one character wide. Two successive glyphs in content order, both ideographs, kana or hangul, form a *downward step* when the second origin is at most 0.35 em to either side of the first and 0.4 to 2 em below it (an *across step* is the same turned 90°). Once a page has at least 2 downward steps, every horizontal ideograph that takes part in a downward step and in no across step is read in the downward direction: columns, right to left. ## Lines Glyphs sorted by baseline join the current line while their baseline is within 0.3 × the smaller size of the line's mean baseline. A smaller run joins a neighbouring line (superscripts and subscripts) when it is a script of one of that line's glyphs: some non-space glyph of the run is at most 0.85 × the size of a non-space glyph of the line, its baseline is within 0.7 em (of the larger size) of that glyph's and it lies within 0.4 em (of its own size) horizontally of it. The test is made on glyph pairs, so two real lines that merely sit near a large glyph stay apart. Space glyphs never decide this: producers park them anywhere (often at the start of the next line). ## Fragments A line is cut into fragments where two successive non-space glyphs are more than 1.25 em apart (mean of their sizes). Fragments are the atoms of block segmentation; a cell of a table or a column of a multi-column page is made of fragments. A wider gap is still a word space, not a cut, when space glyphs span it end to end (holes of at most 0.1 em): a font whose space is 1.25 em wide, or a justified line, would otherwise split into cells. ## Blocks (XY-cut on fragments) A region (initially the whole page, per direction) is split recursively, with an explicit stack: 1. A region with one fragment, or fragments of one line only, is a block. 2. **Vertical cut:** if an empty stripe at least 1.25 em (of the region's median fragment size) wide separates the fragments' extents along the line, split at every such stripe. Columns are ordered left to right, or right to left when more right-to-left than left-to-right letters occupy the region. Before cutting, if the region looks like a **table** it becomes one block instead: at least 3 rows (distinct lines), at least 60% of the rows have fragments in more than one column, and the median fragment width is at most 10 em. A table's lines are its rows; cells are separated by a tab. 3. **Horizontal cut:** otherwise, if an empty band at least 0.5 em tall separates the fragments, split at every such band, top to bottom. 4. **Mixed cut:** otherwise, if lines with a gutter (several fragments) alternate with single-fragment lines, split at the changes (a heading set tight above two columns). 5. Otherwise the region is a block. **Column bands.** When the horizontal cut finds bands (step 3), successive bands are first joined if each is made of columns with at least one multi-line column (an empty stripe of at least 1.25 em, with two or more lines in a column on its side) and the stripes of the two bands overlap by at least 1.25 em. This is the case where something spanning both columns (a full-width note, a caption) defeats the vertical cut, and the columns' paragraph gaps happen to sit at the same heights: cutting at them would read the columns band by band. The joined bands go back on the stack and are cut into columns by step 2. Bands of single lines (rows of a form or table) are never joined this way. If every band joins, the region is left uncut by step 3 and goes on to step 4. The vertical cut is tried first so columns whose paragraphs happen to share gaps are not cut across; a full-width title is separated by the horizontal cut because it blocks every vertical stripe. ## Words Within a fragment, glyphs sorted along the line start a new word at a space glyph or when the gap exceeds 0.15 em (0.6 em next to ideographs, kana and hangul, which are written without spaces). The first word of each fragment after the first starts a cell. A space glyph drawn over the glyph before it (by more than half of its own width; some producers park the trailing space of a line at its start) neither ends the word nor adds text. ## Bidirectional text (UAX #9) Lines are read in visual order (by x) and put into logical order by running the Unicode Bidirectional Algorithm (UAX #9, Unicode 17 data, rules X1–X10, W1–W7, N0–N2, I1–I2, L1, L2 in `Bidi.cs`) on the visual string and reordering it: applying the reordering to text that a layout engine already reordered gives back the logical text. The paragraph direction of a block is right-to-left when its right-to-left letters (R, AL) outnumber its left-to-right letters. Words and the words of a line come out in logical order. Mirrored-bracket glyph substitution (rule L4) is not undone; brackets are taken as extracted. Conformance: all 91,707 cases of `BidiCharacterTest.txt` (Unicode 17.0.0) pass (set `ONEPDF_UNICODE` to its directory to run them). ## Structure tree When the document is tagged (`/StructTreeRoot`), the tree is walked depth first through `/K` (explicit stack, each indirect element once, at most 2²² nodes). For each page, marked-content ids are ranked in tree order; the ids under one structure element form a group. Inline-level elements (`Span`, `Link`, `Quote`, `Code`, `Em`, `Strong`, `Sub`, `Sup`, `Note`, `Reference`, `Lbl`, ruby and the like) and the purely grouping `NonStruct` and `Private` share the group of the element around them, so a style change or a link does not cut a line out of its paragraph. A page uses the tree when **at least 90%** of its visible, non-blank glyphs carry an id the tree lists; otherwise it falls back to geometry and records an informational diagnostic. With the tree, groups are laid out separately and emitted in tree order. Glyphs the tree does not list (artifacts such as running heads) are laid out on their own and placed above the content when their centre is in the top 24% of the page, otherwise below it. ## Headers and footers Whole-document extraction flags a block as header (or footer) when it lies entirely in the top (bottom) 12% of the page and the same text (digits replaced by `#`, lower-cased, white space collapsed) appears in that band on at least half of the pages and at least two. Flagged blocks keep their place in the reading order and `Role`; `ExcludeHeadersAndFooters` omits them from the reading-order text. ## Plain text modes - **ReadingOrder:** blocks separated by a blank line, lines by a line feed, cells by a tab. With `JoinHyphenatedWords`, a line ending in a hyphen (`-`, U+00AD, U+2010) after a letter, followed by a line starting with a lowercase letter, is joined to it without the hyphen. - **ContentOrder:** glyphs in producer order. A new line starts when the direction or baseline changes by more than 0.5 em or the position jumps back by more than 3 em; a space is inserted for a gap over the word-gap threshold. - **Layout:** lines are formed over the whole page by baseline (no block segmentation), words are put on a grid whose cell is 0.5 × the median glyph size wide (column = round((x − left margin) / cell), at least one blank after the previous word, at most 400 columns); a gap between baselines over 1.6 line pitches adds up to 3 blank lines. ## Implementation notes The analysis keeps its working data in arrays rented from the shared pool (items, fragments, sort keys, word runs) and builds strings, word and line objects only for the result; regions of the XY-cut are runs of one index array that a split rewrites in place. None of this is visible in the output, and none of the thresholds depend on it. Where a sort could leave equal keys in either order (the per-line sort by x, the baseline sort, the line order inside a block), the same introsort over the same key sequence is used as before, so ties resolve identically; the duplicate search uses a total order (x, then stream position), so any correct sort gives the same visit order. A page's analysis uses no shared mutable state: pages of a document may be analyzed on any number of threads and the results do not depend on the degree. ## Not done yet Table cell spans, justified-text hyphen detection by font, non-axis text angles, mirrored-bracket repair and an `/Artifact`-aware header detector (the tagged-page rule above is the only artifact handling). Thresholds are untuned beyond the synthetic and generated test sets. # Extraction JSON The JSON form of text extraction: glyphs, words, lines and blocks per page. It is written by `PdfExtractionJson` (`ToJson`, `Write`) and is meant to be compared byte for byte across implementations, so it is versioned and fully deterministic. - **Format:** `onepdf.extraction`, **version 1**. A reader must check both members. Additive changes (new members) keep the version; renaming, removing or changing the meaning of a member raises it. - **Encoding:** UTF-8, no byte-order mark. Strings are escaped as JSON requires and nothing more (non-ASCII text is written as it is). Unpaired surrogates in glyph text have been replaced by U+FFFD before writing. - **Numbers:** coordinates and sizes are rounded to three decimals (ties to even), written in invariant form without an exponent and without trailing zeros; negative zero is written `0`. Integers are plain. - **Member order** is the order below. Whitespace is insignificant (the writer can indent). ## Coordinate space All coordinates are in *upright page space*: points, origin at the bottom-left of the page as a viewer shows it (page `/Rotate` and box already applied), y up. A box is `[left, bottom, right, top]`. ## Shape ``` { "format": "onepdf.extraction", "version": 1, "pageCount": , "pages": [ Page, ... ] } Page = { "index": , zero-based page index "width": , "height": , "usedStructureTree": , true when the structure tree decided the order "diagnostics": [ , ... ], notes about this page (for example a structure tree that covered too little) "text": , the page's reading-order plain text: blocks separated by "\n\n", lines by "\n", table cells by "\t" "blocks": [ Block, ... ], reading order "glyphs": [ Glyph, ... ] omitted when the writer is asked not to include glyphs } Block = { "role": "body" | "header" | "footer", "table": , a grid of short cells; lines are rows "bounds": [l, b, r, t], "lines": [ Line, ... ] } Line = { "direction": "ltr" | "rtl", base direction used to put the line in logical order "vertical": , vertical writing "bounds": [l, b, r, t], "cells": [ , ... ], indices into "words" where a cell (a run set apart by a wide gap) starts; first is 0 "text": , words joined by " ", cells by "\t" "words": [ Word, ... ] logical order (right to left on the page for right-to-left text) } Word = { "text": , logical order "bounds": [l, b, r, t], "visible": , false when every glyph is invisible "glyphs": [ , ... ] indices into the page's "glyphs", visual order along the reading direction } Glyph = { "text": , Unicode (after ligature decomposition and /ActualText substitution; may be empty) "font": | null, "fontId": , -1 when unknown "size": , em size in points "matrix": [a, b, c, d, e, f], glyph space (em) to page space "advance": , em units along the writing direction "bounds": [l, b, r, t], "vertical": , "color": "#rrggbb" | null, fill colour "renderMode": , 0 to 7 "visible": , false for invisible text (render mode 3, OCR layers) "mcid": marked-content id, -1 for none } ``` The glyphs listed are the ones that took part in extraction: clipped and off-page glyphs and duplicates (fake bold) are already gone unless the options kept them, and a glyph inside an `/ActualText` span carries the replacement text (first glyph) or empty text. # Page operations Reorder, delete, insert, rotate and duplicate pages of a document; copy pages from other documents; merge documents; split one into several. All of it works on an editable document (`PdfOpenOptions.AllowEditing`, see [writing.md](https://recursiveai.org/onepdf/docs/writing/)) and is written with `Save` or `SaveIncremental` like any other change. ```csharp using var doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true }); doc.MovePage(fromIndex: 0, toIndex: 4); // the cover goes to the back doc.RemovePages([2, 3]); // bookmarks, links and names that led there are repaired doc.RotatePage(1, 90); // clockwise, added to the page's current /Rotate doc.InsertBlankPage(0, 595, 842); using var appendix = PdfDocument.Open("appendix.pdf"); doc.ImportPages(appendix); // all pages of another document, appended doc.Save("out.pdf"); using var merged = PdfDocument.Merge([a, b, c]); // a new document foreach (PdfDocument part in PdfDocument.Split(big, pagesPerPart: 10)) { part.Save(...); part.Dispose(); } ``` Page indices are zero-based. Take `doc.Pages` again after an operation: `PdfPage` objects, outlines, the form and the other views built before it are out of date (`PageCount`, `Pages`, `Outline`, `Form`, `PageLabels`, `NamedDestinationNames` and `OptionalContentGroups` are refreshed). Changing a document is for one thread at a time. ## The calls | Call | What it does | |---|---| | `PdfDocument.CreateEmpty(options?)` | A new editable document with no pages (PDF 1.7, in memory). | | `InsertBlankPage(index, width, height)` | A page with an empty `/Resources` and the given `/MediaBox` (points). Returns the page. | | `RemovePage(index)`, `RemovePages(indexes)` | Deletes pages and repairs what pointed at them (below). | | `MovePage(from, to)` | `to` is the page's index afterwards. | | `ReorderPages(newOrder)` | `newOrder[i]` is the current index of the page that becomes page `i`; a permutation or `ArgumentException`. | | `RotatePage(index, degrees)`, `SetPageRotation(index, degrees)` | Relative and absolute `/Rotate`; multiples of 90, normalised to 0/90/180/270. | | `DuplicatePage(index)` | A copy right after the page, with its own annotations; content and resources are shared. | | `ImportPages(source, indexes?, insertAt = -1, options?, ct)` | Copies pages (null = all) from `source`, which may be any document, read-only or encrypted, or the document itself. | | `PdfDocument.Merge(sources, options?, ct)` | A new document holding all pages of the sources in order. Limits are the first source's. No `/Info`: call `SetInfo`. | | `PdfDocument.Split(source, ranges \| pagesPerPart, ct)` | New documents, one per `PdfPageRange(start, count)`. | `PdfImportOptions`: `ImportOutlines`, `ImportFormFields`, `ImportStructure`, all true by default. Merge and split results live in memory and do not refer to their sources, which may be disposed straight away. Encryption plays no part until `Save`: an encrypted source imports into a plain document, and `PdfSaveOptions.Encryption = Set` encrypts the result. ## What a copied page brings A page is copied by walking its object graph with an explicit work stack (no recursion on graph depth, cycles end the walk). Each object of the source is copied once however many pages use it; a later import from the same source into the same target reuses what the earlier one copied (not when the source is editable). Streams are copied as stored (filters and bytes untouched). - **Content and resources:** contents, fonts, images, colour spaces, patterns, shadings, XObjects, graphics states. - **Inherited attributes (ISO 32000-2 7.7.3.4):** `/Resources`, `/MediaBox`, `/CropBox` and `/Rotate` that the page got from the page tree are written into the copied page. The target's page tree is rebuilt as a balanced tree (at most 32 kids per node) with every page carrying its own values; a resource dictionary that many pages shared stays one shared object. - **Annotations** with their appearance streams; each imported page gets its own annotation objects (a page imported twice gets two sets). `/P` points at the new page. - **Links:** a link whose destination page was imported follows it; one whose destination was left behind keeps its rectangle but loses `/Dest` or its go-to action. Named destinations used by imported links are added to the target's name tree, renamed with a `_2`, `_3` suffix when the name is taken by a different place. - **Bookmarks:** the source's outline items that lead to an imported page are appended after the target's top-level items. An item whose own target was left behind but which has such items below it stays as a heading without a target. - **Form fields:** the widgets of imported pages come with the field tree above them (only the kids that came along), merged under the target's `/AcroForm` `/Fields`. A top-level name already used in the target gets `_2`, `_3`, … and an Info diagnostic says so. The source's `/DA` and `/Q` are put on the imported fields that lack their own; the source's default resources (`/DR`) are added to the target's, and a name that is taken by another object gets a free name (`Helv` becomes `Helv_2`) which is also rewritten in the imported default appearance strings. A signature field arrives without its value (it signs the bytes of another file). `/XFA` of the target is dropped, since it would no longer describe the fields. - **`/NeedAppearances`:** that flag belongs to a whole document, and setting it would redraw every field of the target. Instead the appearance of each text and choice field is generated from the source (as `PdfAnnotation.GenerateAppearance` does) and stored in the copied widget. Only if a widget cannot be given one does the target get the flag. - **Optional content:** the groups the copied content uses are added to the target's `/OCProperties` with their default visibility. - **Page labels:** each page keeps its label scheme (a source without labels counts as plain decimal numbers from 1). The target's `/PageLabels` is rewritten as the shortest list of ranges for the new order and is dropped when no page had labels. - **Structure (tagged PDF):** the structure tree is pruned to the elements that hold marked content of imported pages or annotations that came along, and appended as new top-level elements of the target's tree (created when needed, with `/MarkInfo`). `/Pg`, `/Obj`, `/StructParents` and `/StructParent` are mapped; role and class maps are merged (the target's entry wins on a clash). Pages copied within the document by `DuplicatePage` are not tagged. - **PDF version:** raised to the source's when that is higher. Not carried: article threads, thumbnails, document-level JavaScript and named-destination trees other than the used names, the source's metadata, output intents, signatures and permissions. `Merge` does not copy `/Info` or XMP either. ## What a deletion repairs `RemovePages` removes the pages from the page tree and then: - **Links** on the remaining pages that went to a deleted page lose `/Dest` or their go-to action (they stay as inert rectangles). - **Bookmarks** whose target was deleted are removed; one that has other bookmarks below it stays as a heading without a target. `/First`, `/Last`, `/Prev`, `/Next`, `/Parent` and `/Count` are rewritten (a closed item stays closed). - **Named destinations** (the catalog `/Dests` dictionary and the `/Names` `/Dests` tree, which is rewritten as one sorted node when anything goes) and the document's `/OpenAction` that led to a deleted page are dropped. - **Form fields** whose widgets were all on deleted pages are removed from the form. - **Page labels** are renumbered for the new order. - The **structure tree and the article threads** refer to pages from deep inside and would keep the deleted content in the file, so they are dropped, with an Info diagnostic. (Reordering and rotating keep them.) A full save writes only what the remaining pages reach: the content of a deleted page is not in the file. An incremental save keeps the old objects in the file, as any incremental save does. ## Limits and failures - The number of objects copied is bounded by the target's `PdfLimits.MaxObjects`, nesting of a single value by `MaxNestingDepth`; both throw `PdfLimitException`. A copy that fails while the pages are being copied (a limit, `OperationCanceledException`) leaves no trace: the object numbers are given back and the page list is unchanged. A failure later, while bookmarks or fields are linked into existing objects, leaves the page list unchanged but may leave unreferenced objects, which a full save drops. - Cyclic `/Parent` and `/Kids`, self-referencing annotations (`/Popup`, `/IRT`, `/Parent` pointing at themselves), looping action `/Next` chains, looping outlines and field parents all terminate; a reference to a page that was not imported becomes null. - A source page tree that the reader had to repair is copied as the reader sees it (`doc.Pages`). ## Known limits - A resource dictionary shared by all pages of a document (common in slide decks) is copied whole into every part of a split, because the pages reach all of it. Resources are not pruned by scanning the content streams. - Links between imported pages from *different* calls of `ImportPages` are not kept: destinations are mapped within one call. - Fields of the same name are renamed, not merged. # Writing Documents open read-only by default. A document opened with `PdfOpenOptions.AllowEditing = true` can be changed and saved, in full or as an incremental update. ```csharp using var doc = PdfDocument.Open("in.pdf", new PdfOpenOptions { AllowEditing = true }); doc.SetInfo("Title", "Quarterly report"); doc.Pages[0].Dictionary.SetEntry("Rotate", new PdfInteger(90)); doc.SaveIncremental(File.Create("out-update.pdf")); // original bytes + an update doc.Save("out-clean.pdf", new PdfSaveOptions { XrefFormat = PdfXrefFormat.Stream, CompressStreams = true }); ``` ## The mutable view - **Objects.** `PdfDictionary.SetEntry`/`RemoveEntry` (null removes), `PdfArray.Append`/`Insert`/`SetItem`/`RemoveAt`/`Clear`, `PdfStream.SetData` (unfiltered or Flate) and `SetStoredBytes` (bytes exactly as given; you keep `/Filter` in step). The simple types have public constructors (`new PdfName("X")`, `new PdfInteger(1)`, `PdfString.FromText("é")`, ...); `new PdfDictionary()` and `new PdfArray()` make containers that belong to no document until they are stored in one. - **Indirect objects.** `doc.AddObject(value)` gives a new object the next number and returns its `PdfReference`; `SetObject(number, value)` replaces an object's content; `RemoveObject(number)` frees it; `doc.CreateStream(data, compress)` makes a stream for the document. An object that already is indirect is stored *by reference* when you put it into a container, and a loose stream is added automatically. - **Metadata.** `SetInfo(key, text)` and `SetXmp(bytes)` create the Info dictionary or the metadata stream when missing and refresh `Info` and `Xmp`. - **Rules.** Values of another document are refused (`ArgumentException`: copy them first); objects of an earlier revision (`doc.Revisions[i].GetObject`) and objects of a read-only document are refused (`InvalidOperationException`). The null object is not stored: it removes. - **What is pinned.** An editable document keeps every object it has read, so a change cannot be lost when the object cache drops an entry; `ObjectCacheBytes` no longer bounds memory for parsed objects (decoded streams stay cached). A change marks the *indirect object it lives in* as changed, however deeply nested it is. - **Concurrency.** Any number of threads may read an editable document, as with a read-only one. Changing it is for one thread at a time and not concurrent with reading or saving. Views built from objects earlier (`Pages`, outlines, fonts) are not refreshed by later changes; take them again. `Info` and `Xmp` are. ## Full save `doc.Save(Stream | path, PdfSaveOptions?, CancellationToken)` writes a new file from the catalog and the Info dictionary: - Objects are walked once, numbered 1, 2, 3 … in the order they are met, and written when met. Unreachable objects, earlier revisions, linearization and hint data, free entries and the damage of a repaired file are gone. `/Length` is always written directly. Nothing but the queue of references still to write is kept in memory. Nesting is walked with an explicit stack and refused beyond `PdfLimits.MaxNestingDepth`. - **Cross-reference form.** `Table` (a classic `xref` table, no object streams), `Stream` (a cross-reference stream with the "up" PNG predictor and, unless `UseObjectStreams` is false, object streams of up to 100 objects or 1 MiB), or `Auto` (the newest revision's form). The catalog is always a plain object, and so are streams and the `/Encrypt` dictionary: readers that need the catalog before they have the key must not have to open an object stream first. - **Version.** The highest of the document's, `MinimumVersion` and what is needed: 1.5 for cross-reference streams, 1.6 for AES-128, 2.0 for AES-256. A header is rewritten with the binary comment line. - **Streams** keep their filters and bytes. `CompressStreams` Flate-compresses streams with no filter (not metadata, not when it would not shrink them). A `/Crypt` filter, which only named the decryption just undone, is dropped. - **`/ID`.** Kept as it is; a document without one gets two random strings. - **Repaired files** save as clean files; dangling references become `null`. - **Limits.** `PdfLimits.MaxObjects` bounds the output; a classic table beyond 10 digits of offset throws `PdfLimitException` (use `Stream`). A stream larger than `MaxDecodedStreamBytes` throws `PdfLimitException` as when reading it. A full save copies each stream as stored and writes `null` for a reference to an object that does not exist, so a damaged file stays as damaged as it was. A PDF/A or PDF/UA save does not: it refuses to write a file that has lost a part it could not read (see "A conformance save never drops content" in [pdfa.md](https://recursiveai.org/onepdf/docs/pdfa/)). ## Incremental save `doc.SaveIncremental(Stream, PdfSaveOptions?, CancellationToken)` copies the original file byte for byte, then appends the objects changed, added or removed since the document was opened (changed objects keep number and generation; removed ones become free entries chained onto the existing free list), a cross-reference section for them only, and a trailer with `/Prev` to the previous section (ISO 32000-2 7.5.6). It continues the newest revision's form (table or stream; a hybrid file gets a table). The update is encrypted like the document. `/ID`'s first string stays and the second changes. Offsets are written relative to the header when the file has junk in front of it, as the file itself does. `doc.Revisions` shows the old and new revisions. Each call writes the changes since *open*, on top of the original file. An update goes on top of the file as it is. A file that needed repair on open may read differently with an update behind it (a stream whose end was found by scanning can run on into the update), so use `Save` for such files. A file that was rebuilt by scanning has no section to chain to: `SaveIncremental` throws `PdfFormatException`; use `Save`. ## Encryption on write `PdfSaveOptions.Encryption`: `Keep` (default; the same key, passwords and dictionary, everything re-encrypted under the new numbers; a plain document stays plain), `Remove` (the document is already open, so access is already granted) or `Set` with `NewEncryption` (`PdfEncryptionOptions`: user and owner password, permission flags, `Aes128` = revision 4 or `Aes256` = revision 6, `EncryptMetadata`). An empty owner password means the user password. Reserved bits of `/P` are set as ISO 32000-2 Table 22 requires. RC4 is only ever written by keeping an RC4 file (a full save, or an incremental one). OnePDF records permission flags and does not enforce them. Output with AES is not byte-identical between runs (random IVs and salts). ## Not covered Page operations and copying pages between documents are in [page-operations.md](https://recursiveai.org/onepdf/docs/page-operations/); content creation is in [content-creation.md](https://recursiveai.org/onepdf/docs/content-creation/) and signing in [signatures.md](https://recursiveai.org/onepdf/docs/signatures/). Linearized output and public-key encryption are later work. # Content creation A document opened with `PdfOpenOptions.AllowEditing`, or made with `PdfDocument.CreateEmpty`, can have content drawn onto its pages. The same builder makes new pages, stamps existing ones and writes reusable forms. ```csharp using var doc = PdfDocument.CreateEmpty(); PdfPage page = doc.InsertBlankPage(0, 612, 792); PdfCreatedFont body = doc.AddFont(File.ReadAllBytes("LiberationSerif-Regular.ttf")); // embedded, subset on save PdfCreatedFont mono = doc.AddStandardFont("Courier"); // not embedded using (PdfPageContentBuilder b = page.BeginContent()) { b.SetFillRgb(0.1, 0.1, 0.5); b.Rectangle(72, 700, 468, 2); b.Fill(); b.SetFillGray(0); b.SetFont(body, 12); b.DrawTextBox("A paragraph that wraps inside a 468 pt wide box. Αβγ Привет", 72, 560, 468, 130, PdfTextAlignment.Justify); b.SetFont(mono, 9); b.DrawText("printed with PdfDocument.Save", 72, 540); b.Commit(); // nothing is written to the page before this } doc.AddTextStamp("DRAFT", doc.AddStandardFont("Helvetica-Bold"), 96, new PdfStampOptions { Opacity = 0.25, RotationDegrees = 45, Color = (1, 0, 0) }); doc.Save("out.pdf", new PdfSaveOptions { DocumentId = new byte[16] }); // reproducible bytes ``` ## Coordinates and numbers Content is written in the page's default user space: origin at the lower left of the `MediaBox` as stored, y up, 1 unit = 1/72 inch (times `/UserUnit`). `/Rotate` and the CropBox are not applied to what you draw, so a page rotated by 90 shows your content rotated too; the stamp helpers compensate for `/Rotate` so a stamp reads upright. Numbers are written rounded to four decimals in the invariant culture, with no fused multiply-add anywhere, so the same calls give the same bytes on every platform. Non-finite numbers are refused. ## Builders `PdfContentBuilder` is the drawing surface; `PdfPageContentBuilder` (from `page.BeginContent(placement)`) and `PdfFormContentBuilder` (from `doc.BeginForm(bbox)`) add `Commit()`. A builder collects content and writes it on `Commit`; disposing it first discards it. Open `Save()` levels are closed by `Commit`. - **Over or under.** `PdfContentPlacement.Overlay` (default) puts the new content after the page's content. The page's own content stays byte for byte as it was, but it is wrapped in `q` ... `Q`, with one extra `Q` per `q` it leaves open (found by a lexical scan), so a stray `cm`, colour, clip or unbalanced `q` cannot reach your content. `Underlay` puts yours first, in its own saved state. Both add a new content stream to the page's `/Contents` array; shared `q` and `Q` streams are created once per document. - **Resources.** Fonts, XObjects and ExtGStates are added to a page-owned copy of the resource dictionary when the page's was inherited or shared, so other pages are not touched. Names are `OpF1`, `OpIm1`, `OpFm1`, `OpGs1`, ... skipping any name already in use. - **Graphics.** `MoveTo`, `LineTo`, `CurveTo`, `Rectangle`, `Ellipse`, `ClosePath`; `Fill`, `Stroke`, `FillAndStroke` (nonzero or even-odd), `Clip`, `EndPath`; `SetLineWidth`, `SetLineCap`, `SetLineJoin`, `SetMiterLimit`, `SetDash`; `Save`/`Restore`, `Transform`, `Translate`, `Scale`, `Rotate` (multiples of 90 are exact). - **Colour and transparency.** DeviceGray, DeviceRGB and DeviceCMYK for fill and stroke (components clamped to 0..1); `SetFillOpacity`/`SetStrokeOpacity`/`SetOpacity` (`/ca`, `/CA`) and `SetBlendMode`, each an ExtGState shared by value within the builder. - **Images.** `doc.AddJpegImage` embeds the JPEG file as it is (`DCTDecode`; size and 1, 3 or 4 components read from the header; Adobe CMYK gets the inverting `/Decode`; progressive is fine; arithmetic-coded and lossless are refused). `doc.AddRawImage` takes 8-bit Gray, RGB or CMYK samples and an optional alpha plane (Flate, and a `/SMask`). `doc.AddPngImage` reads a PNG file (see [PNG images](#png-images)). `DrawImage(img, x, y, w, h)` places an image in the box in current coordinates; `DrawImage(PdfImage, ...)` draws again an image XObject of the same document that the interpreter reported. - **Forms.** `doc.BeginForm(bbox)` returns a builder; `Commit()` gives a `PdfFormXObject`, drawn any number of times, on any pages, with `DrawForm`, positioned by the transform stack. `DrawForm(PdfStream)` draws a Form XObject already in the document. - **Stamps.** `doc.AddTextStamp(text, font, size, options, pages)` and `doc.AddImageStamp(image, w, h, options, pages)` draw into one shared form and place it on each chosen page (default all), centred on the CropBox (or `CenterX`/`CenterY`), rotated, at an opacity and blend mode, over or under the content. ## Text `SetFont(font, size)`, `SetCharacterSpacing`, `SetWordSpacing`, `SetTextRenderMode`; text is painted with the fill colour (and stroke colour and line width in stroke modes). `DrawText(text, x, y)` places the start of the baseline, or give a full text matrix. `MeasureText` returns the width with the current font, size and spacing; `WrapText(text, width)` splits at spaces and `\n` and breaks a word wider than the line between characters; `DrawTextAligned` places one line in a span; `DrawTextBox` wraps into a box (first baseline one ascent below the top, `size * lineSpacing` apart) and returns the text that did not fit. Justified lines widen word gaps; the last line of a paragraph stays left aligned. `DrawText` and the text-box methods do no shaping: no kerning, ligatures, contextual forms, bidirectional reordering or complex-script layout. Characters are placed one after another in the order of the string, using the font's `cmap` and advance widths. Control characters are skipped. Text that needs shaping is drawn from glyphs a shaping engine has already chosen: see [Shaped glyph runs](#shaped-glyph-runs). ### Fonts - **Standard 14** (`AddStandardFont`): names and the usual aliases (`Arial`, `TimesNewRoman`, `CourierNew`, ...). Text is WinAnsi; Symbol and ZapfDingbats use their built-in encodings. Widths come from the Adobe metrics; viewers draw with their own copy of the font. A character outside the encoding is drawn as `?` and recorded in `doc.Diagnostics`. - **Embedded** (`AddFont(bytes, options)`): a `.ttf`, `.otf` or `.ttc` face (`PdfEmbedFontOptions.FaceIndex`). The font is written as a Type 0 font with `Identity-H`, a `CIDFontType2` (TrueType outlines) or `CIDFontType0` (CFF outlines) descendant, `/W` widths (rounded to whole thousandths of the size, the same numbers `MeasureText` uses) and a `/ToUnicode` map, so text extraction recovers the characters. Codes are assigned in order of first use (code 1, 2, ...) in a subset, so the same text always gives the same codes. - **Subset.** By default the font is cut down when the document is saved (so more text can still be added until then): the glyphs used, plus the components of composite glyphs, renumbered compactly, in a font named `ABCDEF+PostScriptName` (six capital letters derived from a hash of the font and glyph list; ISO 32000-2 9.6.4). TrueType subsets keep `head`, `hhea`, `maxp`, `hmtx`, `loca`, `glyf` and the hinting tables `cvt `, `fpgm`, `prep`; `cmap`, `name`, `post` and `OS/2` are dropped. CFF subsets are written as CID-keyed CFF (a name-keyed font becomes one FD) with a charset in which CID equals glyph index; charstrings are copied as they are, and subroutines the kept glyphs never call are emptied to a lone `return`. A glyph that uses the deprecated `endchar` accent form is refused. - **Whole font.** `Subset = false`, or a font whose `fsType` has the no-subsetting bit (0x0100), is embedded unchanged (CID = glyph index; for a CID-keyed CFF font, the charset CID). - **Licensing.** `fsType` restricted-license (0x0002 without 0x0004 or 0x0008) and bitmap-only (0x0200) fonts are refused with `PdfFontEmbeddingException`. `IgnoreEmbeddingRestrictions` embeds anyway; whether you may is your responsibility. - **Errors.** A damaged font gives `PdfFormatException`: an unknown sfnt version, missing tables, a `loca` that is not ascending or points past `glyf` (all checked in `AddFont`); a composite glyph that contains itself, nests deeper than 8, or names a missing component (found when the glyph is subset, at `Save`). Subsetting reads at most 65,535 glyphs and never allocates more than the font's own size times a small constant. A character with no glyph is drawn as `.notdef` (glyph 0) and recorded in `doc.Diagnostics`. ## PNG images `doc.AddPngImage(bytes)` (also on `PdfDocumentWriter`) reads a PNG (W3C PNG / ISO 15948) and returns a `PdfCreatedImage` that `DrawImage` places like a JPEG. It is a minimal reader, not a codec: it reads what a PDF image needs and writes nothing. | PNG | Result | |---|---| | Gray 1, 2, 4, 8, 16 bits; RGB 8, 16; palette 1, 2, 4, 8 | **Passed through.** The IDAT payload goes into a `FlateDecode` stream with `/Predictor 15` (ISO 32000-2 7.4.4.4) byte for byte; nothing is decoded or recompressed. Palette images become `/Indexed` over `DeviceRGB`, padded to 2^bits entries. | | Adam7 interlaced | Decoded with the library's inflater, written as plain packed rows, Flate-compressed on save. | | Gray+alpha, RGB+alpha (8, 16) | Decoded; colour samples as above, alpha as a `/SMask` of the same bit depth. | | `tRNS` on gray or RGB | A colour-key `/Mask`; the image data is still passed through. | | `tRNS` on a palette | A colour-key `/Mask` when the transparent entries are fully transparent and form one run; otherwise decoded, with an 8-bit `/SMask` built from the indices. | | `iCCP` with an RGB or gray profile that matches the image | `ICCBased` colour space (the compressed profile is stored as it is). A profile that does not match, or is damaged, is dropped with a warning. | `gAMA`, `cHRM`, `sRGB`, `sBIT`, text chunks and the other ancillary chunks are ignored, and samples are used as stored. APNG animation frames are ignored: the default image is used. A CRC mismatch in IHDR, PLTE or IDAT is a `PdfFormatException`; in `tRNS` or `iCCP` the chunk is dropped with a warning in `Diagnostics`. Pixel count and decoded size are checked against `PdfLimits` from the header, before any pixel memory is allocated; the decoded size of an interlaced image or an image with alpha is computed from the header and the inflater is capped at that size, so a deflate bomb cannot grow past the image. ## Gradients `PdfGradient.Axial(x0, y0, x1, y1, stops)` and `PdfGradient.Radial(x0, y0, r0, x1, y1, r1, stops)` describe a smooth colour transition; `builder.PaintGradient(gradient)` paints it over the current clipping region (the `sh` operator, ISO 32000-2 8.7.4.2), and `builder.FillRectangle(x, y, w, h, gradient)` is a rectangle clip plus that. To fill any other shape, clip to it: ```csharp b.Save(); b.Ellipse(50, 50, 120, 80); b.Clip(); b.PaintGradient(PdfGradient.Axial(50, 50, 170, 130, [new PdfGradientStop(0, PdfGradientColor.Rgb(1, 0.8, 0)), new PdfGradientStop(1, PdfGradientColor.Rgb(0.8, 0, 0.2))])); b.Restore(); ``` - Coordinates are in the user space current where the gradient is painted, so `Transform`, `Rotate` and `Scale` before the call carry it along. The current fill opacity applies. - Stops (`PdfGradientStop`: position 0..1 and a `PdfGradientColor` in DeviceGray, DeviceRGB or DeviceCMYK) must be in one space and not decrease; the colour before the first and after the last is held, and two stops at one position make a hard edge. Two stops become one exponential function (type 2), more become a stitching function (type 3). Bad input is an `ArgumentException`. - `extendStart` and `extendEnd` (default true) say whether the end colours continue past the start and end of the axis, or past the circles of a radial gradient. - Each call adds a shading resource; gradients are immutable, so one may be used on any number of builders. ## Shaped glyph runs `builder.DrawGlyphRun(x, y, glyphs, text)` draws glyphs a shaping engine has already chosen and placed, with an embedded font (`SetFont` with a font from `AddFont`). It takes plain data and depends on no shaping library. ```csharp b.SetFont(devanagari, 24); // "कि": KA + VOWEL SIGN I. The I-matra is drawn to the left of the consonant: two glyphs, one cluster. b.DrawGlyphRun(50, 100, [new PdfShapedGlyph(matraGid, Cluster: 0, Advance: 400), new PdfShapedGlyph(kaGid, Cluster: 0, Advance: 600)], "कि"); ``` - Each `PdfShapedGlyph` has a glyph index, a **cluster** (the UTF-16 index in `text` of the first character it stands for), an **advance** and an optional x and y **offset**, all in thousandths of the font size. Glyphs go in drawing order (left to right on the page), so right-to-left text is given in visual order, as shaping engines return it. A cluster's text runs from its index to the next larger cluster index present in the run, or to the end of the text. - **Extraction returns logical text.** A cluster of one glyph adds the cluster's text to the font's `/ToUnicode` map (one glyph to one or more characters: a ligature `fi` is one entry). A cluster of several glyphs (reordered or split Indic syllables, a base with marks), a glyph that `/ToUnicode` already ties to different text, or a cluster that returns later in the run is wrapped in a `/Span` with `/ActualText` holding the cluster's text (ISO 32000-2 14.9.4). Text that has glyphs only inside such spans has no entry of its own. - **Placement is exact.** Pen movement is the given advances, not the font's widths; character spacing set with `SetCharacterSpacing` is compensated. Offsets move a glyph without moving the pen (x as a position adjustment, y as text rise). The call returns the distance moved in points. Glyph 0 (the missing-glyph box) is allowed; a glyph index outside the font, a cluster outside the text, or a non-finite number is an `ArgumentOutOfRangeException`. - **Subsetting** is unchanged: the glyphs used are collected and the subset is cut at save time (or, with `PdfDocumentWriter`, at `Finish`). ## Marked content, artifacts and tagging On any builder: `BeginMarkedContent(tag)` and `BeginMarkedContent(tag, mcid)` (`BMC`/`BDC`), `BeginSpan(actualText, alternateText, language, expansion)`, `BeginArtifact(type, subtype, boundingBox)` and `EndMarkedContent()`. Marked-content sequences and `Save`/`Restore` pairs must nest (ISO 32000-2 14.6.1): `Restore` throws while a sequence begun after its `Save` is open, and the other way round. `Commit` closes whatever is still open. A builder alone does not build a structure tree: it writes the marks. `PdfDocumentWriter` builds the tree. ## Streaming creation `PdfDocumentWriter` writes a document to a stream as it is made, for documents too large to hold: a 10,000-page report, a 250-page table. Each page is written when committed, with its content stream, link annotations and parent-tree entry; images, forms and structure elements are written when they are made or ended. What stays in memory is bookkeeping: eight bytes per object offset, the page-tree entry, the parent-tree entry per page, bookmarks and named destinations, and the fonts. Measured with tagged pages of six paragraphs and a link each, live managed memory grows by about 580 bytes per page. ```csharp using var file = File.Create("report.pdf"); using var w = new PdfDocumentWriter(file, new PdfDocumentWriterOptions { Language = "en" }); PdfCreatedFont font = w.AddFont(File.ReadAllBytes("LiberationSerif-Regular.ttf")); PdfFormXObject total = w.ReserveForm(new PdfRectangle(0, 0, 40, 12)); // "of Y": filled once the count is known PdfStructureElement document = w.BeginStructureElement("Document"); for (int i = 0; i < 10_000; i++) { PdfStreamPageBuilder p = w.BeginPage(595, 842); PdfStructureElement para = w.BeginStructureElement("P"); p.BeginTaggedContent(para); // BDC with the page's next MCID, bound to the element p.SetFont(font, 11); p.DrawText("Paragraph text", 50, 780); p.EndMarkedContent(); w.EndStructureElement(); p.BeginArtifact(PdfArtifactType.Pagination, PdfArtifactSubtype.Footer); p.DrawText($"Page {i + 1} of ", 250, 30); p.Save(); p.Translate(310, 30); p.DrawForm(total); p.Restore(); p.EndMarkedContent(); p.Commit(); } w.EndStructureElement(); PdfFormContentBuilder last = w.BeginForm(total); // fill the reserved form last.SetFont(font, 11); last.DrawText(w.PageCount.ToString(), 0, 2); last.Commit(); w.Finish(); ``` - **Written at once:** pages, content, annotations, images (`AddJpegImage`, `AddPngImage`, `AddRawImage`), forms (`BeginForm`), ended structure elements. **Kept until `Finish`:** fonts (their subsets are cut then, so any glyph can still be added), forms from `ReserveForm`, the page tree (nodes of 64 pages), the structure tree's root and parent tree, the outline, named destinations, the information dictionary and the catalog. - **One page is open at a time.** A page builder that is disposed without `Commit` is dropped; one with structure bound to it must be committed. `Finish` throws if a page, a structure element or a reserved form is still open. - **Output:** a compressed cross-reference stream by default, or a classic table (`PdfXrefFormat.Table`); no object streams, since objects are written as they become final. Reproducible with `DocumentId`. Encryption is not available on this path; save the result through `PdfDocument` for that. - **Outline and destinations:** `AddBookmark(title, level, pageIndex, top)` (levels start at 0 and rise by at most one) and `AddNamedDestination(name, pageIndex, top)`, with `AddLinkToDestination` for links that name a destination, defined before or after the link. `AddLink(area, uri, element, description)` writes a URI link. - **Page labels (ISO 32000-2 12.4.2):** `AddPageLabelRange(firstPageIndex, style, prefix, start)` starts a range at a page already begun; ranges are added in page order and a document whose first range is not at page 0 gets plain decimal labels before it. - **Tagged structure (ISO 32000-2 14.7, 14.8):** `BeginStructureElement(type, options)` / `EndStructureElement()` nest elements; the first one makes the file tagged (`/MarkInfo /Marked true`, `/StructTreeRoot`, the page's `/StructParents`, `/ParentTree`, `/ParentTreeNextKey`, `/Tabs /S`). `PdfStructureElementOptions` carries `/Alt`, `/ActualText`, `/Lang`, `/T`, `/E`, `/ID` and an attribute object (`PdfStructureAttributes.TableHeader("Column")`, `Table(colSpan, rowSpan)`, `List("Decimal")`). `MapRole` adds `/RoleMap` entries for custom types. An element can take content from several pages (the extra pages use `MCR` entries); a link ties its annotation to a `Link` element with an `OBJR` entry and the annotation's `/StructParent`. An element is written when it ends, so memory follows the nesting depth, not the size of the document. `BeginStructureElement(parent, type, options)` begins an element under an explicit parent and `EndStructureElement(element)` ends a specific one (after its own children), for content that continues on later pages: several branches can stay open, for example the cells of a table row that splits, and children are listed in the order they were begun. Content that is neither tagged nor an artifact is the caller's to avoid; the writer does not check it. - **Reading back:** text extraction follows the tree when the page is covered by it (`PdfPageText.UsedStructureTree`); page import and redaction keep the tree consistent. veraPDF reports no structure rule failures when such a file is saved as PDF/A-2b; the PDF/UA-1 profile still wants an identification entry in the XMP metadata and an `/ID` on `Note` elements. ## Reproducible output The same calls give the same bytes: content numbers are fixed-precision, subset tags and code assignment are deterministic, and nothing reads the clock. A new document gets a random trailer `/ID` unless `PdfSaveOptions.DocumentId` (16 bytes) is given; an existing document keeps its `/ID`. Encrypted output is not reproducible (random IVs and salts). ## Not covered Text shaping itself (glyph runs take the result of a shaping engine); Type 1 and bare CFF font files as input; variable-font instances; vertical writing; emoji and colour fonts (only outlines are embedded); a general page layout engine, tables or images fitted to boxes; checking that all content of a tagged document is tagged or an artifact; encryption of streamed output; compressing content streams of `PdfDocument.Save` (use `PdfSaveOptions.CompressStreams`). To save a created document as PDF/A, see [PDF/A](https://github.com/recursive-ai-oss/onepdf-csharp/blob/main/docs/pdfa.md). # Redaction `PdfDocument.Redact` removes what is drawn inside given areas of a page: the glyphs, the vector paths, the image samples, the annotations and form values under them, and (on request) metadata that repeats the text. It rewrites the content; nothing is hidden under a box. After `Save`, extraction, a byte search of the decoded streams and other readers find nothing of the removed text. ```csharp using PdfDocument doc = PdfDocument.Open("in.pdf", new PdfOpenOptions { AllowEditing = true }); PdfRedactionResult r = doc.Redact( [new PdfRedactionRegion(0, new PdfRectangle(72, 600, 300, 620))], new PdfRedactionOptions { Overlay = new PdfRedactionOverlay(), MetadataTerms = ["Jane Doe"] }); doc.Save("out.pdf"); ``` `ApplyRedactAnnotations` does the same for the `/Redact` annotations in the file (quad points or `/Rect`, `/IC`, `/OverlayText`) and removes them. ## What the rewrite does The interpreter runs the page and reports, for every operator, the device calls it caused and the bytes it occupies. The rewrite is made from that reading, so fonts, widths, matrices and clips decide what is under an area exactly as they decide what is drawn. Areas are in the page's default user space (before `/Rotate`). - **Text.** A glyph is removed when its box meets an area. The box is the advance wide, from 0.2 em below the baseline to 0.9 em above, united with the outline's control bounds; vertical glyphs use their vertical advance. A glyph that overlaps by no more than 0.01 pt does not count. `FullyContainedOnly` removes only glyphs wholly inside. `Tj`, `TJ`, `'` and `"` become a `TJ` whose numbers move the remaining glyphs back to where they were. A Type 3 glyph is removed whole. - **Vector paths.** `RemovePaths` (default) drops a fill, stroke or both when the path's control bounds (widened by half the line width for strokes) meet an area, including the coordinates. A clipping path stays. `ClipOut` keeps the path but draws it inside a clip that excludes the areas; the geometry then remains readable in the file. - **Shadings** (`sh`) whose clipped extent meets an area are dropped. - **Images.** Samples inside an area are blanked in the image's stored sample domain (zero, the unpainted value for a mask, or transparent through the soft mask) and the image is written again as Flate. DCT, JPX and JBIG2 are decoded with the decoder registry (`PdfRedactionOptions.Decoders`); with no decoder for the codec the image is removed whole. Inline images that meet an area are removed whole. Images are copied before they change, so other pages that share one keep the original. - **Form XObjects** are copied before they are changed; other uses keep the original. Annotation appearances that are kept are redacted the same way. - **Soft masks.** The group of a soft mask (`/SMask` in an `ExtGState`) is read at the place it is selected, in the coordinate system the mask has there, and edited by the rules above: text, vector paths and image samples under an area are removed from it. The graphics state and the group are copied, so other pages and other uses keep the original. Because a mask works pixel by pixel, content removed from it changes nothing outside the area. - **Tiling patterns.** A pattern cell is one procedure repeated across the shape it paints, so the part under an area cannot be cut out of it. The whole use goes instead: a path that meets an area and is painted with a tiling pattern is removed even with `ClipOut` (the cell's text and images would stay readable in the file), and a glyph or stencil image painted with a pattern goes with its own removal. A tiling pattern that no remaining paint uses is replaced by an empty cell of the same name, so its text and images are no longer reachable. A pattern used by paint outside the areas is kept whole. - **Type 3 glyphs.** A glyph is removed from its string when its box meets an area or when anything its procedure paints (a path, an image, text, a shading) does, wherever the glyph itself stands. The procedure is shared by every place the glyph is shown, so it stays while any other place on the page still shows the glyph, and goes with the last one: the font is copied without the procedures of glyphs the page no longer shows (and without the resources only they used), and the page's font entry points at the copy. - **Unused resources.** On a page that something was removed from, the fonts, XObjects, graphics states, patterns and shadings that the rewritten content no longer refers to are dropped from its resource dictionary (and from the copies of forms and mask groups it makes). A font that remaining operators still select but that no remaining text uses is replaced by a bare Helvetica reference of the same name, so the font program is unreachable and the selecting operator stays valid. Only the copy changes: a resource dictionary shared with another page is not touched, and a page nothing was removed from keeps its resources as they were. Nothing is dropped when an annotation without an appearance resource dictionary could still use the page's, or when a Type 3 font without resources of its own may name things in them. - **Marked content.** `/ActualText`, `/Alt` and `/E` of the marked-content sequences and structure elements that cover removed content are removed. Structure-tree text of redacted marked content is dropped. - **Annotations** that meet an area are removed with their pop-ups (`Remove`, default), or kept with their appearance redacted and `/Contents`, `/RC` and values cleared (`KeepRedacted`). Form fields whose widget meets an area lose their value. - **Metadata.** `MetadataTerms` clears matching Info entries, XMP elements and attributes, and matching outline titles (case-insensitive substring). `RemoveJavaScript`, `RemoveEmbeddedFiles` and `RemoveThumbnails` strip those parts; thumbnails of redacted pages always go. - **Overlay.** `PdfRedactionOverlay` (or a region's own `FillColor` and `OverlayText`) is drawn over each area after the content is gone. - **Comments** in rewritten content streams are dropped, because a comment can hold text from the page. ## Fail closed Anything the rewrite cannot check is not copied through. If a page's content cannot be read completely, the call throws `PdfRedactionException` naming the page and the document is unchanged. This covers an undecodable content stream or form, a limit being hit, a form XObject that uses itself, and a text operator whose glyphs do not match its string. The work is two-phase: every page is planned first and applied only if all of them succeeded. Bytes after the last operator the interpreter executed (a string or array cut off by the end of the data, operands without an operator) are dropped from the rewritten stream, since they were never checked against the areas. A page with areas is always written back from what was read. ## Limits These are not covered; look at the output before relying on it for them. - A Type 3 font that a form or annotation appearance with nothing to remove also shows keeps the procedures of glyphs removed elsewhere on the page: that form is not rewritten, so it still points at the original font. - Resources that nothing selects any more are dropped, but a font, pattern or XObject that a remaining operator selects stays selected (as a stand-in for fonts and tiling patterns that nothing paints with). Colour spaces and property lists are not pruned. - Embedded font programs of fonts that remaining text still uses are kept whole, including outlines of glyphs that were removed. They hold shapes, not the text, but a font subset made for the page does name its glyphs. - Text in outlines (bookmark titles) is cleared only through `MetadataTerms`; destinations and named-destination names are not touched. - Signatures are invalid once the document is saved. `PdfRedactionResult.SignaturesInvalidated` and a warning say so. - Only a full `Save` is supported afterwards (it writes no earlier revisions and no unreferenced objects); `SaveIncremental` is refused, because the old bytes would remain in the file. - Text that is only visible as an image (a scan without a text layer) is removed only by the image blanking above. - Matching is by geometry. A word that is drawn twice in different places is removed only where an area covers it. # Forms: filling and flattening Documents opened with `PdfOpenOptions.AllowEditing = true` can have their form fields filled and their annotations baked into the page. Both work with `Save` and `SaveIncremental`, and on encrypted documents (new objects are encrypted like the rest). Field scripts (`/AA` calculate, format, validate, keystroke) are data: they never run, and values are stored exactly as given. ```csharp using var doc = PdfDocument.Open("form.pdf", new PdfOpenOptions { AllowEditing = true }); PdfAcroForm form = doc.Form!; form.SetValue("applicant.name", "Ada Lovelace"); // text form.SetValue("country", "Norway"); // combo or list: export value or display text form.SetValues("interests", ["art", "music"]); // multi-select list box form.SetChecked("agree", true); // check box form.SetValue("size", "M"); // radio group: on-state name or /Opt export value doc.SaveIncremental(File.Create("filled.pdf")); doc.Flatten(); // bake everything into the page, drop the form doc.Save("flat.pdf"); ``` ## Filling `PdfFormField.SetValue`, `SetValues`, `SetChecked` and `Reset`, and the same by fully qualified name on `PdfAcroForm` (plus `ResetAll` and `GenerateMissingAppearances`). A value that cannot be stored throws `PdfFormValueException` (an `ArgumentException`) with a `Reason` and changes nothing: | Reason | When | |---|---| | `UnknownField` | no field has that fully qualified name | | `WrongType` | the call does not fit the field (a check for a text field, a value for a push button or signature) | | `ReadOnly` | the field has the read-only flag; `PdfFillOptions.OverrideReadOnly` fills it anyway | | `NotInOptions` | the value is not in `/Opt` (combo, list), or not an on-state of the widgets (check box, radio) | | `TooLong` | a text value is longer than `/MaxLen` | | `NotMultiSelect` | several values for a field that is not a multi-select list box | | `NotTerminal` | the field has child fields; fill the leaves | A document that is not editable throws `InvalidOperationException`; a value larger than `PdfLimits.MaxMetadataBytes` throws `PdfLimitException`. - **Text** (ISO 32000-2 12.7.4.3): `/V` becomes a text string (PDFDocEncoding when possible, else UTF-16BE) and a stale `/RV` is removed. `/MaxLen` counts Unicode code points; a longer value is refused, or cut to the first `/MaxLen` code points with `TruncateToMaxLength`. Comb fields, multi-line, password and `/Q` alignment are drawn from the flags by the appearance generator. A value of `null` removes `/V`. - **Combo and list boxes** (12.7.4.4): the value is matched against the export values of `/Opt`, then against display texts; `/V` stores the export value. An editable combo box (flag bit 19) accepts any text. A multi-select list box takes several values: `/V` is an array and `/I` the selected indices, both in option order, duplicates dropped. Entries of `/Opt` that are neither strings nor two-string arrays are ignored. - **Check boxes** (12.7.4.2.3): the value is an on-state name (a key of a widget's `/AP /N` other than `Off`), `Off`, or null; `SetChecked(true)` uses the first widget's on-state, or `Yes` when no appearance names one. `/V` and every widget's `/AS` are set (widgets that have the state go on, others off). - **Radio buttons** (12.7.4.2.4): as for check boxes, by state name; when `/Opt` is present the state names are indices and the option text is accepted as the value. Only the first widget with the state turns on, unless the radios-in-unison flag (bit 26) is set, which turns on every widget that has it. `Off` or `SetChecked(false)` turns all off. - **Appearances**: for each widget the normal appearance is built by the appearance generator (`docs/appearance-generation.md`) and stored as a Form XObject in `/AP /N` (text and choice: replaced; buttons: added under the on-state when the stored dictionary has no stream for it; stored art for the state is kept). Fonts named by `/DA` come from `/DR`; built-in fonts are stored once per form as indirect objects. `BBox` and `Matrix` are rounded to four decimals like the generated content. `PdfFillOptions.GenerateAppearances = false` changes only `/V` and `/AS`. - **`/NeedAppearances`** is cleared only when every widget has a normal appearance (a button that is off needs none). When that moment arrives, text and choice fields not filled by the caller are rebuilt first, so clearing the flag never makes an old appearance authoritative. `GenerateMissingAppearances()` stores whatever is missing and applies the same rule. - **Reset** sets `/V` to the inherited `/DV` (or removes it; off for buttons). A default that is not among the options is stored as it is. `ResetAll` skips read-only fields unless overridden, push buttons and signatures. - Pages, `Form` and other views taken before an edit are not refreshed by value changes to the same objects; `Flatten` rebuilds `Pages` and `Form`, so take them again afterwards. ## Flattening `PdfDocument.Flatten(PdfFlattenOptions?)` draws the normal appearance of widgets (and, with `IncludeMarkup`, of every annotation except links and pop-ups) into the page and removes the annotations. - **Placement** (12.5.5): the appearance's `/Matrix`-transformed `/BBox` is fitted onto `/Rect`; the result is `q a b c d e f cm /Name Do Q` in a content stream appended to the page. The original content is wrapped in `q` … `Q` so a leaked graphics state cannot move the art. A stored appearance stream is used as the XObject itself (`/Subtype /Form` is added when missing); a missing one is generated in memory, exactly as `page.Interpret` does (including the `/NeedAppearances` rule), and stored. `NoRotate` on a rotated page keeps the art upright, pinned at the upper left corner of `/Rect`; `NoZoom` has no meaning in a page. - **Visibility**: `Visibility = View` (default) draws what is not Hidden and not NoView; `Print` draws what has the Print flag and is not Hidden. Annotations hidden by optional content are not drawn. Annotations not drawn are still removed. - **Selection**: `Fields` (default true) and `FieldNames` (fully qualified names, each with the fields under it; null means all, including widgets outside the field tree). `IncludeMarkup` adds the other annotation types; hidden ones are removed too. - **Fields**: flattened leaves leave the field tree and `/CO`; parents left without children go too; `/AcroForm` is dropped when no field is left (`PdfFlattenResult.FormRemoved`). Signed signature fields stay, with their widgets (`SignedKept`). - **Pop-ups** go with their parent annotation. Structure-tree entries pointing at removed widgets are left as they are. - The flattened page interprets as the original page with its annotations, except that flattened art is drawn under annotations that are kept, and text extraction now includes the field values. ## Limits Everything is bounded before work: values by `PdfLimits.MaxMetadataBytes`, value lists and widget counts by `PdfLimits.MaxObjects`, drawn text and items by the generator's limits (`MaxAppearanceText`, `MaxAppearanceItems`); the field tree is walked iteratively with cycle detection and the nesting limit; copying of font dictionaries is iterative and counted. Names are looked up in a dictionary built once, so filling a form of 100,000 fields is linear. # Appearance generation When an annotation has no usable normal appearance, `page.Interpret` builds one in memory and draws it through the same path as a stored `/AP` (ISO 32000-2 12.5.5). The same appearance is available as data from `PdfAnnotation.GenerateAppearance()`. Nothing is written to the document. ## When it applies - `PdfInterpretOptions.GenerateAppearances` is true (the default). Set it to false for the 1.0 behaviour: nothing is drawn and an Info diagnostic says so. - The annotation is visible by its flags, in the selected annotation mode, and not hidden by optional content. - Its normal appearance is missing: no `/AP`, no `/N`, or a state dictionary with no stream for the current `/AS`. Links are never generated. - Widgets also when the form sets `/NeedAppearances true` (12.7.3): every text and choice field is rebuilt from its value, replacing a stored appearance; check boxes, radio buttons and push buttons keep a stored appearance for their state and are generated only when there is none, because their stored art (icons, custom marks) cannot be rebuilt from the dictionary. - Button art under `/NeedAppearances` is a place where viewers differ. ISO 32000-2 12.7.3 says a viewer may construct appearances for all widgets; in a black-box comparison with stored art that `/MK` could not reproduce (a red square with a cross for `/CA (4)`, a triangle for `/CA (l)`, a pink frame for a push button caption), one viewer redrew all three button kinds from `/MK`, another drew the stored art exactly as with the flag off, and the third could not be observed. OnePDF keeps the stored art; a button without a stored appearance for its state is still generated. - Text extraction does not interpret annotation appearances and is unaffected. - Every generated appearance records an Info diagnostic `Drew a generated appearance (...)`; problems record Warning or Info diagnostics and never throw. ## Result `PdfGeneratedAppearance` has `Dictionary` (a form XObject dictionary with `/BBox`, `/Matrix` for rotated widgets, `/Resources`), `Content` (unfiltered bytes; numbers have at most four decimals, so output is deterministic), `Rect` and `Description`. Fonts in `/Resources` are the field's own dictionaries when the `/DA` names one found in the field's or the form's `/DR`, otherwise direct standard 14 dictionaries. For markup, `Rect` is the annotation rectangle grown to hold the generated geometry (line endings, stroke width); `/BBox` equals `Rect` and the drawing is in page coordinates. For widgets `Rect` is the annotation rectangle and the box is `[0 0 w h]`. ## Widgets (12.7.4) Field attributes (`/FT /Ff /V /DA /Q /MaxLen /Opt /TI /I /DR`) are inherited through `/Parent`, at most `PdfLimits.MaxRecursionDepth` levels. `/DA` and `/Q` fall back to the AcroForm's. - **Frame**: `/MK /BG` fills; `/MK /BC` strokes with width and style from `/BS` (S solid, D dashed with `/D`, B beveled, I inset, U underline) or `/Border`. No `/BC` means no border. Radio buttons use circles. `/MK /R` (0, 90, 180, 270) rotates through `/Matrix`. - **Text field**: value `/V` (string, name or text stream), default `/DA` colour, left, centred or right by `/Q`. Auto size (`0 Tf`): single line is three quarters of the inner height, reduced to fit the width, minimum 4; multi-line starts at 12 and shrinks to fit, minimum 4. Multi-line wraps at spaces, inside over-long words, and at CR/LF. Comb (`/MaxLen`, no multi-line or password) centres one character per cell and draws cell separators. Password shows `*` per character. Text is clipped to the inner box and wrapped in `/Tx BMC ... EMC`. - **Combo box**: the display string for the exported value (`/Opt` pairs). **List box**: the options from `/TI`, one row per `1.15 x size` (12 for auto), selected rows (`/V`, `/I`) highlighted. No drop-down arrow is drawn. - **Check box / radio button**: drawn when on. On is `/AS` not `Off`; without `/AS`, `/V` not `Off` (radio buttons also compare with the on-state key of `/AP /N`, `/Opt` or the widget's index among `/Kids`). The mark comes from `/MK /CA` and is drawn as vectors for the ZapfDingbats check (4), cross (8), circle (l), diamond (u) and square (n), so it needs no font; other characters use ZapfDingbats text. Colour is the `/DA` colour. - **Push button**: `/MK /CA` caption centred, auto size up to 12. Icons (`/MK /I`, `/TP`) are not drawn. - **Signature fields** get nothing (Info diagnostic). - **Fonts and characters**: the `/DA` font if it is a simple (single-byte) font, else the standard 14 face named by the alias (`Helv`, `TiRo`, `Cour`, `ZaDb`, `Symb`, bold and italic forms), else Helvetica. A character the font cannot encode is tried in Helvetica and Symbol (Omega, Delta and micro in their Adobe-list spellings too); if none can, it is dropped and one Warning counts the dropped characters. Right-to-left shaping is not done. ## Markup (12.5.6) Common: `/C` stroke (default black; yellow for Highlight), `/IC` fill, `/CA` opacity through an ExtGState, `/BS` or `/Border` width (default 1), style and dashes. Highlight uses blend mode Multiply. | Subtype | Drawn | |---|---| | Square, Circle | The rectangle reduced by `/RD`, inset by half the width, filled and stroked. The cloudy border `/BE` is drawn as a plain border. | | Line | `/L` with `/LE` endings (None, Square, Circle, Diamond, OpenArrow, ClosedArrow, Butt, ROpenArrow, RClosedArrow, Slash) and leader lines `/LL /LLE /LLO`. Captions (`/Cap`) are not drawn. | | Polygon, PolyLine | `/Vertices`, closed and filled (Polygon) or open with `/LE` endings (PolyLine). | | Ink | Each `/InkList` stroke with round caps and joins. | | Highlight, Underline, StrikeOut, Squiggly | One shape per `/QuadPoints` quadrilateral in the order viewers write (upper left, upper right, lower left, lower right); with no quad points the rectangle. | | FreeText | Background `/C`, border only when `/BS` or `/Border` is present, colour from `/DA`, text from `/Contents` wrapped and aligned by `/Q`, callout `/CL` with the `/LE` ending. Rich text `/RC` is not used. | | Text | A 20 by 20 note icon at the upper left of the rectangle: Note, Comment and Help have their own shapes, other names draw as Note. Colour `/C`, default yellow. | Other subtypes (Stamp, Caret, FileAttachment, Sound, Redact, 3D, ...) keep the old behaviour: nothing is drawn and an Info diagnostic says so. ## Limits All checked before the work they guard, with a diagnostic when something is cut off: field and annotation text at `PdfLimits.MaxAppearanceText` characters (65,536); vertices, ink points, quadrilaterals, list rows, comb cells, wrapped lines and squiggle steps at `PdfLimits.MaxAppearanceItems` (65,536); `/DA` at 512 tokens and font size at 1000; rectangles and coordinates beyond 10^6 (widgets) or 10^7 (markup) points, empty rectangles and non-finite numbers skip the annotation with a Warning. `/Parent` chains stop at the recursion limit. A font that is not a simple font is replaced by a standard one. ## Checked against Widgets of real authoring-tool files (US federal forms made by Acrobat-family tools and a veraPDF sample) with the stored appearance stripped in memory: text equal, drawn bounds within 4 points (about 1 point for fixed-size text). See `AppearanceGenerationTests`. # Signature verification OnePDF reads the signatures in a PDF as data and checks them. It never runs anything in the file and never touches a network: no certificate downloads, no OCSP or CRL fetches, no time-stamp requests. Verification is on demand; opening a document does no signature work. Signing is described [below](#signing). using var doc = PdfDocument.Open("contract.pdf"); var options = new PdfSignatureVerifyOptions { TrustAnchors = [rootCertificate] }; PdfSignatureReport report = doc.VerifySignatures(options); foreach (PdfSignatureVerification v in report.Signatures) { Console.WriteLine($"{v.Signature.FieldName}: valid={v.IsValid} integrity={v.Integrity} signature={v.SignatureCheck} chain={v.Chain} coverage={v.Coverage}"); } `doc.GetSignatures()` lists `PdfSignature` objects without verifying (name, reason, location, contact, `/M`, `/SubFilter`, `/ByteRange`, `Contents`, `CertificationLevel`, `FieldLock`, `CoveredRevision`). `PdfFormField.GetSignature()` gives one field's. ## What is checked Each check has its own result; `IsValid` is true only when the byte range is valid, the digest matches, the signature verifies, the chain is `Trusted` at the validation time, any time-stamp is valid, revocation is not `Revoked`, and no disallowed change follows. A weak algorithm and unchecked revocation are warnings, not failures. | Result | Meaning | |---|---| | `ByteRange` | `Valid`, or why not (`Missing`, `Malformed`, `DoesNotStartAtZero`, `OverlapOrGap`, `OutsideFile`, `ContentsMismatch`, `DoesNotCoverRevision`) | | `Integrity` | the digest of the signed bytes equals the digest the signature commits to (`messageDigest`, or the signed content for the legacy forms) | | `SignatureCheck` | the signature over the signed attributes verifies with the signer certificate's key | | `Chain`, `ChainElements` | the chain from the signer certificate at `ValidationTime`: `Trusted`, `UntrustedRoot`, `Expired`, `NotYetValid`, `Incomplete`, `Revoked`, `Invalid` | | `Timestamp` | the RFC 3161 token: imprint, the authority's signature, its chain at the token's time, the time itself | | `Revocation` | `NotChecked` unless `RevocationCheck` was supplied | | `Coverage`, `Changes` | see below | | `Warnings` | everything that went wrong or could not be checked, in words | A check that cannot run (an unsupported algorithm, no signer certificate) is `NotChecked` with a warning, never a crash. ### Byte range (ISO 32000-2 12.8.1) The range must be four non-negative integers `[0 b c d]`; the bytes from `b` to `c` must be exactly the hex string `<...>` that is the value of `/Contents` (a range that leaves out only the digits between the brackets is accepted with a warning); `c + d` must be where a revision ends (an update's `%%EOF`), possibly followed only by white space, or the end of the file. All offsets are checked as `long` against the file length before anything is read. Linearized files chain their first-page cross-reference section after the main one although it lies earlier in the file; it counts as the same revision, not as an update. `CoveredRevision` is the index in `PdfDocument.Revisions`. ### Algorithms | Kind | Supported | |---|---| | Sub-filters | `adbe.pkcs7.detached`, `ETSI.CAdES.detached`, `ETSI.RFC3161`; `adbe.pkcs7.sha1` (weak); `adbe.x509.rsa_sha1` is reported unsupported and weak | | Digests | SHA-1 (weak), SHA-256, SHA-384, SHA-512. SHA-224 is reported unsupported | | Signatures | RSA PKCS#1 v1.5 (`rsaEncryption` or `shaNWithRSA`), RSASSA-PSS (hash and MGF1 hash equal, salt as long as the hash is what the platform checks), ECDSA (DER, any curve the platform has) | | CAdES | the signing-certificate attribute (v1 and v2) must name the signing certificate | The signature algorithm must agree with the digest algorithm of the signer info; if they differ the signature is `Invalid`. RSA keys below 2048 bits are flagged weak. The CMS reader accepts BER (including indefinite lengths) and checks every length before slicing; it is bounded (nesting 32, 16 signers, 256 certificates, 64 attributes per set, 1 MiB of embedded content). ### Time and chains The validation time is, in order: `PdfSignatureVerifyOptions.ValidationTime` if set; the time of a verified time-stamp token (`TimePolicy.Auto`); the `signingTime` attribute (or `/M`), flagged as the signer's unverified claim; otherwise now. `TimePolicy.Now` forces now. The chain is built with the platform's `X509Chain` with downloads and revocation checking off, using the certificates in the signature, the `ExtraCertificates` and the trust anchors. Without anchors (and without `UseSystemTrustStore`) no chain is trusted. Revocation: pass `RevocationCheck`, a function that answers for a certificate and its issuer from CRLs or OCSP responses you already hold. Without it the result says revocation was not checked. Revocation data embedded in a signature is not used. ## Changes after signing The file is compared as revisions. For a signature covering revision *r*, every object written by later revisions is compared with its state in *r* (structural equality, so rewriting an object unchanged is not a change). The rules are advisory; a viewer may be stricter. - **Level.** The `/P` of the DocMDP transform of the certifying signature (`/Perms /DocMDP` in revision *r*): 1 no changes, 2 form fill and signing, 3 also annotations. A field lock's `/P` can lower it. With no certification the level is taken as **2**: form fill and signing pass, everything else is flagged. - **Always allowed:** `/DSS` validation data; document time-stamps; cross-reference and object-stream objects; a changed document information dictionary or catalog `/Metadata` (reported as `Other`, allowed). - **Level 2:** a field's `/V`, `/AP`, `/AS`, `/I`; adding fields to `/AcroForm` and widgets to a page's `/Annots`; adding a signature. - **Level 3:** also adding, removing and changing other annotations. - **Field locks:** a `/Lock` dictionary on a signature field, or a FieldMDP transform in the signature's `/Reference`, of this or an earlier signature: `All`, `Include` or `Exclude` with field names. A change to a locked field is not allowed whatever the level. - **Everything else** is not allowed: page content and resources, the page tree, other catalog entries, deleted objects. New objects take the category of the changed object that refers to them. Each `PdfSignatureChange` has the object number, a `PdfChangeCategory` (`FormFieldValue`, `NewSignature`, `Annotation`, `PageContent`, `ValidationData`, `Other`), whether it is allowed and a reason. `Coverage` is `WholeDocument` (nothing follows), `EarlierRevisionAllowedChanges`, `EarlierRevisionDisallowedChanges`, or `Unknown` (the byte range is not valid). Set `AnalyzeModifications = false` to skip the comparison. ## Known attacks and what the result says The published attacks on PDF signature validation (universal signature forgery, incremental saving, signature wrapping, shadow and certification attacks) are each reproduced by a crafted file in `tests/OnePdf.Tests/Signatures/AttackTests.cs`. **Detected** means `IsValid` is false. **Advisory** means the permission level of the document lets the change through, so `IsValid` stays true and the change is listed in `Changes` (with its category and `Allowed = true`); callers who show a document as "signed and unchanged" should show those entries. | Class | Variants tested | Outcome | |---|---|---| | Universal signature forgery | `/ByteRange` or `/Contents` absent, null, empty, all zeros, wrong type, `[0 0 0 0]`, negative, real numbers, wrong count, shifted gap, indirect object or indirect number | Detected. An indirect `/ByteRange` or `/Contents` is refused (`Malformed`) even when it is correct, because the specification requires direct values | | Incremental saving | rewritten content, page, page tree, catalog, font; objects appended after `%%EOF` with no table, with a trailer only, or with a bad `startxref`; a trailer that points at a new catalog; an update stretched into the signed range; a rewritten signature dictionary or catalog to lower the certification | Detected. Data after the last `%%EOF` is reported as an unsigned change; a later `/Root` that differs from the signed one is a disallowed change | | Signature wrapping | second range starting early or inside the first, extra objects or a new table hidden in the excluded area, range moved past them, wrong lengths, more than one gap | Detected: the gap must be exactly the `/Contents` string and the range must end at a revision boundary | | Shadow: hide and replace | page content, page objects, font replaced by an update; xref switching to a second definition prepared before signing | Detected | | Shadow: form values | a field's value or appearance replaced, or pointed at a prepared appearance stream, under P=2 or 3 | **Advisory**: allowed by the level and listed as `FormFieldValue`. Detected under P=1 | | Evil annotation | annotation added under P=1 or 2 | Detected. Under P=3 **advisory**: listed as `Annotation`, allowed | | Evil annotation as a form widget | a widget with an appearance added under P=2 | **Advisory**: a new form field is a permitted change at level 2, listed as `FormFieldValue`. Detected under P=1 | | Sneaky signature | an unsigned signature field with an appearance added under P=2; a signature added under P=1 | The field is **advisory** at P=2 and detected at P=1; the added signature is detected at P=1 | | Certificate | untrusted root, expired signer at the validation time, key usage without `digitalSignature`/`nonRepudiation`, extended key usage for TLS or time-stamping only, a time-stamp signed by a certificate without the time-stamping purpose | Detected (`Chain`, `KeyUsage`, `Timestamp`) | `RequireSigningKeyUsage` (default true) makes a certificate that may not sign documents fail `IsValid`; with it off the problem is still in `KeyUsage` and the warnings. A key usage extension needs `digitalSignature` or `nonRepudiation`; an extended key usage, when present, must include document signing, e-mail protection, Adobe PDF signing or any purpose. Two things the result does not do: it reads signatures from the newest catalog only, so a later `/Root` whose catalog has no `/AcroForm` shows no signatures (the report says so and is never "all valid"); and a field whose `/FT` is not `/Sig` is not treated as a signature. ## Limits A signature value larger than the file cannot occur; the excluded area is capped by `PdfLimits.MaxMetadataBytes`; object comparison is capped at 2^18 nodes per object and 2^20 changed objects. Cancellation is checked while the byte ranges are hashed and between signatures. ## Signing Signing is always an incremental update (ISO 32000-2 7.5.6), so earlier signatures stay valid and the original bytes are an exact prefix of the output. The document must be opened with `PdfOpenOptions.AllowEditing`; changes made through the mutable view before the call (a filled field, say) are part of the signed update. using var doc = PdfDocument.Open("contract.pdf", new PdfOpenOptions { AllowEditing = true }); var credential = PdfSigningCredential.FromCertificate(signerWithKey, [intermediate]); doc.Sign("contract-signed.pdf", credential, new PdfSigningOptions { FieldName = "Approval", // an unsigned signature field, or a new name Rectangle = new PdfRectangle(72, 72, 272, 132), // omit for an invisible signature Reason = "I approve this contract", Timestamp = request => MyTsaClient.Post(request.RequestDer.ToArray()), // makes it PAdES B-T }); `Sign` takes a `Stream` or a path and writes nothing unless the whole signature succeeded. It holds the output in memory (about the file size). After a call, successful or failed, the document holds the pending signature objects: reopen the output (or the file) to sign again; a second call on the same instance throws `PdfSigningException` (`DocumentUsed`). To sign more than once, chain the outputs: sign, open the result with `AllowEditing`, fill a field if you like, sign again, add a document time-stamp. ### Profiles | `PdfSigningOptions.Format` | Sub-filter | Signed attributes | Notes | |---|---|---|---| | `PadesBaseline` (default) | `ETSI.CAdES.detached` | `contentType` (id-data), `messageDigest`, `signingCertificateV2` | No `signingTime` (ETSI EN 319 142-1 6.3: the time is `/M`). The signer certificate and the chain you gave are embedded. With `Timestamp`: B-T, the token is the `signatureTimeStampToken` unsigned attribute. | | `Pkcs7Detached` | `adbe.pkcs7.detached` | `contentType`, `messageDigest`, `signingTime` | For older consumers. | Both use `issuerAndSerialNumber`, an empty detached `encapContentInfo`, digest identifiers with absent parameters (RFC 5754), and `sha*WithRSAEncryption`, RSASSA-PSS (hash = MGF1 hash, salt = hash length) or `ecdsa-with-SHA*`. A pre-2.0 file that has no signatures yet also gets `/Extensions /ESIC` (level 2 for CAdES, 1 for a document time-stamp; EN 319 142-1 5.6); a file that is already signed is not touched there, since a catalog change would count against the earlier signature. ### Keys and external signers `PdfSigningCredential.FromCertificate(cert, chain)` needs an RSA or ECDSA private key the process can use (`NoPrivateKey` otherwise). `FromExternalSigner(cert, signer, chain)` calls `signer(PdfSignRequest)` with the digest of the DER signed attributes, the hash algorithm, the key type and the RSA padding; it returns the signature value (RSA: the raw signature; ECDSA: DER or IEEE P1363). OnePDF checks the value with the certificate's public key and refuses a wrong one (`SignerFailed`). OnePDF does not build chains: `chain` is what gets embedded after the signer certificate. ### Time-stamps `PdfTimestampCallback` gets a `PdfTimestampRequest` (hash algorithm, message imprint, nonce, and the DER `TimeStampReq` with `certReq` set) and returns the `TimeStampResp` or the bare token. OnePDF checks the status (granted or granted with modifications), that the token is SignedData holding a TSTInfo, that its imprint equals the one sent and that it echoes the nonce; otherwise `TimestampRejected`. It does not verify the authority's signature or trust at signing time; `VerifySignatures` does. `AddDocumentTimestamp` writes a `/DocTimeStamp` field (`Timestamp1`, ...) whose token covers the whole file before it. ### Certification and locks `PdfSigningOptions.Certify` adds the DocMDP transform (`/Reference`) and `/Perms /DocMDP`; only the first signature of a document may certify (`CertificationNotFirst`). `FieldLock` writes a `/Lock` dictionary on the signature field. ### Visible signatures A rectangle (or an existing field with one) gives a visible signature. Its normal appearance is a Form XObject built with the content builder: an optional frame, an optional image on the left (a `PdfCreatedImage` of the same document, at most 40 percent of the width) and text lines (default: signer name, date, reason, location), in Helvetica sized to fit up to 10 points. An existing field that already has an appearance keeps it unless `Appearance` is given. ### Space for `/Contents` The placeholder is `ContentsReserve` bytes (default: an estimate from the certificates, key size and, for a time-stamp, 8 KiB; 12 KiB for a document time-stamp), written as twice as many hex digits and patched in place after the byte range is known. If the CMS does not fit, `ContentsTooSmall` carries `RequiredBytes`; `PdfSigningResult.ContentsUsed` tells what an earlier signature of the same kind took. The `/Contents` string of a signature dictionary is never encrypted (ISO 32000-2 7.6.2), and the reader does not decrypt it: the string of a dictionary with `/Type /Sig` or `/DocTimeStamp`, or one holding `/ByteRange`, is returned as stored. The rest of the update is encrypted like the document. ### Not done in signing The time-stamp authority's own signature is not checked when signing. A time-stamp token is embedded as the callback returned it (a BER token is not re-encoded to DER). Long-term validation data (`/DSS`, PAdES B-LT and B-LTA), several signers in one CMS, signature policies, commitment types and `/Prop_Build` are not written. Ed25519 and Ed448 keys are not supported (the platform has no such primitive). ## Not done in verification The public-key security handler, long-term validation data (`/DSS`) beyond noting it, CRL/OCSP parsing, and Ed25519/Ed448 (the platform has no such primitive) are not part of verification. # PDF/A OnePDF writes PDF/A-2b, 2u, 2a, 3b, 3u and 3a (ISO 19005-2 and -3, on ISO 32000-1) from a full save, and checks documents against the requirements it can verify from the file's structure. ```csharp using var doc = PdfDocument.CreateEmpty(); doc.FontProvider = OnePdf.Fonts.FontsPackage.Provider; // Liberation faces for the standard fonts doc.SetInfo("Title", "Quarterly report"); PdfPage page = doc.InsertBlankPage(0, 595, 842); PdfCreatedFont body = doc.AddFont(File.ReadAllBytes("LiberationSerif-Regular.ttf")); using (PdfPageContentBuilder b = page.BeginContent()) { b.SetFont(body, 12); b.DrawText("Hello, archive.", 72, 700); b.Commit(); } doc.AttachFile("data.xml", xmlBytes, new PdfAttachmentOptions { MimeType = "text/xml", Relationship = PdfAttachmentRelationship.Data }); doc.Save("out.pdf", new PdfSaveOptions { PdfA = new PdfAOptions { Part = PdfAPart.Part3, Conformance = PdfAConformance.U }, DocumentId = id16, // optional: the same input then gives the same bytes }); ``` ## What it is, and is not - **It writes PDF/A for documents made or edited with OnePDF**, and for existing files that already meet the requirements. Level B (visual reproducibility) and level U (B plus a Unicode mapping for all text) of parts 2 and 3. Level A (tagged structure) is offered for documents that are already tagged, including existing files tagged through [the tagging API](https://recursiveai.org/onepdf/docs/tagging/): see [pdfua.md](https://recursiveai.org/onepdf/docs/pdfua/). - **It is not a validator.** `CheckPdfA` verifies what is listed below and nothing else; a clean result does not prove conformance. The tests use the veraPDF command-line validator as a black box (see "Verification"). - **It does not convert.** No fonts are embedded into foreign content, no transparency or colour is rewritten, nothing is flattened, no JavaScript is removed. A file that breaks a requirement is refused, and the violations tell you what to change. ## API | | | |---|---| | `PdfSaveOptions.PdfA` (`PdfAOptions`) | Target part (`Part2` default, `Part3`), level (`B` default, `U`, `A`: tagged, see [pdfua.md](https://recursiveai.org/onepdf/docs/pdfua/)), `Language` (fallback `/Lang` for level A), and `EmbedLibraryFonts` (default true). A full save then prepares, checks, and writes, or throws `PdfAException` and writes nothing. Needs an editable document (`AllowEditing`, or `CreateEmpty`). Not available for `SaveIncremental`. | | `PdfDocument.PreparePdfA(options)` | Makes the repairs below in memory and returns what is still wrong. Calling it again changes nothing more. | | `PdfDocument.CheckPdfA(options)` | Reports every violation of the document as it stands, without changing it. Works on read-only documents. | | `PdfAException.Violations` | `PdfAViolation`: `Clause` (ISO 19005-2 clause number), `Message`, `ObjectNumber` (-1 for the file as a whole), `PageIndex` (-1 when not on a page). Large numbers of one kind (a hundred bad operators) are cut after twenty; at most 2000 violations are listed. | | `PdfDocument.AttachFile(name, data, options)` | Adds an embedded file (Flate-compressed) with `/F`, `/UF`, `/AFRelationship`, MIME type and dates, entered in the `EmbeddedFiles` name tree and the catalog's `/AF`. | A PDF/A save changes the document: it adds the output intent, the XMP packet and the other repairs to the in-memory document, as `SetInfo` or `AddObject` would. The file on disk is not touched. A failed save leaves those repairs in place. The header is `%PDF-1.7` with the binary comment line, whatever the source said. A source that was PDF 2.0 only because of AES-256 encryption is therefore written as 1.7 when the encryption is removed. Features that exist only in PDF 2.0 are not detected, and a validator will report them. Encrypted documents are refused unless the save also sets `Encryption = PdfEncryptionAction.Remove` (then the output is plain); `Encryption = Set` with PDF/A throws `ArgumentException`. ## What is written | Item | Clause | What OnePDF does | |---|---|---| | Header, binary comment | 6.1.2 | `%PDF-1.7`, then `%` and four bytes above 127 (every full save writes the comment). | | File identifiers | 6.1.3 | `/ID` written in the trailer (`DocumentId` makes it fixed, otherwise random). | | Cross-reference formats, object and stream spacing, no data after `%%EOF` | 6.1.4, 6.1.7.1, 6.1.9, 6.1.3 | The writer's own layout; classic table or cross-reference stream with object streams (both allowed in part 2). Checked with veraPDF. | | Output intent | 6.2.3 | A `GTS_PDFA1` intent with `/DestOutputProfile`, an sRGB profile generated by OnePDF (below), added once when the document has no PDF/A intent. | | Metadata stream | 6.6.2.1 | `/Metadata` with `/Type /Metadata /Subtype /XML`, unfiltered, UTF-8, no `bytes` or `encoding` attribute. | | XMP identification and properties | 6.6.2.3, 6.6.4 | `pdfaid:part` and `pdfaid:conformance`; `dc:title`, `dc:creator`, `dc:description`, `pdf:Keywords`, `pdf:Producer`, `xmp:CreatorTool`, `xmp:CreateDate`, `xmp:ModifyDate` taken from the Info dictionary, so the two agree. Only properties the XMP specification itself defines are used, so no extension schema block is needed. No clock is read. An existing packet that already claims the same part and level is kept as it is; any other packet is replaced. | | Standard fonts added by OnePDF | 6.2.11.4.1 | With the Fonts package registered (or a `PdfDocument.FontProvider` that supplies faces): the font becomes an embedded non-symbolic TrueType font (Liberation, a subset with a Windows Unicode `cmap` holding the glyphs the font's encoding reaches, Flate-compressed, named with a six-letter subset tag as ISO 32000-2 9.6.4 requires), `/Widths` from the Adobe font metrics (the font program's own widths where they differ by more than one unit), `/FontDescriptor`, WinAnsi encoding (a `Differences` array when the original encoding was Standard). This applies to fonts from `AddStandardFont` and to the fonts of appearances OnePDF generated; fonts that came with the file are never replaced. Symbol and ZapfDingbats have no substitute and are reported. | | Fonts from `AddFont` | 6.2.11 | Already embedded subsets (TrueType and CFF), Type 0 with Identity-H, `/CIDToGIDMap Identity` for TrueType, `/W`, `/ToUnicode`. | | Annotation flags | 6.3.2 | `/F` created as Print (4), or Print added. Other bits are left, and a hidden or invisible annotation stays a violation. | | Annotation appearances | 6.3.3 | Widgets: `GenerateMissingAppearances` (which also clears `/NeedAppearances`, 6.4.1). Other annotations without `/AP`: the generated appearance of the 1.1 generator, stored as `/AP /N`. Appearance entries other than `/N` (`/R`, `/D`) are removed. | | Attachments (part 3) | 6.8 | `/F`, `/UF`, `/AFRelationship`, `/EF` stream with MIME `/Subtype` and `/Params /ModDate`, listed in `/AF`. A file with no date from the caller gets the Info modification (else creation) date. | ### The sRGB profile `SrgbProfile` builds an ICC.1:2001-12 (version 2.1) display profile of 2,612 bytes from the published parameters: the Rec. 709 primaries and D65 white point (IEC 61966-2-1 Table 1), adapted to the D50 profile connection space with the Bradford matrix, and the sRGB decoding curve sampled at 1024 points. Class `mntr`, colour space `RGB `, PCS `XYZ `, tags `desc`, `cprt`, `wtpt`, `rXYZ`, `gXYZ`, `bXYZ`, `rTRC`, `gTRC`, `bTRC` (one curve shared by the three). The creation date is fixed so the bytes never change. It is embedded once, Flate-compressed, as the intent's `/DestOutputProfile` with `/N 3`. The code is original; no vendor profile is copied. ### Device colour ISO 19005-2 6.2.4.3 ties the device spaces to the output intent: - DeviceRGB needs an RGB intent. OnePDF's intent is RGB, so DeviceRGB is fine. - DeviceGray needs any intent (an RGB intent is fine). - DeviceCMYK needs a CMYK intent. OnePDF has no CMYK profile, so CMYK content is a violation. An existing document that already carries a CMYK `GTS_PDFA1` intent keeps it, and then DeviceRGB is the violation. - ICCBased, CalRGB, Lab, Separation, DeviceN and Indexed spaces are not device spaces; ICC profile headers are checked (class, colour space, version below 5), spot colourant consistency is not. - A `DefaultRGB`, `DefaultGray` or `DefaultCMYK` entry is not taken into account: device colour without an intent is reported even when a default space would allow it. ## A conformance save never drops content A PDF/A save (and a PDF/UA save, see [pdfua.md](https://recursiveai.org/onepdf/docs/pdfua/)) writes every part of the document it reaches, or it fails. If a part cannot be read, the check reports it and the save throws `PdfAException` listing it, instead of writing a file that quietly lacks the part and then claiming conformance. The finding names the object (number and generation) and what it is: an embedded font program, a font descriptor or other font part, an ICC profile, an image, a page or form content stream, an XMP stream, an embedded file, an annotation or a structure node. Three situations count: - **A stream that cannot be read or decoded:** its bytes cannot be read, it cannot be decrypted cleanly (bad padding, a partial block, a missing IV, an unknown crypt filter), or its filter data is cut short or invalid (truncated or corrupt Flate, LZW, ASCII85 and the like). Repairs that lose nothing are not findings: a wrong `/Length` found by scanning for `endstream`, a missing or wrong Flate checksum, a missing end marker, trailing bytes after the data. Streams that need an image codec (DCT, JPX, JBIG2) are read up to that filter. - **A string damaged by decryption:** a string in any object the scan reaches whose ciphertext had bad padding, a partial last block or a missing IV. It reads as far as it decrypted, and a normal save writes those bytes again; a conformance save would claim to preserve text it does not have. The finding names the key (`the string under /Title`) and the object. - **A reference that leads nowhere** where the part is needed: `FontFile`, `FontFile2`, `FontFile3`, `FontDescriptor`, `DescendantFonts`, `ToUnicode`, `CIDToGIDMap`, `Widths`, `W`, `Encoding`, `DestOutputProfile`, `ICCBased` profiles, page `Contents`, `SMask`, `Mask`, `EF`, `AF`, `Annots`, `Kids`, `Pages`, `StructTreeRoot`, structure `K`, and every entry of the page resource categories (`Font`, `XObject`, `ColorSpace`, `Pattern`, `Shading`, `ExtGState`, `Properties`) and of `CharProcs`. A normal save writes `null` for such a reference; a conformance save refuses. A missing appearance stream, output intent or XMP packet is not a finding: the preparers write those. An XMP packet that exists but cannot be read is a finding and is never replaced by one built from the Info dictionary. The check and the save run the same scan, so a document the check passes is saved without loss and one whose save would fail is reported by the check. Clauses: 6.2.11.4.1 (font programs and font parts), 6.2.4.2 (ICC profiles), 6.2.8 (images), 6.2.2 (content streams), 6.6.2.1 (XMP), 6.8 (embedded files), 6.1.7.1 (anything else); the standard has no clause for "cannot be read" itself. A save without `PdfSaveOptions.PdfA` or `PdfUA` is unchanged: it copies streams as stored and writes `null` for a missing object, as before. ## What is checked (and refused) Checked by `CheckPdfA` and again, after the repairs, by every PDF/A save. Clause numbers are those of ISO 19005-2; part 3 uses the same numbers. | Clause | Checked | |---|---| | 6.1.2, 6.1.3 | Header version 1.0 to 1.7 (when reading a file); `/ID` present (when reading); no `/Encrypt`. | | 6.1.7 | No `/F`, `/FFilter`, `/FDecodeParms` in a stream; filters are the standard ones, not LZW, `Crypt` only with `Identity`; content streams that cannot be decoded. | | 6.1.10 | Inline images: no LZW or Crypt. | | 6.1.12 | Permissions dictionary only `UR3` and `DocMDP`. | | 6.1.13 | Integers beyond 32 bits, reals out of range, strings over 32767 bytes, names over 127 bytes, more than 8,388,607 objects, `q` nesting deeper than 28, page boxes smaller than 3 or larger than 14400 units. | | 6.2.2 | Operators not defined in ISO 32000-1; form XObjects and patterns that use resources without a `/Resources` of their own. | | 6.2.3, 6.2.4.2 | Output intent profile header (class prtr or mntr, RGB, CMYK or GRAY, version below 5), several intents naming different profiles; ICCBased profile headers. | | 6.2.4.3 | Device colour spaces against the output intent (above), in content operators, resources, images, shadings and inline images. | | 6.2.5, 6.2.6, 6.2.10 | `/TR`, `/TR2` other than Default, `/HTP`, halftone types and names, soft-mask `/TR`; rendering intents (`/Intent`, `/RI`); blend modes; transparency on a page without output intent and without a page group `/CS`. | | 6.2.8, 6.2.9 | Image `/Alternates`, `/OPI`, `/Interpolate true`, `/BitsPerComponent`; form `/OPI`, `/PS`, `/Ref`; PostScript XObjects. | | 6.2.11 | Fonts must be embedded (not Type 3); `/Subtype`, `/BaseFont`; `FirstChar`, `LastChar` and `Widths` of embedded simple fonts; `FontFile3` subtype; Type 0 `CIDToGIDMap`, predefined or embedded CMap, one descendant; TrueType encodings (WinAnsi or MacRoman, none when symbolic); **glyph 0 (.notdef) referenced by text drawn with a font from `AddFont`** (6.2.11.8). Type 3 glyph procedures are scanned like other content. | | 6.2.11.7 (level U) | Every font has a `/ToUnicode` or is of an exempt kind: WinAnsi, MacRoman or MacExpert encoding; Type 1 and Type 3 whose `Differences` names are in the Adobe Glyph List; Adobe-GB1, CNS1, Japan1 or Korea1 CID fonts; non-symbolic TrueType. | | 6.3 | Annotation types (no 3D, Sound, Screen, Movie or unknown types); `/F` with Print and without Invisible, Hidden, NoView and ToggleNoView; an appearance dictionary with only `/N`, a stream (a dictionary of states for button widgets); an appearance for every annotation except Popup, Link and zero-sized rectangles. | | 6.4, 6.5 | Widget and field `/A` and `/AA`; `/NeedAppearances`; `/XFA`; `/NeedsRendering`; catalog and page `/AA`; action types (only GoTo, GoToR, GoToE, Thread, URI, Named, SubmitForm; Named only NextPage, PrevPage, FirstPage, LastPage); a JavaScript name tree. | | 6.6 | Metadata stream present, unfiltered, well-formed, claiming the requested part and level. | | 6.8 | Part 2: **any embedded file** or file attachment annotation is refused (the standard allows PDF/A attachments in part 2; OnePDF does not write or verify them). Part 3: `/F` and `/UF`, a valid `/AFRelationship`, an `/EF` stream with a MIME `/Subtype` and `/Params /ModDate`, and a listing in an `/AF` array. | | 6.9 | Optional content configurations: a unique `/Name`, no `/AS`. | ## Not checked These need the font programs, colour management or XMP schema validation that OnePDF leaves to a validator, or concern features OnePDF does not write: - 6.1.6 hexadecimal string syntax, 6.1.8 name encoding (the writer produces valid ones; foreign files are not rechecked). - 6.1.13: DeviceN with more than 32 colourants, CIDs above 65535. - 6.2.4.2 overprint mode with ICCBased CMYK; 6.2.4.4 spot colour `Colorants` and Separation consistency. - 6.2.8.3 JPEG 2000 details. - 6.2.11: whether every referenced glyph exists in a foreign font program (6.2.11.4.1-2), width consistency between `/Widths` and a foreign font program (6.2.11.5), `CIDSet` and `CharSet` accuracy, TrueType `cmap` contents (6.2.11.6), CIDSystemInfo compatibility, `ToUnicode` values (not 0, FEFF, FFFE), `ActualText` (level A). Fonts written by `AddFont` and the substituted standard fonts are accepted by the validator on the test set. - 6.4.3 signatures: a PDF/A save rewrites the file, which invalidates existing signatures; signing a PDF/A file afterwards is an incremental update and is not checked against 6.4.3. - 6.6.2.3 XMP property types and extension schemas in an existing packet that already claims the requested part and level. - 6.9 `/Order` completeness. - Unused (unreachable) objects: a full save drops them, so they cannot make the output fail. - Fonts used only in text render mode 3 are not exempted from embedding; embed them, or keep the file out of PDF/A. ## Verification `tests/OnePdf.Tests/PdfA` generates documents with embedded TrueType and CFF fonts, standard fonts through the Fonts package, JPEG and raw images with alpha, transparency and blend modes, vector paths, filled and flattened forms, annotations, multiple pages with page operations and a stamp, and attachments; saves each as 2b, 2u, 3b and 3u and runs the veraPDF command-line validator (`ONEPDF_VERAPDF` names its launcher script; the tests skip without it). The refusals, the sRGB profile, the XMP round trip and determinism have tests of their own. The `pdfa` fuzz target checks, prepares and saves arbitrary input and requires that any file it writes passes `CheckPdfA`. ## Limits and known gaps - Standard fonts are subset to every glyph their encoding can reach (about 220 per face; a test file with four faces went from 834 KB to 112 KB), not to the glyphs the pages show, because finding those would mean reading all content. Liberation is licensed under the SIL Open Font License, whose FAQ (entry 1.12) allows embedding OFL fonts in a document "either in full or a subset"; the subset keeps the face name behind its subset tag. - A font from a provider other than the Fonts package must be a TrueType-flavoured font with a Unicode `cmap`, and its `fsType` must allow embedding; otherwise the font stays unembedded and is reported. - `PreparePdfA` and PDF/A saves read the whole document (every reachable object and content stream). Time and memory grow with the file; `PdfLimits` apply. # PDF/UA and PDF/A level A OnePDF can write **PDF/UA-1** (ISO 14289-1), **PDF/UA-2** (ISO 14289-2, on PDF 2.0; see [PDF/UA-2](#pdfua-2)) and **PDF/A-2a / 3a** (ISO 19005-2 and -3, level A: tagged), alone or in one file, and can check a document against the requirements it is able to verify. ## What is promised, and what is not - **Only for tagged documents: ones OnePDF laid out or created with tags, and existing files tagged through [the tagging API](https://recursiveai.org/onepdf/docs/tagging/).** The input must already be a tagged document: a structure tree, marked content on every painted mark, alternate text where it is needed, a title. OnePDF checks that and repairs the clerical parts; it **does not tag on its own judgement, guess alternate text, invent titles or guess a language**. The tagging API writes the structure a caller decides on (the caller supplies every role and every alternate text); OnePDF never works the roles out. It is not a converter for arbitrary files. An untagged file is refused with the reasons. - **A clean check is not proof of conformance.** `CheckPdfUA` verifies the rules listed below and nothing else; PDF/UA also asks for things only a person can judge (a logical reading order, meaningful alternate text, sufficient contrast, correct heading use). The tests run the veraPDF validator (PDF/UA-1, PDF/A-2a, PDF/A-3a) as a black box on the documents OnePDF writes; passing it is evidence, not certification. Use a validator and a screen-reader review before claiming conformance for a document. - It writes the identification only when every check it makes passes. ## API ```csharp LayoutDocument layout = LayoutDocument.Compose(doc => { doc.Title("Annual report").Author("A. Writer").Language("en-US"); doc.Page(page => { /* headings, tables with header cells, figures with .AltText(...) */ }); }); using var file = File.Create("report.pdf"); layout.Save(file, new PdfSaveOptions { PdfUA = new PdfUAOptions(), // PDF/UA-1 PdfA = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.A }, // optional: PDF/A-2a as well }); ``` | | | |---|---| | `PdfSaveOptions.PdfUA` (`PdfUAOptions`) | A full save prepares, checks and writes, or throws `PdfUAException` and writes nothing. With `PdfA` too, the failures of both come in one `PdfAException` (PDF/UA entries have clauses starting `PDF/UA-1`). Needs an editable document. Not available for `SaveIncremental`. | | `PdfUAOptions.Language` | A `/Lang` to write when the catalog has none. Without it, a missing language is a violation. | | `PdfUAOptions.EmbedLibraryFonts` | Default true: standard fonts added by `AddStandardFont` become embedded Liberation faces when the Fonts package is registered (as for PDF/A). | | `PdfAConformance.A`, `PdfAOptions.Language` | PDF/A level A; `Language` is the same fallback. | | `PdfDocument.PreparePdfUA(options)` | Makes the repairs below in memory and returns what is still wrong. | | `PdfDocument.CheckPdfUA()` | Reports every violation as the document stands; changes nothing; works on read-only documents. | | `PdfUAException.Violations` | `PdfUAViolation`: `Checkpoint` (Matterhorn Protocol checkpoint, such as `13` for graphics), `Clause` (ISO 14289-1), `Message`, `ObjectNumber`, `PageIndex`. At most 2000 are listed; more than twenty of one kind are cut off. | | `LayoutDocument.Author`, `Subject`, `Keywords` | Information dictionary entries (and XMP `dc:creator`, `dc:description`, `pdf:Keywords` in a PDF/A or PDF/UA save). `Title` and `Language` existed already. | A layout document reaches these through `LayoutDocument.Save(stream, PdfSaveOptions)`. That path lays the document out through the streaming writer, then reopens the finished file and saves it through `PdfDocument`; **the whole file is held in memory while it is checked and rewritten**. The streaming `Save(stream, PdfDocumentWriterOptions)` cannot do this: refusing a document needs the finished structure tree, every page's content and the final font set, which the streaming writer by design does not keep. For documents too big for that, write with the streaming path and check the result (`CheckPdfUA`) rather than rewriting. ## Repairs (no judgement needed) | Repair | Requirement | |---|---| | `/MarkInfo << /Marked true >>` when a structure tree exists | ISO 14289-1 7.1 | | `/Lang` from `PdfUAOptions.Language` when the catalog has none | 7.2 | | `/ViewerPreferences /DisplayDocTitle true` when there is a title | 7.1 | | XMP packet with `pdfuaid:part` 1 and `dc:title` taken from the Info title (declared as an extension schema when PDF/A is also requested, ISO 19005-2 6.6.2.3) | 5, 7.1 | | `/Tabs /S` on pages that have annotations | 7.18.3 | | `/Contents` on a link annotation that has none: the structure element's `/Alt` or `/ActualText`, else the link's address (layout links already carry the text they cover) | 7.18.5 | | Liberation faces for standard fonts OnePDF added | 7.21.4.1 | An XMP packet that already claims everything requested is kept; any other packet is replaced by one built from the Info dictionary (as for PDF/A). ## What is checked (and refused) Each finding names the Matterhorn checkpoint and the ISO 14289-1 clause. - **Tagging (01, 06; 7.1):** `/MarkInfo /Marked true`, `/Suspects` not true, a `/StructTreeRoot`; every painted mark (paths, text, images, shadings, forms, inline images) is inside marked content that is either an `/Artifact` or carries an `/MCID`; tagged content is not nested in an artifact; every `/MCID` used on a page is referenced by the structure tree; a page with tagged content has `/StructParents`. Content inside form XObjects is taken to inherit the tagging of the `Do` that paints it and is not scanned. - **Structure types (02; 7.1):** every type is one of the ISO 32000-1 standard types or reaches one through the `/RoleMap`; no circular mapping; no standard type remapped. PDF 2.0-only types (`Aside`, `Title`, …) need a mapping. - **Metadata and dictionaries (06, 07; 7.1):** XMP packet with `pdfuaid:part` 1 and a non-empty `dc:title`; `DisplayDocTitle` true. - **Language (11; 7.2):** a catalog `/Lang` that is a plausible BCP 47 tag. - **Figures (13; 7.3, 7.7):** `Figure` and `Formula` elements have `/Alt` or `/ActualText`. - **Headings (14; 7.4):** the first numbered heading is H1; a numbered heading is at most one level deeper than the one before; `H` and `H1`…`H6` are not mixed. - **Tables (15; 7.5):** at least one `TH` cell; `TR` in `Table`/`THead`/`TBody`/`TFoot`; cells in `TR`; every row covers the same number of columns once `ColSpan` and `RowSpan` are counted. - **Lists (16; 7.6):** `L` contains `LI` (or a `Caption`); `LI` is in `L`; `Lbl` and `LBody` are in `LI`. - **Annotations (28; 7.18):** every link annotation is an object reference inside a `Link` structure element and has `/Contents` (or its element has `/Alt`); widgets are in `Form` elements and have a `/TU`; other annotations (except Popup, TrapNet, PrinterMark) are in the tree and have `/Contents`; each has a `/StructParent`; pages with annotations have `/Tabs /S`. - **Fonts (31; 7.21):** every font other than Type 3, in page and form resources, has an embedded font program; a font has a `/ToUnicode` map, or an encoding that determines Unicode (a standard encoding without `/Differences`, or an Adobe CJK ordering). - **Content that a save would lose (31 for fonts, else 01; 7.21.4.1, 7.1):** a stream that cannot be read, decrypted or decoded (corrupt or truncated filter data) and a reference to a needed part that leads to no object, such as a missing `/FontFile2`. The finding names the object and what it is. A PDF/UA save, like a PDF/A save, never drops content: it fails with these findings instead (the full list of what counts is in [pdfa.md](https://recursiveai.org/onepdf/docs/pdfa/)). In PDF/UA-2 the clause is that of the matching checkpoint. - **Security (26; 7.16):** an encrypted document, or new encryption in the save options, must keep the accessibility-extraction permission. Not checked: reading order, contrast and colour use, the meaning of alternate text, heading semantics beyond the level sequence, `Headers`/`ID` association of complex tables, scope attributes, optional content names, embedded file descriptions, media and flicker rules, XFA and JavaScript rules, fonts used only in invisible text, the Unicode mapping of foreign symbolic fonts, forms inside XObjects. Bookmarks for long documents (ISO 14289-1 7.17) are not required by this check. ## PDF/UA-2 PDF/UA-2 (ISO 14289-2) is built on PDF 2.0. Choose it with `PdfUAOptions.Part`: ```csharp layout.Save(file, new PdfSaveOptions { PdfUA = new PdfUAOptions { Part = PdfUAPart.Part2 } }); ``` The promises are those of part 1 (above): tagged documents only (laid out or created by OnePDF, or tagged through [the tagging API](https://recursiveai.org/onepdf/docs/tagging/) with roles the caller supplies), no tagging on OnePDF's own judgement, no invented titles, alternate text or language, **and a clean check is not proof of conformance**. A part-2 save is checked with veraPDF's `ua2` flavour as a black box in the tests; passing it is evidence, not certification. | | | |---|---| | `PdfUAOptions.Part` | `PdfUAPart.Part1` (default) or `PdfUAPart.Part2`. Part 1 output is unchanged. | | `PdfDocument.CheckPdfUA(PdfUAPart part, ct)` | The check against the given part. A part-2 check of a file that is not PDF 2.0, or whose elements name no namespace, says so. | | `PdfDocument.PreparePdfUA(options)` | With `Part2`, also makes the namespace repairs below (the version is written by the save). | | `PdfUAViolation.Part`, `.Clause` | 1 or 2, and the clause of that part. | | `PdfDocumentWriter.SetBookmarkElement`, `.SetDestinationElement` | Tie a bookmark or named destination to the structure element its target is in; a writer producing PDF 2.0 (`Version` "2.0") then writes structure destinations. | | `StructureGroup`, `Slot.Aside()`, `.Footnote()`, `.Title()`, `.Group(type)` | Layout: group content in a `Sect`, `Div`, `Part`, `BlockQuote`, `Aside`, `FENote` or `Title` element. | **Repairs for part 2** (no judgement): | Repair | Requirement | |---|---| | The file is written as PDF 2.0 (`%PDF-2.0`) | ISO 14289-2 requires a PDF 2.0 file | | `/Namespaces` on the structure tree root and `/NS` on every element: the PDF 2.0 namespace (`http://iso.org/pdf2/ssn`) for the types it defines, the PDF 1.7 namespace for the 1.7 types it lacks (`TOC`, `TOCI`, `Reference`, `Art`, `BlockQuote`, `Code`, `Index`, `Quote`, `BibEntry`, `Private`) | 8.2.4 (ISO 32000-2 14.7.4.2) | | `Note` becomes `FENote` | ISO 32000-2 14.8.4.4.2 | | `/Ref` on a `TOCI` taken from the structure destination of the link inside it | 8.2.5.8 | | XMP `pdfuaid:part` 2 and `pdfuaid:rev` 2024, with the title | 5, 8.11.1 | | Everything the part-1 repairs do that part 2 also asks for: `/MarkInfo`, `/Lang` from `PdfUAOptions.Language`, `DisplayDocTitle`, `/Tabs /S`, link `/Contents`, Liberation faces for standard fonts | 6.2, 8.4.4, 8.11.2, 8.9.3.3 | **Refused for part 2** (each finding names the clause of ISO 14289-2; the clause numbers are those that veraPDF's `ua2` profile reports the rule under, and a rule it does not check is filed under the nearest heading): - **Tagging (8.2.2):** real content outside marked content or an artifact; MCIDs missing from the tree; a missing `/StructParents` (and the part-1 tagging findings). - **Namespaces (8.2.4):** an element naming no namespace or a namespace that is neither the PDF 2.0 nor the PDF 1.7 one; a type its namespace does not define. Custom types and namespace role maps are not written: they are refused. - **Headings (8.2.5.12):** the unnumbered `H` is not allowed; use `H1`…`H6`. Heading level order is **not** checked for part 2 (neither does veraPDF's profile): that is a person's judgement. - **Table of contents (8.2.5.8):** a `TOCI` with no `/Ref` that cannot be filled from its link. - **Links (8.2.5.20):** a link annotation inside a `Link` or `Reference` element; no `/Contents` demand (the repair supplies it). - **Lists (8.2.5.25):** a list with labels names its numbering (`ListNumbering` other than `None`). Nesting rules of the structure table (`L` contains `LI`, `LI` contains `Lbl` and `LBody`, rows in tables) are checked as in part 1. - **Tables (8.2.5.26):** regular tables (rows cover the same number of columns). A header cell is **not** demanded (veraPDF's profile does not demand it; layout tables still write one). - **Figures (8.2.5.28.2):** `Figure` has `/Alt` or `/ActualText`; `Formula` as in part 1. - **Destinations (8.8):** every link, GoTo action and outline item that goes inside the document is a structure destination, not a page. OnePDF cannot choose the element for an existing page destination, so a file with one is refused; layout and `PdfDocumentWriter` (version 2.0, `SetBookmarkElement`/`SetDestinationElement`) write structure destinations. - **Language, metadata, viewer preferences, fonts (8.4.4, 8.11, 8.4.5.5.1):** as in part 1, with `pdfuaid:rev`. - **Pages with annotations (8.9.3.3):** a `/Tabs` entry (A, W or S; the repair writes S). - **PDF 2.0 file (general):** a check of a file that is not PDF 2.0 says so; a save always writes 2.0. **With PDF/A:** PDF/A-2 and PDF/A-3 are PDF 1.7 formats, so `PdfA` together with `Part2` throws `PdfAException` (one violation) and writes nothing. PDF/A-4 is not supported. **Layout.** A layout document saved with `Part2` is laid out as PDF 2.0: bookmarks, anchors and table-of-contents links go to the structure element they point at (for an anchor around a heading, the heading; for content with no element of its own, the element around it). Headings are `H1`…`H6` as before. `Aside()`, `Footnote()` and `Title()` write the 2.0-only types `Aside`, `FENote` and `Title`, which part 1 refuses (they have no ISO 32000-1 equivalent and OnePDF writes no role map for them). Layout has no inline `Em`/`Strong`. Author-supplied things stay the author's: title, language, alternate text. **Inline, caption, ruby, warichu and MathML structure (part 2).** Each finding names its clause of ISO 14289-2; all but the associated-file rule are in veraPDF's `ua2` profile too, and the tests compare both. | Rule | Clause | |---|---| | `Ruby` has exactly `RB`, `RT` or `RB`, `RP`, `RT`, `RP`, in that order | 8.2.5.23 | | `Warichu` has exactly `WP`, `WT`, `WP` | 8.2.5.24 | | A `Caption` is the first or the last child of its parent | 8.2.5.27 | | MathML elements (namespace `http://www.w3.org/1998/Math/MathML`, accepted by the namespace check) occur only below a `Formula` or another MathML element | 8.2.5.29 | | A file in a `Formula`'s `/AF` has an `/AFRelationship` and embeds its data (`/EF`) | 8.2.5.29 (ISO 32000-2 7.11.3, 14.13) | | The structure type table (ISO 14289-2 / ISO/TS 32005 Table 5): a parent may not contain certain children (this is what keeps `Em` and `Strong` inline: they may not hold `P`, `L`, `Table`, `Sect` and the other block elements, and `Document`, `Sect`, `Table`, `L` and the like may not hold them directly); some children occur at most once (`Caption`, `THead`, `TFoot`, `H`, `Sect` below a heading); `Document`, `Sect`, `Table`, `TR`, `L`, `TOC` and similar do not hold marked content directly | Table 5 | The table is data in `PdfUA2Table.cs`. Only standard elements are compared (custom types are refused by the namespace rule). An element counts as a grouping element, for the `Link`/`Annot`/`FENote`/`Reference` caption rule, when it holds no marked content itself. Not checked, because they need a person's judgement or the clause text is not fixed enough to decide: whether `Em` and `Strong` express real emphasis; whether a `Caption` belongs to what it sits beside; whether ruby text is the reading of its base; whether MathML is a faithful or complete rendering of the formula, and whether the associated file is MathML at all; the 51 table rows that depend on whether an element is used as a grouping or non-grouping element (`Annot`, `Reference`, `Form`, `Link`, ... below), and `StructTreeRoot` holding exactly one `Document`; the rest of "Not checked" above. ## PDF/A level A Level A is level U plus a tagged logical structure (ISO 19005-2, 6.7). A save as 2a or 3a repairs `/MarkInfo` and the language fallback, and checks: `/MarkInfo` (6.7.2.2), a structure tree (6.7.3.3), standard or role-mapped types and no circular or remapped types (6.7.3.4), a language (6.7.4), content that is tagged or an artifact, and alternate text for figures. Everything in [pdfa.md](https://recursiveai.org/onepdf/docs/pdfa/) applies too. XMP carries `pdfaid:conformance` `A`. ## Verification `tests/OnePdf.Layout.Tests/PdfUATests.cs` lays out a document with headings, a multi-page table with header cells and spans, nested lists, figures with alternate text, internal and external links, a header and a footer with page totals, and saves it as PDF/UA-1, PDF/A-2a, PDF/A-3a, and 2a and 3a combined with PDF/UA-1; each file passes veraPDF (the combined ones under both profiles). `PdfUA2Tests.cs` in both test projects does the same for PDF/UA-2 under the `ua2` flavour: a richer layout document (title, table of contents, bookmarks, aside, footnote, inline objects, lists, a table with row and column headers, figures, links) and writer-made documents, a refusal for each rule above that veraPDF also reports, a check that a file damaged in each way veraPDF finds is also found by `CheckPdfUA`, and the committed reference `testdata/layout-references/documents/accessible-ua2.pdf`. `tests/OnePdf.Tests/PdfUA` does the same with documents made by `PdfDocumentWriter`, and has a test for every refusal above and for `CheckPdfUA` listing all violations of a deliberately bad document. veraPDF is run through `ONEPDF_VERAPDF`; the tests skip without it. The `pdfa` fuzz target also covers PDF/UA and level A: a file that is written must pass the checkers. # Tagging existing PDFs There are two layers. The **happy path** (`PdfDocument.Tag`) covers the common case: you say what each block is, OnePDF builds the structure the way the rules below describe, and each default is an option. The **plan API** underneath (`CreateTaggingPlan`, `PdfTagElement`, `ApplyTagging`) gives full control over every element and every reference; the happy path is built on it, not beside it. Use the happy path first, and drop to the plan for what the options do not reach. OnePDF can add a structure tree to a PDF that has none. **You decide the roles; OnePDF writes the structure.** A tool that works out what each block is (a heading, a paragraph, a list item, a table cell, a figure) builds a plan from a text extraction, and `ApplyTagging` turns that plan into marked content in the page streams, a structure tree, a parent tree and the catalog entries a tagged PDF needs. OnePDF never guesses a role, never invents alternate text and never judges whether the roles are right. A clean PDF/UA check afterwards is evidence, not proof of conformance (see [pdfua.md](https://recursiveai.org/onepdf/docs/pdfua/)). Pages look the same and extract the same afterwards. Only marked-content operators are added, text-showing operators are split at glyph boundaries without moving a glyph, and nothing is drawn differently. The tests check this against the extraction, the display list and two independent renderers. ## The happy path ```csharp using PdfDocument doc = PdfDocument.Open("in.pdf", new PdfOpenOptions { AllowEditing = true }); // Extract the whole document with the glyphs kept (running headers and footers are found across pages). IReadOnlyList text = doc.ExtractText(new PdfTextOptions { RetainGlyphs = true, UseStructureTree = false }); var options = new PdfTagOptions { Title = "Harbor tide tables 2026", Language = "en" }; options.Figures.Add(new PdfTagFigure(0, new PdfRectangle(100, 300, 300, 450), "Chart of the tide heights")); doc.Tag(text, block => /* your decision for this block */ block.Block.Lines[0].Text switch { var t when IsHeading(t) => PdfBlockRole.Heading(1), var t when IsListItem(t) => PdfBlockRole.ListItem, var t when IsTableRow(t) => PdfBlockRole.Table, _ => PdfBlockRole.Paragraph, // or null / PdfBlockRole.Artifact for decoration }, options); doc.Save("out.pdf", new PdfSaveOptions { PdfUA = new PdfUAOptions() }); // or Save / SaveIncremental ``` `doc.Tag(...)` is `doc.PlanTagging(...)` followed by `doc.ApplyTagging(plan)`. `PlanTagging` returns the plan without touching the document, so you can add to it or change it with the plan API before applying. **The decision is yours.** The delegate returns a `PdfBlockRole` for every block it is asked about: `Heading(1..6)`, `Paragraph`, `ListItem`, `Table`, `TableContinued`, `Figure(alt)`, or `Artifact` (also what `null` and `default` mean). OnePDF never works a role out. ### What OnePDF builds from the roles | Role | Structure | Option | |---|---|---| | `Heading(n)` | `H1`–`H6` holding the block | | | `Paragraph` | `P` holding the block | | | `ListItem` | Consecutive list items (artifacts in between do not break the run) share one `L`; each is an `LI`. Any other role, a figure or a table ends the list. | | | – the label | A first word that is a bullet or a number (`•`, `-`, `1.`, `(2)`, `a)`, `iv.`) becomes `Lbl`, the rest of the item `LBody`. An item without a label word is an `LI` with an `LBody` only. | `SplitListLabels` (default true), `IsListLabel` | | – blocks of several items | A block with several lines is cut into items at each line that starts with a label word; lines before the first label word form their own item. Without any label word a block is one item. | `SplitListBlocks` (default true) | | `Table` | A `Table`; every line is a `TR`, every cell of a line (`PdfTextLine.CellStarts`) a cell. The first row is `TH` with scope `Column`, the others `TD`. | `TableHeaderRows` (1), `TableHeaderColumns` (0), `HeaderRowScope` (Column), `HeaderColumnScope` (Row) | | `TableContinued` | More body rows added to the table before it, also across pages. Without a table before it, it starts a new one. | | | `Figure(alt)` | A `Figure` holding the block's own text, with `Alt`. | | | figures by area | Each `PdfTagFigure` (page, area, alt) becomes a `Figure` of the images and paths inside the area, placed before the first block of its page that lies lower. | `Figures` | | header, footer | Blocks that extraction flags as running headers or footers become pagination artifacts (`/Subtype /Header` or `/Footer`); the delegate is not asked about them. | `KeepHeadersAndFooters` (false) | | link annotation | The words its rectangle covers (word centres inside it, on the block that holds most of them) get a `Link` element with the annotation as an object reference, inside the element of that block. The annotation gets `/Contents` from the words if it has none. A link over an artifact, a list label or no text is left alone and noted in `Warnings`. | `TagLinks` (true) | | anything else | A mark or text that no element holds is an artifact. | | `PdfTagOptions.Customize` is called for every element the happy path creates (including `L`, `LI`, `Lbl`, `LBody`, `TR`, `TH`, `TD` and `Link`) with the block it came from and before it gets children. Use it to set `Alt`, `Lang`, `ActualText`, `Scope`, `RowSpan` or `ColSpan`. Elements made for `Figures` have no block and do not go through the hook; set their text in the `PdfTagFigure`. Limits of the happy path, by design: one role per block (a block that holds a heading and a paragraph has to be tagged with the plan API, which can reference lines and words), a table row is a line of the block, and spans are not guessed. The result of `Tag` is the `PdfTaggingResult` of `ApplyTagging`, with the plan's warnings in front. ## The plan API ```csharp using PdfDocument doc = PdfDocument.Open("in.pdf", new PdfOpenOptions { AllowEditing = true }); // 1. Extract with the glyphs kept: the plan refers to this extraction. IReadOnlyList pages = doc.ExtractText(new PdfTextOptions { RetainGlyphs = true, UseStructureTree = false }); // 2. Assign roles. PdfTaggingPlan plan = doc.CreateTaggingPlan(); plan.Title = "Harbor tide tables 2026"; plan.Language = "en"; plan.Add(PdfTagRole.H1, PdfTagContent.Block(pages[0], 0)); plan.Add(PdfTagRole.P, PdfTagContent.Block(pages[0], 1)); PdfTagElement list = plan.Add(PdfTagRole.L); foreach (PdfTextLine line in pages[0].Blocks[2].Lines) { PdfTagElement item = list.Add(PdfTagRole.LI); item.Add(PdfTagRole.Lbl, PdfTagContent.Words(pages[0], line, 0)); item.Add(PdfTagRole.LBody, PdfTagContent.Words(pages[0], line, 1, line.Words.Count - 1)); } plan.Add(PdfTagRole.Figure, PdfTagContent.Graphics(pages[0], new PdfRectangle(100, 300, 300, 450))).Alt = "Chart of the tide heights"; plan.ArtifactHeadersAndFooters(pages); // 3. Write it, then save. PdfTaggingResult result = doc.ApplyTagging(plan); doc.Save("out.pdf", new PdfSaveOptions { PdfUA = new PdfUAOptions() }); // or Save / SaveIncremental ``` The document must be opened with `AllowEditing`. `ApplyTagging` changes only the editable view, so nothing is written until you save. Both `Save` and `SaveIncremental` work; an incremental save appends to the original bytes. A save with `PdfSaveOptions.PdfUA` also repairs the clerical parts (XMP identification, display of the title, tab order) and refuses the file with every violation listed when something else is missing, for example fonts that are not embedded. ## Roles and elements `PdfTagRole` names the standard structure types: `P`, `H`, `H1`–`H6`, `L`, `LI`, `Lbl`, `LBody`, `Table`, `THead`, `TBody`, `TFoot`, `TR`, `TH`, `TD`, `Caption`, `Figure`, `Formula`, `Link`, `Span`, `Div`, `Part`, `Art`, `Sect`, `BlockQuote`, `Quote`, `Note`, `Reference`, `Code`, `Form`, `TOC`, `TOCI`. The enum names are the PDF names. An element (`PdfTagElement`) has a role and children that are other elements or content. It can carry `Alt`, `ActualText`, `Lang`, and for `TH`/`TD` a `Scope`, `RowSpan` and `ColSpan`. `plan.Add(role, content…)` adds a top-level element, `element.Add(role, content…)` a child, and `element.Add(content)` content to an existing element. The writer puts everything under one `Document` element. An element may hold content from several pages; a table can continue over pages. Nesting is checked: rows belong in a table, cells in a row, list items in a list, `Lbl` and `LBody` in a list item, and content cannot sit directly in a `L`, `LI`, `Table` or `TR`. A violation throws `PdfTaggingException` with `PdfTaggingFailure.InvalidNesting` before anything is changed. ## Content references References name things the extraction found, so no new identifiers exist. They come from `PdfTagContent`: | Factory | Names | |---|---| | `Block(page, block or index)` | every glyph of a block | | `Lines(page, block, firstLine, count)` | a run of lines of a block, for example one list item of a block that holds a whole list | | `Words(page, line, firstWord, count)` | a run of words in one line, for example a bullet or a link's text | | `Cell(page, line, cellIndex)` | the words of one table cell of a line (`PdfTextLine.CellStarts`) | | `Graphics(page or pageIndex, area)` | the non-text marks (images, paths, shadings) whose bounds lie inside the area, in upright page space, give or take one point | | `Annotation(annotation)` | a link or other annotation, tied to the element by an object reference | Rules the writer enforces: - The extraction must have been made with `RetainGlyphs = true` and must come from the same document; otherwise the call throws `ForeignContent`. - The page must still show what the extraction saw. A page edited after the extraction throws `ContentChanged`. - Each glyph, mark and annotation belongs to at most one element. A second reference to the same thing, or two graphics areas that both contain a mark, throws `DuplicateContent`. - A blank glyph that nobody claimed (a space the font draws) goes with the text next to it, so a paragraph is not cut up by its spaces. - A `Link` element should hold the link's text and its `Annotation`. The annotation gets an object reference (`OBJR`) and a `/StructParent`. If it has no `/Contents`, the writer sets it from the element's `Alt`, else from the link's words (ISO 14289-1 7.18.5). ## Artifacts Anything painted that no element references becomes an artifact: rules, backgrounds, shapes, invisible text and text the plan leaves out. You can also say what it is: - `plan.Artifact(content, kind)` with `PdfArtifactKind.Header`, `Footer`, `Watermark`, `Layout`, `Page` or `Plain`. The first three are written as pagination artifacts, `Layout` and `Page` with their type, `Plain` as a bare `/Artifact`. - `plan.ArtifactHeadersAndFooters(pages)` marks every block extraction flagged as a running header or footer (whole-document extraction detects them by repetition). An element's claim on the same content wins. Artifact sequences the file already had are removed and rebuilt, so text the file called an artifact can be real content in the plan, and nothing ends up as tagged content inside an artifact. Other marked content the file had (`/Span`, `/OC` optional content and so on) is kept as it was. ## How the content is rewritten - **Splitting.** A text-showing operator whose glyphs belong to different elements is split at glyph boundaries into several operators with the same positions. A `TJ` array is split at its string boundaries and adjustments are carried over. Marked content never straddles `q`/`Q`, and a sequence that starts inside a text object ends inside the same one. - **Form XObjects.** A form drawn by `Do` whose content belongs to one element is wrapped whole at the `Do`. A form whose content belongs to several elements, or to elements and artifacts, is copied for that use: the page draws the copy, which carries its own marked content and `/StructParents`, and the tree reaches it through `MCR` entries with `/Stm`. The original form is left alone, so the same form drawn twice on a page or on two pages gets one copy per use, and nested mixed forms are copied at every level. A form that draws itself is refused as unreadable. - **Shared content.** Pages that share one content stream, or whose content is spread over several streams, get their own rewritten streams; the shared original is not modified. - **Inline images** are wrapped whole; their data is never rewritten. - **Hidden content.** Text and marks that optional content hides produce no drawing events, so they are wrapped as artifacts. - **Operators.** The operator count of the tagged page is checked against `PdfLimits.MaxOperatorsPerPage` before the page is written. ## Refusals `ApplyTagging` throws `PdfTaggingException` (with `Reason` and, where it applies, `PageIndex`) and leaves the document as it was: | Reason | When | |---|---| | `AlreadyTagged` | the catalog has a `/StructTreeRoot` or `/MarkInfo /Marked true`, or a page has marked content with an `/MCID` | | `ForeignContent` | a reference is from another document, from an extraction without glyphs, to a page the document does not have, or to an annotation that is not a page's | | `DuplicateContent` | the same glyph, mark or annotation is referenced twice | | `InvalidNesting` | an element is placed where its role cannot go | | `ContentChanged` | the page shows a different number of glyphs than the extraction saw | | `ContentUnreadable` | the page cannot be rewritten safely: a form draws itself or nests deeper than the recursion limit, or glyphs do not match their string | Calls also throw `PdfLimitException` when the tree would be deeper than `PdfLimits.MaxNestingDepth` or hold too many elements (while the plan is built), or when a tagged page would exceed the operator limit; `InvalidOperationException` when the document is not editable or the plan was applied before; `ArgumentException` for a plan from another document; and `OperationCanceledException` on cancellation. Documents that are tagged already are not retagged or merged: remove the structure yourself first if you mean to replace it. ## What is not done - No role is chosen and no alternate text is invented. A `Figure` without `Alt` makes the PDF/UA check, and a PDF/UA save, report it. - No reading order is derived from the plan beyond the order of elements and their content. - The visual result is unchanged by design, which also means defects in the source (fonts that are not embedded, text without a Unicode mapping) stay and are reported by the PDF/UA check. - Annotation appearance streams are not tagged; annotations are tied to the tree by an object reference. - Tagging splits a text operator where owners change, which reaches later glyphs by a different sum of the same advances (differences around 1e-13 pt). Extraction is unchanged for every committed test file, but on a page of thousands of overprinted copies, or glyphs with zero advance, a threshold in duplicate collapsing or word spacing can fall the other way for a glyph. Glyph order, text and positions stay the same. # Layout `OnePdf.Layout` generates documents from code: you describe pages with nested delegates, it lays them out (shaped text, tables, lists, images, headers and footers, page numbers) and writes a tagged PDF through OnePDF's streaming writer, one page at a time. It ships as the package `RecursiveAI.OnePdf.Layout` (`dotnet add package RecursiveAI.OnePdf.Layout`), which depends on `RecursiveAI.OnePdf`, `RecursiveAI.OnePdf.Fonts` and `RecursiveAI.OneFont`. ```csharp LayoutDocument.Compose(doc => { doc.Title("Quarterly report").Language("en"); doc.Page(page => { page.Size(PageSize.A4); page.Margin(2.Cm()); page.Header().Text("Acme Corp").FontSize(9).FontColor(Colors.Grey); page.Content().Column(col => { col.Spacing(10); col.Item().Heading(1).Text("Quarterly report").FontSize(22).Bold(); col.Item().Text("Sales rose in every region.").Justify(); col.Item().Table(table => { table.Columns(c => { c.Relative(2); c.Constant(60); }); table.CellBorder(0.5, Colors.LightGrey); table.HeaderRow(r => { r.Cell().Text("Region").Bold(); r.Cell().Text("Sales").Bold(); }); table.Row(r => { r.Cell().Text("North"); r.Cell().AlignEnd().Text("1,204"); }); table.Row(r => { r.Cell().Text("South"); r.Cell().AlignEnd().Text("987"); }); }); }); page.Footer().AlignCenter().Text(t => { t.FontSize(9); t.Span("Page "); t.CurrentPageNumber(); t.Span(" of "); t.TotalPages().ReservedDigits(2); }); }); }).Save("report.pdf"); ``` ## The fluent API `LayoutDocument.Compose` runs your delegate once and returns the finished description (the C# 14 extension `PdfDocument.Compose` does the same). Nothing is laid out until you save. - **Document** (`DocumentComposer`): `Title`, `Language`, `Direction`/`RightToLeft`, `Fonts` (the font registry, see [layout-text.md](https://recursiveai.org/onepdf/docs/layout-text/)), `Debug`, `Tagged(false)`, `WidowsAndOrphans`, `MaxPages`, and `Page` once per section. - **Page** (`PageComposer`): `Size` (`PageSize.A4` and the other constants, or width and height in points), `Landscape`, `Margin`, `Direction`, page numbering (`NumberStyle`, `StartNumber`, `LabelPrefix`) and the slots `Header`, `Footer`, `FirstPageHeader`/`FirstPageFooter`, `EvenPageHeader`/`EvenPageFooter`, `Background`, `Foreground` and `Content`. Each `Page` call starts a new section on a new page. - **Slots.** Every place that holds one child is a `Slot`. You chain decorators on it and end with one thing that fills it: `Text`, `Column`, `Row`, `Layers`, `List`, `Table`, `Image`, `Canvas`, `Spacer`, `PageBreak`, `Component` or `Content`. A slot takes exactly one such call. - **Units** are points. `10.Mm()`, `2.Cm()` and `1.Inch()` convert; `Colors` has named colours and `LayoutColor.FromHex("#336699")` parses a hex colour. ### Decorators Decorators wrap the slot from the outside in, in the order you write them: ```csharp LayoutDocument.Compose(doc => doc.Page(page => { page.Content().Column(col => { // Read from the outside in: a margin, then a rounded border with a background, then padding, then the text. col.Item() .Margin(8) .Border(1.5, Colors.Blue).Rounded(6).Background(Colors.LightGrey) .Padding(10) .Text("A rounded, padded box"); }); })).Save("chain.pdf"); ``` `Margin`, `Border`, `Background`, `Rounded`, `Opacity`, `Width`/`Height` (and `Min`/`Max`), `AspectRatio`, `Padding` and the nine-point `Align*` methods all become one box when the order allows it: margin first, then border and background and size, then padding, then alignment. Writing them in another order gives nested boxes, which look the same on the page and cost a little more; the result is correct either way. `Rounded` can sit anywhere. Spelled-out sides exist for every inset (`PaddingTop`, `MarginStart`, `BorderEnd` and so on); `Start` and `End` flip in right-to-left documents. Other decorators: `Rotate`, `Scale`, `Translate`, `KeepTogether`, `EnsureSpace`, `KeepWithNext`, `ExpandHeight`, `Name` (the label the debug overlay and errors use). `AlignMiddle` and `AlignBottom` make the slot as tall as the space offered, because there is nothing to centre in otherwise. - `Rotate` and `Scale` do not split their child: if the content would need a page break inside them, layout throws `PdfLayoutException`. Rotating by 90 degrees makes the child's width the page height, so put a `Width` box inside the rotation. - Text fills the width it is offered, so `AlignCenter().Text(...)` aligns the text, not a box around it. ### Gradient backgrounds `Background` also takes a `LayoutGradient`, linear at any angle or radial, with two or more colour stops: ```csharp LayoutDocument.Compose(doc => doc.Page(page => { page.Content().Column(col => { col.Spacing(10); // 90 degrees runs left to right; the first stop is reached in one corner and the last in the opposite one. col.Item().Height(60).Rounded(8) .Background(LayoutGradient.Linear(90, Colors.Blue, Colors.LightGrey)) .Padding(10).Text("Linear").FontColor(Colors.White); // Several stops, each with a position from 0 to 1, here in DeviceCMYK (DeviceGray works too; one space per gradient). col.Item().Height(60).Background(LayoutGradient.Horizontal( new LayoutGradientStop(0, PdfGradientColor.Cmyk(0, 1, 1, 0)), new LayoutGradientStop(0.3, PdfGradientColor.Cmyk(0, 0, 1, 0)), new LayoutGradientStop(1, PdfGradientColor.Cmyk(1, 0.6, 0, 0)))); // Circles around a centre given as fractions of the box. col.Item().Height(60).Background(LayoutGradient.Circle( [new LayoutGradientStop(0, Colors.White), new LayoutGradientStop(1, Colors.Blue)], centerX: 0.25, centerY: 0.5, extent: RadialExtent.FarthestCorner)); }); })).Save("gradients.pdf"); ``` - **Linear.** `Linear(angle, stops)`: the angle is in degrees clockwise from up, so 0 runs bottom to top, 90 left to right, 180 top to bottom (`Horizontal` and `Vertical` are 90 and 180). The gradient line passes through the centre of the box and is `|w sin a| + |h cos a|` long, so the first stop is reached exactly in one corner and the last in the opposite one. `Linear(angle, from, to)` is the two-colour form. - **Radial.** `Circle(stops, centerX, centerY, extent)`: the first stop is at the centre, the last on the circle that touches the closest or farthest side or corner (`RadialExtent`, default the farthest corner). Circles only; no ellipses. - **Stops.** Two or more `LayoutGradientStop(position, colour)` with positions from 0 to 1 that do not decrease, all in one colour space (a `LayoutColor` is DeviceRGB; `PdfGradientColor.Gray` and `.Cmyk` give the others). The end colours are held before the first and after the last stop. Two stops at one position make a hard edge. A bad list throws `ArgumentException` where the gradient is created. - **With the rest of the box.** The gradient fills the area inside the margin and is clipped to rounded corners; the border paints over it; `Opacity` fades it (the content stays opaque); `Rotate`, `Scale` and `Translate` carry it along; it works as the background of a layer, a page (`page.Background()`), a table row (`TableRowDescriptor.Background`) and a table cell (`CellBackground`). `BackgroundGradient` replaces `Background` when both are set on an element. - **Pages.** A box that splits over pages paints the whole gradient again in each piece, scaled to that piece, so every page shows the full transition. (A gradient that continues across the break would need the box's total height, which is unknown when the first piece is placed.) A table row or cell does the same. - **Output.** Each gradient is a PDF shading (axial type 2 or radial type 3, ISO 32000-2 8.7.4.5) painted with `sh` under the box's clip; stops become one exponential function or a stitching function. It is an artifact in tagged output and passes PDF/A and PDF/UA-1 checks (a PDF/A save refuses a CMYK gradient, as it does any DeviceCMYK colour, because OnePDF has no CMYK profile to offer). To fill any other shape in a canvas, see [Content creation](https://recursiveai.org/onepdf/docs/content-creation/#gradients). ### Text `Text("...")` returns a `TextDescriptor` with `FontSize`, `FontFamily`, `FontColor`, `Bold`, `Italic`, `Underline`, `Strike`, `LetterSpacing`, `LineHeight`, `Justify`, `AlignStart`/`AlignCenter`/`AlignEnd`, `FirstLineIndent`, `Language` and `Hyphenator`. `Text(t => ...)` builds rich text from `Span`, `Line`, `CurrentPageNumber`, `TotalPages` and `SectionPages`; a span has the same formatting methods plus `Superscript` and `Subscript`. Settings made on the descriptor are the default for its spans. A page number is shaped like any other text. A paragraph that holds a page number stays on one line (it does not wrap) and is as wide as its content. "Total pages" is not known until the end, so its width is reserved: `ReservedDigits` (default 4) says how many digits to leave room for. In a centred footer that reserve shows as a gap, so use `ReservedDigits(2)` for documents under 100 pages. Details of shaping, fallback fonts, bidirectional text and hyphenation are in [layout-text.md](https://recursiveai.org/onepdf/docs/layout-text/). ### Inline objects An icon, a small picture, a badge or a drawn checkbox can sit in a line of text and flow with it. In `Text(t => ...)`, `Inline(slot => ...)` puts a small element tree in the line (the slot takes any content, so a padded, bordered box with its own text works), `InlineImage(bytes, width, height)` a JPEG or PNG of a fixed size, and `InlineCanvas(width, height, surface => ...)` a drawing. Each returns an `InlineDescriptor` for the object's size, alignment, description and link. ```csharp LayoutDocument.Compose(doc => doc.Page(page => { page.Content().Text(t => { t.Span("Build "); t.InlineCanvas(10, 10, s => { s.Builder.SetFillRgb(0.1, 0.6, 0.2); s.Builder.Ellipse(0, 0, s.Width, s.Height); s.Builder.Fill(); }).AltText("passing").AlignTextBottom(); t.Span(" passed; the release notes "); t.Inline(s => s.Background(Colors.LightGrey).Padding(3, 1).Text("NEW").FontSize(8).Bold()).AltText("new"); t.Span(" and a logo "); t.InlineImage(File.ReadAllBytes("logo.png"), 12, 12).AltText("Company logo").AlignMiddle(); t.Span(" flow with the words around them."); }); })).Save("inline.pdf"); ``` - **Size.** A picture or canvas has the size you give. An `Inline` element takes its content's widest natural width (no wrapping) and the height that gives; `Width`, `Height` and `MaxWidth` override that, and with a `MaxWidth` the content wraps inside it. The element is laid out once and never splits: a page break inside it raises `PdfLayoutException`. - **Vertical alignment.** `Baseline(offset)` (the default) puts the object's own baseline on the line's baseline, raised by `offset` points; a picture's baseline is its bottom edge, and an element that contains text takes the baseline of its first line, so a badge's text lines up with the words around it (`InnerBaseline` sets it by hand). `AlignTop` and `AlignBottom` align to the top and bottom of the line box; `AlignMiddle` centres the object on the middle of the text (halfway between the font's ascent and descent); `AlignTextTop` and `AlignTextBottom` put its edges at the font's ascent and descent. The font is that of the text the object is in, and `FontSize` and the other format methods of the descriptor change it for that spot. - **Line height.** A line grows to hold an object taller than its text, above the baseline, below it, or (for top and bottom) at the side the alignment names; a line is never shorter than its text would make it. `FixedLineHeight()` keeps the height the text gives, and a taller object sticks out into the lines around it. - **Breaking and justification.** An object is one unbreakable unit, as wide as it is, with a line break allowed before and after it (it is U+FFFC, line break class CB in UAX #14), by first fit and by total fit alike. `KeepWithNeighbours()` removes both breaks. Justification widens the spaces and leaves the object alone. An object wider than the line sits alone on a line that overflows, and a note says so. Underlines and strike-throughs of the text around it stop at the object. - **Right to left.** The object has the direction of its position (bidi class ON, UAX #9), so between right-to-left words it is placed with them. - **Tagging and extraction.** In a tagged document an object with `AltText` is a `Figure` inside the paragraph, between the paragraph's content, with `/Alt`; the text extractor returns the description in its place (an invisible space under `/ActualText`, ISO 32000-2 14.9.4), and never a U+FFFC. `Artifact()` leaves the object out of the tree and out of extraction; a drawn canvas without a description is an artifact. A picture with neither description nor `Artifact()` is reported, as `Image` is. Put an `AltText` on the object (not only inside its content) when extraction should return it. - **Links.** `Hyperlink(uri)` and `LinkToAnchor(id)` on the descriptor link the object's box; inside an `Inline` slot, `Hyperlink`, `LinkToAnchor` and `Anchor` work as they do anywhere. The link annotation is tied to a `Link` structure element that holds the figure. - **Limits.** One text can hold as many objects as it has characters (the paragraph limit of 16 million); a text with page numbers cannot hold objects. Outside layout, `TextParagraph` takes `TextSpan.ForInline(new InlineBox(width, height) { Paint = ... })`, and `TextRenderer.Draw` draws it; see [layout-text.md](https://recursiveai.org/onepdf/docs/layout-text/#inline-objects). ### Rows, columns, layers, lists, images - `Column(c => ...)` with `Spacing`, `ItemAlignment` and `Item()`; `Row(r => ...)` with `Spacing`, `AlignTop`/`AlignMiddle`/`AlignBottom`, and `ConstantItem(width)`, `RelativeItem(weight)` or `AutoItem()`. - `Layers(l => ...)`: `PrimaryLayer()` sets the size and may span pages; `Layer()` slots (a watermark, a frame) are laid out at its size. - `List(l => ...)`: `Bulleted`, `Numbered(style)`, `Bullet`, `Start`, `Suffix`, `MarkerFormat`, marker font, size, colour, width and gap, and `Item()` (or `Item("custom marker")`). Lists nest by putting a `List` inside an item. - `Image(bytes | path | PdfCreatedImage)` for JPEG and PNG with `FitWidth` (the default), `FitHeight`, `Contain`, `Cover`, `Stretch`, `Dpi` and `Align`. Give figures `AltText(...)`, or `Artifact()` for decoration. - `Svg(bytes | string | SvgImage)` places an SVG as vector content, with the same fits, `Dpi`, `Align` and `AltText` (the SVG title is the default); see [layout-svg.md](https://recursiveai.org/onepdf/docs/layout-svg/). - `Canvas(height, surface => ...)` hands you a clipped drawing surface; `surface.DrawSvg(svg, x, y, width, height)` draws an SVG into it. ```csharp LayoutDocument.Compose(doc => doc.Page(page => { page.Content().Column(col => { col.Spacing(8); col.Item().Width(120).Image(File.ReadAllBytes("logo.png")).AltText("Company logo"); col.Item().Width(200).Height(80).Image("photo.jpg").Cover().AltText("A photo"); }); })).Save("images.pdf"); ``` ### Multi-column flow `MultiColumn(cols => ...)` pours its items through equal columns: down the first, then down the next, and onto the next page after the last column. It goes anywhere a container does: the page's content slot, a section, a box, or between full-width items of a `Column`, as in the sample below. ```csharp string text = string.Concat(Enumerable.Repeat("Text poured through columns breaks between them the way it breaks between pages. ", 12)); LayoutDocument.Compose(doc => { doc.Title("Newsletter").Language("en"); doc.Page(page => { page.Size(PageSize.A5); page.Margin(28); page.Content().Column(col => { col.Spacing(8); col.Item().Heading(1).Text("Harbour Gazette").FontSize(20).Bold(); // Two columns that flow down, then across, then onto the next page. The last page is balanced. col.Item().MultiColumn(cols => { cols.Count(2).Gap(16).Rule(0.5, Colors.Grey).Spacing(6); cols.Item().Heading(2).Text("Tides").FontSize(12).Bold(); cols.Item().Text(text).FontSize(9).Justify(); cols.ColumnBreak(); cols.Item().Text("Starts the second column.").FontSize(9); cols.Item().Text(text).FontSize(9).Justify(); }); col.Item().Text("Back to full width.").FontSize(8); }); }); }).Save("columns.pdf"); ``` - **Set-up:** `Count` (1 to 32, default 2), `Gap` (default 12 pt), `Rule(width, colour)` for a line in the middle of each gap (decoration, drawn only between columns that have content), `Spacing` between items inside a column, `Item()` for content and `ColumnBreak()` to end the column (ignored at the top of a column, so no empty column appears; after the last column it moves on to the next page). The element objects are `MultiColumn` and `ColumnBreak`. - **Breaking:** a column plays the part of a page. Paragraphs split with the widow and orphan rules, tables split between rows and repeat header rows in every column they reach, `KeepTogether`, `KeepWithNext` and `EnsureSpace` apply, and a heading never ends a column. An element taller than a column splits or raises `PdfLayoutException` exactly as it would on a page. - **Fill:** `Balanced()` (the default) shortens the columns of the last region, where the content ends, to the least height that still holds it, so they end at about the same level. Regions that run to the bottom of the page are unaffected. `Sequential()` fills each column to the bottom before starting the next. The search is a bisection with at most 12 trial layouts of the remaining content and a resolution of a quarter of a point, whatever the content. Content that cannot be divided (a paragraph shorter than the widow and orphan rules need, one tall picture) stays in as few columns as it needs and the others stay empty; balancing never breaks a rule to even the columns out. - **Direction:** in a right-to-left document the first column is the rightmost; the flow still reads column 1, 2, 3. - **Tagging and extraction:** the structure tree and the content order follow the flow, column by column. A paragraph that splits across columns is one `P` with content in both. Text extraction returns the flow order. - **Limits:** with unbounded width a flow has one column (a note is recorded). A `PageBreak` placed directly in the flow ends the column like a `ColumnBreak`. ### Tables `Table(t => ...)` takes `Columns` (`Auto`, `Constant`, `Relative`), `HeaderRow`, `Row` and `FooterRow`, `CellPadding`, `CellBorder`, `RowBackground` for striping and `MeasuredRows`. Each row has `Cell()` slots; a cell can `Span(columns, rows)`, set its own padding, background, borders and vertical alignment, and mark itself a row header (`RowHeader`). Header and footer rows repeat on every page. The sample at the top of this page has a small table; the layout rules (column widths, splitting, collapsed borders) are in [Tables](#tables-1) below. ### Pagination, links, structure `KeepTogether`, `KeepWithNext`, `EnsureSpace(height)` and `PageBreak()` control breaks (rules and defaults under [Pagination core](#pagination-core)). `Hyperlink(uri)`, `LinkToAnchor(id)`, `Anchor(id)` and `Bookmark(title, level)` make links and the outline. `Heading(level)`, `AltText`, `Language` and `Artifact` set the tags; tagging is on by default. `Aside()`, `Footnote()`, `Title()` and `Group(type)` add the PDF 2.0 grouping types `Aside`, `FENote`, `Title`, `Sect`, `Div`, `Part` and `BlockQuote` (`StructureGroup`); the 2.0-only ones can be saved as PDF/UA-2 but not as PDF/UA-1 (see [pdfua.md](https://recursiveai.org/onepdf/docs/pdfua/)). ```csharp LayoutDocument.Compose(doc => doc.Page(page => { page.Size(PageSize.A5); page.Content().Column(col => { col.Item().Bookmark("Contents").Anchor("top").Heading(1).Text("Contents"); col.Item().List(list => { list.Numbered(); list.Item().LinkToAnchor("top").Text("Back to the top"); list.Item().Hyperlink("https://example.com/").Text("A web page"); }); }); })).Save("navigation.pdf"); ``` ### Table of contents `TableOfContents(toc => ...)` lists the document's headings, with page numbers, leaders and links. Entries come from `Heading` elements and from `TocEntry(title, level)` wrappers for things that are not headings; `Heading(level, inToc, tocTitle)` leaves a heading out or lists it under another text. `Levels(min, max)` (default 1 to 3), `Leader` (default a dot; null for none), `Indent`, `Gap`, `Spacing`, `Style` and `Level(n, style)` (font, size, colour, line spacing and `SpaceBefore` per level) shape it. The number is the page's label, so a roman front section shows "iv" and a `LabelPrefix` of "A-" shows "A-12", exactly as a viewer's page-label box does. ```csharp LayoutDocument.Compose(doc => { doc.Title("Field notes").Language("en"); doc.Page(page => { page.NumberStyle(NumberingStyle.LowerRoman); page.Content().Column(col => { col.Item().Text("Contents").FontSize(20).Bold(); col.Item().TableOfContents(toc => toc.Levels(1, 2).Level(1, s => s.Weight = 700)); }); }); doc.Page(page => { page.StartNumber(1); page.Content().Column(col => { col.Item().Heading(1).Text("The harbour").FontSize(16).Bold(); col.Item().Text("Gulls and ropes."); col.Item().PageBreak(); col.Item().Heading(1).Text("The orchard").FontSize(16).Bold(); col.Item().Heading(2).Text("Windfalls").Bold(); col.Item().TocEntry("A note on cider", 2).Text("Pressed in October."); }); }); }).Save("toc.pdf"); ``` ### Values that depend on the page Headers and footers are laid out for every page, so they can show numbers that depend on what landed on that page: "Brought forward" at the head and "Carried forward" at the foot of a statement, a page subtotal, the first and last entry of a dictionary page, the section a page is in. Content contributes while it is placed, and a `DynamicText` formats the result when the page is drawn. - `Accumulate(account, amount)` wraps content (a table cell is the usual place) and adds a `decimal` to a named account on the page where the content starts. `Marker(key, text)` records a text the same way. Neither draws anything or changes size. Because they ride on the content, a row that a keep rule or a page break moves to the next page takes its amount with it. - `DynamicText(v => ...)` is one line of text whose string comes from a callback given the page's `PageValues`: `Sum(account)` (this page), `Total(account)` (up to the end of this page, what a footer carries forward), `BroughtForward(account)` (up to the end of the previous page, zero on the first), `First(key)` and `Last(key)` (on this page, in reading order, else null), `Current(key)` (the last one at or before the end of this page) and `Entering(key)` (in force when the page starts), plus `PageIndex`, `PageNumber`, `PageLabel` and `SectionIndex`. Culture and number format are the callback's. The style methods are `FontSize`, `FontColor`, `Bold`, `Weight`, `FontFamily`, `Sample` (a typical text, for sizing in an unbounded space) and `AlignStart`, `AlignEnd`, `AlignLeft`, `AlignRight` and `AlignCenter`; a text in an aligned slot takes that alignment, as `Text` does. - Totals run across sections; use another account name to start again. Sums are `decimal`, so money adds exactly; a sum beyond `decimal` range is a `PdfLayoutException`. ```csharp var culture = System.Globalization.CultureInfo.InvariantCulture; LayoutDocument.Compose(doc => { doc.Title("Account statement").Language("en-GB"); doc.Page(page => { page.Size(PageSize.A5); page.Margin(28); page.Header().DynamicText(v => $"Statement, {v.First("month")} - brought forward {v.BroughtForward("amount").ToString("N2", culture)}"); page.Footer().DynamicText(v => $"Carried forward {v.Total("amount").ToString("N2", culture)}").AlignEnd().Bold(); page.Content().Table(t => { t.Columns(c => { c.Constant(60); c.Relative(); c.Constant(70); }); for (int i = 1; i <= 90; i++) { int n = i; t.Row(r => { r.KeepTogether(); r.Cell().Marker("month", n <= 45 ? "October" : "November").Text($"{n:00}.{(n <= 45 ? 10 : 11)}"); r.Cell().Text($"Payment {n}"); r.Cell().Accumulate("amount", n * 12.35m).AlignEnd().Text((n * 12.35m).ToString("N2", culture)); }); } }); }); }).Save("statement.pdf"); ``` How it works. The height of a `DynamicText` is one line of its font, fixed before the page is known (a header's height decides how much room the body has), so the callback cannot change the layout. Contributions are collected from the placed pieces of the page's flowing content, walked in reading order (columns left to right, or right to left in right-to-left documents), after page breaking and before the page is drawn; the header, footer, background and foreground do not contribute, but table header rows repeated on each page do (they are placed on each page). An element that splits over pages contributes on the page where it starts. The callback runs once for each page that is written: the count-only runs a table of contents makes do not draw, so nothing is counted twice, and a page's values are known when the page is written, which keeps streaming output streaming. A text wider than its box is squeezed horizontally, with a note, rather than wrapped. In a header, footer, background or foreground it is an artifact like the rest of that band; in the body it is a tagged paragraph. The callback should have no side effects and must be deterministic for output to be. ### Components Implement `IComponent` to reuse a piece of layout. `Component()` creates one with its parameterless constructor. ```csharp sealed class Address(string name, string city) : IComponent { public void Compose(Slot slot) => slot.Column(column => { column.Item().Text(name).Bold(); column.Item().Text(city); }); } LayoutDocument.Compose(doc => doc.Page(page => page.Content().Component(new Address("Ada Lovelace", "London")))) .Save("address.pdf"); ``` ### Right to left and fonts `doc.RightToLeft()` mirrors rows, table columns, start/end alignment and insets, and sets the base direction of text. Fonts are registered on `doc.Fonts` (grouped by family, weight and style); the default is Liberation Sans from `RecursiveAI.OnePdf.Fonts`, embedded as a subset. System fonts are never used unless you add them. ```csharp LayoutDocument.Compose(doc => { doc.RightToLeft().Language("ar"); doc.Fonts.Add(File.ReadAllBytes("NotoSansArabic-Regular.ttf"), "Noto Sans Arabic"); doc.Fonts.FallbackFamilies.Add("Noto Sans Arabic"); doc.Page(page => page.Content().Row(row => { row.ConstantItem(80).Text("\u0645\u0631\u062D\u0628\u0627"); row.RelativeItem().Text("first item sits at the right"); })); }).Save("rtl.pdf"); ``` ### Saving `Save(path)` and `ToBytes()` stream the document page by page with the small memory footprint described under [Output](#output). Everything that needs the finished file goes through core: `Save(stream, PdfSaveOptions)` lays the document out, then rewrites it with the options (PDF/A, PDF/UA-1, encryption, object streams); the whole file is in memory while it does. For signing, `ToPdfDocument()` returns an open `PdfDocument` to sign with the core API. Use a fixed `PdfDocumentWriterOptions.DocumentId` when you need byte-identical output. ```csharp LayoutDocument layout = LayoutDocument.Compose(doc => { doc.Title("Archive copy").Language("en"); doc.Page(page => page.Content().Text("Archive copy")); }); layout.Save("plain.pdf"); // streamed page by page using (var archive = File.Create("archive.pdf")) // laid out, then rewritten by the core library { layout.Save(archive, new PdfSaveOptions { PdfA = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.B }, }); } using (var secret = File.Create("secret.pdf")) { layout.Save(secret, new PdfSaveOptions { Encryption = PdfEncryptionAction.Set, NewEncryption = new PdfEncryptionOptions { UserPassword = "open sesame" }, }); } ``` ### The element tree underneath The facade builds the element objects described below; they stay public. They are the lower layer for code that builds trees from data, for custom elements' tests, and an escape hatch: `slot.Element(element)` puts any element object into a slot. A facade document and the same document built from element objects produce identical bytes (a test lays out a 50-page report both ways). ## Element tree A tree of plain element objects. Elements hold properties and no layout state, so one element may appear several times and a tree can be laid out again; two runs give the same bytes (use a fixed `PdfDocumentWriterOptions.DocumentId`). - **Containers:** `Column` (top to bottom, spacing, item alignment), `Row` (constant, relative and automatic cell widths, spacing), `Layers` (one primary layer sets the size and may split; the others are laid out at its size on every page it occupies). - **Box:** margin, border (per side, solid/dashed/dotted), padding, background, corner radius, background colour or [gradient](#gradient-backgrounds), opacity, fixed/min/max width and height, aspect ratio, nine-point alignment of the child. Logical `Start`/`End` insets and borders sit beside the physical ones. - **Transforms:** `Rotate` (box = bounding box of the turned child), `Scale`, `Translate` (moves the drawing, not the layout space). - **Leaves:** `Text` (shaped paragraphs on OneFont: fonts with fallback, rich spans, bidi, justification, hyphenation hook (bundled patterns in `RecursiveAI.OnePdf.Hyphenation`), widow/orphan control; see [layout-text.md](https://recursiveai.org/onepdf/docs/layout-text/)), `Image` (JPEG or PNG; fit width, fit height, contain, cover, stretch), `Canvas` (a callback with the content builder inside a clipped box), `Spacer`. - **Structure:** `Table` and `ItemList` (below), `Heading`, `PageNumber`, and the navigation wrappers `Link`, `Anchor` and `Bookmark`. - **Flow rules:** `PageBreak`, `KeepTogether`, `EnsureSpace`, and `Element.KeepWithNext`. A wrapper that only adds something to its child (`Box`, `Link`, `Anchor`, `Bookmark`, `Heading`, `KeepTogether`, `Component`) passes a keep-with-next request on, so it is enough to set it on the heading's text. - **Columns:** `MultiColumn` pours items through equal columns with a gap, an optional rule and balanced or sequential fill; `ColumnBreak` ends a column (see [Multi-column flow](#multi-column-flow)). - **Reuse:** derive from `Component` and return a composed tree from `Compose`. - **Pages:** `LayoutDocument.Pages` holds `LayoutPage` sections. Each has size, orientation, margin, direction, `Header`, `Footer`, `Background`, `Foreground` and `Content`, and starts on a new page. Header and footer are laid out on every page and their height comes off the content area; background and foreground use the whole page. They are written as artifacts (see below). `FirstHeader`/`FirstFooter` replace the ordinary ones on a section's first page, `EvenHeader`/`EvenFooter` on pages with an even displayed number (a first page counts as first); an empty `Spacer` gives no header. - **Direction:** `LayoutDocument.Direction`, `LayoutPage.Direction` (null: the document's) and `Element.Direction`; see [Tagging and direction](#tagging-and-direction). ## Tables `Table` has `Columns` (constant, relative or automatic), `HeaderRows`, `Rows`, `FooterRows`, a default `CellPadding` and `CellBorder`, and a `RowBackground` delegate from the body row index to a colour for striping. A `TableRow` holds `TableCell`s (content, `ColumnSpan`, `RowSpan`, padding, background colour or gradient, borders, vertical alignment); cells fill the grid from the start and skip places taken by row spans above. - **Widths.** Constant columns keep their width. Automatic columns take the widest content of their cells (from `MeasureWidths`: the longest word to the unwrapped line) when there is room, and shrink towards the narrowest content when there is not. Relative columns share what the automatic ones leave by weight, never below their narrowest content. If even the narrowest contents do not fit, all non-constant columns are scaled down and words may spill (a note is recorded). A cell over several columns that needs more than they have asks the rest of the columns that can still grow, evenly. Content decides widths only for automatic columns and for the minimum of relative ones, so a table whose columns are all constant measures nothing, and one with relative columns but no automatic column measures the header, the footer and the first 100 body rows (just enough to keep a relative column from being narrower than its longest word; a later row with a longer word wraps inside its column). Any automatic column makes every body row count. `MeasuredRows` overrides both. - **Pages.** Header and footer rows are laid out again on each page the table occupies and their height comes off every page; they never split. A page always holds at least the start of a body row besides them. Body rows split between lines of their cells; `TableRow.KeepTogether` moves a row instead (a row taller than a page splits with a note). Rows joined by row spans form one unit that moves whole; a unit taller than a page raises `PdfLayoutException`, as does any cell that cannot fit an empty page (`Page[3] > Content > Table > Row 14 > Cell 2 > Image`; header and footer rows are `Header Row n` and `Footer Row n`). - **Borders** are collapsed: two cells that meet share one line centred on the grid line, half its width in each cell's inset. The wider border wins; at equal width solid beats dashed beats dotted; then the cell before (above, or at the start side) wins. Cell borders are `Start`/`End`, so a right-to-left table mirrors. Lines on the outside of a table, and at the top and bottom of each page's piece, are drawn inside it. - **Cost.** The grid and column widths are made once when the table starts; a page then costs its own rows plus header and footer. A 10,000-row table of four cells per row (about 250 pages) lays out in time proportional to its rows: 1,000 rows in 0.30 s, 10,000 rows in 3.0 s (Debug build, shared host), allocating 116 and 1,190 MiB; peak resident about 355 MiB for the whole test process. ## Lists `ItemList` holds `ListItem`s whose content is any element (a nested list goes in a `Column` after the entry's text). Bulleted lists use `Bullet` (default a bullet at depth 0, an en dash at depth 1, alternating); numbered lists use `NumberStyle` (decimal, letters, roman), `Start` and `Suffix`; `MarkerFormat` and `ListItem.MarkerText`/`Marker` give custom markers. All entries share one marker column as wide as the widest marker, so contents line up on a hanging indent and markers end at the same place. A continued entry repeats no marker. ## Pages, numbers, links - **Page numbers.** `PageNumber` shows the current number, or a total. A total is not known while pages are laid out: it is a form XObject reserved on first use and drawn on each page, filled once the document (or section) ends, with the number right-aligned inside a box of `ReservedDigits` digits. More digits than reserved are drawn condensed with a note. `Render(PdfDocument)` cannot fill a form later, so it lays the document out once into a throw-away writer to count pages, then renders. Put "Page N of M" in a `Row` with `Spacing`; a trailing space does not count towards a text's width. - **Numbering.** Each section has `NumberStyle`, `StartNumber` (default: continue) and `LabelPrefix`. The document gets page labels (ISO 32000-2 12.4.2) where numbering does not simply continue. `SectionTotal` counts a section's pages in its own style. - **Table of contents.** A table shows page numbers that depend on its own length and on pages not yet laid out. A document that contains one is laid out without drawing, as many times as it takes for the entries a run finds (title, level, page, label) to equal the entries the table drew in that run: the first run draws an empty table, the second the numbers of the first, a third confirms; at most six, with a note if they do not settle. One more run then writes the file. The number column is as wide as the widest label seen and never narrows between runs, so a label gaining a digit cannot make the runs chase each other. Only the entry list is kept between runs, so streaming memory is unchanged, and a document without a table pays nothing. Headings and entries are collected from the placed body pages in drawing order (not from table cells, repeated header rows, headers or footers), and each gets a named destination `onepdf-toc-N`. The search for a table before layout walks every container of this package (including `MultiColumn`) and components, and asks any element that holds others through one internal hook; set `LayoutOptions.TableOfContents` to true for a table it cannot see. An entry is not split across pages: a long title wraps and the number sits on its last line; the leader is a grid anchored at the number's edge, so dots line up from entry to entry. In right-to-left content the numbers move to the left. Structure: `TOC` holding one `TOCI` per entry, each a `Reference` holding a `Link` that owns the link annotation (ISO 32000-2 14.8.4.3.3, 14.8.4.4.2); the leader is an artifact. - **Links and anchors.** `Link` covers its child's box (every piece of a split child) with a URI or the name of an `Anchor`; anchors are named destinations at the top of their child's first piece, and links may precede them. Undefined and duplicate names are noted. `Bookmark` adds an outline entry (levels lifted to one below the previous entry when they jump). Links and anchors inside `Rotate` or `Scale` are not recorded. Only the streaming writer can record links, destinations, bookmarks and labels; `Render(PdfDocument)` leaves them out with a note. ## Tagging and direction **Tagged PDF is on by default** (`LayoutOptions.Tagged`; set it false for no structure tree and no marked content at all). The structure tree is written from the element tree while the pages are drawn (ISO 32000-2 14.7, 14.8): | Element | Structure | |---|---| | `Text`, `PageNumber` | `P` (inside a `Heading`, `Link` or list marker the text is that element's own content) | | `Heading` | `H1`–`H6` from `Level` | | `Table` | `Table` > `THead`, `TBody`, `TFoot` > `TR` > `TH`/`TD`; header-row cells are `TH` with `/Scope /Column`, `TableCell.IsHeader` makes a `TH` with `/Scope /Row`; spans become `/ColSpan` and `/RowSpan` | | `ItemList` | `L` (`/ListNumbering`) > `LI` > `Lbl`, `LBody`; nested lists sit in the `LBody` | | `Image` | `Figure` with `/Alt` from `AltText` | | `Link` | `Link` holding what it covers; its annotation is tied to it (`OBJR`) and its `/Contents` is `AltText` or the text it covers | | anything with `AltText` | one `Figure` with that description | Boxes, backgrounds, borders, rules, a `Canvas` without `AltText`, the debug overlay and anything with `Element.Artifact` set are artifacts. Header, footer, background and foreground are pagination artifacts. An element that splits over pages stays one structure element with content on each page: a paragraph, a table (`TBody`, a row and each cell too, so a row that splits is one `TR`), a list entry. **Repeated table rows:** the header rows are written as structure on the table's first page and as artifacts on the pages that repeat them, the footer rows as structure on the last page and artifacts before it, so a reader that follows the tree meets each row once (a single `THead` and `TFoot`, rather than one per page). `Element.AltText`, `Element.Language` (BCP 47; `/Lang` is written where it differs from the surrounding content) and `Element.Artifact` are plain properties; `LayoutDocument.Title` and `LayoutDocument.Language` set the information dictionary's title, `/ViewerPreferences /DisplayDocTitle` and the catalog's `/Lang` in `Save` (render into your own writer with `PdfDocumentWriterOptions.Language` and `DisplayDocumentTitle`). An image without `AltText` that is not an artifact is a `LayoutDiagnostic`, once per element. A document built in memory with `Render(PdfDocument)` cannot write a structure tree and is untagged. Tagging costs little time and memory and a good deal of file size. Measured on one run (Release, Ryzen 9 7950X3D, .NET 10.0.11, median of five): a 48-page report with a 500-row table took 145 ms tagged against 138 ms untagged, allocating 183 against 166 MiB, and its file grew from 125 KB to 523 KB (every table cell is a structure element); a 498-page justified book took 1.69 s against 1.68 s, allocating 2,572 against 2,558 MiB, and grew from 1.53 MB to 1.94 MB. Nothing is ever left untagged: every painted mark is tagged content or an artifact, which the tests check by interpreting the written pages. **PDF/UA-1 and PDF/A level A.** `Save(stream, new PdfSaveOptions { PdfUA = new PdfUAOptions() })` writes PDF/UA-1, and `PdfA = new PdfAOptions { Conformance = PdfAConformance.A }` writes PDF/A-2a (`Part = PdfAPart.Part3` for 3a); both together give one file for both. The save repairs `/Lang`, `DisplayDocTitle`, tab order, link descriptions and the XMP identification, and **refuses with every violation listed** what the document itself must provide: a title, a figure's `AltText` (mark decoration `Artifact`), heading levels without gaps starting at H1, tables with header cells, and so on. These are checked in the finished file, so this path holds the whole file in memory (see [Output](#output)). `LayoutDocument.Author`, `Subject` and `Keywords` (and `doc.Author(...)` in the fluent API) fill the information dictionary and the XMP packet. This is for documents OnePDF laid out; a clean result is not proof of conformance. Read [pdfua.md](https://recursiveai.org/onepdf/docs/pdfua/) for what is and is not checked. **Right to left.** `LayoutDocument.Direction` (or a section's or element's `Direction`) mirrors everything that has a start and an end: rows run right to left, table columns and cell borders, list markers hang on the right with the content to their left, `Start`/`End` alignment, `PaddingStart`/`End`, `MarginStart`/`End`, `BorderStart`/`End`, `Translate` and a paragraph's first-line indent. Paragraphs take their base direction from it and are reordered by UAX #9. An element marked `Direction = LeftToRight` inside a right-to-left document stays left to right. `Left` and `Right` stay physical, and so do page margins. ## Measure, then place Every element answers one question: *placed in this much width and height, what do you take?* The answer is `Complete`, `Partial` (a continuation object says where the next page resumes), `NoFit`, or `Break`. The offered height is the space left on the page; width is the container's width (infinite when unbounded, for natural sizes). The result is a tree of placed boxes (parent-relative, y down) that is drawn after the page is complete and then dropped. `Constraint.AtTop` tells an element that nothing sits above it on its page, so a new page would not give it more room. An element that cannot fit when `AtTop` is set throws `PdfLayoutException` itself, so the error names the real culprit: `Page[1] > Content > Column > Item 2 > Image`, with the width and height offered and wanted. ## Pagination core `Pagination/Flow.cs` knows nothing about element types. A container exposes its children as an `IFlowSource`; `FlowPaginator.Fill` places them top to bottom for one page and returns where to continue. `PageSequencer` repeats that over pages and hands each page out before the next is laid out, which is what lets the streaming writer drop pages as they are done. Rules: - Items go in order; spacing separates neighbours on a page and is never added at a page top. An item whose gap alone would pass the bottom starts the next page. - A partial item ends the page. Its continuation is the first thing on the next one. - Keep-with-next: an item with the flag is placed only if the chain up to the first item without it fits whole, and that item at least begins, on the same page. Otherwise the page ends before it. At the top of a page the rule cannot help and the chain is placed as it comes. - A break ends the page; at the top of a page (nothing placed yet) it is ignored. A break with nothing after it still starts a final blank page. - Items made of countable units (text lines) take the widow and orphan rules (default 2 and 2, `LayoutOptions`): a paragraph starts on a page only with at least *orphans* lines there and leaves at least *widows* for the next; a paragraph shorter than both together is not split. - `KeepTogether` moves a child that would split to the next page. A child taller than a page splits anyway and a `LayoutDiagnostic` is recorded. - A `Box` that splits keeps top margin, border and padding on its first piece and the bottom ones on its last; corners are rounded only at those outer ends. A box with a fixed or maximum height, or an aspect ratio, never splits and cuts off taller content with a note. ## Output `LayoutDocument.Save(Stream)` writes through `PdfDocumentWriter`: each page is committed when laid out. `Render(PdfDocument)` appends to an editable in-memory document; `Render(PdfDocumentWriter)` streams into a writer the caller finishes. Debug mode (`LayoutOptions.Debug`) draws every placed element's box and name in red. Memory, one-line pages through the streaming path (NativeAOT, linux-x64): 1,000 pages 27 MB peak resident, 10,000 pages 68 MB. # Layout text: the paragraph engine The rules as built. The engine turns styled inline text into lines and draws them; it does not depend on the element tree (`src/OnePdf.Layout/Paragraphs/`), and the `Text` element is one client of it. Shaping, bidi and break data come from OneFont. ```csharp var fonts = TextFontRegistry.CreateDefault(); // Liberation Sans/Serif/Mono fonts.Add(File.ReadAllBytes("NotoSansArabic-Regular.ttf"), "Noto Arabic"); fonts.FallbackFamilies.Add("Noto Arabic"); var paragraph = new TextParagraph( [new TextSpan("Plain, "), new TextSpan("bold", new TextStyle { Weight = 700 }), new TextSpan(" and مرحبا.")]) { Alignment = TextAlignment.Justify, Hyphenator = myHyphenator }; ShapedParagraph shaped = paragraph.Shape(fonts); // width-independent ParagraphLayout layout = shaped.Layout(300); // lines, heights, baselines (ParagraphLayout first, ParagraphLayout rest) = layout.Split(3); var renderer = new TextRenderer(document); // one per document or writer renderer.Draw(builder, first, x: 50, top: 780); ``` ## Fonts - A registry holds faces by family, weight, width class and italic. A style asks for families in order; the face of a family is the one with the right italic flag, then the nearest width class, then the nearest weight (an equally near heavier face wins for a requested weight of 500 or more). Weights are never synthesized. - `CreateDefault` registers the twelve Liberation faces from `RecursiveAI.OnePdf.Fonts` and the aliases `Sans`, `Serif`, `Mono`, `Monospace`. The default family is `Sans`. Text in them is shaped and embedded as a subset of the glyphs used (a one-page Latin page is about 13 kB). - Standard-14 names (`Helvetica`, `Arial`, `Times-Roman`, `Courier-Bold`, ...) select the unshaped path: advances come from the metric-compatible Liberation face, no kerning, no ligatures, no fallback, drawn with the PDF standard font and not embedded. A standard font cannot be combined with others in one chain. - Characters a family lacks come from the later families of the style, then from the registry's `FallbackFamilies`. OneFont picks the face per grapheme cluster. A character no face has is drawn as the first face's missing-glyph box and reported in `ShapedParagraph.Warnings`. - System fonts are consulted only after `UseSystemFonts(provider)`. ## Pipeline 1. **Normalise.** `\r\n`, `\r`, U+2028, U+2029 become `\n`; tabs become spaces; other control characters are dropped; U+00AD is removed and remembered. Each `\n`-separated line is its own bidi paragraph and its own list of lines; the line before a `\n` is never justified. 2. **Itemise.** Within a line, runs of one bidi level (UAX #9, `Direction` forced or taken from the first strong character) and one shaping group (same families, weight, italic, width, language and script position) are shaped together, so Arabic joining and kerning continue across a change of colour or size. Spans that differ only in size, colour, spacing or decoration still share a run. 3. **Shape.** `FontFallbackChain.ShapeRun` per run. Each cluster becomes one item with its glyphs, advance and the text it stands for. Letter spacing is added to the cluster's advance. 4. **Superscript and subscript.** Shaped once with `sups`/`subs` on and once without; if the glyphs differ the font's own forms are used at full size. Otherwise the text is scaled to 0.65 and shifted by the font's `ySuperscriptYOffset`/`ySubscriptYOffset` (0.35 em up and 0.14 em down when the font gives none). 5. **Break.** Opportunities are UAX #14 (`TextSegmentation`) plus soft hyphens and `IHyphenator` points, which are used only where UAX #14 offers no break. Thai and similar scripts break at spaces and punctuation only (no dictionary); ideographs break between characters. 6. **Fill, greedy first fit.** A line takes the last opportunity at which its content, without trailing spaces, fits; trailing spaces hang. At a hyphenation point the hyphen's width counts. If no opportunity fits, the piece up to the next one stays whole on a line and the line has `Overflows = true`. 7. **Align.** `Start`/`End` follow the direction of that bidi paragraph; `Left`/`Right` do not. The first-line indent is on the start side and shrinks the width of the first line. `Justify` adds equal extra width to every stretchable space (U+0020, U+00A0, U+3000) so a justified line is exactly the width (±1e-9); a line with no space stays at the start edge unless `JustifyWithLetterSpacing` spreads the extra between characters. 8. **Line boxes.** Each item contributes the ascent and descent of its font at its size (`hhea`, or the typographic values when `USE_TYPO_METRICS` is set) plus half the line gap on each side. With `LineHeight` set (em of the style's size) the box is that tall, extra space split above and below; the tallest span decides. Raised or lowered script extends a line only when no `LineHeight` is set. An empty line has the height of the font at its position. ## Knuth–Plass line breaking By default lines are broken greedily (first fit). A paragraph can opt in to total-fit breaking after Knuth and Plass, "Breaking Paragraphs into Lines" (Software: Practice and Experience 11, 1981): the breaks of the whole paragraph are chosen together, so a loose line is not the price of a tight one three lines later. Greedy stays the default and its output does not change. ```csharp using OnePdf.Layout; LayoutDocument.Compose(doc => { doc.LineBreaking(LineBreakMode.KnuthPlass); // every text of the document, or doc.Page(p => p.Content().Text(t => { t.Justify(); t.Hyphenator(myHyphenator); t.OptimalLineBreaks(new KnuthPlassOptions { Tolerance = 300 }); // this text only (GreedyLineBreaks() opts out) t.Span(longText); })); }); // the paragraph engine directly var paragraph = new TextParagraph(text) { Alignment = TextAlignment.Justify, LineBreakMode = LineBreakMode.KnuthPlass }; ``` - **Model.** The paper's boxes are the shaped clusters, its glue the spaces (stretching by half and shrinking by a third of their width) and its penalties the break opportunities: UAX #14 breaks cost nothing, a break after a hyphen in the text costs `ExplicitHyphenPenalty`, and a soft hyphen or `IHyphenator` point costs `HyphenPenalty` and adds the hyphen's width to its line. A line's adjustment ratio is the change its spaces must make over the change they can make; badness is `100·|r|³` up to 10000; demerits are `(LinePenalty + badness)² ± penalty²`, plus `ConsecutiveHyphenDemerits`, `FinalHyphenDemerits` (the last line follows a hyphen) and `AdjacentFitnessDemerits` (neighbouring lines of very different looseness: very loose, loose, decent, tight). The dynamic programme keeps one node per break and looseness class and prunes as the paper does. - **Text it applies to.** Justified lines use their spaces; a justified line with no space to stretch is as bad as can be, so single-word lines are avoided. Left, right and centred text counts as ragged: lines cannot shrink, and slack is judged against a quarter of the line width. The last line of a paragraph and lines before a hard line end are free to be short. Lines of different widths (a first-line indent) are handled; mixed fonts and sizes, bidi paragraphs and clusters are used as shaped. Justification then spreads each line's slack evenly over its spaces, as for greedy lines, and a line the breaker squeezed has its spaces narrowed. - **Defaults** (`KnuthPlassOptions`, TeX's plain values): `Tolerance` 200, `PreTolerance` 100, `LinePenalty` 10, `HyphenPenalty` 50, `ExplicitHyphenPenalty` 50, `ConsecutiveHyphenDemerits` 10000, `FinalHyphenDemerits` 5000, `AdjacentFitnessDemerits` 10000, `Looseness` 0, `EmergencyStretch` 0.1 of the line width. `Looseness` asks for that many lines more or fewer than the optimum when the paragraph can be set so. With `PreTolerance` 100 a paragraph that sets well without hyphens gets none; a negative value always considers them. - **Passes and fallbacks.** Pass 1 has no hyphens and accepts badness up to `PreTolerance` (skipped when the paragraph has no hyphenation points or `PreTolerance` is negative). Pass 2 allows hyphens up to `Tolerance`. Pass 3 repeats it with emergency stretch. Pass 4 accepts any line that is not too full and, when a piece wider than the line leaves no choice, puts that piece alone on an overfull line as greedy does, so a paragraph is always set. Each hard line (separated by a line end) is solved on its own. - **Bounds.** Time and memory are proportional to the number of break opportunities times the candidate line starts within one line width. A hard line of more than 500000 clusters, or a search with more than 4096 live candidates (looseness multiplies them), is broken greedily instead and `ShapedParagraph.Warnings` says so. There is no recursion. Pagination is unaffected: a Knuth–Plass paragraph is an ordinary list of lines, so splitting, widows and orphans and keep rules work on it as before. The breaker never looks beyond its paragraph. - **Determinism.** Plain double arithmetic in a fixed order; ties go to the earlier break; output is byte-identical between runs. - **Cost.** Compared with greedy, `book500` (500 justified pages) allocates about 21 % more and takes between no measurable difference and about 15 % more time (measured locally on a busy machine; see `bench/OnePdf.Layout.Benchmarks/README.md`). ## Measure, split, content widths - `ShapedParagraph.MinContentWidth`: the widest piece between break opportunities, counting a hyphen where the piece ends at a hyphenation point. `MaxContentWidth`: the longest line with nothing wrapped, with the first-line indent. - `ParagraphLayout.Lines` give `Top`, `Height`, `Baseline`, `Width`, `X` and the text range of each line; any boundary between two lines is a place to split. `Split(n)` returns the first `n` lines and a continuation holding the rest exactly as an unsplit layout laid them out, moved up to the top; drawing the two parts draws the same lines. `HeightOf(first, count)` sums heights for widow/orphan decisions. ## Drawing `TextRenderer.Draw` draws a range of lines. Per line it orders the items visually (UAX #9 rule L2 on the item levels) and groups neighbours of one font and level into one `DrawGlyphRun` each, with glyph advances that include letter spacing and justification, so the glyphs sit where the layout put them. Right-to-left runs are drawn in display order; the text given with each run is in logical order, so extraction returns logical text. - A line-end hyphen is the font's hyphen glyph (U+2010 if the font has it, else `-`) mapped to U+00AD, as tagged PDF asks for incidental hyphens (ISO 32000-2 14.8.2.2.3). Standard-font text draws `-`. - Underline and strike-through are filled rectangles in the text colour, at the position and thickness of `post` and `OS/2`, at least 0.25 pt thick. - Spans with `TextStyle.Link` are returned as `TextLinkArea` rectangles (line-box height); placing the annotation is up to the caller. - The builder's graphics state is restored. ## Inline objects A span can hold an object instead of text: `TextSpan.ForInline(InlineObject, style)`. `InlineBox(width, height)` is the standalone kind, drawn by a `Paint` callback (a `CanvasSurface` clipped to the box) and/or an `Image` (`PdfCreatedImage`); `InlineElement` (layout `Text` only) holds an element tree. Common settings: `Alignment` (`InlineAlignment`: baseline, top, middle, bottom, text top, text bottom), `BaselineOffset`, `InnerBaseline`, `GrowsLine`, `KeepWithNeighbours`, `AltText`, `Artifact` and `Link`. - **In the paragraph** the object is one character, U+FFFC OBJECT REPLACEMENT CHARACTER, one item as wide as the object. It has line break class CB (a break is allowed before and after, UAX #14 rule LB20) and bidi class ON (it takes the level of its position, UAX #9), so right-to-left lines place it correctly. `KeepWithNeighbours` clears the two opportunities after the line breaker has found them. A U+FFFC in ordinary span text is dropped. Hyphenation points never fall next to an object. - **Line box.** The object takes the font of the span it stands in, which gives the line its minimum height and the ascent and descent that middle, text-top and text-bottom use. Objects against the baseline or the text come first and raise the line's ascent and descent as far as they reach; objects aligned to the top then add what they lack below the baseline, and objects aligned to the bottom what they lack above it, in that order. With `GrowsLine` false the object is placed by the same rules but changes nothing. `ParagraphLine.Baseline` and `Height` are the result. - **Drawing.** `TextRenderer.Draw` closes its saved graphics state around each stretch of text and draws the object between stretches, so the object may open marked content of its own. An object with `AltText` is preceded by an invisible space inside a `/Span` with `/ActualText`, so extraction returns the description; one with `Link` is reported in the `TextLinkArea`s. Without a layout host nothing is tagged. - **Content widths** (`MinContentWidth`, `MaxContentWidth`) count the object like a word. ## Colour glyphs A font with OpenType `COLR` version 0 and `CPAL` tables (colour emoji, colour icon fonts) draws its colour glyphs in colour. A base glyph that has a `COLR` record is drawn as its layers, bottom to top; each layer is a glyph of the same font filled with a palette colour (OpenType `COLR` version 0, `CPAL`). The foreground entry 0xFFFF takes the text colour. Glyphs without a record draw as before, and a document whose fonts have no `COLR` table produces the same bytes as without this feature. ```csharp doc.Fonts.Add(File.ReadAllBytes("ColorEmoji.ttf"), "Emoji"); page.Content().Text(t => { t.Span("Done ").FontSize(14); t.Span("\U0001F600").FontFamily("Emoji", "Sans").FontSize(24); // palette 0 t.Span("\U0001F600").FontFamily("Emoji", "Sans").FontSize(24).Palette(ColorPalette.Dark); }); doc.Options.ColorPalette = ColorPalette.Light; // the default for text that does not choose ``` - **Palettes.** `ColorPalette.Default` is palette 0. `ColorPalette.Light` and `Dark` pick the first palette that `CPAL` version 1 tags for light (0x0001) or dark (0x0002) backgrounds, and palette 0 when the font tags none. `ColorPalette.FromIndex(n)` picks palette n, and palette 0 when the font has fewer. The choice is `TextStyle.Palette`, `Text.Palette`, the fluent `Palette(...)` on spans and texts, or `LayoutOptions.ColorPalette` for text that does not choose. - **Alpha** in a palette entry is not used: layers are painted opaque. This keeps PDF/A and tagged output free of transparency. - **Output.** Each layer is a one-glyph text run in its fill colour, and the layers of one glyph sit inside one `/Span` with `/ActualText` of the character (ISO 32000-2 14.9.4), so extraction returns the character once, not once per layer. The layers' glyphs are subset and embedded with the font like any other glyph, and the span lies inside the paragraph's tagged content, so the layers are part of the text, not artifacts. Colour glyph runs are written one glyph per `BT`/`ET`, which makes them larger than plain text. - **Tables that cannot be used.** The tables are read with bounds checks, and a table that does not hold together is rejected whole: record arrays outside the table, base glyphs out of order or outside the font, more than 256 layers on a glyph, layers that name a glyph outside the font or a palette entry that does not exist, palettes that point outside `CPAL`, a missing `CPAL`, or a `COLR` version above 1. Text in such a font is drawn in one colour and the layout records one diagnostic per font (`LayoutDocument.Diagnostics`). Layers are looked up once per glyph and never followed further, so there are no cycles to detect. - **COLR version 1** (paint graphs: gradients, transforms, compositing) is not drawn. A glyph that only has a version 1 description draws its plain outline, with one diagnostic per font; a glyph that also has a version 0 record is drawn from that. - **Not included:** bitmap and SVG colour fonts (`CBDT`, `sbix`, `SVG `), palette alpha and `CPAL` palette labels. ## Limits and behaviour A paragraph holds at most 2^24 characters. Layout work is linear in the text for any number of lines: the scan for a break stops once the content has passed the width. The shaped paragraph is immutable; `Layout` caches hyphen glyphs, so use one shaped paragraph from one thread at a time. Output is byte-identical between runs (no fused multiply-add, fixed evaluation order). Not included: vertical writing, COLR version 1 and bitmap colour fonts, tab stops. ## Hyphenation patterns The `RecursiveAI.OnePdf.Hyphenation` package supplies `IHyphenator`s built on Liang's algorithm (the method of TeX). Layout does not depend on it, and without a hyphenator only soft hyphens break words. ```csharp using OnePdf.Hyphenation; page.Content().Text(t => { t.Language("de-DE"); t.Hyphenator(Hyphenators.Auto()); // picks patterns from each span's language; no language, no hyphenation t.Justify(); t.Span("Donaudampfschifffahrtsgesellschaft ..."); }); ``` - `Hyphenators.For("en-GB")` gives one language's `PatternHyphenator` (null when none is bundled); `Hyphenators.Auto(options, defaultLanguage)` dispatches on the language of each word's style. `Hyphenators.Languages` lists the 26 bundled ids; `Resolve` maps a tag to one (`en` is American unless the region is GB, AU, NZ, IE, ZA, IN, SG or HK; `de` is the 1996 spelling, `de-1901` the traditional one, `de-CH` the Swiss traditional patterns; `pt-BR` is `pt`). - **Defaults.** Words under 5 characters are not hyphenated. At least `LeftMin` letters stay before a hyphen and `RightMin` after it; by default these are the language's own typesetting minimums from its pattern file (2 and 3 for American and British English, Portuguese, Slovak, Estonian, Welsh and Irish; 2 and 2 for most others, 1 and 1 for monotonic Greek). All three are set through `HyphenationOptions`. Words over 255 characters are left alone. - **Exceptions** in the pattern files replace the patterns for that word, and are held to the same minimums. Custom data goes through `PatternHyphenator.FromPatterns`. - **Output.** The engine draws the hyphen from the line's font and maps it to U+00AD, so the text layer reads `hyphen\u00ADation`; extraction with `JoinHyphenatedWords` returns the whole word. Tagged and PDF/A output stay compliant (checked with veraPDF). - **Data.** Each language is a compressed embedded resource read the first time it is used. Sources, licences and per-file notices are in the package's `THIRD-PARTY-NOTICES.md`; the data is regenerated with `tools/hyphenation/generate.py`. Not bundled, for licence reasons: Swedish, Finnish, Icelandic, Catalan, Croatian, Russian, Turkish, Esperanto, Czech, Hungarian, Latvian and Romanian. - A word is the run of letters and combining marks the engine finds, so apostrophes and hyphens split words (`l'avion` is `l` and `avion`). # SVG pictures in layout `RecursiveAI.OnePdf.Layout` draws SVG as native PDF vector content: paths, fills, strokes, shadings, clips, soft masks and real text. Nothing is rasterised except the pictures the SVG itself embeds. Core gains no SVG code; the code is in the layout package, over core's public content builder. ```csharp // In a layout: sized like an image (contain, cover, stretch, fit width, fit height, a DPI for the natural size). doc.Page(page => page.Content().Column(col => { col.Item().Width(120).Svg(File.ReadAllBytes("logo.svg")).Contain().AltText("Company logo"); })); // With the content builder (a canvas, a page overlay, a form). SvgImage logo = SvgImage.Parse(File.ReadAllBytes("logo.svg")); IReadOnlyList notes = logo.Draw(builder, x: 40, y: 700, width: 120, height: 60); // or inside a canvas element: surface.DrawSvg(logo, 0, 0, surface.Width, surface.Height); ``` - `SvgImage.Parse(bytes | string, SvgLimits?)` reads the document at once. A malformed or over-limit document throws `PdfFormatException` or `PdfLimitException` there, before layout. `Width`/`Height` are the natural size in points (one CSS pixel is 0.75 pt), `Title` and `Description` come from the root's `` and `<desc>`. - `Draw` returns the notes (what was dropped or approximated), each once. In layout the notes go to `LayoutDocument.Diagnostics`. - `SvgPicture` is the element (`Slot.Svg(...)` in the fluent API). It never splits across pages. - Text is shaped through OneFont with the layout's registered fonts, then the Liberation defaults, and is extractable (`/ToUnicode` as for layout text). `SvgDrawOptions.Fonts` chooses the registry for a plain builder. ## Tagging A tagged layout document writes an SVG picture as a `Figure`. Alternate text is the author's `AltText`, else the SVG's `<title>`, else its `<desc>`. With none of them, and the picture not marked `Artifact`, layout records the same diagnostic as for an image and PDF/UA-1 saves refuse the document (checkpoint 13). A tagged PDF/UA-1 document with an SVG passes veraPDF's `ua1` profile. ## Supported - **Structure:** `svg` (nested too), `g`, `defs`, `use`, `symbol`, `switch`, `a`, `viewBox`, `preserveAspectRatio`, `transform`, `transform-origin` (also on gradients and patterns), `display`, `visibility`, `overflow`, root `background-color`. - **Shapes:** `rect` (with rounded corners), `circle`, `ellipse`, `line`, `polyline`, `polygon`, `path` with every command including arcs (arcs and quadratics become cubics). - **Paint:** colours (hex, `rgb()`, `hsl()`, names, `currentColor`), `fill-rule`, `fill-opacity`, `stroke` width, dash and offset, caps, joins, miter limit, `stroke-opacity`, `paint-order`, `context-fill`/`context-stroke` (a gradient or pattern keeps the context element's box and coordinate system), zero-length subpaths drawn as dots under round and square caps, the fallback colour after a `url()` when the server cannot paint a shape with no size, `opacity` (group opacity through a transparency group), `mix-blend-mode`, `isolation`. - **Paint servers:** linear and radial gradients (with `href` inheritance, `gradientTransform`, both unit systems, `fx`/`fy`/`fr`, stop opacity) as PDF shadings; patterns (`patternUnits`, `patternContentUnits`, `patternTransform`, `viewBox`); gradients and patterns on fills and on strokes. - **Clipping and masks:** `clipPath` (units, `transform`, `clip-path` on the element and on its children, `clip-rule` per child, text children). A clip of one non-even-odd shape is a PDF clip path; a clip with text, nested clips or several shapes with even-odd rules is built as a luminosity soft mask instead, `mask` (luminance, nested) as PDF soft masks. - **Markers:** on `path`, `line`, `polyline`, `polygon` and the basic shapes (`rect`, `circle`, `ellipse`); `marker-start/mid/end`, `markerUnits`, `orient`, `refX/refY`, `viewBox`. - **Images:** `data:` URIs holding PNG or JPEG, through core's PNG reader and JPEG passthrough. - **Text:** `text`, `tspan`, `x/y/dx/dy` lists, `text-anchor`, `font-family/size/weight/style`, `letter-spacing`, `word-spacing`, `text-decoration`, `baseline-shift`, `dominant-baseline` (approximate), fill and stroke, `direction`/`unicode-bidi`. - **CSS:** presentation attributes, the `style` attribute and `<style>` sheets (type, class, id, attribute, descendant, child and sibling selectors, structural pseudo-classes such as `:first-child` and `:nth-child()`, `:not()`, `!important`) and `inherit`. CSS custom properties (`var()`) are not supported. ## Approximations - `spreadMethod="repeat"` and `"reflect"` are unrolled into stops (up to 64 periods; beyond that the last band extends, with a note). - A stroke with a gradient whose stops have different opacities is drawn opaque (fills handle varying stop opacity with a soft mask). - Patterns are drawn as repeated forms clipped to the shape (up to 4096 tiles per shape); on strokes they are PDF tiling patterns. - Blend modes are PDF's; the backdrop is the page, which in a document is usually white. `hue`, `saturation`, `color`, `luminosity` and `plus-lighter` are not mapped. - Gradients and patterns on text are drawn as one solid colour, with a note. - `shape-rendering` and `image-rendering` are ignored (PDF viewers choose their own anti-aliasing and interpolation). Glyph positions and baselines can differ from a browser by a fraction of a point. ## Dropped, never run, never fetched Scripts, SMIL animation, event attributes, `foreignObject`, external references (files, URLs, other SVG files, `use` of another document) are dropped with a note. A filter draws the unfiltered content with a note (no filter is mapped). Also not drawn: `textPath`, `tref`, vertical writing, `rotate` and `textLength` on text, SVG or GIF/WebP data URIs inside an `image`. The XML reader refuses DTD entity expansion and external entities; nothing opens a file or a connection. ## Limits `SvgLimits` (all checked before the work they guard; a document over one fails with `PdfLimitException`): document bytes (32 MiB), elements (500,000), depth (256), attributes per element, names, namespace bindings, instantiated elements through `use` (500,000), `use` chain depth (32), layer depth (32), path segments (4,000,000), CSS bytes, rules and matching steps, data-URI image bytes (16 MiB each, 64 MiB together), text characters (1,000,000, counted again for each `use` copy). Element visits for bounding boxes share the instance bound. Gradient stops are capped at 250 and tiles at 4096 per shape. The `svg` fuzz target (`fuzz/`) runs the whole path. ## Checked against `tools/OnePdf.Layout.SvgCompare` renders each SVG twice: through a layout PDF (viewed by `RecursiveAI.OneImage.Pdf`) and directly by `RecursiveAI.OneImage.Svg`, both black boxes, and counts pixels that differ. A page passes when at most 0.5 % of its pixels differ by more than 32 levels. `testdata/layout-svg/` holds our own sample files and `testdata/layout-references/documents/svg.pdf` a reference document. <!-- https://recursiveai.org/onepdf/docs/pdf-fonts/ --> # PDF fonts How a font dictionary becomes glyphs. Code: `src/OnePdf/Text/Fonts/`. The font programs, CMaps and encodings underneath are in `FontPrograms/` and `FontData/`. ## What a font gives the caller `PdfDocument.GetFont(dictionary)` (internal) returns a `PdfFont`, built once per font object and shared by all threads. `font.Next(bytes, position, out glyph)` / `font.Decode(bytes, list)` split a shown string into `FontGlyph`s. A glyph is immutable and shared by every use of the same code: | Member | Meaning | |---|---| | `Code`, `CodeLength` | the code and the bytes it took (1 for simple fonts, 1–4 for CMaps) | | `Cid`, `Gid` | CID (the code for simple fonts); glyph index in the font program, -1 without a program | | `Unicode`, `HasUnicode` | text, or U+FFFD with `HasUnicode` false | | `Exists` | the program has a real glyph for it (not .notdef); for Type 3, a glyph procedure exists | | `Advance` | horizontal advance for font size 1 (width / 1000; Type 3: width through `FontMatrix`) | | `VerticalAdvance`, `OriginX`, `OriginY` | vertical writing: `W2`/`DW2` w1y and position vector v, size 1 | | `OutlineMatrix` | glyph space to text space, with the horizontal scale of a substitute applied | | `GetOutline()` | unhinted outline in glyph space (see Tricky TrueType fonts for the exception), empty when missing or damaged | | `CharProc` | Type 3 glyph content stream | Word spacing applies to a single-byte code 32 (`CodeLength == 1 && Code == 32`), as ISO 32000-2 9.3.3 says; the caller decides. ## Unicode chain 1. `/ToUnicode` (a NUL-only or empty mapping counts as no mapping). 2. Simple fonts: the glyph name from the encoding through the Adobe Glyph List (`uniXXXX`, `uXXXX`, suffixes and `_` components included; ZapfDingbats names first for that font). 3. Composite fonts: a Unicode-coded CMap (`Uni*-UCS2/UTF16/UTF32`): the code is the character. 4. The embedded program: a Unicode cmap read backwards (lowest code wins), then the program's glyph name through the AGL. 5. Composite fonts: the character collection's CID to Unicode table (Japan1, GB1, CNS1, Korea1, KR), then the CIDs that table leaves out: vertical variants and half-width forms, found by reading the `-V` and half-width predefined CMaps backwards; for the single-byte range of a legacy CMap, ASCII (and half-width katakana for Shift-JIS). 6. U+FFFD, glyph kept. Ligature glyphs report their ligature character (`fi` is U+FB01); `Ligatures.Decompose` gives the letters for the extractor. Presentation forms and compatibility ideographs are reported as the file says; folding them is the extractor's job. ## Glyph selection - **Type 1 / CFF (simple):** name from the encoding → program glyph; then the name before a `.` suffix; then the program's built-in encoding; then, if Unicode is known, a `uniXXXX` glyph. Encoding start: `/BaseEncoding` or the named encoding; else the program's built-in encoding; else (non-embedded Standard 14) its AFM encoding; else StandardEncoding for non-symbolic fonts. - **TrueType (simple), ISO 32000-2 9.6.5.4:** with an `/Encoding` and not symbolic: name → Unicode → (3,1) cmap (or any Unicode cmap); name → MacRoman code → (1,0); name → `post` table. Otherwise (3,0) with the code, `F000`, `F100`, `F200` offsets; then (1,0) with the code; then a Unicode cmap reading the code as WinAnsi, then raw. Each step is tried only when the previous gave glyph 0. - **Composite:** CMap → CID. CIDFontType2 with a TrueType program: `/CIDToGIDMap` stream or identity. Everything else: the CFF charset (`GlyphForCid`; identity for CFF that is not CID-keyed). A code the CMap does not map is CID 0. - **Widths:** simple: `/Widths` by `FirstChar`, else `MissingWidth`; without `/Widths` the Standard 14 AFM, then the program's own advance. Composite: `/W`, else `/DW` (1000). Vertical: `/W2`, else `/DW2` (default 880, -1000) with v = (w0/2, DW2[0]). ## Substitution A font with no usable program (none embedded, or the embedded one failed to parse, recorded as a warning diagnostic) asks the provider with a `PdfFontRequest` (name without subset tag, family, Standard 14 alias, bold/italic/serif/fixed/symbolic, CID ordering, vertical). The substitute's glyph is found by name, then by the glyph's Unicode through its cmap, and scaled horizontally so its advance equals the PDF width (ratio limited to 0.4–2.5, ignored within 1%). A substitute may list `Fallbacks` (more fonts tried in order for characters the first lacks, simple fonts only) and a `MapCodePoint` function applied to the Unicode value before the cmap lookup; the `RecursiveAI.OnePdf.Fonts` package uses both for Symbol and ZapfDingbats (`FontsPackage.Register()`). Without a substitute the glyph has no outline but keeps its width and Unicode. `PdfFontProviders.Default` = close matches among installed fonts, then providers added with `PdfFontProviders.Register(provider)` (one call, no scanning), then any installed sans, serif, monospace or symbol family by flags. `PdfDocument.FontProvider` overrides it for one document; `PdfFontProviders.UseSystemFonts = false` turns the installed fonts off. Installed fonts are listed once (name tables only) from the platform's font directories plus `ONEPDF_FONT_DIRS`; nothing is fetched. ## Tricky TrueType fonts Outlines are not hinted, and no font code runs, with one narrow exception. A few CJK TrueType fonts (the DynaLab-designed MingLiU, PMingLiU and DFKai-SB families that ship with Windows) store composite glyphs whose component offsets are provisional: the composite's own instructions move the components into place. Read without running them, a glyph such as U+6700 in a MingLiU subset comes out with strokes displaced or missing (in the committed `cjk-04` sample its width is 893 units instead of 964 and it overlaps the real shape by 46% instead of 91%). For a font recognised as one of these, `SfntFont` runs the font program and control value program once and then each glyph's instructions (components first, then the composite's, with its phantom points). It runs them at one pixel per font unit, so that rounding to the grid moves a point by at most one unit and nothing is grid-fitted in practice; the result stays in font units. Every other font takes the plain path and never executes font code. Recognition uses the font's own data only (`TrickyFonts`): 1. a family, full or PostScript name in the `name` table (IDs 1, 4, 6, 16) that, without a subset tag and ignoring case and punctuation, starts with `mingliu`, `pmingliu`, `dfkaisb` or `dfkaishu` (DFKai-SB's PostScript name is `DFKaiShu-SB-Estd-BF`); or 2. when subsetting removed or renamed the names, the checksums of the `fpgm`, `prep` and `cvt ` tables (computed from the bytes; these tables are copied unchanged into subsets) equal those of the 2001 MingLiU/PMingLiU release (`28233BF1`, `A344A1EB`, `05BCF058`) or of DFKai-SB (`5A30CA3B`, `13A42602`, `11E5EAD4`). The interpreter (`TrueTypeHinter`) is a safe-code implementation of the instruction set in the OpenType specification, in version 35 semantics (movement along both axes). It is a budgeted sandbox: at most 2²⁰ instructions for each of the font and control value programs and for all programs of one glyph together (components and composite), 2¹⁸ per single glyph program, call depth 32, stack, storage, CVT, function and twilight sizes from `maxp` and fixed caps, 2¹⁴ points per program, and every index, jump target and instruction length checked. A program that exceeds a bound, misbehaves, or turns instructions off in `prep` makes the font fall back to its plain outlines when it happens at load. At glyph time the scope is narrower: a program that fails on its own leaves just its component or composite unhinted (its points are untouched, the rest of the glyph keeps the other programs' results); only when the shared per-glyph 2²⁰ budget is spent is the whole glyph drawn from its plain outline. Either way one warning diagnostic names the first reason. Nothing else about the font changes: advance widths, Unicode and glyph selection are the same as before. ## Limits Font programs ≤ 64 MiB, CMap/ToUnicode streams ≤ 8 MiB, `CIDToGIDMap` ≤ 128 KiB, `/W` and `/W2` ≤ 2²⁰ entries, ≤ 2¹⁸ cached glyphs per composite font, ≤ 4096 cached outlines per font, ≤ 4096 direct (unnumbered) font dictionaries cached per document. Decoding errors in a font's streams are diagnostics, never exceptions. ## In the interpreter The content interpreter reads each shown string with `font.Next` (so the Unicode chain, widths and vertical metrics above are what text positioning uses). A glyph run's `PdfGlyph` carries the glyph id, Unicode (null when unknown) and the outline as a path in text-space units for font size 1 (the font's outline matrix applied; quadratic segments raised to cubics); a missing glyph has no outline and still advances. In vertical writing the offsets are those of the glyph origin and the glyph carries v and w1. A Type 3 run carries the text with no outlines, then each glyph with a procedure runs as content between `BeginType3Glyph` and `EndType3Glyph`: CTM = font matrix x text space, the font's `/Resources` (else the current ones), patterns anchored to glyph space, `d1` ignoring later colour operators, nesting and cycles limited with forms and patterns. Only render mode 3 affects Type 3 text. <!-- https://recursiveai.org/onepdf/docs/migrating-from-pdfpig/ --> # Migrating from PdfPig This maps the PdfPig calls most programs use to their OnePDF equivalents. It is based on PdfPig's public documentation and README; check its docs for details of its behavior. OnePDF's API is smaller in some places and larger in others, so this is a map, not a drop-in replacement. ## The differences that matter first | | PdfPig | OnePDF | |---|---|---| | Namespace | `UglyToad.PdfPig` | `OnePdf` (text types in `OnePdf.Text`) | | Page numbers | `GetPage(1)` is the first page | **Zero-based**: `doc.Pages[0]`. Printed page labels are separate: `page.Label`. | | Coordinates | PDF points, origin bottom-left | PDF points, origin bottom-left, in an upright page space (rotation applied) | | Errors | Mostly lenient, some exceptions | Lenient by default, repairs recorded in `doc.Diagnostics`; `PdfOpenOptions.Strict` turns warnings into errors | | Untrusted input | You set your own guards | `PdfLimits` on by default, checked before allocation | | Lifetime | `PdfDocument` is `IDisposable` | `PdfDocument` is `IDisposable`; it holds the file open and reads by position | | Threads | Check PdfPig's docs before sharing a document across threads | Any number of threads can read pages of one document | ## Opening | PdfPig | OnePDF | |---|---| | `PdfDocument.Open("a.pdf")` | `PdfDocument.Open("a.pdf")` | | `PdfDocument.Open(stream)` / `Open(bytes)` | `PdfDocument.Open(stream)` / `Open(ReadOnlyMemory<byte>)`; non-seekable streams: `await PdfDocument.OpenAsync(stream)` | | `PdfDocument.Open("a.pdf", new ParsingOptions { Password = "pw" })` | `PdfDocument.Open("a.pdf", new PdfOpenOptions { Password = "pw" })` | | `ParsingOptions.UseLenientParsing` | Lenient is the default; `new PdfOpenOptions { Strict = true }` for the opposite | | `ParsingOptions.SkipMissingFonts` | Not needed: a missing font is substituted and recorded in `Diagnostics` | | `document.NumberOfPages` | `doc.PageCount` | | `document.Version` | `doc.Version` | | `document.IsEncrypted` | `doc.IsEncrypted` | | (try/catch around `Open`) | `PdfDocument.TryOpen(path, out var doc, out var error)` | ```csharp // PdfPig using var document = PdfDocument.Open("a.pdf", new ParsingOptions { Password = "secret" }); // OnePDF using var doc = OnePdf.PdfDocument.Open("a.pdf", new OnePdf.PdfOpenOptions { Password = "secret" }); ``` A wrong or missing password throws `PdfPasswordException`. The user or the owner password opens the file. `doc.Permissions` reports the permission flags; they are not enforced. ## Pages | PdfPig | OnePDF | |---|---| | `document.GetPages()` | `doc.Pages` (an `IReadOnlyList<PdfPage>`; `foreach` works) | | `document.GetPage(n)` (one-based) | `doc.Pages[n - 1]` | | `page.Number` | `page.Index` (zero-based) | | `page.Width`, `page.Height` | `page.CropBox.Width`, `page.CropBox.Height` (other boxes: `MediaBox`, `TrimBox`, `BleedBox`, `ArtBox`); `page.Rotate` (degrees), `page.UserUnit` | | `page.GetAnnotations()` | `page.Annotations` (links: `page.Links`) | | `page.GetImages()` | Images arrive through the device: interpret the page and handle `DrawImage`, or read the XObjects from `page.Resources` | | `document.GetBookmarks()` | `doc.Outline` | | Embedded files, form fields | `doc.EmbeddedFiles`, `doc.Form` | ## Metadata | PdfPig | OnePDF | |---|---| | `document.Information.Title` / `Author` / `Subject` / `Keywords` / `Creator` / `Producer` | `doc.Info.Title` / `Author` / `Subject` / `Keywords` / `Creator` / `Producer` | | `document.Information.CreationDate` / `ModifiedDate` (strings) | `doc.Info.CreationDate` / `ModificationDate` (`DateTimeOffset?`) | | `document.GetXmpMetadata()` | `doc.Xmp` (parsed fields plus `RawBytes`) | | Other Info keys | `doc.Info.Entries`, or `doc.Info.Dictionary` for the raw COS dictionary | ## Text PdfPig's `Page.Text` is the content-stream order of letters. OnePDF's default is reading order (structure tree for tagged files, geometry otherwise). Use `PdfTextMode.ContentOrder` for the nearest equivalent of `Page.Text`. | PdfPig | OnePDF | |---|---| | `page.Text` | `pageText.GetText(PdfTextMode.ContentOrder)` | | `ContentOrderTextExtractor.GetText(page)` | `pageText.GetText(PdfTextMode.ContentOrder)` | | `page.GetWords()` | `pageText.Words` (each `PdfTextWord` has `Text` and `Bounds`) | | `word.Text`, `word.BoundingBox` | `word.Text`, `word.Bounds` (`PdfRectangle`: `Left`, `Bottom`, `Right`, `Top`) | | `page.Letters` | `pageText.Glyphs` (`PdfTextGlyph`) from `page.ExtractText()`; whole-document `doc.ExtractText()` drops glyphs to bound memory, so pass `RetainGlyphs = true` or call `pageText.ReadGlyphs()` | | `letter.Value` | `glyph.Text` | | `letter.Location` / `StartBaseLine` | `glyph.Origin` / `glyph.Matrix` | | `letter.GlyphRectangle` | `glyph.Bounds` (axis-aligned) or `glyph.Quad` (rotated) | | `letter.PointSize` | `glyph.Size` | | `letter.FontName` | `glyph.FontName` | | `letter.Color` | `glyph.FillColor` | | Docstrum / recursive X-Y cut / reading-order analyzers (`DocstrumBoundingBoxes`, `UnsupervisedReadingOrderDetector`) | Built in: `pageText.Blocks` > `Lines` > `Words`, already in reading order; `PdfTextMode.Layout` for a `pdftotext -layout` style string | | Hidden / OCR text | Included and flagged: `glyph.IsVisible`, `word.IsVisible` | ```csharp // PdfPig using var document = PdfDocument.Open("a.pdf"); foreach (Page page in document.GetPages()) { Console.WriteLine(page.Text); foreach (Word word in page.GetWords()) Console.WriteLine($"{word.Text} {word.BoundingBox}"); } // OnePDF using var doc = OnePdf.PdfDocument.Open("a.pdf"); foreach (OnePdf.Text.PdfPageText page in doc.ExtractText()) // pages in parallel { Console.WriteLine(page.Text); foreach (OnePdf.Text.PdfTextWord word in page.Words) Console.WriteLine($"{word.Text} {word.Bounds}"); } ``` `doc.ExtractText()` does every page in parallel and also flags headers and footers by their repetition across pages; the degree is `PdfTextOptions.MaxDegreeOfParallelism` (default min(4, cores)). To keep a long document within memory it does not keep each page's glyph list (`PdfTextOptions.RetainGlyphs`); every plain-text mode, words, lines and blocks are still there, and `pageText.ReadGlyphs()` reads the glyphs of a page on request. Things that behave differently and can change your output: - Ligatures are decomposed by default (`DecomposeLigatures`); `/ActualText` replaces the text it covers. - Line-end hyphens are kept unless you set `JoinHyphenatedWords`. - Text that is clipped out or off the page is excluded unless you set `IncludeClippedAndOffPage`. - Duplicate fake-bold text is collapsed (`CollapseDuplicates`). - Right-to-left text comes out in logical order (UAX #9), not in the visual order of the content stream. - Words with no Unicode mapping come out as U+FFFD; OnePDF never guesses by OCR. ## Beyond text PdfPig has no real page rendering. In OnePDF, `page.Interpret(device)` drives an `IPdfDevice` with paths, glyph runs, images, shadings and groups; `PdfDisplayList` is the recording device. Producing pixels is the job of a separate package, `RecursiveAI.OneImage.Pdf`, built on OneImage (see [PDF pages as images](https://recursiveai.org/oneimage/docs/pdf/)). ## What OnePDF does not have PdfPig builds documents from nothing with `PdfDocumentBuilder`. OnePDF does too, with `PdfDocument.CreateEmpty`, `InsertBlankPage` and the content builder ([content creation](https://github.com/recursive-ai-oss/onepdf-csharp/blob/main/docs/content-creation.md)). It also changes and saves existing documents, including incremental updates and encryption ([writing](https://github.com/recursive-ai-oss/onepdf-csharp/blob/main/docs/writing.md)). The core content builder does no text shaping; `RecursiveAI.OnePdf.Layout` shapes complex scripts (Arabic, Indic and more) when it lays out text (see [layout text](https://recursiveai.org/onepdf/docs/layout-text/)). <!-- https://recursiveai.org/onepdf/docs/migrating-from-pdfsharp/ --> # Migrating from PDFsharp This maps the PDFsharp calls most programs use to their OnePDF equivalents for opening, reading, changing, saving and page operations. It is based on PDFsharp's public documentation (docs.pdfsharp.net, version 6.x) and its published API reference; check those for details of its behavior. OnePDF's API is smaller in some places and larger in others, so this is a map, not a drop-in replacement. ## What OnePDF does differently PDFsharp is first of all a library for *creating* PDF files: you draw with `XGraphics`, use `XFont`, `XImage` and MigraDoc. OnePDF reads, extracts, interprets into a device and edits existing files. It also draws text, images and shapes onto new or existing pages ([content creation](https://github.com/recursive-ai-oss/onepdf-csharp/blob/main/docs/content-creation.md)), and the separate package `RecursiveAI.OnePdf.Layout` is a layout engine in MigraDoc's place, with shaped text (see [layout](https://recursiveai.org/onepdf/docs/layout/)). ## The differences that matter first | | PDFsharp | OnePDF | |---|---|---| | Namespaces | `PdfSharp.Pdf`, `PdfSharp.Pdf.IO` | `OnePdf` (text types in `OnePdf.Text`) | | Page numbers | `Pages[0]` is the first page | Zero-based too: `doc.Pages[0]`. Printed page labels are separate: `page.Label`. | | Reading vs changing | `PdfReader.Open(path, PdfDocumentOpenMode.…)` decides: `Modify`, `Import`, `ReadOnly`, `InformationOnly` | One mode of reading. Changing is opt-in: `new PdfOpenOptions { AllowEditing = true }`. Pages can be imported from any open document, read-only or not. | | New document | `new PdfDocument()` | `PdfDocument.CreateEmpty()` | | Untrusted input | You set your own guards | `PdfLimits` on by default, checked before allocation | | Damaged files | Throws for what it cannot read | Repairs what it can (record in `doc.Diagnostics`); `PdfOpenOptions.Strict` turns warnings into errors | | Page size of a new page | `AddPage()` gives a default size (`page.Size`, `page.Width`) | You give the size: `InsertBlankPage(index, width, height)` (points) | | Threads | Check PDFsharp's docs before sharing a document across threads | Any number of threads can read one document; changes are for one thread at a time | | Lifetime | `PdfDocument` is not disposable | `PdfDocument` is `IDisposable`; it holds the file open and reads by position. Merge and split results live in memory. | ## Opening | PDFsharp | OnePDF | |---|---| | `PdfReader.Open("a.pdf", PdfDocumentOpenMode.Import)` | `PdfDocument.Open("a.pdf")` (the document is only read; import from it with `ImportPages`) | | `PdfReader.Open("a.pdf", PdfDocumentOpenMode.Modify)` | `PdfDocument.Open("a.pdf", new PdfOpenOptions { AllowEditing = true })` | | `PdfReader.Open("a.pdf", PdfDocumentOpenMode.ReadOnly)` / `InformationOnly` | `PdfDocument.Open("a.pdf")`; pages and objects are read when first used, so opening for a page count or metadata is cheap | | `PdfReader.Open(stream, mode)` | `PdfDocument.Open(stream)` / `Open(ReadOnlyMemory<byte>)`; non-seekable streams: `await PdfDocument.OpenAsync(stream)` | | `PdfReader.Open("a.pdf", "password")` | `PdfDocument.Open("a.pdf", new PdfOpenOptions { Password = "password" })` | | `document.PageCount` | `doc.PageCount` | | `document.Version` | `doc.Version` (a string such as `1.7`; a save raises it when the content needs it) | | `document.SecuritySettings` (permission flags of an opened file) | `doc.IsEncrypted`, `doc.Permissions` | A wrong or missing password throws `PdfPasswordException`. PDFsharp's documentation notes that a file with an owner password cannot be opened for modification with the user password (open it in `Import` mode instead and copy the pages). OnePDF has no such split: either password opens the file, and `doc.Permissions` reports the flags without enforcing them. Honouring them is up to your application. ```csharp // PDFsharp: pages of an owner-protected file, opened with the user password var import = PdfReader.Open(path, userPassword, PdfDocumentOpenMode.Import); var output = new PdfDocument(); foreach (PdfPage page in import.Pages) output.AddPage(page); output.Save("copy.pdf"); // OnePDF using var import = PdfDocument.Open(path, new PdfOpenOptions { Password = userPassword }); using var output = PdfDocument.CreateEmpty(); output.ImportPages(import); output.Save("copy.pdf"); ``` ## Pages | PDFsharp | OnePDF | |---|---| | `document.Pages` / `document.PageCount` | `doc.Pages` / `doc.PageCount` | | `document.AddPage()` | `doc.InsertBlankPage(doc.PageCount, width, height)` | | `document.InsertPage(index)` | `doc.InsertBlankPage(index, width, height)` | | `document.AddPage(importedPage)` | `doc.ImportPages(source, [pageIndex])` (indices in `source`) | | `document.Pages.Insert(index, page)` | `doc.ImportPages(source, [pageIndex], insertAt: index)` | | `document.Pages.RemoveAt(index)` | `doc.RemovePage(index)` / `doc.RemovePages(indexes)` | | Moving a page (remove it and insert it again) | `doc.MovePage(from, to)`; a whole new order: `doc.ReorderPages(newOrder)` | | `page.Rotate = 90` (absolute, a multiple of 90) | `doc.SetPageRotation(index, 90)`; relative: `doc.RotatePage(index, 90)` | | `page.Width`, `page.Height`, `page.MediaBox`, `page.CropBox` | `page.CropBox.Width` / `Height` (the visible area; `MediaBox`, `TrimBox`, `BleedBox`, `ArtBox` also), `page.Rotate`, `page.UserUnit` | | Duplicating a page (import the same page again) | `doc.DuplicatePage(index)` | | `page.Annotations` | `page.Annotations`, `page.Links` | OnePDF's page objects are read views; operations are methods of the document, and `doc.Pages` is rebuilt after each one (take pages again after calling an operation). Details of what each operation does to bookmarks, links, forms and the rest are in [page-operations.md](https://recursiveai.org/onepdf/docs/page-operations/). ### Merge ```csharp // PDFsharp var output = new PdfDocument(); foreach (string file in files) { var input = PdfReader.Open(file, PdfDocumentOpenMode.Import); foreach (PdfPage page in input.Pages) output.AddPage(page); } output.Save("merged.pdf"); // OnePDF var inputs = files.Select(f => PdfDocument.Open(f)).ToList(); using (PdfDocument merged = PdfDocument.Merge(inputs)) { merged.SetInfo("Title", "Merged"); merged.Save("merged.pdf"); } foreach (var d in inputs) d.Dispose(); ``` `Merge` carries more than the pages: bookmarks, named destinations the links use, form fields (with clashing names renamed), optional content, page labels and the structure tree of tagged files. PDFsharp's documentation says it does not write object streams, so files that contain them can grow when saved; OnePDF reads and writes them (`PdfSaveOptions.UseObjectStreams`). ### Split ```csharp // PDFsharp: one file per page for (int i = 0; i < input.PageCount; i++) { var output = new PdfDocument(); output.AddPage(input.Pages[i]); output.Save($"page-{i + 1}.pdf"); } // OnePDF IReadOnlyList<PdfDocument> parts = PdfDocument.Split(input, pagesPerPart: 1); for (int i = 0; i < parts.Count; i++) { parts[i].Save($"page-{i + 1}.pdf"); parts[i].Dispose(); } // or by ranges: PdfDocument.Split(input, [new PdfPageRange(0, 3), new PdfPageRange(3, 5)]) ``` Each part holds the objects its pages reach. A resource dictionary shared by every page of the source (typical of slide decks) is copied whole into each part. ## Metadata | PDFsharp | OnePDF | |---|---| | `document.Info.Title` (read) | `doc.Info.Title`, `Author`, `Subject`, `Keywords`, `Creator`, `Producer`, `CreationDate`, `ModificationDate` (dates are `DateTimeOffset?`) | | `document.Info.Title = "x"` | `doc.SetInfo("Title", "x")` (editable document; null removes the entry) | | XMP metadata | `doc.Xmp` (parsed fields plus raw bytes); `doc.SetXmp(bytes)` | | Outlines (`document.Outlines`) | `doc.Outline` (read; kept in step by page operations) | ## Saving | PDFsharp | OnePDF | |---|---| | `document.Save("out.pdf")` / `Save(stream)` | `doc.Save("out.pdf")` / `doc.Save(stream)`: a complete new file | | (no incremental update) | `doc.SaveIncremental(stream)`: the original bytes plus an update | | `document.Options.FlateEncodeMode`, compression options | `PdfSaveOptions.CompressStreams`, `XrefFormat`, `UseObjectStreams` | | `document.SecuritySettings.UserPassword` / `OwnerPassword` | `PdfSaveOptions { Encryption = PdfEncryptionAction.Set, NewEncryption = new PdfEncryptionOptions { UserPassword = …, OwnerPassword = … } }` | | `SecurityHandler.SetEncryptionToV4UsingAES()` (the default) | `Cipher = PdfCipher.Aes128` | | `SecurityHandler.SetEncryptionToV5()` | `Cipher = PdfCipher.Aes256` (the default of `PdfEncryptionOptions`) | | `SecurityHandler.SetEncryptionToNoneAndResetPasswords()` | `Encryption = PdfEncryptionAction.Remove` | | `SecuritySettings.PermitPrint`, `PermitExtractContent`, … | `PdfEncryptionOptions.Permissions` (`PdfPermissions.Print`, `CopyContent`, `ModifyContents`, `ModifyAnnotations`, `FillForms`, `AssembleDocument`, `PrintHighQuality`, …) | | RC4 for new files | Not offered for new files; an existing RC4 file saved with `Keep` stays RC4 | An encrypted source never needs special handling for page operations: it is read with its password, and what you write is plain unless you ask for encryption. ## Forms PDFsharp's documentation describes limited AcroForm support. OnePDF reads field values and kinds (`doc.Form`, `FindField`), merges and renames fields when pages are imported, and draws fields with missing appearances (`PdfAnnotation.GenerateAppearance`). It also fills, resets and flattens fields with typed validation ([forms](https://github.com/recursive-ai-oss/onepdf-csharp/blob/main/docs/forms.md)). ## Beyond what PDFsharp does - **Text extraction** (PDFsharp's FAQ says it has no high-level text extraction): `doc.ExtractText()`; see the [PdfPig guide](https://recursiveai.org/onepdf/docs/migrating-from-pdfpig/) for the API. - **Interpreting pages** into a drawing device or a recorded display list (`page.Interpret`), which a renderer can replay. - **Repair** of broken files and **limits** for hostile ones. ## Things that behave differently and can change your output - Imported pages are *copied*, not shared: a page imported twice becomes two pages with their own annotations, and nothing refers back to the source document. - Inherited page attributes (`/Resources`, `/MediaBox`, `/CropBox`, `/Rotate`) are written into the imported page. - `ImportPages` copies pages from a document you have open; there is no separate "import mode" document type, and the source is never changed. - Deleting a page repairs bookmarks, links and destinations instead of leaving them dangling, and drops the structure tree of a tagged file (it would keep the deleted content reachable). <!-- https://recursiveai.org/onepdf/docs/migrating-from-questpdf/ --> # Migrating from QuestPDF `OnePdf.Layout` generates documents from code in the same style as QuestPDF: you describe pages with nested delegates, chain decorators on a place that holds one child, and fill it with text, a column, a table and so on. This guide maps the concepts a QuestPDF program uses to their `OnePdf.Layout` counterparts and lists what differs and what is missing. It is based on QuestPDF's public documentation ([features overview](https://www.questpdf.com/features-overview.html) and the pages it links to); check those for details of QuestPDF's behavior. The names here are OnePDF's own, so this is a map, not a drop-in replacement: expect to rename calls, not to rethink the structure. `OnePdf.Layout` ships as the package `RecursiveAI.OnePdf.Layout` (see [layout.md](https://recursiveai.org/onepdf/docs/layout/)). ## What is the same - A document is composed in a delegate, once; layout and drawing happen at save time. - Each page has a size, margins and named slots for header, content, footer, background and foreground. - A slot takes decorators (padding, border, background, alignment, size limits) and one child. - `Column`, `Row`, `Layers`, `Table` and `List` take delegates that add items. - Reusable pieces are classes that implement a one-method interface. - Page numbers, hyperlinks, internal links, page breaks and keep-together rules exist, and a layout that cannot fit throws an exception that names the element path. - Units are points, with helpers for millimetres, centimetres and inches. ## Concept map | QuestPDF concept | OnePDF.Layout | |---|---| | Compose a document | `LayoutDocument.Compose(doc => ...)`, or `PdfDocument.Compose(...)` | | Generate a file / stream / bytes | `layout.Save(path)`, `layout.Save(stream, PdfSaveOptions)`, `layout.ToBytes()` | | Page with size and margin | `doc.Page(page => { page.Size(PageSize.A4); page.Margin(2.Cm()); ... })` | | Header / content / footer / background / foreground slots | `page.Header()`, `Content()`, `Footer()`, `Background()`, `Foreground()` | | Different first or even page header | `FirstPageHeader()`, `EvenPageHeader()` (and the footers) | | Padding, border, background, rounded corners | `Padding`, `Border`, `Background`, `Rounded` (per-side variants, `Start`/`End` for direction-aware sides) | | Width / height, min / max | `Width`, `Height`, `MinWidth`, `MaxHeight` and so on | | Aspect ratio | `AspectRatio` | | Alignment | `AlignLeft`, `AlignCenter`, `AlignRight`, `AlignTop`, `AlignMiddle`, `AlignBottom`, plus `AlignStart`/`AlignEnd` | | Extend to the available space | `ExpandHeight()` (`AlignMiddle` and `AlignBottom` imply it) | | Column, Row | `Column` with `Item()`; `Row` with `ConstantItem`, `RelativeItem`, `AutoItem` | | Layers (watermark, letterhead) | `Layers` with `Layer()` and `PrimaryLayer()` | | Table, spans, repeating header and footer | `Table`: `Columns`, `HeaderRow`, `Row`, `FooterRow`, `cell.Span(columns, rows)` | | Lists | `List`: `Bulleted`, `Numbered`, custom markers | | Image | `Image(bytes or path)` with `FitWidth`, `FitHeight`, `Contain`, `Cover`, `Stretch` (JPEG and PNG) | | Canvas-style drawing | `Canvas(height, surface => ...)` | | Rotate, scale, offset | `Rotate`, `Scale`, `Translate` | | Prevent page break, ensure space, page break | `KeepTogether`, `EnsureSpace`, `PageBreak` (and `KeepWithNext`) | | Text, spans, styles | `Text(...)` with `FontSize`, `Bold`, `FontColor`, ...; `Text(t => t.Span(...))` | | Page numbers | `t.CurrentPageNumber()`, `t.TotalPages()`, `t.SectionPages()` inside `Text(t => ...)` | | Hyperlink | `slot.Hyperlink(uri)` | | Section and internal links | `Anchor(id)`, `LinkToAnchor(id)`, `Bookmark(title, level)` | | Components | `IComponent` with `Compose(Slot slot)`; `slot.Component(instance)` or `slot.Component<T>()` | | Content direction | `doc.RightToLeft()`, `page.RightToLeft()`, `slot.RightToLeft()` | | Debug area, debug pointer | `doc.Debug()` draws every element's box and name; `slot.Name("...")` labels an element in errors | | Document metadata, PDF/A | `doc.Title(...)`, `doc.Language(...)`; PDF/A and encryption through `Save(stream, PdfSaveOptions)` | ## A page, side by side A header, a body with a table and a footer with page numbers, written the OnePDF way: ```csharp LayoutDocument.Compose(doc => { doc.Title("Invoice 1042").Language("en"); doc.Page(page => { page.Size(PageSize.A4); page.Margin(2.Cm()); page.Header().Text("Acme Corp").FontSize(9).FontColor(Colors.Grey); page.Content().Column(col => { col.Spacing(8); col.Item().Heading(1).Text("Invoice 1042").FontSize(20).Bold(); col.Item().Table(table => { table.Columns(c => { c.Relative(3); c.Constant(70); }); table.HeaderRow(r => { r.Cell().Text("Item").Bold(); r.Cell().AlignEnd().Text("Price").Bold(); }); table.Row(r => { r.Cell().Text("Widget"); r.Cell().AlignEnd().Text("12.00"); }); }); }); page.Footer().AlignCenter().Text(t => { t.CurrentPageNumber(); t.Span(" / "); t.TotalPages().ReservedDigits(2); }); }); }).Save("invoice.pdf"); ``` If you know QuestPDF, the shape is the same: slots, decorator chains and nested delegates. The differences are in the details below. ## Differences that matter first | | QuestPDF | OnePDF.Layout | |---|---|---| | Decorator order | Each decorator wraps what follows (see its documentation for details) | The same, and neighbouring box decorators are merged into one box when their order allows it. The result is identical either way. | | Text alignment | Chosen on the text | Chosen on the text. Text fills the width it is offered, so `AlignCenter().Text(...)` centres the text itself. | | Total pages | Part of the text | Width is reserved before the total is known: `ReservedDigits` (default 4). Use `ReservedDigits(2)` in short documents so a centred footer has no gap. | | Tagged PDF | Opt in for accessibility | On by default: paragraphs, tables, lists, figures and links are tagged; headers, footers, borders and backgrounds are artifacts. Add `Heading(n)`, `AltText`, `Language`; `Tagged(false)` turns it off. | | Shaping | Fonts and shaping from its native stack | OneFont, managed: OpenType GSUB/GPOS, Arabic, Indic, USE and Thai shapers, UAX #9 bidi, UAX #14 line breaking, per-span font fallback. | | Fonts | Fonts registered with the library | `doc.Fonts.Add(bytes, family)`; default Liberation Sans from `RecursiveAI.OnePdf.Fonts`, embedded as a subset. System fonts are never used unless you add them, so output is the same on every machine. | | Dependencies | Native binaries | Pure managed, BCL only, NativeAOT- and trim-compatible | | Memory | See its documentation | Pages are written as they are laid out; 10,000 one-line pages need about 68 MB peak resident (NativeAOT, linux-x64) | | Overflow | Layout exception with element path | `PdfLayoutException` with the path (`Page[3] > Content > Table > Row 14 > Cell 2 > Image`), and the space offered and wanted | | Previewing | A companion app with hot reload | None; render with your own viewer, or with `RecursiveAI.OneImage.Pdf` | ## Licensing QuestPDF is licensed under its Community, Professional and Enterprise licenses; whether you need to pay depends on who you are (for example an organisation's annual gross revenue). Read the current terms on its [license selection guide](https://www.questpdf.com/license/guide.html) and [legal terms page](https://www.questpdf.com/license/). `OnePdf.Layout` is `MIT OR Apache-2.0` like the rest of OnePDF, with no revenue test and no registration. The font packages are OFL 1.1 and ship their notices. ## What is missing Things QuestPDF's public documentation lists that `OnePdf.Layout` does not have yet: - Shadows and scale-to-fit, flip, unconstrained and shrink wrappers. - Inline flow of arbitrary elements (an inlined layout); hyperlinks inside a text span (put `Hyperlink` on the slot that holds the text instead). Elements embedded in text are supported: see [inline objects](https://recursiveai.org/onepdf/docs/layout/#inline-objects). - Show-once / skip-once / show-if helpers and lazy content. (Multi-column flow and table of contents generation are supported: see [layout](https://recursiveai.org/onepdf/docs/layout/#multi-column-flow).) Plain C# `if` statements and loops in your delegates replace the conditional ones; header-only-on-first-page is `FirstPageHeader()`. - A page number inside a paragraph keeps the paragraph on one line; text with page numbers does not wrap. - Charts, barcodes and maps (draw them with `Canvas` or pass the rendered image). SVG is supported: see [SVG in layout](https://recursiveai.org/onepdf/docs/layout-svg/). - A previewer or hot reload. - Document operations such as merging and encryption are not part of the layout types, but OnePDF core has them: [page operations](https://recursiveai.org/onepdf/docs/page-operations/), [writing](https://recursiveai.org/onepdf/docs/writing/) and `Save(stream, PdfSaveOptions)`. - Author, subject and keywords on `LayoutDocument` (set them on the `PdfDocument` from `ToPdfDocument()`). ## Moving a component ```csharp sealed class Address(string name, string city) : IComponent { public void Compose(Slot slot) => slot.Column(column => { column.Item().Text(name).Bold(); column.Item().Text(city); }); } ``` Use it with `slot.Component(new Address("Ada Lovelace", "London"))`. A component receives the slot it fills, so it chooses its own content and not the decorators around it. <!-- https://recursiveai.org/onepdf/docs/repair/ --> # Reading and repair rules How OnePDF opens a file, and what it does when the file is damaged. Every repair adds a `PdfDiagnostic` (severity Warning, with object number and offset where known) to `PdfDocument.Diagnostics`. With `PdfOpenOptions.Strict` each of them throws `PdfFormatException` instead. Informational notes (Info) never throw. ## The fallback chain 1. **Trust the cross-reference data.** Find the last `startxref` in the final 8 KiB, read the section it names, then follow `/Prev` back to the first section. Sections are tables (`xref` ... `trailer`) or cross-reference streams (ISO 32000-2 7.5.8). In a hybrid file the table's `/XRefStm` supplies entries for objects the table lists as free (7.5.8.4). Each section is one revision (`PdfDocument.Revisions`, oldest first). 2. **Fix individual objects by scanning.** When an entry's offset does not lead to `n g obj` with the expected number, the file is scanned once (shared by all later lookups) for object headers and the object is read from where it is found. Diagnostic: "the cross-reference offset ... does not point at the object". An object that cannot be found reads as null (severity Error). 3. **Rebuild.** If the xref data cannot be read at all (no `startxref`, no section at the offset, a damaged or truncated table or trailer, a `/Prev` loop, a subsection claiming more entries than the file can hold) or the trailer's `/Root` does not lead to a dictionary, the whole file is scanned and the cross-reference data is replaced by what the scan finds. Diagnostic: "the cross-reference data is unusable (...); rebuilt by scanning the file for objects". Limit violations (`PdfLimitException`) are never repaired: they propagate. ## Rules in detail - **Header.** `%PDF-` is looked for in the first 1024 bytes. Bytes before it are ignored (Info). A missing header is a warning. Offsets in `startxref` and the xref data are tried as absolute file positions, then relative to the header; the first that lands on a section is used for the whole file (Info). - **Table entries.** Entries are read as tokens, not fixed 20-byte lines, so 19-byte and CR-only lines work. An entry of type `n` with offset 0 is free. A first subsection that starts at 1 although its first entry is the free head (`0000000000 65535 f`) is renumbered from 0 (warning). - **Trailer.** The newest trailer is used; `Root`, `Info`, `ID` and `Encrypt` it lacks are taken from older trailers. Each revision keeps its own trailer. - **Stream length** (7.3.8.2). `/Length` (direct, or indirect) is trusted only if `endstream` follows the data after at most 16 white-space bytes. Otherwise the data ends at the next `endstream`, else `endobj`, else the end of the file, less one end-of-line (CRLF, LF or CR). Diagnostic: "the stream's /Length ... is wrong". A `/Length` that is indirect and cannot be resolved takes the same path. The length of an object stream or xref stream is never read from inside an object stream. - **Stream keyword.** `stream` may be followed by spaces before its end-of-line; CR alone is accepted. - **Missing `endobj`.** Any token other than `endobj` after an object's value is a warning; reading continues. - **Unterminated containers.** An array or dictionary cut off by `endobj`, `stream`, `obj`, `xref`, `trailer`, `startxref` or the end of data is closed there (warning). A stray `]` or `>>` is ignored. A dictionary key that is not a name skips that token; a key without a value is dropped. Null values are dropped (a missing key and a null value are the same, 7.3.7). - **Numbers.** Leading signs, a second decimal point, and a minus after digits are tolerated (`--5` is -5, `1.2.3` is 1.2, `5-3` is 5). Integers beyond 64 bits become reals. - **Strings and names.** Unterminated strings run to the end of data. Bad `#xx` escapes in names stay literal. Name bytes that are not valid UTF-8 are read as Latin-1. - **Object streams.** The entry's index is checked against the stream's header; a wrong index is found by number (warning). `/N` and `/First` are validated against the decoded size before anything is allocated. When `/N` or `/First` is missing, negative, too large, or `/First` lies inside the header, the header is salvaged (warning "invalid /N or /First"): the leading run of `number offset` integer pairs is the header (at most `/N` pairs when `/N` is sane, else as many as the data allows), and `/First` is taken to be where that run ends. A header that stops early keeps the pairs read so far and the rest of the stream's objects read as null (warning). An object stream that is missing or not a stream makes its objects read as null (warning). Strict mode throws for each of these. - **References.** A reference to a missing, free or null object resolves to null. A chain of references to references longer than `PdfLimits.MaxRecursionDepth` (or a loop) reads as null (warning). A load that needs itself is a `PdfFormatException`. - **Page tree.** `Pages` (and `PageCount`) walk `/Pages` iteratively once; the inheritable attributes `Resources`, `MediaBox`, `CropBox` and `Rotate` come from the nearest ancestor that has them. A page without a usable `MediaBox` is US Letter (warning); a `CropBox` is clipped to the `MediaBox`; a `Rotate` that is not a multiple of 90 is 0 (warning). Nesting beyond `MaxNestingDepth` is skipped (warning). A node reached twice is skipped (warning); a kid that is not a dictionary is skipped (warning); a node without `/Kids` is a page unless `/Type` is `/Pages`. - **External streams.** A stream with `/F` reads as empty (warning). Nothing outside the document is ever opened. - **Encryption.** `/Encrypt` is read as soon as the cross-reference data is, before the catalog or any object stream is trusted (a file rebuilt by scanning authenticates before its object streams are opened). `PdfOpenOptions.Password` is tried as user then owner password; the empty password is always tried. A wrong or missing password is `PdfPasswordException`; an unsupported or malformed handler is `PdfEncryptionException`. Everything parsed before authentication is dropped from the cache. Strings are decrypted when their object is parsed, stream data when it is read (before filters), under the object number and generation of the containing indirect object. Exempt: the `/Encrypt` dictionary, cross-reference streams, strings inside object streams (the stream is encrypted as a whole) and the trailer `/ID`. A `Crypt` filter names the crypt filter to use (`Identity` leaves the data alone) and is not part of the decode chain. A metadata stream is left in the clear when `/EncryptMetadata` is false. Damaged ciphertext (bad padding, partial block) decrypts as far as it goes with a warning. ## The rebuild scan One pass over the file, byte by byte, no whole-file buffer: - `obj` preceded by `<number> <generation>` and not by `end`, followed by a delimiter or white space, starts an object. The object is parsed; for a stream the body is skipped using a trusted `/Length` or the next `endstream`, so bytes inside stream data (an embedded PDF, say) are not taken for objects. - The last definition of a number wins. `trailer` followed by a dictionary is a trailer candidate; so is the dictionary of any `/Type /XRef` stream. - The trailer used is the last candidate with a `/Root`; `Root`, `Info`, `ID` and `Encrypt` are each taken from the latest candidate that has them. With no candidate, the last `/Type /Catalog` object is used (warning). - Objects inside object streams are added after the scan. An object stream counts as defined where it sits in the file, so a later direct definition of the same number wins over its contents and a later object stream wins over an earlier definition. An object stream that cannot be read is skipped (warning). - The result is a single revision. More than `PdfLimits.MaxObjects` objects raises `PdfLimitException`. ## Limits checked while reading | Limit | Checked | |---|---| | `MaxObjects` | declared subsection and `/Index` counts before reading entries; headers during the rebuild scan; page-tree nodes | | `MaxNestingDepth` | on entering each array or dictionary level (explicit stack, no recursion) | | `MaxDecodedStreamBytes`, `MaxExpansionRatio` | by the filters while they run; raw stored size before reading it | | `MaxRecursionDepth` | reference-to-reference chains | ## The typed layer Name trees, number trees, outlines, form field trees and action chains are walked iteratively with a visited set, so a node reached twice (a cycle) is skipped with one warning, and with `MaxObjects` / `MaxNestingDepth` bounds. A malformed tree key, a non-dictionary page or annotation entry, a destination that leads nowhere, an unparseable date or an XMP packet that is not well-formed XML never throws: the value reads as null or empty (XMP keeps its raw bytes). DTDs in XMP are refused, never expanded. Stream-backed text (embedded files, XFA, XMP) is size-checked against the limits before it is read; over the limit the item is ignored with a warning, except `PdfEmbeddedFile.ReadContent`, which throws `PdfLimitException` because the caller asked for it. <!-- https://recursiveai.org/onepdf/docs/display-list-format/ --> # Display-list text format **Format version: 6** The text form of a `PdfDisplayList` (or of any interpretation, written straight from the interpreter). It is used for snapshot tests and is meant to be comparable across implementations: it is plain UTF-8 text, one record per line, with `\n` line ends. Any change to what is written bumps the version below and the number in the header line. ## Header The first line is `onepdf-display-list 6`: the magic word and the format version. ## Numbers Numbers are rounded to four decimals (ties to even), printed in plain decimal with no exponent, no trailing zeros and no negative zero: `0`, `12`, `-0.5`, `3.1416`. The rounding and printing use integer arithmetic on the scaled value, so all platforms print the same text. Non-finite values print as `nan`, `inf`, `-inf`; magnitudes of 1e14 or more print as `huge` or `-huge`. A *matrix* is `[a b c d e f]`. A *box* is `left bottom right top`. ## Lines Each line is indented by two spaces per open bracket (page, clip, group, soft mask, marked content, annotation); the indentation is informative. The record keyword is the first word. | Record | Meaning | |---|---| | `page box=<box> name=<CropBox\|MediaBox\|BleedBox\|TrimBox\|ArtBox> rotate=<0\|90\|180\|270> unit=<n> scale=<n> size=<w> <h> base=<matrix>` | Start of a page: the selected box in default user space, the rotation and user unit applied, the total scale, the output size and the matrix from default user space to the output (y grows downward). | | `end-page` | End of the page. | | `gs ctm=<matrix> fill=<color> stroke=<color> lw=<n> cap=<n> join=<n> miter=<n> dash=[<n> ...] phase=<n> flat=<n> ri=<name> fa=<n> sa=<n> bm=<name> sm=<id> ais=<0\|1> op=<fill><stroke><mode> [tk=0]` | The graphics state that applies to the records that follow, written whenever it differs from the last one written. `ctm` maps the user space of the following paths to the output. `fa`/`sa` are the fill and stroke alpha, `bm` the blend mode, `sm` the soft mask id (0 = none), `op` three characters: fill overprint, stroke overprint (0/1) and the overprint mode. `tk=0` appears only when the text knockout flag (`/TK`, ISO 32000-2 9.3.8) is off; it is on by default and then not written. | | `fill <nonzero\|evenodd> <path>` | Fill a path. | | `stroke <path>` | Stroke a path. | | `fillstroke <nonzero\|evenodd> <path>` | Fill and stroke one path as a single knockout group, from the `B`, `B*`, `b` and `b*` operators (ISO 32000-2 11.7.4.4); the path is already closed for `b` and `b*`. A fill followed by a separate stroke (`f` then `S`) stays two records. | | `clip <nonzero\|evenodd> <path>` | Intersect the clip with a path; opens a clip bracket. | | `text-clip` | Intersect the clip with the glyphs shown in clipping render modes since the last `BT`; opens a clip bracket. | | `pop-clip` | Close the most recent clip bracket. | | `text font=<name> size=<n> mode=<0-7> tc=<n> tw=<n> th=<n> rise=<n> tm=<matrix> vert=<0\|1> adv=<x> <y> codes=<hex> glyphs=[<glyph> ...]` | One shown string. `th` is the horizontal scale as a factor. `tm` is the text matrix at the start of the string. `adv` is the total displacement of the text matrix. `codes` are the string bytes in lower-case hex. Each glyph is `<code>@<offsetX>,<offsetY>,<width>` optionally followed by `:g<glyph id>` and `:u<UTF-8 hex>` when known, and, in a vertical run, `:v<vx>,<vy>,<w1>` (the position vector and vertical advance in em units; the offsets are those of the glyph origin, ISO 32000-2 9.7.4.3). Outlines are not written. A Type 3 font's run lists its text with no outlines; the glyph procedures follow as `begin-glyph` brackets. | | `image <inline\|/Name> bytes=<n> fnv=<hex> data=<finished> dict=<object>` | An image drawn into the unit square of the CTM: the length and FNV-1a 64-bit hash of its encoded data (`bytes=-1` when unreadable), the finished image and its dictionary. `<finished>` is `decoded:<w>x<h>:<colour space name>:<fnv of the 8-bit samples>`, `stencil:<w>x<h>:<fnv of the coverage plane>`, `encoded:<w>x<h>` (no decoder for its codec), `none` (unusable dictionary) or `limit` (over an image limit); a mask or colour key adds `:alpha=<w>x<h>:<fnv>`. Decoding uses the default decoder registry, which ships empty. | | `shading type=<1-7> cs=<final space> fn=<0\|1> bbox=<box\|none> bg=<(c ...)\|none> aa=<0\|1> ...` | The `sh` operator, or the shading inside a shading pattern. The shading is decoded and evaluated by OnePDF; the line states what a device receives. Type-specific fields follow: type 1 `domain=(x0 x1 y0 y1) matrix=<matrix> at-centre=(c ...)`; types 2 and 3 `coords=(...) domain=(t0 t1) extend=<0\|1><0\|1> at-start=(c ...) at-mid=(c ...) at-end=(c ...)` (the colours the evaluator returns at the start, middle and end of the domain, in the final space); types 4 to 7 `vertices=<n> triangles=<n>`, for patch meshes also `patches=<n> div=<n>`, then `fnv=<hash>` (FNV-1a 64 of the rounded vertex data and the triangle indices) and `truncated` when data was dropped at a limit. | | `pattern-def #<id> /Name tiling paint=<1\|2> tile=<1-3> bbox=<box> step=<x> <y> matrix=<matrix>` / `end-pattern-def` | The first use of a tiling pattern in a colour: its definition, followed (indented, between the two records) by the records of its cell, which are written like a page's content with pattern space as user space. `matrix` maps pattern space to the output. Colours name the pattern as `pattern(#<id> ...)`. | | `pattern-def #<id> /Name shading matrix=<matrix>` / `end-pattern-def` | The first use of a shading pattern: one `shading` record between the two. | | `begin-group bbox=<box> isolated=<0\|1> knockout=<0\|1> cs=<object>` / `end-group` | A transparency group. | | `begin-softmask id=<n> type=<luminosity\|alpha> bbox=<box> bc=<object> tr=<object> trtable=<hex\|none>` / `end-softmask` | A soft mask definition; its content sits between the two records. `trtable` is the 256-byte transfer table OnePDF evaluated from `/TR` (ISO 32000-2 11.6.5.2), 512 lower-case hex digits, or `none` for identity. | | `begin-mc /Tag props=<object>` / `end-mc` | A marked-content sequence. `props` is `null` without properties. | | `begin-glyph font=<name> code=<n> name=<glyph name\|none> adv=<n> [u=<UTF-8 hex>]` / `end-glyph` | A Type 3 glyph procedure, run as content after the `text` record that showed it (not for render mode 3 or a code without a procedure). The records between the two are written like a page's content; the `gs` line before `begin-glyph` has a CTM that already includes the font matrix and the text space, so glyph coordinates map straight to the output. | | `begin-annot /Subtype rect=<box> flags=<n>` / `end-annot` | An annotation appearance. | ## Paths A path is a space-separated list of `M x y`, `L x y`, `C x1 y1 x2 y2 x3 y3` and `Z`, in the user space of the CTM in force. ## Colors `gray(g)`, `rgb(r g b)`, `cmyk(c m y k)`, `calgray(g)`, `calrgb(r g b)`, `lab(L a b)`, `icc<N>:<profile length>:<FNV-1a 64 of the profile>(c ...)`, and for pattern colors `pattern(#<id> /Name)` (followed by the base space name and tint components for uncolored patterns); `<id>` numbers the patterns in order of first use and refers to a `pattern-def` record written before the `gs` line that uses it. ## Objects Dictionaries, arrays and other objects print in a canonical form: dictionary keys sorted by ordinal, `/Name` with non-regular characters as `#xx`, strings as `<hex>`, references as `n g R`, streams as `stream<<...>>`, `true`, `false`, `null`; nesting beyond eight levels prints `...`. ## Versioning The version is an integer that changes whenever the output for any input could change (new records, new fields, different rounding). Readers must reject a version they do not know. History: 2 added the `data=` field to image lines and the `calgray`/`calrgb` colour spaces (before, CalGray and CalRGB printed as `gray` and `rgb`); an ICC profile that is damaged or does not match its component count now prints as the device space with that many components. Version 3 replaced the unevaluated `shading /Name obj=` record by the decoded `shading type=...` record, added `pattern-def`, and changed pattern colours to `pattern(#<id> /Name ...)`. Version 4 added the `begin-glyph` bracket for Type 3 glyph procedures and the `:v` suffix of vertical glyphs; glyph runs now carry real glyph ids, Unicode and widths from the PDF fonts. Version 5 added the `fillstroke` record (before, `B`/`b` wrote a `fill` and a `stroke` record) and the `tk=0` field of `gs`. Version 6 added the `trtable=` field of `begin-softmask`. <!-- https://recursiveai.org/onepdf/docs/changelog/ --> # Changelog All notable changes are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versions follow [Semantic Versioning](https://semver.org/). ## [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)` (and `PlanTagging`, 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 one `L` with `Lbl` and `LBody`, table rows and cells from the lines and their cells with a header row, headers and footers as pagination artifacts, link annotations as `Link` elements with object references, figures by area, everything else an artifact. Each default is a `PdfTagOptions` setting, and `Customize` can 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 true` or contain marked content with an `/MCID` are refused with `PdfTaggingException`, as are references that come from another document, content referenced twice, impossible nesting and pages that cannot be rewritten safely. A link annotation without `/Contents` gets the link's alternate text or its words. See `docs/tagging.md`. ### Changed - `CheckPdfUA` follows a Form XObject drawn by a `Do` outside marked content: it reports the form only when it paints something unmarked, and checks the form's own marked content against `MCR` entries with `/Stm` in the structure tree and its `/StructParents`. Before, any such `Do` was 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 for `Ruby` (ISO 14289-2 8.2.5.23), `Warichu` (8.2.5.24), `Caption` position (8.2.5.27), MathML placement and associated files (8.2.5.29), and the parent/child structure type table (Table 5), which keeps `Em` and `Strong` inline. The MathML namespace is accepted. Part 1 is unchanged. See `docs/pdfua.md` for 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 `PdfDisplayList` stores 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 of `docs/redaction.md` shrinks 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 `null` for an embedded font program that did not exist (and keep unreadable streams without saying so). `CheckPdfA`, `CheckPdfUA`, `PreparePdfA`, `PreparePdfUA` and 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 throws `PdfAException`/`PdfUAException` with the finding and writes nothing, and an unreadable XMP packet is no longer replaced by one built from Info. Saves without `PdfA`/`PdfUA` are byte-identical to before. See `docs/pdfa.md`. - A stream's dictionary now reports the object identity of its stream: `PdfStream.Dictionary.Reference` and `SourceOffset` equal 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 `.Dictionary` stored 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 on `Reference` missed 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.PageIndex` and `Page` were -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. New `PdfDestination.IsStructureDestination` and `StructureElement`; `Page` is now the page dictionary for these destinations. A GoTo action's `/SD` is 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`/`PdfUA` are unchanged. See `docs/pdfa.md`. ### Added - SVG pictures in layout, as vector PDF content. `SvgImage` (`Parse` from bytes or text, `Width`, `Height`, `Title`, `Description`, `Draw` into any `PdfContentBuilder`), `SvgDrawOptions`, `SvgLimits`, the `SvgPicture` element, the fluent `Slot.Svg(...)` with `SvgDescriptor` (fit modes, DPI, alignment, `AltText`, `Artifact`) and `CanvasSurface.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`/nested `svg`, markers, `viewBox` and `preserveAspectRatio`, 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 a `Figure` whose 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 a `SvgLimits` setting that raises `PdfLimitException`. There is an `svg` fuzz target. See `docs/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)` and `ClearSoftMask()` apply and remove a soft mask made from a form, and `PdfSoftMaskKind` chooses 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, or `Part2`), `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 `/NS` on every element (`Note` becomes `FENote`), fills `/Ref` on table-of-contents items from the link inside them, and identifies the file with `pdfuaid:part` 2 and `pdfuaid:rev` 2024. It refuses what a person has to supply or what OnePDF cannot choose, with the clause of ISO 14289-2 for each finding: the unnumbered `H`, a list with labels and no `ListNumbering`, 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) throws `PdfAException`. `PdfDocumentWriter.SetBookmarkElement` and `SetDestinationElement` write 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 gains `StructureGroup` and `Slot.Aside()`, `Footnote()`, `Title()` and `Group(type)` for the 2.0 structure types. Checked with veraPDF's `ua2` flavour as a black box. See `docs/pdfua.md`. - Colour glyphs in layout: OpenType `COLR` version 0 glyphs with `CPAL` palettes are drawn as their layers in palette colours, the foreground entry in the text colour. `ColorPalette` (`Default`, `Light`, `Dark`, `FromIndex`) chosen by `TextStyle.Palette`, `Text.Palette`, the fluent `Palette(...)` or `LayoutOptions.ColorPalette`. Each glyph's layers sit in one `/ActualText` span, 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 in `COLR` version 1, are drawn in one colour with one diagnostic per font. See `docs/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` (`InlineBox` for a drawn or picture box, `InlineElement` for an element subtree in a layout `Text`), `InlineAlignment` (baseline with an offset, top, middle, bottom, text top, text bottom), `InnerBaseline`, `GrowsLine`, `KeepWithNeighbours`, `AltText`, `Artifact` and `Link`; the fluent `TextDescriptor.Inline`, `InlineImage` and `InlineCanvas` with `InlineDescriptor`. 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 a `Figure` inside the paragraph with `/Alt` (or an artifact), the text extractor returns its description through `/ActualText` and 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. See `docs/layout.md` and `docs/layout-text.md`. - Per-page values in layout: `Accumulate` adds a `decimal` to a named account and `Marker` records a text under a key, both wrapping content (`Slot.Accumulate`, `Slot.Marker`); `DynamicText` (`Slot.DynamicText`, styled by `DynamicTextDescriptor`) is a line of text computed for each page by a callback from `PageValues`: `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. See `docs/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, and `KnuthPlass`), `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`/`GreedyLineBreaks` and `DocumentComposer.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. See `docs/layout-text.md`. - `RecursiveAI.OnePdf.Hyphenation`, a new package: `PatternHyphenator` (Liang's pattern algorithm with exception lists, minimum word length and left/right minimums) and `Hyphenators` (`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 with `TextDescriptor.Hyphenator(Hyphenators.Auto())`; layout itself does not depend on it and hyphenation stays opt-in. Pattern data is generated by `tools/hyphenation/generate.py` from 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. See `docs/layout-text.md`. - Gradient backgrounds in layout: `LayoutGradient` (`Linear` at any angle, `Horizontal`, `Vertical`, `Circle` with a centre and `RadialExtent`) and `LayoutGradientStop`, taken by `Slot.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. See `docs/layout.md`. - Multi-column flow in layout: `MultiColumn` (element) and `Slot.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`), `Spacing` between items, and `ColumnBreak`. 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. See `docs/layout.md`. - Content builder: `PdfGradient` (`Axial`, `Radial`), `PdfGradientStop`, `PdfGradientColor` (DeviceGray, DeviceRGB, DeviceCMYK), `PdfContentBuilder.PaintGradient` and `FillRectangle`. Written as axial and radial shadings with exponential and stitching functions. See `docs/content-creation.md`. - Generated table of contents in layout: `TableOfContents` (levels, leader, indent, gap, spacing, per-level `TocLevelStyle`), `TocEntry` for explicit entries, `Heading.InToc` and `Heading.TocTitle`, `LayoutOptions.TableOfContents`, and the fluent `Slot.TableOfContents`, `Slot.TocEntry`, `Slot.Heading(level, inToc, tocTitle)` and `TocDescriptor`. 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 tagged `TOC`/`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. See `docs/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 every `PdfDocumentWriter`) 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.ToOwned` returns 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 (`AddFont` with 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 a `FontProgramException`. - 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 `OBJR` entries 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 (a `Form` element, 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 `q` and `Q` inside 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.PreparePdfUA` and `CheckPdfUA`, `PdfUAException` and `PdfUAViolation` (Matterhorn checkpoint and ISO clause per finding). A save repairs `/MarkInfo`, `/Lang` (from `PdfUAOptions.Language`), `DisplayDocTitle`, `/Tabs /S`, link `/Contents` and 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. See `docs/pdfua.md`. - PDF/A level A: `PdfAConformance.A` for parts 2 and 3 (PDF/A-2a, 3a), alone or combined with PDF/UA-1 (the `pdfuaid` schema is declared as an XMP extension schema); `PdfAOptions.Language`. `XmpMetadata.PdfUAPart`. - Layout: `LayoutDocument.Author`, `Subject` and `Keywords` (and the same on the fluent `DocumentComposer`); `LayoutDocument.Save(stream, PdfSaveOptions)` takes `PdfUA` and level A. ## [1.7.0] - 2026-10-24 ### Added - `PdfFontSource`: parses a font once and shares it between documents (`PdfDocument.AddFont` and `PdfDocumentWriter.AddFont` overloads), so producing many documents with the same face does not parse and validate it each time. - `PdfDocumentWriter` packs 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; `tRNS` becomes a colour-key mask where it can, and a matching `iCCP` profile becomes an `ICCBased` colour space. A `png` fuzz target covers it. See `docs/content-creation.md`. - `PdfContentBuilder.DrawGlyphRun` and `PdfShapedGlyph`: 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 `/ToUnicode` map, or through `/ActualText` where 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 and `Save`/`Restore` must 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 brings `PdfStreamPageBuilder`, 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, `OBJR` entries 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)` and `EndStructureElement(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 on `RecursiveAI.OnePdf`, `RecursiveAI.OnePdf.Fonts` and `RecursiveAI.OneFont` 1.1.0: a fluent layout layer. `LayoutDocument.Compose` takes 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; `IComponent` for reuse; `Save` streams page by page, `Save(stream, PdfSaveOptions)` adds PDF/A and encryption, `ToPdfDocument` hands the result to the core API. The element objects stay public as the lower layer. See `docs/layout.md` and `docs/migrating-from-questpdf.md`. - `layout` fuzz 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.MeasuredRows` is `int?` (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.Compact` makes a row as wide as its items. `Rotate` and `Scale` throw `PdfLayoutException` when 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.Redact` removes 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. `ApplyRedactAnnotations` applies the `/Redact` annotations in a file. A page that cannot be read completely fails the call with `PdfRedactionException` and leaves the document unchanged. A `redact` fuzz target covers it. See `docs/redaction.md`. - `PdfSoftMaskInfo.TransferTable`: the soft mask's `/TR` transfer function (ISO 32000-2 11.6.5.2) evaluated by OnePDF into 256 bytes (entry `i` is the output for `i / 255`, rounded half up and clamped), null for no `/TR` or `/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-softmask` gains `trtable=`. ### Changed - Documented that viewers disagree on whether `/NeedAppearances` regenerates 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 `/Contents` string of a signature dictionary (`/Type /Sig` or `/DocTimeStamp`, or any dictionary with `/ByteRange`) is no longer decrypted on read (ISO 32000-2 7.6.2), so the raw bytes, `PdfSignature.Contents` and 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 `/NeedAppearances` once every widget has an appearance, and embeds Liberation faces (from `RecursiveAI.OnePdf.Fonts`) for standard fonts OnePDF itself added. What is still wrong is refused with `PdfAException`, whose `Violations` list 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.CheckPdfA` reports without changing anything, `PreparePdfA` makes the repairs, and `PdfDocument.AttachFile` adds attachments for part 3. Output is deterministic with `PdfSaveOptions.DocumentId`. A `pdfa` fuzz target covers it. See `docs/pdfa.md`. - `XmpMetadata.PdfAPart` and `PdfAConformance` read the PDF/A identification of a packet. ### Fixed - Importing pages from a document whose fonts were added with `AddFont` copied 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 `/DocMDP` certification level and the field lock; `PdfFormField.GetSignature()` returns one field's signature. `PdfSignature.Verify` and `PdfDocument.VerifySignatures` check, with the platform's hashes, RSA (PKCS#1 v1.5 and PSS) and ECDSA and its X.509 chain builder (no `System.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.RFC3161` document time-stamps, and the legacy `adbe.pkcs7.sha1` (flagged weak); `adbe.x509.rsa_sha1` is 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 `RevocationCheck` callback 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; `IsValid` is a convenience. See `docs/signatures.md`. - A `signature` fuzz target (ASN.1/CMS parsing, certificates, chain building, byte ranges and change analysis; also time-stamp responses). - Signing. `PdfDocument.Sign` writes an incremental update holding an approval or certification signature (`PdfCertificationLevel` 1 to 3, an optional field `/Lock`), and `PdfDocument.AddDocumentTimestamp` adds an `ETSI.RFC3161` document time-stamp; both need a document opened with `AllowEditing`, keep the original bytes as an exact prefix and keep encryption (the `/Contents` string is never encrypted, ISO 32000-2 7.6.2). - Formats: PAdES baseline B-B (`ETSI.CAdES.detached` with `contentType`, `messageDigest` and `signingCertificateV2`, no `signingTime`, and the ESIC extensions entry for pre-2.0 files), B-T when `PdfSigningOptions.Timestamp` is set, or `adbe.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) or `FromExternalSigner`, 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 `PdfTimestampCallback` that 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 `PdfSigningException` with a `PdfSigningError` reason and write nothing. Our own DER writer builds the CMS; no `System.Security.Cryptography.Pkcs`. - `PdfSigningResult` reports the reserved and used `/Contents` size. See `docs/signatures.md`. ### 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 `/Root` at a different catalog is a disallowed change. - The certification level is read from the signed revision, not from the newest dictionary. - An indirect `/ByteRange` or `/Contents` is refused. - A signer certificate whose key usage lacks `digitalSignature`/`nonRepudiation`, or whose extended key usage is only for other purposes, now fails `IsValid` (`PdfSignatureVerification.KeyUsage`; `PdfSignatureVerifyOptions.RequireSigningKeyUsage = false` allows it). A time-stamp signed by a certificate without the time-stamping purpose is invalid. - 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 `PdfSigningException` instead of a platform exception. ## [1.3.0] - 2026-10-23 ### Added - Form filling. `PdfFormField.SetValue`, `SetValues`, `SetChecked` and `Reset`, and `PdfAcroForm.SetValue`, `SetValues`, `SetChecked`, `ResetAll` and `GenerateMissingAppearances`, for documents opened with `AllowEditing`: text (with `/MaxLen`), combo and list boxes (export or display values, multi-select), check boxes and radio groups. `/V` and `/AS` are updated, generated normal appearances are stored in `/AP`, and `/NeedAppearances` is cleared once every widget has one. Refused values throw `PdfFormValueException` (an `ArgumentException`) with a `PdfFormValueError` reason; `PdfFillOptions` controls read-only fields, truncation and appearance generation. Scripts never run. See `docs/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 a `PdfFlattenResult`. - The `appearance` fuzz target also fills, flattens, saves and reopens. - Page operations on editable documents: `RemovePage`/`RemovePages`, `MovePage`, `ReorderPages`, `RotatePage`/`SetPageRotation`, `InsertBlankPage`, `DuplicatePage`, and `PdfDocument.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.ImportPages` copies pages with their whole dependency graph (resources, fonts, images, annotations with appearances; inherited `/Resources`, `/MediaBox`, `/CropBox` and `/Rotate` written 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.Merge` joins documents; `PdfDocument.Split` cuts one into several (`PdfPageRange`, or a number of pages per part). See `docs/page-operations.md`. - Content creation. `PdfPage.BeginContent` returns a `PdfPageContentBuilder` that adds content over or under a page's own (the existing content is wrapped so its graphics state cannot leak in); `PdfDocument.BeginForm` builds a reusable Form XObject. `PdfContentBuilder` writes 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) and `AddFont` for TrueType and OpenType (CFF) files, written as Type 0 fonts with Identity-H, a `/W` array and a `/ToUnicode` map, subset to the glyphs used (own `glyf`/`loca` and CFF subsetters; ISO 32000-2 9.6.4 tags; the `OS/2` `fsType` embedding and no-subsetting bits are honoured, with `PdfFontEmbeddingException` and an explicit override). `AddTextStamp` and `AddImageStamp` watermark chosen pages through one shared form. `PdfSaveOptions.DocumentId` fixes the trailer `/ID` so saves are reproducible. See `docs/content-creation.md`. - A `fontsubset` fuzz target. - `docs/migrating-from-pdfsharp.md`, a guide for moving from PDFsharp. - The `save` fuzz target also runs page operations, merge and split. - `PdfImageDecodeRequest.TargetWidth` and `TargetHeight`: 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`/`TargetHeight` expose 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.TextKnockout` carries the ExtGState `/TK` flag (default true; saved and restored with `q`/`Q`). `IPdfDevice.FillStrokePath` (a default interface method, also virtual on `PdfDevice`) receives `B`, `B*`, `b` and `b*` as one call; its default calls `FillPath` then `StrokePath`, so existing devices behave as before. `PdfDisplayList` records it as one item and replays it as `FillStrokePath`. The display-list text format is version 5 (`fillstroke` record, `tk=0` in `gs`). - Writing. `PdfOpenOptions.AllowEditing` opens 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. See `docs/writing.md`. - `PdfDocument.Save` writes 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.SaveIncremental` appends an update that leaves the original bytes untouched. Options are in `PdfSaveOptions` and `PdfEncryptionOptions`. - A `save` fuzz target (open, change, save, reopen). ### Fixed - `PdfImage` encoded data of an encrypted document (`GetEncodedData`) was the ciphertext; it is now the stored bytes with the encryption removed. - A `startxref` that 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 `/MK` and `/BS` borders and rotation) and the Square, Circle, Line, Polygon, PolyLine, Ink, Highlight, Underline, StrikeOut, Squiggly, FreeText and Text annotations. See `docs/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.MaxAppearanceText` and `PdfLimits.MaxAppearanceItems` bound 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.Strict` for errors. - Filters: Flate, LZW, ASCIIHex, ASCII85, RunLength, PNG and TIFF predictors, CCITT fax and Crypt; DCT, JPX and JBIG2 through the `IPdfImageDecoder` registry. - 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)` drives `IPdfDevice` with paths, glyph runs, images, shadings, patterns, transparency groups, soft masks, optional content and annotation appearances; `PdfDisplayList` records and replays calls, with a versioned text format. - Limits on by default (`PdfLimits`) and fuzzed parsers, filters and font readers; no `unsafe` code. 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 with `FontsPackage.Register()`. [1.11.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.11.0 [1.10.2]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.10.2 [1.10.1]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.10.1 [1.10.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.10.0 [1.9.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.9.0 [1.8.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.8.0 [1.7.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.7.0 [1.6.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.6.0 [1.5.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.5.0 [1.4.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.4.0 [1.3.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.3.0 [1.2.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.2.0 [1.1.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.1.0 [1.0.1]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.0.1 [1.0.0]: https://github.com/recursive-ai-oss/onepdf-csharp/releases/tag/v1.0.0 <!-- https://recursiveai.org/onepdf/cookbook/add-image-watermark/ --> # Add an image or a watermark to a PDF Draw a PNG or JPEG onto a page of an existing PDF in C#, stamp a translucent “DRAFT” across every page, and put a faint logo behind the content. ```csharp using OnePdf; using PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true }); // An image on one page. PNG is read and stored losslessly; JPEG is embedded as it is (AddJpegImage). PdfCreatedImage logo = doc.AddPngImage(File.ReadAllBytes("logo.png")); PdfCreatedImage photo = doc.AddJpegImage(File.ReadAllBytes("photo.jpg")); PdfPage first = doc.Pages[0]; using (PdfPageContentBuilder b = first.BeginContent()) // over the existing content { b.DrawImage(logo, 522, 738, 36, 36); // x, y of the lower left corner, width, height (points) b.DrawImage(photo, 54, 100, 240, 150); b.Commit(); // nothing is written before Commit } // A text watermark on every page, upright even on rotated pages. doc.AddTextStamp("DRAFT", doc.AddStandardFont("Helvetica-Bold"), 120, new PdfStampOptions { Opacity = 0.15, RotationDegrees = 45, Color = (0.6, 0.1, 0.1) }); // An image watermark under the content of pages 2 and 3, centered on each page. doc.AddImageStamp(logo, 200, 200, new PdfStampOptions { Opacity = 0.1, Placement = PdfContentPlacement.Underlay }, pageIndexes: [1, 2]); doc.Save("report-stamped.pdf"); ``` The first page gets the logo in its top right corner and the photo near the bottom. Every page gets “DRAFT” diagonally at 15% opacity, and pages 2 and 3 get the logo, large and faint, under their text. ## Images - `AddJpegImage` embeds the JPEG file as it is, without decoding or recompressing it. Baseline and progressive JPEGs with 1, 3 or 4 components work; arithmetic-coded and lossless JPEGs are refused. - `AddPngImage` reads a PNG and stores it losslessly. An alpha channel becomes a soft mask, so transparency is kept. - Other formats go in as raw samples through `AddRawImage`, or convert them to PNG first. - An image added once can be drawn on any number of pages and is stored once. ## Placing content - Coordinates are in points from the lower left corner of the page, y going up. A US Letter page is 612 × 792 points. - `page.BeginContent()` draws over the existing content. Pass `PdfContentPlacement.Underlay` to draw under it. Either way the page’s own content is left byte for byte as it was. - The builder also draws text (`SetFont`, `DrawText`, `DrawTextBox`), lines, rectangles and gradients. Text drawn this way isn’t shaped: for Arabic, Indic and other complex scripts use OnePdf.Layout (see [Arabic and Hindi text](https://recursiveai.org/onepdf/cookbook/arabic-hindi-text/)). - Content you draw isn’t rotated with the page, so on a page with `/Rotate` it appears turned. The stamp helpers compensate, so stamps read upright. ## Stamps `AddTextStamp` and `AddImageStamp` draw into one shared form and place it on each chosen page (all pages by default), centered on the visible area unless you set `CenterX` and `CenterY`. `PdfStampOptions` sets the opacity, rotation, color, blend mode and placement over or under the content. A standard font such as Helvetica isn’t embedded. For PDF/A, or to choose your own typeface, embed a TrueType or OpenType font with `doc.AddFont(bytes)`. All drawing calls are in [Content creation](https://recursiveai.org/onepdf/docs/content-creation/). For images, logos and watermarks on existing pages, see [Add an image to a PDF in C#](https://recursiveai.org/onepdf/articles/add-image-to-pdf-csharp/). <!-- https://recursiveai.org/onepdf/cookbook/arabic-hindi-text/ --> # Arabic and Hindi text in a PDF Write Arabic (right to left, letters joined) and Hindi (conjuncts and vowel signs in place) into a PDF in C# with OnePdf.Layout, and read it back. ```sh dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0 ``` The fonts are Noto Sans Arabic and Noto Sans Devanagari, free under the SIL Open Font License from Google Fonts. Any TrueType or OpenType font that covers the script works. ```csharp using OnePdf; using OnePdf.Layout; LayoutDocument.Compose(doc => { doc.Title("Greetings").Language("en"); doc.Fonts.Add(File.ReadAllBytes("NotoSansArabic-Regular.ttf"), "Noto Sans Arabic"); doc.Fonts.Add(File.ReadAllBytes("NotoSansDevanagari-Regular.ttf"), "Noto Sans Devanagari"); doc.Fonts.FallbackFamilies.Add("Noto Sans Arabic"); // used for characters the main font lacks doc.Fonts.FallbackFamilies.Add("Noto Sans Devanagari"); doc.Page(page => { page.Margin(54); page.Content().Column(col => { col.Spacing(12); col.Item().Text("Arabic, right to left:").FontSize(10); col.Item().Language("ar").RightToLeft() .Text("مرحبا بالعالم. هذا نص عربي في ملف PDF.").FontSize(20); col.Item().Text("Hindi:").FontSize(10); col.Item().Language("hi").Text("नमस्ते दुनिया। यह पीडीएफ़ में हिंदी पाठ है।").FontSize(20); }); }); }).Save("greetings.pdf"); // Extraction returns the text in logical order, as it was written. using PdfDocument pdf = PdfDocument.Open("greetings.pdf"); Console.WriteLine(pdf.Pages[0].ExtractText().Text); ``` ```text Arabic, right to left: مرحبا بالعالم. هذا نص عربي في ملف PDF. Hindi: नमस्ते दुनिया। यह पीडीएफ़ में हिंदी पाठ है। ``` On the page the Arabic line runs from the right margin, its letters in their joined forms, and the Latin “PDF” inside it keeps left-to-right order. In the Hindi line the vowel sign ि of “दुनिया” and “हिंदी” is drawn before its consonant, and “स्त” in “नमस्ते” is one conjunct, as the script requires. ## How it works - OnePdf.Layout shapes text with the OneFont engine: Arabic joining, Indic reordering and conjuncts, kerning and ligatures. The fonts are embedded as subsets. - Bidirectional text follows the Unicode bidirectional algorithm. `RightToLeft()` sets the base direction of an item, and `doc.RightToLeft()` mirrors a whole document: rows, table columns, alignment and list markers. - Fallback families are tried, cluster by cluster, for anything the main font lacks. Here the default Liberation Sans has neither script, so the Arabic and Devanagari text comes from the fallbacks. - `Language` marks the language of the text in the tagged output, for screen readers, and tells `Hyphenators.Auto()` which patterns to use where there are any. - Extraction returns logical order, because each run of glyphs is written with the text it stands for. ## Good to know - Use OnePdf.Layout for these scripts. Text drawn with the core content builder (`DrawText`) isn’t shaped, so Arabic comes out unjoined and Hindi with its vowel signs out of place. - A character no registered font has is drawn as the font’s missing-glyph box and reported in the layout’s warnings. - Hyphenation patterns (`RecursiveAI.OnePdf.Hyphenation`) cover 26 language variants, none of them Arabic or Hindi. Fonts, fallback, bidirectional text and shaping are in [Layout text](https://recursiveai.org/onepdf/docs/layout-text/). A longer walk-through on shaping, right-to-left layout and font fallback: [Arabic and Hindi text in a C# PDF](https://recursiveai.org/onepdf/articles/arabic-text-pdf-csharp/). <!-- https://recursiveai.org/onepdf/cookbook/create-pdf-a/ --> # Create PDF/A Write PDF/A-2b and PDF/A-3u files in C# from documents made or edited with OnePDF, attach data to PDF/A-3, and check files from elsewhere first. OnePDF writes PDF/A for documents it made or edited, and for existing files that already meet the requirements. It doesn’t convert arbitrary files: it embeds no fonts into content it didn’t create and rewrites no colors or transparency. A file that breaks a requirement is refused, with every violation listed, and nothing is written. ```sh dotnet add package RecursiveAI.OnePdf --version 1.11.0 dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0 ``` ```csharp using OnePdf; using OnePdf.Layout; // A new document, written straight to PDF/A-2b. LayoutDocument minutes = LayoutDocument.Compose(doc => { doc.Title("Minutes, October meeting").Language("en-US"); doc.Page(page => { page.Margin(54); page.Content().Text("The committee approved the harbor trial report."); }); }); using (FileStream file = File.Create("minutes-pdfa.pdf")) minutes.Save(file, new PdfSaveOptions { PdfA = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.B } }); // A file OnePDF made earlier, as PDF/A-3u with its data attached. using (PdfDocument doc = PdfDocument.Open("invoice.pdf", new PdfOpenOptions { AllowEditing = true })) { doc.AttachFile("invoice.xml", File.ReadAllBytes("invoice.xml"), new PdfAttachmentOptions { MimeType = "text/xml", Relationship = PdfAttachmentRelationship.Data, ModificationDate = File.GetLastWriteTimeUtc("invoice.xml"), // PDF/A-3 requires a date on every attachment }); doc.Save("invoice-pdfa3.pdf", new PdfSaveOptions { PdfA = new PdfAOptions { Part = PdfAPart.Part3, Conformance = PdfAConformance.U } }); } // A file from elsewhere: make the repairs OnePDF can make, then see what is still wrong before saving. using (PdfDocument doc = PdfDocument.Open("of345.pdf", new PdfOpenOptions { AllowEditing = true })) { var options = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.B }; IReadOnlyList<PdfAViolation> problems = doc.PreparePdfA(options); if (problems.Count == 0) { doc.Save("of345-pdfa.pdf", new PdfSaveOptions { PdfA = options }); } else { Console.WriteLine($"of345.pdf cannot be PDF/A-2b as it is: {problems.Count} violations, for example:"); foreach (PdfAViolation v in problems.Take(4)) Console.WriteLine($" {v.Clause}: {v.Message} (object {v.ObjectNumber})"); } } ``` ```text of345.pdf cannot be PDF/A-2b as it is: 6 violations, for example: 6.2.11.4.1: The font ZapfDingbats is not embedded. (object 20) 6.2.11.4.1: The font Times-Roman is not embedded. (object 191) 6.3.3: A button widget's /N must be a dictionary of appearance states. (object 188) 6.4.1: A widget annotation has an /A or /AA entry. (object 188) ``` The veraPDF validator passes `minutes-pdfa.pdf` as PDF/A-2b and `invoice-pdfa3.pdf` as PDF/A-3u. ## What a PDF/A save does A save with `PdfSaveOptions.PdfA` makes the repairs that need no judgement and then checks the result: - adds an sRGB output intent with a color profile generated by OnePDF, - writes the XMP metadata, taken from the document information so the two agree, - embeds Liberation faces for standard fonts that OnePDF itself added (Helvetica, Times, Courier), when the `RecursiveAI.OnePdf.Fonts` package is registered, - sets the print flag on annotations and generates missing form-field appearances, - writes the header and file identifiers the standard asks for. Parts 2 and 3 are supported, at levels B (the pages look the same everywhere), U (B, plus Unicode for all text) and A (U, plus tagged structure: see [Create PDF/UA](https://recursiveai.org/onepdf/cookbook/create-pdf-ua/)). OnePDF writes attachments only in PDF/A-3 files, and each needs a modification date: the sample sets one from the file. ## Good to know - `PreparePdfA` makes the repairs in memory and returns what is still wrong. `CheckPdfA` only reports, and works on read-only documents. A save that fails throws `PdfAException` with the same list in `Violations`. - OnePDF is not a validator. A clean check covers what OnePDF can verify from the file’s structure, not every rule of the standard. Run a validator such as veraPDF before you rely on a file. - DeviceCMYK color is refused, because OnePDF’s output intent is RGB. - An encrypted document needs `Encryption = PdfEncryptionAction.Remove` in the same save, since PDF/A forbids encryption. - Existing signatures don’t survive a PDF/A save, which rewrites the file. Sign afterwards. - A part of the file that can’t be read, such as a damaged font program, makes the save fail rather than leave the part out. What is written, checked and not checked, clause by clause, is in [PDF/A](https://recursiveai.org/onepdf/docs/pdfa/). A longer walk-through with PDF/A-3 XML attachments: [Create PDF/A in C#](https://recursiveai.org/onepdf/articles/create-pdf-a-csharp/). <!-- https://recursiveai.org/onepdf/cookbook/create-pdf-ua/ --> # Create PDF/UA Generate a tagged PDF in C# that is PDF/UA-1 and PDF/A-2a in one file, and see what OnePDF refuses when a title or alternate text is missing. ```sh dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0 ``` ```csharp using OnePdf; using OnePdf.Layout; byte[] logo = File.ReadAllBytes("logo.png"); LayoutDocument summary = LayoutDocument.Compose(doc => { doc.Title("Harbor trial summary").Author("Port office").Language("en-US"); doc.Page(page => { page.Size(PageSize.Letter); page.Margin(54); page.Header().Text("Port office").FontSize(9); // headers and footers are tagged as artifacts page.Content().Column(col => { col.Spacing(10); col.Item().Width(48).Image(logo).AltText("Port office logo"); col.Item().Heading(1).Text("Harbor trial summary").FontSize(22).Bold(); col.Item().Text("Six crews took part. Catches by quay:"); col.Item().Table(table => { table.Columns(c => { c.Relative(); c.Constant(100); }); table.CellPadding(4); table.CellBorder(0.5, Colors.Grey); table.HeaderRow(r => { r.Cell().Text("Quay").Bold(); r.Cell().AlignEnd().Text("Catch (kg)").Bold(); }); table.Row(r => { r.Cell().Text("North"); r.Cell().AlignEnd().Text("1,204"); }); table.Row(r => { r.Cell().Text("South"); r.Cell().AlignEnd().Text("987"); }); }); col.Item().Heading(2).Text("Next steps").FontSize(14).Bold(); col.Item().List(list => { list.Bulleted(); list.Item().Text("Repeat the trial in spring."); list.Item().Hyperlink("https://example.com/harbor").Text("Read the full report online."); }); }); }); }); // PDF/UA-1 and PDF/A-2a in one file. using (FileStream file = File.Create("summary-ua.pdf")) { summary.Save(file, new PdfSaveOptions { PdfUA = new PdfUAOptions(), PdfA = new PdfAOptions { Part = PdfAPart.Part2, Conformance = PdfAConformance.A }, }); } // What OnePDF cannot decide for you is refused, with every reason listed. LayoutDocument untitled = LayoutDocument.Compose(doc => { doc.Language("en-US"); // no title doc.Page(page => page.Content().Column(col => { col.Item().Width(48).Image(logo); // no alternate text col.Item().Heading(2).Text("Starts at level 2"); // heading levels must start at 1 })); }); try { untitled.Save(new MemoryStream(), new PdfSaveOptions { PdfUA = new PdfUAOptions() }); } catch (PdfUAException e) { foreach (PdfUAViolation v in e.Violations) Console.WriteLine($"checkpoint {v.Checkpoint}: {v.Message}"); } ``` ```text checkpoint 06: The XMP packet has no dc:title: a PDF/UA file needs a title. checkpoint 07: The catalog's /ViewerPreferences does not set /DisplayDocTitle true, so viewers would show the file name instead of the title. checkpoint 13: A Figure structure element has neither alternate text (/Alt) nor replacement text (/ActualText). checkpoint 14: The first heading is H2; the heading hierarchy has to start at H1. ``` `summary-ua.pdf` passes the veraPDF validator as PDF/UA-1 and as PDF/A-2a. The second document was refused and nothing was written. ## How it works OnePdf.Layout tags everything it lays out: headings become `H1` to `H6`, tables `Table` with `TH` header cells, lists `L` with `LI` items, images `Figure` with your alternate text, links `Link`, and headers, footers, borders and backgrounds are marked as artifacts. A save with `PdfSaveOptions.PdfUA` then: - **repairs the clerical parts:** the document language (from `PdfUAOptions.Language` when the document has none), showing the title instead of the file name, tab order, link descriptions and the XMP identification; - **refuses what only you can provide**, listing every violation with its Matterhorn Protocol checkpoint: a title, alternate text for each image (or `Artifact()` for decoration), headings that start at level 1 without skipping levels, header cells in every table. `PdfUAOptions { Part = PdfUAPart.Part2 }` writes PDF/UA-2 on PDF 2.0 instead. PDF/UA-2 can’t be combined with PDF/A-2 or PDF/A-3, which are PDF 1.7 formats. ## Good to know - OnePDF never guesses alternate text, a title, a language or the role of content. Existing untagged files are refused. You can tag one with roles you supply through the tagging API: see [Tagging](https://recursiveai.org/onepdf/docs/tagging/). - A clean save is not proof of conformance. PDF/UA also asks for things only a person can judge: a sensible reading order, meaningful alternate text, enough contrast. Check with a validator and a screen reader. - `CheckPdfUA()` reports the violations of any document, including read-only ones, without changing it. - The PDF/UA save checks the finished file, so the whole document is held in memory while it is written. Plain `Save(path)` streams page by page. The full lists of repairs and checks are in [PDF/UA](https://recursiveai.org/onepdf/docs/pdfua/), and the tagging rules of the layout package in [Layout](https://recursiveai.org/onepdf/docs/layout/). A longer walk-through with PDF/UA-2 and tagging an existing file: [PDF/UA in C#](https://recursiveai.org/onepdf/articles/pdf-ua-csharp/). <!-- https://recursiveai.org/onepdf/cookbook/decrypt-pdf/ --> # Decrypt or encrypt a PDF Remove the password from a PDF you are allowed to open, in C#, or encrypt one with AES-256, a password and permission flags, with OnePDF. ```csharp using OnePdf; // Encrypt: AES-256 with a password to open the file and an owner password. using (PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true })) { doc.Save("locked.pdf", new PdfSaveOptions { Encryption = PdfEncryptionAction.Set, NewEncryption = new PdfEncryptionOptions { UserPassword = "open sesame", OwnerPassword = "owner secret", Permissions = PdfPermissions.Print | PdfPermissions.PrintHighQuality, // printing only }, }); } // Decrypt: open with either password, then save without encryption. try { using PdfDocument doc = PdfDocument.Open("locked.pdf", new PdfOpenOptions { Password = "open sesame", AllowEditing = true }); Console.WriteLine($"{doc.Encryption?.Cipher}, opened as {doc.AccessLevel}, permissions: {doc.Permissions}"); doc.Save("unlocked.pdf", new PdfSaveOptions { Encryption = PdfEncryptionAction.Remove }); } catch (PdfPasswordException) { Console.WriteLine("wrong or missing password"); } using (PdfDocument plain = PdfDocument.Open("unlocked.pdf")) Console.WriteLine($"unlocked.pdf: encrypted={plain.Encryption is not null}, {plain.PageCount} pages"); // Many files have only an owner password. They open without one, and the same save writes a plain copy. using (PdfDocument doc = PdfDocument.Open("opm1644.pdf", new PdfOpenOptions { AllowEditing = true })) { Console.WriteLine($"opm1644.pdf: {doc.Encryption?.Cipher}, opened as {doc.AccessLevel}"); doc.Save("opm1644-plain.pdf", new PdfSaveOptions { Encryption = PdfEncryptionAction.Remove }); } ``` ```text Aes256, opened as User, permissions: Print, PrintHighQuality unlocked.pdf: encrypted=False, 6 pages opm1644.pdf: Rc4, opened as User ``` `opm1644.pdf` is OPM Form 1644, a US government form encrypted with RC4 and an owner password only. `qpdf` reports both `unlocked.pdf` and `opm1644-plain.pdf` as not encrypted. ## Decrypting - OnePDF reads the standard security handler, revisions 2 to 6: RC4 40 and 128 bits, AES-128 and AES-256. - `PdfOpenOptions.Password` is tried as the user password, then as the owner password. The empty password is always tried, which is why files with only an owner password open without one. - A wrong or missing password throws `PdfPasswordException`. A file encrypted with certificates (the public-key handler) isn’t supported and throws `PdfEncryptionException`. - `PdfEncryptionAction.Remove` in the save writes the document without encryption. ## Encrypting - `PdfEncryptionAction.Set` with `PdfEncryptionOptions` encrypts on save: `UserPassword` to open the file (empty for none), `OwnerPassword`, `Permissions` and `Cipher` (`Aes256`, the default, or `Aes128`). RC4 is never written for a new encryption. - `Encryption = PdfEncryptionAction.Keep`, the default, saves an encrypted document with the same passwords and permissions. - Encrypted output isn’t byte-identical between runs, because of random initialization vectors and salts. ## Permissions OnePDF reports the permission flags in `doc.Permissions` and doesn’t enforce them. Honoring them, for example not removing encryption from a file whose owner restricted changes, is up to your application. The details are in [Writing](https://recursiveai.org/onepdf/docs/writing/) and [Reading and repair rules](https://recursiveai.org/onepdf/docs/repair/). <!-- https://recursiveai.org/onepdf/cookbook/extract-text/ --> # Extract text from a PDF Read the text of every page of a PDF in C# in reading order, leave out running headers and footers, and keep the page layout when you need it, with OnePDF. ```sh dotnet add package RecursiveAI.OnePdf --version 1.11.0 ``` ```csharp using OnePdf; using OnePdf.Text; using PdfDocument doc = PdfDocument.Open("report.pdf"); // Whole document: pages are read in parallel, and running headers and footers are found by their repetition. var options = new PdfTextOptions { Mode = PdfTextMode.ReadingOrder, // the default; also ContentOrder and Layout ExcludeHeadersAndFooters = true, // leave repeated headers and footers out of the text }; IReadOnlyList<PdfPageText> pages = doc.ExtractText(options); using (StreamWriter output = File.CreateText("report.txt")) { for (int i = 0; i < pages.Count; i++) { output.WriteLine($"--- page {i + 1}"); output.WriteLine(pages[i].Text); } } // One page, keeping the arrangement on the page, like pdftotext -layout. PdfPageText page = doc.Pages[0].ExtractText(new PdfTextOptions { Mode = PdfTextMode.Layout }); Console.WriteLine(page.Text); ``` `report.txt` gets each page’s text, columns read one after the other. The console gets the first page in layout mode: ```text Week 1 Prepared by Jane Doe, account 4471-2290. The harbor trial ran for six weeks. Each crew logged its ... Left column: crews on the north quay. Left column: Right column: crews on the south quay. Right column: crews on the north quay. Left column: crews on the crews on the south quay. Right column: crews on the ``` ## The three modes - **`ReadingOrder`** (the default) orders blocks by the structure tree when the file is tagged and the tree covers the page, and by the page’s geometry otherwise. Blocks are separated by a blank line and lines by a line feed. - **`ContentOrder`** keeps the order in which the content stream draws the glyphs. - **`Layout`** puts words on a monospace grid that keeps the page’s arrangement, like `pdftotext -layout`. `pageText.GetText(PdfTextMode.ContentOrder)` gives a page’s text in another mode, worked out from the same page while the document is still open. ## Headers and footers Whole-document extraction flags a block as a header or footer when it sits in the top or bottom 12% of the page and the same text appears there on at least half of the pages. Digits are ignored when comparing, so “Page 3 of 6” counts, and so does a “Week 3” title that opens every page: in the run above, `ExcludeHeadersAndFooters` dropped the “Week” titles along with the running header and the page numbers. Check the result before you exclude. Each block’s `Role` tells you what was flagged. Single-page `page.ExtractText()` doesn’t flag anything, because it has only one page to compare. ## Good to know - Ligatures are split into letters, and Arabic and Hebrew presentation forms are folded to their base characters. Text marked with `/ActualText` replaces what it covers. - Invisible text, such as the text layer an OCR tool adds to a scan, is included and flagged: `word.IsVisible` is false. - `JoinHyphenatedWords = true` joins a word split by a hyphen at the end of a line. - Long documents stay small in memory: whole-document extraction doesn’t keep each page’s glyphs unless you set `RetainGlyphs`. - OnePDF does no OCR. A scanned page without a text layer gives no text. For word boxes, see [Get words and their positions](https://recursiveai.org/onepdf/cookbook/word-positions/). Every threshold of the layout analysis is in [Text layout and reading order](https://recursiveai.org/onepdf/docs/text-layout/), and the versioned JSON output is in [Extraction JSON](https://recursiveai.org/onepdf/docs/extraction-json/). For a longer walk-through with an ASP.NET Core upload endpoint and a comparison with PdfPig and iText, see [Extract text from PDF in C#](https://recursiveai.org/onepdf/articles/extract-text-from-pdf-csharp/). To pull out the embedded pictures as well: [Extract images from a PDF in C#](https://recursiveai.org/onepdf/articles/extract-images-from-pdf-csharp/). <!-- https://recursiveai.org/onepdf/cookbook/fill-flatten-forms/ --> # Fill and flatten form fields List a PDF form’s fields, fill text fields and check boxes in C#, save the filled form, then flatten it so the values become part of the page, with OnePDF. ```csharp using OnePdf; using (PdfDocument doc = PdfDocument.Open("of345.pdf", new PdfOpenOptions { AllowEditing = true })) { PdfAcroForm form = doc.Form ?? throw new InvalidOperationException("The file has no form."); Console.WriteLine($"{form.TerminalFields.Count()} fields, XFA: {form.HasXfa}"); // Field names, kinds and allowed values. foreach (PdfFormField field in form.TerminalFields.Take(8)) { string options = field.Options.Count > 0 ? $" options: {string.Join(", ", field.Options.Select(o => o.Export))}" : ""; Console.WriteLine($" {field.FullName} ({field.Kind}) = {field.Value}{options}"); } try { form.SetValue("name", "Ada Lovelace"); form.SetValue("dob", "12/10/1985"); form.SetChecked("xno1", true); form.SetValue("missing", "x"); // no such field } catch (PdfFormValueException e) { Console.WriteLine($"refused: {e.Reason}"); // nothing was changed by the refused call } doc.Save("filled.pdf"); // still a fillable form } // Bake the values into the page and drop the form. using (PdfDocument doc = PdfDocument.Open("filled.pdf", new PdfOpenOptions { AllowEditing = true })) { PdfFlattenResult result = doc.Flatten(); Console.WriteLine($"form removed: {result.FormRemoved}"); doc.Save("flat.pdf"); } using (PdfDocument flat = PdfDocument.Open("flat.pdf")) Console.WriteLine($"form after flattening: {(flat.Form is null ? "none" : "still there")}; text has the name: {flat.Pages[0].ExtractText().Text.Contains("Ada Lovelace")}"); ``` ```text 53 fields, XFA: False name (Text) = dob (Text) = title (Text) = homeaddr (Text) = agcy (Text) = xyes1 (CheckBox) = xno1 (CheckBox) = xyes2 (CheckBox) = refused: UnknownField form removed: True form after flattening: none; text has the name: True ``` The form is OF 345, a US government form. The values set before the refused call are kept: a call that fails changes nothing, but it doesn’t undo earlier calls. ## Filling - Fields are found by fully qualified name (`parent.child`). `TerminalFields` lists the fields that hold values. - `SetValue` fills text fields, combo boxes and list boxes (by export value or display text) and radio groups (by state name or option text). `SetValues` fills a multi-select list box. `SetChecked` sets a check box. - A value that can’t be stored throws `PdfFormValueException`, whose `Reason` says why: `UnknownField`, `WrongType`, `ReadOnly`, `NotInOptions`, `TooLong`, `NotMultiSelect` or `NotTerminal`. `PdfFillOptions` can override read-only fields or cut text to the field’s maximum length. - OnePDF draws each filled field’s appearance and stores it, so the value shows in every viewer. - Field scripts never run. Calculated fields aren’t recomputed and format scripts aren’t applied: values are stored exactly as given. XFA form data isn’t filled. ## Flattening `Flatten()` draws each field’s appearance into the page and removes the fields; when none are left, the form itself goes (`FormRemoved`). The values are then ordinary page content, so text extraction finds them and nobody can edit them in a viewer. - `PdfFlattenOptions.FieldNames` flattens only the named fields. `IncludeMarkup` also flattens comments and other markup annotations. - Signed signature fields are kept. - Flattening works on encrypted documents, and new objects are encrypted like the rest. Every rule is in [Forms](https://recursiveai.org/onepdf/docs/forms/), and how appearances are drawn is in [Appearance generation](https://recursiveai.org/onepdf/docs/appearance-generation/). Longer walk-throughs on real government forms: [Fill PDF form fields in C#](https://recursiveai.org/onepdf/articles/fill-pdf-form-csharp/) and [Flatten a PDF form in C#](https://recursiveai.org/onepdf/articles/flatten-pdf-csharp/). <!-- https://recursiveai.org/onepdf/cookbook/generate-tables/ --> # Generate a PDF with tables Generate a multi-page PDF table in C# with OnePdf.Layout: column widths, striped rows, header and footer rows on every page, spans and page numbers. ```sh dotnet add package RecursiveAI.OnePdf.Layout --version 1.11.0 ``` ```csharp using System.Globalization; using OnePdf.Layout; var us = CultureInfo.GetCultureInfo("en-US"); var rows = Enumerable.Range(1, 120) .Select(i => (Date: new DateOnly(2026, 1, 1).AddDays(i * 2), Crew: $"Crew {(i % 6) + 1}", Kg: 40m + (i * 37 % 90))) .ToList(); LayoutDocument.Compose(doc => { doc.Title("Catch log, 2026").Language("en-US"); doc.Page(page => { page.Size(PageSize.Letter); page.Margin(54); page.Content().Column(col => { col.Spacing(12); col.Item().Heading(1).Text("Catch log, 2026").FontSize(20).Bold(); col.Item().Table(table => { table.Columns(c => { c.Constant(90); // a fixed width in points c.Relative(2); // shares the rest of the width by weight c.Relative(1); }); table.CellPadding(4); table.CellBorder(0.5, Colors.LightGrey); table.RowBackground(i => i % 2 == 1 ? LayoutColor.FromHex("#F2F2F2") : null); // striped body rows table.HeaderRow(r => // repeated at the top of every page { r.Cell().Text("Date").Bold(); r.Cell().Text("Crew").Bold(); r.Cell().AlignEnd().Text("Catch (kg)").Bold(); }); foreach (var row in rows) { table.Row(r => { r.Cell().Text(row.Date.ToString("MMM d", us)); r.Cell().Text(row.Crew); r.Cell().AlignEnd().Text(row.Kg.ToString("N1", us)); }); } table.FooterRow(r => // repeated at the bottom of every page { r.Cell().Span(2).Text("Total, all crews").Bold(); // one cell over two columns r.Cell().AlignEnd().Text(rows.Sum(x => x.Kg).ToString("N1", us)).Bold(); }); }); }); page.Footer().AlignCenter().Text(t => { t.FontSize(9); t.Span("Page "); t.CurrentPageNumber(); t.Span(" of "); t.TotalPages().ReservedDigits(2); }); }); }).Save("catch-log.pdf"); ``` The 120 rows fill five US Letter pages. Each page starts with the “Date, Crew, Catch (kg)” header row and ends with the total row, above the page number. ## Columns - `Constant(points)` keeps a fixed width. - `Relative(weight)` shares the width left over by weight, and never gets narrower than its longest word. - `Auto()` takes the width of its widest content when there is room. An `Auto` column makes every row count for the width, so prefer `Constant` and `Relative` for long tables. ## Rows and cells - `HeaderRow` and `FooterRow` repeat on every page the table reaches and never split. Body rows split between lines of text; `r.KeepTogether()` moves a row to the next page instead. - `Span(columns, rows)` merges cells. `CellBackground`, `CellBorder`, `CellPadding` and `CellAlign` set one cell. `RowHeader()` makes a cell a row header for screen readers. - Borders are collapsed: where two cells meet they share one line. - A cell holds any content: text, an image, a nested column, another table. ## Good to know - `ReservedDigits` sets the room left for the total page count, which isn’t known until the end. Reserve as many digits as the count can have; a centered footer shows the unused room as a gap. - For a running subtotal per page (“Carried forward”), use `Accumulate` on the amount cells and `DynamicText` in the footer, as shown in [Layout](https://recursiveai.org/onepdf/docs/layout/). - The output is tagged, with `TH` header cells, so the same document can be saved as PDF/UA: see [Create PDF/UA](https://recursiveai.org/onepdf/cookbook/create-pdf-ua/). - `Save(path)` writes page by page, so memory stays small for long tables, and layout time grows in proportion to the number of rows. - Text is set in Liberation Sans unless you register other fonts on `doc.Fonts`. Every table option and the column-width rules are in [Layout](https://recursiveai.org/onepdf/docs/layout/). For a complete multi-page invoice with an ASP.NET Core download, see [Create PDF invoices and reports in C#](https://recursiveai.org/onepdf/articles/create-pdf-csharp/). Coming from MigraDoc tables: [Migrate from PDFsharp and MigraDoc](https://recursiveai.org/onepdf/articles/migrate-from-pdfsharp/). <!-- https://recursiveai.org/onepdf/cookbook/merge-pdfs/ --> # Merge PDFs Merge PDF files in C# with OnePDF, keeping bookmarks, links and form fields, merge PDFs held in memory, or append chosen pages to a file. ```csharp using OnePdf; string[] files = ["chapter-1.pdf", "chapter-2.pdf"]; List<PdfDocument> sources = files.Select(f => PdfDocument.Open(f)).ToList(); try { using PdfDocument merged = PdfDocument.Merge(sources); merged.SetInfo("Title", "Collected chapters"); // Merge copies no metadata merged.Save("merged.pdf"); Console.WriteLine($"{merged.PageCount} pages"); foreach (PdfOutlineItem bookmark in merged.Outline) Console.WriteLine($" bookmark: {bookmark.Title}"); } finally { foreach (PdfDocument source in sources) source.Dispose(); } // PDFs held in memory: open the bytes, merge, and get bytes back. byte[] first = File.ReadAllBytes("chapter-1.pdf"); byte[] second = File.ReadAllBytes("chapter-2.pdf"); using (PdfDocument a = PdfDocument.Open(first)) using (PdfDocument b = PdfDocument.Open(second)) using (PdfDocument both = PdfDocument.Merge([a, b])) { var buffer = new MemoryStream(); both.Save(buffer); Console.WriteLine($"{buffer.Length:N0} bytes in memory"); } // Append only the first page of another file to an existing document. using (PdfDocument target = PdfDocument.Open("chapter-1.pdf", new PdfOpenOptions { AllowEditing = true })) using (PdfDocument other = PdfDocument.Open("chapter-2.pdf")) { target.ImportPages(other, [0]); target.Save("chapter-1-plus.pdf"); Console.WriteLine($"chapter-1-plus.pdf: {target.PageCount} pages"); } ``` ```text 4 pages bookmark: Chapter 1, part 1 bookmark: Chapter 1, part 2 bookmark: Chapter 2, part 1 bookmark: Chapter 2, part 2 32,147 bytes in memory chapter-1-plus.pdf: 3 pages ``` ## What comes along - **Bookmarks** that lead to copied pages are appended after the existing ones. - **Links** between copied pages keep working. A link to a page that was left behind keeps its rectangle and loses its target. - **Form fields** come with the field tree above them. A field name already used in the target gets a `_2`, `_3` suffix, so fields of the same name are renamed, not merged. - **Tagged structure**, page labels and optional content of the copied pages. - Each shared font or image is copied once, however many pages use it. Not carried: the sources' metadata, signatures, document-level JavaScript and permissions. `Merge` writes no title, so set one with `SetInfo`. ## Good to know - `Merge` and `ImportPages` take any open document, including encrypted ones. An encrypted source imports as plain content, so a merged result is unencrypted unless you save it with `PdfSaveOptions.Encryption = PdfEncryptionAction.Set`. - The merged document lives in memory and doesn’t refer to its sources, which you can dispose as soon as `Merge` returns. - `ImportPages(source, indexes, insertAt)` inserts at a position instead of appending. To go the other way, see [Split a PDF](https://recursiveai.org/onepdf/cookbook/split-pdf/). Every rule for copied pages is in [Page operations](https://recursiveai.org/onepdf/docs/page-operations/). A longer walk-through with bookmarks, links, form fields and an ASP.NET Core endpoint: [Merge PDF files in C#](https://recursiveai.org/onepdf/articles/merge-pdf-csharp/). <!-- https://recursiveai.org/onepdf/cookbook/redact/ --> # Redact text in a PDF Find a name on every page of a PDF in C#, remove it from the content, cover the spot with a black box, and clear it from the metadata. OnePDF removes the text from the file. Nothing is just hidden under a box. ```csharp using OnePdf; using OnePdf.Text; string[] phrase = ["Jane", "Doe"]; using PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true }); // Find the phrase on every page and collect the boxes of its words. var regions = new List<PdfRedactionRegion>(); for (int p = 0; p < doc.PageCount; p++) { PdfPageText text = doc.Pages[p].ExtractText(); foreach (PdfTextLine line in text.Blocks.SelectMany(b => b.Lines)) { IReadOnlyList<PdfTextWord> words = line.Words; for (int i = 0; i + phrase.Length <= words.Count; i++) { if (!phrase.Select((s, k) => words[i + k].Text.TrimEnd(',', '.', ';') == s).All(match => match)) continue; PdfRectangle[] boxes = words.Skip(i).Take(phrase.Length).Select(w => w.Bounds).ToArray(); var area = new PdfRectangle(boxes.Min(r => r.Left), boxes.Min(r => r.Bottom), boxes.Max(r => r.Right), boxes.Max(r => r.Top)); regions.Add(new PdfRedactionRegion(p, area)); } } } // Remove what is under the boxes, draw black boxes over them, and clear the name from the metadata. PdfRedactionResult result = doc.Redact(regions, new PdfRedactionOptions { Overlay = new PdfRedactionOverlay(), MetadataTerms = [string.Join(" ", phrase)], }); Console.WriteLine($"{regions.Count} areas, {result.GlyphsRemoved} glyphs removed on {result.PagesProcessed} pages"); doc.Save("redacted.pdf"); // a full save; SaveIncremental is refused after Redact using PdfDocument check = PdfDocument.Open("redacted.pdf"); bool found = check.ExtractText().Any(page => page.Text.Contains("Jane")); Console.WriteLine($"\"Jane\" still in the text: {found}"); ``` ```text 6 areas, 54 glyphs removed on 6 pages "Jane" still in the text: False ``` `pdftotext` agrees: the first line of the page now reads “Prepared by” and then the account number. ## What is removed `Redact` rewrites the page content. Inside each area it removes: - the glyphs whose boxes meet the area, - vector paths and shadings that meet it, - image samples under it (the image is blanked there and stored again), - annotations, and the values of form fields whose widgets meet it, - `/ActualText` and alternate text of the tagged content it removed. `MetadataTerms` clears matching entries from the document information, the XMP metadata and bookmark titles. `RemoveJavaScript`, `RemoveEmbeddedFiles` and `RemoveThumbnails` strip those parts too. ## Good to know - **Save with `Save`.** After `Redact`, `SaveIncremental` is refused, because the old content would stay in the file’s earlier revision. - **It fails closed.** If any page’s content can’t be read completely, `Redact` throws `PdfRedactionException` naming the page and changes nothing. - **Areas are in the page’s own coordinates**, before its rotation. Word boxes from text extraction have the rotation applied, so on a rotated page convert them first. On an unrotated page, as here, they can be used as they are. - **Matching is by position.** Only the places your areas cover are removed. A word drawn in two places needs two areas, and text that exists only as an image, such as a scan without a text layer, is removed only by blanking the image. - **Signatures** become invalid once the redacted document is saved. `result.SignaturesInvalidated` says so. - JPEG and other compressed images are blanked when a decoder is registered in `PdfRedactionOptions.Decoders`. Without one, an image that meets an area is removed whole. The full list of what is and isn’t covered is in [Redaction](https://recursiveai.org/onepdf/docs/redaction/). Finding words is covered in [Get words and their positions](https://recursiveai.org/onepdf/cookbook/word-positions/). For the whole workflow, including image areas, metadata and checking the result, see [Redact a PDF in C#](https://recursiveai.org/onepdf/articles/redact-pdf-csharp/). <!-- https://recursiveai.org/onepdf/cookbook/repair-damaged-pdf/ --> # Repair a damaged PDF Open a damaged or truncated PDF in C#, see what OnePDF repaired, and save a clean copy, or reject damaged files outright with strict mode. The input here is a PDF whose last lines were cut off, and with them `startxref`, the pointer to its cross-reference table. ```csharp using OnePdf; // Lenient by default: what can be repaired is repaired, and each repair is recorded. using (PdfDocument doc = PdfDocument.Open("damaged.pdf")) { Console.WriteLine($"{doc.PageCount} pages, title: {doc.Info.Title}"); doc.Save("repaired.pdf"); // a clean file with a new cross-reference table foreach (PdfDiagnostic d in doc.Diagnostics) // severity, object number, offset, message Console.WriteLine(d); } // Strict: the first problem throws instead. try { using PdfDocument strict = PdfDocument.Open("damaged.pdf", new PdfOpenOptions { Strict = true }); } catch (PdfFormatException e) { Console.WriteLine($"strict: {e.Message}"); } // TryOpen never throws for a file that will not open. if (PdfDocument.TryOpen("not-a-pdf.txt", out PdfDocument? other, out PdfException? error)) other.Dispose(); else Console.WriteLine($"not-a-pdf.txt: {error.Message}"); ``` ```text 6 pages, title: Harbor trial report Warning: The cross-reference data is unusable (There is no startxref near the end of the file.); rebuilt by scanning the file for objects. strict: The cross-reference data is unusable (There is no startxref near the end of the file.); rebuilt by scanning the file for objects. not-a-pdf.txt: The file has no usable cross-reference data, trailer or document catalog. ``` `qpdf --check` finds no errors in `repaired.pdf`. ## The repair chain 1. **Trust the cross-reference data.** Read the table or stream that `startxref` names, then the earlier ones. 2. **Fix single objects by scanning.** An entry that doesn’t lead to its object is found by scanning the file for object headers. A stream whose `/Length` is wrong ends at its `endstream`. 3. **Rebuild.** When the cross-reference data can’t be read at all, the whole file is scanned and rebuilt from the objects it holds, as here. Each repair is a `PdfDiagnostic` on `doc.Diagnostics`, with the severity, the object number and the byte offset when known. Some repairs only happen when an object is first read, so read the list after the work you need, as the sample does after `Save`. ## Good to know - `Save` writes a clean file: new cross-reference data, correct lengths, no unreachable objects. Content that was truly lost stays lost: a reference to a missing object is written as `null`. - Don’t use `SaveIncremental` on a repaired file. A file that was rebuilt by scanning has no cross-reference section to append to, so it throws. - A page that can’t be parsed throws only when you use that page, so one bad page doesn’t stop you reading the others. - Limits on decoded size, nesting, object count and more are on by default, and are never relaxed by repair: a file that exceeds them throws `PdfLimitException`. - `Strict = true` turns every repair into a `PdfFormatException`, for pipelines that should reject damaged input. Every rule is in [Reading and repair rules](https://recursiveai.org/onepdf/docs/repair/). <!-- https://recursiveai.org/onepdf/cookbook/rotate-delete-pages/ --> # Rotate, delete and reorder pages Rotate, delete, move and insert pages of an existing PDF in C# with OnePDF; bookmarks and links to deleted pages are repaired. ```csharp using OnePdf; using PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true }); Console.WriteLine($"before: {doc.PageCount} pages"); doc.RotatePage(0, 90); // clockwise, added to the page's current rotation doc.SetPageRotation(1, 0); // absolute: upright, whatever it was doc.RemovePages([2, 3]); // zero-based; bookmarks and links to them are repaired doc.MovePage(fromIndex: 0, toIndex: 3); // the first page goes to the end doc.InsertBlankPage(0, 612, 792); // a blank US Letter page in front (width, height in points) // Pages taken before an operation are out of date; read doc.Pages again. for (int i = 0; i < doc.PageCount; i++) Console.WriteLine($"page {i + 1}: rotate {doc.Pages[i].Rotate}"); doc.Save("report-edited.pdf"); ``` ```text before: 6 pages page 1: rotate 0 page 2: rotate 0 page 3: rotate 0 page 4: rotate 0 page 5: rotate 90 ``` The new first page is the blank one, and the old first page, now turned 90 degrees, is last. | Call | What it does | |---|---| | `RotatePage(index, degrees)` | Adds to the page’s rotation, clockwise. Multiples of 90. | | `SetPageRotation(index, degrees)` | Sets the rotation outright. | | `RemovePage(index)`, `RemovePages(indexes)` | Deletes pages. | | `MovePage(from, to)` | `to` is the page’s index afterwards. | | `ReorderPages(newOrder)` | `newOrder[i]` is the current index of the page that becomes page `i`. | | `InsertBlankPage(index, width, height)` | A blank page, size in points (612 × 792 is US Letter, 595 × 842 is A4). | | `DuplicatePage(index)` | A copy right after the page. | ## Good to know - The document must be opened with `AllowEditing = true`. Page indexes are zero-based. - `PdfPage` objects, the outline and the form taken before an operation are out of date afterwards. Read `doc.Pages` again. - Deleting a page drops bookmarks and named destinations that led to it, empties links that pointed at it, removes form fields whose widgets were all on it, and renumbers page labels. The structure tree of a tagged file is dropped on deletion, with a diagnostic, because it would keep the deleted content in the file. Rotating and reordering keep it. - `Save` writes only what the remaining pages use, so a deleted page’s content is gone from the file. `SaveIncremental` keeps the old objects, as any incremental update does: don’t use it to get rid of content. Copying pages between files is in [Merge PDFs](https://recursiveai.org/onepdf/cookbook/merge-pdfs/). Everything else is in [Page operations](https://recursiveai.org/onepdf/docs/page-operations/). <!-- https://recursiveai.org/onepdf/cookbook/sign-and-verify/ --> # Sign a PDF and verify its signatures Sign a PDF in C# with a certificate from a .pfx file, verify the signature against your own root, and see how a change after signing is reported. The sample uses `signer.pfx`, a certificate with its private key, and `root.cer`, the root certificate that issued it. Use your own, or make a test pair with .NET’s `CertificateRequest` as in [Getting started](https://recursiveai.org/onepdf/docs/getting-started/). ```csharp using System.Security.Cryptography.X509Certificates; using OnePdf; using X509Certificate2 signer = X509CertificateLoader.LoadPkcs12FromFile("signer.pfx", "test"); using X509Certificate2 root = X509CertificateLoader.LoadCertificateFromFile("root.cer"); // Sign: an invisible PAdES signature, written as an incremental update. using (PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true })) { PdfSigningResult signed = doc.Sign("signed.pdf", PdfSigningCredential.FromCertificate(signer), new PdfSigningOptions { FieldName = "Approval", Reason = "Approved for release", Location = "Springfield", }); Console.WriteLine($"signed field {signed.FieldName}, {signed.ContentsUsed:N0} of {signed.ContentsCapacity:N0} bytes reserved"); } Verify("signed.pdf"); // Change the signed file: stamp a page and save the change as another incremental update. using (PdfDocument doc = PdfDocument.Open("signed.pdf", new PdfOpenOptions { AllowEditing = true })) { doc.AddTextStamp("COPY", doc.AddStandardFont("Helvetica"), 48, pageIndexes: [0]); using FileStream output = File.Create("changed.pdf"); doc.SaveIncremental(output); } Verify("changed.pdf"); void Verify(string path) { using PdfDocument doc = PdfDocument.Open(path); PdfSignatureReport report = doc.VerifySignatures(new PdfSignatureVerifyOptions { TrustAnchors = [root] }); foreach (PdfSignatureVerification v in report.Signatures) { Console.WriteLine($"{path}: {v.Signature.FieldName} valid={v.IsValid} integrity={v.Integrity} signature={v.SignatureCheck} chain={v.Chain} coverage={v.Coverage}"); foreach (PdfSignatureChange change in v.Changes.Where(c => !c.Allowed)) Console.WriteLine($" not allowed: object {change.ObjectNumber}, {change.Category}"); } } ``` ```text signed field Approval, 1,691 of 3,175 bytes reserved signed.pdf: Approval valid=True integrity=Valid signature=Valid chain=Trusted coverage=WholeDocument changed.pdf: Approval valid=False integrity=Valid signature=Valid chain=Trusted coverage=EarlierRevisionDisallowedChanges not allowed: object 7, PageContent not allowed: object 74, PageContent not allowed: object 75, PageContent not allowed: object 76, PageContent not allowed: object 77, PageContent not allowed: object 78, PageContent ``` The signature over the original bytes is still mathematically intact in `changed.pdf`, but the page content changed after it, so the signature is no longer valid for the document as it now reads. ## Signing - Signing is always an incremental update. The original bytes stay an exact prefix of the output, so earlier signatures stay valid. The document must be opened with `AllowEditing`, and changes made before the call, such as a filled field, are part of what is signed. - The default format is PAdES baseline B-B (`ETSI.CAdES.detached`). `Format = PdfSignatureFormat.Pkcs7Detached` writes `adbe.pkcs7.detached` for older consumers. - **Time-stamps.** Set `Timestamp` to a callback that sends `request.RequestDer` to your time-stamp authority and returns its response, and the signature becomes PAdES B-T. `AddDocumentTimestamp` adds a document time-stamp. - **Visible signatures.** Set `Rectangle` (and `PageIndex`) for a visible signature with the signer’s name, the date and the reason. Leave it out for an invisible one. - **Certification.** `Certify` makes a certification signature that states which changes are allowed afterwards. Only the first signature of a document can certify. - **Keys you can’t export.** `PdfSigningCredential.FromExternalSigner` hands OnePDF’s digest to your code, for a hardware module or a cloud key service, and checks the signature value you return. - RSA (PKCS#1 v1.5 and PSS) and ECDSA keys are supported. Long-term validation data (PAdES B-LT and B-LTA) isn’t written. - To sign twice, sign, open the output with `AllowEditing`, and sign again. ## Verifying - `IsValid` is true only when the byte range is sound, the digest matches, the signature verifies, the chain is trusted at the validation time, any time-stamp is valid, and nothing disallowed follows the signature. - Nothing is fetched. A chain is trusted only when it leads to a certificate in `TrustAnchors`, or to the system store with `UseSystemTrustStore`. Revocation is reported as not checked unless you pass a `RevocationCheck` function that answers from CRLs or OCSP responses you already have. - `Changes` lists every object written after the signature with a category (`FormFieldValue`, `NewSignature`, `Annotation`, `PageContent`, `ValidationData`, `Other`) and whether the document’s permissions allow it. Filling a form field after signing is allowed by default; changing page content isn’t. - `Warnings` explains anything that failed or couldn’t be checked, such as a weak algorithm. The checks, the change rules and how known attacks on PDF signatures are reported are in [Signatures](https://recursiveai.org/onepdf/docs/signatures/). A longer walk-through with PAdES B-T time stamps, visible signatures, HSM and cloud keys: [Digitally sign a PDF in C#](https://recursiveai.org/onepdf/articles/sign-pdf-csharp/). <!-- https://recursiveai.org/onepdf/cookbook/split-pdf/ --> # Split a PDF Split a PDF into one file per page, or into chosen page ranges, in C# with OnePDF. Bookmarks, links and form fields go with their pages. ```csharp using OnePdf; using PdfDocument doc = PdfDocument.Open("report.pdf"); // One file per page. IReadOnlyList<PdfDocument> pages = PdfDocument.Split(doc, pagesPerPart: 1); for (int i = 0; i < pages.Count; i++) { using PdfDocument part = pages[i]; part.Save($"page-{i + 1}.pdf"); } // Chosen ranges: pages 1-2 and pages 3-6. PdfPageRange takes a zero-based start and a count. IReadOnlyList<PdfDocument> sections = PdfDocument.Split(doc, [new PdfPageRange(0, 2), new PdfPageRange(2, 4)]); string[] names = ["intro.pdf", "body.pdf"]; for (int i = 0; i < sections.Count; i++) { using PdfDocument part = sections[i]; part.SetInfo("Title", $"Harbor trial report, {Path.GetFileNameWithoutExtension(names[i])}"); part.Save(names[i]); Console.WriteLine($"{names[i]}: {part.PageCount} pages, {part.Outline.Count} bookmarks"); } ``` ```text intro.pdf: 2 pages, 2 bookmarks body.pdf: 4 pages, 4 bookmarks ``` The run also wrote `page-1.pdf` to `page-6.pdf`, one page each. - `Split(doc, pagesPerPart: n)` cuts the document into parts of `n` pages, the last one possibly shorter. `Split(doc, ranges)` makes one document per `PdfPageRange(start, count)`, with a zero-based start. To extract pages, pass a single range. - Each part is a new document in memory that doesn’t refer to the source. Dispose each one after saving it. - A part keeps the bookmarks that lead to its pages, the links between its own pages and the form fields whose widgets are on them. - Parts carry no metadata from the source: set a title with `SetInfo` as the sample does. - A resource dictionary that every page shares, common in slide decks, is copied whole into each part, so parts of such a file can be larger than their share of the original. To put files together, see [Merge PDFs](https://recursiveai.org/onepdf/cookbook/merge-pdfs/). The rules are in [Page operations](https://recursiveai.org/onepdf/docs/page-operations/). A longer walk-through, into single pages, page ranges, fixed-size parts or one file per bookmark: [Split a PDF in C#](https://recursiveai.org/onepdf/articles/split-pdf-csharp/). <!-- https://recursiveai.org/onepdf/cookbook/word-positions/ --> # Get words and their positions Read every word on a PDF page with its bounding box in C#, group words into rows, and find the value printed next to a label, with OnePDF. OnePDF has no table-extraction API. Words with positions are the building block: this recipe reads an invoice’s rows and its total from them. ```csharp using OnePdf; using OnePdf.Text; using PdfDocument doc = PdfDocument.Open("invoice.pdf"); PdfPageText text = doc.Pages[0].ExtractText(); // Rows of the page: words grouped by baseline, top to bottom, each row left to right. var rows = text.Words .GroupBy(w => Math.Round(w.Bounds.Bottom)) .OrderByDescending(row => row.Key); // y grows upward foreach (var row in rows) Console.WriteLine($"y={row.Key,4} " + string.Join(" ", row.OrderBy(w => w.Bounds.Left).Select(w => $"{w.Text}@{w.Bounds.Left:F0}"))); // The value printed to the right of a label, on the same baseline. PdfTextWord label = text.Words.First(w => w.Text == "Total"); PdfTextWord value = text.Words .Where(w => w.Bounds.Left > label.Bounds.Right && Math.Abs(w.Bounds.Bottom - label.Bounds.Bottom) < 2) .OrderBy(w => w.Bounds.Left) .First(); Console.WriteLine($"Total: {value.Text} at ({value.Bounds.Left:F1}, {value.Bounds.Bottom:F1})"); ``` ```text y= 725 No.@491 2026-104@512 y= 712 October@473 26,@515 2026@534 y= 708 Invoice@54 y= 680 Bill@54 to:@72 Example@87 Co.,@133 100@156 Main@178 Street,@204 Springfield@240 y= 647 Item@60 Qty@354 Price@436 Amount@511 y= 624 Design@60 review@97 2@367 $450.00@423 $900.00@513 y= 600 Implementation,@60 hours@141 12@361 $120.00@423 $1,440.00@504 y= 576 Hosting,@60 October@103 1@367 $80.00@429 $80.00@519 y= 553 Total@437 $2,420.00@504 y= 54 Payment@248 due@286 within@304 30@330 days.@342 Total: $2,420.00 at (503.6, 552.7) ``` - `Bounds` is a `PdfRectangle` (`Left`, `Bottom`, `Right`, `Top`, `Width`, `Height`) in points, with the origin at the lower left of the page and y going up. The page’s `/Rotate` is already applied, so the coordinates are those of the page as a viewer shows it. - A word is a run of glyphs without a space between them, so punctuation stays attached (“Co.,”). - `text.Words` lists every word of the page. `text.Blocks`, each block’s `Lines` and each line’s `Words` give the same words grouped in reading order, which suits running text better than tables. - Rounding the baseline to the nearest point is enough for a generated document. For scans with an OCR layer or slightly skewed text, group words whose baselines are within a fraction of the font size instead. - `word.IsVisible` is false for invisible text, such as an OCR layer under a scanned image. To black out the words you found, see [Redact text](https://recursiveai.org/onepdf/cookbook/redact/). How words, lines and blocks are formed is in [Text layout and reading order](https://recursiveai.org/onepdf/docs/text-layout/). For the whole extraction workflow, see [Extract text from PDF in C#](https://recursiveai.org/onepdf/articles/extract-text-from-pdf-csharp/). Coming from PdfPig’s word API: [Migrate from PdfPig](https://recursiveai.org/onepdf/articles/migrate-from-pdfpig/).