add support for ppm images
This commit is contained in:
@@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## Project
|
||||
|
||||
`att_lock` — an Android-style 4×4 dot-pattern screen locker for wlroots-based
|
||||
Wayland compositors. Single C11 binary, ~1500 LOC across four `src/*.c` files. No test suite exists.
|
||||
Wayland compositors. Single C11 binary, ~2000 LOC across five `src/*.c` files. No test suite exists.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -21,10 +21,11 @@ nix develop --command ninja -C build # incremental rebu
|
||||
Add new dependencies to `nativeDeps`/`libDeps` in `flake.nix` *and* to `meson.build`, never to a
|
||||
global environment.
|
||||
|
||||
**Gotcha:** this repo is not git-tracked, so a plain `src = ./.` would vacuum the local `build/` dir
|
||||
into the Nix sandbox and break `nix build` (meson chokes on a stale pre-configured build dir).
|
||||
`flake.nix` filters `build` and `result*` out via `lib.cleanSourceWith` — keep that filter (or init
|
||||
a git repo) whenever touching `flake.nix`.
|
||||
**Gotcha:** the repo is a git repo now, so `nix build` only sees **git-tracked** files — a new
|
||||
`src/*.c` has to be `git add`ed before it will build, or meson fails with `File src/... does not
|
||||
exist` while `ninja -C build` keeps working fine. `flake.nix` additionally filters `build` and
|
||||
`result*` out via `lib.cleanSourceWith`; that filter predates the git repo (it kept a plain
|
||||
`src = ./.` from vacuuming a stale pre-configured `build/` into the sandbox) and is harmless to keep.
|
||||
|
||||
## Running it
|
||||
|
||||
@@ -129,8 +130,24 @@ Three things about the stored hash are deliberate and load-bearing:
|
||||
corrupt file and `main()` routes that into the setup flow.
|
||||
|
||||
**Rendering** (`src/render.c`) — `wl_shm` + Cairo only, no EGL/GPU. Two buffers per output with a
|
||||
`busy` flag driven by `wl_buffer.release`; `buffer_get()` reallocates on size change. Background
|
||||
images are PNG-only (Cairo's built-in loader).
|
||||
`busy` flag driven by `wl_buffer.release`; `buffer_get()` reallocates on size change. The background
|
||||
is drawn cover-fit (`fmax` of the two axis ratios), so it follows a rotation with no extra work.
|
||||
|
||||
**Background images** (`src/image.c`) — `image_load()` takes PNG or PPM (both `P6` binary and `P3`
|
||||
ASCII, 8- or 16-bit samples), by path or `-` for stdin: `image_gen | att_lock -i -`. Cairo only
|
||||
decodes PNG and only from a file or stream, so the input is always buffered whole and then sniffed by
|
||||
magic — which is also what lets a pipe work, since it can be neither reopened nor seeked. PPM decodes
|
||||
straight into `CAIRO_FORMAT_RGB24`, a native-endian `0x00RRGGBB` word per pixel (write through a
|
||||
`uint32_t*`, don't assemble bytes by hand, or the channels swap on the way out).
|
||||
|
||||
Two things here are deliberate:
|
||||
|
||||
- **Every failure returns NULL and is non-fatal.** A bad image must degrade to the plain dark
|
||||
background, never to a locked session with nothing drawn on it. Decoding happens in `main()` before
|
||||
`wl_display_connect()`, which also means `-i -` drains the pipe *before* taking the lock rather
|
||||
than holding a blank screen for as long as the producer runs.
|
||||
- **`-i -` refuses an interactive stdin** (`isatty`), because `att_lock -i -` typed at a shell would
|
||||
otherwise block on the terminal forever with the session not yet locked.
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ sources = files(
|
||||
'src/main.c',
|
||||
'src/pattern.c',
|
||||
'src/render.c',
|
||||
'src/image.c',
|
||||
'src/auth.c',
|
||||
)
|
||||
|
||||
|
||||
@@ -152,6 +152,11 @@ int pattern_to_string(const struct att_pattern *p, char *buf, size_t n);
|
||||
void render_output(struct att_output *o);
|
||||
void render_all(struct att *app);
|
||||
|
||||
/* image.c ------------------------------------------------------------------ */
|
||||
/* PNG or PPM (P3/P6), by path or "-" for stdin. Returns a cairo_surface_t*,
|
||||
* or NULL after reporting why -- a missing background is never fatal. */
|
||||
void *image_load(const char *path);
|
||||
|
||||
/* auth.c ------------------------------------------------------------------- */
|
||||
enum auth_hash_state {
|
||||
AUTH_HASH_NONE, /* no stored pattern yet */
|
||||
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
#include "att_lock.h"
|
||||
|
||||
#include <cairo.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Background image loading.
|
||||
*
|
||||
* The image is decoded once at startup, before the lock surface exists, so a
|
||||
* malformed or truncated file costs a diagnostic and a plain dark background --
|
||||
* never a locked session with nothing drawn on it. Every failure path here
|
||||
* returns NULL and lets main() carry on locking.
|
||||
*
|
||||
* Cairo only knows how to read PNG, and only from a file or a stream, so both
|
||||
* formats are sniffed from a fully buffered copy of the input. That is what
|
||||
* makes `-i -` work: a pipe cannot be reopened or seeked, so it has to be read
|
||||
* to the end before anything can decide what it is. */
|
||||
|
||||
/* An 8K RGB16 PPM is ~200 MB; past this we are being handed garbage. */
|
||||
#define IMAGE_MAX_BYTES (512u * 1024 * 1024)
|
||||
/* Bounds w*h*4 well inside size_t and keeps a bogus header from asking Cairo
|
||||
* for an absurd allocation. */
|
||||
#define IMAGE_MAX_DIM 32767
|
||||
|
||||
struct buf {
|
||||
const unsigned char *p;
|
||||
size_t n;
|
||||
size_t pos;
|
||||
};
|
||||
|
||||
/* --- input ---------------------------------------------------------------- */
|
||||
|
||||
/* Read fd to EOF. Returns a malloc'd buffer, or NULL (with *len untouched). */
|
||||
static unsigned char *read_all(int fd, size_t *len)
|
||||
{
|
||||
size_t cap = 1 << 16, n = 0;
|
||||
unsigned char *b = malloc(cap);
|
||||
if (!b)
|
||||
return NULL;
|
||||
|
||||
for (;;) {
|
||||
if (n == cap) {
|
||||
if (cap >= IMAGE_MAX_BYTES) {
|
||||
fprintf(stderr, "att_lock: image exceeds %u MiB\n",
|
||||
IMAGE_MAX_BYTES >> 20);
|
||||
free(b);
|
||||
return NULL;
|
||||
}
|
||||
size_t ncap = cap * 2;
|
||||
unsigned char *nb = realloc(b, ncap);
|
||||
if (!nb) {
|
||||
free(b);
|
||||
return NULL;
|
||||
}
|
||||
b = nb;
|
||||
cap = ncap;
|
||||
}
|
||||
ssize_t r = read(fd, b + n, cap - n);
|
||||
if (r < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
free(b);
|
||||
return NULL;
|
||||
}
|
||||
if (r == 0)
|
||||
break;
|
||||
n += (size_t)r;
|
||||
}
|
||||
|
||||
*len = n;
|
||||
return b;
|
||||
}
|
||||
|
||||
/* --- PPM ------------------------------------------------------------------ */
|
||||
|
||||
static int ppm_byte(struct buf *b)
|
||||
{
|
||||
return b->pos < b->n ? b->p[b->pos++] : -1;
|
||||
}
|
||||
|
||||
/* Read one header integer, skipping whitespace and #-comments.
|
||||
*
|
||||
* Consumes exactly one byte after the digits. That is deliberate: PPM specifies
|
||||
* a single whitespace character between the maxval and the start of binary
|
||||
* raster data, so leaving the position here puts it on the first sample. */
|
||||
static bool ppm_uint(struct buf *b, unsigned long *out)
|
||||
{
|
||||
int c;
|
||||
|
||||
for (;;) {
|
||||
c = ppm_byte(b);
|
||||
if (c < 0)
|
||||
return false;
|
||||
if (c == '#') {
|
||||
while ((c = ppm_byte(b)) >= 0 && c != '\n' && c != '\r')
|
||||
;
|
||||
if (c < 0)
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
if (!isspace(c))
|
||||
break;
|
||||
}
|
||||
if (!isdigit(c))
|
||||
return false;
|
||||
|
||||
unsigned long v = 0;
|
||||
do {
|
||||
if (v > (ULONG_MAX - 9) / 10)
|
||||
return false;
|
||||
v = v * 10 + (unsigned long)(c - '0');
|
||||
c = ppm_byte(b);
|
||||
} while (c >= 0 && isdigit(c));
|
||||
|
||||
*out = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline uint32_t scale_sample(unsigned long v, unsigned long maxval)
|
||||
{
|
||||
if (v > maxval)
|
||||
v = maxval;
|
||||
return maxval == 255 ? (uint32_t)v
|
||||
: (uint32_t)((v * 255 + maxval / 2) / maxval);
|
||||
}
|
||||
|
||||
/* Decode a P3 (ASCII) or P6 (binary) PPM. `b` is positioned after the magic. */
|
||||
static cairo_surface_t *ppm_decode(struct buf *b, bool ascii)
|
||||
{
|
||||
unsigned long w, h, maxval;
|
||||
|
||||
if (!ppm_uint(b, &w) || !ppm_uint(b, &h) || !ppm_uint(b, &maxval)) {
|
||||
fprintf(stderr, "att_lock: malformed PPM header\n");
|
||||
return NULL;
|
||||
}
|
||||
if (w == 0 || h == 0 || w > IMAGE_MAX_DIM || h > IMAGE_MAX_DIM) {
|
||||
fprintf(stderr, "att_lock: unsupported PPM size %lux%lu\n", w, h);
|
||||
return NULL;
|
||||
}
|
||||
if (maxval == 0 || maxval > 65535) {
|
||||
fprintf(stderr, "att_lock: unsupported PPM maxval %lu\n", maxval);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int wide = maxval > 255; /* two bytes per sample */
|
||||
if (!ascii) {
|
||||
size_t need = (size_t)w * h * 3 * (wide ? 2 : 1);
|
||||
if (b->n - b->pos < need) {
|
||||
fprintf(stderr, "att_lock: truncated PPM raster\n");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
cairo_surface_t *surf = cairo_image_surface_create(
|
||||
CAIRO_FORMAT_RGB24, (int)w, (int)h);
|
||||
if (cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS) {
|
||||
fprintf(stderr, "att_lock: cannot allocate %lux%lu image: %s\n",
|
||||
w, h, cairo_status_to_string(cairo_surface_status(surf)));
|
||||
cairo_surface_destroy(surf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
unsigned char *data = cairo_image_surface_get_data(surf);
|
||||
int stride = cairo_image_surface_get_stride(surf);
|
||||
|
||||
for (unsigned long y = 0; y < h; y++) {
|
||||
/* RGB24 is a 32-bit 0x00RRGGBB word in native byte order, and
|
||||
* Cairo guarantees the stride keeps rows 32-bit aligned. */
|
||||
uint32_t *row = (uint32_t *)(void *)(data + (size_t)y * (size_t)stride);
|
||||
|
||||
for (unsigned long x = 0; x < w; x++) {
|
||||
unsigned long s[3];
|
||||
|
||||
if (ascii) {
|
||||
if (!ppm_uint(b, &s[0]) || !ppm_uint(b, &s[1]) ||
|
||||
!ppm_uint(b, &s[2])) {
|
||||
fprintf(stderr, "att_lock: truncated PPM raster\n");
|
||||
cairo_surface_destroy(surf);
|
||||
return NULL;
|
||||
}
|
||||
} else if (wide) {
|
||||
const unsigned char *q = b->p + b->pos;
|
||||
s[0] = ((unsigned long)q[0] << 8) | q[1];
|
||||
s[1] = ((unsigned long)q[2] << 8) | q[3];
|
||||
s[2] = ((unsigned long)q[4] << 8) | q[5];
|
||||
b->pos += 6;
|
||||
} else {
|
||||
const unsigned char *q = b->p + b->pos;
|
||||
s[0] = q[0];
|
||||
s[1] = q[1];
|
||||
s[2] = q[2];
|
||||
b->pos += 3;
|
||||
}
|
||||
|
||||
row[x] = scale_sample(s[0], maxval) << 16 |
|
||||
scale_sample(s[1], maxval) << 8 |
|
||||
scale_sample(s[2], maxval);
|
||||
}
|
||||
}
|
||||
|
||||
cairo_surface_mark_dirty(surf);
|
||||
return surf;
|
||||
}
|
||||
|
||||
/* --- PNG ------------------------------------------------------------------ */
|
||||
|
||||
static cairo_status_t png_read(void *closure, unsigned char *data,
|
||||
unsigned int length)
|
||||
{
|
||||
struct buf *b = closure;
|
||||
if (length > b->n - b->pos)
|
||||
return CAIRO_STATUS_READ_ERROR;
|
||||
memcpy(data, b->p + b->pos, length);
|
||||
b->pos += length;
|
||||
return CAIRO_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static cairo_surface_t *png_decode(struct buf *b)
|
||||
{
|
||||
cairo_surface_t *surf =
|
||||
cairo_image_surface_create_from_png_stream(png_read, b);
|
||||
if (cairo_surface_status(surf) == CAIRO_STATUS_SUCCESS)
|
||||
return surf;
|
||||
|
||||
fprintf(stderr, "att_lock: could not decode PNG: %s\n",
|
||||
cairo_status_to_string(cairo_surface_status(surf)));
|
||||
cairo_surface_destroy(surf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* --- entry point ---------------------------------------------------------- */
|
||||
|
||||
void *image_load(const char *path)
|
||||
{
|
||||
bool from_stdin = strcmp(path, "-") == 0;
|
||||
int fd;
|
||||
|
||||
if (from_stdin) {
|
||||
/* Without this, `att_lock -i -` typed at a shell would block on
|
||||
* the terminal forever -- with the session not yet locked. */
|
||||
if (isatty(STDIN_FILENO)) {
|
||||
fprintf(stderr, "att_lock: -i - expects an image on stdin, "
|
||||
"but stdin is a terminal\n");
|
||||
return NULL;
|
||||
}
|
||||
fd = STDIN_FILENO;
|
||||
} else {
|
||||
fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
fprintf(stderr, "att_lock: cannot open image '%s': %s\n",
|
||||
path, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
unsigned char *bytes = read_all(fd, &len);
|
||||
if (!from_stdin)
|
||||
close(fd);
|
||||
if (!bytes) {
|
||||
fprintf(stderr, "att_lock: cannot read image '%s': %s\n",
|
||||
path, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct buf b = { .p = bytes, .n = len, .pos = 0 };
|
||||
cairo_surface_t *surf = NULL;
|
||||
|
||||
if (len >= 8 && memcmp(bytes, "\x89PNG\r\n\x1a\n", 8) == 0) {
|
||||
surf = png_decode(&b);
|
||||
} else if (len >= 2 && bytes[0] == 'P' &&
|
||||
(bytes[1] == '3' || bytes[1] == '6')) {
|
||||
b.pos = 2;
|
||||
surf = ppm_decode(&b, bytes[1] == '3');
|
||||
} else {
|
||||
fprintf(stderr, "att_lock: '%s' is not a PNG or PPM image\n", path);
|
||||
}
|
||||
|
||||
free(bytes);
|
||||
return surf;
|
||||
}
|
||||
+10
-14
@@ -830,7 +830,9 @@ static void usage(const char *argv0)
|
||||
"Android-style 4x4 pattern screen locker for wlroots compositors.\n"
|
||||
"\n"
|
||||
"Options:\n"
|
||||
" -i, --image PATH Background image (PNG), also accepted positionally\n"
|
||||
" -i, --image PATH Background image (PNG or PPM), also accepted\n"
|
||||
" positionally; \"-\" reads it from stdin, e.g.\n"
|
||||
" image_gen | %s -i -\n"
|
||||
" -p, --pam-service NAME PAM service for the password fallback (default: login)\n"
|
||||
" -m, --min-dots N Minimum dots in a pattern (default: 4)\n"
|
||||
" --setup Force (re)creation of the stored pattern\n"
|
||||
@@ -839,7 +841,7 @@ static void usage(const char *argv0)
|
||||
"On first run (or with --setup) you draw the pattern twice to store it as a\n"
|
||||
"salted hash under $XDG_DATA_HOME/att_lock/pattern.hash. Start typing at the\n"
|
||||
"unlock screen to fall back to PAM password authentication.\n",
|
||||
argv0);
|
||||
argv0, argv0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
@@ -890,18 +892,12 @@ int main(int argc, char **argv)
|
||||
app.mode = (force_setup || hstate != AUTH_HASH_OK)
|
||||
? MODE_SETUP_FIRST : MODE_UNLOCK;
|
||||
|
||||
if (app.image_path) {
|
||||
cairo_surface_t *bg =
|
||||
cairo_image_surface_create_from_png(app.image_path);
|
||||
if (cairo_surface_status(bg) == CAIRO_STATUS_SUCCESS) {
|
||||
app.bg_surface = bg;
|
||||
} else {
|
||||
fprintf(stderr, "att_lock: could not load image '%s' "
|
||||
"(PNG only): %s\n", app.image_path,
|
||||
cairo_status_to_string(cairo_surface_status(bg)));
|
||||
cairo_surface_destroy(bg);
|
||||
}
|
||||
}
|
||||
/* Before connecting to the display on purpose: with `-i -` this drains the
|
||||
* pipe to EOF, and doing that while already holding the lock would show a
|
||||
* blank screen for as long as the producer takes. image_load() reports its
|
||||
* own failures and returns NULL, which just means no background. */
|
||||
if (app.image_path)
|
||||
app.bg_surface = image_load(app.image_path);
|
||||
|
||||
app.display = wl_display_connect(NULL);
|
||||
if (!app.display) {
|
||||
|
||||
Reference in New Issue
Block a user