PDF Web Viewer
    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.

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

    Savechain(saveToOrigin: true)
    Save aschain(saveToOrigin: false)

    chain(saveToOrigin):
    saveToOrigin and the document has a writable source
    overwrite that file
    otherwise, settle on a destination:
    exactly one targetwrite there, no question asked
    more than one targetshow 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 sdk.openDocument({ file, path });
    • 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 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) Veto before the document is written to the machine.
    *Config.isSaveAllowed(file, target) (the three drives) Veto before the library uploads.
    customProviders[].saveOver / .saveInto Turns your own source into a save target.
    onExportFile(file) 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.