177 lines
7.0 KiB
Python
177 lines
7.0 KiB
Python
# -*- 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()
|