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
« 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.
3Implementation Note: The not a number (NAN) marker is used to indicate absence of scale command.
4"""
6from collections.abc import Iterable
7from enum import Enum
8import math
10from liitos import log
12Modus = Enum('Modus', 'COPY SCALE')
13NAN = float('nan')
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
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.
26 Examples:
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)
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)
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
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
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)
73 return modus, rescale
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.
79 Examples:
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)
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
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)
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)
148 return modus, rescale
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.
154 Examples:
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']
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)
177 return outgoing