# MZC-1 · 数字编织图纸传输格式

状态：v1（2026-10-23）。用于二维码、剪贴板、聊天文本与文件中转。

MZC-1 只承载**数字图纸**：针法矩阵、读图规则、图块、分段、起收针说明。格式中**没有任何图片字段**；带原图的完整分享继续使用 `.maozhizhi` 文件，不进入 MZC-1。

实现：`apps/ios/MaoZhiZhi/Services/MZC1Codec.swift`、`apps/android/.../domain/Mzc1.kt`。针法注册表见 [`stitch-registry.json`](stitch-registry.json)。

## 1. 基本类型

- `u8`：无符号 8 位整数。
- `varint`：无符号 LEB128，最多 5 字节，值域 0…2³¹−1；第 5 字节的高位必须为 0。超长、截断或溢出均视为格式错误。
- `string`：`varint` UTF-8 字节长度 + UTF-8 字节。长度按字节计，内容按字符校验。
- `u32le`：4 字节小端无符号整数，仅用于 CRC32。
- CRC32：IEEE 802.3（zlib/PNG）多项式 `0xEDB88320`，初值 `0xFFFFFFFF`，末尾异或 `0xFFFFFFFF`。
- 压缩：**raw DEFLATE**（RFC 1951），不带 zlib/gzip 头。
- 文本：`base64url`（RFC 4648 §5，字母表 `A-Z a-z 0-9 - _`），导出不带 `=` 填充；解码器可接受填充。
- 整数与标志位不因平台而改变字节序；只有 CRC32 使用小端。

## 2. 信封

```text
偏移   长度        字段
0      4           magic = "MZC1" (4D 5A 43 31)
4      1           formatVersion = 1
5      1           registryVersion = 1
6      1           flags
7      varint      storedBodyLength
+      varint      plainBodyLength
+      N           storedBody
+      4           crc32le(plainBody)
```

`flags`：

| bit | 含义 |
|---|---|
| 0 | `circular` 圈织 |
| 1 | `frontView` 图示为正面效果 |
| 2 | `firstRowRightSide` 作品第一行为正面 |
| 3 | `firstRowRightToLeft` 第一行从右向左织 |
| 4 | `deflate`：`storedBody` 是 `plainBody` 的 raw DEFLATE |
| 5–7 | 保留，必须为 0 |

规则：

- `plainBodyLength ≤ 8 MiB`，`storedBodyLength ≤ 8 MiB`。
- 未设 deflate 时，两个长度必须相等且 `storedBody` 就是 `plainBody`。
- 设 deflate 时，解压后长度必须等于 `plainBodyLength`；解压失败或超限均拒绝。
- CRC32 始终对**未压缩**的 `plainBody` 计算。
- 导入时先校验 magic、版本、长度、解压、CRC，再解析 body。

## 3. Body

```text
name: string                    1–160 字符
note: string                    0–5000 字符
blockCount: varint              1–12
repeat blockCount times:
  blockName: string             0–160 字符（空名允许）
  rowCount: varint              1–400
  firstPrintedRow: varint       0–10000
  columns: varint               1–240
  rows: rowCount 行（见 §4）
sectionCount: varint            1–100
repeat sectionCount times:
  blockIndex: varint            0…blockCount−1
  firstRow: varint              1…该块行数
  lastRow: varint               firstRow…该块行数
  repeats: varint               1–100
  restartStitches: varint       0 = 无；否则 1–240
  startNote: string             0–2000 字符
castOnStitches: varint          0 = 无；否则 1–240
firstRowIsCastOn: u8            0 = 无；1 = false；2 = true
finishingNote: string           0–5000 字符
```

校验：

- 所有图块列数必须一致（同宽矩阵）。
- `Σ (lastRow − firstRow + 1) × repeats ≤ 20000`。
- 分段的行范围必须落在对应图块内。
- 字符长度按 Swift/Kotlin 的字符数口径；UTF-8 字节长度只用于读流。

## 4. 行与单元格

### 4.1 行操作码

每行以 1 字节开始：

| 值 | 含义 |
|---|---|
| `0x00` | 完整行：随后是若干 RLE 游程 |
| `0x01` | 与上一行完全相同（第 0 行不允许） |

### 4.2 完整行

完整行按列从左到右（与存储顺序一致，不因实际织造方向反转）写入游程：

```text
while remaining > 0:
  runLength: varint      1…remaining
  cell: u8               针法代码；0xFF 时后续 varint 为注册表 ID
  remaining -= runLength
```

- 游程必须恰好覆盖 `columns` 格；游程长度为 0 或超出剩余列数即格式错误。
- 单元格固定 1 字节；已知针法使用 §5 的代码。
- `0xFF` 为转义：后随 `varint` 稳定符号 ID（注册表 ID）。这样未来针法无需加宽单元格。
- 未登记的代码或转义 ID：将该格降级为 `U`，记录警告（符号 ID/行/列），**不中断整体解析**。
- 保留代码空间：`0x00–0x7F` 核心；`0x80–0xFE` 扩展；`0xFF` 转义。当前 17 个针法占 `0x00–0x10`。
- 行级 RLE 与「与上一行相同」在 DEFLATE 之前执行。

## 5. 针法注册表

稳定 ID 与线格式代码：

| ID | 代码 | key | 中文名 | 消耗 | 产出 | 逆操作 | 分组宽度 |
|---:|---|---|---|---:|---:|---|---:|
| 0 | `.` | noStitch | 无针格 | 0 | 0 | 0 | 1 |
| 1 | `K` | knit | 下针 | 1 | 1 | 2 | 1 |
| 2 | `P` | purl | 上针 | 1 | 1 | 1 | 1 |
| 3 | `U` | unknown | 未知 | 1 | 1 | —（不可逆） | 1 |
| 4 | `O` | yarnOver | 挂针 | 0 | 1 | 4 | 1 |
| 5 | `M` | makeLeft | 左扭加针 | 0 | 1 | —（不可逆） | 1 |
| 6 | `N` | makePurl | 扭加针（上针） | 0 | 1 | —（不可逆） | 1 |
| 7 | `T` | makeRight | 右扭加针 | 0 | 1 | —（不可逆） | 1 |
| 8 | `S` | slipFront | 滑针（线在前） | 1 | 1 | —（不可逆） | 1 |
| 9 | `L` | leftDecrease | 左上二并一 | 2 | 1 | 11 | 1 |
| 10 | `R` | rightDecrease | 右上二并一 | 2 | 1 | 12 | 1 |
| 11 | `D` | rightPurlDecrease | 右上二并一（上针） | 2 | 1 | 9 | 1 |
| 12 | `E` | leftPurlDecrease | 左上二并一（上针） | 2 | 1 | 10 | 1 |
| 13 | `A` | cableFront6 | 3针前置交叉 | 1 | 1 | —（不可逆） | 6 |
| 14 | `B` | cableBack6 | 3针后置交叉 | 1 | 1 | —（不可逆） | 6 |
| 15 | `C` | cableFront4 | 2针前置交叉 | 1 | 1 | —（不可逆） | 4 |
| 16 | `F` | cableBack2 | 1针后置交叉 | 1 | 1 | —（不可逆） | 2 |

- 逆操作是图面正面符号的逆；`null` 表示不能通用推断，反面遇到时必须保留为待核对。
- 麻花按整组执行；每格消耗/产出各 1，`groupWidth` 表示一组占用的连续格数。
- 注册表版本 1 的完整机器可读定义在 `stitch-registry.json`；新增针法只改该表并提升 `registryVersion`，线格式单元格宽度不变。
- 新针法优先使用未占用的核心 ID；超过 127 使用扩展 ID；超过 254 使用 `0xFF + varint`。未知 ID 一律降级为 `U`。

## 6. 文本形态与分片

### 6.1 单码

```text
MZC1:<base64url(envelope)>
```

- 解码器接受首尾空白；前缀区分大小写。
- 建议单个二维码文本不超过 2300 字符（QR Version 40、纠错 M 的字节模式上限 2331，留出余量）。
- 文本长度上限 12,000,000 字符；超过即拒绝。
- 失败原因使用稳定短码：`bad-magic`、`format-version`、`registry-version`、`truncated`、`varint`、`flags`、`length`、`checksum`、`deflate`、`blocks`、`rows`、`columns`、`row-op`、`runs`、`sections`、`name`、`note`、`casting`、`base64`、`size`、`shard`、`shard-merge`。

### 6.2 多码分片

单码装不下时：

```text
MZC1S:<i>/<n>:<crc32hex>:<base64url(chunk)>
```

- `i` 从 1 开始，`n` 为 2–99；`crc32hex` 是**完整二进制信封**的 CRC32，8 位小写十六进制。
- 每个 `chunk` 是完整信封的一段连续字节；`base64url` 解码后按 `i` 顺序直接拼接。
- 所有分片的 `n` 与 `crc32hex` 必须一致；缺片、重复 i、越界 i、拼接后 CRC 不符均拒绝（`shard-merge`）。
- 单码文本超过 2300 字符时开始分片；每片原始字节建议 1500 B（base64url 后 2000 字符，含头部约 2022 字符，安全落在 QR-M 内）。
- 同一张图的单码与分片不能混用；扫描器应先识别单码，再进入分片收集。

## 7. 印刷与二维码

- 纠错等级：**M**（QR Version 40 字节模式约 2331 B）。若印刷尺寸受限可使用 L（约 2953 B），但需在版面说明。
- 静区：四边各 **4 模块**（共 8 模块宽），必须保持纯白，不得裁切或叠加文字。
- 模块尺寸：屏幕上不小于 6 px/模块；印刷不小于 **0.4 mm/模块**，推荐 0.5–0.6 mm。
- 对比度：深色模块与纸面反差足够（建议纯黑/白，不用浅灰或彩色底）；避免反光纸与覆膜。
- 导出 PNG：边长至少 1024 px；实现按 `(模块数 + 8) × scale` 生成，`scale ≥ 8`，V40 时约 1480 px。
- 单张成品建议 35–50 mm 见方；多张时按 `1…n` 标注「第 i/n 张」，可排成网格或沿页边排列。
- 分片二维码要同时保留序号与总数；建议在每张下方印 `i/n` 与同一图表名称，避免混页。扫码时按序号合并。
- 不要把二维码缩放到低于 0.4 mm/模块；打印后先用手机试扫，再批量印刷。

## 8. 兼容与迁移

- MZC-1 与旧的 `MZZ1:`（raw DEFLATE + JSON）不是同一格式。新版本导出只产生 `MZC1:` / `MZC1S:`。
- 旧的 `MZZF1:`（含原图完整编码）从传输入口移除；带原图的分享只走 `.maozhizhi` 文件。
- 为迁移保留：导入可接受不含任何图片字段的 `MZZ1:` 数字编码；发现 `sourceImageBase64`、`correctedImageBase64` 或 `MZZF1:` 时明确拒绝。
- `.maozhizhi` 文件格式、项目私有 schema、iCloud 逻辑不变。

## 9. 实测尺寸

以当前仓库示例（含全部数字、不含图片）编码并启用 DEFLATE：

| 示例 | 针数 × 总行数 | 信封字节（iOS / Android） | 文本字符（iOS / Android） | 单码（≤2300 字符） |
|---|---:|---:|---:|---|
| `guernsey-scarf` | 29 × 384 | 726 / 727 | 973 / 975 | 是 |
| `yokota-hat` | 150 × 101 | 1472 / 1289 | 1968 / 1724 | 是 |
| `yokota-hat` 前 64 行 | 150 × 64 | 255 / 1113 | 345 / 1489 | 是 |

两端都启用 raw DEFLATE；压缩器实现不同（Apple Compression 与 zlib）会让同样内容产生不同长度，但不影响互读。五份示例（含苏菲、毛儿版）在两端的往返测试均通过；跨端解码结果见 `docs/verification/2026-10-22-android-scan-base64.md`。格式本身不依赖这些样例。

### 2026-10-24 追加针法 ID

在 registryVersion=1 下按既有未知 ID 降级契约追加17、18，不重排0—16。17=G/cableFront5（前置3针，2下，再织暂存3针）；18=H/cableBack5（后置2针，3下，再织暂存2针）。两者groupWidth=5，逐格消费/产出各1。旧接收端遇到这两个ID降级为U并阻止确认，不得把它们当作原来的6针麻花。`.maozhizhi` 格式版本独立升级至v5，MZC-1信封版本不变。

## Envelope v2（2026-10-24）

文本前缀仍为 `MZC1:` / `MZC1S:`，magic 和分片/CRC/压缩不变。格式版本字节为 2 时，解压后的 body 是**无图片的 v6 图纸 JSON**，而不是 v1 的针法数字流。registry 字节仍为 1。两端接收 v1/v2，旧接收端在版本检查时拒绝 v2。

仅 `hasTechniques` 图纸使用 v2；旧图纸继续编码 v1。v2 禁止顶层和图块的 source/corrected Base64 图片字段；接收后仍进入 `ChartPackage` 的严格校验并创建未确认的新草稿。不得把带针法快照、用线或分次说明的图纸编码成丢弃扩展的 v1。
