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