scitex 2.7.0__py3-none-any.whl → 2.7.3__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.
Files changed (297) hide show
  1. scitex/__init__.py +6 -2
  2. scitex/__version__.py +1 -1
  3. scitex/audio/README.md +52 -0
  4. scitex/audio/__init__.py +384 -0
  5. scitex/audio/__main__.py +129 -0
  6. scitex/audio/_tts.py +334 -0
  7. scitex/audio/engines/__init__.py +44 -0
  8. scitex/audio/engines/base.py +275 -0
  9. scitex/audio/engines/elevenlabs_engine.py +143 -0
  10. scitex/audio/engines/gtts_engine.py +162 -0
  11. scitex/audio/engines/pyttsx3_engine.py +131 -0
  12. scitex/audio/mcp_server.py +757 -0
  13. scitex/bridge/_helpers.py +1 -1
  14. scitex/bridge/_plt_vis.py +1 -1
  15. scitex/bridge/_stats_vis.py +1 -1
  16. scitex/dev/plt/__init__.py +272 -0
  17. scitex/dev/plt/plot_mpl_axhline.py +28 -0
  18. scitex/dev/plt/plot_mpl_axhspan.py +28 -0
  19. scitex/dev/plt/plot_mpl_axvline.py +28 -0
  20. scitex/dev/plt/plot_mpl_axvspan.py +28 -0
  21. scitex/dev/plt/plot_mpl_bar.py +29 -0
  22. scitex/dev/plt/plot_mpl_barh.py +29 -0
  23. scitex/dev/plt/plot_mpl_boxplot.py +28 -0
  24. scitex/dev/plt/plot_mpl_contour.py +31 -0
  25. scitex/dev/plt/plot_mpl_contourf.py +31 -0
  26. scitex/dev/plt/plot_mpl_errorbar.py +30 -0
  27. scitex/dev/plt/plot_mpl_eventplot.py +28 -0
  28. scitex/dev/plt/plot_mpl_fill.py +30 -0
  29. scitex/dev/plt/plot_mpl_fill_between.py +31 -0
  30. scitex/dev/plt/plot_mpl_hexbin.py +28 -0
  31. scitex/dev/plt/plot_mpl_hist.py +28 -0
  32. scitex/dev/plt/plot_mpl_hist2d.py +28 -0
  33. scitex/dev/plt/plot_mpl_imshow.py +29 -0
  34. scitex/dev/plt/plot_mpl_pcolormesh.py +31 -0
  35. scitex/dev/plt/plot_mpl_pie.py +29 -0
  36. scitex/dev/plt/plot_mpl_plot.py +29 -0
  37. scitex/dev/plt/plot_mpl_quiver.py +31 -0
  38. scitex/dev/plt/plot_mpl_scatter.py +28 -0
  39. scitex/dev/plt/plot_mpl_stackplot.py +31 -0
  40. scitex/dev/plt/plot_mpl_stem.py +29 -0
  41. scitex/dev/plt/plot_mpl_step.py +29 -0
  42. scitex/dev/plt/plot_mpl_violinplot.py +28 -0
  43. scitex/dev/plt/plot_sns_barplot.py +29 -0
  44. scitex/dev/plt/plot_sns_boxplot.py +29 -0
  45. scitex/dev/plt/plot_sns_heatmap.py +28 -0
  46. scitex/dev/plt/plot_sns_histplot.py +29 -0
  47. scitex/dev/plt/plot_sns_kdeplot.py +29 -0
  48. scitex/dev/plt/plot_sns_lineplot.py +31 -0
  49. scitex/dev/plt/plot_sns_scatterplot.py +29 -0
  50. scitex/dev/plt/plot_sns_stripplot.py +29 -0
  51. scitex/dev/plt/plot_sns_swarmplot.py +29 -0
  52. scitex/dev/plt/plot_sns_violinplot.py +29 -0
  53. scitex/dev/plt/plot_stx_bar.py +29 -0
  54. scitex/dev/plt/plot_stx_barh.py +29 -0
  55. scitex/dev/plt/plot_stx_box.py +28 -0
  56. scitex/dev/plt/plot_stx_boxplot.py +28 -0
  57. scitex/dev/plt/plot_stx_conf_mat.py +28 -0
  58. scitex/dev/plt/plot_stx_contour.py +31 -0
  59. scitex/dev/plt/plot_stx_ecdf.py +28 -0
  60. scitex/dev/plt/plot_stx_errorbar.py +30 -0
  61. scitex/dev/plt/plot_stx_fill_between.py +31 -0
  62. scitex/dev/plt/plot_stx_fillv.py +28 -0
  63. scitex/dev/plt/plot_stx_heatmap.py +28 -0
  64. scitex/dev/plt/plot_stx_image.py +28 -0
  65. scitex/dev/plt/plot_stx_imshow.py +28 -0
  66. scitex/dev/plt/plot_stx_joyplot.py +28 -0
  67. scitex/dev/plt/plot_stx_kde.py +28 -0
  68. scitex/dev/plt/plot_stx_line.py +28 -0
  69. scitex/dev/plt/plot_stx_mean_ci.py +28 -0
  70. scitex/dev/plt/plot_stx_mean_std.py +28 -0
  71. scitex/dev/plt/plot_stx_median_iqr.py +28 -0
  72. scitex/dev/plt/plot_stx_raster.py +28 -0
  73. scitex/dev/plt/plot_stx_rectangle.py +28 -0
  74. scitex/dev/plt/plot_stx_scatter.py +29 -0
  75. scitex/dev/plt/plot_stx_shaded_line.py +29 -0
  76. scitex/dev/plt/plot_stx_violin.py +28 -0
  77. scitex/dev/plt/plot_stx_violinplot.py +28 -0
  78. scitex/fig/__init__.py +352 -0
  79. scitex/{vis → fig}/backend/_parser.py +1 -1
  80. scitex/{vis → fig}/canvas.py +1 -1
  81. scitex/{vis → fig}/editor/_defaults.py +70 -5
  82. scitex/fig/editor/_edit.py +751 -0
  83. scitex/{vis → fig}/editor/_qt_editor.py +181 -1
  84. scitex/fig/editor/flask_editor/_bbox.py +1276 -0
  85. scitex/fig/editor/flask_editor/_core.py +624 -0
  86. scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
  87. scitex/fig/editor/flask_editor/_renderer.py +739 -0
  88. scitex/{vis → fig}/editor/flask_editor/templates/__init__.py +1 -1
  89. scitex/fig/editor/flask_editor/templates/_html.py +834 -0
  90. scitex/fig/editor/flask_editor/templates/_scripts.py +3136 -0
  91. scitex/{vis → fig}/editor/flask_editor/templates/_styles.py +625 -18
  92. scitex/{vis → fig}/io/__init__.py +13 -1
  93. scitex/fig/io/_bundle.py +973 -0
  94. scitex/{vis → fig}/io/_canvas.py +1 -1
  95. scitex/{vis → fig}/io/_data.py +1 -1
  96. scitex/{vis → fig}/io/_export.py +1 -1
  97. scitex/{vis → fig}/io/_load.py +1 -1
  98. scitex/{vis → fig}/io/_panel.py +1 -1
  99. scitex/{vis → fig}/io/_save.py +1 -1
  100. scitex/{vis → fig}/model/__init__.py +1 -1
  101. scitex/{vis → fig}/model/_annotations.py +1 -1
  102. scitex/{vis → fig}/model/_axes.py +1 -1
  103. scitex/{vis → fig}/model/_figure.py +1 -1
  104. scitex/{vis → fig}/model/_guides.py +1 -1
  105. scitex/{vis → fig}/model/_plot.py +1 -1
  106. scitex/{vis → fig}/model/_styles.py +1 -1
  107. scitex/{vis → fig}/utils/__init__.py +1 -1
  108. scitex/io/__init__.py +10 -26
  109. scitex/io/_bundle.py +434 -0
  110. scitex/io/_flush.py +5 -2
  111. scitex/io/_load.py +98 -0
  112. scitex/io/_load_modules/_H5Explorer.py +5 -2
  113. scitex/io/_load_modules/_canvas.py +2 -2
  114. scitex/io/_load_modules/_image.py +3 -4
  115. scitex/io/_load_modules/_txt.py +4 -2
  116. scitex/io/_metadata.py +34 -324
  117. scitex/io/_metadata_modules/__init__.py +46 -0
  118. scitex/io/_metadata_modules/_embed.py +70 -0
  119. scitex/io/_metadata_modules/_read.py +64 -0
  120. scitex/io/_metadata_modules/_utils.py +79 -0
  121. scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
  122. scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
  123. scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
  124. scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
  125. scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
  126. scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
  127. scitex/io/_metadata_modules/read_metadata_png.py +39 -0
  128. scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
  129. scitex/io/_qr_utils.py +5 -3
  130. scitex/io/_save.py +548 -30
  131. scitex/io/_save_modules/_canvas.py +3 -3
  132. scitex/io/_save_modules/_image.py +5 -9
  133. scitex/io/_save_modules/_tex.py +7 -4
  134. scitex/io/utils/h5_to_zarr.py +11 -9
  135. scitex/msword/__init__.py +255 -0
  136. scitex/msword/profiles.py +357 -0
  137. scitex/msword/reader.py +753 -0
  138. scitex/msword/utils.py +289 -0
  139. scitex/msword/writer.py +362 -0
  140. scitex/plt/__init__.py +5 -2
  141. scitex/plt/_subplots/_AxesWrapper.py +6 -6
  142. scitex/plt/_subplots/_AxisWrapper.py +15 -9
  143. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
  144. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
  145. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
  146. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
  147. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
  148. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
  149. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
  150. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
  151. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
  152. scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
  153. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
  154. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
  155. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
  156. scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
  157. scitex/plt/_subplots/_FigWrapper.py +6 -6
  158. scitex/plt/_subplots/_SubplotsWrapper.py +28 -18
  159. scitex/plt/_subplots/_export_as_csv.py +35 -5
  160. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +8 -0
  161. scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +10 -21
  162. scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +18 -7
  163. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +28 -12
  164. scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +10 -4
  165. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +13 -1
  166. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +12 -2
  167. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +10 -3
  168. scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -4
  169. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +18 -3
  170. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +44 -36
  171. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +14 -2
  172. scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +11 -5
  173. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
  174. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
  175. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +14 -3
  176. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
  177. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +14 -2
  178. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
  179. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +16 -6
  180. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +29 -19
  181. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
  182. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +22 -5
  183. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +18 -14
  184. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +18 -14
  185. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +18 -14
  186. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +10 -2
  187. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
  188. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +18 -9
  189. scitex/plt/ax/_plot/_stx_ecdf.py +4 -2
  190. scitex/plt/gallery/_generate.py +421 -14
  191. scitex/plt/io/__init__.py +53 -0
  192. scitex/plt/io/_bundle.py +490 -0
  193. scitex/plt/io/_layered_bundle.py +1343 -0
  194. scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
  195. scitex/plt/styles/__init__.py +14 -0
  196. scitex/plt/styles/presets.py +78 -0
  197. scitex/plt/utils/__init__.py +13 -1
  198. scitex/plt/utils/_collect_figure_metadata.py +10 -14
  199. scitex/plt/utils/_configure_mpl.py +6 -18
  200. scitex/plt/utils/_crop.py +32 -14
  201. scitex/plt/utils/_csv_column_naming.py +54 -0
  202. scitex/plt/utils/_figure_mm.py +116 -1
  203. scitex/plt/utils/_hitmap.py +1643 -0
  204. scitex/plt/utils/metadata/__init__.py +25 -0
  205. scitex/plt/utils/metadata/_core.py +9 -10
  206. scitex/plt/utils/metadata/_dimensions.py +6 -3
  207. scitex/plt/utils/metadata/_editable_export.py +405 -0
  208. scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
  209. scitex/schema/__init__.py +109 -16
  210. scitex/schema/_canvas.py +1 -1
  211. scitex/schema/_plot.py +1015 -0
  212. scitex/schema/_stats.py +2 -2
  213. scitex/stats/__init__.py +117 -0
  214. scitex/stats/io/__init__.py +29 -0
  215. scitex/stats/io/_bundle.py +156 -0
  216. scitex/tex/__init__.py +4 -0
  217. scitex/tex/_export.py +890 -0
  218. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/METADATA +11 -1
  219. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/RECORD +238 -170
  220. scitex/io/memo.md +0 -2827
  221. scitex/plt/REQUESTS.md +0 -191
  222. scitex/plt/_subplots/TODO.md +0 -53
  223. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -559
  224. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1609
  225. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -447
  226. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +0 -110
  227. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +0 -88
  228. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +0 -103
  229. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +0 -106
  230. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +0 -92
  231. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +0 -92
  232. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +0 -92
  233. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +0 -84
  234. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +0 -84
  235. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +0 -83
  236. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +0 -93
  237. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +0 -91
  238. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +0 -91
  239. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +0 -91
  240. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +0 -97
  241. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +0 -98
  242. scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +0 -84
  243. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +0 -101
  244. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +0 -96
  245. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +0 -95
  246. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +0 -95
  247. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +0 -93
  248. scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +0 -95
  249. scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +0 -95
  250. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +0 -83
  251. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +0 -92
  252. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +0 -121
  253. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +0 -84
  254. scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +0 -110
  255. scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +0 -92
  256. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +0 -95
  257. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +0 -96
  258. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +0 -95
  259. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +0 -95
  260. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +0 -92
  261. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +0 -84
  262. scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +0 -94
  263. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +0 -109
  264. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +0 -108
  265. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +0 -93
  266. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +0 -84
  267. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +0 -96
  268. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +0 -96
  269. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +0 -96
  270. scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +0 -99
  271. scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +0 -100
  272. scitex/vis/__init__.py +0 -177
  273. scitex/vis/editor/_edit.py +0 -390
  274. scitex/vis/editor/flask_editor/_bbox.py +0 -529
  275. scitex/vis/editor/flask_editor/_core.py +0 -168
  276. scitex/vis/editor/flask_editor/_renderer.py +0 -393
  277. scitex/vis/editor/flask_editor/templates/_html.py +0 -513
  278. scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
  279. /scitex/{vis → fig}/README.md +0 -0
  280. /scitex/{vis → fig}/backend/__init__.py +0 -0
  281. /scitex/{vis → fig}/backend/_export.py +0 -0
  282. /scitex/{vis → fig}/backend/_render.py +0 -0
  283. /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
  284. /scitex/{vis → fig}/editor/__init__.py +0 -0
  285. /scitex/{vis → fig}/editor/_dearpygui_editor.py +0 -0
  286. /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
  287. /scitex/{vis → fig}/editor/_mpl_editor.py +0 -0
  288. /scitex/{vis → fig}/editor/_tkinter_editor.py +0 -0
  289. /scitex/{vis → fig}/editor/flask_editor/__init__.py +0 -0
  290. /scitex/{vis → fig}/editor/flask_editor/_utils.py +0 -0
  291. /scitex/{vis → fig}/io/_directory.py +0 -0
  292. /scitex/{vis → fig}/model/_plot_types.py +0 -0
  293. /scitex/{vis → fig}/utils/_defaults.py +0 -0
  294. /scitex/{vis → fig}/utils/_validate.py +0 -0
  295. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/WHEEL +0 -0
  296. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/entry_points.txt +0 -0
  297. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/licenses/LICENSE +0 -0
scitex/tex/_export.py ADDED
@@ -0,0 +1,890 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: 2025-12-11 16:00:00
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/tex/_export.py
5
+
6
+ """
7
+ Export SciTeX writer documents to LaTeX format.
8
+
9
+ This module converts the intermediate document format (from scitex.msword
10
+ or scitex.writer) into LaTeX source files.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import re
17
+ import shutil
18
+ import subprocess
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Any, Dict, List, Optional, Tuple
22
+
23
+ # Journal-specific document class configurations
24
+ JOURNAL_PRESETS = {
25
+ "article": {
26
+ "document_class": "article",
27
+ "class_options": [],
28
+ "required_packages": [],
29
+ },
30
+ "ieee": {
31
+ "document_class": "IEEEtran",
32
+ "class_options": ["conference"],
33
+ "required_packages": ["cite", "amsmath", "algorithmic"],
34
+ },
35
+ "elsevier": {
36
+ "document_class": "elsarticle",
37
+ "class_options": ["preprint", "12pt"],
38
+ "required_packages": ["lineno", "hyperref"],
39
+ },
40
+ "springer": {
41
+ "document_class": "svjour3",
42
+ "class_options": ["smallextended"],
43
+ "required_packages": [],
44
+ },
45
+ "aps": {
46
+ "document_class": "revtex4-2",
47
+ "class_options": ["aps", "prl", "preprint"],
48
+ "required_packages": [],
49
+ },
50
+ "mdpi": {
51
+ "document_class": "article",
52
+ "class_options": [],
53
+ "required_packages": ["mdpi"],
54
+ },
55
+ "acm": {
56
+ "document_class": "acmart",
57
+ "class_options": ["sigconf"],
58
+ "required_packages": [],
59
+ },
60
+ }
61
+
62
+
63
+ def export_tex(
64
+ writer_doc: Dict[str, Any],
65
+ output_path: str | Path,
66
+ document_class: str = "article",
67
+ packages: Optional[List[str]] = None,
68
+ preamble: Optional[str] = None,
69
+ image_dir: Optional[str | Path] = None,
70
+ export_images: bool = True,
71
+ journal_preset: Optional[str] = None,
72
+ class_options: Optional[List[str]] = None,
73
+ use_bibtex: bool = False,
74
+ ) -> Path:
75
+ """
76
+ Export a SciTeX writer document to LaTeX format.
77
+
78
+ Parameters
79
+ ----------
80
+ writer_doc : dict
81
+ SciTeX writer document structure containing:
82
+ - blocks: List of document blocks (headings, paragraphs, captions, etc.)
83
+ - metadata: Document metadata (title, author, etc.)
84
+ - images: Image references with binary data
85
+ - references: Bibliography entries
86
+ output_path : str | Path
87
+ Output path for the .tex file.
88
+ document_class : str
89
+ LaTeX document class (article, report, book, etc.).
90
+ Overridden if journal_preset is specified.
91
+ packages : list[str] | None
92
+ Additional LaTeX packages to include.
93
+ preamble : str | None
94
+ Additional preamble content.
95
+ image_dir : str | Path | None
96
+ Directory to save extracted images. If None, uses
97
+ "{output_stem}_figures/" next to the output .tex file.
98
+ Set export_images=False to skip image export.
99
+ export_images : bool
100
+ Whether to export images to files. Default True.
101
+ journal_preset : str | None
102
+ Use a journal-specific preset: "ieee", "elsevier", "springer",
103
+ "aps", "mdpi", "acm". Sets document_class and required packages.
104
+ class_options : list[str] | None
105
+ Document class options (e.g., ["12pt", "twocolumn"]).
106
+ use_bibtex : bool
107
+ If True, generate \\bibliography{} instead of thebibliography.
108
+ Creates a .bib file alongside the .tex file.
109
+
110
+ Returns
111
+ -------
112
+ Path
113
+ The path to the written .tex file.
114
+
115
+ Examples
116
+ --------
117
+ >>> from scitex.msword import load_docx
118
+ >>> from scitex.tex import export_tex
119
+ >>> doc = load_docx("manuscript.docx")
120
+ >>> export_tex(doc, "manuscript.tex")
121
+ PosixPath('manuscript.tex')
122
+
123
+ >>> # Export for IEEE conference
124
+ >>> export_tex(doc, "manuscript.tex", journal_preset="ieee")
125
+
126
+ >>> # Export with custom image directory
127
+ >>> export_tex(doc, "manuscript.tex", image_dir="./figures")
128
+ """
129
+ output_path = Path(output_path)
130
+
131
+ # Apply journal preset if specified
132
+ effective_class = document_class
133
+ effective_options = class_options or []
134
+ extra_packages = []
135
+
136
+ if journal_preset and journal_preset in JOURNAL_PRESETS:
137
+ preset = JOURNAL_PRESETS[journal_preset]
138
+ effective_class = preset["document_class"]
139
+ effective_options = preset["class_options"] + (class_options or [])
140
+ extra_packages = preset["required_packages"]
141
+
142
+ # Extract components from writer_doc
143
+ blocks = writer_doc.get("blocks", [])
144
+ metadata = writer_doc.get("metadata", {})
145
+ references = writer_doc.get("references", [])
146
+ images = writer_doc.get("images", [])
147
+
148
+ # Handle image export
149
+ image_map: Dict[str, str] = {} # hash -> relative path
150
+ if export_images and images:
151
+ if image_dir is None:
152
+ image_dir = output_path.parent / f"{output_path.stem}_figures"
153
+ else:
154
+ image_dir = Path(image_dir)
155
+
156
+ image_dir.mkdir(parents=True, exist_ok=True)
157
+ image_map = _write_images_to_dir(images, image_dir, output_path.parent)
158
+
159
+ # Combine packages
160
+ all_packages = extra_packages + (packages or [])
161
+
162
+ # Build LaTeX content
163
+ latex_content = _build_latex_document(
164
+ blocks=blocks,
165
+ metadata=metadata,
166
+ references=references,
167
+ document_class=effective_class,
168
+ class_options=effective_options,
169
+ packages=all_packages if all_packages else None,
170
+ preamble=preamble,
171
+ image_map=image_map,
172
+ use_bibtex=use_bibtex,
173
+ output_stem=output_path.stem,
174
+ )
175
+
176
+ # Write to file
177
+ output_path.write_text(latex_content, encoding="utf-8")
178
+
179
+ # Generate .bib file if using bibtex
180
+ if use_bibtex and references:
181
+ bib_path = output_path.with_suffix(".bib")
182
+ bib_content = _generate_bibtex(references)
183
+ bib_path.write_text(bib_content, encoding="utf-8")
184
+
185
+ return output_path
186
+
187
+
188
+ def _generate_bibtex(references: List[Dict[str, Any]]) -> str:
189
+ """Generate BibTeX content from references."""
190
+ entries = []
191
+ for ref in references:
192
+ num = ref.get("number", len(entries) + 1)
193
+ text = ref.get("text", ref.get("raw", ""))
194
+
195
+ # Basic entry - in practice, would parse author/title/year
196
+ entry = f"""@misc{{ref{num},
197
+ note = {{{text}}}
198
+ }}"""
199
+ entries.append(entry)
200
+
201
+ return "\n\n".join(entries)
202
+
203
+
204
+ def _write_images_to_dir(
205
+ images: List[Dict[str, Any]],
206
+ image_dir: Path,
207
+ tex_parent: Path,
208
+ ) -> Dict[str, str]:
209
+ """
210
+ Write images to directory and return hash->relative_path mapping.
211
+
212
+ Parameters
213
+ ----------
214
+ images : list
215
+ List of image dicts with 'hash', 'extension', 'data' keys.
216
+ image_dir : Path
217
+ Directory to write images to.
218
+ tex_parent : Path
219
+ Parent directory of the .tex file (for relative paths).
220
+
221
+ Returns
222
+ -------
223
+ dict
224
+ Mapping from image hash to relative path for LaTeX.
225
+ """
226
+ image_map = {}
227
+ fig_counter = 0
228
+
229
+ for img in images:
230
+ img_hash = img.get("hash")
231
+ ext = img.get("extension", ".png")
232
+ data = img.get("data")
233
+
234
+ if data is None or img_hash is None:
235
+ continue
236
+
237
+ # Skip duplicates (same hash = same image content)
238
+ if img_hash in image_map:
239
+ continue
240
+
241
+ fig_counter += 1
242
+ filename = f"fig_{fig_counter}{ext}"
243
+ filepath = image_dir / filename
244
+
245
+ # Write image data
246
+ filepath.write_bytes(data)
247
+
248
+ # Store relative path from tex file location
249
+ try:
250
+ rel_path = filepath.relative_to(tex_parent)
251
+ except ValueError:
252
+ rel_path = filepath
253
+
254
+ image_map[img_hash] = str(rel_path)
255
+
256
+ return image_map
257
+
258
+
259
+ def _build_latex_document(
260
+ blocks: List[Dict[str, Any]],
261
+ metadata: Dict[str, Any],
262
+ references: List[Dict[str, Any]],
263
+ document_class: str,
264
+ class_options: Optional[List[str]] = None,
265
+ packages: Optional[List[str]] = None,
266
+ preamble: Optional[str] = None,
267
+ image_map: Optional[Dict[str, str]] = None,
268
+ use_bibtex: bool = False,
269
+ output_stem: str = "document",
270
+ ) -> str:
271
+ """Build complete LaTeX document content."""
272
+ if image_map is None:
273
+ image_map = {}
274
+ lines = []
275
+
276
+ # Document class with options
277
+ if class_options:
278
+ opts = ",".join(class_options)
279
+ lines.append(f"\\documentclass[{opts}]{{{document_class}}}")
280
+ else:
281
+ lines.append(f"\\documentclass{{{document_class}}}")
282
+ lines.append("")
283
+
284
+ # Default packages
285
+ default_packages = [
286
+ "inputenc",
287
+ "fontenc",
288
+ "amsmath",
289
+ "amssymb",
290
+ "graphicx",
291
+ "hyperref",
292
+ ]
293
+
294
+ # Package options
295
+ package_options = {
296
+ "inputenc": "utf8",
297
+ "fontenc": "T1",
298
+ }
299
+
300
+ for pkg in default_packages:
301
+ opt = package_options.get(pkg)
302
+ if opt:
303
+ lines.append(f"\\usepackage[{opt}]{{{pkg}}}")
304
+ else:
305
+ lines.append(f"\\usepackage{{{pkg}}}")
306
+
307
+ # Additional packages
308
+ if packages:
309
+ for pkg in packages:
310
+ if pkg not in default_packages:
311
+ lines.append(f"\\usepackage{{{pkg}}}")
312
+
313
+ lines.append("")
314
+
315
+ # Metadata
316
+ if metadata.get("title"):
317
+ title = _escape_latex(metadata["title"])
318
+ lines.append(f"\\title{{{title}}}")
319
+ if metadata.get("author"):
320
+ author = _escape_latex(metadata["author"])
321
+ lines.append(f"\\author{{{author}}}")
322
+
323
+ lines.append("")
324
+
325
+ # Additional preamble
326
+ if preamble:
327
+ lines.append(preamble)
328
+ lines.append("")
329
+
330
+ # Begin document
331
+ lines.append("\\begin{document}")
332
+ lines.append("")
333
+
334
+ # Title
335
+ if metadata.get("title"):
336
+ lines.append("\\maketitle")
337
+ lines.append("")
338
+
339
+ # Track list state for proper itemize/enumerate environments
340
+ in_list = False
341
+ list_type = None
342
+
343
+ # Process blocks
344
+ for i, block in enumerate(blocks):
345
+ btype = block.get("type")
346
+
347
+ # Handle list transitions
348
+ if btype == "list-item":
349
+ item_list_type = block.get("list_type", "unordered")
350
+ if not in_list:
351
+ env = "enumerate" if item_list_type == "ordered" else "itemize"
352
+ lines.append(f"\\begin{{{env}}}")
353
+ in_list = True
354
+ list_type = item_list_type
355
+ elif in_list:
356
+ # Close list environment
357
+ env = "enumerate" if list_type == "ordered" else "itemize"
358
+ lines.append(f"\\end{{{env}}}")
359
+ lines.append("")
360
+ in_list = False
361
+ list_type = None
362
+
363
+ block_latex = _convert_block_to_latex(block, image_map)
364
+ if block_latex:
365
+ lines.append(block_latex)
366
+
367
+ # Close any open list
368
+ if in_list:
369
+ env = "enumerate" if list_type == "ordered" else "itemize"
370
+ lines.append(f"\\end{{{env}}}")
371
+ lines.append("")
372
+
373
+ # References section
374
+ if references:
375
+ lines.append("")
376
+ if use_bibtex:
377
+ lines.append(f"\\bibliographystyle{{plain}}")
378
+ lines.append(f"\\bibliography{{{output_stem}}}")
379
+ else:
380
+ lines.append("\\begin{thebibliography}{99}")
381
+ for ref in references:
382
+ ref_latex = _convert_reference_to_latex(ref)
383
+ if ref_latex:
384
+ lines.append(ref_latex)
385
+ lines.append("\\end{thebibliography}")
386
+
387
+ # End document
388
+ lines.append("")
389
+ lines.append("\\end{document}")
390
+
391
+ return "\n".join(lines)
392
+
393
+
394
+ def _convert_block_to_latex(
395
+ block: Dict[str, Any],
396
+ image_map: Optional[Dict[str, str]] = None,
397
+ ) -> Optional[str]:
398
+ """Convert a single block to LaTeX."""
399
+ if image_map is None:
400
+ image_map = {}
401
+
402
+ btype = block.get("type", "paragraph")
403
+ text = block.get("text", "")
404
+
405
+ if not text and btype not in ("table", "image", "caption", "equation"):
406
+ return None
407
+
408
+ if btype == "heading":
409
+ return _convert_heading(block)
410
+ elif btype == "paragraph":
411
+ return _convert_paragraph(block)
412
+ elif btype == "caption":
413
+ return _convert_caption(block, image_map)
414
+ elif btype == "table":
415
+ return _convert_table(block)
416
+ elif btype == "image":
417
+ return _convert_image(block, image_map)
418
+ elif btype == "list-item":
419
+ return _convert_list_item(block)
420
+ elif btype == "equation":
421
+ return _convert_equation(block)
422
+ elif btype == "reference-paragraph":
423
+ # Skip - handled separately in references section
424
+ return None
425
+ else:
426
+ # Default: treat as paragraph
427
+ return _escape_latex(text) + "\n"
428
+
429
+
430
+ def _convert_equation(block: Dict[str, Any]) -> str:
431
+ """Convert an equation block to LaTeX."""
432
+ latex = block.get("latex", "")
433
+ text = block.get("text", "")
434
+
435
+ if latex:
436
+ # Use the converted LaTeX from OMML
437
+ return f"\\begin{{equation}}\n{latex}\n\\end{{equation}}\n"
438
+ elif text:
439
+ # Fallback: wrap text in equation environment
440
+ return f"\\begin{{equation}}\n{_escape_latex(text)}\n\\end{{equation}}\n"
441
+ return ""
442
+
443
+
444
+ def _convert_heading(block: Dict[str, Any]) -> str:
445
+ """Convert a heading block to LaTeX."""
446
+ level = block.get("level", 1)
447
+ text = _escape_latex(block.get("text", ""))
448
+
449
+ # Map heading levels to LaTeX commands
450
+ level_commands = {
451
+ 1: "section",
452
+ 2: "subsection",
453
+ 3: "subsubsection",
454
+ 4: "paragraph",
455
+ 5: "subparagraph",
456
+ }
457
+
458
+ command = level_commands.get(level, "paragraph")
459
+ return f"\\{command}{{{text}}}\n"
460
+
461
+
462
+ def _convert_paragraph(block: Dict[str, Any]) -> str:
463
+ """Convert a paragraph block to LaTeX."""
464
+ runs = block.get("runs", [])
465
+
466
+ if runs:
467
+ # Build paragraph from formatted runs
468
+ parts = []
469
+ for run in runs:
470
+ run_text = _escape_latex(run.get("text", ""))
471
+ if run.get("bold"):
472
+ run_text = f"\\textbf{{{run_text}}}"
473
+ if run.get("italic"):
474
+ run_text = f"\\textit{{{run_text}}}"
475
+ if run.get("underline"):
476
+ run_text = f"\\underline{{{run_text}}}"
477
+ parts.append(run_text)
478
+ return "".join(parts) + "\n"
479
+ else:
480
+ return _escape_latex(block.get("text", "")) + "\n"
481
+
482
+
483
+ def _convert_caption(
484
+ block: Dict[str, Any],
485
+ image_map: Optional[Dict[str, str]] = None,
486
+ ) -> str:
487
+ """Convert a caption block to LaTeX figure/table environment."""
488
+ if image_map is None:
489
+ image_map = {}
490
+
491
+ caption_type = block.get("caption_type", "")
492
+ number = block.get("number", "")
493
+ caption_text = _escape_latex(block.get("caption_text", block.get("text", "")))
494
+ image_hash = block.get("image_hash")
495
+
496
+ if caption_type == "figure":
497
+ # Check if we have an associated image
498
+ image_path = None
499
+ if image_hash and image_hash in image_map:
500
+ image_path = image_map[image_hash]
501
+
502
+ lines = [
503
+ "\\begin{figure}[htbp]",
504
+ "\\centering",
505
+ ]
506
+
507
+ if image_path:
508
+ # Remove extension for includegraphics
509
+ image_path_no_ext = image_path.rsplit(".", 1)[0] if "." in image_path else image_path
510
+ lines.append(f"\\includegraphics[width=0.8\\textwidth]{{{image_path_no_ext}}}")
511
+ else:
512
+ lines.append(f"% Image placeholder for Figure {number}")
513
+
514
+ lines.extend([
515
+ f"\\caption{{{caption_text}}}",
516
+ f"\\label{{fig:{number}}}",
517
+ "\\end{figure}",
518
+ "",
519
+ ])
520
+ return "\n".join(lines)
521
+
522
+ elif caption_type == "table":
523
+ # Table captions - typically above the table
524
+ return f"% Table {number}: {caption_text}\n"
525
+
526
+ else:
527
+ return f"% Caption: {caption_text}\n"
528
+
529
+
530
+ def _convert_image(
531
+ block: Dict[str, Any],
532
+ image_map: Optional[Dict[str, str]] = None,
533
+ ) -> str:
534
+ """Convert an image block to LaTeX includegraphics."""
535
+ if image_map is None:
536
+ image_map = {}
537
+
538
+ image_hash = block.get("image_hash") or block.get("hash")
539
+ width = block.get("width", "0.8\\textwidth")
540
+
541
+ if image_hash and image_hash in image_map:
542
+ image_path = image_map[image_hash]
543
+ # Remove extension for includegraphics
544
+ image_path_no_ext = image_path.rsplit(".", 1)[0] if "." in image_path else image_path
545
+
546
+ lines = [
547
+ "\\begin{figure}[htbp]",
548
+ "\\centering",
549
+ f"\\includegraphics[width={width}]{{{image_path_no_ext}}}",
550
+ "\\end{figure}",
551
+ "",
552
+ ]
553
+ return "\n".join(lines)
554
+
555
+ return "% Image placeholder\n"
556
+
557
+
558
+ def _convert_table(block: Dict[str, Any]) -> str:
559
+ """Convert a table block to LaTeX."""
560
+ rows = block.get("rows", [])
561
+ if not rows:
562
+ return ""
563
+
564
+ num_cols = len(rows[0]) if rows else 0
565
+ col_spec = "|" + "c|" * num_cols
566
+
567
+ lines = [
568
+ "\\begin{table}[htbp]",
569
+ "\\centering",
570
+ f"\\begin{{tabular}}{{{col_spec}}}",
571
+ "\\hline",
572
+ ]
573
+
574
+ for i, row in enumerate(rows):
575
+ escaped_cells = [_escape_latex(str(cell)) for cell in row]
576
+ lines.append(" & ".join(escaped_cells) + " \\\\")
577
+ lines.append("\\hline")
578
+
579
+ lines.extend([
580
+ "\\end{tabular}",
581
+ "\\end{table}",
582
+ "",
583
+ ])
584
+
585
+ return "\n".join(lines)
586
+
587
+
588
+ def _convert_list_item(block: Dict[str, Any]) -> str:
589
+ """Convert a list item to LaTeX."""
590
+ text = _escape_latex(block.get("text", ""))
591
+ return f"\\item {text}\n"
592
+
593
+
594
+ def _convert_reference_to_latex(ref: Dict[str, Any]) -> str:
595
+ """Convert a reference entry to LaTeX bibitem."""
596
+ number = ref.get("number")
597
+ text = _escape_latex(ref.get("text", ref.get("raw", "")))
598
+
599
+ if number:
600
+ return f"\\bibitem{{ref{number}}} {text}"
601
+ else:
602
+ return f"\\bibitem{{}} {text}"
603
+
604
+
605
+ def _escape_latex(text: str) -> str:
606
+ """Escape special LaTeX characters."""
607
+ if not text:
608
+ return ""
609
+
610
+ # Characters that need escaping in LaTeX
611
+ replacements = [
612
+ ("\\", "\\textbackslash{}"),
613
+ ("&", "\\&"),
614
+ ("%", "\\%"),
615
+ ("$", "\\$"),
616
+ ("#", "\\#"),
617
+ ("_", "\\_"),
618
+ ("{", "\\{"),
619
+ ("}", "\\}"),
620
+ ("~", "\\textasciitilde{}"),
621
+ ("^", "\\textasciicircum{}"),
622
+ ]
623
+
624
+ # Apply replacements (order matters - backslash first)
625
+ result = text
626
+ for old, new in replacements:
627
+ # Skip if already escaped
628
+ if old == "\\":
629
+ # Don't escape existing LaTeX commands
630
+ result = re.sub(r'(?<!\\)\\(?![a-zA-Z{])', new, result)
631
+ else:
632
+ result = result.replace(old, new)
633
+
634
+ return result
635
+
636
+
637
+ @dataclass
638
+ class CompileResult:
639
+ """Result of LaTeX compilation.
640
+
641
+ Attributes
642
+ ----------
643
+ success : bool
644
+ Whether compilation succeeded.
645
+ pdf_path : Path | None
646
+ Path to generated PDF, or None if failed.
647
+ exit_code : int
648
+ Process exit code.
649
+ stdout : str
650
+ Standard output from compiler.
651
+ stderr : str
652
+ Standard error from compiler.
653
+ log_content : str
654
+ Content of .log file if available.
655
+ errors : list[str]
656
+ Extracted error messages.
657
+ warnings : list[str]
658
+ Extracted warning messages.
659
+ """
660
+ success: bool
661
+ pdf_path: Optional[Path]
662
+ exit_code: int
663
+ stdout: str
664
+ stderr: str
665
+ log_content: str = ""
666
+ errors: List[str] = None
667
+ warnings: List[str] = None
668
+
669
+ def __post_init__(self):
670
+ if self.errors is None:
671
+ self.errors = []
672
+ if self.warnings is None:
673
+ self.warnings = []
674
+
675
+
676
+ def compile_tex(
677
+ tex_path: str | Path,
678
+ output_dir: Optional[str | Path] = None,
679
+ compiler: str = "pdflatex",
680
+ runs: int = 2,
681
+ clean: bool = True,
682
+ timeout: int = 120,
683
+ ) -> CompileResult:
684
+ """
685
+ Compile a LaTeX file to PDF.
686
+
687
+ Parameters
688
+ ----------
689
+ tex_path : str | Path
690
+ Path to the .tex file.
691
+ output_dir : str | Path | None
692
+ Output directory for PDF. If None, uses same directory as tex file.
693
+ compiler : str
694
+ LaTeX compiler to use: "pdflatex", "xelatex", "lualatex", or "latexmk".
695
+ Default is "pdflatex".
696
+ runs : int
697
+ Number of compilation passes (for references/ToC). Default is 2.
698
+ Ignored if compiler is "latexmk".
699
+ clean : bool
700
+ Remove auxiliary files (.aux, .log, .out, etc.) after compilation.
701
+ Default is True.
702
+ timeout : int
703
+ Timeout in seconds for each compilation pass. Default is 120.
704
+
705
+ Returns
706
+ -------
707
+ CompileResult
708
+ Compilation result with success status, PDF path, and logs.
709
+
710
+ Examples
711
+ --------
712
+ >>> from scitex.tex import compile_tex
713
+ >>> result = compile_tex("manuscript.tex")
714
+ >>> if result.success:
715
+ ... print(f"PDF created: {result.pdf_path}")
716
+ ... else:
717
+ ... print(f"Errors: {result.errors}")
718
+
719
+ >>> # Use latexmk for automatic multi-pass compilation
720
+ >>> result = compile_tex("manuscript.tex", compiler="latexmk")
721
+
722
+ Notes
723
+ -----
724
+ Requires LaTeX to be installed on the system (texlive, miktex, etc.).
725
+ """
726
+ tex_path = Path(tex_path).absolute()
727
+
728
+ if not tex_path.exists():
729
+ return CompileResult(
730
+ success=False,
731
+ pdf_path=None,
732
+ exit_code=1,
733
+ stdout="",
734
+ stderr=f"File not found: {tex_path}",
735
+ errors=[f"File not found: {tex_path}"],
736
+ )
737
+
738
+ # Determine output directory
739
+ if output_dir is None:
740
+ output_dir = tex_path.parent
741
+ else:
742
+ output_dir = Path(output_dir).absolute()
743
+ output_dir.mkdir(parents=True, exist_ok=True)
744
+
745
+ # Check if compiler is available
746
+ compiler_cmd = shutil.which(compiler)
747
+ if compiler_cmd is None:
748
+ return CompileResult(
749
+ success=False,
750
+ pdf_path=None,
751
+ exit_code=127,
752
+ stdout="",
753
+ stderr=f"Compiler not found: {compiler}",
754
+ errors=[f"Compiler not found: {compiler}. Install texlive or miktex."],
755
+ )
756
+
757
+ # Build command
758
+ if compiler == "latexmk":
759
+ cmd = [
760
+ compiler,
761
+ "-pdf",
762
+ "-interaction=nonstopmode",
763
+ f"-output-directory={output_dir}",
764
+ str(tex_path),
765
+ ]
766
+ runs = 1 # latexmk handles multi-pass
767
+ else:
768
+ cmd = [
769
+ compiler,
770
+ "-interaction=nonstopmode",
771
+ "-halt-on-error",
772
+ f"-output-directory={output_dir}",
773
+ str(tex_path),
774
+ ]
775
+
776
+ # Run compilation
777
+ stdout_all = []
778
+ stderr_all = []
779
+ exit_code = 0
780
+
781
+ for run_num in range(runs):
782
+ try:
783
+ result = subprocess.run(
784
+ cmd,
785
+ cwd=tex_path.parent,
786
+ capture_output=True,
787
+ text=True,
788
+ timeout=timeout,
789
+ )
790
+ stdout_all.append(f"=== Pass {run_num + 1} ===\n{result.stdout}")
791
+ stderr_all.append(result.stderr)
792
+ exit_code = result.returncode
793
+
794
+ # If compilation failed, don't continue
795
+ if exit_code != 0:
796
+ break
797
+
798
+ except subprocess.TimeoutExpired:
799
+ return CompileResult(
800
+ success=False,
801
+ pdf_path=None,
802
+ exit_code=124,
803
+ stdout="\n".join(stdout_all),
804
+ stderr=f"Compilation timed out after {timeout} seconds",
805
+ errors=[f"Compilation timed out after {timeout} seconds"],
806
+ )
807
+ except Exception as e:
808
+ return CompileResult(
809
+ success=False,
810
+ pdf_path=None,
811
+ exit_code=1,
812
+ stdout="\n".join(stdout_all),
813
+ stderr=str(e),
814
+ errors=[str(e)],
815
+ )
816
+
817
+ # Check for output PDF
818
+ pdf_name = tex_path.stem + ".pdf"
819
+ pdf_path = output_dir / pdf_name
820
+
821
+ # Read log file for detailed errors/warnings
822
+ log_path = output_dir / (tex_path.stem + ".log")
823
+ log_content = ""
824
+ errors = []
825
+ warnings = []
826
+
827
+ if log_path.exists():
828
+ try:
829
+ log_content = log_path.read_text(encoding="utf-8", errors="replace")
830
+ errors, warnings = _parse_latex_log(log_content)
831
+ except Exception:
832
+ pass
833
+
834
+ # Clean auxiliary files
835
+ if clean:
836
+ aux_extensions = [".aux", ".log", ".out", ".toc", ".lof", ".lot",
837
+ ".bbl", ".blg", ".fls", ".fdb_latexmk", ".synctex.gz"]
838
+ for ext in aux_extensions:
839
+ aux_file = output_dir / (tex_path.stem + ext)
840
+ if aux_file.exists():
841
+ try:
842
+ aux_file.unlink()
843
+ except Exception:
844
+ pass
845
+
846
+ success = exit_code == 0 and pdf_path.exists()
847
+
848
+ return CompileResult(
849
+ success=success,
850
+ pdf_path=pdf_path if pdf_path.exists() else None,
851
+ exit_code=exit_code,
852
+ stdout="\n".join(stdout_all),
853
+ stderr="\n".join(stderr_all),
854
+ log_content=log_content,
855
+ errors=errors,
856
+ warnings=warnings,
857
+ )
858
+
859
+
860
+ def _parse_latex_log(log_content: str) -> Tuple[List[str], List[str]]:
861
+ """Parse LaTeX log file for errors and warnings."""
862
+ errors = []
863
+ warnings = []
864
+
865
+ lines = log_content.split("\n")
866
+
867
+ for i, line in enumerate(lines):
868
+ # Error patterns
869
+ if line.startswith("!"):
870
+ # Collect multi-line error message
871
+ error_lines = [line]
872
+ for j in range(i + 1, min(i + 5, len(lines))):
873
+ if lines[j].startswith("l.") or lines[j].strip() == "":
874
+ break
875
+ error_lines.append(lines[j])
876
+ errors.append(" ".join(error_lines))
877
+
878
+ elif "Error:" in line or "Fatal error" in line:
879
+ errors.append(line.strip())
880
+
881
+ # Warning patterns
882
+ elif "Warning:" in line:
883
+ warnings.append(line.strip())
884
+ elif "Underfull" in line or "Overfull" in line:
885
+ warnings.append(line.strip())
886
+
887
+ return errors, warnings
888
+
889
+
890
+ __all__ = ["export_tex", "compile_tex", "CompileResult"]