GM.Documents
1.0.0
dotnet add package GM.Documents --version 1.0.0
NuGet\Install-Package GM.Documents -Version 1.0.0
<PackageReference Include="GM.Documents" Version="1.0.0" />
<PackageVersion Include="GM.Documents" Version="1.0.0" />
<PackageReference Include="GM.Documents" />
paket add GM.Documents --version 1.0.0
#r "nuget: GM.Documents, 1.0.0"
#:package GM.Documents@1.0.0
#addin nuget:?package=GM.Documents&version=1.0.0
#tool nuget:?package=GM.Documents&version=1.0.0
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 (TDefinitionis 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 aDocumentResult, because extraction yields data, not a file). Implemented for PDF (PdfPig) and Word (OpenXML); resolveIEnumerable<IDocumentTextExtractor>and pick byCanExtract(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 likeGM.FileStorage.FileUploadRequest, so values flow straight through.DocumentResult— the output: a rewound, ready-to-read stream +DocumentMetadata. Owns its stream (await using).DocumentMetadata—FileName,ContentType,SizeInBytes, nullablePageCount(null when pagination doesn't apply, rather than a misleading1), and aPropertiesbag for type-specific extras (imagewidth/height,sheetCount, …).DocFormat— MIME type + canonical extension, compared by value, with a well-known set (DocFormat.Jpeg,.Pdf,.Docx, …) andFromContentType/FromFileNameresolvers.
Errors derive from GM.Exceptions.CustomException (DocumentException →
UnsupportedDocumentFormatException, 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
MaxOutputSizeInBytessteps 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.
- Inspect —
InspectAsyncreads just the header for dimensions/format and flagsHasGpsMetadatawithout 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:
- Generate — QuestPDF, 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.)
- Extract — PdfPig (Apache-2.0) for pulling text/words/coordinates out of existing PDFs.
Separate interface (
IDocumentTextExtractoror a Pdf-specific service), because extraction returns text/data, not aDocumentResult.
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
MaxInputSizeInBytesguard andInspectAsync(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 | Versions 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. |
-
net10.0
- GM.Exceptions (>= 1.2.0)
- Microsoft.Extensions.Options (>= 10.0.0)
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 |