Changelog
1.2.0 (2026-11-10)
Section titled “1.2.0 (2026-11-10)”PdfOptimizerOptions.MaxBilevelImageDpi(--max-bilevel-dpi) gives bilevel images (raw, CCITT, JBIG2) their own downsampling resolution, in place ofMaxImageDpi. Null keeps the old behavior.Smallestnow sets it to 300: at 150 dpi one bit per pixel made scanned text ragged, which the image quality gate does not judge, since a bilevel reduction is exact by construction.- OCR: ONNX Runtime 1.31.1. One build property now sets its version everywhere it is named.
- Hyphenation, off by default, by Liang’s algorithm as TeX defines it.
PdfParagraph.Hyphenatortakes aPdfHyphenator: words then break at the line’s end where the language allows, showing a hyphen, whilePdfParagraph.Textkeeps them whole and extracted text marks the hyphen with U+00AD.PdfHyphenator.Createtakes patterns and exceptions in TeX’s formats,WithExceptionsadds words, andMinWordLength,MinLeft,MinRightandHyphenateAllCapsset the limits.FindBreaksandInsertSoftHyphenswork on text of your own. Left alone: words with a soft hyphen, words joined to a digit or holding a capital past the first letter, words of all capitals by default, web and e-mail addresses and paths, words over 64 letters. A point inside a ligature is skipped, and fonts without a hyphen glyph do not hyphenate. Results do not depend on the current culture. PdfNetStack.Hyphenation, a new optional package with patterns from TeX’s hyph-utf8 foren-us,en-gb,de(reformed spelling),fr,nl,es,it,pt,af,be,bg,cy,da,el-monoton,el-polyton,et,eu,ga,gl,hr,la,lt,nb,nn,pl,sk,sl,sqanduk, all under permissive licenses, loaded one language at a time on first use byPdfHyphenationPatterns.ForLanguage. Languages whose only patterns have other licenses (among themru,sv,cs,hu,tr,caandfi) are left out;eng/check-licenses.ps1now checks every set against the allowlist.hyphens: autoinPdfNetStack.Htmlhyphenates text in the language of the nearestlangattribute whenHtmlConverterOptions.Hyphenatorsfinds patterns for it, withPdfHyphenationPatterns.ForLanguagefor instance. Without the option nothing changes, andautobehaves likemanual. Min-content sizes count the points, so narrow table columns can hyphenate instead of overflowing. Words are judged as written: text undertext-transform: uppercasehyphenates like its source words, while text written in capitals stays whole.text-transform: uppercasewrites the German sharp s as SS, as CSS Text 3 asks, with or without hyphenation:Straßenow readsSTRASSE, where it readSTRAßE. Other characters whose full upper case takes several letters keep their simple mapping.- Fixed: justifying a line that ended at a soft hyphen could widen the hyphen along with the spaces, when the run the hyphen belonged to ended with a space.
- New package
PdfNetStack.Ocr.Tesseract:TesseractOcrEngine, anIOcrEnginethat runs an installedtesseractexecutable (versions before 4 are refused; CI runs 5.3.4), soPdfOcrcan read any language Tesseract has data for and run where ONNX Runtime has no build. One process per image, started without a shell: the pixels go in on standard input as a bitmap, with no temporary files, and word boxes and confidence come back as TSV on standard output, parsed as untrusted text. Language names are checked before they reach a command line. A missing executable or missing language data fails atLoad, saying where to get it.Timeout,MaxOutputBytesand cancellation kill the process, andMaxConcurrentProcesseslimits how many run at once. Tesseract and its data are installed separately, and ONNX Runtime’s native libraries stay out: an application using only this engine publishes 3 MB of libraries instead of 241 MB. Pages scanned sideways are not detected with this engine. A CI job runs it against Tesseract from the Ubuntu packages on x64 and arm64.
1.1.0 (2026-11-05)
Section titled “1.1.0 (2026-11-05)”-
QuantizeImages(--quantize) reduces raw 8 bit RGB images to a palette of at most 256 colors in their own color space, for the drawings and screenshots where JPEG rings around fine labels and barely saves anything. Each image gets the smallest palette, of 256, 64, 16 or 4 colors, that keepsMinImageSsim, or 0.97 without a gate, and no more than 8 sample steps of mean change with at most 1% of pixels moved by over 32, which catches a hue drift luminance cannot see. A color holding most of its cluster stays exact, so white stays white. WithConvertImagesToJpegthe smaller candidate wins image by image. The proof expands the palette and checks the same bounds, andPdfEquivalence.FindDifferenceaccepts such images when given a minimum SSIM. On a technical manual full of screenshots and diagrams,JpegQuality = 50alone saved 18.0% and addingQuantizeImagessaved 47.9%.QuantizeImagesalone saved 43.7%, with pages rendered by PDFium closer to the source than withJpegQuality = 50alone (mean SSIM 0.9999 against 0.9996). The pass costs CPU: about 2.7 times as long on that file, single threaded. On the public corpus it took the lossy profile from 29.5% to 32.1% with no failure: 15 of the 65 rewritten documents got smaller, up to 45%, and none got larger. -
The corpus benchmark measures a third profile,
balanced, the preset of that name: 23.4% on the public corpus, at full resolution, with no page below its PSNR warning.--profilespicks others. -
PdfOptimizerOptions.BalancedandPdfOptimizerOptions.Smallest(--preset balanced,--preset smallest) are ready sets of lossy options.Defaultstays lossless. -
The corpus benchmark also runs for every version tag and attaches its report to the GitHub release, with a comparison against the report of the previous release made by
tools/corpus_compare.py: savings, time and peak memory per profile, and per document any rewrite that grew by more than 1% of the original, took 1.5 times as long, needed 1.3 times the memory, stopped being rewritten or now fails a check. Reports record each document’s SHA-256, so only identical files are compared. -
Opening documents with control over memory, for serverless hosts and large files.
PdfLoadOptionsgathers the password,MaxDecodedStreamBytes, a newMaxImagePixels, theMode(PdfLoadMode.InMemoryorOnDemand),SpoolThresholdandTempDirectory, taken by new overloadsPdfDocument.Open(string, PdfLoadOptions),Open(Stream, PdfLoadOptions)andOpen(ReadOnlyMemory<byte>, PdfLoadOptions); the existing overloads keep working and call them.- The input is read once.
Open(Stream)copied a stream into a growing buffer and then into an array; now aMemoryStreamwhose buffer is exposed is used without copying, a seekable stream is read into an array of its length (4.0 MB allocated for a 4.0 MB file, was 8.0 MB from aMemoryStream), and any other stream is read in pieces joined once. - On demand from a path, the document opens the file itself and keeps it until it is disposed.
PdfDocumentis nowIDisposable: disposing a document read on demand closes what it opened and makes later use throwObjectDisposedException; the caller’s stream is never disposed, and disposing a document in memory does nothing. - A stream that cannot seek (an HTTP request body, a storage download) can be read on demand:
OpenOnDemandandOpencopy it to memory up toSpoolThreshold(16 MiB) and past it to a temporary file inTempDirectory(the system’s temporary folder by default; /tmp on AWS Lambda), opened withDeleteOnCloseon Windows and unlinked once open elsewhere, and deleted when the document is disposed or opening fails. PdfDocument.OpenAsync(Stream, PdfLoadOptions?, CancellationToken)reads the stream with asynchronous reads only, for hosts that forbid synchronous I/O, and deletes any partial copy when cancelled.MaxImagePixels(2^28 by default, the previous fixed cap) refuses an image with more pixels before its pixels are allocated, withPdfFormatException, for raw samples, JPEG, JPEG 2000, JBIG2 and CCITT images, by the dictionary’s size and by the size the data declares.PdfImage.Load(byte[], int, long maxPixels)does the same for JPEG, PNG, GIF, BMP, TIFF and WebP files, withFormatException.PdfDocument.ReleaseUnusedObjects()frees the objects a document read on demand has parsed and nothing uses any more; they are parsed again when needed. Objects that differ from the file, and objects still referenced anywhere (found by a full garbage collection), are kept, so no edit is lost. Reading 300 pages that each have their own font kept 1,503 parsed objects and 7.8 MB; releasing every 50 pages kept 301 objects and 1.0 MB.
- The input is read once.
-
PdfDocument.Save(Stream)writes progressively whenPdfSaveOptions.Optimizeis off: each object, or batch of objects for an object stream, goes to the stream as soon as it is serialized, offsets are counted as bytes go out, and the cross-reference data comes last. Memory beyond the document follows the largest object, not the file; a document opened withOpenOnDemandhas its stream data copied from the source as it is written. The bytes are the onesToArrayreturns for the same options. On a generated 278 MB file opened on demand, an unoptimized save peaked at 3 MB of live memory (51 MB working set) instead of 835 MB, and took 0.9 to 1.2 s instead of 1.4 to 5.6 s on a busy machine. With the optimizer on, the default, the file is still built in memory, as the proof needs all of it; the XML docs say so. -
Without the optimizer, encryption is applied in the same single pass; the optimized path still encrypts the proven file as a last step.
-
PdfDocument.SaveAsync(Stream, PdfSaveOptions?, CancellationToken)andSaveAsync(string, ...): the same output, written with asynchronous calls only and flushed, for response bodies in ASP.NET Core and Azure Functions isolated, which refuse synchronous IO. Cancellation is observed before each object and by each write; a cancelled or failed save throws, andSaveAsync(path)then leaves the file as it was. -
HtmlConverter.ConvertToPdfAsync(html, stream, options, cancellationToken): layout on its own thread with no caller thread waiting, thenSaveAsync. -
Streaming optimizer:
PdfOptimizerOptions.Streaming. The source is read on demand (asOpenOnDemandreads it), streams are recompressed a batch at a time (at most 32 MiB of source data) and written straight away, and the rewrite goes to a temporary file: the destination’s own temporary file forOptimizeFileAsync, otherwise one in the system temporary folder. The proof is the in-memory proof, run on a reader over that file: every reachable object compared, every stream by decoded content. Duplicates are found from what streams decode to before anything is written, so the output can differ from the in-memory rewrite (JPEG files differing only in metadata stay two); on the test documents and the benchmark file it is the same.OptimizeAsyncreads aFileStreamorMemoryStreamin place and copies any other input to a temporary file first, asynchronously; the caller’s output is only written asynchronously. Linearization cannot be combined with it.OptimizationStatistics.Streamedreports the mode, andpdfnetstack optimize --streamingturns it on, without an input size limit. On a generated 278 MB file: 50 MB of live memory (300 to 460 MB working set) against 810 MB (1.4 GB), and no slower (8 to 11 s against 10 to 15 s on a busy machine), measured bytools/PdfNetStack.MemoryBench. -
Serverless and containers. The core, HTML and OCR libraries publish with Native AOT and trimming with no warnings (
PdfNetStack.Ocris now marked AOT compatible too), checked in CI on linux-x64 and linux-arm64 for .NET 8 and .NET 10 bytests/PdfNetStack.AotSmoke, which roots all three libraries and then creates, opens, optimizes, extracts text and images from, reads predefined CJK CMaps in and converts HTML to PDF. CI also runs the test suite with invariant globalization and under tr-TR (PDFNETSTACK_TEST_CULTUREsets the culture of a test run). -
PdfFontCollection.AddDirectoryandPdfFontCollection.Systemskip a directory that cannot be listed instead of giving up on the whole tree, search at most 16 levels deep without following links to directories (a link cycle used to recurse until the path was too long), and visit files in ordinal order, so the same fonts win on every machine. With no home directory (HOME unset, as in some serverless hosts) the per user font directories are skipped; they used to resolve against the current directory. -
Variable font instances are held weakly by their font. A font shared by a long running process, such as one in a
PdfFontCollectionused for every HTML conversion, no longer keeps every location it was ever asked for (each a full font program); an instance stays shared while a document or the application holds it. -
Name constraints fail closed without ICU. Under invariant globalization .NET performs no NFKC normalization or IDNA mapping, so a directory string or host name with characters outside ASCII cannot be put in canonical form; it now counts as undecided (excluded, and not permitted) instead of being compared as written, which let full width letters pass an excluded subtree.
-
Cold start: the syllable grammars of the Khmer, Myanmar and Universal Shaping Engine shapers are built when such text is first shaped, not with the first shaper. The first line of text in an embedded font allocates 360 KB less and, under the JIT, takes about 10% less time.
tools/measure_cold_start.pymeasures the first open and save, optimization, text extraction, HTML conversion and embedded font text in a fresh process. -
Samples under
samples/serverless: a Native AOT AWS Lambda function (provided.al2023, arm64) that optimizes PDFs uploaded to S3 and converts HTML to PDF behind a function URL, with a SAM template, and an Azure Functions isolated worker app with an HTTP trigger for HTML and an Event Grid blob trigger for optimization. Both are built in CI. -
OCR finds pages scanned sideways or upside down. When most of the lines read on a page do not run left to right (the detector found them tall, or the line orientation model turned them), a copy reduced to 1600 pixels on its long side is read in each of the four orientations and the one with the most confident left to right text wins; the page is then read turned back and the words map back to page space through the turn.
PdfOcrPage.Orientationsays how far the content stood turned;PdfOcrOptions.DetectOrientation(on by default) turns it off. Upright pages cost nothing more. Sideways pages now read in line order. -
OCR rasterizer hook:
PdfOcrOptions.Rasterizertakes anIPdfPageRasterizerthe application supplies, for pages with no scan to decode (text as vector outlines, vector art, encodings not decoded). It renders the page as shown atRasterizerDpi(300, at mostMaxOcrDpi, lowered to fitMaxImagePixels); its result is checked for size and shape, and its failures are reported per page. PdfNetStack still does not render; a PDFium based renderer can be plugged in. -
pdfnetstack-ocr, a command line tool in its own package,PdfNetStack.Ocr.Tool:pdfnetstack-ocr in.pdf out.pdf --languages latin,arabic [--models folder], with--text,--json,--no-orientation,--all-pages,--min-confidence,--threadsand--password. It carries the models and ONNX Runtime’s native libraries for Windows, Linux and macOS (a 70 MB package), soPdfNetStack.Toolstays small and free of ONNX.pdfnetstack ocrruns it when it is installed and says how to install it when it is not. -
eng/check-licenses.ps1allows a package underonlyInto ship with several named projects. -
Signature validation confirms qualified status against EU trusted lists the application supplies; the library still downloads nothing.
PdfTrustedList.Loadreads ETSI TS 119 612 lists (the list of lists and national lists) from bytes or a stream, with DTDs prohibited, at most 32 MiB and 64 levels: scheme information, pointers to other lists with their signing certificates, providers, services with type, status, history, digital identities (certificates, subject key identifiers, subject names), qualifications with their criteria and additional service information. Each list’s enveloped XMLDSig signature is verified against the certificates supplied for it, or for a national list against the pointers of a verified list of lists (Load(xml, listOfLists)): every reference must digest correctly and one must cover the whole list, identifiers must be unique, and Canonical XML 1.0 and 1.1 and Exclusive XML Canonicalization are implemented in the library (no new dependency), checked against Microsoft’s implementation in the tests. RSA PKCS #1 v1.5, RSA-PSS with any salt length (also for id-RSASSA-PSS keys) and ECDSA, with SHA-256, 384 and 512; SHA-1 is refused. The signer must be valid at the signed XAdES signing time, or else at the issue date. -
PdfValidationOptions.TrustedListsandTrustedListsAsTrustAnchors; every signer getsPdfCertificateValidation.TrustedListQualification(Qualificationat the signing time,AtIssuance, the decidingService,Reasons) as ETSI TS 119 615 decides it: the CA/QC service whose identity is the public key of a CA above the certificate, its status from the history at issuance and at the signing time (granted, or supervised or accredited before eIDAS), the qualifiers its criteria select (key usage, policies, extended key usage, subject attributes, nested lists) and the certificate’s QCStatements. Conflicts, criteria or critical extensions the library cannot evaluate, and missing or expired lists give the newPdfQualification.Indeterminate. So does a certificate whose own checks did not pass: the lists are then not consulted. Services are matched by public key only: a listed certificate with the same key, or a listed X509SKI equal to the SHA-1 of the certificate’s subjectPublicKey (RFC 5280 method 1). The subject key identifier extension a certificate carries is never used. -
Trusted list anchors (
TrustedListsAsTrustAnchors, default true) are restricted. Only the identities of CA/QC and CA/PKC services anchor a chain, and only for a validation time at which the service’s entry in force has an approved status (granted, recognised at national level, or before eIDAS under supervision, in cessation or accredited) and no critical extension the library does not interpret. TSA/QTST and TSA services anchor time stamp authority chains under the same rules, never a signer’s. OCSP, CRL and other services, and withdrawn, deprecated or never approved ones, anchor nothing. The default stays true: lists are only used when their signature verified against certificates the application supplied, and without anchors the qualification could never be confirmed. A chain that reaches none of these anchors fails, and the problems say what the list names its top certificate as. -
Timestamps get
PdfTimestampInfo.QualifiedTimestamp: Passed only when the authority’s certificate checks passed, the token is intact, and a TSA/QTST service granted at the validation time has as its identity the authority’s own key, or the key of a CA of its chain that the list names as that TSA/QTST service. Otherwise Failed when no such service was granted, or Indeterminate when the authority’s checks did not pass.tools/PdfNetStack.TrustedListCheckreads and verifies the live EU lists for a manual check; the tests generate every list and PKI. -
JPEG 2000 images decode tile by tile, a band of rows at a time, straight into 8 bit pixels. Only a window of rows per resolution and a few rows of code-blocks per subband are held in 32 bits, so the decode limit has to cover the pixels and those rows instead of 32 bit planes of the whole image: a 600 dpi color page (4812 by 7541, one tile) now decodes within the default 256 MiB, peaking near 190 MB where it needed about 1 GB. Tiled images reuse one tile’s buffers. The samples are unchanged, bit for bit.
-
JPEG 2000 decoding is faster with unchanged output: tier-1 keeps each stripe column’s state in one word and skips columns with nothing to code, the MQ decoder renormalizes a byte at a time, rows are lifted with their low and high-pass halves apart and columns several at a time, and the color transform and level shift run in one pass. A 6 megapixel lossy RGB image decodes in about 60% of the time it took; lossless images gain less, being bound by the arithmetic decoder.
-
JBIG2 regions combine with the page a byte at a time instead of a pixel at a time, and an immediate text region that draws onto the page the same way symbol by symbol (OR onto a white region, AND onto a black one, or REPLACE) is drawn straight onto it with no bitmap of its own. MMR runs fill bytes, and generic regions read their context rows by offset. A synthetic 600 dpi text page decodes about four times faster.
-
PdfPageImage.Decodemaps one component images of 8 bits or less (bilevel scans, gray, indexed) through a table of each sample value’s pixel instead of converting every pixel, and returns a JPEG 2000 image’s 8 bit gray or RGB pixels without copying them. With the JBIG2 changes, a synthetic 600 dpi text page comes out ofDecodefour to five times faster. -
tools/make_jpx_fixtures.py --largewrites a large synthetic page for measuring memory and time; it is not committed. -
Optimizer: bilevel images are coded as CCITT Group 4 when that is smaller (
PdfOptimizerOptions.OptimizeBilevelImages, on by default;--no-group4turns it off). It takes 1 bit images stored as samples under any general filters, and CCITT Group 3 data that decodes without a flaw and whose parameters leave nothing to a reader’s defaults. The encoder follows ITU-T T.6 and writes the same bytes as libtiff. The proof decodes both versions and requires every pixel to match, andPdfEquivalenceaccepts such a recoding from any tool on the same terms.OptimizationStatistics.ImagesRecodedAsGroup4counts them. -
Lossy optimizer options reach scanned documents.
MaxImageDpireduces bilevel images (raw, CCITT or JBIG2) and keeps them bilevel: each new pixel takes the share of the source that covers it, the less common value wins past half, and a stroke thinner than the new pixels keeps its middle, so text stays legible; the result is Group 4 or Flate. It also reduces gray and RGB JPEG 2000 images, which become JPEG atJpegQuality(or lossless samples without it), andConvertImagesToJpegturns photographic JPEG 2000 images into JPEG. The proof decodes JPEG 2000, JBIG2 and CCITT sources, compares a bilevel output with the same reduction of its source, and allows a JPEG 2000 image without /BitsPerComponent to gain /BitsPerComponent 8. A JBIG2Globals stream no image uses any more is dropped. -
Optimizer: every JPEG the lossy options make is checked against the proof’s color and coarse scale bounds before it is chosen, so a small image a JPEG would shift too far stays as it was instead of failing the proof of the whole file.
-
The lossy image pass shares a memory budget of twice
MaxDecodedStreamBytesbetween its workers, so a document of large JPEG 2000 scans no longer decodes one per thread at once: peak memory on two scanned books of the benchmark corpus fell from 5.7 and 7.0 GiB to 2.2 and 2.7. -
CCITT decoding: a row whose every pixel changes color (a 1 or 2 pixel wide image, for one) no longer ends decoding there.
-
tools/scan_bench.pywrites synthetic scans of each kind a scanner produces (1 bit under Flate and LZW, Group 3 fax, Group 4, JBIG2, gray and color JPEG 2000) and measures both optimizer profiles on them. -
HTML conversion is 1.5 to 1.9 times faster with the same PDF, byte for byte. On the benchmark samples (fastest of 125 conversions each, on a busy 8 core machine): a one page invoice went from 46 to 31 ms, a one page report from 30 to 20 ms, a 21 page invoice with a 640 row table from 465 to 244 ms, a 15 page report with lists, images, SVG charts and a grid from 110 to 68 ms, and a 52 page text from 316 to 193 ms. The first conversion in a new process is 1.1 to 1.8 times faster. What changed:
ConvertToPdfsaves without the optimizer’s equivalence proof, which read both files back and took about half the optimizer’s time: the converter draws all the content itself. NewPdfSaveOptions.VerifyOptimization(default true) andHtmlConverterOptions.SaveOptions(default null: asPdfSaveOptions.Defaultwithout the proof); setSaveOptionstoPdfSaveOptions.Defaultto keep the proof.- Each page paints the parts of the document that reach it instead of walking all of it.
- Table cells are measured for the column widths and laid out from the same text, fonts, shaping and break opportunities, and collapsed borders read each style once per table.
- Shaped runs are kept for the rest of the conversion (up to 256 characters a run and 256K characters in all), so repeated labels and numbers shape once.
- OpenType lookups pass over glyphs their first coverage does not list, as HarfBuzz’s set digests do; each stage’s lookups are worked out once per font. A hostile font’s step budget is spent more slowly as a result; output is otherwise the same.
- Installed fallback fonts are looked up only when a character needs them, so a document in the generic families never scans the installed fonts: its first conversion in a process went from 352 to 258 ms.
- Font subsets are made and compressed side by side when a document is saved.
- Less work per character in inline layout: the bidi algorithm is skipped for text below U+0590 in a paragraph that is not right to left (every level is 0 there), fonts are chosen once per run of one style, break opportunities need no sorting, and CSS keywords and colors are read once per value. Attribute values without character references, such as data: URIs, are copied whole.
- The optimizer’s content analysis reads operators without a string each, and font program merging reads each program once.
- Fixed:
ConvertFileToDocumentdroppedMaxPageswhen it set the base address.
-
tools/PdfNetStack.Benchmarkshas three larger HTML samples (a 21 page invoice, a 15 page report with lists, images, SVG and a grid, a 52 page text);quick,coldandsamplestake a sample name,samplesalso writes the PDFs and their page counts, andquickcounts the allocations of every thread. -
HTML: absurd sizes no longer exhaust memory. A font size such as 1e300px laid out one line across an unbounded number of pages. Computed lengths are now clamped to a million px either way (percentages to a million percent), font sizes to 10000px, unitless line heights to 1000 and page sizes to 14400 points a side, and
HtmlConverterOptions.MaxPages(default 10000) ends a conversion that needs more pages with the newHtmlConversionException. -
Variable fonts in font matching.
PdfFontCollectioncounts a variable font for every weight, width and slant its axes cover, and registers its named instances as faces; faces are also found by full or PostScript name.Find(family, PdfFontQuery)matches width, then style, then weight with ranges as CSS Fonts 4 does (5.2), and returns the instance at the values found, withOpticalSizeandVariationsapplied over them (7.2): ital for italic, slnt for oblique, never both.PdfFontStylegives normal, italic or oblique at any angle. Static families match as before, now also by OS/2 width class. -
HTML:
@font-facetakes ranges infont-weight,font-stretch(andfont-width) andfont-style(oblique 0deg 15deg), and thefont-variation-settingsandfont-named-instancedescriptors. A missing descriptor isauto: the face matches as normal and the variation follows the style unclamped.font-style: oblique <angle>,font-optical-sizing(opsz set to the font size in CSS px) andfont-variation-settingsare supported; thefontshorthand resets the last two. Rules with identical descriptors: the last one wins. -
Variable font instances are made once per location and the default location is the font itself, so a document embeds one program per location drawn, however many sizes use it.
@font-facerules that share a file share its font. A damaged variation table makes matching fall back to the font’s default instance. -
HTML bounds the variable font instances a document can ask for: axis values snap to a grid (whole units for wght, wdth and opsz, half degrees for slnt, tenths for ital, a hundredth of the range for other axes) and each font makes at most 32 instances per conversion, the nearest one standing in beyond that. Font values that are not finite are dropped and the rest clamped, and a style value that font matching still rejects falls back to the family’s default instead of failing the conversion.
font-stretchaccepts only finite percentages. -
PdfFontQuerychecks each value when it is set and copiesVariations, so an invalid query throws where it is built. -
tools/make_variable_font_fixtures.pyalso builds Test-Axes.ttf, a font with wght, wdth, opsz, ital and slnt axes for the matching tests. -
PDF/A-1b output (
PdfConformance.PdfA1B): PDF 1.4 with a classic cross-reference table and no object streams, also when the optimizer runs. Transparency (opacity, soft masks, alpha images, blend modes, transparency groups), JPEG 2000, optional content, embedded files, file attachment and other post PDF 1.4 annotations, OpenType font programs and ICC profiles newer than version 2 are refused with the rest of the problems; subset CID fonts get a CIDSet stream read from the embedded program. -
PDF/A-4, 4e and 4f output (
PdfA4,PdfA4E,PdfA4F): PDF 2.0 withpdfaid:rev2020. The information dictionary written holds only the modification date; the document keeps its values and the XMP carries them. Every embedded file is an associated file with its relationship; 4f needs at least one and takes any type, plain 4 and 4e take PDF/A-1, 2 and 4 files only, and 4e allows 3D and RichMedia annotations with their GoTo3DView and RichMediaExecute actions. -
PDF/A checks for every part: actions are walked (open action, links, widgets, bookmarks and
Nextchains) and Launch, JavaScript, ResetForm, ImportData, Hide, media and other forbidden types, and named actions other than page navigation, are refused, as are additional actions on pages, widgets and fields, XFA, transfer functions, external stream data, PostScript and reference XObjects and OPI. LZW streams are re-encoded with Flate, image interpolation is switched off and NeedAppearances is removed. Saving an invoice as anything but PDF/A-3 is refused. -
Signature validation, the X.509 checks of ETSI EN 319 102-1: certificate policy processing as in RFC 5280, 6.1 with policy mappings, policy constraints and inhibit anyPolicy, on the policy graph of RFC 9618 (
PdfValidationOptions.AcceptablePolicies,RequireExplicitPolicy,InhibitPolicyMapping,InhibitAnyPolicy;PdfCertificateValidation.PolicyandValidPolicies); name constraints on DNS names, email addresses, URI hosts, directory names and IP addresses, the trust anchor’s included (PdfCertificateCheck.NameConstraints), with hosts compared in one canonical form (one trailing dot dropped, IDN as ASCII, lower case, URIs read bySystem.Uri), directory strings decoded in every string type and prepared as RFC 4518 says, and anything that cannot be read or judged failing closed, including constraints that cannot be read, which fail every certificate below them; and the QCStatements of ETSI EN 319 412-5 (PdfCertificateCheck.QcStatements,PdfQcStatements), with the qualification they claim (PdfCertificateValidation.Qualification: QESig, QESeal, AdESig-QC and the rest). No trusted list is downloaded. Critical policy, name constraint and QC statement extensions no longer fail a certificate as unknown. -
Text extraction is about 3.8 times faster with identical output:
ExtractTexton a 60 page, 27,000 word report in an embedded TrueType font went from 255 ms to 67 ms and from 300 MB to 69 MB allocated. Reading order sorts runs of touching letters instead of single letters and decides each line once per size and baseline; the content interpreter no longer copies the text state for every glyph, shares operator names and reads CID widths without a copy per glyph. -
HTML conversion does less repeated work per line of text: a tab width is measured only for runs that contain a tab, the paragraph text is copied once for all its runs, and a font’s OpenType features are read once per script. Output is unchanged.
-
The command line tool is published as native executables, compiled ahead of time, for linux-x64, linux-arm64, win-x64, osx-x64 and osx-arm64, attached to each GitHub release with a
SHA256SUMS.txt. No .NET install is needed to run them. -
tools/PdfNetStack.Benchmarksmeasures text extraction and HTML conversion with BenchmarkDotNet; it is not part of the solution or the packages. -
Timing tests take the fastest of several runs, so a busy machine cannot fail them.
-
Shaping for more scripts, matching HarfBuzz glyph for glyph on the fonts we compare:
- The Universal Shaping Engine, with its categories generated from the Unicode Character
Database and Microsoft’s overrides by
tools/make_shaping_data.py: Balinese, Javanese, Sinhala, Tibetan, Sundanese, Batak, Buginese, Cham, Tai Tham, Lepcha, Meetei Mayek, Chakma, Grantha, Newa, Sharada, Tirhuta and the other Brahmic scripts HarfBuzz gives it, Indic fonts made for it (dev3 and the other version 3 tags), and Mongolian, N’Ko, Adlam and the other scripts that join like Arabic (a Mongolian free variation selector takes its letter’s form), and Egyptian hieroglyph clusters. - Thai and Lao: sara am splits into nikhahit and sara aa, and fonts without Thai substitutions get their marks placed through the Windows or Macintosh private use glyphs.
- Khmer (coeng ro and pre-base vowels reordered, split vowels decomposed, cfar), Myanmar (kinzi, medials and pre-base vowels reordered) and Hangul (syllables composed or decomposed for the font, ljmo, vjmo and tjmo, tone marks).
- All shapers: invalid vowel sequences of the Indic and USE scripts get a dotted circle, cmap format 14 variation sequences pick their glyphs, a combining grapheme joiner that blocks no reordering is skipped by lookups, a ZWJ keeps a mark from attaching to the base before it (mark and mkmk see joiners), and contextual lookups whose nested lookups add glyphs index the sequence as HarfBuzz does.
- The Universal Shaping Engine, with its categories generated from the Unicode Character
Database and Microsoft’s overrides by
-
Text extraction reads right to left lines in logical order when they start or end with a Latin word: the paragraph direction comes from both ends of the line, and punctuation next to the word stays in it (“To.” rather than “To .”). Numbers in right to left lines follow the weak rules of UAX #9: after Arabic, “2026-114” (drawn 114-2026) reads in logical order, while dates, amounts and percentages stay whole.
-
Public benchmark corpus:
eng/corpus/manifest.jsonlists 77 public domain and openly licensed PDFs (U.S. government publications, arXiv papers under CC BY 4.0, public domain scans and Chinese, Japanese and Korean documents from Wikimedia Commons, and test files of the veraPDF, qpdf, PDFium, pypdf and PDF Association projects) by URL, SHA-256, size, license, where the license is stated and what each file exercises. No file is committed.tools/fetch_corpus.pydownloads and verifies them;tools/corpus_bench.pyrunstools/PdfNetStack.CorpusRunneron each one, one process per measurement, for lossless and lossy optimization (size, time, peak memory), checks every rewrite again withPdfEquivalence, compares the text and decoded images the library reads before and after, compares PDFium renders, and writes a Markdown and a JSON report. TheCorpus benchmarkworkflow runs it weekly and on demand. -
Fixed, found by the corpus:
- A cross-reference
/Prevchain that loops is treated as damage and the file is rebuilt from its objects. A newest section naming itself as/Prevused to hide every older section, so the file opened without its page tree, and the optimizer (withAllowRepairedFilesor not) could rewrite it as a document without pages. - Content stream compaction judges whether a stream ends inside a comment from its tokens. A
%inside a string on the last line, common in the two byte codes of composite fonts, was taken for a comment once the stream was compacted onto one line, so the proof rejected the rewrite and the whole document was returned unchanged. - JBIG2: the work budget is eight times the page instead of four. Scans that code the page as one symbol placed by a text region spend five, and failed to decode.
- A cross-reference
-
Optimizer, lossless structural passes, each proven by the equivalence check against its own analysis of the source (docs/FIDELITY.md gives the rules):
PruneUnusedResourcesremoves fonts, XObjects, graphics states, patterns, shadings, color spaces and property lists that no content using the resource dictionary names, following forms, tiling patterns, Type 3 glyph procedures, soft masks and annotation appearances, and inherited and shared resource dictionaries. Anything it cannot read for certain is kept.CompactContentStreamsrewrites content streams with minimal white space, canonical numbers, shortest strings and no comments; inline image data is copied exactly.MergeFontSubsetsmerges subsets of one TrueType font embedded separately, as merged documents carry them, into one program when no glyph any page shows changes.- Deduplication compares streams by their decoded content, so differently encoded copies of a font or image merge.
Linearize(opt-in) writes a linearized file (Annex F) with page offset, shared object and outline hint tables, laid out the way qpdf checks it. Inherited page attributes move onto the pages.OptimizationStatisticsreportsResourcesPruned,ContentStreamsCompacted,FontProgramsMergedandLinearized; the command line tool takes--linearize,--no-prune,--no-compactand--no-font-merge.
-
PdfDocument.OpenOnDemand(Stream)opens a document on a seekable stream without reading it into memory: objects are parsed when first used and stream data is read when asked for, with 64 bit offsets.PdfDocument.SaveIncremental(Stream)saves the changes since opening as an incremental update, appending in place when given the source stream; existing signatures stay valid. -
PdfEquivalence.FindDifferenceaccepts the structural passes above and inherited page attributes moved onto pages. -
tools/make_optimizer_samples.pyandtools/measure_optimizer.pymeasure each pass on synthetic files and check linearized output with qpdf when it is installed. -
Annotations:
PdfAnnotationCollection.AddStamp(the fourteen standard stamps, or a custom one from aPdfFormor a drawing callback),AddInk,AddSquare,AddCircle,AddLine(withPdfLineEndingat either end),AddPolygon,AddPolylineandAddFileAttachment(the file embedded behind an icon;PdfAnnotation.Attachmentreads it back).PdfAnnotationOptionssets the color, interior color, border width, dashes, opacity, comment, author and subject. Each comes with a generated appearance; below full opacity the drawing is a transparency group faded as a whole. PDF/A-3 output declares the files of file attachment annotations as associated files, and PDF/A-2 refuses them. -
Annotation flattening:
PdfAnnotationCollection.Flatten(annotation),Flatten(options)for a page andPdfDocument.FlattenAnnotations(options)draw each annotation’s normal appearance (or the one its state selects) into the page content, fitted to its rectangle by bounding box and matrix as ISO 32000-1, 12.5.5 describes, upright on rotated pages for NoRotate annotations and inside its optional content group, then remove it with its pop up. Hidden and NoView annotations stay; links and widgets stay unlessPdfFlattenOptionsasks for them, and a flattened widget leaves the form.PdfAcroForm.Flattenuses the same placement. -
Form fields travel with their pages:
Pages.Import,MergeandSplitcopy each widget’s field and ancestors into the target form, with the source form’s default appearance, alignment, resources and calculation order. A top level field whose name the target already uses is renamed with the first free suffix_2,_3and so on. Before, widgets were copied without their fields. -
Format actions: a field whose format action calls
AFNumber_Format,AFPercent_Format,AFDate_FormatEx,AFDate_Format,AFTime_Format,AFTime_FormatExorAFSpecial_Formatshows its value formatted in the generated appearance (red for negative numbers when asked), andPdfTextField.FormattedTextreturns it. The call is parsed and the formatting done in C#; no JavaScript runs, and any other script leaves the value as it is. -
Fixed: form flattening threw on a widget whose appearance entry was not a stream.
-
HTML grid layout follows CSS Grid 1 and 2 further:
grid-auto-flowwithcolumnanddensepacking; line names in track lists andrepeat(), placement by name and byspan name, with thename-startandname-endlines of areas and the implicit lines past the explicit grid; implicit tracks before the explicit grid for negative lines;subgridon rows, columns or both, with the parent’s line names, the subgrid’s own name list, its own gaps, and its items sizing the parent’s tracks through its margins, borders and padding.repeat(auto-fill)counts the other tracks of the list when working out how many repetitions fit, and the empty tracks ofrepeat(auto-fit)collapse with their gaps. Thegrid-templateandgridshorthands are read, area strings andauto-flowincluded. -
HTML named pages: the
pageproperty works on any block in the block flow, not only children ofbody(CSS Paged Media 3, 8). A change of page name forces a break, and the content after it is laid out at its own page type’s width. A box that spans the break is sliced: its top edge shows before the break and its bottom edge after, and the margin after the break is kept. -
HTML pagination: a forced break before the first content or after the last no longer adds an empty page, so
break-before: pageon the first element starts the document on page one. -
Fixed: HTML boxes with a
z-indexnow paint in stacking order (CSS 2.1, Appendix E). A negativez-indexpaints over the stacking context’s background and borders and under its blocks, floats and text; a positive one paints over all of it, in z-index order. Aposition: fixedbox with a negativez-indexpaints under the content of every page, where before it painted over it. Positioned boxes, flex items and grid items with az-index, and boxes with opacity below one or a transform, start a stacking context. -
Fixed: HTML
text-decoration-style: wavydraws a wave instead of a straight line, for underline, overline and line-through. Its amplitude and wavelength follow the line’s thickness, and it is clipped to the decorated text. -
PAdES B-T, B-LT and B-LTA.
PdfTimestampClientbuilds RFC 3161 requests (hash, random nonce, optional policy) and checks the reply (status, hash, nonce, signature, a critical time stamping extended key usage); the application supplies the transport.PdfSignatureOptions.Timestampadds a signature timestamp,PdfSigner.AddDocumentTimestampa /DocTimeStamp, andPdfSigner.AddValidationDatathe document security store: certificate chains, OCSP responses and CRLs from anIPdfRevocationSource, and a VRI entry per signature, in an incremental update. Only revocation data that matches the certificate and is signed by its issuer or delegated responder is kept; delta and partitioned CRLs are not used. The library makes no network calls. -
Signature validation:
PdfSigner.Verify(pdf, PdfValidationOptions)builds chains to caller supplied trust anchors and checks validity at the timestamp’s time when its authority validates (otherwise now), key usage (signer, CA with path length, time stamp authority) and revocation from the document security store or supplied data, with OCSP freshness and delegated responders.PdfCertificateValidationreports each check per certificate as passed, failed, indeterminate or not checked, with the reasons.PdfSignatureInfonow also reports document timestamps and signature timestamps. -
Factur-X and ZUGFeRD:
PdfInvoice.Attachembeds the invoice XML (checked for well formedness, without DTDs) as factur-x.xml, or xrechnung.xml for XRECHNUNG, with the profile’s relationship; saving as PDF/A-3 writes the fx: XMP properties (MINIMUM to EXTENDED and XRECHNUNG) with their extension schema, checks the attachment is still there, and keeps the metadata when an opened invoice is saved again. -
PDF/A-3: the catalog’s /AF array now references the attachment’s file specification itself instead of a copy of it.
-
CI: a veraPDF job validates PDF/A-2b, 2u, 3b and 3u files made by the library, including a Factur-X invoice signed to PAdES B-LTA.
-
Lossy image options for the optimizer, all off by default:
MaxImageDpi(--max-dpi) downsamples images drawn above 1.5 times the target resolution, measured from the largest placement found on pages, in forms, tiling patterns, soft mask groups and annotation appearances. The filter is Lanczos with three lobes. Masks, images with a color key mask or a /Matte soft mask, bilevel, indexed, 16 bit, Lab, Separation and DeviceN images, and images reachable from anything the measurement does not walk (Type 3 glyphs, thumbnails, alternates) are left alone. A downsampled JPEG stays a JPEG, never finer than it was.ConvertImagesToJpeg(--to-jpeg) re-encodes raw 8 bit gray and RGB photographs as baseline JPEG atJpegQuality(forward DCT, libjpeg quality tables, 4:2:0 below quality 90, Huffman tables optimized per image). A classifier leaves line art, screenshots, charts, palette and bilevel images lossless. A lossy result is kept only when it is at least 10% smaller than the best lossless one.MinImageSsim(--min-ssim) is a perceptual gate on luminance SSIM: any image whose lossy change, requantization included, falls below it stays as it was, andImagesKeptByQualityGatecounts them. The proof checks resampled and converted images against the source resampled to their new size (components, aspect, mean color, a coarse comparison and SSIM), and with a gate also requantized ones. An image whose data is carried over unchanged must keep its size.PdfEquivalence.FindDifferencetakes the same gate and the optimizer’s exclusions: soft masks, masks, stencil and color keyed images must match exactly, and images with a /Mask or a /Matte soft mask keep their size.- A JPEG with a color key mask is no longer requantized, since the key matches exact sample values.
ProgressiveJpeg(--progressive) also writes every JPEG the optimizer produces as progressive, with libjpeg’s scan script and Huffman tables per scan, and keeps it when smaller. Coefficients are unchanged, so this one is lossless.OptimizationStatisticsreportsImagesDownsampled,ImagesConvertedToJpegandImagesKeptByQualityGate, and the result’s reason names every lossy change.
-
CompressionEffort.Exhaustive(--exhaustive): an original Deflate encoder in the manner of Zopfli, written from RFC 1951 and published descriptions: optimal parsing over every match length with costs refined over several passes, block splitting, and code lengths limited by package-merge. zlib’s output is kept whenever it is smaller. On the synthetic files oftools/make_lossy_corpus.pyit saves 3 to 5% more than level 9 on text, scans and photographs (nothing on line art and screenshots), for 2 to 150 times the CPU time, the most on long repetitive streams. -
PdfNetStack.Azureis discontinued so the project can focus on PDF itself. Reading from and writing to storage belongs to the application, around the stream APIs. Version 1.0.0 stays on NuGet and keeps working. -
Variable fonts:
PdfOpenTypeFont.GetInstancegives a named instance (NamedInstances) or any axis values (VariationAxes,IsVariable), embedded as a static font. TrueType outlines move by their gvar deltas, with untouched points interpolated and composite offsets varied; advances come from HVAR or the phantom points, font metrics from MVAR (with hhea kept in step), control values from cvar, weight and width classes and the italic angle from the axes, through avar. CFF2 fonts load (at their default instance) and convert to CID-keyed CFF: blends resolve in charstrings and Private DICTs, subroutines are inlined, and argument runs longer than CFF’s 48 operands are split. The shaper applies GDEF variation deltas to GPOS values and anchors, and GSUB and GPOS feature variations. Outlines, advances and metrics match fontTools’ instancer and shaping matches HarfBuzz on the synthetic fixtures and on the variable fonts we compare. Damaged variation data is aFontFormatException. -
Unicode normalization in the shaper, as HarfBuzz’s normalizer does it: characters the font lacks decompose into ones it has, marks sort into canonical order (with the shaping tailorings for Hebrew, Arabic and others), a base and mark compose when the font has the composite, and spaces and the non-breaking hyphen without glyphs draw with the space and hyphen at their own widths. Font fallback,
CanRenderand HTML text use the same coverage. Data generated from the Unicode Character Database 16.0 bytools/make_shaping_data.py. -
Indic shaping for Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil, Telugu, Kannada and Malayalam, per Microsoft’s script development specifications: syllables, base consonant, reph, pre-base matras and consonants, the basic forms one feature at a time, final reordering, presentation forms, old (deva) and new (dev2) font specifications, and dotted circles for broken clusters. It matches HarfBuzz on a synthetic Devanagari and Bengali fixture and on the three faces of a font covering all nine scripts.
-
Kashida justification (
PdfParagraph.Kashida, off by default): justified Arabic lines elongate one join per word with tatweel, at the point Arabic typography prefers, up to three tatweel widths, before spaces widen. Tatweels carry no text, so copying gives the original words. -
Fixed: a mark drawn with a vertical offset (Arabic dots and vowels in many fonts) moved every glyph after it by its horizontal offset, so letters failed to join and lines came out wider than measured. Spaces at the edge of a right to left run are now written outside its ActualText, so viewers no longer read them on the wrong side of the neighboring run.
-
Tagged PDF from HTML (
HtmlConverterOptions.Tagged, on by default): headings, paragraphs, lists, tables with header and data cells, figures with their alt text, links tied to their annotations, sections and quotes form a structure tree in reading order (ISO 32000-1, 14.8); backgrounds, borders, text shadows, repeated table headers, fixed boxes and page margins are artifacts. The catalog is marked and viewers show the document title. -
JPEG 2000 images decode (
PdfPageImage.Decode, JPXDecode): JP2 files and raw codestreams, ITU-T T.800 part 1. Every progression order and progression change, precincts, layers, tiles and tile-parts, PPM, PPT, SOP and EPH, all code-block styles, ROI, the 5/3 and 9/7 wavelets, RCT and ICT, subsampled, signed and 1 to 16 bit components, JP2 palettes and channel definitions. The image dictionary’s color space wins, otherwise the JP2 header’s (ISO 32000-1, 7.4.9). Lossless images decode bit exact and lossy ones within one step of OpenJPEG. Sizes, counts and work are bounded. SMaskInData is not applied; HTJ2K (part 15) and part 2 extensions are not supported. -
JBIG2 images decode (
PdfPageImage.Decode, JBIG2Decode with JBIG2Globals): generic, refinement, text and halftone regions with their symbol and pattern dictionaries and code tables, arithmetic, Huffman and MMR coded, per ITU-T T.88. Output is bounded by the image’s size, and damaged data is aPdfFormatException. Tested bit for bit against PDFium on the T.88 Annex H test datastream and synthetic streams. -
Named destinations:
PdfDocument.NamedDestinationslists the names of the /Dests name tree and the catalog’s /Dests, resolves them to pages, and renames them with every bookmark, link and GoTo action that refers to them, in their own string or name form, rewriting the name tree as one sorted leaf; colliding renames are refused.PdfOutline.DestinationNamegives the name a bookmark points at. -
GIF images:
PdfImage.Loadreads GIF 87a and 89a (the first frame, interlaced or not, with its transparent color as a soft mask), soDrawImageand<img>in HTML take them too. -
BMP, TIFF and WebP images:
PdfImage.Loadreads BMP (1 to 32 bits, RLE4 and RLE8, bit fields, top down rows, alpha, an embedded JPEG or PNG), TIFF (strips and tiles, chunky and planar, uncompressed, PackBits, LZW and Deflate with the predictor, CCITT modified Huffman, Group 3 and Group 4, JPEG; bilevel, gray, palette, RGB, CMYK and YCbCr samples of 1 to 16 bits, associated and unassociated alpha, BigTIFF) and WebP (lossy and lossless, with alpha, the first frame of an animation). Alpha becomes a soft mask. A TIFF page stored as one strip of fax or JPEG data is embedded as is (CCITTFaxDecode, DCTDecode).PdfImage.GetPageCountandPdfImage.Load(path, page)give every page of a multi-page TIFF. Lossy WebP decodes to the pixels libwebp gives, bit for bit. Decoding is bounded, and damaged files fail with aFormatException.<img>in HTML takes all three formats. -
PdfGraphics.DrawImagefits an image into a box (PdfImageFit: fill, contain, cover, none, scale down), aligns it there (PdfImageAlignment, clipping what spills over) and fades it with an opacity. -
Form creation:
PdfAcroForm.AddTextField,AddCheckBox,AddRadioGroup(buttons may span pages),AddComboBox,AddListBox,AddSignatureFieldandAddPushButton(optionally opening a web address), styled withPdfFieldOptions(standard font, size, colors, border, tooltip, read only, required, alignment, multiline, password, length limit, comb, editable, multiple selection). Every new field comes with its appearance and the print flag. -
New package
PdfNetStack.Ocr:PdfOcr.MakeSearchablefinds each page’s scan, turns it upright, reads it and lays the words over it as invisible text;Recognizereports without changing the document.OnnxOcrEngineruns the PP-OCRv5 mobile models (PaddleOCR, Apache-2.0) on ONNX Runtime for Latin and Arabic script, with DB box detection, line orientation, CTC decoding, word boxes and right to left ordering in managed code, and model files checked against pinned SHA-256 hashes (tools/fetch_ocr_models.py). ONNX Runtime’s telemetry is turned off before the first model loads.IOcrEngineplugs in any other engine. -
New package
PdfNetStack.Ocr.Models(Apache-2.0 models, versioned on its own from 1.0.0): the four PP-OCRv5 model files with their license and notices, copied next to the application on build and publish;OnnxOcrEngine.Load()without a folder loads them from there. The files are never committed: CI and the release workflow fetch them, and packing fails when one is missing or does not match its pin. The license check now also covers bundled files and keeps ONNX Runtime out of every package butPdfNetStack.Ocr. -
OCR reads scans stored as several images: strips or tiles side by side, or a text mask over a background. The pieces are painted in drawing order into one gray canvas in page space, at the finest piece resolution up to
MaxOcrDpi, decoded one at a time, with image count, combined pixels, canvas size and overlap capped before anything is decoded; word boxes map back to the page through the canvas. -
Text extraction:
PdfPage.ExtractText()returns a page’s text in reading order,GetLetters()each glyph with its text, bounds, baseline, font name, visible size and render mode, andGetWords()the words with their bounds. Glyphs are placed as the specification positions them (character, word and horizontal scaling, rise, TJ adjustments, forms, Type 3 font matrices, vertical writing). Text comes from ToUnicode maps (bfchar and bfrange, ligatures, surrogate pairs, codes of 1 to 4 bytes), from the simple font encodings and /Differences with the Adobe Glyph List, from embedded CMaps with usecmap, the Identity and Unicode predefined CMaps, and embedded TrueType cmaps; spans marked with ActualText are replaced by their text, so right to left text the library writes reads back in logical order. Lines follow each run’s own baseline, so rotated and vertical text read in order; columns of prose are found by XY cuts and read one after the other, right to left for Arabic and Hebrew pages, while tables keep reading row by row. CMaps, fonts and content are bounded and never throw. -
CJK text extraction: Adobe’s predefined CMaps for the Adobe-Japan1, GB1, CNS1, Korea1 and KR collections (all 180 of cmap-resources, among them every CJK CMap of ISO 32000-1 Table 118, such as 90ms-RKSJ-H, GBK-EUC-H, B5pc-H, KSCms-UHC-H and the Uni…-UCS2, UTF16, UTF8 and UTF32 forms) split codes and give CIDs, so widths and vertical metrics follow the CMap, and the CID to Unicode tables of mapping-resources-pdf give text to composite fonts without a ToUnicode map, by the CMap’s collection or the CIDFont’s /CIDSystemInfo (ISO 32000-1, 9.10.2). The data is packed into one Brotli stream per collection (196 KB in all) and decompressed on first use. Embedded CMaps that use a predefined one inherit its collection, and keep their own /WMode.
-
Simple fonts without /Encoding read their codes through the built-in encoding of an embedded CFF program (FontFile3, Type1C or OpenType): the Standard and Expert encodings and custom encodings of both formats with supplements, glyphs named through predefined or custom charsets.
-
PdfGraphics.DrawInvisibleText: invisible, searchable text stretched to a box, optionally rotated, for OCR text layers. A built in glyphless font (Type0, Identity-H, one empty glyph, identity ToUnicode) takes any script; right to left text and characters outside the BMP also carry ActualText. -
Image extraction:
PdfPage.GetImages()lists the images a page draws (XObjects, inside forms too, and inline images) with their placement, bounds and resolution.PdfPageImage.Decode()returns aPdfBitmapof 8 bit gray or RGB pixels from raw samples of 1 to 16 bits, from JPEGs (inverse DCT, fancy upsampling and color conversion as libjpeg does them, within two steps per sample) and from CCITT fax data (Group 3 one and two dimensional, Group 4, bit exact with libtiff), through the device, Cal, ICC based, Indexed and Lab spaces, and Separation and DeviceN (exact for a Type 2 tint transform, gray otherwise);PdfBitmap.ToPng()saves it.GetEncodedData()returns a JPEG or JPEG 2000 image as stored. -
New package
PdfNetStack.Html: HTML and CSS to PDF in pure C#, without a browser.HtmlConverter.ConvertToPdf,ConvertToDocumentandConvertFileToDocument, configured withHtmlConverterOptions(base address, resource loader, fonts, page style, extra style sheet, bookmarks).- HTML parsed the way browsers repair it, with the legacy presentational attributes.
- CSS: cascade, inheritance,
var(), Selectors Level 4, media queries for print,@import,@font-face,@pagewith margin boxes,calc()and friends, CSS Color 4. - Layout: blocks with margin collapsing, inline text with Unicode line breaking, bidi and
shaping, floats, relative, absolute and fixed positioning, tables in both border models,
flexbox, grid with fr tracks,
minmax(),repeat()and named areas, inline blocks, lists, counters and generated content. - Paged media: page sizes and margins,
:first,:leftand:right, named pages laid out at their own size, running headers and footers with page counters and named strings (string-set,string()), running elements (position: running()shown withcontent: element(), any content: logos, tables, styled text, changing with the page where they stand, with page counters inside), margin boxes that widen when their neighbours are empty, forced and avoided breaks, orphans and widows, table headers repeated on every page. - Painting: backgrounds with images and gradients, rounded borders, shadows, outlines,
transforms,
object-fit, decorations; links and bookmarks; document metadata from the head. - Safe by default for untrusted HTML: no network access without a loader, files only from
the base folder, and limits on nesting, selectors,
var()expansion, spans and tiling. - It depends on the exact
PdfNetStackversion it ships with.
-
The
PdfNetStackandPdfNetStack.Azurepackages are checked against 1.0.0 on every pack, so a change that would break code built against the published version fails the build. -
SVG:
PdfSvg.LoadandParse, andPdfGraphics.DrawSvg, draw SVG as PDF vectors: shapes, paths with arcs, groups,useandsymbol, nested viewports withviewBoxandpreserveAspectRatio, transforms, solid colors, linear and radial gradients, clip paths, opacity, dashes, text in the standard fonts, embedded images, and<style>rules with type, class and id selectors. Each drawing is written once per document. Doctypes are skipped, so entity expansion cannot be abused, and size, depth andusenesting are capped. InPdfNetStack.Html, SVG works in<img>, CSS backgrounds and inline<svg>. -
Public object model:
PdfObject,PdfNull,PdfBoolean,PdfNumber,PdfName,PdfString,PdfArray,PdfDictionary,PdfStreamandPdfReferenceinPdfNetStack.Objects, withPdfDocument.Catalog,Resolve,AddObjectandGetStreamData, andPdfPage.Dictionary, for features the high level API does not cover. -
Drawing:
PdfPage.CreateGraphics()returns aPdfGraphicscanvas (top left origin, points) for new and existing pages, in front of or behind the existing content.PdfPathwith lines, Bezier curves, arcs, ellipses, polygons and rounded rectangles;PdfPen,PdfColor(gray, RGB, CMYK, opacity), solid, linear and radial gradient brushes; transforms, saved states, clipping. Only changed graphics state is written, and shared page resources are never modified. -
Text with the 14 standard fonts:
PdfStandardFont,PdfGraphics.DrawString(fill with any brush including gradients, stroke with a pen, or both) andPdfFont.MeasureString, with ascent, descent, line height, cap height, x-height and underline metrics. Latin text uses WinAnsiEncoding; up to 37 further characters per font and document (Central European letters, the minus sign) get codes through /Differences. Symbol and ZapfDingbats take their Unicode characters. Control and format characters are skipped; a character the font lacks throws. -
Embedded fonts:
PdfOpenTypeFont.Loadreads TrueType and OpenType (CFF) fonts from .ttf, .otf, .ttc/.otc collections, WOFF and WOFF2 files. Drawn text embeds a subset of the glyphs used as a Type 0 font (CIDFontType2 or CIDFontType0C), with a W array and a ToUnicode map, so text is searchable and copyable in any script. Pair kerning from GPOS (formats 1 and 2, extension lookups) or the kern table. Line metrics follow browsers (typo metrics when the font sets USE_TYPO_METRICS, else hhea). Fonts whose fsType forbids embedding are refused; damaged fonts raiseFontFormatException. -
PdfFontCollection: add fonts, files and directories, or usePdfFontCollection.Systemfor installed fonts;Find(family, weight, italic)matches as CSS does. Scanning reads only the name, OS/2 and head tables; fonts load on first use. -
WOFF2 fonts:
PdfOpenTypeFont.Load,PdfFontCollection(.woff2 files) and@font-faceinPdfNetStack.Htmlread WOFF 2.0, including transformed glyf, loca and hmtx tables and font collections. Table checksums and head.checkSumAdjustment are recomputed. .NET 8 and later decompress with the built-in Brotli decoder; .NET Standard 2.0 uses a managed Brotli decoder (RFC 7932) with the static dictionary embedded. Lengths, offsets and counts are all checked, the decompressed size is capped at 128 MB as for WOFF, and damaged files raiseFontFormatException. -
PdfFontgainsStrikeoutPositionandStrikeoutThickness. -
Complex text:
DrawStringandMeasureStringrun the Unicode bidirectional algorithm and an OpenType shaper. Arabic and Persian join and take their contextual forms, ligatures form (lam-alef, fi), vowel marks sit on their letters, mixed right to left and left to right text is ordered correctly with mirrored brackets, and kerning follows GPOS or the kern table as HarfBuzz applies them. Shaped and right to left text carries ActualText, so it copies and searches in logical order. -
Paragraph layout:
PdfParagraph(rich text spans ofPdfTextStyle),PdfTextLayoutandPdfGraphics.DrawText. Wrapping at Unicode line break opportunities, start, end, left, right, center and justified alignment, right to left paragraphs, fallback fonts per style, letter and word spacing, underline and strikethrough, first line indent, CSS line height, soft hyphens, tabs, and drawing a range of lines for text split across pages. -
Images:
PdfImage.Loadfor JPEG (embedded without recompression, EXIF orientation, Adobe CMYK, ICC profiles, JFIF density) and PNG (all color types and bit depths, Adam7 interlacing, tRNS as color key or soft mask, alpha channels as soft masks, iCCP, pHYs; simple PNGs keep their compressed data with PNG predictors).PdfGraphics.DrawImageat natural or given size. Each image is written once per document. -
Form XObjects:
PdfForm.Createfor reusable drawings,PdfForm.FromPageto place any page of any document (rotation honored, resources imported once),PdfGraphics.DrawForm, andPdfGraphics.Formfor drawing into a form. -
Annotations:
PdfPage.Annotationsreads every annotation (kind, rectangle, contents, link address, destination page including named destinations) and adds web and page links, notes, text markup (highlight, underline, strikeout, squiggly) and free text, each with an appearance stream.PdfGraphics.AddLinkmaps a canvas rectangle through the current transform. -
PdfGraphics.BlendMode: the 16 PDF blend modes, saved and restored with the graphics state. -
Interactive forms:
PdfDocument.Formlists every field (PdfTextField,PdfCheckBoxField,PdfRadioButtonField,PdfChoiceField,PdfPushButtonField,PdfSignatureField) with fully qualified names, flags, options and widgets. Setting values regenerates appearances (borders, backgrounds, DA font and color, auto size, multiline, comb, password) and drops XFA.PdfAcroForm.Flattenstamps the appearances into the pages and removes the form. -
Document features:
PdfDocument.Viewer(page layout, page mode, window options, right to left reading, opening page and zoom),PdfDocument.PageLabels(roman, decimal and letter ranges with prefixes, label lookup),PdfDocument.Attachments(embed, read, replace, remove files) andPdfDocument.Language. -
PDF/A:
PdfSaveOptions.Conformancewrites PDF/A-2b, 2u, 3b or 3u. Problems that cannot be repaired are listed together in one exception; the output gets an sRGB output intent with a profile the library generates, XMP metadata synchronized with the document information, printable annotations, and PDF/A-3 associated file entries for attachments. -
Digital signatures:
PdfSigner.Signadds a PAdES B-B signature (ETSI.CAdES.detached, SHA-256, 384 or 512, RSA or ECDSA, ESS signing certificate v2) in an incremental update, matching the file’s cross-reference style, optionally with a visible appearance.PdfSigner.Verifychecks each signature’s byte range, digest and signature and reports the signer certificate. The CMS structure is encoded by the library; no new dependency. -
Packages now include
THIRD-PARTY-NOTICES.md, which carries Adobe’s notice for the standard font metrics.
1.0.0 (2026-10-28)
Section titled “1.0.0 (2026-10-28)”First release on NuGet: PdfNetStack, PdfNetStack.Tool and PdfNetStack.Azure.
- JPEG images: lossless re-encoding on by default (
OptimizeJpegImages). Huffman tables are rebuilt for each image, progressive and restart marked JPEGs become baseline, and segments PDF readers ignore are dropped. Coefficients are untouched and verification compares them one by one. - Opt-in JPEG requantization (
JpegQuality, CLI--jpeg-quality): coarser tables at a libjpeg quality, never finer than the image’s own, applied in the coefficient domain so the color model cannot change. Soft masks stay lossless. Verification checks each coefficient is exactly the requantized source value.OptimizationStatistics.JpegImagesRecompressedandJpegImagesRequantized; the Azure sidecar records the quality, and lossy blobs carryjpegqualitymetadata. - Document editing API:
PdfDocument.Open,Create,Save,Merge,Split; pages can be added, inserted, removed, moved, rotated and imported from other documents with shared resources copied once;Infofor metadata with Unicode and date support;Outlinesfor reading, editing and writing bookmarks while keeping their original styling and actions. - CLI:
merge,split,extract,rotate,meta,encrypt,decrypt. - Encryption: open documents protected with any standard security handler revision (RC4, AES-128,
AES-256), with user or owner password; write AES-256 or AES-128 with permissions; documents
opened with the owner password keep their protection on save.
PdfDocument.Security. - .NET Standard 2.0 target: .NET Framework 4.6.2+, Mono and Unity. Tests run on .NET Framework 4.8.
- Contribution guide with an AI-assisted contribution policy, and a code of conduct.
- Hardening from review: bounded resolution depth, bounded object regions, predictor row clamping, identity checks in deduplication and verification, per document decode budget.
- JPEG reader hardening: oversubscribed Huffman tables are refused before use, at most 64 scans per image, frames that claim more blocks than their data can code are refused as decompression bombs, JFIF and Adobe segments are kept once, and an image with a component no scan covers is left untouched.
- Verification accepts requantized coefficients only on the images the optimizer requantized; every other JPEG must stay coefficient identical.
- Requantization follows the Adobe marker over
/ColorTransformwhen choosing chroma tables, as ISO 32000-1 specifies. - NuGet packages: icon, a readme per package, package validation.
First public version.
- Lossless optimizer: unreachable object removal, object streams, compressed xref streams, stream recompression, PNG prediction for raw images, iterative deduplication, compact syntax.
- Equivalence verifier on every rewrite, public as
PdfEquivalence.FindDifference. - Refuses encrypted, signed, incrementally updated and damaged files unless allowed.
- PDF/A-1 detection with classic xref output.
pdfnetstackcommand line tool: optimize, batch, inspect, verify.PdfNetStack.Azure: Blob Storage pipeline with content-addressed output and provenance sidecar.