Most web applications still handle PDF manipulation by uploading the user's file to a remote server, invoking Ghostscript or QPDF in a container, and streaming the result back. For contracts, tax filings, medical records, or identity documents, that round-trip introduces unnecessary storage liability and network latency.
When building the browser-side PDF tools on Sunasty, the goal was to execute every operation—merging, page reordering, stream compression, deskewing, and metadata removal—inside the user's tab using Web Workers, ArrayBuffer transfers, and WebAssembly.
1. Zero-Copy Buffer Transfer to Web Workers
Parsing an 80 MB scanned PDF on the main UI thread will freeze scrolling and input events for several seconds. When a user selects a file via an <input type="file"> or drag-and-drop target, reading it as an ArrayBuffer and passing it in the transfer list of Worker.postMessage moves ownership of the memory block in constant time without duplicating bytes in RAM:
// Main thread: transfer the underlying ArrayBuffer to the PDF worker
async function dispatchPdfTask(file, operation, options = {}) {
const buffer = await file.arrayBuffer();
const worker = new Worker(new URL('./pdf-worker.js', import.meta.url), {
type: 'module'
});
return new Promise((resolve, reject) => {
worker.onmessage = (event) => {
worker.terminate();
if (event.data.error) {
reject(new Error(event.data.error));
} else {
resolve(new Blob([event.data.outputBuffer], { type: 'application/pdf' }));
}
};
worker.onerror = (err) => {
worker.terminate();
reject(err);
};
// Passing [buffer] in the second argument detaches it from the main thread
worker.postMessage({ operation, buffer, options }, [buffer]);
});
}
2. Merging Object Graphs Without Corrupting Cross-References
A PDF file is a directed graph of numbered indirect objects (obj ... endobj) indexed by a cross-reference table (xref) or cross-reference stream at the tail of the file. Concatenating two PDFs requires copying the page tree nodes (/Type /Page) and their transitive dependencies (fonts, form XObjects, color spaces, content streams) into a target document and assigning fresh object IDs so references do not collide.
In the Sunasty PDF Merger, we iterate over each source file inside the Worker, copy the requested page indices into a unified document catalog, and serialize with object streams enabled (useObjectStreams: true) so repeated structural dictionaries are compressed together:
import { PDFDocument } from 'pdf-lib';
export async function mergePdfBuffers(sourceBuffers) {
const mergedPdf = await PDFDocument.create();
for (const buffer of sourceBuffers) {
const sourceDoc = await PDFDocument.load(buffer, {
ignoreEncryption: false,
updateMetadata: false
});
const pageIndices = sourceDoc.getPageIndices();
const copiedPages = await mergedPdf.copyPages(sourceDoc, pageIndices);
for (const page of copiedPages) {
mergedPdf.addPage(page);
}
}
const bytes = await mergedPdf.save({ useObjectStreams: true });
return bytes.buffer;
}
3. Inspecting and Purging /Info and XMP Metadata
PDF files store provenance in two distinct structures:
-
The Trailer
/InfoDictionary: Legacy key-value strings such as/Author,/Creator,/Producer,/CreationDate, and/ModDate. -
The Catalog
/MetadataStream: An uncompressed or Flate-encoded XML packet following the Adobe XMP specification (<x:xmpmeta>), which often retains the original Author name, operating system, and document history even after a user clears the standard properties dialog.
When sanitizing a document—for example in the Sunasty PDF Metadata Remover—modifying only the /Info dictionary leaves the XMP stream intact. Both references must be cleared:
import { PDFDocument, PDFName } from 'pdf-lib';
export async function stripPdfMetadata(inputBuffer) {
const pdfDoc = await PDFDocument.load(inputBuffer, { updateMetadata: false });
// 1. Clear standard Info dictionary keys
pdfDoc.setTitle('');
pdfDoc.setAuthor('');
pdfDoc.setSubject('');
pdfDoc.setKeywords([]);
pdfDoc.setProducer('');
pdfDoc.setCreator('');
// 2. Delete the XMP /Metadata stream reference from the document Catalog
const catalog = pdfDoc.catalog;
if (catalog.has(PDFName.of('Metadata'))) {
catalog.delete(PDFName.of('Metadata'));
}
// 3. Remove custom non-standard keys from the Info dictionary
const infoRef = pdfDoc.context.trailerInfo.Info;
if (infoRef) {
const infoDict = pdfDoc.context.lookup(infoRef);
if (infoDict && typeof infoDict.keys === 'function') {
for (const key of infoDict.keys()) {
infoDict.delete(key);
}
}
}
return (await pdfDoc.save({ useObjectStreams: true })).buffer;
}
4. Client-Side Compression and Image Downsampling
High-resolution RGB images embedded as raw Flate streams instead of baseline JPEG (/DCTDecode) are the most common cause of bloat.
In the Sunasty PDF Compressor, we apply two techniques:
-
Structural & Stream Recompression: Parsing indirect objects, removing unreferenced orphan objects, and packing non-stream objects into compressed
/ObjStmcontainers. -
Raster Downsampling (150 DPI / 200 DPI): Rendering each page at a controlled scale onto an
OffscreenCanvas, encoding to progressive JPEG at quality 0.75, and rebuilding a clean PDF container.
Interactive demonstration and tools available on Sunasty.
Top comments (0)