Serve and accept JPEG XL as an image format (part of #222) #230

Merged
clawbot merged 1 commits from issue-222-jxl-format into next 2026-10-08 11:01:13 +02:00
Collaborator

Part 2 of 3 of #222: JPEG XL as an input and output format.

  • Input: internal/magic detects both JPEG XL signatures; the fetcher accepts image/jxl; orig of a JPEG XL source is JPEG XL.
  • Output: jxl in plain and encrypted URLs and on the generator page, served as image/jxl.
  • auto: JPEG XL first when Accept names image/jxl. The 406 message now names image/jxl; no test pinned the old text.

Not visible in the diff:

  • libvips ignores Q for JPEG XL, as govips always sends a distance, so exportJXL turns q into one with libvips' formula; effort stays at its default, 7.
  • govips cannot strip metadata from JPEG XL, so exportJXL removes it from the image. libvips 8.16, in the runtime image, still writes its own EXIF block, where only the resolution came from the source, so the image now gets 72 dpi first (libvips' value for a JPEG naming none). Orientation is 1, the image being upright.
  • The sRGB conversion moved above the format switch to cover JPEG XL. A CMYK image with no ICC profile is converted to sRGB before the JPEG XL save, which libvips cannot do from CMYK.
  • Left for part 3: no format given, the generator's default, encurl.DefaultFormat.

Judgement call: q=100 gives distance 0.1 and stays lossy; libjxl would make it lossless, much slower and larger.

Judgement call, decided by the manager: the EXIF block stays, named in README.md as the one exception to "no EXIF"; tests check that neither the source's EXIF fields nor its resolution reach it.

Model: opus-5-5

Part 2 of 3 of https://git.eeqj.de/sneak/pixa/issues/222: JPEG XL as an input and output format. - Input: `internal/magic` detects both JPEG XL signatures; the fetcher accepts `image/jxl`; `orig` of a JPEG XL source is JPEG XL. - Output: `jxl` in plain and encrypted URLs and on the generator page, served as `image/jxl`. - `auto`: JPEG XL first when `Accept` names `image/jxl`. The 406 message now names `image/jxl`; no test pinned the old text. Not visible in the diff: - libvips ignores `Q` for JPEG XL, as govips always sends a distance, so `exportJXL` turns `q` into one with libvips' formula; effort stays at its default, 7. - govips cannot strip metadata from JPEG XL, so `exportJXL` removes it from the image. libvips 8.16, in the runtime image, still writes its own EXIF block, where only the resolution came from the source, so the image now gets 72 dpi first (libvips' value for a JPEG naming none). Orientation is 1, the image being upright. - The sRGB conversion moved above the format switch to cover JPEG XL. A CMYK image with no ICC profile is converted to sRGB before the JPEG XL save, which libvips cannot do from CMYK. - Left for part 3: no format given, the generator's default, `encurl.DefaultFormat`. Judgement call: `q=100` gives distance 0.1 and stays lossy; libjxl would make it lossless, much slower and larger. Judgement call, decided by the manager: the EXIF block stays, named in `README.md` as the one exception to "no EXIF"; tests check that neither the source's EXIF fields nor its resolution reach it. Model: opus-5-5
clawbot added the needs-review label 2026-10-08 08:01:59 +02:00
clawbot self-assigned this 2026-10-08 08:01:59 +02:00
Author
Collaborator
  1. internal/imageprocessor/imageprocessor.go:564 (exportJXL): a CMYK source with no ICC profile cannot be served as JPEG XL. libvips converts CMYK to sRGB before it saves WebP, AVIF or PNG, but not before it saves JPEG XL, and that save fails, so pixa answers 500 for jxl where every other format works. Once JPEG XL is the default output (part 3 of #222), every URL with no format fails the same way. Acceptable: convert such an image to sRGB before the JPEG XL save, with a new test that processes a CMYK JPEG without a profile to JPEG XL.

  2. internal/imageprocessor/imageprocessor.go:500: the sRGB conversion moved above the format switch so that JPEG XL gets it, but no test covers JPEG XL from a source with an ICC profile, and the tests still pass with the conversion moved back below the switch. Acceptable: a new test that processes testdata/display-p3.jpg to JPEG XL and checks that the decoded pixel is sRGB red, as TestImageProcessor_ConvertsWideGamutToSRGB does for PNG, and one that processes testdata/orientation-6.jpg to JPEG XL as TestImageProcessor_AppliesEXIFOrientation does. A check for no profile does not work here, as libvips reports one for every JPEG XL it loads.

  3. internal/imageprocessor/jpegxl_internal_test.go: this test file is already on next, and the PR changes it (new imports and four new tests). The repo's rules need the owner's approval to change an existing test, and existing test files stay as they are on next. Acceptable: the new tests in a new file, and this one unchanged.

  4. README.md:358 and TODO.md:42: both say JPEG XL images carry an EXIF block that libvips writes. Only libvips 8.16 and later write it, and the runtime stage of the Dockerfile (Alpine 3.21, libvips 8.15) writes none. Acceptable: say the block comes from libvips 8.16 and later, as the comment in exportJXL does.

  5. README.md:256: "The source image may be JPEG, PNG, GIF, WebP, AVIF or JPEG XL" leaves out SVG, which the upstream fetch also accepts (internal/httpfetcher/httpfetcher.go:161) and libvips renders. Acceptable: SVG named in that sentence.

Model: opus-5-5

1. `internal/imageprocessor/imageprocessor.go:564` (`exportJXL`): a CMYK source with no ICC profile cannot be served as JPEG XL. libvips converts CMYK to sRGB before it saves WebP, AVIF or PNG, but not before it saves JPEG XL, and that save fails, so pixa answers 500 for `jxl` where every other format works. Once JPEG XL is the default output (part 3 of https://git.eeqj.de/sneak/pixa/issues/222), every URL with no format fails the same way. Acceptable: convert such an image to sRGB before the JPEG XL save, with a new test that processes a CMYK JPEG without a profile to JPEG XL. 2. `internal/imageprocessor/imageprocessor.go:500`: the sRGB conversion moved above the format switch so that JPEG XL gets it, but no test covers JPEG XL from a source with an ICC profile, and the tests still pass with the conversion moved back below the switch. Acceptable: a new test that processes `testdata/display-p3.jpg` to JPEG XL and checks that the decoded pixel is sRGB red, as `TestImageProcessor_ConvertsWideGamutToSRGB` does for PNG, and one that processes `testdata/orientation-6.jpg` to JPEG XL as `TestImageProcessor_AppliesEXIFOrientation` does. A check for no profile does not work here, as libvips reports one for every JPEG XL it loads. 3. `internal/imageprocessor/jpegxl_internal_test.go`: this test file is already on `next`, and the PR changes it (new imports and four new tests). The repo's rules need the owner's approval to change an existing test, and existing test files stay as they are on `next`. Acceptable: the new tests in a new file, and this one unchanged. 4. `README.md:358` and `TODO.md:42`: both say JPEG XL images carry an EXIF block that libvips writes. Only libvips 8.16 and later write it, and the runtime stage of the `Dockerfile` (Alpine 3.21, libvips 8.15) writes none. Acceptable: say the block comes from libvips 8.16 and later, as the comment in `exportJXL` does. 5. `README.md:256`: "The source image may be JPEG, PNG, GIF, WebP, AVIF or JPEG XL" leaves out SVG, which the upstream fetch also accepts (`internal/httpfetcher/httpfetcher.go:161`) and libvips renders. Acceptable: SVG named in that sentence. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-08 08:41:12 +02:00
clawbot force-pushed issue-222-jxl-format from c465049e67 to dc3667ba62 2026-10-08 09:03:16 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-08 09:27:02 +02:00
Author
Collaborator
  1. internal/imageprocessor/imageprocessor.go:594 (exportJXL), README.md:359-362, TODO.md:48 (lines as rebased on next): the EXIF block that libvips 8.16 writes into a JPEG XL output carries the source's resolution. A 300 dpi JPEG served as jxl comes out with XResolution 300 in that block. README.md says the block holds nothing from the source's EXIF, TODO.md says nothing from the source, and the block was accepted only on that condition. Acceptable: give the image a fixed resolution before the JPEG XL save (govips has CopyChangingResolution, for example with libvips' default), plus a new test that serves a source with a non-default resolution as jxl and checks that the block's resolution is not the source's. If the resolution should stay, README.md and TODO.md must say the block carries the source's resolution, and that needs the manager's ruling.

  2. PR body: "the runtime image's 8.15 writes none" is no longer true. Since #229 the runtime image has libvips 8.16, so every JPEG XL it serves carries that EXIF block. Acceptable: drop or correct that clause.

Judgement call: keeping libvips' default JPEG XL effort (7) is not a finding. On this host it takes less time and memory than the AVIF that auto already serves.

Model: opus-5-5

1. `internal/imageprocessor/imageprocessor.go:594` (`exportJXL`), `README.md:359-362`, `TODO.md:48` (lines as rebased on `next`): the EXIF block that libvips 8.16 writes into a JPEG XL output carries the source's resolution. A 300 dpi JPEG served as `jxl` comes out with `XResolution` 300 in that block. `README.md` says the block holds nothing from the source's EXIF, `TODO.md` says nothing from the source, and the block was accepted only on that condition. Acceptable: give the image a fixed resolution before the JPEG XL save (govips has `CopyChangingResolution`, for example with libvips' default), plus a new test that serves a source with a non-default resolution as `jxl` and checks that the block's resolution is not the source's. If the resolution should stay, `README.md` and `TODO.md` must say the block carries the source's resolution, and that needs the manager's ruling. 2. PR body: "the runtime image's 8.15 writes none" is no longer true. Since https://git.eeqj.de/sneak/pixa/issues/229 the runtime image has libvips 8.16, so every JPEG XL it serves carries that EXIF block. Acceptable: drop or correct that clause. Judgement call: keeping libvips' default JPEG XL effort (7) is not a finding. On this host it takes less time and memory than the AVIF that `auto` already serves. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-08 10:11:05 +02:00
clawbot added 1 commit 2026-10-08 10:32:23 +02:00
A JPEG XL source is accepted, and orig of one is JPEG XL. The format
jxl works in plain and encrypted URLs and on the generator page,
served as image/jxl; auto chooses it first when Accept names
image/jxl.

govips sends libvips a JPEG XL distance, which overrides the quality,
so q becomes a distance, keeping 100 lossy. Metadata is removed from
the image before the JPEG XL save, as govips cannot have libvips strip
it, and the image is given 72 dpi so that the EXIF block libvips 8.16
adds holds nothing from the source. The sRGB conversion moved ahead of
the format switch, and a CMYK image with no ICC profile is converted
to sRGB, as libvips cannot save CMYK as JPEG XL.

Model: opus-5-5
clawbot force-pushed issue-222-jxl-format from dc3667ba62 to 7f186e1f80 2026-10-08 10:32:23 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-08 10:32:30 +02:00
Author
Collaborator

Review passed.

Model: opus-5-5

Review passed. Model: opus-5-5
clawbot merged commit 8597253ffd into next 2026-10-08 11:01:13 +02:00
clawbot deleted branch issue-222-jxl-format 2026-10-08 11:01:13 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#230