Converter Plugin

Convert documents to 24 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.

Install the package

Install the latest stable Converter plugin:

bash
dotnet add package Doconut.NET8.Converter

To pin the plugin to the current 26.7.0 release, pass the version separately:

bash
dotnet add package Doconut.NET8.Converter --version 26.7.0

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

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.NET8.Converter, installed alongside the base viewer package.

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

This call throws at startup for a missing license, a legacy TRIAL file, or a non-temporary license that does not grant the Converter capability — an InvalidOperationException raised from inside AddDoconut(), before the app serves requests. Temporary Demo/NFR registrations are accepted; after their calendar expiry, conversion remains available with watermarked output. There is no silent free tier. See License Setup for how licenses are loaded.

Convert 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(upload, ".xlsx", ConversionTarget.Png, password: null, ct: ct);
csharp
Stream html = await converter.WordToHtmlAsync("report.docx", ct);
csharp
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.

Target formats

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

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) to its own fixed set of allowed targets. 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

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.lic";
    options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>();
    options.AddConverterWidget(widget =>
    {
        widget.MaxUploadMb = 25;
    });
});
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>

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). AddConverterWidget() still requires the Converter plugin to be registered and a license that grants Converter — it doesn't grant conversion rights on its own.

Customize the widget

Init options passed to Doconut.convert(selector, 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)

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

javascript
const conv = Doconut.convert('#doconut-convert', { basePath: '/doconut' });
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

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):

RoutePurposeSuccess response
POST ?convert=open (multipart, field 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

Uploaded source bytes are stashed server-side with a 30-minute TTL; 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 whose lifetime follows the viewer's session cache, independent of the stash.

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.

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
open413Uploaded file exceeds MaxUploadMb{ "error": "File is too large." }
open400No multipart body, no file, or a source extension that can't be converted{ "error": "..." }
run400Malformed token (not a GUID), or a target that doesn't parse to a ConversionTarget{ "error": "Invalid token." } / { "error": "Unknown target format." }
run400target isn't in the source's allowedTargets{ "error": "That target format is not available for this file." }
run404The stashed upload has expired (30-minute TTL) or the token was never opened{ "error": "Upload expired — please re-open the file." }
open, run500Processing failed internally{ "error": "<sanitized message>" } — sanitized the same way as every other Doconut error path; never leaks internal engine names
download400Malformed token (not a GUID)status only
download404Unknown or expired download tokenstatus only

Resource ownership

The converter returns a seekable MemoryStream positioned at zero. The caller owns that stream and should dispose it after copying or returning its contents. The DocumentConverter service itself is stateless and is resolved from dependency injection; do not construct or dispose the service manually.

For the web widget, the upload and download stashes have independent 30-minute TTLs. A viewer resultToken follows the viewer session lifetime instead. Closing a viewer result does not delete a still-valid download stash, and resetting the browser widget does not extend either TTL.

Troubleshooting

SymptomCheck
Resolving DocumentConverter failsConverterPlugin registration happened inside AddDoconut()
Application fails during startupThe loaded license grants Converter
Stream conversion says the format is unsupportedsourceExtension includes the leading dot
Widget JavaScript loads but requests return 404AddConverterWidget() was not called
Widget requests use the wrong URLbasePath matches the branch where UseDoconut() is mapped
Target is missingUse allowedTargets returned by convert=open; not every source supports every enum target
Download expiredRepeat convert=open/convert=run; stash tokens are intentionally temporary

Watermarking

With ConverterPlugin registered, the host's license is in one of three states:

License stateStartup gateConversion output
Paid viewer license granting Converter, within its validity periodPassesClean — watermarked: false
Active evaluation (demo/NFR) licensePassesConverts successfully, stamped with the evaluation watermark — watermarked: true
Unlicensed, a legacy TRIAL file, or a non-temporary license that doesn't grant ConverterApp never starts — the startup gate described above throws
Expired Temporary/Demo licenseRegistration survives expiryConverts with the evaluation watermark — watermarked: true

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.

Was this page helpful?