phongn opened a new issue, #13772:
URL: https://github.com/apache/trafficserver/issues/13772

   ## Summary
   
   Three related output-policy problems in `webp_transform`:
   
   1. **Encoder quality is inherited from the source image.** The plugin has 
never set a quality, so it inherits whatever ImageMagick carries over from 
decoding. That produces two cliffs: a q100 JPEG becomes a lossless WebP, and a 
lossless WebP becomes a q100 JPEG.
   2. **Transparency is lost.** A WebP with alpha converted to JPEG shows the 
colour data stored under its transparent pixels.
   3. **The cached `Content-Type` can disagree with the cached body.** The 
target type is stamped before the body is seen, and only the client-facing copy 
is corrected afterwards.
   
   Measurements below use ImageMagick 7.1.2-32 (Q16 HDRI, OpenMP) through 
traffic_server `master`, with sources encoded from lossless originals.
   
   ## 1. Inherited quality
   
   ImageMagick's coders behave as follows (7.1.2-32):
   - **JPEG reader:** estimates the source quality from the quantization tables.
   - **WebP reader:** sets quality 100 for any lossless (VP8L) input 
(`coders/webp.c:301`).
   - **WebP writer:** switches to lossless when quality >= 100 
(`coders/webp.c:1170`).
   - **JPEG writer:** uses the inherited quality, or 92 if none is set.
   
   | input (6 photos, 768x512) | output today | size vs input |
   |---|---|---|
   | JPEG q85 | WebP q85 | 0.85x |
   | JPEG q99 | WebP q99 | 0.48x |
   | **JPEG q100** | **lossless WebP** | **1.00x** |
   | lossy WebP | JPEG q92 | 2.05x |
   | **lossless WebP** | **JPEG q100** | 0.98x (photos), **4.2x** (logos) |
   
   - **q100 JPEG is also a CPU problem.** The lossless WebP encode runs on the 
ET_NET thread:
   
   | input | q99 | q100 |
   |---|---|---|
   | 768x512 | 88 ms | 620–690 ms |
   | 1920x1277 | 0.52 s | **2.58 s**, output 3.2 MB from a 3.0 MB input |
   
   - **Lossless WebP is the normal format for logos and UI graphics.** These 
are exactly the objects most likely to reach the WebP -> JPEG path. A 158 KB 
lossless logo becomes a 383 KB q100 JPEG.
   
   ## 2. Transparency
   
   `Image::magick("JPEG")` drops the alpha channel. What gets encoded is the 
RGB stored under transparent pixels, which is often junk (visible coloured 
fringes or garbage outside the shape) rather than a background.
   
   ## 3. Content-Type labelling
   
   `ImageTransform::handleReadResponseHeaders()` sets the server response's 
`Content-Type` to the target type before the body arrives. When the transform 
falls back to the original bytes (signature mismatch, decode error), 
`handleSendResponseHeaders()` relabels only the client response. The cached 
object keeps the target label over the original body; the code comment notes 
this as a known issue.
   
   Reproduced on `master` with the cache on. Each row is two requests for the 
same URL; the second is a cache hit (`cRs f`).
   
   | origin body | client `Accept` | 1st response | 2nd response (from cache) |
   |---|---|---|---|
   | truncated WebP (undecodable) | `*/*` | image/webp, WebP body | 
**image/jpeg, WebP body** |
   | truncated WebP (undecodable) | `image/jpeg` | image/webp, WebP body | 
**image/jpeg, WebP body** |
   | `Content-Type: image/png`, body not a PNG | WebP-capable | image/png | 
**image/webp**, same body |
   
   ## Proposal
   
   A prototype (ImageMagick backend, docs included, not yet a PR) is on the 
branch 
[`phongn:webp-transform-png`](https://github.com/phongn/trafficserver/tree/webp-transform-png),
 commit 
[`db10c79`](https://github.com/phongn/trafficserver/commit/db10c79f827b3ce23c1dc218e51f6af1480b091e).
   
   **Explicit, configurable quality:**
   - Add `webp_quality=<1-100>` (default 80) and `jpeg_quality=<1-100>` 
(default 85).
   - Never inherit quality from the source.
   - WebP output is always lossy: `webp:lossless=false`, so 100 doesn't 
silently switch to lossless.
   
   **PNG for transparent WebP:** a client without `image/webp` in `Accept` is 
treated as accepting JPEG/GIF/PNG.
   - A WebP with an alpha channel converts to PNG, which is lossless and keeps 
transparency.
   - An opaque WebP converts to JPEG as today.
   - The decision uses the decoder's alpha flag. libwebp's encoders set it only 
when transparency is present; a file from another encoder that flags alpha on 
an opaque image would become a PNG, which costs size but is still correct.
   
   **Label the body actually sent:**
   - Set `Content-Type` on the transform response (`TSHttpTxnTransformRespGet`) 
immediately before the first `produce()`.
   - `HttpTransact::handle_transform_ready()` builds both the client response 
and the cached copy's headers from the transform response when output starts. 
So one label covers converted output, the PNG fallback, and pass-through.
   - This removes the early stamp in `handleReadResponseHeaders()` and the 
relabel in `handleSendResponseHeaders()`.
   
   **Optional: size guard.** If the WebP output is not smaller than the 
original JPEG/PNG, send the original; the client accepts both. A fixed quality 
otherwise inflates low-quality sources: a q50 JPEG grows 1.26x at Q80.
   
   **Stats:**
   - Add `plugin.webp_transform.convert_to_png`.
   - Count only successful conversions. Today the counter increments after 
decode, before encode.
   
   ## Results with the prototype (defaults 80/85)
   
   Averages over 6 photos unless noted:
   
   | input | today | proposed |
   |---|---|---|
   | JPEG q100 | 521 KB lossless WebP | 71 KB lossy WebP |
   | lossless WebP logo with alpha (one 158 KB logo), legacy client | 383 KB 
q100 JPEG, broken transparency | 325 KB PNG, pixel-exact |
   | lossless WebP photo (opaque), legacy client | 475 KB q100 JPEG | 100 KB 
JPEG q85 |
   | lossy WebP, legacy client | 144 KB JPEG q92 | 96 KB JPEG q85 |
   
   Verified with the cache enabled:
   - Every case's second request returns the same `Content-Type` and body, and 
all are cache hits except one.
     - The exception is a truncated WebP passed through to a `*/*` client, 
which ATS core refetches each time by design.
     - The three labelling cases above are now correct on both requests.
   - Invalid quality values are logged and the defaults kept.
   - The chunked over-cap 502 path is unchanged.
   
   ## Compatibility
   
   **These defaults change output for every existing deployment:**
   - smaller WebP for high-quality sources
   - PNG instead of JPEG for transparent WebP
   - different JPEG quality
   
   This should go out with a release note, or in a release where output changes 
are expected.
   
   ## Open question: PNG for clients that list only `image/jpeg`
   
   A client whose `Accept` names only `image/jpeg` would receive PNG for a 
transparent WebP. The existing `webp_transform_invalid_input` replay sends 
exactly that header, though for an opaque image. One option is to send PNG only 
when `Accept` allows it (`image/png`, `image/*`, `*/*`), and otherwise flatten 
onto white and send JPEG. Is that worth the extra rule?
   
   ## Open question: default quality
   
   Fixed Q80 beats inherited quality for sources at about q85 and above. It 
loses below that:
   
   | source | today | fixed Q80 |
   |---|---|---|
   | q75 JPEG | 58 KB WebP | 69 KB WebP (+18%) |
   | q50 JPEG | 42 KB WebP | the 62 KB original, via the size guard |
   
   An alternative is to cap rather than fix: `min(estimated source quality, 
configured)`. ImageMagick provides the estimate; other backends would need a 
small JPEG quantization-table parser. Feedback welcome.
   
   ## Notes
   
   - **Autests:**
     - `webp_transform_invalid_input` (empty body, 1-byte body, non-JPEG JPEG, 
valid 1x1 WebP -> JPEG) passes when replayed by hand against the prototype.
     - The other tests exercise unchanged paths.
     - New tests should cover alpha -> PNG, q100 -> lossy, and cached labels on 
pass-through.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to