Files
note2any/docs/image-pipeline.md
2025-10-09 12:39:24 +08:00

171 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Image Pipeline & Cover Strategy
> 对应代码核心:`src/markdown/local-file.ts`, `src/article-render.ts` 以及相关预处理逻辑。
> 若需总体结构参见 `architecture.md`,更广设计参见 `detaildesign.md`。
## 1. 目标
- 统一处理所有图片来源Wikilink、标准 Markdown、远程 URL、Base64、生成图 (Mermaid/Excalidraw/SVG)。
- 保证上传后 HTML 使用微信可访问的正式 URL。
- 自动封面选取:未指定时从正文首图推断。
- WebP → JPG 转换保障兼容性。
- 默认封面配置:若无正文/画廊候选,使用 `defaultCoverPic` (settings)。
- 手动 frontmatter 回退metadata 缺失时行级解析 `title/author/image`
## 2. 图片来源与归一化
| 来源 | 输入示例 | 归一化策略 | 备注 |
|------|----------|-----------|------|
| Wikilink | `![[foo.png]]` / `![[foo.png|120x80]]` | Marked 扩展直接生成 `<img src="vault://...">` 并登记 | 支持尺寸 / 位置扩展语法 |
| Markdown | `![alt](assets/img/foo.png)` | (可选) 预处理转换成 `![[foo.png]]` | 依赖设置:`enableMarkdownImageToWikilink` |
| 远程 URL | `<img src="https://...">` | 解析为远程上传任务 | 跳过已是微信域名 |
| Base64 | `<img src="data:image/png;base64,...">` | 解析成 Blob 上传 | 生成虚拟 id 构造文件名 |
| Mermaid | 源码块 → SVG/DOM | 转 PNG (html-to-image) / 留 SVG | 替换占位容器 |
| Excalidraw | `![[diagram.excalidraw]]` | 远程接口取 SVG 或转 PNG | 需 AuthKey |
| SVG 文件 | `![[icon.svg]]` | 内联 SVG 内容 | 直接嵌入,不上传 |
## 3. 关键数据结构
```ts
interface ImageInfo {
resUrl: string; // 初始占位或本地资源 URL (vault path 映射)
filePath: string; // Vault 内实际文件路径
url: string | null; // 上传后微信返回 URL
media_id: string | null; // 上传后素材 IDtype=image 时)
}
```
管理容器:`Map<string, ImageInfo>`,键值为 `resUrl`(初始唯一标识)。
## 4. 收集阶段
1. Marked 扩展 (`LocalFile.markedExtension`) 在 token 遍历时识别 LocalImage。
2. 根据 wikilink / 尺寸参数解析出真实 vault 文件路径:`assetsManager.getResourcePath()`
3. 调用 `LocalImageManager.setImage(res.resUrl, info)` 建档。
4. Markdown 图片(标准语法)若转换启用,提前转成 wikilink 进入同一逻辑;否则需未来扩展 tokenizer 直接登记。
5. Gallery
- 短代码:`{{<gallery dir="..."/>}}{{<load-photoswipe>}}` → 目录枚举(受 `galleryPrePath` + `galleryNumPic` 影响)→ 多行 `![[...]]` 注入。
- 块级:`{{<gallery>}} ... {{</gallery>}}``figure src="..." link="..." caption="..." >` 解析 src basename 加入候选;`link` 预留后续包装;`caption` 未来映射题注。
## 5. 上传阶段
顺序:
```mermaid
graph TD
A[准备 token] --> B[cachedElementsToImages]
B --> C[uploadLocalImage]
C --> D[uploadRemoteImage]
D --> E[replaceImages]
E --> F[输出 HTML / 复制 / 发布]
```
### 5.1 本地图片 `uploadLocalImage`
流程:
- 遍历登记表,读取 vault 文件二进制。
- 若扩展名 `.webp` → wasm (`PrepareImageLib`) 转 JPG。
- 调用 `wxUploadImage(blob, filename, token, type)`
- 更新 `ImageInfo.url` / `media_id`
- 失败:`Notice + console.error`,继续后续项。
### 5.2 远程图片 `uploadRemoteImage`
匹配 `<img src="http...">`
- 跳过已在微信域名(`mmbiz.qpic.cn`)。
- 下载 → Blob → (webp 转换) → 上传。
- 更新 Map键为原始 src。
Base64
- 解析 data URI → 生成 Blob/扩展名 → 上传。
### 5.3 替换阶段 `replaceImages`
- 遍历 DOM `<img>` → 查询 Map → 使用上传后 `url` 覆盖 `src`
- 未登记:输出警告(潜在渲染管线遗漏)。
## 6. 自动封面策略(更新)
触发点:`getMetadata()`;若存在 `thumb_media_id` 则跳过。决策链:
1. frontmatter cover/image非空若 metadataCache 缺失,则手动行级解析首个 `---` 块。
2. 正文本地图候选markdown + wikilink
3. gallery 展开产生的首图(短代码或块级 figure 列表)。
4. `defaultCoverPic`settings 配置,允许相对路径 / URL / wikilink
5. 否则封面为空。
Edge Cases
| 情况 | 处理 |
|------|------|
| frontmatter cover: "" | 视为未配置,继续回退 |
| 所有正文图片都是 http(s) | 跳过正文阶段,转 gallery -> defaultCoverPic |
| gallery 块无有效 figure | 不贡献候选 |
| defaultCoverPic 不存在 | 仍返回该引用;上传阶段可能失败(待校验增强) |
| markdown 图片后缀非图片 | 跳过 |
| 重复图片 | 以最先出现的为准 |
## 7. 正则索引
| 目的 | 正则 | 描述 |
|------|------|------|
| Frontmatter 删除 | `/^(---)$.+?^(---)$.+?/ims` | 首段 YAML |
| Markdown 图片 | `/!\[[^\]]*\]\(([^)\s]+)\)/g` | 捕获路径 group1 |
| Wikilink 图片 | `/!\[\[(.+?)\]\]/g` | 捕获内部资源 |
| Gallery 块 | `{{<gallery>}}([\s\S]*?){{<\/gallery>}}` | 包裹内容 |
| Gallery figure | `{{<figure\s+src=\"([^\"]+)\"[^>]*>}}` | 提取 src |
| figure link | `link=\"([^\"]+)\"` | 可选外链属性 |
| WebP 检测 | `/\.webp$/i` | 扩展判断 |
| Base64 前缀 | `^data:image/` | 判断内嵌图 |
## 8. 失败与恢复策略
| 环节 | 失败示例 | 策略 |
|------|----------|------|
| wasm 初始化 | 网络慢 / 未加载 | 继续使用原 webp 上传(失败概率增加) |
| 本地文件找不到 | 路径错误 / 移动 | 在控制台警告,图片跳过 |
| 上传 403/401 | token 失效 | 抛异常终止流程(需重新获取 token |
| 单图 errcode !=0 | 大小/格式不合规 | Notice 提示 + 继续其他 |
| 替换未命中 | DOM 没登记 | 记录警告,建议加入补偿扫描 |
## 9. 性能优化方向
| 问题 | 现状 | 改进 |
|------|------|------|
| 上传串行 | 顺序 await | 并发池 (N=3~5) + 重试退避 |
| WebP 转换阻塞 | 单线程 | 预热 wasm + 批量并行 |
| 远程下载串行 | 每图 await | 统一收集 Promise.all 控制并发 |
| 首图扫描重复 | 重新正则两次 | 合并一次统一匹配 pipeline |
## 10. 与发布流程的接口点
| 函数 | 上下游 | 说明 |
|------|--------|------|
| `cachedElementsToImages()` | 延迟图形 → 图片 | 在上传前确保所有图形成为 <img> 可被收集 |
| `uploadLocalImage()` | 发布前 | 更新本地图片 URL/media_id |
| `uploadRemoteImage()` | 发布前 | 远程/内嵌资源统一化 |
| `replaceImages()` | 发布前 | DOM HTML 成为最终状态 |
| `getArticleContent()` | 发布 / 复制 / 导出 | 输出含最终图片 URL 的 HTML |
## 11. 未来扩展
| 需求 | 设想 |
|------|------|
| CDN 直传 | 微信前置 → 自建或 OSS 中转 |
| 图片压缩 | 上传前可选压缩 (canvas/wasm) |
| 失效重传 | 保存上传映射 + 校验缺失再补传 |
| 分块上传 | 大图分片(若接口支持) |
| 图片校验 | MD5 去重避免重复上传 |
| defaultCoverPic 校验 | 渲染时验证存在性 + Notice |
| 多默认封面池 | 数组随机选择减少视觉重复 |
| caption alias | figure caption -> wikilink alias/figcaption |
| link 包裹 | link 属性生成 `<a>` 包裹 `<img>` |
## 12. 快速检查清单 (Debug Checklist)
- 图片是否在 `LocalImageManager.images` Map 中?
- 是否执行了 `cachedElementsToImages()`Mermaid/Excalidraw
- 上传后 `media_id` 是否为空?(格式/大小不合规)
- DOM 替换后 `<img src>` 是否为微信域?
- 自动封面未生效检查frontmatter cover / 正文本地图是否存在 / gallery 是否展开 / defaultCoverPic 是否配置。
- 重复封面日志确认日志节流是否失效3 秒窗口内只应出现一次)。
## 13. 示例
输入 Markdown
```
---
title: 示例
---
![首图](assets/img/a.png)
正文...
![[b-second.jpg]]
```
处理:
1. frontmatter 去除后扫描:首个匹配为 Markdown 图片 a.png → 自动封面 `![[a.png]]`
2. a.png 转 wikilink (开启转换时) → 登记b-second.jpg 登记。
3. 上传顺序a.png → b-second.jpg。
4. 最终 HTML `<img src="https://mmbiz.qpic.cn/...">`
---
如需补充“并发上传样例代码”或“封面过滤策略扩展”,请提出。