PDF Web SDK Documentation

Comprehensive Guide for Developers: UI & SDK Integration, Configuration, and API Reference

PDF Web SDK
    Preparing search index...

    Saving & Downloading

    The viewer treats every place a file can come from or go to as a source: your own cloud, the built-in drives, and the user's machine. A source that can be written to is a save target, and saving is always "write this document to a target" — never a browser download in disguise. The recents history is the one source that owns nothing: each entry belongs to the source it was opened from.

    Setting the sources up is Sign-in & Cloud Drives; this page is about what the user sees and which config decides it.

    Action Where What it does
    Save main menu, Ctrl+S, the "Save" button of the unsaved-changes dialog Writes the document back over the file it was opened from.
    Save as main menu, Ctrl+Shift+S Writes it to a target the user picks.
    Download toolbar CTA only Exports a copy. Never a save: the document keeps its path, its source and its unsaved state.

    Downloading is deliberately outside the save flow — it is the one action that does not ask where anything belongs.

    Save optimized, Convert, Extract images and Split produce a new artefact — a compressed copy, a converted file, a zip of images or pages — not the document itself. They go straight out through the export path, never through the save chain: nothing is written back to the document's source and its path and unsaved state are untouched.

    onExportFile is therefore the single place a host receives all of them — and, when deviceConfig implements no writing, the device saves too.

    Tools → Convert offers eight target formats:

    Format Runs Result
    Word, Excel, PowerPoint, PDF/A online, through apiToolsUrl (license / auth) .docx, .xlsx, .pptx, PDF/A .pdf
    Image locally a .zip of one PNG per page
    TXT locally .txt
    Markdown locally .md for LLMs (headings, lists, comments)
    JSON locally .json structured content for apps and AI
    • Content to include — selecting Markdown or JSON shows a toggle per content kind (document properties, bookmarks, text, header and footer, layout, comments and annotations, links, form fields, Fill & Sign, attachments). Header and footer is off by default; Layout is JSON-only. These map 1:1 to PdfContentKind — see Structured content for AI in the SDK operations guide.
    • Progress and cancel — local conversions show the per-page percentage on the Convert button; online ones show upload / processing / finishing. Closing the dialog cancels the conversion in progress.
    • Protected documents — for Image, TXT, Markdown and JSON the owner password is asked for only when the document's permissions forbid copying content; a document that merely has an owner password converts without a prompt.
    • Every result leaves through onExportFile with its claims ('Convert.PdfToJson', 'Convert.PdfToMarkdown', 'Convert.PdfToTxt', 'Convert.PdfToImage', …).

    Every save and export callback is handed a claims list alongside the file: the operations its contents went through, as claim ids ('Ocr', 'Create.CombineFiles', 'Edit.Optimize.ReduceFileSize', …).

    It is the whole chain, not the last step, and it belongs to the file rather than to the document it came from: a tool that produces a new artefact carries over what its source already had and adds its own, while the source itself stays as it was — saving the still-open document afterwards reports only what is actually in its bytes. A document the viewer derived from another (a merge result, a signed revision) inherits its sources' claims instead of starting empty, so a compressed export of a merged, OCR'd document reports the OCR, the merge and the compression.

    Ordered oldest first, and deliberately not sorted — the order is the order things happened. The same list is available on the document itself through sdk.IDocumentModel.listDocumentClaims(), and reaches every callback a file leaves through: onExportFile, deviceConfig.saveOver / saveInto, the customProviders save methods, the drives' isSaveAllowed guard, and onPrint.

    The one exception is onSnapshotArea, which always receives just ['View.Snapshot']: a capture is a raster of a page region rather than the document, so the document's own claims are not in the image and do not travel with it.

    Save and Save as are the same chain, entered with or without permission to reuse the document's source:

    Save     → chain(saveToOrigin: true)
    Save as → chain(saveToOrigin: false)

    chain(saveToOrigin):
    saveToOrigin and the document has a writable source
    → overwrite that file
    otherwise, settle on a destination:
    exactly one target → write there, no question asked
    more than one target → show the target picker (source · folder · file name)

    There is always at least one target, so the chain never dead-ends — but what it is depends on the host, not on a platform check, and it is named after what it does:

    • a desktop shell implements deviceConfig.saveOver / saveInto and writes the file itself, so the target is a real place and is offered as This device: Save afterwards overwrites the same file, and Document properties shows it under File location;
    • a browser page cannot write anywhere, so the same target is offered as Download, with the toolbar action's own icon, and choosing it exports. That is not a location, and the document is no more saved than before. Nothing about it is touched: its source, its path, its name and its undo history all stay as they were, no location is shown and no "Saved to…" notice appears (the download is its own feedback). A document opened from Dropbox and downloaded this way still has Dropbox as its source, still carries the unsaved *, and still keeps its own name — the name typed in the dialog belongs to the copy that left, not to the document.

    A connected drive or a host source with save methods is a target too, and having more than one is what makes the picker worth showing.

    deviceConfig: {
    // "Save" — overwrite the file the document came from. item.path is the location you handed
    // over when you opened it.
    saveOver: async (item, file) => {
    await shell.write(item.path!, new Uint8Array(await file.arrayBuffer()));
    return item; // saved, still in the same place
    },

    // "Save as" — no folder to pass: show your own dialog.
    saveInto: async (name, file) => {
    const path = await shell.pickSavePath(name);
    if (!path) return null; // cancelled
    await shell.write(path, new Uint8Array(await file.arrayBuffer()));
    return { id: path, name: path.split(/[\\/]/).pop()!, isFolder: false, path };
    },

    // Read a file back by path. Only this puts the machine's files in the recents
    // history: the viewer records an open only when it could load the file again.
    readPath: async (path) => (await shell.exists(path) ? await readAsFile(path) : null),

    // Optional veto in front of either.
    isSaveAllowed: async (file, target) => (target ? confirmOverwrite(target.path) : true),
    }

    The item you return is what the document adopts: its path is the location shown under File location and the one the next Save overwrites. Return no path and nothing is adopted — the save is treated like the export it resembles, with no location to point at.

    For Save to have something to overwrite from the start, the document needs a path, and only the shell knows it — pass it when you open the file. That is all it takes: a document with a path has the device as its source, so Save overwrites that file through your saveOver and Document properties shows the path under File location. There is no source to declare separately, and it makes no difference whether the file arrived by double-click, by your own open command, or from Recents — an entry that came off the machine carries the same path.

    await PdfEditor({
    /* … */
    initialDocument: { file: await readAsFile(openedPath), path: openedPath },
    });

    // or per document
    await editor.ui.pdfWebService.openDocument(file, { path });

    A file your app fetched from your own source (a customProviders entry) has no path — pass its source instead, and Save calls that provider's saveOver(item, …): openDocument(file, { origin: { providerId, item } }). See Opening a file from your own code.

    • Save is always in the menu and greys out while there is nothing to do — no writable source, or no edits since the last save (the same signal as the * next to the file name).
    • Ctrl+S is deliberately more forgiving than the greyed-out item: it enters the chain anyway, so a new document lands on the device instead of the shortcut doing nothing. This is the only way the shortcut differs from the menu.
    • Save as is always available; it always has at least the device or the download to offer.
    • A save into a cloud, or onto the device through your callback, raises a snackbar naming the target — and an error snackbar when the target refuses or fails, leaving the document unsaved. A browser export stays silent: the download is its own feedback.
    Source Becomes a save target when Who writes the bytes
    This device always your deviceConfig.saveOver / saveInto where they exist, otherwise the export
    OneDrive / Google Drive / Dropbox / NetExplorer connected the library (see isSaveAllowed to veto)
    Your own source (customProviders) it implements saveOver / saveInto you
    Recents never — it is a history, not a place whoever owns the entry: opening one adopts that source, so Save writes back there
    • Whatever leaves the viewer is a PDF, so its name always ends in .pdf — including a name typed into Save as.
    • Opening an image or a text file creates a document (the viewer renders it into a new PDF), exactly like starting a blank one. It is not that file: it carries no path and no source, so Save cannot overwrite the original — it asks where to put the PDF instead.
    • After a Save as, the chosen file becomes the document's source: the next Save overwrites it, not the file the document started from.
    • A document that carries a path but no source of its own belongs to the device at that path — that is what a plain disk open is. It is only a source where your deviceConfig writes files; in a browser page a path locates nothing, so nothing is inferred from it.
    • Document properties shows the current source under File location — the same place Save writes to.
    Option Effect
    deviceConfig.isSaveAllowed(file, target, claims) Veto before the document is written to the machine.
    *Config.isSaveAllowed(file, target, claims) (the built-in drives) Veto before the library uploads.
    customProviders[].saveOver / .saveInto Turns your own source into a save target; both receive claims.
    onExportFile(file, claims) Intercepts every export, including the browser-side device target.
    layoutConfig.navigationBar.controls.downloadButton Shows or hides the Download CTA.
    layoutConfig.topBar.controls.mainMenu.options.save / .saveAs Shows or hides the menu items.