137 lines
3.4 KiB
Go
137 lines
3.4 KiB
Go
package icon
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"math"
|
|
"os"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/log"
|
|
)
|
|
|
|
var magic = []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}
|
|
|
|
func IsPNG(data []byte) bool {
|
|
return bytes.HasPrefix(data, magic)
|
|
}
|
|
|
|
func Load(path string) (image.Image, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.VFSNotFound, "the icon file cannot be read").With(log.F("path", path))
|
|
}
|
|
return Decode(data, path)
|
|
}
|
|
|
|
func Decode(data []byte, name string) (image.Image, error) {
|
|
if !IsPNG(data) {
|
|
return nil, errs.New(errs.AssetImportFailed, "the icon "+name+" is not a PNG file")
|
|
}
|
|
img, err := png.Decode(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, errs.Wrap(err, errs.AssetImportFailed, "the icon "+name+" is not a valid PNG")
|
|
}
|
|
if b := img.Bounds(); b.Dx() < 16 || b.Dy() < 16 {
|
|
return nil, errs.New(errs.AssetImportFailed, "the icon "+name+" is smaller than 16x16")
|
|
}
|
|
return img, nil
|
|
}
|
|
|
|
func PNG(img image.Image) ([]byte, error) {
|
|
var b bytes.Buffer
|
|
enc := png.Encoder{CompressionLevel: png.BestCompression}
|
|
if err := enc.Encode(&b, img); err != nil {
|
|
return nil, errs.Wrap(err, errs.VFSIO, "cannot encode the icon")
|
|
}
|
|
return b.Bytes(), nil
|
|
}
|
|
|
|
func premultiplied(src image.Image) ([]float64, int, int) {
|
|
b := src.Bounds()
|
|
w, h := b.Dx(), b.Dy()
|
|
out := make([]float64, w*h*4)
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
r, g, bb, a := src.At(b.Min.X+x, b.Min.Y+y).RGBA()
|
|
i := (y*w + x) * 4
|
|
out[i], out[i+1], out[i+2], out[i+3] = float64(r), float64(g), float64(bb), float64(a)
|
|
}
|
|
}
|
|
return out, w, h
|
|
}
|
|
|
|
func area(pm []float64, sw, sh int, x0, y0, wx, wy float64) [4]float64 {
|
|
var c [4]float64
|
|
total := 0.0
|
|
for sy := int(y0); sy < int(math.Ceil(y0+wy)) && sy < sh; sy++ {
|
|
fy := math.Min(float64(sy+1), y0+wy) - math.Max(float64(sy), y0)
|
|
if fy <= 0 {
|
|
continue
|
|
}
|
|
for sx := int(x0); sx < int(math.Ceil(x0+wx)) && sx < sw; sx++ {
|
|
fx := math.Min(float64(sx+1), x0+wx) - math.Max(float64(sx), x0)
|
|
if fx <= 0 {
|
|
continue
|
|
}
|
|
k := fx * fy
|
|
i := (sy*sw + sx) * 4
|
|
for j := range c {
|
|
c[j] += pm[i+j] * k
|
|
}
|
|
total += k
|
|
}
|
|
}
|
|
if total > 0 {
|
|
for j := range c {
|
|
c[j] /= total
|
|
}
|
|
}
|
|
return c
|
|
}
|
|
|
|
func bilinear(pm []float64, sw, sh int, fx, fy float64) [4]float64 {
|
|
fx = math.Max(0, math.Min(fx, float64(sw-1)))
|
|
fy = math.Max(0, math.Min(fy, float64(sh-1)))
|
|
x0, y0 := int(fx), int(fy)
|
|
x1, y1 := min(x0+1, sw-1), min(y0+1, sh-1)
|
|
tx, ty := fx-float64(x0), fy-float64(y0)
|
|
var c [4]float64
|
|
for j := 0; j < 4; j++ {
|
|
a := pm[(y0*sw+x0)*4+j]*(1-tx) + pm[(y0*sw+x1)*4+j]*tx
|
|
b := pm[(y1*sw+x0)*4+j]*(1-tx) + pm[(y1*sw+x1)*4+j]*tx
|
|
c[j] = a*(1-ty) + b*ty
|
|
}
|
|
return c
|
|
}
|
|
|
|
func channel(v, a float64) uint8 {
|
|
return uint8(math.Round(math.Max(0, math.Min(255, v/a*255))))
|
|
}
|
|
|
|
func Resize(src image.Image, w, h int) *image.NRGBA {
|
|
pm, sw, sh := premultiplied(src)
|
|
dst := image.NewNRGBA(image.Rect(0, 0, w, h))
|
|
sx, sy := float64(sw)/float64(w), float64(sh)/float64(h)
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
var c [4]float64
|
|
if sx >= 1 && sy >= 1 {
|
|
c = area(pm, sw, sh, float64(x)*sx, float64(y)*sy, sx, sy)
|
|
} else {
|
|
c = bilinear(pm, sw, sh, (float64(x)+0.5)*sx-0.5, (float64(y)+0.5)*sy-0.5)
|
|
}
|
|
if c[3] <= 0 {
|
|
continue
|
|
}
|
|
dst.SetNRGBA(x, y, color.NRGBA{
|
|
R: channel(c[0], c[3]), G: channel(c[1], c[3]), B: channel(c[2], c[3]),
|
|
A: uint8(math.Round(math.Min(255, c[3]/65535*255))),
|
|
})
|
|
}
|
|
}
|
|
return dst
|
|
}
|