230 lines
5.2 KiB
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)
|
|
}
|