# -*- coding: utf-8 -*-
"""
manual-pages-to-pdf.py —— 把各系列手册页 PNG 打包成 PDF (供"下载数据表 PDF"用)

背景: 尺寸图窗口显示的手册页是 PNG (manual\\<系列>\\pXX.png)。客户还需要能
下载 PDF 文档 → 本脚本把每个系列的手册页按序合成一个 PDF, 输出
manual\\<系列>\\<系列>_manual.pdf, 并把 datasheet 附件写进 series\\<系列>.meta.json
(软件里【下载数据表 PDF】按钮直接可用, 网页/桌面两端一致)。

纯标准库实现 (zlib 解/压缩 PNG + 手写 PDF), 无任何第三方依赖。
真实手册替换 pXX.png 后重跑本脚本即可更新 PDF。

用法: python manual-pages-to-pdf.py
"""
import json
import os
import struct
import zlib

BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MANUAL_DIR = os.path.join(BASE, 'catalog', 'manual')
SERIES_DIR = os.path.join(BASE, 'catalog', 'series')

# A4 页面 (pt)
PAGE_W, PAGE_H = 595.28, 841.89


def parse_png(path):
    """解析 PNG → (width, height, RGB 字节串)。支持 8bit 灰度/RGB/RGBA, 非隔行。"""
    with open(path, 'rb') as f:
        data = f.read()
    assert data[:8] == b'\x89PNG\r\n\x1a\n', '非 PNG: %s' % path
    pos, idat, w, h, bd, ct, interlace = 8, b'', 0, 0, 0, 0, 0
    while pos < len(data):
        (length,) = struct.unpack('>I', data[pos:pos + 4])
        typ = data[pos + 4:pos + 8]
        chunk = data[pos + 8:pos + 8 + length]
        if typ == b'IHDR':
            w, h, bd, ct, _, _, interlace = struct.unpack('>IIBBBBB', chunk)
        elif typ == b'IDAT':
            idat += chunk
        elif typ == b'IEND':
            break
        pos += 12 + length
    assert bd == 8 and interlace == 0, '仅支持 8bit 非隔行 PNG'
    channels = {0: 1, 2: 3, 4: 2, 6: 4}.get(ct)
    assert channels, '不支持的色彩类型 %d' % ct
    raw = zlib.decompress(idat)
    bpp = channels
    stride = w * bpp
    out = bytearray()
    prev = bytearray(stride)
    for y in range(h):
        f = raw[y * (stride + 1)]
        line = bytearray(raw[y * (stride + 1) + 1:(y + 1) * (stride + 1)])
        if f == 1:      # Sub
            for i in range(bpp, stride):
                line[i] = (line[i] + line[i - bpp]) & 0xFF
        elif f == 2:    # Up
            for i in range(stride):
                line[i] = (line[i] + prev[i]) & 0xFF
        elif f == 3:    # Average
            for i in range(stride):
                a = line[i - bpp] if i >= bpp else 0
                line[i] = (line[i] + ((a + prev[i]) >> 1)) & 0xFF
        elif f == 4:    # Paeth
            for i in range(stride):
                a = line[i - bpp] if i >= bpp else 0
                b = prev[i]
                c = prev[i - bpp] if i >= bpp else 0
                p = a + b - c
                pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
                pr = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
                line[i] = (line[i] + pr) & 0xFF
        if channels == 4:   # RGBA → RGB (白底合成)
            rgb = bytearray(w * 3)
            for i in range(w):
                a4 = line[i * 4 + 3]
                for k in range(3):
                    rgb[i * 3 + k] = (line[i * 4 + k] * a4 + 255 * (255 - a4)) // 255
            out += rgb
        elif channels == 1:  # 灰度 → RGB
            rgb = bytearray(w * 3)
            for i in range(w):
                rgb[i * 3] = rgb[i * 3 + 1] = rgb[i * 3 + 2] = line[i]
            out += rgb
        else:
            out += line
        prev = line
    return w, h, bytes(out)


def build_pdf(images):
    """images: [(w, h, rgb_bytes)] → PDF 字节串 (每图一页 A4 等比铺满)"""
    objs = []          # (num, bytes)
    kids = []

    def add(body):
        objs.append((len(objs) + 1, body))
        return len(objs)

    for (w, h, rgb) in images:
        # 等比缩放铺满 A4
        scale = min(PAGE_W / w, PAGE_H / h)
        dw, dh = w * scale, h * scale
        x0, y0 = (PAGE_W - dw) / 2.0, (PAGE_H - dh) / 2.0
        stream = b'q %f 0 0 %f %f %f cm /Im0 Do Q' % (dw, dh, x0, y0)
        img_num = add(b'<< /Type /XObject /Subtype /Image /Width %d /Height %d '
                      b'/ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode '
                      b'/Length %d >>\nstream\n' % (w, h, len(zlib.compress(rgb, 6)))
                      + zlib.compress(rgb, 6) + b'\nendstream')
        content_num = add(b'<< /Length %d >>\nstream\n' % len(stream) + stream + b'\nendstream')
        page_num = add(b'<< /Type /Page /Parent %d 0 R /MediaBox [0 0 %f %f] '
                       b'/Resources << /XObject << /Im0 %d 0 R >> >> /Contents %d 0 R >>\n'
                       % (0, PAGE_W, PAGE_H, img_num, content_num))
        kids.append(page_num)

    pages_num = len(objs) + 1
    kids_ref = b' '.join(b'%d 0 R' % k for k in kids)
    pages_obj = (b'<< /Type /Pages /Kids [%s] /Count %d >>' % (kids_ref, len(kids)))
    objs.append((pages_num, pages_obj))
    catalog_num = pages_num + 1
    objs.append((catalog_num, b'<< /Type /Catalog /Pages %d 0 R >>' % pages_num))
    # 修正 page 里的 Parent 引用
    for i, (n, body) in enumerate(objs):
        if b'/Parent 0 0 R' in body:
            objs[i] = (n, body.replace(b'/Parent 0 0 R', b'/Parent %d 0 R' % pages_num))

    out = bytearray(b'%PDF-1.4\n%\xe2\xe3\xcf\xd3\n')
    offsets = []
    for n, body in objs:
        offsets.append(len(out))
        out += b'%d 0 obj\n' % n + body + b'\nendobj\n'
    xref_pos = len(out)
    out += b'xref\n0 %d\n' % (len(objs) + 1)
    out += b'0000000000 65535 f \n'
    for off in offsets:
        out += b'%010d 00000 n \n' % off
    out += b'trailer\n<< /Size %d /Root %d 0 R >>\nstartxref\n%d\n%%%%EOF\n' % (len(objs) + 1, catalog_num, xref_pos)
    return bytes(out)


def update_meta(series, pdf_name):
    meta_path = os.path.join(SERIES_DIR, series + '.meta.json')
    with open(meta_path, 'r', encoding='utf-8') as f:
        obj = json.load(f)
    atts = list(obj.get('attachments', []))
    atts = [a for a in atts if a.get('kind') != 'datasheet']
    atts.append({'kind': 'datasheet', 'lang': '', 'source': 'manual/%s/%s' % (series, pdf_name), 'model': None})
    obj['attachments'] = atts
    with open(meta_path, 'w', encoding='utf-8') as f:
        json.dump(obj, f, ensure_ascii=False)


def main():
    made = 0
    for series in sorted(os.listdir(MANUAL_DIR)):
        d = os.path.join(MANUAL_DIR, series)
        if not os.path.isdir(d):
            continue
        pngs = sorted(f for f in os.listdir(d) if f.lower().endswith('.png'))
        if not pngs:
            continue
        images = [parse_png(os.path.join(d, p)) for p in pngs]
        pdf_name = series + '_manual.pdf'
        with open(os.path.join(d, pdf_name), 'wb') as f:
            f.write(build_pdf(images))
        update_meta(series, pdf_name)
        made += 1
        print('OK %s: %d 页 → %s' % (series, len(images), pdf_name))
    print('完成 %d 个系列 PDF' % made)
    print('提示: 替换手册页 PNG 后重跑本脚本更新 PDF, 再【重建全量目录】+ 发布')


if __name__ == '__main__':
    main()
