pandoc_latex_absolute_image._main.add_latex()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 21
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 21
rs 10
c 0
b 0
f 0
cc 3
nop 2
1
#!/usr/bin/env python
2
3
"""
4
Pandoc filter for adding image at absolute position in LaTeX.
5
"""
6
7
from __future__ import annotations
8
9
import re
10
from typing import Any
11
12
from panflute import (
13
    Block,
14
    Code,
15
    CodeBlock,
16
    Div,
17
    Doc,
18
    Element,
19
    Header,
20
    MetaInlines,
21
    MetaList,
22
    RawBlock,
23
    RawInline,
24
    Span,
25
    debug,
26
    run_filter,
27
)
28
29
30
def absolute_image(elem: Element, doc: Doc) -> list[Element] | None:
31
    """
32
    Apply absolute image transformation to element.
33
34
    Parameters
35
    ----------
36
    elem
37
        The element
38
    doc
39
        The original document.
40
41
    Returns
42
    -------
43
    list[Element] | None
44
        The additional elements if any.
45
    """
46
    # Is it in the right format and is it a Span, Div?
47
    if doc.format in ("latex", "beamer") and isinstance(
48
        elem, Span | Div | Code | CodeBlock | Header
49
    ):
50
        # Is there a latex-absolute-image attribute?
51
        if (
52
            "latex-absolute-image" in elem.attributes
53
            or "latex-absolute-image-reset" in elem.attributes
54
        ):
55
            return add_latex(
56
                elem,
57
                latex_code(
58
                    elem.attributes,
59
                    {
60
                        "image": "latex-absolute-image",
61
                        "image-odd": "latex-absolute-image-odd",
62
                        "image-even": "latex-absolute-image-even",
63
                        "reset": "latex-absolute-reset",
64
                        "reset-odd": "latex-absolute-reset-odd",
65
                        "reset-even": "latex-absolute-reset-even",
66
                        "width": "latex-absolute-width",
67
                        "width-odd": "latex-absolute-width-odd",
68
                        "width-even": "latex-absolute-width-even",
69
                        "height": "latex-absolute-height",
70
                        "height-odd": "latex-absolute-height-odd",
71
                        "height-even": "latex-absolute-height-even",
72
                        "anchor": "latex-absolute-anchor",
73
                        "anchor-odd": "latex-absolute-anchor-odd",
74
                        "anchor-even": "latex-absolute-anchor-even",
75
                        "x-coord": "latex-absolute-x-coord",
76
                        "x-coord-odd": "latex-absolute-x-coord-odd",
77
                        "x-coord-even": "latex-absolute-x-coord-even",
78
                        "y-coord": "latex-absolute-y-coord",
79
                        "y-coord-odd": "latex-absolute-y-coord-odd",
80
                        "y-coord-even": "latex-absolute-y-coord-even",
81
                        "opacity": "latex-absolute-opacity",
82
                        "opacity-odd": "latex-absolute-opacity-odd",
83
                        "opacity-even": "latex-absolute-opacity-even",
84
                    },
85
                ),
86
            )
87
88
        # Get the classes
89
        classes = set(elem.classes)
90
        # Loop on all font size definition
91
        # noinspection PyUnresolvedReferences
92
        for definition in doc.defined:
93
            # Are the classes correct?
94
            if classes >= definition["classes"]:
95
                return add_latex(elem, definition["latex"])
96
97
    return None
98
99
100
def add_latex(elem: Element, latex: str) -> list[Element] | None:
101
    """
102
    Add latex code.
103
104
    Parameters
105
    ----------
106
    elem
107
        Current element
108
    latex
109
        Latex code
110
111
    Returns
112
    -------
113
    list[Element] | None
114
        The additional elements if any.
115
    """
116
    if bool(latex):
117
        if isinstance(elem, Block):
118
            return [RawBlock(latex, "tex"), elem]
119
        return [RawInline(latex, "tex"), elem]
120
    return None
121
122
123
# pylint: disable=too-many-arguments,too-many-locals,too-many-statements
124
def latex_code(definition: dict[str, Any], keys: dict[str, str]) -> str:
125
    """
126
    Get the latex code.
127
128
    Parameters
129
    ----------
130
    definition
131
        The defition
132
    keys
133
        Key mapping
134
135
    Returns
136
    -------
137
    str
138
        The latex code.
139
    """
140
    path = definition.get(keys["image"])
141
    path_odd = definition.get(keys["image-odd"], path)
142
    path_even = definition.get(keys["image-even"], path)
143
144
    reset = definition.get(keys["reset"])
145
    reset_odd = definition.get(keys["reset-odd"], reset)
146
    reset_even = definition.get(keys["reset-even"], reset)
147
148
    width = get_latex_size(definition.get(keys["width"]))
149
    width_odd = get_latex_size(definition.get(keys["width-odd"]), width)
150
    width_even = get_latex_size(definition.get(keys["width-even"]), width)
151
152
    height = get_latex_size(definition.get(keys["height"]))
153
    height_odd = get_latex_size(definition.get(keys["height-odd"]), height)
154
    height_even = get_latex_size(definition.get(keys["height-even"]), height)
155
156
    anchor = get_anchor(definition.get(keys["anchor"]))
157
    anchor_odd = get_anchor(definition.get(keys["anchor-odd"]), anchor)
158
    anchor_even = get_anchor(definition.get(keys["anchor-even"]), anchor)
159
160
    opacity = get_opacity(definition.get(keys["opacity"]))
161
    opacity_odd = get_opacity(definition.get(keys["opacity-odd"]), opacity)
162
    opacity_even = get_opacity(definition.get(keys["opacity-even"]), opacity)
163
164
    x_coord = get_latex_size(
165
        definition.get(keys["x-coord"], "0cm"),
166
        "0cm",
167
    )
168
    x_coord_odd = get_latex_size(
169
        definition.get(keys["x-coord-odd"], x_coord),
170
        x_coord,
171
    )
172
    x_coord_even = get_latex_size(
173
        definition.get(keys["x-coord-even"], x_coord), x_coord
174
    )
175
176
    y_coord = get_latex_size(
177
        definition.get(keys["y-coord"], "0cm"),
178
        "0cm",
179
    )
180
    y_coord_odd = get_latex_size(definition.get(keys["y-coord-odd"], y_coord), y_coord)
181
    y_coord_even = get_latex_size(
182
        definition.get(keys["y-coord-even"], y_coord),
183
        y_coord,
184
    )
185
186
    if reset_odd:
187
        picture_odd = """
188
"""
189
    else:
190
        options = []
191
        if width_odd:
192
            options.append(f"width={width_odd}")
193
        if height_odd:
194
            options.append(f"height={height_odd}")
195
        options = ",".join(options)
196
197
        node_options = []
198
        if anchor_odd:
199
            node_options.append(f"anchor={anchor_odd}")
200
        if opacity_odd:
201
            node_options.append(f"opacity={opacity_odd}")
202
        node_options = ",".join(node_options)
203
204
        picture_odd = f"""
205
\\begin{{tikzpicture}}[
206
    overlay,                         % Do our drawing on an overlay instead of inline
207
    remember picture,                % Allow us to share coordinates with other drawings
208
    shift=(current page.north west), % Set the top (north) left (west) as the origin
209
    yscale=-1,                       % Switch the y-axis to increase down the page
210
    inner sep=0,                     % Remove inner separator
211
]
212
\\node[{node_options}] at ({x_coord_odd}, {y_coord_odd})
213
    {{\\includegraphics[{options}]{{{path_odd}}}}};
214
\\end{{tikzpicture}}
215
"""
216
217
    if reset_even:
218
        picture_even = """
219
"""
220
    else:
221
        options = []
222
        if width_even:
223
            options.append(f"width={width_even}")
224
        if height_odd:
225
            options.append(f"height={height_even}")
226
        options = ",".join(options)
227
228
        node_options = []
229
        if anchor_even:
230
            node_options.append(f"anchor={anchor_even}")
231
        if opacity_even:
232
            node_options.append(f"opacity={opacity_even}")
233
        node_options = ",".join(node_options)
234
235
        picture_even = f"""
236
\\begin{{tikzpicture}}[
237
    overlay,                         % Do our drawing on an overlay instead of inline
238
    remember picture,                % Allow us to share coordinates with other drawings
239
    shift=(current page.north west), % Set the top (north) left (west) as the origin
240
    yscale=-1,                       % Switch the y-axis to increase down the page
241
    inner sep=0,                     % Remove inner separator
242
]
243
\\node[{node_options}] at ({x_coord_even}, {y_coord_even})
244
  {{\\includegraphics[{options}]{{{path_even}}}}};
245
\\end{{tikzpicture}}
246
"""
247
248
    return f"""
249
\\renewcommand\\PandocLaTeXAbsoluteImage{{%
250
\\ifodd\\value{{page}}%
251
{picture_odd.strip()}
252
\\else
253
{picture_even.strip()}
254
\\fi
255
}}
256
"""
257
258
259
def get_latex_size(size: str | None, default: str | None = None) -> str | None:
260
    """
261
    Get the correct size.
262
263
    Parameters
264
    ----------
265
    size
266
        The initial size
267
    default
268
        The default size
269
270
    Returns
271
    -------
272
    str | None
273
        The correct size.
274
    """
275
    if size is None:
276
        return default
277
    regex = re.compile("^(\\d+(\\.\\d*)?)(?P<unit>pt|mm|cm|in|em)?$")
278
    if regex.match(size):
279
        if regex.match(size).group("unit"):
280
            return size
281
        return size + "pt"
282
    debug(
283
        f"[WARNING] pandoc-latex-absolute-image: "
284
        f"size must be a correct LaTeX length; using {default}"
285
    )
286
    return default
287
288
289
def get_anchor(anchor: str | None, default: str | None = None) -> str | None:
290
    """
291
    Get the anchor.
292
293
    Parameters
294
    ----------
295
    anchor
296
        The initial anchor
297
    default
298
        The default anchor
299
300
    Returns
301
    -------
302
    str | None
303
        The correct anchor.
304
    """
305
    if anchor in (
306
        "north",
307
        "south",
308
        "west",
309
        "east",
310
        "north west",
311
        "north east",
312
        "south west",
313
        "south east",
314
    ):
315
        return anchor
316
    return default
317
318
319
def get_opacity(opacity: str | None, default: str | None = None) -> str | None:
320
    """
321
    Get the opacity.
322
323
    Parameters
324
    ----------
325
    opacity
326
        The initial opacity
327
    default
328
        The default opacity
329
330
    Returns
331
    -------
332
    str | None
333
        The correct opacity.
334
    """
335
    if opacity:
336
        if re.match("^(1.0)|(0.\\d+)$", opacity):
337
            return opacity
338
        debug(
339
            f"[WARNING] pandoc-latex-absolute-image: "
340
            f"opacity must be a correct opacity; using {default}"
341
        )
342
    return default
343
344
345
def add_definition(doc: Doc, definition: dict[str, Any]) -> None:
346
    """
347
    Add definition to document.
348
349
    Parameters
350
    ----------
351
    doc
352
        The original document
353
    definition
354
        The definition
355
    """
356
    # Get the classes
357
    classes = definition["classes"]
358
359
    # Add a definition if correct
360
    if bool(classes):
361
        latex = latex_code(
362
            definition,
363
            {
364
                "image": "image",
365
                "image-odd": "image-odd",
366
                "image-even": "image-even",
367
                "reset": "reset",
368
                "reset-odd": "reset-odd",
369
                "reset-even": "reset-even",
370
                "width": "width",
371
                "width-odd": "width-odd",
372
                "width-even": "width-even",
373
                "height": "height",
374
                "height-odd": "height-odd",
375
                "height-even": "height-even",
376
                "anchor": "anchor",
377
                "anchor-odd": "anchor-odd",
378
                "anchor-even": "anchor-even",
379
                "x-coord": "x-coord",
380
                "x-coord-odd": "x-coord-odd",
381
                "x-coord-even": "x-coord-even",
382
                "y-coord": "y-coord",
383
                "y-coord-odd": "y-coord-odd",
384
                "y-coord-even": "y-coord-even",
385
                "opacity": "opacity",
386
                "opacity-odd": "opacity-odd",
387
                "opacity-even": "opacity-even",
388
            },
389
        )
390
        if latex:
391
            # noinspection PyUnresolvedReferences
392
            doc.defined.append({"classes": set(classes), "latex": latex})
393
394
395
def prepare(doc: Doc) -> None:
396
    """
397
    Prepare the document.
398
399
    Parameters
400
    ----------
401
    doc
402
        The original document.
403
    """
404
    # Prepare the definitions
405
    doc.defined = []
406
407
    # Get the meta data
408
    # noinspection PyUnresolvedReferences
409
    meta = doc.get_metadata("pandoc-latex-absolute-image")
410
411
    if isinstance(meta, list):
412
        # Loop on all definitions
413
        for definition in meta:
414
            # Verify the definition
415
            if (
416
                isinstance(definition, dict)
417
                and "classes" in definition
418
                and isinstance(definition["classes"], list)
419
            ):
420
                add_definition(doc, definition)
421
422
423
def finalize(doc: Doc) -> None:
424
    """
425
    Finalize the document.
426
427
    Parameters
428
    ----------
429
    doc
430
        The original document
431
    """
432
    # Add header-includes if necessary
433
    if "header-includes" not in doc.metadata:
434
        doc.metadata["header-includes"] = MetaList()
435
    # Convert header-includes to MetaList if necessary
436
    elif not isinstance(doc.metadata["header-includes"], MetaList):
437
        doc.metadata["header-includes"] = MetaList(doc.metadata["header-includes"])
438
439
    doc.metadata["header-includes"].append(
440
        MetaInlines(RawInline("\\usepackage{tikz}", "tex"))
441
    )
442
    doc.metadata["header-includes"].append(
443
        MetaInlines(RawInline("\\newcommand\\PandocLaTeXAbsoluteImage{}", "tex"))
444
    )
445
    doc.metadata["header-includes"].append(
446
        MetaInlines(
447
            RawInline(
448
                "\\AddToHook{shipout/background}{\\PandocLaTeXAbsoluteImage}", "tex"
449
            )
450
        )
451
    )
452
453
454
def main(doc: Doc | None = None) -> Doc:
455
    """
456
    Transform the pandoc document.
457
458
    Arguments
459
    ---------
460
    doc
461
        The pandoc document
462
463
    Returns
464
    -------
465
    Doc
466
        The transformed document
467
    """
468
    return run_filter(absolute_image, prepare=prepare, finalize=finalize, doc=doc)
469
470
471
if __name__ == "__main__":
472
    main()
473