svg-ultralight 0.38.0__py3-none-any.whl → 0.39.1__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.
Potentially problematic release.
This version of svg-ultralight might be problematic. Click here for more details.
- svg_ultralight/bounding_boxes/bound_helpers.py +3 -10
- svg_ultralight/bounding_boxes/supports_bounds.py +4 -9
- svg_ultralight/bounding_boxes/type_bound_collection.py +1 -1
- svg_ultralight/bounding_boxes/type_bound_element.py +1 -4
- svg_ultralight/bounding_boxes/type_bounding_box.py +149 -266
- svg_ultralight/bounding_boxes/type_padded_text.py +96 -199
- svg_ultralight/layout.py +22 -13
- svg_ultralight/query.py +33 -23
- svg_ultralight/transformations.py +19 -6
- {svg_ultralight-0.38.0.dist-info → svg_ultralight-0.39.1.dist-info}/METADATA +1 -1
- {svg_ultralight-0.38.0.dist-info → svg_ultralight-0.39.1.dist-info}/RECORD +13 -13
- {svg_ultralight-0.38.0.dist-info → svg_ultralight-0.39.1.dist-info}/WHEEL +1 -1
- {svg_ultralight-0.38.0.dist-info → svg_ultralight-0.39.1.dist-info}/top_level.txt +0 -0
|
@@ -12,7 +12,7 @@ There is a getter and setter for each of the four padding values. These *do not*
|
|
|
12
12
|
the text element. For instance, if you decrease the left padding, the left margin
|
|
13
13
|
will move, *not* the text element.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
_There is a getter and setter for each of lmargin, rmargin, baseline, and capline.
|
|
16
16
|
These *do* move the element, but do not scale it. For instance, if you move the
|
|
17
17
|
leftmargin to the left, the right margin (and the text element with it) will move to
|
|
18
18
|
the left.
|
|
@@ -66,8 +66,9 @@ from __future__ import annotations
|
|
|
66
66
|
|
|
67
67
|
from typing import TYPE_CHECKING
|
|
68
68
|
|
|
69
|
-
from svg_ultralight.bounding_boxes.
|
|
69
|
+
from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
|
|
70
70
|
from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
|
|
71
|
+
from svg_ultralight.transformations import new_transformation_matrix, transform_element
|
|
71
72
|
|
|
72
73
|
if TYPE_CHECKING:
|
|
73
74
|
from lxml.etree import (
|
|
@@ -77,7 +78,7 @@ if TYPE_CHECKING:
|
|
|
77
78
|
_Matrix = tuple[float, float, float, float, float, float]
|
|
78
79
|
|
|
79
80
|
|
|
80
|
-
class PaddedText(
|
|
81
|
+
class PaddedText(BoundElement):
|
|
81
82
|
"""A line of text with a bounding box and padding."""
|
|
82
83
|
|
|
83
84
|
def __init__(
|
|
@@ -99,14 +100,14 @@ class PaddedText(SupportsBounds):
|
|
|
99
100
|
:param lpad: Left padding.
|
|
100
101
|
"""
|
|
101
102
|
self.elem = elem
|
|
102
|
-
self.
|
|
103
|
+
self.unpadded_bbox = bbox
|
|
103
104
|
self.base_tpad = tpad
|
|
104
105
|
self.rpad = rpad
|
|
105
106
|
self.base_bpad = bpad
|
|
106
107
|
self.lpad = lpad
|
|
107
108
|
|
|
108
109
|
@property
|
|
109
|
-
def
|
|
110
|
+
def bbox(self) -> BoundingBox:
|
|
110
111
|
"""Return a BoundingBox around the margins and cap/baseline.
|
|
111
112
|
|
|
112
113
|
:return: A BoundingBox around the margins and cap/baseline.
|
|
@@ -117,22 +118,27 @@ class PaddedText(SupportsBounds):
|
|
|
117
118
|
instance around multiple text elements (a <g> elem).
|
|
118
119
|
"""
|
|
119
120
|
return BoundingBox(
|
|
120
|
-
self.
|
|
121
|
+
self.x,
|
|
122
|
+
self.y,
|
|
123
|
+
self.width,
|
|
124
|
+
self.height,
|
|
121
125
|
)
|
|
122
126
|
|
|
123
|
-
@
|
|
124
|
-
def
|
|
125
|
-
"""
|
|
126
|
-
return self.bbox.transformation
|
|
127
|
+
@bbox.setter
|
|
128
|
+
def bbox(self, value: BoundingBox) -> None:
|
|
129
|
+
"""Set the bounding box of this PaddedText.
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
:param value: The new bounding box.
|
|
132
|
+
:effects: The text element is transformed to fit the new bounding box.
|
|
133
|
+
"""
|
|
134
|
+
msg = "Cannot set bbox of PaddedText, use transform() instead."
|
|
135
|
+
raise NotImplementedError(msg)
|
|
130
136
|
|
|
131
137
|
def transform(
|
|
132
138
|
self,
|
|
133
139
|
transformation: _Matrix | None = None,
|
|
134
140
|
*,
|
|
135
|
-
scale: float | None = None,
|
|
141
|
+
scale: tuple[float, float] | float | None = None,
|
|
136
142
|
dx: float | None = None,
|
|
137
143
|
dy: float | None = None,
|
|
138
144
|
):
|
|
@@ -143,8 +149,9 @@ class PaddedText(SupportsBounds):
|
|
|
143
149
|
:param dx: the x translation
|
|
144
150
|
:param dy: the y translation
|
|
145
151
|
"""
|
|
146
|
-
|
|
147
|
-
self.
|
|
152
|
+
tmat = new_transformation_matrix(transformation, scale=scale, dx=dx, dy=dy)
|
|
153
|
+
self.unpadded_bbox.transform(tmat)
|
|
154
|
+
_ = transform_element(self.elem, tmat)
|
|
148
155
|
|
|
149
156
|
@property
|
|
150
157
|
def tpad(self) -> float:
|
|
@@ -152,7 +159,7 @@ class PaddedText(SupportsBounds):
|
|
|
152
159
|
|
|
153
160
|
:return: The scaled top padding of this line of text.
|
|
154
161
|
"""
|
|
155
|
-
return self.base_tpad * self.
|
|
162
|
+
return self.base_tpad * self.unpadded_bbox.scale[1]
|
|
156
163
|
|
|
157
164
|
@tpad.setter
|
|
158
165
|
def tpad(self, value: float) -> None:
|
|
@@ -160,7 +167,7 @@ class PaddedText(SupportsBounds):
|
|
|
160
167
|
|
|
161
168
|
:param value: The new top padding.
|
|
162
169
|
"""
|
|
163
|
-
self.base_tpad = value / self.
|
|
170
|
+
self.base_tpad = value / self.unpadded_bbox.scale[1]
|
|
164
171
|
|
|
165
172
|
@property
|
|
166
173
|
def bpad(self) -> float:
|
|
@@ -168,7 +175,7 @@ class PaddedText(SupportsBounds):
|
|
|
168
175
|
|
|
169
176
|
:return: The scaled bottom padding of this line of text.
|
|
170
177
|
"""
|
|
171
|
-
return self.base_bpad * self.
|
|
178
|
+
return self.base_bpad * self.unpadded_bbox.scale[1]
|
|
172
179
|
|
|
173
180
|
@bpad.setter
|
|
174
181
|
def bpad(self, value: float) -> None:
|
|
@@ -176,82 +183,18 @@ class PaddedText(SupportsBounds):
|
|
|
176
183
|
|
|
177
184
|
:param value: The new bottom padding.
|
|
178
185
|
"""
|
|
179
|
-
self.base_bpad = value / self.
|
|
186
|
+
self.base_bpad = value / self.unpadded_bbox.scale[1]
|
|
180
187
|
|
|
181
188
|
@property
|
|
182
|
-
def
|
|
183
|
-
"""The left margin of this line of text.
|
|
184
|
-
|
|
185
|
-
:return: The left margin of this line of text.
|
|
186
|
-
"""
|
|
187
|
-
return self.bbox.x - self.lpad
|
|
188
|
-
|
|
189
|
-
@lmargin.setter
|
|
190
|
-
def lmargin(self, value: float) -> None:
|
|
191
|
-
"""Set the left margin of this line of text.
|
|
192
|
-
|
|
193
|
-
:param value: The left margin of this line of text.
|
|
194
|
-
"""
|
|
195
|
-
self.transform(dx=value + self.lpad - self.bbox.x)
|
|
196
|
-
|
|
197
|
-
@property
|
|
198
|
-
def rmargin(self) -> float:
|
|
199
|
-
"""The right margin of this line of text.
|
|
200
|
-
|
|
201
|
-
:return: The right margin of this line of text.
|
|
202
|
-
"""
|
|
203
|
-
return self.bbox.x2 + self.rpad
|
|
204
|
-
|
|
205
|
-
@rmargin.setter
|
|
206
|
-
def rmargin(self, value: float) -> None:
|
|
207
|
-
"""Set the right margin of this line of text.
|
|
208
|
-
|
|
209
|
-
:param value: The right margin of this line of text.
|
|
210
|
-
"""
|
|
211
|
-
self.transform(dx=value - self.rpad - self.bbox.x2)
|
|
212
|
-
|
|
213
|
-
@property
|
|
214
|
-
def capline(self) -> float:
|
|
215
|
-
"""The top of this line of text.
|
|
216
|
-
|
|
217
|
-
:return: The top of this line of text.
|
|
218
|
-
"""
|
|
219
|
-
return self.bbox.y - self.tpad
|
|
220
|
-
|
|
221
|
-
@capline.setter
|
|
222
|
-
def capline(self, value: float) -> None:
|
|
223
|
-
"""Set the top of this line of text.
|
|
224
|
-
|
|
225
|
-
:param value: The top of this line of text.
|
|
226
|
-
"""
|
|
227
|
-
self.transform(dy=value + self.tpad - self.bbox.y)
|
|
228
|
-
|
|
229
|
-
@property
|
|
230
|
-
def baseline(self) -> float:
|
|
231
|
-
"""The bottom of this line of text.
|
|
232
|
-
|
|
233
|
-
:return: The bottom of this line of text.
|
|
234
|
-
"""
|
|
235
|
-
return self.bbox.y2 + self.bpad
|
|
236
|
-
|
|
237
|
-
@baseline.setter
|
|
238
|
-
def baseline(self, value: float) -> None:
|
|
239
|
-
"""Set the bottom of this line of text.
|
|
240
|
-
|
|
241
|
-
:param value: The bottom of this line of text.
|
|
242
|
-
"""
|
|
243
|
-
self.transform(dy=value - self.bpad - self.bbox.y2)
|
|
244
|
-
|
|
245
|
-
@property
|
|
246
|
-
def padded_width(self) -> float:
|
|
189
|
+
def width(self) -> float:
|
|
247
190
|
"""The width of this line of text with padding.
|
|
248
191
|
|
|
249
192
|
:return: The scaled width of this line of text with padding.
|
|
250
193
|
"""
|
|
251
|
-
return self.
|
|
194
|
+
return self.unpadded_bbox.width + self.lpad + self.rpad
|
|
252
195
|
|
|
253
|
-
@
|
|
254
|
-
def
|
|
196
|
+
@width.setter
|
|
197
|
+
def width(self, value: float) -> None:
|
|
255
198
|
"""Scale to padded_width = width without scaling padding.
|
|
256
199
|
|
|
257
200
|
:param width: The new width of this line of text.
|
|
@@ -263,27 +206,34 @@ class PaddedText(SupportsBounds):
|
|
|
263
206
|
baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
|
|
264
207
|
*and* y2) when scaling.
|
|
265
208
|
"""
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
self.
|
|
209
|
+
y2 = self.y2
|
|
210
|
+
|
|
211
|
+
no_margins_old = self.unpadded_bbox.width
|
|
212
|
+
no_margins_new = value - self.lpad - self.rpad
|
|
213
|
+
scale = no_margins_new / no_margins_old
|
|
214
|
+
self.transform(scale=(scale, scale))
|
|
215
|
+
|
|
216
|
+
self.y2 = y2
|
|
270
217
|
|
|
271
218
|
@property
|
|
272
|
-
def
|
|
219
|
+
def height(self) -> float:
|
|
273
220
|
"""The height of this line of text with padding.
|
|
274
221
|
|
|
275
222
|
:return: The scaled height of this line of text with padding.
|
|
276
223
|
"""
|
|
277
|
-
return self.
|
|
224
|
+
return self.unpadded_bbox.height + self.tpad + self.bpad
|
|
278
225
|
|
|
279
|
-
@
|
|
280
|
-
def
|
|
281
|
-
"""Scale to
|
|
226
|
+
@height.setter
|
|
227
|
+
def height(self, value: float) -> None:
|
|
228
|
+
"""Scale to height without scaling padding.
|
|
282
229
|
|
|
283
230
|
:param height: The new height of this line of text.
|
|
284
231
|
:effects: the text_element bounding box is scaled to height - tpad - bpad.
|
|
285
232
|
"""
|
|
286
|
-
|
|
233
|
+
y2 = self.y2
|
|
234
|
+
scale = value / self.height
|
|
235
|
+
self.transform(scale=(scale, scale))
|
|
236
|
+
self.y2 = y2
|
|
287
237
|
|
|
288
238
|
@property
|
|
289
239
|
def x(self) -> float:
|
|
@@ -291,15 +241,31 @@ class PaddedText(SupportsBounds):
|
|
|
291
241
|
|
|
292
242
|
:return: The left margin of this line of text.
|
|
293
243
|
"""
|
|
294
|
-
return self.
|
|
244
|
+
return self.unpadded_bbox.x - self.lpad
|
|
295
245
|
|
|
296
246
|
@x.setter
|
|
297
247
|
def x(self, value: float) -> None:
|
|
298
248
|
"""Set the left margin of this line of text.
|
|
299
249
|
|
|
300
|
-
:param value: The
|
|
250
|
+
:param value: The left margin of this line of text.
|
|
251
|
+
"""
|
|
252
|
+
self.transform(dx=value + self.lpad - self.unpadded_bbox.x)
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def cx(self) -> float:
|
|
256
|
+
"""The horizontal center of this line of text.
|
|
257
|
+
|
|
258
|
+
:return: The horizontal center of this line of text.
|
|
259
|
+
"""
|
|
260
|
+
return self.x + self.width / 2
|
|
261
|
+
|
|
262
|
+
@cx.setter
|
|
263
|
+
def cx(self, value: float) -> None:
|
|
264
|
+
"""Set the horizontal center of this line of text.
|
|
265
|
+
|
|
266
|
+
:param value: The horizontal center of this line of text.
|
|
301
267
|
"""
|
|
302
|
-
self.
|
|
268
|
+
self.x += value - self.cx
|
|
303
269
|
|
|
304
270
|
@property
|
|
305
271
|
def x2(self) -> float:
|
|
@@ -307,134 +273,65 @@ class PaddedText(SupportsBounds):
|
|
|
307
273
|
|
|
308
274
|
:return: The right margin of this line of text.
|
|
309
275
|
"""
|
|
310
|
-
return self.
|
|
276
|
+
return self.unpadded_bbox.x2 + self.rpad
|
|
311
277
|
|
|
312
278
|
@x2.setter
|
|
313
279
|
def x2(self, value: float) -> None:
|
|
314
280
|
"""Set the right margin of this line of text.
|
|
315
281
|
|
|
316
|
-
:param value: The
|
|
282
|
+
:param value: The right margin of this line of text.
|
|
317
283
|
"""
|
|
318
|
-
self.
|
|
284
|
+
self.transform(dx=value - self.rpad - self.unpadded_bbox.x2)
|
|
319
285
|
|
|
320
286
|
@property
|
|
321
287
|
def y(self) -> float:
|
|
322
|
-
"""The
|
|
288
|
+
"""The top of this line of text.
|
|
323
289
|
|
|
324
|
-
:return: The
|
|
290
|
+
:return: The top of this line of text.
|
|
325
291
|
"""
|
|
326
|
-
return self.
|
|
292
|
+
return self.unpadded_bbox.y - self.tpad
|
|
327
293
|
|
|
328
294
|
@y.setter
|
|
329
295
|
def y(self, value: float) -> None:
|
|
330
|
-
"""Set the
|
|
331
|
-
|
|
332
|
-
:param value: The new capline of this line of text.
|
|
333
|
-
"""
|
|
334
|
-
self.capline = value
|
|
335
|
-
|
|
336
|
-
@property
|
|
337
|
-
def y2(self) -> float:
|
|
338
|
-
"""The baseline of this line of text.
|
|
339
|
-
|
|
340
|
-
:return: The baseline of this line of text.
|
|
341
|
-
"""
|
|
342
|
-
return self.baseline
|
|
343
|
-
|
|
344
|
-
@y2.setter
|
|
345
|
-
def y2(self, value: float) -> None:
|
|
346
|
-
"""Set the baseline of this line of text.
|
|
347
|
-
|
|
348
|
-
:param value: The new baseline of this line of text.
|
|
349
|
-
"""
|
|
350
|
-
self.baseline = value
|
|
351
|
-
|
|
352
|
-
@property
|
|
353
|
-
def width(self) -> float:
|
|
354
|
-
"""The width of this line of text with padding.
|
|
355
|
-
|
|
356
|
-
:return: The scaled width of this line of text with padding.
|
|
357
|
-
"""
|
|
358
|
-
return self.padded_width
|
|
359
|
-
|
|
360
|
-
@width.setter
|
|
361
|
-
def width(self, value: float) -> None:
|
|
362
|
-
"""Scale to width without scaling padding.
|
|
363
|
-
|
|
364
|
-
:param value: The new width of this line of text.
|
|
365
|
-
:effects: the text_element bounding box is scaled to width - lpad - rpad.
|
|
366
|
-
|
|
367
|
-
Svg_Ultralight BoundingBoxes preserve x and y when scaling. This is
|
|
368
|
-
consistent with how rectangles, viewboxes, and anything else defined by x, y,
|
|
369
|
-
width, height behaves in SVG. This is unintuitive for text, because the
|
|
370
|
-
baseline is near y2 (y + height) not y. So, we preserve baseline (alter y
|
|
371
|
-
*and* y2) when scaling.
|
|
372
|
-
"""
|
|
373
|
-
baseline = self.baseline
|
|
374
|
-
self.padded_width = value
|
|
375
|
-
self.baseline = baseline
|
|
376
|
-
|
|
377
|
-
@property
|
|
378
|
-
def height(self) -> float:
|
|
379
|
-
"""The height of this line of text with padding.
|
|
380
|
-
|
|
381
|
-
:return: The scaled height of this line of text with padding.
|
|
382
|
-
"""
|
|
383
|
-
return self.padded_height
|
|
384
|
-
|
|
385
|
-
@height.setter
|
|
386
|
-
def height(self, value: float) -> None:
|
|
387
|
-
"""Scale to height without scaling padding.
|
|
388
|
-
|
|
389
|
-
:param value: The new height of this line of text.
|
|
390
|
-
:effects: the text_element bounding box is scaled to height - tpad - bpad.
|
|
391
|
-
"""
|
|
392
|
-
self.padded_height = value
|
|
393
|
-
|
|
394
|
-
@property
|
|
395
|
-
def cx(self) -> float:
|
|
396
|
-
"""The x coordinate of the center between margins.
|
|
397
|
-
|
|
398
|
-
:return: the x coordinate of the center between margins
|
|
399
|
-
"""
|
|
400
|
-
return self.lmargin + self.padded_width / 2
|
|
401
|
-
|
|
402
|
-
@cx.setter
|
|
403
|
-
def cx(self, value: float):
|
|
404
|
-
"""Set the x coordinate of the center between margins.
|
|
296
|
+
"""Set the top of this line of text.
|
|
405
297
|
|
|
406
|
-
:param value:
|
|
298
|
+
:param value: The top of this line of text.
|
|
407
299
|
"""
|
|
408
|
-
self.
|
|
300
|
+
self.transform(dy=value + self.tpad - self.unpadded_bbox.y)
|
|
409
301
|
|
|
410
302
|
@property
|
|
411
303
|
def cy(self) -> float:
|
|
412
|
-
"""The
|
|
304
|
+
"""The horizontal center of this line of text.
|
|
413
305
|
|
|
414
|
-
:return:
|
|
306
|
+
:return: The horizontal center of this line of text.
|
|
415
307
|
"""
|
|
416
|
-
return self.
|
|
308
|
+
return self.y + self.height / 2
|
|
417
309
|
|
|
418
310
|
@cy.setter
|
|
419
|
-
def cy(self, value: float):
|
|
420
|
-
"""Set the
|
|
311
|
+
def cy(self, value: float) -> None:
|
|
312
|
+
"""Set the horizontal center of this line of text.
|
|
421
313
|
|
|
422
|
-
:param value:
|
|
314
|
+
:param value: The horizontal center of this line of text.
|
|
423
315
|
"""
|
|
424
|
-
self.
|
|
316
|
+
self.y += value - self.cy
|
|
425
317
|
|
|
426
318
|
@property
|
|
427
|
-
def
|
|
428
|
-
"""The
|
|
319
|
+
def y2(self) -> float:
|
|
320
|
+
"""The bottom of this line of text.
|
|
429
321
|
|
|
430
|
-
:return:
|
|
322
|
+
:return: The bottom of this line of text.
|
|
431
323
|
"""
|
|
432
|
-
return self.
|
|
324
|
+
return self.unpadded_bbox.y2 + self.bpad
|
|
433
325
|
|
|
434
|
-
@
|
|
435
|
-
def
|
|
436
|
-
"""Set the
|
|
326
|
+
@y2.setter
|
|
327
|
+
def y2(self, value: float) -> None:
|
|
328
|
+
"""Set the bottom of this line of text.
|
|
437
329
|
|
|
438
|
-
:param value:
|
|
330
|
+
:param value: The bottom of this line of text.
|
|
439
331
|
"""
|
|
440
|
-
self.
|
|
332
|
+
self.transform(dy=value - self.bpad - self.unpadded_bbox.y2)
|
|
333
|
+
|
|
334
|
+
lmargin = x
|
|
335
|
+
rmargin = x2
|
|
336
|
+
capline = y
|
|
337
|
+
baseline = y2
|
svg_ultralight/layout.py
CHANGED
|
@@ -144,11 +144,11 @@ def pad_and_scale(
|
|
|
144
144
|
"""Expand and scale the pad argument. If necessary, scale image.
|
|
145
145
|
|
|
146
146
|
:param viewbox: viewbox to pad (x, y, width height)
|
|
147
|
-
:param pad: padding to add around image, in user units or inches.
|
|
147
|
+
:param pad: padding to add around image, in user units or inches. If a
|
|
148
148
|
sequence, it should be (top, right, bottom, left). if a single float or
|
|
149
|
-
string, it will be applied to all sides.
|
|
150
|
-
then left and right.
|
|
151
|
-
|
|
149
|
+
string, it will be applied to all sides. If two floats, top and bottom
|
|
150
|
+
then left and right. If three floats, top, left and right, then bottom.
|
|
151
|
+
If four floats, top, right, bottom, left.
|
|
152
152
|
:param print_width: width of print area, in user units (float), a string
|
|
153
153
|
with a unit specifier (e.g., "452mm"), or just a unit specifier (e.g.,
|
|
154
154
|
"pt")
|
|
@@ -169,7 +169,7 @@ def pad_and_scale(
|
|
|
169
169
|
If the width and height *are* specified, the user units become whatever they
|
|
170
170
|
need to be to fit that requirement. For instance, if the viewbox width is 96
|
|
171
171
|
and the width argument is "1in", then the user units are *still* pixels,
|
|
172
|
-
because there are 96 pixels in an inch. If the viewbox
|
|
172
|
+
because there are 96 pixels in an inch. If the viewbox width is 2 and the
|
|
173
173
|
width argument is "1in", then the user units are 1/2 of an inch (i.e., 48
|
|
174
174
|
pixels) each, because there are 2 user units in an inch. If the viewbox
|
|
175
175
|
width is 3 and the width argument is "1yd", the each user unit is 1 foot.
|
|
@@ -195,17 +195,26 @@ def pad_and_scale(
|
|
|
195
195
|
the unit designators without changing the scale.
|
|
196
196
|
|
|
197
197
|
Print aspect ratio is ignored. Viewbox aspect ratio is preserved. For
|
|
198
|
-
instance,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
instance, if you created two images
|
|
199
|
+
|
|
200
|
+
* x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="6in"
|
|
201
|
+
|
|
202
|
+
* x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="12in"
|
|
203
|
+
|
|
204
|
+
... (note that the images only vary in print_width_), the first image would be
|
|
205
|
+
rendered at 6.5x12.5 inches and the second at 12.5x24.5 inches. The visible
|
|
206
|
+
content in the viewbox would be exactly twice as wide in the larger image, but
|
|
207
|
+
the padding would remain 0.25 in both images. Despite setting `print_width_` to
|
|
208
|
+
exactly 6 or 12 inches, you would not get an image exactly 6 or 12 inches wide.
|
|
209
|
+
Despite a viewbox aspect ratio of 1:2, you would not get an output image of
|
|
210
|
+
exactly 1:2. If you want to use padding and need a specific output image size or
|
|
211
|
+
aspect ratio, remember to subtract the padding width from your print_width or
|
|
212
|
+
print_height.
|
|
204
213
|
|
|
205
214
|
Scaling attributes are returned as a dictonary that can be "exploded" into
|
|
206
215
|
the element constructor, e.g., {"width": "12.5in", "height": "12.5in"}.
|
|
207
216
|
|
|
208
|
-
* If
|
|
217
|
+
* If neither a print_width nor print_height is specified, no scaling
|
|
209
218
|
attributes will be returned.
|
|
210
219
|
|
|
211
220
|
* If either is specified, both a width and height will be returned (even if
|
|
@@ -242,7 +251,7 @@ def pad_and_scale(
|
|
|
242
251
|
a 16" x 9" image with viwebox(0, 0, 14, 7), pad_="1in", print_width_="14in"
|
|
243
252
|
... then scale the printout with dpu_=2 to get a 32" x 18" image with the
|
|
244
253
|
same viewbox. This means the padding will be 2" on all sides, but the image
|
|
245
|
-
will be identical (just twice as
|
|
254
|
+
will be identical (just twice as wide and twice as high) as the 16" x 9" image.
|
|
246
255
|
"""
|
|
247
256
|
pads = expand_pad_arg(pad)
|
|
248
257
|
|
svg_ultralight/query.py
CHANGED
|
@@ -63,7 +63,7 @@ def _fill_ids(*elem_args: EtreeElement) -> None:
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def _normalize_views(elem: EtreeElement) -> None:
|
|
66
|
-
"""Create a square
|
|
66
|
+
"""Create a square viewBox for any element with an svg tag.
|
|
67
67
|
|
|
68
68
|
:param elem: an etree element
|
|
69
69
|
|
|
@@ -110,51 +110,61 @@ def map_elems_to_bounding_boxes(
|
|
|
110
110
|
IMPORTANT: path cannot end with ``.exe``.
|
|
111
111
|
Use something like ``"C:\\Program Files\\Inkscape\\inkscape"``
|
|
112
112
|
:param elem_args: xml element (written to a temporary file then queried)
|
|
113
|
-
:return: svg elements
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
:return: input svg elements and any descendents of those elements mapped
|
|
114
|
+
`BoundingBox(x, y, width, height)`
|
|
115
|
+
So return dict keys are the input elements themselves with one exception: a
|
|
116
|
+
string key, "svg", is mapped to a bounding box around all input elements.
|
|
117
|
+
:effects: temporarily adds an id attribute if any ids are missing. These are
|
|
118
|
+
removed if the function completes. Existing, non-unique ids will break this
|
|
119
|
+
function.
|
|
120
|
+
|
|
121
|
+
Bounding boxes are relative to svg viewBox. If, for instance, viewBox x == -10,
|
|
119
122
|
all bounding-box x values will be offset -10. So, everything is wrapped in a root
|
|
120
|
-
element with a "normalized"
|
|
121
|
-
elements ("child root elements" sounds wrong, but it works)
|
|
122
|
-
normalized as well. This works even with a root element around a
|
|
123
|
-
input elem_args can be root elements or "normal" elements like
|
|
124
|
-
or "text" or a mixture of both.
|
|
123
|
+
element, `envelope` with a "normalized" viewBox, `viewBox=(0, 0, 1, 1)`. That
|
|
124
|
+
way, any child root elements ("child root elements" sounds wrong, but it works)
|
|
125
|
+
viewBoxes are normalized as well. This works even with a root element around a
|
|
126
|
+
root element, so input elem_args can be root elements or "normal" elements like
|
|
127
|
+
"rect", "circle", or "text" or a mixture of both. Bounding boxes output here will
|
|
128
|
+
work as expected in any viewBox.
|
|
125
129
|
|
|
126
130
|
The ``inkscape --query-all svg`` call will return a tuple:
|
|
127
131
|
|
|
128
132
|
(b'svg1,x,y,width,height\\r\\elem1,x,y,width,height\\r\\n', None)
|
|
129
133
|
where x, y, width, and height are strings of numbers.
|
|
130
134
|
|
|
131
|
-
This calls the command and formats the output into a dictionary.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
a
|
|
136
|
-
|
|
135
|
+
This calls the command and formats the output into a dictionary. There is a
|
|
136
|
+
little extra complexity to handle cases with duplicate elements. Inkscape will
|
|
137
|
+
map bounding boxes to element ids *if* those ids are unique. If Inkscape
|
|
138
|
+
encounters a duplicate ID, Inkscape will map the bounding box of that element to
|
|
139
|
+
a string like "rect1". If you pass unequal elements with the same id, I can't
|
|
140
|
+
help you, but you might pass the same element multiple times. If you do this,
|
|
141
|
+
Inkscape will find a bounding box for each occurrence, map the first occurrence
|
|
142
|
+
to the id, then map subsequent occurrences to a string like "rect1". This
|
|
143
|
+
function will handle that.
|
|
137
144
|
"""
|
|
138
145
|
if not elem_args:
|
|
139
146
|
return {}
|
|
140
147
|
_fill_ids(*elem_args)
|
|
141
|
-
envelope = _envelop_copies(*elem_args)
|
|
142
148
|
|
|
149
|
+
envelope = _envelop_copies(*elem_args)
|
|
143
150
|
with NamedTemporaryFile(mode="wb", delete=False, suffix=".svg") as svg_file:
|
|
144
151
|
svg = write_svg(svg_file, envelope)
|
|
145
152
|
with Popen(f'"{inkscape}" --query-all {svg}', stdout=PIPE) as bb_process:
|
|
146
153
|
bb_data = str(bb_process.communicate()[0])[2:-1]
|
|
147
154
|
os.unlink(svg_file.name)
|
|
155
|
+
|
|
148
156
|
bb_strings = re.split(r"[\\r]*\\n", bb_data)[:-1]
|
|
149
157
|
id2bbox = dict(map(_split_bb_string, bb_strings))
|
|
150
158
|
|
|
151
159
|
elem2bbox: dict[EtreeElement | Literal["svg"], BoundingBox] = {}
|
|
152
160
|
for elem in _iter_elems(*elem_args):
|
|
153
|
-
|
|
154
|
-
if
|
|
161
|
+
elem_id = elem.attrib.get("id")
|
|
162
|
+
if not (elem_id): # id removed in a previous loop
|
|
163
|
+
continue
|
|
164
|
+
elem2bbox[elem] = id2bbox[elem_id]
|
|
165
|
+
if elem_id.startswith(_TEMP_ID_PREFIX):
|
|
155
166
|
del elem.attrib["id"]
|
|
156
|
-
|
|
157
|
-
elem2bbox["svg"] = scene_bbox
|
|
167
|
+
elem2bbox["svg"] = BoundingBox.merged(*id2bbox.values())
|
|
158
168
|
return elem2bbox
|
|
159
169
|
|
|
160
170
|
|
|
@@ -52,13 +52,19 @@ def mat_dot(mat1: _Matrix, mat2: _Matrix) -> _Matrix:
|
|
|
52
52
|
return (aa, bb, cc, dd, ee, ff)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def mat_apply(
|
|
55
|
+
def mat_apply(matrix: _Matrix, point: tuple[float, float]) -> tuple[float, float]:
|
|
56
56
|
"""Apply an svg-style transformation matrix to a point.
|
|
57
57
|
|
|
58
|
-
:param mat1: transformation matrix (
|
|
58
|
+
:param mat1: transformation matrix (a, b, c, d, e, f) describing a 3x3 matrix
|
|
59
|
+
with an implied third row of (0, 0, 1)
|
|
60
|
+
[[a, c, e], [b, d, f], [0, 0, 1]]
|
|
59
61
|
:param mat2: point (x, y)
|
|
60
62
|
"""
|
|
61
|
-
|
|
63
|
+
a, b, c, d, e, f = matrix
|
|
64
|
+
x, y = point
|
|
65
|
+
result_x = a * x + c * y + e
|
|
66
|
+
result_y = b * x + d * y + f
|
|
67
|
+
return result_x, result_y
|
|
62
68
|
|
|
63
69
|
|
|
64
70
|
def mat_invert(tmat: _Matrix) -> _Matrix:
|
|
@@ -99,7 +105,7 @@ def get_transform_matrix(elem: EtreeElement) -> _Matrix:
|
|
|
99
105
|
def new_transformation_matrix(
|
|
100
106
|
transformation: _Matrix | None = None,
|
|
101
107
|
*,
|
|
102
|
-
scale: float | None = None,
|
|
108
|
+
scale: tuple[float, float] | float | None = None,
|
|
103
109
|
dx: float | None = None,
|
|
104
110
|
dy: float | None = None,
|
|
105
111
|
) -> _Matrix:
|
|
@@ -109,10 +115,17 @@ def new_transformation_matrix(
|
|
|
109
115
|
svg-style transformation matrix.
|
|
110
116
|
"""
|
|
111
117
|
transformation = transformation or (1, 0, 0, 1, 0, 0)
|
|
112
|
-
|
|
118
|
+
|
|
119
|
+
if isinstance(scale, float):
|
|
120
|
+
scale_x, scale_y = (scale, scale)
|
|
121
|
+
elif scale is None:
|
|
122
|
+
scale_x, scale_y = (1, 1)
|
|
123
|
+
else:
|
|
124
|
+
scale_x, scale_y = cast("tuple[float, float]", scale)
|
|
125
|
+
|
|
113
126
|
dx = dx or 0
|
|
114
127
|
dy = dy or 0
|
|
115
|
-
return mat_dot((
|
|
128
|
+
return mat_dot((scale_x, 0, 0, scale_y, dx, dy), transformation)
|
|
116
129
|
|
|
117
130
|
|
|
118
131
|
def transform_element(elem: EtreeElement, matrix: _Matrix) -> EtreeElement:
|