cryptnox-sdk-arduino 1.0.0
Arduino library for Cryptnox Hardware Wallet
Loading...
Searching...
No Matches
patch_latex.py
Go to the documentation of this file.
1"""Post-process Doxygen's refman.tex before pdflatex, to match the Cryptnox PDF style.
2
3Usage: python3 docs/patch_latex.py docs/latex/refman.tex ["Friendly Title"]
4"""
5import os
6import re
7import sys
8
9path = sys.argv[1]
10title = sys.argv[2] if len(sys.argv) > 2 else None
11
12# pdflatex can't embed the committed SVG logo directly, so convert it to a PDF beside
13# refman.tex for the \includegraphics{cryptnox-logo-dark} on the cover (cairosvg in CI).
14try:
15 import cairosvg
16except ImportError as _e:
17 raise RuntimeError(
18 "PDF docs build requires cairosvg to generate the cover logo; "
19 "install it (pip install cairosvg)."
20 ) from _e
21cairosvg.svg2pdf(
22 url=os.path.join(os.path.dirname(path), os.pardir, "cryptnox-logo.svg"),
23 write_to=os.path.join(os.path.dirname(path), "cryptnox-logo-dark.pdf"),
24)
25
26with open(path, encoding="utf-8") as _f:
27 s = _f.read()
28
29# Use TeX Gyre Heros (sans) for the whole document, like the Sphinx PDFs.
30# Doxygen already defaults to \sfdefault; just swap its Helvetica for TeX Gyre Heros.
31s = s.replace(r"\usepackage[scaled=.90]{helvet}", "\\usepackage{tgheros}\n\\usepackage{tgtermes}")
32
33# Remove the "Generated by Doxygen X.Y.Z" credit from the title page
34s = re.sub(r"\{\\large Generated by Doxygen[^}]*\}\\\\\s*", "", s)
35
36# Right-align the title-page block and put the logo inside the same flushright as the
37# title, with identical spacing to the Sphinx covers (\vskip 1em / logo / \vskip 2em).
38# Must run BEFORE the title-font edit below, which rewrites \Large -> \LARGE\bfseries.
39s = s.replace(
40 "\\begin{center}%\n {\\Large",
41 "\\begin{flushright}%\n"
42 " \\vskip -0.5em%\n"
43 " \\includegraphics[width=7cm]{cryptnox-logo-dark}\\par\n"
44 " \\vskip 2em%\n"
45 " {\\Large",
46)
47s = s.replace(
48 "\\end{center}\n \\end{titlepage}",
49 "\\end{flushright}\n \\vfill\n \\begin{flushright}\\large June 20, 2026\\end{flushright}\n \\end{titlepage}",
50)
51
52# Title: use the friendly name (passed as arg) + "Manual", in the Sphinx cover font.
53# Falls back to the Doxygen project name if no title is given.
54if title:
55 s = re.sub(r"\{\\Large [^}]*\}\\\\",
56 lambda m: r"{\LARGE\sffamily\bfseries " + title + r" Manual}\\", s, count=1)
57else:
58 s = re.sub(r"\{\\Large ([^}]*)\}\\\\", r"{\\LARGE\\sffamily\\bfseries \1 Manual}\\\\", s, count=1)
59
60# Version: prefix with "Release", unify the font (\large\itshape), and add more space
61# above it (title -> version gap), via the \\ optional length
62s = re.sub(r"\‍[1ex\‍]\\large (.*?) \\\\", r"[1.5em]{\\large\\itshape Release \1}\\\\", s, count=1)
63
64# Headings (chapters, sections, "Contents") in the sans heading font, like the
65# Sphinx PDFs and the Yubico manual (TeX Gyre Heros bold), keeping Doxygen's gray.
66s = re.sub(
67 r"\\doxyallsectionsfont\{%\s*\\fontseries\{bc\}\\selectfont%\s*\\color\{darkgray\}%\s*\}",
68 lambda m: (
69 r"\doxyallsectionsfont{\sffamily\bfseries\color{black}}"
70 "\n\\makeatletter"
71 "\n\\renewcommand{\\@makechapterhead}[1]{%"
72 "\n \\vspace*{50\\p@}{\\parindent\\z@\\raggedright\\sffamily"
73 "\n \\ifnum\\c@secnumdepth>\\m@ne"
74 "\n \\huge\\bfseries\\color{black}\\@chapapp\\space\\thechapter\\par\\nobreak\\vskip 20\\p@\\fi"
75 "\n \\interlinepenalty\\@M\\Huge\\bfseries\\color{black}#1\\par\\nobreak\\vskip 40\\p@}}"
76 "\n\\renewcommand{\\@makeschapterhead}[1]{%"
77 "\n \\vspace*{50\\p@}{\\parindent\\z@\\raggedright\\sffamily"
78 "\n \\interlinepenalty\\@M\\Huge\\bfseries\\color{black}#1\\par\\nobreak\\vskip 40\\p@}}"
79 "\n\\makeatother"
80 ),
81 s, count=1,
82)
83
84# One-sided layout (no blank filler pages)
85s = s.replace(r"\documentclass[twoside]", r"\documentclass[oneside]")
86
87# Footer: replace "Generated by Doxygen" with the copyright, in the sans heading font
88# (footer only; the title-page credit uses \large, so it is left untouched)
89s = s.replace(
90 r"\bfseries\scriptsize Generated by Doxygen",
91 r"\sffamily\fontsize{11}{13.6}\selectfont \copyright{} 2026 Cryptnox SA",
92)
93
94# Running header: sans, regular weight, 11pt — same size as the Sphinx PDFs
95s = s.replace(r"\fancyhead[LE, RO]{\bfseries\thepage}", r"\fancyhead[LE, RO]{\sffamily\fontsize{11}{13.6}\selectfont\thepage}")
96s = s.replace(r"\fancyhead[LO]{\bfseries\rightmark}", r"\fancyhead[LO]{\sffamily\fontsize{11}{13.6}\selectfont\rightmark}")
97s = s.replace(r"\fancyhead[RE]{\bfseries\leftmark}", r"\fancyhead[RE]{\sffamily\fontsize{11}{13.6}\selectfont\leftmark}")
98
99# Sans-serif TOC entries
100s = s.replace(r"\tableofcontents", r"\addtocontents{toc}{\protect\sffamily}\tableofcontents", 1)
101
102# 4pt gap between header/footer text and their rules
103s = s.replace(
104 r"\pagestyle{fancyplain}",
105 r"\def\headruleskip{4pt}\def\footruleskip{4pt}\pagestyle{fancyplain}",
106 1,
107)
108
109# pdflatex (inputenc utf8) aborts on any Unicode char it doesn't know. Declare the
110# math relations we want rendered as real glyphs (used in @c doc comments, e.g.
111# "len <= 254"); everything else non-ASCII is transliterated to safe ASCII at the end
112# of this script. Without this, pdflatex stops with "Unicode character not set up".
113s = s.replace(
114 r"\begin{document}",
115 "\\DeclareUnicodeCharacter{2264}{\\ensuremath{\\leq}}\n"
116 "\\DeclareUnicodeCharacter{2265}{\\ensuremath{\\geq}}\n"
117 "\\begin{document}",
118 1,
119)
120
121# Drop the alphabetical index
122s = s.replace(r"\printindex", "")
123s = s.replace(r"\addcontentsline{toc}{chapter}{\indexname}", "")
124
125# Replace the default top gap with just the top rule (the logo is injected inside the
126# flushright above, matching the Sphinx layout)
127s = s.replace(r"\vspace*{7cm}", r"\noindent\rule{\textwidth}{1pt}\par")
128
129with open(path, "w", encoding="utf-8") as _f:
130 _f.write(s)
131print("patched", path)
132
133# Section/subsection headings are defined in doxygen.sty (not refman.tex) with a
134# hardcoded \normalfont (serif); switch them to the sans heading font + gray.
135sty = os.path.join(os.path.dirname(path), "doxygen.sty")
136if os.path.exists(sty):
137 with open(sty, encoding="utf-8") as _f:
138 d = _f.read()
139 d = d.replace(r"{\raggedright\normalfont", r"{\raggedright\sffamily\color{black}")
140 with open(sty, "w", encoding="utf-8") as _f:
141 _f.write(d)
142 print("patched", sty)
143
144# Strip the "Download this documentation as PDF" link from the mainpage (README) —
145# pointless inside the PDF itself. Kept in README.md for GitHub/HTML.
146idx = os.path.join(os.path.dirname(path), "index.tex")
147if os.path.exists(idx):
148 with open(idx, encoding="utf-8") as _f:
149 i = _f.read()
150 i = re.sub(r"^.*Download this documentation as PDF.*\n", "", i, flags=re.MULTILINE)
151 with open(idx, "w", encoding="utf-8") as _f:
152 _f.write(i)
153 print("patched", idx)
154
155# Final safety net: Doxygen emits non-ASCII from source comments and the README
156# (box-drawing art in directory trees / wiring diagrams, arrows, stray symbols) into
157# every *.tex file. pdflatex's inputenc fatally stops on any codepoint it doesn't
158# know, and which ones appear changes as the docs change — so instead of declaring
159# each one, transliterate to ASCII here. Chars LaTeX already renders well are kept;
160# box-drawing is mapped to ASCII; anything else is dropped so the build cannot fail
161# on an unexpected glyph. Runs last, over all .tex files (the .sty files are pure
162# LaTeX and contain no such literals).
163import glob
164
165# Codepoints inputenc/textcomp already render (verified in CI logs) plus the math
166# relations declared above — left untouched so their nicer glyphs survive.
167keep = frozenset({
168 0x00A0, 0x00B2, 0x00B3, 0x00D7, # nbsp, superscript 2/3, multiplication
169 0x2013, 0x2014, 0x2026, # en/em dash, ellipsis
170 0x2018, 0x2019, 0x201C, 0x201D, # curly quotes
171 0x2190, 0x2192, 0x2194, # left/right/both arrows
172 0x2264, 0x2265, # <= and >= (declared in the preamble)
173})
174
175
176def _to_ascii(match):
177 cp = ord(match.group(0))
178 if cp in keep:
179 out = match.group(0)
180 elif 0x2500 <= cp <= 0x257F:
181 # Box-drawing block: verticals -> '|', corners/tees -> '+', the rest -> '-'.
182 if cp in (0x2502, 0x2503, 0x2551):
183 out = "|"
184 elif 0x250C <= cp <= 0x254B:
185 out = "+"
186 else:
187 out = "-"
188 else:
189 out = ""
190 return out
191
192
193for tex in glob.glob(os.path.join(os.path.dirname(path), "*.tex")):
194 with open(tex, encoding="utf-8") as _f:
195 body = _f.read()
196 # refman.tex's preamble holds Doxygen's own \doxynewunicodechar{<char>}{...}
197 # declarations; transliterating those would empty the first argument and crash.
198 # Only rewrite the document body. Other .tex files are pure body (no \begin{document}),
199 # so the marker is absent and the whole file is processed.
200 marker = "\\begin{document}"
201 split = body.find(marker)
202 head = body[:split] if split != -1 else ""
203 tail = body[split:] if split != -1 else body
204 fixed = head + re.sub(r"[^\x00-\x7F]", _to_ascii, tail)
205 if fixed != body:
206 with open(tex, "w", encoding="utf-8") as _f:
207 _f.write(fixed)
208 print("transliterated", tex)
_to_ascii(match)