check / check (push) Waiting to run
magic.DetectFormat returns an imagerequest.Format. ValidateMagicBytes returns the format it checked against the upstream Content-Type. The cache reads a cached source's format from its file's first bytes, as it reads a variant's. The proxy hands the format to Process. Process refuses a format that is not a source format, and input libvips would load with another loader, such as an SVG passing as AVIF. Format.MIMEType declares each MIME string once, and the fetcher, the cache's variant read and the Accept matching use it. magic.MIMEType, magic.ImageFormat, MIMEToImageFormat, magic's SVG detection, detectFormat, formatFromString, FormatToMIME and Result.InputFormat are gone. Model: opus-5-5
135 lines
3.7 KiB
Go
135 lines
3.7 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 returns the format to serve for the requested format: for
|
|
// the format auto, the format formatForAccept chooses from r's Accept header,
|
|
// and any other format as it is. For auto it 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 the image can be served.
|
|
func (s *Handlers) chooseAutoFormat(
|
|
w http.ResponseWriter, r *http.Request, requested imagerequest.Format,
|
|
) (imagerequest.Format, bool) {
|
|
if requested != imagerequest.FormatAuto {
|
|
return requested, 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
|
|
}
|
|
|
|
return format, 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[imagerequest.FormatJXL.MIMEType()] > 0 {
|
|
return imagerequest.FormatJXL, nil
|
|
}
|
|
|
|
if qualities[imagerequest.FormatAVIF.MIMEType()] > 0 {
|
|
return imagerequest.FormatAVIF, nil
|
|
}
|
|
|
|
if qualities[imagerequest.FormatWebP.MIMEType()] > 0 {
|
|
return imagerequest.FormatWebP, nil
|
|
}
|
|
|
|
// For JPEG, the most specific entry the header has decides
|
|
quality, named := qualities[imagerequest.FormatJPEG.MIMEType()]
|
|
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
|
|
}
|