Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pikepdf/_cpphelpers.py: 22%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

60 statements  

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 collections.abc import Callable 

12from typing import TYPE_CHECKING 

13from warnings import warn 

14 

15from pikepdf.exceptions import PdfError 

16from pikepdf.objects import Name 

17 

18if TYPE_CHECKING: 

19 from pikepdf._core import Pdf 

20 from pikepdf.objects import Dictionary 

21 

22 

23def update_xmp_pdfversion(pdf: Pdf, version: str) -> None: 

24 """Update XMP metadata to specified PDF version.""" 

25 if Name.Metadata not in pdf.Root: 

26 return # Don't create an empty XMP object just to store the version 

27 

28 try: 

29 with pdf.open_metadata( 

30 set_pikepdf_as_editor=False, update_docinfo=False 

31 ) as meta: 

32 if 'pdf:PDFVersion' in meta: 

33 meta['pdf:PDFVersion'] = version 

34 except Exception as e: 

35 raise PdfError("While trying to update XMP metadata, an error occurred") from e 

36 

37 

38def _alpha(n: int) -> str: 

39 """Excel-style column numbering A..Z, AA..AZ..BA..ZZ.., AAA.""" 

40 if n < 1: 

41 raise ValueError(f"Can't represent {n} in alphabetic numbering") 

42 p = [] 

43 while n > 0: 

44 n, r = divmod(n - 1, 26) 

45 p.append(r) 

46 base = ord('A') 

47 ords = [(base + v) for v in reversed(p)] 

48 return ''.join(chr(o) for o in ords) 

49 

50 

51def _roman(n: int) -> str: 

52 """Convert integer n to Roman numeral representation as a string.""" 

53 if not (1 <= n <= 5000): 

54 raise ValueError(f"Can't represent {n} in Roman numerals") 

55 roman_numerals = ( 

56 (1000, 'M'), 

57 (900, 'CM'), 

58 (500, 'D'), 

59 (400, 'CD'), 

60 (100, 'C'), 

61 (90, 'XC'), 

62 (50, 'L'), 

63 (40, 'XL'), 

64 (10, 'X'), 

65 (9, 'IX'), 

66 (5, 'V'), 

67 (4, 'IV'), 

68 (1, 'I'), 

69 ) 

70 roman = "" 

71 for value, numeral in roman_numerals: 

72 while n >= value: 

73 roman += numeral 

74 n -= value 

75 return roman 

76 

77 

78LABEL_STYLE_MAP: dict[str, Callable[[int], str]] = { 

79 "D": str, 

80 "A": _alpha, 

81 "a": lambda x: _alpha(x).lower(), 

82 "R": _roman, 

83 "r": lambda x: _roman(x).lower(), 

84} 

85 

86 

87def label_from_label_dict(label_dict: int | Dictionary) -> str: 

88 """Convert a label dictionary returned by qpdf into a text string.""" 

89 if isinstance(label_dict, int): 

90 return str(label_dict) 

91 

92 label = '' 

93 if Name.P in label_dict: 

94 prefix = label_dict[Name.P] 

95 label += str(prefix) 

96 

97 # If there is no S, return only the P portion 

98 if Name.S in label_dict: 

99 # St defaults to 1 

100 numeric_value = label_dict[Name.St] if Name.St in label_dict else 1 

101 if not isinstance(numeric_value, int): 

102 warn( 

103 "Page label dictionary has invalid non-integer start value", UserWarning 

104 ) 

105 numeric_value = 1 

106 

107 style = label_dict[Name.S] 

108 if isinstance(style, Name): 

109 style_fn = LABEL_STYLE_MAP[str(style)[1:]] 

110 value = style_fn(numeric_value) 

111 label += value 

112 else: 

113 warn("Page label dictionary has invalid page label style", UserWarning) 

114 

115 return label