Skip to content

geomotif.io.png

Write a PNG still, in pure standard library.

A still is the finished drawing one more time: where the GIF is a moving picture, a PNG is a picture that does not move, and this module is its release form. It renders a design once -- as a full-color, palette-free frame, antialiased or hard-edged on request -- and then encodes that frame with nothing but the standard library: the rows are filtered, zlib compresses them, and every chunk gets its CRC-32, which is the whole of the PNG file format.

The writer answers to the same styling vocabulary as the GIF writer (ink, background, thickness, padding, antialias), so a design that looks right as a GIF looks the same way as a PNG. Unlike the GIF it keeps every color it was drawn in, because a PNG has no 256-color budget::

from geomotif.io.png import save_png
from geomotif.motifs import Rose

save_png(Rose(n=7).build(), "rose.png")

It can write three ways, chosen by color:

  • "rgb" (the default) -- truecolor, three bytes a pixel. Lossless and full color; the picture to reach for by default.
  • "rgba" -- truecolor with an alpha channel, four bytes a pixel. With transparent it leaves the background empty; without, the alpha is opaque.
  • "indexed" -- a palette of at most 256 colors, one byte a pixel. The smallest files, at the cost of a palette; the same median-cut quantizer the GIF uses shrinks the frame down, with optional dithering.

Functions:

Name Description
to_png

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

save_png

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

to_png

to_png(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, color: str = 'rgb', compression: int = 6, transparent: bool = False) -> bytes

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

A still is drawn once, exactly the way :func:to_gif draws a frame, and then written as a PNG instead of being quantized toward a GIF's colors. The styling parameters are shared with the GIF writer so one summoning controls both.

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

When antialiasing into an indexed frame, how many shades an edge may blend into per color pair. Ignored for the truecolor paths, which keep every level.

8
color str

"rgb", "rgba" or "indexed" -- how the PNG stores the picture. See the module docstring for what each means.

'rgb'
compression int

zlib level from 0 (fast, big) to 9 (slow, small). Must be an integer in that range.

6
transparent bool

Leave the background empty instead of painting background. A pixel with no ink is written with alpha 0 and an antialiased edge with the ink and its coverage as the alpha -- the straight alpha a PNG stores. This implies color="rgba" (a JPEG has no alpha, so this is where transparency lives in this library). Off by default.

False

Returns:

Type Description
bytes

A complete PNG file.

Raises:

Type Description
ValueError

If color is not one of the three, compression is out of range, or an indexed frame would need more than 256 palette colors.

Source code in src/geomotif/io/png.py
def to_png(
    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,
    color: str = "rgb",
    compression: int = 6,
    transparent: bool = False,
) -> bytes:
    """Render a design -- or re-encode a :class:`~geomotif.io.Raster` -- as PNG.

    A still is drawn once, exactly the way :func:`to_gif` draws a frame, and
    then written as a PNG instead of being quantized toward a GIF's colors.
    The styling parameters are shared with the GIF writer so one summoning
    controls both.

    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
        When antialiasing into an indexed frame, how many shades an edge
        may blend into per color pair. Ignored for the truecolor paths,
        which keep every level.
    color : str
        ``"rgb"``, ``"rgba"`` or ``"indexed"`` -- how the PNG stores the
        picture. See the module docstring for what each means.
    compression : int
        zlib level from 0 (fast, big) to 9 (slow, small). Must be an
        integer in that range.
    transparent : bool
        Leave the background empty instead of painting ``background``. A
        pixel with no ink is written with alpha 0 and an antialiased edge
        with the ink and its coverage as the alpha -- the straight alpha a
        PNG stores. This implies ``color="rgba"`` (a JPEG has no alpha, so
        this is where transparency lives in this library). Off by default.

    Returns
    -------
    bytes
        A complete PNG file.

    Raises
    ------
    ValueError
        If ``color`` is not one of the three, ``compression`` is out of
        range, or an indexed frame would need more than 256 palette colors.
    """
    if color not in _COLOR_TYPE:
        raise ValueError(f"color must be one of {sorted(_COLOR_TYPE)}, got {color!r}")
    if not _MIN_COMPRESSION <= compression <= _MAX_COMPRESSION:
        raise ValueError(
            f"compression must be between {_MIN_COMPRESSION} and {_MAX_COMPRESSION}, "
            f"got {compression}"
        )
    if transparent:
        color = "rgba"

    raster = _frame(
        source,
        color=color,
        width=width,
        height=height,
        padding=padding,
        ink=ink,
        background=background,
        thickness=thickness,
        dot_radius=dot_radius,
        antialias=antialias,
        aa_level=aa_level,
        transparent=transparent,
    )
    return _encode(raster, _COLOR_TYPE[color], compression)

save_png

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

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

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

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

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