feat: exportAsHtml でローカル画像を data URI として埋め込む (#314)#353
Merged
Conversation
- 新規 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
marked this pull request as ready for review
July 18, 2026 16:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
exportAsHtml でローカル画像 (
等) を data URI として HTML に埋め込む。エクスポート後の HTML を外部ブラウザで開いても画像が表示されるようになる (#314)。SlidePreview / exportAsPdf は既にscripta-asset://で解決済みのため対象外 (issue スコープ通り)。関連 Issue
Closes #314
移行 Stage
変更内容
fs:read-base64(electron/main/ipc/fs.ts):MAX_READ_FILE_BYTES = 64MBを fs:read と共通で強制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:readFileBase64失敗時は元 src を残す (broken image でも export 自体は完遂)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成功参照 md を HTML export → 外部ブラウザで画像表示 / 20+ 画像文書で OOM しない / http 画像 は fetch されず元 src 維持) — sandbox 制約でローカル不可、CI / リリース時に確認スクリーンショット
なし (UI 変更なし、export 経路の内部改善)
検証エビデンス
リスク分類
tier: medium — classify-risk.sh の判定 (reasons 空、default medium)
実行した検証
tsc --noEmit -p tsconfig.web.jsontsc --noEmit -p tsconfig.node.jsontsc --noEmit -p tsconfig.e2e.jsonbiome check src/ electron/ e2e/vitest runelectron-vite buildsecurity(detect)security(confirm 再走)レビュー指摘と対応
security per-finding:
src/lib/resolve-html-images.ts:47(detect) — unbounded Promise.all で N 画像 × 最大 64MB Buffer.alloc により main プロセス OOM リスク → CONFIRMED+FIXED (commita14e26d: dedup + worker-pool bounded concurrency K=4、test 2 ケース追加)src/lib/resolve-html-images.ts:44(confirm) — bounded concurrency 実装後も「エクスポート全体の埋め込みバイト数上限」が未対応 → UNRESOLVED (skill ルールに従い max 2 runs で追加 fix はせず、追跡先へ回す)追跡先
src/lib/resolve-html-images.ts:44— 全体埋め込みバイト数上限 (theoretical OOM、real user impact 小)Draft 判定
a14e26d、bounded concurrency K=4 + dedup)。UNRESOLVED (全体バイト上限、real user impact 小) は追跡 issue exportAsHtml: 埋め込み画像の累積バイト数に上限を設ける (renderer OOM 対策) #354 で起票済み、追跡先 URL 記載により fix-or-issue ポリシー要件を満たす