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.
| Surface | Use 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:
dotnet add package Doconut.NETStandard.ConverterTo pin the plugin to the release this documentation describes, pass the version separately:
dotnet add package Doconut.NETStandard.Converter --version 26.10.0Keep 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.
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:
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 state | Startup | Conversion output |
|---|---|---|
Paid viewer license granting Converter, within its version window | Starts | Clean — watermarked: false |
| Active Temporary (Demo/NFR) license | Starts | Converts, with the evaluation watermark — watermarked: true |
| Expired Temporary (Demo/NFR) license | Starts | Converts, with the evaluation watermark — watermarked: true |
No license, a legacy TRIAL file, a license without Converter, or a paid license outside its version window | AddDoconut() 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.
// Inject DocumentConverter; its constructor is internal, so never `new` it.
Stream pdf = await converter.ConvertAsync("contract.docx", ConversionTarget.Pdf, ct: ct);// 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);Stream html = await converter.WordToHtmlAsync("report.docx", ct);// 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:
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, IcoNot 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:
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:
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
<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
| Option | Type | Default | Notes |
|---|---|---|---|
basePath | string | /doconut | Base path for the ?convert= endpoints; it must match the ASP.NET branch where UseDoconut() is actually mounted (normally coordinated through MiddlewarePath) |
resPath | string | /doconut-res | Accepted for configuration consistency with other Doconut widgets; the converter widget doesn't currently build any URL from it |
maxUploadMb | number | 25 | Client-side pre-check only — rejects an oversized file before uploading. The server enforces its own cap independently and answers 413 if it's exceeded |
licenseUrl | string | null | null | When set, turns the watermark notice on the result screen into a link to this URL |
labels | object | {} | Overrides any subset of the widget's English default strings (drop text, buttons, aria-live announcements, error messages) |
Callbacks
| Callback | Fires when | Payload |
|---|---|---|
onReady() | The widget has rendered its idle/drop screen | — |
onSourceLoaded({ token, pages, sourceExt, allowedTargets }) | ?convert=open succeeds | source session token, page count, source extension (no leading dot), allowed target list |
onConverted({ downloadToken, resultToken, resultPages, downloadName, watermarked, target }) | ?convert=run succeeds | same fields as the run response, plus the target that was requested |
onDownload({ downloadName, downloadToken }) | The user clicks the Download link | fires alongside the browser's native download — it does not intercept or replace it |
onError({ phase, message }) | An open or run request fails | phase 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:
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 thisLabels
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'.
| Key | English default |
|---|---|
dropPrompt | Drag & drop a document here |
dropHint | PDF, Word, Excel, PowerPoint, images and more |
chooseFile | Choose file |
uploading | Uploading {name}… |
page / pages | 1 page / {n} pages |
noTargets | No conversion targets are available for this file. |
convertTo / convert | Convert to / Convert |
converting | Converting to {target}… |
readyToDownload | Ready to download |
download / convertAnother | Download / Convert another |
watermarkNotice | Output is watermarked — get a license to remove it. |
retry | Retry |
genericError | Something went wrong. Please try again. |
sizeLimit | That file is larger than the {mb} MB upload limit. Please choose a smaller file. |
serverError | The server could not process this request. Please try again. |
networkError | Could 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:
#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.
| Route | Purpose | Success response |
|---|---|---|
POST ?convert=open (multipart, one file) | Upload and open a source document for preview | 200 — { token, pages, sourceExt, allowedTargets } |
POST ?token=<token>&convert=run&target=<ext> | Convert the stashed source to target | 200 — { downloadToken, resultToken, resultPages, downloadName, watermarked } |
GET ?convert=download&token=<downloadToken> | Stream the converted file | 200 — 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:
| Route | Status | When | Body |
|---|---|---|---|
| any | 404 | The widget isn't enabled (AddConverterWidget() was never called) — checked before any of the three routes dispatch | status only |
| any | 405 | Wrong HTTP verb (open/run require POST; download requires GET) | status only |
open | 400 | Not a multipart upload | Expected a multipart file upload. |
open | 400 | No file, or an empty one | No file uploaded. |
open | 400 | A source extension that can't be converted | This file type cannot be converted. |
open | 403 | The file type needs a capability the license does not grant (a DICOM file without Dicom) | Converting this file type requires the Dicom capability license. |
open | 413 | Uploaded file exceeds MaxUploadMb, checked against Content-Length and while the upload is read | File is too large. |
run | 400 | Malformed token (not a GUID) | Invalid token. |
run | 400 | A target that doesn't parse to a ConversionTarget | Unknown target format. |
run | 400 | target isn't in the source's allowedTargets | That target format is not available for this file. |
run | 404 | The stashed upload has expired or the token was never opened | Upload expired — please re-open the file. |
open, run | 500 | Processing failed internally | A sanitized message — sanitized the same way as every other Doconut error path; never exposes internal details |
download | 400 | Malformed token (not a GUID) | status only |
download | 404 | Unknown or expired download token | status only |
A complete round trip with fetch:
// 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
- Install the plugin next to the core package, at the same version:
dotnet add package Doconut.NETStandard.Converter. - Add the license file. Put
Doconut.Viewer.Converter.licin the same folder asDoconut.Viewer.lic. - Set the server cap.
MaxUploadMbinAddConverterWidgetis the limit the server enforces; the widget'smaxUploadMbis only a client-side check. Keep the two equal. - Serve the UI from the same origin as the Doconut endpoints, or reverse-proxy them (see Build your own frontend).
- Dispose conversion results. The stream
ConvertAsyncreturns 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():
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
Ttlafter it was written; reading it does not extend that window. FarmKeymust be at least 32 bytes and the same on every node. Load it from a secret store; never hard-code it. Registration throws anArgumentExceptionwhenRootPathis missing orFarmKeyis shorter than 32 bytes.- Point
RootPathat 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
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
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
| Symptom | Cause | Fix |
|---|---|---|
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 window | Install 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 plugin | Add 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 registered | Add AddPlugin<ConverterPlugin>() inside AddDoconut() |
| Stream conversion says the format is unsupported | sourceExtension has no leading dot | Pass ".xlsx", not "xlsx" |
ConvertAsync throws Converting '.dcm' requires the Dicom capability license. | A DICOM source, and the license does not grant Dicom | Use a license that grants Dicom |
| The output is watermarked | The license is temporary (Demo/NFR) | Use a paid license that grants Converter |
The widget loads but its requests answer 404 | AddConverterWidget() was not called, or basePath does not match the branch where UseDoconut() is mounted | Call AddConverterWidget(); fix basePath |
open answers 403 Converting this file type requires the Dicom capability license. | A DICOM upload, and the license does not grant Dicom | Use a license that grants Dicom |
open answers 413 File is too large. | The upload exceeds MaxUploadMb | Raise 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 allowedTargets | Offer 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 upload | Open the file again; in a farm, register AddDoconutDistributedWidgets |
| The widget fails when the UI is on another origin | The endpoints have no CORS support | Serve the UI from the same origin, or reverse-proxy the Doconut paths |
| The theme does not apply | The variables are set on an ancestor or on an element inside the mount; the widget's own .dcw rule sets them on the mount | Target 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: injectDoconut.DocumentConverterinstead.- This one
ConversionTargetenum covers every source family. The previous library had a per-family*ConfigConvertertype —WordConfigConverter,ExcelConfigConverter, and so on — each with its own nestedEnTargetFormatenum, plus aSourceFormatConverterenum 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?