@soulehshaikh99/ghostscript-wasm
v0.1.0
Published
Ghostscript (pdfwrite) compiled to WASM
Readme
@soulehshaikh99/ghostscript-wasm ·

Ghostscript 10.08.0 compiled to WebAssembly, built with only the pdfwrite device. Runs in the browser, in Web Workers and in Node.js.
Made for PDF to PDF/A conversion (PDF/A-1b, 2b, 3b) entirely client side, but any pdfwrite job works (rewrite, downsample, convert colors, ...).
What's inside
| Component | Version |
| --- | --- |
| Ghostscript | 10.08.0 (release tarball, SHA-256 pinned in Dockerfile) |
| Emscripten (emsdk) | 6.0.13 |
| Bundled libs | zlib, libpng, libjpeg, lcms2mt, FreeType, OpenJPEG, jbig2dec (from the Ghostscript tarball) |
| Devices | pdfwrite only |
| Fonts | URW base35 (the standard 35 PostScript fonts) |
| ICC profiles | Ghostscript's iccprofiles/ (incl. srgb.icc) |
Fonts, init files, CMaps and ICC profiles are compiled into the binary's %rom% filesystem, so no extra files are fetched at runtime. The CJK fallback font (DroidSansFallback.ttf) is left out to save 3.9 MB: PDFs that use non-embedded CJK fonts may render with substitutes.
Size: gs.wasm is about 16 MB (about 9 MB gzipped).
Installation
npm install @soulehshaikh99/ghostscript-wasmQuick start (Node.js)
const createGs = require("@soulehshaikh99/ghostscript-wasm");
const fs = require("fs");
const gs = await createGs({ print: console.log, printErr: console.error });
gs.FS.writeFile("/in.pdf", fs.readFileSync("in.pdf"));
const code = gs.callMain([
"-dSAFER", "-dBATCH", "-dNOPAUSE", "-sDEVICE=pdfwrite",
"-sOutputFile=/out.pdf", "/in.pdf",
]);
if (code === 0) fs.writeFileSync("out.pdf", gs.FS.readFile("/out.pdf"));callMain returns gs's exit code (it never calls process.exit) and can be called again on the same instance.
PDF/A
PDF/A needs an OutputIntent. Write a definition file that points at the sRGB profile in %rom%, and pass it before the input:
/ICCProfile (%rom%iccprofiles/srgb.icc) def
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
[{icc_PDFA} << /N 3 >> /PUT pdfmark
[{icc_PDFA} ICCProfile (r) file /PUT pdfmark
[/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark
[{OutputIntent_PDFA} <<
/Type /OutputIntent /S /GTS_PDFA1
/DestOutputProfile {icc_PDFA}
/OutputConditionIdentifier (sRGB IEC61966-2.1)
/Info (sRGB IEC61966-2.1)
/RegistryName (http://www.color.org)
>> /PUT pdfmark
[{Catalog} << /OutputIntents [ {OutputIntent_PDFA} ] >> /PUT pdfmarkgs.FS.writeFile("/PDFA_def.ps", pdfaDef);
gs.callMain([
"-dSAFER", "-dBATCH", "-dNOPAUSE", "-sDEVICE=pdfwrite",
"-dPDFA=2", // 1, 2 or 3
"-dPDFACompatibilityPolicy=1", // drop what PDF/A forbids, keep the file compliant
"-sColorConversionStrategy=RGB",
"-sOutputFile=/out.pdf", "/PDFA_def.ps", "/in.pdf",
]);Patched for PDF/A (patches/pdfa-actions.patch): with -dPDFA, actions PDF/A forbids (JavaScript, Launch, Sound, Movie, ResetForm, ImportData, Hide, SetOCGState, Rendition, Trans, GoTo3DView, ...) are removed from the document, links and outlines, with a **** Warning: <type> action not permitted in PDF/A, removed. line each; annotations lose their additional actions (/AA). Upstream gs copies these through and the output fails PDF/A validation.
PDFACompatibilityPolicy: 0 keeps forbidden features (output claims PDF/A but is not compliant), 1 drops them with a warning each, 2 aborts. Ghostscript produces conformance level b only.
Browser / Web Worker
import createGs from "@soulehshaikh99/ghostscript-wasm";
import wasmUrl from "@soulehshaikh99/ghostscript-wasm/dist/gs.wasm?url"; // Vite
const gs = await createGs({ locateFile: () => wasmUrl, print, printErr });Run it in a Web Worker: a conversion blocks the thread it runs on. Large inputs can be mounted with WORKERFS instead of copied into memory.
Options
createGs(options) accepts locateFile, print, printErr, thisProgram (default gs), preRun, wasmBinary and instantiateWasm. The instance exposes callMain, FS, NODEFS, WORKERFS and ENV. Types: dist/gs.d.ts.
Building from source
Requires Docker.
docker build -t soulehshaikh99/ghostscript-wasm .
docker compose up # copies dist/ out of the image, runs npm ci && npm testOr copy dist out by hand: docker create the image, then docker cp <id>:/opt/gs/. dist.
To update Ghostscript, change GS_VERSION, GS_TAG and GS_SHA256 in the Dockerfile. build.sh configures gs for Emscripten (native gcc builds the aux tools), applies patches/*.patch and links bin/gs.js + bin/gs.wasm.
License
Ghostscript is © Artifex Software, Inc. and licensed under the GNU AGPL v3 (see also LICENSE-ghostscript). This package, its build scripts and JS glue are distributed under the same license. If you ship it in a network service, you must offer the corresponding source. A commercial license is available from Artifex.
