Skip to content

feat: exportAsHtml でローカル画像を data URI として埋め込む (#314)#353

Merged
ymnao merged 2 commits into
mainfrom
feature/html-export-data-uri
Jul 18, 2026
Merged

feat: exportAsHtml でローカル画像を data URI として埋め込む (#314)#353
ymnao merged 2 commits into
mainfrom
feature/html-export-data-uri

Conversation

@ymnao

@ymnao ymnao commented Jul 18, 2026

Copy link
Copy Markdown
Owner

概要

exportAsHtml でローカル画像 (![](./foo.png) 等) を data URI として HTML に埋め込む。エクスポート後の HTML を外部ブラウザで開いても画像が表示されるようになる (#314)。SlidePreview / exportAsPdf は既に scripta-asset:// で解決済みのため対象外 (issue スコープ通り)。

関連 Issue

Closes #314

移行 Stage

  • Stage 0〜6 いずれも該当せず (移行後の内部機能改善)

変更内容

  • 新規 IPC fs:read-base64 (electron/main/ipc/fs.ts):
    • workspace 内 path-guard (window-scoped)
    • 画像拡張子ホワイトリスト (png/jpg/jpeg/gif/webp/svg/bmp/avif) で任意 binary の吸い上げを封止
    • MAX_READ_FILE_BYTES = 64MB を fs:read と共通で強制
  • 共有 MIME マップ src/types/image.ts — renderer 側 embedHtmlImagesAsDataUri と main 側 ext 検証の 1 SOT。tsconfig.node.json の include 経由で bundle 境界を跨いで共有 (session 101 の LruCache 統合と同 pattern)
  • resolveImageToOsPath 分離 (src/lib/image-src.ts) — resolveImageSrc (asset URL 返却) は薄い wrapper 化、挙動不変
  • embedHtmlImagesAsDataUri 追加 (src/lib/resolve-html-images.ts) — async post-processor:
    • http(s) / data: / blob: / 未対応拡張子はそのまま維持
    • readFileBase64 失敗時は元 src を残す (broken image でも export 自体は完遂)
    • 同一 osPath は 1 回だけ read し複数 img で data URI を共有 (dedup)
    • worker pool で並列度 4 に bound (main 側 Buffer.alloc の一時ピークを ~256MB に抑制、codex-review MEDIUM/92 対応)
  • exportAsHtml を async 化markdownToHtml → embedHtmlImagesAsDataUri → buildHtmlDocument → writeFile の順で data URI 埋め込みを挟む

動作確認

  • ./node_modules/.bin/tsc --noEmit -p tsconfig.web.json / .node.json / .e2e.json 全 pass
  • ./node_modules/.bin/biome check src/ electron/ e2e/ (344 files) 全 pass
  • ./node_modules/.bin/vitest run — 2576 pass / 2 skipped (前 main 2547 → +29 は本 PR の新規 test)
  • ./node_modules/.bin/electron-vite build 成功
  • 実 Electron 手動確認 (![alt](./foo.png) 参照 md を HTML export → 外部ブラウザで画像表示 / 20+ 画像文書で OOM しない / http 画像 は fetch されず元 src 維持) — sandbox 制約でローカル不可、CI / リリース時に確認

スクリーンショット

なし (UI 変更なし、export 経路の内部改善)

検証エビデンス

リスク分類

tier: medium — classify-risk.sh の判定 (reasons 空、default medium)

実行した検証

種別 コマンド 結果
Typecheck (web) tsc --noEmit -p tsconfig.web.json PASS
Typecheck (node) tsc --noEmit -p tsconfig.node.json PASS
Typecheck (e2e) tsc --noEmit -p tsconfig.e2e.json PASS
Lint biome check src/ electron/ e2e/ PASS (344 files)
テスト vitest run PASS (2576 pass / 2 skipped)
ビルド electron-vite build 成功
レビュー codex-review security (detect) 1 finding surface (MEDIUM/92)
レビュー codex-review security (confirm 再走) UNRESOLVED 1 (bounded concurrency 対応済、全体バイト上限は追跡回し)
e2e (renderer-only) 未実施 ローカル sandbox 制約、CI で確認
e2e (実 Electron) 未実施 ローカル sandbox 制約、CI で確認

レビュー指摘と対応

Perspective Verdict Findings Confirmed Refuted Fixed Report-only Unresolved
security FIXED (partial) 1 (detect) + 1 (confirm) 1 0 1 0 1

security per-finding:

  • MEDIUM/92 src/lib/resolve-html-images.ts:47 (detect) — unbounded Promise.all で N 画像 × 最大 64MB Buffer.alloc により main プロセス OOM リスク → CONFIRMED+FIXED (commit a14e26d: dedup + worker-pool bounded concurrency K=4、test 2 ケース追加)
  • MEDIUM/92 src/lib/resolve-html-images.ts:44 (confirm) — bounded concurrency 実装後も「エクスポート全体の埋め込みバイト数上限」が未対応 → UNRESOLVED (skill ルールに従い max 2 runs で追加 fix はせず、追跡先へ回す)

追跡先

Finding (file:line — summary) Issue URL
src/lib/resolve-html-images.ts:44 — 全体埋め込みバイト数上限 (theoretical OOM、real user impact 小) #354

Draft 判定

ymnao added 2 commits July 19, 2026 01:05
- 新規 IPC `fs:read-base64` (electron/main/ipc/fs.ts) を追加。
  workspace 内 path-guard + 画像拡張子ホワイトリスト
  (png/jpg/jpeg/gif/webp/svg/bmp/avif) + MAX_READ_FILE_BYTES (64MB) の
  3 段防御。任意 binary の吸い上げは不可。
- 共有 MIME マップ `src/types/image.ts` (renderer 側 embedHtmlImagesAsDataUri と
  main 側 ext 検証の 1 SOT、tsconfig.node.json の include に含まれるので
  bundle 境界を跨いで共有)。
- `src/lib/image-src.ts`: `resolveImageToOsPath` を分離抽出。
  `resolveImageSrc` (asset URL 返却) は薄い wrapper に (挙動不変、既存
  test で covered)。
- `src/lib/resolve-html-images.ts`: async `embedHtmlImagesAsDataUri`
  を追加。ローカル画像を data URI に置換、http(s)/data:/blob:/未対応拡張子
  はそのまま、readFileBase64 失敗時も元 src を残す (broken image でも
  export 自体は完遂)。複数画像は Promise.all で並列 IPC。
- `src/lib/export.ts`: `exportAsHtml` が
  `embedHtmlImagesAsDataUri(markdownToHtml(preprocessed), filePath)`
  を await するよう更新。scripta-asset:// では外部ブラウザから解決できないため
  HTML export のみ data URI 化 (SlidePreview / exportAsPdf は既に
  asset protocol で解決済みで対象外、issue #314 スコープ通り)。

テスト (+27 新規、計 2574 pass):
- fs.test.ts: readFileBase64Impl 8 ケース (png/jpeg/case-insensitive/
  非画像拡張子 reject / extensionless reject / workspace 外 reject /
  他 window reject / サイズ超 reject / ENOENT)
- resolve-html-images.test.ts: embedHtmlImagesAsDataUri 10 ケース
  (relative/absolute/svg mime/http skip/data skip/mp4 skip/失敗 fallback/
  Promise.all 並列/部分失敗/case-insensitive)
- export.test.ts: exportAsHtml 3 ケース (data URI 埋め込み / http はそのまま /
  失敗時も broken image で完遂)
- image-src.test.ts: resolveImageToOsPath 5 ケース
- codex-review security の MEDIUM/92 (embed で Promise.all が
  unbounded → main 側 Buffer.alloc N * 64MB で OOM リスク) 対応
- 同一 osPath は 1 回だけ read し複数 img で data URI を共有 (dedup)
- worker pool で並列度 EMBED_CONCURRENCY=4 に bound。main IPC が
  event loop 単スレッド直列化なので K を上げても wall-clock は改善
  しない → K=4 で main 側 Buffer.alloc の一時ピークを ~256MB に抑制
- test: dedup (3 img 参照 → 1 read) + bounded concurrency (20 img で
  peak 並列度 ≤ 4) の 2 ケース追加。全 vitest 2576 pass
@ymnao
ymnao marked this pull request as ready for review July 18, 2026 16:22
@ymnao
ymnao merged commit 616ea89 into main Jul 18, 2026
9 checks passed
@ymnao
ymnao deleted the feature/html-export-data-uri branch July 18, 2026 16:23
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.

exportAsHtml: ローカル画像を data URI で埋め込む (外部ブラウザ portability)

1 participant