batoolset-gui 0.2.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.
Files changed (74) hide show
  1. batoolset/GUI/__init__.py +0 -0
  2. batoolset/GUI/customdialog.py +136 -0
  3. batoolset/GUI/exception_handler.py +74 -0
  4. batoolset/GUI/list_gui.py +514 -0
  5. batoolset/GUI/multi_image_display.py +1038 -0
  6. batoolset/GUI/open.py +325 -0
  7. batoolset/GUI/overlay_hints.py +45 -0
  8. batoolset/GUI/paint2.py +1369 -0
  9. batoolset/GUI/scrollablepaint.py +1165 -0
  10. batoolset/dialogs/__init__.py +0 -0
  11. batoolset/dialogs/opensave.py +228 -0
  12. batoolset/drawings/__init__.py +0 -0
  13. batoolset/drawings/math_text.py +294 -0
  14. batoolset/drawings/pdf_svg_renderer.py +376 -0
  15. batoolset/drawings/shapes/Position.py +183 -0
  16. batoolset/drawings/shapes/__init__.py +0 -0
  17. batoolset/drawings/shapes/automated_alignment_drawing_refactored.py +346 -0
  18. batoolset/drawings/shapes/circle2d.py +99 -0
  19. batoolset/drawings/shapes/compute_optimal_size_all_merged.py +328 -0
  20. batoolset/drawings/shapes/ellipse2d.py +336 -0
  21. batoolset/drawings/shapes/freehand2d.py +235 -0
  22. batoolset/drawings/shapes/group.py +1985 -0
  23. batoolset/drawings/shapes/image2d.py +3157 -0
  24. batoolset/drawings/shapes/line2d.py +579 -0
  25. batoolset/drawings/shapes/point2d.py +257 -0
  26. batoolset/drawings/shapes/polygon2d.py +357 -0
  27. batoolset/drawings/shapes/polyline2d.py +235 -0
  28. batoolset/drawings/shapes/rect2d.py +388 -0
  29. batoolset/drawings/shapes/rectangle2d.py +601 -0
  30. batoolset/drawings/shapes/scalebar.py +310 -0
  31. batoolset/drawings/shapes/serializablefont.py +133 -0
  32. batoolset/drawings/shapes/serializableguiparams.py +14 -0
  33. batoolset/drawings/shapes/square2d.py +102 -0
  34. batoolset/drawings/shapes/test_publication_style.py +220 -0
  35. batoolset/drawings/shapes/textorientation.py +45 -0
  36. batoolset/drawings/shapes/tools.py +467 -0
  37. batoolset/drawings/shapes/transform2D.py +350 -0
  38. batoolset/drawings/shapes/txt2d.py +615 -0
  39. batoolset/drawings/svg_clip_renderer.py +247 -0
  40. batoolset/drawings/svg_embed.py +165 -0
  41. batoolset/drawings/svg_text_renderer.py +183 -0
  42. batoolset/drawings/widgets/__init__.py +0 -0
  43. batoolset/drawings/widgets/col.py +343 -0
  44. batoolset/drawings/widgets/image_group.py +248 -0
  45. batoolset/drawings/widgets/image_organizer.py +96 -0
  46. batoolset/drawings/widgets/paint.py +184 -0
  47. batoolset/drawings/widgets/vectorial.py +256 -0
  48. batoolset/figure/__init__.py +0 -0
  49. batoolset/figure/alignment.py +822 -0
  50. batoolset/figure/column.py +1117 -0
  51. batoolset/figure/fig_tools.py +285 -0
  52. batoolset/figure/minimal_debugger_scale_etc.py +139 -0
  53. batoolset/figure/panel.py +697 -0
  54. batoolset/figure/row.py +852 -0
  55. batoolset/figure/user_selection.py +287 -0
  56. batoolset/icons/__init__.py +0 -0
  57. batoolset/icons/tools.py +248 -0
  58. batoolset/luts/__init__.py +0 -0
  59. batoolset/luts/lut_minimal_test.py +1308 -0
  60. batoolset/luts/lutcreatorfromformula.py +209 -0
  61. batoolset/luts/luteditor.py +243 -0
  62. batoolset/pyqts/__init__.py +0 -0
  63. batoolset/pyqts/pleasewaitdialog.py +14 -0
  64. batoolset/pyqts/tools.py +760 -0
  65. batoolset/serializations/__init__.py +0 -0
  66. batoolset/serializations/tools.py +1338 -0
  67. batoolset/theme.py +647 -0
  68. batoolset/uis/__init__.py +0 -0
  69. batoolset/uis/blinker.py +56 -0
  70. batoolset_gui-0.2.0.dist-info/METADATA +54 -0
  71. batoolset_gui-0.2.0.dist-info/RECORD +74 -0
  72. batoolset_gui-0.2.0.dist-info/WHEEL +5 -0
  73. batoolset_gui-0.2.0.dist-info/licenses/LICENSE +24 -0
  74. batoolset_gui-0.2.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,228 @@
1
+ import os
2
+ from batoolset.settings.global_settings import set_UI # set the UI to qtpy
3
+ set_UI()
4
+ import sys
5
+ from qtpy.QtWidgets import QApplication, QFileDialog,QCheckBox, QComboBox
6
+ from os.path import expanduser
7
+
8
+ # parent_window = None, extensions = "Supported Files (*.jpg *.tif *.png);;All Files (*)",
9
+ # path = expanduser('~'),
10
+ # almost there just add the possibility to chose the extensions by default and the alike stuff!!
11
+ class SaveFileDialogWithOptions(QFileDialog):
12
+ def __init__(self, *args, **kwargs):
13
+ super().__init__(*args, **kwargs)
14
+ self.widgets = []
15
+
16
+ def add_widget(self, widget):
17
+ self.widgets.append(widget)
18
+
19
+ def get_widget_values(self):
20
+ return [widget.isChecked() for widget in self.widgets]
21
+
22
+ def getOpenFileName(self, parent=None,
23
+ caption="Select a File",
24
+ dir=expanduser('~'),
25
+ filter="All files (*.*)",
26
+ selectedFilter="",
27
+ options=None):
28
+
29
+ # configuration of the dialog window: make sure it's complete!
30
+ self.setWindowTitle(caption)
31
+ self.setDirectory(dir)
32
+ self.setNameFilter(filter)
33
+ self.setFileMode(QFileDialog.AnyFile)
34
+ self.setAcceptMode(QFileDialog.AcceptOpen) # "Open" button
35
+ if selectedFilter != "":
36
+ self.selectNameFilter(selectedFilter)
37
+ if options != None:
38
+ self.setOptions(options)
39
+
40
+ self.setOption(QFileDialog.DontUseNativeDialog, True)
41
+
42
+ layout = self.layout()
43
+
44
+ if self.widgets:
45
+ for iii, widget in enumerate(self.widgets):
46
+ layout.addWidget(widget, 4+iii, 0, 1, 4)
47
+
48
+ # user interaction and result return
49
+ if self.exec_():
50
+ widget_values = self.get_widget_values()
51
+ return widget_values, list(self.selectedFiles())[0]
52
+ else:
53
+ return ""
54
+
55
+
56
+ class OpenFileDialogWithOptions(QFileDialog):
57
+ def __init__(self, *args, **kwargs):
58
+ super().__init__(*args, **kwargs)
59
+ self.widgets = []
60
+
61
+ def add_widget(self, widget):
62
+ self.widgets.append(widget)
63
+
64
+ def get_widget_values(self):
65
+ return [widget.isChecked() for widget in self.widgets]
66
+
67
+ def getSaveFileName(self, parent=None,
68
+ caption="Save a File",
69
+ dir=expanduser('~'),
70
+ filter="All files (*.*)",
71
+ selectedFilter="",
72
+ options=None):
73
+
74
+ # configuration of the dialog window: make sure it's complete!
75
+ self.setWindowTitle(caption)
76
+ self.setDirectory(dir)
77
+ self.setNameFilter(filter)
78
+ self.setFileMode(QFileDialog.AnyFile)
79
+ self.setAcceptMode(QFileDialog.AcceptSave) # "Save" button
80
+ if selectedFilter != "":
81
+ self.selectNameFilter(selectedFilter)
82
+ if options != None:
83
+ self.setOptions(options)
84
+
85
+ self.setOption(QFileDialog.DontUseNativeDialog, True)
86
+
87
+ layout = self.layout()
88
+
89
+ if self.widgets:
90
+ for iii, widget in enumerate(self.widgets):
91
+ layout.addWidget(widget, 4+iii, 0, 1, 4)
92
+
93
+ # user interaction and result return
94
+ if self.exec_():
95
+ widget_values = self.get_widget_values()
96
+ return widget_values, self.selectedFiles()[0]
97
+ else:
98
+ return ""
99
+
100
+ def saveFileWithOptions():
101
+ fileDialog = SaveFileDialogWithOptions()
102
+
103
+ # Add checkboxes
104
+ fileDialog.add_widget(QCheckBox("consolidate"))
105
+ fileDialog.add_widget(QCheckBox("blabla_bli"))
106
+
107
+ return fileDialog.getOpenFileName()
108
+ # print("Widget values:", result[0])
109
+ # print("Selected file:", result[1])
110
+
111
+ def openFileNameDialog(parent_window=None, extensions="Supported Files (*.jpg *.tif *.png);;All Files (*)",
112
+ path=expanduser('~')):
113
+ """
114
+ Opens a file dialog to select a single file.
115
+
116
+ Args:
117
+ parent_window (object): Parent window object. Default is None.
118
+ extensions (str): File extension filters for the dialog. Default is "Supported Files (*.jpg *.tif *.png);;All Files (*)".
119
+ path (str): Initial path for the dialog. Default is user's home directory.
120
+
121
+ Returns:
122
+ str: Selected file name.
123
+
124
+ # Examples:
125
+ # >>> filename = openFileNameDialog()
126
+ """
127
+ if not os.path.exists(path):
128
+ path = expanduser('~')
129
+ fileName, _ = QFileDialog.getOpenFileName(parent_window, "Select a File", path, extensions,
130
+ options=QFileDialog.DontUseNativeDialog)
131
+ return fileName
132
+
133
+
134
+ def openFileNamesDialog(parent_window=None, extensions="Supported Files (*.jpg *.tif *.png);;All Files (*)",
135
+ path=expanduser('~')):
136
+ """
137
+ Opens a file dialog to select multiple files.
138
+
139
+ Args:
140
+ parent_window (object): Parent window object. Default is None.
141
+ extensions (str): File extension filters for the dialog. Default is "Supported Files (*.jpg *.tif *.png);;All Files (*)".
142
+ path (str): Initial path for the dialog. Default is user's home directory.
143
+
144
+ Returns:
145
+ list: List of selected file names.
146
+
147
+ # Examples:
148
+ # >>> files = openFileNamesDialog()
149
+ """
150
+ if not os.path.exists(path):
151
+ path = expanduser('~')
152
+ files, _ = QFileDialog.getOpenFileNames(parent_window, "Select Files", path, extensions,
153
+ options=QFileDialog.DontUseNativeDialog)
154
+ return files
155
+
156
+
157
+ def saveFileDialog(parent_window=None, path=expanduser('~'), extensions="All Files (*);;Text Files (*.txt)",
158
+ default_ext=None):
159
+ """
160
+ Opens a file dialog to save a file.
161
+
162
+ Args:
163
+ parent_window (object): Parent window object. Default is None.
164
+ path (str): Initial path for the dialog. Default is user's home directory.
165
+ extensions (str): File extension filters for the dialog. Default is "All Files (*);;Text Files (*.txt)".
166
+ default_ext (str): Default file extension. Default is None.
167
+
168
+ Returns:
169
+ str: Selected file name to save.
170
+
171
+ # Examples:
172
+ # >>> filename = saveFileDialog(default_ext='.tif')
173
+ """
174
+ try:
175
+ if not os.path.exists(path) and not os.path.exists(os.path.dirname(path)):
176
+ path = expanduser('~')
177
+ except:
178
+ path = expanduser('~')
179
+ fd = QFileDialog(parent_window, "Save a File", path, extensions, options=QFileDialog.DontUseNativeDialog)
180
+ if default_ext is not None:
181
+ fd.setDefaultSuffix(default_ext)
182
+ fd.setAcceptMode(QFileDialog.AcceptSave)
183
+ selected = fd.exec()
184
+ if selected:
185
+ fileName = fd.selectedFiles()[0]
186
+ return fileName
187
+ else:
188
+ return
189
+
190
+
191
+ def openDirectoryDialog(parent_window=None, path=expanduser('~')):
192
+ """
193
+ Opens a dialog to select a directory.
194
+
195
+ Args:
196
+ parent_window (object): Parent window object. Default is None.
197
+ path (str): Initial path for the dialog. Default is user's home directory.
198
+
199
+ Returns:
200
+ str: Selected directory path.
201
+
202
+ # Examples:
203
+ # >>> folder = openDirectoryDialog()
204
+ """
205
+ if not os.path.exists(path):
206
+ path = expanduser('~')
207
+ folderName = QFileDialog.getExistingDirectory(parent_window, "Select a Directory", path,
208
+ options=QFileDialog.DontUseNativeDialog)
209
+ if folderName is not None:
210
+ if not folderName.strip():
211
+ return None
212
+ if not folderName.endswith("/") and not folderName.endswith("\\"):
213
+ folderName += '/'
214
+ return folderName
215
+
216
+
217
+ if __name__ == '__main__':
218
+ app = QApplication(sys.argv)
219
+ # filename = saveFileDialog(default_ext='.tif')
220
+
221
+ filename = saveFileWithOptions() # I can add options to this stuff and the content of the options is saved
222
+
223
+ # TODO --> finalize that to make it really usable
224
+ # or pop a widow first for parameters then for saving or vice versa -−> maybe simpler for now
225
+ # try that on windows to see if that works
226
+
227
+ print(filename)
228
+ sys.exit(0)
File without changes
@@ -0,0 +1,294 @@
1
+ """LaTeX maths inside text labels: ``$…$`` typeset in place.
2
+
3
+ A label's QTextDocument is left untouched (it is what is saved and edited);
4
+ for drawing and sizing, ``typeset(doc)`` returns a copy in which every
5
+ ``$…$`` is replaced by an inline object drawn by ``MathObject`` as a vector
6
+ path from Matplotlib's mathtext, at the size and colour of the surrounding
7
+ text. ``\\$`` stays a literal dollar.
8
+
9
+ Engines (label_math_engine): with a LaTeX installation each formula is typeset
10
+ by LaTeX itself — full LaTeX: siunitx, bm, any amsmath construct — in the
11
+ label's own font when XeLaTeX is available (letters, digits and Greek from the
12
+ font, symbols from Latin Modern Math), else in Latin Modern (sans-serif maths
13
+ for sans-serif labels), and kept as an SVG in the
14
+ caches of batoolset.latex_tools (PyFigures puts the figure's own
15
+ ``equations/labels`` folder first, so the formulas travel with the figure and
16
+ render identically on computers without TeX). Otherwise Matplotlib's mathtext
17
+ is used, with the label's own font family for letters when Matplotlib knows it
18
+ ("custom" fontset), else STIX sans (or STIX for serif fonts). The LaTeX export
19
+ still hands the $…$ to LaTeX itself.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import functools
24
+ import re
25
+
26
+ from qtpy.QtCore import QPointF, QRectF, QSizeF, Qt
27
+ from qtpy.QtGui import (QColor, QFont, QFontInfo, QFontMetricsF, QPainterPath, QTextCharFormat,
28
+ QTextCursor, QTextDocument, QTextFormat)
29
+
30
+ try: # PyQt
31
+ from qtpy.QtGui import QPyTextObject as _TextObjectBase
32
+ except ImportError: # PySide
33
+ from qtpy.QtCore import QObject
34
+ from qtpy.QtGui import QTextObjectInterface
35
+
36
+ class _TextObjectBase(QObject, QTextObjectInterface):
37
+ pass
38
+
39
+ MATH = re.compile(r"(?<!\\)\$(?!\s)([^$\n]*?[^\s\\$])\$")
40
+ OBJECT_TYPE = int(QTextFormat.ObjectTypes.UserObject.value if hasattr(QTextFormat.ObjectTypes.UserObject, "value")
41
+ else QTextFormat.ObjectTypes.UserObject) + 41
42
+ PROP_TEX = int(QTextFormat.Property.UserProperty.value if hasattr(QTextFormat.Property.UserProperty, "value")
43
+ else QTextFormat.Property.UserProperty) + 41
44
+ _OBJECT_CHAR = ""
45
+
46
+
47
+ def has_math(text) -> bool:
48
+ return bool(text) and MATH.search(text) is not None
49
+
50
+
51
+ def _fontset_for(family, italic_ok=True):
52
+ """rcParams for mathtext matching *family* (see module docstring)."""
53
+ from matplotlib import font_manager
54
+ try:
55
+ font_manager.findfont(font_manager.FontProperties(family=family), fallback_to_default=False)
56
+ return {"mathtext.fontset": "custom", "mathtext.rm": family, "mathtext.it": f"{family}:italic",
57
+ "mathtext.bf": f"{family}:bold", "mathtext.sf": family}
58
+ except Exception:
59
+ serif = any(k in (family or "").lower() for k in ("times", "serif", "roman", "georgia", "garamond"))
60
+ return {"mathtext.fontset": "stix" if serif else "stixsans"}
61
+
62
+
63
+ @functools.lru_cache(maxsize=512)
64
+ def math_path(tex, size_px, family):
65
+ """(QPainterPath with the baseline at y=0, advance width, ascent, descent)
66
+ of the formula *tex* (without dollars) at *size_px* pixels per em."""
67
+ import matplotlib
68
+ from matplotlib.font_manager import FontProperties
69
+ from matplotlib.textpath import TextPath
70
+ with matplotlib.rc_context(_fontset_for(family)):
71
+ tp = TextPath((0, 0), f"${tex}$", size=size_px, prop=FontProperties(family=family, size=size_px))
72
+ verts = tp.vertices
73
+ codes = tp.codes
74
+ path = QPainterPath()
75
+ i = 0
76
+ n = len(verts)
77
+ while i < n:
78
+ code = codes[i] if codes is not None else (1 if i == 0 else 2)
79
+ x, y = float(verts[i][0]), -float(verts[i][1]) # y up -> y down
80
+ if code == 1:
81
+ path.moveTo(x, y)
82
+ i += 1
83
+ elif code == 2:
84
+ path.lineTo(x, y)
85
+ i += 1
86
+ elif code == 3 and i + 1 < n:
87
+ path.quadTo(QPointF(x, y), QPointF(float(verts[i + 1][0]), -float(verts[i + 1][1])))
88
+ i += 2
89
+ elif code == 4 and i + 2 < n:
90
+ path.cubicTo(QPointF(x, y), QPointF(float(verts[i + 1][0]), -float(verts[i + 1][1])),
91
+ QPointF(float(verts[i + 2][0]), -float(verts[i + 2][1])))
92
+ i += 3
93
+ else: # CLOSEPOLY / STOP
94
+ path.closeSubpath()
95
+ i += 1
96
+ box = path.boundingRect()
97
+ width = max(box.right(), 0.0) + size_px * 0.06
98
+ ascent = max(-box.top(), 0.0)
99
+ descent = max(box.bottom(), 0.0)
100
+ return path, width, ascent, descent
101
+
102
+
103
+ def _em_px(font: QFont) -> float:
104
+ if font.pixelSize() > 0:
105
+ return float(font.pixelSize())
106
+ info = QFontInfo(font)
107
+ if info.pixelSize() > 0 and font.pointSizeF() > 0:
108
+ # the same px/pt the text layout uses, without QFontInfo's rounding
109
+ return font.pointSizeF() * info.pixelSize() / max(info.pointSizeF(), 1e-6)
110
+ return float(info.pixelSize() or 12)
111
+
112
+
113
+ def label_math_engine():
114
+ """"auto" (default: LaTeX when installed, or when the formula was already
115
+ typeset by LaTeX and is cached; else mathtext), "latex" or "mathtext"
116
+ (environment variable PYFIGURES_LABEL_MATH)."""
117
+ import os
118
+ value = (os.environ.get("PYFIGURES_LABEL_MATH") or "auto").strip().lower()
119
+ return value if value in ("auto", "latex", "mathtext") else "auto"
120
+
121
+
122
+ _SERIF_HINTS = ("times", "serif", "roman", "georgia", "garamond", "palatino", "computer modern",
123
+ "cmu", "latin modern", "baskerville", "cambria", "book")
124
+
125
+
126
+ @functools.lru_cache(maxsize=256)
127
+ def _svg_renderer(path):
128
+ from qtpy.QtSvg import QSvgRenderer
129
+ return QSvgRenderer(path)
130
+
131
+
132
+ def _latex_math(tex, font, color):
133
+ """(svg path, width, height, depth) in points from the LaTeX installation
134
+ or its caches (batoolset.latex_tools), or None (-> mathtext)."""
135
+ engine = label_math_engine()
136
+ if engine == "mathtext":
137
+ return None
138
+ try:
139
+ from batoolset import latex_tools as LT
140
+ except ImportError:
141
+ return None
142
+ size_pt = font.pointSizeF() if font.pointSizeF() > 0 else _em_px(font) * 0.75
143
+ family = (font.family() or "").lower()
144
+ sans = "sans" in family or not any(h in family for h in _SERIF_HINTS)
145
+ # the label's own font for letters and digits (XeLaTeX), the family Qt drew with
146
+ from qtpy.QtGui import QFontInfo
147
+ font_files = _font_files(QFontInfo(font).family() or font.family())
148
+ return LT.inline_math_svg(tex, round(size_pt, 2), sans=sans, color=color.name(),
149
+ compile_missing=LT.latex_available(), font_files=font_files)
150
+
151
+
152
+ @functools.lru_cache(maxsize=64)
153
+ def _font_files(family):
154
+ try:
155
+ from batoolset.latex_tools import font_files_for
156
+ return font_files_for(family)
157
+ except Exception:
158
+ return None
159
+
160
+
161
+ class MathObject(_TextObjectBase):
162
+ """Draws the formula stored in an object character's format. Placed with
163
+ Qt's AlignMiddle (centred half an x-height above the baseline, i.e. on the
164
+ maths axis), its box sized so the formula's own baseline lands on the text
165
+ baseline. Typeset by LaTeX when available (label_math_engine), else by
166
+ Matplotlib's mathtext."""
167
+
168
+ def _geometry(self, fmt):
169
+ cf = _char_format(fmt)
170
+ font = cf.font()
171
+ tex = str(fmt.property(PROP_TEX))
172
+ brush = cf.foreground()
173
+ color = brush.color() if brush.style() != Qt.BrushStyle.NoBrush else QColor("black")
174
+ em = _em_px(font)
175
+ latex = _latex_math(tex, font, color)
176
+ if latex is not None:
177
+ svg, w_pt, h_pt, d_pt = latex
178
+ size_pt = font.pointSizeF() if font.pointSizeF() > 0 else em * 0.75
179
+ k = em / max(size_pt, 1e-6) # px per pt, as the text uses
180
+ drawing = ("svg", svg, w_pt * k, h_pt * k, d_pt * k)
181
+ width, ascent, descent = w_pt * k, h_pt * k, d_pt * k
182
+ else:
183
+ path, width, ascent, descent = math_path(tex, round(em, 3), font.family())
184
+ drawing = ("path", path, color)
185
+ half_x = QFontMetricsF(font).xHeight() / 2.0
186
+ # Qt (AlignMiddle): object ascent = (h + half_x) / 2, descent = (h - half_x) / 2
187
+ height = max(2 * ascent - half_x, 2 * descent + half_x, 1.0)
188
+ return drawing, width, height, (height + half_x) / 2.0
189
+
190
+ # Qt calls these two from C++: an exception escaping them aborts the
191
+ # process (PyQt), so a formula that cannot be typeset is left empty
192
+ def intrinsicSize(self, doc, pos, fmt):
193
+ try:
194
+ _, width, height, _ = self._geometry(fmt)
195
+ return QSizeF(width, height)
196
+ except Exception:
197
+ _log_failure(fmt)
198
+ return QSizeF(0, 0)
199
+
200
+ def drawObject(self, painter, rect, doc, pos, fmt):
201
+ try:
202
+ drawing, width, _, baseline = self._geometry(fmt)
203
+ painter.save()
204
+ try:
205
+ painter.translate(rect.left(), rect.top() + baseline)
206
+ if SOURCE_MARKERS:
207
+ _draw_marker(painter, fmt, width)
208
+ elif drawing[0] == "svg":
209
+ _, svg, w, h, d = drawing
210
+ # the SVG's baseline is y = 0: its box spans [-h, +d]
211
+ _svg_renderer(svg).render(painter, QRectF(0.0, -h, w, h + d))
212
+ else:
213
+ painter.fillPath(drawing[1], drawing[2])
214
+ finally:
215
+ painter.restore()
216
+ except Exception:
217
+ _log_failure(fmt)
218
+
219
+
220
+ # Editable exports (PowerPoint, Illustrator) set this while they record the
221
+ # figure: each formula is then drawn as a marker text run carrying its TeX
222
+ # source, so the export can rebuild it as a native equation instead of outlines.
223
+ SOURCE_MARKERS = False
224
+ MARKER = "PYFMATH:"
225
+
226
+
227
+ def _draw_marker(painter, fmt, width):
228
+ cf = _char_format(fmt)
229
+ tex = str(fmt.property(PROP_TEX))
230
+ painter.setFont(cf.font())
231
+ painter.drawText(QPointF(0.0, 0.0), MARKER + "%.3f:" % width + tex.encode("utf-8").hex())
232
+
233
+
234
+ def _char_format(fmt):
235
+ return fmt if isinstance(fmt, QTextCharFormat) else fmt.toCharFormat()
236
+
237
+
238
+ def _log_failure(fmt):
239
+ import traceback
240
+ from batoolset.tools.logger import TA_logger
241
+ TA_logger().error("cannot typeset $" + str(fmt.property(PROP_TEX)) + "$:\n" + traceback.format_exc())
242
+
243
+
244
+ _OWNER_CACHE = {} # id(owner) -> (weakref to owner, key, typeset document)
245
+
246
+
247
+ def typeset_for(owner, doc: QTextDocument) -> QTextDocument:
248
+ """typeset(doc), cached per *owner* (a label or text box) until *doc*
249
+ changes. The cache lives here, not on the owner, so it is never saved
250
+ with the figure nor copied with the shape."""
251
+ import weakref
252
+ key = (id(doc), doc.revision(), doc.defaultFont().toString())
253
+ entry = _OWNER_CACHE.get(id(owner))
254
+ if entry is not None and entry[0]() is owner and entry[1] == key:
255
+ return entry[2]
256
+ out = typeset(doc)
257
+ try:
258
+ ref = weakref.ref(owner, lambda _r, k=id(owner): _OWNER_CACHE.pop(k, None))
259
+ except TypeError:
260
+ return out
261
+ _OWNER_CACHE[id(owner)] = (ref, key, out)
262
+ return out
263
+
264
+
265
+ def typeset(doc: QTextDocument) -> QTextDocument:
266
+ """A copy of *doc* with every $…$ drawn as maths (see module docstring);
267
+ *doc* itself if it has none."""
268
+ if not has_math(doc.toPlainText()):
269
+ return doc
270
+ out = doc.clone()
271
+ handler = MathObject()
272
+ out.documentLayout().registerHandler(OBJECT_TYPE, handler)
273
+ out._pyf_math_handler = handler # keep it alive with the document
274
+ text = out.toPlainText()
275
+ for m in reversed(list(MATH.finditer(text))):
276
+ cursor = QTextCursor(out)
277
+ cursor.setPosition(m.start() + 1) # format of the formula's first char
278
+ base = cursor.charFormat()
279
+ cursor.setPosition(m.start())
280
+ cursor.setPosition(m.end(), QTextCursor.MoveMode.KeepAnchor)
281
+ fmt = QTextCharFormat(base)
282
+ fmt.setObjectType(OBJECT_TYPE)
283
+ fmt.setProperty(PROP_TEX, m.group(1))
284
+ fmt.setVerticalAlignment(QTextCharFormat.VerticalAlignment.AlignMiddle)
285
+ cursor.insertText(_OBJECT_CHAR, fmt)
286
+ # escaped dollars are shown as dollars
287
+ cursor = QTextCursor(out)
288
+ while True:
289
+ cursor = out.find("\\$", cursor)
290
+ if cursor.isNull():
291
+ break
292
+ cursor.insertText("$")
293
+ out.adjustSize()
294
+ return out