A small PPM image viewer written in Zig using raylib via raylib-zig.
- Zig
0.16.0or newer
Dependencies are fetched automatically by the Zig package manager on first build.
The viewer takes the path to a PPM file as its only argument:
zig build run -- images/p6.ppmThe -- separates Zig's own arguments from the program's. Two sample images are included:
| File | Format | Size on disk |
|---|---|---|
images/p3.ppm |
P3 (ASCII) | 2.9 MB |
images/p6.ppm |
P6 (binary) | 733 KB |
Both are the same 500 × 500 image — the P3 encoding is roughly 4× larger because every channel is written out as decimal text.
To just build the executable (output lands in zig-out/bin/):
zig buildPaths are resolved relative to the current working directory, so run from the project root or pass an absolute path.
| Key | Action |
|---|---|
Esc |
Close the window |
The window is sized to the image, and the image is drawn at 1:1 into it — there is no zoom, pan, or scaling.
Reading happens in three stages:
parseHeaderreads the magic number and dimensions, and records the byte offset where pixel data begins. That offset is what makes binary P6 work: after the maxval there is exactly one whitespace byte, and everything past it is raw data that must never be treated as text.decodeturns the raster into tightly packed RGB bytes — three per pixel. P6 is a straight copy; P3 is parsed from decimal text. This runs once, before the window opens.drawImagewalks the decoded buffer each frame and draws one rectangle per pixel.
Decoding up front is what keeps the P3 path cheap. Parsing 750,000 decimal numbers per frame at 30 fps would be 22.5 million parseInt calls a second; doing it once means the render loop is identical for both formats.
src/main.zig
├── Header # dimensions, format, raster byte offset
├── parseHeader # magic number, dimensions, where pixel data starts
├── decode # P3 text or P6 binary -> packed RGB bytes
├── drawImage # per-pixel draw from the decoded buffer
└── main # args, file read, window setup, render loop
maxvalis assumed to be 255. The header field is skipped rather than read. Files using a smaller maximum render too dark, and 16-bit files (maxval above 255) are not supported at all.- Only whole-line header comments are skipped. A comment that follows a value on the same line, like
500 500 # dimensions, will fail to parse. - The format field is not validated. Anything that isn't exactly
P6is decoded as P3. - Files are capped at 4 MB. A larger image is rejected rather than read.
- Every pixel is redrawn every frame as an individual rectangle. Uploading the decoded buffer to a texture once would be far faster, at the cost of not driving the pixels directly.