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]