Skip to content

geomotif.io.jpeg

Write a JPEG still, in pure standard library.

A JPEG is the still picture that trades a little of a PNG's exactness for a smaller file, and this module is the lossy half of the pair: where the GIF squeezes into 256 colors and the PNG keeps every one, a JPEG throws away what an eye would not miss -- the small high-frequency detail in an 8x8 block -- and codes what is left with Huffman's method. It renders a design once, as the same full-color frame the PNG and GIF writers draw, and then encodes it with nothing but the standard library and a little arithmetic.

Baseline JPEG is, at heart, a small pipeline that this module walks exactly once: the RGB sample is changed into the luminance-plus-color space the human eye is better at, the two color channels are halved (4:2:0, because the eye cares less about their detail), every 8x8 block is transformed with the DCT, the transformed coefficients are divided by a quality-scaled table (that is the lossy step), and what survives is zig-zag-scanned and Huffman-coded against the standard tables. quality chooses how much survives::

from geomotif.io.jpeg import save_jpeg
from geomotif.motifs import Rose

save_jpeg(Rose(n=7).build(), "rose.jpg", quality=92)

The writer answers to the same styling vocabulary as the PNG and GIF writers (ink, background, thickness, padding, antialias), so a design that looks right in one looks the same way here.

Functions:

Name Description
to_jpeg

Render a design -- or re-encode a :class:~geomotif.io.Raster -- as JPEG.

save_jpeg

Render a design -- or re-encode a raster -- as JPEG and write it.

to_jpeg

to_jpeg(source: Design | Raster, *, width: int = 480, height: int = 480, padding: float = 8.0, ink: str = '#0b0b0b', background: str = '#ffffff', thickness: int = 1, dot_radius: int | None = None, antialias: bool = False, aa_level: int = 8, quality: int = 85) -> bytes

Render a design -- or re-encode a :class:~geomotif.io.Raster -- as JPEG.

A still is drawn once, exactly the way :func:to_png draws a frame, and then encoded with baseline JPEG: 4:2:0 chroma subsampling, an 8x8 DCT on each block, quality-scaled quantization, and Huffman coding against the reference tables. The styling parameters are shared with the PNG and GIF writers so one summoning controls all three.

Parameters:

Name Type Description Default
source Design or Raster

What to write. A design is drawn; a raster is encoded as it is.

required
width int

Canvas size in pixels. Ignored when source is already a raster.

480
height int

Canvas size in pixels. Ignored when source is already a raster.

480
padding float

Margin reserved on all four sides, in pixels.

8.0
ink str

Default stroke color and the color behind everything.

'#0b0b0b'
background str

Default stroke color and the color behind everything.

'#0b0b0b'
thickness int

Stroke width in pixels.

1
dot_radius int

Radius for loose points. Defaults to thickness.

None
antialias bool

Supersample and blend edges. Off by default, so the edges are the same hard Bresenham edges they have always been.

False
aa_level int

How many shades an antialiased edge may blend into. A JPEG keeps full color anyway, so this only bounds the drawing, not the encode.

8
quality int

From 0 (smallest, most loss) to 100 (closest to the original). This is what the quantization tables are scaled by.

85

Returns:

Type Description
bytes

A complete baseline JPEG file.

Raises:

Type Description
ValueError

If quality is outside 0-100.

Source code in src/geomotif/io/jpeg.py
def to_jpeg(
    source: Design | Raster,
    *,
    width: int = 480,
    height: int = 480,
    padding: float = 8.0,
    ink: str = "#0b0b0b",
    background: str = "#ffffff",
    thickness: int = 1,
    dot_radius: int | None = None,
    antialias: bool = False,
    aa_level: int = 8,
    quality: int = 85,
) -> bytes:
    """Render a design -- or re-encode a :class:`~geomotif.io.Raster` -- as JPEG.

    A still is drawn once, exactly the way :func:`to_png` draws a frame, and
    then encoded with baseline JPEG: 4:2:0 chroma subsampling, an 8x8 DCT on
    each block, quality-scaled quantization, and Huffman coding against the
    reference tables. The styling parameters are shared with the PNG and GIF
    writers so one summoning controls all three.

    Parameters
    ----------
    source : Design or Raster
        What to write. A design is drawn; a raster is encoded as it is.
    width, height : int
        Canvas size in pixels. Ignored when ``source`` is already a raster.
    padding : float
        Margin reserved on all four sides, in pixels.
    ink, background : str
        Default stroke color and the color behind everything.
    thickness : int
        Stroke width in pixels.
    dot_radius : int, optional
        Radius for loose points. Defaults to ``thickness``.
    antialias : bool
        Supersample and blend edges. Off by default, so the edges are the
        same hard Bresenham edges they have always been.
    aa_level : int
        How many shades an antialiased edge may blend into. A JPEG keeps full
        color anyway, so this only bounds the drawing, not the encode.
    quality : int
        From 0 (smallest, most loss) to 100 (closest to the original). This is
        what the quantization tables are scaled by.

    Returns
    -------
    bytes
        A complete baseline JPEG file.

    Raises
    ------
    ValueError
        If ``quality`` is outside 0-100.
    """
    if not _MIN_QUALITY <= quality <= _MAX_QUALITY:
        raise ValueError(
            f"quality must be between {_MIN_QUALITY} and {_MAX_QUALITY}, got {quality}"
        )

    rgb_frame = _rgb_frame(
        source,
        width=width,
        height=height,
        padding=padding,
        ink=ink,
        background=background,
        thickness=thickness,
        dot_radius=dot_radius,
        antialias=antialias,
        aa_level=aa_level,
    )
    return _encode(rgb_frame, quality)

save_jpeg

save_jpeg(source: Design | Raster, path: str | PathLike[str], **kwargs: Any) -> Path

Render a design -- or re-encode a raster -- as JPEG and write it.

Keyword arguments are passed straight through to :func:to_jpeg.

Source code in src/geomotif/io/jpeg.py
def save_jpeg(source: Design | Raster, path: str | PathLike[str], **kwargs: Any) -> pathlib.Path:
    """Render a design -- or re-encode a raster -- as JPEG and write it.

    Keyword arguments are passed straight through to :func:`to_jpeg`.
    """
    target = pathlib.Path(path)
    target.write_bytes(to_jpeg(source, **kwargs))
    return target