fix junie's bad idea
This commit is contained in:
129
utils/compress_image.go
Normal file
129
utils/compress_image.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
// CompressImage takes a base‑64‑encoded image (JPEG, PNG or GIF) and returns
|
||||
// a base‑64‑encoded version that is at most maxLength in size, or an error.
|
||||
func CompressImage(b64 string, maxLength int) (string, error) {
|
||||
raw, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("base64 decode: %w", err)
|
||||
}
|
||||
if len(raw) <= maxLength {
|
||||
return b64, nil // small enough already
|
||||
}
|
||||
|
||||
switch mime := http.DetectContentType(raw); mime {
|
||||
case "image/gif":
|
||||
return compressGIF(raw, maxLength)
|
||||
default: // jpeg, png, webp, etc. → treat as raster
|
||||
return compressRaster(raw, maxLength)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Raster path (jpeg / png / single‑frame gif) ----------
|
||||
|
||||
func compressRaster(src []byte, maxLength int) (string, error) {
|
||||
img, _, err := image.Decode(bytes.NewReader(src))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode raster: %w", err)
|
||||
}
|
||||
|
||||
quality := 95
|
||||
for {
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err != nil {
|
||||
return "", fmt.Errorf("jpeg encode: %w", err)
|
||||
}
|
||||
if buf.Len() <= maxLength {
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
if quality > 20 {
|
||||
quality -= 5
|
||||
continue
|
||||
}
|
||||
|
||||
// down‑scale 80%
|
||||
b := img.Bounds()
|
||||
if b.Dx() < 100 || b.Dy() < 100 {
|
||||
return "", fmt.Errorf("cannot compress below %.02fMiB without destroying image", float64(maxLength)/1048576.0)
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, int(float64(b.Dx())*0.8), int(float64(b.Dy())*0.8)))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Bounds(), img, b, draw.Over, nil)
|
||||
img = dst
|
||||
quality = 95 // restart ladder
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Animated GIF path ----------
|
||||
|
||||
func compressGIF(src []byte, maxLength int) (string, error) {
|
||||
g, err := gif.DecodeAll(bytes.NewReader(src))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("gif decode: %w", err)
|
||||
}
|
||||
|
||||
for {
|
||||
var buf bytes.Buffer
|
||||
if err := gif.EncodeAll(&buf, g); err != nil {
|
||||
return "", fmt.Errorf("gif encode: %w", err)
|
||||
}
|
||||
if buf.Len() <= maxLength {
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
// down‑scale every frame by 80%
|
||||
w, h := g.Config.Width, g.Config.Height
|
||||
if w < 100 || h < 100 {
|
||||
return "", fmt.Errorf("cannot compress animated GIF below 5 MiB without excessive quality loss")
|
||||
}
|
||||
|
||||
nw, nh := int(float64(w)*0.8), int(float64(h)*0.8)
|
||||
for i, frm := range g.Image {
|
||||
// convert paletted frame → RGBA for scaling
|
||||
rgba := image.NewRGBA(frm.Bounds())
|
||||
draw.Draw(rgba, rgba.Bounds(), frm, frm.Bounds().Min, draw.Src)
|
||||
|
||||
// scaled destination
|
||||
dst := image.NewRGBA(image.Rect(0, 0, nw, nh))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Bounds(), rgba, rgba.Bounds(), draw.Over, nil)
|
||||
|
||||
// quantize back to paletted using default encoder quantizer
|
||||
paletted := image.NewPaletted(dst.Bounds(), nil)
|
||||
draw.FloydSteinberg.Draw(paletted, paletted.Bounds(), dst, dst.Bounds().Min)
|
||||
|
||||
g.Image[i] = paletted
|
||||
}
|
||||
g.Config.Width, g.Config.Height = nw, nh
|
||||
// loop back and test size again …
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Helpers for precise MIME decodes (optional) ----------
|
||||
|
||||
func decode(r *bytes.Reader, mime string) (image.Image, error) {
|
||||
switch mime {
|
||||
case "image/jpeg":
|
||||
return jpeg.Decode(r)
|
||||
case "image/png":
|
||||
return png.Decode(r)
|
||||
case "image/gif":
|
||||
// for single‑frame GIFs only – call DecodeAll in caller if animated
|
||||
return gif.Decode(r)
|
||||
default:
|
||||
i, _, err := image.Decode(r)
|
||||
return i, err // let the stdlib guess the format
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user