What changed, from the point of view of someone who has to decide whether to install this version.
The format follows Keep a Changelog 1.1.0, and the version numbers
follow Semantic Versioning 2.0.0. A version gets an entry here
before it is tagged, not after: docs/release-checklist.md G3 refuses a release whose notes do not exist,
and a version with no notes is a version a consumer cannot evaluate.
1.0.0-preview.1 was published to nuget.org on 2026-10-15, as four packages — SharpCorePDF.Domain,
.Application, .Infrastructure and .DependencyInjection — pushed by publish.yml from tag
v1.0.0-preview.1. The SharpCorePDF entry point did not exist in that tree, so the only id on the feed
that a consumer could have installed was a layer. 1.0.0-preview.2 and 1.0.0-preview.3 were bumped here
and never pushed, which is why this file keeps no entries for them: there is no consumer of a version no
feed ever served.
1.0.0 is therefore the first version that changes the shape of the install rather than only the
contents of the layers, and the entry below is written against the published preview.1 rather than
against an empty feed.
Nothing yet. 1.4.0 is on the feed: the five packages were pushed by publish.yml from tag v1.4.0 on
2026-11-04, and the entry below it is the version that was released.
-
An image's colour space is read, not guessed from its name, so the spaces and bit depths that used to stay payloads now yield pixels.
DecodedPixelswasnullfor every image whose samples were not already one byte per channel ofDeviceGrayorDeviceRGB— a bilevel scan, a palette image, a CMYK photograph, anICCBasedone — even though the payload had been decoded and nothing about it was unknown. The read now resolves/ColorSpacethrough the document (PdfSampleSpaces), following a resource name, an[/Indexed base hival lookup]array to the palette it points at — a stream or a string, decoded through the same filters as any payload — and an[/ICCBased profile]array to the component count its/Nstates, and a sample that is narrower than a byte is widened instead of a row being read as a continuous bit stream.PixelFormatgains a third value,Cmyk32: four device samples per pixel with no ink conversion, because turning cyan, magenta, yellow and black into light is a rendering decision rather than a fact of the file, and neither an ICC profile nor a tint transform is applied anywhere on this route. ASeparation,DeviceNorLabspace, sixteen bits per sample, and a palette image whose indices run past its own table stay payloads, and the record states which one it is. The expansion is bounded by the sameMaxInlineContentBytesbudget that governsData, so widening a large image cannot allocate past it. -
An image compressed with LZW is decoded instead of being handed back compressed.
LZWDecodesat in the list of filters the reader named and passed through, so a scan stored the way TIFF stores one was reported withIsDecodablecleared andDecodedPixelsnulleven though the library could already read everything about it but the samples.PdfFiltersnow carries a decoder for the coding PDF inherits from TIFF — nine-bit codes growing to twelve, the table built as the stream is read,EarlyChangehonoured — so such an image yields its samples on both the read and the copy route, and theImageFilterUnsupportedwarning it used to raise is gone. The same decoder serves object streams, so a document that stores one behind LZW now reads instead of raisingPdfUnsupportedFeatureException, and the abbreviated name/LZWis accepted wherever/LZWDecodeis — which it was not before. The one convention a decoder can get wrong on its own account is the code at which the field widens, so the tests decode a strip libtiff wrote as well as one this repository's own test encoder wrote: a round trip cannot catch a rule that is wrong on both sides, andEarlyChangeexists precisely because a decoder's table is one entry behind an encoder's.
- A materialised payload is handed back as a view of the buffer the copy route filled, not as a second array
of the same size.
ReadImageDataAsyncandReadEmbeddedFileDataAsyncreturnedReadOnlyMemory<byte>that already was the payload, and then copied it withToArray(); both now return the stream's own buffer, sliced to its length, so reading a large attachment into memory costs one copy of it rather than two. Nothing observable changes but the allocation: the bytes, the length and the lifetime after the call are the same.MergeToBytesAsynckeeps its copy — abyte[]of exactly the merged length cannot be the buffer of a stream that grew by doubling — and its documentation now says so, instead of leaving the copy to be found in a profiler.ReadPageDetailAsyncis written withawait foreachand reads exactly as it did. The same pass documents the destination path ofMergeToFileAsync,SaveImageDataAsyncandSaveEmbeddedFileAsync: it is used as given, no root is enforced, and confining a path that came from an untrusted source is the caller's job. That boundary is stated once inSECURITY.md, once in the manual's chapter on error handling, and in each member's XML documentation.
- A filter name this library has never seen was reported as a damaged payload rather than as an
unsupported filter.
PdfImageFiltersrecognised five names it does not decode —JPXDecode,JBIG2Decode,CCITTFaxDecode,RunLengthDecodeandCrypt— and let every other name fall through to the arm that reports data a filter refused. A producer that invents a filter name therefore had its image reported asImageDataUnreadable— "the payload could not be decoded" — whiledocs/manual/src/08-inspection.mdandspecs/001-pdf-content-inspection/contracts/warnings.mdboth describe such an image asImageFilterUnsupported: nothing was decoded because the decoder is not there, not because the bytes are damaged, and the stored bytes are still what the read carries. The pass-through decision is now the complement of the four filters the library decodes, so the two cases stay apart however many names a document states. - An image with no filter never yielded pixels, so the simplest image of all was the one shape the read
model could not state.
ImagePixelstreated anullfilter chain as "nothing here to widen" and returned before it looked at the samples, which meant an unfiltered image — and every inline image written without a filter — was reported withDecodedPixelsnullwhile the same bytes behind/FlateDecodewere exposed, anddocs/manual/src/08-inspection.mdhad always described the unfiltered image as the simplest case rather than an exception. The condition now asks whether the chain containsDCTDecode— the one payload whose samples are not bytes yet — instead of whether there is a chain at all. - A form whose field tree was too deep lost fields without saying so.
ReadFormFieldsAsyncfollowed the tree to a hardcoded 32 levels and, past that point — or at a reference that repeated an object it had already read — returned without a word, so a truncated form read exactly like a shorter one. The walk now usesPdfEngineOptions.MaxNestingDepth, the same guard the page tree and the content reads already use, and reports the cut with the newPdfWarningKind.FormFieldDepthExceeded; a repeated object is reported asFormFieldUnreadable. The fields read above the cut are still returned, which is what makes "this form has five fields" and "this form was too deep to finish" two different answers. Adding the enum member is additive: the set is open, so an existingswitchkeeps working through itsdefaultarm. - The signature search had a depth of its own, and it was silent too.
SignatureValidatorcarried a privateMaxFieldDepth = 32and returned past it without a word, so a document whose form nests deeper than that answered "these are its signatures" with nothing to suggest the list could be short. The walk now usesPdfEngineOptions.MaxNestingDepth, the same limit as everything else, and recordsPdfWarningKind.FormFieldDepthExceededon the reader. A signature answer has no warnings channel, so the cut is also reported as the newPdfSignatureProblemKind.FormFieldTreeTooDeepon every validation that was returned:IntegrityandTruststill describe the signature that is named, and the new problem says the answer is narrower than the document rather than that a signature is wrong. The page annotations are still scanned as well, which is where a signature a viewer shows carries its widget, so the cut costs the fields between the root and the widget and not the signature itself.
1.3.0 is on the feed: the five packages were pushed by publish.yml from tag v1.3.0 on 2026-10-24.
- Chapter 25 describes how this manual is built.
docs/manual/src/25-how-this-manual-is-built.mdwalks the four stages — Markdown to HTML, HTML to pages, measure, finish — and states what each one refuses: the converter's subset of Markdown (no raw HTML, no nested list, no image), the six class names the highlighter emits so that the stylesheet stays the contract,--no-pdf-header-footerand why the browser prints no header of its own, the forward search that locates a heading without pulling the cursor back into an earlier chapter, the second pass that replaces the placeholders with measured numbers and the re-render that follows a number which moved a heading, and the overlay, outline and metadata SharpCorePDF writes. It records the coordinate trap that put the footer at the top of the page, and the command line with its five switches. The preface no longer asks for Python to build the book.
- The manual is generated by the library it documents.
tools/SharpCorePDF.Manualreplacesdocs/manual/build-manual.py, anddotnet run --project .\tools\SharpCorePDF.Manualis the build: it writesdocs/SharpCorePDF-Manual-<version>.pdf(the1.3.0edition unless--versionsays otherwise) and there is no second route. The tool is a console project inSharpCorePDF.slnxunder/tools/and needs nothing beyond the .NET SDK and Chrome or Edge —SHARPCOREPDF_CHROMEpoints it at a browser installed elsewhere, and a missing browser is an error message that says so rather than a stack trace. The Markdown conversion, the highlighting, the contents page, the two passes that measure the heading pages, the running footer, the bookmarks and the document metadata are all written with SharpCorePDF's own reader, authoring surface and edit plan, so no pypdf and no Pygments take part. The rebuilt edition is 26 chapters, 118 pages and 222 bookmarks; the1.2.0entry below records the edition as it was first released, at 25 chapters and 113 pages.
- The quickstart promised one package and then built a container.
SharpCorePDFbrings every SharpCorePDF assembly, butServiceCollectionandBuildServiceProvider()live inMicrosoft.Extensions.DependencyInjection, which the registration package does not reference — it takes...Abstractions, the assemblyAddSharpCorePDF()extends. A project that followed Chapter 3 and Chapter 10 as they were written did not compile until it added that package itself. Both chapters now say so, where the first provider is built. G8 found it: the published1.2.0packages, installed into an emptynet11.0console app, failed on the first build withCS1061onIServiceCollection. - The manual builder printed the running footer under the top edge. The Python builder wrote the
footer's text matrix 30 points above the bottom edge; the port passed the same 30 to a canvas that counts
down from the top-left corner, so every page carried the footer in its top margin, where it reads as a
header.
ManualFinishernow converts the anchor (page.HeightPoints - 30, the rule 12 points closer to the text), which puts the footer back inside the 18mm bottom margin the stylesheet declares. Reading the printed page's content stream —1 0 0 1 48.2 30 Tmafter the fix,811.92before it — is what caught it; a screenshot of the page did not. - A layer was drawn under the state the page left open. The streams of a page reach a reader as one
stream, so a producer that states a transform outside a
q— Chromium's print output opens every page with.24 0 0 -.24 0 841.92 cm— had that transform still in force wherePdfEditPlan.Overlaydrew its form: the layer landed at a quarter of its size, in the corner of the page instead of where it was made. The plan now frames the original content inq/Qwhenever it draws a layer, as it already did for a resized page, so what a page states cannot reach what the plan appends — which is whatdocs/parity.mdrow 6 asked for in the first place. The page's own bytes are still copied unchanged.PdfDocumentEditorTests.Places_a_layer_where_it_was_made_when_the_page_leaves_its_state_openis the test; the manual's footer is how the fault surfaced, as a short dash of purple above the first line of every page.
- A document is annotated without touching its content.
PdfAnnotationscreates the ten kinds a viewer draws — a link to a URL or to a page, a sticky note, free text, a stamp, a highlight, an underline, a strike-out, a square, a circle, a line and ink — andPdfEditPlan.AddAnnotation,UpdateAnnotationandRemoveAnnotationwrite them through the same immutable plan as every other edit. The page's/Annotsarray is rebuilt and the page's content streams are never rewritten, so a page the plan does not name keeps every object it had; an annotation removed stops being reachable and is therefore not written at all. Every annotation the engine creates carries an appearance stream of its own — a note with a folded corner, free text auto-sized to its box, a centred stamp caption, filled quads, elliptical arcs, lines and ink — drawn in the annotation's own space and compressed, so a viewer that does not synthesise appearances and a printer, which never does, still show it; a caller that wants its own operators suppliesPdfAnnotationAppearance. An annotation without an identifier is namedscpdf-N, which is what a later operation of the same plan names it by. The refusals happen before a byte is written: a link with both a page and a URL, a markup without a quad, a line with the same point at both ends, an unknown flag bit, an opacity outside 0–1, a translucent annotation for PDF/A-1b and an identifier the page does not carry. A rectangle that reaches past the page is clipped with anAnnotationClippedwarning, and a link to a page the same plan deletes is dropped with anAnnotationDestinationUnresolvedwarning. - A form is described, filled and flattened through the same plan.
PdfFormFieldscreates the five fields a form is built from — text, check box, radio button, choice (list box or combo box) and push button — andPdfFormgroups them with the style a field that states none is drawn in.PdfEditPlan.SetForm,SetFormField,ResetFormandFlattenForm(whole document or one page) carry them, and a field is addressed by its name, dotted when it is a leaf of a tree: writing a name the document already has fills that field in place and leaves every other field alone. The flags a document stores are derived from the properties (IsReadOnly,IsRequired,IsMultiline,IsComboand the rest), so no bit the editor cannot honour can be asked for. The refusals happen before a byte is written: a name with an empty part, a choice value that is not an option unless the combo box is editable, repeated or empty options, a button that states a value, a value longer than the maximum length, a text field that is multiline and a password field at once, options or a length on a kind that holds neither, two fields that share a name, and a plan that writes one name twice. - The form writer fills, resets and flattens.
PdfEditEnginenow writes the fields a plan states, soSetFormFieldon a document that already carries the name fills that field where it stands — its rectangle, its ancestors and its widgets survive, and only/V,/ASand/APchange — while a name it does not carry becomes a new field, a dotted name written as a tree. Every widget gets an appearance the engine draws (a wrapped or shrunk value, a masked password, a highlighted list selection, a tick, a dot, a caption),NeedAppearancesis writtenfalse, and the form carries a/DRwith the fonts it names.ResetFormrestores each field's/DV;FlattenFormdraws the widgets into the page content so a reader without form support still shows the values, then takes them off the page and drops the form. A read-only field and a signature field are refused with the field's name, an XFA packet is removed with the newXfaFormRemovedwarning, and an action on a field is copied inert withFormFieldActionPreserved. Thefill-formexample writes, fills and flattens a form, and the three files pass qpdf, pypdf and PyMuPDF. - An authored page draws a PNG, a JPEG, a BMP or a GIF image.
PdfImage.Openreads a file once —FromPng,FromJpeg,FromBmpandFromGifare the same read with the format stated, and refuse a file whose signature disagrees — and reportsFormat,WidthPixels,HeightPixels,ColorSpace,BitsPerComponent,HasAlpha,NaturalWidth,NaturalHeight,FrameCountand aKeythat makes two opens of one file a single image object.PdfPageCanvas.DrawImagetakes either the bytes or thePdfImage, at an explicit size or at the size the file's resolution states. A JPEG is passed through untouched underDCTDecode, withDeviceGray,DeviceRGBor — when an AdobeAPP14segment states the transform —DeviceCMYKand the/Decodearray that flips its inverted samples, and a file it cannot read (an EXIF orientation, arithmetic coding, a precision other than eight bits, four components with no transform) is refused by name. A BMP is decoded from its palette or its twenty-four and thirty-two bit pixels in either row order, and a GIF's first frame is LZW-decoded, de-interlaced, and given a soft mask from its transparent palette entry; the frames after the first are counted byFrameCountand not drawn, because PDF has no animation. - A PNG that states its own colour profile keeps it. An
iCCPchunk becomes an/ICCBasedcolour space with the profile in its own Flate stream —/Nfrom the profile's signature,/Alternatethe device space to fall back to — instead of being dropped, which would show the picture through whatever profile the viewer happens to use.PdfImage.ColorSpacereportsIccBasedfor such a file, and a profile that does not describe the samples (aGRAYprofile on an RGB image) is refused by name. - An authored document carries an XMP packet, bookmarks, and pages copied from another file.
PdfDocumentBuilder.WithXmpwrites an XMP packet into the catalog exactly as it was given and uncompressed, from bytes or from thePdfXmpMetadataa reader returned.WithOutlinewrites the bookmark tree — nesting, open or closed state, page destinations, URLs, and the/XYZcorner a stated area names — and refuses a bookmark that points at a page the document does not have rather than writing a dangling one.ImportPageandImportPagescopy pages out of another document with the merge engine's own copier, so an imported page keeps its content, resources, annotations, rotation and geometry, and the attributes it inherited from its own page tree are stated on the copy. - A typeface says what its metrics are, and measures text without a page.
PdfTypeface.Metricsreturns aPdfTextMetricsin the face's own font units — em size, ascent, descent, cap height, x-height and the line heightLineHeight(size)derives from them — for a standard font out of metrics generated from the AFMs, and for an embedded TrueType face out of itshead,hhea,maxp,hmtxandcmaptables, with the cap and x height falling back to the outline ofHandxwhenOS/2states neither.PdfTypeface.Measure(text, size)returns the advance width of a string, with optional character and word spacing, so a caller can centre a title or size a column before anything is built. A standard font measures through WinAnsi and refuses withArgumentExceptiona character it cannot encode rather than guessing a width; an embedded face refuses a character itscmapdoes not map, and a size that is not a positive finite number is refused by both. - Text wraps into a box and is drawn line by line.
PdfTextLayouter.Layouttakes a string, aPdfTextStyleand aPdfTextLayoutOptions— width, alignment, line height,BreakWords,Overflowand direction — and returns thePdfTextLayoutthatPdfPageCanvas.DrawTextLayoutdraws: a paragraph per line break, greedy wrapping at spaces by measured word width,Left,Center,RightandJustifyplacement (a justified line stretches the gaps between its words, not the words themselves), a leading that is the option, then the style's, then the typeface's own line height, andRefuse,BreakWords,CliporContinuefor a word that does not fit the width. The layout's coordinates are its own: the origin is the top-left corner of the box and y grows downwards, so the first baseline is one ascent below the point the layout is drawn at. A right-to-left paragraph is refused withPdfUnsupportedFeatureExceptionrather than drawn backwards. - An edit records one page of another document over or under a page of the source.
PdfEditPlan.OverlayandUnderlaytake a one-page document, a page number and aPdfOverlayOptions— placement (Fit,Stretch,OriginalSize), opacity,Clipand the six separable blend modes — and write the layer as a form XObject that keeps the resources of the document it came from, so a font or image name in the layer cannot collide with a name the page already means something else by. The page's own streams are copied unchanged: an overlay appends a stream that draws the form, an underlay prepends one, and the opacity and blend mode of the layer travel in anExtGStateof its own. The layer follows its page through a move, a duplicate shares it, and a deletion removes it with the page. A translucent layer is refused on a PDF/A-1b output, whose format has no transparency. - An edit replaces the bookmarks, the page labels, the viewer preferences and the XMP packet.
PdfEditPlan.SetOutlinewrites the bookmark tree as stated, with page destinations numbered in the order the edit leaves the pages in, andPdfOutline.Emptyremoves the panel the source carried;SetPageLabelswrites ranges in any of the six styles the standard names, andPdfPageLabeling.Noneremoves the labels instead of reporting them as dropped;SetViewerPreferenceswrites the window, panel, layout, binding and print-scaling preferences in place of the ones the source had; andSetXmpwrites the packet as given, uncompressed. A bookmark or a label range that names a page the result does not have is refused before the source is opened, and a later operation of the same kind replaces an earlier one. - An edit appends a revision to the source instead of rewriting it.
PdfEditOutputOptions.UseIncrementalUpdatecopies the bytes of the source into the result unchanged and writes the edited objects, the pages and the catalog after them as one more revision, whose cross-reference section chains back to the one the source ended with through/Prev. That is what keeps a signature: the bytes it covers are still the bytes it covered, so the outcome reportsPriorSignaturePreservedfor the signature andIncrementalUpdateAppendedfor the revision, while the same edit as a complete rewrite reportsPriorSignatureInvalidated. New objects are numbered above every object the source used, because a reused number would make an earlier revision ambiguous to a reader that resolves through the newest section first. The version of the source is kept — an appended revision cannot change the header it follows — and a plan that asks for another one is written anyway, withIncrementalUpdateVersionKept. The path refuses what it cannot honour: a source that cannot seek, an encrypted source, whose appended objects would need the file key of the revision they follow, a cross-reference table the reader had to rebuild by scanning, a destination that already holds bytes, and a plan that also asks for encryption or a PDF/A profile. - The examples draw a page of shapes. The
draw-shapesscenario (examples/SharpCorePDF.Examples/DrawingExamples.cs) writes one page of vector content with the authoring surface: a round-capped dashed rule with a dash phase, a two-subpath path whose even-odd fill leaves a hole, a clipped column of stripes drawn past its own clip, and a group of shapes turned and moved by a singleSetTransform. It is the artifact the drawing row ofdocs/parity.mdis validated against. - The examples watermark a document. A new
watermarkscenario inexamples/SharpCorePDF.Examples/PdfEditExamples.csbuilds a stamp as a document of its own — one A4 page whoseDRAFTis turned forty-five degrees — and places it on every page of an existing document as an overlay at 18 % opacity inMultiply, then prints the operations and the warnings the edit reports and reads its own output back, the page's sentence whole and the stamp a letter per block because each turned glyph has its own baseline. It is the artifact the overlay row ofdocs/parity.mdis validated against, besideedit-pages, whichspecs/003-pdf-platform-parity/quickstart.mdstep 4 names. - An image read states its pixels, not only its bytes.
PdfImageInfocarriesDecodedPixelsandPixelFormat, so a caller that wants to look at a picture no longer writes a decoder first: the samples arrive tightly packed, top row first, one byte per sample, asGray8orRgb24, and their length is exactly the geometry times the channels. They are present only when nothing is left to interpret — the library decoded the payload itself, the width, height and bit depth are stated, every sample is eight bits, and the colour space is one whose samples are its channels (DeviceGray,CalGray,G,DeviceRGB,CalRGB,RGB) — so a palette image, a sixteen-bit image, aCMYKorICCBasedone, and any payload the reader passed through keepnulland are described byFilter,ColorSpaceandIsDecodableinstead. A JPEG stays a JPEG: its bytes are already a format the caller can read, soPixelFormatisnullfor aDCTDecodeimage rather than a second decoder being shipped for it. - The examples archive a page and then sign it. The
sign-and-archivescenario (examples/SharpCorePDF.Examples/ArchiveExamples.cs) writes one authored page as PDF/A-3b, signs that file with a certificate it makes for the run and never writes down, and reports the integrity and the trust of what it produced as two separate verdicts —integrity Valid, trust Trustedwith the run's own certificate as the anchor — with a timestamp token only whenSCPDF_TIMESTAMP_TOKENsupplies one. The order is the promise: the conversion writes the archival bytes and the signer appends a revision, so the signature covers the whole file (coverageENTIRE_FILE, as pyHanko reports it) while the structures the conversion wrote are still the ones a validator reads. Converting a signed file would be the reverse and is not offered, because it rewrites the bytes the signature covers.
- A drawn text layout landed a box height above its origin.
PdfPageCanvas.DrawTextLayouttook the top ofPdfTextLayout.Boundsoff every baseline, and that top is the height of the box, so a five-line block asked for at 100 points from the top of an A4 page was drawn at 37.5 — 71 points too high, with its first line almost off the page. Each run is now placed at its own start point added to the origin, andPdfDocumentBuilderTests.Draws_the_layout_from_the_top_left_of_its_boxpins the result against the same baselines drawn one at a time withDrawText. - A caller-supplied annotation appearance is written, with the face it names.
PdfAnnotationAppearancewas validated, its bounding box became the/BBoxof the form XObject, its font decided that a/Helvresource was written — and itsContent, the operator stream the record exists for, was dropped: the writer drew its own appearance and a caller who stated one got the engine's drawing instead. The operators are now the appearance, verbatim, wrapped in the same form XObject; and the face written as/Helvis the one the record names rather than the annotation's text style, which is what the record's own documentation promised.PdfDocumentEditorTests.Writes_the_operators_of_a_supplied_annotation_appearancewrites a square with its own stream inCourier-Boldand asserts both.
- The manual is the
1.2.0edition.docs/SharpCorePDF-Manual-1.2.0.pdfis rebuilt from the sources — 25 chapters, 113 pages, bookmarks and contents page numbers measured — and it is the first edition whose chapters describe the surface without dating it: the availability notes on chapters 16 to 19 used to say which part of their subject the1.1.0package did not carry, and they now say what1.2adds instead. The preface and the version note name1.2.0, the installation chapter installs1.2.0, and the1.1.0edition stays beside the new one as the record of what that release shipped.docs/README.mdand the repository README point at the new file. - The manual documents the layers and the document-level edits.
docs/manual/src/14-editing.mdgains the six operation rows, a section on overlays and underlays with every option they take, and a section on the four edits that state a value of the document; it now also states that a value the plan states replaces the one an edit would otherwise drop or report, and that PDF/A-1b refuses a translucent layer. - The manual documents text layout.
docs/manual/src/16-text-layout.mdis new: measuring withPdfTypeface.Measureand the metrics in font units,PdfTextLayouter.Layoutand every option it takes, the wrap rules, alignment and justification, the overflow policies, theDrawTextLayoutcoordinate space, the result records, determinism and what a layout deliberately does not do. Chapter 12 now points at it instead of stating that nothing measures or wraps, the cookbook gains a sub-recipe for a paragraph that comes from a template, and the preface explains why chapter 16 is no longer reserved.docs/SharpCorePDF-Manual-1.1.0.pdfwas rebuilt from the sources — 21 chapters, 92 pages, bookmarks and contents page numbers measured. The chapter states at its head that the1.1.0package does not carry the surface it describes. - The manual documents drawing.
docs/manual/src/15-drawing.mdis new and chapter 15 is no longer reserved: coordinates and the trap inPdfRectangle's edge names, lines, rectangles, ellipses, paths and the two fill rules,PdfPaintandPdfStrokewith the operator each member writes, alpha as an/ExtGState, dashes in points, clipping, transforms (including that a positiveRotateturns content clockwise on a canvas whose y axis points down, and how to compose a group so the rotation does not turn the placement), the saved graphics state, and what the surface deliberately does not do. Chapter 12 points at it for shapes, the preface reserves only chapters 17–19, anddocs/README.mdlists it. - Every
dotnet add packageline names the version that exists. The installation chapter and the README asked for1.1.0's predecessor —--version 1.0.0in three places — and the producer examples said a merged document is stampedSharpCorePDF 1.0.0. Both now say1.1.0, which is what the feed serves and what the domain assembly's version produces.
- PDF/A conformance is what a validator says it is, not what this library says.
PdfAConversionResultreports what a conversion could or could not meet; the verdict comes from running veraPDF against the output (chapter 24,docs/tools/validate-pdfa.py). The archival files this release is evidenced on were validated before the signature was appended, so a validator run on the signed copy is the caller's step — chapter 23 says why the order cannot be the other way round. - A timestamp is only as good as the token the caller supplies. There is no network call:
PdfSignatureOptions.TimestampTokensigns what the caller hands over, and without one the signature states its ownsigningTimeand nothing more. Revocation checking, trust lists and certificate chain building are a verifier's job here, which is whyPdfSignatureTrustreports what the file carries rather than what a PKI would decide. PdfImageInfo.DecodedPixelsis present for the images whose samples need no interpretation. A JPEG is a payload, a palette or sixteen-bit image is described but not expanded, and aCMYKorICCBasedimage keeps its declared colour space — the reader statesFilter,ColorSpaceandIsDecodablefor those rather than guessing at a conversion (chapter 8).- Layout analysis reports what it measured, not a guarantee. Blocks, reading order and the extracted
text are scored against
docs/tools/score-layout.pyand the fixture indocs/measurements.md; a table is a block of text with the rows the heuristics found, not a parsed table. - The remaining gates are the ones the release checklist still calls open. There is no symbol package
yet, so a debugger shows no source for a stack trace, and nothing is code-signed. The 90% new-code floor
of G9 is enforced by
ci.ymland read again bypublish.ymlfor the tagged commit; the measurement for this version's code is indocs/measurements.md. Rendering, format conversion, OCR and conformance validation stay outside the product, as ADR-001 states.
- An existing document can be edited into a new one.
IPdfDocumentEditor, resolved byAddSharpCorePDF(), applies an immutablePdfEditPlanto one source:RotatePageadds a quarter turn to a page,DeletePageremoves it,MovePagechanges its position,DuplicatePagecopies it to another position, andSetMetadataoverrides document information. Page numbers are one-based and are resolved against the state produced by the preceding operations, so a plan reads like a sequence of edits. Each operation is a value; the plan is validated before the source is read, and an operation that reaches past the last page fails withPdfEditPlanValidationExceptionbefore any output is written.PdfEditResultreports the page count, bytes written, output version, elapsed time, the operations applied and the warnings the edit produced. - Pages can be cropped and resized without re-rendering their content.
CropPagesets an explicit/CropBoxinside the media box: a box that reaches outside it is clamped and reported asPdfWarningKind.PageGeometryNormalized, and a box with no overlap is refused before any output is written.ResizePagechanges/MediaBoxand wraps the untouched original content streams in a graphics state (q, acmtransform, thenQ), soPdfPageResizeMode.Scalescales the content to the new size andPdfPageResizeMode.Preservekeeps it at its original size and centres it. A crop box set before a resize is carried through the same transform. - Editing copies pages, it does not re-render them. The engine writes a complete new document and
copies every surviving page's content streams, resources, fonts, images and annotations verbatim;
inherited
MediaBox,CropBox,ResourcesandRotateare materialised onto each page. Bookmarks, named destinations and form widgets that name a source page are redirected to the first surviving occurrence, or to the first page of the result when the named page did not survive. Page labels are dropped when an operation changes the page order or count, and an XMP packet is dropped when document information is overridden; both decisions are reported throughPdfWarningKind.PageLabelsDroppedandPdfWarningKind.XmpPacketDroppedrather than silently producing a document whose metadata contradicts itself. - An edit is reproducible when its inputs are fixed.
PdfEditOutputOptionschooses the output version, metadata overrides, producer, modification date and document identifier. The file identifier is derived from the version, date, page count, identifier and applied operations, so a fixedModificationDateandDocumentIdmake repeated edits of the same source byte-identical. The editing release reads encrypted sources through the existing standard security handler; the result is written in the clear unlessEncryptionis set (see below). - A page written from nothing.
PdfDocumentBuildercreates an in-memory document — one or more pages of anyPdfPageSize— whose pages are drawn throughPdfPageCanvas:DrawTextsets a line in a standard or an embedded font at a baseline, in a top-left coordinate system withydownwards, andDrawImageplaces a PNG at an explicit size or at the size itspHYschunk states.Build()returns aPdfByteArraySource, so the page can lead aMergePlanwithout a temporary file;ToArray()andSaveAsync(Stream)are the other two routes.WithMetadatawrites the document information andPdfAuthoringOptionsfixes the creation date and document identifier, which makes repeated builds byte-identical. - Two levels of font, neither of them a system font.
PdfTypeface.Standardselects one of the twelve standard PDF faces withWinAnsiEncodingand no font file;PdfTypeface.FromTrueTypeembeds a TrueType program the caller supplies, subsetted to the glyphs the pages draw, with a/ToUnicodemap so the text —é,è,à,ç,ô,ïincluded — extracts again. Glyph numbers are preserved, unused outlines are left out, and the program is never looked up in the machine's font folder. A character the face cannot encode is refused with the character and code point in the message, rather than replaced. - PNG images without an imaging dependency. Eight bit, non-interlaced PNGs of colour types 0, 2, 3,
4 and 6 are embedded: an alpha channel becomes an
/SMask, a palette withtRNSis expanded to RGB and a soft mask, a colour key becomes/Mask, and an opaque image keeps its originalIDATbytes with a PNG predictor instead of being recompressed. Interlaced and sixteen bit images are refused with an explanation; JPEG and other formats are out of scope. One image drawn on several pages is one object. - Reading what a document already contains.
IPdfContentReader, resolved byAddSharpCorePDF()and constructible by hand, answers the questions a caller asks before drawing anything:ReadImagesAsyncreturns every image a page range paints — one entry per object, carrying every page that draws it, with the bytes the file holds (aDCTDecodeimage is never re-encoded) and the area it occupies;ReadAnnotationsAsyncreturns comments, highlights, stamps and links with their geometry, their text and their target, resolving direct and named destinations;ReadFormFieldsAsyncreturns the field tree with fully-qualified names, types, values and every widget of each field;ReadMetadataAsyncreturns the XMP packet byte-exact with thedc:,pdf:andxmp:values parsed beside it;ReadEmbeddedFilesAsyncreturns the attachments with a distinct identifier per entry;ReadOutlineAsyncreturns the bookmark tree with its destinations resolved; andReadPageDetailsAsyncyields everything known about one page at a time, so a 200-page index never holds a report for the whole file. - One payload rule for every read. A record carries its bytes inline only within
PdfEngineOptions.MaxInlineContentBytes(new, default 256 KiB,0for metadata only); above itDataisnull,ByteLengthstays truthful andIsBufferedsays so, with no warning because nothing failed.CopyImageDataAsync,CopyImageMaskDataAsyncandCopyEmbeddedFileDataAsyncwrite such a payload to a stream the caller supplies through a pooled 64 KiB buffer, andReadImageDataAsync/SaveImageDataAsyncand their embedded-file twins are the convenience members beside them. AReadOnlyMemory<byte>that escapes a call never aliases a rented buffer. - Reading the text the pages draw.
IPdfContentReader.ReadTextAsyncreturnsPdfTextBlockrecords in the order a reader would take them — down the page, then along the line — rather than in the order the content stream happens to draw them. A block carries itsText,LineCount, page andBoundingBox, and holds thePdfWordrecords it is built from, each with a baseline, a font size and a font name and thePdfGlyphrecords behind it, so a caller that needs a coordinate never parses the page a second time. Words end where the document draws a space and where aTJadjustment is wide enough to stand in for one;Tc,Tw,Tz,TL,Ts,Td,TD,Tm,T*andq/Qare all honoured, so a line inside a scaled or rotated form lands where a viewer draws it. A simple font is read through its/Encoding, its/Differencesand the standard glyph names, a composite font through its/ToUnicodemap, and the width of a code comes from/Widthsor/Wwith the fallbacks the specification states. One page overload,ReadTextAsync(source, page), reads a single page without a range. - Text is read or reported, never guessed. A glyph whose code no font entry maps is skipped and its
font named in one
TextUnreadablewarning per page, so a document whose encoding this library cannot follow reads as fewer words plus a remark instead of a page of plausible nonsense, and a page whose font is unreadable still leaves the pages around it readable. - Eleven new
PdfWarningKindmembers —ContentStreamUnreadable,ImageFilterUnsupported,ImageDataUnreadable,XObjectResourceMissing,NamedDestinationUnresolved,EmbeddedFileUnreadable,EmbeddedFileNameCollision,XmpPacketMalformed,FormFieldUnreadable,AnnotationUnreadableandTextUnreadable— so a filter the library does not decode, a damaged image, a name that does not resolve, a malformed XMP packet or a font it cannot read is reported as data instead of being skipped in silence. - The layout comes out with the text.
IPdfLayoutReader, resolved byAddSharpCorePDF()and constructible by hand, reads a page the way a search or an analytics pipeline needs it:ReadLettersAsyncreturns onePdfLetterper glyph in content order, with its Unicode value, its font size, the font the document declares, its box, its baseline start and end and the direction that baseline runs in;ReadWordsAsyncgroups those letters intoPdfWordrecords that keep the letters they were grouped from, with the order of the page as the default andPdfWordExtractionStrategy.Rawfor a caller that needs the order of the producer; andReadHyperlinksAsyncreturns the clickable area of every link with either the URI it opens or the destination it jumps to.PdfWordExtractionOptionsstates both rules in ems — a quarter em as a space, half an em as one line — and refuses a negative distance or a strategy this release does not implement when it is created, before a document is opened. Whitespace letters can be kept in the word they follow, so the words of a page concatenate back to the text it draws, and a letter whose baseline runs towards the left is read in its own direction with a box that covers the area between its two baseline points. - A page's blocks, and the order a reader takes them in.
IPdfLayoutReader.ReadBlocksAsyncgroups the words of a page intoPdfTextBlockrecords — the text, the words it keeps, the area it covers and its one-based position on the page — by the segmentationPdfLayoutOptionsselects:Raw, which reproduces the producer's drawing order and is the baseline a comparison starts from;NearestNeighbour, which groups the lines of a paragraph;RecursiveXYCut, the default, which splits a region along the widest vertical gutter and then the widest horizontal one, so two columns and a heading are found without knowing what a column is; andDocstrum, which measures the page's own leading instead of assuming it. The reading order is chosen separately, because a page can be segmented correctly and still be listed in the wrong order:ColumnAware, the default, reports the columns of a page left to right and the blocks of a column top to bottom, andRawkeeps the order the segmentation produced. The segmentation and the order travel with the result, so a page that yielded no block is not the same answer as a page that was never segmented;PdfLayoutOptions.IncludeDecorationsdecides whether a block that carries no letter and no digit — a rule drawn as a run of hyphens, a lone bullet — is reported; and a strategy only groups the letters and words a read produced, never changing what one of them says. - The reading of a page written down for another tool.
IPdfLayoutReader.ExportAsyncwrites what a block read produced asPdfLayoutFormat.Alto,PageXml,HocrorSvgText, straight into a stream the caller supplies: ALTO 4 with aPageper page, aTextBlock, aTextLineand aStringper word; PAGE XML with aTextRegionper block, aTextLineper line and the reading order that names them; hOCR as XHTML withocr_page,ocr_carea,ocr_par,ocr_lineandocrx_wordelements, each carrying itsbbox; and SVG with onetextelement per word on the baseline it is drawn on. Coordinates are the page's own numbers with the origin moved to the top left corner and the y axis turned down, which is the convention of every one of the four formats; text is escaped for the document it lands in, and a character XML cannot carry becomes the replacement character instead of aborting the write. A page that draws no text still produces its page element, a range is written in ascending page order, and no document states a timestamp or a random identifier, so the same read produces the same bytes on every run. The call returns the blocks it wrote, with the segmentation and the order that produced them and the warnings the read made, so an export of a page whose font could not be mapped is not silent. A destination that cannot be written and a format this release does not produce are both refused before the document is opened, and the exports describe text only:PdfLayoutOptions.IncludeImagesis reserved for the image regions a later release places beside it. - A document can be signed without rewriting a byte that is already signed.
IPdfDocumentSigner— resolved byAddSharpCorePDF()and constructible by hand like every other engine port — appends one revision to a source and returns aPdfSignatureInforead back from the file it wrote: the field, the filter and sub-filter, the digest, the signer, the byte range and the revision the signature covers. The CMS is built here rather than bySignedCms, because a detached signature over a reserved byte range needs the digest of everything except its own placeholder, the signed attributes CAdES names, and a size known before the value exists: the writer reservesByteRangeandContents, hashes the revision with the gap removed, patches the range, and only then writes the signature into it. The certificate's key decides the algorithm — RSA PKCS#1 v1.5 or ECDSA — and the options decide the digest. A certificate with no private key, an encrypted document and a revision too small for the chain are refused before anything is written, so a half-signed file cannot exist; a visible appearance writes a form XObject, a widget and an/Annotsentry in that same revision, and/AcroFormis added when the document had none. - A signature can carry a timestamp the caller obtained.
PdfSignatureOptions.TimestampProvideris the whole of it: the library hands the caller'sIPdfTimestampProviderthe digest of the signature value and embeds the RFC 3161 token it answers with as thesignatureTimeStampTokenunsigned attribute. Nothing here opens a socket, and a provider that throws stops the signature instead of leaving a document that only looks signed. The token travels unverified, and says so: a token that cannot be read is aTimestampInvalidproblem rather than a claim about its time, andPdfTrustOptions.CheckTimestampTrustis reserved for the release that checks the authority's own signature. - Every signature in a document is checked, and the two questions are answered apart.
IPdfDocumentSignatureValidatorfinds each signature field in the form tree and on the page widgets, reads its byte range and its CMS, and reports onePdfSignatureValidationper signature:Integrity— the digest the CMS carries matches the bytes the range covers — and, only when the caller names anchors throughPdfTrustOptions,Trust— the chain leads to one of them. They are separate results because they are separate failures: a document changed after it was signed is not the same defect as a certificate that leads nowhere, andIsValidspeaks only about integrity, which is the half this library can decide on its own. No machine trust store is consulted, no certificate is fetched and no authority is asked whether one was revoked; a placeholder signature, an unreadable structure or a range that does not reach the end of the file isUnknown, neverValid. - A document can be written for archival delivery.
IPdfAWriter, resolved byAddSharpCorePDF()and constructible by hand, writes a complete PDF/A-1b, -2b or -3b document: the version the profile names, an uncompressed UTF-8 XMP packet carryingpdfaid:partandpdfaid:conformancewith the document information mirrored into it, an/N 3output intent built from a generated sRGB colour profile, and/NeedAppearances falseon a form.PdfEditOutputOptions.PdfAProfileroutes an ordinary edit through the same writer. The analysis runs before a byte is written: a font that is not embedded, a CID subset that states no/CIDSetfor 1b, transparency,JPXDecode/LZWDecode, optional content or an attachment the profile forbids, an annotation without an appearance stream, a JavaScript/Launchand similar action, or an external file stream is refused withIsWritten = false, a named requirement and an untouched destination — never written with a label it does not deserve. What can be removed without changing a page is removed and reported asPdfWarningKind.PdfAFeatureRemoved.docs/tools/validate-pdfa.pywraps veraPDF; the release is recorded indocs/parity.mdon veraPDF 1.30.3 reportingPASSfor all three profiles of both an authored page and a real "Microsoft: Print To PDF" document. - An edit can write an encrypted result.
PdfEditOutputOptions.Encryptionapplies the same standard security handler a merge uses — AES-256 by default, AES-128 and RC4 available — and the identifier of the result keys it. The engine refuses a plan that asks for a PDF/A profile and encryption together, before the source is read, and withVerifyOutputon it reads its own encrypted output back with the password it was written with. - Embedded TrueType subsets now state a
/CIDSet. ISO 19005-1 requires the set of CIDs a subset describes, and veraPDF failed a PDF/A-1b conversion without it. The writer emits one that names every CID the preserved numbering makes present, which also satisfies the PDF/A-2 rule for subsets that carry a set.
- Images decode to the samples their filter names, and a mask can be fetched. A
/Predictorimage is now inflated and undone one row at a time instead of into a single buffer, so an image whose stored form is small but whose samples are large is counted rather than materialised and the copy route writes rows as it produces them. An inline image's data ends where its header says it does — the separator afterIDis consumed once andEIis found even when a delimiter follows it — so a sample stream containing a space followed byEIis no longer cut short, and an image with noEIat all keeps what it read and raisesImageDataUnreadableinstead of ending in silence, and a page's streams are separated only between streams, so an unterminated image ends at the last byte the page holds. An inline dictionary whose value is a name (/CS /G) keeps its colour space instead of losing it and misreading the entry after it. Three smaller corrections ride with them:CopyImageMaskDataAsyncfetches a soft or explicit mask by the(ImageId, Kind)its record carries and refuses a colour key mask, which has no payload, by name;PdfImageInfo.IsInlinetells an inline image from an object; and every image warning names the source and the page, plus the image wherever the failure belongs to one, so a 200-page report can be triaged. - The examples no longer contain a password. The encryption example in
examples/wrote a literal user password and a literal owner password into the document it demonstrates — the kind of constant a static analysis scanner reports as a hardcoded credential, and the kind a reader copies into their own code. Both are now made per run, the user password is printed so the document can still be opened, and the example opens it with the value it just wrote. No package changes with this; the finding is what turned the project's security rating red.
- What 1.1 delivers, and what it defers to 1.2. The capability described in this entry is what
1.1.0ships: page creation with standard and embedded fonts, PNG images, the path writer, every page operation of the editor (rotate, delete, move, duplicate, reorder, crop, resize, extract, split, import, insert, append and metadata overrides), reading with layout analysis and the four text exporters, digital signatures and PDF/A-1b, -2b and -3b output. Text measurement and word wrap, JPEG/BMP/GIF, overlays, annotation and form authoring, and incremental updates are not in1.1.0. They stay in the parity matrix as1.2rows with a named task rather than disappearing, so a caller can see the gap instead of discovering it:docs/parity.mdnames the release of every row, and the specification carries the dated scope amendment behind that split. - The product boundary now includes creation and editing. Constitution 2.0.0 and ADR-001 define SharpCorePDF as a dependency-free PDF creation, editing and reading library. The 1.1 target is capability parity with PDFsharp 6.5 for creation and processing and PdfPig 0.1.x for reading and layout analysis; rendering, conversion, OCR, conformance validation, services and CLIs stay outside the product. This release implements page-assembly editing only — page content, annotations and forms are still copied, not rewritten.
- A release waits for the quality gate, on the commit being released.
publish.ymlreads the SonarCloud verdict in asonar-gatejob that runs before the build, before the pack and before the OIDC exchange, and stops when the gate is red, when the snapshot it judged carried no coverage report, or when the newest analysis ofmainis of a different commit than the tag. It changes when a version appears on the feed, not what is in it; the rule it adds is that a tag goes on the commit whose analysis is the newest onmain.
One shortfall is stated rather than left for a reader to find: 77.76% of the executable lines this version
adds are covered by the suite, against the 90% new-code floor in docs/release-checklist.md G9 —
ADR-002 records the measurement, the eight parser and validator
files the uncovered lines sit in, and the decision to ship with the number stated rather than to move it with
tests that only execute lines. The floor itself is unchanged and is re-measured on 1.2's new code. What the
suite does instead is evidence behaviour: 748 tests on both legs, 21 parity rows each naming a test and an
external command, and pyHanko, qpdf, veraPDF 1.30.3, pypdf and PyMuPDF run against the output.
Two things are as they were at 1.0.0: there is no symbol package yet, so a debugger shows no source for a stack trace, and nothing is code-signed. Both remain gates in the release checklist rather than intentions, and neither changes what the packages contain.
SharpCorePDF, the package to install. The four layers stay separate assemblies with separate API snapshots, but a consumer no longer has to work out which of them to reference:SharpCorePDFis a meta-package whose only content is a dependency onSharpCorePDF.Domain,.Application,.Infrastructureand.DependencyInjection, anddotnet add package SharpCorePDFis the whole install. The four remain installable on their own.NOTICE, stating in one page who holds the copyright, what third-party material exists (no third-party source code;Microsoft.Extensions.*inSharpCorePDF.DependencyInjectiononly; test fixtures generated bydocs/tools/make-fixtures.py) and what the name is not.SECURITY.md,CODE_OF_CONDUCT.mdandCONTRIBUTING.mdat the repository root: a private reporting route with a 7-day acknowledgement, Contributor Covenant 2.1 with two enforcement channels, and the build, test and review bar a change has to clear..github/: issue forms for a defect and a feature request, a pull request template, and a Dependabot configuration for the NuGet packages and the workflow actions.SonarAnalyzer.CSharpin the build (src/Directory.Build.props, dev-time only). The rules SonarCloud applies now run indotnet build, at the line that causes them. The three rules this repository answers with a decision rather than a change (S101,S1694,S4790) are silenced per file insrc/.editorconfigwith the reason, and their lines carry// NOSONAR.- The public surface is an approved snapshot (
Microsoft.CodeAnalysis.PublicApiAnalyzers, dev-time only). Each project declares what it exports inPublicAPI.Shipped.txtand queues additions inPublicAPI.Unshipped.txt, and RS0016/RS0017 are build errors: a public member the snapshot does not list, or an entry that outlives its member, stops the build rather than the next release.
publish.ymlchecks the entry point differently from the layers. Every package still has to carrylib/net10.0andlib/net11.0and the README nuget.org renders;SharpCorePDFcarries no assemblies by design, so it is held to the opposite rule instead: it must depend on all four layers.- The trusted publishing policy needs two subjects, not one.
SharpCorePDF.*matches the four layers and does not matchSharpCorePDFitself, which has no dot after the product name; a policy holding only the glob pushes four packages and refuses the fifth. ci.ymlenforces the SonarCloud quality gate instead of reporting it. The scan passessonar.qualitygate.waittobegin— theendphase rejects every property but the token — so a failing gate fails the run, and the gate's coverage condition reads the report this run produced:dotnet-coverage collectmeasures the net11.0 leg, and the Visual Studio XML it yields is named exactly (artifacts/coverage/net11.0.xml) instead of by a glob. Coverage is measured on every run rather than only when a SonarCloud token exists, and every action is pinned to a commit hash rather than a tag.- Codecov is gone, and with it the second coverage report. One service measuring the number is the
arrangement that cannot disagree with itself:
codecov.ymlasked for 90% on the project and on the change, while SonarCloud was already importing the same measurement, so the gate holds the number and the cobertura conversion that only Codecov read is no longer produced. The 90% is a condition to enter on the quality gate on sonarcloud.io — above Sonar's 80% default on new code, and the only place it can be enforced once the file that declared it is gone — andREADME.md,CONTRIBUTING.mdanddocs/release-checklist.mdsay so instead of pointing at a file in the tree. - The SonarCloud findings are fixed in the source, not silenced: loops that read better as
Where/Select/Distinct, a nested ternary in the AES object-key derivation, two helpers moved into the walker that uses them, andPdfReferencenow implementsIEquatable<PdfReference>, so theEqualsoverload it already carried is the one a generic comparer calls.
Two things are known and deliberately not promised: there is no symbol package yet, so a debugger shows no source for a stack trace, and nothing is code-signed. Both are gates in the release checklist rather than intentions. Neither affects what the packages contain or what the assemblies do.
The shipped leg is two, not one: lib/net10.0 and lib/net11.0 carry the same API, as preview.1
already advertised, because a 1.0.0 that narrowed to a single framework would withdraw a target the feed
has promised since 2026-10-15 (docs/release-checklist.md G2). The public surface is the one frozen by
PublicAPI.Shipped.txt — additions after this version go through PublicAPI.Unshipped.txt and a minor
version, not through a silent change to a shipped signature.
Never pushed. Both names exist only as values the repository's <Version> passed through, so no feed ever
served them, nothing was downloaded from them, and skipping them broke no promise to anyone.
The first and — until 1.0.0 — only published version: SharpCorePDF.Domain, .Application,
.Infrastructure and .DependencyInjection at 1.0.0-preview.1, from tag v1.0.0-preview.1. No entry
was written here for it at the time, and this heading does not invent one after the fact; it records that
the four ids are live and immutable, which is why their next version is 1.0.0 and not a re-cut of
preview.1.