Converter Plugin

Convert documents to 45 target formats

The Converter plugin turns Doconut into a document-conversion service. It contributes the engine behind the public DocumentConverter facade, and — opt-in — a drop-in widget with its own HTTP contract, so you can convert documents from C#, from the widget, or from a frontend you write yourself.

SurfaceUse it when
DocumentConverter (C#)You convert on the server, with no browser involved
Drop-in widget (doconutConverter.js)You want a ready-made upload, convert and download screen in your page
HTTP endpoints (?convert=open|run|download)You build your own conversion UI against a documented server contract

Install the package

Install the latest stable Converter plugin:

bash
dotnet add package Doconut.NETStandard.Converter

To pin the plugin to the release this documentation describes, pass the version separately:

bash
dotnet add package Doconut.NETStandard.Converter --version 26.10.0

Keep the Converter package at the same version as Doconut.NETStandard. The package ID is Doconut.NETStandard.Converter; .<version> appears only in the downloaded .nupkg filename.

The plugin's license file, Doconut.Viewer.Converter.lic, goes in the same folder as Doconut.Viewer.lic; it is read together with the viewer license.

Register the plugin

There is no AddConverter() method — Doconut's plugin model is uniform. Every plugin, Converter included, registers the same way: call AddPlugin<TPlugin>() inside AddDoconut(). ConverterPlugin ships in its own NuGet package, Doconut.NETStandard.Converter, installed alongside the base viewer package.

csharp
builder.Services.AddDoconut(options =>
{
    options.LicensePath = "Doconut.Viewer.lic";
    options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>();
});

That is all the C# facade needs. The widget and its HTTP endpoints are a separate opt-in, AddConverterWidget() (see Drop-in widget).

The plugin needs a license that grants the Converter capability. Without one, AddDoconut() throws an InvalidOperationException at startup, before the app serves a request:

text
The Converter plugin is registered via AddPlugin but no active license grants Converter. Remove the AddPlugin<...>() call or install a license (or trial/demo) that includes Converter.

It throws for no license, a legacy TRIAL file, a license without Converter, and a paid license used outside its version window. Temporary Demo/NFR licenses are accepted. There is no silent free tier. See License Setup for how licenses are loaded.

Licensing & watermark

License stateStartupConversion output
Paid viewer license granting Converter, within its version windowStartsClean — watermarked: false
Active Temporary (Demo/NFR) licenseStartsConverts, with the evaluation watermark — watermarked: true
Expired Temporary (Demo/NFR) licenseStartsConverts, with the evaluation watermark — watermarked: true
No license, a legacy TRIAL file, a license without Converter, or a paid license outside its version windowAddDoconut() throws—

Both call paths compute the flag from the same rule: the DocumentConverter C# facade derives it internally from the license's IsViewerLicensed and IsTemporary state, and the widget's ?convert=run handler makes the equivalent check (IsViewerLicensed && !IsTrial && !IsTemporary) to fill the watermarked field it returns. An integration can be built and tested end-to-end on an evaluation license before purchase — only the output bytes change.

A DICOM source (.dcm, .ima) also needs the Dicom capability. Without it, ConvertAsync throws a LicenseException (Converting '.dcm' requires the Dicom capability license.) and the widget's open answers 403.

Use from C#

Every conversion returns a seekable MemoryStream positioned at 0, ready to read or copy immediately. Resolve DocumentConverter from DI wherever you need it — it is stateless by design, so a single instance is safe to reuse across requests.

csharp
// Inject DocumentConverter; its constructor is internal, so never `new` it.
Stream pdf = await converter.ConvertAsync("contract.docx", ConversionTarget.Pdf, ct: ct);
csharp
// sourceExtension includes the leading dot. password is null unless the document is protected.
Stream png = await converter.ConvertAsync(stream, ".xlsx", ConversionTarget.Png, password: null, ct: ct);
csharp
Stream html = await converter.WordToHtmlAsync("report.docx", ct);
csharp
// html: a Stream holding the HTML to turn back into a Word document, such as the result of WordToHtmlAsync.
Stream docx = await converter.HtmlToWordAsync(html, ConversionTarget.Docx, ct);

Two details that are easy to get wrong: sourceExtension on the stream overload must include the leading dot (".xlsx", not "xlsx") — the converter matches it against the format catalog and a bare extension won't resolve. And despite its name, WordToHtmlAsync returns Task<Stream>, not Task<string> — you get the HTML document (images embedded as Base64) as a stream, the same as every other conversion result.

The caller owns the returned stream and should dispose it after copying or returning its contents. The DocumentConverter service itself is resolved from dependency injection; do not construct or dispose it manually.

Target formats

ConversionTarget (namespace Doconut.Configs.Conversion) has 45 members:

text
Pdf, Docx, Doc, Html, Xlsx, Pptx, Png, Jpeg, Csv, Tiff, Bmp, Gif, Svg, Xml,
Txt, Xls, Jp2, Rtf, Odt, Ods, Odp, Epub, Xps, Webp, Msg, Eml, Emlx, Mhtml,
Xlsm, Xlsb, Ppt, Docm, Dot, Dotx, Dotm, Vsdx, Vssx, Vstx, Vsdm, Vssm, Vstm,
Vdx, Vsx, Vtx, Ico

Not every source converts to every target — the plugin maps each source's format family (Word, Excel, PowerPoint, PDF, CAD, Image, Email, Diagram, Project/Task, PSD, web document, DICOM) to its own fixed set of allowed targets. Converting a DICOM source (.dcm, .ima) also needs a license that grants the Dicom capability; its targets are Pdf, Png, Jpeg, Bmp, Tiff and Gif. Don't hardcode this enum as your UI's target list: ?convert=open returns the actual allowedTargets for whatever file was just uploaded, and that's what should drive a picker.

Drop-in widget

Enable the endpoints

The widget's ?convert=open|run|download endpoints are opt-in and disabled out of the box — secure by default. Enable them server-side, alongside the plugin registration:

csharp
builder.Services.AddDoconut(options =>
{
    options.LicensePath = "Doconut.Viewer.lic";
    options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>();
    options.AddConverterWidget(widget =>
    {
        widget.MaxUploadMb = 25;
    });
});

AddConverterWidget() with no argument enables the widget with the defaults (MaxUploadMb = 25). It does not grant conversion rights by itself: it needs AddPlugin<ConverterPlugin>(), or AddDoconut() throws:

text
AddConverterWidget() was called but the Converter plugin is not registered. Add options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>() (the widget needs it), or remove the AddConverterWidget() call.

Without AddConverterWidget(), the three ?convert= endpoints answer 404 — but the JS file itself is still served regardless (it's a plain embedded static resource; only the endpoints it talks to are gated).

Embed it

html
<div id="doconut-convert"></div>
<script src="/doconut-res/js/doconutConverter.js"></script>
<script>
  Doconut.convert('#doconut-convert', { basePath: '/doconut', resPath: '/doconut-res', maxUploadMb: 25 });
</script>

Doconut.convert(selectorOrElement, options) takes a CSS selector or an element, throws if the mount is not found, and returns a handle. The widget uploads a file, shows a preview of page 1, lets the user pick one of the allowedTargets, converts, previews the result when it can, and offers the download.

Options

OptionTypeDefaultNotes
basePathstring/doconutBase path for the ?convert= endpoints; it must match the ASP.NET branch where UseDoconut() is actually mounted (normally coordinated through MiddlewarePath)
resPathstring/doconut-resAccepted for configuration consistency with other Doconut widgets; the converter widget doesn't currently build any URL from it
maxUploadMbnumber25Client-side pre-check only — rejects an oversized file before uploading. The server enforces its own cap independently and answers 413 if it's exceeded
licenseUrlstring | nullnullWhen set, turns the watermark notice on the result screen into a link to this URL
labelsobject{}Overrides any subset of the widget's English default strings (drop text, buttons, aria-live announcements, error messages)

Callbacks

CallbackFires whenPayload
onReady()The widget has rendered its idle/drop screen—
onSourceLoaded({ token, pages, sourceExt, allowedTargets })?convert=open succeedssource session token, page count, source extension (no leading dot), allowed target list
onConverted({ downloadToken, resultToken, resultPages, downloadName, watermarked, target })?convert=run succeedssame fields as the run response, plus the target that was requested
onDownload({ downloadName, downloadToken })The user clicks the Download linkfires alongside the browser's native download — it does not intercept or replace it
onError({ phase, message })An open or run request failsphase is 'open' or 'run'; message is the sanitized server error (or a client-side message for the upload-size pre-check)

A callback that throws is caught and logged; it never breaks the widget's flow.

Imperative handle

Doconut.convert() returns the widget instance itself — hang onto it to drive the widget programmatically:

javascript
const conv = Doconut.convert('#doconut-convert', { basePath: '/doconut' });
const file = document.querySelector('#picker').files[0]; // a File from your own <input type="file" id="picker">
conv.reset();         // back to the idle/drop screen; does not re-fire onReady
conv.loadFile(file);  // starts the flow with a File object; no-op unless currently idle
conv.destroy();       // removes listeners, empties the mount; the instance is unusable after this

Labels

Every string has an English default; labels overrides any subset and the rest stay English. Fragments that change at runtime use {placeholders}, such as uploading: 'Uploading {name}…' and pages: '{n} pages'.

KeyEnglish default
dropPromptDrag & drop a document here
dropHintPDF, Word, Excel, PowerPoint, images and more
chooseFileChoose file
uploadingUploading {name}…
page / pages1 page / {n} pages
noTargetsNo conversion targets are available for this file.
convertTo / convertConvert to / Convert
convertingConverting to {target}…
readyToDownloadReady to download
download / convertAnotherDownload / Convert another
watermarkNoticeOutput is watermarked — get a license to remove it.
retryRetry
genericErrorSomething went wrong. Please try again.
sizeLimitThat file is larger than the {mb} MB upload limit. Please choose a smaller file.
serverErrorThe server could not process this request. Please try again.
networkErrorCould not reach the server. Please check your connection and try again.

Further keys cover the drop zone's aria-label (dropAriaLabel), the preview alt texts (previewAlt, resultAlt), the linked form of the watermark notice used with licenseUrl (watermarkNoticePrefix, watermarkNoticeLink, watermarkNoticeSuffix), an unreadable response (badResponse) and the aria-live announcements (annReady, annUploading, annLoaded, annLoadedNoTargets, annConverting, annDone, annError).

A JSON error from the server is shown as the server wrote it; serverError is used only when a failed response carries no JSON error, such as the 404 of a disabled widget.

Theming

The widget adds the dcw class to its mount element and reads its colors, radius and font from CSS custom properties, so you can theme it without touching its internals:

css
#doconut-convert.dcw {
  --dcw-accent: #6b46c1;
  --dcw-radius: 12px;
  --dcw-font: "Inter", system-ui, sans-serif;
}

Defaults: --dcw-accent: #2b6cb0, --dcw-bg: #ffffff, --dcw-fg: #1a202c, --dcw-muted: #718096, --dcw-border: #cbd5e0, --dcw-radius: 8px, and a system UI font for --dcw-font.

Build your own frontend

The widget is just a client for this HTTP contract — build your own frontend against it directly for a different UX. All three routes sit under the ASP.NET branch where UseDoconut() is mounted (normally /doconut), and they answer 404 until AddConverterWidget() is called. Errors carry a JSON body, { "error": "..." }, except where the table says "status only". An unknown ?convert= value answers 400.

RoutePurposeSuccess response
POST ?convert=open (multipart, one file)Upload and open a source document for preview200 — { token, pages, sourceExt, allowedTargets }
POST ?token=<token>&convert=run&target=<ext>Convert the stashed source to target200 — { downloadToken, resultToken, resultPages, downloadName, watermarked }
GET ?convert=download&token=<downloadToken>Stream the converted file200 — file bytes, Content-Disposition: attachment, Cache-Control: no-store

sourceExt in the open response has no leading dot (e.g. "docx") — the opposite convention from the sourceExtension parameter on DocumentConverter.ConvertAsync, which requires one. allowedTargets lists lower-case target names ("pdf", "html", …); pass one of them as target, which is matched case-insensitively. open uses the first file in the multipart body.

resultToken is null, and resultPages is 0, when the result cannot be previewed. downloadName is the source file name with the target's extension.

The preview images use the standard viewer image route, {basePath}?token={token}&page=1&thumb=1&width={px}, with the token from open or the resultToken from run. The widget asks for width=240.

Uploaded source bytes are stashed server-side and kept for 30 minutes after they were last read; once that window lapses, run answers 404 and the file must be re-opened. The converted result lives in the same stash — downloadToken gets its own fresh 30-minute window when the conversion completes — while resultToken is an ordinary viewer session token for previewing the result. When the result can be previewed, downloadToken and resultToken are the same value. Closing a viewer result does not delete a still-valid download stash, and resetting the browser widget does not extend either window.

Failure modes, grouped by route:

RouteStatusWhenBody
any404The widget isn't enabled (AddConverterWidget() was never called) — checked before any of the three routes dispatchstatus only
any405Wrong HTTP verb (open/run require POST; download requires GET)status only
open400Not a multipart uploadExpected a multipart file upload.
open400No file, or an empty oneNo file uploaded.
open400A source extension that can't be convertedThis file type cannot be converted.
open403The file type needs a capability the license does not grant (a DICOM file without Dicom)Converting this file type requires the Dicom capability license.
open413Uploaded file exceeds MaxUploadMb, checked against Content-Length and while the upload is readFile is too large.
run400Malformed token (not a GUID)Invalid token.
run400A target that doesn't parse to a ConversionTargetUnknown target format.
run400target isn't in the source's allowedTargetsThat target format is not available for this file.
run404The stashed upload has expired or the token was never openedUpload expired — please re-open the file.
open, run500Processing failed internallyA sanitized message — sanitized the same way as every other Doconut error path; never exposes internal details
download400Malformed token (not a GUID)status only
download404Unknown or expired download tokenstatus only

A complete round trip with fetch:

javascript
// file: the File to convert, for example from an <input type="file">.
async function convertFile(file) {
  const base = '/doconut';

  // 1. open: upload the file
  const form = new FormData();
  form.append('file', file, file.name);
  const openResp = await fetch(base + '?convert=open', { method: 'POST', body: form });
  if (!openResp.ok) throw new Error((await openResp.json()).error);
  const opened = await openResp.json();

  // 2. run: convert to PDF when it is allowed, otherwise to the first allowed target
  const target = opened.allowedTargets.includes('pdf') ? 'pdf' : opened.allowedTargets[0];
  const runResp = await fetch(base + '?token=' + encodeURIComponent(opened.token) +
    '&convert=run&target=' + target, { method: 'POST' });
  if (!runResp.ok) throw new Error((await runResp.json()).error);
  const converted = await runResp.json();

  // 3. download: a plain GET the browser can follow
  window.location.href = base + '?convert=download&token=' + encodeURIComponent(converted.downloadToken);
}

The endpoints are same-origin: this version has no CORS support. Serve your UI and the widget script from the same origin as Doconut, or reverse-proxy both /doconut paths (the middleware path and the resources path) under your UI's origin.

In production

  1. Install the plugin next to the core package, at the same version: dotnet add package Doconut.NETStandard.Converter.
  2. Add the license file. Put Doconut.Viewer.Converter.lic in the same folder as Doconut.Viewer.lic.
  3. Set the server cap. MaxUploadMb in AddConverterWidget is the limit the server enforces; the widget's maxUploadMb is only a client-side check. Keep the two equal.
  4. Serve the UI from the same origin as the Doconut endpoints, or reverse-proxy them (see Build your own frontend).
  5. Dispose conversion results. The stream ConvertAsync returns is pooled; disposing it returns its buffers to the pool.

Web farms

The widget keeps each upload and its result in memory on the node that served open. Behind a load balancer without sticky sessions, a later run, download or preview request can reach another node and not find the upload. Register the shared, encrypted widget store once, after AddDoconut():

csharp
byte[] farmKey = Convert.FromBase64String(builder.Configuration["Doconut:FarmKey"]);

builder.Services.AddDoconutDistributedWidgets(store =>
{
    store.RootPath = @"\\fileshare\doconut";   // a folder every node can read and write
    store.FarmKey = farmKey;                   // at least 32 bytes, identical on every node
    // store.Ttl = TimeSpan.FromMinutes(30);   // how long an upload is kept (the default)
});
  • A single server needs nothing: without this call the in-memory store is used.
  • Uploads and results are encrypted at rest on the share (AES-256, encrypt-then-MAC), and each file is bound to its token, so one token can never read another's file.
  • On the share, an upload or a result is kept for Ttl after it was written; reading it does not extend that window.
  • FarmKey must be at least 32 bytes and the same on every node. Load it from a secret store; never hard-code it. Registration throws an ArgumentException when RootPath is missing or FarmKey is shorter than 32 bytes.
  • Point RootPath at the same shared location the distributed viewer uses.

API reference

The facade and the widget options are in the Doconut namespace; ConversionTarget is in Doconut.Configs.Conversion. ConverterPlugin is in Doconut.Plugins.Converter.

DocumentConverter

csharp
public sealed class DocumentConverter
{
    public Task<Stream> ConvertAsync(
        string filePath, ConversionTarget target,
        string? password = null, CancellationToken ct = default);

    public Task<Stream> ConvertAsync(
        Stream source, string sourceExtension, ConversionTarget target,
        string? password = null, CancellationToken ct = default);

    public Task<Stream> WordToHtmlAsync(string filePath, CancellationToken ct = default);

    public Task<Stream> HtmlToWordAsync(
        Stream htmlSource, ConversionTarget outputFormat = ConversionTarget.Docx,
        CancellationToken ct = default);
}

Resolved from dependency injection; the constructor is internal. sourceExtension includes the leading dot. HtmlToWordAsync writes a .doc file when outputFormat is ConversionTarget.Doc, and a .docx file for any other value.

ConversionTarget

The 45 output formats listed in Target formats.

ConverterWidgetOptions

csharp
public sealed class ConverterWidgetOptions
{
    public int MaxUploadMb { get; set; } = 25;
}

Set through DoconutOptions.AddConverterWidget(Action<ConverterWidgetOptions>? configure = null), which returns the DoconutOptions so calls can be chained.

Troubleshooting

SymptomCauseFix
AddDoconut() throws The Converter plugin is registered via AddPlugin but no active license grants Converter.No license, a TRIAL file, a license without Converter, or a paid license outside its version windowInstall a license that grants Converter, or remove AddPlugin<ConverterPlugin>()
AddDoconut() throws AddConverterWidget() was called but the Converter plugin is not registered.The widget is enabled without its pluginAdd options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>()
Resolving DocumentConverter throws No IDocumentConverter is registered. Add the converter plugin: options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>().ConverterPlugin is not registeredAdd AddPlugin<ConverterPlugin>() inside AddDoconut()
Stream conversion says the format is unsupportedsourceExtension has no leading dotPass ".xlsx", not "xlsx"
ConvertAsync throws Converting '.dcm' requires the Dicom capability license.A DICOM source, and the license does not grant DicomUse a license that grants Dicom
The output is watermarkedThe license is temporary (Demo/NFR)Use a paid license that grants Converter
The widget loads but its requests answer 404AddConverterWidget() was not called, or basePath does not match the branch where UseDoconut() is mountedCall AddConverterWidget(); fix basePath
open answers 403 Converting this file type requires the Dicom capability license.A DICOM upload, and the license does not grant DicomUse a license that grants Dicom
open answers 413 File is too large.The upload exceeds MaxUploadMbRaise MaxUploadMb in AddConverterWidget; the widget's maxUploadMb is only a client-side check
run answers 400 That target format is not available for this file.The target is not one of the source's allowedTargetsOffer only the allowedTargets from open
run or download answers 404 (Upload expired — please re-open the file. from run)The 30-minute window elapsed, or a web-farm node does not have the uploadOpen the file again; in a farm, register AddDoconutDistributedWidgets
The widget fails when the UI is on another originThe endpoints have no CORS supportServe the UI from the same origin, or reverse-proxy the Doconut paths
The theme does not applyThe variables are set on an ancestor or on an element inside the mount; the widget's own .dcw rule sets them on the mountTarget the mount itself: #doconut-convert.dcw or #doconut-convert

Framework notes

  • viewer.Converter.GetConverter() does not exist here. Conversion is no longer reached through the viewer instance: inject Doconut.DocumentConverter instead.
  • This one ConversionTarget enum covers every source family. The previous library had a per-family *ConfigConverter type — WordConfigConverter, ExcelConfigConverter, and so on — each with its own nested EnTargetFormat enum, plus a SourceFormatConverter enum for the input. All of them are gone; see the migration guide.
  • Windows only, like the rest of the .NET Standard package.

Was this page helpful?