Files
AMI/scripts/export_can_pdf.py
T

149 lines
6.7 KiB
Python

"""Render the verified Chinese CAN Markdown document as a printable PDF."""
import argparse
import html
import re
from pathlib import Path
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib.styles import ParagraphStyle
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
KeepTogether, PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle,
)
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / 'docs/AMI_CAN_DBC说明.md'
OUTPUT = ROOT / 'output/pdf/AMI_CAN报文说明.pdf'
REMOTE = 'https://git.acetaffy.ltd/acetaffy/AMI/src/branch/main/'
def inline(text):
"""Escape Markdown and retain useful links in the printed document."""
text = html.escape(text)
text = re.sub(r'`([^`]+)`', r'<font color="#155E75">\1</font>', text)
def link(match):
label, target = match.groups()
if not target.startswith(('http://', 'https://')):
target = REMOTE + (target[3:] if target.startswith('../') else 'docs/' + target)
return f'<link href="{target}" color="#155E75">{label}</link>'
return re.sub(r'\[([^\]]+)\]\(([^)]+)\)', link, text)
def export(font_path):
pdfmetrics.registerFont(TTFont('AMIChinese', str(font_path), subfontIndex=0))
page_width, page_height = landscape(A4)
width = page_width - 80
body = ParagraphStyle('Body', fontName='AMIChinese', fontSize=9.3,
leading=15, spaceAfter=8, wordWrap='CJK', alignment=TA_LEFT)
heading = ParagraphStyle('Heading', parent=body, fontSize=16, leading=23,
textColor=colors.HexColor('#12485E'), spaceBefore=3,
spaceAfter=12, keepWithNext=True)
subheading = ParagraphStyle('Subheading', parent=body, fontSize=11,
leading=17, textColor=colors.HexColor('#12485E'),
spaceBefore=8, spaceAfter=6, keepWithNext=True)
title = ParagraphStyle('Title', parent=heading, fontSize=25, leading=32)
cell = ParagraphStyle('Cell', parent=body, fontSize=8.5, leading=12, spaceAfter=0)
code = ParagraphStyle('Code', parent=body, fontSize=8, leading=13,
backColor=colors.HexColor('#F2F5F7'), borderPadding=8)
lines = SOURCE.read_text(encoding='utf-8').splitlines()
story = []
i = 0
while i < len(lines):
line = lines[i]
if not line.strip():
i += 1
continue
if line.startswith('# '):
story.append(Paragraph('AMI CAN 报文说明', title))
elif line.startswith('## '):
section = line[3:]
if section != '使用范围':
story.append(PageBreak())
story.append(Paragraph(inline(section), heading))
elif line.startswith('### '):
story.append(Paragraph(inline(line[4:]), subheading))
elif line.startswith('|'):
rows = []
while i < len(lines) and lines[i].startswith('|'):
row = [x.strip() for x in lines[i].strip().strip('|').split('|')]
if not all(re.fullmatch(r':?-+:?', x) for x in row):
rows.append([Paragraph(inline(x), cell) for x in row])
i += 1
count = len(rows[0])
proportions = {
2: [0.15, 0.85],
3: [0.17, 0.40, 0.43],
4: [0.12, 0.15, 0.20, 0.53],
6: [0.11, 0.055, 0.04, 0.28, 0.365, 0.15],
8: [0.32, 0.055, 0.055, 0.10, 0.09, 0.08, 0.06, 0.24],
}[count]
total = sum(proportions)
table = Table(rows, colWidths=[width*x/total for x in proportions],
repeatRows=1, hAlign='LEFT')
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#DDECF2')),
('ROWBACKGROUNDS', (0, 1), (-1, -1),
[colors.white, colors.HexColor('#F5F8FA')]),
('LINEBELOW', (0, 0), (-1, 0), 0.8, colors.HexColor('#7DABB9')),
('LINEBELOW', (0, 1), (-1, -1), 0.3, colors.HexColor('#D7E0E5')),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 7),
('RIGHTPADDING', (0, 0), (-1, -1), 7),
('TOPPADDING', (0, 0), (-1, -1), 6),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
]))
if i+1 < len(lines) and not lines[i].strip() and lines[i+1].startswith('`'):
group = [table, Spacer(1, 10), Paragraph(inline(lines[i+1]), body)]
if story and isinstance(story[-1], Paragraph) and story[-1].style is subheading:
group.insert(0, story.pop())
story.append(KeepTogether(group))
i += 2
else:
story.extend([table, Spacer(1, 10)])
continue
elif line.startswith('```'):
block = []
i += 1
while i < len(lines) and not lines[i].startswith('```'):
block.append(html.escape(lines[i]))
i += 1
story.append(KeepTogether([Paragraph('<br/>'.join(block), code)]))
else:
paragraph = [line]
while i+1 < len(lines) and lines[i+1].strip() and not lines[i+1].startswith(('#', '|', '```')):
i += 1
paragraph.append(lines[i])
story.append(Paragraph(inline(' '.join(paragraph)), body))
i += 1
def footer(canvas, doc):
canvas.saveState()
canvas.setStrokeColor(colors.HexColor('#C4D8E1'))
canvas.line(40, 31, page_width-40, 31)
canvas.setFont('AMIChinese', 8)
canvas.setFillColor(colors.HexColor('#506775'))
canvas.drawString(40, 19, 'AMI-2.5 | 2026-10-04 | CAN1 500K / CAN2 250K')
canvas.drawRightString(page_width-40, 19, f'{doc.page}')
canvas.restoreState()
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
document = SimpleDocTemplate(str(OUTPUT), pagesize=landscape(A4),
leftMargin=40, rightMargin=40,
topMargin=36, bottomMargin=44,
title='AMI CAN 报文说明', author='AMI / YMR',
subject='AMI CAN DBC v2.5: CAN1 500K / CAN2 250K')
document.build(story, onFirstPage=footer, onLaterPages=footer)
print(OUTPUT)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--font', type=Path, default=Path('C:/Windows/Fonts/simhei.ttf'),
help='CJK TrueType font path (default: Windows SimHei)')
export(parser.parse_args().font)