From 54328377d05eeee5880415a698d554db80450b80 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Thu, 8 Oct 2026 06:19:05 +0200 Subject: [PATCH] Install libvips JPEG XL support and require it at startup (part of #222) script/bootstrap --cgo installs vips-jxl when the package manager it uses is apk, as Alpine's vips package lacks JPEG XL support; the nix and brew libvips include it, as does apt's from Debian 12 and Ubuntu 24.04 on. The runtime stage of the Dockerfile installs vips-jxl too. imgcache.NewService calls the new imageprocessor.CheckJPEGXLSupport before it builds the image processor and fails with its error, which names the fix, so pixad does not start without the support. JPEG XL becomes the default output later in the issue. New tests check the support and save an image as JPEG XL with govips and load it back. Model: opus-5-5 --- Dockerfile | 9 ++-- README.md | 9 ++-- TODO.md | 8 +++ internal/imageprocessor/imageprocessor.go | 18 +++++++ .../imageprocessor/jpegxl_internal_test.go | 52 +++++++++++++++++++ internal/imgcache/service.go | 7 +++ script/bootstrap | 21 ++++++-- 7 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 internal/imageprocessor/jpegxl_internal_test.go diff --git a/Dockerfile b/Dockerfile index d811825..edd7c82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,9 @@ FROM golang:1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66 WORKDIR /src -# script/bootstrap --cgo installs the build dependencies (a C compiler -# and the libvips and libheif headers) and downloads the Go modules. +# script/bootstrap --cgo installs the build dependencies (a C compiler, +# the libvips and libheif headers, and libvips' JPEG XL support, which +# the tests need) and downloads the Go modules. COPY script/ ./script/ COPY go.mod go.sum ./ RUN script/bootstrap --cgo @@ -80,9 +81,11 @@ RUN version="${VERSION:-$(git describe --tags --always)}"; \ # alpine:3.21, 2026-02-25 FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 -# Install runtime dependencies only +# Install runtime dependencies only. vips-jxl is libvips' JPEG XL +# support, without which pixad does not start. RUN apk add --no-cache \ vips \ + vips-jxl \ libheif \ ca-certificates \ tzdata \ diff --git a/README.md b/README.md index cd7b4c2..fd33ff7 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,10 @@ another part of pixa failed to stop. A request not finished by then is cut off. `docker stop` waits 10 seconds before it kills the container. Outside Docker, pixa needs libvips (the image has 8.15) and libheif to run, as -it uses libvips through CGO; building it also needs their development files, +it uses libvips through CGO. pixad does not start unless libvips has its JPEG XL +support, which on Alpine is the `vips-jxl` package and which the nix and brew +packages of libvips include, as do the apt ones from Debian 12 and Ubuntu 24.04 +on. Building pixa also needs the development files of libvips and libheif, `pkg-config` and a C compiler. `script/bootstrap --cgo` installs all of these, as the `Dockerfile` does where it compiles pixa. Plain `script/bootstrap`, which `script/setup` and `script/cibuild` run, installs git, make and Go, and Node, @@ -585,8 +588,8 @@ provide: - `script/bootstrap` — install git, make, Go, Node, Yarn and prettier and download the Go modules (idempotent); with `--cgo`, the C compiler and the - libvips and libheif libraries that compiling pixa needs instead of Node, Yarn - and prettier + libvips (with its JPEG XL support) and libheif libraries that compiling and + testing pixa need instead of Node, Yarn and prettier - `script/setup` — make a fresh clone ready for development (bootstrap, then install-precommit) - `script/projectname` — output the project name ("pixa") diff --git a/TODO.md b/TODO.md index 53bb84d..a272151 100644 --- a/TODO.md +++ b/TODO.md @@ -30,6 +30,14 @@ P2: security: per-IP rate limiting on the image routes # Completed Steps +- 2026-10-08 libvips' JPEG XL support is installed and required (part of #222): + `script/bootstrap --cgo` installs `vips-jxl` when its package manager is apk, + as Alpine's `vips` package lacks the support, and the runtime stage of the + `Dockerfile` installs it too. `imgcache.NewService` fails, naming the fix, + when `imageprocessor.CheckJPEGXLSupport` finds that libvips cannot load and + save JPEG XL, so pixad does not start without it. A test saves an image as + JPEG XL with govips and loads it back. JPEG XL is not yet a format pixa + serves. - 2026-10-08 SQLite writes no longer fail with "database is locked" under load (closes #223): `internal/database` opens the database with one connection, so pixa's own reads and writes run on it one at a time instead of competing for diff --git a/internal/imageprocessor/imageprocessor.go b/internal/imageprocessor/imageprocessor.go index f26ed57..b7c2718 100644 --- a/internal/imageprocessor/imageprocessor.go +++ b/internal/imageprocessor/imageprocessor.go @@ -37,6 +37,24 @@ func initVips() { }) } +// errNoJPEGXL is returned by CheckJPEGXLSupport. +var errNoJPEGXL = errors.New("libvips lacks JPEG XL support: install " + + "vips-jxl on Alpine, or use a libvips built with libjxl") + +// CheckJPEGXLSupport returns an error, naming the fix, when libvips +// cannot load and save JPEG XL. +func CheckJPEGXLSupport() error { + initVips() + + // govips counts a format as supported when libvips has its loader; + // libvips builds the JPEG XL loader and saver together. + if !vips.IsTypeSupported(vips.ImageTypeJXL) { + return errNoJPEGXL + } + + return nil +} + // Format represents supported output image formats. type Format string diff --git a/internal/imageprocessor/jpegxl_internal_test.go b/internal/imageprocessor/jpegxl_internal_test.go new file mode 100644 index 0000000..373d35d --- /dev/null +++ b/internal/imageprocessor/jpegxl_internal_test.go @@ -0,0 +1,52 @@ +package imageprocessor + +import ( + "testing" + + "github.com/davidbyttow/govips/v2/vips" +) + +// TestCheckJPEGXLSupport fails when libvips lacks JPEG XL support, as on +// Alpine without the vips-jxl package. +func TestCheckJPEGXLSupport(t *testing.T) { + t.Parallel() + + err := CheckJPEGXLSupport() + if err != nil { + t.Fatalf("CheckJPEGXLSupport() error = %v", err) + } +} + +// TestLibvipsSavesAndLoadsJPEGXL saves an image as JPEG XL with govips and +// loads it back. It fails when libvips lacks JPEG XL support, as on Alpine +// without the vips-jxl package. +func TestLibvipsSavesAndLoadsJPEGXL(t *testing.T) { + t.Parallel() + + img, err := vips.NewImageFromBuffer(createTestJPEG(t, 64, 48)) + if err != nil { + t.Fatalf("failed to load test JPEG: %v", err) + } + + defer img.Close() + + jxl, _, err := img.ExportJxl(vips.NewJxlExportParams()) + if err != nil { + t.Fatalf("ExportJxl() error = %v", err) + } + + loaded, err := vips.NewImageFromBuffer(jxl) + if err != nil { + t.Fatalf("failed to load the JPEG XL image: %v", err) + } + + defer loaded.Close() + + if loaded.Format() != vips.ImageTypeJXL { + t.Errorf("loaded format = %s, want jxl", vips.ImageTypes[loaded.Format()]) + } + + if loaded.Width() != 64 || loaded.Height() != 48 { + t.Errorf("loaded size = %dx%d, want 64x48", loaded.Width(), loaded.Height()) + } +} diff --git a/internal/imgcache/service.go b/internal/imgcache/service.go index 2a40c8c..532903e 100644 --- a/internal/imgcache/service.go +++ b/internal/imgcache/service.go @@ -100,6 +100,13 @@ func NewService(cfg *ServiceConfig) (*Service, error) { allowHTTP = cfg.FetcherConfig.AllowHTTP } + // JPEG XL is to become the default output format, so pixad does not + // start without it. + err := imageprocessor.CheckJPEGXLSupport() + if err != nil { + return nil, err + } + maxResponseSize := fetcherCfg.MaxResponseSize processor := imageprocessor.New(imageprocessor.Params{ MaxInputBytes: maxResponseSize, diff --git a/script/bootstrap b/script/bootstrap index 31abd17..fbada47 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -14,11 +14,12 @@ # script/fmt-check: all the host needs, as # the checks compile pixa in Docker # script/bootstrap --cgo git, make, Go, and a C compiler and the -# CGO image libraries (pkg-config, vips, -# libheif) for the govips bindings instead -# of Node: to compile pixa, in the -# Dockerfile's test phase and build stage, -# which format nothing +# CGO image libraries (pkg-config, vips +# with its JPEG XL support, libheif) for +# the govips bindings instead of Node: to +# compile pixa, in the Dockerfile's test +# phase and build stage, which format +# nothing set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" @@ -150,6 +151,16 @@ ensure_cgo_deps() { if ! pkg-config --exists vips; then pkg_install vips libvips-dev vips vips-dev fi + # libvips' JPEG XL loader and saver are in the nix and brew vips + # packages, and in apt's from Debian 12 and Ubuntu 24.04 on, but in + # the package vips-jxl on Alpine. detect_pkgmgr is called only where + # apk exists, as on apt it updates the package lists. + if ! missing apk; then + detect_pkgmgr + fi + if [ "$PKGMGR" = "apk" ] && ! apk info -e vips-jxl >/dev/null; then + apk add --no-cache vips-jxl + fi if ! pkg-config --exists libheif; then pkg_install libheif libheif-dev libheif libheif-dev fi