check / check (push) Waiting to run
A JPEG XL source is accepted, and orig of one is JPEG XL. The format jxl works in plain and encrypted URLs and on the generator page, served as image/jxl; auto chooses it first when Accept names image/jxl. govips sends libvips a JPEG XL distance, which overrides the quality, so q is turned into a distance, keeping 100 lossy. govips cannot have libvips strip metadata from JPEG XL, so it is removed from the image before saving; libvips 8.16 and later still add an EXIF block of their own, with nothing from the source. The conversion to sRGB moved ahead of the format switch, and a CMYK image with no ICC profile is converted to sRGB too, as libvips cannot save CMYK as JPEG XL. Model: opus-5-5
136 lines
3.5 KiB
Go
136 lines
3.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"mime"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"sneak.berlin/go/pixa/internal/imgcache"
|
|
)
|
|
|
|
// Errors for an Accept header that an auto URL cannot be served for.
|
|
var (
|
|
errInvalidAccept = errors.New("invalid Accept header")
|
|
errNotAcceptable = errors.New("not acceptable: auto serves " +
|
|
"image/jxl, image/avif, image/webp or image/jpeg")
|
|
)
|
|
|
|
// chooseAutoFormat replaces the format auto in req with the format
|
|
// formatForAccept chooses from r's Accept header, and adds Vary: Accept to the
|
|
// response, which then depends on that header. It answers 400 for an Accept
|
|
// header that is not valid and 406 for one that allows none of the formats,
|
|
// and reports whether req can be served. Any other format is left as it is.
|
|
func (s *Handlers) chooseAutoFormat(
|
|
w http.ResponseWriter, r *http.Request, req *imgcache.ImageRequest,
|
|
) bool {
|
|
if req.Format != imgcache.FormatAuto {
|
|
return true
|
|
}
|
|
|
|
w.Header().Add("Vary", "Accept")
|
|
|
|
format, err := formatForAccept(strings.Join(r.Header.Values("Accept"), ","))
|
|
if errors.Is(err, errNotAcceptable) {
|
|
s.respondError(w, err.Error(), http.StatusNotAcceptable)
|
|
|
|
return false
|
|
}
|
|
|
|
if err != nil {
|
|
s.respondError(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return false
|
|
}
|
|
|
|
req.Format = format
|
|
|
|
return true
|
|
}
|
|
|
|
// formatForAccept returns the format an auto URL is served in for the Accept
|
|
// header accept: JPEG XL when it names image/jxl, else AVIF when it names
|
|
// image/avif, else WebP when it names image/webp, else JPEG when its most
|
|
// specific entry of image/jpeg, image/* and */* allows it, or when it names
|
|
// nothing. A q of 0 refuses a format. JPEG XL, AVIF and WebP must be named, as
|
|
// clients that cannot show them send image/* and */* too.
|
|
func formatForAccept(accept string) (imgcache.ImageFormat, error) {
|
|
qualities, err := parseAccept(accept)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(qualities) == 0 {
|
|
return imgcache.FormatJPEG, nil
|
|
}
|
|
|
|
if qualities["image/jxl"] > 0 {
|
|
return imgcache.FormatJXL, nil
|
|
}
|
|
|
|
if qualities["image/avif"] > 0 {
|
|
return imgcache.FormatAVIF, nil
|
|
}
|
|
|
|
if qualities["image/webp"] > 0 {
|
|
return imgcache.FormatWebP, nil
|
|
}
|
|
|
|
// For JPEG, the most specific entry the header has decides
|
|
quality, named := qualities["image/jpeg"]
|
|
if !named {
|
|
quality, named = qualities["image/*"]
|
|
}
|
|
|
|
if !named {
|
|
quality = qualities["*/*"]
|
|
}
|
|
|
|
if quality > 0 {
|
|
return imgcache.FormatJPEG, nil
|
|
}
|
|
|
|
return "", errNotAcceptable
|
|
}
|
|
|
|
// parseAccept returns the q of each media range the Accept header accept
|
|
// names, 1 where it gives none. A media range named more than once keeps its
|
|
// lowest q, so a refusal is never overridden. A media range that does not
|
|
// parse, or a q that is not a number from 0 to 1, is an error.
|
|
func parseAccept(accept string) (map[string]float64, error) {
|
|
qualities := make(map[string]float64)
|
|
|
|
for entry := range strings.SplitSeq(accept, ",") {
|
|
// A header field list may hold empty entries
|
|
if strings.TrimSpace(entry) == "" {
|
|
continue
|
|
}
|
|
|
|
mediaRange, params, err := mime.ParseMediaType(entry)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %q: %w", errInvalidAccept, entry, err)
|
|
}
|
|
|
|
quality := 1.0
|
|
|
|
if qParam, given := params["q"]; given {
|
|
quality, err = strconv.ParseFloat(qParam, 64)
|
|
inRange := quality >= 0 && quality <= 1
|
|
|
|
if err != nil || !inRange {
|
|
return nil, fmt.Errorf("%w: %q: q is not a number from 0 to 1",
|
|
errInvalidAccept, entry)
|
|
}
|
|
}
|
|
|
|
previous, named := qualities[mediaRange]
|
|
if !named || quality < previous {
|
|
qualities[mediaRange] = quality
|
|
}
|
|
}
|
|
|
|
return qualities, nil
|
|
}
|