From 4b3082f79fbb09bab3f9d0f57bb52e372393c03a Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:19:25 -0500 Subject: [PATCH 1/3] jpegio moves here from displayif (media modules roadmap, Phase 1) modules/jpegio is displayif's src/jpegio and tests/jpegio, unchanged in behaviour: CircuitPython's JpegDecoder API on the vendored TJpgDec, and LVGL's JPEG decoder when lvgl-micropython is in the build. Its own glue (manifest.py, micropython.cmake/.mk) replaces displayif's; displayif's build.h and __revision__ stamp are gone (the build record has the commit). A firmware that wants JPEG now names jpegio; all includes it. .github/workflows/jpegio.yml builds it from a bare clone with build_mp.py and runs its golden corpus with and without LVGL, plus the one-TJpgDec check (these moved from displayif's clean-build workflow). exercise_build.sh's Picos name jpegio, as displayif gave it them before. Locally: unix --modules jpegio passes the corpus (7 frames, 28 digests); unix with jpegio,lvgl-micropython passes it beside LVGL and puts 3 frames through lv.image exactly, with exactly one jd_prepare in the firmware. --- .github/workflows/jpegio.yml | 87 ++ .gitignore | 1 + README.md | 3 +- docs/build-plan.md | 6 +- docs/newcomers.md | 2 +- modules/jpegio/README.md | 342 +++++ modules/jpegio/manifest.py | 5 + modules/jpegio/micropython.cmake | 38 + modules/jpegio/micropython.mk | 40 + modules/jpegio/src/jpegio.c | 541 ++++++++ modules/jpegio/src/lvgl_decoder.c | 304 +++++ modules/jpegio/src/lvgl_decoder.h | 27 + modules/jpegio/src/tjpgd/tjpgd.c | 1157 +++++++++++++++++ modules/jpegio/src/tjpgd/tjpgd.h | 123 ++ modules/jpegio/src/tjpgd/tjpgdcnf.h | 32 + modules/jpegio/tests/frames/README.md | 17 + .../tests/frames/baseline_jfif_320x240.jpg | Bin 0 -> 11172 bytes .../tests/frames/baseline_jfif_64x48.jpg | Bin 0 -> 1540 bytes .../jpegio/tests/frames/c920e_320x240_dri.jpg | Bin 0 -> 8064 bytes .../frames/dri_first_mjpeg_style_320x240.jpg | Bin 0 -> 11348 bytes modules/jpegio/tests/frames/golden_tjpgd.json | 54 + .../jpegio/tests/frames/grayscale_64x48.jpg | Bin 0 -> 960 bytes modules/jpegio/tests/frames/nodht_320x240.jpg | Bin 0 -> 10740 bytes .../jpegio/tests/frames/odd_size_37x29.jpg | Bin 0 -> 1427 bytes .../tests/frames/progressive_320x240.jpg | Bin 0 -> 6074 bytes modules/jpegio/tests/frames/reference.json | 603 +++++++++ .../tests/frames/restart_dri_320x240.jpg | Bin 0 -> 11348 bytes modules/jpegio/tests/jpegwalk.py | 198 +++ modules/jpegio/tests/make_corpus.py | 244 ++++ modules/jpegio/tests/reference.py | 170 +++ modules/jpegio/tests/test_jpegio.py | 580 +++++++++ modules/jpegio/tests/test_jpegio_lvgl.py | 416 ++++++ scripts/exercise_build.sh | 4 +- variants/windows/pydevices/mpconfigvariant.h | 2 +- 34 files changed, 4989 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/jpegio.yml create mode 100644 modules/jpegio/README.md create mode 100644 modules/jpegio/manifest.py create mode 100644 modules/jpegio/micropython.cmake create mode 100644 modules/jpegio/micropython.mk create mode 100644 modules/jpegio/src/jpegio.c create mode 100644 modules/jpegio/src/lvgl_decoder.c create mode 100644 modules/jpegio/src/lvgl_decoder.h create mode 100644 modules/jpegio/src/tjpgd/tjpgd.c create mode 100644 modules/jpegio/src/tjpgd/tjpgd.h create mode 100644 modules/jpegio/src/tjpgd/tjpgdcnf.h create mode 100644 modules/jpegio/tests/frames/README.md create mode 100644 modules/jpegio/tests/frames/baseline_jfif_320x240.jpg create mode 100644 modules/jpegio/tests/frames/baseline_jfif_64x48.jpg create mode 100644 modules/jpegio/tests/frames/c920e_320x240_dri.jpg create mode 100644 modules/jpegio/tests/frames/dri_first_mjpeg_style_320x240.jpg create mode 100644 modules/jpegio/tests/frames/golden_tjpgd.json create mode 100644 modules/jpegio/tests/frames/grayscale_64x48.jpg create mode 100644 modules/jpegio/tests/frames/nodht_320x240.jpg create mode 100644 modules/jpegio/tests/frames/odd_size_37x29.jpg create mode 100644 modules/jpegio/tests/frames/progressive_320x240.jpg create mode 100644 modules/jpegio/tests/frames/reference.json create mode 100644 modules/jpegio/tests/frames/restart_dri_320x240.jpg create mode 100644 modules/jpegio/tests/jpegwalk.py create mode 100644 modules/jpegio/tests/make_corpus.py create mode 100644 modules/jpegio/tests/reference.py create mode 100644 modules/jpegio/tests/test_jpegio.py create mode 100644 modules/jpegio/tests/test_jpegio_lvgl.py diff --git a/.github/workflows/jpegio.yml b/.github/workflows/jpegio.yml new file mode 100644 index 00000000..6bcfaca9 --- /dev/null +++ b/.github/workflows/jpegio.yml @@ -0,0 +1,87 @@ +# jpegio from a bare clone, the way a user builds it: build_mp.py fetches +# MicroPython and the pinned modules itself. Its golden corpus without LVGL, +# then beside lvgl-micropython as LVGL's JPEG decoder. (These checks moved +# here with jpegio from displayif's clean-build workflow, 2026-10-06.) +name: jpegio + +on: + push: + branches: [main] + paths: + - 'modules/jpegio/**' + - 'build_mp.py' + - 'modules.lock' + - 'patches/**' + - '.github/workflows/jpegio.yml' + pull_request: + paths: + - 'modules/jpegio/**' + - 'build_mp.py' + - 'modules.lock' + - 'patches/**' + - '.github/workflows/jpegio.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + unix: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v7 + + - name: Install the unix port's build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libffi-dev pkg-config + + - name: Build the unix port with jpegio + run: python3 build_mp.py --port unix --modules jpegio + + - name: The golden corpus (no Pillow needed) + run: ./builds/unix/standard/micropython modules/jpegio/tests/test_jpegio.py + + - name: The LVGL-less contract (must print SKIP) + # register_lvgl_decoder() and lvgl_decoders() are module attributes on + # every build, the first raising RuntimeError here, the second + # returning () (modules/jpegio/README.md). + run: | + ./builds/unix/standard/micropython modules/jpegio/tests/test_jpegio_lvgl.py | tee lvgl.out + grep -q 'register_lvgl_decoder() -> RuntimeError' lvgl.out + grep -q 'lvgl_decoders() == ()' lvgl.out + grep -q '^SKIP' lvgl.out + + unix-lvgl: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + + - name: Install the unix port's build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libffi-dev pkg-config + + - name: Build the unix port with jpegio and lvgl-micropython + run: python3 build_mp.py --port unix --modules jpegio,lvgl-micropython + + - name: One TJpgDec in the firmware, and it is jpegio's + # LVGL's own tjpgd.o compiles empty (LV_USE_TJPGD 0 in lvgl-bindings), + # so exactly one object defines jd_prepare, and jpegio's two objects + # call into it. + working-directory: builds/unix/standard + run: | + find . -name '*.o' -exec nm -A {} + 2>/dev/null | grep ' T jd_prepare$' > jd.txt + cat jd.txt + test "$(wc -l < jd.txt)" = 1 + grep -q '^./jpegio/src/tjpgd/tjpgd.o:' jd.txt + nm jpegio/src/jpegio.o | grep -q ' U jd_prepare$' + nm jpegio/src/lvgl_decoder.o | grep -q ' U jd_decomp$' + + - name: The golden corpus, unchanged beside LVGL + run: ./builds/unix/standard/micropython modules/jpegio/tests/test_jpegio.py + + - name: JPEGs through lv.image + run: ./builds/unix/standard/micropython modules/jpegio/tests/test_jpegio_lvgl.py diff --git a/.gitignore b/.gitignore index f3db3148..d0fbc351 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ !/modules/manifest.py !/modules/all/ !/modules/castif/ +!/modules/jpegio/ !/modules/tflite/ !/modules/vision/ !/modules/.esp-vision/ diff --git a/README.md b/README.md index fe6766bc..a37902e8 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ New here? The [newcomer's guide](docs/newcomers.md) explains the model; the fetches when a module or a toolchain isn't there. In the PyDevices workspace both are links to the sibling checkouts instead. - `modules/` — `manifest.py` (the manifest every build freezes), `all/` - (every module), `castif/` (the ESP32-P4 cast, which lives only here), + (every module), `castif/` (the ESP32-P4 cast) and `jpegio/` (the JPEG + decoder, from displayif), which live only here, `tflite/` and `vision/` (esp-vision's modules, from the pin in `.esp-vision/`; see [docs/esp-vision.md](docs/esp-vision.md)), and the modules themselves, linked or cloned. diff --git a/docs/build-plan.md b/docs/build-plan.md index a91841ad..3db738bb 100644 --- a/docs/build-plan.md +++ b/docs/build-plan.md @@ -36,6 +36,7 @@ micropython-pydevices/ │ ├── manifest.py tracked: reads the module list build_mp.py sets, includes each, raises on a missing one │ ├── all/manifest.py tracked: every sibling with a root manifest.py, except opt-in ones │ ├── castif/ tracked: lives only here (moves from usermods/); ESP32-P4 only +│ ├── jpegio/ tracked: lives only here (moved from displayif, 2026-10-06); every port │ ├── audiodsp -> ../../audiodsp ignored symlinks in the workspace, │ ├── audioif -> ../../audioif clones at the modules.lock commit anywhere else │ ├── audiocomponents, cameraif, displayif, lvgl-micropython, palettes, @@ -242,8 +243,9 @@ Deferred, because the build script doesn't need them: - **A CI leg that builds the union** on one desktop port, so building only what you ask for doesn't lose the cross-port check that caught usbif's breaks in September. -- **jpegio** out of displayif into its own repo, and whether esp-vision's - sensor and H.264 replace cameraif (and castif's encoder). esp-vision's +- **jpegio** left displayif on 2026-10-06 for `modules/jpegio` here (the + anchor's media modules roadmap), not a repo of its own. Whether esp-vision's + sensor and H.264 replace cameraif (and castif's encoder) stays open. esp-vision's `tflite` and image stack are in as modules; where `sensor` stands is in [esp-vision.md](esp-vision.md). - **CircuitPython.** It doesn't read our manifests, and none of this reaches diff --git a/docs/newcomers.md b/docs/newcomers.md index cea66eb9..7024e401 100644 --- a/docs/newcomers.md +++ b/docs/newcomers.md @@ -105,7 +105,7 @@ way autosize grows the app partition when the image doesn't fit. | `build_mp.py` | The build command. | | `UPSTREAM` | The MicroPython release the patches apply to. | | `modules.lock`, `deps.lock` | What gets fetched when it's missing. | -| `modules/` | The module manifest, `all/`, `castif/`, `tflite/` and `vision/` ([esp-vision's](esp-vision.md)), and the modules. | +| `modules/` | The module manifest, `all/`, the modules that live only here (`castif/`, `jpegio/`, and `tflite/` and `vision/`, [esp-vision's](esp-vision.md)), and the rest, linked or cloned. | | `patches/` | The patch series and `apply_patches.py`. | | `variants/` | Our unix, windows and webassembly variants, and the esp32 settings and board variants. | diff --git a/modules/jpegio/README.md b/modules/jpegio/README.md new file mode 100644 index 00000000..ab64a6ec --- /dev/null +++ b/modules/jpegio/README.md @@ -0,0 +1,342 @@ +# jpegio — baseline JPEG decoder (CircuitPython API, TJpgDec R0.03) + +`import jpegio` gives MicroPython the same `JpegDecoder` CircuitPython ships +natively, so one script decodes on both — CircuitPython uses its own, a +MicroPython firmware built with this module uses this one. It exists so a +`displaydev` firmware can show a camera's MJPEG frames (or a JPEG on flash) +without carrying LVGL for its decoder (org `docs/jpegio-vision.md`). + +```bash +./build_mp.py --port esp32 --board ESP32_GENERIC_P4 --variant C6_WIFI --flash 16MB --modules displayif,jpegio +``` + +It lived in displayif until 2026-10-06, when it moved here on its own (the +media modules roadmap's Phase 1), so a firmware that wants JPEG now names +`jpegio` (`all` includes it). Platform-neutral C: built on every port from this +directory's `micropython.mk` / `micropython.cmake`. Output is native-order RGB565, block by block, straight +from TJpgDec — no full-frame buffer inside the module. + +Beside the `lvgl-micropython` usermod it is also LVGL's JPEG decoder: the +same TJpgDec, registered through LVGL's public `lv_image_decoder_create` +API, so `lv.image` shows JPEGs (webcam MJPEG frames included) with one +decoder in the firmware — see [LVGL image decoder](#lvgl-image-decoder). + +```python +import jpegio +decoder = jpegio.JpegDecoder() # allocates its ~3.7 KB work area once +width, height = decoder.open("/sd/photo.jpg") +buf = bytearray(width * height * 2) # RGB565, tight, row-major +decoder.decode(buf) +display_drv.blit_rect(buf, 0, 0, width, height) + +# camera: hand each MCU block straight to the display, no frame buffer +width, height = decoder.open(frame) # bytes/bytearray/memoryview of one MJPEG frame +decoder.decode(lambda x, y, w, h, mv: display_drv.blit_rect(mv, x, y, w, h), 1) +``` + +## API + +### `jpegio.JpegDecoder()` + +No arguments. Holds TJpgDec's decoder state and its `TJPGD_WORKSPACE_SIZE` +(3500-byte, CircuitPython's number) work area inside the object, allocated +once — create one decoder and reuse it, as CP's docs advise. + +### `open(source) -> (width, height)` + +Positional-only, like CP. `source` is one of: + +- `str` — a path; opened `"rb"` by the module and closed after `decode()` + (or when `open()` fails, or on the next `open()`). +- a bytes-like object (`bytes`, `bytearray`, `memoryview`, anything with the + buffer protocol) — read in place through the buffer protocol; the source is + not copied. Keep it alive and unchanged until `decode()` returns. +- a binary stream: any object with MicroPython's native stream protocol + (`open(...)` files, `io.BytesIO`, sockets). Unseekable streams are fine; + skipped segments are read through, never seeked. Text streams, + pure-Python objects that merely define `read()`, and anything else + (`None`, an int, a list) are not accepted (TypeError). + +Parses the headers (`jd_prepare`) and returns the image size. `width` and +`height` are also readable as properties and track the last `open()`: +valid after a successful one (still valid after its `decode()`, so +`blit_rect(buf, 0, 0, decoder.width, decoder.height)` works), RuntimeError +`width needs a successful open()` before any `open()` or after one that +failed — a failed `open()` never leaves the previous image's size behind. + +**What is sniffed: nothing beyond SOI.** The stream goes to `jd_prepare`, +which scans for `FF D8` and then takes the segments in whatever order they +arrive. There is deliberately no JFIF/APP0 check: UVC MJPEG frames from a +webcam are `SOI → DQT → SOF0 → DHT → … → DRI → SOS`, not JFIF-first, and an +`is_jpg()`-style sniff rejects every one of them. Restart markers (DRI) are +handled by TJpgDec. + +### `decode(target, scale=0, x=0, y=0, *, stride=None) -> None` + +Decodes the image opened by the last `open()`. One-shot, like CP: the stream +is consumed, so `open()` again before the next `decode()` (RuntimeError +`decode() without open()` otherwise). + +- `scale` 0..3 — downscale by 1, 1/2, 1/4, 1/8 (TJpgDec's `JD_USE_SCALE`). + The decoded image is `(width >> scale) x (height >> scale)`; 1/8 is the + cheap one (DC-only, no IDCT). Other values: ValueError. +- `x`, `y` (0..65535, the range of the image size itself) — where the + decoded image's top-left corner lands, in pixels, in both target modes. + Anything else: ValueError. +- `target` — either a **buffer** or a **callable**. + +**Buffer target.** A writable buffer-protocol object (`bytearray`, +`memoryview`, a `framebuf.FrameBuffer`-compatible object) of native-order +RGB565 pixels, row-major. Rows are `stride` pixels apart (default: the +decoded width, i.e. a tight buffer of exactly +`(width >> scale) * (height >> scale) * 2` bytes). Before decoding, the +module checks `x + decoded_width <= stride` and that the last pixel written +fits in the buffer; otherwise ValueError naming the numbers, e.g. +`target too small: 320x240 at (0, 0) with stride 320 needs 153600 bytes, +buffer has 1024`. The fit is decided without computing +`(y + decoded_height - 1) * stride`, so a `stride` / `y` pair whose product +wraps `size_t` (32 bits on the MCU ports) is refused too (`... the last +pixel's offset overflows size_t`), never let through. Because the default +`stride` is the decoded width, `x > 0` always needs `stride` — the row +width of the target in pixels — and the error says so (`x + decoded width +(5 + 320) exceeds the default stride 320 (the decoded width): pass +stride=...`); `y > 0` needs only a buffer with enough rows. A rejected call +leaves the opened image in place, so `decode()` can be retried with a +better target without another `open()`. Each TJpgDec output block is +copied row by row into place. + +**Callable target.** Called once per TJpgDec output block, in raster order: + +```python +callback(x, y, w, h, mv) +``` + +`x, y` are the block's position (with `decode()`'s `x`, `y` added), `w, h` +its size, `mv` a read-only `memoryview('H')` of `w * h` native-order RGB565 +pixels — `len(mv) == w * h`, `mv[0]` is the first pixel as an int, and +`bytes(mv)` / any buffer-protocol consumer (`display_drv.blit_rect`) sees +`w * h * 2` bytes. `mv` views TJpgDec's own output buffer and is valid only +until the callback returns; a reference kept past that reads a zero-length +view (the module truncates it after each decode). One memoryview object is +created per decoder and reused for every block, so the callback path +allocates nothing per block. Blocks are MCU-sized (8x8, 16x8 or 16x16 pixels +before scaling) and clipped at the right/bottom edge; together they cover +the decoded image exactly once. An exception raised inside the callback +propagates out of `decode()`; the decoder closes its source and needs a new +`open()`. `stride` is not accepted with a callable (TypeError). + +**No byte swapping.** Pixels are native-order RGB565 (on little-endian +targets `0xF800` red is the bytes `00 F8`). Displays that want big-endian +RGB565 get it from `display_drv`, which byte-swaps itself; CP's +`RGB565_SWAPPED` output is CP's convention, not this module's. + +## Errors + +TJpgDec's `JRESULT` codes map to exceptions whose message starts with the +TJpgDec name, so a caller can tell them apart without module constants: + +| `JRESULT` | Exception | When | +|-----------|-----------|------| +| `JDR_INP` | `ValueError` | input ended before SOI, or the JPEG is truncated (an I/O error on a stream raises `OSError(errno)` instead) | +| `JDR_FMT1` | `ValueError` | unsupported or malformed JPEG: corrupt entropy data, a missing DQT/DHT — **this is what a DHT-less MJPEG frame produces** | +| `JDR_FMT2` | `ValueError` | right format but not supported (unused by R0.03) | +| `JDR_FMT3` | `ValueError` | not baseline: **progressive** (SOF2), lossless, arithmetic, CMYK, or 4:1:1 / 4:4:0 chroma subsampling | +| `JDR_MEM1` | `MemoryError` | the image's tables do not fit the 3500-byte work area | +| `JDR_MEM2` | `MemoryError` | a segment is larger than TJpgDec's 512-byte input buffer (`JD_SZBUF`) | +| `JDR_PAR` | `ValueError` | parameter error (scale is validated before TJpgDec sees it) | +| `JDR_INTR` | `RuntimeError` | output function interrupted the decode (the module's output functions never do) | + +A DHT-less frame (some cameras omit the standard Huffman tables and expect +the decoder to supply them) fails `open()` with `JDR_FMT1`; whether the +module should inject the default tables is an open decision recorded in the +vision doc, not something done silently here. + +## Differences from CircuitPython + +One, deliberate: `decode()` takes a buffer or a callable instead of a +`displayio.Bitmap`, and consequently has **no** `x1`/`y1`/`x2`/`y2` crop +window and no `skip_source_index` / `skip_dest_index`. Those are +`bitmaptools.blit` palette semantics that only make sense against a +`displayio.Bitmap`; the `stride` keyword and the callable target are the +replacements. Everything else — constructor, `open()` (positional-only, +str/buffer/stream), the `(width, height)` return, `scale` 0..3, `x`/`y`, +one-shot decode — follows CP's `shared-bindings/jpegio`. + +Also unlike CP, TJpgDec here emits native-order RGB565 (CP patches its copy +to byte-swap for `RGB565_SWAPPED`); see above. And errors are typed +(`ValueError` / `MemoryError` / `RuntimeError`) rather than CP's uniform +`RuntimeError`. + +## Build + +- `jpegio.c` — the module (`MP_REGISTER_MODULE(MP_QSTR_jpegio, ...)`). +- `tjpgd/` — vendored TJpgDec: `tjpgd.c`, `tjpgd.h`, `tjpgdcnf.h`. The + firmware's only TJpgDec, always compiled. +- `lvgl_decoder.c` / `lvgl_decoder.h` — the LVGL image decoder on that + TJpgDec; compiled only beside `lvgl-micropython` (below). +- `micropython.mk` / `micropython.cmake` — the module's glue (`manifest.py` + names it with `c_module`). + +`tjpgdcnf.h` is CircuitPython's, verbatim: `JD_SZBUF 512`, `JD_FORMAT 1` +(RGB565), `JD_USE_SCALE 1`, `JD_TBLCLIP 1`, `JD_FASTDECODE 1`. `jpegio.c` +refuses to build with any other `JD_FORMAT`. + +**With LVGL (D4 of the org's `docs/jpegio-vision.md`: jpegio +self-detects).** LVGL's own TJPGD is off by config on MicroPython +(`LV_USE_TJPGD 0` in lvgl-bindings' `lv_conf.h`), so nothing clashes with +the vendored copy and LVGL has no JPEG decoder of its own; this module +supplies one. When lvgl-micropython is in the same build (`USER_C_MODULES` +names it, which a `build_mp.py` build with both modules does), both glue files +compile `lvgl_decoder.c` with `-DJPEGIO_LVGL_DECODER=1`; its `lvgl/lvgl.h` +resolves through the bindings directory lvgl-micropython puts on the include +path. `JPEGIO_LVGL=0` / `1` (make) or `-DJPEGIO_LVGL=OFF` / `ON` (CMake) +forces the decision. Without lvgl-micropython nothing links against LVGL +(this module's CI builds it both ways), but the module still exports +`register_lvgl_decoder()` and `lvgl_decoders()`; see *Why these two names +are unconditional* below. + +## NOTICE — TJpgDec + +`tjpgd/tjpgd.c`, `tjpgd/tjpgd.h` and `tjpgd/tjpgdcnf.h` are ChaN's TJpgDec +R0.03 with patch1, taken from CircuitPython's `lib/tjpgd/src/` (which +imported it from http://elm-chan.org/fsw/tjpgd/00index.html), with two +deviations. First, the RGB565 store in `mcu_output` is ChaN's original +`*d++ = w;` where CircuitPython has `__builtin_bswap16(w)`. Second, the +`stdint.h` guard at the top of `tjpgd.h` tests for the header with +`__has_include` rather than for `_WIN32`: upstream's comment says it means +"a compiler without stdint.h", but mingw defines `_WIN32` *and* ships +`stdint.h`, where `uint32_t` is `unsigned int` rather than the fallback's +`unsigned long`, so every mingw cross-build failed with conflicting types +(displayif#25). The fallback typedefs are kept for the compiler the comment +was written for, MSVC before 2010. Both CircuitPython's copy and ChaN's +R0.03 still carry the platform test; LVGL's fork sidesteps it by including +`stdint.h` unconditionally. The files keep ChaN's license header: + +> Copyright (C) 2021, ChaN, all right reserved. +> +> The TJpgDec module is a free software and there is NO WARRANTY. +> No restriction on use. You can use, modify and redistribute it for +> personal, non-profit or commercial products UNDER YOUR RESPONSIBILITY. +> Redistributions of source code must retain the above copyright notice. + +`jpegio.c` is a port of CircuitPython's `shared-bindings/jpegio` and +`shared-module/jpegio` (Copyright (c) 2023 Jeff Epler for Adafruit +Industries, MIT). + +## LVGL image decoder + +The decoder itself (`lvgl_decoder.c`) is compiled only beside +`lvgl-micropython` (see Build); the two Python names below exist on every +build. It is an ordinary LVGL image decoder named `"jpegio"` +(`lv_image_decoder_t.name`), created with `lv_image_decoder_create`, so +`lv.image(...).set_src(...)` and every other LVGL image path decode +baseline JPEGs through this module's TJpgDec: + +```python +import lvgl as lv +import jpegio +lv.init() +jpegio.register_lvgl_decoder() # needed only if `import jpegio` ran before lv.init() + +with open("/sd/photo.jpg", "rb") as f: + jpeg = f.read() +dsc = lv.image_dsc_t({"header": {"w": 320, "h": 240, "cf": lv.COLOR_FORMAT.RAW}, + "data_size": len(jpeg), "data": jpeg}) +lv.image(lv.screen_active()).set_src(dsc) +``` + +**Registration.** Two ways, both idempotent — registering twice never adds +a second decoder: + +- On `import jpegio`, if LVGL is already initialised (`lv.init()` ran), the + module registers itself. MicroPython runs a built-in module's `__init__` + on every `import` statement (`MICROPY_MODULE_BUILTIN_INIT`, on by default + at the "extra features" ROM level: unix, esp32, rp2, stm32, mimxrt, + SAMD51; not on SAMD21's "basic" level). Upstream's windows variants sit at + the core level and leave it off; the PyDevices windows variant in + micropython-pydevices turns it on, so `micropython.exe` registers on import + like unix. On any other windows build, call `register_lvgl_decoder()`. +- `jpegio.register_lvgl_decoder() -> bool` registers explicitly: `True` + when this call added the decoder, `False` when it was already there, + `RuntimeError` when LVGL is not initialised. Use it when `import jpegio` + came before `lv.init()`, and after every `lv.deinit()` / `lv.init()` cycle + (LVGL clears its decoder list on deinit). On a build with no decoder in it + the call always raises `RuntimeError`, and the message names both the cause + and the way out: *no lvgl-micropython sibling at build time (rebuild with + `JPEGIO_LVGL=1` to force it)*. + +`jpegio.lvgl_decoders() -> tuple` lists LVGL's registered image decoders by +name in the order LVGL consults them — `("jpegio", "LODEPNG", "BIN")` once +registered (a new decoder goes to the head of LVGL's list), `()` before +`lv.init()`. A diagnostic: the binding cannot walk that list from Python +(MicroPython's builtin-method self check refuses +`lv.image_decoder_t.get_next(None)`), and it is how the test proves that +registering twice adds nothing. On a build with no decoder in it, `()` +always. + +**Why these two names are unconditional.** The general rule and the check that +enforces it are in [../../docs/qstrs-and-usermod-defines.md](../../docs/qstrs-and-usermod-defines.md); +this is where it was first found. They are module attributes on +every build, and only `lvgl_decoder.c` is conditional. Partly so the API +never silently disappears when the sibling scan misses — a decoder-less +build says so, in an exception, instead of raising `AttributeError` — and +partly because the CMake ports cannot compile them any other way. +MicroPython's `py/mkrules.cmake` builds the QSTR-extraction preprocessor +flags from the *port target's* own `COMPILE_DEFINITIONS` +(`get_target_property(MICROPY_CPP_DEF ${MICROPY_TARGET} COMPILE_DEFINITIONS)`), +so a usermod's `INTERFACE` define — which is how `micropython.cmake` +passes `JPEGIO_LVGL_DECODER=1` — never reaches `makeqstrdefs.py`. An +`MP_QSTR_*` referenced only inside +`#if JPEGIO_LVGL_DECODER` is therefore never collected on a CMake port, and +the build fails with `error: 'MP_QSTR_register_lvgl_decoder' undeclared`. +In MicroPython v1.28.0 that means esp32 and rp2, the only two ports with a +`CMakeLists.txt`; samd, mimxrt and stm32 carry CMake glue here for the day +they gain one, and build through their Makefile today. (The Makefile ports +put the define in `CFLAGS_USERMOD`, which `py/py.mk` folds into `CFLAGS`, +and their QSTR pass reads `CFLAGS`, which is why unix builds never showed +it.) `MP_QSTR___init__` survives inside the `#if` only because it is one of +MicroPython's static-pool qstrs (`py/makeqstrdata.py`), not because +anything here collects it, so any *new* `MP_QSTR_*` put under that `#if` +brings the break straight back. Do not re-conditionalise either function. + +**Sources.** A variable source (`lv.image_dsc_t`) whose `data` starts with +the SOI marker `FF D8`, or a file (through a registered `lv.fs_drv_t`, e.g. +lvgl-bindings' `fs_driver.py`) whose first two bytes are `FF D8` — the +extension is not consulted. Nothing more is sniffed: LVGL's own `is_jpg()` +wants a 10-byte JFIF signature and so rejects every UVC MJPEG frame; here +`jd_prepare` judges, for both source kinds, in the decoder's *info* step. +So the image size LVGL sees is the stream's own — the `w`/`h` in your +`lv.image_dsc_t` header are not echoed (LVGL's TJPGD echoed them) — and a +pixel buffer that merely starts `FF D8` fails `jd_prepare` and falls +through to LVGL's built-in decoder. `cf` in the header should be +`lv.COLOR_FORMAT.RAW` (or `RAW_ALPHA`), the convention for "not pixels"; +`data_size` must be the JPEG's length. + +**Output.** `LV_COLOR_FORMAT_RGB565`, native byte order, stride `w * 2`, +the same pixels `JpegDecoder.decode()` produces (the LVGL test asserts +jpegio's own golden digests through `lv.image`). LVGL's software renderer +blends RGB565 into an RGB565 display by row copy, its fast path. + +**Memory.** The decoder decodes the whole image in its *open* step into a +draw buffer of `w * h * 2` bytes (from LVGL's image-cache allocator, i.e. +the MicroPython heap), which LVGL frees at *close* — every refresh, unless +LVGL's image cache is on (`LV_CACHE_DEF_SIZE`, 0 in lvgl-bindings). That is +what LVGL's own full-decode decoders (`lv_lodepng.c`) do. It is not the +block-wise, no-frame-buffer path `JpegDecoder.decode(callable)` offers: +LVGL's `lv_tjpgd.c` decodes MCU by MCU only through exports it added to its +private TJpgDec (`jd_mcu_load`, `jd_mcu_output`, `jd_restart`), which ChaN's +R0.03 does not have, and `jd_decomp` is all-or-nothing. A camera preview +that must not hold a frame buffer uses `jpegio.JpegDecoder` and +`display_drv.blit_rect` directly. Scaling (`scale` 1..3) is a +`JpegDecoder` feature, not exposed to LVGL (its zoom is its own). + +**Errors.** A source the decoder does not recognise in its *info* step (no +`FF D8`, or `jd_prepare` refuses the headers: progressive, DHT-less, not a +JPEG at all) is not claimed (`LV_RESULT_INVALID`), so LVGL tries its other +decoders and, when none claims it, draws nothing. A source whose *info* +succeeded but whose *open* fails (a truncated scan, a decode error inside +the frame, no memory for the `w * h * 2` buffer) also draws nothing, but +LVGL does **not** try another decoder after a failed open — its +`lv_image_decoder.c` assumes a decoder that could read the info can open +the image — so the failure is final for that draw and logged at +`LV_LOG_WARN`. No Python exception is raised from inside LVGL's draw path. diff --git a/modules/jpegio/manifest.py b/modules/jpegio/manifest.py new file mode 100644 index 00000000..47e3c42c --- /dev/null +++ b/modules/jpegio/manifest.py @@ -0,0 +1,5 @@ +# jpegio: JPEG decoding into RGB565 (CircuitPython's jpegio API, on ChaN's +# TJpgDec), and LVGL's JPEG decoder when lvgl-micropython is in the build. +# C only; every port. Moved here from displayif (media modules roadmap, +# Phase 1). +c_module(".") diff --git a/modules/jpegio/micropython.cmake b/modules/jpegio/micropython.cmake new file mode 100644 index 00000000..c3f277e2 --- /dev/null +++ b/modules/jpegio/micropython.cmake @@ -0,0 +1,38 @@ +# jpegio CMake glue (see micropython.mk for what the variables mean). + +set(JPEGIO_DIR ${CMAKE_CURRENT_LIST_DIR}/src) + +add_library(usermod_jpegio INTERFACE) +target_include_directories(usermod_jpegio INTERFACE + ${JPEGIO_DIR}/tjpgd +) +target_link_libraries(usermod INTERFACE usermod_jpegio) + +# The vendored TJpgDec is the firmware's only one (LVGL's is off by config). +target_sources(usermod_jpegio INTERFACE + ${JPEGIO_DIR}/jpegio.c + ${JPEGIO_DIR}/tjpgd/tjpgd.c +) + +# The LVGL image decoder (lvgl_decoder.c) is built only when lvgl-micropython is +# in this build, decided from USER_C_MODULES the way micropython.mk decides it: +# what is on disk beside this module says nothing about what this build +# selected, and jpegio has to build without LVGL. Override with -DJPEGIO_LVGL=ON/OFF. +# The decoder includes "lvgl/lvgl.h" and "lvgl/src/...", which resolve through +# the bindings directory lvgl-micropython itself puts on the include path, so +# nothing here needs to know where the bindings are. +if(NOT DEFINED JPEGIO_LVGL) + if("${USER_C_MODULES}" MATCHES "lvgl-micropython") + set(JPEGIO_LVGL ON) + else() + set(JPEGIO_LVGL OFF) + endif() +endif() + +if(JPEGIO_LVGL) + target_compile_definitions(usermod_jpegio INTERFACE JPEGIO_LVGL_DECODER=1) + # No qstrs of its own (the Python-visible names live in jpegio.c). + target_sources(usermod_jpegio INTERFACE + ${JPEGIO_DIR}/lvgl_decoder.c + ) +endif() diff --git a/modules/jpegio/micropython.mk b/modules/jpegio/micropython.mk new file mode 100644 index 00000000..0dc8f417 --- /dev/null +++ b/modules/jpegio/micropython.mk @@ -0,0 +1,40 @@ +# jpegio: CircuitPython-compatible JPEG decoder (TJpgDec R0.03, CP's config). +# Platform-neutral C, built on every port. +# +# The vendored src/tjpgd/tjpgd.c is the firmware's only TJpgDec. LVGL's own copy +# is off by config (LV_USE_TJPGD 0 in lvgl-bindings' lv_conf.h on MicroPython), +# so when lvgl-micropython is in the same build jpegio also compiles +# lvgl_decoder.c: an LVGL image decoder on this TJpgDec, registered through +# LVGL's public lv_image_decoder_create API. Without it nothing here changes +# and the build stays LVGL-less. + +JPEGIO_MOD_DIR := $(USERMOD_DIR) +JPEGIO_DIR := $(JPEGIO_MOD_DIR)/src + +CFLAGS_USERMOD += -I$(JPEGIO_DIR)/tjpgd + +SRC_USERMOD_C += $(JPEGIO_DIR)/jpegio.c + +# Library source: no qstrs, so it stays out of the QSTR scan. +SRC_USERMOD_LIB_C += $(JPEGIO_DIR)/tjpgd/tjpgd.c + +# lvgl-micropython is in this build if USER_C_MODULES names it -- either as a +# module directory itself (MicroPython 1.29 c_module()) or as a parent +# directory one level above it (py.mk's glob). Override with JPEGIO_LVGL=0/1 +# on the make command line. +JPEGIO_LVMP_DIR := $(firstword $(filter %/lvgl-micropython,$(USER_C_MODULES:/=)) $(foreach d,$(USER_C_MODULES),$(wildcard $(d)/lvgl-micropython))) +JPEGIO_LVGL ?= $(if $(JPEGIO_LVMP_DIR),1,0) + +ifeq ($(JPEGIO_LVGL),1) +# lvgl_decoder.c includes "lvgl/lvgl.h" and "lvgl/src/...", which resolve +# through the -I$(BINDINGS_DIR) lvgl-micropython adds, wherever its bindings +# are (a sibling checkout or its own fetched .deps/), so nothing here derives +# that path. +CFLAGS_USERMOD += -DJPEGIO_LVGL_DECODER=1 +# No qstrs of its own (the Python-visible names live in jpegio.c). +SRC_USERMOD_LIB_C += $(JPEGIO_DIR)/lvgl_decoder.c +# lvgl.h with LV_USE_FLOAT trips -Werror=double-promotion / float-conversion +# on ports that append those after CFLAGS_USERMOD (unix, webassembly): the +# same per-object suppression lvgl-micropython puts on LVGL's own objects. +$(eval $(BUILD)/$(patsubst $(JPEGIO_MOD_DIR)/%,$(notdir $(JPEGIO_MOD_DIR))/%,$(JPEGIO_DIR)/lvgl_decoder.o): CFLAGS += -Wno-double-promotion -Wno-float-conversion) +endif diff --git a/modules/jpegio/src/jpegio.c b/modules/jpegio/src/jpegio.c new file mode 100644 index 00000000..5d04346d --- /dev/null +++ b/modules/jpegio/src/jpegio.c @@ -0,0 +1,541 @@ +// SPDX-License-Identifier: MIT +// jpegio: CircuitPython-compatible baseline JPEG decoder (TJpgDec R0.03) as a +// MicroPython native module. +// +// Port of CircuitPython's shared-bindings/jpegio + shared-module/jpegio +// (Copyright (c) 2023 Jeff Epler for Adafruit Industries, MIT) with the +// platform's RGB565 buffer or a per-block callback in place of +// displayio.Bitmap. The API contract is in README.md next to this file. + +#include +#include + +#include "py/obj.h" +#include "py/objarray.h" +#include "py/runtime.h" +#include "py/stream.h" +#include "py/builtin.h" +#include "py/mperrno.h" + +#include "tjpgd.h" + +#if JD_FORMAT != 1 +#error "jpegio needs TJpgDec built with JD_FORMAT 1 (RGB565) -- see tjpgd/tjpgdcnf.h" +#endif + +// Defined by micropython.mk / micropython.cmake only when the lvgl-micropython +// sibling usermod is in the same build: jpegio then also registers an LVGL +// image decoder on its TJpgDec (lvgl_decoder.c). +#ifndef JPEGIO_LVGL_DECODER +#define JPEGIO_LVGL_DECODER 0 +#endif +#if JPEGIO_LVGL_DECODER +#include "lvgl_decoder.h" +#endif + +// CircuitPython's work-area size (shared-module/jpegio/JpegDecoder.h). Holds +// TJpgDec's 512-byte stream buffer, the quantiser and Huffman tables, and the +// MCU + IDCT scratch for a 16x16 (4:2:0) MCU. Allocated once, inside the object. +#define TJPGD_WORKSPACE_SIZE 3500 + +typedef struct _jpegio_jpegdecoder_obj_t { + mp_obj_base_t base; + JDEC decoder; + uint8_t workspace[TJPGD_WORKSPACE_SIZE]; + // Source handed to open(): a buffer-protocol object or a binary stream. + mp_obj_t source; // MP_OBJ_NULL once closed + mp_buffer_info_t bufinfo; // unread remainder of a buffer source + bool owns_source; // opened from a str path by us: close it when done + bool ready; // jd_prepare succeeded and decode() has not consumed it + uint16_t width, height; // of the last successfully opened image; 0 after a failed open() + // Target of the decode() in progress. Exactly one of callback / pixels is live. + mp_obj_t callback; // callable target, else MP_OBJ_NULL + mp_obj_t target; // buffer target object, kept alive while its pointer is used + uint16_t *pixels; // buffer target, native-order RGB565 + size_t stride; // buffer row stride in pixels + mp_int_t x, y; // placement of the decoded image + mp_obj_t block_view; // reusable memoryview handed to the callback per block +} jpegio_jpegdecoder_obj_t; + +#define JPEGIO_MIN(a, b) ((a) < (b) ? (a) : (b)) + +// decode()'s x / y limit: the image size is a uint16 in TJpgDec (and in +// CircuitPython's Bitmap), so a placement beyond it is never meaningful. It +// also keeps every coordinate sum below 2^17, so the buffer-fit arithmetic +// cannot overflow and the callback's coordinates always fit a small int. +#define JPEGIO_MAX_COORD 0xFFFF + +// --- TJpgDec result → exception -------------------------------------------- + +static void jpegio_raise_jresult(JRESULT rc) { + switch (rc) { + case JDR_OK: + return; + case JDR_INTR: + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("JDR_INTR: interrupted by output function")); + case JDR_INP: + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("JDR_INP: input ended early (no SOI marker, or truncated JPEG)")); + case JDR_MEM1: + mp_raise_msg_varg(&mp_type_MemoryError, MP_ERROR_TEXT("JDR_MEM1: image needs more than the %d-byte work area"), TJPGD_WORKSPACE_SIZE); + case JDR_MEM2: + mp_raise_msg_varg(&mp_type_MemoryError, MP_ERROR_TEXT("JDR_MEM2: a JPEG segment is larger than the %d-byte input buffer"), JD_SZBUF); + case JDR_PAR: + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("JDR_PAR: parameter error")); + case JDR_FMT1: + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("JDR_FMT1: unsupported or malformed JPEG (missing tables or corrupt data)")); + case JDR_FMT2: + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("JDR_FMT2: right format but not supported")); + case JDR_FMT3: + default: + mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("JDR_FMT3: progressive JPEG is not supported (nor lossless, arithmetic, CMYK, or 4:1:1 / 4:4:0 subsampling)")); + } +} + +// --- source lifecycle ------------------------------------------------------- + +static void jpegio_close(jpegio_jpegdecoder_obj_t *self) { + if (self->owns_source && self->source != MP_OBJ_NULL) { + mp_stream_close(self->source); + } + self->owns_source = false; + self->source = MP_OBJ_NULL; + self->ready = false; + memset(&self->bufinfo, 0, sizeof(self->bufinfo)); +} + +// TJpgDec input function for a buffer-protocol source: hand out the next +// `len` bytes of the caller's buffer (no copy of the source itself; TJpgDec +// fills its own 512-byte stream buffer from it). NULL dest means skip. +static size_t jpegio_buffer_input(JDEC *jd, uint8_t *dest, size_t len) { + jpegio_jpegdecoder_obj_t *self = jd->device; + size_t n = JPEGIO_MIN(len, self->bufinfo.len); + if (dest != NULL) { + memcpy(dest, self->bufinfo.buf, n); + } + self->bufinfo.buf = (uint8_t *)self->bufinfo.buf + n; + self->bufinfo.len -= n; + return n; +} + +// TJpgDec input function for a native binary stream (file, io.BytesIO, +// socket, ...). NULL dest means skip: read it through a scratch buffer rather +// than seeking, so unseekable streams work. +static size_t jpegio_stream_input(JDEC *jd, uint8_t *dest, size_t len) { + jpegio_jpegdecoder_obj_t *self = jd->device; + if (dest == NULL) { + uint8_t skip[128]; + size_t total = 0; + while (total < len) { + size_t n = jpegio_stream_input(jd, skip, JPEGIO_MIN(len - total, sizeof(skip))); + if (n == 0) { + break; + } + total += n; + } + return total; + } + int errcode = 0; + mp_uint_t n = mp_stream_rw(self->source, dest, len, &errcode, MP_STREAM_RW_READ); + if (errcode != 0) { + // An I/O failure is more useful than the JDR_INP it would become. + mp_raise_OSError(errcode); + } + return n; +} + +// --- TJpgDec output functions ----------------------------------------------- + +// Buffer target: copy each block's rows into place. TJpgDec hands blocks in +// raster order, already clipped to the image edge, as w*h native-order RGB565. +static int jpegio_buffer_output(JDEC *jd, void *data, JRECT *rect) { + jpegio_jpegdecoder_obj_t *self = jd->device; + size_t w = (size_t)rect->right - rect->left + 1; + size_t h = (size_t)rect->bottom - rect->top + 1; + const uint16_t *src = data; + // decode() proved (y + dh - 1) * stride + x + dw <= the buffer's pixel + // count without overflowing, so every index formed here is inside it. + uint16_t *dst = self->pixels + ((size_t)self->y + rect->top) * self->stride + (size_t)self->x + rect->left; + for (size_t row = 0; row < h; row++) { + memcpy(dst + row * self->stride, src + row * w, w * sizeof(uint16_t)); + } + return 1; +} + +// Callable target: callback(x, y, w, h, block) with `block` a view of TJpgDec's +// own output buffer -- valid only until the callback returns. One view object +// is reused for every block, so the callback path allocates nothing per block. +static int jpegio_callback_output(JDEC *jd, void *data, JRECT *rect) { + jpegio_jpegdecoder_obj_t *self = jd->device; + size_t w = (size_t)rect->right - rect->left + 1; + size_t h = (size_t)rect->bottom - rect->top + 1; + mp_obj_array_t *view = MP_OBJ_TO_PTR(self->block_view); + view->items = data; + #if MICROPY_PY_BUILTINS_MEMORYVIEW + view->len = w * h; // memoryview('H'): one element per pixel + #else + view->len = w * h * 2; // bytearray fallback: length in bytes + #endif + mp_obj_t args[5] = { + MP_OBJ_NEW_SMALL_INT(self->x + rect->left), + MP_OBJ_NEW_SMALL_INT(self->y + rect->top), + MP_OBJ_NEW_SMALL_INT(w), + MP_OBJ_NEW_SMALL_INT(h), + self->block_view, + }; + mp_call_function_n_kw(self->callback, 5, 0, args); + return 1; +} + +static void jpegio_decode_done(jpegio_jpegdecoder_obj_t *self) { + if (self->block_view != MP_OBJ_NULL) { + // A reference the callback kept must not read the workspace later. + mp_obj_array_t *view = MP_OBJ_TO_PTR(self->block_view); + view->items = NULL; + view->len = 0; + } + self->callback = MP_OBJ_NULL; + self->target = MP_OBJ_NULL; + self->pixels = NULL; + jpegio_close(self); +} + +// --- JpegDecoder ------------------------------------------------------------ + +static mp_obj_t jpegio_jpegdecoder_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + (void)all_args; + mp_arg_check_num(n_args, n_kw, 0, 0, false); + jpegio_jpegdecoder_obj_t *self = mp_obj_malloc(jpegio_jpegdecoder_obj_t, type); + memset(&self->decoder, 0, sizeof(self->decoder)); + self->source = MP_OBJ_NULL; + memset(&self->bufinfo, 0, sizeof(self->bufinfo)); + self->owns_source = false; + self->ready = false; + self->width = 0; + self->height = 0; + self->callback = MP_OBJ_NULL; + self->target = MP_OBJ_NULL; + self->pixels = NULL; + self->stride = 0; + self->x = 0; + self->y = 0; + self->block_view = MP_OBJ_NULL; + return MP_OBJ_FROM_PTR(self); +} + +static void jpegio_jpegdecoder_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + (void)kind; + jpegio_jpegdecoder_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (self->ready) { + mp_printf(print, "", self->width, self->height); + } else { + mp_printf(print, ""); + } +} + +// open(source) -> (width, height). Positional-only, like CircuitPython. +static mp_obj_t jpegio_jpegdecoder_open(mp_obj_t self_in, mp_obj_t source) { + jpegio_jpegdecoder_obj_t *self = MP_OBJ_TO_PTR(self_in); + jpegio_close(self); + // width / height track the last open(): a failed one reports nothing. + self->width = 0; + self->height = 0; + + bool owns = false; + if (mp_obj_is_str(source)) { + source = mp_call_function_2(MP_OBJ_FROM_PTR(&mp_builtin_open_obj), source, MP_OBJ_NEW_QSTR(MP_QSTR_rb)); + owns = true; + } + + size_t (*infunc)(JDEC *, uint8_t *, size_t); + if (mp_get_buffer(source, &self->bufinfo, MP_BUFFER_READ)) { + infunc = jpegio_buffer_input; + } else { + // Not mp_get_stream(): that reads the type's protocol slot unguarded, + // and a type without one (int, None, list, a Python class with a + // read() method) yields a wild pointer -- a segfault, not a TypeError. + const mp_stream_p_t *stream = MP_OBJ_TYPE_GET_SLOT_OR_NULL(mp_obj_get_type(source), protocol); + if (stream != NULL && stream->read != NULL && !stream->is_text) { + infunc = jpegio_stream_input; + } else { + mp_raise_TypeError(MP_ERROR_TEXT("source must be a str path, a bytes-like object, or a binary stream")); + } + } + self->source = source; + self->owns_source = owns; + + // Nothing is sniffed here: jd_prepare scans for SOI itself and then takes + // the segments in whatever order the stream carries them (UVC MJPEG frames + // are not JFIF-first). A stream error inside it is raised by the input + // function; close the source on that path too. + JRESULT rc = JDR_OK; + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + rc = jd_prepare(&self->decoder, infunc, self->workspace, sizeof(self->workspace), self); + nlr_pop(); + } else { + jpegio_close(self); + nlr_jump(nlr.ret_val); + } + if (rc != JDR_OK) { + jpegio_close(self); + jpegio_raise_jresult(rc); + } + self->ready = true; + self->width = self->decoder.width; + self->height = self->decoder.height; + + mp_obj_t elems[] = { + MP_OBJ_NEW_SMALL_INT(self->width), + MP_OBJ_NEW_SMALL_INT(self->height), + }; + return mp_obj_new_tuple(MP_ARRAY_SIZE(elems), elems); +} +static MP_DEFINE_CONST_FUN_OBJ_2(jpegio_jpegdecoder_open_obj, jpegio_jpegdecoder_open); + +// decode(target, scale=0, x=0, y=0, *, stride=None) -> None +static mp_obj_t jpegio_jpegdecoder_decode(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + jpegio_jpegdecoder_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + + enum { ARG_target, ARG_scale, ARG_x, ARG_y, ARG_stride }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_target, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_scale, MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_x, MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_y, MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_stride, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + if (!self->ready) { + mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("%q() without %q()"), MP_QSTR_decode, MP_QSTR_open); + } + mp_int_t scale = args[ARG_scale].u_int; + if (scale < 0 || scale > 3) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%q must be 0..3, not %d"), MP_QSTR_scale, (int)scale); + } + mp_int_t x = args[ARG_x].u_int; + mp_int_t y = args[ARG_y].u_int; + if (x < 0 || x > JPEGIO_MAX_COORD || y < 0 || y > JPEGIO_MAX_COORD) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("x, y must be 0..%d, not (%ld, %ld)"), JPEGIO_MAX_COORD, (long)x, (long)y); + } + size_t dw = self->width >> scale; + size_t dh = self->height >> scale; + + mp_obj_t target = args[ARG_target].u_obj; + int (*outfunc)(JDEC *, void *, JRECT *); + if (mp_obj_is_callable(target)) { + if (args[ARG_stride].u_obj != mp_const_none) { + mp_raise_TypeError(MP_ERROR_TEXT("stride applies to buffer targets only")); + } + if (self->block_view == MP_OBJ_NULL) { + #if MICROPY_PY_BUILTINS_MEMORYVIEW + self->block_view = mp_obj_new_memoryview('H', 0, NULL); + #else + self->block_view = mp_obj_new_bytearray_by_ref(0, NULL); + #endif + } + self->callback = target; + self->target = MP_OBJ_NULL; + self->pixels = NULL; + self->stride = 0; + outfunc = jpegio_callback_output; + } else { + mp_buffer_info_t buf; + mp_get_buffer_raise(target, &buf, MP_BUFFER_WRITE); + bool stride_given = args[ARG_stride].u_obj != mp_const_none; + size_t stride = dw; + if (stride_given) { + mp_int_t s = mp_obj_get_int(args[ARG_stride].u_obj); + if (s < 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%q must be >= 0, not %ld"), MP_QSTR_stride, (long)s); + } + stride = s; + } + if ((size_t)x + dw > stride) { + if (stride_given) { + mp_raise_msg_varg(&mp_type_ValueError, + MP_ERROR_TEXT("x + decoded width (%d + %d) exceeds stride %lu"), + (int)x, (int)dw, (unsigned long)stride); + } + // The default stride is the decoded width: x > 0 can never fit it. + mp_raise_msg_varg(&mp_type_ValueError, + MP_ERROR_TEXT("x + decoded width (%d + %d) exceeds the default stride %d (the decoded width): pass stride= to place at x > 0"), + (int)x, (int)dw, (int)stride); + } + // Buffer fit. The last pixel written is at index last_row * stride + + // x + dw - 1, so the buffer needs last_row * stride + tail pixels. + // x, y, dw, dh are all < 2^16, so the sums cannot overflow; the + // product can (stride is any mp_int_t, and size_t is 32 bits on the + // MCU ports), so it is compared by division instead of computed -- + // a wrapped product must not slip under buf.len. + if (dw != 0 && dh != 0) { + size_t npix = buf.len / sizeof(uint16_t); + size_t tail = (size_t)x + dw; + size_t last_row = (size_t)y + dh - 1; + bool fits = tail <= npix && (last_row == 0 || stride <= (npix - tail) / last_row); + if (!fits) { + if (last_row == 0 || stride <= (SIZE_MAX / 2 - tail) / last_row) { + size_t need = (last_row * stride + tail) * sizeof(uint16_t); + mp_raise_msg_varg(&mp_type_ValueError, + MP_ERROR_TEXT("target too small: %dx%d at (%d, %d) with stride %lu needs %lu bytes, buffer has %lu"), + (int)dw, (int)dh, (int)x, (int)y, (unsigned long)stride, (unsigned long)need, (unsigned long)buf.len); + } + mp_raise_msg_varg(&mp_type_ValueError, + MP_ERROR_TEXT("target too small: %dx%d at (%d, %d) with stride %lu: the last pixel's offset overflows size_t; buffer has %lu bytes"), + (int)dw, (int)dh, (int)x, (int)y, (unsigned long)stride, (unsigned long)buf.len); + } + } + self->callback = MP_OBJ_NULL; + self->target = target; + self->pixels = buf.buf; + self->stride = stride; + outfunc = jpegio_buffer_output; + } + self->x = x; + self->y = y; + + // One-shot, like CircuitPython: the stream is consumed, so open() again + // before the next decode(). + self->ready = false; + JRESULT rc = JDR_OK; + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + rc = jd_decomp(&self->decoder, outfunc, (uint8_t)scale); + nlr_pop(); + } else { + jpegio_decode_done(self); + nlr_jump(nlr.ret_val); + } + jpegio_decode_done(self); + if (rc != JDR_OK && rc != JDR_INTR) { + jpegio_raise_jresult(rc); + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(jpegio_jpegdecoder_decode_obj, 1, jpegio_jpegdecoder_decode); + +// width / height: read-only; track the last open() (valid after a successful +// one, through its decode(), until the next open(); 0 after a failed one). +static void jpegio_jpegdecoder_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + if (dest[0] != MP_OBJ_NULL) { + return; // store / delete: not supported (leaves dest[0] set -> AttributeError) + } + if (attr == MP_QSTR_width || attr == MP_QSTR_height) { + jpegio_jpegdecoder_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (self->width == 0 && self->height == 0) { + mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("%q needs a successful %q()"), attr, MP_QSTR_open); + } + dest[0] = MP_OBJ_NEW_SMALL_INT(attr == MP_QSTR_width ? self->width : self->height); + return; + } + dest[1] = MP_OBJ_SENTINEL; // continue the lookup in locals_dict (open, decode) +} + +static const mp_rom_map_elem_t jpegio_jpegdecoder_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_open), MP_ROM_PTR(&jpegio_jpegdecoder_open_obj) }, + { MP_ROM_QSTR(MP_QSTR_decode), MP_ROM_PTR(&jpegio_jpegdecoder_decode_obj) }, +}; +static MP_DEFINE_CONST_DICT(jpegio_jpegdecoder_locals_dict, jpegio_jpegdecoder_locals_dict_table); + +static MP_DEFINE_CONST_OBJ_TYPE( + jpegio_jpegdecoder_type, + MP_QSTR_JpegDecoder, + MP_TYPE_FLAG_NONE, + make_new, jpegio_jpegdecoder_make_new, + print, jpegio_jpegdecoder_print, + attr, jpegio_jpegdecoder_attr, + locals_dict, &jpegio_jpegdecoder_locals_dict + ); + +// --- LVGL image decoder ------------------------------------------------------ +// +// register_lvgl_decoder() and lvgl_decoders() are module attributes on EVERY +// build; only the decoder itself (lvgl_decoder.c) is conditional. Two reasons: +// the API must not silently vanish when the sibling scan misses, and on the +// CMake ports (esp32, rp2, samd, mimxrt, stm32) a QSTR referenced only inside +// `#if JPEGIO_LVGL_DECODER` is never collected -- py/mkrules.cmake builds the +// QSTR preprocessor flags from the port target's own COMPILE_DEFINITIONS, so a +// usermod's INTERFACE define never reaches makeqstrdefs.py and the build fails +// with `MP_QSTR_register_lvgl_decoder undeclared`. See README.md. + +// register_lvgl_decoder() -> bool: register jpegio's decoder with the running +// LVGL. True when this call added it, False when it was already registered +// (a second registration never adds a second decoder). RuntimeError when +// LVGL is not initialised: call lv.init() first. Needed after `import jpegio` +// ran before lv.init(), and after every lv.deinit() / lv.init() cycle (LVGL +// clears its decoder list on deinit). Without the decoder compiled in +// (JPEGIO_LVGL_DECODER 0) it always raises RuntimeError, naming why. +static mp_obj_t jpegio_register_lvgl_decoder(void) { + #if JPEGIO_LVGL_DECODER + if (jpegio_lvgl_decoder_registered()) { + return mp_const_false; + } + if (!jpegio_lvgl_decoder_register()) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("register_lvgl_decoder() needs an initialised LVGL: lv.init() first")); + } + return mp_const_true; + #else + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("jpegio was built without its LVGL decoder: lvgl-micropython was not in the build (name both modules, or rebuild with JPEGIO_LVGL=1)")); + #endif +} +static MP_DEFINE_CONST_FUN_OBJ_0(jpegio_register_lvgl_decoder_obj, jpegio_register_lvgl_decoder); + +// lvgl_decoders() -> tuple of str: the names of LVGL's registered image +// decoders in the order LVGL consults them (`("jpegio", "LODEPNG", "BIN")` +// once registered); empty before lv.init(). A diagnostic: MicroPython's +// builtin-method self check keeps `lv.image_decoder_t.get_next(None)` from +// walking the list in Python, and this is how the test proves a second +// registration added nothing. Without the decoder compiled in +// (JPEGIO_LVGL_DECODER 0) it is always the empty tuple. +static mp_obj_t jpegio_lvgl_decoders(void) { + #if JPEGIO_LVGL_DECODER + size_t n = 0; + while (jpegio_lvgl_decoder_name_at(n) != NULL) { + n++; + } + mp_obj_tuple_t *names = MP_OBJ_TO_PTR(mp_obj_new_tuple(n, NULL)); + for (size_t i = 0; i < n; i++) { + const char *name = jpegio_lvgl_decoder_name_at(i); + names->items[i] = mp_obj_new_str(name, strlen(name)); + } + return MP_OBJ_FROM_PTR(names); + #else + return mp_const_empty_tuple; + #endif +} +static MP_DEFINE_CONST_FUN_OBJ_0(jpegio_lvgl_decoders_obj, jpegio_lvgl_decoders); + +#if JPEGIO_LVGL_DECODER +// Module __init__: MicroPython calls it on `import jpegio` (every import +// statement, on ports with MICROPY_MODULE_BUILTIN_INIT). If LVGL is already +// initialised, register now, so `import lvgl; lv.init(); import jpegio` +// needs nothing more. Idempotent, and silent when LVGL is not up yet. +static mp_obj_t jpegio___init__(void) { + jpegio_lvgl_decoder_register(); // no-op (false) until lv.init() has run + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_0(jpegio___init___obj, jpegio___init__); +#endif + +// --- module ----------------------------------------------------------------- + +static const mp_rom_map_elem_t jpegio_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_jpegio) }, + { MP_ROM_QSTR(MP_QSTR_JpegDecoder), MP_ROM_PTR(&jpegio_jpegdecoder_type) }, + #if JPEGIO_LVGL_DECODER + { MP_ROM_QSTR(MP_QSTR___init__), MP_ROM_PTR(&jpegio___init___obj) }, + #endif + // Unconditional: see the LVGL section above (the API stays, and the CMake + // ports' QSTR pass cannot see a name that lives only inside the #if). + { MP_ROM_QSTR(MP_QSTR_register_lvgl_decoder), MP_ROM_PTR(&jpegio_register_lvgl_decoder_obj) }, + { MP_ROM_QSTR(MP_QSTR_lvgl_decoders), MP_ROM_PTR(&jpegio_lvgl_decoders_obj) }, +}; +static MP_DEFINE_CONST_DICT(jpegio_module_globals, jpegio_module_globals_table); + +const mp_obj_module_t jpegio_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&jpegio_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_jpegio, jpegio_module); diff --git a/modules/jpegio/src/lvgl_decoder.c b/modules/jpegio/src/lvgl_decoder.c new file mode 100644 index 00000000..d72406ce --- /dev/null +++ b/modules/jpegio/src/lvgl_decoder.c @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: MIT +// jpegio's LVGL image decoder: baseline JPEG through jpegio's own TJpgDec, +// registered with LVGL's public lv_image_decoder_create API. Phase 2 of the +// org's docs/jpegio-vision.md: LVGL's built-in TJPGD is off (LV_USE_TJPGD 0 +// in lvgl-bindings' lv_conf.h on MicroPython), so a firmware with both +// usermods carries one TJpgDec -- jpegio's, vendored in tjpgd/ -- and this +// file is that object code's second user. +// +// Structure follows lvgl/src/libs/tjpgd/lv_tjpgd.c (read, never modified) +// for registration and the info/open/close split. The pixel path does not: +// lv_tjpgd.c decodes MCU by MCU from get_area_cb through LVGL-only exports +// (jd_mcu_load, jd_mcu_output, jd_restart, JDEC.rst/rsc/pool_original) that +// ChaN's R0.03 -- the copy jpegio ships -- does not have, and it hardcodes +// RGB888 (header.cf/stride at its lines 95-98, 117-120, 207-210, 264). This +// decoder does what LVGL's own full-decode decoders do (lv_lodepng.c at +// v9.5.0): open() decodes the whole image once with jd_decomp into a draw +// buffer from LVGL's image-cache handlers, hands it back as dsc->decoded, +// and close() destroys it unless the image cache kept it. No get_area_cb: +// LVGL only calls that when open() returns no pixels, and ChaN's jd_decomp +// is all-or-nothing, so an incremental path would re-decode the frame per +// area. Output is native-order RGB565 (JD_FORMAT 1 in tjpgd/tjpgdcnf.h, +// LV_COLOR_16_SWAP unset), labelled LV_COLOR_FORMAT_RGB565, stride w * 2. +// +// Source sniff is SOI only (FF D8), for a variable source's first two bytes +// or a file's -- UVC MJPEG frames are not JFIF-first and lv_tjpgd.c's +// 10-byte JFIF signature rejects every one of them. jd_prepare then judges: +// decoder_info parses the headers for both source kinds, so width/height are +// the stream's (not echoed from the caller's lv_image_dsc_t header) and a +// pixel buffer that happens to start FF D8 falls through to LVGL's built-in +// decoder instead of being claimed. + +#include "lvgl/lvgl.h" +#include "lvgl/src/draw/lv_image_decoder_private.h" // lv_image_decoder_t.info_cb, lv_image_cache_data_t +#include "lvgl/src/core/lv_global.h" // LV_GLOBAL_DEFAULT()->image_cache_draw_buf_handlers + +#include + +#include "tjpgd.h" +#include "lvgl_decoder.h" + +#if JD_FORMAT != 1 +#error "jpegio's LVGL decoder labels its output RGB565: TJpgDec must be built with JD_FORMAT 1" +#endif +#if defined(LV_COLOR_16_SWAP) && LV_COLOR_16_SWAP /* -Wundef clean: lv_conf.h does not define it */ +#error "jpegio's LVGL decoder emits native-order RGB565; LV_COLOR_16_SWAP is not supported" +#endif + +// CircuitPython's TJpgDec work-area size, the same 3500 bytes jpegio.c gives +// its JpegDecoder (TJPGD_WORKSPACE_SIZE there). Enough for the 512-byte +// stream buffer, the tables, and a 16x16 MCU; allocated per open, freed +// before it returns. +#define JPEGIO_LVGL_WORKSPACE_SIZE 3500 + +#define image_cache_draw_buf_handlers &(LV_GLOBAL_DEFAULT()->image_cache_draw_buf_handlers) + +// One decode session: the input TJpgDec pulls from and the RGB565 rows it +// pushes to. Exactly one of buf / file is live. +typedef struct { + const uint8_t *buf; // variable source: unread remainder + size_t len; + lv_fs_file_t *file; // file source, else NULL + uint8_t *dst; // decoded->data (NULL while only preparing) + uint32_t stride; // decoded->header.stride, bytes +} jpegio_lvgl_session_t; + +static lv_result_t decoder_info(lv_image_decoder_t *decoder, lv_image_decoder_dsc_t *dsc, lv_image_header_t *header); +static lv_result_t decoder_open(lv_image_decoder_t *decoder, lv_image_decoder_dsc_t *dsc); +static void decoder_close(lv_image_decoder_t *decoder, lv_image_decoder_dsc_t *dsc); + +// --- registration ----------------------------------------------------------- + +static lv_image_decoder_t *find_registered(void) { + lv_image_decoder_t *dec = NULL; + while ((dec = lv_image_decoder_get_next(dec)) != NULL) { + if (dec->info_cb == decoder_info) { + return dec; + } + } + return NULL; +} + +bool jpegio_lvgl_decoder_registered(void) { + return lv_is_initialized() && find_registered() != NULL; +} + +const char *jpegio_lvgl_decoder_name_at(size_t i) { + if (!lv_is_initialized()) { + return NULL; + } + lv_image_decoder_t *dec = NULL; + while ((dec = lv_image_decoder_get_next(dec)) != NULL) { + if (i-- == 0) { + return dec->name != NULL ? dec->name : ""; + } + } + return NULL; +} + +bool jpegio_lvgl_decoder_register(void) { + if (!lv_is_initialized()) { + return false; + } + if (find_registered() != NULL) { + return true; // registering twice adds nothing + } + // lv_image_decoder_create inserts at the head of LVGL's list, so this + // decoder is consulted before the built-in one (which claims any + // variable source with a known cf) -- same position lv_tjpgd_init took. + lv_image_decoder_t *dec = lv_image_decoder_create(); + if (dec == NULL) { + return false; + } + lv_image_decoder_set_info_cb(dec, decoder_info); + lv_image_decoder_set_open_cb(dec, decoder_open); + lv_image_decoder_set_close_cb(dec, decoder_close); + dec->name = JPEGIO_LVGL_DECODER_NAME; + return true; +} + +// --- TJpgDec input / output ------------------------------------------------- + +static size_t input_func(JDEC *jd, uint8_t *dest, size_t len) { + jpegio_lvgl_session_t *s = jd->device; + if (s->file != NULL) { + if (dest != NULL) { + uint32_t rn = 0; + if (lv_fs_read(s->file, dest, (uint32_t)len, &rn) != LV_FS_RES_OK) { + return 0; + } + return rn; + } + uint32_t pos = 0; + lv_fs_tell(s->file, &pos); + lv_fs_seek(s->file, (uint32_t)(pos + len), LV_FS_SEEK_SET); + return len; + } + size_t n = len < s->len ? len : s->len; + if (dest != NULL) { + memcpy(dest, s->buf, n); + } + s->buf += n; + s->len -= n; + return n; +} + +// TJpgDec hands MCU blocks in raster order, clipped to the image edge, as +// w*h native-order RGB565: copy each row into the draw buffer at its place. +static int output_func(JDEC *jd, void *bitmap, JRECT *rect) { + jpegio_lvgl_session_t *s = jd->device; + size_t w = (size_t)rect->right - rect->left + 1; + size_t h = (size_t)rect->bottom - rect->top + 1; + const uint8_t *src = bitmap; + uint8_t *dst = s->dst + (size_t)rect->top * s->stride + (size_t)rect->left * 2; + for (size_t row = 0; row < h; row++) { + memcpy(dst + row * s->stride, src + row * w * 2, w * 2); + } + return 1; +} + +static bool starts_with_soi(const uint8_t *p, size_t n) { + return n >= 2 && p[0] == 0xFF && p[1] == 0xD8; +} + +// Point the session at dsc's source. For a file the caller supplies the open +// handle (decoder_info gets dsc->file from LVGL, decoder_open opens its own). +static bool session_from_source(jpegio_lvgl_session_t *s, const lv_image_decoder_dsc_t *dsc, lv_fs_file_t *file) { + memset(s, 0, sizeof(*s)); + if (dsc->src_type == LV_IMAGE_SRC_VARIABLE) { + const lv_image_dsc_t *img = dsc->src; + if (!starts_with_soi(img->data, img->data_size)) { + return false; + } + s->buf = img->data; + s->len = img->data_size; + return true; + } + if (dsc->src_type == LV_IMAGE_SRC_FILE && file != NULL) { + uint8_t soi[2]; + uint32_t rn = 0; + if (lv_fs_seek(file, 0, LV_FS_SEEK_SET) != LV_FS_RES_OK + || lv_fs_read(file, soi, sizeof(soi), &rn) != LV_FS_RES_OK + || !starts_with_soi(soi, rn) + || lv_fs_seek(file, 0, LV_FS_SEEK_SET) != LV_FS_RES_OK) { + return false; + } + s->file = file; + return true; + } + return false; +} + +// --- LVGL decoder callbacks ------------------------------------------------- + +static lv_result_t decoder_info(lv_image_decoder_t *decoder, lv_image_decoder_dsc_t *dsc, lv_image_header_t *header) { + LV_UNUSED(decoder); + jpegio_lvgl_session_t s; + // LVGL opened dsc->file for a file source and seeks it to 0 before each + // decoder's info_cb; it closes it afterwards. + if (!session_from_source(&s, dsc, dsc->src_type == LV_IMAGE_SRC_FILE ? &dsc->file : NULL)) { + return LV_RESULT_INVALID; + } + uint8_t *work = lv_malloc(JPEGIO_LVGL_WORKSPACE_SIZE); + if (work == NULL) { + return LV_RESULT_INVALID; + } + JDEC jd; + JRESULT rc = jd_prepare(&jd, input_func, work, JPEGIO_LVGL_WORKSPACE_SIZE, &s); + lv_free(work); + if (rc != JDR_OK) { + LV_LOG_INFO("jpegio: jd_prepare error %d", rc); + return LV_RESULT_INVALID; + } + header->cf = LV_COLOR_FORMAT_RGB565; + header->w = jd.width; + header->h = jd.height; + header->stride = (uint32_t)jd.width * 2; + return LV_RESULT_OK; +} + +static lv_result_t decoder_open(lv_image_decoder_t *decoder, lv_image_decoder_dsc_t *dsc) { + lv_fs_file_t *f = NULL; + if (dsc->src_type == LV_IMAGE_SRC_FILE) { + f = lv_malloc(sizeof(lv_fs_file_t)); + if (f == NULL) { + return LV_RESULT_INVALID; + } + if (lv_fs_open(f, dsc->src, LV_FS_MODE_RD) != LV_FS_RES_OK) { + lv_free(f); + return LV_RESULT_INVALID; + } + } + jpegio_lvgl_session_t s; + lv_draw_buf_t *decoded = NULL; + uint8_t *work = NULL; + JRESULT rc = JDR_FMT1; + + if (session_from_source(&s, dsc, f)) { + work = lv_malloc(JPEGIO_LVGL_WORKSPACE_SIZE); + if (work != NULL) { + JDEC jd; + rc = jd_prepare(&jd, input_func, work, JPEGIO_LVGL_WORKSPACE_SIZE, &s); + if (rc == JDR_OK) { + decoded = lv_draw_buf_create_ex(image_cache_draw_buf_handlers, jd.width, jd.height, + LV_COLOR_FORMAT_RGB565, LV_STRIDE_AUTO); + if (decoded != NULL) { + s.dst = decoded->data; + s.stride = decoded->header.stride; + rc = jd_decomp(&jd, output_func, 0); + } else { + rc = JDR_MEM1; + } + } + lv_free(work); + } else { + rc = JDR_MEM1; + } + } + if (f != NULL) { + lv_fs_close(f); + lv_free(f); + } + if (rc != JDR_OK) { + LV_LOG_WARN("jpegio: decode error %d", rc); + if (decoded != NULL) { + lv_draw_buf_destroy(decoded); + } + return LV_RESULT_INVALID; + } + + // From here the same as lv_lodepng.c's decoder_open at v9.5.0. + lv_draw_buf_t *adjusted = lv_image_decoder_post_process(dsc, decoded); + if (adjusted == NULL) { + lv_draw_buf_destroy(decoded); + return LV_RESULT_INVALID; + } + if (adjusted != decoded) { + lv_draw_buf_destroy(decoded); + decoded = adjusted; + } + dsc->decoded = decoded; + + if (dsc->args.no_cache || !lv_image_cache_is_enabled()) { + return LV_RESULT_OK; + } + lv_image_cache_data_t search_key; + search_key.src_type = dsc->src_type; + search_key.src = dsc->src; + search_key.slot.size = decoded->data_size; + lv_cache_entry_t *entry = lv_image_decoder_add_to_cache(decoder, &search_key, decoded, NULL); + if (entry == NULL) { + lv_draw_buf_destroy(decoded); + dsc->decoded = NULL; + return LV_RESULT_INVALID; + } + dsc->cache_entry = entry; + return LV_RESULT_OK; +} + +static void decoder_close(lv_image_decoder_t *decoder, lv_image_decoder_dsc_t *dsc) { + LV_UNUSED(decoder); + if (dsc->args.no_cache || !lv_image_cache_is_enabled()) { + lv_draw_buf_destroy((lv_draw_buf_t *)dsc->decoded); + } +} diff --git a/modules/jpegio/src/lvgl_decoder.h b/modules/jpegio/src/lvgl_decoder.h new file mode 100644 index 00000000..96318c2b --- /dev/null +++ b/modules/jpegio/src/lvgl_decoder.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +// jpegio's LVGL image decoder (lvgl_decoder.c). Compiled only when the +// lvgl-micropython sibling usermod is on the same build (JPEGIO_LVGL_DECODER). +#ifndef JPEGIO_LVGL_DECODER_H +#define JPEGIO_LVGL_DECODER_H + +#include +#include + +// The name LVGL reports for it (lv_image_decoder_t.name). +#define JPEGIO_LVGL_DECODER_NAME "jpegio" + +// Register jpegio's decoder with the running LVGL (lv_image_decoder_create). +// Idempotent: a second call finds the first registration and adds nothing. +// Returns true when the decoder is registered after the call, false when LVGL +// is not initialised (lv_init() first) or the registration allocation failed. +bool jpegio_lvgl_decoder_register(void); + +// True when LVGL is initialised and jpegio's decoder is in its decoder list. +bool jpegio_lvgl_decoder_registered(void); + +// Name of the i-th decoder in LVGL's list (the order LVGL consults them), +// NULL past the end or when LVGL is not initialised. Diagnostic for +// jpegio.lvgl_decoders(): the binding cannot walk the list from Python. +const char *jpegio_lvgl_decoder_name_at(size_t i); + +#endif diff --git a/modules/jpegio/src/tjpgd/tjpgd.c b/modules/jpegio/src/tjpgd/tjpgd.c new file mode 100644 index 00000000..576399c1 --- /dev/null +++ b/modules/jpegio/src/tjpgd/tjpgd.c @@ -0,0 +1,1157 @@ +/*----------------------------------------------------------------------------/ +/ TJpgDec - Tiny JPEG Decompressor R0.03 w/patch1 (C)ChaN, 2021 +/-----------------------------------------------------------------------------/ +/ The TJpgDec is a generic JPEG decompressor module for tiny embedded systems. +/ This is a free software that opened for education, research and commercial +/ developments under license policy of following terms. +/ +/ Copyright (C) 2021, ChaN, all right reserved. +/ +/ * The TJpgDec module is a free software and there is NO WARRANTY. +/ * No restriction on use. You can use, modify and redistribute it for +/ personal, non-profit or commercial products UNDER YOUR RESPONSIBILITY. +/ * Redistributions of source code must retain the above copyright notice. +/ +/-----------------------------------------------------------------------------/ +/ Oct 04, 2011 R0.01 First release. +/ Feb 19, 2012 R0.01a Fixed decompression fails when scan starts with an escape seq. +/ Sep 03, 2012 R0.01b Added JD_TBLCLIP option. +/ Mar 16, 2019 R0.01c Supprted stdint.h. +/ Jul 01, 2020 R0.01d Fixed wrong integer type usage. +/ May 08, 2021 R0.02 Supprted grayscale image. Separated configuration options. +/ Jun 11, 2021 R0.02a Some performance improvement. +/ Jul 01, 2021 R0.03 Added JD_FASTDECODE option. +/ Some performance improvement. +/----------------------------------------------------------------------------*/ + +#include "tjpgd.h" + + +#if JD_FASTDECODE == 2 +#define HUFF_BIT 10 /* Bit length to apply fast huffman decode */ +#define HUFF_LEN (1 << HUFF_BIT) +#define HUFF_MASK (HUFF_LEN - 1) +#endif + + +/*-----------------------------------------------*/ +/* Zigzag-order to raster-order conversion table */ +/*-----------------------------------------------*/ + +static const uint8_t Zig[64] = { /* Zigzag-order to raster-order conversion table */ + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63 +}; + + + +/*-------------------------------------------------*/ +/* Input scale factor of Arai algorithm */ +/* (scaled up 16 bits for fixed point operations) */ +/*-------------------------------------------------*/ + +static const uint16_t Ipsf[64] = { /* See also aa_idct.png */ + (uint16_t)(1.00000*8192), (uint16_t)(1.38704*8192), (uint16_t)(1.30656*8192), (uint16_t)(1.17588*8192), (uint16_t)(1.00000*8192), (uint16_t)(0.78570*8192), (uint16_t)(0.54120*8192), (uint16_t)(0.27590*8192), + (uint16_t)(1.38704*8192), (uint16_t)(1.92388*8192), (uint16_t)(1.81226*8192), (uint16_t)(1.63099*8192), (uint16_t)(1.38704*8192), (uint16_t)(1.08979*8192), (uint16_t)(0.75066*8192), (uint16_t)(0.38268*8192), + (uint16_t)(1.30656*8192), (uint16_t)(1.81226*8192), (uint16_t)(1.70711*8192), (uint16_t)(1.53636*8192), (uint16_t)(1.30656*8192), (uint16_t)(1.02656*8192), (uint16_t)(0.70711*8192), (uint16_t)(0.36048*8192), + (uint16_t)(1.17588*8192), (uint16_t)(1.63099*8192), (uint16_t)(1.53636*8192), (uint16_t)(1.38268*8192), (uint16_t)(1.17588*8192), (uint16_t)(0.92388*8192), (uint16_t)(0.63638*8192), (uint16_t)(0.32442*8192), + (uint16_t)(1.00000*8192), (uint16_t)(1.38704*8192), (uint16_t)(1.30656*8192), (uint16_t)(1.17588*8192), (uint16_t)(1.00000*8192), (uint16_t)(0.78570*8192), (uint16_t)(0.54120*8192), (uint16_t)(0.27590*8192), + (uint16_t)(0.78570*8192), (uint16_t)(1.08979*8192), (uint16_t)(1.02656*8192), (uint16_t)(0.92388*8192), (uint16_t)(0.78570*8192), (uint16_t)(0.61732*8192), (uint16_t)(0.42522*8192), (uint16_t)(0.21677*8192), + (uint16_t)(0.54120*8192), (uint16_t)(0.75066*8192), (uint16_t)(0.70711*8192), (uint16_t)(0.63638*8192), (uint16_t)(0.54120*8192), (uint16_t)(0.42522*8192), (uint16_t)(0.29290*8192), (uint16_t)(0.14932*8192), + (uint16_t)(0.27590*8192), (uint16_t)(0.38268*8192), (uint16_t)(0.36048*8192), (uint16_t)(0.32442*8192), (uint16_t)(0.27590*8192), (uint16_t)(0.21678*8192), (uint16_t)(0.14932*8192), (uint16_t)(0.07612*8192) +}; + + + +/*---------------------------------------------*/ +/* Conversion table for fast clipping process */ +/*---------------------------------------------*/ + +#if JD_TBLCLIP + +#define BYTECLIP(v) Clip8[(unsigned int)(v) & 0x3FF] + +static const uint8_t Clip8[1024] = { + /* 0..255 */ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, + 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, + /* 256..511 */ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + /* -512..-257 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + /* -256..-1 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +#else /* JD_TBLCLIP */ + +static uint8_t BYTECLIP (int val) +{ + if (val < 0) return 0; + if (val > 255) return 255; + return (uint8_t)val; +} + +#endif + + + +/*-----------------------------------------------------------------------*/ +/* Allocate a memory block from memory pool */ +/*-----------------------------------------------------------------------*/ + +static void* alloc_pool ( /* Pointer to allocated memory block (NULL:no memory available) */ + JDEC* jd, /* Pointer to the decompressor object */ + size_t ndata /* Number of bytes to allocate */ +) +{ + char *rp = 0; + + + ndata = (ndata + 3) & ~3; /* Align block size to the word boundary */ + + if (jd->sz_pool >= ndata) { + jd->sz_pool -= ndata; + rp = (char*)jd->pool; /* Get start of available memory pool */ + jd->pool = (void*)(rp + ndata); /* Allocate requierd bytes */ + } + + return (void*)rp; /* Return allocated memory block (NULL:no memory to allocate) */ +} + + + + +/*-----------------------------------------------------------------------*/ +/* Create de-quantization and prescaling tables with a DQT segment */ +/*-----------------------------------------------------------------------*/ + +static JRESULT create_qt_tbl ( /* 0:OK, !0:Failed */ + JDEC* jd, /* Pointer to the decompressor object */ + const uint8_t* data, /* Pointer to the quantizer tables */ + size_t ndata /* Size of input data */ +) +{ + unsigned int i, zi; + uint8_t d; + int32_t *pb; + + + while (ndata) { /* Process all tables in the segment */ + if (ndata < 65) return JDR_FMT1; /* Err: table size is unaligned */ + ndata -= 65; + d = *data++; /* Get table property */ + if (d & 0xF0) return JDR_FMT1; /* Err: not 8-bit resolution */ + i = d & 3; /* Get table ID */ + pb = alloc_pool(jd, 64 * sizeof (int32_t));/* Allocate a memory block for the table */ + if (!pb) return JDR_MEM1; /* Err: not enough memory */ + jd->qttbl[i] = pb; /* Register the table */ + for (i = 0; i < 64; i++) { /* Load the table */ + zi = Zig[i]; /* Zigzag-order to raster-order conversion */ + pb[zi] = (int32_t)((uint32_t)*data++ * Ipsf[zi]); /* Apply scale factor of Arai algorithm to the de-quantizers */ + } + } + + return JDR_OK; +} + + + + +/*-----------------------------------------------------------------------*/ +/* Create huffman code tables with a DHT segment */ +/*-----------------------------------------------------------------------*/ + +static JRESULT create_huffman_tbl ( /* 0:OK, !0:Failed */ + JDEC* jd, /* Pointer to the decompressor object */ + const uint8_t* data, /* Pointer to the packed huffman tables */ + size_t ndata /* Size of input data */ +) +{ + unsigned int i, j, b, cls, num; + size_t np; + uint8_t d, *pb, *pd; + uint16_t hc, *ph; + + + while (ndata) { /* Process all tables in the segment */ + if (ndata < 17) return JDR_FMT1; /* Err: wrong data size */ + ndata -= 17; + d = *data++; /* Get table number and class */ + if (d & 0xEE) return JDR_FMT1; /* Err: invalid class/number */ + cls = d >> 4; num = d & 0x0F; /* class = dc(0)/ac(1), table number = 0/1 */ + pb = alloc_pool(jd, 16); /* Allocate a memory block for the bit distribution table */ + if (!pb) return JDR_MEM1; /* Err: not enough memory */ + jd->huffbits[num][cls] = pb; + for (np = i = 0; i < 16; i++) { /* Load number of patterns for 1 to 16-bit code */ + np += (pb[i] = *data++); /* Get sum of code words for each code */ + } + ph = alloc_pool(jd, np * sizeof (uint16_t));/* Allocate a memory block for the code word table */ + if (!ph) return JDR_MEM1; /* Err: not enough memory */ + jd->huffcode[num][cls] = ph; + hc = 0; + for (j = i = 0; i < 16; i++) { /* Re-build huffman code word table */ + b = pb[i]; + while (b--) ph[j++] = hc++; + hc <<= 1; + } + + if (ndata < np) return JDR_FMT1; /* Err: wrong data size */ + ndata -= np; + pd = alloc_pool(jd, np); /* Allocate a memory block for the decoded data */ + if (!pd) return JDR_MEM1; /* Err: not enough memory */ + jd->huffdata[num][cls] = pd; + for (i = 0; i < np; i++) { /* Load decoded data corresponds to each code word */ + d = *data++; + if (!cls && d > 11) return JDR_FMT1; + pd[i] = d; + } +#if JD_FASTDECODE == 2 + { /* Create fast huffman decode table */ + unsigned int span, td, ti; + uint16_t *tbl_ac = 0; + uint8_t *tbl_dc = 0; + + if (cls) { + tbl_ac = alloc_pool(jd, HUFF_LEN * sizeof (uint16_t)); /* LUT for AC elements */ + if (!tbl_ac) return JDR_MEM1; /* Err: not enough memory */ + jd->hufflut_ac[num] = tbl_ac; + memset(tbl_ac, 0xFF, HUFF_LEN * sizeof (uint16_t)); /* Default value (0xFFFF: may be long code) */ + } else { + tbl_dc = alloc_pool(jd, HUFF_LEN * sizeof (uint8_t)); /* LUT for AC elements */ + if (!tbl_dc) return JDR_MEM1; /* Err: not enough memory */ + jd->hufflut_dc[num] = tbl_dc; + memset(tbl_dc, 0xFF, HUFF_LEN * sizeof (uint8_t)); /* Default value (0xFF: may be long code) */ + } + for (i = b = 0; b < HUFF_BIT; b++) { /* Create LUT */ + for (j = pb[b]; j; j--) { + ti = ph[i] << (HUFF_BIT - 1 - b) & HUFF_MASK; /* Index of input pattern for the code */ + if (cls) { + td = pd[i++] | ((b + 1) << 8); /* b15..b8: code length, b7..b0: zero run and data length */ + for (span = 1 << (HUFF_BIT - 1 - b); span; span--, tbl_ac[ti++] = (uint16_t)td) ; + } else { + td = pd[i++] | ((b + 1) << 4); /* b7..b4: code length, b3..b0: data length */ + for (span = 1 << (HUFF_BIT - 1 - b); span; span--, tbl_dc[ti++] = (uint8_t)td) ; + } + } + } + jd->longofs[num][cls] = i; /* Code table offset for long code */ + } +#endif + } + + return JDR_OK; +} + + + + +/*-----------------------------------------------------------------------*/ +/* Extract a huffman decoded data from input stream */ +/*-----------------------------------------------------------------------*/ + +static int huffext ( /* >=0: decoded data, <0: error code */ + JDEC* jd, /* Pointer to the decompressor object */ + unsigned int id, /* Table ID (0:Y, 1:C) */ + unsigned int cls /* Table class (0:DC, 1:AC) */ +) +{ + size_t dc = jd->dctr; + uint8_t *dp = jd->dptr; + unsigned int d, flg = 0; + +#if JD_FASTDECODE == 0 + uint8_t bm, nd, bl; + const uint8_t *hb = jd->huffbits[id][cls]; /* Bit distribution table */ + const uint16_t *hc = jd->huffcode[id][cls]; /* Code word table */ + const uint8_t *hd = jd->huffdata[id][cls]; /* Data table */ + + + bm = jd->dbit; /* Bit mask to extract */ + d = 0; bl = 16; /* Max code length */ + do { + if (!bm) { /* Next byte? */ + if (!dc) { /* No input data is available, re-fill input buffer */ + dp = jd->inbuf; /* Top of input buffer */ + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return 0 - (int)JDR_INP; /* Err: read error or wrong stream termination */ + } else { + dp++; /* Next data ptr */ + } + dc--; /* Decrement number of available bytes */ + if (flg) { /* In flag sequence? */ + flg = 0; /* Exit flag sequence */ + if (*dp != 0) return 0 - (int)JDR_FMT1; /* Err: unexpected flag is detected (may be collapted data) */ + *dp = 0xFF; /* The flag is a data 0xFF */ + } else { + if (*dp == 0xFF) { /* Is start of flag sequence? */ + flg = 1; continue; /* Enter flag sequence, get trailing byte */ + } + } + bm = 0x80; /* Read from MSB */ + } + d <<= 1; /* Get a bit */ + if (*dp & bm) d++; + bm >>= 1; + + for (nd = *hb++; nd; nd--) { /* Search the code word in this bit length */ + if (d == *hc++) { /* Matched? */ + jd->dbit = bm; jd->dctr = dc; jd->dptr = dp; + return *hd; /* Return the decoded data */ + } + hd++; + } + bl--; + } while (bl); + +#else + const uint8_t *hb, *hd; + const uint16_t *hc; + unsigned int nc, bl, wbit = jd->dbit % 32; + uint32_t w = jd->wreg & ((1UL << wbit) - 1); + + + while (wbit < 16) { /* Prepare 16 bits into the working register */ + if (jd->marker) { + d = 0xFF; /* Input stream has stalled for a marker. Generate stuff bits */ + } else { + if (!dc) { /* Buffer empty, re-fill input buffer */ + dp = jd->inbuf; /* Top of input buffer */ + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return 0 - (int)JDR_INP; /* Err: read error or wrong stream termination */ + } + d = *dp++; dc--; + if (flg) { /* In flag sequence? */ + flg = 0; /* Exit flag sequence */ + if (d != 0) jd->marker = d; /* Not an escape of 0xFF but a marker */ + d = 0xFF; + } else { + if (d == 0xFF) { /* Is start of flag sequence? */ + flg = 1; continue; /* Enter flag sequence, get trailing byte */ + } + } + } + w = w << 8 | d; /* Shift 8 bits in the working register */ + wbit += 8; + } + jd->dctr = dc; jd->dptr = dp; + jd->wreg = w; + +#if JD_FASTDECODE == 2 + /* Table serch for the short codes */ + d = (unsigned int)(w >> (wbit - HUFF_BIT)); /* Short code as table index */ + if (cls) { /* AC element */ + d = jd->hufflut_ac[id][d]; /* Table decode */ + if (d != 0xFFFF) { /* It is done if hit in short code */ + jd->dbit = wbit - (d >> 8); /* Snip the code length */ + return d & 0xFF; /* b7..0: zero run and following data bits */ + } + } else { /* DC element */ + d = jd->hufflut_dc[id][d]; /* Table decode */ + if (d != 0xFF) { /* It is done if hit in short code */ + jd->dbit = wbit - (d >> 4); /* Snip the code length */ + return d & 0xF; /* b3..0: following data bits */ + } + } + + /* Incremental serch for the codes longer than HUFF_BIT */ + hb = jd->huffbits[id][cls] + HUFF_BIT; /* Bit distribution table */ + hc = jd->huffcode[id][cls] + jd->longofs[id][cls]; /* Code word table */ + hd = jd->huffdata[id][cls] + jd->longofs[id][cls]; /* Data table */ + bl = HUFF_BIT + 1; +#else + /* Incremental serch for all codes */ + hb = jd->huffbits[id][cls]; /* Bit distribution table */ + hc = jd->huffcode[id][cls]; /* Code word table */ + hd = jd->huffdata[id][cls]; /* Data table */ + bl = 1; +#endif + for ( ; bl <= 16; bl++) { /* Incremental search */ + nc = *hb++; + if (nc) { + d = w >> (wbit - bl); + do { /* Search the code word in this bit length */ + if (d == *hc++) { /* Matched? */ + jd->dbit = wbit - bl; /* Snip the huffman code */ + return *hd; /* Return the decoded data */ + } + hd++; + } while (--nc); + } + } +#endif + + return 0 - (int)JDR_FMT1; /* Err: code not found (may be collapted data) */ +} + + + + +/*-----------------------------------------------------------------------*/ +/* Extract N bits from input stream */ +/*-----------------------------------------------------------------------*/ + +static int bitext ( /* >=0: extracted data, <0: error code */ + JDEC* jd, /* Pointer to the decompressor object */ + unsigned int nbit /* Number of bits to extract (1 to 16) */ +) +{ + size_t dc = jd->dctr; + uint8_t *dp = jd->dptr; + unsigned int d, flg = 0; + +#if JD_FASTDECODE == 0 + uint8_t mbit = jd->dbit; + + d = 0; + do { + if (!mbit) { /* Next byte? */ + if (!dc) { /* No input data is available, re-fill input buffer */ + dp = jd->inbuf; /* Top of input buffer */ + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return 0 - (int)JDR_INP; /* Err: read error or wrong stream termination */ + } else { + dp++; /* Next data ptr */ + } + dc--; /* Decrement number of available bytes */ + if (flg) { /* In flag sequence? */ + flg = 0; /* Exit flag sequence */ + if (*dp != 0) return 0 - (int)JDR_FMT1; /* Err: unexpected flag is detected (may be collapted data) */ + *dp = 0xFF; /* The flag is a data 0xFF */ + } else { + if (*dp == 0xFF) { /* Is start of flag sequence? */ + flg = 1; continue; /* Enter flag sequence */ + } + } + mbit = 0x80; /* Read from MSB */ + } + d <<= 1; /* Get a bit */ + if (*dp & mbit) d |= 1; + mbit >>= 1; + nbit--; + } while (nbit); + + jd->dbit = mbit; jd->dctr = dc; jd->dptr = dp; + return (int)d; + +#else + unsigned int wbit = jd->dbit % 32; + uint32_t w = jd->wreg & ((1UL << wbit) - 1); + + + while (wbit < nbit) { /* Prepare nbit bits into the working register */ + if (jd->marker) { + d = 0xFF; /* Input stream stalled, generate stuff bits */ + } else { + if (!dc) { /* Buffer empty, re-fill input buffer */ + dp = jd->inbuf; /* Top of input buffer */ + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return 0 - (int)JDR_INP; /* Err: read error or wrong stream termination */ + } + d = *dp++; dc--; + if (flg) { /* In flag sequence? */ + flg = 0; /* Exit flag sequence */ + if (d != 0) jd->marker = d; /* Not an escape of 0xFF but a marker */ + d = 0xFF; + } else { + if (d == 0xFF) { /* Is start of flag sequence? */ + flg = 1; continue; /* Enter flag sequence, get trailing byte */ + } + } + } + w = w << 8 | d; /* Get 8 bits into the working register */ + wbit += 8; + } + jd->wreg = w; jd->dbit = wbit - nbit; + jd->dctr = dc; jd->dptr = dp; + + return (int)(w >> ((wbit - nbit) % 32)); +#endif +} + + + + +/*-----------------------------------------------------------------------*/ +/* Process restart interval */ +/*-----------------------------------------------------------------------*/ + +static JRESULT restart ( + JDEC* jd, /* Pointer to the decompressor object */ + uint16_t rstn /* Expected restert sequense number */ +) +{ + unsigned int i; + uint8_t *dp = jd->dptr; + size_t dc = jd->dctr; + +#if JD_FASTDECODE == 0 + uint16_t d = 0; + + /* Get two bytes from the input stream */ + for (i = 0; i < 2; i++) { + if (!dc) { /* No input data is available, re-fill input buffer */ + dp = jd->inbuf; + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return JDR_INP; + } else { + dp++; + } + dc--; + d = d << 8 | *dp; /* Get a byte */ + } + jd->dptr = dp; jd->dctr = dc; jd->dbit = 0; + + /* Check the marker */ + if ((d & 0xFFD8) != 0xFFD0 || (d & 7) != (rstn & 7)) { + return JDR_FMT1; /* Err: expected RSTn marker is not detected (may be collapted data) */ + } + +#else + uint16_t marker; + + + if (jd->marker) { /* Generate a maker if it has been detected */ + marker = 0xFF00 | jd->marker; + jd->marker = 0; + } else { + marker = 0; + for (i = 0; i < 2; i++) { /* Get a restart marker */ + if (!dc) { /* No input data is available, re-fill input buffer */ + dp = jd->inbuf; + dc = jd->infunc(jd, dp, JD_SZBUF); + if (!dc) return JDR_INP; + } + marker = (marker << 8) | *dp++; /* Get a byte */ + dc--; + } + jd->dptr = dp; jd->dctr = dc; + } + + /* Check the marker */ + if ((marker & 0xFFD8) != 0xFFD0 || (marker & 7) != (rstn & 7)) { + return JDR_FMT1; /* Err: expected RSTn marker was not detected (may be collapted data) */ + } + + jd->dbit = 0; /* Discard stuff bits */ +#endif + + jd->dcv[2] = jd->dcv[1] = jd->dcv[0] = 0; /* Reset DC offset */ + return JDR_OK; +} + + + + +/*-----------------------------------------------------------------------*/ +/* Apply Inverse-DCT in Arai Algorithm (see also aa_idct.png) */ +/*-----------------------------------------------------------------------*/ + +static void block_idct ( + int32_t* src, /* Input block data (de-quantized and pre-scaled for Arai Algorithm) */ + jd_yuv_t* dst /* Pointer to the destination to store the block as byte array */ +) +{ + const int32_t M13 = (int32_t)(1.41421*4096), M2 = (int32_t)(1.08239*4096), M4 = (int32_t)(2.61313*4096), M5 = (int32_t)(1.84776*4096); + int32_t v0, v1, v2, v3, v4, v5, v6, v7; + int32_t t10, t11, t12, t13; + int i; + + /* Process columns */ + for (i = 0; i < 8; i++) { + v0 = src[8 * 0]; /* Get even elements */ + v1 = src[8 * 2]; + v2 = src[8 * 4]; + v3 = src[8 * 6]; + + t10 = v0 + v2; /* Process the even elements */ + t12 = v0 - v2; + t11 = (v1 - v3) * M13 >> 12; + v3 += v1; + t11 -= v3; + v0 = t10 + v3; + v3 = t10 - v3; + v1 = t11 + t12; + v2 = t12 - t11; + + v4 = src[8 * 7]; /* Get odd elements */ + v5 = src[8 * 1]; + v6 = src[8 * 5]; + v7 = src[8 * 3]; + + t10 = v5 - v4; /* Process the odd elements */ + t11 = v5 + v4; + t12 = v6 - v7; + v7 += v6; + v5 = (t11 - v7) * M13 >> 12; + v7 += t11; + t13 = (t10 + t12) * M5 >> 12; + v4 = t13 - (t10 * M2 >> 12); + v6 = t13 - (t12 * M4 >> 12) - v7; + v5 -= v6; + v4 -= v5; + + src[8 * 0] = v0 + v7; /* Write-back transformed values */ + src[8 * 7] = v0 - v7; + src[8 * 1] = v1 + v6; + src[8 * 6] = v1 - v6; + src[8 * 2] = v2 + v5; + src[8 * 5] = v2 - v5; + src[8 * 3] = v3 + v4; + src[8 * 4] = v3 - v4; + + src++; /* Next column */ + } + + /* Process rows */ + src -= 8; + for (i = 0; i < 8; i++) { + v0 = src[0] + (128L << 8); /* Get even elements (remove DC offset (-128) here) */ + v1 = src[2]; + v2 = src[4]; + v3 = src[6]; + + t10 = v0 + v2; /* Process the even elements */ + t12 = v0 - v2; + t11 = (v1 - v3) * M13 >> 12; + v3 += v1; + t11 -= v3; + v0 = t10 + v3; + v3 = t10 - v3; + v1 = t11 + t12; + v2 = t12 - t11; + + v4 = src[7]; /* Get odd elements */ + v5 = src[1]; + v6 = src[5]; + v7 = src[3]; + + t10 = v5 - v4; /* Process the odd elements */ + t11 = v5 + v4; + t12 = v6 - v7; + v7 += v6; + v5 = (t11 - v7) * M13 >> 12; + v7 += t11; + t13 = (t10 + t12) * M5 >> 12; + v4 = t13 - (t10 * M2 >> 12); + v6 = t13 - (t12 * M4 >> 12) - v7; + v5 -= v6; + v4 -= v5; + + /* Descale the transformed values 8 bits and output a row */ +#if JD_FASTDECODE >= 1 + dst[0] = (int16_t)((v0 + v7) >> 8); + dst[7] = (int16_t)((v0 - v7) >> 8); + dst[1] = (int16_t)((v1 + v6) >> 8); + dst[6] = (int16_t)((v1 - v6) >> 8); + dst[2] = (int16_t)((v2 + v5) >> 8); + dst[5] = (int16_t)((v2 - v5) >> 8); + dst[3] = (int16_t)((v3 + v4) >> 8); + dst[4] = (int16_t)((v3 - v4) >> 8); +#else + dst[0] = BYTECLIP((v0 + v7) >> 8); + dst[7] = BYTECLIP((v0 - v7) >> 8); + dst[1] = BYTECLIP((v1 + v6) >> 8); + dst[6] = BYTECLIP((v1 - v6) >> 8); + dst[2] = BYTECLIP((v2 + v5) >> 8); + dst[5] = BYTECLIP((v2 - v5) >> 8); + dst[3] = BYTECLIP((v3 + v4) >> 8); + dst[4] = BYTECLIP((v3 - v4) >> 8); +#endif + + dst += 8; src += 8; /* Next row */ + } +} + + + + +/*-----------------------------------------------------------------------*/ +/* Load all blocks in an MCU into working buffer */ +/*-----------------------------------------------------------------------*/ + +static JRESULT mcu_load ( + JDEC* jd /* Pointer to the decompressor object */ +) +{ + int32_t *tmp = (int32_t*)jd->workbuf; /* Block working buffer for de-quantize and IDCT */ + int d, e; + unsigned int blk, nby, i, bc, z, id, cmp; + jd_yuv_t *bp; + const int32_t *dqf; + + + nby = jd->msx * jd->msy; /* Number of Y blocks (1, 2 or 4) */ + bp = jd->mcubuf; /* Pointer to the first block of MCU */ + + for (blk = 0; blk < nby + 2; blk++) { /* Get nby Y blocks and two C blocks */ + cmp = (blk < nby) ? 0 : blk - nby + 1; /* Component number 0:Y, 1:Cb, 2:Cr */ + + if (cmp && jd->ncomp != 3) { /* Clear C blocks if not exist (monochrome image) */ + for (i = 0; i < 64; bp[i++] = 128) ; + + } else { /* Load Y/C blocks from input stream */ + id = cmp ? 1 : 0; /* Huffman table ID of this component */ + + /* Extract a DC element from input stream */ + d = huffext(jd, id, 0); /* Extract a huffman coded data (bit length) */ + if (d < 0) return (JRESULT)(0 - d); /* Err: invalid code or input */ + bc = (unsigned int)d; + d = jd->dcv[cmp]; /* DC value of previous block */ + if (bc) { /* If there is any difference from previous block */ + e = bitext(jd, bc); /* Extract data bits */ + if (e < 0) return (JRESULT)(0 - e); /* Err: input */ + bc = 1 << (bc - 1); /* MSB position */ + if (!(e & bc)) e -= (bc << 1) - 1; /* Restore negative value if needed */ + d += e; /* Get current value */ + jd->dcv[cmp] = (int16_t)d; /* Save current DC value for next block */ + } + dqf = jd->qttbl[jd->qtid[cmp]]; /* De-quantizer table ID for this component */ + tmp[0] = d * dqf[0] >> 8; /* De-quantize, apply scale factor of Arai algorithm and descale 8 bits */ + + /* Extract following 63 AC elements from input stream */ + memset(&tmp[1], 0, 63 * sizeof (int32_t)); /* Initialize all AC elements */ + z = 1; /* Top of the AC elements (in zigzag-order) */ + do { + d = huffext(jd, id, 1); /* Extract a huffman coded value (zero runs and bit length) */ + if (d == 0) break; /* EOB? */ + if (d < 0) return (JRESULT)(0 - d); /* Err: invalid code or input error */ + bc = (unsigned int)d; + z += bc >> 4; /* Skip leading zero run */ + if (z >= 64) return JDR_FMT1; /* Too long zero run */ + if (bc &= 0x0F) { /* Bit length? */ + d = bitext(jd, bc); /* Extract data bits */ + if (d < 0) return (JRESULT)(0 - d); /* Err: input device */ + bc = 1 << (bc - 1); /* MSB position */ + if (!(d & bc)) d -= (bc << 1) - 1; /* Restore negative value if needed */ + i = Zig[z]; /* Get raster-order index */ + tmp[i] = d * dqf[i] >> 8; /* De-quantize, apply scale factor of Arai algorithm and descale 8 bits */ + } + } while (++z < 64); /* Next AC element */ + + if (JD_FORMAT != 2 || !cmp) { /* C components may not be processed if in grayscale output */ + if (z == 1 || (JD_USE_SCALE && jd->scale == 3)) { /* If no AC element or scale ratio is 1/8, IDCT can be ommited and the block is filled with DC value */ + d = (jd_yuv_t)((*tmp / 256) + 128); + if (JD_FASTDECODE >= 1) { + for (i = 0; i < 64; bp[i++] = d) ; + } else { + memset(bp, d, 64); + } + } else { + block_idct(tmp, bp); /* Apply IDCT and store the block to the MCU buffer */ + } + } + } + + bp += 64; /* Next block */ + } + + return JDR_OK; /* All blocks have been loaded successfully */ +} + + + + +/*-----------------------------------------------------------------------*/ +/* Output an MCU: Convert YCrCb to RGB and output it in RGB form */ +/*-----------------------------------------------------------------------*/ + +static JRESULT mcu_output ( + JDEC* jd, /* Pointer to the decompressor object */ + int (*outfunc)(JDEC*, void*, JRECT*), /* RGB output function */ + unsigned int x, /* MCU location in the image */ + unsigned int y /* MCU location in the image */ +) +{ + const int CVACC = (sizeof (int) > 2) ? 1024 : 128; /* Adaptive accuracy for both 16-/32-bit systems */ + unsigned int ix, iy, mx, my, rx, ry; + int yy, cb, cr; + jd_yuv_t *py, *pc; + uint8_t *pix; + JRECT rect; + + + mx = jd->msx * 8; my = jd->msy * 8; /* MCU size (pixel) */ + rx = (x + mx <= jd->width) ? mx : jd->width - x; /* Output rectangular size (it may be clipped at right/bottom end of image) */ + ry = (y + my <= jd->height) ? my : jd->height - y; + if (JD_USE_SCALE) { + rx >>= jd->scale; ry >>= jd->scale; + if (!rx || !ry) return JDR_OK; /* Skip this MCU if all pixel is to be rounded off */ + x >>= jd->scale; y >>= jd->scale; + } + rect.left = x; rect.right = x + rx - 1; /* Rectangular area in the frame buffer */ + rect.top = y; rect.bottom = y + ry - 1; + + + if (!JD_USE_SCALE || jd->scale != 3) { /* Not for 1/8 scaling */ + pix = (uint8_t*)jd->workbuf; + + if (JD_FORMAT != 2) { /* RGB output (build an RGB MCU from Y/C component) */ + for (iy = 0; iy < my; iy++) { + pc = py = jd->mcubuf; + if (my == 16) { /* Double block height? */ + pc += 64 * 4 + (iy >> 1) * 8; + if (iy >= 8) py += 64; + } else { /* Single block height */ + pc += mx * 8 + iy * 8; + } + py += iy * 8; + for (ix = 0; ix < mx; ix++) { + cb = pc[0] - 128; /* Get Cb/Cr component and remove offset */ + cr = pc[64] - 128; + if (mx == 16) { /* Double block width? */ + if (ix == 8) py += 64 - 8; /* Jump to next block if double block heigt */ + pc += ix & 1; /* Step forward chroma pointer every two pixels */ + } else { /* Single block width */ + pc++; /* Step forward chroma pointer every pixel */ + } + yy = *py++; /* Get Y component */ + *pix++ = /*R*/ BYTECLIP(yy + ((int)(1.402 * CVACC) * cr) / CVACC); + *pix++ = /*G*/ BYTECLIP(yy - ((int)(0.344 * CVACC) * cb + (int)(0.714 * CVACC) * cr) / CVACC); + *pix++ = /*B*/ BYTECLIP(yy + ((int)(1.772 * CVACC) * cb) / CVACC); + } + } + } else { /* Monochrome output (build a grayscale MCU from Y comopnent) */ + for (iy = 0; iy < my; iy++) { + py = jd->mcubuf + iy * 8; + if (my == 16) { /* Double block height? */ + if (iy >= 8) py += 64; + } + for (ix = 0; ix < mx; ix++) { + if (mx == 16) { /* Double block width? */ + if (ix == 8) py += 64 - 8; /* Jump to next block if double block height */ + } + if (JD_FASTDECODE >= 1) { + *pix++ = BYTECLIP(*py++); /* Get and store a Y value as grayscale */ + } else { + *pix++ = *py++; /* Get and store a Y value as grayscale */ + } + } + } + } + + /* Descale the MCU rectangular if needed */ + if (JD_USE_SCALE && jd->scale) { + unsigned int x, y, r, g, b, s, w, a; + uint8_t *op; + + /* Get averaged RGB value of each square correcponds to a pixel */ + s = jd->scale * 2; /* Number of shifts for averaging */ + w = 1 << jd->scale; /* Width of square */ + a = (mx - w) * (JD_FORMAT != 2 ? 3 : 1); /* Bytes to skip for next line in the square */ + op = (uint8_t*)jd->workbuf; + for (iy = 0; iy < my; iy += w) { + for (ix = 0; ix < mx; ix += w) { + pix = (uint8_t*)jd->workbuf + (iy * mx + ix) * (JD_FORMAT != 2 ? 3 : 1); + r = g = b = 0; + for (y = 0; y < w; y++) { /* Accumulate RGB value in the square */ + for (x = 0; x < w; x++) { + r += *pix++; /* Accumulate R or Y (monochrome output) */ + if (JD_FORMAT != 2) { /* RGB output? */ + g += *pix++; /* Accumulate G */ + b += *pix++; /* Accumulate B */ + } + } + pix += a; + } /* Put the averaged pixel value */ + *op++ = (uint8_t)(r >> s); /* Put R or Y (monochrome output) */ + if (JD_FORMAT != 2) { /* RGB output? */ + *op++ = (uint8_t)(g >> s); /* Put G */ + *op++ = (uint8_t)(b >> s); /* Put B */ + } + } + } + } + + } else { /* For only 1/8 scaling (left-top pixel in each block are the DC value of the block) */ + + /* Build a 1/8 descaled RGB MCU from discrete comopnents */ + pix = (uint8_t*)jd->workbuf; + pc = jd->mcubuf + mx * my; + cb = pc[0] - 128; /* Get Cb/Cr component and restore right level */ + cr = pc[64] - 128; + for (iy = 0; iy < my; iy += 8) { + py = jd->mcubuf; + if (iy == 8) py += 64 * 2; + for (ix = 0; ix < mx; ix += 8) { + yy = *py; /* Get Y component */ + py += 64; + if (JD_FORMAT != 2) { + *pix++ = /*R*/ BYTECLIP(yy + ((int)(1.402 * CVACC) * cr / CVACC)); + *pix++ = /*G*/ BYTECLIP(yy - ((int)(0.344 * CVACC) * cb + (int)(0.714 * CVACC) * cr) / CVACC); + *pix++ = /*B*/ BYTECLIP(yy + ((int)(1.772 * CVACC) * cb / CVACC)); + } else { + *pix++ = yy; + } + } + } + } + + /* Squeeze up pixel table if a part of MCU is to be truncated */ + mx >>= jd->scale; + if (rx < mx) { /* Is the MCU spans rigit edge? */ + uint8_t *s, *d; + unsigned int x, y; + + s = d = (uint8_t*)jd->workbuf; + for (y = 0; y < ry; y++) { + for (x = 0; x < rx; x++) { /* Copy effective pixels */ + *d++ = *s++; + if (JD_FORMAT != 2) { + *d++ = *s++; + *d++ = *s++; + } + } + s += (mx - rx) * (JD_FORMAT != 2 ? 3 : 1); /* Skip truncated pixels */ + } + } + + /* Convert RGB888 to RGB565 if needed */ + if (JD_FORMAT == 1) { + uint8_t *s = (uint8_t*)jd->workbuf; + uint16_t w, *d = (uint16_t*)s; + unsigned int n = rx * ry; + + do { + w = (*s++ & 0xF8) << 8; /* RRRRR----------- */ + w |= (*s++ & 0xFC) << 3; /* -----GGGGGG----- */ + w |= *s++ >> 3; /* -----------BBBBB */ + *d++ = w; /* displayif: native byte order (ChaN R0.03 as released; CircuitPython byte-swaps here) */ + } while (--n); + } + + /* Output the rectangular */ + return outfunc(jd, jd->workbuf, &rect) ? JDR_OK : JDR_INTR; +} + + + + +/*-----------------------------------------------------------------------*/ +/* Analyze the JPEG image and Initialize decompressor object */ +/*-----------------------------------------------------------------------*/ + +#define LDB_WORD(ptr) (uint16_t)(((uint16_t)*((uint8_t*)(ptr))<<8)|(uint16_t)*(uint8_t*)((ptr)+1)) + + +JRESULT jd_prepare ( + JDEC* jd, /* Blank decompressor object */ + size_t (*infunc)(JDEC*, uint8_t*, size_t), /* JPEG strem input function */ + void* pool, /* Working buffer for the decompression session */ + size_t sz_pool, /* Size of working buffer */ + void* dev /* I/O device identifier for the session */ +) +{ + uint8_t *seg, b; + uint16_t marker; + unsigned int n, i, ofs; + size_t len; + JRESULT rc; + + + memset(jd, 0, sizeof (JDEC)); /* Clear decompression object (this might be a problem if machine's null pointer is not all bits zero) */ + jd->pool = pool; /* Work memroy */ + jd->sz_pool = sz_pool; /* Size of given work memory */ + jd->infunc = infunc; /* Stream input function */ + jd->device = dev; /* I/O device identifier */ + + jd->inbuf = seg = alloc_pool(jd, JD_SZBUF); /* Allocate stream input buffer */ + if (!seg) return JDR_MEM1; + + ofs = marker = 0; /* Find SOI marker */ + do { + if (jd->infunc(jd, seg, 1) != 1) return JDR_INP; /* Err: SOI was not detected */ + ofs++; + marker = marker << 8 | seg[0]; + } while (marker != 0xFFD8); + + for (;;) { /* Parse JPEG segments */ + /* Get a JPEG marker */ + if (jd->infunc(jd, seg, 4) != 4) return JDR_INP; + marker = LDB_WORD(seg); /* Marker */ + len = LDB_WORD(seg + 2); /* Length field */ + if (len <= 2 || (marker >> 8) != 0xFF) return JDR_FMT1; + len -= 2; /* Segent content size */ + ofs += 4 + len; /* Number of bytes loaded */ + + switch (marker & 0xFF) { + case 0xC0: /* SOF0 (baseline JPEG) */ + if (len > JD_SZBUF) return JDR_MEM2; + if (jd->infunc(jd, seg, len) != len) return JDR_INP; /* Load segment data */ + + jd->width = LDB_WORD(&seg[3]); /* Image width in unit of pixel */ + jd->height = LDB_WORD(&seg[1]); /* Image height in unit of pixel */ + jd->ncomp = seg[5]; /* Number of color components */ + if (jd->ncomp != 3 && jd->ncomp != 1) return JDR_FMT3; /* Err: Supports only Grayscale and Y/Cb/Cr */ + + /* Check each image component */ + for (i = 0; i < jd->ncomp; i++) { + b = seg[7 + 3 * i]; /* Get sampling factor */ + if (i == 0) { /* Y component */ + if (b != 0x11 && b != 0x22 && b != 0x21) { /* Check sampling factor */ + return JDR_FMT3; /* Err: Supports only 4:4:4, 4:2:0 or 4:2:2 */ + } + jd->msx = b >> 4; jd->msy = b & 15; /* Size of MCU [blocks] */ + } else { /* Cb/Cr component */ + if (b != 0x11) return JDR_FMT3; /* Err: Sampling factor of Cb/Cr must be 1 */ + } + jd->qtid[i] = seg[8 + 3 * i]; /* Get dequantizer table ID for this component */ + if (jd->qtid[i] > 3) return JDR_FMT3; /* Err: Invalid ID */ + } + break; + + case 0xDD: /* DRI - Define Restart Interval */ + if (len > JD_SZBUF) return JDR_MEM2; + if (jd->infunc(jd, seg, len) != len) return JDR_INP; /* Load segment data */ + + jd->nrst = LDB_WORD(seg); /* Get restart interval (MCUs) */ + break; + + case 0xC4: /* DHT - Define Huffman Tables */ + if (len > JD_SZBUF) return JDR_MEM2; + if (jd->infunc(jd, seg, len) != len) return JDR_INP; /* Load segment data */ + + rc = create_huffman_tbl(jd, seg, len); /* Create huffman tables */ + if (rc) return rc; + break; + + case 0xDB: /* DQT - Define Quaitizer Tables */ + if (len > JD_SZBUF) return JDR_MEM2; + if (jd->infunc(jd, seg, len) != len) return JDR_INP; /* Load segment data */ + + rc = create_qt_tbl(jd, seg, len); /* Create de-quantizer tables */ + if (rc) return rc; + break; + + case 0xDA: /* SOS - Start of Scan */ + if (len > JD_SZBUF) return JDR_MEM2; + if (jd->infunc(jd, seg, len) != len) return JDR_INP; /* Load segment data */ + + if (!jd->width || !jd->height) return JDR_FMT1; /* Err: Invalid image size */ + if (seg[0] != jd->ncomp) return JDR_FMT3; /* Err: Wrong color components */ + + /* Check if all tables corresponding to each components have been loaded */ + for (i = 0; i < jd->ncomp; i++) { + b = seg[2 + 2 * i]; /* Get huffman table ID */ + if (b != 0x00 && b != 0x11) return JDR_FMT3; /* Err: Different table number for DC/AC element */ + n = i ? 1 : 0; /* Component class */ + if (!jd->huffbits[n][0] || !jd->huffbits[n][1]) { /* Check huffman table for this component */ + return JDR_FMT1; /* Err: Nnot loaded */ + } + if (!jd->qttbl[jd->qtid[i]]) { /* Check dequantizer table for this component */ + return JDR_FMT1; /* Err: Not loaded */ + } + } + + /* Allocate working buffer for MCU and pixel output */ + n = jd->msy * jd->msx; /* Number of Y blocks in the MCU */ + if (!n) return JDR_FMT1; /* Err: SOF0 has not been loaded */ + len = n * 64 * 2 + 64; /* Allocate buffer for IDCT and RGB output */ + if (len < 256) len = 256; /* but at least 256 byte is required for IDCT */ + jd->workbuf = alloc_pool(jd, len); /* and it may occupy a part of following MCU working buffer for RGB output */ + if (!jd->workbuf) return JDR_MEM1; /* Err: not enough memory */ + jd->mcubuf = alloc_pool(jd, (n + 2) * 64 * sizeof (jd_yuv_t)); /* Allocate MCU working buffer */ + if (!jd->mcubuf) return JDR_MEM1; /* Err: not enough memory */ + + /* Align stream read offset to JD_SZBUF */ + if (ofs %= JD_SZBUF) { + jd->dctr = jd->infunc(jd, seg + ofs, (size_t)(JD_SZBUF - ofs)); + } + jd->dptr = seg + ofs - (JD_FASTDECODE ? 0 : 1); + + return JDR_OK; /* Initialization succeeded. Ready to decompress the JPEG image. */ + + case 0xC1: /* SOF1 */ + case 0xC2: /* SOF2 */ + case 0xC3: /* SOF3 */ + case 0xC5: /* SOF5 */ + case 0xC6: /* SOF6 */ + case 0xC7: /* SOF7 */ + case 0xC9: /* SOF9 */ + case 0xCA: /* SOF10 */ + case 0xCB: /* SOF11 */ + case 0xCD: /* SOF13 */ + case 0xCE: /* SOF14 */ + case 0xCF: /* SOF15 */ + case 0xD9: /* EOI */ + return JDR_FMT3; /* Unsuppoted JPEG standard (may be progressive JPEG) */ + + default: /* Unknown segment (comment, exif or etc..) */ + /* Skip segment data (null pointer specifies to remove data from the stream) */ + if (jd->infunc(jd, 0, len) != len) return JDR_INP; + } + } +} + + + + +/*-----------------------------------------------------------------------*/ +/* Start to decompress the JPEG picture */ +/*-----------------------------------------------------------------------*/ + +JRESULT jd_decomp ( + JDEC* jd, /* Initialized decompression object */ + int (*outfunc)(JDEC*, void*, JRECT*), /* RGB output function */ + uint8_t scale /* Output de-scaling factor (0 to 3) */ +) +{ + unsigned int x, y, mx, my; + uint16_t rst, rsc; + JRESULT rc; + + + if (scale > (JD_USE_SCALE ? 3 : 0)) return JDR_PAR; + jd->scale = scale; + + mx = jd->msx * 8; my = jd->msy * 8; /* Size of the MCU (pixel) */ + + jd->dcv[2] = jd->dcv[1] = jd->dcv[0] = 0; /* Initialize DC values */ + rst = rsc = 0; + + rc = JDR_OK; + for (y = 0; y < jd->height; y += my) { /* Vertical loop of MCUs */ + for (x = 0; x < jd->width; x += mx) { /* Horizontal loop of MCUs */ + if (jd->nrst && rst++ == jd->nrst) { /* Process restart interval if enabled */ + rc = restart(jd, rsc++); + if (rc != JDR_OK) return rc; + rst = 1; + } + rc = mcu_load(jd); /* Load an MCU (decompress huffman coded stream, dequantize and apply IDCT) */ + if (rc != JDR_OK) return rc; + rc = mcu_output(jd, outfunc, x, y); /* Output the MCU (YCbCr to RGB, scaling and output) */ + if (rc != JDR_OK) return rc; + } + } + + return rc; +} diff --git a/modules/jpegio/src/tjpgd/tjpgd.h b/modules/jpegio/src/tjpgd/tjpgd.h new file mode 100644 index 00000000..0831e229 --- /dev/null +++ b/modules/jpegio/src/tjpgd/tjpgd.h @@ -0,0 +1,123 @@ +/*----------------------------------------------------------------------------/ +/ TJpgDec - Tiny JPEG Decompressor R0.03 include file (C)ChaN, 2021 +/----------------------------------------------------------------------------*/ +#ifndef DEF_TJPGDEC +#define DEF_TJPGDEC + +#ifdef __cplusplus +extern "C" { +#endif + +#include "tjpgdcnf.h" +#include + +/* Deviation from ChaN R0.03 (see src/jpegio/README.md): upstream selects the +/ fallback typedefs with `#if defined(_WIN32)`, while its own comment says the +/ test is meant to be "a compiler without stdint.h". mingw is both at once -- +/ it defines _WIN32 and ships stdint.h, where uint32_t is `unsigned int`, not +/ `unsigned long` -- so every mingw cross-build failed with conflicting types +/ for uint32_t. Test for the header, and keep the fallback for the compiler +/ the comment actually meant (MSVC before 2010 shipped no stdint.h). */ +#ifndef JD_HAS_STDINT /* -DJD_HAS_STDINT=0/1 overrides the detection */ +# if defined(__has_include) +# if __has_include() +# define JD_HAS_STDINT 1 +# else +# define JD_HAS_STDINT 0 +# endif +# elif defined(_MSC_VER) && _MSC_VER < 1600 +# define JD_HAS_STDINT 0 +# else +# define JD_HAS_STDINT 1 +# endif +#endif + +#if JD_HAS_STDINT +#include +#else /* VC++ before 2010, or another compiler without stdint.h */ +typedef unsigned char uint8_t; +typedef unsigned short uint16_t; +typedef short int16_t; +typedef unsigned long uint32_t; +typedef long int32_t; +#endif + +#if JD_FASTDECODE >= 1 +typedef int16_t jd_yuv_t; +#else +typedef uint8_t jd_yuv_t; +#endif + + +/* Error code */ +typedef enum { + JDR_OK = 0, /* 0: Succeeded */ + JDR_INTR, /* 1: Interrupted by output function */ + JDR_INP, /* 2: Device error or wrong termination of input stream */ + JDR_MEM1, /* 3: Insufficient memory pool for the image */ + JDR_MEM2, /* 4: Insufficient stream input buffer */ + JDR_PAR, /* 5: Parameter error */ + JDR_FMT1, /* 6: Data format error (may be broken data) */ + JDR_FMT2, /* 7: Right format but not supported */ + JDR_FMT3 /* 8: Not supported JPEG standard */ +} JRESULT; + + + +/* Rectangular region in the output image */ +typedef struct { + uint16_t left; /* Left end */ + uint16_t right; /* Right end */ + uint16_t top; /* Top end */ + uint16_t bottom; /* Bottom end */ +} JRECT; + + + +/* Decompressor object structure */ +typedef struct JDEC JDEC; +struct JDEC { + size_t dctr; /* Number of bytes available in the input buffer */ + uint8_t* dptr; /* Current data read ptr */ + uint8_t* inbuf; /* Bit stream input buffer */ + uint8_t dbit; /* Number of bits availavble in wreg or reading bit mask */ + uint8_t scale; /* Output scaling ratio */ + uint8_t msx, msy; /* MCU size in unit of block (width, height) */ + uint8_t qtid[3]; /* Quantization table ID of each component, Y, Cb, Cr */ + uint8_t ncomp; /* Number of color components 1:grayscale, 3:color */ + int16_t dcv[3]; /* Previous DC element of each component */ + uint16_t nrst; /* Restart inverval */ + uint16_t width, height; /* Size of the input image (pixel) */ + uint8_t* huffbits[2][2]; /* Huffman bit distribution tables [id][dcac] */ + uint16_t* huffcode[2][2]; /* Huffman code word tables [id][dcac] */ + uint8_t* huffdata[2][2]; /* Huffman decoded data tables [id][dcac] */ + int32_t* qttbl[4]; /* Dequantizer tables [id] */ +#if JD_FASTDECODE >= 1 + uint32_t wreg; /* Working shift register */ + uint8_t marker; /* Detected marker (0:None) */ +#if JD_FASTDECODE == 2 + uint8_t longofs[2][2]; /* Table offset of long code [id][dcac] */ + uint16_t* hufflut_ac[2]; /* Fast huffman decode tables for AC short code [id] */ + uint8_t* hufflut_dc[2]; /* Fast huffman decode tables for DC short code [id] */ +#endif +#endif + void* workbuf; /* Working buffer for IDCT and RGB output */ + jd_yuv_t* mcubuf; /* Working buffer for the MCU */ + void* pool; /* Pointer to available memory pool */ + size_t sz_pool; /* Size of momory pool (bytes available) */ + size_t (*infunc)(JDEC*, uint8_t*, size_t); /* Pointer to jpeg stream input function */ + void* device; /* Pointer to I/O device identifiler for the session */ +}; + + + +/* TJpgDec API functions */ +JRESULT jd_prepare (JDEC* jd, size_t (*infunc)(JDEC*,uint8_t*,size_t), void* pool, size_t sz_pool, void* dev); +JRESULT jd_decomp (JDEC* jd, int (*outfunc)(JDEC*,void*,JRECT*), uint8_t scale); + + +#ifdef __cplusplus +} +#endif + +#endif /* _TJPGDEC */ diff --git a/modules/jpegio/src/tjpgd/tjpgdcnf.h b/modules/jpegio/src/tjpgd/tjpgdcnf.h new file mode 100644 index 00000000..d3e52baa --- /dev/null +++ b/modules/jpegio/src/tjpgd/tjpgdcnf.h @@ -0,0 +1,32 @@ +/*----------------------------------------------*/ +/* TJpgDec System Configurations R0.03 */ +/*----------------------------------------------*/ + +#define JD_SZBUF 512 +/* Specifies size of stream input buffer */ + +#define JD_FORMAT 1 +/* Specifies output pixel format. +/ 0: RGB888 (24-bit/pix) +/ 1: RGB565 (16-bit/pix) +/ 2: Grayscale (8-bit/pix) +*/ + +#define JD_USE_SCALE 1 +/* Switches output descaling feature. +/ 0: Disable +/ 1: Enable +*/ + +#define JD_TBLCLIP 1 +/* Use table conversion for saturation arithmetic. A bit faster, but increases 1 KB of code size. +/ 0: Disable +/ 1: Enable +*/ + +#define JD_FASTDECODE 1 +/* Optimization level +/ 0: Basic optimization. Suitable for 8/16-bit MCUs. +/ 1: + 32-bit barrel shifter. Suitable for 32-bit MCUs. +/ 2: + Table conversion for huffman decoding (wants 6 << HUFF_BIT bytes of RAM) +*/ diff --git a/modules/jpegio/tests/frames/README.md b/modules/jpegio/tests/frames/README.md new file mode 100644 index 00000000..9f670d75 --- /dev/null +++ b/modules/jpegio/tests/frames/README.md @@ -0,0 +1,17 @@ +# jpegio test corpus + +Generated by [`../make_corpus.py`](../make_corpus.py) (Pillow 12.3.0); marker sequences and sizes below come from [`../jpegwalk.py`](../jpegwalk.py). `reference.json` is the Pillow fidelity reference written by [`../reference.py`](../reference.py) -- per baseline frame and scale 0..3: RGB565 buffer sha256 and per-channel means. It is a tolerance reference, not a bit-exact golden (TJpgDec and libjpeg differ in IDCT rounding and chroma upsampling). `golden_tjpgd.json` is the bit-exact one: TJpgDec's own sha256 per frame and scale, written by [`../test_jpegio.py`](../test_jpegio.py) `--record` and asserted on every run. + +Synthetic frames share one procedural pattern: colour bars (top 40 %), an x/y/diagonal RGB gradient (40-70 %), a 1-px black/white checker (bottom left) and a red|blue split (bottom right). Marker sequences omit the RSTn markers inside the scan; their count is given with the DRI interval (a DRI of 0 means the segment is present but restart intervals are disabled). `TRAILING` = bytes after EOI. + +| file | what / how made | markers | size | sampling | bytes | DRI | notes | +|------|-----------------|---------|------|----------|-------|-----|-------| +| `baseline_jfif_320x240.jpg` | baseline JFIF still of the test pattern at 320x240; Pillow save(quality=90, subsampling='4:2:0') | `SOI APP0 DQT DQT SOF0 DHT DHT DHT DHT SOS EOI` | 320x240 SOF0 | 4:2:0 | 11172 | - | 2 DQT tables in 2 segments; APP0=JFIF | +| `baseline_jfif_64x48.jpg` | baseline JFIF still of the test pattern; Pillow save(quality=90, subsampling='4:2:0') | `SOI APP0 DQT DQT SOF0 DHT DHT DHT DHT SOS EOI` | 64x48 SOF0 | 4:2:0 | 1540 | - | 2 DQT tables in 2 segments; APP0=JFIF | +| `c920e_320x240_dri.jpg` | real camera frame: Logitech C920e over usbif UVC on ESP32-S3, captured by the usbif session 2026-09-03; DHT present; DRI present | `SOI DQT SOF0 DHT DHT DHT DHT COM APP0 DRI SOS EOI TRAILING` | 320x240 SOF0 | 4:2:2 | 8064 | 0 (0 RST) | 3 DQT tables in 1 segment; COM 8 bytes (all zero); APP0 payload is not JFIF (14 bytes, all zero); 22 bytes after EOI (ff001415..) | +| `dri_first_mjpeg_style_320x240.jpg` | restart_dri_320x240 with its segments re-ordered by jpegwalk to SOI DQT DQT SOF0 DHT DHT DHT DHT APP0 DRI SOS (APP0/JFIF after the tables, as a Logitech C920e emits UVC MJPEG); Pillow decodes it pixel-identical to restart_dri_320x240 | `SOI DQT DQT SOF0 DHT DHT DHT DHT APP0 DRI SOS EOI` | 320x240 SOF0 | 4:2:0 | 11348 | 4 (74 RST) | 2 DQT tables in 2 segments; APP0=JFIF | +| `grayscale_64x48.jpg` | baseline grayscale (one component): pattern(64,48).convert('L'); Pillow save(quality=90) | `SOI APP0 DQT SOF0 DHT DHT SOS EOI` | 64x48 SOF0 | gray | 960 | - | 1 DQT table in 1 segment; APP0=JFIF | +| `nodht_320x240.jpg` | baseline_jfif_320x240 with every DHT (FFC4) segment removed by jpegwalk -- the 'MJPEG without Huffman tables' case; a decoder must either inject the standard tables (ITU T.81 K.3) or refuse it; Pillow/libjpeg-turbo decodes it by injecting the standard tables (pixels identical to baseline_jfif_320x240) | `SOI APP0 DQT DQT SOF0 SOS EOI` | 320x240 SOF0 | 4:2:0 | 10740 | - | 2 DQT tables in 2 segments; APP0=JFIF | +| `odd_size_37x29.jpg` | baseline, odd dimensions so the right column and bottom row of MCUs are partial (37 = 2x16 + 5, 29 = 1x16 + 13); Pillow save(quality=90, subsampling='4:2:0') | `SOI APP0 DQT DQT SOF0 DHT DHT DHT DHT SOS EOI` | 37x29 SOF0 | 4:2:0 | 1427 | - | 2 DQT tables in 2 segments; APP0=JFIF | +| `progressive_320x240.jpg` | the 320x240 pattern saved progressive (SOF2): Pillow save(progressive=True, subsampling='4:2:0'); TJpgDec is baseline-only and must refuse it | `SOI APP0 DQT DQT SOF2 DHT DHT SOS DHT SOS DHT SOS DHT SOS DHT SOS DHT SOS SOS DHT SOS DHT SOS DHT SOS EOI` | 320x240 SOF2 | 4:2:0 | 6074 | - | 2 DQT tables in 2 segments; APP0=JFIF | +| `restart_dri_320x240.jpg` | as baseline_jfif_320x240 but with restart markers: Pillow save(..., restart_marker_blocks=4) -> DRI segment + RSTn every 4 MCUs | `SOI APP0 DQT DQT SOF0 DHT DHT DHT DHT DRI SOS EOI` | 320x240 SOF0 | 4:2:0 | 11348 | 4 (74 RST) | 2 DQT tables in 2 segments; APP0=JFIF | diff --git a/modules/jpegio/tests/frames/baseline_jfif_320x240.jpg b/modules/jpegio/tests/frames/baseline_jfif_320x240.jpg new file mode 100644 index 0000000000000000000000000000000000000000..38fe65d466ae8a70f23f836018638b34ecd6aa26 GIT binary patch literal 11172 zcmeHN3sh5A7QG>S42Tg4YV9ai?TFx~sMJEM@iVAp5Cy5}N6e_BsGSaepo$`|)>=C_ ziaJHa@=>fHbQY;P_@RywQR#q+Hlkv!pcpVJ3PA%Q$-6!G<>e6)x=3eSU9L5WT+GTn z@1C>wIcMLO`@lDF4eA*(_3f#M$3ut*{}BEWO+kDI2l4|iNBD8-=H%q)=;Z3+;@qvf zYxnMMu5NDbJ$m(Y@8QwI&8=teo*teeFE6j|!ajX_i~9Bwd5Op(JU(3G=p=A*5{TU0 z+(p0og-;`oZm5wrk-G$`an+Z!Myy#NB&iK^w{weMQ2LRo;zRq{eQ}SP+YCN zcKt@x%{zB%?y-bK85mat;c9|wQdVW$CFnL*HNRc`eY~3bH<;BW%Fg(~xpTBN?uxOUc26#`` zde`m^4;CRwnk46XnnW$zMm3KB4|3fXEu$-rsST5pd}PXnx5%pE{3Nor+4_-93U2+n z_uALrz^#AJyLP*+>%TPse6=8q_U|=kD&}_>nW$<|$~mfzwo~O#2iZVH3#S)F7sfZA*JftC6_a*hWEy>ud=gvBEUYOI)FxDfWRAN~*JlRtq&jDs zkxekIwu$we3%(A(I6ic1^Y|c`O(g;Ji`E4w3ep-PMy55)OK3V3ePHIxb*tQR*3G^i zswvnPTd`~hj2dw9R8^aBX?|=q^MUHT*(JHgOLUQ2a$@gtNC})DOmGN35&!{wQVT(c zF+qoFa<`5#?0I11$s7*M6ad&bhy<2hC&>c%D#dwi`We-!t4+j@2^UA(CdGP&;tqa{ z^&tu?U#8Hk#!%6NmBBdOzf<}UxuxN8OCmrUTSq&nB0$oNOfgFPdn5Xg=mTZ?hE4G4GFa^z)4WpmB z08_2AlGoqd9g&=kAbvz!!!qAAAs|ZG;WCRT$z?)t`|WYkbS|ryHa5zn_d)C80W`wY9_rM17J%U((B0vz@-w<8VH3?%aVo1&WRp#S!BJLn-nh-{0&26=Juu20br_=`8tGkyQB#O5HW;lg_jsLhm{y!uN-q{`|A>t)14st}yD9_ZR4Ub*WcM&%U@pdPv2+P^b^PCQ-H z4(KmkM{5fp!6MI8{lE3%nhgIMK`FfeXrD>nYUB16S(^A>f2c0$BEU%<(Mnt0mGRV{ zpwgv&Ix#(0d<3JrDYn?u?+Om2%Ae>{X+h-x=X9#h52GO%HB{s>+Ya1}=>pVG^c&@> z@<(#b6AcCPypys~kG!8gRFbkT-wXvFk=v!kN%ftoa$1VfJ-OkKVt_li8ftWvoY%g? zX=l@us-G$~Qr&8uI0d6mZc9=%!nQcI91tl>0Nb#xE#}29KdZ!O6tE_xZ>W+5m&DB; z812MJQ(9SW+7%P$Uy){od|7={FMRlh;U&mtI=B$hj}rHiO%rbQbOGA9;8o>UlOY~WAw2&LK14czXhx4;Q7N(t%Op_+N}4?5l;&QiGTv0g3)*0`Gec- zB(R$AS70XxyL|DqG_b2SntkI3alyT!VUK=)_6g-e@h^BL5I-@xvqwuvhDR1_Y>9Rwjb``bOEg+z~B81<3LI<(jUd>$;O<( z^jZz&*-~h_OOc&^;3rt$qYln9_JYe8x;1EjMwobE6L6oXBi_c725Rh=RG)hJ8erT5 z=toex8dY-7FtPdxV@Go?^%#ZERk!sGlQxG_N*P888UA;dmeNtwz3N8cW$HP!Vb_h? zwMAkAfPD_iV?!_7Vglz&_Fy^{e>*e3v@P11u`}~4?~@|=_3!&+%e#v!Uo#yyv;Xr? z!vQp$$R{^lh68+LJ`64|kT^;7|C~@`%WchXX@h=CXSVpV2h`P_v7I41L;veHrx&k= zwAbKaWr^@vsJVWV_l>?F`?nVAW0?n~rU$0v@#!S^KK6XRNZKMyw%B0gCp<2ELB1zU zv0iSr@{FbMI)GLi&WlSY PvZo`=ALhY(KCb#1V~pw8 literal 0 HcmV?d00001 diff --git a/modules/jpegio/tests/frames/baseline_jfif_64x48.jpg b/modules/jpegio/tests/frames/baseline_jfif_64x48.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a56c3bdaa63ceed2a25aec989872e1883ba4eb57 GIT binary patch literal 1540 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 A%>V!Z literal 0 HcmV?d00001 diff --git a/modules/jpegio/tests/frames/dri_first_mjpeg_style_320x240.jpg b/modules/jpegio/tests/frames/dri_first_mjpeg_style_320x240.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dbd2d69f24a0f735357c6a4877c9428ac79be464 GIT binary patch literal 11348 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 PsOIOjt`Az;|8D{SUw_lX literal 0 HcmV?d00001 diff --git a/modules/jpegio/tests/frames/nodht_320x240.jpg b/modules/jpegio/tests/frames/nodht_320x240.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b8ee573ed4e86f0c4d40d9399d3ca76a45ec3cd7 GIT binary patch literal 10740 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 literal 0 HcmV?d00001 diff --git a/modules/jpegio/tests/frames/progressive_320x240.jpg b/modules/jpegio/tests/frames/progressive_320x240.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3084c960d5316706d37833ddfbf872e45993db5d GIT binary patch literal 6074 zcmeHL3piET9$)*sy^eD_ClR98T!)4uQOe_}5akGY)vl}-+a0A-PT&)+W)owuiyW__gZ`J zHQGGd1{jWZ4t4-R06<^`qj$gxKqL^*1`7!`GL1|ok;sZvDut%3sI07{sHCJqpT2hY6Kxd9THiAOjcm4D5oQsim^APV;SrHJ zfWSnEOl0&QKodg63m7W|GKEG(6d+R@asUw!(P(6aM5NNl6a@k@dK)Mc5kO!N88Flr zt3bH6wXt}+ihHOQ?FoMgB8eEO5*M5LmWKuwTcyNFm&OFT`^*$yU&6e*JA#;_q_9`JZC;6UTpp;&wt#;-a^NFrM{>E)KViPFaxMw z)~S+Pr`QwqPy-%{GT%4|Xvu&)=38z9CA>K8y{;FEW{ep?5%@#Qay@+^4Cn7|6Mf@) zGT#_@G>TKxA!ZCb76lB)LvdH^&))Y+{l=8XQsBkGA}{;d8w(%JLj~Wx2S)8t=*;(f zf_HfQr;GSq`TFNVggZ}q{vteM9l3>U?p_eykmfD2^sx*p?92JW@xE98piZgDtPTCe zY8KhNqO9*bhqXsK_Z!_bkQP-rANQ1a^}k***eMaj6!>uhYhMTll$y>roiM7~C$L=o zs#DZqB#zxJt`;?xU3Lkq?Nxdf{&|m%z%!w_|JChM`pD+N)~9jJ{a*~1Wj_)`Q!HYi z<7-Uyh^lSR0RZTvC>?D$B~;#dArog2yH?x~9s>Yh3joYY)GF+ft<0h8&o>l6y3IbU z?>Yd~oN5C>InJ|+u$&?00f4PNxLV=TR%c+V9_Qmd%Y+vPS2Yc;c7#kABsDlK`q;)Y zV79g8OwhHA375{dMksIh#dk!4>O~V2aP@xayLwhW@0t#-o9GDB8w$tO$30{l{wpMS zV?SX6gh(V%(XCrn5CV~;YmDziv1(E4~F8Xv#(ik`ccu*tavfnC+bI8Mzi*a(f)Wy zR#5W&h!!`=jZ%K5SRh>d*M=XQjqLi&5x1*1iXGBO1?$Sw#ObUr^<(FRnL2m14(Y)B zE?@!zLP8KSjVRAAk+L$OQkO(s{_W8Y0-0fB>*CtJ&K=J(0nPGDPmh)@RTt0KZ7{ud z-n;L+ZKs=(Bexwc-IB5OT!+t{%|$b%cRtr?KWO zciotf$~z$2vMAPs*Q+W7E)Phz>^DUe8|hob{q4&~DI#*G{Q;JX_SZMtN>B6?Lgt=c z>N6v47SCzr=k}`;`j6HqoJgtZ4&AU$fS58{v<;Q6?$`Y!ciG&$4{gft;O_9V3 z^FNu>Nsvx*r-!&xfA@bK(n+4l2b0YGULK^6kNi~aNFN{hDcj}Y>r4~>_MY%QLqGr{ z%zymz+j{n!HcomTXUz{BN7tbw*F$-n)lyEn6cQ#H*pMG=@{ObGq5OrA1+Q2qbKy)NOx|3kXQHvMD$qDX5(J5RfyU_=0cYMK1gK29Bsf*3gj&cKpl&fU zZYYv*pZN>rFeI2u$8ecTd<10&Ft0KnJKlDtQTn#~;BK16swt>3cjFIDK@~l&nSx+f zX~dcnIj|ayo$J7J9{?Bw^_O3An+98>UyXuFg&h{Z^|*dbc%9G2diIy^QN=J~Z33Hs0gYmczQuUvG4ZQSYUT z_BveYbL+8&fVG%#Gl55S>Z-`Jwd3XvC_3v&0xV&Cm3DtPiK{}lxR$ZTVVE|=#o zKWeaMylLD2XSR!dWfDD$X{)A}o1nLJmo3}tXuHjkT{e($)y0O*v}L<&%B{6cP14Iv z(mU+}hvQAnQ4g}<3~B8q$jwcn<-qUWrrd&4v^y;@Yf1cJlOv~{aF(#E*7Vj+YO@hm}ap$iH#Cy9oELQ%7Q zE#^SC7)A&)b=*HR)sMPcFmw!cyMA~Gb$@v2@ef?{ANMsHPeHZbky0=f^_MVtu-2n3 F{{%y)RP+D< literal 0 HcmV?d00001 diff --git a/modules/jpegio/tests/frames/reference.json b/modules/jpegio/tests/frames/reference.json new file mode 100644 index 00000000..76a20d43 --- /dev/null +++ b/modules/jpegio/tests/frames/reference.json @@ -0,0 +1,603 @@ +{ + "_about": [ + "Pillow fidelity reference for tests/jpegio/frames; written by tests/jpegio/reference.py.", + "scales[k]: decode at scale k (1/2**k) -> width x height RGB565 buffer, little-endian", + "uint16, row-major; sha256_rgb565 is the digest of that buffer; mean_rgb are per-channel", + "means of the 8-bit reference, mean_rgb565 the means of r&0xF8, g&0xFC, b&0xF8 (what a", + "decoder's RGB565 output unpacks to). Downscaling is a truncating integer box average of", + "the full libjpeg decode (as TJpgDec's mcu_output does), NOT draft()/reduce().", + "Compare with a tolerance: TJpgDec's IDCT, chroma replication and DC-only 1/8 differ." + ], + "pillow": "12.3.0", + "rgb565": "((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3), little-endian uint16 (native order on all PyDevices targets)", + "scale_dims": "width = source_width >> scale, height = source_height >> scale (partial-MCU remainder dropped, as TJpgDec does)", + "frames": { + "baseline_jfif_320x240.jpg": { + "sha256_file": "e94699a7290e129f8e1cfb24e3beef441aa9e0155fcfd3618807e0d044aed841", + "bytes": 11172, + "width": 320, + "height": 240, + "sof": "SOF0", + "sampling": "4:2:0", + "components": 3, + "dri": null, + "markers": "SOI APP0 DQT DQT SOF0 DHT DHT DHT DHT SOS EOI", + "scales": { + "0": { + "width": 320, + "height": 240, + "sha256_rgb565": "7080526506e0c21eb112b46ea79e2bbb6d6b0e730ed37f4d5c3cdd59a5324ef4", + "mean_rgb": [ + 127.2433, + 108.2792, + 125.872 + ], + "mean_rgb565": [ + 123.8593, + 106.9648, + 122.5144 + ] + }, + "1": { + "width": 160, + "height": 120, + "sha256_rgb565": "9f57b34a205cf1f884a20c156ba5b92dffba66e9684e1a4032eaf311fa472ee1", + "mean_rgb": [ + 127.0509, + 108.102, + 125.6853 + ], + "mean_rgb565": [ + 123.3308, + 106.6765, + 121.9887 + ] + }, + "2": { + "width": 80, + "height": 60, + "sha256_rgb565": "5b661b9fef344f03d4c9aaf193a999263f7f04c0cee291b7f46f00ff747054fb", + "mean_rgb": [ + 126.9865, + 108.0429, + 125.5894 + ], + "mean_rgb565": [ + 123.1633, + 106.5925, + 121.64 + ] + }, + "3": { + "width": 40, + "height": 30, + "sha256_rgb565": "13dd202945e4b105bc0897b096362448c11add633446f61cfa602044bb7ca8bf", + "mean_rgb": [ + 126.9417, + 108.0033, + 125.5508 + ], + "mean_rgb565": [ + 123.0333, + 106.6033, + 121.6333 + ] + } + } + }, + "baseline_jfif_64x48.jpg": { + "sha256_file": "15015df784e5947c294367f0eb2d2cca3d8d909fb5082b57c5aa18afdf68efb6", + "bytes": 1540, + "width": 64, + "height": 48, + "sof": "SOF0", + "sampling": "4:2:0", + "components": 3, + "dri": null, + "markers": "SOI APP0 DQT DQT SOF0 DHT DHT DHT DHT SOS EOI", + "scales": { + "0": { + "width": 64, + "height": 48, + "sha256_rgb565": "af9b927c88efdb1ca2b4e042f80dea2cc778fa13c829e71960dc2aae6814fb7f", + "mean_rgb": [ + 127.4395, + 107.7321, + 126.0452 + ], + "mean_rgb565": [ + 124.0443, + 106.3477, + 122.7474 + ] + }, + "1": { + "width": 32, + "height": 24, + "sha256_rgb565": "00462f0ae49c146bcdad4288283d29c245814249945b2068631a0c5e38e6f992", + "mean_rgb": [ + 127.1458, + 107.4505, + 125.7721 + ], + "mean_rgb565": [ + 123.7292, + 106.0365, + 122.3542 + ] + }, + "2": { + "width": 16, + "height": 12, + "sha256_rgb565": "c2de4bd14598e9ecfc06f8d977df5b812abc8c4676c530b0003c4b956aca3ca6", + "mean_rgb": [ + 127.0312, + 107.3802, + 125.6094 + ], + "mean_rgb565": [ + 123.5417, + 105.8542, + 121.625 + ] + }, + "3": { + "width": 8, + "height": 6, + "sha256_rgb565": "082f5afec0ee17965ccdc3e90bbf05d667a19cdfcd578bfa04bd877a27480c73", + "mean_rgb": [ + 126.9375, + 107.2917, + 125.5208 + ], + "mean_rgb565": [ + 123.0, + 106.0, + 121.3333 + ] + } + } + }, + "c920e_320x240_dri.jpg": { + "sha256_file": "31968f6632c73dd72703e18260bf684ab5adacc9e99529d7b21e331f3137f535", + "bytes": 8064, + "width": 320, + "height": 240, + "sof": "SOF0", + "sampling": "4:2:2", + "components": 3, + "dri": 0, + "markers": "SOI DQT SOF0 DHT DHT DHT DHT COM APP0 DRI SOS EOI TRAILING", + "scales": { + "0": { + "width": 320, + "height": 240, + "sha256_rgb565": "be7607923e546ad79e385ff86a96d7bdb8c0058a4d0603cccb8e97191d837772", + "mean_rgb": [ + 177.6693, + 132.6757, + 133.5783 + ], + "mean_rgb565": [ + 173.9996, + 131.185, + 130.1168 + ] + }, + "1": { + "width": 160, + "height": 120, + "sha256_rgb565": "b25b3713700f792500ee2b359c9dd88412ace9bc8dfa7c9b38373f7abd4bdf7b", + "mean_rgb": [ + 177.3574, + 132.3479, + 133.2605 + ], + "mean_rgb565": [ + 173.7704, + 130.849, + 129.8004 + ] + }, + "2": { + "width": 80, + "height": 60, + "sha256_rgb565": "505e9d03211b1aa89ab9c0e85f3a31f7cb53c629d01d734df64466aa358102b1", + "mean_rgb": [ + 177.2421, + 132.2442, + 133.159 + ], + "mean_rgb565": [ + 173.705, + 130.7358, + 129.705 + ] + }, + "3": { + "width": 40, + "height": 30, + "sha256_rgb565": "1bdd157b7988cc7cbbe4a95eb2c24c834472c7f426d37ca5a20b10ba0bc35b3b", + "mean_rgb": [ + 177.21, + 132.2125, + 133.1092 + ], + "mean_rgb565": [ + 173.7867, + 130.6667, + 129.6267 + ] + } + } + }, + "dri_first_mjpeg_style_320x240.jpg": { + "sha256_file": "fce7f537a5bacca2fd8dcebee4b3e6304cabd8f50fa5c442635d9dfb803dc0e8", + "bytes": 11348, + "width": 320, + "height": 240, + "sof": "SOF0", + "sampling": "4:2:0", + "components": 3, + "dri": 4, + "markers": "SOI DQT DQT SOF0 DHT DHT DHT DHT APP0 DRI SOS EOI", + "scales": { + "0": { + "width": 320, + "height": 240, + "sha256_rgb565": "7080526506e0c21eb112b46ea79e2bbb6d6b0e730ed37f4d5c3cdd59a5324ef4", + "mean_rgb": [ + 127.2433, + 108.2792, + 125.872 + ], + "mean_rgb565": [ + 123.8593, + 106.9648, + 122.5144 + ] + }, + "1": { + "width": 160, + "height": 120, + "sha256_rgb565": "9f57b34a205cf1f884a20c156ba5b92dffba66e9684e1a4032eaf311fa472ee1", + "mean_rgb": [ + 127.0509, + 108.102, + 125.6853 + ], + "mean_rgb565": [ + 123.3308, + 106.6765, + 121.9887 + ] + }, + "2": { + "width": 80, + "height": 60, + "sha256_rgb565": "5b661b9fef344f03d4c9aaf193a999263f7f04c0cee291b7f46f00ff747054fb", + "mean_rgb": [ + 126.9865, + 108.0429, + 125.5894 + ], + "mean_rgb565": [ + 123.1633, + 106.5925, + 121.64 + ] + }, + "3": { + "width": 40, + "height": 30, + "sha256_rgb565": "13dd202945e4b105bc0897b096362448c11add633446f61cfa602044bb7ca8bf", + "mean_rgb": [ + 126.9417, + 108.0033, + 125.5508 + ], + "mean_rgb565": [ + 123.0333, + 106.6033, + 121.6333 + ] + } + } + }, + "grayscale_64x48.jpg": { + "sha256_file": "5da348ab045a05fce417a9012e23752bdc763a58356d7d03b144829557179f27", + "bytes": 960, + "width": 64, + "height": 48, + "sof": "SOF0", + "sampling": "gray", + "components": 1, + "dri": null, + "markers": "SOI APP0 DQT SOF0 DHT DHT SOS EOI", + "scales": { + "0": { + "width": 64, + "height": 48, + "sha256_rgb565": "8eba70fc8596e08e44def1ed58ed51cc2009d070481d8ef6ef3bbb3653abec8e", + "mean_rgb": [ + 115.5286, + 115.5286, + 115.5286 + ], + "mean_rgb565": [ + 111.875, + 114.1367, + 111.875 + ] + }, + "1": { + "width": 32, + "height": 24, + "sha256_rgb565": "eb9fbd807d5b66d6eb6272d698bd417dfd6bb00a10e67abe1c028eb983ec23ae", + "mean_rgb": [ + 115.3594, + 115.3594, + 115.3594 + ], + "mean_rgb565": [ + 111.4583, + 113.8802, + 111.4583 + ] + }, + "2": { + "width": 16, + "height": 12, + "sha256_rgb565": "77953436e19b7eaec7b5edd796b794d81ad132f14dcb11a9c484c9d9be9a734c", + "mean_rgb": [ + 115.2865, + 115.2865, + 115.2865 + ], + "mean_rgb565": [ + 111.375, + 113.8542, + 111.375 + ] + }, + "3": { + "width": 8, + "height": 6, + "sha256_rgb565": "5c66e9361a9ad52f285084c3c3efeca001bc28397bd7ea589eda71f9712f7a79", + "mean_rgb": [ + 115.25, + 115.25, + 115.25 + ], + "mean_rgb565": [ + 111.6667, + 113.9167, + 111.6667 + ] + } + } + }, + "nodht_320x240.jpg": { + "sha256_file": "d87e5e2e2129fcdea4add8ac09b44ed1c3b0e58964fb31ebefc751bd8c988146", + "bytes": 10740, + "width": 320, + "height": 240, + "sof": "SOF0", + "sampling": "4:2:0", + "components": 3, + "dri": null, + "markers": "SOI APP0 DQT DQT SOF0 SOS EOI", + "scales": { + "0": { + "width": 320, + "height": 240, + "sha256_rgb565": "7080526506e0c21eb112b46ea79e2bbb6d6b0e730ed37f4d5c3cdd59a5324ef4", + "mean_rgb": [ + 127.2433, + 108.2792, + 125.872 + ], + "mean_rgb565": [ + 123.8593, + 106.9648, + 122.5144 + ] + }, + "1": { + "width": 160, + "height": 120, + "sha256_rgb565": "9f57b34a205cf1f884a20c156ba5b92dffba66e9684e1a4032eaf311fa472ee1", + "mean_rgb": [ + 127.0509, + 108.102, + 125.6853 + ], + "mean_rgb565": [ + 123.3308, + 106.6765, + 121.9887 + ] + }, + "2": { + "width": 80, + "height": 60, + "sha256_rgb565": "5b661b9fef344f03d4c9aaf193a999263f7f04c0cee291b7f46f00ff747054fb", + "mean_rgb": [ + 126.9865, + 108.0429, + 125.5894 + ], + "mean_rgb565": [ + 123.1633, + 106.5925, + 121.64 + ] + }, + "3": { + "width": 40, + "height": 30, + "sha256_rgb565": "13dd202945e4b105bc0897b096362448c11add633446f61cfa602044bb7ca8bf", + "mean_rgb": [ + 126.9417, + 108.0033, + 125.5508 + ], + "mean_rgb565": [ + 123.0333, + 106.6033, + 121.6333 + ] + } + } + }, + "odd_size_37x29.jpg": { + "sha256_file": "9efd41499254db78e4b92868095537b4f0f624c8d3fbea2f3a7d683286ef5c01", + "bytes": 1427, + "width": 37, + "height": 29, + "sof": "SOF0", + "sampling": "4:2:0", + "components": 3, + "dri": null, + "markers": "SOI APP0 DQT DQT SOF0 DHT DHT DHT DHT SOS EOI", + "scales": { + "0": { + "width": 37, + "height": 29, + "sha256_rgb565": "5f7b9495686f03d91250e8acb3eb0e75579cf61e8348e60d93a2eb968507af73", + "mean_rgb": [ + 127.4529, + 109.0522, + 128.205 + ], + "mean_rgb565": [ + 124.1454, + 107.5415, + 124.7046 + ] + }, + "1": { + "width": 18, + "height": 14, + "sha256_rgb565": "0c9846c9bc3b267e0bb0b91dd06cda6ad903ec741002180277561506ade15996", + "mean_rgb": [ + 128.5437, + 112.2262, + 128.7857 + ], + "mean_rgb565": [ + 125.0476, + 110.6508, + 125.1746 + ] + }, + "2": { + "width": 9, + "height": 7, + "sha256_rgb565": "d8c2e4957c329082965f2bf8258805ae3c1ee120034960b524404fd6c42bd68d", + "mean_rgb": [ + 128.4444, + 112.127, + 128.6508 + ], + "mean_rgb565": [ + 124.5714, + 110.7302, + 125.4603 + ] + }, + "3": { + "width": 4, + "height": 3, + "sha256_rgb565": "e61c57b332eb7bbce8dfdf2809b28c7fcd0dda2845d6380eac2c9eb98b9b1cd3", + "mean_rgb": [ + 133.6667, + 128.6667, + 132.9167 + ], + "mean_rgb565": [ + 130.0, + 127.0, + 128.6667 + ] + } + } + }, + "restart_dri_320x240.jpg": { + "sha256_file": "bd75b35a1555213b58d493e10738c5fc342572452a88a6e2279765b61fc2c1e9", + "bytes": 11348, + "width": 320, + "height": 240, + "sof": "SOF0", + "sampling": "4:2:0", + "components": 3, + "dri": 4, + "markers": "SOI APP0 DQT DQT SOF0 DHT DHT DHT DHT DRI SOS EOI", + "scales": { + "0": { + "width": 320, + "height": 240, + "sha256_rgb565": "7080526506e0c21eb112b46ea79e2bbb6d6b0e730ed37f4d5c3cdd59a5324ef4", + "mean_rgb": [ + 127.2433, + 108.2792, + 125.872 + ], + "mean_rgb565": [ + 123.8593, + 106.9648, + 122.5144 + ] + }, + "1": { + "width": 160, + "height": 120, + "sha256_rgb565": "9f57b34a205cf1f884a20c156ba5b92dffba66e9684e1a4032eaf311fa472ee1", + "mean_rgb": [ + 127.0509, + 108.102, + 125.6853 + ], + "mean_rgb565": [ + 123.3308, + 106.6765, + 121.9887 + ] + }, + "2": { + "width": 80, + "height": 60, + "sha256_rgb565": "5b661b9fef344f03d4c9aaf193a999263f7f04c0cee291b7f46f00ff747054fb", + "mean_rgb": [ + 126.9865, + 108.0429, + 125.5894 + ], + "mean_rgb565": [ + 123.1633, + 106.5925, + 121.64 + ] + }, + "3": { + "width": 40, + "height": 30, + "sha256_rgb565": "13dd202945e4b105bc0897b096362448c11add633446f61cfa602044bb7ca8bf", + "mean_rgb": [ + 126.9417, + 108.0033, + 125.5508 + ], + "mean_rgb565": [ + 123.0333, + 106.6033, + 121.6333 + ] + } + } + } + }, + "not_referenced": { + "progressive_320x240.jpg": "SOF2: TJpgDec is baseline-only and must refuse it" + } +} diff --git a/modules/jpegio/tests/frames/restart_dri_320x240.jpg b/modules/jpegio/tests/frames/restart_dri_320x240.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4641809584ccdcdb783b6b5eb829c7912c082208 GIT binary patch literal 11348 zcmeHMeOOc189yNb1XM(Wc7Cy1I}!TmEVh1Bjn+}CQxMRluEoqc%EWE>fvYHRtJb>F z+0|J=Dw07fAKJ%E9j)cDrWBPHkN`=Dpe-N`6n0{BAsHd={<4i4lSULyE*9OdXJ5;?j$J3EaU z?K*n2o2#4Kn6cx>jT!4P*3E6)#Bm-I#Gam>qunPxHBtQ3c(JFL1Q7^fj>yr)(a}Xb z#%+w)<`@1Nd5l6Wg5^TN4CLS;5PAskHRJ_(Itthqvb+QikfW3HC>K{Z7?3>J`#C2j(>XI3a1G{TbyU?6hD99Sn8-}UcQj)8C=>j)9=k)2VGnzPI~HR zlb`*?EbrMq^Iv#z!NNt0SN_WXl~u2w{)cT5yQB8}Fet6H#}%lp2%Iz1kR$s_gM zhs8m%f{e=)E-^W##rqo1E~{?w@lI3TIb_`B>YetEYr9WJWxEfvu~o%{DLW^@-05|j z42&!bmp;0m7SDJ|ihm9(2y@{NjCQzpFDBC@xUBkI(RMXlO~&0AB}JO9V{~U#_s#^9 zGVy^p$e;2_xfSFu7F2J5-!JEP-KVhH*a<0^LY7VSEjp*(UUZI@=1b@Vm?tw`+n*GE zn~qEM7fahNJaQBVE%(A2TkvUZ##cTlKAr9ON^HbeY74$HoAId!#iy|ypVmfv<+k9{ z*^IAbP<*Af<14cfpV}6D8k_NH2gO%zJ3gI__)2WSS86l9vO)2wZO5mv5uet`g!)x} z8tP~IxIwqpkTq$oLD3vt8>3Rqu8k?)5~Yeb1{Zrvy4G;s1?~i-EGW`j#-2~!mZF`} zkfN>GR%Dw2Fwf6}?72 z)|pfO*T7muW?N5DECV9cKm=@U^lO&U9PKjQ?i;jRr7_o-o7Zri0FG|IL8qL6ED9|( z3Vj(A&`%eUEH_aP)As}W`=ufXM9KksB3%K++6%mAPBs%5*8{Oy#V}$HL|+3JKTQsI z8q_!F1=Q-&9&H>A?W+;U`zoYdBQYq5-R&BsejWQRp->qK3kfs8Q=~Xn}__(B2O4ee{iBGg- z7Vzkr@d0U8$i@j&zXyDPmlQFET@TZ#OF+KWEi2i`8#V94eXcNCj%7J6y-9qqPc~

KFMv?2w>My!-v6!jAR!In}?!IZBUTE#T z16U!ATgH=OE)kLc3ck^%kw$x%$P!B2lxrK=K^6F-ZV=XfES(gdHR!|MOs_=o-WZVb zQW4gi+?Jwwtd2dG1u6s+&yG6`k~Rv>tC@?&bt2nuWS?LS-&(b#--s)*_H5NT8J-rN ze+b9ghQxktPzJN;Io55(HAtV%PRWVP-s1}bI%H+)mQJia!%K_Pj~thGgju!?uI;Z* zgr!;|;70FI*c0JH&k2Dy}j94b$SH$6#g8w}ree7h}-pt0RGi>o@pMvsnMIuD%>9QV9%SKc-jql~#1 zF}kg+Fe;2rZC8~4rqqfPCOJlPyecqiUGpSk+6#8a24r@vScp+k9!9gN&Xu=yq?c02 zGBrxOE#sG1xp-^Ml3I-BVMGUFlr#MiZxaWpZ(m}PuNGqTXNYB>po8~#`oRK09j-UMhBzvzp2P)wkq3zjwsmmJN3N9UyZ@&!?#U~ViTF~8I_)@ zUT{KhI>elgQ8YqkI~Wy4@f(I7A}vU8jcj7PS_1PvsYxcdlmyln%ibeidxAKBeIV^j zsS_)xrm99p$_S%3%Wg>;bOmIIM0Z%N>&(z2qV)+kK{l#9#zpNlHp7elJ4OYR>C2A2 zy@>uebh=N1$R%t-qVX%Er26!{YL|TGO^n(CnLFM54xvbr+?xhfk%a+H>JlyM8l#V< zeg{I7`SwVdHzb!Zx_jbD#?&YHFQN*YjfsqlcDhqCRR?V{6Qd>-R(Hdx)3A+fbh0@Z z*-00zyJ{*585u_Rl%@-G^4=Grt4_*|$1(bR863e-sU zWESbHRm`uUQN5&O=VtqtKH?u2>k~q4Z;^af+wm3<0MA)IlJs2Yeup%L4>@h(#1>(TvUv7{YB6?mC$VNEy9bCeJzZBM>P7c!gUK#KRhO;hW==u;T!-I>O?OmYZe zPw@Is2zEA-*$jdW$TUq4hd~rO<}+d{ONm_DK8UYRSiJzFxm261Yu4Hr1bJT=BD1MGHSf&KVekmykU1%eq)@Z#l0#F#0_@M+>8?jz0uY^ zxmeb|9zZEDikUX=E{HRfA;WqPZc6zB-xS$OmqI3Dt}z>uI-b5{9S~NiFrH!rxP0is z{eMO*cejHl*dx`EMEZO2atYSigIRmT7^%*H>I|F4k>C=0l-M3IhL22^?SaStEqSI8 z^Ct+UbXmN&BG3KtTJN-YL@x6u6I+esG{!&6<8G3FvDzMt*dxa9Vby(Pv*>$X29MU* zqr~=zF(Q-U?9p0#l-M3IhL248d35<;9^LzSQXh{V+4+XA=G}+OW+B0)_Glb?#27x! zH*BgiY!ZQkpgl@#j~K&8CjC5GJ(x%LekR_>qephW5sx0~xig%jM;?id$m9uRGV(}l TfK2*%bje^IUHaqCIx7AP%7qb- literal 0 HcmV?d00001 diff --git a/modules/jpegio/tests/jpegwalk.py b/modules/jpegio/tests/jpegwalk.py new file mode 100644 index 00000000..811b6a3d --- /dev/null +++ b/modules/jpegio/tests/jpegwalk.py @@ -0,0 +1,198 @@ +"""jpegwalk -- a tiny JPEG marker walker. + +Plain Python with no third-party imports, written so it also runs under +MicroPython (the jpegio unix tests can import it to assert the corpus's +marker sequences). No regexes: JPEG entropy-coded data is binary and the +only safe way to find the next marker is to walk it byte by byte honouring +byte stuffing (FF 00) and RSTn markers. + +walk(data) -> list of segments, each a tuple + + (marker, name, offset, raw) + +where ``raw`` is the exact bytes of that segment: the FF xx marker, plus the +two-byte length and payload for markers that carry one. The SOS segment's +``raw`` also carries the entropy-coded scan data that follows it, up to the +next non-RSTn marker, so RSTn markers stay inside the scan data. Therefore + + b"".join(s[3] for s in walk(data)) == data + +and a file can be rebuilt after dropping or re-ordering segments. +""" + +_NAMES = { + 0x01: "TEM", + 0xC0: "SOF0", 0xC1: "SOF1", 0xC2: "SOF2", 0xC3: "SOF3", + 0xC4: "DHT", + 0xC5: "SOF5", 0xC6: "SOF6", 0xC7: "SOF7", + 0xC8: "JPG", 0xC9: "SOF9", 0xCA: "SOF10", 0xCB: "SOF11", + 0xCC: "DAC", 0xCD: "SOF13", 0xCE: "SOF14", 0xCF: "SOF15", + 0xD8: "SOI", 0xD9: "EOI", 0xDA: "SOS", 0xDB: "DQT", 0xDC: "DNL", + 0xDD: "DRI", 0xDE: "DHP", 0xDF: "EXP", 0xFE: "COM", +} + +SOF_MARKERS = (0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, + 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF) + + +def marker_name(m): + if 0xD0 <= m <= 0xD7: + return "RST%d" % (m - 0xD0) + if 0xE0 <= m <= 0xEF: + return "APP%d" % (m - 0xE0) + if 0xF0 <= m <= 0xFD: + return "JPG%d" % (m - 0xF0) + return _NAMES.get(m, "0xFF%02X" % m) + + +def _standalone(m): + return m == 0xD8 or m == 0xD9 or m == 0x01 or (0xD0 <= m <= 0xD7) + + +def walk(data): + data = bytes(data) + n = len(data) + segs = [] + pos = 0 + while pos < n: + if data[pos] != 0xFF: + raise ValueError("expected a marker at offset %d, got 0x%02X" % (pos, data[pos])) + start = pos + while pos + 1 < n and data[pos + 1] == 0xFF: # fill bytes + pos += 1 + if pos + 1 >= n: + raise ValueError("truncated marker at offset %d" % start) + m = data[pos + 1] + if _standalone(m): + pos += 2 + segs.append((m, marker_name(m), start, data[start:pos])) + if m == 0xD9: + if pos != n: + segs.append((None, "TRAILING", pos, data[pos:])) + break + continue + if pos + 4 > n: + raise ValueError("truncated segment length at offset %d" % pos) + length = (data[pos + 2] << 8) | data[pos + 3] + if length < 2 or pos + 2 + length > n: + raise ValueError("bad segment length %d at offset %d" % (length, pos)) + pos += 2 + length + if m == 0xDA: + # Entropy-coded data: skip to the next marker that is not a + # stuffed zero (FF 00), an RSTn (FF D0..D7) or a fill byte. + p = pos + while True: + q = data.find(b"\xff", p) + if q < 0 or q + 1 >= n: + p = n + break + b = data[q + 1] + if b == 0x00 or (0xD0 <= b <= 0xD7): + p = q + 2 + continue + if b == 0xFF: + p = q + 1 + continue + p = q + break + pos = p + segs.append((m, marker_name(m), start, data[start:pos])) + return segs + + +def rebuild(segs): + return b"".join(s[3] for s in segs) + + +def sequence(segs): + """Marker names in file order; RSTn markers live inside SOS and are not listed.""" + return [s[1] for s in segs] + + +def sof_info(seg): + """Decode a SOFn payload -> dict(precision, height, width, components=[(id, h, v, tq), ...]).""" + raw = seg[3] + p = 4 + info = { + "precision": raw[p], + "height": (raw[p + 1] << 8) | raw[p + 2], + "width": (raw[p + 3] << 8) | raw[p + 4], + } + nc = raw[p + 5] + comps = [] + p += 6 + for _ in range(nc): + comps.append((raw[p], raw[p + 1] >> 4, raw[p + 1] & 0x0F, raw[p + 2])) + p += 3 + info["components"] = comps + return info + + +def sampling_name(comps): + """'4:4:4' / '4:2:2' / '4:2:0' / 'gray' / 'HxV' from SOF component sampling factors.""" + if len(comps) == 1: + return "gray" + h, v = comps[0][1], comps[0][2] + if (h, v) == (1, 1): + return "4:4:4" + if (h, v) == (2, 1): + return "4:2:2" + if (h, v) == (2, 2): + return "4:2:0" + return "%dx%d" % (h, v) + + +def dri_interval(seg): + raw = seg[3] + return (raw[4] << 8) | raw[5] + + +def count_rst(seg): + """Number of RSTn markers inside an SOS segment's scan data.""" + raw = seg[3] + n = 0 + p = 0 + while True: + q = raw.find(b"\xff", p) + if q < 0 or q + 1 >= len(raw): + return n + if 0xD0 <= raw[q + 1] <= 0xD7: + n += 1 + p = q + 1 + + +def describe(data): + """Summary dict for a whole file: sequence, dims, sampling, DRI, RST count, byte size.""" + segs = walk(data) + d = {"bytes": len(data), "sequence": sequence(segs)} + for s in segs: + if s[0] in SOF_MARKERS and "width" not in d: + i = sof_info(s) + d["width"] = i["width"] + d["height"] = i["height"] + d["precision"] = i["precision"] + d["components"] = len(i["components"]) + d["sampling"] = sampling_name(i["components"]) + d["sof"] = s[1] + elif s[0] == 0xDD: + d["dri"] = dri_interval(s) + elif s[0] == 0xDA: + d["rst_markers"] = d.get("rst_markers", 0) + count_rst(s) + return d + + +def format_description(d): + parts = ["%dx%d" % (d.get("width", 0), d.get("height", 0)), + d.get("sof", "?"), d.get("sampling", "?"), + "%d bytes" % d["bytes"]] + if "dri" in d: + parts.append("DRI=%d (%d RST)" % (d["dri"], d.get("rst_markers", 0))) + return " ".join(d["sequence"]) + " | " + ", ".join(parts) + + +if __name__ == "__main__": + import sys + for path in sys.argv[1:]: + with open(path, "rb") as f: + data = f.read() + print("%s: %s" % (path, format_description(describe(data)))) diff --git a/modules/jpegio/tests/make_corpus.py b/modules/jpegio/tests/make_corpus.py new file mode 100644 index 00000000..4bc824f8 --- /dev/null +++ b/modules/jpegio/tests/make_corpus.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +"""Build the jpegio test corpus in frames/ and write frames/README.md. + +Run: python3 modules/jpegio/tests/make_corpus.py (needs Pillow; 12.3 used) + +Every synthetic frame is the same procedural test pattern, drawn in pure +Python and encoded by Pillow/libjpeg: + + * top 40 % -- eight 100 % colour bars (white, yellow, cyan, green, + magenta, red, blue, black): hard chroma edges every w/8 px + * 40 % .. 70 % -- gradient: R along x, G along y, B along the anti-diagonal + * bottom band -- left half a 1-px black/white checker (the highest + spatial frequency an 8x8 DCT can carry, exercises the + AC tables and 4:2:0 chroma against a luma edge); right + half a pure-red / pure-blue split (a saturated chroma + edge that chroma subsampling has to straddle) + +Files that are derived (nodht, dri_first) are made by re-writing the marker +segments with jpegwalk -- never by regex on the binary. Real camera frames +named c920e_*.jpg are never touched; they are only described in the README. +""" +import os +import sys + +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import jpegwalk # noqa: E402 + +FRAMES = os.path.join(HERE, "frames") +QUALITY = 90 +RESTART_BLOCKS = 4 # DRI interval in MCUs for the restart frames + +C920E_PROVENANCE = ("Logitech C920e over usbif UVC on ESP32-S3, captured by the usbif " + "session 2026-09-03; DHT present; DRI present") + +BARS = [(255, 255, 255), (255, 255, 0), (0, 255, 255), (0, 255, 0), + (255, 0, 255), (255, 0, 0), (0, 0, 255), (0, 0, 0)] + + +def pattern(w, h): + """The synthetic test pattern as a Pillow RGB image (see module docstring).""" + y_bars = max(1, (h * 2) // 5) + y_grad = max(y_bars + 1, (h * 7) // 10) + buf = bytearray(w * h * 3) + i = 0 + for y in range(h): + for x in range(w): + if y < y_bars: + r, g, b = BARS[x * 8 // w] + elif y < y_grad: + r = x * 255 // max(1, w - 1) + g = (y - y_bars) * 255 // max(1, y_grad - y_bars - 1) + b = 255 - (x + y) * 255 // max(1, w + h - 2) + elif x < w // 2: + r = g = b = 255 if (x + y) & 1 else 0 + else: + r, g, b = (255, 0, 0) if x < (w * 3) // 4 else (0, 0, 255) + buf[i] = r + buf[i + 1] = g + buf[i + 2] = b + i += 3 + return Image.frombytes("RGB", (w, h), bytes(buf)) + + +def write(name, data): + with open(os.path.join(FRAMES, name), "wb") as f: + f.write(data) + return data + + +def read(name): + with open(os.path.join(FRAMES, name), "rb") as f: + return f.read() + + +def save(name, im, **kw): + kw.setdefault("quality", QUALITY) + im.save(os.path.join(FRAMES, name), "JPEG", **kw) + return read(name) + + +def decoded(name): + return Image.open(os.path.join(FRAMES, name)).convert("RGB").tobytes() + + +def strip_dht(data): + segs = [s for s in jpegwalk.walk(data) if s[0] != 0xC4] + out = jpegwalk.rebuild(segs) + seq = jpegwalk.sequence(jpegwalk.walk(out)) + assert "DHT" not in seq, seq + for must in ("SOI", "SOF0", "SOS", "EOI"): + assert must in seq, (must, seq) + return out + + +def dri_first(data): + """Re-order a JFIF file's segments the way a Logitech C920e emits UVC MJPEG: + SOI DQT.. SOF0 DHT.. APP0 DRI SOS (i.e. NOT JFIF-first).""" + order = ["SOI", "DQT", "SOF0", "DHT", "APP0", "DRI", "SOS", "EOI"] + by = {} + for s in jpegwalk.walk(data): + by.setdefault(s[1], []).append(s) + segs = [] + for name in order: + segs.extend(by.pop(name)) + assert not by, "unexpected segments: %s" % sorted(by) + return jpegwalk.rebuild(segs) + + +# name -> (how it is made) +DESCRIPTIONS = {} + + +def notes(data): + """Walker-derived facts a marker sequence alone does not show.""" + out = [] + dqt_tables = dqt_segs = 0 + for m, name, _off, raw in jpegwalk.walk(data): + if name == "APP0": + ident = raw[4:9] + out.append("APP0=JFIF" if ident == b"JFIF\0" else + "APP0 payload is not JFIF (%d bytes, %s)" % ( + len(raw) - 4, "all zero" if not any(raw[4:]) else raw[4:12].hex())) + elif m == 0xDB: + dqt_segs += 1 + p = 4 + while p < len(raw): + dqt_tables += 1 + p += 1 + (128 if raw[p] >> 4 else 64) + elif name == "COM": + out.append("COM %d bytes%s" % (len(raw) - 4, " (all zero)" if not any(raw[4:]) else "")) + elif name == "TRAILING": + out.append("%d bytes after EOI (%s..)" % (len(raw), raw[:4].hex())) + out.insert(0, "%d DQT table%s in %d segment%s" % ( + dqt_tables, "s" if dqt_tables != 1 else "", dqt_segs, "s" if dqt_segs != 1 else "")) + return "; ".join(out) + + +def build(): + os.makedirs(FRAMES, exist_ok=True) + p64 = pattern(64, 48) + p320 = pattern(320, 240) + + DESCRIPTIONS["baseline_jfif_64x48.jpg"] = ( + "baseline JFIF still of the test pattern; Pillow save(quality=%d, subsampling='4:2:0')" % QUALITY) + save("baseline_jfif_64x48.jpg", p64, subsampling="4:2:0") + + DESCRIPTIONS["baseline_jfif_320x240.jpg"] = ( + "baseline JFIF still of the test pattern at 320x240; Pillow save(quality=%d, " + "subsampling='4:2:0')" % QUALITY) + base320 = save("baseline_jfif_320x240.jpg", p320, subsampling="4:2:0") + + DESCRIPTIONS["restart_dri_320x240.jpg"] = ( + "as baseline_jfif_320x240 but with restart markers: Pillow " + "save(..., restart_marker_blocks=%d) -> DRI segment + RSTn every %d MCUs" + % (RESTART_BLOCKS, RESTART_BLOCKS)) + restart = save("restart_dri_320x240.jpg", p320, subsampling="4:2:0", + restart_marker_blocks=RESTART_BLOCKS) + assert decoded("restart_dri_320x240.jpg") == decoded("baseline_jfif_320x240.jpg") + + DESCRIPTIONS["nodht_320x240.jpg"] = ( + "baseline_jfif_320x240 with every DHT (FFC4) segment removed by jpegwalk -- the " + "'MJPEG without Huffman tables' case; a decoder must either inject the standard " + "tables (ITU T.81 K.3) or refuse it") + write("nodht_320x240.jpg", strip_dht(base320)) + try: + same = decoded("nodht_320x240.jpg") == decoded("baseline_jfif_320x240.jpg") + DESCRIPTIONS["nodht_320x240.jpg"] += ( + "; Pillow/libjpeg-turbo decodes it by injecting the standard tables (pixels %s " + "baseline_jfif_320x240)" % ("identical to" if same else "DIFFER from")) + except Exception as e: # noqa: BLE001 + DESCRIPTIONS["nodht_320x240.jpg"] += "; Pillow refuses it (%s: %s)" % (type(e).__name__, e) + + DESCRIPTIONS["progressive_320x240.jpg"] = ( + "the 320x240 pattern saved progressive (SOF2): Pillow save(progressive=True, " + "subsampling='4:2:0'); TJpgDec is baseline-only and must refuse it") + save("progressive_320x240.jpg", p320, subsampling="4:2:0", progressive=True) + + DESCRIPTIONS["odd_size_37x29.jpg"] = ( + "baseline, odd dimensions so the right column and bottom row of MCUs are partial " + "(37 = 2x16 + 5, 29 = 1x16 + 13); Pillow save(quality=%d, subsampling='4:2:0')" % QUALITY) + save("odd_size_37x29.jpg", pattern(37, 29), subsampling="4:2:0") + + DESCRIPTIONS["grayscale_64x48.jpg"] = ( + "baseline grayscale (one component): pattern(64,48).convert('L'); " + "Pillow save(quality=%d)" % QUALITY) + save("grayscale_64x48.jpg", p64.convert("L")) + + DESCRIPTIONS["dri_first_mjpeg_style_320x240.jpg"] = ( + "restart_dri_320x240 with its segments re-ordered by jpegwalk to SOI DQT DQT SOF0 " + "DHT DHT DHT DHT APP0 DRI SOS (APP0/JFIF after the tables, as a Logitech C920e " + "emits UVC MJPEG); Pillow decodes it pixel-identical to restart_dri_320x240") + write("dri_first_mjpeg_style_320x240.jpg", dri_first(restart)) + assert decoded("dri_first_mjpeg_style_320x240.jpg") == decoded("restart_dri_320x240.jpg") + seq = jpegwalk.describe(read("dri_first_mjpeg_style_320x240.jpg"))["sequence"] + assert seq[:3] == ["SOI", "DQT", "DQT"] and seq[3] == "SOF0" and seq[-3:] == ["DRI", "SOS", "EOI"], seq + assert seq.index("APP0") > seq.index("DHT"), seq + + +def readme(): + names = sorted(n for n in os.listdir(FRAMES) if n.lower().endswith(".jpg")) + lines = [ + "# jpegio test corpus", + "", + "Generated by [`../make_corpus.py`](../make_corpus.py) (Pillow %s); marker sequences and" + " sizes below come from [`../jpegwalk.py`](../jpegwalk.py). `reference.json` is the" + " Pillow fidelity reference written by [`../reference.py`](../reference.py) -- per" + " baseline frame and scale 0..3: RGB565 buffer sha256 and per-channel means. It is a" + " tolerance reference, not a bit-exact golden (TJpgDec and libjpeg differ in IDCT" + " rounding and chroma upsampling). `golden_tjpgd.json` is the bit-exact one: TJpgDec's" + " own sha256 per frame and scale, written by [`../test_jpegio.py`](../test_jpegio.py)" + " `--record` and asserted on every run." % Image.__version__, + "", + "Synthetic frames share one procedural pattern: colour bars (top 40 %), an x/y/diagonal" + " RGB gradient (40-70 %), a 1-px black/white checker (bottom left) and a red|blue" + " split (bottom right). Marker sequences omit the RSTn markers inside the scan; their" + " count is given with the DRI interval (a DRI of 0 means the segment is present but restart" + " intervals are disabled). `TRAILING` = bytes after EOI.", + "", + "| file | what / how made | markers | size | sampling | bytes | DRI | notes |", + "|------|-----------------|---------|------|----------|-------|-----|-------|", + ] + for name in names: + d = jpegwalk.describe(read(name)) + if name.startswith("c920e_"): + what = "real camera frame: %s" % C920E_PROVENANCE + else: + what = DESCRIPTIONS.get(name, "(not produced by make_corpus.py)") + dri = "%d (%d RST)" % (d["dri"], d.get("rst_markers", 0)) if "dri" in d else "-" + lines.append("| `%s` | %s | `%s` | %dx%d %s | %s | %d | %s | %s |" % ( + name, what, " ".join(d["sequence"]), d["width"], d["height"], d.get("sof", "?"), + d.get("sampling", "?"), d["bytes"], dri, notes(read(name)))) + lines.append("") + with open(os.path.join(FRAMES, "README.md"), "w") as f: + f.write("\n".join(lines)) + return names + + +if __name__ == "__main__": + build() + for name in readme(): + print("%-36s %s" % (name, jpegwalk.format_description(jpegwalk.describe(read(name))))) diff --git a/modules/jpegio/tests/reference.py b/modules/jpegio/tests/reference.py new file mode 100644 index 00000000..c08431cb --- /dev/null +++ b/modules/jpegio/tests/reference.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Pillow fidelity reference for the jpegio corpus -> frames/reference.json. + +Run: python3 modules/jpegio/tests/reference.py (needs Pillow; 12.3 used) + +For every baseline (SOF0) frame in frames/ that Pillow can decode: + + 1. full decode with Pillow to 8-bit RGB (libjpeg-turbo: islow IDCT, "fancy" + triangle chroma upsampling; grayscale is expanded to R=G=B); + 2. for scale 0..3, integer box averaging of that full decode in pure Python: + each output pixel is the sum of a (1<> scale) x floor(h >> scale). + That mirrors what TJpgDec R0.03 does in mcu_output(): it averages the same + squares with the same truncating shift and clips per MCU so the remainder + columns/rows of a partial MCU are dropped. Pillow's draft() (libjpeg's + DCT-domain scaling) and reduce() (rounds to nearest) are deliberately NOT + used: neither matches TJpgDec's arithmetic. + 3. pack to RGB565 exactly as tjpgd.c does under JD_FORMAT 1, + ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3), stored as little-endian + uint16 (native order on every PyDevices target), row-major, no stride; + 4. record width, height, sha256 of the packed buffer, and per-channel means + of the 8-bit image (mean_rgb) and of the channels recoverable from the + RGB565 buffer, i.e. r&0xF8, g&0xFC, b&0xF8 (mean_rgb565). + +This is a FIDELITY reference, not a bit-exact golden: TJpgDec's fixed-point +IDCT, nearest-neighbour chroma replication and (at scale 3) DC-only blocks all +differ from libjpeg by a little, so a test compares means (and, if it wants, +per-pixel error against a decode of the same frame) with a tolerance. The +sha256 is there so an exact match, should one occur, can be recognised. +""" +import hashlib +import json +import os +import sys + +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import jpegwalk # noqa: E402 + +FRAMES = os.path.join(HERE, "frames") +OUT = os.path.join(FRAMES, "reference.json") +SCALES = (0, 1, 2, 3) + + +def box_down(rgb, w, h, scale): + """Truncating integer box average of packed 8-bit RGB by 2**scale (TJpgDec style).""" + if scale == 0: + return bytes(rgb), w, h + f = 1 << scale + sh = 2 * scale + ow, oh = w >> scale, h >> scale + out = bytearray(ow * oh * 3) + o = 0 + for oy in range(oh): + y0 = oy * f + for ox in range(ow): + r = g = b = 0 + for yy in range(y0, y0 + f): + base = (yy * w + ox * f) * 3 + row = rgb[base:base + f * 3] + r += sum(row[0::3]) + g += sum(row[1::3]) + b += sum(row[2::3]) + out[o] = r >> sh + out[o + 1] = g >> sh + out[o + 2] = b >> sh + o += 3 + return bytes(out), ow, oh + + +def pack_rgb565(rgb): + n = len(rgb) // 3 + out = bytearray(n * 2) + for i in range(n): + j = 3 * i + v = ((rgb[j] & 0xF8) << 8) | ((rgb[j + 1] & 0xFC) << 3) | (rgb[j + 2] >> 3) + out[2 * i] = v & 0xFF + out[2 * i + 1] = v >> 8 + return bytes(out) + + +def means(rgb): + n = len(rgb) // 3 + return [round(sum(rgb[c::3]) / n, 4) for c in range(3)] + + +def means_565(rgb): + n = len(rgb) // 3 + return [round(sum(v & 0xF8 for v in rgb[0::3]) / n, 4), + round(sum(v & 0xFC for v in rgb[1::3]) / n, 4), + round(sum(v & 0xF8 for v in rgb[2::3]) / n, 4)] + + +def reference_for(path): + im = Image.open(path).convert("RGB") + w, h = im.size + full = im.tobytes() + scales = {} + for s in SCALES: + rgb, ow, oh = box_down(full, w, h, s) + packed = pack_rgb565(rgb) + scales[str(s)] = { + "width": ow, + "height": oh, + "sha256_rgb565": hashlib.sha256(packed).hexdigest(), + "mean_rgb": means(rgb), + "mean_rgb565": means_565(rgb), + } + return scales + + +def main(): + doc = { + "_about": [ + "Pillow fidelity reference for modules/jpegio/tests/frames; written by modules/jpegio/tests/reference.py.", + "scales[k]: decode at scale k (1/2**k) -> width x height RGB565 buffer, little-endian", + "uint16, row-major; sha256_rgb565 is the digest of that buffer; mean_rgb are per-channel", + "means of the 8-bit reference, mean_rgb565 the means of r&0xF8, g&0xFC, b&0xF8 (what a", + "decoder's RGB565 output unpacks to). Downscaling is a truncating integer box average of", + "the full libjpeg decode (as TJpgDec's mcu_output does), NOT draft()/reduce().", + "Compare with a tolerance: TJpgDec's IDCT, chroma replication and DC-only 1/8 differ.", + ], + "pillow": Image.__version__, + "rgb565": "((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3), little-endian uint16 (native order on all PyDevices targets)", + "scale_dims": "width = source_width >> scale, height = source_height >> scale (partial-MCU remainder dropped, as TJpgDec does)", + "frames": {}, + "not_referenced": {}, + } + for name in sorted(os.listdir(FRAMES)): + if not name.lower().endswith(".jpg"): + continue + path = os.path.join(FRAMES, name) + with open(path, "rb") as f: + data = f.read() + d = jpegwalk.describe(data) + entry = { + "sha256_file": hashlib.sha256(data).hexdigest(), + "bytes": len(data), + "width": d["width"], + "height": d["height"], + "sof": d["sof"], + "sampling": d["sampling"], + "components": d["components"], + "dri": d.get("dri"), + "markers": " ".join(d["sequence"]), + } + if d["sof"] != "SOF0": + doc["not_referenced"][name] = "%s: TJpgDec is baseline-only and must refuse it" % d["sof"] + continue + try: + entry["scales"] = reference_for(path) + except Exception as e: # noqa: BLE001 + doc["not_referenced"][name] = "Pillow cannot decode it: %s: %s" % (type(e).__name__, e) + continue + doc["frames"][name] = entry + print("%-36s %dx%d %s" % (name, d["width"], d["height"], + " ".join("s%s=%dx%d" % (k, v["width"], v["height"]) + for k, v in entry["scales"].items()))) + for name, why in doc["not_referenced"].items(): + print("%-36s not referenced: %s" % (name, why)) + with open(OUT, "w") as f: + json.dump(doc, f, indent=1) + f.write("\n") + print("wrote", OUT) + + +if __name__ == "__main__": + main() diff --git a/modules/jpegio/tests/test_jpegio.py b/modules/jpegio/tests/test_jpegio.py new file mode 100644 index 00000000..fb203e12 --- /dev/null +++ b/modules/jpegio/tests/test_jpegio.py @@ -0,0 +1,580 @@ +"""jpegio -- unix MicroPython tests against the corpus and the Pillow reference. + +The Phase 1 gate of the org's docs/jpegio-vision.md, checked mechanically: + + 1. every baseline corpus frame opens to reference.json's size and decodes + at scales 0..3 into a tight RGB565 buffer whose per-channel means sit + within a tolerance of the Pillow reference (the measured error is + printed beside the tolerance); TJpgDec's own digest per frame/scale is + kept in frames/golden_tjpgd.json -- recorded on the first run (or with + --record), asserted on every later one; + 2. the callback target covers the decoded image exactly once at every + scale, in raster order, and reassembles to the buffer target's bytes; + 3. the buffer target honours x / y / stride, leaves the rest of a larger + buffer alone, and refuses bad geometry -- x / y outside 0..65535, a + stride / y product that wraps size_t -- with the numbers in the message; + 4. the DHT-less frame is refused with JDR_FMT1 -- the Phase 1 contract; + 5. the progressive frame, truncated frames, a bare SOI, and non-JPEG bytes + all raise (never crash), and open() rejects non-source objects; + 6. the restart-marker frames decode to the baseline frame's digests; + 7. real c920e_* camera frames open, decode at every scale and are digested; + 8. one decoder object re-opens across frames and source kinds, and + width / height follow the last open() (a failed one reports nothing); + 9. 50 decodes do not trend gc.mem_free() down. + +Run from anywhere (paths come from __file__) with the +unix MicroPython built with this repo on USER_C_MODULES: + + builds/unix/standard/micropython modules/jpegio/tests/test_jpegio.py [--record] + +Plain asserts; needs only the built interpreter and frames/. Pillow's side is +frozen in frames/reference.json by reference.py, so Pillow is not needed here. +""" +import binascii +import gc +import hashlib +import io +import json +import math +import os +import sys +import time + +import jpegio + +HERE = __file__.rsplit("/", 1)[0] if "/" in __file__ else "." +FRAMES = HERE + "/frames" +REFERENCE = FRAMES + "/reference.json" +GOLDEN = FRAMES + "/golden_tjpgd.json" +SCALES = (0, 1, 2, 3) +RECORD = "--record" in sys.argv[1:] + +# --- fidelity tolerance ------------------------------------------------------ +# +# reference.json holds per-channel means of Pillow's (libjpeg-turbo) decode, +# box-averaged the way TJpgDec's mcu_output() scales. TJpgDec is not +# bit-identical to libjpeg: its fixed-point IDCT rounds differently, it +# replicates chroma where libjpeg interpolates ("fancy upsampling"), and at +# scale 3 it emits each block's DC value (chroma from the whole 16x16 MCU at +# 4:2:0) instead of a box average. Per pixel those differences are large on +# the synthetic pattern's hard chroma edges (up to ~150 levels at scale 0, +# ~190 at scale 3, measured against Pillow while writing this test) but they +# alternate in sign, so the means stay close. Measured |mean error| on this +# corpus, TJpgDec R0.03 with CircuitPython's tjpgdcnf.h, 2026-09-03: +# +# scales 0..2, every frame: max 1.27 (odd_size_37x29 s2, blue) +# scale 3, 1200 decoded px (320x240 -> 40x30): max 1.59 (blue) +# scale 3, 48 decoded px (64x48 -> 8x6): max 1.67 (blue) +# scale 3, 12 decoded px (37x29 -> 4x3): max 9.33 (red) +# +# Tolerance: 2.0 levels at scales 0..2 (1.6x the measured worst; one RGB565 +# step is 4 levels in green, 8 in red/blue, so a systematic one-step shift in +# any channel trips it). At scale 3: 2.0 + 32 / sqrt(n), n = decoded pixels +# (2.9 at 40x30, 6.6 at 8x6, 11.2 at 4x3): DC-only chroma is wrong by a bounded +# amount per pixel and averages out as 1/sqrt(n), so a tiny image needs more +# room. Means alone are a weak witness (a scrambled block order keeps them; a +# swapped R/B on this near-symmetric pattern moves them by ~1.4), which is why +# they are paired with the golden digests and the positional checks below. +MEAN_TOL = 2.0 +MEAN_TOL_SCALE3_K = 32.0 + + +def mean_tolerance(scale, npixels): + if scale < 3: + return MEAN_TOL + return MEAN_TOL + MEAN_TOL_SCALE3_K / math.sqrt(npixels) + + +# --- positional checks on the synthetic pattern ----------------------------- +# +# make_corpus.py draws: top 40 % eight colour bars; 40-70 % a gradient; bottom +# band = 1-px checker (left half) and a red|blue split (right half). Sampling +# the centre of each bar (and of the red / blue halves) pins channel identity +# and block placement, which means cannot. Measured deviation at those +# samples: 0 on every frame/scale the rules below admit, 8 on the 64x48 split. +# A 4:2:0 chroma sample spans 2 source px (a 16x16 MCU at scale 3), so bars +# narrower than 8 source px (16 at scale 3) are not sampled: their centre +# chroma is a mix of two bars, by construction of the format, not by defect. +BARS = ((255, 255, 255), (255, 255, 0), (0, 255, 255), (0, 255, 0), + (255, 0, 255), (255, 0, 0), (0, 0, 255), (0, 0, 0)) +POS_TOL = 24 +BAR_MIN_SRC_PX = {0: 8, 1: 8, 2: 8, 3: 16} + + +def pattern_geometry(w, h): + y_bars = max(1, (h * 2) // 5) + y_grad = max(y_bars + 1, (h * 7) // 10) + return y_bars, y_grad + + +def rgb565_to_888(v): + return ((v >> 8) & 0xF8, (v >> 3) & 0xFC, (v << 3) & 0xF8) + + +def pixel(buf, stride, x, y): + i = 2 * (y * stride + x) + return rgb565_to_888(buf[i] | (buf[i + 1] << 8)) + + +def expect_rgb(rgb): + return (rgb[0] & 0xF8, rgb[1] & 0xFC, rgb[2] & 0xF8) + + +def luma(rgb): + return (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) // 1000 + + +# --- helpers ----------------------------------------------------------------- + +def sha256_hex(buf): + return binascii.hexlify(hashlib.sha256(buf).digest()).decode() + + +def means565(buf, n): + r = g = b = 0 + for i in range(0, 2 * n, 2): + lo = buf[i] + hi = buf[i + 1] + r += hi & 0xF8 + g += ((hi & 0x07) << 5) | ((lo >> 5) << 2) + b += (lo << 3) & 0xF8 + return (r / n, g / n, b / n) + + +def read_file(name): + with open(FRAMES + "/" + name, "rb") as f: + return f.read() + + +def expect_raises(exc, fn, *needles): + """fn() must raise exc (or a subclass) whose str() contains every needle.""" + try: + fn() + except exc as e: + msg = str(e) + for needle in needles: + assert needle in msg, "expected %r in %s: %s" % (needle, type(e).__name__, msg) + return e + except Exception as e: # noqa: BLE001 + raise AssertionError("expected %s, got %s: %s" % (exc.__name__, type(e).__name__, e)) + raise AssertionError("expected %s, nothing raised" % exc.__name__) + + +def decode_tight(dec, source, scale): + w, h = dec.open(source) + dw, dh = w >> scale, h >> scale + buf = bytearray(dw * dh * 2) + dec.decode(buf, scale) + return dw, dh, buf + + +def decode_via_callback(dec, source, scale): + """decode(callable): coverage mask + raster-order check; returns the reassembled buffer.""" + w, h = dec.open(source) + dw, dh = w >> scale, h >> scale + mask = bytearray(dw * dh) + out = bytearray(dw * dh * 2) + state = [0, -1, -1, None] # blocks, last y, last x, last view + + def cb(x, y, bw, bh, mv): + assert bw > 0 and bh > 0 and 0 <= x and 0 <= y, (x, y, bw, bh) + assert x + bw <= dw and y + bh <= dh, "block %dx%d at (%d, %d) outside %dx%d" % (bw, bh, x, y, dw, dh) + assert len(mv) == bw * bh, (len(mv), bw, bh) + raw = bytes(mv) + assert len(raw) == bw * bh * 2 + assert y > state[1] or (y == state[1] and x > state[2]), "not raster order: (%d, %d) after (%d, %d)" % (x, y, state[2], state[1]) + state[1], state[2] = y, x + for r in range(bh): + o = (y + r) * dw + x + assert not any(mask[o:o + bw]), "pixel written twice in row %d, x %d..%d" % (y + r, x, x + bw) + mask[o:o + bw] = b"\x01" * bw + out[2 * o:2 * (o + bw)] = raw[2 * r * bw:2 * (r + 1) * bw] + state[0] += 1 + state[3] = mv + + dec.decode(cb, scale) + gaps = mask.count(b"\x00") if hasattr(mask, "count") else sum(1 for m in mask if not m) + assert gaps == 0, "%d of %d pixels never written at scale %d" % (gaps, dw * dh, scale) + assert state[3] is not None and len(state[3]) == 0, "block view must be emptied after decode()" + return out, state[0] + + +def check_pattern(name, scale, buf, dw, dh, w, h): + """Positional checks on the synthetic frames; returns the number of samples checked.""" + y_bars, y_grad = pattern_geometry(w, h) + band = y_bars >> scale + gray = name.startswith("grayscale_") + checked = 0 + bar_src_px = w / 8 + if band >= 2 and dw >= 8 and (gray or bar_src_px >= BAR_MIN_SRC_PX[scale]): + for i, rgb in enumerate(BARS): + x = int((i + 0.5) * dw / 8) + got = pixel(buf, dw, x, band // 2) + if gray: + L = luma(rgb) + want = expect_rgb((L, L, L)) + else: + want = expect_rgb(rgb) + err = max(abs(got[c] - want[c]) for c in range(3)) + assert err <= POS_TOL, "%s s%d bar %d at (%d, %d): got %s want %s" % (name, scale, i, x, band // 2, got, want) + checked += 1 + if scale < 3 and w >= 64 and not gray: + y = ((y_grad >> scale) + dh) // 2 + for xf, rgb in ((5 / 8, (255, 0, 0)), (7 / 8, (0, 0, 255))): + x = int(dw * xf) + got = pixel(buf, dw, x, y) + want = expect_rgb(rgb) + err = max(abs(got[c] - want[c]) for c in range(3)) + assert err <= POS_TOL, "%s s%d split at (%d, %d): got %s want %s" % (name, scale, x, y, got, want) + checked += 1 + if gray: + # TJpgDec builds R=G=B=Y for a one-component image: after RGB565 + # packing r == b exactly and g - r is 0 or 4 on every pixel. + for i in range(0, 2 * dw * dh, 2): + r, g, b = rgb565_to_888(buf[i] | (buf[i + 1] << 8)) + assert r == b and 0 <= g - r <= 4, "%s s%d pixel %d not grey: %s" % (name, scale, i // 2, (r, g, b)) + checked += dw * dh + return checked + + +def load_golden(): + try: + with open(GOLDEN) as f: + return json.load(f)["frames"] + except OSError: + return None + + +def write_golden(digests): + lines = ['{', ' "_about": [', + ' "TJpgDec\'s own sha256 per corpus frame and scale: the RGB565 buffer decode() fills",', + ' "(native-order uint16, row-major, tight), written by test_jpegio.py --record.",', + ' "A parity digest, not a fidelity claim: its evidence is the Pillow mean/positional",', + ' "check test_jpegio.py prints beside it. Re-record deliberately after a TJpgDec or",', + ' "tjpgdcnf.h change, never to make a red run green."', + ' ],', + ' "decoder": "TJpgDec R0.03 patch1, CircuitPython tjpgdcnf.h (JD_FORMAT 1, JD_USE_SCALE 1, JD_TBLCLIP 1, JD_FASTDECODE 1)",', + ' "frames": {'] + names = sorted(digests) + for i, name in enumerate(names): + lines.append(' "%s": {' % name) + for s in SCALES: + lines.append(' "%d": "%s"%s' % (s, digests[name][s], "," if s != SCALES[-1] else "")) + lines.append(' }%s' % ("," if i != len(names) - 1 else "")) + lines += [' }', '}', ''] + with open(GOLDEN, "w") as f: + f.write("\n".join(lines)) + + +# --- the tests ----------------------------------------------------------------- + +def main(): + t0 = time.time() + with open(REFERENCE) as f: + ref = json.load(f) + frames = ref["frames"] + dec = jpegio.JpegDecoder() + golden = None if RECORD else load_golden() + digests = {} + refused = {} + worst = {} + samples = 0 + + # 1. sizes, tight decode at every scale, means vs Pillow, digests vs golden + print("== 1. corpus frames: size, tight decode, means vs Pillow (tolerance beside), digest") + for name in sorted(frames): + entry = frames[name] + try: + w, h = dec.open(FRAMES + "/" + name) + except ValueError as e: + refused[name] = str(e) + print("%-36s open() refused: %s" % (name, e)) + continue + assert (w, h) == (entry["width"], entry["height"]), (name, w, h) + assert (dec.width, dec.height) == (w, h) + digests[name] = {} + for s in SCALES: + r = entry["scales"][str(s)] + dw, dh, buf = decode_tight(dec, FRAMES + "/" + name, s) + assert (dw, dh) == (r["width"], r["height"]), (name, s, dw, dh) + assert len(buf) == dw * dh * 2 + m = means565(buf, dw * dh) + err = [m[c] - r["mean_rgb565"][c] for c in range(3)] + tol = mean_tolerance(s, dw * dh) + worst[s] = max(worst.get(s, 0.0), max(abs(e) for e in err)) + digest = sha256_hex(buf) + digests[name][s] = digest + status = "recorded" + if golden is not None: + g = golden.get(name, {}).get(str(s)) + status = "golden match" if g == digest else "GOLDEN MISMATCH (golden %s)" % (g[:16] if g else None) + print("%-36s s%d %4dx%-4d mean err r=%+6.3f g=%+6.3f b=%+6.3f tol %5.2f sha256 %s.. %s" % ( + name, s, dw, dh, err[0], err[1], err[2], tol, digest[:16], status)) + for c, e in enumerate(err): + assert abs(e) <= tol, "%s s%d channel %d mean error %.3f exceeds %.2f" % (name, s, c, e, tol) + if golden is not None: + assert status == "golden match", "%s s%d: TJpgDec digest changed" % (name, s) + if name.startswith("c920e_"): + print("%-36s s%d means r=%.2f g=%.2f b=%.2f (Pillow r=%.2f g=%.2f b=%.2f)" % ( + name, s, m[0], m[1], m[2], r["mean_rgb565"][0], r["mean_rgb565"][1], r["mean_rgb565"][2])) + else: + samples += check_pattern(name, s, buf, dw, dh, w, h) + print("worst |mean err| per scale: " + " ".join("s%d=%.3f" % (s, worst[s]) for s in SCALES) + + " (tolerance %.1f at s0..2, %.1f + %.0f/sqrt(n) at s3)" % (MEAN_TOL, MEAN_TOL, MEAN_TOL_SCALE3_K)) + print("positional samples checked on the synthetic frames: %d" % samples) + assert samples > 0 + if golden is None: + write_golden(digests) + print("golden: RECORDED %d digests to %s" % (sum(len(v) for v in digests.values()), GOLDEN)) + else: + extra = sorted(set(golden) - set(digests)) + assert not extra, "golden has frames this run did not decode: %s" % extra + print("golden: %d digests match %s" % (sum(len(v) for v in digests.values()), GOLDEN)) + + # 6. restart markers and marker order must not change a pixel + print("== 6. restart / marker-order frames vs baseline_jfif_320x240.jpg") + base = digests["baseline_jfif_320x240.jpg"] + for other in ("restart_dri_320x240.jpg", "dri_first_mjpeg_style_320x240.jpg"): + for s in SCALES: + same = digests[other][s] == base[s] + print("%-36s s%d %s" % (other, s, "== baseline" if same else "FINDING: differs %s vs baseline %s" % (digests[other][s], base[s]))) + assert same, "%s decodes differently from the baseline at scale %d" % (other, s) + + # 7. real camera frames: every c920e_*.jpg in frames/ (means printed above when referenced) + print("== 7. c920e camera frames") + cams = sorted(n for n in os.listdir(FRAMES) if n.startswith("c920e_") and n.endswith(".jpg")) + assert cams, "no c920e_*.jpg in %s" % FRAMES + for name in cams: + if name in digests: + print("%-36s referenced: decoded and digested at every scale (see section 1)" % name) + continue + w, h = dec.open(FRAMES + "/" + name) + digests[name] = {} + for s in SCALES: + dw, dh, buf = decode_tight(dec, FRAMES + "/" + name, s) + m = means565(buf, dw * dh) + digests[name][s] = sha256_hex(buf) + print("%-36s s%d %4dx%-4d means r=%.2f g=%.2f b=%.2f sha256 %s.. (not in reference.json)" % ( + name, s, dw, dh, m[0], m[1], m[2], digests[name][s][:16])) + + # 2. callback path: exactly-once coverage at every scale, same bytes as the buffer path + print("== 2. callback coverage") + for name in sorted(digests): + src = read_file(name) + for s in SCALES: + out, blocks = decode_via_callback(dec, src, s) + assert sha256_hex(out) == digests[name][s], "%s s%d: callback bytes differ from buffer path" % (name, s) + print("%-36s s%d %4d blocks, every pixel once, reassembled == buffer path" % (name, s, blocks)) + # the callback path allocates nothing per block (README): a fixed-arity + # Python function costs no heap per call, so the whole decode should too. + full = read_file("baseline_jfif_320x240.jpg") + + def sink(x, y, w, h, mv): + pass + + dec.open(full) + dec.decode(sink, 1) + dec.open(full) + gc.collect() + a0 = gc.mem_alloc() + dec.decode(sink, 0) + grew = gc.mem_alloc() - a0 + print("callback decode of 320x240 at scale 0 (300 blocks) allocated %d bytes" % grew) + assert grew < 300, "per-block allocation on the callback path" + # a raise inside the callback propagates and consumes the open() + dec.open(full) + + class Stop(Exception): + pass + + def stop(x, y, w, h, mv): + raise Stop("at (%d, %d)" % (x, y)) + + expect_raises(Stop, lambda: dec.decode(stop), "at (0, 0)") + expect_raises(RuntimeError, lambda: dec.decode(bytearray(8)), "decode() without open()") + expect_raises(TypeError, lambda: (dec.open(full), dec.decode(sink, stride=5)), "stride") + # x, y are bounded like the image size (0..65535) on this path too, and + # inside the bound the callback gets them whole: before the fix 2**62 + # went through MP_OBJ_NEW_SMALL_INT unchecked and arrived negative. + small = read_file("baseline_jfif_64x48.jpg") + dec.open(small) + expect_raises(ValueError, lambda: dec.decode(sink, 0, 2 ** 62, 0), "0..65535", str(2 ** 62)) + expect_raises(ValueError, lambda: dec.decode(sink, 0, 0, 65536), "0..65535", "65536") + first = [] + dec.decode(lambda x, y, w, h, mv: first.append((x, y)) if not first else None, 3, 65535, 65535) + assert first == [(65535, 65535)], first + print("callback x, y: 2**62 and 65536 refused (0..65535); at (65535, 65535) the first block reports (65535, 65535)") + + # 3. buffer path with x, y, stride; untouched region stays untouched; bad geometry + print("== 3. buffer target: offsets, stride, untouched region, errors") + for name, s in (("baseline_jfif_64x48.jpg", 0), ("odd_size_37x29.jpg", 0), ("baseline_jfif_320x240.jpg", 1), ("c920e_320x240_dri.jpg", 2)): + src = read_file(name) + dw, dh, tight = decode_tight(dec, src, s) + x, y, stride, rows = 5, 3, dw + 13, dh + 7 + big = bytearray(b"\xa5" * (stride * rows * 2)) + want = bytearray(big) + for r in range(dh): + o = ((y + r) * stride + x) * 2 + want[o:o + dw * 2] = tight[r * dw * 2:(r + 1) * dw * 2] + dec.open(src) + dec.decode(big, s, x, y, stride=stride) + assert big == want, "%s s%d: offset decode wrote outside or wrote wrong pixels" % (name, s) + # keyword form, and a memoryview slice as the target + big2 = bytearray(b"\xa5" * (stride * rows * 2 + 6)) + dec.open(src) + dec.decode(memoryview(big2)[6:], scale=s, x=x, y=y, stride=stride) + assert big2[6:] == want and big2[:6] == b"\xa5" * 6 + print("%-36s s%d %dx%d at (%d, %d) stride %d into %d rows: region exact, %d other bytes untouched" % ( + name, s, dw, dh, x, y, stride, rows, len(big) - dw * dh * 2)) + w, h = dec.open(full) + n = w * h * 2 + expect_raises(ValueError, lambda: dec.decode(bytearray(1000)), "320x240", "153600", "1000") + # x > 0 with the default stride can never fit; the message says what to pass + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, 1, 0), "1 + 320", "default stride 320", "pass stride=") + e = expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, 1, 0, stride=320), "1 + 320", "stride 320") + assert "default" not in str(e), e + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, 0, 1), "(0, 1)", "154240", "153600") + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, 0, 0, stride=319), "320", "319") + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, 0, 0, stride=-5), "-5") + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, -1, 0), "-1") + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, 0, -2), "-2") + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 4), "4") + expect_raises(ValueError, lambda: dec.decode(bytearray(n), -1), "-1") + expect_raises(TypeError, lambda: dec.decode(b"\x00" * n)) + # x, y are bounded like the image size (0..65535); the message keeps the + # value whole rather than printing it through an int + for bad in (65536, 2 ** 40, 2 ** 62): + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, bad, 0), "0..65535", str(bad)) + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, 0, bad), "0..65535", str(bad)) + # the buffer-fit check must not wrap: (y + h - 1) * stride is 2**bits + # here, i.e. 0 in size_t, so an unchecked product sees only the last + # row's 320 pixels (640 bytes), passes a 1000-byte buffer, and the decode walks off + # it (SIGSEGV before the fix). Then the product wrapping to a small + # non-zero count, which the 153600-byte buffer would also have passed. + bits = 64 if sys.maxsize > 2 ** 32 else 32 + y_wrap = 2 ** 15 - (h - 1) + e = expect_raises(ValueError, lambda: dec.decode(bytearray(1000), 0, 0, y_wrap, stride=2 ** (bits - 15)), + "target too small", "overflows size_t", "buffer has 1000") + print("wrapping geometry refused: %s" % e) + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, 0, y_wrap, stride=2 ** (bits - 15) + 1), + "target too small", "overflows size_t") + # the widest legal placement is arithmetic the check must get right, not refuse + expect_raises(ValueError, lambda: dec.decode(bytearray(n), 0, 65535, 0, stride=65535 + 320), + "target too small", "needs %d bytes" % ((239 * (65535 + 320) + 65535 + 320) * 2)) + # a rejected decode() does not consume the open(): retry without re-opening + buf = bytearray(n) + dec.decode(buf) + assert sha256_hex(buf) == digests["baseline_jfif_320x240.jpg"][0] + print("bad geometry: 21 rejections named their numbers; the open() survived them and decoded") + + # 4. the DHT-less frame: Phase 1 refuses it (JDR_FMT1); nothing is injected + print("== 4. nodht_320x240.jpg") + # The contract Phase 1 chose (README.md, "Errors"): a frame + # without Huffman tables fails open() with JDR_FMT1; injecting the T.81 + # K.3 tables is the open decision in docs/jpegio-vision.md, not something + # this module does silently. If a later phase injects them, this section + # becomes: open() -> (320, 240) and digests == baseline_jfif_320x240.jpg. + assert "nodht_320x240.jpg" in refused, "nodht frame decoded -- the module injected tables? update section 4" + assert sorted(refused) == ["nodht_320x240.jpg"], "unexpected refusals: %s" % refused + e = expect_raises(ValueError, lambda: dec.open(FRAMES + "/nodht_320x240.jpg"), "JDR_FMT1", "unsupported or malformed") + expect_raises(RuntimeError, lambda: dec.decode(bytearray(8)), "decode() without open()") + print("refused at open(): %s" % e) + + # 5. refusals that must raise, never crash + print("== 5. progressive, truncated, bare SOI, non-JPEG, non-source objects") + e = expect_raises(ValueError, lambda: dec.open(FRAMES + "/progressive_320x240.jpg"), "JDR_FMT3", "progressive") + print("progressive: %s" % e) + rst = read_file("restart_dri_320x240.jpg") + half = rst[:len(rst) // 2] + assert dec.open(half) == (320, 240) # headers are intact, the scan is not + e = expect_raises(ValueError, lambda: dec.decode(bytearray(n)), "JDR_INP") + print("truncated scan (bytes, %d of %d): decode() -> %s" % (len(half), len(rst), e)) + assert dec.open(io.BytesIO(half)) == (320, 240) + expect_raises(ValueError, lambda: dec.decode(lambda *a: None), "JDR_INP") + expect_raises(ValueError, lambda: dec.open(rst[:100]), "JDR_INP") + print("truncated headers (100 bytes): open() refused") + for label, src in (("bare SOI", b"\xff\xd8"), ("b'hello'", b"hello"), ("empty", b"")): + e = expect_raises(ValueError, lambda: dec.open(src), "JDR_INP") + expect_raises(RuntimeError, lambda: dec.decode(bytearray(8)), "decode() without open()") + print("%s: open() -> %s; no decode possible" % (label, e)) + expect_raises(OSError, lambda: dec.open(FRAMES + "/does_not_exist.jpg")) + + class Readable: + def read(self, n): + return b"" + + for label, src in (("int", 5), ("None", None), ("list", [1, 2]), ("float", 1.5), + ("object()", object()), ("Python read()", Readable()), + ("text file", open(FRAMES + "/README.md"))): + expect_raises(TypeError, lambda: dec.open(src), "source must be") + print("non-source objects (int, None, list, float, object, a Python read(), a text file): TypeError") + expect_raises(RuntimeError, lambda: jpegio.JpegDecoder().width, "width needs a successful open()") + + # 8. one decoder, many frames and source kinds + print("== 8. re-open: frames in sequence, path / bytes / file / BytesIO / memoryview") + order = ("baseline_jfif_64x48.jpg", "baseline_jfif_320x240.jpg", "odd_size_37x29.jpg", "grayscale_64x48.jpg", "c920e_320x240_dri.jpg") + for name in order: + for s in (0, 2): + dw, dh, buf = decode_tight(dec, FRAMES + "/" + name, s) + assert sha256_hex(buf) == digests[name][s], "%s s%d after other frames: digest changed" % (name, s) + assert (dec.width, dec.height) == (frames[name]["width"], frames[name]["height"]) + print("sequence %s: digests unchanged" % " -> ".join(order)) + with open(FRAMES + "/baseline_jfif_320x240.jpg", "rb") as fobj: + for label, src in (("path", FRAMES + "/baseline_jfif_320x240.jpg"), ("bytes", full), ("bytearray", bytearray(full)), + ("memoryview", memoryview(full)), ("file", fobj), ("BytesIO", io.BytesIO(full))): + dw, dh, buf = decode_tight(dec, src, 1) + assert sha256_hex(buf) == digests["baseline_jfif_320x240.jpg"][1], label + print("%-10s -> same digest at scale 1" % label) + # open() twice: the second image wins; a failed open() clears the first + dec.open(FRAMES + "/baseline_jfif_64x48.jpg") + assert dec.open(full) == (320, 240) + buf = bytearray(n) + dec.decode(buf) + assert sha256_hex(buf) == digests["baseline_jfif_320x240.jpg"][0] + dec.open(full) + expect_raises(ValueError, lambda: dec.open(b"hello")) + expect_raises(RuntimeError, lambda: dec.decode(buf), "decode() without open()") + # ... and width / height do not report the superseded image either + # (before the fix they still read 320x240 here) + expect_raises(RuntimeError, lambda: dec.width, "width needs a successful open()") + expect_raises(RuntimeError, lambda: dec.height, "height needs a successful open()") + for exc, fail in ((TypeError, lambda: dec.open(5)), + (OSError, lambda: dec.open(FRAMES + "/does_not_exist.jpg")), + (ValueError, lambda: dec.open(FRAMES + "/progressive_320x240.jpg"))): + assert dec.open(full) == (320, 240) + expect_raises(exc, fail) + expect_raises(RuntimeError, lambda: dec.width, "successful open()") + # after decode() they still read: blit_rect(buf, 0, 0, dec.width, dec.height) + dec.open(full) + dec.decode(buf) + assert (dec.width, dec.height) == (320, 240) + print("second open() supersedes the first; a failed open() leaves nothing to decode or report") + + # 9. memory: 50 decodes into the same buffer must not trend mem_free down + print("== 9. memory over 50 decodes of 320x240 at scale 0") + buf = bytearray(n) + dec.open(full) + dec.decode(buf) + gc.collect() + f0 = gc.mem_free() + for _ in range(50): + dec.open(full) + dec.decode(buf) + f1 = gc.mem_free() + gc.collect() + f2 = gc.mem_free() + for _ in range(50): + dec.open(full) + dec.decode(buf) + gc.collect() + f3 = gc.mem_free() + print("mem_free before %d, after 50 (uncollected) %d, collected %d, after 100 collected %d" % (f0, f1, f2, f3)) + print("uncollected garbage per open()+decode(): %d bytes" % ((f0 - f1) // 50)) + assert f0 - f1 < 50 * 512, "more than 512 bytes of garbage per decode" + assert f2 >= f0 - 1024 and f3 >= f2 - 1024, "mem_free trends down across decodes" + + print("jpegio tests passed: %d frames, %d digests, %.2f s" % ( + len(digests), sum(len(v) for v in digests.values()), time.time() - t0)) + + +main() diff --git a/modules/jpegio/tests/test_jpegio_lvgl.py b/modules/jpegio/tests/test_jpegio_lvgl.py new file mode 100644 index 00000000..c2a1bbd1 --- /dev/null +++ b/modules/jpegio/tests/test_jpegio_lvgl.py @@ -0,0 +1,416 @@ +"""jpegio -- its LVGL image decoder, on unix MicroPython built beside lvgl-micropython. + +The LVGL half of the Phase 2 gate in the org's docs/jpegio-vision.md, checked +mechanically: + + 1. registration: `import jpegio` before lv.init() registers nothing; after + lv.init() the decoder appears once -- by the module's import-time hook + where the port has MICROPY_MODULE_BUILTIN_INIT, and by + jpegio.register_lvgl_decoder() everywhere -- and registering again adds + no second decoder; it sits at the head of LVGL's list (counted with + jpegio.lvgl_decoders(): the binding cannot walk LVGL's decoder list + from Python -- MicroPython's builtin-method self check refuses + lv.image_decoder_t.get_next(None)); + 2. lv.image on three corpus frames (a JFIF still, the restart-marker frame, + a real non-JFIF C920e frame -- the SOI-only sniff through LVGL) flushed + to a 320x240 RGB565 frame buffer gives EXACTLY jpegio's own golden + digests from frames/golden_tjpgd.json at scale 0. Exact equality is the + gate (RGB565 into RGB565 at opa 255 is a row copy): a digest that + differs FAILS the test, after printing both digests and, as + diagnostics only, the BARS positional pixels and the Pillow means of + reference.json -- those tell a gross fault (wrong colours, wrong + stride) from a subtle one (pixels shuffled) but never pass anything. + "Something was drawn" is witnessed against the digest of the screen + background alone, not against a zero buffer: LVGL paints the + background into every flush, so the buffer is never zero; + 3. decoder_info tells the truth, read back through lv.image's source size: + the size LVGL reports is the stream's own (a wrong w/h in the + lv.image_dsc_t header is not echoed); non-JPEG bytes and a bare SOI are + not claimed by jpegio -- they fall through to LVGL's built-in decoder, + which echoes the header (jpegio never does); and an RGB565 pixel buffer + that happens to start FF D8 is drawn by that built-in decoder as pixels; + 4. a truncated scan draws nothing (the frame is the background alone) and + does not crash; + 5. a file source through fs_driver.py (when frozen in) decodes the same; + 6. 20 refreshes (each an open/decode/close) do not trend gc.mem_free() down; + 7. lv.deinit() / lv.init() drops the decoder; registering again restores it + and decodes. Last on purpose, with no gc.collect() after the re-init: + on this build lv.init() -> display -> lv.deinit() -> lv.init() -> display + -> one refresh -> gc.collect() dies with SIGBUS in MicroPython's + gc_sweep_run_finalisers -> mp_load_method_maybe (a finaliser-flagged GC + block whose type pointer is garbage) with no jpegio involved at all: + reproduced with a label on a build without displayif (lvgl-micropython + glue, not this decoder). + +Skips (exit 0, prints SKIP) when `import lvgl` fails: the interpreter was +built without the lvgl-micropython usermod, and this module's LVGL-less +CI job runs this file too. Before skipping it asserts the other +half of the contract, the one that only a decoder-less build can check: +jpegio.register_lvgl_decoder() and jpegio.lvgl_decoders() are module +attributes on EVERY build, the first raising RuntimeError that names the +missing sibling, the second returning (). + + builds/unix/standard/micropython modules/jpegio/tests/test_jpegio_lvgl.py +""" +import sys + +try: + import lvgl as lv +except ImportError: + # No lvgl-micropython in this build, so lvgl_decoder.c was not compiled -- + # but the two module attributes are unconditional (jpegio.c keeps them out + # of the #if, so the CMake ports' QSTR pass sees their names and the API + # never silently disappears). This is the only build that can check it. + import jpegio + + try: + jpegio.register_lvgl_decoder() + except RuntimeError as e: + assert "built without its LVGL decoder" in str(e), e + print("no LVGL decoder compiled in: register_lvgl_decoder() -> RuntimeError: %s" % e) + else: + raise AssertionError("register_lvgl_decoder() returned without the decoder compiled in") + assert jpegio.lvgl_decoders() == (), jpegio.lvgl_decoders() + print("no LVGL decoder compiled in: lvgl_decoders() == ()") + + print("SKIP: no `lvgl` module in this interpreter (build with the lvgl-micropython usermod)") + sys.exit(0) + +import binascii +import gc +import hashlib +import json + +import jpegio + +HERE = __file__.rsplit("/", 1)[0] if "/" in __file__ else "." +FRAMES = HERE + "/frames" +GOLDEN = FRAMES + "/golden_tjpgd.json" +REFERENCE = FRAMES + "/reference.json" +W, H = 320, 240 +DECODER = "jpegio" +# Platforms whose interpreters run a built-in module's __init__ on import +# (MICROPY_MODULE_BUILTIN_INIT), so `import jpegio` after lv.init() registers. +DESKTOP_WITH_BUILTIN_INIT = ("linux", "darwin", "win32") +CORPUS = ("baseline_jfif_320x240.jpg", "restart_dri_320x240.jpg", "c920e_320x240_dri.jpg") + +# The synthetic pattern (make_corpus.py): eight colour bars in the top 40 %. +# Same positional check as test_jpegio.py's check_pattern(), scale 0 only. +BARS = ((255, 255, 255), (255, 255, 0), (0, 255, 255), (0, 255, 0), + (255, 0, 255), (255, 0, 0), (0, 0, 255), (0, 0, 0)) +POS_TOL = 24 +MEAN_TOL = 2.0 + + +def sha256_hex(buf): + return binascii.hexlify(hashlib.sha256(buf).digest()).decode() + + +def read_file(name): + with open(FRAMES + "/" + name, "rb") as f: + return f.read() + + +def pixel(buf, stride, x, y): + i = 2 * (y * stride + x) + v = buf[i] | (buf[i + 1] << 8) + return ((v >> 8) & 0xF8, (v >> 3) & 0xFC, (v << 3) & 0xF8) + + +def means565(buf, n): + r = g = b = 0 + for i in range(0, 2 * n, 2): + lo = buf[i] + hi = buf[i + 1] + r += hi & 0xF8 + g += ((hi & 0x07) << 5) | ((lo >> 5) << 2) + b += (lo << 3) & 0xF8 + return (r / n, g / n, b / n) + + +def check_bars(name, buf): + y = (H * 2) // 5 // 2 + for i, rgb in enumerate(BARS): + x = int((i + 0.5) * W / 8) + got = pixel(buf, W, x, y) + want = (rgb[0] & 0xF8, rgb[1] & 0xFC, rgb[2] & 0xF8) + err = max(abs(got[c] - want[c]) for c in range(3)) + assert err <= POS_TOL, "%s bar %d at (%d, %d): got %s want %s" % (name, i, x, y, got, want) + return len(BARS) + + +def check_means(name, buf, reference): + want = reference["frames"][name]["scales"]["0"]["mean_rgb565"] + got = means565(buf, W * H) + err = [got[c] - want[c] for c in range(3)] + for c in range(3): + assert abs(err[c]) <= MEAN_TOL, "%s channel %d mean error %.3f exceeds %.1f" % (name, c, err[c], MEAN_TOL) + return err + + +def decoders(): + """Names of LVGL's registered image decoders, in the order LVGL consults them.""" + return list(jpegio.lvgl_decoders()) + + +def expect_raises(exc, fn, *needles): + try: + fn() + except exc as e: + msg = str(e) + for needle in needles: + assert needle in msg, "expected %r in %s: %s" % (needle, type(e).__name__, msg) + return e + except Exception as e: # noqa: BLE001 + raise AssertionError("expected %s, got %s: %s" % (exc.__name__, type(e).__name__, e)) + raise AssertionError("expected %s, nothing raised" % exc.__name__) + + +class Frame: + """The display's flush target: a zeroed W x H native RGB565 buffer.""" + + def __init__(self): + self.buf = bytearray(W * H * 2) + self.flushes = 0 + + def zero(self): + for i in range(len(self.buf)): + self.buf[i] = 0 + self.flushes = 0 + + def flush_cb(self, disp, area, color_p): + aw = area.x2 - area.x1 + 1 + ah = area.y2 - area.y1 + 1 + data = color_p.__dereference__(aw * ah * 2) + for row in range(ah): + o = ((area.y1 + row) * W + area.x1) * 2 + self.buf[o:o + aw * 2] = data[row * aw * 2:(row + 1) * aw * 2] + self.flushes += 1 + disp.flush_ready() + +def setup_display(frame): + """320x240 RGB565 display like lvgl-bindings/tools/test_lvgl_smoke.py's _setup_display.""" + disp = lv.display_create(W, H) + disp.set_color_format(lv.COLOR_FORMAT.RGB565) + buf = lv.draw_buf_create(W, H, lv.COLOR_FORMAT.RGB565, 0) + disp.set_draw_buffers(buf, None) + disp.set_render_mode(lv.DISPLAY_RENDER_MODE.PARTIAL) + disp.set_flush_cb(frame.flush_cb) + return disp, buf + + +def image_dsc(jpeg, w=W, h=H, cf=None): + if cf is None: + cf = lv.COLOR_FORMAT.RAW + return lv.image_dsc_t({"header": {"w": w, "h": h, "cf": cf}, "data_size": len(jpeg), "data": jpeg}) + + +def show(disp, frame, src): + """Put `src` on a fresh screen as the only widget and render it into frame.""" + scr = lv.screen_active() + scr.clean() + img = lv.image(scr) + img.set_src(src) + img.set_pos(0, 0) + frame.zero() + scr.invalidate() + lv.refr_now(disp) + return img + + +def background_digest(disp, frame): + """Digest of a refresh with nothing on the screen: what "nothing drawn" looks like.""" + scr = lv.screen_active() + scr.clean() + frame.zero() + scr.invalidate() + lv.refr_now(disp) + assert frame.flushes > 0 + return sha256_hex(frame.buf) + + +def src_size(img): + """The size lv_image took from lv_image_decoder_get_info (0x0 when no decoder claimed the source).""" + return (img.get_src_width(), img.get_src_height()) + + +def main(): + with open(GOLDEN) as f: + golden = json.load(f)["frames"] + with open(REFERENCE) as f: + reference = json.load(f) + jpegs = {name: read_file(name) for name in CORPUS} + + # 1. registration + print("== 1. registration") + assert not lv.is_initialized() + assert jpegio.lvgl_decoders() == (), jpegio.lvgl_decoders() + expect_raises(RuntimeError, jpegio.register_lvgl_decoder, "lv.init() first") + lv.init() + names = decoders() + assert DECODER not in names, "decoder present before any registration: %s" % names + print("after lv.init(): decoders %s (no %s yet)" % (names, DECODER)) + import jpegio as jpegio_again # noqa: F401 -- a second import statement re-runs the module's __init__ hook + auto = decoders().count(DECODER) + assert auto <= 1, decoders() + # The desktop interpreters all have the hook: unix at its extra-features + # ROM level, windows through the PyDevices variant (displayif#41). + if sys.platform in DESKTOP_WITH_BUILTIN_INIT: + assert auto == 1, ( + "%s: `import jpegio` after lv.init() registered nothing -- this build lacks " + "MICROPY_MODULE_BUILTIN_INIT (decoders %s)" % (sys.platform, decoders()) + ) + print("import-time registration (MICROPY_MODULE_BUILTIN_INIT): %s" % ("yes" if auto == 1 else "no -- explicit call needed")) + added = jpegio.register_lvgl_decoder() + assert added is (auto == 0), (added, auto) + assert decoders().count(DECODER) == 1, decoders() + assert jpegio.register_lvgl_decoder() is False + assert jpegio.register_lvgl_decoder() is False + names = decoders() + assert names.count(DECODER) == 1, "registering three times left %s" % names + assert names[0] == DECODER, "%s must be consulted first (head of LVGL's list): %s" % (DECODER, names) + print("register_lvgl_decoder() x3: one decoder, first in line: %s" % names) + + # 2. lv.image on the corpus -> flush buffer -> jpegio's own golden digests + print("== 2. lv.image through the flush buffer vs golden_tjpgd.json (scale 0)") + frame = Frame() + disp, draw_buf = setup_display(frame) + bg = background_digest(disp, frame) + for name in CORPUS: + assert bg != golden[name]["0"], "the empty screen already hashes to %s's golden" % name + print("background-only refresh: sha256 %s.. (what 'nothing drawn' looks like)" % bg[:16]) + exact = 0 + mismatched = [] + keep = [] + for name in CORPUS: + dsc = image_dsc(jpegs[name]) + keep.append(dsc) + img = show(disp, frame, dsc) + assert frame.flushes > 0, "no flush during refr_now" + assert (img.get_width(), img.get_height()) == (W, H), (name, img.get_width(), img.get_height()) + digest = sha256_hex(frame.buf) + assert digest != bg, "%s: the frame is the background alone -- nothing was drawn" % name + want = golden[name]["0"] + if digest == want: + exact += 1 + print("%-28s %d flush(es) sha256 %s.. == golden: EXACT" % (name, frame.flushes, digest[:16])) + continue + mismatched.append(name) + print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + print("!! %s: DIGEST MISMATCH through lv.image -- this fails the test" % name) + print("!! got %s" % digest) + print("!! golden %s" % want) + print("!! diagnostics (never a pass): positional / mean checks") + print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + try: + err = check_means(name, frame.buf, reference) + checked = 0 if name.startswith("c920e_") else check_bars(name, frame.buf) + print("%-28s diagnostics: mean err r=%+.3f g=%+.3f b=%+.3f within %.1f, %d bar samples within %d" + " -- colours and stride are right, so the pixels are shuffled (a subtle fault)" + % (name, err[0], err[1], err[2], MEAN_TOL, checked, POS_TOL)) + except AssertionError as e: + print("%-28s diagnostics: %s -- a gross fault (colours or stride)" % (name, e)) + print("digests exact through lv.image: %d of %d" % (exact, len(CORPUS))) + assert exact == len(CORPUS), "not the exact gate: %s differ from golden_tjpgd.json through lv.image" % mismatched + + # 3. decoder_info tells the truth (read back through the widget's source size) + print("== 3. decoder_info: stream size wins, non-JPEG not claimed, FF D8 pixels not claimed") + wrong = image_dsc(jpegs["c920e_320x240_dri.jpg"], 1, 1) + img = show(disp, frame, wrong) + assert src_size(img) == (W, H), "header 1x1 was echoed: %s" % (src_size(img),) + assert (img.get_width(), img.get_height()) == (W, H) + assert sha256_hex(frame.buf) == golden["c920e_320x240_dri.jpg"]["0"], "header 1x1: digest differs from golden" + print("dsc header 1x1 -> lv.image sees %dx%d from jd_prepare, digest EXACT" % src_size(img)) + # LVGL's built-in BIN decoder claims any variable source whose header cf + # is not UNKNOWN (RAW included) and echoes the header; jpegio's decoder + # sits ahead of it and never echoes (its size comes from jd_prepare or + # not at all), so a header-sized result means jpegio did not claim it. + junk = b"hello, not a jpeg at all " * 8 + img = show(disp, frame, image_dsc(junk, 8, 8)) + assert src_size(img) == (8, 8), src_size(img) + soi_only = b"\xff\xd8" + b"\x00" * 62 + img = show(disp, frame, image_dsc(soi_only, 8, 8)) + assert src_size(img) == (8, 8), src_size(img) + print("non-JPEG bytes and a bare SOI with cf RAW: not claimed by jpegio (LVGL's built-in decoder echoes the 8x8 header)") + pixels = b"\xff\xd8" + b"\x00\x00" * 7 # 8x1 RGB565 whose first pixel is 0xD8FF + px_dsc = image_dsc(pixels, 8, 1, lv.COLOR_FORMAT.RGB565) + img = show(disp, frame, px_dsc) + assert src_size(img) == (8, 1), src_size(img) + assert (img.get_width(), img.get_height()) == (8, 1) + assert pixel(frame.buf, W, 0, 0) == (0xD8, 0x1C, 0xF8), pixel(frame.buf, W, 0, 0) + assert pixel(frame.buf, W, 1, 0) == (0, 0, 0) + print("an RGB565 pixel buffer starting FF D8 falls through to LVGL's built-in decoder (8x1, first pixel 0xD8FF)") + + # 4. a truncated scan: headers parse (info OK), the decode fails, nothing crashes + print("== 4. truncated scan") + rst = jpegs["restart_dri_320x240.jpg"] + half = rst[:len(rst) // 2] + half_dsc = image_dsc(half) + img = show(disp, frame, half_dsc) + assert src_size(img) == (W, H), src_size(img) # headers parse: info OK + assert sha256_hex(frame.buf) == bg, "half a scan drew something: %s" % sha256_hex(frame.buf) + print("half a scan (%d of %d bytes): info OK, open refused, nothing drawn, refr_now survived" % (len(half), len(rst))) + + # 5. a file source through fs_driver.py (frozen in by lvgl-micropython's manifest) + print("== 5. file source") + try: + import fs_driver + except ImportError: + fs_driver = None + if fs_driver is None: + print("fs_driver not frozen in: file-source path not exercised here") + else: + fs_driver.register("S") + path = "S:" + FRAMES + "/c920e_320x240_dri.jpg" + img = show(disp, frame, path) + assert src_size(img) == (W, H), src_size(img) + digest = sha256_hex(frame.buf) + want = golden["c920e_320x240_dri.jpg"]["0"] + assert digest == want, "file source DIGEST MISMATCH: got %s golden %s" % (digest, want) + print("%s: sha256 %s.. == golden: EXACT" % (path, digest[:16])) + img = show(disp, frame, "S:" + FRAMES + "/README.md") + assert src_size(img) == (0, 0), src_size(img) + assert sha256_hex(frame.buf) == bg + print("a non-JPEG file (README.md) is not claimed, nothing drawn") + + # 6. memory over repeated refreshes (each: open -> decode -> close) + print("== 6. memory over 20 refreshes of the C920e frame") + dsc = image_dsc(jpegs["c920e_320x240_dri.jpg"]) + img = show(disp, frame, dsc) + assert sha256_hex(frame.buf) == golden["c920e_320x240_dri.jpg"]["0"] + for _ in range(3): + img.invalidate() + lv.refr_now(disp) + gc.collect() + f0 = gc.mem_free() + for _ in range(20): + img.invalidate() + lv.refr_now(disp) + f1 = gc.mem_free() + gc.collect() + f2 = gc.mem_free() + print("mem_free before %d, after 20 (uncollected) %d, collected %d" % (f0, f1, f2)) + assert f2 >= f0 - 4096, "mem_free trends down across refreshes (a %d-byte decode buffer per refresh?)" % (W * H * 2) + + # 7. deinit / init drops the decoder; register again and decode once more + print("== 7. lv.deinit() / lv.init()") + lv.deinit() + assert jpegio.lvgl_decoders() == () + expect_raises(RuntimeError, jpegio.register_lvgl_decoder, "lv.init() first") + lv.init() + assert DECODER not in decoders() + assert jpegio.register_lvgl_decoder() is True + assert decoders().count(DECODER) == 1 + print("decoder gone after deinit, back after register_lvgl_decoder(): %s" % decoders()) + frame = Frame() + disp, draw_buf = setup_display(frame) + keep.append(image_dsc(jpegs["baseline_jfif_320x240.jpg"])) + img = show(disp, frame, keep[-1]) + assert sha256_hex(frame.buf) == golden["baseline_jfif_320x240.jpg"]["0"], "digest changed after the deinit/init cycle" + print("baseline_jfif_320x240.jpg after the cycle: digest EXACT") + + lv.deinit() + print("jpegio LVGL tests passed: %d corpus frames through lv.image, %d digests exact" % (len(CORPUS), exact)) + + +main() diff --git a/scripts/exercise_build.sh b/scripts/exercise_build.sh index 2ba31129..c183a788 100755 --- a/scripts/exercise_build.sh +++ b/scripts/exercise_build.sh @@ -219,9 +219,9 @@ print('RUN', sys.implementation._machine, np.sum(np.array([1, 2, 3])), board_con if ! command -v arm-none-eabi-gcc > /dev/null; then section "build $m_t: SKIPPED, arm-none-eabi-gcc is missing (apt-get install gcc-arm-none-eabi libnewlib-arm-none-eabi)" elif [ $m_t = pico ]; then - build pico --port rp2 --board RPI_PICO --modules displayif,pygraphics,palettes,pdwidgets + build pico --port rp2 --board RPI_PICO --modules displayif,jpegio,pygraphics,palettes,pdwidgets else - build pico2 --port rp2 --board RPI_PICO2 --modules displayif,lvgl-micropython + build pico2 --port rp2 --board RPI_PICO2 --modules displayif,jpegio,lvgl-micropython fi done diff --git a/variants/windows/pydevices/mpconfigvariant.h b/variants/windows/pydevices/mpconfigvariant.h index 8d8d8130..d46f0d24 100644 --- a/variants/windows/pydevices/mpconfigvariant.h +++ b/variants/windows/pydevices/mpconfigvariant.h @@ -5,5 +5,5 @@ #define MICROPY_UNIX_DEFAULT_HEAP_SIZE (8 * 1024 * 1024) // Run a built-in module's __init__ on import, as unix does. The windows port // sits at the core ROM level, where this is off, so `import jpegio` after -// lv.init() never registered displayif's LVGL decoder (displayif#41). +// lv.init() never registered jpegio's LVGL decoder (displayif#41, from when jpegio lived there). #define MICROPY_MODULE_BUILTIN_INIT (1) From d31cae6616b52fbe8cb3b8219639fedf3eaa4337 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:53:18 -0500 Subject: [PATCH 2/3] jpegio: big coordinates are a ValueError on 32-bit boards too; board checks decode()'s x and y went through MP_ARG_INT, so on a 32-bit board an int past a machine word (the test's 2**62) raised OverflowError from the argument parser before jpegio's own "0..65535" check; on 64-bit unix it fit and the check caught it. They are parsed as objects now, and anything that isn't a small int is out of range: the same ValueError, with the value in the message, on every platform. The golden test read sys.argv, which the esp32 builds lack; it uses getattr(sys, "argv", []). tests/board_lvgl_glass.py draws a corpus JPEG with lv.image and compares the panel framebuffer with jpegio's own decode of the same file. On the P4 DEV-KIT with the KeDei panel: 76800 of 76800 pixels match, and 76.86 % when the image is planted 4 px off. --- modules/jpegio/src/jpegio.c | 25 ++++++++-- modules/jpegio/tests/board_lvgl_glass.py | 59 ++++++++++++++++++++++++ modules/jpegio/tests/test_jpegio.py | 2 +- 3 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 modules/jpegio/tests/board_lvgl_glass.py diff --git a/modules/jpegio/src/jpegio.c b/modules/jpegio/src/jpegio.c index 5d04346d..af7c454c 100644 --- a/modules/jpegio/src/jpegio.c +++ b/modules/jpegio/src/jpegio.c @@ -300,8 +300,11 @@ static mp_obj_t jpegio_jpegdecoder_decode(size_t n_args, const mp_obj_t *pos_arg static const mp_arg_t allowed_args[] = { { MP_QSTR_target, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_scale, MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_x, MP_ARG_INT, {.u_int = 0} }, - { MP_QSTR_y, MP_ARG_INT, {.u_int = 0} }, + // Objects, not MP_ARG_INT: an int too big for a machine word must be + // the same "0..65535" ValueError on a 32-bit board as on 64-bit unix, + // not an OverflowError from the argument parser. + { MP_QSTR_x, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_INT(0)} }, + { MP_QSTR_y, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_INT(0)} }, { MP_QSTR_stride, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; @@ -314,10 +317,22 @@ static mp_obj_t jpegio_jpegdecoder_decode(size_t n_args, const mp_obj_t *pos_arg if (scale < 0 || scale > 3) { mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%q must be 0..3, not %d"), MP_QSTR_scale, (int)scale); } - mp_int_t x = args[ARG_x].u_int; - mp_int_t y = args[ARG_y].u_int; + mp_obj_t x_obj = args[ARG_x].u_obj; + mp_obj_t y_obj = args[ARG_y].u_obj; + if (!mp_obj_is_int(x_obj) || !mp_obj_is_int(y_obj)) { + mp_raise_TypeError(MP_ERROR_TEXT("x, y must be int")); + } + // Any coordinate that isn't a small int is far beyond 65535. + mp_int_t x = mp_obj_is_small_int(x_obj) ? MP_OBJ_SMALL_INT_VALUE(x_obj) : -1; + mp_int_t y = mp_obj_is_small_int(y_obj) ? MP_OBJ_SMALL_INT_VALUE(y_obj) : -1; if (x < 0 || x > JPEGIO_MAX_COORD || y < 0 || y > JPEGIO_MAX_COORD) { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("x, y must be 0..%d, not (%ld, %ld)"), JPEGIO_MAX_COORD, (long)x, (long)y); + vstr_t vstr; + mp_print_t print; + vstr_init_print(&vstr, 32, &print); + mp_obj_print_helper(&print, x_obj, PRINT_REPR); + mp_print_str(&print, ", "); + mp_obj_print_helper(&print, y_obj, PRINT_REPR); + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("x, y must be 0..%d, not (%s)"), JPEGIO_MAX_COORD, vstr_null_terminated_str(&vstr)); } size_t dw = self->width >> scale; size_t dh = self->height >> scale; diff --git a/modules/jpegio/tests/board_lvgl_glass.py b/modules/jpegio/tests/board_lvgl_glass.py new file mode 100644 index 00000000..508a9216 --- /dev/null +++ b/modules/jpegio/tests/board_lvgl_glass.py @@ -0,0 +1,59 @@ +# On a board with a display, LVGL and jpegio: draw a corpus JPEG with +# lv.image and compare what lands in the panel's framebuffer, pixel for pixel, +# with jpegio decoding the same file into a buffer. The framebuffer is what +# the glass shows, so a match means LVGL drew this JPEG on the glass through +# jpegio's decoder. +# +# mpftp put frames/baseline_jfif_320x240.jpg /baseline_jfif_320x240.jpg +# mpftp run board_lvgl_glass.py +# +# Either byte order counts as a match (panels differ on RGB565 byte order). +import board_config +import display_driver # noqa: F401 (LVGL on board_config's display) +import jpegio +import lvgl as lv + +PATH = "/baseline_jfif_320x240.jpg" + +dec = jpegio.JpegDecoder() +w, h = dec.open(PATH) +ref = bytearray(w * h * 2) +dec.decode(ref) + +scr = lv.screen_active() +scr.clean() +scr.set_style_bg_color(lv.color_black(), 0) +img = lv.image(scr) +try: + import fs_driver # frozen in by lvgl-micropython's manifest: LVGL reads files through it + + fs_driver.register("S") + img.set_src("S:" + PATH) +except ImportError: # no file driver: hand LVGL the bytes + with open(PATH, "rb") as f: + data = f.read() + img.set_src(lv.image_dsc_t({"header": {"w": w, "h": h, "cf": lv.COLOR_FORMAT.RAW}, "data_size": len(data), "data": data})) +img.set_pos(0, 0) +lv.refr_now(None) + +d = board_config.display_drv +fb, fb2, nbytes, stride = d.framebuffers() +print("panel %dx%d, image %dx%d, stride %d, second buffer %s" % (d.width, d.height, w, h, stride, fb2 is not None)) + + +def matches(buf): + same = 0 + for y in range(h): + row = y * stride + for x in range(w): + i, j = row + 2 * x, 2 * (y * w + x) + a0, a1, b0, b1 = buf[i], buf[i + 1], ref[j], ref[j + 1] + if (a0 == b0 and a1 == b1) or (a0 == b1 and a1 == b0): + same += 1 + return same + + +best = max(matches(fb), matches(fb2) if fb2 is not None else 0) +total = w * h +print("pixels matching jpegio's own decode: %d of %d (%.2f %%)" % (best, total, 100 * best / total)) +print("PASS" if best == total else "FAIL") diff --git a/modules/jpegio/tests/test_jpegio.py b/modules/jpegio/tests/test_jpegio.py index fb203e12..99d3a22d 100644 --- a/modules/jpegio/tests/test_jpegio.py +++ b/modules/jpegio/tests/test_jpegio.py @@ -47,7 +47,7 @@ REFERENCE = FRAMES + "/reference.json" GOLDEN = FRAMES + "/golden_tjpgd.json" SCALES = (0, 1, 2, 3) -RECORD = "--record" in sys.argv[1:] +RECORD = "--record" in getattr(sys, "argv", [])[1:] # --- fidelity tolerance ------------------------------------------------------ # From b29e4a3dee4767a5c70abfa349396bf6480d83f1 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:24:05 -0500 Subject: [PATCH 3/3] modules.lock: displayif without jpegio (#50), lvgl-micropython checking for jpegio (#21) --- modules.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules.lock b/modules.lock index 02bc4d26..dde5db73 100644 --- a/modules.lock +++ b/modules.lock @@ -7,8 +7,8 @@ audiocomponents https://github.com/PyDevices/audiocomponents.git 883d591784 audiodsp https://github.com/PyDevices/audiodsp.git 7da36f7bf016236c8df02505a653ea35dc62d600 audioif https://github.com/PyDevices/audioif.git b6b35a48a0cd23ddc549f8c0643d77ffe778da28 cameraif https://github.com/PyDevices/cameraif.git 77b20b128800d5e1e23aab4c131e58f46b12cad2 -displayif https://github.com/PyDevices/displayif.git b8366781c580da3a1683713e774d4d4e7aaf939f -lvgl-micropython https://github.com/PyDevices/lvgl-micropython.git 8f7d89d0f8170ef526d5bd798c4bc0d05651309e +displayif https://github.com/PyDevices/displayif.git 17b3750828d5bfc7dc33a6f068e98a17ec3389d6 +lvgl-micropython https://github.com/PyDevices/lvgl-micropython.git d42aeff1514987f2fd9d71c79e9737ebda7fa088 palettes https://github.com/PyDevices/palettes.git 6f0870f0c8a0b48c56d2150b3f800bbbee7761e1 pdwidgets https://github.com/PyDevices/pdwidgets.git 306c1e20e0c3c63066dab6d10155fcf165a6ba13 pydevices https://github.com/PyDevices/pydevices.git 36cba5f9da32a9d9005e7fe725ce8bd70aa21921