The byte-exact JPEG encoder is only used for trickplay #149
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
#140 landed
crates/jellyfin-images/src/jpeg_encode.rs, whose output isbyte-identical to libjpeg-turbo's — verified at 793/793 over 61 images × 13
qualities against
cjpeg -baseline -optimize -sample 2x2 -dct int, including1×1, odd width and odd height, grayscale content, both sides of the
5000/q↔200-2qbranch at quality 50 and 51, and quality 100.Only
trickplay.rscalls it. The served-image path still encodes throughimage::codecs::jpeg::JpegEncoder(crates/jellyfin-images/src/lib.rs:306), andcollages are PNG.
#140's own aside was that a byte-exact encoder would close the position
collage.rsrecords — that a picture this server draws is not upstream'sbytes. It did not: a scaled JPEG this server serves is still not upstream's
bytes, and it now could be, for the cost of routing that path through the
encoder that already exists.
What has to be settled first
The encoder is exact given identical pixels, so the scaling step is where the
two would still differ, and that is the whole of the work:
for per request. It is not necessarily 90 —
DEFAULT_QUALITYis what arequest that names none gets, and a request may name one.
warning: matching Skia there needed the scale factor applied twice, nearest
sampling with a float reciprocal nudged one unit in the last place, and fancy
chroma upsampling off below a two-pixel block.
If the resampler does not agree, this closes as "the encoder is exact and the
scaler is not, and here is the measurement" — which is a better recorded
position than the one
collage.rscarries today.House rules
Tests run under
cargo nextest; upstream's source is the checkout inupstream/at the pin. Generated artifacts are regenerated, never edited.The answer keys under
crates/jellyfin-images/tests/edges/are generated with-baseline:cjpegdefaults toforce_baseline=FALSEand writes 16-bitquantisation tables at low qualities, which is not what Skia asks for.
Landed in
9139e6c. Closes as the issue anticipated: the encoder is exact andthe scaler is not, and here is the measurement.
The encoder half
scale()now encodes throughencode_jpegrather thanimage::codecs::jpeg,so the served path and the trickplay sheets go through the same byte-exact
encoder. Collages stay PNG.
What has to be settled first — settled
Upstream's served-image path asks for quality 100, not 90. The
documentation on
GetImageInternalsays "Defaults to 90", and it is wrong forthis route:
ImageController.cs:1910isQuality = quality ?? 100. Measuredagainst a pinned server rather than read off the source, on one poster at
maxWidth=200&format=jpg:Ninety is what the account picture (
ImageController.cs:1505) and thesplashscreen (
:1696) pass, and this server already passed ninety for both.The item route now passes a hundred; it was serving half-size pictures.
The scaler half — it does not agree
Skia downscales through a mipmap chain sampled trilinearly
(
SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear)) and upscaleswith Mitchell cubic (
SkiaEncoder.cs:542-566). Nothing theimagecrate offersreproduces it. Measured against what a pinned server served for
tests/pictures/band-500x334.jpg, mean absolute difference per sample:The same measured on three film posters (1000x1500 to 2000x3000, not
committable), where the round trip is a cleaner floor:
Lanczos3 — what this server already used — is the closest of what is on offer,
tied with CatmullRom. No size agrees byte for byte at any filter. So the
resampler stays as it was, and the position is now a check rather than a
comment:
crates/jellyfin-images/src/scaled.rsholds five committed answer keysand asserts both bounds, and it fails the day a resampler makes one agree.
The keys are of a picture already in
tests/pictures/, uploaded to a containerof its own over a copy of the enriched snapshot and read back — so the source is
synthetic and the answer is regenerable.