Handle AVIF source images in thumb pipeline
Decode AVIF via avifdec when Go's image.Decode can't, and pass-through small AVIFs (<=800px wide) by caching the original bytes instead of re-encoding.
This commit is contained in:
@@ -150,6 +150,18 @@ func buildAvifThumb(src, outPath string) error {
|
|||||||
return fmt.Errorf("image too big (>%d bytes)", thumbMaxBytes)
|
return fmt.Errorf("image too big (>%d bytes)", thumbMaxBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isAvif(body) {
|
||||||
|
// If the source AVIF is already within our width budget, skip the
|
||||||
|
// decode/resize/re-encode roundtrip and cache the original bytes.
|
||||||
|
w, _, infoErr := avifInfo(body)
|
||||||
|
if infoErr == nil && w > 0 && w <= thumbWidth {
|
||||||
|
return writeFile(outPath, body)
|
||||||
|
}
|
||||||
|
body, err = avifToPNG(body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("avifdec: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
src2, _, err := image.Decode(bytes.NewReader(body))
|
src2, _, err := image.Decode(bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("decode: %w", err)
|
return fmt.Errorf("decode: %w", err)
|
||||||
@@ -202,3 +214,89 @@ func buildAvifThumb(src, outPath string) error {
|
|||||||
}
|
}
|
||||||
return os.Rename(stagedOut, outPath)
|
return os.Rename(stagedOut, outPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAvif reports whether body looks like an AVIF file via the ISO-BMFF "ftyp"
|
||||||
|
// brand at bytes 4..12 ("ftypavif"/"ftypavis").
|
||||||
|
func isAvif(body []byte) bool {
|
||||||
|
if len(body) < 12 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if string(body[4:8]) != "ftyp" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
brand := string(body[8:12])
|
||||||
|
return brand == "avif" || brand == "avis"
|
||||||
|
}
|
||||||
|
|
||||||
|
// avifInfo returns the width and height of an AVIF by shelling out to
|
||||||
|
// `avifdec --info`. It still decodes the image internally but doesn't write
|
||||||
|
// pixels to disk, so it's cheaper than full avifToPNG.
|
||||||
|
func avifInfo(body []byte) (int, int, error) {
|
||||||
|
in, err := os.CreateTemp("", "pete-avif-info-*.avif")
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
inPath := in.Name()
|
||||||
|
defer os.Remove(inPath)
|
||||||
|
if _, err := in.Write(body); err != nil {
|
||||||
|
in.Close()
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
if err := in.Close(); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
out, err := exec.Command("avifdec", "--info", inPath).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
rest, ok := strings.CutPrefix(line, "* Resolution")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, dims, ok := strings.Cut(rest, ":")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var w, h int
|
||||||
|
if _, err := fmt.Sscanf(strings.TrimSpace(dims), "%dx%d", &w, &h); err == nil {
|
||||||
|
return w, h, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, 0, fmt.Errorf("no resolution in avifdec output")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeFile atomically writes body to path via a sibling temp + rename.
|
||||||
|
func writeFile(path string, body []byte) error {
|
||||||
|
staged := path + ".tmp"
|
||||||
|
if err := os.WriteFile(staged, body, 0o644); err != nil {
|
||||||
|
os.Remove(staged)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(staged, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// avifToPNG decodes AVIF bytes to PNG bytes by shelling out to avifdec.
|
||||||
|
func avifToPNG(body []byte) ([]byte, error) {
|
||||||
|
in, err := os.CreateTemp("", "pete-avif-in-*.avif")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inPath := in.Name()
|
||||||
|
defer os.Remove(inPath)
|
||||||
|
if _, err := in.Write(body); err != nil {
|
||||||
|
in.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := in.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
outPath := inPath + ".png"
|
||||||
|
defer os.Remove(outPath)
|
||||||
|
cmd := exec.Command("avifdec", inPath, outPath)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
return os.ReadFile(outPath)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user