{
  "lead": "ZIP、gzip、PNG 和网页传输里的 HTTP 压缩，背后都是同一个算法：DEFLATE（RFC 1951）。它分两步：先用 LZ77 把重复出现的内容换成「往回数 d 个字节、复制 l 个」的引用，再用哈夫曼编码让常见的符号用更短的比特表示。这个示例完整实现了压缩和解压，输出的是标准格式，能被任何 zlib、浏览器或解压软件读取。",
  "formulas": [
    [
      "平均码长 ≥ H = −Σ p<sub>i</sub> log<sub>2</sub> p<sub>i</sub>",
      "哈夫曼编码逼近香农熵下限"
    ]
  ],
  "terms": [
    [
      "LZ77 与哈希链",
      "从左到右扫描，对当前位置的前 3 个字节做哈希，在哈希表里找到最近几个以相同 3 字节开头的位置，逐一比较，取最长的匹配（最长 258 字节，最远 32 KB）。压缩级别越高，沿哈希链找得越远，越慢但越小。"
    ],
    [
      "惰性匹配",
      "4 级以上会多看一步：如果下一个位置能找到更长的匹配，就先把当前字节原样输出，把匹配留给下一个位置。这能让压缩率再提高几个百分点。"
    ],
    [
      "哈夫曼编码",
      "字面量（0–255）、结束符和 29 种长度区间共用一张 286 符号的码表，32 KB 内的距离另用一张 30 符号的码表。按出现频率建哈夫曼树，出现越多的符号码越短；码长上限 15 位，超出时把频率减半重建，直到满足限制。"
    ],
    [
      "块与三种编码",
      "每 16384 个符号组成一个块，分别估算三种写法的比特数：原样存储（适合随机数据）、标准预定义的固定码表（适合很短的数据）、为这个块专门生成的动态码表（通常最小），选最小的那种。动态码表本身也要压缩：码长序列先做游程编码，再用一张小哈夫曼表编码。"
    ]
  ],
  "flow": [
    [
      "js",
      "JS 把文字转成 UTF-8 字节（或读取拖入的文件），调用 <code>deflate(数据, 级别)</code>。"
    ],
    [
      "rust",
      "Rust 做 LZ77 匹配、为每块选择最优编码并按位输出，得到原始 DEFLATE 流；<code>deflate_stats()</code> 返回字面量、匹配和各类块的数量。"
    ],
    [
      "js",
      "JS 再用浏览器内置的 <code>CompressionStream('deflate-raw')</code> 压缩同一份数据，然后交叉校验：浏览器解压 Rust 的结果，Rust 的 <code>inflate()</code> 解压浏览器的结果，都必须还原成原始数据。"
    ],
    [
      "rust",
      "<code>lz77_tokens()</code> 返回前 600 字节的匹配过程，JS 把往回引用的片段高亮显示。"
    ]
  ],
  "perf": [
    "测试时与 Python 的 zlib 做了双向对照：1,360 次「本实现压缩 → zlib 解压」和 1,088 次「zlib 压缩 → 本实现解压」全部还原，覆盖空数据、随机字节、长游程、源代码和中文文本。",
    "6 级压缩下，同一批数据的总大小与 zlib 6 级相差不到 0.1%。页面上几十 KB 的示例中，Rust 版耗时通常不到 1 毫秒，浏览器的 <code>CompressionStream</code> 反而要几毫秒：它是异步的流式接口，数据量小时，创建流和调度的开销占了大头。两者的压缩率基本相同。",
    "随机字节几乎无法压缩，此时编码器会选择「原样存储」块，结果只比原数据多每 64 KB 5 个字节，不会越压越大。"
  ],
  "src": [
    [
      "crates/tools/compress/src/lib.rs",
      ""
    ],
    [
      "www/compress/index.js",
      ""
    ]
  ]
}
