Coverage for papersize/__init__.py: 99%
80 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-22 09:43 +0100
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-22 09:43 +0100
1# Copyright Louis Paternault 2014-2025
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
16"""Paper size related data and functions
18In this module:
20- the default unit (input and output) is point (``pt``);
21- every numbers are returned as :class:`decimal.Decimal` objects.
22"""
24import gettext
25import importlib.resources
26import re
27from decimal import Decimal
29__version__ = "1.6.1"
30__AUTHOR__ = "Louis Paternault (spalax@gresille.org)"
31__COPYRIGHT__ = "(C) 2014-2025 Louis Paternault. GNU GPL 3 or later."
33SIZES = {
34 # http://www.printernational.org/iso-paper-sizes.php
35 "4a0": "1682mm x 2378mm",
36 "2a0": "1189mm x 1682mm",
37 "a0": "841mm x 1189mm",
38 "a1": "594mm x 841mm",
39 "a2": "420mm x 594mm",
40 "a3": "297mm x 420mm",
41 "a4": "210mm x 297mm",
42 "a5": "148mm x 210mm",
43 "a6": "105mm x 148mm",
44 "a7": "74mm x 105mm",
45 "a8": "52mm x 74mm",
46 "a9": "37mm x 52mm",
47 "a10": "26mm x 37mm",
48 "b0": "1000mm x 1414mm",
49 "b1": "707mm x 1000mm",
50 "b2": "500mm x 707mm",
51 "b3": "353mm x 500mm",
52 "b4": "250mm x 352mm",
53 "b5": "176mm x 250mm",
54 "b6": "125mm x 176mm",
55 "b7": "88mm x 125mm",
56 "b8": "62mm x 88mm",
57 "b9": "44mm x 62mm",
58 "b10": "31mm x 44mm",
59 "a2extra": "445mm x 619mm",
60 "a3extra": "322mm x 445mm",
61 "a3super": "305mm x 508mm",
62 "supera3": "305mm x 487mm",
63 "a4extra": "235mm x 322mm",
64 "a4super": "229mm x 322mm",
65 "supera4": "227mm x 356mm",
66 "a4long": "210mm x 348mm",
67 "a5extra": "173mm x 235mm",
68 "sob5extra": "202mm x 276mm",
69 # http://www.engineeringtoolbox.com/office-paper-sizes-d_213.html
70 "letter": "8.5in x 11in",
71 "legal": "8.5in x 14in",
72 "executive": "7in x 10in",
73 "tabloid": "11in x 17in",
74 "statement": "5.5in x 8.5in",
75 "halfletter": "5.5in x 8.5in",
76 "folio": "8in x 13in",
77 # http://hplipopensource.com/hplip-web/tech_docs/page_sizes.html
78 "flsa": "8.5in x 13in",
79 # http://www.coding-guidelines.com/numbers/ndb/units/area.txt
80 "flse": "8.5in x 13in",
81 # http://jexcelapi.sourceforge.net/resources/javadocs/2_6_10/docs/jxl/format/PaperSize.html
82 "note": "8.5in x 11in",
83 "11x17": "11in x 17in",
84 "10x14": "10in x 14in",
85 # https://en.wikipedia.org/w/index.php?title=Paper_size&oldid=814180250
86 "c0": "917mm × 1297mm",
87 "c1": "648mm × 917mm",
88 "c2": "458mm × 648mm",
89 "c3": "324mm × 458mm",
90 "c4": "229mm × 324mm",
91 "c5": "162mm × 229mm",
92 "c6": "114mm × 162mm",
93 "c7": "81mm × 114mm",
94 "c8": "57mm × 81mm",
95 "c9": "40mm × 57mm",
96 "c10": "28mm × 40mm",
97 "juniorlegal": "5in × 8in",
98 "memo": "halfletter",
99 "governmentletter": "8in × 10in",
100 "governmentlegal": "8.5in × 13in",
101 "ledger": "17in x 11in",
102 "arch1": "9in x 12in",
103 "arch2": "12in x 18in",
104 "arch3": "18in x 24in",
105 "arch4": "24in x 36in",
106 "arch5": "30in x 42in",
107 "arch6": "36in x 48in",
108 "archa": "arch1",
109 "archb": "arch2",
110 "archc": "arch3",
111 "archd": "arch4",
112 "arche1": "arch5",
113 "arche": "arch6",
114 "arche2": "26in x 38in",
115 "arche3": "27in x 39in",
116}
117"""Dictionary of named sizes.
119Keys are names (e.g. ``a4``, ``letter``) and values are strings,
120human-readable, and parsable by :func:`parse_papersize` (e.g. ``21cm x
12129.7cm``).
122"""
124# Source: http://en.wikibooks.org/wiki/LaTeX/Lengths
125UNITS = {
126 "": Decimal("1"), # Default is point (pt)
127 "pt": Decimal("1"), # point
128 "mm": Decimal("7227") / Decimal("2540"), # millimeter
129 "cm": Decimal("7227") / Decimal("254"), # centimeter
130 "in": Decimal("72.27"), # inch
131 "bp": Decimal("803") / Decimal("800"), # big point
132 "pc": Decimal("12"), # pica
133 "dd": Decimal("1238") / Decimal("1157"), # didot
134 "cc": Decimal("14856") / Decimal("1157"), # cicero
135 "nd": Decimal("685") / Decimal("642"), # new didot
136 "nc": Decimal("1370") / Decimal("107"), # new cicero
137 "sp": Decimal("1") / Decimal("65536"), # scaled point
138}
139"""Dictionary of units.
141Keys are unit abbreviation (e.g. ``pt`` or ``cm``), and values are their value
142in points (e.g. ``UNITS['pt']`` is 1, ``UNITS['pc']``] is 12), as
143:class:`decimal.Decimal` objects.
144"""
147def _(text):
148 """Dummy function to mark strings as translatable by gettext."""
149 return text
152UNITS_HELP = {
153 "pt": _("point (desktop publishing point: 1/72 inch, or about 0.353mm)"),
154 "mm": _("millimeter"),
155 "cm": _("centimeter"),
156 "in": _("inch (exactly 25.4 mm)"),
157 "bp": _("big point (25.4/72 mm)"),
158 "pc": _("pica (12 points, or 1/6 inch)"),
159 "dd": _("Didot point (1238 pt = 1157 dd)"),
160 "cc": _("cicero (12 Didot points)"),
161 "nd": _("new Didot (3/8 mm)"),
162 "nc": _("new cicero (12 new Didot points)"),
163 "sp": _("scaled point (1/2^16 pt)"),
164}
165"""Human description of each unit.
167Keys are unit abbreviation (e.g. ``pt`` or ``cm``),
168and values are strings explaining the meaning of this unit.
169You can use it to list and explain to your users the available units.
171Note that the descriptions are :ref:`translated <i18n>`.
172"""
174SIZES_HELP = {
175 # http://www.printernational.org/iso-paper-sizes.php
176 "4A0": _("168.2cm x 237.8cm (ISO 216, four times an A0)"),
177 "2A0": _("118.9cm x 168.2cm (ISO 216, twice an A0)"),
178 "A0": _(
179 "84.1cm x 1189cm (ISO 216, has an aspect ratio of √2, and an area of 1 m²)"
180 ),
181 "A1": _("59.4cm x 84.1cm (ISO 216, half an A0)"),
182 "A2": _("42cm x 59.4cm (ISO 216, half an A1)"),
183 "A3": _("29.7cm x 42cm (ISO 216, half an A2)"),
184 "A4": _("21cm x 29.1cm (ISO 216, half an A3)"),
185 "A5": _("14.8cm x 21cm (ISO 216, half an A4)"),
186 "A6": _("10.5cm x 14.8cm (ISO 216, half an A5)"),
187 "A7": _("7.4cm x 10.5cm (ISO 216, half an A6)"),
188 "A8": _("5.2cm x 7.4cm (ISO 216, half an A7)"),
189 "A9": _("3.7cm x 5.2cm (ISO 216, half an A8)"),
190 "A10": _("2.6cm x 3.7cm (ISO 216, half an A9)"),
191 "B0": _("100cm x 141.4cm (ISO 216)"),
192 "B1": _("70.7cm x 100cm (ISO 216, half a B0)"),
193 "B2": _("50cm x 70.7cm (ISO 216, half a B1)"),
194 "B3": _("35.3cm x 50cm (ISO 216, half a B2)"),
195 "B4": _("25cm x 35.2cm (ISO 216, half a B3)"),
196 "B5": _("17.6cm x 25cm (ISO 216, half a B4)"),
197 "B6": _("12.5cm x 17.6cm (ISO 216, half a B5)"),
198 "B7": _("8.8cm x 12.5cm (ISO 216, half a B6)"),
199 "B8": _("6.2cm x 8.8cm (ISO 216, half a B7)"),
200 "B9": _("4.4cm x 6.2cm (ISO 216, half a B8)"),
201 "B10": _("3.1cm x 4.4cm (ISO 216, half a B9)"),
202 "A2extra": _("445mm x 619mm"),
203 "A3extra": _("322mm x 445mm"),
204 "A3super": _("305mm x 508mm"),
205 "superA3": _("305mm x 487mm"),
206 "A4extra": _("235mm x 322mm"),
207 "A4super": _("229mm x 322mm"),
208 "superA4": _("227mm x 356mm"),
209 "A4long": _("210mm x 348mm"),
210 "A5extra": _("173mm x 235mm"),
211 "sob5extra": _("202mm x 276mm"),
212 # http://www.engineeringtoolbox.com/office-paper-sizes-d_215.html
213 "letter": _("8.5in x 11in"),
214 "legal": _("8.5in x 14in"),
215 "executive": _("7in x 10in"),
216 "tabloid": _("11in x 17in"),
217 "statement": _("5.5in x 8.5in"),
218 "halfletter": _("5.5in x 8.5in"),
219 "folio": _("8in x 13in"),
220 # http://hplipopensource.com/hplip-web/tech_docs/page_sizes.html
221 "flsa": _("8.5in x 13in"),
222 # http://www.coding-guidelines.com/numbers/ndb/units/area.txt
223 "flse": _("8.5in x 13in"),
224 # http://jexcelapi.sourceforge.net/resources/javadocs/2_6_10/docs/jxl/format/PaperSize.html
225 "note": _("8.5in x 11in"),
226 "11x17": _("11in x 17in"),
227 "10x14": _("10in x 14in"),
228 # https://en.wikipedia.org/w/index.php?title=Paper_size&oldid=814180250
229 "C0": _("91.7cm × 129.7cm (ISO 269)"),
230 "C1": _("64.8cm × 91.7cm (ISO 269, half a C0)"),
231 "C2": _("45.8cm × 64.8cm (ISO 269, half a C1)"),
232 "C3": _("32.4cm × 45.8cm (ISO 269, half a C2)"),
233 "C4": _("22.9cm × 32.4cm (ISO 269, half a C3)"),
234 "C5": _("16.2cm × 22.9cm (ISO 269, half a C4)"),
235 "C6": _("11.4cm × 16.2cm (ISO 269, half a C5)"),
236 "C7": _("8.1cm × 11.4cm (ISO 269, half a C6)"),
237 "C8": _("5.7cm × 8.1cm (ISO 269, half a C7)"),
238 "C9": _("4cm × 5.7cm (ISO 269, half a C8)"),
239 "C10": _("2.8cm × 4cm (ISO 269, half a C9)"),
240 "juniorlegal": _("5in × 8in"),
241 "memo": _("synonym for halfletter"),
242 "governmentletter": _("8in × 10in"),
243 "governmentlegal": _("8.5in × 13in"),
244 "ledger": _("17in x 11in"),
245 "Arch1": _("9in x 12in (architectural size)"),
246 "Arch2": _("12in x 18in (architectural size)"),
247 "Arch3": _("18in x 24in (architectural size)"),
248 "Arch4": _("24in x 36in (architectural size)"),
249 "Arch5": _("30in x 42in (architectural size)"),
250 "Arch6": _("36in x 48in (architectural size)"),
251 "ArchA": _("other name for Arch1 (architectural size)"),
252 "ArchB": _("other name for Arch2 (architectural size)"),
253 "ArchC": _("other name for Arch3 (architectural size)"),
254 "ArchD": _("other name for Arch4 (architectural size)"),
255 "ArchE1": _("other name for Arch5 (architectural size)"),
256 "ArchE": _("other name for Arch6 (architectural size)"),
257 "ArchE2": _("26in x 38in (architectural size)"),
258 "ArchE3": _("27in x 39in (architectural size)"),
259}
260"""Human description of each paper size.
262Keys are size abbreviation (e.g. ``A4`` or ``letter``),
263and values are strings explaining the meaning of this size.
264You can use it to list and explain to your users the available paper sizes.
266For historical reasons, keys of ``SIZES`` are lower cases, while keys of ``SIZES_HELP`` are not.
267But case aside, those dictionaries contain exactly the same set of keys.
269Note that the descriptions are :ref:`translated <i18n>`.
270"""
272PORTRAIT = True
273"""Constant corresponding to the portrait orientation
275That is, height greater than width.
276"""
278LANDSCAPE = False
279"""Constant corresponding to the landscape orientation
281That is, width greater than height.
282"""
284__UNITS_RE = rf"""({"|".join(UNITS.keys())})"""
285__SIZE_RE = rf"([\d.]+){__UNITS_RE}"
286__PAPERSIZE_RE = r"^(?P<width>{size}) *[x× ]? *(?P<height>{size})$".format(
287 size=__SIZE_RE
288)
290__SIZE_COMPILED_RE = re.compile(f"^{__SIZE_RE}$".format("size"))
291__PAPERSIZE_COMPILED_RE = re.compile(__PAPERSIZE_RE.format("width", "height"))
294class PapersizeException(Exception):
295 """All exceptions of this module inherit from this one."""
298class CouldNotParse(PapersizeException):
299 """Raised when a string could not be parsed.
301 :param str string: String that could not be parsed.
302 """
304 def __init__(self, string):
305 super().__init__()
306 self.string = string
308 def __str__(self):
309 return f"Could not parse string '{self.string}'."
312class UnknownOrientation(PapersizeException):
313 """Raised when type of argument Orientation is wrong.
315 :param obj string: Object wrongly provided as an orientation.
316 """
318 def __init__(self, string):
319 super().__init__()
320 self.string = string
322 def __str__(self):
323 return f"'{self.string}' is not one of `papersize.PORTRAIT` or `papersize.LANDSCAPE`"
326def convert_length(length, orig, dest):
327 """Convert length from one unit to another.
329 :param decimal.Decimal length: Length to convert, as any object convertible
330 to a :class:`decimal.Decimal`.
331 :param str orig: Unit of ``length``, as a string which is a key of
332 :data:`UNITS`.
333 :param str dest: Unit in which ``length`` will be converted, as a string
334 which is a key of :data:`UNITS`.
336 Due to floating point arithmetic, there can be small rounding errors.
338 >>> convert_length(0.1, "cm", "mm")
339 Decimal('1.000000000000000055511151231')
340 """
341 return (Decimal(UNITS[orig]) * Decimal(length)) / Decimal(UNITS[dest])
344def parse_length(string, unit="pt"):
345 """Return a length corresponding to the string.
347 :param str string: The string to parse, as a length and a unit, for
348 instance ``10.2cm``.
349 :param str unit: The unit of the return value, as a key of :data:`UNITS`.
350 :return: The length, in an unit given by the ``unit`` argument.
351 :rtype: :class:`decimal.Decimal`
353 >>> parse_length("1cm", "mm")
354 Decimal('1E+1')
355 >>> parse_length("1cm", "cm")
356 Decimal('1')
357 >>> parse_length("10cm")
358 Decimal('284.5275590551181102362204724')
359 """
360 match = __SIZE_COMPILED_RE.match(string)
361 if match is None:
362 raise CouldNotParse(string)
363 return convert_length(Decimal(match.groups()[0]), match.groups()[1], unit)
366def parse_couple(string, unit="pt"):
367 """Return a tuple of dimensions.
369 :param str string: The string to parse, as "LENGTHxLENGTH" (where LENGTH
370 are length, parsable by :func:`parse_length`). Example: ``21cm x
371 29.7cm``. The separator can be ``x``, ``×`` or empty, surrounded by an
372 arbitrary number of spaces. For instance: ``2cmx3cm``, ``2cm x 3cm``,
373 ``2cm×3cm``, ``2cm 3cm``.
374 :rtype: :class:`tuple`
375 :return: A tuple of :class:`decimal.Decimal`, representing the dimensions.
377 >>> parse_couple("1cm 10cm", "mm")
378 (Decimal('1E+1'), Decimal('1E+2'))
379 >>> parse_couple("1mm 10mm", "cm")
380 (Decimal('0.1'), Decimal('1'))
381 """
382 try:
383 match = __PAPERSIZE_COMPILED_RE.match(string).groupdict()
384 return (parse_length(match["width"], unit), parse_length(match["height"], unit))
385 except AttributeError as error:
386 raise CouldNotParse(string) from error
389def parse_papersize(string, unit="pt"):
390 """Return the papersize corresponding to string.
392 :param str string: The string to parse. It can be either a named size (as
393 keys of constant :data:`SIZES`), or a couple of lengths (that will be
394 processed by :func:`parse_couple`). The named paper sizes are case
395 insensitive. The following strings return the same size: ``a4``,
396 ``A4``, ``21cm 29.7cm``, ``210mmx297mm``, ``21cm × 297mm``…
397 :param str unit: The unit of the return values.
398 :return: The paper size, as a couple of :class:`decimal.Decimal`.
399 :rtype: :class:`tuple`
401 >>> parse_papersize("A4", "cm")
402 (Decimal('21.00000000000000000000000000'), Decimal('29.70000000000000000000000000'))
403 >>> parse_papersize("21cm x 29.7cm", "mm")
404 (Decimal('210.0000000000000000000000000'), Decimal('297.0000000000000000000000000'))
405 >>> parse_papersize("10 100")
406 (Decimal('10'), Decimal('100'))
407 """
408 if string.lower() in SIZES:
409 return parse_papersize(SIZES[string.lower()], unit)
410 return parse_couple(string, unit)
413def is_portrait(width, height, *, strict=False, fuzzy=False, ndigits=7):
414 """Return whether paper orientation is portrait
416 That is, height greater or equal to width.
418 :param width: Width of paper, as any sortable object.
419 :param height: Height of paper, as any sortable object.
420 :param bool strict: If ``False``, square format (width equals height) is considered portrait;
421 if ``True`` square format is not considered portrait.
422 :param bool fuzzy: If ``True``, comparison is done up to ``ndigits`` digits.
423 :param int ndigits: Number of digits when using fuzzy comparison.
425 >>> is_portrait(11, 10)
426 False
427 >>> is_portrait(10, 10)
428 True
429 >>> is_portrait(10, 11)
430 True
431 """
432 if strict and is_square(width, height, fuzzy=fuzzy, ndigits=ndigits):
433 return False
434 if fuzzy:
435 return round(height - width, ndigits=ndigits) >= 0
436 return width <= height
439def is_landscape(width, height, *, strict=False, fuzzy=False, ndigits=7):
440 """Return whether paper orientation is landscape
442 That is, width greater or equal to height.
444 :param width: Width of paper, as any sortable object.
445 :param height: Height of paper, as any sortable object.
446 :param strict: If ``False``, square format (width equals height) is considered landscape;
447 if ``True`` square format is not considered landscape.
448 :param bool fuzzy: If ``True``, comparison is done up to ``ndigits`` digits.
449 :param int ndigits: Number of digits when using fuzzy comparison.
451 >>> is_landscape(11, 10)
452 True
453 >>> is_landscape(10, 10)
454 True
455 >>> is_landscape(10, 11)
456 False
457 """
458 if strict and is_square(width, height):
459 return False
460 if fuzzy:
461 return round(width - height, ndigits=ndigits) >= 0
462 return height <= width
465def is_square(width, height, *, fuzzy=False, ndigits=7):
466 """Return whether paper is a square (width equals height).
468 :param width: Width of paper, as any sortable object.
469 :param height: Height of paper, as any sortable object.
470 :param bool fuzzy: If ``True``, comparison is done up to ``ndigits`` digits.
471 :param int ndigits: Number of digits when using fuzzy comparison.
473 >>> is_square(11, 10)
474 False
475 >>> is_square(10, 10)
476 True
477 >>> is_square(10, 10.00000001, fuzzy=False)
478 False
479 >>> is_square(10, 10.00000001, fuzzy=True)
480 True
481 >>> is_square(10, 10.00000001, fuzzy=True, ndigits=10)
482 False
483 """
484 if fuzzy:
485 return round(width - height, ndigits) == 0
486 return width == height
489def rotate(size, orientation):
490 """Return the size, rotated if necessary to make it portrait or landscape.
492 :param tuple size: Couple paper of dimension, as sortable objects
493 (:class:`int`, :class:`float`, :class:`decimal.Decimal`…).
494 :param orientation: Return format, one of ``PORTRAIT`` or ``LANDSCAPE``.
495 :return: The size, as a couple of dimensions, of the same type of the
496 ``size`` parameter.
497 :rtype: :class:`tuple`
499 >>> rotate((21, 29.7), PORTRAIT)
500 (21, 29.7)
501 >>> rotate((21, 29.7), LANDSCAPE)
502 (29.7, 21)
503 """
504 if orientation == PORTRAIT:
505 return (min(size), max(size))
506 if orientation == LANDSCAPE:
507 return (max(size), min(size))
508 raise UnknownOrientation(orientation)
511def translation_directory():
512 """Return an context manager proiding a directory in which translation files are located.
514 .. versionadded:: 1.5.0
515 """
516 return importlib.resources.as_file(
517 importlib.resources.files(__package__) / "translations"
518 )