pdfmath 0.1.0__tar.gz

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.
Files changed (90) hide show
  1. pdfmath-0.1.0/LICENSE +21 -0
  2. pdfmath-0.1.0/NOTICE +20 -0
  3. pdfmath-0.1.0/PKG-INFO +425 -0
  4. pdfmath-0.1.0/README.md +389 -0
  5. pdfmath-0.1.0/pdfmath/__init__.py +0 -0
  6. pdfmath-0.1.0/pdfmath/asciimath/__init__.py +5 -0
  7. pdfmath-0.1.0/pdfmath/asciimath/serializer.py +330 -0
  8. pdfmath-0.1.0/pdfmath/cli/__init__.py +0 -0
  9. pdfmath-0.1.0/pdfmath/cli/main.py +752 -0
  10. pdfmath-0.1.0/pdfmath/corpus/__init__.py +0 -0
  11. pdfmath-0.1.0/pdfmath/corpus/arxiv.py +194 -0
  12. pdfmath-0.1.0/pdfmath/corpus/compile.py +114 -0
  13. pdfmath-0.1.0/pdfmath/corpus/generate.py +202 -0
  14. pdfmath-0.1.0/pdfmath/corpus/grammar.py +476 -0
  15. pdfmath-0.1.0/pdfmath/corpus/harness.py +318 -0
  16. pdfmath-0.1.0/pdfmath/corpus/milestone.py +83 -0
  17. pdfmath-0.1.0/pdfmath/corpus/shrink.py +126 -0
  18. pdfmath-0.1.0/pdfmath/corpus/strategies.py +71 -0
  19. pdfmath-0.1.0/pdfmath/debug/__init__.py +0 -0
  20. pdfmath-0.1.0/pdfmath/debug/explain.py +82 -0
  21. pdfmath-0.1.0/pdfmath/debug/svg.py +238 -0
  22. pdfmath-0.1.0/pdfmath/detection/__init__.py +35 -0
  23. pdfmath-0.1.0/pdfmath/detection/equations.py +344 -0
  24. pdfmath-0.1.0/pdfmath/detection/inline.py +410 -0
  25. pdfmath-0.1.0/pdfmath/eval/__init__.py +0 -0
  26. pdfmath-0.1.0/pdfmath/eval/arxiv_eval.py +172 -0
  27. pdfmath-0.1.0/pdfmath/eval/asciimath_roundtrip.py +160 -0
  28. pdfmath-0.1.0/pdfmath/eval/latexml.py +64 -0
  29. pdfmath-0.1.0/pdfmath/eval/mathml_compare.py +216 -0
  30. pdfmath-0.1.0/pdfmath/eval/omml_roundtrip.py +193 -0
  31. pdfmath-0.1.0/pdfmath/eval/roundtrip.py +203 -0
  32. pdfmath-0.1.0/pdfmath/extraction/__init__.py +0 -0
  33. pdfmath-0.1.0/pdfmath/extraction/model.py +227 -0
  34. pdfmath-0.1.0/pdfmath/extraction/pdfminer_backend.py +242 -0
  35. pdfmath-0.1.0/pdfmath/extraction/symbolscraper_adapter.py +118 -0
  36. pdfmath-0.1.0/pdfmath/fonts/__init__.py +0 -0
  37. pdfmath-0.1.0/pdfmath/fonts/data/__init__.py +0 -0
  38. pdfmath-0.1.0/pdfmath/fonts/data/extra_symbols.py +211 -0
  39. pdfmath-0.1.0/pdfmath/fonts/data/tex_encodings.py +1629 -0
  40. pdfmath-0.1.0/pdfmath/fonts/mathparams.py +180 -0
  41. pdfmath-0.1.0/pdfmath/fonts/metrics.py +129 -0
  42. pdfmath-0.1.0/pdfmath/fonts/normalize.py +191 -0
  43. pdfmath-0.1.0/pdfmath/fonts/symbols.py +410 -0
  44. pdfmath-0.1.0/pdfmath/fonts/tfm.py +251 -0
  45. pdfmath-0.1.0/pdfmath/geometry/__init__.py +0 -0
  46. pdfmath-0.1.0/pdfmath/geometry/bbox.py +77 -0
  47. pdfmath-0.1.0/pdfmath/geometry/index.py +169 -0
  48. pdfmath-0.1.0/pdfmath/latex/__init__.py +0 -0
  49. pdfmath-0.1.0/pdfmath/latex/data/__init__.py +0 -0
  50. pdfmath-0.1.0/pdfmath/latex/data/tex_commands.py +1034 -0
  51. pdfmath-0.1.0/pdfmath/latex/serializer.py +331 -0
  52. pdfmath-0.1.0/pdfmath/mathml/__init__.py +0 -0
  53. pdfmath-0.1.0/pdfmath/mathml/lgeval.py +184 -0
  54. pdfmath-0.1.0/pdfmath/mathml/serializer.py +250 -0
  55. pdfmath-0.1.0/pdfmath/omml/__init__.py +5 -0
  56. pdfmath-0.1.0/pdfmath/omml/serializer.py +345 -0
  57. pdfmath-0.1.0/pdfmath/parse/__init__.py +0 -0
  58. pdfmath-0.1.0/pdfmath/parse/accents.py +140 -0
  59. pdfmath-0.1.0/pdfmath/parse/atoms.py +82 -0
  60. pdfmath-0.1.0/pdfmath/parse/axis.py +79 -0
  61. pdfmath-0.1.0/pdfmath/parse/blocks.py +122 -0
  62. pdfmath-0.1.0/pdfmath/parse/context.py +193 -0
  63. pdfmath-0.1.0/pdfmath/parse/delimiters.py +192 -0
  64. pdfmath-0.1.0/pdfmath/parse/driver.py +425 -0
  65. pdfmath-0.1.0/pdfmath/parse/fractions.py +274 -0
  66. pdfmath-0.1.0/pdfmath/parse/matrices.py +247 -0
  67. pdfmath-0.1.0/pdfmath/parse/operators.py +190 -0
  68. pdfmath-0.1.0/pdfmath/parse/radicals.py +154 -0
  69. pdfmath-0.1.0/pdfmath/parse/rows.py +293 -0
  70. pdfmath-0.1.0/pdfmath/parse/scripts.py +415 -0
  71. pdfmath-0.1.0/pdfmath/parse/spacing.py +137 -0
  72. pdfmath-0.1.0/pdfmath/parse/style.py +75 -0
  73. pdfmath-0.1.0/pdfmath/parse/units.py +290 -0
  74. pdfmath-0.1.0/pdfmath/speech/__init__.py +7 -0
  75. pdfmath-0.1.0/pdfmath/speech/engine.py +192 -0
  76. pdfmath-0.1.0/pdfmath/speech/speak.js +35 -0
  77. pdfmath-0.1.0/pdfmath/tree/__init__.py +0 -0
  78. pdfmath-0.1.0/pdfmath/tree/nodes.py +374 -0
  79. pdfmath-0.1.0/pdfmath/units.py +38 -0
  80. pdfmath-0.1.0/pdfmath/wordperfect/__init__.py +5 -0
  81. pdfmath-0.1.0/pdfmath/wordperfect/serializer.py +301 -0
  82. pdfmath-0.1.0/pdfmath.egg-info/PKG-INFO +425 -0
  83. pdfmath-0.1.0/pdfmath.egg-info/SOURCES.txt +88 -0
  84. pdfmath-0.1.0/pdfmath.egg-info/dependency_links.txt +1 -0
  85. pdfmath-0.1.0/pdfmath.egg-info/entry_points.txt +2 -0
  86. pdfmath-0.1.0/pdfmath.egg-info/requires.txt +12 -0
  87. pdfmath-0.1.0/pdfmath.egg-info/top_level.txt +1 -0
  88. pdfmath-0.1.0/pyproject.toml +60 -0
  89. pdfmath-0.1.0/setup.cfg +4 -0
  90. pdfmath-0.1.0/tests/test_readme.py +126 -0
pdfmath-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam DePrince
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pdfmath-0.1.0/NOTICE ADDED
@@ -0,0 +1,20 @@
1
+ pdfmath
2
+ Copyright 2026 Adam DePrince
3
+
4
+ Attribution for the data this project generates from other people's work. None
5
+ of it is code; it is all factual tables, and the generators are in tools/ so
6
+ they can be re-run.
7
+
8
+ * Glyph-name and encoding tables generated from the Adobe Font Metrics files
9
+ distributed with TeX Live for the Computer Modern and AMS fonts.
10
+ * Math-symbol classifications composed from LaTeXML's DefMath declarations
11
+ (LaTeXML is a work of the US National Institute of Standards and Technology
12
+ and is not subject to copyright in the United States) and from LaTeX's own
13
+ \DeclareMathSymbol tables.
14
+ * WordPerfect 5.1 equation-language vocabulary checked against the equation
15
+ parser in the wp51 project.
16
+
17
+ pdfmath speaks mathematics through MathJax's speech-rule-engine
18
+ (https://github.com/Speech-Rule-Engine/speech-rule-engine), which is licensed
19
+ under the Apache License 2.0 and is installed separately; it is invoked as a
20
+ subprocess and no part of it is included here.
pdfmath-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,425 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdfmath
3
+ Version: 0.1.0
4
+ Summary: A decompiler for TeX mathematics: born-digital PDF -> layout tree -> Presentation MathML
5
+ Author-email: Adam DePrince <adam.deprince@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/adamdeprince/pdfmath
8
+ Project-URL: Repository, https://github.com/adamdeprince/pdfmath
9
+ Project-URL: Issues, https://github.com/adamdeprince/pdfmath/issues
10
+ Keywords: tex,latex,pdf,mathml,mathematics,accessibility,decompiler,asciimath,omml,speech
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
20
+ Classifier: Topic :: Text Processing :: Markup :: LaTeX
21
+ Classifier: Topic :: Adaptive Technologies
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ License-File: NOTICE
26
+ Requires-Dist: pdfminer.six>=20231228
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == "dev"
29
+ Requires-Dist: hypothesis>=6; extra == "dev"
30
+ Provides-Extra: oracles
31
+ Requires-Dist: py-asciimath>=0.3; extra == "oracles"
32
+ Requires-Dist: pypandoc_binary>=1.13; extra == "oracles"
33
+ Provides-Extra: pdf
34
+ Requires-Dist: pikepdf>=8; extra == "pdf"
35
+ Dynamic: license-file
36
+
37
+ # pdfmath
38
+
39
+ A decompiler for TeX mathematics. It reads a born-digital PDF and gives you back the
40
+ equations — as MathML, speech, AsciiMath, LaTeX, Word's OMML, or WordPerfect 5.1.
41
+
42
+ ```
43
+ old TeX-generated PDF
44
+ ↓ positioned glyphs and rules
45
+ ↓ inferred TeX boxes and relationships
46
+ ↓ reconstructed math layout tree
47
+ MathML · speech · AsciiMath · LaTeX · OMML · WP5.1
48
+ ```
49
+
50
+ Older mathematical papers are full of PDFs that show correct mathematics and record none
51
+ of it: no MathML, no tagging, often not even a usable ToUnicode map. The usual answer is
52
+ to rasterise the equation and ask a model to guess the LaTeX back. This project takes the
53
+ opposite view: **a TeX-produced PDF is not a picture of an equation, it is the compiled
54
+ output of a deterministic layout program, and the program's rules are published.** So we
55
+ do not recognise the equation from its appearance — we recover the structure implied by
56
+ the layout, and we can say in points how well it fits.
57
+
58
+ This README is a tutorial. For how it works inside, see [docs/architecture.md]; for how
59
+ well it works, [docs/evaluation.md].
60
+
61
+ ---
62
+
63
+ ## Install
64
+
65
+ **Required:** Python 3.10 or newer. That is all you need to decompile a PDF.
66
+
67
+ ```bash
68
+ git clone <this repo> && cd math
69
+ python3 -m venv .venv && source .venv/bin/activate
70
+ pip install -e ".[dev]"
71
+ pdfmath --help
72
+ ```
73
+
74
+ **Optional, each unlocking one thing:**
75
+
76
+ | You want | Install | Needed for |
77
+ |---|---|---|
78
+ | exact font metrics, the sample document | a TeX distribution (MacTeX, TeX Live) | TFM metrics, `benchmark`, `roundtrip` |
79
+ | speech | Node, then `cd tools/sre && npm install` | `pdfmath speak` |
80
+ | the serialiser oracle tests | `pip install -e ".[oracles]"` | AsciiMath and OMML round-trip tests |
81
+ | scoring against a paper's own source | `brew install latexml` | `pdfmath arxiv` |
82
+
83
+ Without TeX, extraction and every output format still work — the metrics come from the
84
+ PDF's own `/Widths` instead of the TFM files, so the residuals stop being meaningful but
85
+ the structure is still recovered.
86
+
87
+ Check the install:
88
+
89
+ ```bash
90
+ python -m pytest tests/unit -q # fast, no TeX needed
91
+ python -m pytest -q # everything, needs TeX
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Tutorial
97
+
98
+ ### 1. Build the sample
99
+
100
+ The repository ships a small document to work on. Build it once:
101
+
102
+ ```bash
103
+ pdflatex -output-directory=examples examples/sample.tex
104
+ ```
105
+
106
+ It has three displayed equations and one inline formula. If you would rather use your
107
+ own paper, any born-digital PDF from pdfTeX will do — just substitute it below.
108
+
109
+ ### 2. Decompile it
110
+
111
+ ```bash
112
+ $ pdfmath extract examples/sample.pdf --asciimath
113
+ E = (x_i^2)/(sqrt(y))
114
+ sum_(k = 1)^n (1)/(k^2) = (pi^2)/(6) - epsilon_n.
115
+ A = ((a,b),(c,d)), quad det A = a d - b c.
116
+ sqrt(a^2 + b^2)
117
+ ```
118
+
119
+ No page number, no coordinates. Three of those are displayed equations, found by their
120
+ shape; the fourth is inside a sentence, found by a different method entirely — see below.
121
+ Pass `--no-inline` for displays only.
122
+
123
+ Ask for MathML instead and you get the real target format:
124
+
125
+ ```bash
126
+ $ pdfmath extract examples/sample.pdf --bbox 286,612,326,641 --mathml
127
+ <math display="block" xmlns="http://www.w3.org/1998/Math/MathML">
128
+ <mrow>
129
+ <mi>E</mi>
130
+ <mo>=</mo>
131
+ <mfrac>
132
+ <msubsup>
133
+ <mi>x</mi>
134
+ <mi>i</mi>
135
+ <mn>2</mn>
136
+ </msubsup>
137
+ <msqrt><mi>y</mi></msqrt>
138
+ </mfrac>
139
+ </mrow>
140
+ </math>
141
+ ```
142
+
143
+ ### 3. Read it aloud
144
+
145
+ ```bash
146
+ $ pdfmath speak examples/sample.pdf
147
+ E equals the fraction with numerator x sub i squared and denominator the square root of y
148
+ the sum from k equals 1 to n of the fraction with numerator 1 and denominator k squared equals the fraction with numerator pi squared and denominator 6 minus epsilon sub n period
149
+ A equals the 2 by 2 matrix Row 1: a b Row 2: c d comma determinant A equals a d minus b c period
150
+ the square root of a squared plus b squared
151
+ ```
152
+
153
+ The last line is the formula from inside the sentence. That is ClearSpeak, which reads the way a person would say it. MathSpeak is unambiguous
154
+ and reversible instead — what you want when checking someone else's algebra:
155
+
156
+ ```bash
157
+ $ pdfmath speak examples/sample.pdf --rules mathspeak --verbosity brief --bbox 286,612,326,641
158
+ upper E equals StartFrac x Sub i Sup 2 Base Over StartRoot y EndRoot EndFrac
159
+ ```
160
+
161
+ `--ssml` emits SSML instead of plain text, so a synthesiser pauses in the right places.
162
+
163
+ Speech is a bridge to MathJax's [speech-rule-engine][sre] (Apache-2.0) rather than rules
164
+ of our own — MathSpeak and ClearSpeak are specified and user-tested, and an approximation
165
+ would be worse in ways that are hard to notice mid-paper. It needs Node:
166
+ `cd tools/sre && npm install`.
167
+
168
+ Two things are changed before speaking, both so you do not hear something that is not on
169
+ the page. A glyph we could not name is written `□` in MathML, which reads aloud as "white
170
+ square" — a real operator, and indistinguishable from one we meant; it is announced as
171
+ unrecognised instead. And the measured inter-atom spacing is dropped, because `mspace` is
172
+ read aloud as "empty", so a `\quad` before an equation number became a spoken word.
173
+
174
+ ### 4. The other formats
175
+
176
+ | Format | Flag | What it is for |
177
+ |---|---|---|
178
+ | Presentation MathML | `--mathml` | the primary target; `--provenance` adds glyph ids |
179
+ | Speech | `speak`, `--speech` | ClearSpeak or MathSpeak text, or SSML |
180
+ | AsciiMath | `--asciimath` | linear, and stays readable magnified |
181
+ | LaTeX | `--latex` | the round-trip oracle, and useful on its own |
182
+ | Office MathML | `--omml` | what a `.docx` stores, so Word can *edit* it |
183
+ | WordPerfect 5.1 | `--wpeq` | the 1989 equation editor's command language |
184
+ | JSON tree | `--tree` | every node with provenance, residuals, confidence |
185
+ | LgEval label graph | `--lg` | comparison against MathSeer/CROHME tooling |
186
+
187
+ Ask for one and it prints bare; ask for several and you get JSON.
188
+
189
+ ```bash
190
+ $ pdfmath extract examples/sample.pdf --latex
191
+ E = \frac{x_{i}^{2}}{\sqrt{y}}
192
+ \sum\limits_{k = 1}^{n} \frac{1}{k^{2}} = \frac{\pi^{2}}{6} - \varepsilon_{n} .
193
+ A = \left( \begin{matrix} a & b \\ c & d \end{matrix} \right) , \mskip 39.006mu \mathrm{det} \mskip 2.988mu A = a d - b c .
194
+ \sqrt{a^{2} + b^{2}}
195
+
196
+ $ pdfmath extract examples/sample.pdf --wpeq
197
+ E = {x sub i sup 2} over {sqrt {y}}
198
+ sum from {k = 1} to n {1} over {k sup 2} = {pi sup 2} over {6} - epsilon sub n .
199
+ A = left ( matrix {a & b # c & d} right ) , ~ det ` A = a d - b c .
200
+ sqrt {a sup 2 + b sup 2}
201
+ ```
202
+
203
+ The LaTeX writer aims to recompile to the identical page rather than to look idiomatic,
204
+ which is why the spacing is explicit; it reports whatever it could not reproduce instead
205
+ of guessing.
206
+
207
+ ### 5. When an equation is missed
208
+
209
+ The sample's last sentence also names a vector `$\mathbf{v}$`, and nothing finds it —
210
+ `\mathbf` and bold prose are the same font, so the distinction is not on the page to be
211
+ found. Give it the box directly:
212
+
213
+ ```bash
214
+ $ pdfmath extract examples/sample.pdf --bbox 178,446,186,456 --style text --asciimath
215
+ v
216
+ ```
217
+
218
+ `--bbox` is `x0,y0,x1,y1` in TeX points from the bottom-left of the page (`--bbox-bp` for
219
+ PDF big points), and `--style` tells the parser which math style to expect, since that
220
+ changes every Appendix G prediction. This is also the workaround when detection misses a
221
+ display — numbered equations are one known case, because the tag pushed out by `\hfill`
222
+ widens the line past what the detector expects.
223
+
224
+ #### How inline detection works
225
+
226
+ An inline formula has no shape to find it by: it sits in the middle of a sentence, on the
227
+ same baseline, in the same paragraph. What it has instead is TeX's own bookkeeping.
228
+
229
+ * **The font.** TeX sets prose from the roman font and mathematics from cmmi, cmsy and
230
+ cmex. There is no way to type a cmmi glyph outside mathematics, so every one is a seed.
231
+ This finds more than variables: the comma in `$[0,1]$` comes from cmmi, and it is the
232
+ only non-roman character in that formula.
233
+ * **The gap, which calibrates itself.** Interword glue stretches so a line can be
234
+ justified; math glue does not. So the word space is a property of *this line*,
235
+ measurable from the line's own gaps — 6.4 mu on one line here, 6.1 on the next — and
236
+ every automatic math space is at most 5 mu. That is what separates `\log n` from two
237
+ words, with no threshold in points that would be wrong at another size.
238
+ * **The character class.** Roman characters do appear in formulas — digits, `+`, `=`,
239
+ parentheses, capital Greek — so a seed grows outward through those and stops at a roman
240
+ *letter*, which is prose. Operator names (`log`, `sin`, `max`) are the exception and are
241
+ recognised as a set.
242
+
243
+ What it cannot do is find a formula containing no math-font glyph at all: `$\mathbf{v}$`
244
+ is cmbx and so is bold prose; `$2$` is a roman digit and so is a page number. Those are
245
+ undecidable rather than hard — TeX threw the distinction away — and they are left alone.
246
+
247
+ **Which is why this is on by default.** A missed inline formula is not silence: its
248
+ characters are still there and are still read. And the formulas this method misses are
249
+ exactly the ones made entirely of ordinary characters, so reading them as characters is
250
+ already right — `$\mathbf{v}$` says "v", which is what it is. The asymmetry runs the
251
+ other way from most detection problems: finding one is a large gain, missing one costs
252
+ nothing, and only a false positive would do harm. `--no-inline` turns it off.
253
+
254
+ ### 6. Ask why
255
+
256
+ Every inference records its evidence in the units of the rule that produced it:
257
+
258
+ ```bash
259
+ $ pdfmath explain examples/sample.pdf --page 1 --bbox 286,612,326,641
260
+ node 7 Fraction [fraction] confidence 0.9990
261
+ bbox 310.48,611.83 .. 325.37,639.57 (pt)
262
+ glyphs [26, 27, 28, 29, 30]
263
+ rules [0, 1]
264
+ children [4, 6]
265
+ evidence:
266
+ rule_width_pt 14.88660
267
+ rule_thickness_pt 0.43763
268
+ expected_default_rule_thickness_pt 0.43799
269
+ thickness_residual_pt -0.00035
270
+ n_above 3
271
+ n_below 3
272
+ style_fit display
273
+ residual_pt +0.00039
274
+ numerator_residual_pt +0.00018
275
+ denominator_residual_pt -0.00039
276
+ axis_height_pt 2.73750
277
+ consistent_with_rule_15 True
278
+ ... (and the measured inter-atom spacing either
279
+ side, in mu, with the atom classes it implies)
280
+ ```
281
+
282
+ A residual of 0.0004 pt is the fraction bar landing exactly where Appendix G rule 15 says
283
+ it must. That is the difference between a decompiler and a recogniser: the number is a
284
+ measurement, not a score.
285
+
286
+ ### 7. Triage a document
287
+
288
+ ```bash
289
+ $ pdfmath survey examples/sample.pdf
290
+ examples/sample.pdf
291
+ equations 3
292
+ glyph recovery 100.000% (43/43)
293
+ Unknown nodes 0
294
+ inferences below 0.90 0
295
+
296
+ page 1 bbox [223.17, 484.9, 389.37, 511.18] confidence 0.9800 glyphs 20
297
+ page 1 bbox [263.06, 546.47, 349.47, 577.01] confidence 0.9900 glyphs 16
298
+ page 1 bbox [285.97, 611.83, 325.37, 639.57] confidence 0.9990 glyphs 7
299
+ ```
300
+
301
+ `--floor 0.9` lists everything below a confidence you choose. A glyph nobody can explain
302
+ becomes an `Unknown` carrying its original geometry — nothing is silently dropped, and
303
+ there is a test for that.
304
+
305
+ ### 8. See what the parser saw
306
+
307
+ ```bash
308
+ pdfmath debug-svg examples/sample.pdf --page 1 --html -o page1.html
309
+ ```
310
+
311
+ Glyph boxes, baselines, extracted rules and the recovered tree, in one SVG with
312
+ toggleable layers. `pdfmath dump` gives the same thing as JSON, one record per glyph,
313
+ before any parsing happens.
314
+
315
+ ---
316
+
317
+ ## Checking the output against something else
318
+
319
+ The interesting question is not whether the tool is confident but whether it is right, so
320
+ every claim has a check that does not run our code.
321
+
322
+ ```bash
323
+ pdfmath roundtrip examples/sample.pdf # recompile the LaTeX, compare pages glyph by glyph
324
+ pdfmath benchmark --suite all --n 200 # generate a corpus with known answers, score it
325
+ pdfmath arxiv math/0211159v1 # score against the paper's own source, via LaTeXML
326
+ ```
327
+
328
+ `roundtrip` is the strongest of the three: it takes what we recovered, compiles it with
329
+ pdfTeX, and compares the resulting page against the original glyph by glyph. `exact`
330
+ means every glyph is the same character from the same font within 0.01 pt.
331
+
332
+ ```bash
333
+ $ pdfmath roundtrip examples/sample.pdf
334
+ examples/sample.pdf
335
+ equations 3
336
+ exact 2 66.7%
337
+ shifted 1 33.3%
338
+ ```
339
+
340
+ It then prints each equation that was not exact, with the LaTeX it recovered and the
341
+ glyphs that moved, so a `shifted` verdict points at the box width we got wrong rather
342
+ than just failing. The command exits non-zero unless every equation is exact, which makes
343
+ it usable as a CI gate.
344
+
345
+ The serialisers are checked the same way, each against a reader that is not ours: MathML
346
+ against LaTeXML, AsciiMath against `py-asciimath`, OMML against pandoc's `docx` reader.
347
+ WordPerfect has no automated check yet — its vocabulary is taken from the equation parser
348
+ in the `wp51` project, but that exposes no command for a bare equation string — so it
349
+ rests on goldens, and the handful of commands still unconfirmed are flagged in the
350
+ output.
351
+
352
+ Numbers and method: [docs/evaluation.md].
353
+
354
+ ---
355
+
356
+ ## How it works
357
+
358
+ Four stages, one file each to start reading from.
359
+
360
+ 1. **Extraction** (`extraction/pdfminer_backend.py`) intercepts pdfminer's renderer and
361
+ records every glyph's character code, font, size and text matrix, plus every rule
362
+ drawn as a filled rectangle. Not a bounding box from a page image — the numbers the
363
+ typesetter used.
364
+ 2. **Font knowledge** (`fonts/tfm.py`) reads the TFM files TeX itself used, giving
365
+ per-character widths, heights, depths and italic corrections, and the σ and ξ
366
+ parameters Appendix G is written in terms of.
367
+ 3. **Parsing** (`parse/`) runs recognisers that each invert one published rule — rule 15
368
+ for fractions, 18a–f for scripts, 11 for radicals, 13 for large operators — and report
369
+ a residual in points rather than a threshold verdict. Anchors are claimed outermost
370
+ first so a nested construct cannot steal its parent's.
371
+ 4. **Serialisation** (`mathml/`, `speech/`, `asciimath/`, `latex/`, `omml/`,
372
+ `wordperfect/`) walks the finished tree. Every node carries provenance back to the
373
+ glyphs and rules it came from; that is not optional.
374
+
375
+ The design decisions, the inverted rules with their closed forms, the thresholds that
376
+ survive and why, and the known limitations with their causes are all in
377
+ [docs/architecture.md].
378
+
379
+ ---
380
+
381
+ ## Status and scope
382
+
383
+ Supported and measured: pdfTeX output, Computer Modern and Latin Modern, AMS symbol
384
+ fonts, displayed equations, display and text style, fractions, scripts, radicals,
385
+ delimiters (including built-up cmex assemblies), large operators with limits, matrices,
386
+ accents, over/underlines, and the inter-atom spacing table.
387
+
388
+ Inline formulas are found by default, from the fonts TeX switched to and the glue it
389
+ inserted rather than from any shape on the page; `--no-inline` restricts to displays.
390
+
391
+ Two-column layouts are fine: the text column is measured from the page's own prose, so a
392
+ narrower one changes nothing.
393
+
394
+ Not yet: documents whose mathematics is not in a TeX math font (a paper set in Times with
395
+ `mathptmx` is not detected at all, which is the edge of the stated scope rather than a
396
+ slope), numbered displays reliably detected, XeTeX/LuaTeX OpenType math, Type 3 fonts, scanned pages (out of scope by
397
+ design), `\overbrace`-style horizontal braces, and alignment recovery in `align` beyond a
398
+ table of rows. [docs/architecture.md] lists the known limitations with reasons.
399
+
400
+ ## Where to read more
401
+
402
+ | Document | What is in it |
403
+ |---|---|
404
+ | [docs/architecture.md] | how it works: pipeline, module map, inverted rules, thresholds, limitations |
405
+ | [docs/evaluation.md] | how well it works: synthetic corpus, real papers, recompilation |
406
+ | [docs/prior-art.md] | the audit of SymbolScraper, MathFIRE and MaxTract, with reuse decisions |
407
+ | [docs/comparison.md] | the MathSeer hypothesis and the protocol for testing it |
408
+
409
+ ## Licence
410
+
411
+ MIT, chosen over Apache 2.0 for GPLv2 compatibility. That matters here because the
412
+ screen readers this work is ultimately for are GPLv2 — NVDA among them — and MIT code can
413
+ be incorporated into them where Apache 2.0 code cannot. The cost is that MIT says nothing
414
+ about patents, which for an implementation of published 1980s typesetting rules is a
415
+ theoretical concern rather than a real one.
416
+
417
+ Dependencies are MIT/BSD/MPL. PyMuPDF is deliberately avoided because it is AGPL-3.0 and
418
+ would be viral for downstream users. `NOTICE` records the third-party tables the
419
+ generated data files are derived from.
420
+
421
+ [docs/architecture.md]: docs/architecture.md
422
+ [docs/evaluation.md]: docs/evaluation.md
423
+ [docs/prior-art.md]: docs/prior-art.md
424
+ [docs/comparison.md]: docs/comparison.md
425
+ [sre]: https://github.com/Speech-Rule-Engine/speech-rule-engine