collective

Browse / github.com/storytold/photocraft-corpus / patch

jxl/: JPEG XL oracles for PhotoCraft's JPEG XL decoder

jxl/: JPEG XL oracles for PhotoCraft's JPEG XL decoder Adds `jxl/`: the JPEG XL oracles that PhotoCraft's JPEG XL decoder (storytold/photocraft#2868) tests against, moved here from that PR's `crates/codecs/tests/fixtures/jxl/` for the same reason the XCF set moved in #6: the app repository commits no test fixtures. **What is in it** - 12 tiny JPEG XL files (12×9) encoded by libjxl's `cjxl` 0.7 plus the source JPEG two of them were made from: 13 files, 7 KB. Lossless 8/16/10-bit and float, RGBA and gray, a VarDCT lossy file, a Display P3 enum colour encoding, a JPEG re-encoded with Exif Orientation 6 and XMP boxes, a losslessly transcoded JPEG wi

Status
open
Score
0.00
Adopters
0 other operators
Author
diegochagas (imported from pull request #7, open)
Base
0d3343b04edda369e277572d4c65ea97bba25b1f
Size
+273 −15 in 18 files
Review
pending
Shared
2026-10-28
Id
a5bbe67b3e977785661097ef

How it went

No other operator has reported using it yet.

Diff

raw · AGENTS.md NOTICE README.md SHA256SUMS jxl/animated.jxl jxl/exif-xmp-orient6.jxl jxl/exif-xmp-orient6.src.jpg jxl/gray8-lossless.jxl jxl/graya16-lossless.jxl jxl/jpeg-recon.jxl jxl/rgb-f32-lossless.jxl jxl/rgb10-lossless.jxl jxl/rgb16-lossless.jxl jxl/rgb8-lossless.jxl jxl/rgb8-lossy.jxl jxl/rgb8-p3.jxl jxl/rgba8-lossless.jxl tools/jxl-fixtures/jxl_fixtures.py

diff --git a/AGENTS.md b/AGENTS.md--- a/AGENTS.md+++ b/AGENTS.md@@ -16,7 +16,7 @@ | What | Where | |---|---| | Authoring clone | `photocraft-corpus/` next to `photocraft/`, a normal clone of `git@github.com:storytold/photocraft-corpus.git`. Work, commit and push here. |-| Consuming copy | `photocraft/corpus/photoshop/` and `photocraft/corpus/gimp/`: gitignored plain directories filled by `cargo xtask corpus --photoshop` and `--gimp`. Never edit them. |+| Consuming copy | `photocraft/corpus/photoshop/`, `photocraft/corpus/gimp/` and `photocraft/corpus/jxl/`: gitignored plain directories filled by `cargo xtask corpus --photoshop`, `--gimp` and `--jxl`. Never edit them. | | Local mode | `cargo xtask corpus --photoshop --local` (or `cargo xtask test-corpus --local`) copies from the authoring clone (or `PHOTOCRAFT_CORPUS_REPO=<path>`) and warns if HEAD differs from the pin. |  It is not a submodule or a subtree: a subtree would put the binaries back into PhotoCraft's@@ -37,7 +37,7 @@ history, and submodules cause init and detached-HEAD friction for contributors. ## 4. Adding or regenerating files, step by step  1. **Edit the generator** (`tools/photoshop-oracles/generate.jsx` for `photoshop/`,-   `tools/gimp-xcf/xcf_fixtures.py` for `gimp/`): add a case, give the file a name that states+   `tools/gimp-xcf/xcf_fixtures.py` for `gimp/`, `tools/jxl-fixtures/jxl_fixtures.py` for `jxl/`): add a case, give the file a name that states    the feature and its parameters, and keep the canvas small. 2. **Prerequisites:**    - a Mac with Adobe Photoshop installed;@@ -50,15 +50,18 @@ history, and submodules cause init and detached-HEAD friction for contributors.     For `gimp/`: GIMP 3.2 (the Flatpak `org.gimp.GIMP`, or `GIMP="<command>"` for another build);    it runs headless and touches no preference.++   For `jxl/`: libjxl's `cjxl` 0.7 or newer on `PATH` and Python with Pillow. 3. **Run** `tools/photoshop-oracles/generate.sh '<regex>'` for the cases you touched (or no    argument for everything). Every line must read `ok` or `skip`. Look at the output.    For `gimp/`, `tools/gimp-xcf/generate.sh` regenerates the whole set (it prints one `wrote`-   line per file and `done`).+   line per file and `done`). For `jxl/`, `python3 tools/jxl-fixtures/jxl_fixtures.py` does (one+   line per file with its size). 4. **Test locally against PhotoCraft:** in the photocraft checkout, run    `cargo xtask test-corpus --local -- --nocapture`. It copies `photoshop/` from this clone and    runs every corpus test (cargo feature `corpus`) with per-file tables. 5. **Always update `SHA256SUMS`** in the same commit:-   `find photoshop gimp -type f \( -name '*.psd' -o -name '*.xcf' -o -name '*.png' \) | LC_ALL=C sort | xargs shasum -a 256 > SHA256SUMS`+   `find photoshop gimp jxl -type f \( -name '*.psd' -o -name '*.xcf' -o -name '*.png' -o -name '*.jxl' -o -name '*.jpg' \) | LC_ALL=C sort | xargs shasum -a 256 > SHA256SUMS`    (add any new source folder and extension to the `find`). 6. **Make one commit per batch**, with a clear message (what changed, which Photoshop version),    then push to `main`.diff --git a/NOTICE b/NOTICE--- a/NOTICE+++ b/NOTICE@@ -10,8 +10,8 @@ docs/brand/LICENSE-brand.txt. Third-party material  None. Every test file here was generated from scratch by the PhotoCraft contributors with the-scripts in tools/ (Adobe Photoshop and GIMP used as the authoring tools; none of their code,-presets, patterns, contours, profiles or other assets are included). Fonts are referenced by name+scripts in tools/ (Adobe Photoshop, GIMP and libjxl's cjxl used as the authoring tools; none of+their code, presets, patterns, contours, profiles or other assets are included). Fonts are referenced by name only and are not included.  Adobe and Photoshop are trademarks or registered trademarks of Adobe Inc. in the United Statesdiff --git a/README.md b/README.md--- a/README.md+++ b/README.md@@ -47,7 +47,7 @@ **Contents:** [What this is](#what-this-is) · [Using this from PhotoCraft](#using-this-from-photocraft) · [Local layout](#local-layout-authoring-clone-consuming-copy-local-mode) · [Layout](#layout) · [Fetching](#fetching) · [Verifying](#verifying) · [Adding or regenerating files](#adding-or-regenerating-files) · [Pinning](#pinning-workflow) ·-[Size guidance](#size-guidance) · [`photoshop/`](#photoshop-photoshop-oracle-psds) · [`gimp/`](#gimp-gimp-xcf-oracles) ·+[Size guidance](#size-guidance) · [`photoshop/`](#photoshop-photoshop-oracle-psds) · [`gimp/`](#gimp-gimp-xcf-oracles) · [`jxl/`](#jxl-jpeg-xl-oracles) · [Provenance](#provenance) · [The Crafting Apps](#the-crafting-apps) · [License and credits](#license-and-credits)  ## What this is@@ -69,6 +69,7 @@ PhotoCraft is the primary consumer. From a PhotoCraft checkout: ```sh cargo xtask corpus --photoshop   # this repo's photoshop/ at the pinned commit -> corpus/photoshop cargo xtask corpus --gimp        # this repo's gimp/ at its pinned commit -> corpus/gimp+cargo xtask corpus --jxl         # this repo's jxl/ at its pinned commit -> corpus/jxl cargo xtask corpus --all         # every corpus: this repo's sets plus psd-tools, ag-psd and PngSuite cargo xtask corpus               # list where each corpus lives and which commit is pinned cargo xtask test-corpus          # fetch everything, then run every corpus test@@ -80,7 +81,8 @@ cargo xtask test-corpus          # fetch everything, then run every corpus test   [`xtask/src/corpus_pins.rs`](https://github.com/storytold/photocraft/blob/main/xtask/src/corpus_pins.rs) (every   corpus pin is in that one file). The manifest the download is checked against is   `xtask/photoshop-corpus.sha256`. `gimp/` has its own pin (`PHOTOCRAFT_CORPUS_GIMP_COMMIT`) and manifest-  (`xtask/gimp-corpus.sha256`), fetched by `cargo xtask corpus --gimp`, so the two sets move independently.+  (`xtask/gimp-corpus.sha256`), fetched by `cargo xtask corpus --gimp`, and `jxl/` likewise+  (`PHOTOCRAFT_CORPUS_JXL_COMMIT`, `xtask/jxl-corpus.sha256`, `--jxl`), so the sets move independently. - **Bumping the pin:** in a PhotoCraft PR, set `PHOTOCRAFT_CORPUS_COMMIT` to the new commit here, then run   `cargo xtask corpus --photoshop --update-manifest` and commit the manifest diff. Re-run the   tests below and raise their floors if more files pass (see [Pinning](#pinning-workflow)).@@ -120,6 +122,7 @@ There is one top-level folder per source, i.e. per application that authored the |---|---|---:| | [`photoshop/`](#photoshop-photoshop-oracle-psds) | Adobe Photoshop 2026 (27.10), driven by [`tools/photoshop-oracles/`](tools/photoshop-oracles) | 258 | | [`gimp/`](#gimp-gimp-xcf-oracles) | GIMP 3.2.6, driven by [`tools/gimp-xcf/`](tools/gimp-xcf) | 28 |+| [`jxl/`](#jxl-jpeg-xl-oracles) | libjxl's `cjxl` 0.7, driven by [`tools/jxl-fixtures/`](tools/jxl-fixtures) | 13 |  The repository also contains: @@ -173,7 +176,7 @@ change behind a pin.    `cargo xtask test-corpus --local`, which copies from this clone and runs the corpus tests (see    [Local layout](#local-layout-authoring-clone-consuming-copy-local-mode)). 4. **Refresh the manifest**, covering every folder:-   `find photoshop gimp -type f \( -name '*.psd' -o -name '*.xcf' -o -name '*.png' \) | LC_ALL=C sort | xargs shasum -a 256 > SHA256SUMS`+   `find photoshop gimp jxl -type f \( -name '*.psd' -o -name '*.xcf' -o -name '*.png' -o -name '*.jxl' -o -name '*.jpg' \) | LC_ALL=C sort | xargs shasum -a 256 > SHA256SUMS` 5. **Make one commit per batch**, with a message that names what changed and the application    version. @@ -277,7 +280,7 @@ above must be installed. ```sh tools/photoshop-oracles/generate.sh               # everything (a few minutes) tools/photoshop-oracles/generate.sh '^text/'      # or a subset, by regular expression on the path-find photoshop gimp -type f \( -name '*.psd' -o -name '*.xcf' -o -name '*.png' \) | LC_ALL=C sort | xargs shasum -a 256 > SHA256SUMS+find photoshop gimp jxl -type f \( -name '*.psd' -o -name '*.xcf' -o -name '*.png' -o -name '*.jxl' -o -name '*.jpg' \) | LC_ALL=C sort | xargs shasum -a 256 > SHA256SUMS ```  The script prints one line per file:@@ -328,12 +331,44 @@ match PhotoCraft's composite closely; the default ones measure the known gap bet - GIMP stamps a modification time into the PNGs, so regenerated files differ byte for byte.   Regenerate only when the set itself changes, then follow the [pinning workflow](#pinning-workflow). +## `jxl/`: JPEG XL oracles++12 tiny JPEG XL files (12×9) encoded by **libjxl's `cjxl` 0.7** through the Python generator in+[`tools/jxl-fixtures/`](tools/jxl-fixtures), plus the source JPEG two of them were made from: 13+files, 7 KB. PhotoCraft's JPEG XL decoder checks lossless files sample for sample against the+procedural formulas the generator drew with, lossy ones by PSNR, and the metadata, orientation,+JPEG reconstruction and animation cases against what was encoded.++| File | What it exercises |+|---|---|+| `rgb8-lossless`, `rgba8-lossless`, `gray8-lossless` | 8-bit RGB, RGBA (alpha from fully transparent to opaque) and gray, bare codestreams, Modular lossless |+| `graya16-lossless`, `rgb16-lossless` | 16-bit gray + alpha and RGB |+| `rgb10-lossless` | 10-bit samples (decode to 16-bit) |+| `rgb-f32-lossless` | 32-bit float RGB with values above 1.0 (from PFM) |+| `rgb8-lossy` | VarDCT at distance 1 (XYB) |+| `rgb8-p3` | lossless, tagged Display P3 as an enum colour encoding (no ICC) |+| `exif-xmp-orient6` | a JPEG with Exif Orientation 6 re-encoded losslessly, wrapped in a container with Exif and XMP boxes |+| `jpeg-recon` | the same JPEG transcoded losslessly with JPEG reconstruction data |+| `animated` | a three-frame animation |+| `exif-xmp-orient6.src.jpg` | the source JPEG of the two files above |++### How it was generated++- `tools/jxl-fixtures/jxl_fixtures.py` writes the source images itself (a minimal PNG writer, a+  PFM, a Pillow JPEG and GIF) and runs `cjxl` on them; it needs `cjxl` 0.7 or newer on `PATH` and+  Pillow. Every pixel is procedural; the formulas are mirrored by PhotoCraft's test.+- The Exif block and the XMP packet are written by the script: Orientation 6 and the text+  "PhotoCraft JPEG XL fixture", nothing else. No camera, no personal data, no ICC profile.+- cjxl's output is deterministic for a given version, so regenerating with another libjxl may+  change bytes; regenerate only when the set itself changes, then follow the+  [pinning workflow](#pinning-workflow).+ ## Provenance  Everything here is original work by the PhotoCraft contributors. The files are generated from-scratch by the scripts in `tools/`, with Adobe Photoshop and GIMP used only as the authoring-tools. Their output was observed clean-room: none of their code, presets, patterns, styles,-contours, ICC profiles or other assets are included. Fonts are referenced by name only. The files+scratch by the scripts in `tools/`, with Adobe Photoshop, GIMP and libjxl's `cjxl` used only as+the authoring tools. Their output was observed clean-room: none of their code, presets, patterns,+styles, contours, ICC profiles or other assets are included. Fonts are referenced by name only. The files contain no personal or private images and no copyrighted material we don't own.  ## The Crafting Apps@@ -385,8 +420,8 @@ PhotoCraft Corpus is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICE Copyright (c) 2026 ArtCraft Team and the PhotoCraft contributors. Required notices are in [NOTICE](NOTICE).  This repository bundles no third-party assets. Every test file was generated from scratch by the-scripts in [`tools/`](tools/), with Adobe Photoshop and GIMP as the authoring tools (see-[Provenance](#provenance)). Fonts are referenced by name only.+scripts in [`tools/`](tools/), with Adobe Photoshop, GIMP and libjxl's `cjxl` as the authoring+tools (see [Provenance](#provenance)). Fonts are referenced by name only.  The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the ArtCraft Team and are not covered by this license. They may be used only unmodified, and only asdiff --git a/SHA256SUMS b/SHA256SUMS--- a/SHA256SUMS+++ b/SHA256SUMS@@ -26,6 +26,19 @@ d932f76b5feaf4f229157a3da75c7e5c85602b3b6b890a3396acc87c2ae4f31c  gimp/mask-perc 819633fb66710f02e575ae2bea79220717537287fe59bef52542b187cd3b9eca  gimp/text-perceptual.xcf 21702736824b70ca50bc9c53f6fa182cc9a5f173f6cc0e1c53fca5a454ec882e  gimp/text.png a6928228a237a1c611de0296ea42bfd713e40e069654a9d4ae57c89222fa1d2b  gimp/text.xcf+05a1b5a6144deb5d90bd2ab211eec19a9e1176a50cb08c2a44f3f5e9b9a2bbfd  jxl/animated.jxl+c773ece848d968910aaeaa30da6340a7beaa73a296f6c1095d58f493629b4bf1  jxl/exif-xmp-orient6.jxl+e594e80b6ea3034d273f5403ae3fd42b9949d0c5c34b3055e55ea94a994efdee  jxl/exif-xmp-orient6.src.jpg+b4fd5502866c31ed09a6914ea2c4ffb329bb5596f03428f974e4c3edebcf9466  jxl/gray8-lossless.jxl+8177b04e9d071d3402fe2467772e45dcf73d5f3bbafc1dd2379a0f325260a1db  jxl/graya16-lossless.jxl+e74c2a2b3c9b243c1ec95de6b6cc17eeee116e5368e50e4709baf68e2d6a7714  jxl/jpeg-recon.jxl+6ea7666e8d3d0a142655c612213ae02173fa312e9c84983865301c76ffd59037  jxl/rgb-f32-lossless.jxl+2d95e32b15768d170c90ab7e4f81e4b4c92c88776705cb4ea48e0af17c23d398  jxl/rgb10-lossless.jxl+d89cb267a9775ce6ef4cacc9b2b6354f0a0054fe84703d6f18e88f7e9b599950  jxl/rgb16-lossless.jxl+201467b584e3db669c724b942507936b82a8efb8b72953d0323942e30713174b  jxl/rgb8-lossless.jxl+ca289851a1c4adb76be2340ddf03f1ff166df5cb441c3599a6097a72605e3fb3  jxl/rgb8-lossy.jxl+0bf9cc59503b265caa1e386eef2a76a29a9f27739180f0c2c82acaf0e12fbb8b  jxl/rgb8-p3.jxl+ab5638f2e45d6c78fc6542bc6b70472bf959630f84d6985f89a1d43c59c7d81b  jxl/rgba8-lossless.jxl a72ac9d56578780866972ee700b44981ed1b68c250c511ec1c823d476a2e394d  photoshop/adjustments/cmyk16/baseline-layer-copy.psd 8673c9f5cfc9cbd3927d49e2366f3c46d1073e6add9f4843560a138cb252afdf  photoshop/adjustments/cmyk16/brightness-contrast-legacy.psd a0bf926902c1e1f2836fc17a303cd3d6abd1d400a4ac22de9a914adcf2a282c6  photoshop/adjustments/cmyk16/brightness-contrast.psddiff --git a/jxl/animated.jxl b/jxl/animated.jxlnew file mode 100644index 0000000000000000000000000000000000000000..4b929f6eb8dc9ab910f1104c45d618a245def531GIT binary patchliteral 697zcmV;q0!ID+3P3PH02UON1PBPA0ssJ{0!tFbf(24w1K2Ai3a6Bm8Do_(Smps(+7JQQzaX)e2?*rN0$PqvQZn$7@uLw?AIhYYyncO&JWG4p>$u9GXbeVVE6<C!AfGQ6F@C(2TzZ0|hpJo`M;5Eo%I2%$lGw&0j=!QhTge6|BU0uC?x*WZ6Mc{I^4fusbC12e2K49Hf{zyvagB`M8Hw1(d|{4vPx<*)Em-t^=^x0ts}q&G;wzC-`wucwq<#paK8@umejH#exM=zV1q@9rJ|{;+EP`Lk$IUh#ybElvmOE36^+l~C{^kM{O1Ulb(iF5Yu1_@^~w1K6rB0>z&Cuf1pg81-m~PxHk@z;!Jp7FclQOdoMOdPQOaw_E>HkFhqu+b}0~q`Q05C#a*bfA#z(CBdIaKuw``RQYVtrtqNX$?q0I2c9cnyM)yPc1`VW){d_g#cLS#AL|%dK}Fs;HBH)zhOmUT+04G3m#nie;jh4fptYR9tvQCr@-LEN6suc+5Nidjf0pj~9SL-yi0$EBh&YlMzjM?82d)f{pj{kz(dOf=FeaVUemI??6ph5ru2n0(K#exM=V1wl>P01vswqkY4WHK{mzyaNzBPXIvCO}l!^s9VC_t=+8R9vjRavS#$NYF1S{Bq7?J6g&;Iyz)tsFQ3%$U5|Vbz%~OcYz}9*u*7oeirZ*GPfzoaTzk`ms-2b!h0LBP$k-ih)><d6DKsY$)6#h7et2+Z@zaPc?P{sv_J!axEURmoE**i(+Y%Nbj!QoZ^|s}B4V!jsI|Cq4k*@o7BJ+}8>;K3-n5zA>%>*5}>qX!3`Y`TTb6gh4h=|V4?7W^EamX#aKcwEn%*+ZK_^&(qcB9FJaqG-2SW0ftKYI>^7bNlmYl=)&zLp6s4X!ZE&xzL!NE2J&htw@literal 0HcmV?d00001diff --git a/jxl/exif-xmp-orient6.jxl b/jxl/exif-xmp-orient6.jxlnew file mode 100644index 0000000000000000000000000000000000000000..9048deb2486f917b69e7f19ab918b4aa3c6efe1bGIT binary patchliteral 803zcmZQzVBqnJ@KNC9YUg5LU=T?wsVvB<$WZ{Y8KAVUYei-ni0|vmparBj7?>FO7?^-8zX&_c(6k=co(u@pjKoST7GV)9Eor@CFN))^TT-_CbhNop#l#~{w0wo#GSLEg>*w|MTzBqnF4mMA2prf25aD!t#mUr8Y|#a1cY)Yrhbz&SM|)1#^=HMq(zB)KX(*)m1R-p;1Nzsv@@_H?<^D0cb{Eu~mhwQesMeQmPe@4HDK@va=~lNwW%aaf7M=@@$ndN=gc>^!3Zjz%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm7xZ>q!uR^WfqiV=I1GZ3`|VQzFD<cEQi7VAk_<PrptL9lY-CEZerir?ZfaghvA&_6p*~D)NoE?t33<hOU<d0Z=jZB!zWPnNyfJ#A3EB~TQpzcJFrM60DK$oN>Ta{#%<fKA8<(N|fVdP|joMDxdn3ry=RH2)aznwD6aQ=(*tJJ@aXL7vFTw1Y6fCg~$g2g@RP8LA8zia-`T%<T|ymYN5QsB&P80ORvHzFpVVt=W;OMW)Ydr!okGI;K1xHbhL>zNI}{`@w7={=urt*&A1-JdyE<Xd>JIZUyc8*z+J4)QY2ne`LaSpK-4>i)p7VOo*^>t*-dOOwpGQ{QPB4RQ%WtjCo0(I&-msjL%eKBPz-})u=#O?5p5w{FBZ(#hVGGY0{BfF2BYixhLFOl))b&bhffhDW_vZTurI>H%^Unfq`zNHSjA&1Cw9MS}TQ;V+Z>Zw`KUH~Sv<{Q2>@GQWPY9gm$n^UJcS$w~1(Y^}}5#Lby*hst4|AxO$i^Y_9bZq1eaGsj)jU`kEd6g?S`m7y*Gw7jXapliteral 0HcmV?d00001diff --git a/jxl/exif-xmp-orient6.src.jpg b/jxl/exif-xmp-orient6.src.jpgnew file mode 100644index 0000000000000000000000000000000000000000..5202cba57078512f663b19913fc3cfaeb58281a7GIT binary patchliteral 1267zcmex=<Nrg(XBj0W1y=g{dBu8(DfvmMddc~@`W1-<`i6Q2`V2Pq6$OdO*{LN8NvY|XzdA3ULckfqH$V{<S3ODsN@GWpo&B*kqDoPEm@(W3>%1*XSQL?wQsj#ZZEyztRNmQuFz&B-gas<2f8n{5STgM{^!>}-ls(yW49+@LCeJX<BGOUld3^~#O)@{7{-4J|D#^$m>lzjf`}GDs+o0^GXscbn}Xpp$5357AF^F7L;V>=P7^;OiaozEwNQnf|{C=3^%l(v?vE`zWJ<DrYEEiyYF<e((2IupFtsI_X$U8v1O`Y392!arAf}anQ6^A#BFIu(B{QH)Qj)DozGD~t&A)a!~DS<F@GC|I;%1O*iw^gdpO-W5lEX^rVvJ1$_FUfZ<N=z$J@CtBsSBUUYzNXx7!DJ@F1(Fb`VC({nX0Gp(bFdZz5<YlNbU?>7v@G!SS#93+{FrvzVG4lTbgMgQtzryBz!BLf2?5HkFK$l&Q(k(tK8;Oonv#lXP8!NA1G$G`++NdvJOqYwi#kY;3H1Cl_1z+tCdFZ!<UpHG=?%WC8&eW)R8B!otkL#>&P9#_XINZ0wxuY-}9d9GqMr0A%rSbMb&UzAVVNpnOK>bS-IHR*uhFjBK{v>5aeLsWZ+?DRAOKfWMmd({C|W&9_S5Lu=9Zah5$w;zW)@a9cAzu3feN+?0G-dw40JRr(8WO2wLp0W7C}}aMMFn6;lM<8r9u&-#)%6#l$|ynz6b-ugLB%+!sELzHOk6@zN>xo=LsQGd)Xdz%(#qMz)y>_*(<?Y6G%P$KGAbDu>**PpzS;Zx#W#tu>Rn0A}ZS5VMU6UqHnL2IyjG40*Enc#8+42=DS8dw7W$U)>J9h3mboj{8zW5-XNJay^vm8;jT-?(|};iJb-o<4j2;^nK4pFV&2`tAFVpT9u<Vq{>3cne5k^_L*fzUreAlU<dh&k*OSrnFU!`6%E;h90S=C3x$=88aYIqCNA7~kW<+>=!0ld(M2vX6_bamzA3<IN`;0h`HId~rxW^Fwx&?|oVB|3iGT1YG{Lj!}|G{fs!heRY;>$}yzus@Qe=ybezZ=?91;QF83FXO%co&V49>F|g3>up5;GbnKETmCEjiB0+AaPc+q$FJ|UkzX|Zl6=4YO20QyryY?&pzX<^9{hf^fliteral 0HcmV?d00001diff --git a/jxl/gray8-lossless.jxl b/jxl/gray8-lossless.jxlnew file mode 100644index 0000000000000000000000000000000000000000..bdf3b68f094cad646bcb6203355a166a46c311e1GIT binary patchliteral 151zcmV;I0BHaJ3P3Or6c+*r0s#Od0!tFbf(24wLk!*;W28z8p>4s|)anEPtaV#6KL>UszGue9;$(PC3ILs*vy<LMx3i}L{BcJm(%#YW1qH@VVrZ|f(!6hg*3BJ+g2FhXnvZfsdzsq+w0Mn@P^am8adp>d$l{DTb1Q}Z&SmfMi*{reYT)jwY+y?E)p*=Z0s%HyE><pMG1Fi2}UJL(u>Lliteral 0HcmV?d00001diff --git a/jxl/graya16-lossless.jxl b/jxl/graya16-lossless.jxlnew file mode 100644index 0000000000000000000000000000000000000000..2e58507f5978e4bad1e0d07a3268dfa127c516feGIT binary patchliteral 214zcmZQzVBqnJ@KNC9YUg5LU=T?wsVvB<$WZ{Y8K5*Lkj?=~t_3oZ|8qGQ{L$#*X5`=yzV2EJy7CPF*8kEra_~^0jUEPLpOP**ycJ&ffJtkqh-<gr&|Nm<#zy6!g^ii8pKlx;ezmyl7R&X#D=Jf(ZHQ@RzbCca)^r69C4c~|d+D<U6$6-LB}FkBY6>?*^i;G8GP@TfPTzH7wyqU&@EgCl=<r2Z~*N5Urybx+L0kUbJc7MF}?pHx)5O2c^g_Yg&0SuP3=oYD+n@InPCkh02w<_K>z>%literal 0HcmV?d00001diff --git a/jxl/jpeg-recon.jxl b/jxl/jpeg-recon.jxlnew file mode 100644index 0000000000000000000000000000000000000000..9e4305eb1e5e95f2edf61b8b134b88dcfaf6b7b9GIT binary patchliteral 1166zcmV;91abQS000b1SWF-d3Wo{+000zbba`-Ucx)g50001Lcx)g5000wecx-S000030z3P3Om!~_5U0q1IBa%4g<+%|w92tvzj9smS@k^n-D0wGtG004lHqp}4<asE#b86%W&zPAO<VLWVF)9b}k+(j98BK4dugm;r|zR9NJoVF$L48v6G~mO1RW0}Vy@zZm0;NiNyszlTl82W}0iZ`DUDR7J6u+i#Gabq?49<YO1TY`f9AR7JF>6%QpLLw9}S*Zo2EX`)<7Rz7JP8R3pf05#1oHP^2sT$-15sY&pdR|M<=~>(@#e|_1I;fo%Y&oza977gBN~y;)^%_zc;u6hUi#^&uipCWvCkg-@WmgW{PN8|AN};<k6-@z>961Z`|;2JU%^yW2>^hQqp}4<zasFQr38jp(PAL-O0U?`FgA9}EKtuIl!?}kXGoT<JG~}Se4s0y4%u&DFhaESxqCfJ?z;FApRe=)`xmu&LMD5p$w%{JeRbIw8!O?1&lAB}X<Qcq2F)mC4Pb=G2!O?KI4pN)3fza?ee7-FDxNciw>uJ~-ip8-6(AiAOH^<dj!#`Q?~r9=hnGlU};%r=y;F?6S{Jd+oO0zj(hIG3qL&Z#T$P-^2tXp{q)pVZ~gVyXCJ=!<C9;$`RAjbe*E&!Pk;UP-;aO({{lY%zP+0%~3kZPV01!$>Nk#wx0RR92009610{{R3Q(|&&VnujqW*bBR0K}kq_j6)(VySXmz1?vnBIdF(B*rG5X4K~KELD*OUT-OM;0w^Ma3Yv9@3c4ZSpbk94b69V?=;GAwD|<cuz`#Bc?0003SVsdX{cx`MT8^!?u<Z3RJz1i16x;$|KZ2(Z)l9U}hhdcnSH)U&R7trx0z1H0YaRg#aV;cVOGBPF(Oj#fHei$GHY{=fco{{JD|=v%q*!ss~H-phP{b>_rTTrjadzs$*7O>li;U)fo*SIj>gOMWX=gwd<?T+x@7lc6|f3ei-Sc(+7ZNDV8N@L^I{BH4=HVz02zBlKr&>OX8<4@u9K6ptJC7U4&{ZMVn~J>|7as$4JU6%rBUo)?~7tc`VRn_zOO#izk(aleY#Sf~)?G*SDbmYbrENn_BY46ueIzI;Vyb`L)Ry0(^W*M-KCj4+#<r_Hoti7*zgwRgZ3U`#*L|S;>?y*k!{e<II_j1>O0Za@4009PScx-Th0005tTLKUO%mWBu2w(_cz{{IdD02lxW009610001hfF+3{<Jbvl?pOf;Mx>yiD-aRG=;-JegQa3nV~kQ%NQl8oz(b3Tm6ZXgi@WCH6A>bFj_dv@A0i+CoKsW)!SgzawjNbtOh{6C+cK`qY000000000Izgd|aXFjg(vGy}GrNQ!`@01Ai|01Se#51=ZB0Pw&8KmrE<002e+000000000000000z04Suuj<&q0$X<+$F<uN3kBsqn7h_~Zcn<(B{c4L5)o*61KiUMfRyREblhDY3#L@rHgiccx)xmgWSbE&E*Z0+4WB$-2eh6^jqEWj%qF@M|&Jpcdzliteral 0HcmV?d00001diff --git a/jxl/rgb-f32-lossless.jxl b/jxl/rgb-f32-lossless.jxlnew file mode 100644index 0000000000000000000000000000000000000000..735d87ed0862b9e1fc70c43efe208186d1cb2ec3GIT binary patchliteral 911zcmZQzVBqnJ@KNC9YUg5LU=T?wsVvB<$WZ{Y8K5*Lkj?>0GN%C<$^W?=42l?zwsJ5rzGC1&h3mt7@4Z3hn|JVb*BN8?-JjW)Mt5oVPS!8=UFtA))-$*X`L)}C_#ykHnDw{JfzEMQ`&U|=|K-40~@fhX1=3JgA!ut6ABzjOJ4EP<xY&*~%@84U6alY#uJX~!(1bW^U^zOE7_y$%Jhe^kiVTaTThGfuZ4#%BP+`5F;2SZ|6(|Gj~cd9B}x4WadocPmEAGhFg0~zwmrVXC9z_Uc78<Rrzsu~i396-L5vkYo^KY9uhs2+VUhNP9U_&N3!#DJjg?iaQ}Vk+zz4U?BgTYt06M)<|$JQ8GzF^MyzS>6S6>CU(d3pPEajB(iJ~5}S+%Tmzc-~Yk>)pHczS#0+F5$9!S*vl_21q_DY|M(d$fZdnzn;B{>!+~yX2-86c6iQye@GtljlL++w88^-;zNBafrjyb>D<yyqZ&=8;#bZoO814GU|unvu>9h2{;f!*F*1XOO8p?xIqnQJN3WXYfQzBJjXqV34?YO)1=!VZQMB85zL~-5;yh@<A0sDCJuf$up82M7Lh%xDz;+1td7V8%f2Fz=u781IDoEL7jJd>)tmJVZ{B_Q@aagTEYPJ^>qB@K7&`YRl()BwpPm?h0_>=Tq6`hwzjqgZ*JhF-F?!%|jpXyX$<|TAItCeP8n8C<k#K6FJAMVTr>p3AxK4gJiz@RuwNuGgWzZZZQyhT{QlM{)K)Ngf{aFU4J%%YNeg@xTkp3=Daj85lecF)=J?U}AU>cyHSdE9>1|z+jj72O*wNr>MS@JN&EwuTEMyd+`D)0O!RLyT;eO0`{5HUWU2V9wzRD5yGKGr#rog_zbJIoVDHu)I-jSAGKAp3@eLBb|3ogG}5c%40V^x%!sPnsbA3pI`9#Vq1a>b=*2laB;rwA5yZ+3eYq;>iq7?jX4dFLv#4y6IaXq&_2J-5)2IxHm-KfjAxjS9le~literal 0HcmV?d00001diff --git a/jxl/rgb10-lossless.jxl b/jxl/rgb10-lossless.jxlnew file mode 100644index 0000000000000000000000000000000000000000..d999e5bf2944f30e288078d371ba1d227a1d9b85GIT binary patchliteral 489zcmV<F0T%xM3P3OvP+SNI0s#PQ2TKygf(24wmtqXgi!ok|7bD~GL{(KCjJgHx0CZ#=zGU3fE8Dg1Ph$tWfraxVlMu{-k5lrmq&Y*0_5n&kD0<p_<W>9{n`Pfj$vc$>eo^bW>zE4q0i{c|b2(ydy^zVROv2fbH6NkONEqS^J+0A0uK^b*eY)O~T(8HHjvSt0-&=&+fpzZAp`?YZQd25w`#X!61G>SCNUZkK)7)Y$4<Yf_4!E0pE?P<Adt5-2Zj{>%@Nh*ZI$^z;YOH3$X{;iKVwrG*VlOizzmsyAf5&E`~(2>&&(>=0(`<<1c0ia{qH^mV3XPYcm4<Pzf72&y1VFV<{zr`hfZDc({{Sri7k$1(0BC9Ye{CiJ|F(UC*8nX4$9=d+0KP|A7YQ`xzAFNM32|)ax+k*N9;8i^Zz?{|kuX_Yg&*lC{`rp|G_~#Q-1NdwIhb>_N{2NcvM0!eoz!B23}>+{!I!TzTSVE(5Ng1j;S`>MIiD6LlG`|bVASv{3jL1h&*>{kC(HlaB9AC!J`zIkOB}jRBpp^(ohAF#Wn3Og}@i^#zszoa3qy;GgEY31E8(Y^ZfmLarq!2*BDfx5O=gfZ-eZm|B0k$`QH5N?*h1WAwaw90JN8&RQ3%3u?h0yliteral 0HcmV?d00001diff --git a/jxl/rgb16-lossless.jxl b/jxl/rgb16-lossless.jxlnew file mode 100644index 0000000000000000000000000000000000000000..c4bf0b5b860b6265f7060707205ca64c0092bdc5GIT binary patchliteral 196zcmZQzVBqnJ@KNC9YUg5LU=T?wsVvB<$WZ{Y8K5*Lkj?=~&IU4)|8qGQ{9zCgXX0RDzWDsET7CPF*8k7*9%o8TwTzG}m^GaZkp;=syxc;#U_6J-4WisFYvj1d?k?;T6K5B33zC!bVt%8z<~_`v}#eaZEn9;%ZP81G(MvN|+-(Jy`HAVUvE_3yeXCYTw`eK$MngMiNDpZ#o%YL|Dwe^L*>(`jg4W6w=Rd??{7`O1pn++Wk7lMlMkk2>`2gMzH_@literal 0HcmV?d00001diff --git a/jxl/rgb8-lossless.jxl b/jxl/rgb8-lossless.jxlnew file mode 100644index 0000000000000000000000000000000000000000..8b5574f20331f2892fa98d4cc690d2e2f66e47aeGIT binary patchliteral 391zcmV;20eJrZ3P3OrP+SNI0s#Qz1xpgef(24whsBHWcz5w4<4H$}Z3_TxTOI)BR_p|vzcGoW}83C2#(!4RNl;PSgiap0}h%3EzP2cR4tkofXU)NeTg*rF-%24DM1if+1WmADrz=7`I9AWtwxNvf#RrP1~HUNK76l-PYZ8xzQam-JE4#erlg2T~0^z@;$lQm{TSir55Fz&EM4H4R1s9mqj2z|NZCw6@h+b=I00Kn%jM7_D*&H6mP}nAh<M-8YW-=-95Q<W{&vdzwX${m=iWfqi~=+P2!tvdoqqG<e@2=)${<doy(9vnr%@Dcy1oRkNI%>MCje8-Xzav_z0S(}Zd94$EQVl?fbzHX={ZauHTz;Pug!nriUpDasevAx2jiDWnU%ssSVZ`mx`J^7nzhlTPttkr)t(WQPKdK>RUi1}?;V5_-xG<;di?#B?*nDM*M7t_J_^|81gL;pm9k9Tojlg0KI2R*r-|!Solj=HnZpr@MYzzpoq6<x4>GGKPSInFzo&z`Oteliteral 0HcmV?d00001diff --git a/jxl/rgb8-lossy.jxl b/jxl/rgb8-lossy.jxlnew file mode 100644index 0000000000000000000000000000000000000000..663ba8d2531ab5063a0852befb4758c9661f6d5cGIT binary patchliteral 146zcmV;D0B!&O3P3QBP+SNA5(ohR3<6!{5&!@dDx-`WyqxR#@<WHkw2kWwZMAJ-NutO&zHc2ONND^xxcpIFfxFFPnn9n@acs;M~u~XEi;sFK{6;dD-u<e$Ljf^Zwk&!V*(#81*zObV4u$j0O5YCZ@Wx&j*(5sm_s{KxsB9GF2qV*R(LA(tov3oroMq+Xi0EBFJu1`(4zA<NyEwliteral 0HcmV?d00001diff --git a/jxl/rgb8-p3.jxl b/jxl/rgb8-p3.jxlnew file mode 100644index 0000000000000000000000000000000000000000..80e5467eb03c1363693b4cd9ee5a1e9f92425996GIT binary patchliteral 391zcmV;20eJrZ3P3Orkjae*0s#Qz1xpgef(24whsBHWcz5w4<4H$}Z3_TxTOI)BR_p|vzcGoW}83C2#(!4RNl;PSgiap0}h%3EzP2cR4tkofXU)NeTg*rF-%24DM1if+1WmADrz=7`I9AWtwxNvf#RrP1~HUNK76l-PYZ8xzQam-JE4#erlg2T~0^z@;$lQm{TSir55Fz&EM4H4R1s9mqj2z|NZCw6@h+b=I00Kn%jM7_D*&H6mP}nAh<M-8YW-=-95Q<W{&vdzwX${m=iWfqi~=+P2!tvdoqqG<e@2=)${<doy(9vnr%@Dcy1oRkNI%>MCje8-Xzav_z0S(}Zd94$EQVl?fbzHX={ZauHTz;Pug!nriUpDasevAx2jiDWnU%ssSVZ`mx`J^7nzhlTPttkr)t(WQPKdK>RUi1}?;V5_-xG<;di?#B?*nDM*M7t_J_^|81gL;pm9k9Tojlg0KI2R*r-|!Solj=HnZpr@MYzzpoq6<x4>GGKPSInFx$P!R`P6literal 0HcmV?d00001diff --git a/jxl/rgba8-lossless.jxl b/jxl/rgba8-lossless.jxlnew file mode 100644index 0000000000000000000000000000000000000000..1dfbe8cd0d0921a9318f7ef2360a58a9fc726906GIT binary patchliteral 453zcmV;$0XqKw3P3QhptukS2oM0=21^pff(24w2gFo7@-Aj%Jh7CnQf;`c$^+owN&w(8zjnn2UHYKC{S6-SQQj{_g%|#Jc)eQlkd9CSdn49%DqE9sD%W~zUK#oRVT}se`RyS_Ezaw^d59C29+<Ud9zL0e3^IOrbl$lS?-BD)WFF=bOB5q{+=3L1{c(9uAuv4^-<LtV;Uz9A-wAK#Dl3xuQsHuu*A#w0{KrpHJN1dx)?60A9OY3k|)=-heXb;vM@O#Fpk!!{pDuz8^xE-%n^UQe%Frw+!gSerPw4urTSs(-*0~W_DC~Fmd<Ikw*^3)OpL<4K1~1?F_Y=wz;LijYAs<ao;{iNxAN6N@!U6aKE9;}{)+#_9w(jK9bN^oA%W}8Ck7<>t>9*tX%azeLz?8BS5pY-I^vk-4{{^qmiuO{x*&qL2bX$djs*gkBfM}>yp&?L*4+i_xkXRP!&*y8-YzK11<p=>NHy@XFXv@HLL@%aN3?kpApULVV-1P1J9Vd+G<e!nu4|BJ{@)GbtHOlTgznvR5S@yKoBuO06_vlhGOk0TrT}fX*A`wqq#z=h`6xaBK80YF1b0ZJ_0rarPk)4literal 0HcmV?d00001diff --git a/tools/jxl-fixtures/jxl_fixtures.py b/tools/jxl-fixtures/jxl_fixtures.pynew file mode 100644--- /dev/null+++ b/tools/jxl-fixtures/jxl_fixtures.py@@ -0,0 +1,207 @@+#!/usr/bin/env python3+"""Writes `jxl/` with libjxl's `cjxl` (an independent encoder), from procedural images that+PhotoCraft's `crates/codecs/tests/jxl.rs` regenerates, so lossless files are compared sample for+sample and lossy ones by PSNR:++    rgb8-lossless.jxl        12x9 RGB 8-bit, bare codestream, Modular lossless+    rgba8-lossless.jxl       RGBA 8-bit, straight alpha with fully transparent and opaque pixels+    gray8-lossless.jxl       Gray 8-bit+    graya16-lossless.jxl     Gray 16-bit + alpha (stored 8-bit by cjxl 0.7: alpha values are multiples of 257)+    rgb16-lossless.jxl       RGB 16-bit+    rgb10-lossless.jxl       RGB 10-bit samples (decodes to 16-bit)+    rgb-f32-lossless.jxl     RGB 32-bit float with values above 1 (from PFM)+    rgb8-lossy.jxl           RGB 8-bit VarDCT, distance 1 (XYB)+    rgb8-p3.jxl              RGB 8-bit lossless tagged Display P3 (enum colour encoding, no ICC)+    exif-xmp-orient6.jxl     JPEG input with EXIF Orientation 6: the codestream carries orientation 6,+                             re-encoded losslessly, wrapped by this script in a container with the EXIF+                             (Orientation 6, ImageDescription) and XMP boxes a camera file would have+    jpeg-recon.jxl           the same JPEG transcoded losslessly (JPEG reconstruction data)+    animated.jxl             3-frame GIF animation++Everything is drawn from scratch here (procedural samples, a synthetic Exif block and XMP packet+whose only text is "PhotoCraft JPEG XL fixture", a three-frame procedural GIF); nothing is read+from disk. The source JPEG of the two JPEG-derived files is kept next to them.++Run with:  python3 tools/jxl-fixtures/jxl_fixtures.py     (needs cjxl 0.7+ on PATH and Pillow)+The pixel formulas are mirrored by `pattern()` in PhotoCraft's crates/codecs/tests/jxl.rs; change both.+"""++import struct+import subprocess+import sys+import tempfile+import zlib+from pathlib import Path++from PIL import Image++# The repository root is two levels up from this file.+OUT = Path(__file__).resolve().parent.parent.parent / "jxl"+W, H = 12, 9+++def pattern(x, y, c, maxval):+    """Integer sample: varied values, every channel different (mirrored in jxl.rs)."""+    return (x * 31 + y * 17 + c * 101 + 7) * 13 % (maxval + 1)+++def alpha(x, y, maxval):+    """Alpha gradient from fully transparent (top-left) to fully opaque (bottom-right)."""+    return min(maxval, (x + y) * maxval // (W + H - 4))+++def smooth(x, y, c):+    """Smooth 8-bit content for the lossy fixture (mirrored in jxl.rs)."""+    return [x * 255 // (W - 1), y * 255 // (H - 1), 128][c]+++def float_sample(x, y, c):+    """Exact binary fractions, some above 1.0 (mirrored in jxl.rs)."""+    return (x * 3 + y * 5 + c * 7) / 64.0+++def png(path, w, h, channels, bits, sample, extra_chunks=()):+    """A minimal PNG writer: `sample(x, y, c)` gives each integer sample."""+    color_type = {1: 0, 2: 4, 3: 2, 4: 6}[channels]+    raw = bytearray()+    for y in range(h):+        raw.append(0)+        for x in range(w):+            for c in range(channels):+                v = sample(x, y, c)+                raw += struct.pack(">H", v) if bits == 16 else bytes([v])++    def chunk(ty, data):+        return struct.pack(">I", len(data)) + ty + data + struct.pack(">I", zlib.crc32(ty + data) & 0xFFFFFFFF)++    out = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, bits, color_type, 0, 0, 0))+    for ty, data in extra_chunks:+        out += chunk(ty, data)+    out += chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + chunk(b"IEND", b"")+    path.write_bytes(out)+++def pfm(path, w, h):+    rows = []+    for y in range(h - 1, -1, -1):  # PFM is bottom-up+        for x in range(w):+            for c in range(3):+                rows.append(float_sample(x, y, c))+    path.write_bytes(f"PF\n{w} {h}\n-1.0\n".encode() + struct.pack(f"<{len(rows)}f", *rows))+++def exif_tiff(orientation):+    """A TIFF-structured EXIF block: Orientation and ImageDescription."""+    desc = b"PhotoCraft JPEG XL fixture\0"+    entries = [+        struct.pack(">HHII", 0x010E, 2, len(desc), 8 + 2 + 12 * 2 + 4),+        struct.pack(">HHIHH", 0x0112, 3, 1, orientation, 0),+    ]+    return b"MM\0*" + struct.pack(">I", 8) + struct.pack(">H", len(entries)) + b"".join(entries) + struct.pack(">I", 0) + desc+++XMP = (+    '<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/">'+    '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" '+    'xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:tiff="http://ns.adobe.com/tiff/1.0/" tiff:Orientation="6">'+    "<dc:title><rdf:Alt><rdf:li xml:lang=\"x-default\">PhotoCraft JPEG XL fixture</rdf:li></rdf:Alt></dc:title>"+    '</rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>'+)+++def jpeg_with_metadata(path):+    """A smooth JPEG with an EXIF APP1 (Orientation 6) and an XMP APP1 segment."""+    img = Image.new("RGB", (W, H))+    img.putdata([tuple(smooth(x, y, c) for c in range(3)) for y in range(H) for x in range(W)])+    # Saved to its final path, then rewritten with XMP below: a NamedTemporaryFile cannot be+    # reopened by name on Windows while it is still open.+    img.save(path, "JPEG", quality=95, exif=b"Exif\0\0" + exif_tiff(6))+    data = Path(path).read_bytes()+    xmp = b"http://ns.adobe.com/xap/1.0/\0" + XMP.encode("utf-8")+    app1 = b"\xff\xe1" + struct.pack(">H", len(xmp) + 2) + xmp+    # Right after SOI; Pillow's JFIF and EXIF segments follow.+    assert data[:2] == b"\xff\xd8"+    path.write_bytes(data[:2] + app1 + data[2:])+++def codestream_of(data):+    """The bare codestream: `data` itself, or the jxlc / jxlp payloads of a container (cjxl 0.7+    writes a container for JPEG input whatever --container says)."""+    if data[:2] == b"\xff\x0a":+        return data+    assert data[:12] == b"\0\0\0\x0cJXL \r\n\x87\n", "not a JPEG XL file"+    out = b""+    pos = 0+    while pos < len(data):+        size, ty = struct.unpack(">I4s", data[pos:pos + 8])+        body = data[pos + 8:pos + size] if size else data[pos + 8:]+        if ty == b"jxlc":+            out += body+        elif ty == b"jxlp":+            out += body[4:]  # after the partial-codestream index+        pos += size or len(data)+    assert out[:2] == b"\xff\x0a"+    return out+++def container(path, codestream, exif, xmp):+    """Wraps a bare codestream in the ISO/IEC 18181-2 container with Exif and XML boxes+    (cjxl 0.7 drops the JPEG's metadata when it re-encodes pixels, so the boxes are written here)."""++    def box(ty, data):+        return struct.pack(">I", 8 + len(data)) + ty + data++    out = box(b"JXL ", b"\r\n\x87\n") + box(b"ftyp", b"jxl \0\0\0\0jxl ")+    out += box(b"Exif", struct.pack(">I", 0) + exif) + box(b"xml ", xmp) + box(b"jxlc", codestream)+    path.write_bytes(out)+++def gif_animation(path):+    frames = []+    for i in range(3):+        img = Image.new("RGB", (W, H))+        img.putdata([((x * 20 + i * 80) % 256, (y * 25) % 256, i * 100) for y in range(H) for x in range(W)])+        frames.append(img)+    frames[0].save(path, "GIF", save_all=True, append_images=frames[1:], duration=100, loop=0)+++def cjxl(src, dst, *args):+    subprocess.run(["cjxl", str(src), str(dst), "--quiet", *args], check=True)+    print(f"{dst.name}: {dst.stat().st_size} bytes")+++def main():+    OUT.mkdir(parents=True, exist_ok=True)+    with tempfile.TemporaryDirectory() as d:+        d = Path(d)+        png(d / "rgb8.png", W, H, 3, 8, lambda x, y, c: pattern(x, y, c, 255))+        cjxl(d / "rgb8.png", OUT / "rgb8-lossless.jxl", "-d", "0", "-e", "3")+        png(d / "rgba8.png", W, H, 4, 8, lambda x, y, c: alpha(x, y, 255) if c == 3 else pattern(x, y, c, 255))+        cjxl(d / "rgba8.png", OUT / "rgba8-lossless.jxl", "-d", "0", "-e", "3")+        png(d / "gray8.png", W, H, 1, 8, lambda x, y, c: pattern(x, y, c, 255))+        cjxl(d / "gray8.png", OUT / "gray8-lossless.jxl", "-d", "0", "-e", "3")+        png(d / "graya16.png", W, H, 2, 16, lambda x, y, c: alpha(x, y, 255) * 257 if c == 1 else pattern(x, y, c, 65535))+        cjxl(d / "graya16.png", OUT / "graya16-lossless.jxl", "-d", "0", "-e", "3")+        png(d / "rgb16.png", W, H, 3, 16, lambda x, y, c: pattern(x, y, c, 65535))+        cjxl(d / "rgb16.png", OUT / "rgb16-lossless.jxl", "-d", "0", "-e", "3")+        # 10-bit samples: cjxl reads the 16-bit PNG as fractions of 65535 and requantizes them to+        # 10 bits, so the PNG holds each 10-bit value scaled up (the exact inverse of that step).+        png(d / "rgb10.png", W, H, 3, 16, lambda x, y, c: round(pattern(x, y, c, 1023) * 65535 / 1023))+        cjxl(d / "rgb10.png", OUT / "rgb10-lossless.jxl", "-d", "0", "-e", "3", "--override_bitdepth=10")+        pfm(d / "rgb.pfm", W, H)+        cjxl(d / "rgb.pfm", OUT / "rgb-f32-lossless.jxl", "-d", "0", "-e", "3")+        png(d / "smooth.png", W, H, 3, 8, smooth)+        cjxl(d / "smooth.png", OUT / "rgb8-lossy.jxl", "-d", "1", "-e", "3")+        cjxl(d / "rgb8.png", OUT / "rgb8-p3.jxl", "-d", "0", "-e", "3", "-x", "color_space=RGB_D65_DCI_Rel_SRG")+        jpeg_with_metadata(d / "meta.jpg")+        cjxl(d / "meta.jpg", d / "orient6.jxl", "-d", "0", "-e", "3", "-j", "0", "--container=0")+        container(OUT / "exif-xmp-orient6.jxl", codestream_of((d / "orient6.jxl").read_bytes()), exif_tiff(6), XMP.encode("utf-8"))+        cjxl(d / "meta.jpg", OUT / "jpeg-recon.jxl", "-j", "1")+        gif_animation(d / "anim.gif")+        cjxl(d / "anim.gif", OUT / "animated.jxl", "-d", "0", "-e", "3")+        (d / "meta.jpg").replace(OUT / "exif-xmp-orient6.src.jpg")+        print("source JPEG kept next to the fixtures for the reconstruction test")+++if __name__ == "__main__":+    sys.exit(main())