docs(33-11): WebP decision note and STACK row
github.com/gen2brain/webp pinned at v0.6.4 (approved 2026-11-04), with how it runs (purego or the wasm2go-transpiled libwebp), the linking consequence, the benchmark and where the code lives.
This commit is contained in:
50
.planning/notes/2026-10-28-webp-gen2brain.md
Normal file
50
.planning/notes/2026-10-28-webp-gen2brain.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Decision: WebP variants of attachment thumbnails through github.com/gen2brain/webp
|
||||
|
||||
**Date:** 2026-10-28 (dependency approved 2026-11-04)
|
||||
**Status:** Accepted
|
||||
**Context:** Phase 33 (SEO output, audit and content, D-85), 33-CONTEXT.md "Image formats (Jakub, 2026-10-28): WebP now, in Go", 33-RESEARCH.md Open Question 3 and Package Legitimacy Audit, plan 33-06 Task 4 (approval), plan 33-11 (P33-42, P33-43)
|
||||
|
||||
## Decision
|
||||
|
||||
Sites serve their featured images as `<picture>` with a WebP source and the original format as the `srcset` fallback. The WebP files are encoded in Go, once per thumbnail size, in a background job, never in the upload request. AVIF is deferred.
|
||||
|
||||
- **`github.com/gen2brain/webp` pinned at exactly v0.6.4.** Published 2026-07-01 (proxy.golang.org, origin `github.com/gen2brain/webp` at tag `v0.6.4`, commit `3dcf5268`), MIT (the bundled libwebp under its BSD licence, `lib/LICENSE.libwebp`). Approved by Jakub on 2026-11-04 (33-06 Task 4). Its go.mod requires `github.com/ebitengine/purego` v0.10.1 and nothing else.
|
||||
- **Why this library.** `golang.org/x/image/webp`, which `attach` already uses, decodes only; the standard library has no WebP encoder. cgo bindings to libwebp would break the cgo-free build of the single binary. gen2brain/webp encodes in Go without cgo.
|
||||
- **What is encoded.** Each thumbnail a caller asks for (`attach.File.ThumbKey` at a width, height and mode) gets one WebP variant next to it, at quality 80, under `attach.VariantKey(thumbKey, "webp")`. The original upload is never re-encoded, and images an editor inserts into a body keep their original file.
|
||||
- **Where it runs.** Journal's conga job `golem15.journal.image_variants` is dispatched after a featured image upload commits, and `journal:image-variants --all` backfills existing images. A slow or failing encode never fails the upload.
|
||||
|
||||
## How it runs (checked in the v0.6.4 module source, 2026-11-04)
|
||||
|
||||
- **First choice: the system libwebp through purego.** At init the package opens `libwebp.so` and `libwebpdemux.so` with `purego.Dlopen` (no cgo). On Linux this only works in a dynamically linked binary.
|
||||
- **Fallback: libwebp transpiled to Go.** When the libraries are missing (or with the build tag `nodynamic`), it uses libwebp compiled to WASM and then transpiled to plain Go source by wasm2go (`libwebp.go`, about 2.7 MB of generated Go). There is no WASM runtime at run time and no wazero; 33-CONTEXT's "WASM fallback" means this transpiled code. `webp.Dynamic()` reports which path is active.
|
||||
- **Linking consequence.** purego's no-cgo runtime support makes a binary that imports the package dynamically linked, even with `CGO_ENABLED=0`: the probe binary needed the ELF interpreter `/lib64/ld-linux-x86-64.so.2` and `libc.so.6`, `libdl.so.2` and `libpthread.so.0`. It runs on glibc hosts such as the Debian servers. It does not run in a `scratch` image, a distroless static image or on musl (Alpine). Building with `-tags nodynamic` keeps the binary statically linked and uses only the transpiled code (checked: `statically linked`, `Dynamic()` reports "dynamic disabled").
|
||||
- **Side effect.** The package registers a `webp` decoder with `image.RegisterFormat`, next to the one `golang.org/x/image/webp` registers. Both decode WebP, so `image.Decode` keeps working whichever one it picks.
|
||||
|
||||
## Benchmark
|
||||
|
||||
- **33-CONTEXT, 2026-10-28.** A 1200 px variant of a 2592×1754 photo: WebP 78 KB against JPEG q80 at 133 KB. Encoding took about 90 ms with the system libwebp and about 510 ms in the fallback. AVIF came out only about 6 % smaller than WebP, at 1.3× to 20× the CPU.
|
||||
- **Probe of v0.6.4 on 2026-11-04** (this workstation, a synthetic 1200×812 gradient, quality 80): about 17 ms through the system libwebp and about 90 ms in the transpiled fallback. WebP was 29.8 KB against 58.4 KB for JPEG q80. The two runs used different images and machines, so compare the paths with each other, not the numbers across runs.
|
||||
|
||||
## Where the code lives (P33-42)
|
||||
|
||||
- **Framework, generic media.** `modules/lagoon/attach/webp` (package `webp`): `Encode(w, img)` at quality 80 and `Variant(ctx, bucket, f, w, h, mode)`. `Variant` writes the variant next to the thumbnail, returns early when it exists, and deletes the key when an encode or close fails. In `attach` itself there is one generic helper, `attach.VariantKey(thumbKey, format)`.
|
||||
- **Why it is not SEO code.** A WebP variant of a thumbnail is a media format. It has nothing to do with SEO, and the framework gets no SEO wording (D-90). The addition is additive at patch level (D-106).
|
||||
- **Only binaries that import it link it.** `attach` does not import the sub-package, so a binary links gen2brain/webp, purego and the linking consequence above only when it imports `modules/lagoon/attach/webp`. In practice that means through sm-journal-plugin.
|
||||
- **The job lives in Journal.** Only plugins register conga jobs, and Journal owns the featured images and their variants table (`golem15_journal_image_variants`, P33-43).
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
- **AVIF through `github.com/gen2brain/avif`.** About 6 % smaller than WebP at 1.3× to 20× the CPU. Deferred.
|
||||
- **A Nuxt image pipeline (sharp).** New npm dependencies with a native module, and the encoding would move into the frontend server.
|
||||
- **cgo bindings to libwebp.** They break the cgo-free cross-compiled builds the applications use.
|
||||
- **Widths only, no new format.** This needs no dependency, but D-85 asks for a modern format.
|
||||
|
||||
## Package legitimacy
|
||||
|
||||
The package-legitimacy seam cannot check Go modules, so gen2brain/webp was gated by the blocking human check of plan 33-06 Task 4 and approved by Jakub at v0.6.4 before any `go get`. v0.6.4 resolves through proxy.golang.org with the origin above (`h1:SUDdmxADOAiPQ+5ylNmuHhuYf2dOi0KgKZHL5vpVCNU=`).
|
||||
|
||||
## Consequences
|
||||
|
||||
- The framework go.mod requires `github.com/gen2brain/webp v0.6.4` (and, through it, `github.com/ebitengine/purego v0.10.1`). Only packages under `modules/lagoon/attach/webp` import it.
|
||||
- An application that mounts sm-journal-plugin links the encoder. Its binary is dynamically linked against glibc unless it is built with `-tags nodynamic`. Installing libwebp on the server makes encoding about five times faster, and nothing else changes. The package opens the unversioned names `libwebp.so` and `libwebpdemux.so`, which Debian ships in `libwebp-dev`.
|
||||
- If the library ever needs replacing, only `modules/lagoon/attach/webp/webp.go` changes. Callers use `Encode` and `Variant`.
|
||||
Reference in New Issue
Block a user