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 served by DocImage.axd, so you can convert documents from C#, from the widget, or from a frontend you write yourself.

Install the package

powershell
Install-Package Doconut.NETFramework.Converter

Keep the Converter package at the same version as Doconut.NETFramework.

Register the plugin

Every plugin registers the same way: AddPlugin<TPlugin>() in DoconutHost.Initialize.

csharp
Doconut.DoconutHost.Initialize(options =>
{
    options.LicensePath = HostingEnvironment.MapPath("~/wwwroot/Doconut.Viewer.lic");
    options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>();
});

If no active license grants the Converter capability — no license, a legacy TRIAL file, or a license without it — DoconutHost.Initialize throws from Application_Start. In System.Web that means every request answers an empty 500, and the reason is only in the Windows event log (source ASP.NET). Temporary Demo/NFR licenses are accepted. See License Setup.

Convert from C#

Every conversion returns a seekable MemoryStream positioned at 0. DocumentConverter is stateless: resolve it from DoconutHost.Services wherever you need it and reuse it freely.

csharp
// Resolve DocumentConverter from DoconutHost.Services; 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"), or it does not resolve. And despite its name, WordToHtmlAsync returns Task<Stream>, not Task<string> — the HTML document (images embedded as Base64) arrives as a stream, like every other result.

In an MVC 5 controller

Make the action asynchronous and await the conversion:

csharp
[HttpPost]
public async Task<ActionResult> ToPdf(HttpPostedFileBase file)
{
    var converter = Doconut.DoconutHost.Services.GetRequiredService<DocumentConverter>();
    Stream pdf = await converter.ConvertAsync(
        file.InputStream, Path.GetExtension(file.FileName), ConversionTarget.Pdf,
        password: null, ct: Response.ClientDisconnectedToken);
    return File(pdf, "application/pdf", Path.GetFileNameWithoutExtension(file.FileName) + ".pdf");
}

In Web Forms: never block on a conversion

A Web Forms page runs on a single-threaded request context. ConvertAsync resumes on that context when it completes, so a page that waits for it synchronously — .Result, .Wait(), .GetAwaiter().GetResult() — holds the very thread the conversion needs to finish. The request never returns. The browser keeps spinning, IIS logs nothing, and the rest of the site keeps working, so it looks like a slow conversion rather than a deadlock.

Mark the page Async="true" and run the conversion as a page async task:

aspx
<%@ Page Language="C#" Async="true" CodeBehind="Convert.aspx.cs" Inherits="MyApp.Convert" %>
csharp
// Web Forms: the @Page directive needs Async="true", and the conversion runs as a page async task.
// Blocking on it instead (.Result, .Wait(), .GetAwaiter().GetResult()) deadlocks the request:
// it never returns, and nothing is logged.
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack) return;

    RegisterAsyncTask(new PageAsyncTask(async ct =>
    {
        var converter = Doconut.DoconutHost.Services.GetRequiredService<DocumentConverter>();

        using (var pdf = await converter.ConvertAsync(
                   Server.MapPath("~/App_Data/contract.docx"), ConversionTarget.Pdf, ct: ct))
        using (var ms = new MemoryStream())
        {
            if (pdf.CanSeek) pdf.Position = 0;
            await pdf.CopyToAsync(ms);

            Response.ContentType = "application/pdf";
            Response.AddHeader("Content-Disposition", "attachment; filename=contract.pdf");
            Response.BinaryWrite(ms.ToArray());
            // Context, not HttpContext.Current: after an await the latter can be null.
            Context.ApplicationInstance.CompleteRequest();
        }
    }));
}

Without Async="true" in the @Page directive, RegisterAsyncTask is silently ignored. After an await, use the page's Context; HttpContext.Current can be null there.

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: each source's format family (Word, Excel, PowerPoint, PDF, CAD, Image, Email, Diagram, Project, PSD, web document) has its own set of allowed targets. Don't hardcode this enum as your UI's target list: ?convert=open returns the allowedTargets for the uploaded file, and that is what should drive a picker.

Drop-in widget

The widget's ?convert=open|run|download endpoints are served by DocImage.axd and are off by default. Enable them next to the plugin registration:

csharp
Doconut.DoconutHost.Initialize(options =>
{
    options.LicensePath = HostingEnvironment.MapPath("~/wwwroot/Doconut.Viewer.lic");
    options.AddPlugin<Doconut.Plugins.Converter.ConverterPlugin>();
    options.AddConverterWidget(widget =>
    {
        widget.MaxUploadMb = 25;
    });
});
// System.Web reads the whole upload before Doconut sees it: keep maxRequestLength (KB) and
// maxAllowedContentLength (bytes) in Web.config above the widget's MaxUploadMb.
html
<div id="doconut-convert"></div>
<script src="/doconut-res/js/doconutConverter.js"></script>
<script>
  Doconut.convert('#doconut-convert', { basePath: '/DocImage.axd', maxUploadMb: 25 });
</script>

Two things to check when the widget misbehaves:

  • basePath: '/DocImage.axd'. The widget's default, /doconut, is the ASP.NET Core endpoint. On this package, the endpoints are the DocImage.axd handler from Web.config.
  • The script and the endpoints are independent. The resource module serves doconutConverter.js whether or not the widget is enabled. Without AddConverterWidget(), the widget loads, paints its drop zone, and looks entirely correct — and its first action answers 404.

Upload limits

ASP.NET reads the whole request body before DocImage.axd runs. An upload above MaxUploadMb but below the host limits gets the widget's own JSON 413 ("File is too large."). An upload above the host limits is rejected by IIS first, with an error page the widget cannot read. Keep both host limits in Web.config above the widget's cap:

xml
<system.web>
  <httpRuntime maxRequestLength="30720" />           <!-- KILOBYTES: 30 MB -->
</system.web>
<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="31457280" />  <!-- BYTES: 30 MB -->
    </requestFiltering>
  </security>
</system.webServer>

Customize the widget

Options passed to Doconut.convert(selector, options):

OptionTypeDefaultNotes
basePathstring/doconutThe endpoint URL. Set '/DocImage.axd' on this package
resPathstring/doconut-resAccepted for consistency with other Doconut widgets; the converter widget does not build any URL from it
maxUploadMbnumber25Client-side pre-check only; the server enforces its own cap
licenseUrlstring | nullnullTurns the watermark notice on the result screen into a link
labelsobject{}Overrides any subset of the widget's English strings

Callbacks:

CallbackFires whenPayload
onReady()The widget has rendered its drop screen—
onSourceLoaded({ token, pages, sourceExt, allowedTargets })?convert=open succeedsSource token, page count, source extension (no leading dot), allowed targets
onConverted({ downloadToken, resultToken, resultPages, downloadName, watermarked, target })?convert=run succeedsThe run response plus the requested target
onDownload({ downloadName, downloadToken })The user clicks DownloadFires alongside the browser's own download
onError({ phase, message })An open or run request failsphase is 'open' or 'run'; message is the sanitized server error

Doconut.convert() returns the widget instance: reset() returns to the drop screen, loadFile(file) starts the flow with a File, and destroy() removes it.

Build your own frontend

The widget is a client for this HTTP contract on DocImage.axd:

RoutePurposeSuccess response
POST /DocImage.axd?convert=open (multipart, field file)Upload and open a source document200 — { token, pages, sourceExt, allowedTargets }
POST /DocImage.axd?token=<token>&convert=run&target=<ext>Convert the uploaded source to target200 — { downloadToken, resultToken, resultPages, downloadName, watermarked }
GET /DocImage.axd?convert=download&token=<downloadToken>Stream the converted file200 — the file, Content-Disposition: attachment

Uploaded sources are kept for 30 minutes; after that, run answers 404 and the file must be opened again. downloadToken gets its own 30-minute window when the conversion completes. resultToken is an ordinary viewer token that follows the viewer session's lifetime.

sourceExt in the open response has no leading dot ("docx") — the opposite of the sourceExtension parameter of ConvertAsync.

RouteStatusWhenBody
any404The widget isn't enabled (AddConverterWidget() was never called)status only
any405Wrong verb (open/run require POST, download requires GET)status only
open413The upload exceeds MaxUploadMb (but not the IIS limits){ "error": "File is too large." }
open400No multipart body, no file, or a type that cannot be converted{ "error": "..." }
open403The file type needs a capability the license does not grant{ "error": "..." }
run400A malformed token, an unknown target, or a target not in allowedTargets{ "error": "..." }
run404The upload expired or was never opened{ "error": "Upload expired — please re-open the file." }
open, run500Processing failed{ "error": "<sanitized message>" }
download400 / 404A malformed, unknown, or expired download tokenstatus only

Resource ownership

The caller owns the MemoryStream a conversion returns and should dispose it after copying or returning it (MVC's File(stream, …) disposes it for you). Do not construct or dispose DocumentConverter yourself — its constructor is internal.

Watermarking

License stateStartupConversion output
Paid viewer license granting ConverterStartsClean — watermarked: false
Active Temporary/Demo licenseStartsConverts, with the evaluation watermark — watermarked: true
No license, a legacy TRIAL file, or a license without ConverterApplication_Start throws; every request answers an empty 500—
Expired Temporary/Demo licenseStartsConverts, with the evaluation watermark — watermarked: true

An integration can be built and tested end to end on an evaluation license before purchase — only the output bytes change.

Troubleshooting

SymptomCheck
Every request answers an empty 500 after adding the pluginThe license grants Converter — Windows event log, source ASP.NET
Resolving DocumentConverter throwsAddPlugin<ConverterPlugin>() is in DoconutHost.Initialize
A Web Forms conversion never returnsThe page blocks on ConvertAsync — use Async="true" and RegisterAsyncTask
Stream conversion says the format is unsupportedsourceExtension includes the leading dot
The widget loads but its requests return 404AddConverterWidget() was not called, or basePath is not '/DocImage.axd'
A large upload shows an IIS error page instead of the widget's messagemaxRequestLength / maxAllowedContentLength are below MaxUploadMb
A target is missingUse the allowedTargets from convert=open
The download expiredRepeat convert=open / convert=run; the tokens are temporary on purpose

Чи була ця сторінка корисною?