Source code for papersize

# Copyright Louis Paternault 2014-2024
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Paper size related data and functions

In this module:

- the default unit (input and output) is point (``pt``);
- every numbers are returned as :class:`decimal.Decimal` objects.
"""

import gettext
import importlib.resources
import re
from decimal import Decimal

__version__ = "1.5.0"
__AUTHOR__ = "Louis Paternault (spalax@gresille.org)"
__COPYRIGHT__ = "(C) 2014-2024 Louis Paternault. GNU GPL 3 or later."

SIZES = {
    # http://www.printernational.org/iso-paper-sizes.php
    "4a0": "1682mm x 2378mm",
    "2a0": "1189mm x 1682mm",
    "a0": "841mm x 1189mm",
    "a1": "594mm x 841mm",
    "a2": "420mm x 594mm",
    "a3": "297mm x 420mm",
    "a4": "210mm x 297mm",
    "a5": "148mm x 210mm",
    "a6": "105mm x 148mm",
    "a7": "74mm x 105mm",
    "a8": "52mm x 74mm",
    "a9": "37mm x 52mm",
    "a10": "26mm x 37mm",
    "b0": "1000mm x 1414mm",
    "b1": "707mm x 1000mm",
    "b2": "500mm x 707mm",
    "b3": "353mm x 500mm",
    "b4": "250mm x 352mm",
    "b5": "176mm x 250mm",
    "b6": "125mm x 176mm",
    "b7": "88mm x 125mm",
    "b8": "62mm x 88mm",
    "b9": "44mm x 62mm",
    "b10": "31mm x 44mm",
    "a2extra": "445mm x 619mm",
    "a3extra": "322mm x 445mm",
    "a3super": "305mm x 508mm",
    "supera3": "305mm x 487mm",
    "a4extra": "235mm x 322mm",
    "a4super": "229mm x 322mm",
    "supera4": "227mm x 356mm",
    "a4long": "210mm x 348mm",
    "a5extra": "173mm x 235mm",
    "sob5extra": "202mm x 276mm",
    # http://www.engineeringtoolbox.com/office-paper-sizes-d_213.html
    "letter": "8.5in x 11in",
    "legal": "8.5in x 14in",
    "executive": "7in x 10in",
    "tabloid": "11in x 17in",
    "statement": "5.5in x 8.5in",
    "halfletter": "5.5in x 8.5in",
    "folio": "8in x 13in",
    # http://hplipopensource.com/hplip-web/tech_docs/page_sizes.html
    "flsa": "8.5in x 13in",
    # http://www.coding-guidelines.com/numbers/ndb/units/area.txt
    "flse": "8.5in x 13in",
    # http://jexcelapi.sourceforge.net/resources/javadocs/2_6_10/docs/jxl/format/PaperSize.html
    "note": "8.5in x 11in",
    "11x17": "11in x 17in",
    "10x14": "10in x 14in",
    # https://en.wikipedia.org/w/index.php?title=Paper_size&oldid=814180250
    "c0": "917mm × 1297mm",
    "c1": "648mm × 917mm",
    "c2": "458mm × 648mm",
    "c3": "324mm × 458mm",
    "c4": "229mm × 324mm",
    "c5": "162mm × 229mm",
    "c6": "114mm × 162mm",
    "c7": "81mm × 114mm",
    "c8": "57mm × 81mm",
    "c9": "40mm × 57mm",
    "c10": "28mm × 40mm",
    "juniorlegal": "5in × 8in",
    "memo": "halfletter",
    "governmentletter": "8in × 10in",
    "governmentlegal": "8.5in × 13in",
    "ledger": "17in x 11in",
    "arch1": "9in x 12in",
    "arch2": "12in x 18in",
    "arch3": "18in x 24in",
    "arch4": "24in x 36in",
    "arch5": "30in x 42in",
    "arch6": "36in x 48in",
    "archa": "arch1",
    "archb": "arch2",
    "archc": "arch3",
    "archd": "arch4",
    "arche1": "arch5",
    "arche": "arch6",
    "arche2": "26in x 38in",
    "arche3": "27in x 39in",
}
"""Dictionary of named sizes.

Keys are names (e.g. ``a4``, ``letter``) and values are strings,
human-readable, and parsable by :func:`parse_papersize` (e.g. ``21cm x
29.7cm``).
"""

# Source: http://en.wikibooks.org/wiki/LaTeX/Lengths
UNITS = {
    "": Decimal("1"),  # Default is point (pt)
    "pt": Decimal("1"),  # point
    "mm": Decimal("7227") / Decimal("2540"),  # millimeter
    "cm": Decimal("7227") / Decimal("254"),  # centimeter
    "in": Decimal("72.27"),  # inch
    "bp": Decimal("803") / Decimal("800"),  # big point
    "pc": Decimal("12"),  # pica
    "dd": Decimal("1238") / Decimal("1157"),  # didot
    "cc": Decimal("14856") / Decimal("1157"),  # cicero
    "nd": Decimal("685") / Decimal("642"),  # new didot
    "nc": Decimal("1370") / Decimal("107"),  # new cicero
    "sp": Decimal("1") / Decimal("65536"),  # scaled point
}
"""Dictionary of units.

Keys are unit abbreviation (e.g. ``pt`` or ``cm``), and values are their value
in points (e.g. ``UNITS['pt']`` is 1, ``UNITS['pc']``] is 12), as
:class:`decimal.Decimal` objects.
"""


def _(text):
    """Dummy function to mark strings as translatable by gettext."""
    return text


UNITS_HELP = {
    "pt": _("point (desktop publishing point: 1/72 inch, or about 0.353mm)"),
    "mm": _("millimeter"),
    "cm": _("centimeter"),
    "in": _("inch (exactly 25.4 mm)"),
    "bp": _("big point (25.4/72 mm)"),
    "pc": _("pica (12 points, or 1/6 inch)"),
    "dd": _("Didot point (1238 pt = 1157 dd)"),
    "cc": _("cicero (12 Didot points)"),
    "nd": _("new Didot (3/8 mm)"),
    "nc": _("new cicero (12 new Didot points)"),
    "sp": _("scaled point (1/2^16 pt)"),
}
"""Human description of each unit.

Keys are unit abbreviation (e.g. ``pt`` or ``cm``),
and values are strings explaining the meaning of this unit.
You can use it to list and explain to your users the available units.

Note that the descriptions are :ref:`translated <i18n>`.
"""

SIZES_HELP = {
    # http://www.printernational.org/iso-paper-sizes.php
    "4A0": _("168.2cm x 237.8cm (ISO 216, four times an A0)"),
    "2A0": _("118.9cm x 168.2cm (ISO 216, twice an A0)"),
    "A0": _(
        "84.1cm x 1189cm (ISO 216, has an aspect ratio of √2, and an area of 1 m²)"
    ),
    "A1": _("59.4cm x 84.1cm (ISO 216, half an A0)"),
    "A2": _("42cm x 59.4cm (ISO 216, half an A1)"),
    "A3": _("29.7cm x 42cm (ISO 216, half an A2)"),
    "A4": _("21cm x 29.1cm (ISO 216, half an A3)"),
    "A5": _("14.8cm x 21cm (ISO 216, half an A4)"),
    "A6": _("10.5cm x 14.8cm (ISO 216, half an A5)"),
    "A7": _("7.4cm x 10.5cm (ISO 216, half an A6)"),
    "A8": _("5.2cm x 7.4cm (ISO 216, half an A7)"),
    "A9": _("3.7cm x 5.2cm (ISO 216, half an A8)"),
    "A10": _("2.6cm x 3.7cm (ISO 216, half an A9)"),
    "B0": _("100cm x 141.4cm (ISO 216)"),
    "B1": _("70.7cm x 100cm (ISO 216, half a B0)"),
    "B2": _("50cm x 70.7cm (ISO 216, half a B1)"),
    "B3": _("35.3cm x 50cm (ISO 216, half a B2)"),
    "B4": _("25cm x 35.2cm (ISO 216, half a B3)"),
    "B5": _("17.6cm x 25cm (ISO 216, half a B4)"),
    "B6": _("12.5cm x 17.6cm (ISO 216, half a B5)"),
    "B7": _("8.8cm x 12.5cm (ISO 216, half a B6)"),
    "B8": _("6.2cm x 8.8cm (ISO 216, half a B7)"),
    "B9": _("4.4cm x 6.2cm (ISO 216, half a B8)"),
    "B10": _("3.1cm x 4.4cm (ISO 216, half a B9)"),
    "A2extra": _("445mm x 619mm"),
    "A3extra": _("322mm x 445mm"),
    "A3super": _("305mm x 508mm"),
    "superA3": _("305mm x 487mm"),
    "A4extra": _("235mm x 322mm"),
    "A4super": _("229mm x 322mm"),
    "superA4": _("227mm x 356mm"),
    "A4long": _("210mm x 348mm"),
    "A5extra": _("173mm x 235mm"),
    "sob5extra": _("202mm x 276mm"),
    # http://www.engineeringtoolbox.com/office-paper-sizes-d_215.html
    "letter": _("8.5in x 11in"),
    "legal": _("8.5in x 14in"),
    "executive": _("7in x 10in"),
    "tabloid": _("11in x 17in"),
    "statement": _("5.5in x 8.5in"),
    "halfletter": _("5.5in x 8.5in"),
    "folio": _("8in x 13in"),
    # http://hplipopensource.com/hplip-web/tech_docs/page_sizes.html
    "flsa": _("8.5in x 13in"),
    # http://www.coding-guidelines.com/numbers/ndb/units/area.txt
    "flse": _("8.5in x 13in"),
    # http://jexcelapi.sourceforge.net/resources/javadocs/2_6_10/docs/jxl/format/PaperSize.html
    "note": _("8.5in x 11in"),
    "11x17": _("11in x 17in"),
    "10x14": _("10in x 14in"),
    # https://en.wikipedia.org/w/index.php?title=Paper_size&oldid=814180250
    "C0": _("91.7cm × 129.7cm (ISO 269)"),
    "C1": _("64.8cm × 91.7cm (ISO 269, half a C0)"),
    "C2": _("45.8cm × 64.8cm (ISO 269, half a C1)"),
    "C3": _("32.4cm × 45.8cm (ISO 269, half a C2)"),
    "C4": _("22.9cm × 32.4cm (ISO 269, half a C3)"),
    "C5": _("16.2cm × 22.9cm (ISO 269, half a C4)"),
    "C6": _("11.4cm × 16.2cm (ISO 269, half a C5)"),
    "C7": _("8.1cm × 11.4cm (ISO 269, half a C6)"),
    "C8": _("5.7cm × 8.1cm (ISO 269, half a C7)"),
    "C9": _("4cm × 5.7cm (ISO 269, half a C8)"),
    "C10": _("2.8cm × 4cm (ISO 269, half a C9)"),
    "juniorlegal": _("5in × 8in"),
    "memo": _("synonym for halfletter"),
    "governmentletter": _("8in × 10in"),
    "governmentlegal": _("8.5in × 13in"),
    "ledger": _("17in x 11in"),
    "Arch1": _("9in x 12in (architectural size)"),
    "Arch2": _("12in x 18in (architectural size)"),
    "Arch3": _("18in x 24in (architectural size)"),
    "Arch4": _("24in x 36in (architectural size)"),
    "Arch5": _("30in x 42in (architectural size)"),
    "Arch6": _("36in x 48in (architectural size)"),
    "ArchA": _("other name for Arch1 (architectural size)"),
    "ArchB": _("other name for Arch2 (architectural size)"),
    "ArchC": _("other name for Arch3 (architectural size)"),
    "ArchD": _("other name for Arch4 (architectural size)"),
    "ArchE1": _("other name for Arch5 (architectural size)"),
    "ArchE": _("other name for Arch6 (architectural size)"),
    "ArchE2": _("26in x 38in (architectural size)"),
    "ArchE3": _("27in x 39in (architectural size)"),
}
"""Human description of each paper size.

Keys are size abbreviation (e.g. ``A4`` or ``letter``),
and values are strings explaining the meaning of this size.
You can use it to list and explain to your users the available paper sizes.

For historical reasons, keys of ``SIZES`` are lower cases, while keys of ``SIZES_HELP`` are not.
But case aside, those dictionaries contain exactly the same set of keys.

Note that the descriptions are :ref:`translated <i18n>`.
"""

PORTRAIT = True
"""Constant corresponding to the portrait orientation

That is, height greater than width.
"""

LANDSCAPE = False
"""Constant corresponding to the landscape orientation

That is, width greater than height.
"""

__UNITS_RE = rf"""({"|".join(UNITS.keys())})"""
__SIZE_RE = rf"([\d.]+){__UNITS_RE}"
__PAPERSIZE_RE = r"^(?P<width>{size}) *[x× ]? *(?P<height>{size})$".format(
    size=__SIZE_RE
)

__SIZE_COMPILED_RE = re.compile(f"^{__SIZE_RE}$".format("size"))
__PAPERSIZE_COMPILED_RE = re.compile(__PAPERSIZE_RE.format("width", "height"))


[docs] class PapersizeException(Exception): """All exceptions of this module inherit from this one."""
[docs] class CouldNotParse(PapersizeException): """Raised when a string could not be parsed. :param str string: String that could not be parsed. """ def __init__(self, string): super().__init__() self.string = string def __str__(self): return f"Could not parse string '{self.string}'."
[docs] class UnknownOrientation(PapersizeException): """Raised when type of argument Orientation is wrong. :param obj string: Object wrongly provided as an orientation. """ def __init__(self, string): super().__init__() self.string = string def __str__(self): return f"'{self.string}' is not one of `papersize.PORTRAIT` or `papersize.LANDSCAPE`"
[docs] def convert_length(length, orig, dest): """Convert length from one unit to another. :param decimal.Decimal length: Length to convert, as any object convertible to a :class:`decimal.Decimal`. :param str orig: Unit of ``length``, as a string which is a key of :data:`UNITS`. :param str dest: Unit in which ``length`` will be converted, as a string which is a key of :data:`UNITS`. Due to floating point arithmetic, there can be small rounding errors. >>> convert_length(0.1, "cm", "mm") Decimal('1.000000000000000055511151231') """ return (Decimal(UNITS[orig]) * Decimal(length)) / Decimal(UNITS[dest])
[docs] def parse_length(string, unit="pt"): """Return a length corresponding to the string. :param str string: The string to parse, as a length and a unit, for instance ``10.2cm``. :param str unit: The unit of the return value, as a key of :data:`UNITS`. :return: The length, in an unit given by the ``unit`` argument. :rtype: :class:`decimal.Decimal` >>> parse_length("1cm", "mm") Decimal('1E+1') >>> parse_length("1cm", "cm") Decimal('1') >>> parse_length("10cm") Decimal('284.5275590551181102362204724') """ match = __SIZE_COMPILED_RE.match(string) if match is None: raise CouldNotParse(string) return convert_length(Decimal(match.groups()[0]), match.groups()[1], unit)
[docs] def parse_couple(string, unit="pt"): """Return a tuple of dimensions. :param str string: The string to parse, as "LENGTHxLENGTH" (where LENGTH are length, parsable by :func:`parse_length`). Example: ``21cm x 29.7cm``. The separator can be ``x``, ``×`` or empty, surrounded by an arbitrary number of spaces. For instance: ``2cmx3cm``, ``2cm x 3cm``, ``2cm×3cm``, ``2cm 3cm``. :rtype: :class:`tuple` :return: A tuple of :class:`decimal.Decimal`, representing the dimensions. >>> parse_couple("1cm 10cm", "mm") (Decimal('1E+1'), Decimal('1E+2')) >>> parse_couple("1mm 10mm", "cm") (Decimal('0.1'), Decimal('1')) """ try: match = __PAPERSIZE_COMPILED_RE.match(string).groupdict() return (parse_length(match["width"], unit), parse_length(match["height"], unit)) except AttributeError as error: raise CouldNotParse(string) from error
[docs] def parse_papersize(string, unit="pt"): """Return the papersize corresponding to string. :param str string: The string to parse. It can be either a named size (as keys of constant :data:`SIZES`), or a couple of lengths (that will be processed by :func:`parse_couple`). The named paper sizes are case insensitive. The following strings return the same size: ``a4``, ``A4``, ``21cm 29.7cm``, ``210mmx297mm``, ``21cm × 297mm``… :param str unit: The unit of the return values. :return: The paper size, as a couple of :class:`decimal.Decimal`. :rtype: :class:`tuple` >>> parse_papersize("A4", "cm") (Decimal('21.00000000000000000000000000'), Decimal('29.70000000000000000000000000')) >>> parse_papersize("21cm x 29.7cm", "mm") (Decimal('210.0000000000000000000000000'), Decimal('297.0000000000000000000000000')) >>> parse_papersize("10 100") (Decimal('10'), Decimal('100')) """ if string.lower() in SIZES: return parse_papersize(SIZES[string.lower()], unit) return parse_couple(string, unit)
[docs] def is_portrait(width, height, *, strict=False, fuzzy=False, ndigits=7): """Return whether paper orientation is portrait That is, height greater or equal to width. :param width: Width of paper, as any sortable object. :param height: Height of paper, as any sortable object. :param bool strict: If ``False``, square format (width equals height) is considered portrait; if ``True`` square format is not considered portrait. :param bool fuzzy: If ``True``, comparison is done up to ``ndigits`` digits. :param int ndigits: Number of digits when using fuzzy comparison. >>> is_portrait(11, 10) False >>> is_portrait(10, 10) True >>> is_portrait(10, 11) True """ if strict and is_square(width, height, fuzzy=fuzzy, ndigits=ndigits): return False if fuzzy: return round(height - width, ndigits=ndigits) >= 0 return width <= height
[docs] def is_landscape(width, height, *, strict=False, fuzzy=False, ndigits=7): """Return whether paper orientation is landscape That is, width greater or equal to height. :param width: Width of paper, as any sortable object. :param height: Height of paper, as any sortable object. :param strict: If ``False``, square format (width equals height) is considered landscape; if ``True`` square format is not considered landscape. :param bool fuzzy: If ``True``, comparison is done up to ``ndigits`` digits. :param int ndigits: Number of digits when using fuzzy comparison. >>> is_landscape(11, 10) True >>> is_landscape(10, 10) True >>> is_landscape(10, 11) False """ if strict and is_square(width, height): return False if fuzzy: return round(width - height, ndigits=ndigits) >= 0 return height <= width
[docs] def is_square(width, height, *, fuzzy=False, ndigits=7): """Return whether paper is a square (width equals height). :param width: Width of paper, as any sortable object. :param height: Height of paper, as any sortable object. :param bool fuzzy: If ``True``, comparison is done up to ``ndigits`` digits. :param int ndigits: Number of digits when using fuzzy comparison. >>> is_square(11, 10) False >>> is_square(10, 10) True >>> is_square(10, 10.00000001, fuzzy=False) False >>> is_square(10, 10.00000001, fuzzy=True) True >>> is_square(10, 10.00000001, fuzzy=True, ndigits=10) False """ if fuzzy: return round(width - height, ndigits) == 0 return width == height
[docs] def rotate(size, orientation): """Return the size, rotated if necessary to make it portrait or landscape. :param tuple size: Couple paper of dimension, as sortable objects (:class:`int`, :class:`float`, :class:`decimal.Decimal`…). :param orientation: Return format, one of ``PORTRAIT`` or ``LANDSCAPE``. :return: The size, as a couple of dimensions, of the same type of the ``size`` parameter. :rtype: :class:`tuple` >>> rotate((21, 29.7), PORTRAIT) (21, 29.7) >>> rotate((21, 29.7), LANDSCAPE) (29.7, 21) """ if orientation == PORTRAIT: return (min(size), max(size)) if orientation == LANDSCAPE: return (max(size), min(size)) raise UnknownOrientation(orientation)
[docs] def translation_directory(): """Return an context manager proiding a directory in which translation files are located. .. versionadded:: 1.5.0 """ return importlib.resources.as_file( importlib.resources.files(__package__) / "translations" )