{"_id":"pdfstudio","_rev":"2-31f1302d53564ac515a961c72625439b","name":"pdfstudio","dist-tags":{"latest":"0.4.0"},"versions":{"0.3.0":{"name":"pdfstudio","version":"0.3.0","keywords":["pdf","wasm","webassembly","qpdf","encrypt","decrypt","merge","split","rotate","browser","client-side"],"author":{"url":"https://github.com/fayazara","name":"Fayaz Ahmed"},"license":"Apache-2.0","_id":"pdfstudio@0.3.0","maintainers":[{"name":"fayazahmed","email":"fayazara@gmail.com"}],"homepage":"https://github.com/fayazara/pdfstudio#readme","bugs":{"url":"https://github.com/fayazara/pdfstudio/issues"},"dist":{"shasum":"ce9e20840f8884a4a99d673fe95002e1328e8061","tarball":"https://registry.npmjs.org/pdfstudio/-/pdfstudio-0.3.0.tgz","fileCount":16,"integrity":"sha512-T2oBUaU6feWSqSkfonxkw5p3TnARm9A2nj0AhyO7mKs/e3hc2FqV5PD1n32+5f3YNPcjMBynHV3B744qPaqvJg==","signatures":[{"sig":"MEUCIG1ubL5o3QGdzKiFyo/KrDilzI/mg7TGk2cqhQkkjw4AAiEA9o4peTit828Rq2dpVYTvvF0/JIMsLgyaUHOSc+3Nk1Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":2382811},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./qpdf.wasm":"./dist/wasm/qpdf.wasm"},"gitHead":"e2e128293a961518b8b2c997c4d3a85e6a38c548","scripts":{"demo":"vite demo --open","test":"vitest run","build":"tsc -p tsconfig.build.json && npm run copy:wasm","copy:wasm":"mkdir -p dist/wasm && cp src/wasm/qpdf.js src/wasm/qpdf.wasm src/wasm/qpdf.d.ts dist/wasm/","build:wasm":"bash scripts/build-wasm.sh"},"_npmUser":{"name":"fayazahmed","email":"fayazara@gmail.com"},"repository":{"url":"git+https://github.com/fayazara/pdfstudio.git","type":"git"},"_npmVersion":"11.16.0","description":"Client-side PDF toolkit powered by qpdf compiled to WebAssembly. Lock, unlock, change/remove passwords, merge, split, and rotate PDFs entirely in the browser — no server, files never leave the device.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","_hasShrinkwrap":false,"devDependencies":{"vite":"^6.0.0","vitest":"^3.0.0","typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/pdfstudio_0.3.0_1783750599649_0.5522919791124461","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"pdfstudio","version":"0.4.0","description":"Client-side PDF toolkit powered by qpdf compiled to WebAssembly. Lock, unlock, change/remove passwords, merge, split, and rotate PDFs entirely in the browser — no server, files never leave the device.","type":"module","license":"Apache-2.0","sideEffects":false,"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./qpdf.wasm":"./dist/wasm/qpdf.wasm"},"scripts":{"build:wasm":"bash scripts/build-wasm.sh","build":"tsc -p tsconfig.build.json && npm run copy:wasm","copy:wasm":"mkdir -p dist/wasm && cp src/wasm/qpdf.js src/wasm/qpdf.wasm src/wasm/qpdf.d.ts dist/wasm/","test":"vitest run","demo":"vite demo --open"},"keywords":["pdf","wasm","webassembly","qpdf","encrypt","decrypt","merge","split","rotate","browser","client-side"],"devDependencies":{"typescript":"^5.6.0","vite":"^6.0.0","vitest":"^3.0.0"},"author":{"name":"Fayaz Ahmed","url":"https://github.com/fayazara"},"repository":{"type":"git","url":"git+https://github.com/fayazara/pdfstudio.git"},"homepage":"https://github.com/fayazara/pdfstudio#readme","bugs":{"url":"https://github.com/fayazara/pdfstudio/issues"},"engines":{"node":">=18"},"gitHead":"c5c1f2d9f378199d1e2d333dbe4ca20e9ff737ad","_id":"pdfstudio@0.4.0","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-yFHlp/VHxKmrYxYA45ACkh24c4mCBa/Al3EmajnlBnN9FzfynZz63sR2do2kBH34XoTAQSXy+ywijAcq0DUcSQ==","shasum":"1bade3ca5d3cef54b0c0c4056f2b0a2520a35d1f","tarball":"https://registry.npmjs.org/pdfstudio/-/pdfstudio-0.4.0.tgz","fileCount":16,"unpackedSize":2384314,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD/pnuMdRMwffBYHTvMAM5V93Iv6C61tORJNpIvSlubYwIgakxPBUtysw6GSQxAjM4v22FwX/ZJQ43vTsq3PqJfWBA="}]},"_npmUser":{"name":"fayazahmed","email":"fayazara@gmail.com"},"directories":{},"maintainers":[{"name":"fayazahmed","email":"fayazara@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pdfstudio_0.4.0_1783760191869_0.14222223783685406"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-11T06:16:39.561Z","modified":"2026-07-11T08:56:32.124Z","0.3.0":"2026-07-11T06:16:39.823Z","0.4.0":"2026-07-11T08:56:32.008Z"},"bugs":{"url":"https://github.com/fayazara/pdfstudio/issues"},"author":{"name":"Fayaz Ahmed","url":"https://github.com/fayazara"},"license":"Apache-2.0","homepage":"https://github.com/fayazara/pdfstudio#readme","keywords":["pdf","wasm","webassembly","qpdf","encrypt","decrypt","merge","split","rotate","browser","client-side"],"repository":{"type":"git","url":"git+https://github.com/fayazara/pdfstudio.git"},"description":"Client-side PDF toolkit powered by qpdf compiled to WebAssembly. Lock, unlock, change/remove passwords, merge, split, and rotate PDFs entirely in the browser — no server, files never leave the device.","maintainers":[{"name":"fayazahmed","email":"fayazara@gmail.com"}],"readme":"# pdfstudio\n\n[![npm](https://img.shields.io/npm/v/pdfstudio)](https://www.npmjs.com/package/pdfstudio)\n[![license](https://img.shields.io/npm/l/pdfstudio)](./LICENSE)\n\n**A client-side PDF toolkit for the browser.** Lock, unlock, change or remove\npasswords, merge, split, extract, and rotate PDFs — with nothing leaving the\ndevice. Powered by [qpdf](https://qpdf.sourceforge.io/) compiled to\nWebAssembly, wrapped in a small, fully typed TypeScript API.\n\n```ts\nimport { createPdfToolkit } from 'pdfstudio';\n\nconst pdf = await createPdfToolkit();\n\nconst locked   = await pdf.lock(file, { userPassword: 'hunter2' });\nconst unlocked = await pdf.unlock(locked, { password: 'hunter2' });\nconst merged   = await pdf.merge([a, b, c]);\nconst pages    = await pdf.split(merged);            // one Uint8Array per page\nconst rotated  = await pdf.rotate(file, { angle: 90 });\n```\n\n## Why\n\nEvery \"unlock PDF\" / \"merge PDF\" site on the internet uploads your documents\nto someone's server. That's a strange default for what is fundamentally a\nlocal file transformation — often of documents (contracts, statements, IDs)\nyou'd least want to upload. The web platform is perfectly capable of doing\nthis work itself; what was missing was a proper library.\n\n`pdfstudio` is qpdf — the same battle-tested C++ engine Linux distributions\nhave shipped for 15+ years — compiled to a 2.1 MB WebAssembly binary, with a\ntypesafe API in front of it. Everything runs in the page (or a worker, or\nNode). No uploads, no server, no telemetry.\n\n## Features\n\n| | |\n|---|---|\n| 🔒 **Lock** | AES-256 encryption (also 128-bit AES and legacy 40-bit RC4), user + owner passwords, granular permissions (printing, modification, extraction) |\n| 🔓 **Unlock** | Decrypt with the user *or* owner password, removing all restrictions |\n| 🔁 **Change password** | Re-encrypt with a new password in a single pass |\n| 🧹 **Remove password** | Produce an unencrypted copy |\n| ➕ **Merge** | Combine whole documents or page selections, mixing encrypted and plain sources |\n| ✂️ **Split** | One document per page, or N pages per document |\n| 🎯 **Extract pages** | Pull any page selection into a new document |\n| 🔄 **Rotate** | Relative or absolute rotation, per page range |\n| 🗑 **Delete pages** | Remove a selection, keep the rest — plus reverse page order |\n| 🂠 **Collate** | Interleave pages from multiple documents (fronts + backs of a scan) |\n| 💧 **Watermark** | Overlay or underlay pages from another PDF — stamps, letterheads |\n| 🗜 **Compress** | Lossless stream recompression + object streams; linearize for fast web view |\n| 🩹 **Repair** | Reconstruct damaged cross-reference tables and recoverable corruption |\n| 📎 **Attachments** | Add, list, extract, and remove embedded files |\n| 🫓 **Flatten** | Bake annotations & form fields into page content |\n| 🔍 **Inspect** | `getInfo()`: PDF version, page count, encryption scheme & permissions, attachments |\n| 🖼 **Images → PDF** | Build a PDF from JPEGs in pure TypeScript (no recompression) |\n| 🛠 **Escape hatch** | Run any qpdf CLI invocation via `raw()` |\n\nWorks in browsers (main thread or Web Worker), in Node.js ≥ 18, and on\nCloudflare Workers — same API everywhere.\n\n## Install\n\n```sh\nnpm install pdfstudio\n```\n\nThe package ships ESM + type declarations + `qpdf.wasm`. Modern bundlers\n(Vite, webpack 5, Rollup, esbuild) pick up the wasm asset automatically via\n`import.meta.url`. If yours doesn't, or you serve the wasm from a CDN, pass\nthe location explicitly:\n\n```ts\nconst pdf = await createPdfToolkit({ wasmUrl: '/assets/qpdf.wasm' });\n```\n\n## Usage\n\n### Setup\n\n```ts\nimport { createPdfToolkit } from 'pdfstudio';\n\n// Loads + compiles the wasm once (~2 MB). Create one and reuse it.\nconst pdf = await createPdfToolkit();\n```\n\nEvery operation accepts a `Uint8Array`, `ArrayBuffer`, or `Blob`/`File`\n(straight from `<input type=\"file\">` or drag-and-drop) and resolves to a\n`Uint8Array` of the resulting document:\n\n```ts\nconst input = document.querySelector<HTMLInputElement>('#file');\nconst bytes = await pdf.rotate(input.files[0], { angle: 90 });\n\n// Download it:\nconst url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));\n```\n\n### Lock (encrypt)\n\n```ts\nconst locked = await pdf.lock(doc, {\n  userPassword: 'open-me',        // required to open the file\n  ownerPassword: 'admin-only',    // full access; defaults to userPassword\n  keyLength: 256,                 // 256 (default) | 128 | 40\n  permissions: {\n    print: 'low',                 // 'full' | 'low' | 'none'\n    modify: 'none',               // 'all' | 'annotate' | 'form' | 'assembly' | 'none'\n    extract: false,               // allow copying text/images\n  },\n});\n```\n\nAn empty `userPassword: ''` with a real `ownerPassword` creates a file that\nopens without a prompt but still enforces the permission restrictions.\n\n### Unlock / remove password / change password\n\n```ts\nconst open = await pdf.unlock(locked, { password: 'open-me' });\n// removePassword() is an alias of unlock()\n\nconst rekeyed = await pdf.changePassword(locked, {\n  currentPassword: 'open-me',\n  newPassword: 'new-secret',\n});\n```\n\nA wrong password rejects with `PdfPasswordError` (a subclass of `PdfError`),\nso you can distinguish \"bad password\" from \"corrupt file\":\n\n```ts\nimport { PdfPasswordError } from 'pdfstudio';\n\ntry {\n  await pdf.unlock(doc, { password: guess });\n} catch (e) {\n  if (e instanceof PdfPasswordError) askAgain();\n  else throw e;\n}\n```\n\n### Merge\n\n```ts\n// Whole documents, in order:\nconst merged = await pdf.merge([a, b, c]);\n\n// Page selections, including encrypted sources:\nconst report = await pdf.merge([\n  { data: cover },\n  { data: body, pages: '2-9' },\n  { data: appendix, password: 'pw', pages: ['1', 'z'] }, // z = last page\n]);\n```\n\n### Split & extract\n\n```ts\nconst singlePages = await pdf.split(doc);                    // Uint8Array[]\nconst chunks      = await pdf.split(doc, { pagesPerFile: 10 });\nconst excerpt     = await pdf.extractPages(doc, { pages: '2-5,9' });\n```\n\n### Rotate\n\n```ts\nawait pdf.rotate(doc, { angle: 90 });                  // all pages, clockwise\nawait pdf.rotate(doc, { angle: -90, pages: '1-3' });   // counter-clockwise\nawait pdf.rotate(doc, { angle: 180, absolute: true }); // set exact rotation\n```\n\n### Delete, reverse, collate\n\n```ts\nconst trimmed  = await pdf.deletePages(doc, { pages: '2-3' });\nconst backward = await pdf.reversePages(doc);\n\n// Interleave: page 1 of A, page 1 of B, page 2 of A, … Great for\n// combining separately scanned fronts and backs:\nconst combined = await pdf.collate([fronts, { data: backs, pages: 'z-1' }]);\n```\n\n### Watermark / stamp\n\nOverlay (or underlay) pages from another PDF. `repeat: 1` tiles a\nsingle-page stamp across the whole document:\n\n```ts\nconst stamped = await pdf.watermark(doc, confidentialStamp, { repeat: 1 });\nconst letterheaded = await pdf.watermark(doc, letterhead, {\n  mode: 'underlay',   // draw behind the page content\n  to: '1',            // first page only\n});\n```\n\n### Compress, linearize, repair\n\n```ts\nconst smaller = await pdf.compress(doc);                    // lossless\nconst fast    = await pdf.linearize(doc);                   // fast web view\nconst fixed   = await pdf.repair(brokenDoc);                // rebuild xref\n```\n\n### Attachments\n\n```ts\nconst withFile = await pdf.addAttachment(doc, {\n  data: jsonBytes,\n  name: 'invoice.json',\n  mimeType: 'application/json',\n});\nawait pdf.listAttachments(withFile);                        // [{ name: 'invoice.json', … }]\nconst bytes = await pdf.getAttachment(withFile, { name: 'invoice.json' });\nconst clean = await pdf.removeAttachment(withFile, { name: 'invoice.json' });\n```\n\n### Flatten\n\nBake annotations and form-field appearances into the page content —\nuseful before printing, splitting, or sharing:\n\n```ts\nconst flat = await pdf.flatten(doc);\n```\n\n### Inspect\n\n```ts\nawait pdf.pageCount(doc);          // number\nawait pdf.isEncrypted(doc);        // boolean\nawait pdf.requiresPassword(doc);   // false for empty-user-password files\n\nconst info = await pdf.getInfo(locked, { password: 'pw' });\n// {\n//   pdfVersion: '2.0', pageCount: 12, encrypted: true,\n//   encryption: {\n//     bits: 256, method: 'AESv3',\n//     userPasswordMatched: true, ownerPasswordMatched: false,\n//     permissions: { print: false, extract: false, modify: false, … },\n//   },\n//   attachments: [],\n// }\n```\n\n### Images → PDF\n\n`imagesToPdf` needs no wasm at all — JPEG data is embedded verbatim\n(no recompression, no quality loss), one page per image:\n\n```ts\nimport { imagesToPdf } from 'pdfstudio';\n\nconst album = await imagesToPdf([scan1, scan2, photo], { dpi: 300 });\n```\n\n### Escape hatch\n\nAnything else qpdf can do is reachable through `raw()`. Inputs are staged as\n`$in0`, `$in1`, …; write output to `$out`:\n\n```ts\n// Two-up page layout? n-up is about the only thing qpdf can't do —\n// but e.g. splitting into groups of pages after each bookmark, etc.:\nconst out = await pdf.raw([doc], ['--pages', '$in0', '1-z:odd', '--', '--empty', '$out']);\n```\n\n### Page selections\n\nAnywhere a `pages` option appears, use qpdf's\n[page range syntax](https://qpdf.readthedocs.io/en/stable/cli.html#page-ranges):\n\n| Selection | Meaning |\n|---|---|\n| `5` | page 5 |\n| `'1-5'` | pages 1–5 |\n| `'1,3,5-9'` | union, in order |\n| `'z'` | last page |\n| `'r2'` | second-to-last |\n| `'z-1'` | all pages, reversed |\n| `'1-9:odd'` | odd positions within the range |\n| `'1-z,x3-4'` | everything except pages 3–4 |\n| `[1, '4-8', 'z']` | arrays mix numbers and ranges |\n\n### Web Workers\n\nOperations are synchronous inside the wasm and run on the calling thread. For\nlarge documents, load the toolkit inside a Worker to keep the UI responsive —\nthe API works there unchanged, and `Uint8Array` results transfer cheaply via\n`postMessage`.\n\n### Cloudflare Workers\n\nWorks on the edge too. Workers forbid runtime wasm compilation, so import\nthe wasm as a module (compiled at deploy time) and pass it in:\n\n```ts\nimport { createPdfToolkit } from 'pdfstudio';\nimport qpdfWasm from 'pdfstudio/qpdf.wasm';\n\nconst pdf = await createPdfToolkit({ wasmModule: qpdfWasm });\n```\n\nA runnable example (self-test route + a `POST /unlock` endpoint) lives in\n[`examples/cloudflare-worker`](examples/cloudflare-worker):\n\n```sh\ncd examples/cloudflare-worker\nnpm install\nnpm run dev     # wrangler dev → http://localhost:8787\n```\n\nMind the platform limits: PDF work needs real CPU time (the paid tier's\nbudget is comfortable, the free tier's ~10 ms is not), and MEMFS lives in\nthe wasm heap, so very large documents press against the 128 MB memory cap.\n\n## Demo\n\n```sh\nnpm install\nnpm run demo\n```\n\nOpens a small UI exercising every operation: drop PDFs in, lock/unlock,\nmerge, split, rotate, download results. All local.\n\n## Building the wasm from source\n\nThe published package includes the compiled `qpdf.wasm`; you only need this\nto upgrade qpdf or change build flags.\n\nRequirements: [Emscripten](https://emscripten.org) and CMake\n(`brew install emscripten cmake`).\n\n```sh\nnpm run build:wasm   # downloads qpdf sources, compiles → src/wasm/\nnpm test             # 43 end-to-end tests through the real wasm\nnpm run build        # emits dist/ (ESM + d.ts + wasm)\n```\n\nBuild details, for the curious:\n\n- qpdf 12.3.2 with its built-in **native crypto** provider — no OpenSSL in\n  the binary; AES/SHA2/MD5/RC4 are qpdf's own implementations.\n- zlib and libjpeg come from Emscripten's ports.\n- Compiled with wasm-native exception handling (`-fwasm-exceptions`) since\n  qpdf uses C++ exceptions for all error reporting.\n- Modularized ES6 output (`-sMODULARIZE -sEXPORT_ES6`) with an in-memory\n  filesystem (MEMFS); each operation stages files in a scratch directory,\n  invokes qpdf's CLI `main()`, and reads the result back.\n- Random data comes from the platform's CSPRNG\n  (`crypto.getRandomValues` via Emscripten).\n\n## License\n\nApache-2.0, same as [qpdf](https://github.com/qpdf/qpdf) itself. The wasm\nbinary also contains zlib (zlib license) and libjpeg (IJG license).\n","readmeFilename":"README.md"}