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:
dotnet add package Doconut.NET6.ConverterTo pin the plugin to the current 26.7.0 release, pass the version separately:
dotnet add package Doconut.NET6.Converter --version 26.7.0Keep the Converter package at the same version as Doconut.NET6. The package ID is
Doconut.NET6.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.NET6.Converter, installed alongside the base viewer package.
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
TRIALfile, or a non-temporary license that does not grant theConvertercapability — anInvalidOperationExceptionraised from insideAddDoconut(), 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.
// 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(upload, ".xlsx", ConversionTarget.Png, password: null, ct: ct);Stream html = await converter.WordToHtmlAsync("report.docx", ct);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
Pdf, Docx, Doc, Html, Xlsx, Pptx, Png, Jpeg, Csv, Tiff, Bmp, Gif, Svg, Xml,
Txt, Xls, Jp2, Rtf, Odt, Ods, Odp, Epub, Xps, WebpNot 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:
builder.Services.AddDoconut(options =>
{
options.LicensePath = "doconut.lic";
options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>();
options.AddConverterWidget(widget =>
{
widget.MaxUploadMb = 25;
});
});<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):
| 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) |
Doconut.convert() returns the widget instance itself — hang onto it to drive the widget programmatically:
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 thisBuild 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):
| Route | Purpose | Success response |
|---|---|---|
POST ?convert=open (multipart, field 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 |
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:
| 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 | 413 | Uploaded file exceeds MaxUploadMb | { "error": "File is too large." } |
open | 400 | No multipart body, no file, or a source extension that can't be converted | { "error": "..." } |
run | 400 | Malformed token (not a GUID), or a target that doesn't parse to a ConversionTarget | { "error": "Invalid token." } / { "error": "Unknown target format." } |
run | 400 | target isn't in the source's allowedTargets | { "error": "That target format is not available for this file." } |
run | 404 | The stashed upload has expired (30-minute TTL) or the token was never opened | { "error": "Upload expired — please re-open the file." } |
open, run | 500 | Processing failed internally | { "error": "<sanitized message>" } — sanitized the same way as every other Doconut error path; never leaks internal engine names |
download | 400 | Malformed token (not a GUID) | status only |
download | 404 | Unknown or expired download token | status 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
| Symptom | Check |
|---|---|
Resolving DocumentConverter fails | ConverterPlugin registration happened inside AddDoconut() |
| Application fails during startup | The loaded license grants Converter |
| Stream conversion says the format is unsupported | sourceExtension includes the leading dot |
| Widget JavaScript loads but requests return 404 | AddConverterWidget() was not called |
| Widget requests use the wrong URL | basePath matches the branch where UseDoconut() is mapped |
| Target is missing | Use allowedTargets returned by convert=open; not every source supports every enum target |
| Download expired | Repeat convert=open/convert=run; stash tokens are intentionally temporary |
Watermarking
With ConverterPlugin registered, the host's license is in one of three states:
| License state | Startup gate | Conversion output |
|---|---|---|
Paid viewer license granting Converter, within its validity period | Passes | Clean — watermarked: false |
| Active evaluation (demo/NFR) license | Passes | Converts successfully, stamped with the evaluation watermark — watermarked: true |
Unlicensed, a legacy TRIAL file, or a non-temporary license that doesn't grant Converter | App never starts — the startup gate described above throws | — |
| Expired Temporary/Demo license | Registration survives expiry | Converts 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?