Skip to main content

sextants

Convert raster images (JPEG, PNG, …) to teletext / viewdata block-mosaic graphics — the 40×25 character page rendered by the SAA5050 and its relatives (BBC Micro Mode 7, Ceefax/Oracle teletext, Prestel viewdata, CEPT videotex).

The name comes from Unicode's own term for the 2×3 sub-pixel mosaic cell these displays draw — the sextants of the "Symbols for Legacy Computing" block (U+1FB00…). Each cell is six sub-pixels sharing a two-colour sub-palette, and a colour change is an in-band control code that costs a whole cell. Encoding an image well is therefore a constrained optimisation, not a simple resample — which is what this library does, row by row, with an optimal dynamic-programming solver.

Attribution

sextants is a derivative work of image2mode7 / image2teletext by Kieran Connell. The conversion algorithm at its heart — the per-row dynamic-programming solver, the sub-pixel error model, and the preprocessing pipeline — is Kieran's work, used here under its MIT licence. This project repackages that code as an installable, src-layout Python library with a split module structure, a Click CLI, and a test suite, and is the basis for further development. Please see LICENSE for the full notice.

Gallery

Each pair shows the source image (left) and the sextants teletext rendering (right) — a 40×25 page of block mosaics from the eight-colour palette. Source images are public domain, via Wikimedia Commons.

The National Parks Preserve Wild Lifegraphic preset

The National Parks Preserve Wild Life

Source: J. Hirt for Work Projects Administration Federal Art Project, New York City, The National Parks Preserve Wild Life (public domain) — Wikimedia Commons.

Yellowstone National Parkgraphic preset

Yellowstone National Park

Source: National Park Service, Yellowstone National Park (public domain) — Wikimedia Commons.

Zion National Parkgraphic preset

Zion National Park

Source: National Park Service, Zion National Park (public domain) — Wikimedia Commons.

Lassen Volcanic National Parkgraphic preset

Lassen Volcanic National Park

Source: National Park Service, Lassen Volcanic National Park (public domain) — Wikimedia Commons.

Photographs

Continuous-tone photographs are harder — teletext has no in-between shades and no skin or earth tones — but bold, high-contrast subjects still read well:

The Blue Marble (Earth from Apollo 17)photo preset

The Blue Marble (Earth from Apollo 17)

Source: NASA/Apollo 17 crew; taken by either Harrison Schmitt or Ron Evans, The Blue Marble (Earth from Apollo 17) (public domain) — Wikimedia Commons.

Jupiterphoto preset

Jupiter

Source: NASA, ESA, and A. Simon (Goddard Space Flight Center), Jupiter (public domain) — Wikimedia Commons.

Fragments

Smaller-than-a-page rectangles from convert_fragment (see below):

an 18×22 tile (CONTAIN fit)graphic preset, 18×22 contain

an 18×22 tile (CONTAIN fit)

Source: J. Hirt for Work Projects Administration Federal Art Project, New York City, The National Parks Preserve Wild Life (public domain) — Wikimedia Commons.

Install

uv add sextants           # or: pip install sextants

Optional numba acceleration for the DP solver:

uv add "sextants[fast]"

Library usage

from pathlib import Path
from PIL import Image
from sextants import convert, convert_image, render_preview
from sextants import PreprocessOptions, SolveOptions

# Preset-aware entry point: a named bundle configures the whole pipeline.
page = convert(Image.open("photo.jpg"), preset="photo")   # 1000-byte teletext page
Path("photo.bin").write_bytes(page)                        # load at &7C00 on a BBC Micro
render_preview(page).save("preview.png")                   # what it looks like on-screen

# Or drive every knob directly with the two typed option groups.
page = convert_image(
    "logo.png",
    preprocess=PreprocessOptions(quant_colors=8, snap=40, saturation=2.0),
    solve=SolveOptions(edge_weight=3.0),
)

convert(image, preset=None, *, preprocess=None, solve=None) is the preset-aware entry point; convert_image(image, preprocess=None, solve=None) takes the options directly. Configuration lives in two frozen, validated dataclasses — PreprocessOptions (tone, colour, palette, resize) and SolveOptions (the per-row solver and its error metric). Passing preprocess= or solve= to convert replaces that whole stage's options; the preset's other stage still applies. Presets bundle sensible options for common sources:

Preset For
photo Portraits, landscapes, general photos
clean Safe universal default; light denoise + snap for unknown/BBS images
smooth Noisy JPEGs, soft gradients
vivid / graphic / flat Punchy colour, logos/cartoons, bold posterised
retro / art Authentic Ceefax look, hand-crafted teletext-art look
level1 Source is already a Mode 7 / Level 1 teletext image
dark / tv / crt Exposure lift; LCD (PAR 1.2); CRT (PAR 1.22)

Fragments (sub-page rectangles)

convert_fragment produces a rectangle smaller than a full page — for tiling, icons, or composing several images onto one page:

from sextants import convert_fragment, Fit

frag = convert_fragment("logo.png", width=12, height=8, fit=Fit.CONTAIN)
frag.width, frag.height          # actual size (CONTAIN may shrink it)

page = bytearray(frag.to_page())         # centred on a blank 40x25 page, or…
frag.blit_into(page, col=5, row=3)       # …composite at a chosen cell

A fragment is self-contained: column 0 of every row is a leading graphics-colour control code and the last column is a trailing reset, so it drops onto a page without its colour bleeding into neighbours (hence width includes those two columns — minimum 4, so at least two content cells; minimum height 1). The Fit mode controls aspect: CONTAIN preserves aspect and may return a smaller rectangle, STRETCH fills the exact size (distorting), and PAD letterboxes/pillarboxes to the exact size with a background colour.

Command line

sextants photo.jpg -o photo.bin --preset photo
sextants photo.jpg --preview preview.png --url    # also print an edit.tf URL

# A 12x8 fragment; writes the raw 96-byte tile (CONTAIN may return fewer rows):
sextants logo.png --width 12 --height 8 --fit contain -o tile.bin
# Place it onto a full page at column 5, row 3 (writes 1000 bytes):
sextants logo.png --width 12 --height 8 --place 5,3 -o page.bin
sextants --help

The default output is a raw 1000-byte page (25 rows × 40 bytes of teletext character codes). It loads directly at &7C00 on a BBC Micro, or paste the --url into edit.tf. Fragment mode writes the raw W×H tile unless --place/--preview/--url ask for a full page.

Development

uv sync            # create the environment
uv run pytest      # run the test suite

The README and its gallery are generated. After changing the showcase spec (scripts/showcase.toml) or the template (scripts/readme_template.md.j2):

uv run --group docs python scripts/fetch_sources.py     # download sources (once)
uv run --group docs python scripts/generate_readme.py   # convert + rebuild README.md

Licence

MIT — see LICENSE. Original algorithm © 2020 Kieran Connell; packaging and modifications © 2026 Robert Smallshire. Showcase source images are public domain, via Wikimedia Commons.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sextants-0.5.0.tar.gz (38.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sextants-0.5.0-py3-none-any.whl (45.3 kB view details)

Uploaded Python 3

File details

Details for the file sextants-0.5.0.tar.gz.

File metadata

  • Download URL: sextants-0.5.0.tar.gz
  • Upload date:
  • Size: 38.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for sextants-0.5.0.tar.gz
Algorithm Hash digest
SHA256 ec95f445349847a624eaec0f2eb00b4aa05cb31cb5242819849ab746167185a6
MD5 37db8954d8a3cba90bf17ef7aac78e60
BLAKE2b-256 c4a7e0b0be315a20c9e8d1a5984044aea319c564e6cca0a4d2665c63e3086d75

See more details on using hashes here.

File details

Details for the file sextants-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: sextants-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 45.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for sextants-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 86dd18c60ca7eb5e87769c292566b11e837733fccfb9cfcc4ff5248be0c2d8f0
MD5 ab648fea63a2d21afa60886e5fd291ce
BLAKE2b-256 d0a275f0d3dc7a4357edc4dc296e2da09646c2f645dd993dbafbe2bb018034e8

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page