Export only selected annotations
Use this guide when you need to export only the annotations currently selected in the viewer, without leaking unrelated review data.
Build the subset export from the current annotation selection first. Don’t call instance.exportInstantJSON(), instance.exportXFDF(), or instance.exportPDF({ flatten: true }) directly on the full document instance if you need selected-only output.
Recommended API sequence
For selective export, use this sequence:
- Read the current selection with
instance.getSelectedAnnotations(). - If nothing is selected, stop and show a clear fallback message instead of exporting everything.
- Convert only the selected annotations to serializable objects with
NutrientViewer.Annotations.toSerializableObject(). - Build a selected-only Instant JSON payload.
- If you need XML Forms Data Format (XFDF) or a flattened PDF, load a second isolated instance with only that subset and export from there.
If you want the UI to react live to selection changes, also listen to annotationSelection.change.
This pattern keeps the export scope explicit and avoids accidentally including unrelated annotations.
Minimal runnable example
The following example shows one practical pattern:
- A visible viewer for selection
- A selected-only Instant JSON export
- A selected-only XFDF export
- A selected-only flattened PDF export
- A safe fallback when no annotations are selected
HTML
This HTML code sets up the viewer container, export buttons, and status message for the example. The JavaScript logic is in a separate index.js file.
<!doctype html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Export only selected annotations</title> <style> body { margin: 0; font-family: sans-serif; } .toolbar { display: flex; gap: 12px; align-items: center; padding: 12px 16px; border-bottom: 1px solid #d9d9d9; } .toolbar button { min-height: 40px; } #status { color: #555; } #viewer { height: calc(100vh - 65px); } </style> </head> <body> <div class="toolbar"> <button id="export-instant-json">Export selected Instant JSON</button> <button id="export-xfdf">Export selected XFDF</button> <button id="export-pdf">Export selected flattened PDF</button> <span id="status">No annotations selected.</span> </div>
<div id="viewer"></div>
<script src="/path/to/pspdfkit.js"></script> <script src="./index.js"></script> </body></html>JavaScript
This JavaScript code implements the logic for reading the current selection, building a selected-only Instant JSON payload, and exporting it directly or through an isolated subset instance for XFDF and PDF output.
let instance;const documentUrl = "/document.pdf";
function setStatus(message) { document.getElementById("status").textContent = message;}
function normalizeSelection(annotations) { if (!annotations) { return []; }
if (typeof annotations.toArray === "function") { return annotations.toArray().filter(Boolean); }
return Array.from(annotations).filter(Boolean);}
function getSelectedAnnotations() { return normalizeSelection(instance.getSelectedAnnotations());}
function getSelectedSubsetInstantJSON() { const selectedAnnotations = getSelectedAnnotations();
if (selectedAnnotations.length === 0) { return null; }
return { format: "https://pspdfkit.com/instant-json/v1", annotations: selectedAnnotations.map((annotation) => NutrientViewer.Annotations.toSerializableObject(annotation) ) };}
async function exportSelectedInstantJSON() { const instantJSON = getSelectedSubsetInstantJSON();
if (!instantJSON) { setStatus("Select at least one annotation before exporting."); return; }
const blob = new Blob([JSON.stringify(instantJSON, null, 2)], { type: "application/json" }); downloadBlob(blob, "selected-annotations.json"); setStatus(`Exported ${instantJSON.annotations.length} selected annotations as Instant JSON.`);}
async function exportFromIsolatedSubset(format) { const instantJSON = getSelectedSubsetInstantJSON();
if (!instantJSON) { setStatus("Select at least one annotation before exporting."); return; }
setStatus(`Preparing selected-only ${format.toUpperCase()} export.`);
const subsetInstance = await NutrientViewer.load({ document: documentUrl, baseUrl: `${window.location.origin}/assets/`, headless: true, instantJSON });
try { await subsetInstance.save();
if (format === "xfdf") { const xfdf = await subsetInstance.exportXFDF(); const blob = new Blob([xfdf], { type: "application/vnd.adobe.xfdf" }); downloadBlob(blob, "selected-annotations.xfdf"); setStatus(`Exported ${instantJSON.annotations.length} selected annotations as XFDF.`); return; }
if (format === "pdf") { const pdf = await subsetInstance.exportPDF({ flatten: true }); const blob = new Blob([pdf], { type: "application/pdf" }); downloadBlob(blob, "selected-annotations.pdf"); setStatus(`Exported ${instantJSON.annotations.length} selected annotations as a flattened PDF.`); } } finally { NutrientViewer.unload(subsetInstance); }}
function downloadBlob(blob, fileName) { const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url);}
async function loadViewer() { instance = await NutrientViewer.load({ container: "#viewer", document: documentUrl, baseUrl: `${window.location.origin}/assets/` });
instance.addEventListener("annotationSelection.change", (annotations) => { const selectedAnnotations = normalizeSelection(annotations);
if (selectedAnnotations.length === 0) { setStatus("No annotations selected."); return; }
setStatus(`${selectedAnnotations.length} annotation(s) selected.`); });
document .getElementById("export-instant-json") .addEventListener("click", exportSelectedInstantJSON); document .getElementById("export-xfdf") .addEventListener("click", () => exportFromIsolatedSubset("xfdf")); document .getElementById("export-pdf") .addEventListener("click", () => exportFromIsolatedSubset("pdf"));}
loadViewer().catch((error) => { setStatus("Viewer failed to load."); console.error(error);});Safe fallback behavior when no selection exists
When no annotations are selected, the safest behavior is to export nothing.
Choose one of the following recommended fallback behaviors:
- Disable selected-only export buttons until a selection exists.
- Show a message such as Select at least one annotation before exporting.
Avoid a fallback that silently exports all annotations, because that defeats the privacy boundary of a selected-only workflow.
Choosing the right selective export format
Use the format that matches the downstream requirement:
| Output | Use it when | Tradeoffs |
|---|---|---|
| Instant JSON | You want a compact selected-only payload for internal app workflows, custom persistence, or later reimport. | Best for internal data exchange. Not the default choice for third-party PDF tools. |
| XFDF | You need to exchange only the selected annotations with Adobe Acrobat or another XFDF-compatible system. | Better for interoperability, but less efficient than Instant JSON for internal app synchronization. |
| Flattened PDF | You need a final shareable file that visually contains only the selected annotations. | Output is no longer an editable annotation subset. Use it for delivery, not ongoing review. |
If you’re choosing a broader persistence strategy first, start with how to choose Instant JSON vs. XFDF vs. server-backed sync.
Comment-thread and data-scope policy
Selected-only export should define the scope explicitly.
Included by default
The workflow in this guide includes only the annotations that are currently selected and then serialized into the subset payload.
Excluded by default
The workflow excludes:
- Unselected annotations
- Unrelated comment threads
- Unrelated review metadata stored elsewhere in your app
- Any full-document export payload you would get from calling export APIs on the main viewer instance
Including related comment threads when required
If your application stores comments separately from annotation records — for example, through comments APIs or custom backend storage — you must add those related comments explicitly.
Recommended policy:
- Treat the selected annotation IDs as the export boundary.
- Find comments whose
rootIdbelongs to that selected set. - Append only those comments to the share payload you send to your backend or integration target.
- Do not include comments for annotations outside the selected set.
If you use Instant JSON comments, refer to the Instant JSON comments schema for the expected comment shape and scope.
Privacy and data-leakage cautions
The main leakage risk is exporting from the wrong instance.
For example:
- Calling
instance.exportInstantJSON()on the active viewer exports the full annotation state, not just the current selection. - Calling
instance.exportXFDF()on the active viewer exports the full XFDF state for the current document. - Calling
instance.exportPDF({ flatten: true })on the active viewer flattens all current annotations into the output PDF.
To keep the boundary safe, always export selected-only XFDF or PDF from an isolated subset instance loaded with only the chosen annotations.
Validation checklist
Use this checklist to confirm the export is truly selected-only:
- Select one annotation and verify that only that annotation appears in the exported artifact.
- Select multiple annotations and verify that only those selected IDs are present.
- Leave all annotations unselected and verify that export is blocked.
- If comments are included, verify that only comments whose
rootIdbelongs to the selected set are exported. - For flattened PDF output, verify that unselected annotations are absent from the resulting file.
- Review your backend payload or downloaded file before sharing it externally.
Related guides
- Importing and exporting annotations with Instant JSON
- Importing and exporting annotations in XFDF
- Importing and exporting annotations in a database
- Select PDF annotations using JavaScript
- Headless PDF annotation