Skip to content

feat: flatten CSS gradients at PDF print time for fast Preview.app rendering#144

Merged
mizzy merged 1 commit into
mainfrom
issue-142-pdf-gradient-flatten
Jul 6, 2026
Merged

feat: flatten CSS gradients at PDF print time for fast Preview.app rendering#144
mizzy merged 1 commit into
mainfrom
issue-142-pdf-gradient-flatten

Conversation

@mizzy

@mizzy mizzy commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Closes #142

問題

peitho export pdf で生成したPDFをmacOSのプレビュー.appで開くと、各ページが真っ白の状態から部分的に描画されていく(実測: 表紙1ページの描画に14.2秒)。

根本原因(実測で特定)

CSSグラデーション。ChromeのSkiaバックエンドはCSSグラデーションをType 4シェーディング(PostScript計算関数)+ソフトマスクとしてPDF化し、macOSのCoreGraphicsはこれをピクセル毎に関数評価するため極端に遅い。さらに「遅い」だけでなく「正しく描画されない」(radial-gradientで描いた星がほぼ消える)ことも確認。対照実験でグラデーションだけをフラット色化するとType3フォントが残っていても5.8ms/ページになり、フォントは無関係と確定。

対策

印刷用 pdf.html にフラット化スクリプトを埋め込み、グラデーション背景だけをChrome自身にラスタライズさせてdata URL画像に置き換える(SVG foreignObject → canvas → toDataURL。Chromeの実レンダリングを使うためCSS解釈のズレが原理的に生じない)。テキストは完全にベクターのまま(選択・検索可能)。run_chrome_print--virtual-time-budget=10000 を追加してスクリプト完了後に印刷する。

忠実なキャプチャを保証できない対象はスキップしてベクターを温存する(劣化方向は常に「遅いが正しい」): url()レイヤー混在 / background-attachment: fixed / background-clip: text / 非normalbackground-blend-mode / 色のclip変化 / 複数フラグメントのインライン要素 / pxに解決できない寸法 / padding・border付き疑似要素 / canvas上限超過。

実測結果(decks/carina、11スライド)

指標 before after
1ページ目の描画(PDFKit) 14,204ms 33ms
全11ページ合計 15,109ms 273ms
/Shading 19 0
/SMask 9 0
テキスト選択 可のまま
星空の見た目 星がほぼ消える 正しく表示

同一背景を共有するスライドは同一data URLになりSkiaが重複排除するため、埋め込み画像は実質1枚分(11スライドで2オブジェクト/789KB)。

設計記録

docs/plans/2026-07-06-pdf-gradient-flatten.md(計測事実・スキップ条件・設計判断)

テスト

  • unit: chrome_print_args の引数列固定、スクリプト埋め込み、</script ガード
  • E2E(ignored、Chrome必須): グラデーションデッキで /Shading 不在+画像存在+フォント存在、既存E2Eも緑
  • 全ゲート緑(workspace test 3連続 / clippy / fmt / bindings / npm build+test+typecheck / shell.js drift)

検証中に発見した別問題(別Issue化予定)

  1. macOSでGUI Chromeが起動中だと --headless=new のChromeが印刷完了後に終了せず peitho export pdf がハングする(本変更とは無関係の既存問題、--virtual-time-budget なしでも再現)
  2. コードブロックにmonospaceフォント指定がないと日本語環境でOsaka-Monoが丸ごと埋め込まれPDFが1.37MB肥大する

🤖 Generated with Claude Code

…ndering

Chrome's Skia backend converts CSS gradients into Type 4 shading
functions + soft masks, which macOS CoreGraphics renders pathologically
slowly (measured: 14.2s for one slide) and incorrectly (radial-gradient
stars mostly invisible). pdf.html now embeds a script that replaces
gradient background-images with PNGs rasterized by Chrome itself
(SVG foreignObject → canvas → data URL) before printing; text stays
fully vector (selectable/searchable). run_chrome_print passes
--virtual-time-budget so the script completes before print.

Targets whose capture cannot be guaranteed faithful are skipped and
keep their vector gradient (slow but correct): url() layers,
background-attachment: fixed, background-clip: text, non-normal
background-blend-mode, color-clip changes, fragmented inline elements,
non-px dimensions, padded/bordered pseudo-elements, oversized canvases.

Measured on a real 11-slide deck: page 1 render 14,204ms → 33ms,
total 15,109ms → 273ms, /Shading 19 → 0, identical visual output.

Closes #142

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mizzy mizzy merged commit 721c7af into main Jul 6, 2026
3 checks passed
@mizzy mizzy deleted the issue-142-pdf-gradient-flatten branch July 6, 2026 10:38
mizzy added a commit that referenced this pull request Jul 6, 2026
Resolves conflicts with PR #144 (gradient flatten for PDF export):
- render.rs: keep both PDF_FLATTEN_JS and BUILTIN_MEASURE_JS consts
- main.rs: adopt chrome_print_args helper atop our run_one_shot_chrome runner
- tests: union both sides

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mizzy mizzy mentioned this pull request Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PDF export: プレビュー.appでの描画が遅い

1 participant