1# SPDX-FileCopyrightText: 2022 James R. Barlow
2# SPDX-License-Identifier: MPL-2.0
3
4"""Support functions called by the C++ library binding layer.
5
6Not intended to be called from Python, and subject to change at any time.
7"""
8
9from __future__ import annotations
10
11from typing import TYPE_CHECKING, Callable
12from warnings import warn
13
14from pikepdf.exceptions import PdfError
15from pikepdf.objects import Name
16
17if TYPE_CHECKING:
18 from pikepdf._core import Pdf
19 from pikepdf.objects import Dictionary
20
21
22def update_xmp_pdfversion(pdf: Pdf, version: str) -> None:
23 """Update XMP metadata to specified PDF version."""
24 if Name.Metadata not in pdf.Root:
25 return # Don't create an empty XMP object just to store the version
26
27 try:
28 with pdf.open_metadata(
29 set_pikepdf_as_editor=False, update_docinfo=False
30 ) as meta:
31 if 'pdf:PDFVersion' in meta:
32 meta['pdf:PDFVersion'] = version
33 except Exception as e:
34 raise PdfError("While trying to update XMP metadata, an error occurred") from e
35
36
37def _alpha(n: int) -> str:
38 """Excel-style column numbering A..Z, AA..AZ..BA..ZZ.., AAA."""
39 if n < 1:
40 raise ValueError(f"Can't represent {n} in alphabetic numbering")
41 p = []
42 while n > 0:
43 n, r = divmod(n - 1, 26)
44 p.append(r)
45 base = ord('A')
46 ords = [(base + v) for v in reversed(p)]
47 return ''.join(chr(o) for o in ords)
48
49
50def _roman(n: int) -> str:
51 """Convert integer n to Roman numeral representation as a string."""
52 if not (1 <= n <= 5000):
53 raise ValueError(f"Can't represent {n} in Roman numerals")
54 roman_numerals = (
55 (1000, 'M'),
56 (900, 'CM'),
57 (500, 'D'),
58 (400, 'CD'),
59 (100, 'C'),
60 (90, 'XC'),
61 (50, 'L'),
62 (40, 'XL'),
63 (10, 'X'),
64 (9, 'IX'),
65 (5, 'V'),
66 (4, 'IV'),
67 (1, 'I'),
68 )
69 roman = ""
70 for value, numeral in roman_numerals:
71 while n >= value:
72 roman += numeral
73 n -= value
74 return roman
75
76
77LABEL_STYLE_MAP: dict[str, Callable[[int], str]] = {
78 "D": str,
79 "A": _alpha,
80 "a": lambda x: _alpha(x).lower(),
81 "R": _roman,
82 "r": lambda x: _roman(x).lower(),
83}
84
85
86def label_from_label_dict(label_dict: int | Dictionary) -> str:
87 """Convert a label dictionary returned by qpdf into a text string."""
88 if isinstance(label_dict, int):
89 return str(label_dict)
90
91 label = ''
92 if Name.P in label_dict:
93 prefix = label_dict[Name.P]
94 label += str(prefix)
95
96 # If there is no S, return only the P portion
97 if Name.S in label_dict:
98 # St defaults to 1
99 numeric_value = label_dict[Name.St] if Name.St in label_dict else 1
100 if not isinstance(numeric_value, int):
101 warn(
102 "Page label dictionary has invalid non-integer start value", UserWarning
103 )
104 numeric_value = 1
105
106 style = label_dict[Name.S]
107 if isinstance(style, Name):
108 style_fn = LABEL_STYLE_MAP[str(style)[1:]]
109 value = style_fn(numeric_value)
110 label += value
111 else:
112 warn("Page label dictionary has invalid page label style", UserWarning)
113
114 return label