@nguyenphp/antigravity-marketing 1.0.18 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -78
- package/package.json +4 -3
- package/templates/.agent/skills/marketing-report-expert/SKILL.md +70 -0
- package/templates/.agent/skills/minimax-docx/LICENSE +21 -0
- package/templates/.agent/skills/minimax-docx/SKILL.md +274 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/academic_styles.xml +250 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/corporate_styles.xml +284 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/default_styles.xml +449 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/aesthetic-rules.xsd +470 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/business-rules.xsd +130 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/common-types.xsd +159 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/wml-subset.xsd +589 -0
- package/templates/.agent/skills/minimax-docx/references/cjk_typography.md +357 -0
- package/templates/.agent/skills/minimax-docx/references/cjk_university_template_guide.md +184 -0
- package/templates/.agent/skills/minimax-docx/references/comments_guide.md +191 -0
- package/templates/.agent/skills/minimax-docx/references/design_good_bad_examples.md +829 -0
- package/templates/.agent/skills/minimax-docx/references/design_principles.md +819 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_element_order.md +308 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part1.md +4061 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part2.md +2820 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part3.md +3381 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_namespaces.md +82 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_units.md +72 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_a_create.md +284 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_b_edit_content.md +295 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_c_apply_template.md +456 -0
- package/templates/.agent/skills/minimax-docx/references/track_changes_guide.md +200 -0
- package/templates/.agent/skills/minimax-docx/references/troubleshooting.md +506 -0
- package/templates/.agent/skills/minimax-docx/references/typography_guide.md +294 -0
- package/templates/.agent/skills/minimax-docx/references/xsd_validation_guide.md +158 -0
- package/templates/.agent/skills/minimax-docx/scripts/doc_to_docx.sh +40 -0
- package/templates/.agent/skills/minimax-docx/scripts/docx_preview.sh +37 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj +19 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs +18 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/AnalyzeCommand.cs +147 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ApplyTemplateCommand.cs +322 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/CreateCommand.cs +324 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/DiffCommand.cs +155 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/EditContentCommand.cs +487 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/FixOrderCommand.cs +108 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/MergeRunsCommand.cs +122 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ValidateCommand.cs +107 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj +15 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/CommentSynchronizer.cs +169 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/ElementOrder.cs +80 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/NamespaceConstants.cs +42 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/RunMerger.cs +81 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/StyleAnalyzer.cs +81 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/TrackChangesHelper.cs +99 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/UnitConverter.cs +23 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples.cs +1832 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch1.cs +910 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch2.cs +999 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch3.cs +1048 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch4.cs +1038 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/CharacterFormattingSamples.cs +1020 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/DocumentCreationSamples.cs +1121 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs +624 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FootnoteAndCommentSamples.cs +675 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/HeaderFooterSamples.cs +838 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ImageSamples.cs +917 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ListAndNumberingSamples.cs +826 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ParagraphFormattingSamples.cs +1199 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/StyleSystemSamples.cs +1487 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TableSamples.cs +1163 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TrackChangesSamples.cs +595 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/CjkHelper.cs +39 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/FontDefaults.cs +24 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/PageSizes.cs +20 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/BusinessRuleValidator.cs +224 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/GateCheckValidator.cs +148 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/ValidationResult.cs +23 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/XsdValidator.cs +69 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx +4 -0
- package/templates/.agent/skills/minimax-docx/scripts/env_check.sh +196 -0
- package/templates/.agent/skills/minimax-docx/scripts/setup.ps1 +274 -0
- package/templates/.agent/skills/minimax-docx/scripts/setup.sh +504 -0
- package/templates/.agent/skills/minimax-multimodal-toolkit/SKILL.md +359 -0
- package/templates/.agent/skills/minimax-pdf/README.md +222 -0
- package/templates/.agent/skills/minimax-pdf/SKILL.md +201 -0
- package/templates/.agent/skills/minimax-pdf/design/design.md +381 -0
- package/templates/.agent/skills/minimax-pdf/scripts/cover.py +1579 -0
- package/templates/.agent/skills/minimax-pdf/scripts/fill_inspect.py +200 -0
- package/templates/.agent/skills/minimax-pdf/scripts/fill_write.py +242 -0
- package/templates/.agent/skills/minimax-pdf/scripts/make.sh +491 -0
- package/templates/.agent/skills/minimax-pdf/scripts/merge.py +112 -0
- package/templates/.agent/skills/minimax-pdf/scripts/palette.py +559 -0
- package/templates/.agent/skills/minimax-pdf/scripts/reformat_parse.py +374 -0
- package/templates/.agent/skills/minimax-pdf/scripts/render_body.py +1055 -0
- package/templates/.agent/skills/minimax-pdf/scripts/render_cover.cjs +111 -0
- package/templates/.agent/skills/minimax-xlsx/SKILL.md +138 -0
- package/templates/.agent/skills/minimax-xlsx/references/create.md +691 -0
- package/templates/.agent/skills/minimax-xlsx/references/edit.md +684 -0
- package/templates/.agent/skills/minimax-xlsx/references/fix.md +37 -0
- package/templates/.agent/skills/minimax-xlsx/references/format.md +768 -0
- package/templates/.agent/skills/minimax-xlsx/references/ooxml-cheatsheet.md +231 -0
- package/templates/.agent/skills/minimax-xlsx/references/read-analyze.md +97 -0
- package/templates/.agent/skills/minimax-xlsx/references/validate.md +772 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/formula_check.py +422 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/libreoffice_recalc.py +248 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/shared_strings_builder.py +163 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/style_audit.py +575 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_add_column.py +395 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_insert_row.py +274 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_pack.py +87 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_reader.py +362 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_shift_rows.py +396 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_unpack.py +130 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/[Content_Types].xml +9 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/_rels/.rels +6 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +19 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +33 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/styles.xml +160 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/workbook.xml +30 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +70 -0
- package/templates/.agent/skills/pptx-generator/SKILL.md +249 -0
- package/templates/.agent/skills/pptx-generator/references/design-system.md +392 -0
- package/templates/.agent/skills/pptx-generator/references/editing.md +162 -0
- package/templates/.agent/skills/pptx-generator/references/pitfalls.md +112 -0
- package/templates/.agent/skills/pptx-generator/references/pptxgenjs.md +420 -0
- package/templates/.agent/skills/pptx-generator/references/slide-types.md +413 -0
- package/templates/.agent/skills/tutorial-video-expert/SKILL.md +88 -0
- package/templates/.agent/skills/ui-ux-pro-max/SKILL.md +170 -585
- package/templates/.agent/skills/vision-analysis/SKILL.md +174 -0
- package/templates/.agent/workflows/analyze.md +3 -0
- package/templates/.agent/workflows/brand-report.md +44 -0
- package/templates/.agent/workflows/report.md +49 -0
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
render_body.py — Build the inner-page PDF from tokens.json + content.json.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python3 render_body.py --tokens tokens.json --content content.json --out body.pdf
|
|
7
|
+
|
|
8
|
+
Block types:
|
|
9
|
+
h1 h2 h3 Headings (h1 adds a full-width accent rule below)
|
|
10
|
+
body Justified prose paragraph
|
|
11
|
+
bullet Bullet list item (• prefix)
|
|
12
|
+
numbered Auto-numbered list item (resets when interrupted)
|
|
13
|
+
callout Highlighted insight box with left accent bar
|
|
14
|
+
table Data table with accent header + alternating rows
|
|
15
|
+
image Inline image from file path
|
|
16
|
+
figure Image with auto-numbered "Figure N:" caption
|
|
17
|
+
code Monospace code block with accent left border
|
|
18
|
+
math Display math formula via matplotlib mathtext
|
|
19
|
+
chart Bar / line / pie chart rendered via matplotlib
|
|
20
|
+
flowchart Process diagram rendered via matplotlib
|
|
21
|
+
bibliography Numbered reference list
|
|
22
|
+
divider Full-width accent rule
|
|
23
|
+
caption Small muted text (e.g., under a figure)
|
|
24
|
+
pagebreak Force a new page
|
|
25
|
+
spacer Vertical whitespace (pt field, default 12)
|
|
26
|
+
|
|
27
|
+
Exit codes: 0 success, 1 bad args/missing file, 2 missing dep, 3 render error
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import io
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
import importlib.util
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── Dependency bootstrap ───────────────────────────────────────────────────────
|
|
39
|
+
def ensure_deps():
|
|
40
|
+
missing = [p for p in ("reportlab", "pypdf")
|
|
41
|
+
if importlib.util.find_spec(p) is None]
|
|
42
|
+
if missing:
|
|
43
|
+
import subprocess
|
|
44
|
+
subprocess.check_call(
|
|
45
|
+
[sys.executable, "-m", "pip", "install",
|
|
46
|
+
"--break-system-packages", "-q"] + missing
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
ensure_deps()
|
|
51
|
+
|
|
52
|
+
from reportlab.platypus import (
|
|
53
|
+
BaseDocTemplate, PageTemplate, Frame,
|
|
54
|
+
Paragraph, Spacer, Table, TableStyle,
|
|
55
|
+
HRFlowable, PageBreak, Flowable, KeepTogether,
|
|
56
|
+
Preformatted, Image as RLImage,
|
|
57
|
+
)
|
|
58
|
+
from reportlab.lib.pagesizes import A4
|
|
59
|
+
from reportlab.lib.styles import ParagraphStyle
|
|
60
|
+
from reportlab.lib.colors import HexColor
|
|
61
|
+
from reportlab.lib.enums import TA_JUSTIFY, TA_CENTER
|
|
62
|
+
from reportlab.pdfbase import pdfmetrics
|
|
63
|
+
from reportlab.pdfbase.ttfonts import TTFont
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── Font registration ──────────────────────────────────────────────────────────
|
|
67
|
+
def register_fonts(tokens: dict):
|
|
68
|
+
"""Register TTF fonts from token font_paths if present."""
|
|
69
|
+
for name, fpath in tokens.get("font_paths", {}).items():
|
|
70
|
+
if os.path.exists(fpath):
|
|
71
|
+
try:
|
|
72
|
+
pdfmetrics.registerFont(TTFont(name, fpath))
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
78
|
+
# Custom Flowables
|
|
79
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
80
|
+
|
|
81
|
+
class CalloutBox(Flowable):
|
|
82
|
+
"""Highlighted insight box: coloured background + 4px left accent bar."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, text: str, style, accent: str, bg: str):
|
|
85
|
+
super().__init__()
|
|
86
|
+
self._para = Paragraph(text, style)
|
|
87
|
+
self._accent = HexColor(accent)
|
|
88
|
+
self._bg = HexColor(bg)
|
|
89
|
+
|
|
90
|
+
def wrap(self, aw, ah):
|
|
91
|
+
self._w = aw
|
|
92
|
+
_, ph = self._para.wrap(aw - 36, ah)
|
|
93
|
+
self._h = ph + 22
|
|
94
|
+
return aw, self._h
|
|
95
|
+
|
|
96
|
+
def draw(self):
|
|
97
|
+
c = self.canv
|
|
98
|
+
c.setFillColor(self._bg)
|
|
99
|
+
c.roundRect(0, 0, self._w, self._h, 5, fill=1, stroke=0)
|
|
100
|
+
c.setFillColor(self._accent)
|
|
101
|
+
c.rect(0, 0, 4, self._h, fill=1, stroke=0)
|
|
102
|
+
self._para.drawOn(c, 18, 11)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class BibliographyItem(Flowable):
|
|
106
|
+
"""Single hanging-indent bibliography entry rendered as [N] text."""
|
|
107
|
+
|
|
108
|
+
LABEL_W = 28
|
|
109
|
+
|
|
110
|
+
def __init__(self, ref_id: str, text: str, style, dark: str):
|
|
111
|
+
super().__init__()
|
|
112
|
+
self._id = ref_id
|
|
113
|
+
self._text = text
|
|
114
|
+
self._style = style
|
|
115
|
+
self._dark = HexColor(dark)
|
|
116
|
+
|
|
117
|
+
def wrap(self, aw, ah):
|
|
118
|
+
self._w = aw
|
|
119
|
+
self._para = Paragraph(self._text, self._style)
|
|
120
|
+
_, ph = self._para.wrap(aw - self.LABEL_W, ah)
|
|
121
|
+
self._h = ph + 4
|
|
122
|
+
return aw, self._h
|
|
123
|
+
|
|
124
|
+
def draw(self):
|
|
125
|
+
c = self.canv
|
|
126
|
+
c.setFillColor(self._dark)
|
|
127
|
+
c.setFont("Helvetica-Bold", 8.5)
|
|
128
|
+
c.drawString(0, self._h - 12, f"[{self._id}]")
|
|
129
|
+
self._para.drawOn(c, self.LABEL_W, 2)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
133
|
+
# Page template (header + footer)
|
|
134
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
135
|
+
|
|
136
|
+
class BeautifulDoc(BaseDocTemplate):
|
|
137
|
+
def __init__(self, path: str, tokens: dict, **kw):
|
|
138
|
+
self._t = tokens
|
|
139
|
+
super().__init__(path, **kw)
|
|
140
|
+
fr = Frame(
|
|
141
|
+
self.leftMargin, self.bottomMargin,
|
|
142
|
+
self.width, self.height, id="body",
|
|
143
|
+
)
|
|
144
|
+
tmpl = PageTemplate(id="main", frames=fr, onPage=self._decorate)
|
|
145
|
+
self.addPageTemplates([tmpl])
|
|
146
|
+
|
|
147
|
+
def _decorate(self, canv, doc):
|
|
148
|
+
t = self._t
|
|
149
|
+
lm = doc.leftMargin
|
|
150
|
+
rm = doc.rightMargin
|
|
151
|
+
pw = doc.pagesize[0]
|
|
152
|
+
ph = doc.pagesize[1]
|
|
153
|
+
top = ph - doc.topMargin
|
|
154
|
+
|
|
155
|
+
canv.saveState()
|
|
156
|
+
|
|
157
|
+
# Header accent rule
|
|
158
|
+
canv.setStrokeColor(HexColor(t["accent"]))
|
|
159
|
+
canv.setLineWidth(1.5)
|
|
160
|
+
canv.line(lm, top + 12, pw - rm, top + 12)
|
|
161
|
+
|
|
162
|
+
# Header: title (left) + date (right)
|
|
163
|
+
canv.setFillColor(HexColor(t["muted"]))
|
|
164
|
+
canv.setFont(t["font_body_rl"], t["size_meta"])
|
|
165
|
+
canv.drawString(lm, top + 16, t["title"].upper())
|
|
166
|
+
canv.drawRightString(pw - rm, top + 16, t.get("date", ""))
|
|
167
|
+
|
|
168
|
+
# Footer rule
|
|
169
|
+
canv.setStrokeColor(HexColor("#DDDDDD"))
|
|
170
|
+
canv.setLineWidth(0.5)
|
|
171
|
+
canv.line(lm, doc.bottomMargin - 12, pw - rm, doc.bottomMargin - 12)
|
|
172
|
+
|
|
173
|
+
# Footer: author (left) + page number (right)
|
|
174
|
+
canv.setFillColor(HexColor(t["muted"]))
|
|
175
|
+
canv.setFont(t["font_body_rl"], t["size_meta"])
|
|
176
|
+
canv.drawString(lm, doc.bottomMargin - 22, t.get("author", ""))
|
|
177
|
+
canv.drawRightString(pw - rm, doc.bottomMargin - 22, str(doc.page))
|
|
178
|
+
|
|
179
|
+
canv.restoreState()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
183
|
+
# Style factory
|
|
184
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
185
|
+
|
|
186
|
+
def make_styles(t: dict) -> dict:
|
|
187
|
+
hf = t["font_display_rl"]
|
|
188
|
+
bf = t["font_body_rl"]
|
|
189
|
+
bfb = t["font_body_b_rl"]
|
|
190
|
+
dk = t["body_text"]
|
|
191
|
+
d = t["dark"]
|
|
192
|
+
mu = t["muted"]
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"h1": ParagraphStyle("H1",
|
|
196
|
+
fontName=hf, fontSize=t["size_h1"],
|
|
197
|
+
leading=t["size_h1"] * 1.3,
|
|
198
|
+
textColor=HexColor(d),
|
|
199
|
+
spaceBefore=t["section_gap"], spaceAfter=4,
|
|
200
|
+
),
|
|
201
|
+
"h2": ParagraphStyle("H2",
|
|
202
|
+
fontName=hf, fontSize=t["size_h2"],
|
|
203
|
+
leading=t["size_h2"] * 1.4,
|
|
204
|
+
textColor=HexColor(d),
|
|
205
|
+
spaceBefore=18, spaceAfter=5,
|
|
206
|
+
),
|
|
207
|
+
"h3": ParagraphStyle("H3",
|
|
208
|
+
fontName=bfb, fontSize=t["size_h3"],
|
|
209
|
+
leading=t["size_h3"] * 1.5,
|
|
210
|
+
textColor=HexColor(d),
|
|
211
|
+
spaceBefore=12, spaceAfter=3,
|
|
212
|
+
),
|
|
213
|
+
"body": ParagraphStyle("Body",
|
|
214
|
+
fontName=bf, fontSize=t["size_body"],
|
|
215
|
+
leading=t["line_gap"],
|
|
216
|
+
textColor=HexColor(dk),
|
|
217
|
+
spaceAfter=t["para_gap"], alignment=TA_JUSTIFY,
|
|
218
|
+
),
|
|
219
|
+
"bullet": ParagraphStyle("Bullet",
|
|
220
|
+
fontName=bf, fontSize=t["size_body"],
|
|
221
|
+
leading=t["line_gap"] - 1,
|
|
222
|
+
textColor=HexColor(dk),
|
|
223
|
+
spaceAfter=4, leftIndent=14,
|
|
224
|
+
),
|
|
225
|
+
"numbered": ParagraphStyle("Numbered",
|
|
226
|
+
fontName=bf, fontSize=t["size_body"],
|
|
227
|
+
leading=t["line_gap"] - 1,
|
|
228
|
+
textColor=HexColor(dk),
|
|
229
|
+
spaceAfter=4, leftIndent=22, firstLineIndent=-22,
|
|
230
|
+
),
|
|
231
|
+
"callout": ParagraphStyle("Callout",
|
|
232
|
+
fontName=bfb, fontSize=t["size_body"] + 0.5, leading=16,
|
|
233
|
+
textColor=HexColor(d),
|
|
234
|
+
),
|
|
235
|
+
"caption": ParagraphStyle("Caption",
|
|
236
|
+
fontName=bf, fontSize=t["size_caption"], leading=13,
|
|
237
|
+
textColor=HexColor(mu), spaceAfter=6,
|
|
238
|
+
alignment=TA_CENTER,
|
|
239
|
+
),
|
|
240
|
+
"table_header": ParagraphStyle("TblH",
|
|
241
|
+
fontName=bfb, fontSize=9.5, leading=13,
|
|
242
|
+
textColor=HexColor("#FFFFFF"),
|
|
243
|
+
),
|
|
244
|
+
"table_cell": ParagraphStyle("TblC",
|
|
245
|
+
fontName=bf, fontSize=9.5, leading=13,
|
|
246
|
+
textColor=HexColor(dk),
|
|
247
|
+
),
|
|
248
|
+
"code": ParagraphStyle("Code",
|
|
249
|
+
fontName="Courier", fontSize=8.5, leading=12.5,
|
|
250
|
+
textColor=HexColor(dk),
|
|
251
|
+
),
|
|
252
|
+
"code_lang": ParagraphStyle("CodeLang",
|
|
253
|
+
fontName="Courier", fontSize=7, leading=10,
|
|
254
|
+
textColor=HexColor(mu),
|
|
255
|
+
),
|
|
256
|
+
"bib": ParagraphStyle("Bib",
|
|
257
|
+
fontName=bf, fontSize=9, leading=14,
|
|
258
|
+
textColor=HexColor(dk),
|
|
259
|
+
),
|
|
260
|
+
"bib_title": ParagraphStyle("BibTitle",
|
|
261
|
+
fontName=hf, fontSize=t["size_h2"],
|
|
262
|
+
leading=t["size_h2"] * 1.4,
|
|
263
|
+
textColor=HexColor(d),
|
|
264
|
+
spaceBefore=t["section_gap"], spaceAfter=8,
|
|
265
|
+
),
|
|
266
|
+
"math_fallback": ParagraphStyle("MathFb",
|
|
267
|
+
fontName="Courier", fontSize=9, leading=13,
|
|
268
|
+
textColor=HexColor(dk),
|
|
269
|
+
),
|
|
270
|
+
"eq_label": ParagraphStyle("EqLabel",
|
|
271
|
+
fontName="Helvetica", fontSize=9, leading=12,
|
|
272
|
+
textColor=HexColor(mu),
|
|
273
|
+
),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
278
|
+
# Shared helpers
|
|
279
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
280
|
+
|
|
281
|
+
def _divider(accent: str) -> HRFlowable:
|
|
282
|
+
return HRFlowable(
|
|
283
|
+
width="100%", thickness=1.2,
|
|
284
|
+
color=HexColor(accent),
|
|
285
|
+
spaceBefore=14, spaceAfter=14,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _image_from_bytes(png_bytes: bytes, usable_w: float,
|
|
290
|
+
max_frac: float = 0.88) -> RLImage:
|
|
291
|
+
"""Create a scaled RLImage from PNG bytes, bounded to max_frac of usable_w."""
|
|
292
|
+
img = RLImage(io.BytesIO(png_bytes))
|
|
293
|
+
max_w = usable_w * max_frac
|
|
294
|
+
if img.drawWidth > max_w:
|
|
295
|
+
scale = max_w / img.drawWidth
|
|
296
|
+
img.drawWidth = max_w
|
|
297
|
+
img.drawHeight = img.drawHeight * scale
|
|
298
|
+
return img
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
302
|
+
# PNG renderers (matplotlib)
|
|
303
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
304
|
+
|
|
305
|
+
from typing import List, Dict, Optional, Any, Union
|
|
306
|
+
|
|
307
|
+
# ... (finding the specific line)
|
|
308
|
+
def _render_math_png(expr: str, dpi: int = 180) -> Union[bytes, None]:
|
|
309
|
+
"""
|
|
310
|
+
Render a LaTeX math expression via matplotlib mathtext.
|
|
311
|
+
No LaTeX binary required — uses matplotlib's built-in math parser.
|
|
312
|
+
Supports: fractions (\\frac), integrals (\\int), sums (\\sum),
|
|
313
|
+
Greek letters, sub/superscripts, etc.
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
import matplotlib
|
|
317
|
+
matplotlib.use("Agg")
|
|
318
|
+
import matplotlib.pyplot as plt
|
|
319
|
+
|
|
320
|
+
fig = plt.figure(figsize=(8, 1.2))
|
|
321
|
+
fig.patch.set_facecolor("white")
|
|
322
|
+
ax = fig.add_axes([0, 0, 1, 1])
|
|
323
|
+
ax.set_axis_off()
|
|
324
|
+
ax.set_facecolor("white")
|
|
325
|
+
ax.text(0.5, 0.5, f"${expr}$",
|
|
326
|
+
fontsize=16, ha="center", va="center",
|
|
327
|
+
transform=ax.transAxes)
|
|
328
|
+
buf = io.BytesIO()
|
|
329
|
+
fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight",
|
|
330
|
+
facecolor="white", pad_inches=0.1)
|
|
331
|
+
plt.close(fig)
|
|
332
|
+
buf.seek(0)
|
|
333
|
+
return buf.read()
|
|
334
|
+
except Exception:
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _render_chart_png(item: dict, accent: str, dpi: int = 150) -> Optional[bytes]:
|
|
339
|
+
"""
|
|
340
|
+
Render bar / line / pie chart to PNG using matplotlib.
|
|
341
|
+
|
|
342
|
+
Required fields:
|
|
343
|
+
chart_type "bar" | "line" | "pie" (default "bar")
|
|
344
|
+
labels list of category strings
|
|
345
|
+
datasets list of {label?, values: list[number]}
|
|
346
|
+
|
|
347
|
+
Optional fields:
|
|
348
|
+
title chart title
|
|
349
|
+
x_label X-axis label
|
|
350
|
+
y_label Y-axis label
|
|
351
|
+
"""
|
|
352
|
+
try:
|
|
353
|
+
import matplotlib
|
|
354
|
+
matplotlib.use("Agg")
|
|
355
|
+
import matplotlib.pyplot as plt
|
|
356
|
+
import matplotlib.colors as mcolors
|
|
357
|
+
import colorsys
|
|
358
|
+
import numpy as np
|
|
359
|
+
|
|
360
|
+
chart_type = item.get("chart_type", "bar")
|
|
361
|
+
title_text = item.get("title", "")
|
|
362
|
+
labels = item.get("labels", [])
|
|
363
|
+
datasets = item.get("datasets", [])
|
|
364
|
+
|
|
365
|
+
# Derive a consistent palette from the document accent color
|
|
366
|
+
r, g, b = mcolors.to_rgb(accent)
|
|
367
|
+
h, s, v = colorsys.rgb_to_hsv(r, g, b)
|
|
368
|
+
palette = [
|
|
369
|
+
colorsys.hsv_to_rgb(
|
|
370
|
+
(h + i * 0.13) % 1.0,
|
|
371
|
+
max(0.35, s - i * 0.08),
|
|
372
|
+
min(0.92, v + i * 0.04),
|
|
373
|
+
)
|
|
374
|
+
for i in range(max(len(datasets), 1))
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
fig, ax = plt.subplots(figsize=(7, 3.6), dpi=dpi)
|
|
378
|
+
fig.patch.set_facecolor("white")
|
|
379
|
+
ax.set_facecolor("white")
|
|
380
|
+
|
|
381
|
+
if chart_type == "bar":
|
|
382
|
+
x = np.arange(len(labels))
|
|
383
|
+
n = max(len(datasets), 1)
|
|
384
|
+
width = 0.68 / n
|
|
385
|
+
for i, ds in enumerate(datasets):
|
|
386
|
+
offset = (i - (n - 1) / 2) * width
|
|
387
|
+
ax.bar(x + offset, ds.get("values", []), width * 0.88,
|
|
388
|
+
label=ds.get("label", f"Series {i+1}"),
|
|
389
|
+
color=palette[i % len(palette)], edgecolor="none")
|
|
390
|
+
ax.set_xticks(x)
|
|
391
|
+
ax.set_xticklabels(labels, fontsize=8.5)
|
|
392
|
+
ax.yaxis.grid(True, alpha=0.25, color="#CCCCCC", linewidth=0.7)
|
|
393
|
+
ax.set_axisbelow(True)
|
|
394
|
+
if item.get("x_label"):
|
|
395
|
+
ax.set_xlabel(item["x_label"], fontsize=8.5)
|
|
396
|
+
if item.get("y_label"):
|
|
397
|
+
ax.set_ylabel(item["y_label"], fontsize=8.5)
|
|
398
|
+
|
|
399
|
+
elif chart_type == "line":
|
|
400
|
+
x = np.arange(len(labels))
|
|
401
|
+
for i, ds in enumerate(datasets):
|
|
402
|
+
ax.plot(x, ds.get("values", []), marker="o", markersize=3.5,
|
|
403
|
+
label=ds.get("label", f"Series {i+1}"),
|
|
404
|
+
color=palette[i % len(palette)], linewidth=1.8)
|
|
405
|
+
ax.set_xticks(x)
|
|
406
|
+
ax.set_xticklabels(labels, fontsize=8.5)
|
|
407
|
+
ax.yaxis.grid(True, alpha=0.25, color="#CCCCCC", linewidth=0.7)
|
|
408
|
+
ax.set_axisbelow(True)
|
|
409
|
+
if item.get("x_label"):
|
|
410
|
+
ax.set_xlabel(item["x_label"], fontsize=8.5)
|
|
411
|
+
if item.get("y_label"):
|
|
412
|
+
ax.set_ylabel(item["y_label"], fontsize=8.5)
|
|
413
|
+
|
|
414
|
+
elif chart_type == "pie":
|
|
415
|
+
vals = datasets[0].get("values", []) if datasets else []
|
|
416
|
+
colors = [
|
|
417
|
+
colorsys.hsv_to_rgb(
|
|
418
|
+
(h + i * 0.11) % 1.0,
|
|
419
|
+
max(0.30, s - i * 0.06),
|
|
420
|
+
min(0.92, v + i * 0.03),
|
|
421
|
+
)
|
|
422
|
+
for i in range(len(vals))
|
|
423
|
+
]
|
|
424
|
+
ax.pie(vals, labels=labels, colors=colors,
|
|
425
|
+
autopct="%1.1f%%", pctdistance=0.82,
|
|
426
|
+
wedgeprops=dict(edgecolor="white", linewidth=1.4),
|
|
427
|
+
textprops=dict(fontsize=8.5))
|
|
428
|
+
|
|
429
|
+
# Shared styling
|
|
430
|
+
for spine in ax.spines.values():
|
|
431
|
+
spine.set_linewidth(0.5)
|
|
432
|
+
spine.set_color("#CCCCCC")
|
|
433
|
+
ax.tick_params(axis="both", length=0, labelsize=8.5)
|
|
434
|
+
if title_text:
|
|
435
|
+
ax.set_title(title_text, fontsize=10, pad=8,
|
|
436
|
+
color="#333333", fontweight="bold")
|
|
437
|
+
if len(datasets) > 1 and chart_type != "pie":
|
|
438
|
+
ax.legend(frameon=False, fontsize=8, loc="upper right")
|
|
439
|
+
|
|
440
|
+
plt.tight_layout(pad=0.4)
|
|
441
|
+
buf = io.BytesIO()
|
|
442
|
+
fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight",
|
|
443
|
+
facecolor="white", pad_inches=0.06)
|
|
444
|
+
plt.close(fig)
|
|
445
|
+
buf.seek(0)
|
|
446
|
+
return buf.read()
|
|
447
|
+
except Exception:
|
|
448
|
+
return None
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _render_flowchart_png(item: dict, accent: str, dark: str,
|
|
452
|
+
muted: str, dpi: int = 130) -> Union[bytes, None]:
|
|
453
|
+
"""
|
|
454
|
+
Render a top-to-bottom flowchart using matplotlib patches and arrows.
|
|
455
|
+
|
|
456
|
+
Node schema: {id, label, shape?}
|
|
457
|
+
shape: "rect" (default) | "diamond" | "oval" | "parallelogram"
|
|
458
|
+
|
|
459
|
+
Edge schema: {from, to, label?}
|
|
460
|
+
Forward edges (to a later node) draw straight arrows.
|
|
461
|
+
Back edges (to an earlier node) draw a curved arc to the right.
|
|
462
|
+
"""
|
|
463
|
+
try:
|
|
464
|
+
import matplotlib
|
|
465
|
+
matplotlib.use("Agg")
|
|
466
|
+
import matplotlib.pyplot as plt
|
|
467
|
+
import matplotlib.patches as mpatch
|
|
468
|
+
from matplotlib.patches import FancyBboxPatch
|
|
469
|
+
import matplotlib.colors as mcolors
|
|
470
|
+
|
|
471
|
+
nodes_list = item.get("nodes", [])
|
|
472
|
+
edges = item.get("edges", [])
|
|
473
|
+
if not nodes_list:
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
nodes = {n["id"]: n for n in nodes_list}
|
|
477
|
+
order = {n["id"]: i for i, n in enumerate(nodes_list)}
|
|
478
|
+
|
|
479
|
+
n_nodes = len(nodes_list)
|
|
480
|
+
BOX_W = 4.2
|
|
481
|
+
BOX_H = 0.58
|
|
482
|
+
STEP_Y = 1.25
|
|
483
|
+
CX = 5.0
|
|
484
|
+
|
|
485
|
+
fig_h = max(3.5, n_nodes * STEP_Y + 0.8)
|
|
486
|
+
fig, ax = plt.subplots(figsize=(6, fig_h), dpi=dpi)
|
|
487
|
+
fig.patch.set_facecolor("white")
|
|
488
|
+
ax.set_facecolor("white")
|
|
489
|
+
ax.set_xlim(0, 10)
|
|
490
|
+
ax.set_ylim(-0.6, n_nodes * STEP_Y + 0.2)
|
|
491
|
+
ax.invert_yaxis()
|
|
492
|
+
ax.axis("off")
|
|
493
|
+
|
|
494
|
+
acc_rgb = mcolors.to_rgb(accent)
|
|
495
|
+
dark_rgb = mcolors.to_rgb(dark)
|
|
496
|
+
muted_rgb = mcolors.to_rgb(muted)
|
|
497
|
+
|
|
498
|
+
# Node positions (cx, cy) — preserves input order
|
|
499
|
+
pos = {nid: (CX, i * STEP_Y) for nid, i in order.items()}
|
|
500
|
+
|
|
501
|
+
# ── Draw edges (behind nodes) ──────────────────────────────────────────
|
|
502
|
+
for edge in edges:
|
|
503
|
+
src, dst = edge.get("from"), edge.get("to")
|
|
504
|
+
if src not in pos or dst not in pos:
|
|
505
|
+
continue
|
|
506
|
+
x1, y1 = pos[src]
|
|
507
|
+
x2, y2 = pos[dst]
|
|
508
|
+
lbl = edge.get("label", "")
|
|
509
|
+
|
|
510
|
+
src_shape = nodes.get(src, {}).get("shape", "rect")
|
|
511
|
+
dst_shape = nodes.get(dst, {}).get("shape", "rect")
|
|
512
|
+
dy_src = BOX_H * (0.80 if src_shape == "diamond" else 0.50)
|
|
513
|
+
dy_dst = BOX_H * (0.80 if dst_shape == "diamond" else 0.50)
|
|
514
|
+
|
|
515
|
+
y_start = y1 + dy_src
|
|
516
|
+
y_end = y2 - dy_dst
|
|
517
|
+
|
|
518
|
+
# Forward edge: straight; back-edge: curved arc
|
|
519
|
+
conn = "arc3,rad=0.0" if y_end > y_start + 0.01 else "arc3,rad=0.42"
|
|
520
|
+
|
|
521
|
+
ax.annotate("",
|
|
522
|
+
xy=(x2, y_end), xytext=(x1, y_start),
|
|
523
|
+
arrowprops=dict(
|
|
524
|
+
arrowstyle="-|>", color=muted_rgb,
|
|
525
|
+
lw=1.0, mutation_scale=10,
|
|
526
|
+
connectionstyle=conn,
|
|
527
|
+
),
|
|
528
|
+
)
|
|
529
|
+
if lbl:
|
|
530
|
+
mid_x = (x1 + x2) / 2 + 0.28
|
|
531
|
+
mid_y = (y_start + y_end) / 2
|
|
532
|
+
ax.text(mid_x, mid_y, lbl, fontsize=7.5,
|
|
533
|
+
color=muted_rgb, ha="left", va="center")
|
|
534
|
+
|
|
535
|
+
# ── Draw nodes (in front of edges) ────────────────────────────────────
|
|
536
|
+
for nid, (cx, cy) in pos.items():
|
|
537
|
+
node = nodes[nid]
|
|
538
|
+
shape = node.get("shape", "rect")
|
|
539
|
+
label = node.get("label", nid)
|
|
540
|
+
left = cx - BOX_W / 2
|
|
541
|
+
bot = cy - BOX_H / 2
|
|
542
|
+
|
|
543
|
+
if shape in ("oval", "terminal"):
|
|
544
|
+
el = mpatch.Ellipse(
|
|
545
|
+
(cx, cy), BOX_W * 0.78, BOX_H * 1.15,
|
|
546
|
+
facecolor=acc_rgb, edgecolor=acc_rgb, linewidth=0,
|
|
547
|
+
)
|
|
548
|
+
ax.add_patch(el)
|
|
549
|
+
ax.text(cx, cy, label, ha="center", va="center",
|
|
550
|
+
fontsize=8.5, fontweight="bold", color="white")
|
|
551
|
+
|
|
552
|
+
elif shape == "diamond":
|
|
553
|
+
d = BOX_W * 0.44
|
|
554
|
+
diamond = plt.Polygon(
|
|
555
|
+
[(cx, cy - d * 0.72), (cx + d, cy),
|
|
556
|
+
(cx, cy + d * 0.72), (cx - d, cy)],
|
|
557
|
+
facecolor="#FFFCF0",
|
|
558
|
+
edgecolor=accent, linewidth=1.2,
|
|
559
|
+
)
|
|
560
|
+
ax.add_patch(diamond)
|
|
561
|
+
ax.text(cx, cy, label, ha="center", va="center",
|
|
562
|
+
fontsize=8, color=dark_rgb)
|
|
563
|
+
|
|
564
|
+
elif shape == "parallelogram":
|
|
565
|
+
skew = 0.30
|
|
566
|
+
para = plt.Polygon(
|
|
567
|
+
[(left + skew, bot), (left + BOX_W + skew, bot),
|
|
568
|
+
(left + BOX_W, bot + BOX_H), (left, bot + BOX_H)],
|
|
569
|
+
facecolor="white",
|
|
570
|
+
edgecolor=accent, linewidth=1.2,
|
|
571
|
+
)
|
|
572
|
+
ax.add_patch(para)
|
|
573
|
+
ax.text(cx, cy, label, ha="center", va="center",
|
|
574
|
+
fontsize=8.5, color=dark_rgb)
|
|
575
|
+
|
|
576
|
+
else: # rect (default)
|
|
577
|
+
rect = FancyBboxPatch(
|
|
578
|
+
(left, bot), BOX_W, BOX_H,
|
|
579
|
+
boxstyle="round,pad=0.04",
|
|
580
|
+
facecolor="white",
|
|
581
|
+
edgecolor=accent, linewidth=1.2,
|
|
582
|
+
)
|
|
583
|
+
ax.add_patch(rect)
|
|
584
|
+
ax.text(cx, cy, label, ha="center", va="center",
|
|
585
|
+
fontsize=8.5, color=dark_rgb)
|
|
586
|
+
|
|
587
|
+
plt.tight_layout(pad=0.2)
|
|
588
|
+
buf = io.BytesIO()
|
|
589
|
+
fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight",
|
|
590
|
+
facecolor="white", pad_inches=0.08)
|
|
591
|
+
plt.close(fig)
|
|
592
|
+
buf.seek(0)
|
|
593
|
+
return buf.read()
|
|
594
|
+
except Exception:
|
|
595
|
+
return None
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
599
|
+
# Block renderers
|
|
600
|
+
#
|
|
601
|
+
# All functions share the same signature:
|
|
602
|
+
# _add_XXX(story: list, item: dict, ctx: dict)
|
|
603
|
+
#
|
|
604
|
+
# ctx keys:
|
|
605
|
+
# tokens dict design tokens from palette.py
|
|
606
|
+
# styles dict ParagraphStyle objects from make_styles()
|
|
607
|
+
# usable_w float usable page width in points
|
|
608
|
+
# acc str accent hex color
|
|
609
|
+
# acc_lt str light accent hex color
|
|
610
|
+
# mu str muted hex color
|
|
611
|
+
# dark str dark hex color
|
|
612
|
+
# figure_n int auto-incrementing figure counter (mutable)
|
|
613
|
+
# numbered_n int auto-incrementing list counter (mutable)
|
|
614
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
615
|
+
|
|
616
|
+
def _add_heading(story: list, item: dict, ctx: dict, level: int):
|
|
617
|
+
key = f"h{level}"
|
|
618
|
+
para = Paragraph(item["text"], ctx["styles"][key])
|
|
619
|
+
if level == 1:
|
|
620
|
+
story.append(KeepTogether([para, _divider(ctx["acc"])]))
|
|
621
|
+
else:
|
|
622
|
+
story.append(para)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _add_body(story: list, item: dict, ctx: dict):
|
|
626
|
+
story.append(Paragraph(item["text"], ctx["styles"]["body"]))
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _add_bullet(story: list, item: dict, ctx: dict):
|
|
630
|
+
story.append(Paragraph(
|
|
631
|
+
f"\u2022\u2002{item['text']}", ctx["styles"]["bullet"]
|
|
632
|
+
))
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _add_numbered(story: list, item: dict, ctx: dict):
|
|
636
|
+
ctx["numbered_n"] += 1
|
|
637
|
+
story.append(Paragraph(
|
|
638
|
+
f"{ctx['numbered_n']}.\u2002{item['text']}",
|
|
639
|
+
ctx["styles"]["numbered"],
|
|
640
|
+
))
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _add_callout(story: list, item: dict, ctx: dict):
|
|
644
|
+
story.append(Spacer(1, 8))
|
|
645
|
+
story.append(CalloutBox(
|
|
646
|
+
item["text"], ctx["styles"]["callout"], ctx["acc"], ctx["acc_lt"]
|
|
647
|
+
))
|
|
648
|
+
story.append(Spacer(1, 8))
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _add_table(story: list, item: dict, ctx: dict):
|
|
652
|
+
t = ctx["tokens"]
|
|
653
|
+
styles = ctx["styles"]
|
|
654
|
+
usable_w = ctx["usable_w"]
|
|
655
|
+
acc = ctx["acc"]
|
|
656
|
+
acc_lt = ctx["acc_lt"]
|
|
657
|
+
|
|
658
|
+
headers = [Paragraph(h, styles["table_header"]) for h in item["headers"]]
|
|
659
|
+
rows = [
|
|
660
|
+
[Paragraph(str(c), styles["table_cell"]) for c in row]
|
|
661
|
+
for row in item.get("rows", [])
|
|
662
|
+
]
|
|
663
|
+
n_cols = len(item["headers"])
|
|
664
|
+
|
|
665
|
+
# Optional col_widths as fractions summing to 1.0
|
|
666
|
+
if "col_widths" in item and len(item["col_widths"]) == n_cols:
|
|
667
|
+
col_w = [usable_w * f for f in item["col_widths"]]
|
|
668
|
+
else:
|
|
669
|
+
col_w = [usable_w / n_cols] * n_cols
|
|
670
|
+
|
|
671
|
+
tbl = Table([headers] + rows, colWidths=col_w)
|
|
672
|
+
tbl.setStyle(TableStyle([
|
|
673
|
+
("BACKGROUND", (0, 0), (-1, 0), HexColor(acc)),
|
|
674
|
+
("TEXTCOLOR", (0, 0), (-1, 0), HexColor("#FFFFFF")),
|
|
675
|
+
("FONTNAME", (0, 0), (-1, 0), t["font_body_b_rl"]),
|
|
676
|
+
("FONTSIZE", (0, 0), (-1, 0), 9.5),
|
|
677
|
+
("TOPPADDING", (0, 0), (-1, 0), 7),
|
|
678
|
+
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
|
|
679
|
+
("ROWBACKGROUNDS", (0, 1), (-1, -1),
|
|
680
|
+
[HexColor("#FFFFFF"), HexColor(acc_lt)]),
|
|
681
|
+
("FONTNAME", (0, 1), (-1, -1), t["font_body_rl"]),
|
|
682
|
+
("FONTSIZE", (0, 1), (-1, -1), 9.5),
|
|
683
|
+
("TOPPADDING", (0, 1), (-1, -1), 6),
|
|
684
|
+
("BOTTOMPADDING", (0, 1), (-1, -1), 6),
|
|
685
|
+
("LEFTPADDING", (0, 0), (-1, -1), 10),
|
|
686
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 10),
|
|
687
|
+
("BOX", (0, 0), (-1, -1), 0.5, HexColor("#CCCCCC")),
|
|
688
|
+
("LINEBELOW", (0, 0), (-1, 0), 1.2, HexColor(acc)),
|
|
689
|
+
("TEXTCOLOR", (0, 1), (-1, -1), HexColor(t["body_text"])),
|
|
690
|
+
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
|
691
|
+
]))
|
|
692
|
+
story.append(tbl)
|
|
693
|
+
if item.get("caption"):
|
|
694
|
+
story.append(Spacer(1, 4))
|
|
695
|
+
story.append(Paragraph(item["caption"], styles["caption"]))
|
|
696
|
+
story.append(Spacer(1, 12))
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _add_image(story: list, item: dict, ctx: dict):
|
|
700
|
+
path = str(item.get("path", item.get("src", "")))
|
|
701
|
+
if not os.path.exists(path):
|
|
702
|
+
story.append(Paragraph(
|
|
703
|
+
f"[Image not found: {path}]", ctx["styles"]["caption"]
|
|
704
|
+
))
|
|
705
|
+
return
|
|
706
|
+
try:
|
|
707
|
+
img = RLImage(path)
|
|
708
|
+
uw = ctx["usable_w"]
|
|
709
|
+
if img.drawWidth > uw:
|
|
710
|
+
scale = uw / img.drawWidth
|
|
711
|
+
img.drawWidth = uw
|
|
712
|
+
img.drawHeight = img.drawHeight * scale
|
|
713
|
+
story.append(img)
|
|
714
|
+
except Exception as e:
|
|
715
|
+
story.append(Paragraph(f"[Image error: {e}]", ctx["styles"]["caption"]))
|
|
716
|
+
return
|
|
717
|
+
if item.get("caption"):
|
|
718
|
+
story.append(Spacer(1, 4))
|
|
719
|
+
story.append(Paragraph(item["caption"], ctx["styles"]["caption"]))
|
|
720
|
+
story.append(Spacer(1, 8))
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _add_figure(story: list, item: dict, ctx: dict):
|
|
724
|
+
"""Like image but auto-numbers the caption as 'Figure N: ...'."""
|
|
725
|
+
ctx["figure_n"] += 1
|
|
726
|
+
raw_cap = item.get("caption", "")
|
|
727
|
+
caption = f"Figure {ctx['figure_n']}: {raw_cap}" if raw_cap \
|
|
728
|
+
else f"Figure {ctx['figure_n']}"
|
|
729
|
+
_add_image(story, {**item, "caption": caption}, ctx)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _add_code(story: list, item: dict, ctx: dict):
|
|
733
|
+
acc = ctx["acc"]
|
|
734
|
+
acc_lt = ctx["acc_lt"]
|
|
735
|
+
mu = ctx["mu"]
|
|
736
|
+
uw = ctx["usable_w"]
|
|
737
|
+
lang = item.get("language", "")
|
|
738
|
+
|
|
739
|
+
pre = Preformatted(item.get("text", ""), ctx["styles"]["code"])
|
|
740
|
+
tbl = Table([[pre]], colWidths=[uw])
|
|
741
|
+
tbl.setStyle(TableStyle([
|
|
742
|
+
("BACKGROUND", (0, 0), (-1, -1), HexColor(acc_lt)),
|
|
743
|
+
("LINEBEFORE", (0, 0), ( 0, -1), 3, HexColor(acc)),
|
|
744
|
+
("BOX", (0, 0), (-1, -1), 0.5, HexColor(mu)),
|
|
745
|
+
("LEFTPADDING", (0, 0), (-1, -1), 14),
|
|
746
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 10),
|
|
747
|
+
("TOPPADDING", (0, 0), (-1, -1), 8),
|
|
748
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
|
749
|
+
]))
|
|
750
|
+
story.append(Spacer(1, 6))
|
|
751
|
+
if lang:
|
|
752
|
+
story.append(Paragraph(lang.upper(), ctx["styles"]["code_lang"]))
|
|
753
|
+
story.append(tbl)
|
|
754
|
+
story.append(Spacer(1, 6))
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _add_math(story: list, item: dict, ctx: dict):
|
|
758
|
+
"""
|
|
759
|
+
Display math block.
|
|
760
|
+
|
|
761
|
+
Fields:
|
|
762
|
+
text LaTeX math expression (without enclosing $)
|
|
763
|
+
label optional equation label, e.g. "(1)" — displayed right-aligned
|
|
764
|
+
caption optional caption below the formula
|
|
765
|
+
|
|
766
|
+
Example:
|
|
767
|
+
{"type": "math", "text": "E = mc^2", "label": "(1)"}
|
|
768
|
+
{"type": "math", "text": "\\\\int_0^\\\\infty e^{-x^2}\\\\,dx = \\\\frac{\\\\sqrt{\\\\pi}}{2}"}
|
|
769
|
+
"""
|
|
770
|
+
acc = ctx["acc"]
|
|
771
|
+
acc_lt = ctx["acc_lt"]
|
|
772
|
+
uw = ctx["usable_w"]
|
|
773
|
+
expr = item.get("text", "").strip()
|
|
774
|
+
label = item.get("label", "").strip()
|
|
775
|
+
|
|
776
|
+
png = _render_math_png(expr)
|
|
777
|
+
|
|
778
|
+
if png is None:
|
|
779
|
+
# Graceful text fallback if matplotlib unavailable
|
|
780
|
+
story.append(Spacer(1, 6))
|
|
781
|
+
pre = Preformatted(f" {expr}", ctx["styles"]["math_fallback"])
|
|
782
|
+
tbl = Table([[pre]], colWidths=[uw])
|
|
783
|
+
tbl.setStyle(TableStyle([
|
|
784
|
+
("BACKGROUND", (0, 0), (-1, -1), HexColor(acc_lt)),
|
|
785
|
+
("LEFTPADDING", (0, 0), (-1, -1), 14),
|
|
786
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 14),
|
|
787
|
+
("TOPPADDING", (0, 0), (-1, -1), 8),
|
|
788
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
|
789
|
+
]))
|
|
790
|
+
story.append(tbl)
|
|
791
|
+
story.append(Spacer(1, 6))
|
|
792
|
+
return
|
|
793
|
+
|
|
794
|
+
img = _image_from_bytes(png, uw, max_frac=0.72)
|
|
795
|
+
story.append(Spacer(1, 10))
|
|
796
|
+
|
|
797
|
+
if label:
|
|
798
|
+
label_w = 44
|
|
799
|
+
formula_w = uw - label_w
|
|
800
|
+
lbl_para = Paragraph(label, ctx["styles"]["eq_label"])
|
|
801
|
+
row_tbl = Table([[img, lbl_para]], colWidths=[formula_w, label_w])
|
|
802
|
+
row_tbl.setStyle(TableStyle([
|
|
803
|
+
("ALIGN", (0, 0), (0, 0), "CENTER"),
|
|
804
|
+
("ALIGN", (1, 0), (1, 0), "RIGHT"),
|
|
805
|
+
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
|
806
|
+
]))
|
|
807
|
+
story.append(row_tbl)
|
|
808
|
+
else:
|
|
809
|
+
row_tbl = Table([[img]], colWidths=[uw])
|
|
810
|
+
row_tbl.setStyle(TableStyle([
|
|
811
|
+
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
|
812
|
+
]))
|
|
813
|
+
story.append(row_tbl)
|
|
814
|
+
|
|
815
|
+
if item.get("caption"):
|
|
816
|
+
story.append(Spacer(1, 4))
|
|
817
|
+
story.append(Paragraph(item["caption"], ctx["styles"]["caption"]))
|
|
818
|
+
story.append(Spacer(1, 10))
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _add_chart(story: list, item: dict, ctx: dict):
|
|
822
|
+
"""
|
|
823
|
+
Render a chart (bar / line / pie) via matplotlib.
|
|
824
|
+
|
|
825
|
+
Fields:
|
|
826
|
+
chart_type "bar" | "line" | "pie" (default "bar")
|
|
827
|
+
title chart title
|
|
828
|
+
labels list of category strings
|
|
829
|
+
datasets list of {label?, values: list[number]}
|
|
830
|
+
x_label X-axis label (bar/line)
|
|
831
|
+
y_label Y-axis label (bar/line)
|
|
832
|
+
caption caption text below chart
|
|
833
|
+
figure bool (default true) — prefix caption with "Figure N:"
|
|
834
|
+
"""
|
|
835
|
+
uw = ctx["usable_w"]
|
|
836
|
+
png = _render_chart_png(item, ctx["acc"])
|
|
837
|
+
|
|
838
|
+
if png is None:
|
|
839
|
+
story.append(Paragraph(
|
|
840
|
+
"[Chart: install matplotlib to render — pip install matplotlib]",
|
|
841
|
+
ctx["styles"]["caption"],
|
|
842
|
+
))
|
|
843
|
+
return
|
|
844
|
+
|
|
845
|
+
img = _image_from_bytes(png, uw, max_frac=0.95)
|
|
846
|
+
story.append(Spacer(1, 8))
|
|
847
|
+
row_tbl = Table([[img]], colWidths=[uw])
|
|
848
|
+
row_tbl.setStyle(TableStyle([("ALIGN", (0, 0), (-1, -1), "CENTER")]))
|
|
849
|
+
story.append(row_tbl)
|
|
850
|
+
|
|
851
|
+
raw_cap = item.get("caption", "")
|
|
852
|
+
use_fig = item.get("figure", True)
|
|
853
|
+
if raw_cap or use_fig:
|
|
854
|
+
ctx["figure_n"] += 1
|
|
855
|
+
prefix = f"Figure {ctx['figure_n']}: " if use_fig else ""
|
|
856
|
+
story.append(Spacer(1, 4))
|
|
857
|
+
story.append(Paragraph(prefix + raw_cap, ctx["styles"]["caption"]))
|
|
858
|
+
story.append(Spacer(1, 10))
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _add_flowchart(story: list, item: dict, ctx: dict):
|
|
862
|
+
"""
|
|
863
|
+
Render a flowchart via matplotlib.
|
|
864
|
+
|
|
865
|
+
Fields:
|
|
866
|
+
nodes list of {id, label, shape?}
|
|
867
|
+
shape: "rect" (default) | "diamond" | "oval" | "parallelogram"
|
|
868
|
+
edges list of {from, to, label?}
|
|
869
|
+
caption caption below the diagram
|
|
870
|
+
figure bool (default true) — prefix caption with "Figure N:"
|
|
871
|
+
"""
|
|
872
|
+
uw = ctx["usable_w"]
|
|
873
|
+
png = _render_flowchart_png(item, ctx["acc"], ctx["dark"], ctx["mu"])
|
|
874
|
+
|
|
875
|
+
if png is None:
|
|
876
|
+
story.append(Paragraph(
|
|
877
|
+
"[Flowchart: install matplotlib to render — pip install matplotlib]",
|
|
878
|
+
ctx["styles"]["caption"],
|
|
879
|
+
))
|
|
880
|
+
return
|
|
881
|
+
|
|
882
|
+
img = _image_from_bytes(png, uw, max_frac=0.78)
|
|
883
|
+
story.append(Spacer(1, 8))
|
|
884
|
+
row_tbl = Table([[img]], colWidths=[uw])
|
|
885
|
+
row_tbl.setStyle(TableStyle([("ALIGN", (0, 0), (-1, -1), "CENTER")]))
|
|
886
|
+
story.append(row_tbl)
|
|
887
|
+
|
|
888
|
+
raw_cap = item.get("caption", "")
|
|
889
|
+
use_fig = item.get("figure", True)
|
|
890
|
+
if raw_cap or use_fig:
|
|
891
|
+
ctx["figure_n"] += 1
|
|
892
|
+
prefix = f"Figure {ctx['figure_n']}: " if use_fig else ""
|
|
893
|
+
story.append(Spacer(1, 4))
|
|
894
|
+
story.append(Paragraph(prefix + raw_cap, ctx["styles"]["caption"]))
|
|
895
|
+
story.append(Spacer(1, 10))
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _add_bibliography(story: list, item: dict, ctx: dict):
|
|
899
|
+
"""
|
|
900
|
+
Numbered reference list with hanging indent.
|
|
901
|
+
|
|
902
|
+
Fields:
|
|
903
|
+
title section heading (default "References"); set "" to suppress
|
|
904
|
+
items list of {id, text}
|
|
905
|
+
|
|
906
|
+
Example:
|
|
907
|
+
{"type": "bibliography",
|
|
908
|
+
"items": [
|
|
909
|
+
{"id": "1", "text": "Smith, J. (2023). Title. Journal, 10(2), 1–15."},
|
|
910
|
+
{"id": "2", "text": "Doe, A. (2022). Another title. Publisher."}
|
|
911
|
+
]}
|
|
912
|
+
"""
|
|
913
|
+
heading = item.get("title", "References")
|
|
914
|
+
if heading:
|
|
915
|
+
story.append(KeepTogether([
|
|
916
|
+
Paragraph(heading, ctx["styles"]["bib_title"]),
|
|
917
|
+
_divider(ctx["acc"]),
|
|
918
|
+
]))
|
|
919
|
+
|
|
920
|
+
for ref in item.get("items", []):
|
|
921
|
+
story.append(Spacer(1, 4))
|
|
922
|
+
story.append(BibliographyItem(
|
|
923
|
+
str(ref.get("id", "")),
|
|
924
|
+
ref.get("text", ""),
|
|
925
|
+
ctx["styles"]["bib"],
|
|
926
|
+
ctx["dark"],
|
|
927
|
+
))
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
931
|
+
# Story builder
|
|
932
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
933
|
+
|
|
934
|
+
# Block types that break a numbered list sequence
|
|
935
|
+
_RESETS_NUMBERED = frozenset({
|
|
936
|
+
"h1", "h2", "h3", "body", "bullet", "callout", "table",
|
|
937
|
+
"image", "figure", "code", "math", "chart", "flowchart",
|
|
938
|
+
"bibliography", "divider", "caption", "pagebreak", "spacer",
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def build_story(content: list, tokens: dict, styles: dict) -> list:
|
|
943
|
+
usable_w = A4[0] - tokens["margin_left"] - tokens["margin_right"]
|
|
944
|
+
|
|
945
|
+
ctx: dict = {
|
|
946
|
+
"tokens": tokens,
|
|
947
|
+
"styles": styles,
|
|
948
|
+
"usable_w": usable_w,
|
|
949
|
+
"acc": tokens["accent"],
|
|
950
|
+
"acc_lt": tokens["accent_lt"],
|
|
951
|
+
"mu": tokens["muted"],
|
|
952
|
+
"dark": tokens["dark"],
|
|
953
|
+
"figure_n": 0,
|
|
954
|
+
"numbered_n": 0,
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
story: list = []
|
|
958
|
+
|
|
959
|
+
for item in content:
|
|
960
|
+
kind = item.get("type", "body")
|
|
961
|
+
|
|
962
|
+
if kind in _RESETS_NUMBERED:
|
|
963
|
+
ctx["numbered_n"] = 0
|
|
964
|
+
|
|
965
|
+
if kind == "h1": _add_heading(story, item, ctx, 1)
|
|
966
|
+
elif kind == "h2": _add_heading(story, item, ctx, 2)
|
|
967
|
+
elif kind == "h3": _add_heading(story, item, ctx, 3)
|
|
968
|
+
elif kind == "body": _add_body(story, item, ctx)
|
|
969
|
+
elif kind == "bullet": _add_bullet(story, item, ctx)
|
|
970
|
+
elif kind == "numbered": _add_numbered(story, item, ctx)
|
|
971
|
+
elif kind == "callout": _add_callout(story, item, ctx)
|
|
972
|
+
elif kind == "table": _add_table(story, item, ctx)
|
|
973
|
+
elif kind == "image": _add_image(story, item, ctx)
|
|
974
|
+
elif kind == "figure": _add_figure(story, item, ctx)
|
|
975
|
+
elif kind == "code": _add_code(story, item, ctx)
|
|
976
|
+
elif kind == "math": _add_math(story, item, ctx)
|
|
977
|
+
elif kind == "chart": _add_chart(story, item, ctx)
|
|
978
|
+
elif kind == "flowchart": _add_flowchart(story, item, ctx)
|
|
979
|
+
elif kind == "bibliography": _add_bibliography(story, item, ctx)
|
|
980
|
+
elif kind == "divider": story.append(_divider(ctx["acc"]))
|
|
981
|
+
elif kind == "caption":
|
|
982
|
+
story.append(Paragraph(item["text"], styles["caption"]))
|
|
983
|
+
elif kind == "pagebreak": story.append(PageBreak())
|
|
984
|
+
elif kind == "spacer": story.append(Spacer(1, item.get("pt", 12)))
|
|
985
|
+
|
|
986
|
+
return story
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
990
|
+
# Main build
|
|
991
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
992
|
+
|
|
993
|
+
def build(tokens: dict, content: list, out_path: str) -> dict:
|
|
994
|
+
register_fonts(tokens)
|
|
995
|
+
styles = make_styles(tokens)
|
|
996
|
+
|
|
997
|
+
doc = BeautifulDoc(
|
|
998
|
+
out_path, tokens,
|
|
999
|
+
pagesize=A4,
|
|
1000
|
+
leftMargin=tokens["margin_left"],
|
|
1001
|
+
rightMargin=tokens["margin_right"],
|
|
1002
|
+
topMargin=tokens["margin_top"],
|
|
1003
|
+
bottomMargin=tokens["margin_bottom"],
|
|
1004
|
+
)
|
|
1005
|
+
doc.build(build_story(content, tokens, styles))
|
|
1006
|
+
|
|
1007
|
+
size = os.path.getsize(out_path)
|
|
1008
|
+
return {"status": "ok", "out": out_path, "size_kb": size // 1024}
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
1012
|
+
# CLI
|
|
1013
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
1014
|
+
|
|
1015
|
+
def main():
|
|
1016
|
+
parser = argparse.ArgumentParser(
|
|
1017
|
+
description="Render body PDF from tokens.json + content.json"
|
|
1018
|
+
)
|
|
1019
|
+
parser.add_argument("--tokens", default="tokens.json")
|
|
1020
|
+
parser.add_argument("--content", default="content.json")
|
|
1021
|
+
parser.add_argument("--out", default="body.pdf")
|
|
1022
|
+
args = parser.parse_args()
|
|
1023
|
+
|
|
1024
|
+
for fpath in (args.tokens, args.content):
|
|
1025
|
+
if not os.path.exists(fpath):
|
|
1026
|
+
print(
|
|
1027
|
+
json.dumps({"status": "error",
|
|
1028
|
+
"error": f"File not found: {fpath}"}),
|
|
1029
|
+
file=sys.stderr,
|
|
1030
|
+
)
|
|
1031
|
+
sys.exit(1)
|
|
1032
|
+
|
|
1033
|
+
with open(args.tokens, encoding="utf-8") as f:
|
|
1034
|
+
tokens = json.load(f)
|
|
1035
|
+
with open(args.content, encoding="utf-8") as f:
|
|
1036
|
+
content = json.load(f)
|
|
1037
|
+
|
|
1038
|
+
try:
|
|
1039
|
+
result = build(tokens, content, args.out)
|
|
1040
|
+
print(json.dumps(result))
|
|
1041
|
+
except Exception as e:
|
|
1042
|
+
import traceback
|
|
1043
|
+
print(
|
|
1044
|
+
json.dumps({
|
|
1045
|
+
"status": "error",
|
|
1046
|
+
"error": str(e),
|
|
1047
|
+
"trace": traceback.format_exc(),
|
|
1048
|
+
}),
|
|
1049
|
+
file=sys.stderr,
|
|
1050
|
+
)
|
|
1051
|
+
sys.exit(3)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
if __name__ == "__main__":
|
|
1055
|
+
main()
|