mathtext2doc 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,12 @@
1
+ """mathtext2doc — 把 AI 生成的数学文本快速转成带公式和函数图像的文档图片。
2
+
3
+ 子模块:
4
+ parser — 解析 Markdown / LaTeX 公式 / @plot 指令
5
+ plotter — 绘制 2D 显函数 / 隐函数 / 多函数同图,输出 PNG
6
+ texgen — 生成完整 .tex 文件(ctex / xeCJK 中文支持)
7
+ compiler — 调用本机 LaTeX 编译并产出 PNG
8
+ cli — 命令行入口
9
+ """
10
+
11
+ __version__ = "0.1.0"
12
+ __all__ = ["parser", "plotter", "texgen", "compiler", "cli"]
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+ import sys
3
+
4
+ if __name__ == "__main__":
5
+ sys.exit(main())
mathtext2doc/cli.py ADDED
@@ -0,0 +1,249 @@
1
+ """mathtext2doc CLI 入口。
2
+
3
+ 用法:
4
+ python -m mathtext2doc.cli input.txt
5
+ python -m mathtext2doc.cli input.txt --compiler xelatex --dpi 200
6
+ python -m mathtext2doc.cli input.txt --keep-intermediates
7
+ python -m mathtext2doc.cli input.txt --overwrite
8
+
9
+ 退出码:
10
+ 0 成功
11
+ 1 解析错误
12
+ 2 绘图错误
13
+ 3 LaTeX 编译错误
14
+ 4 输入/输出错误(文件不存在、输出已存在等)
15
+ 5 其它未预期错误
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import os
22
+ import sys
23
+ import traceback
24
+ from typing import Dict, List
25
+
26
+ from . import __version__
27
+ from .parser import (
28
+ DisplayMath,
29
+ Heading,
30
+ ListItem,
31
+ Paragraph,
32
+ ParseError,
33
+ Plot,
34
+ PlotParseError,
35
+ parse_document,
36
+ )
37
+ from .plotter import PlotRenderError, render_plot
38
+ from .texgen import generate_tex
39
+ from .compiler import CompileError, compile_tex_to_png
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # 主流程
44
+ # ---------------------------------------------------------------------------
45
+
46
+ def run(input_path: str, args: argparse.Namespace) -> int:
47
+ """主流程。返回退出码。"""
48
+ # 0. 校验参数
49
+ if not (0 < args.plot_width <= 1.0):
50
+ sys.stderr.write(
51
+ f"错误:--plot-width 必须在 (0, 1] 之间,得到 {args.plot_width}\n"
52
+ )
53
+ return 4
54
+
55
+ # 1. 检查输入文件
56
+ if not os.path.isfile(input_path):
57
+ sys.stderr.write(f"错误:输入文件不存在:{input_path}\n")
58
+ return 4
59
+ try:
60
+ with open(input_path, "r", encoding="utf-8") as f:
61
+ text = f.read()
62
+ except Exception as ex:
63
+ sys.stderr.write(f"错误:读取输入文件失败:{ex}\n")
64
+ return 4
65
+
66
+ out_dir = os.path.dirname(os.path.abspath(input_path)) or "."
67
+ base = os.path.splitext(os.path.basename(input_path))[0]
68
+ tex_path = os.path.join(out_dir, base + ".tex")
69
+
70
+ # 2. 检查输出是否已存在
71
+ if not args.overwrite:
72
+ if os.path.isfile(tex_path):
73
+ sys.stderr.write(
74
+ f"错误:输出 .tex 已存在:{tex_path}(使用 --overwrite 覆盖)\n"
75
+ )
76
+ return 4
77
+ # 检查 PNG
78
+ for fn in os.listdir(out_dir):
79
+ if fn.startswith(base + "-") and fn.endswith(".png"):
80
+ sys.stderr.write(
81
+ f"错误:输出 PNG 已存在:{os.path.join(out_dir, fn)}"
82
+ f"(使用 --overwrite 覆盖)\n"
83
+ )
84
+ return 4
85
+
86
+ # 3. 解析文档
87
+ try:
88
+ blocks = parse_document(text)
89
+ except PlotParseError as ex:
90
+ sys.stderr.write(f"绘图指令解析错误:{ex}\n")
91
+ return 1
92
+ except ParseError as ex:
93
+ sys.stderr.write(f"文档解析错误:{ex}\n")
94
+ return 1
95
+
96
+ # 4. 渲染所有 plot
97
+ plot_paths: Dict[int, str] = {}
98
+ plot_idx = 0
99
+ for b in blocks:
100
+ if isinstance(b, Plot):
101
+ plot_idx += 1
102
+ png_name = f"{base}-plot{plot_idx}.png"
103
+ png_path = os.path.join(out_dir, png_name)
104
+ # 计算该图的有效显示宽度(单图 width 优先,否则全局默认)
105
+ # 用于 auto-DPI:让大图小图都有相同的有效分辨率
106
+ effective_width = b.width if b.width is not None else args.plot_width
107
+ try:
108
+ render_plot(b, png_path, dpi=args.dpi, display_width=effective_width)
109
+ except PlotRenderError as ex:
110
+ sys.stderr.write(f"绘图错误(第 {plot_idx} 个 @plot):{ex}\n")
111
+ return 2
112
+ plot_paths[id(b)] = png_name # .tex 中用相对名
113
+
114
+ # 5. 生成 .tex
115
+ try:
116
+ tex_content = generate_tex(blocks, plot_paths, default_plot_width=args.plot_width)
117
+ except Exception as ex:
118
+ sys.stderr.write(f"生成 .tex 失败:{ex}\n")
119
+ return 5
120
+ try:
121
+ with open(tex_path, "w", encoding="utf-8") as f:
122
+ f.write(tex_content)
123
+ except Exception as ex:
124
+ sys.stderr.write(f"写入 .tex 失败:{ex}\n")
125
+ return 4
126
+
127
+ sys.stdout.write(f"已生成 {tex_path}\n")
128
+
129
+ # 6. 编译并产出 PNG
130
+ try:
131
+ result = compile_tex_to_png(
132
+ tex_path,
133
+ out_dir,
134
+ compiler=args.compiler,
135
+ dpi=args.dpi,
136
+ overwrite=args.overwrite,
137
+ keep_intermediates=args.keep_intermediates,
138
+ )
139
+ except CompileError as ex:
140
+ sys.stderr.write(f"LaTeX 编译失败:{ex}\n")
141
+ return 3
142
+
143
+ # 7. 输出结果
144
+ sys.stdout.write(f"使用编译器:{result.compiler}\n")
145
+ if result.fallback_used:
146
+ sys.stdout.write("(已回退到 PDF→PNG 路径)\n")
147
+ sys.stdout.write(f"输出 PNG:\n")
148
+ for p in result.png_paths:
149
+ sys.stdout.write(f" {p}\n")
150
+ if result.pdf_path:
151
+ sys.stdout.write(f"保留 PDF:{result.pdf_path}\n")
152
+ if result.log_path:
153
+ sys.stdout.write(f"保留日志:{result.log_path}\n")
154
+
155
+ # 8. 清理 plot PNG(若不保留中间文件)
156
+ if not args.keep_intermediates:
157
+ for path in plot_paths.values():
158
+ full = os.path.join(out_dir, path)
159
+ if os.path.isfile(full):
160
+ try:
161
+ os.remove(full)
162
+ except Exception:
163
+ pass
164
+
165
+ return 0
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # argparse
170
+ # ---------------------------------------------------------------------------
171
+
172
+ def build_parser() -> argparse.ArgumentParser:
173
+ p = argparse.ArgumentParser(
174
+ prog="mathtext2doc",
175
+ description=(
176
+ "把 AI 生成的数学文本快速转成带公式和函数图像的文档图片。"
177
+ "支持 Markdown、$...$/$$...$$ LaTeX 公式、@plot{...} 绘图指令。"
178
+ ),
179
+ formatter_class=argparse.RawDescriptionHelpFormatter,
180
+ epilog=(
181
+ "示例:\n"
182
+ " python -m mathtext2doc.cli input.txt\n"
183
+ " python -m mathtext2doc.cli input.txt --compiler xelatex --dpi 200\n"
184
+ " python -m mathtext2doc.cli input.txt --overwrite --keep-intermediates\n"
185
+ ),
186
+ )
187
+ p.add_argument("input", help="输入文件路径(UTF-8 纯文本)")
188
+ p.add_argument(
189
+ "--compiler",
190
+ choices=["xelatex", "lualatex", "pdflatex"],
191
+ default=None,
192
+ help="指定 LaTeX 编译器;不指定则自动选择(优先 xelatex)",
193
+ )
194
+ p.add_argument(
195
+ "--dpi",
196
+ type=int,
197
+ default=150,
198
+ help="目标有效 DPI(每英寸显示长度的像素数),默认 150。"
199
+ "实际 savefig DPI 会根据每个 @plot 的显示宽度自动调整,"
200
+ "保持有效分辨率一致:大图高分、小图低分",
201
+ )
202
+ p.add_argument(
203
+ "--plot-width",
204
+ type=float,
205
+ default=0.7,
206
+ help="函数图在文档中的宽度(相对于页面宽度 \\paperwidth,0~1),默认 0.7",
207
+ )
208
+ p.add_argument(
209
+ "--keep-intermediates",
210
+ action="store_true",
211
+ default=True,
212
+ help="保留函数图 PNG 和 .log/.pdf/.aux 等中间文件(默认开启)",
213
+ )
214
+ p.add_argument(
215
+ "--no-keep-intermediates",
216
+ dest="keep_intermediates",
217
+ action="store_false",
218
+ help="不保留中间文件(仅保留最终 PNG 和 .tex)",
219
+ )
220
+ p.add_argument(
221
+ "--overwrite",
222
+ action="store_true",
223
+ default=False,
224
+ help="覆盖已有输出文件",
225
+ )
226
+ p.add_argument(
227
+ "--version",
228
+ action="version",
229
+ version=f"mathtext2doc {__version__}",
230
+ )
231
+ return p
232
+
233
+
234
+ def main(argv: List[str] | None = None) -> int:
235
+ parser = build_parser()
236
+ args = parser.parse_args(argv)
237
+ try:
238
+ return run(args.input, args)
239
+ except KeyboardInterrupt:
240
+ sys.stderr.write("已中断\n")
241
+ return 5
242
+ except Exception as ex:
243
+ sys.stderr.write(f"未预期错误:{ex}\n")
244
+ traceback.print_exc()
245
+ return 5
246
+
247
+
248
+ if __name__ == "__main__":
249
+ sys.exit(main())