# 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 扩展直接生成 `
` 并登记 | 支持尺寸 / 位置扩展语法 |
| Markdown | `` | (可选) 预处理转换成 `![[foo.png]]` | 依赖设置:`enableMarkdownImageToWikilink` |
| 远程 URL | `
` | 解析为远程上传任务 | 跳过已是微信域名 |
| 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; // 上传后素材 ID(type=image 时)
}
```
管理容器:`Map`,键值为 `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:
- 短代码:`{{}}{{}}` → 目录枚举(受 `galleryPrePath` + `galleryNumPic` 影响)→ 多行 `![[...]]` 注入。
- 块级:`{{}} ... {{}}` 内 `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`
匹配 `
`:
- 跳过已在微信域名(`mmbiz.qpic.cn`)。
- 下载 → Blob → (webp 转换) → 上传。
- 更新 Map,键为原始 src。
Base64:
- 解析 data URI → 生成 Blob/扩展名 → 上传。
### 5.3 替换阶段 `replaceImages`
- 遍历 DOM `
` → 查询 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 块 | `{{}}([\s\S]*?){{<\/gallery>}}` | 包裹内容 |
| Gallery figure | `{{]*>}}` | 提取 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()` | 延迟图形 → 图片 | 在上传前确保所有图形成为
可被收集 |
| `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 属性生成 `` 包裹 `
` |
## 12. 快速检查清单 (Debug Checklist)
- 图片是否在 `LocalImageManager.images` Map 中?
- 是否执行了 `cachedElementsToImages()`(Mermaid/Excalidraw)?
- 上传后 `media_id` 是否为空?(格式/大小不合规)
- DOM 替换后 `
` 是否为微信域?
- 自动封面未生效?检查:frontmatter cover / 正文本地图是否存在 / gallery 是否展开 / defaultCoverPic 是否配置。
- 重复封面日志?确认日志节流是否失效(3 秒窗口内只应出现一次)。
## 13. 示例
输入 Markdown:
```
---
title: 示例
---

正文...
![[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 `
`。
---
如需补充“并发上传样例代码”或“封面过滤策略扩展”,请提出。