Library (API)
Reference DynDyn.Pdf.Core and call the converter in-process — no external service, no CLI
subprocess, no network. The same engine backs the CLI and the Web service,
so output is identical regardless of which surface you use.
Install / reference
Target framework: net10.0.
| Package | Contains | Native dependency |
|---|---|---|
DynDyn.Pdf.Core |
The engine, options, output writers, batch converter | None |
DynDyn.Pdf.Engine |
The default fully-managed extraction engine (ManagedExtractionEngine), AddDynDynPdf() DI extension |
None |
DynDyn.Pdf.Interop |
Managed P/Invoke surface over pdfium: safe handles, runtime initialization | Pulls in the native binaries transitively, but the managed shim itself never throws if they're absent |
DynDyn.Pdf.Native |
The pdfium-based native extraction engine, the raster-fallback rasterizer, and the ground-truth renderer | Yes — reference this only when you actually need raster output |
dotnet add package DynDyn.Pdf.Core
dotnet add package DynDyn.Pdf.Engine # fully-managed engine + DI helper
The interface
namespace DynDyn.Pdf.Core;
/// Converts a PDF document to pixel-faithful HTML. Stateless and safe for concurrent use.
public interface IPdfToHtmlConverter
{
Task<ConversionResult> ConvertAsync(
string inputPdfPath,
IOutputWriter output,
ConversionOptions options,
IProgress<ConversionProgress>? progress = null,
CancellationToken cancellationToken = default);
}
Two implementations:
| Implementation | Engine | Native dependency |
|---|---|---|
DocumentConverter (recommended) |
Pluggable IExtractionEngine (pass new ManagedExtractionEngine()) |
Only if you request a raster background |
PdfToHtmlConverter |
native | Always |
Quick start (fully-managed, no native dependency)
using DynDyn.Pdf.Core;
using DynDyn.Pdf.Core.Engine;
using DynDyn.Pdf.Core.Output;
using DynDyn.Pdf.Engine;
IPdfToHtmlConverter converter = new DocumentConverter(
new ManagedExtractionEngine(),
new NativeRasterizerFactory(), // supplies raster fallback if/when a page needs it; harmless otherwise
loggerFactory: null); // ILoggerFactory optional
var result = await converter.ConvertAsync(
"document.pdf",
new SingleFileOutputWriter(), // packaging: see the table below
new ConversionOptions { Dpi = 150, Pages = PageRange.Parse("1-20") });
Console.WriteLine($"{result.PageCount} pages → {result.OutputFiles[0]} " +
$"({result.Warnings.Count} warnings, {result.Elapsed.TotalSeconds:0.0}s)");
The native variant is new PdfToHtmlConverter(loggerFactory) with the same ConvertAsync call —
use it only when you specifically want native extraction (requires DynDyn.Pdf.Interop and
usually DynDyn.Pdf.Native).
Output writers (IOutputWriter)
| Writer | Produces |
|---|---|
SingleFileOutputWriter |
One self-contained .html (fonts/images/SVG inlined as data URIs) |
MultiPageOutputWriter |
A folder: per-page page-NNNN.html + shared assets/ (fonts, CSS) + index.html |
ViewerOutputWriter |
One self-contained HTML app: bookmarks/TOC sidebar, page nav, zoom |
result.OutputFiles lists every file written (in page order for the multi-page writer).
Dependency injection
using DynDyn.Pdf.Engine;
services.AddDynDynPdf(); // IPdfToHtmlConverter, stateless singleton, fully-managed engine
AddDynDynPdf() lives in DynDyn.Pdf.Engine and registers the fully-managed
ManagedExtractionEngine. With nothing else registered, conversions run with zero native
dependencies and every page keeps its vector background.
Raster fallback is opt-in: register an IPageRasterizerFactory as well, and the converter picks it
up automatically.
using DynDyn.Pdf.Engine;
using DynDyn.Pdf.Native; // optional package
services.AddDynDynPdfNativeRasterizer(); // pdfium-backed raster fallback
services.AddDynDynPdf();
Order does not matter — AddDynDynPdf() resolves the factory from the provider at construction
time, so a deployment that simply omits the native package (or publishes with
-p:NativeAssets=false) degrades to the vector background instead of failing.
IPdfToHtmlConverter is stateless and safe for concurrent use — a single instance can serve
parallel conversions.
Progress & cancellation
ConvertAsync reports ConversionProgress between pipeline steps and honors a CancellationToken:
var progress = new Progress<ConversionProgress>(p =>
Console.Write($"\r{p.Stage}: {p.PagesExtracted}/{p.TotalPages} extracted, " +
$"{p.PagesComposed}/{p.TotalPages} composed ({p.Fraction:P0})"));
await converter.ConvertAsync("doc.pdf", writer, options, progress, cts.Token);
ConversionProgress fields: TotalPages, PagesExtracted, PagesComposed, Stage, and
Fraction (phase-weighted [0,1], advances monotonically across extract/fonts/compose/write).
Result types
public sealed record ConversionResult(
int PageCount,
IReadOnlyList<string> OutputFiles,
IReadOnlyList<ConversionWarning> Warnings,
TimeSpan Elapsed);
public readonly record struct ConversionWarning(int? PageIndex, string Message, WarningCode Code = WarningCode.Unspecified)
{
public WarningSeverity Severity => Code.Severity(); // derived from Code, never set independently
}
Warnings are non-fatal problems (e.g. a font that failed validation and was embedded raw). They are logged and returned — never embedded in the output HTML.
Message stays human-readable free text and may change wording between releases — do not
pattern-match it. Code is the stable classifier for programmatic consumers; Severity is derived
from Code:
| Severity | Meaning |
|---|---|
Info |
Nothing was lost — an intended default or deliberate substitution |
Degraded |
Content rendered, but approximated |
Loss |
Content was dropped and is not represented anywhere in the output. An unclassified (Unspecified) code also maps to Loss, fail-safe |
Batch conversion (BatchConverter)
Convert many PDFs concurrently with per-document failure isolation:
using DynDyn.Pdf.Core;
var batch = new BatchConverter(
converterFactory: () => new DocumentConverter(new ManagedExtractionEngine(),
new NativeRasterizerFactory(), loggerFactory),
loggerFactory);
BatchResult result = await batch.ConvertAsync(
inputPaths: ["a.pdf", "b.pdf", "c.pdf"],
outputWriterFactory: _ => new SingleFileOutputWriter(), // a FRESH writer per document
options: new ConversionOptions { Dpi = 120 },
batchOptions: new BatchOptions { MaxConcurrentDocuments = 8 });
Console.WriteLine($"{result.Succeeded}/{result.TotalDocuments} ok, " +
$"{result.TotalPages} pages in {result.Elapsed.TotalSeconds:0.0}s");
foreach (DocumentConversionResult doc in result.Documents) // INPUT order (deterministic)
{
Console.WriteLine(doc.Success
? $"ok {doc.InputPath}: {doc.Result!.PageCount} pages"
: $"FAIL {doc.InputPath}: {doc.Error}");
}
Contract:
converterFactorymust return a NEW converter per call — each document gets its own engine/reader/font registry, so documents share no mutable state.outputWriterFactoryis called once per input — return a fresh writer each time.- One document faulting never sinks the batch: its slot is recorded as a failure and the rest continue. Only cancellation aborts the whole run.
Documentsis in input order, not completion order, so aggregates are deterministic.
BatchOptions:
| Property | Default | Meaning |
|---|---|---|
MaxConcurrentDocuments |
max(1, cores / 2) |
How many PDFs convert at once |
PerDocumentParallelism |
null → max(1, cores / MaxConcurrentDocuments) |
Overrides each document's internal parallelism so jobs × per-doc never oversubscribes the CPU |
BatchResult exposes TotalDocuments, Succeeded, Failed, TotalPages, TotalWarnings,
OutputFiles (across all succeeded documents, input order), and Elapsed.
Multi-document parallelism is the ~1.35x throughput lever for the managed engine and depends on Server GC being enabled.
ConversionOptions reference
Immutable record; construct with an object initializer or with. Full table (with the CLI flag
and REST field that set each) is in configuration.md. Summary:
| Property | Type | Default | Notes |
|---|---|---|---|
Dpi |
int |
120 |
CSS pixel scale = Dpi / 72. Clamp 18–600 in front-ends |
OutputDirectory |
string? |
input's dir | Where output is written |
Pages |
PageRange |
PageRange.All |
PageRange.Parse("1-5,8,10-") |
LineGrouping |
LineGroupingMode |
Always |
Always / Auto / Never |
LineGroupingOptions |
LineGroupingOptions |
tuned defaults | Vertical/horizontal/font-size join tolerances |
Background |
BackgroundMode |
Svg |
Svg / Raster / None |
RasterFallback |
RasterFallback |
Auto |
Never / Auto / Always; no-op without a rasterizer |
SemanticStructure |
bool |
false |
Tagged-PDF <p>/headings/lists/tables via display:contents |
FontEmbedding |
FontEmbedding |
Woff1 |
Woff1 / RawTtf (debug) |
MaxImageDimension |
int |
4096 |
Cap for resampled background images (device px/axis) |
JpegQuality |
int |
85 |
Opaque background image quality (1–100) |
MaxDegreeOfParallelism |
int |
ProcessorCount |
Managed post-processing threads |
Deterministic |
bool |
false |
Byte-reproducible output (fixed font timestamps/ordering) |
Password |
string? |
none | User password for an encrypted PDF (managed engine only) |
Scale |
double |
derived | Read-only: Dpi / 72.0 |
ConversionLimits-derived fields (MaxPages, MaxPagePixels, MaxEstimatedModelBytes,
ConversionTimeout) are the DoS-boundary caps applied to untrusted input — they exist on every
entry point that accepts an upload (see the WebAssembly build's explicit wiring for a
worked example) and are documented in full in configuration.md and
security.md.
Related: the same engine is exposed as an HTTP job API for out-of-process callers,
as a CLI for scripting, and compiled to WebAssembly for in-browser use
with no upload at all. See managed-vs-native.md for when to reach for
DynDyn.Pdf.Native instead of the pure-managed path shown above.