# BST-TIMEOUT: 1200 # 38 cases, each an encode or decode; the 60s default is nowhere near . "$BST_LIB" # libjxl 0.10.5 at SRCREV 6aa76f31 (meta-oe recipes-multimedia/libjxl) -- the # JPEG XL REFERENCE implementation, the encoder and decoder the format is # defined by, ported here with its command-line tools turned back on. # # WHAT THIS FILE ASSERTS, AND WHY IT IS A CLAIM ABOUT ARITHMETIC RATHER THAN # ABOUT SYSCALLS. cjxl and djxl are a file-to-file pair, so the acceptance gate # is a ROUND TRIP: encode losslessly, decode back, and compare the result with # the source byte for byte. A green on those cases says this arm's DCT, its # modular-mode arithmetic and its entropy coder all produce the same numbers the # format requires -- not merely that the encoder ran without crashing. That is # the same standard 57-libpng.test holds through pngvalid, reached here with a # far smaller corpus because the round trip IS the oracle: a lossless codec that # is wrong anywhere cannot return what it was given. # # WHY THE CORPUS IS PNM AND PAM AND NOT PNG. A whole-file cmp only means # something if the container is byte-stable, and PNG is not -- djxl's PNG writer # picks its own chunk layout and zlib settings, so a re-encoded PNG differs from # the source in ways that have nothing to do with the pixels. PNM carries no # compression and its header is fully determined: lib/extras/enc/pnm.cc:96 emits # "P%c\n%zu %zu\n%u\n", which is byte for byte what src/libjxl_mkfixtures.py # writes. So cmp(1) on a .ppm compares pixels and nothing else. # # The PNG is still in the corpus, for a different question: it holds the SAME # pixels as libjxl-rgb.ppm (asserted at fixture-generation time), so # cjxl(png) -> djxl -> ppm compared against the ppm asserts that libjxl's PNG # decode path and its PNM decode path land on the same image. # # EVERY INVOCATION PASSES --num_threads=0, DELIBERATELY. libjxl's jxl_threads # links Threads::Threads and cjxl defaults to -1, meaning "one worker per # hardware thread". This guest is a single wasm instance with a fixed memory # ceiling and no reason to spawn any; 0 keeps the work on the calling thread, so # a red here is the codec's arithmetic and never a thread that could not be # created. It also makes the encode deterministic, which the round trips need. W=/tmp/bst-libjxl.$$ mkdir -p "$W" || { bst_notok libjxl/stage "could not create $W"; return 2>/dev/null || exit 0; } require cjxl libjxl/present # ------------------------------------------------------------------ surface --- # check_rc with `test` is an ash builtin and forks nothing, unlike check/check_out # whose command substitution does -- worth having on an arm where every exec is a # fresh wasm module instantiation. for b in cjxl djxl jxlinfo; do check_rc "libjxl/prog-$b" 0 test -x "/usr/bin/$b" done # The three archives the port installs. Named rather than counted: jxl is the # codec, jxl_threads the parallel runner cjxl and djxl link, jxl_cms the colour # management built on skcms (JPEGXL_ENABLE_SKCMS=ON). for a in libjxl.a libjxl_threads.a libjxl_cms.a libjxl_extras_codec.a; do check_rc "libjxl/lib-$a" 0 test -f "/usr/lib/$a" done for h in encode.h decode.h types.h thread_parallel_runner.h; do check_rc "libjxl/header-$h" 0 test -f "/usr/include/jxl/$h" done for pc in libjxl.pc libjxl_threads.pc libjxl_cms.pc; do check_rc "libjxl/pkgconfig-$pc" 0 test -f "/usr/lib/pkgconfig/$pc" done # NEITHER PLUGIN SHIPS, and the mime one is asserted ABSENT rather than left # unmentioned. shared-mime-info is not on this image, so image-jxl.xml would be # input to a database nothing builds, and `inherit mime`'s RDEPENDS on # shared-mime-info-data would fail do_rootfs for the whole image. Asserted as a # positive so that the file APPEARING here -- which would mean the package # config came back on, and with it that unsatisfiable runtime edge -- is what # turns the case red. if [ -e /usr/share/mime/packages/image-jxl.xml ]; then bst_notok libjxl/no-mime-plugin \ "image-jxl.xml shipped: PACKAGECONFIG mime is back on, and with it an RDEPENDS on shared-mime-info-data that this image cannot satisfy" else bst_ok libjxl/no-mime-plugin fi # Shape, not absence: this arm converts to shared libraries per recipe # (BRINTOS-1740), so a .so here is a conversion, not a policy breach. bst_lib_shape libjxl/static-only "/usr/lib/libjxl.so" "/usr/lib/libjxl.so.0.10" # jpegli is disabled in the bbappend; its libjpeg.so would be both shared and a # collision with the libjpeg-turbo that is the real libjpeg on this image. if [ -e /usr/lib/libjpeg.so.62 ]; then bst_notok libjxl/no-jpegli-libjpeg "jpegli's libjpeg.so.62 shipped: JPEGXL_ENABLE_JPEGLI came back on" else bst_ok libjxl/no-jpegli-libjpeg fi check_out libjxl/version "0.10.5" cjxl --version # ------------------------------------------------------------------ fixtures --- # Staged into tmpfs rather than read from the rootfs: the deployed root is # virtiofs and read-only, the tools write beside their input, and a top-level # redirect from that root is the dup2-513 trap the packager README documents. for f in libjxl-rgb.ppm libjxl-rgb.png libjxl-gray.pgm libjxl-alpha.pam \ libjxl-photo.jpg libjxl-anim.gif; do cp "$BST_DATA/$f" "$W/$f" 2>/dev/null done check_rc libjxl/fixtures-staged 0 test -s "$W/libjxl-rgb.ppm" # --------------------------------------------------------- lossless round trip --- # THE HEADLINE CASE. -d 0 is mathematically lossless, so the decoded PPM must be # the source PPM byte for byte -- header included, since both are written to the # one format pnm.cc emits. # # -d 0 ALONE, NEVER WITH -q 100. The two spell the same request but cjxl rejects # the pair outright -- tools/cjxl_main.cc:661 exits EXIT_FAILURE with "Must not # set both --distance and --quality" before any codec code runs. check_rc libjxl/rgb-encode 0 cjxl --num_threads=0 -d 0 "$W/libjxl-rgb.ppm" "$W/rgb.jxl" check_rc libjxl/rgb-decode 0 djxl --num_threads=0 "$W/rgb.jxl" "$W/rgb.out.ppm" check_rc libjxl/rgb-roundtrip-exact 0 cmp -s "$W/libjxl-rgb.ppm" "$W/rgb.out.ppm" # The same claim for a single-channel image, which takes a different path through # the modular encoder than the three-channel one above. check_rc libjxl/gray-encode 0 cjxl --num_threads=0 -d 0 "$W/libjxl-gray.pgm" "$W/gray.jxl" check_rc libjxl/gray-decode 0 djxl --num_threads=0 "$W/gray.jxl" "$W/gray.out.pgm" check_rc libjxl/gray-roundtrip-exact 0 cmp -s "$W/libjxl-gray.pgm" "$W/gray.out.pgm" # The PNG decode path, asserted against the PNM one. Both fixtures hold the same # pixels by construction, so a lossless encode of either must decode to the same # PPM. This is what makes libpng's presence in DEPENDS a tested fact rather than # a recipe line: without PNG_FOUND at configure time cjxl cannot read the file at # all and rgb-png-encode goes red. check_rc libjxl/rgb-png-encode 0 cjxl --num_threads=0 -d 0 "$W/libjxl-rgb.png" "$W/rgbpng.jxl" check_rc libjxl/rgb-png-decode 0 djxl --num_threads=0 "$W/rgbpng.jxl" "$W/rgbpng.out.ppm" check_rc libjxl/png-pnm-agree 0 cmp -s "$W/libjxl-rgb.ppm" "$W/rgbpng.out.ppm" # ------------------------------------------------------------------- alpha --- # Compared on the PIXEL PAYLOAD, not the whole file. djxl may carry alpha as an # extra channel and emit a header with an additional TUPLTYPE line -- a # difference in how the channel is DESCRIBED, not in what it holds. Both files # are cut at their ENDHDR and what follows is compared. pam_payload() { off=$(grep -abo ENDHDR "$1" 2>/dev/null | head -1 | cut -d: -f1) [ -n "$off" ] || return 1 tail -c "+$((off + 8))" "$1" } check_rc libjxl/alpha-encode 0 cjxl --num_threads=0 -d 0 "$W/libjxl-alpha.pam" "$W/alpha.jxl" check_rc libjxl/alpha-decode 0 djxl --num_threads=0 "$W/alpha.jxl" "$W/alpha.out.pam" pam_payload "$W/libjxl-alpha.pam" > "$W/alpha.src.raw" 2>/dev/null pam_payload "$W/alpha.out.pam" > "$W/alpha.out.raw" 2>/dev/null check_rc libjxl/alpha-roundtrip-exact 0 cmp -s "$W/alpha.src.raw" "$W/alpha.out.raw" # The alpha channel must survive as alpha, not be flattened into the colour. # jxlinfo reads it out of the codestream header and spells the pixel format # "8-bit RGB+Alpha" -- capital A, and matched as a literal substring, so the # needle carries the colour model with it and a flattened image cannot satisfy # it by accident. check_out libjxl/alpha-declared "RGB+Alpha" jxlinfo "$W/alpha.jxl" # -------------------------------------------------------------------- lossy --- # A STATED BOUND, AND IT IS DELIBERATELY COARSE. -d 1 is JPEG XL's # "visually lossless" distance; what that costs in maximum absolute channel # error on this particular fixture has NOT been measured on target, so the # ceiling below is a sanity bound rather than a tight one -- it separates "the # lossy path works" from "the lossy path returned noise", which is what this # case is for. The measured value is echoed on every run so the bound can be # tightened from evidence instead of guessed again. check_rc libjxl/lossy-encode 0 cjxl --num_threads=0 -d 1 "$W/libjxl-rgb.ppm" "$W/lossy.jxl" check_rc libjxl/lossy-decode 0 djxl --num_threads=0 "$W/lossy.jxl" "$W/lossy.out.ppm" # Lossy must actually cost less than lossless, or -d 1 did not take. sz0=$(wc -c < "$W/rgb.jxl" 2>/dev/null || echo 0) sz1=$(wc -c < "$W/lossy.jxl" 2>/dev/null || echo 0) if [ "$sz1" -gt 0 ] && [ "$sz1" -lt "$sz0" ]; then bst_ok libjxl/lossy-smaller else bst_notok libjxl/lossy-smaller "lossless=$sz0 lossy=$sz1: -d 1 did not produce a smaller file" fi # Difference between the lossless and the lossy decode, asserted on the MEAN # absolute per-channel error and not the maximum. Both PPMs carry the identical # 13-byte header, so the payload starts at value 14. A pipeline, so the redirect # forks and the dup2-513 trap does not apply. # # THE MEAN, BECAUSE THE MAXIMUM MEASURES THE FIXTURE RATHER THAN THE CODEC. The # blue channel is (x*x + y*y) & 0xff, whose mod-256 wraps are step discontinuities # of nearly full scale; VarDCT rings at those edges, so a single pixel there # dominates any max-abs figure while saying nothing about whether the codec works. # Measured on the reference build at this exact fixture: mean 9.27, max 174, with # the max carried entirely by that channel (per-channel max 59 / 45 / 174, mean # 5.31 / 3.54 / 18.94). The bound below is set against the mean with headroom; a # lossy path returning noise would land near 85, the mean of a uniform byte # difference, so 25 separates working from broken with room on both sides. # The maximum is still echoed on every run, as a diagnostic rather than a gate. sep=$( { od -An -v -tu1 "$W/rgb.out.ppm"; echo SEP; od -An -v -tu1 "$W/lossy.out.ppm"; } | awk ' /SEP/ { s=1; next } { for (i = 1; i <= NF; i++) { if (s == 0) A[++n] = $i + 0; else B[++m] = $i + 0 } } END { if (n != m || n == 0) { print "LEN"; exit } mx = 0; sum = 0; c = 0 for (k = 14; k <= n; k++) { d = A[k] - B[k]; if (d < 0) d = -d sum += d; c++; if (d > mx) mx = d } if (c == 0) { print "LEN"; exit } printf "%d %d\n", (sum / c) + 0.5, mx }' 2>/dev/null ) mean=${sep% *} mx=${sep#* } echo "libjxl: lossy d=1 mean abs channel diff = $mean (bound 25), max = $mx" case "$mean" in ''|LEN|*[!0-9]*) bst_notok libjxl/lossy-distance "could not compute a difference (got '$sep')" ;; *) if [ "$mean" -le 25 ]; then bst_ok libjxl/lossy-distance else bst_notok libjxl/lossy-distance "mean abs channel diff $mean exceeds the stated bound of 25 at -d 1 (max $mx)"; fi ;; esac # ------------------------------------------------- JPEG transcode and rebuild --- # THE STRONGEST CASE IN THE FILE, because it is exact by DESIGN rather than by # tolerance. JPEG XL can carry an existing JPEG bitstream losslessly: cjxl stores # what it needs to rebuild the original entropy-coded stream, and djxl -j # reconstructs it. The output must be the input, byte for byte -- not merely the # same pixels. Nothing but a correct implementation returns that file. # # It is also what makes JPEGXL_ENABLE_TRANSCODE_JPEG (upstream default, left on) # a tested fact. check_rc libjxl/jpeg-transcode 0 cjxl --num_threads=0 "$W/libjxl-photo.jpg" "$W/photo.jxl" check_rc libjxl/jpeg-reconstruct 0 djxl --num_threads=0 "$W/photo.jxl" "$W/photo.out.jpg" check_rc libjxl/jpeg-exact 0 cmp -s "$W/libjxl-photo.jpg" "$W/photo.out.jpg" # jxlinfo names the reconstruction data in the container when it is present. check_out libjxl/jpeg-reconstruction-box "JPEG" jxlinfo "$W/photo.jxl" # ---------------------------------------------------------------- animation --- # GIF is the animated input, decoded through giflib -- which the bbappend adds to # DEPENDS for exactly this reason. A red on gif-encode means GIF_FOUND was false # at configure time and the codec was compiled out. check_rc libjxl/gif-encode 0 cjxl --num_threads=0 -d 0 "$W/libjxl-anim.gif" "$W/anim.jxl" check_out libjxl/gif-animation "nimation" jxlinfo "$W/anim.jxl" # ----------------------------------------------------------------- jxlinfo --- # The dimensions the fixture was written at, read back out of the codestream. check_out libjxl/info-dimensions "64x48" jxlinfo "$W/rgb.jxl" # ------------------------------------------------------------ effort levels --- # -e selects the encoder's speed/density tradeoff. Every level must still be # LOSSLESS at -d 0 -- the effort changes how hard the encoder looks, never what # the decoder gets back. Three points across the range rather than all nine, so # the suite stays inside its bound. for e in 1 4 7; do if cjxl --num_threads=0 -d 0 -e "$e" "$W/libjxl-rgb.ppm" "$W/e$e.jxl" >/dev/null 2>&1 && djxl --num_threads=0 "$W/e$e.jxl" "$W/e$e.ppm" >/dev/null 2>&1 && cmp -s "$W/libjxl-rgb.ppm" "$W/e$e.ppm"; then bst_ok "libjxl/effort-$e-lossless" else bst_notok "libjxl/effort-$e-lossless" "cjxl -e $e -d 0 did not round-trip exactly" fi done # ------------------------------------------------------------ error handling --- # A truncated codestream must be REFUSED, not silently half-decoded. djxl signals # through its exit status: tools/djxl_main.cc returns EXIT_FAILURE on every # decode error, so 1 is the whole of the contract. head -c 200 "$W/rgb.jxl" > "$W/trunc.jxl" 2>/dev/null check_rc libjxl/corrupt-truncated 1 djxl --num_threads=0 "$W/trunc.jxl" "$W/trunc.ppm" # Not-a-jxl-at-all, which fails at signature rather than mid-stream. check_rc libjxl/corrupt-notjxl 1 djxl --num_threads=0 "$W/libjxl-rgb.ppm" "$W/notjxl.ppm" # And the encoder's own refusal of an input it cannot read. The input has to be # one NO codec can read: lib/extras/dec/decode.cc sniffs the bytes rather than # the extension and tries JXL among the rest, so a .jxl handed to cjxl is # decoded and re-encoded -- rc=0, and correctly so. Bytes that match no sniffer # are what leaves only the refusal path. printf 'this is not an image in any codec cjxl can read\n' > "$W/garbage.bin" check_rc libjxl/encode-badinput 1 cjxl --num_threads=0 "$W/garbage.bin" "$W/bad.jxl" rm -rf "$W"