view pdfautonup/paper.py @ 1:d0832175b1b2 upstream

ADD: the original upstream of pdfautonup v1.12.1
author Franz Glasner <fzglas.hg@dom66.de>
date Tue, 16 Sep 2025 13:39:50 +0200
parents
children 00d59dc616ba
line wrap: on
line source

# Copyright 2014-2024 Louis Paternault and contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

"""Paper-size related functions"""

import os
import subprocess
import typing
from decimal import Decimal

import papersize

from . import errors


def parse_lc_paper(string) -> tuple[Decimal, Decimal]:
    """Parse LC_PAPER locale variable

    We assume units are milimeters.
    """
    dimensions = {}
    for line in string.split("\n"):
        if line.startswith("width="):
            dimensions["width"] = papersize.parse_length(f"{line[6:]}mm")
        if line.startswith("height="):
            dimensions["height"] = papersize.parse_length(f"{line[7:]}mm")
    if len(dimensions) == 2:
        return (dimensions["width"], dimensions["height"])
    raise errors.CouldNotParse(string)


def target_papersize(target_size) -> tuple[Decimal, Decimal]:
    """Return the target paper size.

    :param str|tuple[decimal.Decimal]|None target_size: Target size,
        if provided by user in command line.
    """
    # pylint: disable=too-many-return-statements

    # Option set by user on command line
    if target_size is not None:
        if isinstance(target_size, str):
            return papersize.parse_papersize(target_size)
        return target_size

    # LC_PAPER environment variable (can be read from "locale -k LC_PAPER"
    try:
        return parse_lc_paper(
            subprocess.check_output(["locale", "-k", "LC_PAPER"], text=True)
        )
    except (FileNotFoundError, subprocess.CalledProcessError, errors.CouldNotParse):
        pass

    # PAPERSIZE environment variable
    try:
        return papersize.parse_papersize(os.environ["PAPERSIZE"].strip())
    except KeyError:
        pass

    # file described by the PAPERCONF environment variable
    try:
        return papersize.parse_papersize(
            # pylint: disable=unspecified-encoding
            open(os.environ["PAPERCONF"])
            .read()
            .strip()
        )
    except errors.CouldNotParse:
        raise
    except:  # pylint: disable=bare-except
        pass

    # content of /etc/papersize
    try:
        # pylint: disable=unspecified-encoding
        return papersize.parse_papersize(open("/etc/papersize").read().strip())
    except errors.CouldNotParse:
        raise
    except:  # pylint: disable=bare-except
        pass

    # stdout of the paperconf command
    try:
        return papersize.parse_papersize(
            subprocess.check_output(["paperconf"], text=True).strip()
        )
    except (FileNotFoundError, subprocess.CalledProcessError, errors.CouldNotParse):
        pass

    # Eventually, if everything else has failed, a4
    return papersize.parse_papersize("a4")


class Margins(typing.NamedTuple):
    """Represents the margins at all four sides of a page."""

    top: Decimal
    right: Decimal
    bottom: Decimal
    left: Decimal

    @classmethod
    def parse(
        cls,
        margin: "None | str | Decimal | typing.Sequence[str | Decimal] | typing.Self",
    ) -> "typing.Self":
        """Normalize a margin specification into a (top, right, bottom, left) quadruple
        and parse any strings into numbers.
        """
        match margin:
            case Margins(top, right, bottom, left):
                pass
            case None:
                return cls.parse(papersize.parse_length(string="0"))
            case str(margin):
                return cls.parse((papersize.parse_length(margin),))
            case Decimal() as margin:
                return cls.parse((margin,))
            case [*margins]:  # "sequence" pattern despite square-brackets
                match tuple(
                    papersize.parse_length(m) if not isinstance(m, Decimal) else m
                    for m in margins
                ):
                    case (all_four_sides,):
                        top = right = bottom = left = all_four_sides
                    case (
                        vertical,
                        horizontal,
                    ):
                        top = bottom = vertical
                        right = left = horizontal
                    case (
                        top,
                        horizontal,
                        bottom,
                    ):
                        left = right = horizontal
                    case (
                        top,
                        right,
                        bottom,
                        left,
                    ):
                        pass
                    case _:
                        return cls.parse(papersize.parse_length(string="0"))
        return cls(top, right, bottom, left)