"""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'\1', 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'{label}' 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('
'.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)