Coverage for liitos/figures.py: 100.00%
55 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-05 17:22:35 +00:00
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-05 17:22:35 +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
9from typing import Union
11from liitos import log
13Modus = Enum('Modus', 'COPY SCALE')
14NAN = float('nan')
16SCALE_START_TRIGGER_STARTSWITH = r'\scale='
17BARE_GRAPHICS_START_STARTSWITH = r'\includegraphics{'
18WRAPPED_GRAPHICS_START_IN = r'\pandocbounded{\includegraphics'
21def filter_seek_scale(line: str, slot: int, modus: Modus, rescale: float, outgoing: list[str]) -> tuple[Modus, float]:
22 r"""Filter line, seek for a scale command, and return updated mnodus, rescale pair.
24 Examples:
26 >>> o = []
27 >>> line = SCALE_START_TRIGGER_STARTSWITH # r'\scale='
28 >>> m, r = filter_seek_scale(line, 0, Modus.COPY, NAN, o)
29 >>> assert not o
30 >>> assert m == Modus.SCALE
31 >>> assert math.isnan(r)
33 >>> o = []
34 >>> m, r = filter_seek_scale('foo', 0, Modus.COPY, NAN, o)
35 >>> assert o == ['foo']
36 >>> assert m == Modus.COPY
37 >>> assert math.isnan(r)
39 >>> o = []
40 >>> m, r = filter_seek_scale(r'\scale=80\%', 0, Modus.COPY, NAN, o)
41 >>> assert not o
42 >>> assert m == Modus.SCALE
43 >>> assert r == 0.8
44 """
45 if line.startswith(SCALE_START_TRIGGER_STARTSWITH):
46 log.info(f'trigger a scale mod for the next figure environment at line #{slot + 1}|{line}')
47 modus = Modus.SCALE
48 scale = line # only for reporting will not pass the filter
49 try:
50 sca = scale.split('=', 1)[1].strip() # \scale = 75\% --> 75\%
51 rescale = float(sca.replace(r'\%', '')) / 100 if r'\%' in sca else float(sca)
52 except Exception as err:
53 log.error(f'failed to parse scale value from {line.strip()} with err: {err}')
54 else:
55 outgoing.append(line)
57 return modus, rescale
60def filter_seek_figure(line: str, slot: int, modus: Modus, rescale: float, outgoing: list[str]) -> tuple[Modus, float]:
61 r"""Filter line, seek for a figure, rescale if applicable, and return updated mnodus, rescale pair.
63 Examples:
65 >>> o = []
66 >>> m, r = filter_seek_figure(r'\includegraphics{', 0, Modus.COPY, NAN, o)
67 >>> assert o == [r'\includegraphics{']
68 >>> assert m == Modus.COPY
69 >>> assert math.isnan(r)
71 >>> o = []
72 >>> m, r = filter_seek_figure('foo', 0, Modus.COPY, 0.8, o)
73 >>> assert o == ['foo']
74 >>> assert m == Modus.COPY
75 >>> assert r == 0.8
77 >>> o = []
78 >>> rescale = 0.8
79 >>> m, r = filter_seek_figure(r'\pandocbounded{\includegraphics', 0, Modus.COPY, rescale, o)
80 >>> assert o[0].startswith(r'\pandocbounded{\includegraphics')
81 >>> assert f'textwidth,height={rescale}' in o[0]
82 >>> assert m == Modus.COPY
83 >>> assert math.isnan(r)
85 >>> o = []
86 >>> m, r = filter_seek_figure(r'\pandocbounded{\includegraphics', 0, Modus.COPY, NAN, o)
87 >>> assert o[0].startswith(r'\pandocbounded{\includegraphics')
88 >>> assert m == Modus.COPY
89 >>> assert math.isnan(r)
90 """
91 if line.startswith(BARE_GRAPHICS_START_STARTSWITH):
92 if not math.isnan(rescale):
93 log.info(f'- found the scale target start at line #{slot + 1}|{line}')
94 target = line.replace(BARE_GRAPHICS_START_STARTSWITH, '{')
95 option = f'[width={round(rescale, 2)}\\textwidth,height={round(rescale, 2)}' '\\textheight,keepaspectratio]'
96 outgoing.append(f'\\includegraphics{option}{target}')
97 else:
98 outgoing.append(line)
99 modus = Modus.COPY
100 rescale = NAN
101 elif WRAPPED_GRAPHICS_START_IN in line:
102 if not math.isnan(rescale):
103 log.info(f'- found the scale target start at line #{slot + 1}|{line}')
104 target = line.replace(WRAPPED_GRAPHICS_START_IN, '').replace('[keepaspectratio]', '')
105 parts = target.split('}}')
106 rest, inside = ('', '') if len(parts) < 2 else (parts[1].lstrip('}'), parts[0] + '}')
107 option = f'[width={round(rescale, 2)}\\textwidth,height={round(rescale, 2)}' '\\textheight,keepaspectratio]'
108 outgoing.append(f'{WRAPPED_GRAPHICS_START_IN}{option}{inside}} {rest}')
109 else:
110 outgoing.append(line)
111 modus = Modus.COPY
112 rescale = NAN
113 else:
114 outgoing.append(line)
116 return modus, rescale
119def scale(incoming: Iterable[str], lookup: Union[dict[str, str], None] = None) -> list[str]:
120 r"""Scan for scale command and if, apply it to the includegraphics LaTeX command.
122 Examples:
124 >>> in_lines = [r'\scale=80\%', '', r'\includegraphics{', '', 'quux']
125 >>> scaled = scale(in_lines)
126 >>> scaled
127 ['', '\\includegraphics[width=0.8\\textwidth,height=0.8\\textheight,keepaspectratio]{', '', 'quux']
130 >>> in_lines = ['foo', '', r'\includegraphics{', '', 'quux']
131 >>> scaled = scale(in_lines)
132 >>> scaled
133 ['foo', '', '\\includegraphics{', '', 'quux']
134 """
135 outgoing: list[str] = []
136 modus = Modus.COPY
137 rescale = NAN
138 for slot, line in enumerate(incoming):
139 line = line.rstrip('\n')
140 if modus == Modus.COPY:
141 modus, rescale = filter_seek_scale(line, slot, modus, rescale, outgoing)
142 else: # if modus == Modus.SCALE:
143 modus, rescale = filter_seek_figure(line, slot, modus, rescale, outgoing)
145 return outgoing