swf2ass-go/types/shapes/BitmapConverter.go

230 lines
5.2 KiB
Go

package shapes
import (
"git.gammaspectra.live/WeebDataHoarder/swf-go/subtypes"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/settings"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"github.com/ctessum/polyclip-go"
"github.com/nfnt/resize"
"golang.org/x/exp/maps"
"image"
"image/color"
"image/png"
math2 "math"
"os"
"runtime"
"slices"
"sync"
"sync/atomic"
)
func ImageToPNG(im image.Image, fname string) error {
f, err := os.Create(fname)
if err != nil {
return err
}
defer f.Close()
err = png.Encode(f, im)
if err != nil {
return err
}
return nil
}
func ConvertBitmapBytesToDrawPathList(imageData []byte, alphaData []byte) (DrawPathList, error) {
im, err := subtypes.DecodeImageBitsJPEG(imageData, alphaData)
if err != nil {
return nil, err
}
drawPathList := ConvertBitmapToDrawPathList(im)
return drawPathList, nil
}
func QuantizeBitmap(i image.Image) image.Image {
if settings.GlobalSettings.BitmapPaletteSize == 0 {
return i
}
size := i.Bounds().Size()
palettedImage := image.NewPaletted(i.Bounds(), nil)
quantizer := MedianCutQuantizer{
NumColor: settings.GlobalSettings.BitmapPaletteSize,
}
quantizer.Quantize(palettedImage, i.Bounds(), i, image.Point{})
// Restore alpha
newIm := image.NewRGBA(i.Bounds())
for y := 0; y < size.Y; y++ {
for x := 0; x < size.X; x++ {
r, g, b, _ := palettedImage.At(x, y).RGBA()
_, _, _, a := i.At(x, y).RGBA()
if a == 0 {
newIm.SetRGBA(x, y, color.RGBA{
R: 0,
G: 0,
B: 0,
A: 0,
})
} else if (a >> 8) == 255 {
newIm.SetRGBA(x, y, color.RGBA{
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: 255,
})
} else {
//reduce alpha resolution a bit
newIm.SetRGBA(x, y, color.RGBA{
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: uint8((a >> 9) << 1),
})
}
}
}
return newIm
}
func ConvertBitmapToDrawPathList(i image.Image) (r DrawPathList) {
size := i.Bounds().Size()
ratioX := 1.0
ratioY := 1.0
maxDimension := max(size.X, size.Y)
if settings.GlobalSettings.BitmapMaxDimension > 0 {
if maxDimension > settings.GlobalSettings.BitmapMaxDimension && size.X == maxDimension {
ratio := float64(maxDimension) / float64(settings.GlobalSettings.BitmapMaxDimension)
w, h := uint(settings.GlobalSettings.BitmapMaxDimension), uint(float64(size.Y)/ratio)
i = resize.Resize(w, h, i, resize.Bicubic)
ratioX = float64(size.X+1) / float64(w+1)
ratioY = float64(size.Y+1) / float64(h+1)
} else if maxDimension > settings.GlobalSettings.BitmapMaxDimension && size.Y == maxDimension {
ratio := float64(maxDimension) / float64(settings.GlobalSettings.BitmapMaxDimension)
w, h := uint(float64(size.X)/ratio), uint(settings.GlobalSettings.BitmapMaxDimension)
i = resize.Resize(w, h, i, resize.Bicubic)
ratioX = float64(size.X+1) / float64(w+1)
ratioY = float64(size.Y+1) / float64(h+1)
}
}
i = QuantizeBitmap(i)
size = i.Bounds().Size()
var wg sync.WaitGroup
var hasAlpha bool
colors := make(map[math.PackedColor]polyclip.Polygon)
for y := 0; y < size.Y; y++ {
for x := 0; x < size.X; x++ {
r, g, b, a := i.At(x, y).RGBA()
p := math.NewPackedColor(uint8(r>>8), uint8(g>>8), uint8(b>>8), uint8(a>>8))
contour := polyclip.Contour{
{float64(x), float64(y)},
{float64(x), float64(y + 1)},
{float64(x + 1), float64(y + 1)},
{float64(x + 1), float64(y)},
}
if p.Alpha() < math2.MaxUint8 {
hasAlpha = true
}
colors[p] = append(colors[p], contour)
}
}
keys := maps.Keys(colors)
var n atomic.Uint64
type result struct {
Color math.PackedColor
Polygon polyclip.Polygon
}
results := make([][]result, runtime.NumCPU())
for cpuN := 0; cpuN < runtime.NumCPU(); cpuN++ {
wg.Add(1)
go func(cpuN int) {
defer wg.Done()
for {
i := n.Add(1) - 1
if i >= uint64(len(keys)) {
break
}
results[cpuN] = append(results[cpuN], result{
Color: keys[i],
Polygon: colors[keys[i]].Simplify(),
})
}
}(cpuN)
}
wg.Wait()
for _, cs := range results {
for _, v := range cs {
colors[v.Color] = v.Polygon
}
}
//Sort from the highest size to lowest
getSize := func(p polyclip.Polygon) (r int) {
for _, c := range p {
r += c.Len()
}
return r
}
slices.SortFunc(keys, func(a, b math.PackedColor) int {
sizeA := getSize(colors[a])
sizeB := getSize(colors[b])
if sizeA > sizeB {
return -1
} else if sizeB > sizeA {
return 1
} else {
return 0
}
})
// Full shape optimizations when alpha is not in use
if !hasAlpha {
//make a rectangle covering the whole first area to optimize this case
r = append(r, DrawPathFill(&FillStyleRecord{
Fill: keys[0].Color(),
}, Rectangle[float64]{
TopLeft: math.NewVector2[float64](0, 0),
BottomRight: math.NewVector2(float64(size.X+1), float64(size.Y+1)),
}.Draw()))
keys = keys[1:]
}
for _, k := range keys {
if k.Alpha() == 0 {
//Skip fully transparent pixels
continue
}
pol := colors[k]
//Draw resulting shape
r = append(r, DrawPathFill(&FillStyleRecord{
Fill: k.Color(),
}, ComplexPolygon{
Pol: pol,
}.GetShape()))
}
scale := math.ScaleTransform(math.NewVector2(ratioX, ratioY))
r2 := r.ApplyMatrixTransform(scale, true)
return r2.(DrawPathList)
}