plotstyle 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. plotstyle/__init__.py +121 -0
  2. plotstyle/_utils/__init__.py +0 -0
  3. plotstyle/_utils/io.py +113 -0
  4. plotstyle/_utils/warnings.py +86 -0
  5. plotstyle/_version.py +24 -0
  6. plotstyle/cli/__init__.py +0 -0
  7. plotstyle/cli/main.py +553 -0
  8. plotstyle/color/__init__.py +42 -0
  9. plotstyle/color/_rendering.py +86 -0
  10. plotstyle/color/accessibility.py +286 -0
  11. plotstyle/color/data/okabe_ito.json +5 -0
  12. plotstyle/color/data/safe_grayscale.json +7 -0
  13. plotstyle/color/data/tol_bright.json +5 -0
  14. plotstyle/color/data/tol_muted.json +5 -0
  15. plotstyle/color/data/tol_vibrant.json +5 -0
  16. plotstyle/color/grayscale.py +284 -0
  17. plotstyle/color/palettes.py +259 -0
  18. plotstyle/core/__init__.py +0 -0
  19. plotstyle/core/export.py +418 -0
  20. plotstyle/core/figure.py +394 -0
  21. plotstyle/core/migrate.py +579 -0
  22. plotstyle/core/style.py +394 -0
  23. plotstyle/engine/__init__.py +0 -0
  24. plotstyle/engine/fonts.py +309 -0
  25. plotstyle/engine/latex.py +287 -0
  26. plotstyle/engine/rcparams.py +352 -0
  27. plotstyle/integrations/__init__.py +0 -0
  28. plotstyle/integrations/seaborn.py +305 -0
  29. plotstyle/preview/__init__.py +50 -0
  30. plotstyle/preview/gallery.py +337 -0
  31. plotstyle/preview/print_size.py +304 -0
  32. plotstyle/py.typed +0 -0
  33. plotstyle/specs/__init__.py +304 -0
  34. plotstyle/specs/_templates.toml +48 -0
  35. plotstyle/specs/acs.toml +36 -0
  36. plotstyle/specs/cell.toml +35 -0
  37. plotstyle/specs/elsevier.toml +35 -0
  38. plotstyle/specs/ieee.toml +35 -0
  39. plotstyle/specs/nature.toml +35 -0
  40. plotstyle/specs/plos.toml +35 -0
  41. plotstyle/specs/prl.toml +35 -0
  42. plotstyle/specs/schema.py +1095 -0
  43. plotstyle/specs/science.toml +35 -0
  44. plotstyle/specs/springer.toml +35 -0
  45. plotstyle/specs/units.py +761 -0
  46. plotstyle/specs/wiley.toml +35 -0
  47. plotstyle/validation/__init__.py +94 -0
  48. plotstyle/validation/checks/__init__.py +95 -0
  49. plotstyle/validation/checks/_base.py +149 -0
  50. plotstyle/validation/checks/colors.py +394 -0
  51. plotstyle/validation/checks/dimensions.py +166 -0
  52. plotstyle/validation/checks/export.py +205 -0
  53. plotstyle/validation/checks/lines.py +147 -0
  54. plotstyle/validation/checks/typography.py +200 -0
  55. plotstyle/validation/report.py +293 -0
  56. plotstyle-0.1.0a1.dist-info/METADATA +271 -0
  57. plotstyle-0.1.0a1.dist-info/RECORD +60 -0
  58. plotstyle-0.1.0a1.dist-info/WHEEL +4 -0
  59. plotstyle-0.1.0a1.dist-info/entry_points.txt +2 -0
  60. plotstyle-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,761 @@
1
+ """Physical dimension and font-size types with unit conversion.
2
+
3
+ This module provides strongly-typed, immutable value objects for representing
4
+ physical measurements (:class:`Dimension`) and typographic sizes
5
+ (:class:`FontSize`). Both types share a common base (:class:`_Measurement`)
6
+ to eliminate duplication while exposing domain-specific convenience methods
7
+ on each subclass.
8
+
9
+ Supported units
10
+ ---------------
11
+ ``mm``, ``cm``, ``in``, ``pt``, ``pica``
12
+
13
+ Public types
14
+ ------------
15
+ :data:`Unit`
16
+ Literal type enumerating every supported unit string.
17
+
18
+ :class:`Dimension`
19
+ Spatial measurement (widths, heights, margins).
20
+
21
+ :class:`FontSize`
22
+ Typographic measurement (font sizes).
23
+
24
+ Exceptions
25
+ ----------
26
+ :class:`DimensionError`
27
+ Base exception; all unit-related errors inherit from this.
28
+
29
+ :class:`UnsupportedUnitError`
30
+ Raised when an unrecognised unit string is encountered.
31
+
32
+ :class:`IncompatibleUnitsError`
33
+ Raised when two measurements of different concrete types are combined.
34
+
35
+ Typical usage
36
+ -------------
37
+ ::
38
+
39
+ from plotstyle.specs.units import Dimension, FontSize
40
+
41
+ width = Dimension(210, "mm") # A4 width
42
+ body = FontSize(10, "pt")
43
+
44
+ print(width.to_inches()) # 8.267716535433071
45
+ print(body.to_mm()) # 3.527777777777778
46
+
47
+ Design notes
48
+ ------------
49
+ * All values are stored internally as plain Python ``float`` — no external
50
+ numeric library is required.
51
+ * Conversion is performed via a single canonical intermediate unit
52
+ (millimetres) to keep the conversion table O(n) rather than O(n²).
53
+ * Both public classes are *frozen* dataclasses, making them hashable and
54
+ safe to use as dictionary keys or in sets.
55
+ * ``slots=True`` avoids the per-instance ``__dict__`` overhead, which
56
+ matters when thousands of measurement objects are created at once.
57
+ """
58
+
59
+ from __future__ import annotations
60
+
61
+ import math
62
+ from dataclasses import dataclass
63
+ from typing import Final, Literal, TypeVar
64
+
65
+ __all__: list[str] = [
66
+ "Dimension",
67
+ "DimensionError",
68
+ "FontSize",
69
+ "IncompatibleUnitsError",
70
+ "Unit",
71
+ "UnsupportedUnitError",
72
+ ]
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Public type alias
76
+ # ---------------------------------------------------------------------------
77
+
78
+ #: Literal type enumerating every supported measurement unit.
79
+ Unit = Literal["mm", "cm", "in", "pt", "pica"]
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Conversion table
83
+ # ---------------------------------------------------------------------------
84
+
85
+ # All factors convert *from* the named unit *to* millimetres.
86
+ # Using millimetres as the canonical pivot keeps the table compact: adding a
87
+ # new unit requires only one new entry rather than N new entries.
88
+ _TO_MM: Final[dict[str, float]] = {
89
+ "mm": 1.0,
90
+ "cm": 10.0,
91
+ "in": 25.4,
92
+ "pt": 25.4 / 72.0, # 1 pt = 1/72 inch (PostScript point)
93
+ "pica": 25.4 / 72.0 * 12.0, # 1 pica = 12 pt
94
+ }
95
+
96
+ # Self-type for arithmetic operators defined on the base class.
97
+ _T = TypeVar("_T", bound="_Measurement")
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # Custom exceptions
102
+ # ---------------------------------------------------------------------------
103
+
104
+
105
+ class DimensionError(ValueError):
106
+ """Base exception for errors raised by this module.
107
+
108
+ Inherits from :class:`ValueError` so callers that catch the built-in
109
+ exception continue to work without modification.
110
+ """
111
+
112
+
113
+ class UnsupportedUnitError(DimensionError):
114
+ """Raised when an unrecognised or unsupported unit string is encountered.
115
+
116
+ Args:
117
+ unit: The offending unit string.
118
+
119
+ Attributes
120
+ ----------
121
+ unit
122
+ The unit string that was rejected.
123
+
124
+ Example::
125
+
126
+ raise UnsupportedUnitError("furlong")
127
+ # UnsupportedUnitError: Unknown unit 'furlong'. Supported units: cm, in, mm, pica, pt
128
+ """
129
+
130
+ def __init__(self, unit: str) -> None:
131
+ supported = ", ".join(sorted(_TO_MM))
132
+ super().__init__(f"Unknown unit {unit!r}. Supported units: {supported}")
133
+ self.unit: str = unit
134
+
135
+
136
+ class IncompatibleUnitsError(DimensionError):
137
+ """Raised when two measurements with incompatible types are combined.
138
+
139
+ Guards against accidentally adding a :class:`Dimension` to a
140
+ :class:`FontSize` — they are semantically distinct even though both
141
+ store a numeric value and a unit.
142
+
143
+ Args:
144
+ left: The left-hand operand type name.
145
+ right: The right-hand operand type name.
146
+
147
+ Attributes
148
+ ----------
149
+ left
150
+ Type name of the left-hand operand.
151
+ right
152
+ Type name of the right-hand operand.
153
+
154
+ Example::
155
+
156
+ Dimension(10, "mm") + FontSize(5, "pt")
157
+ # IncompatibleUnitsError: Cannot combine 'Dimension' with 'FontSize': …
158
+ """
159
+
160
+ def __init__(self, left: str, right: str) -> None:
161
+ super().__init__(
162
+ f"Cannot combine {left!r} with {right!r}: operands must be the same measurement type."
163
+ )
164
+ self.left: str = left
165
+ self.right: str = right
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # Internal helpers
170
+ # ---------------------------------------------------------------------------
171
+
172
+
173
+ def _validate_unit(unit: str) -> Unit:
174
+ """Return *unit* unchanged if it is supported, otherwise raise.
175
+
176
+ Args:
177
+ unit: Arbitrary string to validate against the supported-unit table.
178
+
179
+ Returns
180
+ -------
181
+ The same string, narrowed to the :data:`Unit` literal type.
182
+
183
+ Raises
184
+ ------
185
+ UnsupportedUnitError: If *unit* is not a key in :data:`_TO_MM`.
186
+
187
+ Example::
188
+
189
+ >>> _validate_unit("mm")
190
+ 'mm'
191
+ >>> _validate_unit("furlong")
192
+ UnsupportedUnitError: Unknown unit 'furlong'. Supported units: …
193
+ """
194
+ if unit not in _TO_MM:
195
+ raise UnsupportedUnitError(unit)
196
+ return unit # type: ignore[return-value]
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Base class — not part of the public API
201
+ # ---------------------------------------------------------------------------
202
+
203
+
204
+ @dataclass(frozen=True, slots=True)
205
+ class _Measurement:
206
+ """Immutable base for physical measurements.
207
+
208
+ Stores a (value, unit) pair and provides the unit-conversion engine
209
+ shared by :class:`Dimension` and :class:`FontSize`. End users should
210
+ not instantiate this class directly.
211
+
212
+ Attributes
213
+ ----------
214
+ value
215
+ The numeric magnitude of the measurement.
216
+ unit
217
+ The unit in which *value* is expressed.
218
+
219
+ Notes
220
+ -----
221
+ ``slots=True`` is used for memory efficiency: slot-based dataclasses
222
+ avoid the per-instance ``__dict__`` overhead, which is meaningful when
223
+ thousands of measurement objects are created (e.g. in a layout engine).
224
+
225
+ The class is ``frozen`` (immutable) so instances are hashable and can
226
+ be used as dictionary keys or cached safely.
227
+ """
228
+
229
+ value: float
230
+ unit: Unit
231
+
232
+ def __post_init__(self) -> None:
233
+ """Validate *unit* and coerce *value* to ``float`` after construction.
234
+
235
+ Raises
236
+ ------
237
+ UnsupportedUnitError: If *unit* is not a recognised unit string.
238
+ TypeError: If *value* cannot be converted to ``float``.
239
+ """
240
+ # Validate and normalise the unit first so the error is descriptive.
241
+ object.__setattr__(self, "unit", _validate_unit(self.unit))
242
+ # Coerce value to float; this catches non-numeric inputs early.
243
+ object.__setattr__(self, "value", float(self.value))
244
+
245
+ # ------------------------------------------------------------------
246
+ # Core conversion
247
+ # ------------------------------------------------------------------
248
+
249
+ def _to_mm_raw(self) -> float:
250
+ """Return the measurement's value expressed in millimetres.
251
+
252
+ This is the single point where unit→mm conversion happens; all
253
+ other conversion methods delegate here to keep rounding errors
254
+ consistent across the unit system.
255
+
256
+ Returns
257
+ -------
258
+ The magnitude in millimetres as a ``float``.
259
+ """
260
+ return self.value * _TO_MM[self.unit]
261
+
262
+ def to(self, target_unit: str) -> float:
263
+ """Convert to any supported target unit.
264
+
265
+ Args:
266
+ target_unit: Destination unit string (e.g. ``"in"``, ``"pt"``).
267
+
268
+ Returns
269
+ -------
270
+ The measurement's magnitude expressed in *target_unit*.
271
+
272
+ Raises
273
+ ------
274
+ UnsupportedUnitError: If *target_unit* is not a recognised unit.
275
+
276
+ Example::
277
+
278
+ >>> Dimension(2.54, "cm").to("in")
279
+ 1.0
280
+ >>> Dimension(72, "pt").to("in")
281
+ 1.0
282
+ """
283
+ validated: Unit = _validate_unit(target_unit)
284
+ return self._to_mm_raw() / _TO_MM[validated]
285
+
286
+ # ------------------------------------------------------------------
287
+ # Arithmetic operators
288
+ # ------------------------------------------------------------------
289
+
290
+ def _check_compatible(self, other: object) -> _Measurement:
291
+ """Assert that *other* is the same concrete type as *self*.
292
+
293
+ Args:
294
+ other: The right-hand operand to inspect.
295
+
296
+ Returns
297
+ -------
298
+ *other* cast to :class:`_Measurement` (for further use).
299
+
300
+ Raises
301
+ ------
302
+ IncompatibleUnitsError: If *other* is a different measurement
303
+ subclass (e.g. mixing :class:`Dimension` with
304
+ :class:`FontSize`).
305
+ TypeError: If *other* is not a :class:`_Measurement` subclass
306
+ at all.
307
+ """
308
+ if not isinstance(other, _Measurement):
309
+ return NotImplemented # type: ignore[return-value]
310
+ if type(self) is not type(other):
311
+ raise IncompatibleUnitsError(type(self).__name__, type(other).__name__)
312
+ return other
313
+
314
+ def __add__(self: _T, other: object) -> _T:
315
+ """Return a new measurement equal to ``self + other``.
316
+
317
+ The result is expressed in *self*'s unit.
318
+
319
+ Args:
320
+ other: A measurement of the same concrete type.
321
+
322
+ Returns
323
+ -------
324
+ A new instance of the same type with the summed value, in
325
+ *self*'s unit.
326
+
327
+ Raises
328
+ ------
329
+ IncompatibleUnitsError: If *other* is a different measurement type.
330
+ TypeError: If *other* is not a measurement object.
331
+
332
+ Example::
333
+
334
+ >>> Dimension(10, "mm") + Dimension(1, "cm")
335
+ Dimension(value=20.0, unit='mm')
336
+ """
337
+ rhs = self._check_compatible(other)
338
+ if rhs is NotImplemented:
339
+ return NotImplemented # type: ignore[return-value]
340
+ # Convert rhs to self's unit before adding to preserve self's unit.
341
+ return type(self)(self.value + rhs.to(self.unit), self.unit) # type: ignore[return-value]
342
+
343
+ def __sub__(self: _T, other: object) -> _T:
344
+ """Return a new measurement equal to ``self - other``.
345
+
346
+ The result is expressed in *self*'s unit.
347
+
348
+ Args:
349
+ other: A measurement of the same concrete type.
350
+
351
+ Returns
352
+ -------
353
+ A new instance of the same type with the difference, in
354
+ *self*'s unit.
355
+
356
+ Raises
357
+ ------
358
+ IncompatibleUnitsError: If *other* is a different measurement type.
359
+ TypeError: If *other* is not a measurement object.
360
+
361
+ Example::
362
+
363
+ >>> Dimension(10, "cm") - Dimension(25, "mm")
364
+ Dimension(value=7.5, unit='cm')
365
+ """
366
+ rhs = self._check_compatible(other)
367
+ if rhs is NotImplemented:
368
+ return NotImplemented # type: ignore[return-value]
369
+ return type(self)(self.value - rhs.to(self.unit), self.unit) # type: ignore[return-value]
370
+
371
+ def __mul__(self: _T, scalar: float) -> _T:
372
+ """Scale a measurement by a dimensionless scalar.
373
+
374
+ Args:
375
+ scalar: A real-valued multiplier.
376
+
377
+ Returns
378
+ -------
379
+ A new instance with ``value * scalar``, in the same unit.
380
+
381
+ Raises
382
+ ------
383
+ TypeError: If *scalar* is not numeric.
384
+
385
+ Example::
386
+
387
+ >>> Dimension(5, "cm") * 3
388
+ Dimension(value=15.0, unit='cm')
389
+ """
390
+ if not isinstance(scalar, (int, float)):
391
+ return NotImplemented # type: ignore[return-value]
392
+ return type(self)(self.value * scalar, self.unit) # type: ignore[return-value]
393
+
394
+ #: Support ``scalar * measurement`` as well as ``measurement * scalar``.
395
+ __rmul__ = __mul__
396
+
397
+ def __truediv__(self: _T, scalar: float) -> _T:
398
+ """Divide a measurement by a dimensionless scalar.
399
+
400
+ Args:
401
+ scalar: A non-zero real-valued divisor.
402
+
403
+ Returns
404
+ -------
405
+ A new instance with ``value / scalar``, in the same unit.
406
+
407
+ Raises
408
+ ------
409
+ TypeError: If *scalar* is not numeric.
410
+ ZeroDivisionError: If *scalar* is zero.
411
+
412
+ Example::
413
+
414
+ >>> Dimension(30, "mm") / 2
415
+ Dimension(value=15.0, unit='mm')
416
+ """
417
+ if not isinstance(scalar, (int, float)):
418
+ return NotImplemented # type: ignore[return-value]
419
+ if scalar == 0:
420
+ raise ZeroDivisionError("Cannot divide a measurement by zero.")
421
+ return type(self)(self.value / scalar, self.unit) # type: ignore[return-value]
422
+
423
+ # ------------------------------------------------------------------
424
+ # Comparison
425
+ # ------------------------------------------------------------------
426
+
427
+ def __lt__(self, other: object) -> bool:
428
+ """Return ``True`` if *self* is strictly less than *other*.
429
+
430
+ Comparison is performed in the canonical unit (mm) so that
431
+ ``Dimension(1, "in") < Dimension(3, "cm")`` evaluates correctly.
432
+
433
+ Args:
434
+ other: A measurement of the same concrete type.
435
+
436
+ Raises
437
+ ------
438
+ IncompatibleUnitsError: If *other* is a different measurement type.
439
+ """
440
+ rhs = self._check_compatible(other)
441
+ if rhs is NotImplemented:
442
+ return NotImplemented # type: ignore[return-value]
443
+ return self._to_mm_raw() < rhs._to_mm_raw()
444
+
445
+ def __le__(self, other: object) -> bool:
446
+ """Return ``True`` if *self* ≤ *other* (cross-unit aware)."""
447
+ rhs = self._check_compatible(other)
448
+ if rhs is NotImplemented:
449
+ return NotImplemented # type: ignore[return-value]
450
+ return self._to_mm_raw() <= rhs._to_mm_raw()
451
+
452
+ def __gt__(self, other: object) -> bool:
453
+ """Return ``True`` if *self* > *other* (cross-unit aware)."""
454
+ rhs = self._check_compatible(other)
455
+ if rhs is NotImplemented:
456
+ return NotImplemented # type: ignore[return-value]
457
+ return self._to_mm_raw() > rhs._to_mm_raw()
458
+
459
+ def __ge__(self, other: object) -> bool:
460
+ """Return ``True`` if *self* ≥ *other* (cross-unit aware)."""
461
+ rhs = self._check_compatible(other)
462
+ if rhs is NotImplemented:
463
+ return NotImplemented # type: ignore[return-value]
464
+ return self._to_mm_raw() >= rhs._to_mm_raw()
465
+
466
+ # ``__eq__`` and ``__hash__`` are defined explicitly to compare by canonical
467
+ # mm value, keeping equality consistent with the comparison operators above.
468
+
469
+ def __eq__(self, other: object) -> bool:
470
+ if not isinstance(other, _Measurement) or type(self) is not type(other):
471
+ return NotImplemented
472
+ return math.isclose(self._to_mm_raw(), other._to_mm_raw(), rel_tol=1e-9)
473
+
474
+ def __hash__(self) -> int:
475
+ return hash(round(self._to_mm_raw(), 6))
476
+
477
+ # ------------------------------------------------------------------
478
+ # Utility
479
+ # ------------------------------------------------------------------
480
+
481
+ def is_close(self, other: _Measurement, rel_tol: float = 1e-9) -> bool:
482
+ """Return whether two measurements are approximately equal.
483
+
484
+ Uses :func:`math.isclose` on the millimetre representations so
485
+ that floating-point rounding differences across unit conversions
486
+ are handled gracefully.
487
+
488
+ Args:
489
+ other: A measurement to compare against *self*.
490
+ rel_tol: Maximum allowed relative difference (default ``1e-9``).
491
+
492
+ Returns
493
+ -------
494
+ ``True`` if the two values are within *rel_tol* of each other.
495
+
496
+ Raises
497
+ ------
498
+ IncompatibleUnitsError: If *other* is a different measurement
499
+ type.
500
+
501
+ Example::
502
+
503
+ >>> a = Dimension(1, "in")
504
+ >>> b = Dimension(25.4, "mm")
505
+ >>> a.is_close(b)
506
+ True
507
+ """
508
+ self._check_compatible(other)
509
+ return math.isclose(self._to_mm_raw(), other._to_mm_raw(), rel_tol=rel_tol)
510
+
511
+ def as_unit(self: _T, target_unit: str) -> _T:
512
+ """Return a new measurement expressed in *target_unit*.
513
+
514
+ Unlike :meth:`to`, which returns a plain ``float``, this method
515
+ returns a fully typed measurement object — useful when normalising
516
+ a collection to a single unit.
517
+
518
+ Args:
519
+ target_unit: The unit for the returned object.
520
+
521
+ Returns
522
+ -------
523
+ A new instance of the same type with the converted value.
524
+
525
+ Raises
526
+ ------
527
+ UnsupportedUnitError: If *target_unit* is unrecognised.
528
+
529
+ Example::
530
+
531
+ >>> Dimension(2.54, "cm").as_unit("mm")
532
+ Dimension(value=25.4, unit='mm')
533
+ """
534
+ return type(self)(self.to(target_unit), target_unit) # type: ignore[return-value]
535
+
536
+ def __repr__(self) -> str:
537
+ """Return an unambiguous string representation.
538
+
539
+ Returns
540
+ -------
541
+ A string of the form ``ClassName(value=…, unit='…')``.
542
+ """
543
+ return f"{type(self).__name__}(value={self.value!r}, unit={self.unit!r})"
544
+
545
+
546
+ # ---------------------------------------------------------------------------
547
+ # Public measurement classes
548
+ # ---------------------------------------------------------------------------
549
+
550
+
551
+ @dataclass(frozen=True, slots=True, eq=False)
552
+ class Dimension(_Measurement):
553
+ """A physical spatial measurement (width, height, margin, etc.).
554
+
555
+ :class:`Dimension` is intended for layout measurements such as page
556
+ widths, margins, and gutter sizes. It inherits full unit-conversion
557
+ and arithmetic support from :class:`_Measurement`.
558
+
559
+ Attributes
560
+ ----------
561
+ value
562
+ Numeric magnitude of the dimension.
563
+ unit
564
+ Unit of measurement: ``"mm"``, ``"cm"``, ``"in"``, ``"pt"``,
565
+ or ``"pica"``.
566
+
567
+ Example::
568
+
569
+ >>> page_width = Dimension(210, "mm") # A4 width
570
+ >>> page_width.to_inches()
571
+ 8.267716535433071
572
+ >>> page_width.to_pt()
573
+ 595.2755905511812
574
+
575
+ >>> margin = Dimension(0.5, "in")
576
+ >>> live_width = page_width - margin.as_unit("mm") * 2
577
+ >>> round(live_width.to_inches(), 4)
578
+ 7.2677
579
+ """
580
+
581
+ # ------------------------------------------------------------------
582
+ # Named convenience converters
583
+ # ------------------------------------------------------------------
584
+
585
+ def to_mm(self) -> float:
586
+ """Return the dimension expressed in millimetres.
587
+
588
+ Returns
589
+ -------
590
+ Value in millimetres.
591
+
592
+ Example::
593
+
594
+ >>> Dimension(1, "in").to_mm()
595
+ 25.4
596
+ """
597
+ return self._to_mm_raw()
598
+
599
+ def to_cm(self) -> float:
600
+ """Return the dimension expressed in centimetres.
601
+
602
+ Returns
603
+ -------
604
+ Value in centimetres.
605
+
606
+ Example::
607
+
608
+ >>> Dimension(100, "mm").to_cm()
609
+ 10.0
610
+ """
611
+ return self.to("cm")
612
+
613
+ def to_inches(self) -> float:
614
+ """Return the dimension expressed in inches.
615
+
616
+ Returns
617
+ -------
618
+ Value in inches.
619
+
620
+ Example::
621
+
622
+ >>> Dimension(25.4, "mm").to_inches()
623
+ 1.0
624
+ """
625
+ return self.to("in")
626
+
627
+ def to_pt(self) -> float:
628
+ """Return the dimension expressed in PostScript points (1 pt = 1/72 in).
629
+
630
+ Returns
631
+ -------
632
+ Value in points.
633
+
634
+ Example::
635
+
636
+ >>> Dimension(1, "in").to_pt()
637
+ 72.0
638
+ """
639
+ return self.to("pt")
640
+
641
+ def to_pica(self) -> float:
642
+ """Return the dimension expressed in picas (1 pica = 12 pt).
643
+
644
+ Returns
645
+ -------
646
+ Value in picas.
647
+
648
+ Example::
649
+
650
+ >>> Dimension(1, "in").to_pica()
651
+ 6.0
652
+ """
653
+ return self.to("pica")
654
+
655
+
656
+ @dataclass(frozen=True, slots=True, eq=False)
657
+ class FontSize(_Measurement):
658
+ """A typographic font size measurement.
659
+
660
+ :class:`FontSize` is semantically distinct from :class:`Dimension`
661
+ even though both wrap a (value, unit) pair. Keeping them separate
662
+ prevents accidental mixing (e.g. adding a font size to a page margin)
663
+ and allows each type to expose domain-appropriate helper methods.
664
+
665
+ Attributes
666
+ ----------
667
+ value
668
+ Numeric magnitude of the font size.
669
+ unit
670
+ Unit of measurement: ``"mm"``, ``"cm"``, ``"in"``, ``"pt"``,
671
+ or ``"pica"``.
672
+
673
+ Notes
674
+ -----
675
+ In typography, ``"pt"`` (PostScript point) and ``"pica"`` are by far
676
+ the most common units. The :meth:`to_pt` and :meth:`to_pica` helpers
677
+ are therefore the primary API surface for this class.
678
+
679
+ Example::
680
+
681
+ >>> body = FontSize(10, "pt")
682
+ >>> body.to_mm()
683
+ 3.527777777777778
684
+ >>> heading = body * 2.4
685
+ >>> round(heading.to_pt(), 1)
686
+ 24.0
687
+ """
688
+
689
+ # ------------------------------------------------------------------
690
+ # Named convenience converters
691
+ # ------------------------------------------------------------------
692
+
693
+ def to_mm(self) -> float:
694
+ """Return the font size expressed in millimetres.
695
+
696
+ Returns
697
+ -------
698
+ Value in millimetres.
699
+
700
+ Example::
701
+
702
+ >>> FontSize(7, "pt").to_mm()
703
+ 2.469444444444444
704
+ """
705
+ return self._to_mm_raw()
706
+
707
+ def to_pt(self) -> float:
708
+ """Return the font size expressed in PostScript points (1 pt = 1/72 in).
709
+
710
+ Returns
711
+ -------
712
+ Value in points.
713
+
714
+ Example::
715
+
716
+ >>> FontSize(1, "pica").to_pt()
717
+ 12.0
718
+ """
719
+ return self.to("pt")
720
+
721
+ def to_pica(self) -> float:
722
+ """Return the font size expressed in picas (1 pica = 12 pt).
723
+
724
+ Returns
725
+ -------
726
+ Value in picas.
727
+
728
+ Example::
729
+
730
+ >>> FontSize(24, "pt").to_pica()
731
+ 2.0
732
+ """
733
+ return self.to("pica")
734
+
735
+ def to_inches(self) -> float:
736
+ """Return the font size expressed in inches.
737
+
738
+ Returns
739
+ -------
740
+ Value in inches.
741
+
742
+ Example::
743
+
744
+ >>> FontSize(72, "pt").to_inches()
745
+ 1.0
746
+ """
747
+ return self.to("in")
748
+
749
+ def to_cm(self) -> float:
750
+ """Return the font size expressed in centimetres.
751
+
752
+ Returns
753
+ -------
754
+ Value in centimetres.
755
+
756
+ Example::
757
+
758
+ >>> FontSize(10, "mm").to_cm()
759
+ 1.0
760
+ """
761
+ return self.to("cm")