# Own the download: export, upload to S3, hand back a link

Hide or take over the SDK's own Download, export the edited document through `viewer.createExporter()`, upload it to your bucket through a presigned URL, then give the person a link from your backend.

- Level: Most requested
- Uses: `toolbar.download`, `callbacks.onDownload`, `viewer.createExporter()`, `exporter.uploadTo()`, `exporter.getBlob()`, `exportCompleted`, `viewer.markSaved()`

The viewer produces the finished PDF; where it goes is your decision. Two ways to start are shown, because they suit different products: keep the toolbar button and take over what it does with `callbacks.onDownload`, or hide it with `toolbar.download: false` and put a Submit action of your own in the toolbar.

## What you get

- The SDK's Download dialog and flow never run; your function does.
- The exported bytes carry what the viewer holds: the form values and signatures the user entered, and Edit Text's pending edits, written in first.
- A whole-document export clears `viewer.hasUnsavedChanges`; whether the upload succeeded is yours to track.

## 1. Hide the button, or take it over

`toolbar.download: false` hides the built-in Download. `callbacks.onDownload` keeps the button and runs your function instead of the SDK's dialog and flow (after `forms.validateRequired`, when that is on). The S3 example in Framework integrations hides Print and Download and adds a toolbar action; `uploadTo()` sends `Content-Type: application/pdf` unless `headers` overrides it.

`viewer.ts`

```ts
const viewer = new PDFViewer(container, doc, {
  toolbar: {
    print: false,
    download: false,
    actions: [
      {
        id: 'submit',
        label: 'Submit Form',
        icon: 'done',
        variant: 'primary',
        onClick: async () => {
          const presignedUrl = await fetchPresignedUrl(); // your API
          const exporter = viewer.createExporter();
          const res = await exporter.uploadTo(presignedUrl, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/pdf' },
          });
          if (!res.ok) {
            showError(res.status); // uploadTo does not reject on an HTTP error
            return;
          }
          showSuccessNotification();
        },
      },
    ],
  },
});

// Or keep the toolbar's Download and take over what it does:
const viewer2 = new PDFViewer(container, doc, {
  callbacks: { onDownload: () => void submit() },
});
```

> A rejected promise from an action's `onClick` is logged and reported through the `error` event. Actions are set when the viewer is built and cannot be disabled or relabeled later.

## 2. Export the bytes yourself when the upload needs more than a PUT

`getBlob()` returns the same export as a `Blob`, for an upload with progress of your own or a multipart form. Every export method takes `flatten`, `invalidateSignatures`, `pages`, `onProgress(done, total)` and `signal`; a `flatten` left out comes from the viewer's `export` option. A flattening export of a signed document is refused with `INVALID_STATE` unless `invalidateSignatures: true` is given.

`save.ts`

```ts
const controller = new AbortController();

const blob = await viewer.createExporter().getBlob({
  flatten: true,
  onProgress: (done, total) => progress.set(done / total),
  signal: controller.signal, // aborting rejects with ABORTED
});

const res = await fetch(presignedUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/pdf' },
  body: blob,
});
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
```

## 3. Know when the export finished, and when it is saved

`exportCompleted` fires on the viewer for the toolbar's Download and for every exporter from `viewer.createExporter()`, with `sizeBytes` and `partial` (`true` for a page range). An export of the whole document clears `hasUnsavedChanges` by itself; nothing fires when it goes back to `false`, so `exportCompleted` is that moment. Only when you save another way (`doc.save()`, an upload of bytes you already had) do you call `viewer.markSaved()` yourself.

`save.ts`

```ts
viewer.on('exportCompleted', ({ sizeBytes, partial }) => {
  if (!partial) uploadStatus.set(`Exported ${sizeBytes} bytes`);
});

// Saving through doc.save() instead: the viewer cannot see it, so tell it.
const bytes = await doc.save();
await fetch('/api/documents/42', { method: 'PUT', body: bytes.slice() });
viewer.markSaved();
```

> A signature drawn into a form field on screen is **not** in `doc.save()`: it lives in the viewer until `viewer.createExporter()` writes it. When a viewer is on screen and you want everything the user sees, use the exporter.

## Gotchas

- `uploadTo()` resolves to the `fetch` `Response` and does **not** reject on an HTTP error status: check `response.ok`. It rejects with `NETWORK_ERROR` when the request could not be sent at all (the address, the network, or a CORS refusal: the response must allow your origin) and `ABORTED` when `signal` aborted it.
- `forms.validateRequired` holds back the toolbar's Download while a required field is empty, with or without your `onDownload`; `viewer.createExporter()` is not held back. Gate it yourself with `doc.forms.getValues()` and the fields.
- A change made while an export ran is not in the bytes and stays unsaved, and an undo back to the saved state still counts.
- Each export method rejects with `FEATURE_NOT_LICENSED` when the license lacks `export`, `INVALID_ARGUMENT` when `pages` is empty or names a page the document does not have, `EXPORT_FAILED` when the document cannot be written, and `DOCUMENT_CLOSED` after `doc.close()`.
- The presigned URL's origin is a request of your own: it needs to be in your page's `connect-src`.

## Related

- [PDFExporter](https://kernstra.com/docs/web-sdk/api/pdfexporter)
- [Export options](https://kernstra.com/docs/web-sdk/config/export)
- [Callbacks](https://kernstra.com/docs/web-sdk/config/callbacks)
- [Unsaved changes done right](https://kernstra.com/docs/web-sdk/recipes/unsaved-changes)
- [S3 presigned URL upload](https://kernstra.com/docs/web-sdk/guides/integrations#s-s3-presigned-url-upload)

Web page: https://kernstra.com/docs/web-sdk/recipes/custom-download-s3
