# lowzag — progressive JPEG for machines that don't have the RAM for it
[](https://github.com/lowzag/lowzag/actions/workflows/ci.yml)
[](LICENSE)
Development is on
[GitLab](https://gitlab.defensiblelogic.com/pub/lowzag); GitHub is the
mirror, and where issues and pull requests from outside arrive. Patches
are welcome in either place — see [`CONTRIBUTING.md`](CONTRIBUTING.md).
A progressive JPEG decoder in portable C99 that decodes at 1/2, 1/4 or 1/8
scale without ever allocating the full coefficient array. On a 12-megapixel
photograph that is the difference between 36 MB and 3.6 MB — still far more
than any microcontroller's internal SRAM, but within reach of an ESP32-S3
with PSRAM, where 36 MB is not.
The name is the trick. JPEG stores a block's 64 coefficients in *zigzag*
order, which walks the 8×8 block diagonally from the top-left outward — and
that order runs lowest spatial frequency to highest. So "the first K
coefficients" and "the low-frequency corner" are the same set, and a decoder
that only wants a reduced-scale image can simply stop reading early. lowzag
keeps the low end of the zigzag and throws the rest away: K = 64, 25, 5, 1
for scale 1/1, 1/2, 1/4, 1/8.
```
12 MP 4:2:0 progressive JPEG
scale 1/1 1/2 1/4 1/8
lowzag 36.2 MB 23.0 MB 8.9 MB 3.6 MB
lowzag + crop 3.4 MB 3.0 MB 2.5 MB 2.3 MB
libjpeg 36.1 MB 36.1 MB 36.1 MB 36.1 MB
```
(The crop row is an 800x480 window of that frame. The two savings are
complementary rather than multiplicative — reducing the scale saves most at
1/8 and nothing at 1/1, cropping saves most at 1/1 — so together they reach
15.8x, not the 107x the two headline figures multiplied would suggest.)
Two different quantities, quoted together only because they are the ones each
decoder lets you see: lowzag's row is its own `info.peak_alloc`; libjpeg's is
the size of the progressive coefficient array it requests. Process peak RSS
tracks both closely — see [`docs/ALGORITHM.md`](docs/ALGORITHM.md) for that
table measured separately.
## Why the second row is flat
Baseline JPEG stores each block's 64 coefficients together, so a decoder can
finish a block, emit it, and forget it. Progressive JPEG interleaves: it
stores a few bits of every block, then a few more, across a dozen or more
scans. Nothing is final until the last scan that touches it has been read, so
a decoder has to hold coefficient storage for the whole frame.
Every general-purpose decoder surveyed for this project sizes that storage
from the *source* dimensions at the full 64 coefficients per block.
libjpeg-turbo's `JBLOCK` is 128 bytes and it requests one per block in
`jdcoefct.c` before it knows or cares what scale you asked for. Asking for a
1/8 thumbnail does not make it smaller — that is what the flat row above is.
The same is true of mozjpeg, stb_image, Go's `image/jpeg` and Rust's
`jpeg-decoder`: DCT scaling reduces output size and decode time, not
progressive coefficient memory. The file-and-function evidence is in
[`docs/PRIOR-ART.md`](docs/PRIOR-ART.md).
It does not have to be that way. A 1/8-scale IDCT only ever reads the top-left
1x1 corner of each block; a 1/4-scale one reads 2x2. The rest of the block is
computed and then discarded. lowzag stores only the corner it will actually
use — the first K coefficients in zigzag order, K = 64, 25, 5, 1 for scale
1/1, 1/2, 1/4, 1/8 — plus, whenever K < 64, a 64-bit map of which coefficients
in the *full* block were nonzero. (K is per component, not per frame: a
subsampled chroma component is kept one step sharper so it lands on the output
grid, and carries a larger K than the luma. That is why the 4:2:0 figures
below are not simply 2K + 8.)
That bitmap is the part that is easy to get wrong, and the reason you cannot
simply throw away the high-frequency scans. An AC refinement scan's bit
consumption depends on which coefficients are already nonzero across its whole
band, including the ones whose values you discarded. Drop that history and
every subsequent scan desynchronizes. So lowzag entropy-decodes every scan in
full; it just never dequantizes, inverse-transforms or upsamples anything
outside the corner.
Memory still scales with the source — in a single forward pass one bitmap per
source block is unavoidable — but the constant drops sharply. Measured bytes
per source block, 4:2:0:
```
scale 1/1 1/2 1/4 1/8
lowzag 128.5 B 81.6 B 31.4 B 12.7 B
libjpeg 128 B 128 B 128 B 128 B
```
`cfg.crop_*` bounds the working set by *region* as well: a block outside the
rectangle keeps only its 8-byte map and no coefficients at all. It does not
break the proportionality to source size, because that map is still one per
source block — 2.26 MB at 12 MP 4:2:0, which is the floor both savings run
into. Dropping the map as well would take multiple passes over the stream,
which is what the MediaTek and Sunplus patents in
[`docs/PRIOR-ART.md`](docs/PRIOR-ART.md) do and lowzag does not.
**Memory is proportional to the source, not to the output.** A 48 MP source
decoded to 1000x750 costs 14.3 MB; a 12 MP source decoded to the same
1000x750 costs 8.9 MB. Size your budget from the largest file you intend to
accept.
## What it does not do
Progressive (SOF2, Huffman) only. Baseline files are your problem — call
`lowzag_probe()` and route them to `stb_image`, TJpgDec, or whatever you
already have. Also unsupported: arithmetic coding, 12-bit precision,
CMYK/Adobe transforms, lossless JPEG, sampling factors above 2. 8-bit, 1 or 3
components, restart markers, and that is the lot.
It does not parse metadata either — EXIF is a TIFF parser and an ICC profile
is a colour-management problem — but it will hand you the bytes as it walks
past them, which is a thing only the decoder is positioned to do for a caller
who is streaming the file. See `cfg.marker` below, and
`lowzag_exif_orientation()` for the one field almost everybody wanted.
The technique is not novel and this project does not claim otherwise. It is
described, closely, in two patent applications that were both abandoned —
Panasonic's US20060067582A1 (2005) and Entropic Communications'
US20160234520A1 (2013) — with adjacent expired art from MediaTek and Sunplus.
What seems to be missing is not the idea but a shipping implementation of it.
[`docs/PRIOR-ART.md`](docs/PRIOR-ART.md) lays out what everyone else does. It
is a literature survey and not a freedom-to-operate opinion; if that
distinction matters to you, get your own advice.
If your ceiling really is tens of kilobytes, use
[bitbank2/JPEGDEC](https://github.com/bitbank2/JPEGDEC) instead. It decodes
progressive JPEG on microcontrollers in about 18 KB of *constant* memory —
independent of image size, where lowzag's is not. On the 12 MP file above that
is 200x less than lowzag at 1/8, and JPEGDEC simply wins. The catch is that it
does it by decoding the DC scan only and forcing 1/8 regardless of what you
asked for: request 1/4 and it returns success with visibly wrong output rather
than an error. lowzag exists to cover 1/1, 1/2 and 1/4, and to apply the DC
refinement scans JPEGDEC skips.
## Using it
From nothing to a decoded image:
```sh
git clone https://gitlab.defensiblelogic.com/pub/lowzag.git
cd lowzag
cmake -S . -B build -DLOWZAG_BUILD_EXAMPLES=ON
cmake --build build
./build/examples/decode_to_ppm examples/sample.jpg 4 out.ppm
```
The last command prints one line and writes one file:
```
examples/sample.jpg: 1024x768 -> 256x192, scale 1/4, 3 comp, peak 582912 bytes
```
`examples/sample.jpg` is committed here — a 1024x768 progressive 4:2:0 render
of a landscape, 38 KB, drawn by `examples/gen_sample.py` beside it. The second
argument is the scale denominator, 1, 2, 4 or 8: pass 1 for the full 1024x768
and 8 for a 128x96 thumbnail. On this file `peak` across those four is 2.39,
1.52, 0.58 and 0.24 MB.
`out.ppm` is a binary PPM — a short ASCII header, then raw RGB, three bytes per
pixel, no compression. Most image viewers open it directly (`xdg-open out.ppm`,
GIMP, feh, Preview); ImageMagick reads it. Failing all that, Pillow will convert
it:
```sh
python3 -c "from PIL import Image; Image.open('out.ppm').save('out.png')"
```
[`examples/decode_to_ppm.c`](examples/decode_to_ppm.c) is that program — 130
lines, and the reference for how the four callbacks fit together.
[`examples/esp32/`](examples/esp32) is the same library in an ESP-IDF project,
decoding a file embedded in the firmware and reporting size, timing and
allocation. [`examples/README.md`](examples/README.md) covers both, including
how to build the host example with no build system at all and how to produce
your own progressive test files.
### The API
Two entry points over one decoder. `lowzag_decode_mem()` takes the file in a
buffer and fills a pixel buffer you supply. `lowzag_decode()` takes four
callbacks and streams: it never holds the compressed file, hands out each row
as it is finished, and takes every byte it needs from an allocator you name.
The first is what you want on a host, where the file is in memory already; the
second is what you want on a part where it could not be. Both take the same
`lowzag_cfg_t`, which is where the scale, the target size, the crop, the
memory ceiling and the cancellation hook live.
Every public struct begins with a `struct_size` you set to `sizeof` it. That
is how lowzag knows which fields exist in the object it was handed, and it is
what makes appending a field a non-event from 2.0.0 onwards. Leave it at zero
and the first call returns `LOWZAG_ERR_VERSION`; so does any value that is not
a size some version of the struct could have had.
Give these structs an initialiser — `LOWZAG_CFG_INIT`, `LOWZAG_INFO_INIT`,
`LOWZAG_ESTIMATE_INIT`, or a `memset` — rather than declaring them bare.
lowzag has to read `struct_size` before it may write anything, so an
uninitialised struct hands it whatever the stack held. Through 1.1.1
`lowzag_decode()` zeroed `*info` for you and a bare `lowzag_info_t info;` was
correct; that is one of the things 2.0.0 gave up to get the field.
Ask what a file costs before deciding to decode it:
```c
#include "lowzag.h"
/* An estimate reads only struct_size, force_scale, target_w, target_h,
crop_* and mem_limit, so the callbacks stay NULL here. */
lowzag_cfg_t cfg = {
.struct_size = sizeof cfg, /* required; LOWZAG_CFG_INIT does it too */
.target_w = 800, .target_h = 480, /* picks the coarsest scale that covers this */
.mem_limit = 8u << 20, /* degrade, or refuse, above this */
};
lowzag_estimate_t est = LOWZAG_ESTIMATE_INIT;
lowzag_err_t err = lowzag_estimate(&cfg, jpeg, len, &est);
if (err != LOWZAG_OK) {
/* LOWZAG_ERR_TRUNCATED here means there was no frame header in the
bytes you passed: read more of the file and ask again. */
fprintf(stderr, "%s\n", lowzag_strerror(err));
return -1;
}
if (est.total_bytes > budget) {
return -1; /* said no, having allocated nothing */
}
size_t out_size = (size_t)est.out_w * est.out_h * 3;
uint8_t *rgb = malloc(out_size);
lowzag_info_t info = LOWZAG_INFO_INIT;
err = lowzag_decode_mem(&cfg, jpeg, len, rgb, out_size, &info);
```
The same `cfg` sizes the buffer and runs the decode, so the two cannot
disagree about the scale, the crop or the ceiling. If you would rather not
estimate at all, `lowzag_decode_mem(&cfg, jpeg, len, NULL, 0, &info)` fills
`info` with the dimensions and returns `LOWZAG_ERR_BUFFER_TOO_SMALL` — the
same code, down the same path, that a real buffer which turns out to be too
small returns.
`est.alloc_bytes` is not an approximation. It is the decoder's own sizing code
run over the frame header, so it equals the `info.peak_alloc` the decode
reports for the same file and config, to the byte, including whatever
degradation `mem_limit` forces — and `est.scale` and `est.chroma_steps` say
which. `est.working_bytes` is the `lowzag_state_size()` block on top of that,
and `est.total_bytes` is the sum. Your pixel buffer is in none of them: it is
`out_w * out_h * 3` and it is yours to find. On `examples/sample.jpg` at full
resolution `alloc_bytes` is 2.39 MB and the output buffer is another 2.36 MB.
Pass the same `target_w`, `target_h` and `mem_limit` to both calls, or the
number you decided on describes a different decode from the one you get.
Estimating reads no further than the frame header, so it also works on a
prefix: a camera JPEG can carry tens of kilobytes of EXIF ahead of its header,
and 64 KB covers anything sane.
`cfg.mem_limit` is a ceiling on everything lowzag allocates except its working
state; 0 means unlimited, which is also what a zeroed config gives you. On
input you did not produce, pass a real number — memory follows the dimensions
in the frame header, and a few hundred bytes of it can legitimately ask for
gigabytes. Through 1.1.1 `lowzag_decode_mem()` took it as a positional
parameter so that nobody could leave it zeroed by accident; it moved into the
config in 2.0.0 because the parameter list was a drifting copy of one, and
would have needed four more parameters for the crop. The reasoning for the
ceiling itself is in [`SECURITY.md`](SECURITY.md).
The callback form is the same decode with the buffering taken out:
```c
static int rd (void *ctx, uint8_t *buf, size_t n) { return fread(buf, 1, n, ctx); }
static void row(void *ctx, int y, int w, const uint8_t *rgb) { /* 3 bytes/px, RGB */ }
static void *al(void *ctx, size_t n) { return malloc(n); }
static void fr(void *ctx, void *p) { free(p); }
lowzag_cfg_t cfg = {
.struct_size = sizeof cfg,
.read = rd, .read_ctx = fp,
.row = row,
.alloc = al, .free = fr,
.target_w = 800, .target_h = 480,
.mem_limit = 4u << 20,
};
lowzag_info_t info = LOWZAG_INFO_INIT;
lowzag_err_t err = lowzag_decode(&cfg, &info);
if (err != LOWZAG_OK) {
fprintf(stderr, "%s\n", lowzag_strerror(err));
}
```
Reading, row output and allocation all go through those four, so `src/lowzag.c`
contains no file I/O, no `malloc` and no platform dependency; `memcpy` and
`memset` are the only libc functions it uses. `lowzag_decode_mem()` is the one
part that needs a heap of its own, so it lives in a second translation unit,
`src/lowzag_mem.c`, and `-DLOWZAG_BUILD_MEM_API=OFF` leaves it out of the
library entirely. Nothing else references it.
Rows arrive in order, exactly once each, always 3 bytes per pixel RGB —
grayscale files arrive with luma replicated, so there is no second code path
to write. `lowzag_decode_mem()` writes the same bytes into your buffer, top row
first, no padding between rows.
When lowzag is choosing the scale itself and the chosen one would exceed
`mem_limit`, it first gives back the extra chroma sharpness and then, if that
is not enough, drops to the next scale down and decodes a smaller image rather
than failing. A tight limit degrades quality silently. Check `info.scale` if
that matters to you. With `force_scale` set, or at 1/8 where there is nothing
left to give up, it returns `LOWZAG_ERR_NOMEM` instead.
There is no static or global mutable state — the compiled object has an empty
`.data` and `.bss` — so concurrent decodes are safe. A single decode is not
reentrant: do not call `lowzag_decode()` from inside its own callbacks, cancel
included. On a part where it matters, you can place the ~17 KB hot working set
yourself via `cfg.state` (see `lowzag_state_size()`) and keep it out of slow
external RAM while the large coefficient arrays come from `alloc`.
### Stopping a decode, and decoding part of one
`cfg.cancel` is polled in both passes — once per marker segment, once per
64 KB while the coefficient arrays are cleared, once per block row of every
scan and once per MCU row of the output pass, about 3,000 times for a 12 MP
file and never inside the per-block or per-pixel loops. Return nonzero and
`lowzag_decode()` releases everything, delivers no further row and returns
`LOWZAG_ERR_CANCELLED`, with `info.rows_delivered` saying how much of the
image arrived. It reaches the entropy pass, which is most of the decode and
the half a row-callback return value could never touch; it is also where a
synchronous decode feeds a watchdog or checks a deadline.
```c
static int stop_cb(void *ctx) { return *(volatile int *)ctx; }
cfg.cancel = stop_cb; cfg.cancel_ctx = &stop_flag;
```
`cfg.crop_x/y/w/h` restricts the decode to a rectangle of the *source* image —
source coordinates, because automatic scale selection and the `mem_limit`
ladder can both land on a scale you did not pick, and an output-space
rectangle would then name a different part of the picture. The rectangle is
snapped outward to whole output pixels, never inward, and `info.crop_*` and
`estimate.crop_*` report what you actually get. Within it the pixels are
bit-identical to the same sub-rectangle of an uncropped decode. A rectangle
covering the whole frame is inert; one covering more than about 93% of a
12 MP frame costs more than no crop at all, because the per-block map that
unstored blocks need is not otherwise allocated at 1/1.
`lowzag_estimate()` will say so before you commit to it.
### Metadata, and a picture out of half a file
Two things arrived in 2.2.0, both off unless you ask, both a zero in a config
that predates them.
`cfg.marker` receives the payload of the APPn segments `cfg.marker_mask`
names — bit *n* is APPn, so `LOWZAG_APP(1)` is EXIF, `LOWZAG_APP(2)` is an ICC
profile, `LOWZAG_APP_ALL` is all sixteen. lowzag interprets none of it. EXIF is
a TIFF parser and a colour profile is a colour-management problem, and a JPEG
decoder that grew either would be a worse JPEG decoder — but it is the one
thing already walking those segments, and you may be streaming a file you never
have in memory.
Payloads arrive in chunks of a few hundred bytes, each with its offset and the
whole payload's length; nothing is buffered, because the alternative is an
allocation whose size is a field in the file. Return 0 for more, a positive
value to drop the rest of that segment, or a negative one to abandon the
decode. Every segment is delivered before row 0, in file order, including one
sitting between two scans.
```c
static int on_marker(void *ctx, int marker, size_t off, size_t total,
const uint8_t *data, size_t len)
{
struct cap *c = ctx;
if (total > MY_CEILING) return 1; /* not this one, thanks */
if (off == 0) c->buf = malloc(total); /* your ceiling, your malloc */
memcpy(c->buf + off, data, len);
c->len = off + len;
return 0;
}
cfg.marker = on_marker; cfg.marker_ctx = ∩
cfg.marker_mask = LOWZAG_APP(1);
/* ...after the decode: 1..8, or 0 if the file does not say. */
int orientation = lowzag_exif_orientation(cap.buf, cap.len);
```
`lowzag_exif_orientation()` is the fifty lines of TIFF walking that almost
every caller of the callback turns out to want. It allocates nothing, recurses
nowhere, follows no sub-IFD, checks every offset against the length it was
given, and answers 0 rather than guessing. It lives in `src/lowzag_mem.c` so
that `src/lowzag.c` keeps referencing nothing but `memcpy` and `memset`, which
means `-DLOWZAG_BUILD_MEM_API=OFF` does not have it.
`cfg.allow_partial` returns an image from a stream that ended early. That is
the format's own feature: each scan refines the last, so a file that stops
after the DC scan and two AC scans still describes a complete, coarser picture
— what a browser shows while a photograph loads. The result is
`LOWZAG_PARTIAL`, never `LOWZAG_OK`, because a caller who asked for an image
and got half a file has to be able to tell.
What arrives is a whole frame: every row, `rows_delivered == out_h`, only the
content coarser. Blocks that no scan reached hold nothing and come out flat
mid-grey, so check `info.dc_coverage` — the percentage of the frame the DC pass
reached, 100 for every complete file — before showing it to anyone.
`info.scans_decoded` and `info.bytes_consumed` say how much of the file
arrived. `lowzag_estimate()` is unaffected: a partial decode allocates exactly
what a complete one does.
A read callback that reports *failure* is still `LOWZAG_ERR_IO` rather than a
partial image — that is your I/O breaking, not a short file. A reader that
would rather have the picture returns 0 for end of input instead.
`examples/preview.c` is both of these as a working program.
### Linking it into something else
Standalone, without the examples:
```sh
cmake -S . -B build && cmake --build build && cmake --install build
```
then `find_package(lowzag)` and link `lowzag::lowzag`, or use the installed
`pkg-config` file. There is also no shame in dropping `src/lowzag.c` and
`include/lowzag.h` straight into your tree; it is two files and they have no
dependencies. Add `src/lowzag_mem.c` if you want `lowzag_decode_mem()` with
them.
As an ESP-IDF component, from the ESP Component Registry:
```sh
idf.py add-dependency "defensiblelogic/lowzag^2.2.1"
```
which is the same thing as writing it into your project's
`main/idf_component.yml`:
```yaml
dependencies:
defensiblelogic/lowzag: "^2.2.1"
```
There is nothing to add to any `CMakeLists.txt` after that. The component
manager places lowzag under `managed_components/` and adds it to the
requirements of the component whose manifest asked for it, so
`#include "lowzag.h"` works from there. In particular there is nothing to add
to that component's `REQUIRES` — and `lowzag` is not the name to add anyway,
because a registry install is called `defensiblelogic__lowzag` in the build
system. Writing the short name is redundant rather than fatal: the component
manager rewrites a bare `lowzag` to the namespaced one (checked on ESP-IDF
5.4.1). Leave it out and there is nothing to keep in step with the namespace.
To follow a branch or an unreleased commit rather than a release, a git
dependency does that instead:
```yaml
dependencies:
lowzag:
git: https://gitlab.defensiblelogic.com/pub/lowzag.git
version: "main"
```
On that route `version` is a raw git ref rather than a semver range, which is a
different thing and not a different spelling; `examples/esp32/README.md` sets
out what it does and does not accept.
The same `CMakeLists.txt` serves both — it detects `ESP_PLATFORM` and
registers as a component, otherwise it builds as an ordinary CMake project.
## Speed
Decode time is a secondary concern here, but it is not bad. Measured on an
ESP32-S3 at 240 MHz targeting an 800x480 panel, comparing the same image
stored both ways (the baseline file produced from the progressive one with
`jpegtran`, so the pixels are identical). The sources are DualCrow's test
cards — ruled radial lines over a flat field, with the size drawn across the
middle — at three sizes. Both decoders were asked for the same power-of-two
reduction — the coarsest one still covering the panel, so 1/1, 1/2 and 1/4
down the rows:
```
source lowzag ROM TJpgDec (baseline)
800x480 0.63 s 0.46 s
1600x1200 1.75 s 1.95 s
4000x3000 5.7 s 9.1 s
```
Progressive is the more expensive format to parse — every scan gets entropy
decoded, and none can be skipped — so at panel resolution baseline wins. The
two cross around a megapixel. From there on the reduction is worth more to
lowzag than to TJpgDec: TJpgDec's 1/2 and 1/4 modes run the full 8x8 inverse
DCT and average the result down afterwards (`tjpgd.c`, `mcu_output()`; only
its 1/8 mode skips the transform), whereas lowzag never stores, dequantizes or
transforms the coefficients it is going to discard. At 12 MP that is worth
about a third of the decode time.
These are single measurements from one board and one set of test cards, not a
benchmark suite, and they are the one set of numbers in this README that
cannot be reproduced on a host.
## Testing
The decoder is differential-tested against libjpeg-turbo's `djpeg` over a
generated 128-file corpus — 122 valid images and 6 deliberately damaged ones —
at all four scales, 488 comparisons, under AddressSanitizer and
UndefinedBehaviorSanitizer. At scale 1 the output is bit-exact against
`djpeg -nosmooth` on all 122 valid corpus images; at reduced scales it is
compared against both box-filtered and Lanczos references, because neither
kernel alone is a fair target for DCT-domain downsampling. Bit-exactness at
scale 1 is an observed property of the current pair of decoders, not something
the harness enforces — it gates at 40 dB so a future libjpeg-turbo rounding
change does not fail the build.
```sh
cd test && make check # needs cjpeg/djpeg/jpegtran from libjpeg-turbo
cd test && make check-api # API and error-path unit tests, no libjpeg needed
cd test && make check-threads # concurrency, under ThreadSanitizer
cd test && make check-api-msan # the same unit tests under MemorySanitizer
cd test/fuzz && make check # libFuzzer, if you have clang; gcc gets a replay
```
The two sanitizer targets each end by planting a fault their own detector must
find — a race, and a read of uninitialised memory — and fail if it goes
unreported, so a green run means the tool looked rather than that it was never
armed. CI runs both on every push; [`SECURITY.md`](SECURITY.md) says what the
MemorySanitizer run does and does not cover.
This is a C parser of untrusted input, and [`SECURITY.md`](SECURITY.md) is the
threat model: what counts as a bug on any input file however malformed, what is
the caller's problem instead, what the fuzzing and analysis work covers, and
where to report what you find. If you are decoding files from the internet,
read the part about `cfg.mem_limit` — memory follows the source dimensions and
the ceiling is opt-in.
## Provenance
Extracted from [DualCrow](https://gitlab.defensiblelogic.com/def/dualcrow),
firmware for ESP32-S3 display panels, where it ships and where the ESP32
numbers above come from.
Both images this library ships — `examples/sample.jpg` and
`examples/esp32/main/sample.jpg` — are original to this project and
procedurally generated: no photograph, no third-party asset, nothing carrying
someone else's licence. The scripts that draw them sit beside them,
`examples/gen_sample.py` and `examples/esp32/main/gen_sample.py`, take no
external input, and reproduce the committed bytes exactly;
`test/check_images.py` runs both in CI and compares byte for byte. The only
other binary files in the repository are the 22 fuzz seeds in
`test/fuzz/corpus/`, a few hundred bytes each and equally synthetic: 18 tiny
JPEGs, most of them deliberately corrupted, and four that are not valid JPEGs
at all. [`test/fuzz/README.md`](test/fuzz/README.md) says what each is for and
how it was made. So there is nothing to attribute and no NOTICE file to find,
and `examples/` can be vendored into a product without tracking anyone down.
The one piece of third-party code that comes near this tree is `stb_image.h`,
which `examples/anyjpeg` fetches at configure time and never vendors — see
[`examples/anyjpeg/README.md`](examples/anyjpeg/README.md).
MIT licensed. See [`docs/ALGORITHM.md`](docs/ALGORITHM.md) if you want to know
how it actually works, or [`docs/PRIOR-ART.md`](docs/PRIOR-ART.md) if you want
to know who did it first.
05fc50bb3f909d439742719b256671f9bfce15d2
idf.py add-dependency "defensiblelogic/lowzag^2.2.1"