aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2025-11-20 22:38:59 +0100
committerDaniel Schadt <kingdread@gmx.de>2025-11-20 22:38:59 +0100
commitb456196f9a9d200bb2003c847836621fd39b43a5 (patch)
treef2d2cb748930aa20bd57d47be30570a5d4fcc61e
parent7171fef21b065715f3ce48cad57ee522cdd53a1e (diff)
downloadfietsboek-b456196f9a9d200bb2003c847836621fd39b43a5.tar.gz
fietsboek-b456196f9a9d200bb2003c847836621fd39b43a5.tar.bz2
fietsboek-b456196f9a9d200bb2003c847836621fd39b43a5.zip
add docstrings & fix lint
-rw-r--r--fietsboek/pdf.py69
-rw-r--r--fietsboek/trackmap.py4
-rw-r--r--fietsboek/views/detail.py5
3 files changed, 71 insertions, 7 deletions
diff --git a/fietsboek/pdf.py b/fietsboek/pdf.py
index a6b40c9..97e1d7b 100644
--- a/fietsboek/pdf.py
+++ b/fietsboek/pdf.py
@@ -1,9 +1,23 @@
+"""PDF generation for tracks.
+
+This module implements functionality that renders a PDF overview for a track.
+The PDF overview consists of a map using OSM tiles, and a table with the
+computed metadata.
+
+PDF generation is done using Typst_ via the `Python bindings`_. Typst provides
+layouting and good typography without too much effort from our side. The Typst
+file is generated from a Jinja2 template, saved to a temporary directory
+together with the track image, and then compiled.
+
+.. _Typst: https://typst.app/
+.. _Python bindings: https://pypi.org/project/typst/
+"""
+
import html.parser
import importlib.resources
import io
import logging
import tempfile
-from dataclasses import dataclass
from itertools import chain
from pathlib import Path
@@ -45,6 +59,13 @@ TO_ESCAPE = {
class HtmlToTypst(html.parser.HTMLParser):
+ """A parser that converts HTML to Typst syntax.
+
+ This is adjusted for the HTML output that the markdown converted produces.
+ It will not work for arbitrary HTML.
+ """
+
+ # pylint: disable=too-many-branches
def __init__(self, out):
super().__init__()
self.out = out
@@ -102,22 +123,46 @@ class HtmlToTypst(html.parser.HTMLParser):
def typst_string(value: str) -> str:
+ """Serializes a string to a string that can be embedded in Typst source.
+
+ This wraps the value in quotes, and escapes all characters.
+
+ :param value: The value to serialize.
+ :return: The serialized string, ready to be embedded.
+ """
return '"' + "".join(f"\\u{{{ord(char):x}}}" for char in value) + '"'
def typst_escape(value: str) -> str:
+ """Escapes Typst markup in the given value.
+
+ :param value: The value to escape.
+ :return: The value with formatting characters escaped.
+ """
return "".join("\\" + char if char in TO_ESCAPE else char for char in value)
def md_to_typst(value: str) -> str:
- html = util.safe_markdown(value).unescape()
+ """Convert Markdown-formatted text to Typst source code.
+
+ :param value: The Markdown source to convert.
+ :return: The Typst code.
+ """
+ html_source = util.safe_markdown(value).unescape()
buffer = io.StringIO()
parser = HtmlToTypst(buffer)
- parser.feed(html)
+ parser.feed(html_source)
return buffer.getvalue()
def draw_map(track: Track, requester: TileRequester, tile_layer: TileLayerConfig, outfile: Path):
+ """Renders the track map.
+
+ :param track: The track.
+ :param requester: The requester which is used to retrieve tiles.
+ :param tile_layer: The OSM tile layer configuration.
+ :param outfile: Path to the output file.
+ """
map_image = trackmap.render(
track.path(),
tile_layer,
@@ -130,7 +175,19 @@ def draw_map(track: Track, requester: TileRequester, tile_layer: TileLayerConfig
def generate(
track: Track, requester: TileRequester, tile_layer: TileLayerConfig, localizer: Localizer
) -> bytes:
- """Generate a PDF representation for the given track."""
+ """Generate a PDF representation for the given track.
+
+ :param track: The track for which to generate a PDF overview.
+ :param requester: The tile requester to render the track map.
+ :param tile_layer: The tile layer to use for the track map.
+ :param localizer: The localizer.
+ :return: The PDF bytes.
+ """
+ # Yes, we could use f-strings, but I don't like embedding the huge
+ # expressions below in f-strings:
+ # pylint: disable=consider-using-f-string
+ # And this is a false positive for typst.compile:
+ # pylint: disable=no-member
env = jinja2.Environment(
loader=jinja2.PackageLoader("fietsboek", "pdf-assets"),
autoescape=False,
@@ -173,9 +230,9 @@ def generate(
"description": track.description,
}
- with tempfile.TemporaryDirectory(prefix=TEMP_PREFIX) as temp_dir:
+ with tempfile.TemporaryDirectory(prefix=TEMP_PREFIX) as temp_dir_name:
+ temp_dir = Path(temp_dir_name)
LOGGER.debug("New PDF generation in %s", temp_dir)
- temp_dir = Path(temp_dir)
font_data = importlib.resources.read_binary("fietsboek", "pdf-assets/Nunito.ttf")
(temp_dir / "Nunito.ttf").write_bytes(font_data)
diff --git a/fietsboek/trackmap.py b/fietsboek/trackmap.py
index 4280e0c..c767852 100644
--- a/fietsboek/trackmap.py
+++ b/fietsboek/trackmap.py
@@ -119,7 +119,9 @@ class TrackMapRenderer:
draw.line(coords, fill=self.color, width=self.line_width, joint="curve")
-def render(track: geo.Path, layer: TileLayerConfig, requester: TileRequester, size: (int, int) = (300, 300)) -> Image.Image:
+def render(
+ track: geo.Path, layer: TileLayerConfig, requester: TileRequester, size: (int, int) = (300, 300)
+) -> Image.Image:
"""Shorthand to construct a :class:`TrackMapRenderer` and render the preview.
:param track: Track to render.
diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py
index b1d3c74..16c896a 100644
--- a/fietsboek/views/detail.py
+++ b/fietsboek/views/detail.py
@@ -288,6 +288,11 @@ def track_map(request: Request):
@view_config(route_name="track-pdf", permission="track.view")
def track_pdf(request: Request):
+ """Endpoint to provide the track's PDF overview.
+
+ :param request: The pyramid request.
+ :return: The HTTP response.
+ """
loader: ITileRequester = request.registry.getUtility(ITileRequester)
layer = request.config.public_tile_layers()[0]
pdf_bytes = pdf.generate(request.context, loader, layer, request.localizer)