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
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Draws the SVG that PyMuPDF writes for a PDF page, clipping included.
|
|
2
|
+
|
|
3
|
+
A PDF placed as a figure panel is converted to SVG (PyMuPDF
|
|
4
|
+
``page.get_svg_image``) and drawn as vectors. Qt 6.4's SVG renderer ignores
|
|
5
|
+
``clipPath``, so a plot curve leaving its axes would be drawn over the whole
|
|
6
|
+
figure, and Qt 6.4's SVG *writer* drops painter clipping as well.
|
|
7
|
+
|
|
8
|
+
``PdfSvgRenderer`` is a QSvgRenderer (Image2D and the exports only need
|
|
9
|
+
``render``, ``defaultSize`` and the aspect-ratio mode) whose ``render`` walks
|
|
10
|
+
the small SVG subset PyMuPDF produces itself: groups, paths, glyph ``use``
|
|
11
|
+
references, images and clip paths, with matrix transforms. Clipping is done by
|
|
12
|
+
the painter for PDF and raster output, and geometrically (path intersection,
|
|
13
|
+
strokes turned into outlines) when the painter writes SVG. Anything outside
|
|
14
|
+
that subset (gradients, masks, patterns...) falls back to QSvgRenderer.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import base64
|
|
19
|
+
import re
|
|
20
|
+
import xml.etree.ElementTree as ET
|
|
21
|
+
|
|
22
|
+
from qtpy.QtCore import QByteArray, QPointF, QRectF, Qt
|
|
23
|
+
from qtpy.QtGui import (QBrush, QColor, QImage, QPaintEngine, QPainter, QPainterPath,
|
|
24
|
+
QPainterPathStroker, QPen, QTransform)
|
|
25
|
+
from qtpy.QtSvg import QSvgRenderer
|
|
26
|
+
|
|
27
|
+
_SVG = '{http://www.w3.org/2000/svg}'
|
|
28
|
+
_XLINK = '{http://www.w3.org/1999/xlink}href'
|
|
29
|
+
_SUPPORTED = {'svg', 'defs', 'g', 'path', 'use', 'clipPath', 'image', 'title', 'desc', 'metadata'}
|
|
30
|
+
_INHERITED = ('fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-width', 'stroke-opacity',
|
|
31
|
+
'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-dasharray')
|
|
32
|
+
_NUM = re.compile(r'[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?')
|
|
33
|
+
_CMD = re.compile(r'[MmLlHhVvCcSsQqTtAaZz]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _tag(el):
|
|
37
|
+
return el.tag.split('}', 1)[-1]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_transform(text):
|
|
41
|
+
"""SVG transform list -> QTransform (matrix, translate, scale, rotate)."""
|
|
42
|
+
t = QTransform()
|
|
43
|
+
if not text:
|
|
44
|
+
return t
|
|
45
|
+
for name, args in re.findall(r'(\w+)\s*\(([^)]*)\)', text):
|
|
46
|
+
v = [float(x) for x in _NUM.findall(args)]
|
|
47
|
+
if name == 'matrix' and len(v) == 6:
|
|
48
|
+
m = QTransform(v[0], v[1], v[2], v[3], v[4], v[5])
|
|
49
|
+
elif name == 'translate' and v:
|
|
50
|
+
m = QTransform.fromTranslate(v[0], v[1] if len(v) > 1 else 0.0)
|
|
51
|
+
elif name == 'scale' and v:
|
|
52
|
+
m = QTransform.fromScale(v[0], v[1] if len(v) > 1 else v[0])
|
|
53
|
+
elif name == 'rotate' and v:
|
|
54
|
+
m = QTransform()
|
|
55
|
+
if len(v) == 3:
|
|
56
|
+
m.translate(v[1], v[2])
|
|
57
|
+
m.rotate(v[0])
|
|
58
|
+
if len(v) == 3:
|
|
59
|
+
m.translate(-v[1], -v[2])
|
|
60
|
+
else:
|
|
61
|
+
continue
|
|
62
|
+
# in SVG the leftmost transform is applied last
|
|
63
|
+
t = m * t
|
|
64
|
+
return t
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_path(d):
|
|
68
|
+
"""SVG path data -> QPainterPath (arcs are drawn as straight segments)."""
|
|
69
|
+
path = QPainterPath()
|
|
70
|
+
tokens = _CMD.findall(d or '')
|
|
71
|
+
i = 0
|
|
72
|
+
cmd = None
|
|
73
|
+
cur = QPointF(0, 0)
|
|
74
|
+
start = QPointF(0, 0)
|
|
75
|
+
last_ctrl = None
|
|
76
|
+
last_cmd = ''
|
|
77
|
+
|
|
78
|
+
def num():
|
|
79
|
+
nonlocal i
|
|
80
|
+
v = float(tokens[i])
|
|
81
|
+
i += 1
|
|
82
|
+
return v
|
|
83
|
+
|
|
84
|
+
while i < len(tokens):
|
|
85
|
+
if tokens[i].isalpha():
|
|
86
|
+
cmd = tokens[i]
|
|
87
|
+
i += 1
|
|
88
|
+
if cmd in 'Zz':
|
|
89
|
+
path.closeSubpath()
|
|
90
|
+
cur = QPointF(start)
|
|
91
|
+
last_cmd = cmd
|
|
92
|
+
continue
|
|
93
|
+
if cmd is None:
|
|
94
|
+
break
|
|
95
|
+
rel = cmd.islower()
|
|
96
|
+
c = cmd.upper()
|
|
97
|
+
base = cur if rel else QPointF(0, 0)
|
|
98
|
+
try:
|
|
99
|
+
if c == 'M':
|
|
100
|
+
cur = QPointF(base.x() + num(), base.y() + num())
|
|
101
|
+
path.moveTo(cur)
|
|
102
|
+
start = QPointF(cur)
|
|
103
|
+
cmd = 'l' if rel else 'L' # further pairs are line-tos
|
|
104
|
+
elif c == 'L':
|
|
105
|
+
cur = QPointF(base.x() + num(), base.y() + num())
|
|
106
|
+
path.lineTo(cur)
|
|
107
|
+
elif c == 'H':
|
|
108
|
+
cur = QPointF((cur.x() if rel else 0.0) + num(), cur.y())
|
|
109
|
+
path.lineTo(cur)
|
|
110
|
+
elif c == 'V':
|
|
111
|
+
cur = QPointF(cur.x(), (cur.y() if rel else 0.0) + num())
|
|
112
|
+
path.lineTo(cur)
|
|
113
|
+
elif c == 'C':
|
|
114
|
+
c1 = QPointF(base.x() + num(), base.y() + num())
|
|
115
|
+
c2 = QPointF(base.x() + num(), base.y() + num())
|
|
116
|
+
cur = QPointF(base.x() + num(), base.y() + num())
|
|
117
|
+
path.cubicTo(c1, c2, cur)
|
|
118
|
+
last_ctrl = c2
|
|
119
|
+
elif c == 'S':
|
|
120
|
+
c1 = (cur * 2 - last_ctrl) if last_ctrl is not None and last_cmd in 'CcSs' else QPointF(cur)
|
|
121
|
+
c2 = QPointF(base.x() + num(), base.y() + num())
|
|
122
|
+
cur = QPointF(base.x() + num(), base.y() + num())
|
|
123
|
+
path.cubicTo(c1, c2, cur)
|
|
124
|
+
last_ctrl = c2
|
|
125
|
+
elif c == 'Q':
|
|
126
|
+
c1 = QPointF(base.x() + num(), base.y() + num())
|
|
127
|
+
cur = QPointF(base.x() + num(), base.y() + num())
|
|
128
|
+
path.quadTo(c1, cur)
|
|
129
|
+
last_ctrl = c1
|
|
130
|
+
elif c == 'T':
|
|
131
|
+
c1 = (cur * 2 - last_ctrl) if last_ctrl is not None and last_cmd in 'QqTt' else QPointF(cur)
|
|
132
|
+
cur = QPointF(base.x() + num(), base.y() + num())
|
|
133
|
+
path.quadTo(c1, cur)
|
|
134
|
+
last_ctrl = c1
|
|
135
|
+
elif c == 'A':
|
|
136
|
+
for _ in range(5):
|
|
137
|
+
num()
|
|
138
|
+
cur = QPointF(base.x() + num(), base.y() + num())
|
|
139
|
+
path.lineTo(cur)
|
|
140
|
+
else:
|
|
141
|
+
break
|
|
142
|
+
except (IndexError, ValueError):
|
|
143
|
+
break
|
|
144
|
+
last_cmd = cmd
|
|
145
|
+
return path
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _color(value, opacity):
|
|
149
|
+
if value is None or value == 'none':
|
|
150
|
+
return None
|
|
151
|
+
col = QColor(value)
|
|
152
|
+
if not col.isValid():
|
|
153
|
+
return None
|
|
154
|
+
col.setAlphaF(max(0.0, min(1.0, opacity)))
|
|
155
|
+
return col
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _float(value, default):
|
|
159
|
+
try:
|
|
160
|
+
return float(value)
|
|
161
|
+
except (TypeError, ValueError):
|
|
162
|
+
return default
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class PdfSvgRenderer(QSvgRenderer):
|
|
166
|
+
"""QSvgRenderer for PyMuPDF's SVG output, with working clip paths."""
|
|
167
|
+
|
|
168
|
+
def __init__(self, svg_bytes):
|
|
169
|
+
super().__init__(QByteArray(svg_bytes))
|
|
170
|
+
root = ET.fromstring(svg_bytes)
|
|
171
|
+
self._root = root
|
|
172
|
+
self._ids = {el.get('id'): el for el in root.iter() if el.get('id')}
|
|
173
|
+
vb = [float(x) for x in _NUM.findall(root.get('viewBox') or '')]
|
|
174
|
+
if len(vb) == 4 and vb[2] > 0 and vb[3] > 0:
|
|
175
|
+
self._view_box = QRectF(*vb)
|
|
176
|
+
else:
|
|
177
|
+
self._view_box = QRectF(0, 0, _float(root.get('width'), 100.0), _float(root.get('height'), 100.0))
|
|
178
|
+
self._path_cache = {}
|
|
179
|
+
self._image_cache = {}
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def supports(svg_bytes):
|
|
183
|
+
"""True if every element of the SVG is in the subset drawn here."""
|
|
184
|
+
try:
|
|
185
|
+
root = ET.fromstring(svg_bytes)
|
|
186
|
+
except ET.ParseError:
|
|
187
|
+
return False
|
|
188
|
+
return all(_tag(el) in _SUPPORTED for el in root.iter())
|
|
189
|
+
|
|
190
|
+
# -- QSvgRenderer API used by PyFigures ---------------------------------
|
|
191
|
+
def render(self, painter, *args):
|
|
192
|
+
if len(args) == 1 and isinstance(args[0], str): # render(painter, elementId)
|
|
193
|
+
return super().render(painter, *args)
|
|
194
|
+
if len(args) >= 2: # render(painter, elementId, bounds)
|
|
195
|
+
return super().render(painter, *args)
|
|
196
|
+
bounds = QRectF(args[0]) if args else QRectF(QPointF(0, 0), QSizeF_from(self.defaultSize()))
|
|
197
|
+
vb = self._view_box
|
|
198
|
+
sx = bounds.width() / vb.width()
|
|
199
|
+
sy = bounds.height() / vb.height()
|
|
200
|
+
dx, dy = bounds.left(), bounds.top()
|
|
201
|
+
if self.aspectRatioMode() == Qt.AspectRatioMode.KeepAspectRatio:
|
|
202
|
+
s = min(sx, sy)
|
|
203
|
+
dx += (bounds.width() - vb.width() * s) / 2.0
|
|
204
|
+
dy += (bounds.height() - vb.height() * s) / 2.0
|
|
205
|
+
sx = sy = s
|
|
206
|
+
to_bounds = QTransform(sx, 0, 0, sy, dx - vb.left() * sx, dy - vb.top() * sy)
|
|
207
|
+
engine = painter.paintEngine()
|
|
208
|
+
geometric = engine is not None and engine.type() == QPaintEngine.Type.SVG
|
|
209
|
+
painter.save()
|
|
210
|
+
try:
|
|
211
|
+
base = painter.worldTransform()
|
|
212
|
+
self._geometric = geometric
|
|
213
|
+
self._base = base
|
|
214
|
+
for child in self._root:
|
|
215
|
+
self._draw(painter, child, to_bounds, {}, None, 1.0)
|
|
216
|
+
finally:
|
|
217
|
+
painter.restore()
|
|
218
|
+
|
|
219
|
+
# -- drawing ---------------------------------------------------------------
|
|
220
|
+
def _clip_path(self, ref, user_to_base):
|
|
221
|
+
m = re.match(r'url\(#([^)]+)\)', ref or '')
|
|
222
|
+
el = self._ids.get(m.group(1)) if m else None
|
|
223
|
+
if el is None:
|
|
224
|
+
return None
|
|
225
|
+
out = QPainterPath()
|
|
226
|
+
out.setFillRule(Qt.FillRule.WindingFill)
|
|
227
|
+
for child in el:
|
|
228
|
+
if _tag(child) != 'path':
|
|
229
|
+
continue
|
|
230
|
+
p = parse_path(child.get('d'))
|
|
231
|
+
if child.get('clip-rule') == 'evenodd' or child.get('fill-rule') == 'evenodd':
|
|
232
|
+
p.setFillRule(Qt.FillRule.OddEvenFill)
|
|
233
|
+
t = parse_transform(child.get('transform')) * user_to_base
|
|
234
|
+
out = out.united(t.map(p)) if not out.isEmpty() else t.map(p)
|
|
235
|
+
nested = el.get('clip-path')
|
|
236
|
+
if nested:
|
|
237
|
+
inner = self._clip_path(nested, user_to_base)
|
|
238
|
+
if inner is not None:
|
|
239
|
+
out = out.intersected(inner)
|
|
240
|
+
return out
|
|
241
|
+
|
|
242
|
+
def _draw(self, painter, el, user_to_base, style, clip, opacity):
|
|
243
|
+
tag = _tag(el)
|
|
244
|
+
if tag in ('defs', 'clipPath', 'title', 'desc', 'metadata'):
|
|
245
|
+
return
|
|
246
|
+
style = dict(style)
|
|
247
|
+
for key in _INHERITED:
|
|
248
|
+
if el.get(key) is not None:
|
|
249
|
+
style[key] = el.get(key)
|
|
250
|
+
opacity *= _float(el.get('opacity'), 1.0)
|
|
251
|
+
t = parse_transform(el.get('transform')) * user_to_base
|
|
252
|
+
if el.get('clip-path'):
|
|
253
|
+
new_clip = self._clip_path(el.get('clip-path'), t)
|
|
254
|
+
if new_clip is not None:
|
|
255
|
+
clip = new_clip if clip is None else clip.intersected(new_clip)
|
|
256
|
+
if tag in ('g', 'svg'):
|
|
257
|
+
for child in el:
|
|
258
|
+
self._draw(painter, child, t, style, clip, opacity)
|
|
259
|
+
elif tag == 'use':
|
|
260
|
+
ref = (el.get(_XLINK) or el.get('href') or '').lstrip('#')
|
|
261
|
+
target = self._ids.get(ref)
|
|
262
|
+
if target is not None:
|
|
263
|
+
shift = QTransform.fromTranslate(_float(el.get('x'), 0.0), _float(el.get('y'), 0.0))
|
|
264
|
+
self._draw(painter, target, shift * t, style, clip, opacity)
|
|
265
|
+
elif tag == 'path':
|
|
266
|
+
d = el.get('d')
|
|
267
|
+
path = self._path_cache.get(d)
|
|
268
|
+
if path is None:
|
|
269
|
+
path = parse_path(d)
|
|
270
|
+
self._path_cache[d] = path
|
|
271
|
+
if style.get('fill-rule') == 'evenodd':
|
|
272
|
+
path = QPainterPath(path)
|
|
273
|
+
path.setFillRule(Qt.FillRule.OddEvenFill)
|
|
274
|
+
self._paint_path(painter, path, t, style, clip, opacity)
|
|
275
|
+
elif tag == 'image':
|
|
276
|
+
self._paint_image(painter, el, t, clip, opacity)
|
|
277
|
+
|
|
278
|
+
def _pen(self, style, opacity):
|
|
279
|
+
col = _color(style.get('stroke'), _float(style.get('stroke-opacity'), 1.0) * opacity)
|
|
280
|
+
if col is None:
|
|
281
|
+
return None
|
|
282
|
+
pen = QPen(col)
|
|
283
|
+
pen.setWidthF(_float(style.get('stroke-width'), 1.0))
|
|
284
|
+
pen.setCapStyle({'round': Qt.PenCapStyle.RoundCap, 'square': Qt.PenCapStyle.SquareCap}
|
|
285
|
+
.get(style.get('stroke-linecap'), Qt.PenCapStyle.FlatCap))
|
|
286
|
+
pen.setJoinStyle({'round': Qt.PenJoinStyle.RoundJoin, 'bevel': Qt.PenJoinStyle.BevelJoin}
|
|
287
|
+
.get(style.get('stroke-linejoin'), Qt.PenJoinStyle.SvgMiterJoin))
|
|
288
|
+
pen.setMiterLimit(_float(style.get('stroke-miterlimit'), 4.0))
|
|
289
|
+
dashes = [x for x in (_float(v, None) for v in _NUM.findall(style.get('stroke-dasharray') or ''))
|
|
290
|
+
if x is not None]
|
|
291
|
+
if dashes and any(dashes) and pen.widthF() > 0:
|
|
292
|
+
if len(dashes) % 2:
|
|
293
|
+
dashes = dashes * 2
|
|
294
|
+
pen.setDashPattern([max(1e-3, x / pen.widthF()) for x in dashes])
|
|
295
|
+
return pen
|
|
296
|
+
|
|
297
|
+
def _paint_path(self, painter, path, t, style, clip, opacity):
|
|
298
|
+
fill = _color(style.get('fill', '#000000'), _float(style.get('fill-opacity'), 1.0) * opacity)
|
|
299
|
+
pen = self._pen(style, opacity)
|
|
300
|
+
if fill is None and pen is None:
|
|
301
|
+
return
|
|
302
|
+
if not self._geometric:
|
|
303
|
+
painter.save()
|
|
304
|
+
painter.setWorldTransform(self._base)
|
|
305
|
+
if clip is not None:
|
|
306
|
+
painter.setClipPath(clip, Qt.ClipOperation.IntersectClip)
|
|
307
|
+
painter.setWorldTransform(t * self._base)
|
|
308
|
+
painter.setBrush(QBrush(fill) if fill is not None else Qt.BrushStyle.NoBrush)
|
|
309
|
+
painter.setPen(pen if pen is not None else Qt.PenStyle.NoPen)
|
|
310
|
+
painter.drawPath(path)
|
|
311
|
+
painter.restore()
|
|
312
|
+
return
|
|
313
|
+
# SVG output: Qt 6.4's SVG writer drops clipping, so clip the geometry
|
|
314
|
+
painter.save()
|
|
315
|
+
painter.setWorldTransform(self._base)
|
|
316
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
317
|
+
if fill is not None:
|
|
318
|
+
area = t.map(path)
|
|
319
|
+
if clip is not None and not clip.contains(area.boundingRect()):
|
|
320
|
+
area = area.intersected(clip)
|
|
321
|
+
painter.setBrush(QBrush(fill))
|
|
322
|
+
painter.drawPath(area)
|
|
323
|
+
if pen is not None:
|
|
324
|
+
if clip is None or clip.contains(t.map(path).boundingRect().adjusted(
|
|
325
|
+
-pen.widthF(), -pen.widthF(), pen.widthF(), pen.widthF())):
|
|
326
|
+
painter.setWorldTransform(t * self._base)
|
|
327
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
328
|
+
painter.setPen(pen)
|
|
329
|
+
painter.drawPath(path)
|
|
330
|
+
else:
|
|
331
|
+
stroker = QPainterPathStroker(pen)
|
|
332
|
+
outline = t.map(stroker.createStroke(path)).intersected(clip)
|
|
333
|
+
painter.setBrush(QBrush(pen.color()))
|
|
334
|
+
painter.drawPath(outline)
|
|
335
|
+
painter.restore()
|
|
336
|
+
|
|
337
|
+
def _paint_image(self, painter, el, t, clip, opacity):
|
|
338
|
+
href = el.get(_XLINK) or el.get('href') or ''
|
|
339
|
+
img = self._image_cache.get(href)
|
|
340
|
+
if img is None:
|
|
341
|
+
m = re.match(r'data:image/[^;]+;base64,(.*)', href, re.S)
|
|
342
|
+
if not m:
|
|
343
|
+
return
|
|
344
|
+
img = QImage.fromData(base64.b64decode(re.sub(r'\s', '', m.group(1))))
|
|
345
|
+
self._image_cache[href] = img
|
|
346
|
+
if img.isNull():
|
|
347
|
+
return
|
|
348
|
+
rect = QRectF(_float(el.get('x'), 0.0), _float(el.get('y'), 0.0),
|
|
349
|
+
_float(el.get('width'), img.width()), _float(el.get('height'), img.height()))
|
|
350
|
+
painter.save()
|
|
351
|
+
painter.setWorldTransform(self._base)
|
|
352
|
+
if clip is not None:
|
|
353
|
+
if self._geometric:
|
|
354
|
+
# an SVG writer cannot clip an image: keep the clip's bounding box
|
|
355
|
+
visible = t.mapRect(rect).intersected(clip.boundingRect())
|
|
356
|
+
if visible.isEmpty():
|
|
357
|
+
painter.restore()
|
|
358
|
+
return
|
|
359
|
+
else:
|
|
360
|
+
painter.setClipPath(clip, Qt.ClipOperation.IntersectClip)
|
|
361
|
+
painter.setWorldTransform(t * self._base)
|
|
362
|
+
painter.setOpacity(painter.opacity() * opacity)
|
|
363
|
+
painter.drawImage(rect, img, QRectF(img.rect()))
|
|
364
|
+
painter.restore()
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def QSizeF_from(size):
|
|
368
|
+
from qtpy.QtCore import QSizeF
|
|
369
|
+
return QSizeF(size.width(), size.height())
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def make_pdf_svg_renderer(svg_bytes):
|
|
373
|
+
"""PdfSvgRenderer if the SVG is in the subset it draws, else QSvgRenderer."""
|
|
374
|
+
if PdfSvgRenderer.supports(svg_bytes):
|
|
375
|
+
return PdfSvgRenderer(svg_bytes)
|
|
376
|
+
return QSvgRenderer(QByteArray(svg_bytes))
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# TODO make these things combined with pack and align commands and make loops
|
|
2
|
+
# this way I can make any component at any specific position !!!
|
|
3
|
+
# this is quite good I think
|
|
4
|
+
import copy
|
|
5
|
+
import traceback
|
|
6
|
+
from itertools import groupby
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from batoolset.figure.alignment import packing_modes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Define a sorting key function to sort objects based on their positions
|
|
13
|
+
def sorting_key(obj):
|
|
14
|
+
try:
|
|
15
|
+
return tuple(sorted(obj.items()))
|
|
16
|
+
except Exception:
|
|
17
|
+
try:
|
|
18
|
+
return tuple(sorted(obj.placement.get_position()))
|
|
19
|
+
except Exception:
|
|
20
|
+
# pass
|
|
21
|
+
return ('free','relative') # if the object does not have a position assume it obeys free/relative positioning
|
|
22
|
+
|
|
23
|
+
def groupby_position(objects):
|
|
24
|
+
from batoolset.drawings.shapes.txt2d import TAText2D
|
|
25
|
+
if isinstance(objects, TAText2D):
|
|
26
|
+
objects = [objects]
|
|
27
|
+
# Sort the objects based on their positions
|
|
28
|
+
sorted_objects = sorted(objects, key=sorting_key)
|
|
29
|
+
# Group the sorted objects based on their positions
|
|
30
|
+
grouped_objects = {key: list(group) for key, group in groupby(sorted_objects, key=sorting_key)}
|
|
31
|
+
return grouped_objects
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Position:
|
|
35
|
+
positions = {
|
|
36
|
+
'top': False,
|
|
37
|
+
'bottom': False,
|
|
38
|
+
'left': False,
|
|
39
|
+
'right': False,
|
|
40
|
+
'center_h': False,
|
|
41
|
+
'center_v': False
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def __init__(self, position=None):
|
|
45
|
+
self.positions = copy.deepcopy(Position.positions)
|
|
46
|
+
if position is not None:
|
|
47
|
+
self.set_position(position)
|
|
48
|
+
|
|
49
|
+
def set_position(self, position):
|
|
50
|
+
for key in self.positions.keys(): # reset all positions
|
|
51
|
+
self.positions[key] = False
|
|
52
|
+
|
|
53
|
+
if not position:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
position = position.lower()
|
|
57
|
+
# except:
|
|
58
|
+
# traceback.print_exc()
|
|
59
|
+
# print('position', position)
|
|
60
|
+
|
|
61
|
+
if 'top' in position:
|
|
62
|
+
self.positions['top'] = True
|
|
63
|
+
if 'bottom' in position:
|
|
64
|
+
self.positions['bottom'] = True
|
|
65
|
+
if 'left' in position:
|
|
66
|
+
self.positions['left'] = True
|
|
67
|
+
if 'right' in position:
|
|
68
|
+
self.positions['right'] = True
|
|
69
|
+
if 'center_h' in position:
|
|
70
|
+
self.positions['center_h'] = True
|
|
71
|
+
if 'center_v' in position:
|
|
72
|
+
self.positions['center_v'] = True
|
|
73
|
+
|
|
74
|
+
def get_position(self):
|
|
75
|
+
return {key: value for key, value in self.positions.items() if value}
|
|
76
|
+
|
|
77
|
+
def check_position(self, position):
|
|
78
|
+
if not position in self.positions:
|
|
79
|
+
return False
|
|
80
|
+
return self.positions[position]
|
|
81
|
+
|
|
82
|
+
def position_to_string(self):
|
|
83
|
+
out = ''
|
|
84
|
+
if self.positions['top']:
|
|
85
|
+
out += 'top '
|
|
86
|
+
if self.positions['bottom']:
|
|
87
|
+
out += 'bottom '
|
|
88
|
+
if self.positions['left']:
|
|
89
|
+
out += 'left '
|
|
90
|
+
if self.positions['right']:
|
|
91
|
+
out += 'right '
|
|
92
|
+
if self.positions['center_h']:
|
|
93
|
+
out += 'center_h '
|
|
94
|
+
if self.positions['center_v']:
|
|
95
|
+
out += 'center_v '
|
|
96
|
+
|
|
97
|
+
return out.strip()
|
|
98
|
+
|
|
99
|
+
def get_packing_orientation(self):
|
|
100
|
+
# try rescue packing mode from position
|
|
101
|
+
# if reference.position.get_position():
|
|
102
|
+
if self.check_position('top'):
|
|
103
|
+
return packing_modes[1]
|
|
104
|
+
elif self.check_position('bottom'):
|
|
105
|
+
return packing_modes[3]
|
|
106
|
+
# elif mode.check_position('left'):
|
|
107
|
+
# mode = packing_modes[0]
|
|
108
|
+
# elif mode.check_position('right'):
|
|
109
|
+
# mode = packing_modes[2]
|
|
110
|
+
# else:
|
|
111
|
+
# # logger.error('unknown positioning --> ignoring')
|
|
112
|
+
# print('unknown position ignoring')
|
|
113
|
+
|
|
114
|
+
def isEmpty(self):
|
|
115
|
+
for key, value in self.positions.items():
|
|
116
|
+
if value:
|
|
117
|
+
return False
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
def __str__(self):
|
|
121
|
+
return self.position_to_string()
|
|
122
|
+
|
|
123
|
+
# very good --> maybe just slightly improve it and fix it --> it will be placed with repsect to the parent stuff
|
|
124
|
+
if __name__ == '__main__':
|
|
125
|
+
# Create a new Position object
|
|
126
|
+
p = Position()
|
|
127
|
+
print(p.get_position()) # --> empty
|
|
128
|
+
|
|
129
|
+
# Set the position
|
|
130
|
+
p.set_position('top left')
|
|
131
|
+
# Get the current position
|
|
132
|
+
print(p.get_position()) # Output: {'top': True, 'left': True}
|
|
133
|
+
|
|
134
|
+
p.set_position('TOp Left')
|
|
135
|
+
# Get the current position
|
|
136
|
+
print(p.get_position()) # Output: {'top': True, 'left': True}
|
|
137
|
+
|
|
138
|
+
p.set_position('Bottom Right')
|
|
139
|
+
# Get the current position
|
|
140
|
+
print(p.get_position()) # Output: {'top': True, 'left': True}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
print('pos to string', p.position_to_string())
|
|
144
|
+
|
|
145
|
+
print(p.check_position('top'))
|
|
146
|
+
print(p.check_position('right'))
|
|
147
|
+
print(p.check_position('bottom'))
|
|
148
|
+
|
|
149
|
+
p2 = Position()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
print(p2.get_position())
|
|
154
|
+
|
|
155
|
+
print(p2.isEmpty())
|
|
156
|
+
print(p.isEmpty())
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if False:
|
|
161
|
+
|
|
162
|
+
# List of objects with positions
|
|
163
|
+
objects = [
|
|
164
|
+
{'top': True, 'left': True},
|
|
165
|
+
{'bottom': True, 'right': True},
|
|
166
|
+
{'top': True, 'left': True},
|
|
167
|
+
{'bottom': True, 'left': True}
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
grouped_objects = groupby_position(objects)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
print(grouped_objects)
|
|
176
|
+
|
|
177
|
+
# Print the grouped objects
|
|
178
|
+
for position, objects_in_group in grouped_objects.items():
|
|
179
|
+
print(f"Position: {position}")
|
|
180
|
+
for obj in objects_in_group:
|
|
181
|
+
print(obj)
|
|
182
|
+
print()
|
|
183
|
+
|
|
File without changes
|