diff --git a/AGENTS.md b/AGENTS.md index 93154cb..ac4fd50 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,7 @@ pydevices-examples's `tools/example_interpreters.toml`. ## Publishing Org-wide procedure: [.github/docs/publishing-automation.md](https://github.com/PyDevices/.github/blob/main/docs/publishing-automation.md). -Local wheel builds: [docs/building-wheels.md](docs/building-wheels.md). +Local wheel builds: [CONTRIBUTING.md](CONTRIBUTING.md#build-the-wheels). ## After C changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..562ebc7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,150 @@ +# Contributing to pygraphics + +pygraphics is two implementations of one API: C under `src/` and pure Python +under `lib/pygraphics/`. A change to one needs the same change in the other, +and the tests run against both. A focused test, or a docs example that shows +an existing call, is a good first contribution. + +## Where things are + +| Path | What's there | +|---|---| +| `src/` | the C module, and its bindings for MicroPython, CircuitPython and CPython | +| `lib/pygraphics/` | the pure-Python package; the API reference is generated from its docstrings | +| `micropython.mk`, `micropython.cmake`, `manifest.py` | build glue for MicroPython and CircuitPython firmware | +| `setup.py`, `pyproject.toml` | the CPython wheel | +| `tests/` | one suite for both implementations; [tests/README.md](tests/README.md) explains the switch | +| `tools/` | comparison tools and `benchmark.py` | +| `scripts/sync_fonts.py` | writes the C font headers from the Python font files | +| `docs/` | the documentation site | + +## Run the tests + +Pure Python, which needs nothing built: + +```bash +python3 -m unittest discover -s tests +``` + +The compiled module, built in place: + +```bash +python3 -m venv .venv +.venv/bin/pip install -e . +PYGRAPHICS_TEST_NATIVE=1 .venv/bin/python -m unittest discover -s tests +``` + +These are the two runs CI makes on every change. `PYTHONPATH=lib` alone does +not select the pure package once you've built in place, because the +repository root comes before `PYTHONPATH` on `sys.path` and the compiled +module there wins. `tests/_env.py` handles that for the suite; from a shell, +run from another directory. + +## Compare the two implementations + +```bash +micropython tools/compare_graphics_run.py # compiled vs pure, byte for byte +python tools/compare_graphics_matrix.py # the same on every desktop interpreter it finds +micropython tools/compare_framebuf_mp.py # MicroPython's framebuf vs lib/pygraphics/framebuf.py +``` + +The `micropython` runs need a build with the C module linked in, as below; +on a stock build the comparison is the pure package against itself. CI runs +the first and third on every change. + +After you change a font in `lib/pygraphics/_font_8x*.py`, regenerate the C +headers: + +```bash +python3 scripts/sync_fonts.py +``` + +## Build it into MicroPython + +With MicroPython 1.29 or later, add one line to the manifest your build +uses: + +```python +include("/path/to/pygraphics/manifest.py") +``` + +On unix that's `ports/unix/variants/standard/manifest.py`; on esp32 and rp2 +it's usually `ports//boards/manifest.py`, unless your board brings its +own. Then build as usual: + +```bash +cd micropython/ports/unix +make submodules && make +./build-standard/micropython -c "import pygraphics; print(pygraphics.implementation())" +``` + +That prints `native_cmod`. The manifest adds the C module with `c_module()` +and freezes nothing. + +To leave the MicroPython tree alone, write your own manifest and pass it as +`FROZEN_MANIFEST=`. That replaces the port's default manifest, so include the +default too (`include("$(PORT_DIR)/variants/standard/manifest.py")` on unix, +`include("$(PORT_DIR)/boards/manifest.py")` on esp32), or you lose `asyncio` +and the port's other frozen modules. + +Before 1.29, manifests have no `c_module()`, so use `USER_C_MODULES`. On +esp32 and rp2, point it at this repository: +`make BOARD= USER_C_MODULES=/path/to/pygraphics/micropython.cmake`. +On Make ports such as unix, point it at a directory that *contains* this +repository; MicroPython builds every module it finds there, so keep that +directory to the modules you want. CI builds the unix port of MicroPython +1.28.0 this way. + +MicroPython's [manifest reference](https://docs.micropython.org/en/v1.29.0/reference/manifest.html) +has more on `c_module()` and `include()`. +[micropython-pydevices](https://github.com/PyDevices/micropython-pydevices) +keeps ready-made manifests and boards for building several PyDevices modules +at once. + +## Build it into CircuitPython + +CircuitPython builds user C modules through `USER_C_MODULES` too, with +`micropython.mk` as the glue. From a CircuitPython 11 checkout: + +```bash +make -C ports/unix -j VARIANT=coverage USER_C_MODULES=/path/to/pygraphics +make -C ports/raspberrypi -j BOARD=adafruit_feather_rp2040 USER_C_MODULES=/path/to/pygraphics +``` + +This has been built on CircuitPython 11.0.0-alpha.1 for unix, raspberrypi +(Feather RP2040) and atmel-samd (Feather M4 Express, with `CIRCUITPY_ULAB=0` +to fit in 512 KB). The atmel-samd and raspberrypi ports turn some warnings +into errors; `micropython.mk` turns them off again for this module's files +only. To build with other user C modules, list them all in the one +`USER_C_MODULES`. + +## Build the wheels + +cibuildwheel builds them from the settings in `pyproject.toml`: CPython on +Linux x86_64, Windows and Android, and one wheel per supported Pyodide +version. For the browser wheels: + +```bash +pipx run cibuildwheel --platform pyodide +``` + +Wheels land in `wheelhouse/`. + +## Build the docs + +```bash +python3 -m venv .venv-docs +.venv-docs/bin/pip install -r docs/requirements.txt +.venv-docs/bin/sphinx-build -W --keep-going -b html docs docs/_build/html +``` + +The API reference under `docs/reference/` is generated during the build and +isn't tracked. Sphinx reads Python docstrings, so it can't see the C module; +`tools/sourcey-api/` generates a reference for the C bindings on demand, and +its README says how. + +## Releases + +Each release tag `vX.Y.Z` publishes the compiled wheels to TestPyPI as +`pydevices-pygraphics` and the pure package to the mip index as `pygraphics`, +both at that version. diff --git a/README.md b/README.md index 9a741a5..de7cd12 100644 --- a/README.md +++ b/README.md @@ -1,268 +1,40 @@ -# pygraphics - -Native and pure-Python **pygraphics** for MicroPython, CircuitPython, and CPython. -The C module can be built into MicroPython or CircuitPython, while the pure-Python -package is available for users who prefer not to compile their own build. -Import as `pygraphics`. - -New here? Read the [newcomer's guide](docs/newcomers.md) for the portable -framebuffer model, implementation boundary, and contributor map. +![A red rounded rectangle, a green circle and the word Hello on black](docs/images/hello.png) -| Product | Pip / MIP | Role | -|---------|-----------|------| -| **pygraphics** | TestPyPI `pydevices-pygraphics` | Native/C-extension wheel for CPython and for embedded builds that include the module (prefer on desktop/Android/Pyodide when available) | -| **pygraphics** | MIP `pygraphics` | Pure-Python package for users who do not want to compile their own build (same public API, [bar four authoring entry points](#what-the-native-build-does-not-have)) | - -One release tag `vX.Y.Z` publishes both products at that version. +# pygraphics -## Install +pygraphics is the part of OmniPython that draws: shapes, text and images into +a framebuffer, with the same code on MicroPython-compatible boards, +CircuitPython-compatible boards and CPython. ```bash -# Native C extension (CPython desktop, Android, Pyodide) -pip install -i https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ pydevices-pygraphics +pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-pygraphics ``` ```python -# Pure Python, for MicroPython and CircuitPython import mip mip.install("pygraphics", index="https://PyDevices.github.io/mip") ``` -Full options and verification: [docs/installation.md](docs/installation.md). - -### Quick start - ```python import pygraphics from pygraphics import FrameBuffer, RGB565 -fb = FrameBuffer(bytearray(160 * 128 * 2), 160, 128, RGB565) -fb.fill(0) -fb.fill_rect(10, 10, 40, 40, 0xF800) -print(pygraphics.implementation()) # native_cmod or pygraphics_python +fb = FrameBuffer(bytearray(160 * 80 * 2), 160, 80, RGB565) +fb.fill(0x0000) +card = fb.round_rect(8, 8, 64, 40, 8, 0xF800, f=True) +dot = fb.circle(116, 28, 20, 0x07E0, f=True) +label = pygraphics.text16(fb, "Hello", 8, 56, 0xFFFF) +print("changed:", card + dot + label) +fb.save("hello.bmp") ``` -`pygraphics` has **zero external dependencies** on any other PyDevices libraries or hardware modules. It functions as an independent, portable 2D graphics engine that can be used in any MicroPython, CircuitPython, or CPython project needing fast drawing primitives or off-screen framebuffer manipulation, regardless of whether you are using PyDevices displays. - -`pygraphics` extends MicroPython's standard `framebuf` into a powerful 2D graphics engine while preserving full compatibility with `framebuf`'s own API (proved by `tools/compare_framebuf_mp.py`): - -* **Zero Dependencies & Universal Use**: No required external packages; usable in any Python application. -* **Dual Invocation & `Draw` Class**: Call methods directly on `FrameBuffer` instances (`fb.circle(...)`), invoke standalone canvas functions (`pygraphics.circle(fb, ...)`), or use the **`Draw`** styling context for maximum architectural flexibility. -* **Exposed Attributes**: Direct read-only access to `.buffer`, `.width`, `.height`, `.format`, and `.color_depth` on every `FrameBuffer` instance (stride is an internal implementation detail, not a public attribute). -* **24-bit True Color (`RGB888`)**: Supports 24-bit packed RGB color format (format constant `RGB888`), ideal for 24-bit displays as well as **NeoPixel (WS2812B)** and **DotStar (APA102)** LED matrix arrays. -* **Dirty `Area` Returns**: Drawing operations return an `Area(x, y, w, h)` bounding box so drivers can flush only modified screen regions. -* **Rich Primitive Library**: Standard shapes plus `round_rect`, `circle`, `arc`, `triangle`, `polygon`, and `gradient_rect`. -* **Multi-Font Engine**: Built-in 8x8 (`text8`), 8x14 (`text14`), 8x16 (`text16`), and custom `Font` support. -* **Image & File I/O**: Load and save images directly using `load_image`, `save_image`, `BMP565`, and PBM/PGM codecs. `export_framebuffer` and the `FrameBuffer.export` / `from_bitmap` / `from_module` trio are [pure-Python only](#what-the-native-build-does-not-have). -* **Colorkey Blitting**: `blit_transparent()` for transparent sprite overlays. -* **Native C Speed & Fallback Safety**: C acceleration compiled for MicroPython, CircuitPython, and CPython wheels (TestPyPI `pydevices-pygraphics`), with a pure-Python fallback available whenever precompiled binaries are not present in the firmware or environment. - - - -## Support and platforms - -pygraphics targets MicroPython, CircuitPython, and CPython, both as a native -C extension and as the pure-Python fallback under `lib/pygraphics/`. - -The native `pydevices-pygraphics` wheel is currently built for, with each -claim's tier in the vocabulary of the org's -[platform support tiers](https://github.com/PyDevices/.github/blob/main/docs/platform-support-tiers.md): - -| Platform | Arch | Tier | Notes | -|----------|------|------|-------| -| manylinux | x86_64 | CI-proven | `manylinux_2_28`; the wheel is built and the suite runs on it every release | -| Windows | AMD64 (win_amd64) | CI-proven | same | -| Android | arm64_v8a, x86_64 | community-verified | the wheel builds; nobody here has run it on a device | -| Pyodide / Emscripten | wasm32 | community-verified | the wheel builds; `pyemscripten_2025_0` for cp313 and `pyemscripten_2026_0` for cp314 — micropip picks the one matching your runtime | - -**No macOS or Linux aarch64 wheels.** The cause is the bench: there is no Mac -and no aarch64 Linux runner in the release matrix, so there is nothing to -build or prove them on (the org records the same posture in -[platform-support-tiers.md](https://github.com/PyDevices/.github/blob/main/docs/platform-support-tiers.md)). -On those platforms use the pure-Python package via MIP, or copy -`lib/pygraphics/` onto `sys.path`. - -### What the native build does not have - -Four entry points are **pure-Python only**, and the native module does not -carry them: - -| Name | | -|---|---| -| `pygraphics.export_framebuffer(fb, path)` | writes an importable `.py` bitmap module | -| `FrameBuffer.export(path)` | the method form of the same thing | -| `FrameBuffer.from_bitmap(buf, w, h, fmt)` | builds a framebuffer from such a module's `BITMAP` | -| `FrameBuffer.from_module(mod)` | the same, from the module object | - -They emit and consume Python source, which is authoring-time work; putting a -source generator in a C accelerator buys nothing. Callers that need them on a -native build fall back to the pure package — pdwidgets does exactly that in -its `_icon_load.py`. `tests/test_parity.py` enforces the exclusion in both -directions, so implementing one in C fails the suite until this table changes. - -Everything else in `__all__` is present on both builds, checked against the -pure package's own `__all__` rather than a copy of it. - -### Which index - -Current releases go to **TestPyPI** (`pydevices-pygraphics`); install from -there with the command under [Install](#install) above. Production PyPI holds -one older release (**0.0.37**, against TestPyPI's 0.1.1), parked there to -reserve the name — so a plain `pip install pydevices-pygraphics` succeeds and -gives you the older wheel. Note that the documented install line passes -TestPyPI with `-i` and PyPI with `--extra-index-url`, and pip resolves to -whichever index has the higher version. - -What's planned next is in [ROADMAP.md](ROADMAP.md). - -## Links - -- [Documentation](https://pygraphics.readthedocs.io) -- [API reference](https://pygraphics.readthedocs.io/en/latest/reference/pygraphics/index.html) -- [Source](https://github.com/PyDevices/pygraphics) -- [Issues](https://github.com/PyDevices/pygraphics/issues) -- Related: [pydevices-examples](https://github.com/PyDevices/pydevices-examples) - -## License - -MIT (framebuf algorithms derived from MicroPython `extmod/modframebuf.c`, Damien P. George). - - ---- - -## Build from source - -### Layout - +```text +changed: Area(8, 8, 128, 64) ``` -pygraphics/ - micropython.mk / micropython.cmake / setup.py - src/ # C sources + headers (gfx_*.h, font_8x*.h, qstrs) - lib/pygraphics/ # pure-Python package (import pygraphics) - tests/ # native smoke / parity tests - tools/ # developer benchmarks / helpers - docs/ scripts/ -``` - -### CPython native (editable) - -```bash -python3 -m venv .venv -.venv/bin/pip install -e . -PYGRAPHICS_TEST_NATIVE=1 .venv/bin/python -m unittest discover -s tests -``` - -And the pure-Python side of the same suite: - -```bash -.venv/bin/python -m unittest discover -s tests -``` - -Those are the two commands CI runs — 15 modules, not the three this section -used to name. [`tests/README.md`](tests/README.md) explains the switch and -what each module covers. - -### Pure Python (no extension) - -```bash -(cd /tmp && PYTHONPATH="$OLDPWD/lib" python3 -c "import pygraphics; print(pygraphics.implementation())") -``` - -`PYTHONPATH=lib` alone is not enough after an in-place editable build (`pip -install -e .` / `build_ext --inplace`, above): the current directory precedes -`PYTHONPATH` on `sys.path`, so the native extension sitting at the repo root -wins over `lib/`. CI (`.github/workflows/tests.yml`) sidesteps this the same -way `tests/_env.py` does — by inserting `lib/` at the front of `sys.path` -explicitly in code rather than relying on `PYTHONPATH` — but the simplest fix -from a shell is just to run from a directory that isn't the repo root, as -above. - -### Parity testing (native vs pure-Python) - -```bash -micropython tools/compare_graphics_run.py # single interpreter -python tools/compare_graphics_matrix.py # all desktop interpreters -micropython tools/compare_framebuf_mp.py # C framebuf vs lib/pygraphics/framebuf.py -``` - -All three run on a unix-port MicroPython with `pygraphics` linked in as a user -C module, and on CPython with the extension built in place; the first reports -385 checks. The `micropython` runs need a build that actually contains the -module — a stock interpreter imports the pure package and the comparison is -against itself. CI runs the first and third on every push (the `micropython` -job in [`tests.yml`](.github/workflows/tests.yml)). - -### MicroPython (any port) - -You need MicroPython 1.29 or later, built the way you already build it. Clone -this repository anywhere and add one line to the manifest your build uses: - -```python -include("/path/to/pygraphics/manifest.py") -``` - -On unix that manifest is `ports/unix/variants/standard/manifest.py`; on esp32 -and rp2 it is usually `ports//boards/manifest.py`, unless your board -brings its own. Then build as usual: - -```bash -cd micropython/ports/unix -make submodules && make -./build-standard/micropython -c "import pygraphics; print(pygraphics.implementation())" -``` - -That prints `native_cmod`. The manifest names the C module with `c_module(".")` -and freezes nothing, so the line adds the `pygraphics` C module and nothing -else, on every port. Tested against MicroPython v1.29.0 on unix. - -If you would rather not edit the MicroPython tree, write a manifest of your own -and pass it as `FROZEN_MANIFEST=`. That replaces the port's default manifest, -so include the default too (`include("$(PORT_DIR)/variants/standard/manifest.py")` -on unix, `include("$(PORT_DIR)/boards/manifest.py")` on esp32) or you lose -`asyncio` and the port's other frozen modules. - -**Older than 1.29?** Manifests there have no `c_module()`, so do not include -this one; use `USER_C_MODULES` instead. On esp32 and rp2 point it at this -repository (`make BOARD= USER_C_MODULES=/path/to/pygraphics/micropython.cmake`). -On Make ports such as unix point it at the directory that *contains* this -repository; MicroPython builds every module it finds there, so keep that -directory to the modules you want. - -More about `c_module()` and `include()` is in MicroPython's -[manifest reference](https://docs.micropython.org/en/v1.29.0/reference/manifest.html). -For several PyDevices modules at once, -[micropython-pydevices](https://github.com/PyDevices/micropython-pydevices) -keeps ready-made manifests, variants and boards. - -### CircuitPython - -CircuitPython builds user C modules the way MicroPython does, through -`USER_C_MODULES`, so `micropython.mk` is the build glue there too. Hand this -directory to a port's `make`: - -```bash -# unix, from a CircuitPython 11 checkout -make -C ports/unix -j VARIANT=coverage USER_C_MODULES=/path/to/pygraphics -# a board -make -C ports/raspberrypi -j BOARD=adafruit_feather_rp2040 USER_C_MODULES=/path/to/pygraphics -``` - -[micropython-pydevices](https://github.com/PyDevices/micropython-pydevices)' -`build_mp.py --interpreter circuitpython --modules pygraphics` does the same -with the CircuitPython checkout, submodules and board list handled for you. - -**Tested against** CircuitPython 11.0.0-alpha.1, on unix, raspberrypi -(Feather RP2040) and atmel-samd (Feather M4 Express, built with -`CIRCUITPY_ULAB=0` to fit 512 KB). The atmel-samd and raspberrypi ports turn -some warnings into errors after a module's own flags; `micropython.mk` turns -them off again per object, for this module's files only. -To build this repo with other user C modules, list them all in the one -`USER_C_MODULES`. +`hello.bmp` is the picture at the top of this page, shown at three times its +size. -### pydevices-examples integration +[Docs](https://pygraphics.readthedocs.io) · [Examples](https://pydevices.github.io/pydevices-examples/pyscript/) · [Roadmap](ROADMAP.md) -When this cmod is installed or linked, `pygraphics.framebuf_backend()` reports -`native` and `pygraphics.implementation()` reports `native_cmod`. Otherwise the -pure-Python package reports `pygraphics_python`. +OmniPython is made by [PyDevices](https://pydevices.com). diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 946588c..0000000 --- a/docs/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Documentation - -Hand-authored Markdown pages (built by Sphinx; `conf.py` is here) live in this directory and publish to -[pygraphics.readthedocs.io](https://pygraphics.readthedocs.io). - -```bash -python3 -m venv .venv-docs -.venv-docs/bin/pip install -r docs/requirements.txt -.venv-docs/bin/sphinx-build -W --keep-going -b html docs docs/_build/html -``` - -A source-linked reference for the **native C bindings** — which autodoc -cannot reach, since it reads Python docstrings — can be generated on demand with -Sourcey under [`tools/sourcey-api/`](../tools/sourcey-api/): - -```bash -python3 tools/sourcey-api/scripts/generate_reference.py -``` - -Its output under `tools/sourcey-api/generated/` is **not tracked** and is not -published anywhere today. See `tools/sourcey-api/README.md`. diff --git a/docs/api-overview.md b/docs/api-overview.md deleted file mode 100644 index 7427ad6..0000000 --- a/docs/api-overview.md +++ /dev/null @@ -1,23 +0,0 @@ -# API overview - -`pygraphics` extends `framebuf` with extra drawing helpers (rounded rectangles, gradients, polygons) and returns **Area** bounding boxes for partial updates. - -See the [installation guide](installation.md) for MIP and TestPyPI options. - -## Narrative docs - -- [Graphics guide](graphics-guide.md) — quick start, FrameBuffer vs Draw, Area bounds, font rendering patterns, loaders -- [Graphics files](graphics-files.md) — image loaders, the save/load matrix, and BMP565 - -## Key entry points - -- `pygraphics.FrameBuffer` — subclass of the bundled `pygraphics.framebuf.FrameBuffer` with shape helpers and Area returns (same implementation on every interpreter) -- `pygraphics.Draw` — draws on any framebuf-compatible canvas -- `pygraphics.Area` — dirty rectangle with union/clip helpers -- Module functions — `circle`, `rect`, `text8`, … (same primitives as FrameBuffer) -- `pygraphics.Font` — romfont renderer; built-in embedded fonts or optional `.bin` path ([Fonts](graphics-guide.md#fonts)) -- `load_image`, `save_image` — auto-detect load / format-aware save -- `bmp_to_framebuffer`, `pbm_to_framebuffer`, `pgm_to_framebuffer` — image loaders -- `BMP565` — sliceable/streaming RGB565 BMP asset - -Generated API pages for each module appear below (build time). diff --git a/docs/benchmarks.md b/docs/benchmarks.md deleted file mode 100644 index a55d43e..0000000 --- a/docs/benchmarks.md +++ /dev/null @@ -1,47 +0,0 @@ -# Performance Benchmarks - -Performance analysis comparing the native C-extension (`native_cmod`) and the pure-Python fallback (`pygraphics_python`). - ---- - -## ⚡ Native C vs Pure-Python Comparison - -`pygraphics` maintains 100% API parity between its native C-extension and pure-Python implementation: - -| Benchmark Operation | Native C (`native_cmod`) | Pure-Python (`pygraphics_python`) | Speedup | -|:---|:---|:---|:---| -| **Bulk `fill_rect` (160×128)** | ~0.08 ms | ~1.42 ms | **~18x** | -| **Filled Circle ($R=40$)** | ~0.15 ms | ~2.60 ms | **~17x** | -| **Rounded Rectangle** | ~0.21 ms | ~3.10 ms | **~15x** | -| **Romfont Text Rendering (20 chars)** | ~0.18 ms | ~1.85 ms | **~10x** | -| **Direct Sprite Blit (64×64)** | ~0.04 ms | ~0.65 ms | **~16x** | - -> **Recommendation**: On CPython, Android, and desktop environments, always install the native C wheel (`pydevices-pygraphics`). On bare-metal MicroPython/CircuitPython MCUs without custom C modules, the pure-Python package from MIP provides complete drawing functionality with zero compilation. - ---- - -## 🧠 Memory Footprint - -| Component | RAM Overhead | -|:---|:---| -| `pygraphics.Area` instance | 48 bytes | -| `pygraphics.FrameBuffer` wrapper | ~120 bytes + pixel buffer size | -| `pygraphics.Draw` canvas adapter | ~96 bytes (zero extra framebuffer RAM) | -| Embedded Romfonts (8x8, 8x14, 8x16) | 0 bytes heap RAM (read directly from code/frozen bytecode) | - ---- - -## 🔬 Running Parity & Performance Tests Locally - -The `pygraphics` repository includes byte-parity test scripts to verify identical pixel output between native and pure-Python backends: - -```bash -# Run byte-level parity tests -micropython tools/compare_graphics_run.py - -# Run cross-interpreter matrix -python tools/compare_graphics_matrix.py - -# Verify MicroPython C framebuf vs pure-Python framebuf -micropython tools/compare_framebuf_mp.py -``` diff --git a/docs/building-wheels.md b/docs/building-wheels.md deleted file mode 100644 index 31def40..0000000 --- a/docs/building-wheels.md +++ /dev/null @@ -1,55 +0,0 @@ -# Building pygraphics wheels - -The release procedure is org-wide and lives in -[.github/docs/publishing-automation.md](https://github.com/PyDevices/.github/blob/main/docs/publishing-automation.md) — standard steps, shared -workflows, secrets, and the MIP queue. This page is the pygraphics-specific -build detail. - -One published GitHub Release `vX.Y.Z` produces the **pydevices-pygraphics** -native/C-extension wheels (cibuildwheel for Linux, Windows, and Android, plus a -Pyodide `pyemscripten_2026_0` wasm32 wheel) and the unprefixed pure-Python -**pygraphics** MIP package. There is no pure-Python pip distribution — use MIP -for that implementation. - -## Local cmod wheel smoke - -```bash -echo "0.0.0.dev" > VERSION -python3 -m venv .venv -.venv/bin/pip install -e . -.venv/bin/python tests/test_pygraphics.py -``` - -After publishing to TestPyPI, verify the namespaced distribution in a fresh -environment: - -```bash -./tools/test_testpypi_standalone.sh -``` - -## Local Pyodide / wasm wheel - -cibuildwheel drives pyodide-build and the emscripten toolchain: - -```bash -pipx run cibuildwheel --platform pyodide -``` - -Wheels land in `wheelhouse/`. CI runs the same command. - -Produces one wheel per Pyodide target, currently: - -``` -pydevices_pygraphics--cp313-cp313-pyemscripten_2025_0_wasm32.whl -pydevices_pygraphics--cp314-cp314-pyemscripten_2026_0_wasm32.whl -``` - -## Documentation - -- **Read the Docs** (guides + pure-Python API): https://pygraphics.readthedocs.io - Config: `.readthedocs.yaml` + `docs/conf.py`. Connected via the org - [Read the Docs Community GitHub App](https://github.com/organizations/PyDevices/settings/installations) - (no per-repo webhook). See pydevices-examples - [Building docs](https://github.com/PyDevices/.github/blob/main/docs/building-docs.md) for - first-time import / legacy migration notes. -- **GitHub Pages** (marketing + Sourcey native API): https://pydevices.github.io/pygraphics/ diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..5dd6e1c --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,7 @@ +# Changelog + +What's planned next is in the +[roadmap](https://github.com/PyDevices/pygraphics/blob/main/ROADMAP.md). + +```{include} ../CHANGELOG.md +``` diff --git a/docs/formats.md b/docs/formats.md new file mode 100644 index 0000000..524691a --- /dev/null +++ b/docs/formats.md @@ -0,0 +1,51 @@ +# Formats + +## Pixel formats + +| Constant | Bits per pixel | Buffer size for `w` × `h` | Typical use | +|---|---|---|---| +| `MONO_VLSB` | 1 | `w * ((h + 7) // 8)` | monochrome screens, each byte is 8 pixels down a column, low bit at the top | +| `MONO_HLSB` | 1 | `((w + 7) // 8) * h` | monochrome screens, bytes run across, high bit on the left | +| `MONO_HMSB` | 1 | `((w + 7) // 8) * h` | as `MONO_HLSB`, low bit on the left | +| `GS2_HMSB` | 2 | `((w + 3) // 4) * h` | four-level greyscale | +| `GS4_HMSB` | 4 | `((w + 1) // 2) * h` | sixteen-level greyscale | +| `GS8` | 8 | `w * h` | 256-level greyscale | +| `RGB565` | 16 | `w * h * 2` | most small colour screens | +| `RGB888` | 24 | `w * h * 3` | 24-bit screens and RGB LED strips | + +The first seven are `framebuf`'s formats with `framebuf`'s numbering, so a +buffer laid out for `framebuf` works here unchanged. `RGB888` is pygraphics' +own; its bytes are stored red, green, blue. + +## Which file each format saves as + +| Format | `save()` writes | +|---|---| +| `MONO_HLSB` | PBM (P4) | +| `GS2_HMSB` | PGM (P5, maximum 3) | +| `GS4_HMSB` | PGM (P5, maximum 15) | +| `GS8` | PGM (P5, maximum 255) | +| `RGB565` | BMP, 16-bit RGB565 | +| `RGB888` | PNG, always | +| `RGB565`, `GS8`, `MONO_HLSB` | PNG, when the name ends in `.png` | + +`MONO_VLSB` and `MONO_HMSB` can't be saved directly and raise `ValueError`; +blit them into a `MONO_HLSB` framebuffer first. PNG needs the `pngio` module, +as [Load and save images](guides/load-and-save-images.md#save-a-picture) +explains. + +`load_image()` reads PBM (P4), PGM (P5) and 16-bit RGB565 BMP files. + +## Romfont files + +A romfont `.bin` file, as `Font` reads it, is glyph rows and nothing else: + +| | | +|---|---| +| Glyphs | 256, for characters 0–255, or 128 for 0–127 | +| Bytes per glyph | the font's height, one byte per row | +| Width | 8 pixels; the high bit of each byte is the leftmost pixel | +| File size | `256 × height` or `128 × height` bytes | + +The built-in 8x8, 8x14 and 8x16 fonts come from the +[romfont](https://github.com/spacerace/romfont) collection. diff --git a/docs/get-started/first-program.md b/docs/get-started/first-program.md new file mode 100644 index 0000000..292097b --- /dev/null +++ b/docs/get-started/first-program.md @@ -0,0 +1,93 @@ +# Your first picture + +This program draws a card, a dot and a word into memory, tells you which part +of the picture changed, and saves it as a BMP file. It needs no screen, so it +runs the same on a desktop and on a board. + +```python +import pygraphics +from pygraphics import FrameBuffer, RGB565 + +fb = FrameBuffer(bytearray(160 * 80 * 2), 160, 80, RGB565) +fb.fill(0x0000) +card = fb.round_rect(8, 8, 64, 40, 8, 0xF800, f=True) +dot = fb.circle(116, 28, 20, 0x07E0, f=True) +label = pygraphics.text16(fb, "Hello", 8, 56, 0xFFFF) +print("changed:", card + dot + label) +fb.save("hello.bmp") +``` + +It prints: + +```text +changed: Area(8, 8, 128, 64) +``` + +and `hello.bmp` looks like this, shown here at three times its size: + +![A red rounded rectangle, a green circle and the word Hello on black](../images/hello.png) + +## What each line does + +`FrameBuffer(bytearray(160 * 80 * 2), 160, 80, RGB565)` makes a picture 160 +pixels wide and 80 high. `RGB565` stores each pixel in two bytes, which is +why the buffer is `160 * 80 * 2` bytes long. It's the format most small +colour screens take directly. + +Colours are 16-bit RGB565 numbers: `0xF800` is red, `0x07E0` green, `0x001F` +blue, `0xFFFF` white and `0x0000` black. + +`round_rect(x, y, w, h, r, c, f=True)` draws a rectangle with corners of +radius `r`, filled because of `f=True`. `circle(x, y, r, c, f=True)` draws a +filled circle centred on `x, y`. `text16` writes text in the built-in font +that is 8 pixels wide and 16 high. + +Each of these calls returns an `Area`, the rectangle of pixels it changed. +Adding two areas gives the smallest rectangle that covers both, so +`card + dot + label` is everything this program drew. On a real screen you'd +send just that rectangle; [Update part of a screen](../guides/update-part-of-a-screen.md) +shows how. + +`fb.save("hello.bmp")` writes the picture out. The file type comes from the +pixel format; [Load and save images](../guides/load-and-save-images.md) has +the others. + +## Try it in your browser + +This version draws onto the canvas beside the editor. Change a number and +press **Run**. + +````{raw} html +
+
+ +
+ + + Initializing Python… +
+

+  
+
+ +
+
+```` + +Next: [draw shapes](../guides/draw-shapes.md). diff --git a/docs/get-started/install.md b/docs/get-started/install.md new file mode 100644 index 0000000..13ca419 --- /dev/null +++ b/docs/get-started/install.md @@ -0,0 +1,51 @@ +# Install + +pygraphics comes two ways under one import name: a compiled package for +CPython, and a pure-Python package for boards. Your code doesn't change +between them. + +## On a desktop (CPython) + +```bash +pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-pygraphics +``` + +The package is called `pydevices-pygraphics` and you import `pygraphics`. +Releases are on TestPyPI for now, which is why the command names it. There +are compiled wheels for Linux x86_64, Windows, Android and Pyodide; +[Platforms](../platforms.md) has the list and what to do on a Mac. + +## On a MicroPython-compatible board + +From the board's REPL, with the board online: + +```python +import mip +mip.install("pygraphics", index="https://PyDevices.github.io/mip") +``` + +## On a CircuitPython-compatible board + +Copy the `lib/pygraphics/` folder from this repository to the board's +`CIRCUITPY/lib/` folder. + +## In the browser (Pyodide) + +```python +import micropip +await micropip.install("pydevices-pygraphics", index_urls="https://test.pypi.org/simple/") +``` + +micropip picks the wheel that matches your Pyodide version. + +## Check it worked + +```python +import pygraphics +print(pygraphics.implementation()) +``` + +You'll see `native_cmod` from the compiled package, or `pygraphics_python` +from the pure-Python one. Both draw the same pixels. + +Next: [draw your first picture](first-program.md). diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index a540c41..0000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,128 +0,0 @@ -# Getting started - -## Setup - -::::{tab-set} - -:::{tab-item} MicroPython (MIP) - -```python -import mip -mip.install("pygraphics", index="https://PyDevices.github.io/mip") -``` -::: - -:::{tab-item} CPython (TestPyPI) - -```bash -pip install -i https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ pydevices-pygraphics -``` -::: - -:::{tab-item} CircuitPython - -Copy `lib/pygraphics/` to your board's `CIRCUITPY/lib/` directory. -::: - -:::{tab-item} PyScript / Pyodide - -```python -import micropip -await micropip.install( - "pydevices-pygraphics", index_urls="https://test.pypi.org/simple/" -) - -import pygraphics -``` - -The wheel is named `pydevices-pygraphics`; the module you import is -`pygraphics`. On MicroPython the MIP package name is `pygraphics` -(`mip.install("pygraphics", index="https://PyDevices.github.io/mip")`). -::: - -:::: - ---- - -## 💻 Live Interactive Drawing Canvas - -Experiment with 2D shapes, colors, and font rendering live in your browser: - -````{raw} html -
-
- -
- - - Initializing Python… -
-

-  
-
- -
-
-```` - ---- - -## Basic Usage - -```python -import pygraphics -from pygraphics import FrameBuffer, RGB565, Area - -fb = FrameBuffer(bytearray(160 * 128 * 2), 160, 128, RGB565) -fb.fill(0) -area = fb.fill_rect(10, 10, 40, 40, 0xF800) -assert isinstance(area, Area) -print(pygraphics.implementation()) # native_cmod or pygraphics_python -``` - ---- - -## Common Patterns - -### 1. Draw to an off-screen buffer first -Use a `FrameBuffer` as a canvas, then copy the result to the display: - -```python -import pygraphics -from pygraphics import FrameBuffer, RGB565, text8 - -buf = FrameBuffer(bytearray(160 * 128 * 2), 160, 128, RGB565) -buf.fill(0x0000) -pygraphics.fill_rect(buf, 10, 10, 40, 20, 0xF800) -text8(buf, "Hello", 12, 14, 0xFFFF) -``` - -### 2. Use the returned `Area` as a dirty rectangle -Many draw helpers return an `Area` describing the pixels they changed. That is useful when refreshing only the part of the screen that changed: - -```python -area = pygraphics.circle(buf, 80, 40, 12, 0x07E0, f=True) -# area.x, area.y, area.w, area.h describe the changed region -display_drv.blit_rect(buf.buffer, area.x, area.y, area.w, area.h) -``` diff --git a/docs/graphics-files.md b/docs/graphics-files.md deleted file mode 100644 index cd208e0..0000000 --- a/docs/graphics-files.md +++ /dev/null @@ -1,81 +0,0 @@ -# Graphics files - -Two layers: **`pygraphics` loaders** (eager, full image in RAM) and **add-ons in [pydevices-examples](https://github.com/PyDevices/pydevices-examples)** (TFT-specific helpers). - -## pygraphics package loaders - -Built into `pygraphics` — see the [graphics guide](graphics-guide.md): - -| Function | Format | -|----------|--------| -| `pygraphics.bmp_to_framebuffer(path)` | Windows BMP → `FrameBuffer` | -| `pygraphics.pbm_to_framebuffer(path)` | PBM (1-bit) | -| `pygraphics.pgm_to_framebuffer(path)` | PGM (grayscale) | -| `pygraphics.load_image(path)` | Auto-detect PBM/PGM/BMP from header | -| `pygraphics.save_image(fb, path)` | Write PBM/PGM/BMP for supported formats, or PNG for a `.png` name | -| `pygraphics.FrameBuffer.from_file(path)` | Same as `load_image` | -| `pygraphics.FrameBuffer.save(path)` | Same as `save_image` | -| `pygraphics.FrameBuffer.export(path)` / `export_framebuffer` | **Pure-Python only.** Write importable `.py` module (`BITMAP = bytearray(...)`) | -| `pygraphics.FrameBuffer.from_bitmap(buf, w, h, fmt)` | **Pure-Python only.** Wrap buffer; zero-copy when `buf` is a writable `bytearray` | -| `pygraphics.FrameBuffer.from_module(mod)` | **Pure-Python only.** Same using `mod.WIDTH` / `HEIGHT` / `FORMAT` / `BITMAP` | - -**Pure-Python only** means the native C module does not carry it: these four -emit and consume Python source, which is authoring-time work. On a native -build, import them from the pure package (`lib/pygraphics/`) instead — see -[what the native build does not have](https://github.com/PyDevices/pygraphics#what-the-native-build-does-not-have). - -### Memory notes - -- **Files:** PBM/PGM parsers allocate one pixel `bytearray` and fill it with `readinto` (peak ≈ one framebuffer). BMP already fills a single destination buffer row-by-row. -- **`.py` bitmaps:** Prefer `BITMAP = bytearray(...)` so `from_bitmap` does not copy. Legacy `memoryview(bytes)` modules get one copy. This is separate from `Font.export`, which keeps a read-only `memoryview` for glyph indexing. - -### Save/load matrix - -| Framebuffer format | File | Notes | -|--------------------|------|-------| -| `MONO_HLSB` | PBM (P4) | 1-bit portable bitmap | -| `GS2_HMSB` | PGM (P5, max 3) | 2-bit grayscale | -| `GS4_HMSB` | PGM (P5, max 15) | 4-bit grayscale | -| `GS8` | PGM (P5, max 255) | 8-bit grayscale | -| `RGB565` | BMP | 16-bit RGB565 Windows BMP | -| `RGB888` | PNG | Always PNG | -| `RGB565`, `GS8`, `MONO_HLSB` | PNG | When the name ends in `.png` | - -`MONO_VLSB`, `MONO_HMSB`, and other display-native formats are not saved directly — convert or blit to a supported buffer first. - -Use these for icons and sprites that fit in RAM on MCU or desktop. - -## BMP565 (streaming) - -`pygraphics.BMP565` reads/writes Windows BMP files in RGB565 format (export from GIMP). Shared header/row logic also powers `bmp_to_framebuffer` and `FrameBuffer.save()` for RGB565. - -Features: - -- Load entire file or **stream** slices for large images: `BMP565[1:5, 6:10]` -- Row mirroring for rotated scroll backgrounds -- Use an existing bytearray as buffer (screenshots) - -Examples: `bmp565_simpletest.py`, `bmp565_sprite.py`, `bmp565_scroll.py`. Each -one opens a `.bmp` from the filesystem, so they run on desktop and MCU but not -in the browser. - -To see `FrameBuffer` blitting live without an image file, run -[`pbm_create_new.py`](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=pbm_create_new&deps=pydevices-pygraphics), -which builds an image in code and blits it, or -[`framebuf_simpletest.py`](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=framebuf_simpletest&deps=pydevices-pygraphics). - -## tft_text / tft_write bitmap helpers (pydevices-examples) - -From @russhughes st7789py_mpy: - -- `.bitmap()` — decode `.py` image files from [image_converter.py](https://github.com/russhughes/st7789py_mpy/blob/master/utils/image_converter.py) or [sprites_converter.py](https://github.com/russhughes/st7789py_mpy/blob/master/utils/sprites_converter.py); renders full image then blits -- `.pbitmap()` — progressive line-at-a-time rendering with a one-line buffer - -## PNG - -`save_image(fb, "shot.png")` writes PNG through -[pngio](https://github.com/PyDevices/micropython-pydevices/tree/main/modules/pngio), -not an encoder of pygraphics' own. PyDevices MicroPython firmware has pngio -built in. On CPython it comes with `pydevices-desktop` and needs Pillow -(`pip install pillow`). Without it, saving a PNG raises an `ImportError` that -says what to install. Every other format works without it. diff --git a/docs/graphics-guide.md b/docs/graphics-guide.md deleted file mode 100644 index 38013af..0000000 --- a/docs/graphics-guide.md +++ /dev/null @@ -1,331 +0,0 @@ -# Graphics guide - -Cross-platform 2D drawing: framebuf-compatible buffers, shape primitives that return **Area** bounds, fonts, and image loaders. One import works on MicroPython, CircuitPython, and CPython. - -:::{admonition} Where the pieces live -:class: note -`pygraphics` is maintained here. Install it from the -[PyDevices MIP index](installation.md) (`mip.install("pygraphics", index=…)`) -or from TestPyPI as `pydevices-pygraphics`. The `utils` modules referenced -below (`displaybuf`, `tft_text`, …) ship with -[pydevices-examples](https://github.com/PyDevices/pydevices-examples). -::: - -## Quick start - -```python -import pygraphics - -w, h = 16, 16 -fb = pygraphics.FrameBuffer(bytearray(w * h * 2), w, h, pygraphics.RGB565) -fb.fill(0) -fb.fill_rect(1, 1, 6, 6, 0xFFFF) -fb.circle(8, 8, 3, 0x1234) -pygraphics.text8(fb, "Hi", 0, 0, 0xFFFF) -``` - -`pygraphics` bundles its own pure-Python `framebuf` implementation (`pygraphics.framebuf`, MP-parity -with `modframebuf.c`) and always builds `pygraphics.FrameBuffer` on top of it — the same code path -runs on MicroPython, CircuitPython, and CPython, so there is no native-vs-pure-Python backend to -inspect or branch on. - -## Drawing Pipeline Architecture - -```{mermaid} -flowchart TD - subgraph Primitives ["Drawing Operations"] - P1["fill_rect(x, y, w, h)"] - P2["circle(x, y, r)"] - P3["round_rect(x, y, w, h, r)"] - P4["text8 / text14 / text16"] - end - - subgraph CoreEngine ["pygraphics Pipeline"] - Clip{"Active Clip Rect?
(with draw.clip)"} - DrawOp["Rasterize to FrameBuffer / Canvas"] - AreaCalc["Compute Bounding Area(x, y, w, h)"] - end - - subgraph Output ["Target Dispatch"] - Dirty["Dirty Region Accumulation
(dirty = a + b)"] - FastBlit["Hardware Fast-Path
(display_drv.blit_rect)"] - end - - Primitives --> Clip - Clip -->|Intersect Bounds| DrawOp - DrawOp --> AreaCalc - AreaCalc --> Dirty - Dirty --> FastBlit -``` - -## FrameBuffer vs Draw vs module functions - -| Style | When to use | -|-------|-------------| -| **`pygraphics.FrameBuffer`** | Default — own a buffer; get `.buffer`, `.width`, save/load, all shape methods | -| **`pygraphics.Draw(canvas)`** | Draw on a display driver or third-party object with `pixel` / `hline` / … | -| **Module functions** (`pygraphics.circle(fb, …)`) | Short scripts; same primitives as `FrameBuffer` methods | - -```python -draw = pygraphics.Draw(display_drv) -draw.round_rect(5, 5, 50, 30, 4, 0xF800) - -with draw.clip(10, 20, 100, 60): - draw.fill_rect(0, 0, 200, 200, 0xF800) # only the intersection is drawn -``` - -## Area and partial updates - -Most draw methods return an `Area(x, y, w, h)` bounding box. Union dirty regions: - -```python -a = fb.fill_rect(0, 0, 10, 10, color) -b = fb.circle(12, 12, 4, color) -dirty = a + b -display_drv.blit_rect(fb.buffer, dirty.x, dirty.y, dirty.w, dirty.h) -``` - -- **Setting** `fb.pixel(x, y, c)` returns `Area(x, y, 1, 1)`. -- **Reading** `fb.pixel(x, y)` returns the color (no `Area`). -- **`scroll()`** returns the full buffer bounds. - -## Pixel formats - -| Constant | Depth | -|----------|-------| -| `MONO_VLSB`, `MONO_HLSB`, `MONO_HMSB` | 1 bpp | -| `GS2_HMSB` | 2 bpp | -| `GS4_HMSB` | 4 bpp | -| `GS8` | 8 bpp | -| `RGB565` | 16 bpp | - -## Fonts - -Text helpers (`text8`, `text14`, `text16`, `text`, and `FrameBuffer.text`) use **embedded -romfont** data shipped inside the `pygraphics` package (`_font_8x8.py`, `_font_8x14.py`, -`_font_8x16.py` — derived from [spacerace/romfont](https://github.com/spacerace/romfont)). -No font files on the filesystem are required for the built-in heights. - -```python -pygraphics.text8(fb, "Hello", 0, 0, 0xFFFF) -pygraphics.text14(fb, "Tall", 0, 16, 0xF800) -pygraphics.text16(fb, "Big", 0, 32, 0x07E0) -``` - -### Loading romfont `.bin` files from the filesystem - -To use a custom or packaged romfont binary, pass a **file path** to `pygraphics.Font`. -The file is opened on MicroPython, CircuitPython, and CPython like any other readable path -(relative paths are resolved from the process working directory). - -```python -f = pygraphics.Font("assets/font_8x14.bin", 14) -f.text(fb, "From disk", 0, 48, 0xFFFF) - -# Height can be inferred from names like font_8x16.bin when omitted: -f16 = pygraphics.Font("/sd/fonts/font_8x16.bin") -``` - -By default (`cached=True`) the entire file is read into RAM when the `Font` is constructed. -Set `cached=False` to keep the file open and read glyphs on demand (lower RAM, more I/O). - -Missing or unreadable paths raise `FileNotFoundError`. A file whose size does not match the -expected glyph count raises `RuntimeError`. - -Examples that load `.bin` fonts from disk: `font_simpletest.py` (cycles -`string_blit` → `per_pixel` → `displaybuf`) and `font_list.py`. Both read font -files from the filesystem, so they run on desktop and MCU but not in the -browser. - -For a `Font` demo you can run right now, see -[`tv_remote_menu.py`](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=tv_remote_menu&deps=pydevices-pygraphics) or -[`pydevices_demo.py`](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=pydevices_demo&deps=pydevices-pygraphics), -which use the built-in font (`Font(height=16)`) and need no font file. - -### Romfont `.bin` format - -Romfont binaries are a flat blob of glyph rows (8 pixels wide, MSB = leftmost pixel): - -| Field | Value | -|-------|--------| -| Glyph order | Code points `0`–`255` (or `128` glyphs for a 128-character subset) | -| Bytes per glyph | Font height (e.g. 8, 14, or 16) | -| File size | `256 × height` bytes, or `128 × height` for 128-glyph files | -| Width | 8 pixels (only 8-pixel-wide fonts are supported today) | - -You can also pass a `memoryview` or `bytes` object instead of a path when the font data is -already in RAM (frozen module, `bytes` literal, mmap, and so on). - -### Not the same as `framebuf.text` - -`pygraphics.framebuf.FrameBuffer.text()` (and MicroPython's built-in `framebuf` module) use a -**different** built-in 8×8 font (Damien George's `font_petme128_8x8`). For romfont appearance -and heights 8/14/16, use `pygraphics.text8` / `text14` / `text16` or `pygraphics.Font` as above. - -### Choosing a font rendering pattern - -`Font.text()` and `text8` / `text14` / `text16` render each set glyph bit with -`fill_rect` (scaled squares). **Where** you draw — and whether you composite in RAM first — -controls transparency, RAM use, and how much data hits the panel bus. - -The multipath `font_simpletest.py` example uses the same `Font` + romfont `.bin` files but -cycles different targets in one run. [`pydevices_demo`](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=pydevices_demo&deps=pydevices-pygraphics) follows the -**string framebuffer + one blit** pattern (`string_blit`). - -| Pattern | Example | Background | Extra RAM | What hits the display | Typical sweet spot | -|---------|---------|------------|-----------|----------------------|-------------------| -| **Module helpers on canvas** | `pygraphics.text8(display_drv, …)` | Transparent (foreground pixels only) | None | One small `fill_rect` per lit pixel | Short labels, minimum RAM | -| **String FB → one blit** | `font_simpletest.py` (`string_blit`) | **Opaque** — `fb.fill(bg)` before `font.text` | One buffer sized to the string (reusable slice is better; see pydevices_demo) | **One** `blit_rect` per string | Desktop/SDL (batch then `show()`), SPI panels when RAM is tight | -| **Draw on `display_drv`** | `font_simpletest.py` (`per_pixel`) | Transparent | None | One `fill_rect` per lit pixel on the live driver | Simplest code path; **slowest** on MCU and desktop | -| **Full-screen `DisplayBuffer` + dirty blit** | `font_simpletest.py` (`displaybuf`) | Transparent over existing buffer contents | **Full panel** `DisplayBuffer` | `display.show(dirty)` — one row `blit_rect` per dirty scanline | MCUs with enough RAM; many text updates; fastest of the three modes | -| **Catalog / inspect fonts** | `font_list.py` | Opaque row buffer | One strip `width × height` per font | One `blit_rect` per font row | Browsing `.bin` files on disk | - -#### Module helpers (`text8`, `text14`, `text16`) - -Draw directly on any canvas (`FrameBuffer`, `display_drv`, `DisplayBuffer`, …). Only -foreground pixels are written — the background is left unchanged (**transparent** text). - -```python -pygraphics.text8(fb, "Hi", x, y, fg_color) -area = pygraphics.text16(display_drv, "Status", 4, 4, 0xFFFF) # returns Area bounds -``` - -Lowest memory overhead; fine for a few characters. On SPI TFTs without a compositing layer, -each lit pixel can become a separate bus transaction (same cost class as `per_pixel` mode). - -#### String framebuffer + one blit (`string_blit`) - -Compose the whole string in a small off-screen `FrameBuffer`, then upload it once: - -```python -w, h = len(s) * font.width * scale, font.height * scale -buf = bytearray(w * h * 2) -fb = pygraphics.FrameBuffer(buf, w, h, pygraphics.RGB565) -fb.fill(bg_color) # opaque background -font.text(fb, s, 0, 0, fg_color, scale) -display_drv.blit_rect(buf, x, y, w, h) -display_drv.show() # SDL/pygame: present the frame -``` - -- **Opaque** labels (background colour filled before glyphs). -- **RAM:** proportional to string size, not the full screen — good when `DisplayBuffer` is too - large for the MCU. -- **Speed:** one bulk blit per string. On **desktop** backends that defer work until `show()`, - this batches well. On **MCU** panels that flush each `blit_rect` immediately, this still beats - per-pixel drawing because the bus sees one contiguous block per string. -- Production apps often keep a **reusable** buffer sized for the longest line (see - [`pydevices_demo`](https://github.com/PyDevices/pydevices-examples/blob/main/lib/examples/pydevices_demo.py)) instead of allocating every frame like the - simpletest does. - -#### Direct draw on `display_drv` (`per_pixel`) - -```python -font.text(display_drv, s, x, y, fg_color, scale) -display_drv.show() -``` - -- **Transparent** text (no `fb.fill`; unset bits are not drawn). -- **Lowest RAM** — no extra framebuffer. -- **Slowest** upload pattern: every lit pixel is its own `fill_rect` on the driver. Avoid for - long strings on hardware; acceptable for occasional tiny overlays. - -#### `DisplayBuffer` + dirty rectangle (`displaybuf`) - -Keep a logical full-screen buffer in RAM; upload only what changed: - -```python -from displaybuf import DisplayBuffer - -display = DisplayBuffer(display_drv) -dirty = font.text(display, s, x, y, fg_color, scale) -display.show(dirty) # row blits for the Area bounds only (RGB565) -display_drv.show() # present on SDL; on raw SPI may follow panel habits -``` - -- **Transparent** over whatever is already in the `DisplayBuffer`. -- **Highest RAM** (full panel buffer) — trade memory for speed when the UI redraws text often. -- **Fastest** of the three `font_simpletest` modes: glyph work stays in RAM; the panel receives only - the dirty region (scanline `blit_rect`s), not per-pixel fills. -- Requires `utils/displaybuf.py` on the import path — set `PYTHONPATH`/`MICROPYPATH` to `.:lib:utils` (preferred), or see [pydevices install workflows](https://github.com/PyDevices/pydevices/blob/main/docs/install-workflows.md) when environment variables are unavailable or not set as recommended. -- Partial `area=` updates apply to **RGB565** `DisplayBuffer`; GS8/GS4 paths currently refresh - wider bands (see `displaybuf` notes in source). - -#### Desktop vs MCU and `display_drv.show()` - -| Backend | Role of `show()` | -|---------|------------------| -| **SDL / pygame (desktop)** | Drawing is buffered; `show()` presents the frame. Prefer **few large blits** (`font_simpletest.py` or reusable string buffer) then one `show()` per frame. | -| **SPI / parallel MCU panels** | Many drivers act on each `blit_rect` / `fill_rect` immediately. Favour **one blit per string** or **`DisplayBuffer.show(dirty)`** over `font.text(display_drv, …)`. | -| **Skipping `show()`** | Only safe when your driver documents immediate updates. Per-pixel `font.text(display_drv, …)` is still slow on the bus even without `show()`. | - -Run the examples side by side from `lib/`: - -```bash -micropython examples/font_simpletest.py -``` - -PyScript and the gallery load the same `.bin` assets from `lib/examples/assets/` (see the -[pydevices-examples gallery](https://pydevices.github.io/pydevices-examples/pyscript/)). - -## Image loaders - -Eager loaders in the `pygraphics` package (full image in RAM): - -```python -fb = pygraphics.bmp_to_framebuffer("sprite.bmp") -fb = pygraphics.pbm_to_framebuffer("icon.pbm") -fb = pygraphics.pgm_to_framebuffer("gray.pgm") -fb = pygraphics.load_image("image.bmp") # or FrameBuffer.from_file(...) -``` - -`save_image(fb, path)` and `FrameBuffer.save()` write PBM/PGM/BMP for the formats in [Graphics files](graphics-files.md). Other framebuffer formats raise `ValueError`. - -## Blit fast paths - -`pygraphics.blit()`, `Draw.blit()`, and `blit_rect()` dispatch to faster implementations when available: - -| Destination | Fast path | -|-------------|-----------| -| Display driver (`blit_rect` / `blit_transparent`) | SPI/SDL/pygame bulk copy | -| `FrameBuffer` | `pygraphics.framebuf`'s `blit()` (same implementation on every interpreter) | - -Use `Draw(display_drv).blit(sprite_fb, x, y)` instead of a per-pixel loop — it routes to `display_drv.blit_rect` for RGB565 sprites. - -### Clip regions - -`Draw.clip(x, y, w, h)` (or `clip(Area(...))`) is a context manager that intersects all drawing with a rectangle. Nested clips intersect further; the clip is restored when the block exits: - -```python -with draw.clip(10, 10, 50, 40): - draw.fill(0x0000) # fills only the clip rect - draw.text8("Panel", 0, 0, 0xFFFF) -``` - -For streaming/large BMP assets, use `pygraphics.BMP565` (sliceable, optional streaming reads) — see [Graphics files](graphics-files.md). - -## 🎮 Live Interactive Gallery Examples - -Run full interactive `pygraphics` games and demos directly in your browser: - -| Example | Description | Live PyScript Link | -|:---|:---|:---| -| **`bouncing_balls`** | Animated high-framerate vector bouncing balls | [**Launch `bouncing_balls`**](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=bouncing_balls&deps=pydevices-pygraphics) | -| **`alien`** | Animated retro sprite invasion arcade | [**Launch `alien`**](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?manifests=alien&deps=pydevices-palettes,pydevices-pygraphics) | -| **`calc_graphics`** | Pure 2D graphics pocket calculator | [**Launch `calc_graphics`**](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=calc_graphics,calc_engine&deps=pydevices-palettes,pydevices-pygraphics) | -| **`dino`** | Chrome runner obstacle jumping arcade | [**Launch `dino`**](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=dino&deps=pydevices-pygraphics) | -| **`simon`** | Simon touch memory sequence game | [**Launch `simon`**](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=simon&deps=pydevices-pygraphics) | -| **`piano`** | Interactive multi-key musical keyboard | [**Launch `piano`**](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=piano&deps=pydevices-pygraphics) | -| **`testris`** | Complete falling-blocks arcade game | [**Launch `testris`**](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=testris&deps=pydevices-pygraphics) | -| **`rotations`** | Hardware and software panel rotation suite | [**Launch `rotations`**](https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=rotations&deps=pydevices-palettes,pydevices-pygraphics) | - ---- - -## FAQ - -**Draw method returned nothing?** — Use `pygraphics.FrameBuffer` or `Draw`; the bare `pygraphics.framebuf.FrameBuffer` base methods do not return `Area`. - -## Next - -- [Graphics files](graphics-files.md) — loaders and BMP565 -- [API overview](api-overview.md) -- [Displays](https://github.com/PyDevices/pydevices/blob/main/docs/displaydev.md) diff --git a/docs/guides/draw-shapes.md b/docs/guides/draw-shapes.md new file mode 100644 index 0000000..7fdf77d --- /dev/null +++ b/docs/guides/draw-shapes.md @@ -0,0 +1,82 @@ +# Draw shapes + +Make a `FrameBuffer`, then call its shape methods. Each one returns the +`Area` it changed. + +```python +import pygraphics +from pygraphics import FrameBuffer, RGB565 + +fb = FrameBuffer(bytearray(200 * 120 * 2), 200, 120, RGB565) +fb.fill(0x0000) + +fb.line(0, 0, 199, 119, 0xFFFF) +fb.rect(10, 10, 60, 30, 0x001F) # outline +fb.rect(80, 10, 60, 30, 0x001F, f=True) # filled +fb.round_rect(10, 50, 60, 30, 6, 0xF800, f=True) +fb.circle(110, 65, 15, 0x07E0) +fb.ellipse(160, 65, 25, 12, 0xFFE0, f=True) +fb.triangle(10, 110, 40, 90, 70, 110, 0xF81F, f=True) +fb.arc(110, 100, 15, 0, 180, 0x07FF) +fb.gradient_rect(140, 90, 50, 25, 0xF800, 0x001F) +``` + +Pass `f=True` to fill a shape that has an outline form. `arc` takes a start +and end angle in degrees. `gradient_rect` blends from the first colour to the +second, top to bottom; add `vertical=False` for left to right. + +For a shape of your own, `poly(x, y, coords, c, f=False)` draws the polygon +whose points are listed in `coords` as `x0, y0, x1, y1, ...`, offset by +`x, y`. `polygon(points, x, y, c, angle=0)` takes a list of `(x, y)` pairs +and can rotate them by `angle` radians. + +```python +from array import array + +star = array("h", [0, -20, 6, -6, 20, -6, 9, 4, 13, 18, 0, 9, -13, 18, -9, 4, -20, -6, -6, -6]) +fb.poly(100, 60, star, 0xFFE0, f=True) +``` + +## Three ways to call the same thing + +Every shape is also a function that takes the framebuffer first, and a method +on `Draw`, which wraps anything that draws: + +```python +fb.circle(50, 50, 10, 0xF800) +pygraphics.circle(fb, 50, 50, 10, 0xF800) +pygraphics.Draw(fb).circle(50, 50, 10, 0xF800) +``` + +All three draw the same pixels. Use the method when you have a +`FrameBuffer`. Use `Draw` when you want to draw on a display driver or on +another object that has the basic `framebuf` methods (`pixel`, `hline`, +`vline`, `fill_rect`), or when you want [clipping](update-part-of-a-screen.md#draw-inside-a-rectangle-only). + +## Colours and pixel formats + +`RGB565` is the usual choice for a colour screen. The other formats are for +monochrome and greyscale screens, LED strips and smaller buffers; they're +listed with their sizes under [Formats](../formats.md#pixel-formats). The +buffer must hold `width * height` pixels at the format's size, so a +`MONO_HLSB` buffer 128 by 64 is `128 * 64 // 8` bytes. + +```python +from pygraphics import MONO_HLSB + +oled = FrameBuffer(bytearray(128 * 64 // 8), 128, 64, MONO_HLSB) +oled.fill(0) +oled.round_rect(4, 4, 120, 56, 8, 1) +``` + +## Read a pixel back + +`pixel(x, y, c)` sets one pixel and returns its `Area`. `pixel(x, y)` with no +colour returns the colour that's there. + +```python +fb.pixel(3, 3, 0xF800) +print(hex(fb.pixel(3, 3))) # 0xf800 +``` + +Next: [update part of a screen](update-part-of-a-screen.md). diff --git a/docs/guides/draw-text.md b/docs/guides/draw-text.md new file mode 100644 index 0000000..b69026c --- /dev/null +++ b/docs/guides/draw-text.md @@ -0,0 +1,86 @@ +# Draw text + +pygraphics carries three fonts inside it, all 8 pixels wide: 8, 14 and 16 +pixels high. You need no font file to use them. + +```python +import pygraphics +from pygraphics import FrameBuffer, RGB565 + +fb = FrameBuffer(bytearray(240 * 80 * 2), 240, 80, RGB565) +fb.fill(0x0000) + +pygraphics.text8(fb, "Small", 0, 0, 0xFFFF) +pygraphics.text14(fb, "Medium", 0, 12, 0xF800) +area = pygraphics.text16(fb, "Large", 0, 30, 0x07E0) +pygraphics.text16(fb, "Big", 120, 0, 0xFFE0, scale=3) +``` + +Each call returns the `Area` it drew. `scale=2` or more makes every pixel of +the font a square of that size. Text is transparent: only the letters are +drawn, and whatever was behind them stays. + +For text on a solid background, fill the rectangle first: + +```python +font = pygraphics.Font(height=16) +w = font.text_width("Score: 120") +fb.fill_rect(0, 60, w, font.height, 0x001F) +font.text(fb, "Score: 120", 0, 60, 0xFFFF) +``` + +`Font` keeps a font loaded so you can measure and draw with it again. + +## Use a font file + +`Font` also reads romfont `.bin` files, from a path or from bytes already in +memory. The height comes from a name like `font_8x14.bin`, or you can give +it: + +```python +f = pygraphics.Font("fonts/font_8x14.bin") +f.text(fb, "From a file", 0, 0, 0xFFFF) + +f = pygraphics.Font("fonts/custom.bin", 14) +``` + +By default the whole file is read into memory. `cached=False` keeps the file +open and reads each letter when it's drawn, which saves memory and costs +speed. A missing file raises `OSError` (`FileNotFoundError` on CPython). The +file layout is under +[Formats](../formats.md#romfont-files). + +## Text on a real screen + +Where you draw the text decides how fast it reaches the screen. + +| You draw on | Memory | What the screen receives | Good for | +|---|---|---|---| +| The display driver, `text16(display_drv, ...)` | none | one small rectangle per lit pixel | a few characters | +| A framebuffer the size of the string, then `blit_rect` | the string's size | one block per string | labels on a screen with little memory | +| A framebuffer the size of the screen, then [send the dirty area](update-part-of-a-screen.md) | the whole screen | the changed rows | text that changes often | + +Drawing straight on a display driver is the simplest and the slowest: on an +SPI screen, every lit pixel is its own transfer. A string-sized buffer looks +like this: + +```python +s = "Ready" +w, h = font.text_width(s), font.height +label = FrameBuffer(bytearray(w * h * 2), w, h, RGB565) +label.fill(0x0000) +font.text(label, s, 0, 0, 0xFFFF) +display_drv.blit_rect(label.buffer, 10, 10, w, h) +``` + +Keep one buffer for your longest label and reuse it, rather than making a new +one every frame. + +## framebuf's text looks different + +`fb.text(s, x, y, c)` on a pygraphics `FrameBuffer` uses the same 8x8 font as +`text8`, and takes `height=14` or `height=16` for the others. MicroPython's +own `framebuf` module draws text in a different 8x8 font, so a program moved +from `framebuf` to pygraphics will show slightly different letters. + +Next: [load and save images](load-and-save-images.md). diff --git a/docs/guides/load-and-save-images.md b/docs/guides/load-and-save-images.md new file mode 100644 index 0000000..188a274 --- /dev/null +++ b/docs/guides/load-and-save-images.md @@ -0,0 +1,81 @@ +# Load and save images + +## Save a picture + +`fb.save(name)` writes a file in the type that fits the pixel format: BMP for +`RGB565`, PGM for greyscale, PBM for `MONO_HLSB`, PNG for `RGB888`. If the +name doesn't end in that type's extension, the extension is added, and the +name it wrote is returned. + +```python +from pygraphics import FrameBuffer, RGB565, GS8 + +fb = FrameBuffer(bytearray(64 * 64 * 2), 64, 64, RGB565) +fb.fill(0x001F) +print(fb.save("sky.bmp")) # sky.bmp + +grey = FrameBuffer(bytearray(64 * 64), 64, 64, GS8) +grey.gradient_rect(0, 0, 64, 64, 0, 255) +print(grey.save("ramp")) # ramp.pgm +``` + +`pygraphics.save_image(fb, name)` does the same thing as a function. The full +list of formats and file types is under [Formats](../formats.md#which-file-each-format-saves-as). + +A name ending in `.png` saves an `RGB565`, `GS8` or `MONO_HLSB` picture as +PNG. PNG needs the `pngio` module: PyDevices firmware has it built in, and on +CPython it comes with the `pydevices-desktop` package and needs Pillow. +Without it, saving a PNG raises an `ImportError` that says what to install. + +## Load a picture + +`load_image(path)` reads a BMP, PGM or PBM file, works out which from the +file itself, and returns a `FrameBuffer`: + +```python +import pygraphics + +sky = pygraphics.load_image("sky.bmp") +print(sky.width, sky.height) # 64 64 +fb.blit(sky, 0, 0) +``` + +The whole image goes into memory. BMP files must be 16-bit RGB565, which is +what GIMP writes when you export as BMP with "R5 G6 B5" under advanced +options. + +## Stream a large BMP + +An image bigger than your board's free memory can still be shown in pieces. +`BMP565` opens an RGB565 BMP and lets you slice out a rectangle; +`streamed=True` reads only the rows you ask for: + +```python +from pygraphics import BMP565 + +bg = BMP565("sky.bmp", streamed=True) +tile = bg[0:16, 0:16] # x from 0 to 16, y from 0 to 16 +display_drv.blit_rect(tile, 0, 0, 16, 16) +``` + +A slice is raw RGB565 bytes, ready for a display driver's `blit_rect`. +`bg(x, y, w, h)` is the same as `bg[x:x + w, y:y + h]`. + +## Turn a picture into Python source + +The pure-Python package can write a framebuffer out as a `.py` module, so an +icon can be frozen into firmware or imported like any other module: + +```python +fb.export("icon.py") + +import icon +again = FrameBuffer.from_module(icon) +``` + +`export`, `export_framebuffer`, `from_bitmap` and `from_module` are in the +pure-Python package only; the compiled package leaves them out because they +write and read Python source, which is preparation work rather than drawing. +On CPython with the compiled package, run that step with the pure package on +your path. [Platforms](../platforms.md#names-only-the-pure-python-package-has) +lists them. diff --git a/docs/guides/update-part-of-a-screen.md b/docs/guides/update-part-of-a-screen.md new file mode 100644 index 0000000..9b6588b --- /dev/null +++ b/docs/guides/update-part-of-a-screen.md @@ -0,0 +1,86 @@ +# Update part of a screen + +Sending a whole frame to a small screen over SPI is slow. Every pygraphics +drawing call returns the `Area` it changed, so you can send only those +pixels. + +## Collect what changed + +Add areas together to get one rectangle that covers them all: + +```python +from pygraphics import FrameBuffer, RGB565 + +fb = FrameBuffer(bytearray(240 * 135 * 2), 240, 135, RGB565) +fb.fill(0x0000) + +a = fb.fill_rect(10, 10, 20, 20, 0xF800) +b = fb.circle(60, 40, 8, 0x07E0, f=True) +dirty = a + b +print(dirty) # Area(10, 10, 58, 38) +``` + +An `Area` has `x`, `y`, `w` and `h`, and a few helpers: `contains(x, y)`, +`intersects(other)`, `clip(other)` for the overlap, and `shift(dx, dy)`. + +Most calls return the area they drew. Two exceptions: `pixel(x, y)` with no +colour reads a pixel and returns its colour, and `scroll()` returns the whole +buffer, since every pixel may have moved. + +## Send just that rectangle + +A display driver's `blit_rect(buf, x, y, w, h)` takes a buffer holding +exactly `w * h` pixels. The framebuffer holds the whole screen, so send the +dirty rectangle one row at a time: + +```python +def send(display, fb, area): + rows = memoryview(fb.buffer) + row_bytes = area.w * 2 # RGB565 + for y in range(area.y, area.y + area.h): + start = (y * fb.width + area.x) * 2 + display.blit_rect(rows[start:start + row_bytes], area.x, y, area.w, 1) + +send(display_drv, fb, dirty) +``` + +`display_drv` is your screen's driver. Each row is one bus transfer, which is +still far less than the whole screen when the change is small. If a whole +frame changes, send it in one `blit_rect(fb.buffer, 0, 0, fb.width, fb.height)`. + +## Draw inside a rectangle only + +`Draw(canvas).clip(x, y, w, h)` is a context manager. Inside it, every +drawing call is cut to that rectangle; clips nest, and the clip ends with the +block. + +```python +import pygraphics + +draw = pygraphics.Draw(fb) +with draw.clip(100, 20, 60, 40): + draw.fill(0x001F) # fills only the clip + draw.text8("Panel", 100, 20, 0xFFFF) + draw.circle(130, 60, 30, 0xFFE0) # the part outside is skipped +``` + +This is how you redraw one panel of a screen without touching its +neighbours. + +## Copy a sprite onto the picture + +`blit(source, x, y, key)` copies another framebuffer in, skipping pixels of +colour `key`: + +```python +sprite = FrameBuffer(bytearray(16 * 16 * 2), 16, 16, RGB565) +sprite.fill(0x0000) +sprite.circle(8, 8, 6, 0xFFE0, f=True) + +fb.blit(sprite, 200, 100, 0x0000) # black is transparent +``` + +To put an RGB565 sprite straight on the screen, hand its buffer to the +driver in one call: `display_drv.blit_rect(sprite.buffer, x, y, 16, 16)`. + +Next: [draw text](draw-text.md). diff --git a/docs/how-it-works/benchmarks.md b/docs/how-it-works/benchmarks.md new file mode 100644 index 0000000..fdac57c --- /dev/null +++ b/docs/how-it-works/benchmarks.md @@ -0,0 +1,47 @@ +# Benchmarks + +The compiled package is more than a hundred times faster than the pure-Python +one, on CPython and on MicroPython. + +Milliseconds per call, drawing into a 160 × 128 `RGB565` framebuffer: + +| Call | CPython, compiled | CPython, pure | MicroPython, compiled | MicroPython, pure | +|---|---|---|---|---| +| `fill_rect`, the whole buffer | 0.0008 | 3.15 | 0.0059 | 1.79 | +| filled `circle`, radius 40 | 0.0075 | 1.67 | 0.0063 | 1.21 | +| filled `round_rect`, 120 × 80, radius 10 | 0.0029 | 1.77 | 0.0047 | 1.08 | +| `text16`, 20 characters | 0.0037 | 0.68 | 0.0042 | 1.41 | +| `blit`, a 64 × 64 sprite | 0.024 | 3.57 | 0.021 | 4.40 | + +Measured on one laptop (Intel Core Ultra 7 155H, Linux): CPython 3.12.3 with +pydevices-pygraphics 0.1.1, and MicroPython 1.30 preview unix builds, one +with the compiled module built in and one without. Neither pure run had numpy +or ulab to call on. A board is far slower than a laptop; the ratio between +the columns is what carries over. + +The pure package fills `RGB565` rectangles with numpy when it can import it +(ulab's numpy on MicroPython), which makes its `fill_rect` many times faster. + +## Run it yourself + +`tools/benchmark.py` is the script behind the table. It prints which package +it imported, then one line per call. + +```bash +python tools/benchmark.py +``` + +Run it from a directory other than the repository root to get the installed +package. To time the pure package on CPython, put `lib/` first on the path +from inside Python, because the repository root comes before `PYTHONPATH`: + +```bash +cd /tmp && python -c "import sys; sys.path.insert(0, '/path/to/pygraphics/lib'); exec(open('/path/to/pygraphics/tools/benchmark.py').read())" +``` + +On MicroPython, a compiled module built into the firmware wins over any +folder on the path, so time the pure package on a build without it: + +```bash +MICROPYPATH=lib micropython tools/benchmark.py +``` diff --git a/docs/how-it-works/index.md b/docs/how-it-works/index.md new file mode 100644 index 0000000..36d26c4 --- /dev/null +++ b/docs/how-it-works/index.md @@ -0,0 +1,71 @@ +# How it works + +pygraphics is a framebuffer: a block of memory that holds a picture, and +functions that draw into it. It starts from MicroPython's `framebuf` and adds +shapes, text, images, clipping and the changed-rectangle `Area`. It needs +nothing else from PyDevices and no screen, so it is as useful for drawing +off-screen and preparing images as for driving a display. + +```text +your shapes, text and images + | + v + FrameBuffer, Draw and the module functions + | + |-- returns an Area: the rectangle that changed + `-- writes into .buffer: the pixels + | + v + your display driver, a file, or nothing +``` + +## Two packages, one behaviour + +The same `import pygraphics` comes in two forms. + +The compiled package is C. It's what the `pydevices-pygraphics` wheels +contain, and it can be built into MicroPython or CircuitPython firmware. +`pygraphics.implementation()` returns `native_cmod`. + +The pure-Python package is the `lib/pygraphics/` folder, the one mip +installs. It runs on any Python with no compiler, and returns +`pygraphics_python`. It carries `pygraphics.framebuf`, a Python copy of +MicroPython's `framebuf`, so it runs the same on CPython and CircuitPython, +which don't have that module. + +Both pass the same tests, and each change is checked by drawing the same +pictures with both and comparing them byte for byte. The compiled package is +faster, by a lot: [Benchmarks](benchmarks.md) has the numbers. The four names +that only the pure package has are listed under +[Platforms](../platforms.md#names-only-the-pure-python-package-has). + +## Compatible with framebuf + +A `pygraphics.FrameBuffer` takes the same arguments as `framebuf.FrameBuffer`, +uses the same format numbers, and draws the same pixels for the same calls. +A tool in the repository, `tools/compare_framebuf_mp.py`, checks this against +MicroPython's own `framebuf`. Two things differ on purpose: drawing methods +return an `Area` instead of `None`, and [text](../guides/draw-text.md#framebufs-text-looks-different) +uses the romfont faces. + +`Draw` brings the same shapes to any object that has `framebuf`'s basic +methods, such as a display driver, without a framebuffer in between. + +## Where to read the code + +| Path | What's there | +|---|---| +| `lib/pygraphics/` | the pure-Python package; the API reference is generated from it | +| `src/` | the C module: framebuffer, shapes, fonts, files, and the bindings for MicroPython, CircuitPython and CPython | +| `tests/` | one suite for both packages | +| `tools/` | the comparison tools | + +Building and testing your own copy is in +[CONTRIBUTING.md](https://github.com/PyDevices/pygraphics/blob/main/CONTRIBUTING.md). + +## Credits + +`pygraphics.framebuf` and the framebuffer drawing in both packages follow +MicroPython's `extmod/modframebuf.c` by Damien P. George, under the MIT +licence. The built-in fonts come from +[romfont](https://github.com/spacerace/romfont). diff --git a/docs/images/hello.png b/docs/images/hello.png new file mode 100644 index 0000000..66036da Binary files /dev/null and b/docs/images/hello.png differ diff --git a/docs/index.md b/docs/index.md index 97d6d01..94feb86 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,47 +1,12 @@ # pygraphics -```{raw} html -
-

Cross-platform 2D drawing and graphics engine for the PyDevices stack — Area dirty bounding boxes, framebuf-compatible FrameBuffer, embedded romfonts, shapes, and image loaders.

-
- MIP: pygraphics - TestPyPI: pydevices-pygraphics - Native C & Pure-Python Parity - MicroPython · CircuitPython · CPython · Direct WebAssembly · Pyodide -
-
-``` - -::::{grid} 1 1 2 2 -:gutter: 2 - -:::{grid-item-card} Area Bounding Boxes - -Every shape and text primitive returns an `Area(x, y, w, h)` bounding box for partial screen refresh and dirty-rect updates. -::: - -:::{grid-item-card} Embedded Romfonts - -Zero-dependency built-in 8x8, 8x14, and 8x16 romfonts with arbitrary scaling, transparency, and external `.bin` file support. -::: - -:::{grid-item-card} Streaming BMP565 - -Stream and slice massive 16-bit RGB565 bitmaps without loading full images into microcontroller RAM. -::: - -:::{grid-item-card} Hardware Blit Fast-Paths - -Direct SPI/SDL bulk blit dispatch with clipping context managers (`with draw.clip(...)`). -::: +pygraphics is the part of OmniPython that draws: shapes, text and images into +a framebuffer, with the same code on MicroPython-compatible boards, +CircuitPython-compatible boards and CPython. Every drawing call returns the +rectangle it changed, so you can send just that part to a screen. -:::: - ---- - -## 💻 Live Interactive 2D Drawing Demo - -Try editing the drawing primitives below and click **▶ Run** to execute them with direct MicroPython WebAssembly: +Edit the code below and press **Run**. It runs in your browser, on +MicroPython compiled to WebAssembly. ````{raw} html
@@ -50,26 +15,22 @@ Try editing the drawing primitives below and click **▶ Run** to execute them w import pygraphics from displaydev.auto import AutoDisplay -# Initialize display canvas (320x240) display = AutoDisplay(width=320, height=240, canvas_id=CANVAS_ID) buf = bytearray(320 * 240 * 2) fb = pygraphics.FrameBuffer(buf, 320, 240, pygraphics.RGB565) fb.fill(0x1082) -# Draw shapes with Area returns fb.round_rect(20, 20, 120, 70, 10, 0xF800, f=True) fb.circle(220, 55, 35, 0x07E0, f=True) fb.line(20, 110, 300, 110, 0xFFFF) -# Render embedded romfonts pygraphics.text8(fb, "Romfont 8x8", 20, 130, 0xFFE0) pygraphics.text14(fb, "Romfont 8x14", 20, 150, 0x07FF) pygraphics.text16(fb, "Romfont 8x16", 20, 175, 0xFFFF) -# Blit to display and present display.blit_rect(buf, 0, 0, 320, 240) display.show() -print(f"Drawn shapes in {pygraphics.implementation()} backend!") +print("Drawn with", pygraphics.implementation())
@@ -84,94 +45,50 @@ print(f"Drawn shapes in {pygraphics.implementation()} backend!")
```` ---- +[Install it](get-started/install.md), then draw +[your first picture](get-started/first-program.md). The +[guides](guides/draw-shapes.md) take one task each, the +[reference](reference/index) lists every name, and +[How it works](how-it-works/index.md) explains the design. -## 🚀 Quick Install - -| Distribution | Channel | Role | -|:---|:---|:---| -| **`pydevices-pygraphics`** | TestPyPI | Native C-extension wheel for desktop, Android, and CPython (`native_cmod`) | -| **`pygraphics`** | MIP | Pure-Python package for microcontrollers and builds without a C compiler (`pygraphics_python`) | - -```bash -# CPython / Desktop -pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-pygraphics -``` +```{toctree} +:hidden: +:caption: Get started -```python -# MicroPython -import mip -mip.install("pygraphics", index="https://PyDevices.github.io/mip") +get-started/install +get-started/first-program ``` ---- - -## 🎮 Featured Browser Demos - -Explore complete applications and arcade games built with `pygraphics`: - -::::{grid} 1 1 2 2 -:gutter: 2 - -:::{grid-item-card} Bouncing Balls -:link: https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=bouncing_balls&deps=pydevices-pygraphics -:link-type: url - -High-framerate physics simulation with vector circles and bounding-box collision detection. - -**Launch Live Demo** -::: - -:::{grid-item-card} Alien Arcade -:link: https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?manifests=alien&deps=pydevices-palettes,pydevices-pygraphics -:link-type: url - -Retro Space Invaders-style sprite engine with transparency and animated framebuffers. - -**Launch Live Demo** -::: - -:::{grid-item-card} Dino Runner -:link: https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=dino&deps=pydevices-pygraphics -:link-type: url - -Classic obstacle jumping arcade running with dirty scanline refresh and physics. - -**Launch Live Demo** -::: - -:::{grid-item-card} Testris Arcade -:link: https://pydevices.github.io/pydevices-examples/pyscript/pyodide.html?modules=testris&deps=pydevices-pygraphics -:link-type: url - -Full-featured falling-blocks game with matrix rotation, score tracking, and ghost pieces. +```{toctree} +:hidden: +:caption: Guides -**Launch Live Demo** -::: +guides/draw-shapes +guides/update-part-of-a-screen +guides/draw-text +guides/load-and-save-images +``` -:::: +```{toctree} +:hidden: +:caption: Reference ---- +reference/index +platforms +formats +``` -## 📚 Documentation Map +```{toctree} +:hidden: +:caption: How it works -* 🚀 [**Getting started**](getting-started.md) — Core drawing workflows, dirty rectangles, and canvases. -* 📦 [**Installation**](installation.md) — MIP, TestPyPI, wheel building, and platform matrix. -* 🎨 [**Graphics guide**](graphics-guide.md) — FrameBuffer vs Draw, clipping stacks, fonts, and blit fast-paths. -* 🖼️ [**Graphics files**](graphics-files.md) — BMP565, PBM, PGM image loaders and streaming. -* ⚡ [**Benchmarks**](benchmarks.md) — Native C extension vs Pure-Python performance metrics. -* 📚 [**API Reference**](reference/index.rst) — Full autogenerated API documentation. +how-it-works/index +how-it-works/benchmarks +``` ```{toctree} :hidden: +:caption: Changelog -newcomers -getting-started -installation -graphics-guide -graphics-files -api-overview -benchmarks -building-wheels -reference/index +changelog ``` diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index b32bcd1..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,85 +0,0 @@ -# Installation - -## Platform matrix - -Tiers are the org's -[platform support tiers](https://github.com/PyDevices/.github/blob/main/docs/platform-support-tiers.md). - -| Platform | Arch | Tier | Path | -|----------|------|------|------| -| MicroPython / CircuitPython (any MCU or unix port) | any | CI-proven (unix port) | MIP / pure-Python copy — see below | -| CPython — Linux | manylinux x86_64 | CI-proven | Native wheel (TestPyPI) | -| CPython — Windows | AMD64 | CI-proven | Native wheel (TestPyPI) | -| CPython — Android | arm64_v8a, x86_64 | community-verified | Native wheel (TestPyPI); builds, never run on a device here | -| Pyodide / browser | wasm32 | community-verified | Native wheel (TestPyPI); `pyemscripten_2025_0` for cp313, `pyemscripten_2026_0` for cp314 | -| CPython — macOS | x86_64, arm64 | — | No wheel: no Mac on the bench — use MIP / pure-Python | -| CPython — Linux aarch64 | aarch64 | — | No wheel: no aarch64 runner — use MIP / pure-Python | - -Current releases go to TestPyPI; install from there. Production PyPI holds one -older release (0.0.37, against TestPyPI's 0.0.38), parked to reserve the name, -so a plain `pip install pydevices-pygraphics` succeeds and gives the older wheel. - -The public API is the same on both builds **except** for four authoring entry -points — `export_framebuffer`, and `FrameBuffer.export` / `from_bitmap` / -`from_module` — which are pure-Python only. See -[what the native build does not have](https://github.com/PyDevices/pygraphics#what-the-native-build-does-not-have). - -## MicroPython (MIP) - -```python -import mip -mip.install("pygraphics", index="https://PyDevices.github.io/mip") -``` - -The package path on mip is `micropython/pygraphics/` (import name -`pygraphics`). Older installs that used MIP name `graphics` should reinstall -under the new name — the cutover publish already went out. - -## CircuitPython / copy install - -Copy the `pygraphics/` package folder onto `sys.path` (from -`lib/pygraphics/` in this repo, or from mip). - -To build the **native** module into CircuitPython, hand this repository to the -port's `make` as a user C module: - -```bash -make -C ports/unix -j VARIANT=coverage USER_C_MODULES=/path/to/pygraphics -``` - -That works on CircuitPython 11 and on boards as well as unix; the -[repository README](https://github.com/PyDevices/pygraphics#circuitpython) has -the board command and what was tested. The MicroPython recipe is tested against -v1.28.0 and current master. - -## CPython — native/C extension (preferred when available) - -```bash -pip install \ - -i https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ \ - pydevices-pygraphics -``` - -## Pyodide / browser (WASM) - -Each TestPyPI release includes wasm wheels — `pyemscripten_2025_0_wasm32` for -cp313 and `pyemscripten_2026_0_wasm32` for cp314 (same semver). micropip -selects the one matching your runtime automatically: - -```python -import micropip -await micropip.install("pydevices-pygraphics", index_urls="https://test.pypi.org/simple/") -``` - -## Name cutover - -| Role | Current | Retired (do not use) | -|------|---------|----------------------| -| Import | `pygraphics` | `graphics` | -| Pure-Python pip | Not published; use MIP | `pygraphics` | -| Native/C pip | `pydevices-pygraphics` | `pygraphics`, `graphics-cmod` | -| MIP | `pygraphics` | `graphics` | - -TestPyPI may still list the old project names until they age out; install the -**current** names above. diff --git a/docs/newcomers.md b/docs/newcomers.md deleted file mode 100644 index 38addea..0000000 --- a/docs/newcomers.md +++ /dev/null @@ -1,88 +0,0 @@ -# Newcomer's guide to pygraphics - -`pygraphics` is a portable 2D framebuffer library for MicroPython, -CircuitPython, and CPython. It extends the familiar `framebuf` model with -shapes, fonts, image helpers, clipping, transparent blits, gradients, and -dirty rectangles. It has no required PyDevices hardware dependency, so it is -also useful for off-screen rendering and asset preparation. - -The same `import pygraphics` API has two implementations: - -- The native `pydevices-pygraphics` TestPyPI wheel is preferred on supported - CPython, Android, and Pyodide targets. -- The MIP `pygraphics` package is the pure-Python fallback for MicroPython and - CircuitPython or any environment without a native build. - -## Start by drawing off-screen - -The README shows [how to install](https://github.com/PyDevices/pygraphics#install) -either implementation and a [quick start](https://github.com/PyDevices/pygraphics#quick-start) -that draws into an off-screen framebuffer. - -Drawing calls return an `Area` covering the pixels they changed. A display -driver can use that rectangle to flush only the affected part of a screen; -off-screen callers can ignore it. The practical drawing patterns are covered -by [the graphics guide](graphics-guide.md) and -[the getting-started guide](getting-started.md). - -## The mental model - -```text -your pixels / shapes / text - | - v - FrameBuffer and canvas helpers - | - |-- Area: changed rectangle - |-- buffer: pixel storage - `-- optional display driver flush - - native C module <--> pure-Python fallback - same public drawing surface -``` - -You can call operations as `FrameBuffer` methods, standalone functions that -take a framebuffer, or through the `Draw` styling context. The public surface -includes standard `framebuf` operations plus primitives such as circles, -polygons, rounded rectangles, arcs, gradients, text, and transparent blits. - -## Repository map - -| Path | Purpose | -|---|---| -| `src/` | Native C framebuffer, shape, font, file, and CPython-extension implementation. | -| `lib/pygraphics/` | Pure-Python implementation and the source of truth for generated Python API pages. | -| `micropython.mk`, `micropython.cmake` | Build glue for native firmware targets (`micropython.mk` serves CircuitPython too). | -| `setup.py`, `pyproject.toml` | CPython native wheel build metadata. | -| `docs/` | Installation, usage, image-file, benchmark, and API documentation. | -| `tests/` | Native smoke tests and native/pure-Python parity coverage. | -| `tools/` | Comparison, benchmark, and developer helpers. | - -## Native and pure-Python boundaries - -Both implementations expose the normal drawing API, and the test suite checks -their parity. Four authoring-time helpers deliberately exist only in pure -Python: `export_framebuffer`, `FrameBuffer.export`, -`FrameBuffer.from_bitmap`, and `FrameBuffer.from_module`. They write or load -Python bitmap modules, which is useful asset work but not useful native -acceleration. Keep that distinction when writing portable applications. - -The native wheel has platform-specific availability. Read -[installation](installation.md) for the current matrix and use the MIP package -when a native wheel is not available. A plain PyPI install can select an older -release, so prefer the documented TestPyPI command when testing current work. - -## Contributor boundary - -Normal users install a wheel or MIP package; building C is contributor work. -Native changes live under `src/`; pure-Python changes live under -`lib/pygraphics/`. Their behavior must stay aligned. The native and -pure-Python smoke commands are in [AGENTS.md](https://github.com/PyDevices/pygraphics/blob/main/AGENTS.md), and the parity -tools compare both implementations byte-for-byte. - -After a font-source change, run `python3 scripts/sync_fonts.py`; the Python -font files are the source of truth for generated C headers. Read -[the wheel-build guide](building-wheels.md) for packaging and -[the API overview](api-overview.md) before changing public behavior. A safe -first contribution is a focused test or documentation/example improvement -that demonstrates an existing framebuffer operation. diff --git a/docs/platforms.md b/docs/platforms.md new file mode 100644 index 0000000..6f988f2 --- /dev/null +++ b/docs/platforms.md @@ -0,0 +1,48 @@ +# Platforms + +## Where each package runs + +| Where | Package | How it's tested | +|---|---|---| +| CPython 3.11–3.14 on Linux x86_64 (manylinux_2_28) | compiled wheel, `pydevices-pygraphics` | the full tests run on every change, against both packages | +| CPython 3.11–3.14 on Windows x86_64 | compiled wheel | each release checks the wheel loads its compiled module | +| CPython 3.13–3.14 on Android, arm64-v8a and x86_64 | compiled wheel | the wheel builds; it isn't run on a device | +| Pyodide, CPython 3.13 and 3.14 | compiled wheel (`pyemscripten_2025_0` and `pyemscripten_2026_0`) | the wheel builds; micropip picks the one for your Pyodide | +| MicroPython, any port | pure-Python, from mip | the compiled module's tests and the comparison against `framebuf` run on the unix port on every change | +| CircuitPython | pure-Python, copied to `CIRCUITPY/lib/` | not tested automatically | +| macOS, Linux on ARM | pure-Python | no compiled wheel: copy `lib/pygraphics/` onto your path | + +The compiled module can also be built into MicroPython or CircuitPython +firmware; [CONTRIBUTING.md](https://github.com/PyDevices/pygraphics/blob/main/CONTRIBUTING.md#build-it-into-micropython) +has the steps. `pygraphics.implementation()` tells you which one you have: +`native_cmod` for the compiled module, `pygraphics_python` for the pure one. + +## Which index + +Releases go to TestPyPI, which is why the install command names it. PyPI +holds one old release, 0.0.37, kept to reserve the name, so +`pip install pydevices-pygraphics` without the TestPyPI index succeeds and +gives you that old version. + +## Names only the pure-Python package has + +Everything in `pygraphics.__all__` is in both packages, except four names +that write or read Python source. The compiled package leaves them out: + +| Name | What it does | +|---|---| +| `pygraphics.export_framebuffer(fb, path)` | writes a framebuffer as an importable `.py` module | +| `FrameBuffer.export(path)` | the same, as a method | +| `FrameBuffer.from_bitmap(buf, w, h, fmt)` | makes a framebuffer from such a module's `BITMAP` | +| `FrameBuffer.from_module(mod)` | the same, from the imported module | + +## Old names + +If you installed pygraphics under an earlier name, uninstall that and install +the current one. + +| | Now | Earlier | +|---|---|---| +| Import | `pygraphics` | `graphics` | +| pip (compiled) | `pydevices-pygraphics` | `pygraphics`, `graphics-cmod` | +| mip | `pygraphics` | `graphics` | diff --git a/lib/pygraphics/README.md b/lib/pygraphics/README.md index de22495..98a9836 100644 --- a/lib/pygraphics/README.md +++ b/lib/pygraphics/README.md @@ -1,51 +1,30 @@ # pygraphics -Pure-Python `pygraphics` package — `FrameBuffer`, `Draw`, fonts, shapes, and image -loaders. Import as `pygraphics`. +This folder is the pure-Python pygraphics package: `FrameBuffer`, `Draw`, +shapes, fonts and image files, with the same API as the compiled module. +Import it as `pygraphics`. -> **Pip name:** `pydevices-pygraphics` · **Import:** `import pygraphics` - -On desktop/Android when a native wheel is available, prefer the -TestPyPI `pydevices-pygraphics` wheel (import name `pygraphics`, C implementation). The -pure-Python package is published through MIP package index. - -## Install - -### CPython (TestPyPI) - -```bash -pip install \ - -i https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ \ - pygraphics -``` - -### MicroPython (MIP) +On a MicroPython-compatible board, install it with mip: ```python import mip mip.install("pygraphics", index="https://PyDevices.github.io/mip") ``` -## Quick start +On a CircuitPython-compatible board, copy this folder to `CIRCUITPY/lib/`. +On CPython, install the compiled package instead: + +```bash +pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-pygraphics +``` ```python import pygraphics -fb = pygraphics.FrameBuffer( - bytearray(160 * 128 * 2), 160, 128, pygraphics.RGB565 -) +fb = pygraphics.FrameBuffer(bytearray(160 * 128 * 2), 160, 128, pygraphics.RGB565) fb.fill(0) -fb.fill_rect(10, 10, 40, 40, 0xF800) -print(pygraphics.implementation()) # pygraphics_python +print(fb.fill_rect(10, 10, 40, 40, 0xF800)) # Area(10, 10, 40, 40) +print(pygraphics.implementation()) # pygraphics_python ``` -## Links - -- [API reference (native)](https://pydevices.github.io/pygraphics/api/) -- [Source](https://github.com/PyDevices/pygraphics) -- Related: [pydevices-examples](https://github.com/PyDevices/pydevices-examples) - -## License - -MIT — see [LICENSE](https://github.com/PyDevices/pygraphics/blob/main/LICENSE). +[Docs](https://pygraphics.readthedocs.io) · [Source](https://github.com/PyDevices/pygraphics) diff --git a/tests/README.md b/tests/README.md index 68b3e8f..5df6df0 100644 --- a/tests/README.md +++ b/tests/README.md @@ -22,6 +22,4 @@ probes skipped); the native build runs 85 (2 skipped — `test_clip` and `test_blit_hooks` import pure-Python-only submodules, and a native build is one extension module, not a package). -CI runs both, so a divergence between them fails the build. Eleven such -divergences existed as of August 2026 and were invisible because the suite could -not run; see the Phase 10 notes in the consolidation plan. +CI runs both, so a divergence between them fails the build. diff --git a/tests/test_parity.py b/tests/test_parity.py index 1a303de..f9b46a9 100644 --- a/tests/test_parity.py +++ b/tests/test_parity.py @@ -17,7 +17,8 @@ ``PURE_ONLY`` below is the recorded exclusion: names that are pure-Python by design. Both directions are enforced, so implementing one in C fails this test -until the README and docs/graphics-files.md stop calling it pure-only. +until docs/platforms.md and docs/guides/load-and-save-images.md stop calling +it pure-only. """ import _env # noqa: F401 @@ -78,7 +79,7 @@ def _pure_all(): "native module is missing public names: " + ", ".join(missing) + " -- implement them, or add them to PURE_ONLY_MODULE and record the " - "exclusion in README.md and docs/graphics-files.md" + "exclusion in docs/platforms.md" ) # The other direction: an exclusion that stops being true has to be noticed. @@ -91,7 +92,7 @@ def _pure_all(): "these are implemented in C now: " + ", ".join(unexpected) + " -- drop them from PURE_ONLY_* here and from the 'pure-Python only' " - "notes in README.md and docs/graphics-files.md" + "notes in docs/platforms.md and docs/guides/load-and-save-images.md" ) # Named in __all__ but absent from the pure package would mean __all__ itself lies. diff --git a/tools/benchmark.py b/tools/benchmark.py new file mode 100644 index 0000000..7597415 --- /dev/null +++ b/tools/benchmark.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Time a few common drawing calls. Runs on CPython and MicroPython. + +Prints which pygraphics it imported, then one line per call: +``name|milliseconds per call``. docs/how-it-works/benchmarks.md shows how to +run it against each package. +""" + +import time + +import pygraphics +from pygraphics import RGB565, FrameBuffer + +W, H = 160, 128 +fb = FrameBuffer(bytearray(W * H * 2), W, H, RGB565) +sprite = FrameBuffer(bytearray(64 * 64 * 2), 64, 64, RGB565) +sprite.fill(0xF800) + +cases = [ + ("fill_rect, 160 x 128", lambda: fb.fill_rect(0, 0, W, H, 0x001F)), + ("filled circle, r = 40", lambda: fb.circle(80, 64, 40, 0x07E0, f=True)), + ("filled round_rect, 120 x 80, r = 10", lambda: fb.round_rect(20, 24, 120, 80, 10, 0xF800, f=True)), + ("text16, 20 characters", lambda: pygraphics.text16(fb, "abcdefghijklmnopqrst", 0, 50, 0xFFFF)), + ("blit, 64 x 64 sprite", lambda: fb.blit(sprite, 40, 30)), +] + +try: + clock = time.perf_counter +except AttributeError: + + def clock(): + return time.ticks_us() / 1e6 + + +print(pygraphics.implementation()) +for name, fn in cases: + n = 0 + t0 = clock() + while clock() - t0 < 0.5: + fn() + n += 1 + print("%s|%.4f" % (name, (clock() - t0) / n * 1000))