画册图片不由页面代码临时压缩、拼接或猜测尺寸。正式链路采用四层边界:
原始图片
→ 可重复执行的图片管线
→ manifest 与派生资源
→ 阅读器消费核心约定是:
原图是事实,管线负责派生,manifest 是唯一交付物,前端不得参与图片加工。
第一阶段 CLI 已在 scripts/comic-pipeline/ 落地,支持源图检查、WebP 构建、产物校验与显式 R2 发布。当前《余光·上》阅读器仍使用低清精灵图与分片;它尚未切换到新 manifest,因此图片管线已经可用,不等于阅读端迁移已经完成。
已实现范围#
当前实现包含:
comic:inspect:检查配置、页码、缺页、重复页、尺寸和比例;comic:build:生成多尺寸 WebP、内容哈希文件名、manifest 与报告;comic:verify:校验文件存在性、宽高、体积与 SHA-256;comic:publish:使用 staging 前缀提升到 R2 正式目录;comic:test:在临时目录生成 fixture,完整覆盖 inspect、build 和 verify;- 本地构建采用临时目录完成后整体替换,失败时不留下半成品。
第一阶段暂不生成 CBZ 和 PDF。它们以后必须从同一份源图和 manifest 派生,不能建立第二套页码事实。
非目标#
本管线不负责:
- AI 放大或重绘;
- 自动裁切、构图修正或补页;
- 自动调色、提亮、锐化、降噪;
- 用超出源图宽度的文件制造“假高清”;
- 在网站运行时加工原图;
build完成后自动执行外部上传。
源文件契约#
一本画册使用一个独立源目录:
comic-source/
└── yuguang-vol-1/
├── work.json
├── cover.png
└── pages/
├── 001.png
├── 002.png
├── ...
└── 030.png页图文件名必须使用三位数字页码。管线不得根据目录顺序重新编号,也不得拿后一页填补缺页。
work.json 的最小结构:
{
"schemaVersion": 1,
"id": "yuguang-vol-1",
"edition": "v1",
"title": "余光·上",
"subtitle": "第一次讲话",
"expectedPages": 30,
"missingPages": [22],
"readingDirection": "ltr",
"expectedAspectRatio": 1.38
}可选字段:
{
"aspectRatioOverrides": [7]
}只有人工确认某页确实采用不同构图时,才能把页码放入 aspectRatioOverrides。它不是跳过全部比例检查的开关。
硬规则:
id在作品生命周期内保持稳定;- 内容变化时必须提升
edition; - 缺页必须显式列入
missingPages; - 已声明缺页的位置不得同时存在页图;
- 不允许重复页码;
- 不裁切、不拉伸、不改变原始构图;
- EXIF 方向可以归一化;
- 输出颜色空间统一为 sRGB;
- 页面内容、亮度和颜色不得被程序主观修改。
支持的输入格式为 PNG、JPEG、WebP 和 TIFF。输出第一阶段统一为 WebP。
派生尺寸规则#
页面候选宽度:
320
960
1600
原图宽度封面额外包含 480px 档:
320
480
960
1600
原图宽度程序只生成不超过源图宽度的候选项,并对重复宽度去重。例如源图宽度为 1450px,页面实际输出为:
001-320.<hash>.webp
001-960.<hash>.webp
001-1450.<hash>.webp每张派生图:
- 保持原始比例;
- 使用
withoutEnlargement; - 归一化 EXIF 方向;
- 转为 sRGB;
- 使用 WebP quality 84、effort 5;
- 文件名包含派生文件 SHA-256 的前 8 位。
各档用途:
| 宽度 | 用途 |
|---|---|
| 320 | 目录缩略图与小尺寸列表 |
| 480 | 画册封面列表 |
| 960 | 普通手机与低倍率阅读 |
| 1600 | 高分屏手机、平板与桌面 |
| 原图宽度 | 放大阅读与最终兜底 |
本地输出契约#
默认构建目录:
.mira-cache/
└── comics/
└── yuguang-vol-1/
└── v1/
├── manifest.json
├── report.json
├── cover/
└── pages/.mira-cache/ 已加入 .gitignore。构建过程先写入同级 staging 目录,完成校验后再替换目标目录。目标目录不会在生成一半时被阅读器或发布命令消费。
也可以显式指定输出目录:
pnpm comic:build -- D:/MiraAssets/comics/yuguang-vol-1 \
--output D:/MiraBuild/yuguang-vol-1-v1manifest 契约#
阅读器只消费 manifest,不读取源目录,也不推断文件名。
{
"schemaVersion": 1,
"pipelineVersion": 1,
"id": "yuguang-vol-1",
"edition": "v1",
"title": "余光·上",
"subtitle": "第一次讲话",
"expectedPages": 30,
"availablePages": 29,
"missingPages": [22],
"readingDirection": "ltr",
"releaseFingerprint": "...",
"cover": {
"original": {
"width": 1456,
"height": 1055,
"aspectRatio": 1.38,
"bytes": 123456,
"sha256": "..."
},
"sources": [
{
"width": 320,
"height": 232,
"src": "cover/cover-320.a38f219c.webp",
"bytes": 18420,
"sha256": "..."
}
]
},
"pages": [
{
"number": 1,
"original": {
"width": 1456,
"height": 1055,
"aspectRatio": 1.38,
"bytes": 234567,
"sha256": "..."
},
"sources": [
{
"width": 960,
"height": 696,
"src": "pages/001-960.f20a177b.webp",
"bytes": 108240,
"sha256": "..."
}
]
}
]
}releaseFingerprint 由作品配置、封面源哈希和全部页图源哈希计算。它用于识别一套输入事实,不包含本机绝对路径和构建时间。
report.json 可以包含构建时间、警告和产物体积,但不会暴露源图完整本地路径。
CLI 使用#
所有 pnpm 参数都放在 -- 后面。
检查源图#
pnpm comic:inspect -- D:/MiraAssets/comics/yuguang-vol-1输出 JSON:
pnpm comic:inspect -- D:/MiraAssets/comics/yuguang-vol-1 --jsoninspect 只读取源文件,不生成图片,不访问 R2。
构建#
pnpm comic:build -- D:/MiraAssets/comics/yuguang-vol-1校验已有构建#
pnpm comic:verify -- .mira-cache/comics/yuguang-vol-1/v1查看发布计划#
pnpm comic:publish -- .mira-cache/comics/yuguang-vol-1/v1 --plan--plan 不执行任何外部操作,也不要求真实密钥。
显式发布#
pnpm comic:publish -- .mira-cache/comics/yuguang-vol-1/v1 --confirm没有 --confirm 时,发布命令直接失败。
R2 环境变量#
发布复用 Mira Mobile 已有的密钥命名:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
R2_ACCOUNT_ID
R2_BUCKET
R2_PUBLIC_BASE_URL # 可选,仅用于打印公开 manifest 地址
R2_COMICS_PREFIX # 可选,默认 mira/comics本地发布依赖 AWS CLI。GitHub Runner 已预装 AWS CLI;Windows 本机执行前需要保证 aws 命令可用。
R2 整目录替换语义#
R2/S3 实际上没有可原子重命名的“文件夹”。因此不能假装一次 rename 就能替换整本画册,也不能直接逐个覆盖正式目录。
当前发布策略:
本地完整构建
→ 上传到 .staging 临时前缀
→ dry-run 校验 staging 与本地一致
→ 先复制全部新图片到 current
→ 复制 report.json
→ 最后复制 manifest.json,完成版本切换
→ manifest 切换后删除 current 中的旧文件
→ dry-run 校验 current 与 staging 完全一致
→ 删除 staging 临时目录正式路径固定为:
mira/comics/<work-id>/current/
├── manifest.json
├── report.json
├── cover/
└── pages/临时路径:
mira/comics/.staging/<work-id>/<edition>-<fingerprint>-<timestamp>/这套策略的效果是:
- 发布完成后,R2 只保留一份
current,不会永久堆积 edition 文件夹; - 正式目录最终与本地构建完全一致,旧文件会被删除;
- 新 manifest 写入之前,新 manifest 引用的图片已经全部存在;
- staging 只在发布期间短暂占用空间,成功后立即清理;
- 发布失败时不会先清空正式目录。
对象存储无法提供真正的目录级原子事务。这里把 manifest.json 作为唯一切换点,是在不引入 Worker 和数据库的前提下最稳妥的边界。
派生图片使用:
Cache-Control: public, max-age=31536000, immutablemanifest 与报告使用:
Cache-Control: public, max-age=60, must-revalidate校验语义#
以下情况直接失败:
- 文件名不是合法三位页码;
- 页码重复;
- 出现未声明缺页;
- 已声明缺页的位置存在文件;
- 图片无法解码或宽高无效;
- 比例偏差超过 5% 且未人工声明 override;
- 派生图宽度超过源图;
- manifest 引用了不存在的文件;
- 文件体积、宽高或 SHA-256 与 manifest 不一致;
- R2 staging 或正式目录在提升后仍与本地构建不一致。
以下情况默认警告:
- 页面比例偏差超过 2%;
- 源图宽度低于 960px;
- 优化后的文件反而大于源文件;
- 构建目录出现未被 manifest 引用的额外文件。
程序不会自动裁切“修正”比例异常。
前端消费契约#
画册列表、详情页和阅读器只根据 manifest 选择资源:
- 画册列表只加载封面小图;
- 页码目录只加载 320px 页图;
- 当前页根据容器宽度与设备像素比选择合适尺寸;
- 默认只预加载前一页和后一页;
- 放大时允许升级到更高尺寸;
- Service Worker 不预缓存整本书;
- 缺页由 manifest 明确渲染;
- 页面在图片返回前使用 manifest 宽高预留位置;
- 阅读进度和缓存键必须包含 edition。
前端不得拼 Base64 分片、生成精灵图、在浏览器里压缩源图或给原画应用全站明暗主题滤镜。
当前迁移顺序#
inspect / build / verify / publish:已完成第一阶段实现;- 用《余光·上》原始页面跑真实构建并审查报告;
- 上传 R2,确认
current/manifest.json与缓存头; - 阅读器切换为逐页 manifest 加载;
- 验证目录、翻页、缩放、缺页、缓存和移动流量;
- 删除精灵图、Base64 分片和运行时 Blob 拼接逻辑;
- 后续再从同一 manifest 增加 CBZ 与 PDF。
不允许长期并存“精灵图协议”和“逐页 manifest 协议”。任何无法由程序重复生成的手工图片处理,都不应进入正式发布链路。