Coverage for liitos / figures.py: 83.70%

72 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-03 22:54:48 +00:00

1"""Apply any scale command to subsequent figure environment. 

2 

3Implementation Note: The not a number (NAN) marker is used to indicate absence of scale command. 

4""" 

5 

6from collections.abc import Iterable 

7from enum import Enum 

8import math 

9 

10from liitos import log 

11 

12Modus = Enum('Modus', 'COPY SCALE') 

13NAN = float('nan') 

14 

15EQ = '=' 

16SP = ' ' 

17SCALE_START_TRIGGER_STARTSWITH = r'\scale' 

18BARE_GRAPHICS_START_STARTSWITH = r'\includegraphics{' 

19WRAPPED_GRAPHICS_START_IN = r'\pandocbounded{\includegraphics' 

20EVEN_MORE_SO = r'\pandocbounded{\includegraphics[keepaspectratio,alt={' # HACK A DID ACK 

21 

22 

23def filter_seek_scale(line: str, slot: int, modus: Modus, rescale: float, outgoing: list[str]) -> tuple[Modus, float]: 

24 r"""Filter line, seek for a scale command, and return updated mnodus, rescale pair. 

25 

26 Examples: 

27 

28 >>> o = [] 

29 >>> line = SCALE_START_TRIGGER_STARTSWITH + EQ # r'\scale=' 

30 >>> m, r = filter_seek_scale(line, 0, Modus.COPY, NAN, o) 

31 >>> assert not o 

32 >>> assert m == Modus.SCALE 

33 >>> assert math.isnan(r) 

34 

35 >>> o = [] 

36 >>> m, r = filter_seek_scale('foo', 0, Modus.COPY, NAN, o) 

37 >>> assert o == ['foo'] 

38 >>> assert m == Modus.COPY 

39 >>> assert math.isnan(r) 

40 

41 >>> o = [] 

42 >>> m, r = filter_seek_scale(r'\scale=80\%', 0, Modus.COPY, NAN, o) 

43 >>> assert not o 

44 >>> assert m == Modus.SCALE 

45 >>> assert r == 0.8 

46 

47 >>> o = [] 

48 >>> m, r = filter_seek_scale(r'\scale 80\%', 0, Modus.COPY, NAN, o) 

49 >>> assert not o 

50 >>> assert m == Modus.SCALE 

51 >>> assert r == 0.8 

52 

53 >>> o = [] 

54 >>> m, r = filter_seek_scale(r'\scale = 0.8', 0, Modus.COPY, NAN, o) 

55 >>> assert not o 

56 >>> assert m == Modus.SCALE 

57 >>> assert r == 0.8 

58 """ 

59 if any(line.startswith(SCALE_START_TRIGGER_STARTSWITH + other) for other in (EQ, SP)): 

60 log.info(f'trigger a scale mod for the next figure environment at line #{slot + 1}|{line}') 

61 modus = Modus.SCALE 

62 scale = line # only for reporting will not pass the filter 

63 try: 

64 # \scale = 75\% --> 75\% 

65 # \scale 75\% --> 75\% 

66 sca = scale.split(EQ, 1)[1].strip() if EQ in scale else SP.join(scale.split()).split(SP, 1)[1].strip() 

67 rescale = float(sca.replace(r'\%', '')) / 100 if r'\%' in sca else float(sca) 

68 except Exception as err: 

69 log.error(f'failed to parse scale value from {line.strip()} with err: {err}') 

70 else: 

71 outgoing.append(line) 

72 

73 return modus, rescale 

74 

75 

76def filter_seek_figure(line: str, slot: int, modus: Modus, rescale: float, outgoing: list[str]) -> tuple[Modus, float]: 

77 r"""Filter line, seek for a figure, rescale if applicable, and return updated mnodus, rescale pair. 

78 

79 Examples: 

80 

81 >>> o = [] 

82 >>> m, r = filter_seek_figure(r'\includegraphics{', 0, Modus.COPY, NAN, o) 

83 >>> assert o == [r'\includegraphics{'] 

84 >>> assert m == Modus.COPY 

85 >>> assert math.isnan(r) 

86 

87 >>> o = [] 

88 >>> m, r = filter_seek_figure('foo', 0, Modus.COPY, 0.8, o) 

89 >>> assert o == ['foo'] 

90 >>> assert m == Modus.COPY 

91 >>> assert r == 0.8 

92 

93 >>> o = [] 

94 >>> rescale = 0.8 

95 >>> m, r = filter_seek_figure(r'\pandocbounded{\includegraphics', 0, Modus.COPY, rescale, o) 

96 >>> assert o[0].startswith(r'\pandocbounded{\includegraphics') 

97 >>> assert f'textwidth,height={rescale}' in o[0] 

98 >>> assert m == Modus.COPY 

99 >>> assert math.isnan(r) 

100 

101 >>> o = [] 

102 >>> m, r = filter_seek_figure(r'\pandocbounded{\includegraphics', 0, Modus.COPY, NAN, o) 

103 >>> assert o[0].startswith(r'\pandocbounded{\includegraphics') 

104 >>> assert m == Modus.COPY 

105 >>> assert math.isnan(r) 

106 """ 

107 if line.startswith(BARE_GRAPHICS_START_STARTSWITH): 

108 if not math.isnan(rescale): 

109 log.info(f'- found the scale target start at line #{slot + 1}|{line}') 

110 target = line.replace(BARE_GRAPHICS_START_STARTSWITH, '{') 

111 option = f'[width={round(rescale, 2)}\\textwidth,height={round(rescale, 2)}' '\\textheight,keepaspectratio]' 

112 outgoing.append(f'\\includegraphics{option}{target}') 

113 else: 

114 outgoing.append(line) 

115 modus = Modus.COPY 

116 rescale = NAN 

117 elif EVEN_MORE_SO in line: 

118 if not math.isnan(rescale): 118 ↛ 130line 118 didn't jump to line 130 because the condition on line 118 was always true

119 log.info(f'- found the scale target start at line #{slot + 1}|{line}') 

120 target = line.replace(WRAPPED_GRAPHICS_START_IN, '').replace('[keepaspectratio', '') 

121 parts = target.split('}}') 

122 rest, inside = ('', '') if len(parts) < 2 else (parts[1].lstrip('}'), parts[0] + '}') 

123 option = f'[width={round(rescale, 2)}\\textwidth,height={round(rescale, 2)}' '\\textheight,keepaspectratio' 

124 patched = f'{WRAPPED_GRAPHICS_START_IN}{option}{inside}}}{rest}' 

125 hack = '}}' 

126 if not patched.endswith(hack): 

127 patched += hack 

128 outgoing.append(patched) 

129 else: 

130 outgoing.append(line) 

131 modus = Modus.COPY 

132 rescale = NAN 

133 elif WRAPPED_GRAPHICS_START_IN in line: 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true

134 if not math.isnan(rescale): 

135 log.info(f'- found the scale target start at line #{slot + 1}|{line}') 

136 target = line.replace(WRAPPED_GRAPHICS_START_IN, '').replace('[keepaspectratio]', '') 

137 parts = target.split('}}') 

138 rest, inside = ('', '') if len(parts) < 2 else (parts[1].lstrip('}'), parts[0] + '}') 

139 option = f'[width={round(rescale, 2)}\\textwidth,height={round(rescale, 2)}' '\\textheight,keepaspectratio' 

140 outgoing.append(f'{WRAPPED_GRAPHICS_START_IN}{option}{inside}}}{rest}') 

141 else: 

142 outgoing.append(line) 

143 modus = Modus.COPY 

144 rescale = NAN 

145 else: 

146 outgoing.append(line) 

147 

148 return modus, rescale 

149 

150 

151def scale(incoming: Iterable[str], lookup: dict[str, str] | None = None) -> list[str]: 

152 r"""Scan for scale command and if, apply it to the includegraphics LaTeX command. 

153 

154 Examples: 

155 

156 >>> in_lines = [r'\scale=80\%', '', r'\includegraphics{', '', 'quux'] 

157 >>> scaled = scale(in_lines) 

158 >>> scaled 

159 ['', '\\includegraphics[width=0.8\\textwidth,height=0.8\\textheight,keepaspectratio]{', '', 'quux'] 

160 

161 

162 >>> in_lines = ['foo', '', r'\includegraphics{', '', 'quux'] 

163 >>> scaled = scale(in_lines) 

164 >>> scaled 

165 ['foo', '', '\\includegraphics{', '', 'quux'] 

166 """ 

167 outgoing: list[str] = [] 

168 modus = Modus.COPY 

169 rescale = NAN 

170 for slot, line in enumerate(incoming): 

171 line = line.rstrip('\n') 

172 if modus == Modus.COPY: 

173 modus, rescale = filter_seek_scale(line, slot, modus, rescale, outgoing) 

174 else: # if modus == Modus.SCALE: 

175 modus, rescale = filter_seek_figure(line, slot, modus, rescale, outgoing) 

176 

177 return outgoing