From ce927ab5d5639e83914c024dd3cc79db3d1159ee Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:06:33 -0500 Subject: [PATCH 1/2] Docs: Sphinx on the shared pydevices-docs package, in place of MkDocs MyST Markdown, autodoc with Google-style docstrings, the shared top bar and theme, and the live demos carried across. A docs workflow builds the site with sphinx-build -W, as Read the Docs does. Docstring-only edits so the reference builds without warnings. --- .github/workflows/docs.yml | 36 +++ .gitignore | 3 +- .readthedocs.yaml | 5 +- AGENTS.md | 4 +- docs/README.md | 6 +- docs/api-overview.md | 2 + docs/assets/pyscript/pydevices-runner.js | 4 - docs/building-wheels.md | 2 +- docs/conf.py | 17 ++ docs/getting-started.md | 56 +++-- docs/graphics-guide.md | 16 +- docs/index.md | 135 +++++++---- docs/requirements.txt | 11 +- docs/stylesheets/extra.css | 295 ----------------------- docs/summary.md | 10 - lib/pygraphics/__init__.py | 2 +- lib/pygraphics/_font.py | 4 +- lib/pygraphics/_framebuf_plus.py | 6 +- lib/pygraphics/framebuf.py | 5 +- mkdocs.yml | 121 ---------- scripts/bundle_pyscript_assets.py | 2 +- scripts/mkdocs_gen_ref_pages.py | 37 --- tools/sourcey-api/README.md | 4 +- 23 files changed, 212 insertions(+), 571 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 docs/assets/pyscript/pydevices-runner.js create mode 100644 docs/conf.py delete mode 100644 docs/stylesheets/extra.css delete mode 100644 docs/summary.md delete mode 100644 mkdocs.yml delete mode 100644 scripts/mkdocs_gen_ref_pages.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..bf70e27 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,36 @@ +# The docs site as Read the Docs builds it: from the repository root, with +# docs/requirements.txt, and every warning a failure. +name: Docs + +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'lib/**' + - '.readthedocs.yaml' + - '.github/workflows/docs.yml' + pull_request: + paths: + - 'docs/**' + - 'lib/**' + - '.readthedocs.yaml' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + docs: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.13' + - name: Install docs requirements + run: pip install -r docs/requirements.txt + - name: Build docs + run: sphinx-build -W --keep-going -b html docs _site diff --git a/.gitignore b/.gitignore index 341f756..45ab446 100644 --- a/.gitignore +++ b/.gitignore @@ -27,5 +27,6 @@ tools/sourcey-api/node_modules/ tools/sourcey-api/dist/ # Generated API reference: regenerate with tools/sourcey-api/scripts/generate_reference.py tools/sourcey-api/generated/ -site/ +docs/_build/ +docs/reference/ .venv-docs/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 4b81448..6a17e5f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,8 +5,9 @@ build: tools: python: "3.13" -mkdocs: - configuration: mkdocs.yml +sphinx: + configuration: docs/conf.py + fail_on_warning: true python: install: diff --git a/AGENTS.md b/AGENTS.md index c5643e3..93154cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,9 +59,9 @@ Font C headers: `python3 scripts/sync_fonts.py` (source of truth is ## Documentation -- MkDocs under `docs/` → https://pygraphics.readthedocs.io (`.readthedocs.yaml`) +- Sphinx under `docs/` (MyST Markdown, `docs/conf.py`) → https://pygraphics.readthedocs.io (`.readthedocs.yaml`) - Sourcey native-C API generator under `tools/sourcey-api/`; output is gitignored and unpublished (regenerate with `scripts/generate_reference.py`) -- Local: `python3 -m venv .venv-docs && .venv-docs/bin/pip install -r docs/requirements.txt && .venv-docs/bin/mkdocs serve` +- Local: `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` - Use practical examples from `pydevices-examples` and emphasize off-screen rendering, dirty-rectangle updates, hit testing, sprite blits, gradients, and text drawing. diff --git a/docs/README.md b/docs/README.md index 725cf80..946588c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,15 +1,15 @@ # Documentation -Hand-authored MkDocs pages live in this directory and publish to +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/mkdocs serve # or: mkdocs build +.venv-docs/bin/sphinx-build -W --keep-going -b html docs docs/_build/html ``` -A source-linked reference for the **native C bindings** — which mkdocstrings +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/): diff --git a/docs/api-overview.md b/docs/api-overview.md index fcd01e8..7427ad6 100644 --- a/docs/api-overview.md +++ b/docs/api-overview.md @@ -1,3 +1,5 @@ +# 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. diff --git a/docs/assets/pyscript/pydevices-runner.js b/docs/assets/pyscript/pydevices-runner.js deleted file mode 100644 index 70fbcf2..0000000 --- a/docs/assets/pyscript/pydevices-runner.js +++ /dev/null @@ -1,4 +0,0 @@ -/** Load the first-party direct MicroPython documentation host. */ -import("https://PyDevices.github.io/assets/chrome/docs-runtime.js").catch((error) => { - console.error("Unable to load the PyDevices documentation runtime:", error); -}); diff --git a/docs/building-wheels.md b/docs/building-wheels.md index 492639a..31def40 100644 --- a/docs/building-wheels.md +++ b/docs/building-wheels.md @@ -47,7 +47,7 @@ pydevices_pygraphics--cp314-cp314-pyemscripten_2026_0_wasm32.whl ## Documentation - **Read the Docs** (guides + pure-Python API): https://pygraphics.readthedocs.io - Config: `.readthedocs.yaml` + `mkdocs.yml`. Connected via the org + 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 diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..eed7eeb --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,17 @@ +"""Sphinx configuration for the pygraphics documentation. + +Everything shared by the PyDevices documentation projects (theme, top bar, +Markdown and docstring setup, the reference map) comes from pydevices-docs. +""" + +from pydevices_docs import configure + +globals().update(configure( + "pygraphics", __file__, + repo="PyDevices/pygraphics", edit_path="docs", + source_dirs=["../lib"], api_packages=["pygraphics"], + # The public names live in private modules and are re-exported by + # pygraphics/__init__.py's __all__, which is what the reference documents. + use_module_all=True, + version_file="../VERSION", +)) diff --git a/docs/getting-started.md b/docs/getting-started.md index 791fad7..a540c41 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -2,38 +2,46 @@ ## Setup -=== "MicroPython (MIP)" +::::{tab-set} - ```python - import mip - mip.install("pygraphics", index="https://PyDevices.github.io/mip") - ``` +:::{tab-item} MicroPython (MIP) -=== "CPython (TestPyPI)" +```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 +``` +::: - ```bash - pip install -i https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ pydevices-pygraphics - ``` +:::{tab-item} CircuitPython -=== "CircuitPython" +Copy `lib/pygraphics/` to your board's `CIRCUITPY/lib/` directory. +::: - Copy `lib/pygraphics/` to your board's `CIRCUITPY/lib/` directory. +:::{tab-item} PyScript / Pyodide -=== "PyScript / Pyodide" +```python +import micropip +await micropip.install( + "pydevices-pygraphics", index_urls="https://test.pypi.org/simple/" +) - ```python - import micropip - await micropip.install( - "pydevices-pygraphics", index_urls="https://test.pypi.org/simple/" - ) +import pygraphics +``` - 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")`). +::: - 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")`). +:::: --- @@ -41,6 +49,7 @@ Experiment with 2D shapes, colors, and font rendering live in your browser: +````{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 0000000000000000000000000000000000000000..66036da12f92165d7568b9d5028757816a192fe7 GIT binary patch literal 979 zcmeAS@N?(olHy`uVBq!ia0y~yV0-{%f8byOlHuYCZy6YvXL-6fhE&{od-rVKY6Ahb zfR0c9*1uUdYfGq$;g9l~bE~?$?n(SGF}^+_ml>!DFKF1l_x*hTa}C~IlKGa)cXqa` zG6-B?=()8*B4fvij{4=6_q8$@796VKKH6;Wr}@y?eSD%O4GnA$jFkB#maTmLc$1C|R9lfmp~HKzd6T)f-dp3$ z%B!$|5okS=6wp;0Bz9XY(4B4mPXDgaZA-8-fT9ou-5a}^XKkold@RZB{<(!<2LYYH z$P2Uss2#{;F*%T9vx3RA(r)S5Pi(bGt|&%B3_;fc)C_bpSTlqPRK{WgbPQ1R!pX}* z4z9ny^Ph~m5)&s15`{Yk#*}Dc4_(0NzvfDW52}KNH;yvq-aJ=W{}Sj6B=e7?h_ja@ zS-Zb;S_$^HjNzk(#HfnRef_5wLBg*~kiF;n74_~0F0do^&;1~OF(du1pB^hXJQrB* zwn%6@X7BpHVFlQ&%;pl?iWjU$#GA+5wn+A0x0dD`sKBkHZ2Z93=7_L7O@5IVPR;#F0IjOvQ4ozh1wE8U8*&Z)^J6CSKeA`@3m%lL(j~ldg zDfg|p-kZkD&HxF*y4ep(7n>O{0Mjolih!ny8GN69EN$-tCI?nfQs`KH!{NcL*CzaR vJAat>&#_Ese*D8t6etH&isBEVI14)U^Y50}?+pIP3sUdt>gTe~DWM4fn{Pc7 literal 0 HcmV?d00001 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))