← All docs

API Reference

Two ways to drive DynDyn.Pdf programmatically:


Library API

Install / reference

Reference the projects (or their packages once published):

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.

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 (it interpolates font names, page counts, exception messages) and may change wording between releases — do not pattern-match it. Code (a WarningCode enum member, e.g. PageExtractionFailed, FontEmbeddingRestricted, ShadingSkipped, FontCharacterCodeUnmapped) is the stable classifier for programmatic consumers; Severity is derived from Code and is one of:

Severity Meaning
Info Nothing was lost — an intended default or deliberate substitution (unknown fsType assumed embeddable, a licence-restricted font not embedded on purpose).
Degraded Content rendered, but approximated (a Type 3 glyph drawn with a substitute font, a mesh shading truncated to its triangle budget, structure nesting cut off while the text itself is kept).
Loss Content was dropped and is not represented anywhere in the output (a page that failed to extract or compose, pages truncated past MaxPages, links or annotations dropped entirely). 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:

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 that sets 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)
Scale double derived Read-only: Dpi / 72.0

REST API

The DynDyn.Pdf.Web service exposes a small HTTP API plus a bundled upload page at /. Content type is JSON unless noted. See deployment.md for hosting, and server.md for a shorter delivery-form overview of the same service.

Job lifecycle

POST   /api/jobs (multipart: file + options)      ──▶  { "jobId": "…", "token": "…" }
        │
        ├── GET    /api/jobs/{id}/events?token=…   (SSE)  ──▶  progress… → done | error
        ├── DELETE /api/jobs/{id}?token=…          cancel an in-flight/queued job
        ├── GET    /api/jobs/{id}/download?token=… the HTML (or a .zip for per-page output)
        └── GET    /api/jobs/{id}/preview?token=…  inline HTML preview (single/viewer only)

A job is created, run on a background task, and its progress/terminal state is streamed over SSE. Results live on the instance's local disk keyed by jobId (in-process registry — no persistence across restarts, no cross-instance sharing; see deployment Scaling).

Job ownership tokens

POST /api/jobs hands back a token alongside the jobId, and it is the only time the token is returned. Every other route for that job — events, download, preview, and the cancelling DELETE — requires the same token as a ?token= query parameter, and answers 403 Forbidden if it is missing or doesn't match.

This is what stops one caller from reading, downloading, or cancelling another caller's job just by guessing or observing a jobId (job ids are not treated as secret; the token is). It matters most for anonymous/open deployments (DynDynPdf:AllowAnonymous=true) where many unrelated callers share one API key or no key at all — the token, not the API key, is what separates their jobs from each other.

If a caller loses the token, the job is unrecoverable through the API: there is no lookup or reissue endpoint. The job still runs to completion (or its timeout) and is evicted normally by DynDynPdf:JobTtlMinutes; the caller must submit a new POST /api/jobs to get a fresh jobId and token.

POST /api/jobs — create a job

Content-Type: multipart/form-data.

Field Required Values Default Maps to
file yes the PDF (binary) — input document (max 1 GB)
engine no managed, native managed extraction engine
format no single, pages, viewer viewer output packaging
rasterFallback no never, auto, always auto ConversionOptions.RasterFallback
background no svg, raster, none svg ConversionOptions.Background
dpi no integer 120 clamped 18–600 server-side
pages no e.g. 1-5,8,10- all PageRange (invalid → all)
lineGrouping no always, auto, never always ConversionOptions.LineGrouping
semanticTags no true/on/1 off ConversionOptions.SemanticStructure
detectDecorative no true/on/1 off ConversionOptions.DetectDecorative — detect running headers/footers and page numbers and emit them as non-selectable decorative text (text stays live; pixels unchanged)
extractStyles no true/on/1 off ConversionOptions.ExtractStyles — deduplicate repeated style declarations into short generated CSS classes (smaller output; off = byte-identical output)
jpegQuality no integer 85 ConversionOptions.JpegQuality, clamped 1–100 server-side
rawTtf no true/on/1 off ConversionOptions.FontEmbedding — true selects RawTtf (debug), otherwise Woff1
password no string none ConversionOptions.Password — user password for an encrypted PDF. Managed engine only

Responses:

curl -sS -X POST http://localhost:8080/api/jobs \
  -F file=@report.pdf \
  -F format=viewer -F engine=managed -F dpi=150 -F pages=1-20
# {"jobId":"3f2c…","token":"9a1e…"}

`GET /api/jobs/

Content-Type: text/event-stream, Cache-Control: no-cache. Each SSE data: line is a JSON object. The stream ends after a terminal done or error event. 404 if the job id is unknown, 403 if token is missing or doesn't match the job's ownership token.

If the client disconnects before the job reaches a terminal state (tab closed, navigation, lost connection), the server cancels the job rather than running it to completion or its timeout with nobody left to read the result.

progress (zero or more):

{ "type": "progress", "stage": "extract", "total": 20,
  "extracted": 7, "composed": 3, "fraction": 0.42 }

done (terminal, success):

{ "type": "done", "engine": "managed", "pages": 20, "warnings": 0, "elapsed": 4.2,
  "downloadUrl": "/api/jobs/3f2c…/download?token=9a1e…",
  "previewUrl":  "/api/jobs/3f2c…/preview?token=9a1e…",
  "downloadName": "report.html" }

downloadUrl and previewUrl already carry the job's ?token=, so a client can follow them directly without re-attaching it. previewUrl is null for the pages format (a zip has no inline preview). engine is the engine this job was configured to run (managed or native, from the request's engine field, normalized) — not an independent verification that engine actually produced the output; conversion fails rather than silently substituting a different engine.

error (terminal, failure):

{ "type": "error", "message": "…" }

A distinct code is set on some failures so the client can react instead of showing a generic error:

code When
password_required The PDF is encrypted and no password field was sent, or it was wrong
cancelled The job was cancelled — via DELETE /api/jobs/{id}, or the client's SSE connection dropped before the job finished

A conversion that exceeds DynDynPdf:ConversionTimeoutSeconds also ends in an error event, with an additional "timedOut": true field instead of a code:

{ "type": "error", "message": "conversion timed out after 600s", "timedOut": true }

Consume with the browser EventSource API:

const es = new EventSource(`/api/jobs/${jobId}/events?token=${token}`);
es.onmessage = (e) => {
  const evt = JSON.parse(e.data);
  if (evt.type === "progress") updateBar(evt.fraction);
  else { es.close(); if (evt.type === "done") window.location = evt.downloadUrl; }
};

`DELETE /api/jobs/

Cancels an in-flight or still-queued job and immediately frees the concurrency slot it holds, instead of letting it run to completion or its timeout.

`GET /api/jobs/

Returns the converted file with Content-Disposition set to downloadName:

404 if the job is unknown or hasn't produced output yet. 403 if token is missing or doesn't match.

`GET /api/jobs/

Serves the single/viewer HTML inline (text/html) for embedding in an <iframe>. 404 for the pages format or an unfinished/unknown job. 403 if token is missing or doesn't match.

GET /health and GET /ready

Never gated by API-key auth (static assets and these two probes are the only unguarded routes).

Authentication

/api/* is gated by API-key middleware (ApiKeyAuthMiddleware), controlled by DynDynPdf:ApiKeys and DynDynPdf:AllowAnonymous (see deployment.md):

/api/* also carries a per-key / per-IP fixed-window rate limit (DynDynPdf:RateLimitRequestsPerMinute, default 120/min) — partitioned on the validated API key when keys are configured, on remote IP otherwise; never on a client-supplied value that was never checked. Every conversion still applies the output-safety posture in security.md regardless of who calls it.