GM.Documents 1.0.0

dotnet add package GM.Documents --version 1.0.0
                    
NuGet\Install-Package GM.Documents -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="GM.Documents" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GM.Documents" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="GM.Documents" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GM.Documents --version 1.0.0
                    
#r "nuget: GM.Documents, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package GM.Documents@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GM.Documents&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=GM.Documents&version=1.0.0
                    
Install as a Cake Tool

GM.Documents

Document processing for the GM.* ecosystem — split by document type, provider-agnostic, and built to sit between upload and persistence:

raw upload ──▶ GM.Documents (validate / normalize / convert) ──▶ GM.FileStorage (persist)

The core defines library-neutral contracts; each document type ships in its own package so you only pull in what you need. No underlying library (OpenXML, ClosedXML, QuestPDF, ImageSharp) ever appears in the surface you code against — you see streams, DocumentSource, DocumentResult, and plain data models.

Package Purpose Backing library Status
GM.Documents Shared abstractions & DI ✅ built
GM.Documents.Images Resize / compress / convert + EXIF-GPS stripping SixLabors.ImageSharp (see below) ✅ built
GM.Documents.Pdf Generate PDFs + extract text QuestPDF + PdfPig (see below) ✅ built
GM.Documents.Word Generate / read .docx OpenXML SDK ✅ built
GM.Documents.Excel Read / write .xlsx ClosedXML (see below) ✅ built

Everything is async and stream-based end to end, so large files never have to be buffered whole in memory except where a format inherently requires it (see Memory).


Core concepts

Three capability contracts, split by direction of data flow:

  • IDocumentConverter — transforms an existing document. Stream in, stream out. CanConvert(from, to) lets a mixed pipeline pick the right one.
  • IDocumentGenerator<TDefinition> — produces a new document from a plain data model (TDefinition is a POCO defined by each type package — PdfDocumentDefinition, WordDocumentDefinition, SpreadsheetDefinition — never a library type). Being generic, you inject exactly the generator you need and nothing else.
  • IDocumentTextExtractor — the read side: pulls text + page structure out of an existing document (DocumentTextResult, not a DocumentResult, because extraction yields data, not a file). Implemented for PDF (PdfPig) and Word (OpenXML); resolve IEnumerable<IDocumentTextExtractor> and pick by CanExtract(format).

Shared content model. Generating a PDF and a Word document shouldn't mean describing the same letter twice, so the PDF and Word generators both consume a library-neutral DocumentContent block model (headings, paragraphs, tables, images, page breaks) built with a small fluent builder:

var content = DocumentContent.Create("KYC Verification", author: "GM")
    .Heading("Identity Verification Report")
    .Paragraph($"Verified on {DateOnly.FromDateTime(DateTime.UtcNow)}.")
    .Table(["Field", "Value"], [["Name", name], ["Status", "Approved"]])
    .Build();

var pdf  = await pdfGen.GenerateAsync(new PdfDocumentDefinition  { Content = content });
var docx = await wordGen.GenerateAsync(new WordDocumentDefinition { Content = content });

Over a shared vocabulary:

  • DocumentSource — the input: a readable stream + file name + DocFormat. Shaped like an upload, and like GM.FileStorage.FileUploadRequest, so values flow straight through.
  • DocumentResult — the output: a rewound, ready-to-read stream + DocumentMetadata. Owns its stream (await using).
  • DocumentMetadataFileName, ContentType, SizeInBytes, nullable PageCount (null when pagination doesn't apply, rather than a misleading 1), and a Properties bag for type-specific extras (image width/height, sheetCount, …).
  • DocFormat — MIME type + canonical extension, compared by value, with a well-known set (DocFormat.Jpeg, .Pdf, .Docx, …) and FromContentType / FromFileName resolvers.

Errors derive from GM.Exceptions.CustomException (DocumentExceptionUnsupportedDocumentFormatException, DocumentProcessingException), so the GM.API middleware maps them to problem-details responses like the rest of the ecosystem. Underlying-library exceptions are wrapped, not surfaced.

Registration

AddGMDocuments() registers the shared services and returns a builder each type package extends, so you compose only what you install:

services.AddGMDocuments(o => o.MaxInputSizeInBytes = 10 * 1024 * 1024) // shared upload guard
        .AddImages(o =>                                               // from GM.Documents.Images
        {
            o.MaxWidth = 2000;
            o.MaxHeight = 2000;
            o.TargetFormat = DocFormat.Jpeg;
            o.StripMetadata = true;   // EXIF + GPS gone (default)
        });
// .AddWord() / .AddExcel() / .AddPdf() layer in later, same call.

Each package also ships a standalone form (services.AddGMDocumentImages(...)) if you'd rather not use the umbrella call. You never register a document type you didn't reference.

GM.Documents.Images

The normalize step for photo uploads — KYC identity documents and liveness frames especially. One decode/re-encode pass does orient → resize → flatten → strip metadata → encode:

public class KycUploadService(IImageProcessor images, IFileStorageService storage)
{
    public async Task<string> StoreDocumentPhotoAsync(Stream upload, string fileName, string contentType, CancellationToken ct)
    {
        var source = DocumentSource.From(upload, fileName, contentType);

        // Resize to fit, compress, strip EXIF/GPS — using the DI-configured defaults.
        await using var normalized = await images.NormalizeAsync(source, ct);

        // DocumentResult lines up with FileUploadRequest — hand it straight to GM.FileStorage.
        var stored = await storage.UploadAsync(
            new FileUploadRequest(normalized.Content, normalized.FileName, normalized.ContentType),
            ct);

        return stored.Key;
    }
}

GM.Documents never references GM.FileStorage — the two just share a stream-shaped boundary, so this hand-off compiles without coupling the packages.

What it does

  • Resize to a max box — ResizeMode.Fit (default, aspect-preserving, won't upscale), Crop, Pad, Stretch.
  • Compress — quality-based for JPEG/WebP; optional MaxOutputSizeInBytes steps quality down to hit a byte ceiling (handy for upload caps).
  • Convert — encode targets JPEG / PNG / WebP (decodes far more).
  • Strip metadata — removes EXIF/IPTC/XMP, i.e. GPS location, device model, timestamps. On by default; orientation is baked into the pixels first so nothing ends up sideways.
  • InspectInspectAsync reads just the header for dimensions/format and flags HasGpsMetadata without a full decode, for up-front validation.

⚠️ Flag — ImageSharp licensing

SixLabors.ImageSharp (v2.0.0+, so including the 3.1.x we use) is under the Six Labors Split License, not Apache-2.0 — Apache coverage ended at v1.0.x. It's free for open-source projects and for organisations under Six Labors' annual-revenue threshold; above that threshold a commercial license is required. This is a deliberate pick for its pure-managed, cross-platform decode/encode (no native dependencies). If the license doesn't fit, the alternatives are Magick.NET (ImageMagick, larger native footprint) or SkiaSharp (native Skia) — both swappable behind IImageProcessor without touching consumers. Confirm the license tier before shipping commercially.

🚩 Flag — KYC-specific normalization belongs in GM.KYC, not here

You asked whether liveness/document checks need more than generic resize/compress. They do — but that extra work is domain logic, not document processing, and should live in GM.KYC:

  • Face-region detection / cropping, glare & blur quality scoring, liveness/spoof signals, MRZ / OCR extraction — these need ML models or domain heuristics and produce decisions (accept/reject/re-capture), not just a transformed file.

Keeping them out of GM.Documents.Images keeps this package small, dependency-light, and reusable for any image (avatars, product photos, receipts). GM.KYC should depend on GM.Documents.Images for the generic transform and add its domain layer on top — the building blocks are here: InspectAsync (dimensions/format/metadata up front) and ProcessAsync (deterministic normalize). If a shared quality-scoring primitive proves useful across domains later, it can graduate into a GM.Documents.Images add-on — but it starts in GM.KYC.


Library & licensing decisions

The remaining type packages are built; the deliberate library/licensing choices behind them:

GM.Documents.Excel — ClosedXML, not EPPlus

Both wrap the same OpenXML format; the difference is licensing, and it's significant past a usage tier:

  • EPPlus is Polyform Noncommercial from v5 onward — commercial use requires a paid per-developer license. Powerful, but a licensing (and cost) commitment for a library used across many GM.* services.
  • ClosedXML is MIT — free for commercial use, no tier — with an ergonomic API that covers the reporting/export needs here (styled cells, formulas, multiple sheets, streaming large exports via IXLWorksheet).

Decision: ClosedXML, for MIT licensing consistent with the rest of GM.*. If a specific service later needs EPPlus-only performance characteristics, it can be introduced as a GM.Documents.Excel.Epplus provider behind the same interface — an explicit, isolated opt-in rather than the default everyone inherits.

GM.Documents.Pdf — QuestPDF to generate, PdfPig to extract

Generation and extraction are genuinely different concerns and want different libraries:

  • GenerateQuestPDF, a fluent, code-first layout engine ideal for invoices, KYC reports, subscription receipts. License flag: QuestPDF is under the QuestPDF Community MIT License — free for open-source and for companies under $1M USD annual revenue; above that a paid license applies. (Same shape of consideration as ImageSharp — flagged, not assumed.)
  • ExtractPdfPig (Apache-2.0) for pulling text/words/coordinates out of existing PDFs. Separate interface (IDocumentTextExtractor or a Pdf-specific service), because extraction returns text/data, not a DocumentResult.

An alternative all-MIT generator is iText's AGPL (viral — avoid) or the lower-level PdfSharp (MIT, but you build layout by hand). QuestPDF's ergonomics win for the document types here; the revenue flag is the trade-off.


A note on memory

I/O is streamed throughout — sources are read sequentially and results are streamed to storage. Two honest caveats:

  • Images must hold the decoded bitmap in memory while processing (inherent to any image library) — a 4000×3000 photo is ~48 MB decoded regardless of the compressed file size. The MaxInputSizeInBytes guard and InspectAsync (header-only) let you reject oversized uploads before committing to a decode.
  • Word/Excel/Pdf render into a memory buffer (their libraries build the document tree in memory), then stream the result out. For very large Excel exports, ClosedXML's InsertData / batch APIs keep the working set bounded — the generator writes rows sequentially rather than materialising a second copy.

Conventions

Built to the shared GM.* conventions: net10.0, single lockstep <Version> in Directory.Build.props, Conventional-Commits → Versionize releases, NuGet Trusted Publishing (OIDC), README + icon packed into every nupkg. Runnable usage lives in GM.Documents.Samples.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (4)

Showing the top 4 NuGet packages that depend on GM.Documents:

Package Downloads
GM.Documents.Images

Image processing for GM.Documents: resize, compress, and format-convert (JPEG/PNG/WebP) raster images, plus EXIF/GPS metadata stripping — the normalize step between a raw photo upload and persistence in GM.FileStorage. Built on SixLabors.ImageSharp for cross-platform decode/encode, but that never leaks: consumers see only the stream-based IImageProcessor and GM.Documents' DocumentSource / DocumentResult. Purpose-built for KYC document and liveness-frame uploads (strip location/camera EXIF by default, cap dimensions and file size). Register with AddGMDocuments().AddImages() or AddGMDocumentImages(). NOTE: ImageSharp is licensed under the Six Labors Split License — free for open-source and for organisations under its revenue threshold, commercial licence required above it; see the GM.Documents README.

GM.Documents.Excel

Excel (.xlsx) read/write for GM.Documents — reports and exports across GM.* services. Writes spreadsheets from a library-neutral SpreadsheetDefinition (sheets, typed cells, headers) and reads existing workbooks back into plain rows, behind IDocumentGenerator<SpreadsheetDefinition> and IExcelReader. Built on ClosedXML (MIT), chosen deliberately over EPPlus — whose v5+ Polyform Noncommercial licence requires a paid commercial licence past a usage tier. ClosedXML types don't leak to consumers. Register with AddGMDocuments().AddExcel().

GM.Documents.Pdf

PDF generation and text extraction for GM.Documents. Generates PDFs (invoices, KYC reports, subscription receipts) from GM.Documents' library-neutral DocumentContent block model via QuestPDF, and extracts text/data from existing PDFs via PdfPig — two deliberately separate concerns behind IDocumentGenerator<PdfDocumentDefinition> and IDocumentTextExtractor. Neither QuestPDF nor PdfPig types leak to consumers. Register with AddGMDocuments().AddPdf(). NOTE: QuestPDF is under the QuestPDF Community MIT License — free for open-source and for companies under $1M USD annual revenue; a paid licence applies above it. See the GM.Documents README.

GM.Documents.Word

Word (.docx) generation and text extraction for GM.Documents. Generates documents (contracts, KYC verification letters, formatted correspondence) from GM.Documents' library-neutral DocumentContent block model via the Open XML SDK, and extracts text from existing .docx files — behind IDocumentGenerator<WordDocumentDefinition> and IDocumentTextExtractor. Open XML types never leak to consumers. Register with AddGMDocuments().AddWord(). Open XML SDK is MIT-licensed.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 129 8/7/2026