@astlide/crispdf

中文文档
0.0.3发布于 2026/11/02MITTypeScriptES Module构建来源已验证近 30 天下载 4090

DOM → high-quality PDF in the browser: raster background + vector text overlay. Selectable, searchable, copyable text.

原始使用文档
文档目录10 节
  1. Install
  2. Usage
  3. API
  4. How It Works
  5. Font Behavior
  6. Validation
  7. Self-check
  8. Browser Requirements
  9. Limitations
  10. License

crispdf

Browser-side DOM to PDF with high visual fidelity and selectable text.

crispdf renders each page as a raster background, then overlays real PDF text on top. The result keeps complex HTML/CSS visually intact while preserving search, selection, and copy/paste for text that can be mapped to a PDF font.

Install

pnpm add @astlide/crispdf
# npm i @astlide/crispdf
# yarn add @astlide/crispdf

Usage

import { domToPdf } from '@astlide/crispdf'

const result = await domToPdf({
  pages: document.querySelectorAll<HTMLElement>('[data-page]'),
  source: { width: 800, height: 600 },
  output: { width: 800, height: 600, unit: 'pt' },
  rasterFormat: 'jpeg',
  jpegQuality: 0.85,
  onProgress: (pageIndex, totalPages) => {
    console.log(`page ${pageIndex}/${totalPages}`)
  },
  onTiming: (event) => {
    console.log(event)
  },
})

if (result.warnings.length > 0) {
  console.warn(result.warnings)
}

const url = URL.createObjectURL(result.blob)

API

function domToPdf(opts: DomToPdfOptions): Promise<DomToPdfResult>

interface DomToPdfOptions {
  pages: ArrayLike<HTMLElement>
  source: { width: number; height: number }
  output: { width: number; height: number; unit: 'pt' }
  rasterFormat?: 'jpeg' | 'png' // default 'jpeg'
  jpegQuality?: number // default 0.85
  onProgress?: (pageIndex: number, totalPages: number) => void
  onTiming?: (event: TimingEvent) => void
  /** Opt-in visual self-check; off by default. Requires `pdfjs-dist`. */
  selfCheck?: { enabled: boolean; threshold?: number }
}

type TimingEvent =
  | { stage: 'walk'; page: number; durationMs: number }
  | { stage: 'capture'; page: number; durationMs: number }
  | { stage: 'fonts'; durationMs: number }
  | { stage: 'emit'; durationMs: number }
  | { stage: 'selfCheck'; durationMs: number }

interface DomToPdfResult {
  blob: Blob
  warnings: string[]
  /** Present only when `selfCheck.enabled` was set. */
  selfCheck?: SelfCheckPageResult[]
}

interface SelfCheckPageResult {
  page: number // 1-indexed
  diff: number // visual difference, 0..1
  exceeded: boolean // diff > threshold
}

How It Works

  1. Walk visible DOM text and record line rectangles through Range, including ::before / ::after generated text that has a literal-string content.
  2. Capture each page as a raster image for visual fidelity (pages are walked and captured concurrently — capture dominates wall-clock).
  3. Discover eligible @font-face rules and fetch/decode WOFF2 web fonts into SFNT/OTF/TTF bytes.
  4. Resolve a Noto Sans JP fallback on demand for CJK text the deck's own fonts can't cover (see Font Behavior).
  5. Emit a PDF with the raster page image plus a selectable text layer.

The PDF writer embeds web fonts as CID-keyed fonts with Identity-H encoding and /ToUnicode maps. Raw WOFF2 containers are never embedded.

Every failure mode is designed to be visible, not silent: text the vector layer can't reproduce still shows in the raster, and validate() (before) plus selfCheck (after) surface the gap rather than dropping content quietly.

Font Behavior

  • Latin text can fall back to the PDF standard fonts: Helvetica, Times, Courier, and their bold/italic variants.
  • Google Fonts web fonts are embedded when discoverable from CSSOM.
  • unicode-range subsets are filtered to the actual code points used in the document, then fetched in parallel; per-character selection routes each glyph to the font that covers it.
  • CJK (kana / kanji / CJK punctuation / fullwidth forms) that no embedded or standard font covers triggers an on-demand Noto Sans JP subset fetch via the Google Fonts text= API — only the characters used (a few KB), not the full multi-MB face. Offline, the text stays in the raster with a warning.
  • Characters not covered by any embeddable font remain visible in the raster layer but may be dropped from the selectable layer with a warning.

Validation

validate(pages) is a static pre-flight check — run it before domToPdf (in CI or at runtime) to catch content that would lose selectable text.

import { validate } from '@astlide/crispdf'

const { ok, errors, warnings } = validate(pages)
if (!ok) {
  // errors mean text will be lost from the selectable layer
  console.error(errors)
}
  • Errors (ok: false): <canvas> / <video> — raster-only elements whose text can never reach the selectable layer.
  • Warnings: mix-blend-mode, filter, backdrop-filter, position: sticky, and 3D transforms — they render but rasterize and won't blend with the vector text overlay, so output may differ from the screen.
function validate(pages: ArrayLike<HTMLElement>): ValidationResult

interface ValidationResult {
  ok: boolean
  errors: ValidationIssue[]
  warnings: ValidationIssue[]
}

interface ValidationIssue {
  rule: string
  message: string
  element: HTMLElement
  selector: string
}

Self-check

With selfCheck.enabled, the generated PDF is rendered back to pixels with pdf.js and each page is diffed against a ground-truth raster — the page as it actually renders, text included (captured separately from the text-suppressed embedding raster, so a page that lost text diffs higher, not lower). Pages that diverge beyond threshold (default 0.1) are reported in result.warnings and result.selfCheck. This turns silent rendering drift — a font that didn't embed, an unsupported CSS feature — into a detectable warning.

The diff is a whole-page mean-abs comparison, which is blunt: it catches gross failures rather than subtle per-glyph drift. Enabling it adds a second capture per page plus the pdf.js render, so it is opt-in.

pdfjs-dist is an optional peer dependency: install it only if you enable self-check. It is loaded via dynamic import, so it never enters your bundle otherwise.

Browser Requirements

The package runs in browsers. It requires DOM APIs, Blob, fetch, document.fonts, Range.getClientRects(), and the canvas/image APIs used by html-to-image. Self-check additionally uses OffscreenCanvas, createImageBitmap, and pdfjs-dist.

Limitations

  • Web font embedding is restricted to Google Fonts hosts (fonts.gstatic.com / fonts.googleapis.com) in the current release.
  • Cross-origin stylesheets that cannot be inspected through CSSOM are skipped.
  • Generated content other than literal-string content (counters, attr(), url() images, quote keywords) stays in the raster layer only.
  • Shadow DOM and iframe content are not walked.
  • The on-demand CJK fallback covers Japanese (Noto Sans JP) at weight 400; other scripts (Korean, Chinese-specific, Thai, …) are not yet auto-fetched.
  • Color emoji support depends on an embeddable font; otherwise emoji are raster only.
  • The API is stable enough to try, but this is still a 0.0.x package.

License

MIT

版本与发布记录

@astlide/crispdf 共发布 2 个版本,首次发布于 2026/05/30;过去 12 个月发布了 2 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.0.3latest2026/11/025 天前
0.0.12026/05/305 个月前

关于 @astlide/crispdf 的常见问题

@astlide/crispdf 是什么?

DOM → high-quality PDF in the browser: raster background + vector text overlay. Selectable, searchable, copyable text.

如何安装 @astlide/crispdf?

在项目目录运行 npm install @astlide/crispdf,或使用 pnpm add / yarn add / bun add。

@astlide/crispdf 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@astlide/crispdf 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@astlide/crispdf 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.12.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@astlide/crispdf 可以商用吗?

@astlide/crispdf 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@astlide/crispdf 的下载量有多少?

根据 npm 官方下载统计,@astlide/crispdf 在 2026/10/07 – 2026/11/05 这 30 天内被下载 4,090 次,日均约 136 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@astlide/crispdf 还在维护吗?

最新版本 0.0.3 发布于 2026/11/02(5 天前);至今共发布 2 个版本,其中过去 12 个月发布了 2 个;首次发布于 2026/05/30。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。