Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/_text_extraction/_layout_mode/_text_state_params.py: 47%

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

73 statements  

1"""A dataclass that captures the CTM and Text State for a tj operation""" 

2 

3import math 

4from dataclasses import dataclass, field 

5from typing import Any, Union 

6 

7from ...generic._font import Font 

8from .. import mult, orient 

9 

10 

11@dataclass 

12class TextStateParams: 

13 """ 

14 Text state parameters and operator values for a single text value in a 

15 TJ or Tj PDF operation. 

16 

17 Attributes: 

18 value (bytes | str): the raw text to be rendered. 

19 font (Font): font object 

20 font_size (int | float): font size 

21 Tc (float): character spacing. Defaults to 0.0. 

22 Tw (float): word spacing. Defaults to 0.0. 

23 Tz (float): horizontal scaling. Defaults to 100.0. 

24 TL (float): leading, vertical displacement between text lines. Defaults to 0.0. 

25 Ts (float): text rise. Used for super/subscripts. Defaults to 0.0. 

26 transform (List[float]): effective transformation matrix. 

27 tx (float): x cood of rendered text, i.e. self.transform[4] 

28 ty (float): y cood of rendered text. May differ from self.transform[5] per self.Ts. 

29 displaced_tx (float): x coord immediately following rendered text 

30 space_tx (float): tx for a space character 

31 font_height (float): effective font height accounting for CTM 

32 flip_vertical (bool): True if y axis has been inverted (i.e. if self.transform[3] < 0.) 

33 rotated (bool): True if the text orientation is rotated with respect to the page. 

34 

35 """ 

36 

37 value: Union[bytes, str] 

38 font: Font 

39 font_size: Union[int, float] 

40 Tc: float = 0.0 

41 Tw: float = 0.0 

42 Tz: float = 100.0 

43 TL: float = 0.0 

44 Ts: float = 0.0 

45 transform: list[float] = field( 

46 default_factory=lambda: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

47 ) 

48 tx: float = field(default=0.0, init=False) 

49 ty: float = field(default=0.0, init=False) 

50 displaced_tx: float = field(default=0.0, init=False) 

51 space_tx: float = field(default=0.0, init=False) 

52 font_height: float = field(default=0.0, init=False) 

53 flip_vertical: bool = field(default=False, init=False) 

54 rotated: bool = field(default=False, init=False) 

55 text: str = "" 

56 _raw_chars: str = "" 

57 

58 def __post_init__(self) -> None: 

59 decoded_value: str = "" 

60 if isinstance(self.value, bytes): 

61 if isinstance(self.font.encoding, str): 

62 try: 

63 # Decode 2-byte UTF-16-BE units 

64 self._raw_chars = self.value.decode(self.font.encoding, "surrogatepass") 

65 except UnicodeDecodeError: 

66 # Fallback for odd byte counts or unmapped 16-bit GIDs/CIDs 

67 self._raw_chars = self.value.decode(self.font.encoding, "surrogateescape") 

68 else: 

69 self._raw_chars = "".join(chr(byte) for byte in self.value) 

70 decoded_value = "".join(self.font.encoding.get(x, chr(x)) for x in self.value) 

71 self.text = "".join( 

72 self.font.character_map.get(x, x) for x in (decoded_value or self._raw_chars) 

73 ) 

74 else: 

75 self.text = self.value 

76 

77 if orient(self.transform) in (90, 270): 

78 self.transform = mult( 

79 [1.0, -self.transform[1], -self.transform[2], 1.0, 0.0, 0.0], 

80 self.transform, 

81 ) 

82 self.rotated = True 

83 # self.transform[0] AND self.transform[3] < 0 indicates true rotation. 

84 # If only self.transform[3] < 0, the y coords are simply inverted. 

85 if orient(self.transform) == 180 and self.transform[0] < -1e-6: 

86 self.transform = mult([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0], self.transform) 

87 self.rotated = True 

88 self.displaced_tx = self.displaced_transform()[4] 

89 self.tx = self.transform[4] 

90 self.ty = self.render_transform()[5] 

91 self.space_tx = round(self.word_tx(self.font.space_char), 3) 

92 if self.space_tx < 1e-6: 

93 # if the " " char is assigned 0 width (e.g. for fine tuned spacing 

94 # with TJ int operators a la crazyones.pdf), calculate space_tx as 

95 # a td_offset of -1 * font.space_width where font.space_width is 

96 # the space_width calculated in _font.py. 

97 self.space_tx = round(self.word_tx("", -self.font.space_width), 3) 

98 self.font_height = self.font_size * math.sqrt( 

99 self.transform[1] ** 2 + self.transform[3] ** 2 

100 ) 

101 # flip_vertical handles PDFs generated by Microsoft Word's "publish" command. 

102 self.flip_vertical = self.transform[3] < -1e-6 # inverts y axis 

103 

104 def font_size_matrix(self) -> list[float]: 

105 """Font size matrix""" 

106 return [ 

107 self.font_size * (self.Tz / 100.0), 

108 0.0, 

109 0.0, 

110 self.font_size, 

111 0.0, 

112 self.Ts, 

113 ] 

114 

115 def displaced_transform(self) -> list[float]: 

116 """Effective transform matrix after text has been rendered.""" 

117 return mult(self.displacement_matrix(), self.transform) 

118 

119 def render_transform(self) -> list[float]: 

120 """Effective transform matrix accounting for font size, Tz, and Ts.""" 

121 return mult(self.font_size_matrix(), self.transform) 

122 

123 def displacement_matrix( 

124 self, word: Union[bytes, str, None] = None, td_offset: float = 0.0 

125 ) -> list[float]: 

126 """ 

127 Text displacement matrix 

128 

129 Args: 

130 word (bytes | str, optional): Defaults to None in which case self.text displacement is 

131 returned. 

132 td_offset (float, optional): translation applied by TD operator. Defaults to 0.0. 

133 

134 """ 

135 word = word if word is not None else self.value 

136 return [1.0, 0.0, 0.0, 1.0, self.word_tx(word, td_offset), 0.0] 

137 

138 def word_tx(self, word: Union[bytes, str], td_offset: float = 0.0) -> float: 

139 """Horizontal text displacement for any word according this text state""" 

140 width: float = 0.0 

141 if isinstance(word, bytes): 

142 word = self._raw_chars 

143 

144 for char in word: 

145 if char == self.font.space_char: 

146 width += self.font.space_width 

147 else: 

148 width += self.font.get_text_width(char) 

149 

150 return ( 

151 (self.font_size * ((width - td_offset) / 1000.0)) 

152 + len(word) * self.Tc 

153 + word.count(self.font.space_char) * self.Tw 

154 ) * (self.Tz / 100.0) 

155 

156 @staticmethod 

157 def to_dict(inst: "TextStateParams") -> dict[str, Any]: 

158 """Dataclass to dict for json.dumps serialization""" 

159 return {k: getattr(inst, k) for k in inst.__dataclass_fields__ if k != "font"}