#!/usr/bin/env python3
# gen_samples.py — 本地造 R2 阅读器测试样本(EPUB/FB2/CBZ/MOBI7)。不联网、不拉真实书。
# 输出到 /tmp/rd_samples/。MOBI7 用手写 PalmDoc(LZ77) 压缩 + PDB 容器(对齐 libmobi mobi_decompress_lz77)。
import os, struct, zlib, base64, json
OUT = "/tmp/rd_samples"
os.makedirs(OUT, exist_ok=True)
def png(w, h, rgb):
"""最小 valid PNG(zlib deflate,RGB 8bit)。"""
raw = b""
for _ in range(h):
raw += b"\x00" + bytes(rgb) * w
def chunk(t, d):
return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xffffffff)
ihdr = struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr)
+ chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b""))
# ---------- EPUB ----------
def make_epub():
from zipfile import ZipFile, ZIP_DEFLATED
cover = png(20, 20, (200, 60, 60))
ch1 = """
第一章
第一章 测试
这是一段带格式的正文,包含实体 & 和标点符号。English words mixed here.
第二段:测试坏标签容错 未闭合 粗斜体

以上是数值实体引用的中文。
"""
ch2 = """
第二章
第二章 测试
第二章正文,用于验证多章节 spine 顺序。
结尾
"""
container = """
"""
opf = """
测试书MeowOS
"""
ncx = """
第一章
第二章
"""
with ZipFile(os.path.join(OUT, "sample.epub"), "w", ZIP_DEFLATED) as z:
z.writestr("mimetype", "application/epub+zip")
z.writestr("META-INF/container.xml", container)
z.writestr("OEBPS/content.opf", opf)
z.writestr("OEBPS/toc.ncx", ncx)
z.writestr("OEBPS/text/ch1.xhtml", ch1.encode("utf-8"))
z.writestr("OEBPS/text/ch2.xhtml", ch2.encode("utf-8"))
z.writestr("OEBPS/images/cover.png", cover)
# ---------- FB2 ----------
def make_fb2():
img_data = base64.b64encode(png(12, 12, (30, 120, 200))).decode()
fb2 = f"""
FB2测试MeoOS
第一章
这是 FB2 段落,含实体 & 与 正文。
{img_data}
"""
open(os.path.join(OUT, "sample.fb2"), "wb").write(fb2.encode("utf-8"))
# ---------- CBZ ----------
def make_cbz():
from zipfile import ZipFile, ZIP_STORED
with ZipFile(os.path.join(OUT, "sample.cbz"), "w", ZIP_STORED) as z:
z.writestr("01.png", png(30, 30, (10, 200, 10)))
z.writestr("02.png", png(40, 20, (200, 10, 10)))
z.writestr("03.png", png(50, 50, (10, 10, 220)))
# ---------- MOBI7 ----------
def palmdoc_compress(data):
"""把字节串压成 PalmDoc LZ77(对话 libmobi mobi_decompress_lz77)。
最简单的正确编码:全部字面量用「长度 1..8 的 run」。"""
out = bytearray()
i = 0
n = len(data)
while i < n:
run = min(8, n - i)
out.append(run) # 0x01..0x08 → 拷贝 run 个字面字节
out += data[i:i+run]
i += run
return bytes(out)
def make_mobi():
text = (
""
"MOBI 测试
"
"这是 Mobi 正文,用于验证 libmobi rawml 抽取与粗解析。
"
"第二段,含 entity & 与标点。
"
""
)
print(" mobi text bytes:", len(text.encode("utf-8")))
raw = text.encode("utf-8")
# PalmDoc 压缩后的文本记录(text_length = 未压缩长度)
comp = palmdoc_compress(raw)
# PDB 记录:
# record0 = PalmDoc 头(16) + MOBI 头(即 mobi_header) + 其余记录 offset 表等,需手排。
# 这里用 libmobi 认识的最小结构:
# record0 内含: compression=2(uncompressed)? 实际 PalmDoc 压缩 compression=2(PalmDoc)?看 mobi.h
# 我们按 MOBI_HEADER 经典布局构造。
import io
# --- 构造 record0 ---
# PalmDoc/EOF 头 16 字节(MOBIRecord0Header)
rec0_hdr = struct.pack(" PalmDoc LZ77? 0=none,1=?,2=PalmDoc
0, # unused
len(raw), # text_length (uncompressed)
1, # record_count (text records)
4096, # record_size
0, # encryption type
) # 12 bytes? 实际是 16 字节: compression,unused,text_length,record_count,record_size,encryption,unknown
# MOBIRecord0Header 实际 16 字节: compression(2),unused(2),text_length(4),record_count(2),record_size(2),encryption(2),unknown(2)
rec0_hdr = struct.pack(">HHIHHHH", 2, 0, len(raw), 1, 4096, 0, 0)
# MOBI 头(MOBIMobiHeader),经典字段(我们只需让 libmobi 能 parse)
mobi_header = bytearray()
mobi_header += b"MOBI" # identifier
mobi_header += struct.pack(">I", 0x100) # header length (0x100)
mobi_header += struct.pack(">I", 0x10000000) # mobi type
mobi_header += struct.pack(">I", 0) # text encoding (0 = cp1252, 65001=utf8)
mobi_header += struct.pack(">I", 0) # unique id
mobi_header += struct.pack(">I", 0) # file version
mobi_header += struct.pack(">I", 0) # orthographic index
mobi_header += struct.pack(">I", 0) # inflection index
mobi_header += struct.pack(">I", 0) # index names
mobi_header += struct.pack(">I", 0) # index keys
mobi_header += struct.pack(">I", 0) # extra index 0..5
mobi_header += struct.pack(">I", 0)
mobi_header += struct.pack(">I", 0)
mobi_header += struct.pack(">I", 0)
mobi_header += struct.pack(">I", 0)
mobi_header += struct.pack(">I", 0)
mobi_header += struct.pack(">I", 0) # first non-book index
mobi_header += struct.pack(">I", 0) # full name offset
mobi_header += struct.pack(">I", 0) # full name length
mobi_header += struct.pack(">I", 0) # locale
mobi_header += struct.pack(">I", 0) # input language
mobi_header += struct.pack(">I", 0) # output language
mobi_header += struct.pack(">I", 0) # min version
mobi_header += struct.pack(">I", 0) # first image index
mobi_header += struct.pack(">I", 0) # huffman record offset
mobi_header += struct.pack(">I", 0) # huffman record count
mobi_header += struct.pack(">I", 0) # huffman table offset
mobi_header += struct.pack(">I", 0) # huffman table length
mobi_header += struct.pack(">I", 0) # EXTH flags
mobi_header += struct.pack(">I", 32) # 32 unknown bytes
mobi_header += b"\x00" * 32
mobi_header += struct.pack(">I", 0) # unknown 0..4 (to 0x100)
mobi_header += b"\x00" * (0x100 - len(mobi_header))
# 内容记录:文本记录 + 一个 0xE98E0B0D EOF 记录
rec1 = comp
eof = struct.pack(">II", 0xE98E0B0D, 0) # EOF 记录
records = [rec0_hdr + bytes(mobi_header), rec1, eof]
# 构造 PDB
name = "TESTMOBI"
pdb = bytearray()
name_block = name.encode("utf-8")[:31].ljust(32, b"\x00")
pdb += name_block
pdb += struct.pack(">H", 0) # attributes
pdb += struct.pack(">H", 0) # version
pdb += struct.pack(">I", 0) # creation date
pdb += struct.pack(">I", 0) # modification
pdb += struct.pack(">I", 0) # backup
pdb += struct.pack(">I", 0) # modification number
pdb += struct.pack(">I", 0) # appInfo
pdb += struct.pack(">I", 0) # sortInfo
pdb += b"BOOK" # type
pdb += b"MOBI" # creator
pdb += struct.pack(">I", 0) # uniqueID seed
pdb += struct.pack(">I", 0) # nextRecordListID
pdb += struct.pack(">H", len(records)) # numRecords
# record 表(8 字节 each): offset(4), attr(1), uniqueID(3)
data_start = 78 + 8 * len(records) + 2 # + appInfo? PDB 有 appInfo+sortInfo? 这里偏移从 78 起步
# 实际 PDB: 头 78 字节,然后 record list,然后 appInfo (2 bytes 0,1), sortInfo(2 bytes) 可缺省
# 我们用 title 记录头: 78 + 8*n; 记录区从 (78+8*n+? ) 起;这里不加 appInfo 让 offset 简单
offs = []
cur = 78 + 8 * len(records)
for r in records:
offs.append(cur)
cur += len(r)
for i, o in enumerate(offs):
pdb += struct.pack(">I", o) # offset (4)
pdb += struct.pack("