Coverage for liitos/figures.py: 97.80%

71 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-28 20:14:46 +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 

9from typing import Union 

10 

11from liitos import log 

12 

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

14NAN = float('nan') 

15 

16SCALE_START_TRIGGER_STARTSWITH = r'\scale=' 

17BARE_GRAPHICS_START_STARTSWITH = r'\includegraphics{' 

18WRAPPED_GRAPHICS_START_IN = r'\pandocbounded{\includegraphics' 

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

20 

21 

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

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

24 

25 Examples: 

26 

27 >>> o = [] 

28 >>> line = SCALE_START_TRIGGER_STARTSWITH # r'\scale=' 

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

30 >>> assert not o 

31 >>> assert m == Modus.SCALE 

32 >>> assert math.isnan(r) 

33 

34 >>> o = [] 

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

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

37 >>> assert m == Modus.COPY 

38 >>> assert math.isnan(r) 

39 

40 >>> o = [] 

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

42 >>> assert not o 

43 >>> assert m == Modus.SCALE 

44 >>> assert r == 0.8 

45 """ 

46 if line.startswith(SCALE_START_TRIGGER_STARTSWITH): 

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

48 modus = Modus.SCALE 

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

50 try: 

51 sca = scale.split('=', 1)[1].strip() # \scale = 75\% --> 75\% 

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

53 except Exception as err: 

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

55 else: 

56 outgoing.append(line) 

57 

58 return modus, rescale 

59 

60 

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

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

63 

64 Examples: 

65 

66 >>> o = [] 

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

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

69 >>> assert m == Modus.COPY 

70 >>> assert math.isnan(r) 

71 

72 >>> o = [] 

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

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

75 >>> assert m == Modus.COPY 

76 >>> assert r == 0.8 

77 

78 >>> o = [] 

79 >>> rescale = 0.8 

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

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

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

83 >>> assert m == Modus.COPY 

84 >>> assert math.isnan(r) 

85 

86 >>> o = [] 

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

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

89 >>> assert m == Modus.COPY 

90 >>> assert math.isnan(r) 

91 """ 

92 if line.startswith(BARE_GRAPHICS_START_STARTSWITH): 

93 if not math.isnan(rescale): 

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

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

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

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

98 else: 

99 outgoing.append(line) 

100 modus = Modus.COPY 

101 rescale = NAN 

102 elif EVEN_MORE_SO in line: 

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

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

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

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

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

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

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

110 hack = '}}' 

111 if not patched.endswith(hack): 

112 patched += hack 

113 outgoing.append(patched) 

114 else: 

115 outgoing.append(line) 

116 modus = Modus.COPY 

117 rescale = NAN 

118 elif WRAPPED_GRAPHICS_START_IN in line: 

119 if not math.isnan(rescale): 

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

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

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

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

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

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

126 else: 

127 outgoing.append(line) 

128 modus = Modus.COPY 

129 rescale = NAN 

130 else: 

131 outgoing.append(line) 

132 

133 return modus, rescale 

134 

135 

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

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

138 

139 Examples: 

140 

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

142 >>> scaled = scale(in_lines) 

143 >>> scaled 

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

145 

146 

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

148 >>> scaled = scale(in_lines) 

149 >>> scaled 

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

151 """ 

152 outgoing: list[str] = [] 

153 modus = Modus.COPY 

154 rescale = NAN 

155 for slot, line in enumerate(incoming): 

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

157 if modus == Modus.COPY: 

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

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

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

161 

162 return outgoing