svg-ultralight 0.48.0__py3-none-any.whl → 0.50.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.

Files changed (37) hide show
  1. svg_ultralight/__init__.py +108 -105
  2. svg_ultralight/animate.py +40 -40
  3. svg_ultralight/attrib_hints.py +13 -14
  4. svg_ultralight/bounding_boxes/__init__.py +5 -5
  5. svg_ultralight/bounding_boxes/bound_helpers.py +189 -189
  6. svg_ultralight/bounding_boxes/padded_text_initializers.py +207 -207
  7. svg_ultralight/bounding_boxes/supports_bounds.py +166 -166
  8. svg_ultralight/bounding_boxes/type_bound_collection.py +71 -71
  9. svg_ultralight/bounding_boxes/type_bound_element.py +65 -65
  10. svg_ultralight/bounding_boxes/type_bounding_box.py +396 -396
  11. svg_ultralight/bounding_boxes/type_padded_text.py +411 -411
  12. svg_ultralight/constructors/__init__.py +14 -14
  13. svg_ultralight/constructors/new_element.py +115 -115
  14. svg_ultralight/font_tools/__init__.py +5 -5
  15. svg_ultralight/font_tools/comp_results.py +295 -293
  16. svg_ultralight/font_tools/font_info.py +793 -792
  17. svg_ultralight/image_ops.py +156 -156
  18. svg_ultralight/inkscape.py +261 -261
  19. svg_ultralight/layout.py +290 -291
  20. svg_ultralight/main.py +183 -198
  21. svg_ultralight/metadata.py +122 -122
  22. svg_ultralight/nsmap.py +36 -36
  23. svg_ultralight/py.typed +5 -0
  24. svg_ultralight/query.py +254 -249
  25. svg_ultralight/read_svg.py +58 -0
  26. svg_ultralight/root_elements.py +87 -87
  27. svg_ultralight/string_conversion.py +244 -244
  28. svg_ultralight/strings/__init__.py +21 -13
  29. svg_ultralight/strings/svg_strings.py +106 -67
  30. svg_ultralight/transformations.py +140 -141
  31. svg_ultralight/unit_conversion.py +247 -248
  32. {svg_ultralight-0.48.0.dist-info → svg_ultralight-0.50.1.dist-info}/METADATA +208 -214
  33. svg_ultralight-0.50.1.dist-info/RECORD +34 -0
  34. svg_ultralight-0.50.1.dist-info/WHEEL +4 -0
  35. svg_ultralight-0.48.0.dist-info/RECORD +0 -34
  36. svg_ultralight-0.48.0.dist-info/WHEEL +0 -5
  37. svg_ultralight-0.48.0.dist-info/top_level.txt +0 -1
svg_ultralight/layout.py CHANGED
@@ -1,291 +1,290 @@
1
- """Manage inferences for pad_ and dpu_ arguments.
2
-
3
- :author: Shay Hill
4
- :created: 2023-02-12
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from collections.abc import Sequence
10
- from typing import Union
11
-
12
- from svg_ultralight.string_conversion import format_number
13
- from svg_ultralight.unit_conversion import Measurement, MeasurementArg
14
-
15
- PadArg = Union[float, str, Measurement, Sequence[Union[float, str, Measurement]]]
16
-
17
-
18
- def expand_pad_arg(pad: PadArg) -> tuple[float, float, float, float]:
19
- """Transform a single value or tuple of values to a 4-tuple of user units.
20
-
21
- :param pad: padding value(s)
22
- :return: 4-tuple of padding values in (scaled) user units
23
-
24
- >>> expand_pad_arg(1)
25
- (1.0, 1.0, 1.0, 1.0)
26
-
27
- >>> expand_pad_arg((1, 2))
28
- (1.0, 2.0, 1.0, 2.0)
29
-
30
- >>> expand_pad_arg("1in")
31
- (96.0, 96.0, 96.0, 96.0)
32
-
33
- >>> expand_pad_arg(("1in", "2in"))
34
- (96.0, 192.0, 96.0, 192.0)
35
-
36
- >>> expand_pad_arg(Measurement("1in"))
37
- (96.0, 96.0, 96.0, 96.0)
38
-
39
- >>> expand_pad_arg((Measurement("1in"), Measurement("2in")))
40
- (96.0, 192.0, 96.0, 192.0)
41
- """
42
- if isinstance(pad, str) or not isinstance(pad, Sequence):
43
- return expand_pad_arg([pad])
44
- as_ms = [m if isinstance(m, Measurement) else Measurement(m) for m in pad]
45
- as_units = [m.value for m in as_ms]
46
- if len(as_units) == 3:
47
- as_units = [*as_units, as_units[1]]
48
- else:
49
- as_units = [as_units[i % len(as_units)] for i in range(4)]
50
- return as_units[0], as_units[1], as_units[2], as_units[3]
51
-
52
-
53
- def pad_viewbox(
54
- viewbox: tuple[float, float, float, float], pads: tuple[float, float, float, float]
55
- ) -> tuple[float, float, float, float]:
56
- """Expand viewbox by padding.
57
-
58
- :param viewbox: viewbox to pad (x, y, width height)
59
- :param pads: padding (top, right, bottom, left)
60
- :return: padded viewbox
61
- """
62
- x, y, width, height = viewbox
63
- top, right, bottom, left = pads
64
- return x - left, y - top, width + left + right, height + top + bottom
65
-
66
-
67
- def _scale_pads(
68
- pads: tuple[float, float, float, float], scale: float
69
- ) -> tuple[float, float, float, float]:
70
- """Scale padding by a factor.
71
-
72
- :param pads: padding to scale (top, right, bottom, left)
73
- :param scale: factor to scale by
74
- :return: scaled padding
75
- """
76
- top, right, bottom, left = pads
77
- return top * scale, right * scale, bottom * scale, left * scale
78
-
79
-
80
- def _infer_scale(
81
- print_h: Measurement, print_w: Measurement, viewbox_h: float, viewbox_w: float
82
- ) -> float:
83
- """Determine size of viewbox units.
84
-
85
- :param print_h: height of print area
86
- :param print_w: width of print area
87
- :param viewbox_h: height of viewbox
88
- :param viewbox_w: width of viewbox
89
- :return: scale factor to apply to viewbox to match print area
90
- :raises ValueError: if no valid scale can be determined
91
-
92
- If one of width or height cannot be used, will defer to the other.
93
-
94
- Will ignore ONE, but not both of these conditions:
95
- * print_w > 0 / viewbox_w == 0
96
- * print_h > 0 / viewbox_h == 0
97
-
98
- Any potential scale would be infinite, so this raises a ValueError
99
-
100
- Will ignore ONE, but not both of these conditions:
101
- * print_w == 0 / viewbox_w > 0
102
- * print_h == 0 / viewbox_h > 0
103
-
104
- The print area is invalid, but there is special handling for this. Interpret
105
- viewbox units as print_w.native_unit and determe print area from viewbox area 1
106
- to 1.
107
-
108
- >>> _infer_scale(Measurement("in"), Measurement("in"), 1, 2)
109
- 96
110
-
111
- Will additionally raise a ValueError for any negative measurement.
112
-
113
- Scaling is safe for zero values. If both are zero, the scaling will be 1.
114
- Padding might add a non-zero value to width or height later, producing a valid
115
- viewbox, but that isn't guaranteed here.
116
- """
117
- if any(x < 0 for x in (print_h.value, print_w.value, viewbox_h, viewbox_w)):
118
- msg = "Negative values are not allowed"
119
- raise ValueError(msg)
120
-
121
- candidate_scales: set[float] = set()
122
- if print_w.value and viewbox_w:
123
- candidate_scales.add(print_w.value / viewbox_w)
124
- if print_h.value and viewbox_h:
125
- candidate_scales.add(print_h.value / viewbox_h)
126
- if candidate_scales:
127
- # size of picture is determined by print area
128
- return min(candidate_scales)
129
- if any([print_w.value, print_h.value]):
130
- msg = "All potential scales would be infinite."
131
- raise ValueError(msg)
132
- # a print unit was given, but not a print size. Size of picture is determined
133
- # by interpreting viewbox dimensions as print_width or print_height units
134
- return print_w.native_unit.value[1]
135
-
136
-
137
- def pad_and_scale(
138
- viewbox: tuple[float, float, float, float],
139
- pad: PadArg,
140
- print_width: MeasurementArg | None = None,
141
- print_height: MeasurementArg | None = None,
142
- dpu: float = 1,
143
- ) -> tuple[tuple[float, float, float, float], dict[str, float | str]]:
144
- """Expand and scale the pad argument. If necessary, scale image.
145
-
146
- :param viewbox: viewbox to pad (x, y, width height)
147
- :param pad: padding to add around image, in user units or inches. If a
148
- sequence, it should be (top, right, bottom, left). if a single float or
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
- :param print_width: width of print area, in user units (float), a string
153
- with a unit specifier (e.g., "452mm"), or just a unit specifier (e.g.,
154
- "pt")
155
- :param print_height: height of print area, in user units (float), a string
156
- with a unit specifier (e.g., "452mm"), or just a unit specifier (e.g.,
157
- "pt")
158
- :param dpu: scale the print units. This is useful when you want to print the
159
- same image at different sizes.
160
- :return: padded viewbox 4-tuple and scaling attributes
161
-
162
- SVGs are built in "user units". An optional width and height (not the
163
- viewbox with and height, these are separate arguments) define the size of
164
- those user units.
165
-
166
- * If the width and height are not specified, the user units are 1 pixel
167
- (1/96th of an inch).
168
-
169
- If the width and height *are* specified, the user units become whatever they
170
- need to be to fit that requirement. For instance, if the viewbox width is 96
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 width is 2 and the
173
- width argument is "1in", then the user units are 1/2 of an inch (i.e., 48
174
- pixels) each, because there are 2 user units in an inch. If the viewbox
175
- width is 3 and the width argument is "1yd", the each user unit is 1 foot.
176
-
177
- To pad around the viewbox, we need to first figure out what the user units
178
- are then scale the padding so it will print (or display) correctly. For
179
- instance, if
180
-
181
- * the viewbox width is 3;
182
- * the width argument is "1yd"; and
183
- * the pad argument is "1in"
184
-
185
- the printed result will be 38" wide. That's 1yd for the width plus 1 inch of
186
- padding on each side. The viewbox will have 1/12 of a unit (3 user units
187
- over 1 yard = 1 foot per user unit) added on each side.
188
-
189
- Ideally, we know the size of the print or display area from the beginning
190
- and build the geometry out at whatever size we want, so no scaling is
191
- necessarily required. Even that won't always work, because some software
192
- doesn't like "user units" and insists on 'pt' or 'in'. If everything is
193
- already in 'pt' or 'in' and you want to keep it that way, just call the
194
- function with print_width="pt" or print_height="in". The function will add
195
- the unit designators without changing the scale.
196
-
197
- Print aspect ratio is ignored. Viewbox aspect ratio is preserved. For
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.
213
-
214
- Scaling attributes are returned as a dictonary that can be "exploded" into
215
- the element constructor, e.g., {"width": "12.5in", "height": "12.5in"}.
216
-
217
- * If neither a print_width nor print_height is specified, no scaling
218
- attributes will be returned.
219
-
220
- * If either is specified, both a width and height will be returned (even if
221
- one argument is None). These will always match the viewbox aspect ratio,
222
- so there is no additional information supplied by giving both, but I've
223
- had unexpected behavior from pandoc when one was missing.
224
-
225
- * If only a unit is given, (e.g., "pt"), the user units (viewbox width and
226
- height) will be interpreted as that unit. This is important for InDesign,
227
- which may not display in image at all if the width and height are not
228
- explicitly "pt".
229
-
230
- * Print ratios are discarded. The viwebox ratio is preserved. For instance,
231
- if the viewbox is (0, 0, 16, 9), giving a 16:9 aspect ratio and the
232
- print_width and print_height are both 100, giving a 1:1 aspect ratio, the
233
- output scaling attributes will be {"width": "100", "height", "56.25"},
234
- preserving viewbox aspect ratio with a "best fit" scaling (i.e, the image
235
- is as large as it can be without exceeding the specified print area).
236
-
237
- You can pass something impossible like a viewbox width of 1 and a print box
238
- of 0. The function will give up, set scaling to 1, and pad the viewbox. This
239
- does not try to guard against bad values sent to lxml.
240
-
241
- All of the above is important when you want your padding in real-world units
242
- (e.g., when you need to guarantee a certain amount of padding above and
243
- below an image in a book layout). However, it does add some complexity,
244
- because aspect ratio is not maintained when print_width increases. Worse, if
245
- there is some geomtry like a background pattern in your padding, then more
246
- or less of that pattern will be visible depending on the print_width.
247
-
248
- That's not hard to work around, just change the padding every time you
249
- change the width. Or, to make it even simpler, use the dpu argument. The dpu
250
- argument will scale the width and the padding together. So, you can produce
251
- a 16" x 9" image with viwebox(0, 0, 14, 7), pad_="1in", print_width_="14in"
252
- ... then scale the printout with dpu_=2 to get a 32" x 18" image with the
253
- same viewbox. This means the padding will be 2" on all sides, but the image
254
- will be identical (just twice as wide and twice as high) as the 16" x 9" image.
255
- """
256
- pads = expand_pad_arg(pad)
257
-
258
- # no print information given, pad and return viewbox
259
- if print_width is None and print_height is None:
260
- padded = pad_viewbox(viewbox, pads)
261
- dims: dict[str, float | str] = {}
262
- if dpu != 1:
263
- dims["width"] = format_number(padded[2] * dpu)
264
- dims["height"] = format_number(padded[3] * dpu)
265
- return padded, dims
266
-
267
- _, _, viewbox_w, viewbox_h = viewbox
268
- print_w = Measurement(print_width or 0)
269
- print_h = Measurement(print_height or 0)
270
-
271
- # match unspecified (None) width or height units.
272
- if print_width is None:
273
- print_w.native_unit = print_h.native_unit
274
- elif print_height is None:
275
- print_h.native_unit = print_w.native_unit
276
-
277
- scale = _infer_scale(print_h, print_w, viewbox_h, viewbox_w)
278
-
279
- print_w.value = viewbox_w * scale
280
- print_h.value = viewbox_h * scale
281
-
282
- # add padding and increase print area
283
- print_w.value += pads[1] + pads[3]
284
- print_h.value += pads[0] + pads[2]
285
-
286
- # scale pads to viewbox to match input size when later scaled to print area
287
- padded_viewbox = pad_viewbox(viewbox, _scale_pads(pads, 1 / scale))
288
- return padded_viewbox, {
289
- "width": (print_w * dpu).get_svg(print_w.native_unit),
290
- "height": (print_h * dpu).get_svg(print_h.native_unit),
291
- }
1
+ """Manage inferences for pad_ and dpu_ arguments.
2
+
3
+ :author: Shay Hill
4
+ :created: 2023-02-12
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Sequence
10
+
11
+ from svg_ultralight.string_conversion import format_number
12
+ from svg_ultralight.unit_conversion import Measurement, MeasurementArg
13
+
14
+ PadArg = float | str | Measurement | Sequence[float | str | Measurement]
15
+
16
+
17
+ def expand_pad_arg(pad: PadArg) -> tuple[float, float, float, float]:
18
+ """Transform a single value or tuple of values to a 4-tuple of user units.
19
+
20
+ :param pad: padding value(s)
21
+ :return: 4-tuple of padding values in (scaled) user units
22
+
23
+ >>> expand_pad_arg(1)
24
+ (1.0, 1.0, 1.0, 1.0)
25
+
26
+ >>> expand_pad_arg((1, 2))
27
+ (1.0, 2.0, 1.0, 2.0)
28
+
29
+ >>> expand_pad_arg("1in")
30
+ (96.0, 96.0, 96.0, 96.0)
31
+
32
+ >>> expand_pad_arg(("1in", "2in"))
33
+ (96.0, 192.0, 96.0, 192.0)
34
+
35
+ >>> expand_pad_arg(Measurement("1in"))
36
+ (96.0, 96.0, 96.0, 96.0)
37
+
38
+ >>> expand_pad_arg((Measurement("1in"), Measurement("2in")))
39
+ (96.0, 192.0, 96.0, 192.0)
40
+ """
41
+ if isinstance(pad, str) or not isinstance(pad, Sequence):
42
+ return expand_pad_arg([pad])
43
+ as_ms = [m if isinstance(m, Measurement) else Measurement(m) for m in pad]
44
+ as_units = [m.value for m in as_ms]
45
+ if len(as_units) == 3:
46
+ as_units = [*as_units, as_units[1]]
47
+ else:
48
+ as_units = [as_units[i % len(as_units)] for i in range(4)]
49
+ return as_units[0], as_units[1], as_units[2], as_units[3]
50
+
51
+
52
+ def pad_viewbox(
53
+ viewbox: tuple[float, float, float, float], pads: tuple[float, float, float, float]
54
+ ) -> tuple[float, float, float, float]:
55
+ """Expand viewbox by padding.
56
+
57
+ :param viewbox: viewbox to pad (x, y, width height)
58
+ :param pads: padding (top, right, bottom, left)
59
+ :return: padded viewbox
60
+ """
61
+ x, y, width, height = viewbox
62
+ top, right, bottom, left = pads
63
+ return x - left, y - top, width + left + right, height + top + bottom
64
+
65
+
66
+ def _scale_pads(
67
+ pads: tuple[float, float, float, float], scale: float
68
+ ) -> tuple[float, float, float, float]:
69
+ """Scale padding by a factor.
70
+
71
+ :param pads: padding to scale (top, right, bottom, left)
72
+ :param scale: factor to scale by
73
+ :return: scaled padding
74
+ """
75
+ top, right, bottom, left = pads
76
+ return top * scale, right * scale, bottom * scale, left * scale
77
+
78
+
79
+ def _infer_scale(
80
+ print_h: Measurement, print_w: Measurement, viewbox_h: float, viewbox_w: float
81
+ ) -> float:
82
+ """Determine size of viewbox units.
83
+
84
+ :param print_h: height of print area
85
+ :param print_w: width of print area
86
+ :param viewbox_h: height of viewbox
87
+ :param viewbox_w: width of viewbox
88
+ :return: scale factor to apply to viewbox to match print area
89
+ :raises ValueError: if no valid scale can be determined
90
+
91
+ If one of width or height cannot be used, will defer to the other.
92
+
93
+ Will ignore ONE, but not both of these conditions:
94
+ * print_w > 0 / viewbox_w == 0
95
+ * print_h > 0 / viewbox_h == 0
96
+
97
+ Any potential scale would be infinite, so this raises a ValueError
98
+
99
+ Will ignore ONE, but not both of these conditions:
100
+ * print_w == 0 / viewbox_w > 0
101
+ * print_h == 0 / viewbox_h > 0
102
+
103
+ The print area is invalid, but there is special handling for this. Interpret
104
+ viewbox units as print_w.native_unit and determe print area from viewbox area 1
105
+ to 1.
106
+
107
+ >>> _infer_scale(Measurement("in"), Measurement("in"), 1, 2)
108
+ 96
109
+
110
+ Will additionally raise a ValueError for any negative measurement.
111
+
112
+ Scaling is safe for zero values. If both are zero, the scaling will be 1.
113
+ Padding might add a non-zero value to width or height later, producing a valid
114
+ viewbox, but that isn't guaranteed here.
115
+ """
116
+ if any(x < 0 for x in (print_h.value, print_w.value, viewbox_h, viewbox_w)):
117
+ msg = "Negative values are not allowed"
118
+ raise ValueError(msg)
119
+
120
+ candidate_scales: set[float] = set()
121
+ if print_w.value and viewbox_w:
122
+ candidate_scales.add(print_w.value / viewbox_w)
123
+ if print_h.value and viewbox_h:
124
+ candidate_scales.add(print_h.value / viewbox_h)
125
+ if candidate_scales:
126
+ # size of picture is determined by print area
127
+ return min(candidate_scales)
128
+ if any([print_w.value, print_h.value]):
129
+ msg = "All potential scales would be infinite."
130
+ raise ValueError(msg)
131
+ # a print unit was given, but not a print size. Size of picture is determined
132
+ # by interpreting viewbox dimensions as print_width or print_height units
133
+ return print_w.native_unit.value[1]
134
+
135
+
136
+ def pad_and_scale(
137
+ viewbox: tuple[float, float, float, float],
138
+ pad: PadArg,
139
+ print_width: MeasurementArg | None = None,
140
+ print_height: MeasurementArg | None = None,
141
+ dpu: float = 1,
142
+ ) -> tuple[tuple[float, float, float, float], dict[str, float | str]]:
143
+ """Expand and scale the pad argument. If necessary, scale image.
144
+
145
+ :param viewbox: viewbox to pad (x, y, width height)
146
+ :param pad: padding to add around image, in user units or inches. If a
147
+ sequence, it should be (top, right, bottom, left). if a single float or
148
+ string, it will be applied to all sides. If two floats, top and bottom
149
+ then left and right. If three floats, top, left and right, then bottom.
150
+ If four floats, top, right, bottom, left.
151
+ :param print_width: width of print area, in user units (float), a string
152
+ with a unit specifier (e.g., "452mm"), or just a unit specifier (e.g.,
153
+ "pt")
154
+ :param print_height: height of print area, in user units (float), a string
155
+ with a unit specifier (e.g., "452mm"), or just a unit specifier (e.g.,
156
+ "pt")
157
+ :param dpu: scale the print units. This is useful when you want to print the
158
+ same image at different sizes.
159
+ :return: padded viewbox 4-tuple and scaling attributes
160
+
161
+ SVGs are built in "user units". An optional width and height (not the
162
+ viewbox with and height, these are separate arguments) define the size of
163
+ those user units.
164
+
165
+ * If the width and height are not specified, the user units are 1 pixel
166
+ (1/96th of an inch).
167
+
168
+ If the width and height *are* specified, the user units become whatever they
169
+ need to be to fit that requirement. For instance, if the viewbox width is 96
170
+ and the width argument is "1in", then the user units are *still* pixels,
171
+ because there are 96 pixels in an inch. If the viewbox width is 2 and the
172
+ width argument is "1in", then the user units are 1/2 of an inch (i.e., 48
173
+ pixels) each, because there are 2 user units in an inch. If the viewbox
174
+ width is 3 and the width argument is "1yd", the each user unit is 1 foot.
175
+
176
+ To pad around the viewbox, we need to first figure out what the user units
177
+ are then scale the padding so it will print (or display) correctly. For
178
+ instance, if
179
+
180
+ * the viewbox width is 3;
181
+ * the width argument is "1yd"; and
182
+ * the pad argument is "1in"
183
+
184
+ the printed result will be 38" wide. That's 1yd for the width plus 1 inch of
185
+ padding on each side. The viewbox will have 1/12 of a unit (3 user units
186
+ over 1 yard = 1 foot per user unit) added on each side.
187
+
188
+ Ideally, we know the size of the print or display area from the beginning
189
+ and build the geometry out at whatever size we want, so no scaling is
190
+ necessarily required. Even that won't always work, because some software
191
+ doesn't like "user units" and insists on 'pt' or 'in'. If everything is
192
+ already in 'pt' or 'in' and you want to keep it that way, just call the
193
+ function with print_width="pt" or print_height="in". The function will add
194
+ the unit designators without changing the scale.
195
+
196
+ Print aspect ratio is ignored. Viewbox aspect ratio is preserved. For
197
+ instance, if you created two images
198
+
199
+ * x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="6in"
200
+
201
+ * x_=0, y_=0, width_=1, height_=2, pad_="0.25in", print_width_="12in"
202
+
203
+ ... (note that the images only vary in print_width_), the first image would be
204
+ rendered at 6.5x12.5 inches and the second at 12.5x24.5 inches. The visible
205
+ content in the viewbox would be exactly twice as wide in the larger image, but
206
+ the padding would remain 0.25 in both images. Despite setting `print_width_` to
207
+ exactly 6 or 12 inches, you would not get an image exactly 6 or 12 inches wide.
208
+ Despite a viewbox aspect ratio of 1:2, you would not get an output image of
209
+ exactly 1:2. If you want to use padding and need a specific output image size or
210
+ aspect ratio, remember to subtract the padding width from your print_width or
211
+ print_height.
212
+
213
+ Scaling attributes are returned as a dictonary that can be "exploded" into
214
+ the element constructor, e.g., {"width": "12.5in", "height": "12.5in"}.
215
+
216
+ * If neither a print_width nor print_height is specified, no scaling
217
+ attributes will be returned.
218
+
219
+ * If either is specified, both a width and height will be returned (even if
220
+ one argument is None). These will always match the viewbox aspect ratio,
221
+ so there is no additional information supplied by giving both, but I've
222
+ had unexpected behavior from pandoc when one was missing.
223
+
224
+ * If only a unit is given, (e.g., "pt"), the user units (viewbox width and
225
+ height) will be interpreted as that unit. This is important for InDesign,
226
+ which may not display in image at all if the width and height are not
227
+ explicitly "pt".
228
+
229
+ * Print ratios are discarded. The viwebox ratio is preserved. For instance,
230
+ if the viewbox is (0, 0, 16, 9), giving a 16:9 aspect ratio and the
231
+ print_width and print_height are both 100, giving a 1:1 aspect ratio, the
232
+ output scaling attributes will be {"width": "100", "height", "56.25"},
233
+ preserving viewbox aspect ratio with a "best fit" scaling (i.e, the image
234
+ is as large as it can be without exceeding the specified print area).
235
+
236
+ You can pass something impossible like a viewbox width of 1 and a print box
237
+ of 0. The function will give up, set scaling to 1, and pad the viewbox. This
238
+ does not try to guard against bad values sent to lxml.
239
+
240
+ All of the above is important when you want your padding in real-world units
241
+ (e.g., when you need to guarantee a certain amount of padding above and
242
+ below an image in a book layout). However, it does add some complexity,
243
+ because aspect ratio is not maintained when print_width increases. Worse, if
244
+ there is some geomtry like a background pattern in your padding, then more
245
+ or less of that pattern will be visible depending on the print_width.
246
+
247
+ That's not hard to work around, just change the padding every time you
248
+ change the width. Or, to make it even simpler, use the dpu argument. The dpu
249
+ argument will scale the width and the padding together. So, you can produce
250
+ a 16" x 9" image with viwebox(0, 0, 14, 7), pad_="1in", print_width_="14in"
251
+ ... then scale the printout with dpu_=2 to get a 32" x 18" image with the
252
+ same viewbox. This means the padding will be 2" on all sides, but the image
253
+ will be identical (just twice as wide and twice as high) as the 16" x 9" image.
254
+ """
255
+ pads = expand_pad_arg(pad)
256
+
257
+ # no print information given, pad and return viewbox
258
+ if print_width is None and print_height is None:
259
+ padded = pad_viewbox(viewbox, pads)
260
+ dims: dict[str, float | str] = {}
261
+ if dpu != 1:
262
+ dims["width"] = format_number(padded[2] * dpu)
263
+ dims["height"] = format_number(padded[3] * dpu)
264
+ return padded, dims
265
+
266
+ _, _, viewbox_w, viewbox_h = viewbox
267
+ print_w = Measurement(print_width or 0)
268
+ print_h = Measurement(print_height or 0)
269
+
270
+ # match unspecified (None) width or height units.
271
+ if print_width is None:
272
+ print_w.native_unit = print_h.native_unit
273
+ elif print_height is None:
274
+ print_h.native_unit = print_w.native_unit
275
+
276
+ scale = _infer_scale(print_h, print_w, viewbox_h, viewbox_w)
277
+
278
+ print_w.value = viewbox_w * scale
279
+ print_h.value = viewbox_h * scale
280
+
281
+ # add padding and increase print area
282
+ print_w.value += pads[1] + pads[3]
283
+ print_h.value += pads[0] + pads[2]
284
+
285
+ # scale pads to viewbox to match input size when later scaled to print area
286
+ padded_viewbox = pad_viewbox(viewbox, _scale_pads(pads, 1 / scale))
287
+ return padded_viewbox, {
288
+ "width": (print_w * dpu).get_svg(print_w.native_unit),
289
+ "height": (print_h * dpu).get_svg(print_h.native_unit),
290
+ }