honk/vendor/humungus.tedunangst.com/r/webs/image/vacuum.go
2022-11-13 16:19:53 +01:00

248 lines
5.7 KiB
Go

//
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// basic image manipulation (resizing)
package image
import (
"bytes"
"fmt"
"image"
_ "image/gif"
"image/jpeg"
"image/png"
"io"
"math"
"golang.org/x/image/draw"
_ "golang.org/x/image/webp"
)
// A returned image in compressed format
type Image struct {
Data []byte
Format string
Width int
Height int
}
// Argument for the Vacuum function
type Params struct {
LimitSize int // max input dimension in pixels
MaxWidth int
MaxHeight int
MaxSize int // max output file size in bytes
Quality int // for jpeg output
}
const dirLeft = 1
const dirRight = 2
func fixrotation(img image.Image, dir int) image.Image {
w, h := img.Bounds().Max.X, img.Bounds().Max.Y
newimg := image.NewRGBA(image.Rectangle{Max: image.Point{X: h, Y: w}})
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
c := img.At(i, j)
if dir == dirLeft {
newimg.Set(j, w-i-1, c)
} else {
newimg.Set(h-j-1, i, c)
}
}
}
return newimg
}
var rotateLeftSigs = [][]byte{
{0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08},
{0x12, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00},
}
var rotateRightSigs = [][]byte{
{0x12, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00},
{0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x06},
}
// Read an image and shrink it down to web scale
func Vacuum(reader io.Reader, params Params) (*Image, error) {
var tmpbuf bytes.Buffer
tee := io.TeeReader(reader, &tmpbuf)
conf, _, err := image.DecodeConfig(tee)
if err != nil {
return nil, err
}
limitSize := 16000
if conf.Width > limitSize || conf.Height > limitSize ||
(params.LimitSize > 0 && conf.Width*conf.Height > params.LimitSize) {
return nil, fmt.Errorf("image is too large: x: %d y: %d", conf.Width, conf.Height)
}
peek := tmpbuf.Bytes()
img, format, err := image.Decode(io.MultiReader(bytes.NewReader(peek), reader))
if err != nil {
return nil, err
}
maxh := params.MaxHeight
maxw := params.MaxWidth
if maxw == 0 {
maxw = 16000
}
if maxh == 0 {
maxh = 16000
}
if params.MaxSize == 0 {
params.MaxSize = 512 * 1024
}
if format == "jpeg" {
for _, sig := range rotateLeftSigs {
if bytes.Contains(peek, sig) {
img = fixrotation(img, dirLeft)
break
}
}
for _, sig := range rotateRightSigs {
if bytes.Contains(peek, sig) {
img = fixrotation(img, dirRight)
break
}
}
}
bounds := img.Bounds()
for bounds.Max.X > maxw || bounds.Max.Y > maxh {
if bounds.Max.X > maxw*2 || bounds.Max.Y > maxh*2 {
bounds.Max.X = bounds.Max.X / 2
bounds.Max.Y = bounds.Max.Y / 2
} else {
if bounds.Max.X > maxw {
r := float64(maxw) / float64(bounds.Max.X)
bounds.Max.X = maxw
bounds.Max.Y = int(float64(bounds.Max.Y) * r)
}
if bounds.Max.Y > maxh {
r := float64(maxh) / float64(bounds.Max.Y)
bounds.Max.Y = maxh
bounds.Max.X = int(float64(bounds.Max.X) * r)
}
}
dst := image.NewRGBA(bounds)
draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil)
img = dst
bounds = img.Bounds()
}
quality := params.Quality
if quality == 0 {
quality = 80
}
var buf bytes.Buffer
for {
switch format {
case "gif":
format = "png"
png.Encode(&buf, img)
case "png":
png.Encode(&buf, img)
case "webp":
format = "jpeg"
fallthrough
case "jpeg":
jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality})
default:
return nil, fmt.Errorf("can't encode format: %s", format)
}
if buf.Len() > params.MaxSize && quality > 30 {
switch format {
case "png":
format = "jpeg"
case "jpeg":
quality -= 10
}
buf.Reset()
continue
}
break
}
rv := &Image{
Data: buf.Bytes(),
Format: format,
Width: img.Bounds().Max.X,
Height: img.Bounds().Max.Y,
}
return rv, nil
}
func lineate(s uint8) float32 {
x := float64(s)
x /= 255.0
if x < 0.04045 {
x /= 12.92
} else {
x += 0.055
x /= 1.055
x = math.Pow(x, 2.4)
}
return float32(x)
}
func delineate(x float64) uint8 {
if x > 0.0031308 {
x = math.Pow(x, 1/2.4)
x *= 1.055
x -= 0.055
} else {
x *= 12.92
}
x *= 255.0
return uint8(x)
}
func gammaConvert(g []float32, d []byte, w int, h int, pixsize int, chanoffset int, stride int) []float32 {
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
g[j*w+i] = lineate(d[j*stride+i*pixsize+chanoffset])
}
}
return g
}
func gammaRevert(d []byte, g []float32, w int, h int, pixsize int, chanoffset int, stride int) {
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
d[j*stride+i*pixsize+chanoffset] = delineate(float64(g[j*w+i]))
}
}
}
func blend(g []float32, s1, s2, s3, s4 int) float32 {
l1 := g[s1]
l2 := g[s2]
l3 := g[s3]
l4 := g[s4]
return ((l1 + l2 + l3 + l4) / 4.0)
}
func oldblend(d []byte, s1, s2, s3, s4 int) byte {
l1 := lineate(d[s1])
l2 := lineate(d[s2])
l3 := lineate(d[s3])
l4 := lineate(d[s4])
return delineate(float64(l1+l2+l3+l4) / 4.0)
}
func squish(d []byte, s1, s2, s3, s4 int) byte {
return uint8((uint32(d[s1]) + uint32(d[s2]) + uint32(d[s3]) + uint32(d[s4])) / 4)
}