check / check (push) Canceled after 0s
The new package internal/imagerequest holds the request type, its two parts Source and Transform, the Format and Fit types, the dimension and fit checks, the /v1/image/ path parsing and the variant key; it imports no other package of pixa. imgcache.ParsedURL, imgcache.ImageRequest, signature.Request, imageprocessor.Request, the second Size, format and fit types and the conversions between them are gone. The image processor takes a Transform only. encurl keeps a private struct with the token's short CBOR field names. VariantKey moves with the function that makes it. Tests follow the moved and renamed code; the tests of the removed conversions go with them. Model: opus-5-5
136 lines
3.6 KiB
Go
136 lines
3.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"mime"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
)
|
|
|
|
// 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 *imagerequest.Request,
|
|
) bool {
|
|
if req.Transform.Format != imagerequest.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.Transform.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) (imagerequest.Format, error) {
|
|
qualities, err := parseAccept(accept)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(qualities) == 0 {
|
|
return imagerequest.FormatJPEG, nil
|
|
}
|
|
|
|
if qualities["image/jxl"] > 0 {
|
|
return imagerequest.FormatJXL, nil
|
|
}
|
|
|
|
if qualities["image/avif"] > 0 {
|
|
return imagerequest.FormatAVIF, nil
|
|
}
|
|
|
|
if qualities["image/webp"] > 0 {
|
|
return imagerequest.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 imagerequest.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
|
|
}
|