{"_id":"@astlide/crispdf","name":"@astlide/crispdf","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"@astlide/crispdf","version":"0.0.1","description":"DOM → high-quality PDF in the browser: raster background + vector text overlay. Selectable, searchable, copyable text.","author":{"name":"Ryusei Hashimoto"},"keywords":["pdf","dom-to-pdf","html-to-pdf","browser","vector-text","raster","client-side"],"homepage":"https://github.com/r-hashi01/vellum#readme","repository":{"type":"git","url":"git+https://github.com/r-hashi01/vellum.git","directory":"packages/core"},"bugs":{"url":"https://github.com/r-hashi01/vellum/issues"},"license":"MIT","type":"module","engines":{"node":">=20"},"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"sideEffects":false,"dependencies":{"@pdf-lib/standard-fonts":"^1.0.0","fontkit":"^2.0.4","html-to-image":"^1.11.13","woff2-encoder":"^2.0.0"},"peerDependencies":{"pdfjs-dist":"^5.0.0"},"peerDependenciesMeta":{"pdfjs-dist":{"optional":true}},"devDependencies":{"@types/fontkit":"^2.0.9","@vitest/browser":"^4.1.5","@vitest/browser-playwright":"^4.1.5","pdfjs-dist":"^5.7.284","playwright":"^1.59.1","tsup":"^8.5.1","typescript":"^6.0.3","vite":"^8.0.10","vitest":"^4.1.5"},"publishConfig":{"access":"public"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit"},"_id":"@astlide/crispdf@0.0.1","_integrity":"sha512-37n7hAoXbi5/OJNiesFgpb6ZsSwM+IuM6DzN6XmBWSXh4QjCb7sSVLFRn97iQmkNB1W+UZ2EC/fTgExp4wBRCg==","_resolved":"/tmp/97c9c51758bba2dbf9f7b09006eec0d7/astlide-crispdf-0.0.1.tgz","_from":"file:astlide-crispdf-0.0.1.tgz","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-37n7hAoXbi5/OJNiesFgpb6ZsSwM+IuM6DzN6XmBWSXh4QjCb7sSVLFRn97iQmkNB1W+UZ2EC/fTgExp4wBRCg==","shasum":"6d042e13d893c3c3c1afab1570863428ab763e56","tarball":"https://registry.npmjs.org/@astlide/crispdf/-/crispdf-0.0.1.tgz","fileCount":6,"unpackedSize":318661,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDmW31L5x4PTxmQ3Kmi91sFyQFqSF21AEvRsYYArgNJuAIgcnVg6tAFPTJh6v1+yNdcihqfCdSRf/pfq05ky+gU89I="}]},"_npmUser":{"name":"r-hashi01","email":"r.hashimoto@dify.ai"},"directories":{},"maintainers":[{"name":"r-hashi01","email":"r.hashimoto@dify.ai"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/crispdf_0.0.1_1780134144154_0.4779288074394612"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-30T09:42:24.029Z","0.0.1":"2026-05-30T09:42:24.303Z","modified":"2026-05-30T09:42:24.482Z"},"maintainers":[{"name":"r-hashi01","email":"r.hashimoto@dify.ai"}],"description":"DOM → high-quality PDF in the browser: raster background + vector text overlay. Selectable, searchable, copyable text.","homepage":"https://github.com/r-hashi01/vellum#readme","keywords":["pdf","dom-to-pdf","html-to-pdf","browser","vector-text","raster","client-side"],"repository":{"type":"git","url":"git+https://github.com/r-hashi01/vellum.git","directory":"packages/core"},"author":{"name":"Ryusei Hashimoto"},"bugs":{"url":"https://github.com/r-hashi01/vellum/issues"},"license":"MIT","readme":"# crispdf\n\nBrowser-side DOM to PDF with high visual fidelity and selectable text.\n\n`crispdf` renders each page as a raster background, then overlays real PDF\ntext on top. The result keeps complex HTML/CSS visually intact while preserving\nsearch, selection, and copy/paste for text that can be mapped to a PDF font.\n\n## Install\n\n```sh\npnpm add @astlide/crispdf\n# npm i @astlide/crispdf\n# yarn add @astlide/crispdf\n```\n\n## Usage\n\n```ts\nimport { domToPdf } from '@astlide/crispdf'\n\nconst result = await domToPdf({\n  pages: document.querySelectorAll<HTMLElement>('[data-page]'),\n  source: { width: 800, height: 600 },\n  output: { width: 800, height: 600, unit: 'pt' },\n  rasterFormat: 'jpeg',\n  jpegQuality: 0.85,\n  onProgress: (pageIndex, totalPages) => {\n    console.log(`page ${pageIndex}/${totalPages}`)\n  },\n  onTiming: (event) => {\n    console.log(event)\n  },\n})\n\nif (result.warnings.length > 0) {\n  console.warn(result.warnings)\n}\n\nconst url = URL.createObjectURL(result.blob)\n```\n\n## API\n\n```ts\nfunction domToPdf(opts: DomToPdfOptions): Promise<DomToPdfResult>\n\ninterface DomToPdfOptions {\n  pages: ArrayLike<HTMLElement>\n  source: { width: number; height: number }\n  output: { width: number; height: number; unit: 'pt' }\n  rasterFormat?: 'jpeg' | 'png' // default 'jpeg'\n  jpegQuality?: number // default 0.85\n  onProgress?: (pageIndex: number, totalPages: number) => void\n  onTiming?: (event: TimingEvent) => void\n  /** Opt-in visual self-check; off by default. Requires `pdfjs-dist`. */\n  selfCheck?: { enabled: boolean; threshold?: number }\n}\n\ntype TimingEvent =\n  | { stage: 'walk'; page: number; durationMs: number }\n  | { stage: 'capture'; page: number; durationMs: number }\n  | { stage: 'fonts'; durationMs: number }\n  | { stage: 'emit'; durationMs: number }\n  | { stage: 'selfCheck'; durationMs: number }\n\ninterface DomToPdfResult {\n  blob: Blob\n  warnings: string[]\n  /** Present only when `selfCheck.enabled` was set. */\n  selfCheck?: SelfCheckPageResult[]\n}\n\ninterface SelfCheckPageResult {\n  page: number // 1-indexed\n  diff: number // visual difference, 0..1\n  exceeded: boolean // diff > threshold\n}\n```\n\n## How It Works\n\n1. Walk visible DOM text and record line rectangles through `Range`, including\n   `::before` / `::after` generated text that has a literal-string `content`.\n2. Capture each page as a raster image for visual fidelity (pages are walked\n   and captured concurrently — capture dominates wall-clock).\n3. Discover eligible `@font-face` rules and fetch/decode WOFF2 web fonts into\n   SFNT/OTF/TTF bytes.\n4. Resolve a Noto Sans JP fallback on demand for CJK text the deck's own fonts\n   can't cover (see Font Behavior).\n5. Emit a PDF with the raster page image plus a selectable text layer.\n\nThe PDF writer embeds web fonts as CID-keyed fonts with Identity-H encoding and\n`/ToUnicode` maps. Raw WOFF2 containers are never embedded.\n\nEvery failure mode is designed to be **visible, not silent**: text the vector\nlayer can't reproduce still shows in the raster, and `validate()` (before) plus\n`selfCheck` (after) surface the gap rather than dropping content quietly.\n\n## Font Behavior\n\n- Latin text can fall back to the PDF standard fonts: Helvetica, Times, Courier,\n  and their bold/italic variants.\n- Google Fonts web fonts are embedded when discoverable from CSSOM.\n- `unicode-range` subsets are filtered to the actual code points used in the\n  document, then fetched in parallel; per-character selection routes each glyph\n  to the font that covers it.\n- **CJK** (kana / kanji / CJK punctuation / fullwidth forms) that no embedded or\n  standard font covers triggers an on-demand Noto Sans JP subset fetch via the\n  Google Fonts `text=` API — only the characters used (a few KB), not the full\n  multi-MB face. Offline, the text stays in the raster with a warning.\n- Characters not covered by any embeddable font remain visible in the raster\n  layer but may be dropped from the selectable layer with a warning.\n\n## Validation\n\n`validate(pages)` is a static pre-flight check — run it before `domToPdf` (in\nCI or at runtime) to catch content that would lose selectable text.\n\n```ts\nimport { validate } from '@astlide/crispdf'\n\nconst { ok, errors, warnings } = validate(pages)\nif (!ok) {\n  // errors mean text will be lost from the selectable layer\n  console.error(errors)\n}\n```\n\n- **Errors** (`ok: false`): `<canvas>` / `<video>` — raster-only elements whose\n  text can never reach the selectable layer.\n- **Warnings**: `mix-blend-mode`, `filter`, `backdrop-filter`,\n  `position: sticky`, and 3D transforms — they render but rasterize and won't\n  blend with the vector text overlay, so output may differ from the screen.\n\n```ts\nfunction validate(pages: ArrayLike<HTMLElement>): ValidationResult\n\ninterface ValidationResult {\n  ok: boolean\n  errors: ValidationIssue[]\n  warnings: ValidationIssue[]\n}\n\ninterface ValidationIssue {\n  rule: string\n  message: string\n  element: HTMLElement\n  selector: string\n}\n```\n\n## Self-check\n\nWith `selfCheck.enabled`, the generated PDF is rendered back to pixels with\npdf.js and each page is diffed against a ground-truth raster — the page as it\nactually renders, text included (captured separately from the text-suppressed\nembedding raster, so a page that lost text diffs *higher*, not lower). Pages\nthat diverge beyond `threshold` (default `0.1`) are reported in\n`result.warnings` and `result.selfCheck`. This turns silent rendering drift — a\nfont that didn't embed, an unsupported CSS feature — into a detectable warning.\n\nThe diff is a whole-page mean-abs comparison, which is blunt: it catches gross\nfailures rather than subtle per-glyph drift. Enabling it adds a second capture\nper page plus the pdf.js render, so it is opt-in.\n\n`pdfjs-dist` is an **optional peer dependency**: install it only if you enable\nself-check. It is loaded via dynamic import, so it never enters your bundle\notherwise.\n\n## Browser Requirements\n\nThe package runs in browsers. It requires DOM APIs, `Blob`, `fetch`,\n`document.fonts`, `Range.getClientRects()`, and the canvas/image APIs used by\n`html-to-image`. Self-check additionally uses `OffscreenCanvas`,\n`createImageBitmap`, and `pdfjs-dist`.\n\n## Limitations\n\n- Web font embedding is restricted to Google Fonts hosts\n  (`fonts.gstatic.com` / `fonts.googleapis.com`) in the current release.\n- Cross-origin stylesheets that cannot be inspected through CSSOM are skipped.\n- Generated content other than literal-string `content` (counters, `attr()`,\n  `url()` images, quote keywords) stays in the raster layer only.\n- Shadow DOM and iframe content are not walked.\n- The on-demand CJK fallback covers Japanese (Noto Sans JP) at weight 400; other\n  scripts (Korean, Chinese-specific, Thai, …) are not yet auto-fetched.\n- Color emoji support depends on an embeddable font; otherwise emoji are raster\n  only.\n- The API is stable enough to try, but this is still a `0.0.x` package.\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-699142dafffb35a5a3b7ba492caf58fa"}