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.
- batoolset/GUI/__init__.py +0 -0
- batoolset/GUI/customdialog.py +136 -0
- batoolset/GUI/exception_handler.py +74 -0
- batoolset/GUI/list_gui.py +514 -0
- batoolset/GUI/multi_image_display.py +1038 -0
- batoolset/GUI/open.py +325 -0
- batoolset/GUI/overlay_hints.py +45 -0
- batoolset/GUI/paint2.py +1369 -0
- batoolset/GUI/scrollablepaint.py +1165 -0
- batoolset/dialogs/__init__.py +0 -0
- batoolset/dialogs/opensave.py +228 -0
- batoolset/drawings/__init__.py +0 -0
- batoolset/drawings/math_text.py +294 -0
- batoolset/drawings/pdf_svg_renderer.py +376 -0
- batoolset/drawings/shapes/Position.py +183 -0
- batoolset/drawings/shapes/__init__.py +0 -0
- batoolset/drawings/shapes/automated_alignment_drawing_refactored.py +346 -0
- batoolset/drawings/shapes/circle2d.py +99 -0
- batoolset/drawings/shapes/compute_optimal_size_all_merged.py +328 -0
- batoolset/drawings/shapes/ellipse2d.py +336 -0
- batoolset/drawings/shapes/freehand2d.py +235 -0
- batoolset/drawings/shapes/group.py +1985 -0
- batoolset/drawings/shapes/image2d.py +3157 -0
- batoolset/drawings/shapes/line2d.py +579 -0
- batoolset/drawings/shapes/point2d.py +257 -0
- batoolset/drawings/shapes/polygon2d.py +357 -0
- batoolset/drawings/shapes/polyline2d.py +235 -0
- batoolset/drawings/shapes/rect2d.py +388 -0
- batoolset/drawings/shapes/rectangle2d.py +601 -0
- batoolset/drawings/shapes/scalebar.py +310 -0
- batoolset/drawings/shapes/serializablefont.py +133 -0
- batoolset/drawings/shapes/serializableguiparams.py +14 -0
- batoolset/drawings/shapes/square2d.py +102 -0
- batoolset/drawings/shapes/test_publication_style.py +220 -0
- batoolset/drawings/shapes/textorientation.py +45 -0
- batoolset/drawings/shapes/tools.py +467 -0
- batoolset/drawings/shapes/transform2D.py +350 -0
- batoolset/drawings/shapes/txt2d.py +615 -0
- batoolset/drawings/svg_clip_renderer.py +247 -0
- batoolset/drawings/svg_embed.py +165 -0
- batoolset/drawings/svg_text_renderer.py +183 -0
- batoolset/drawings/widgets/__init__.py +0 -0
- batoolset/drawings/widgets/col.py +343 -0
- batoolset/drawings/widgets/image_group.py +248 -0
- batoolset/drawings/widgets/image_organizer.py +96 -0
- batoolset/drawings/widgets/paint.py +184 -0
- batoolset/drawings/widgets/vectorial.py +256 -0
- batoolset/figure/__init__.py +0 -0
- batoolset/figure/alignment.py +822 -0
- batoolset/figure/column.py +1117 -0
- batoolset/figure/fig_tools.py +285 -0
- batoolset/figure/minimal_debugger_scale_etc.py +139 -0
- batoolset/figure/panel.py +697 -0
- batoolset/figure/row.py +852 -0
- batoolset/figure/user_selection.py +287 -0
- batoolset/icons/__init__.py +0 -0
- batoolset/icons/tools.py +248 -0
- batoolset/luts/__init__.py +0 -0
- batoolset/luts/lut_minimal_test.py +1308 -0
- batoolset/luts/lutcreatorfromformula.py +209 -0
- batoolset/luts/luteditor.py +243 -0
- batoolset/pyqts/__init__.py +0 -0
- batoolset/pyqts/pleasewaitdialog.py +14 -0
- batoolset/pyqts/tools.py +760 -0
- batoolset/serializations/__init__.py +0 -0
- batoolset/serializations/tools.py +1338 -0
- batoolset/theme.py +647 -0
- batoolset/uis/__init__.py +0 -0
- batoolset/uis/blinker.py +56 -0
- batoolset_gui-0.2.0.dist-info/METADATA +54 -0
- batoolset_gui-0.2.0.dist-info/RECORD +74 -0
- batoolset_gui-0.2.0.dist-info/WHEEL +5 -0
- batoolset_gui-0.2.0.dist-info/licenses/LICENSE +24 -0
- 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
|