Optimize performance, implement bitmap shapes, implement clipping settings

This commit is contained in:
DataHoarder 2023-11-24 05:02:08 +01:00
parent c115d9d2f8
commit 159b72e22b
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
48 changed files with 1563 additions and 239 deletions

View file

@ -7,13 +7,18 @@ import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
"runtime"
"slices"
"strconv"
"sync"
"sync/atomic"
"time"
)
type Renderer struct {
Header []string
RunningBuffer []*line.EventLine
Display shapes.Rectangle[float64]
}
func NewRenderer(frameRate float64, display shapes.Rectangle[float64]) *Renderer {
@ -25,6 +30,7 @@ func NewRenderer(frameRate float64, display shapes.Rectangle[float64]) *Renderer
frameRate *= settings.GlobalSettings.VideoRateMultiplier
return &Renderer{
Display: display,
Header: []string{
"[Script Info]",
"; Script generated by swf2ass Renderer",
@ -72,7 +78,7 @@ func (r *Renderer) RenderFrame(frameInfo types.FrameInformation, frame types.Ren
animated := 0
for _, object := range frame {
obEntry := *BakeRenderedObjectGradients(object)
obEntry := *BakeRenderedObjectsFillables(object)
object = &obEntry
object.MatrixTransform = scale.Multiply(object.MatrixTransform) //TODO: order?
@ -120,54 +126,56 @@ func (r *Renderer) RenderFrame(frameInfo types.FrameInformation, frame types.Ren
fmt.Printf("[ASS] Total %d objects, %d flush, %d buffer, %d animated tags.\n", len(frame), len(r.RunningBuffer), len(runningBuffer), animated)
//Flush non dupes
for _, l := range r.RunningBuffer {
l.Name += fmt.Sprintf(" f:%d>%d~%d", l.Start, l.End, l.End-l.Start+1)
l.DropCache()
result = append(result, l.Encode(frameInfo.GetFrameDuration()))
}
result = append(result, r.Flush(frameInfo)...)
r.RunningBuffer = runningBuffer
return result
}
func (r *Renderer) Flush(frameInfo types.FrameInformation) (result []string) {
result = make([]string, 0, len(r.RunningBuffer))
for _, l := range r.RunningBuffer {
l.Name += fmt.Sprintf(" f:%d>%d~%d", l.Start, l.End, l.End-l.Start+1)
l.DropCache()
result = append(result, l.Encode(frameInfo.GetFrameDuration()))
func threadedRenderer(buf []*line.EventLine, duration time.Duration) []string {
if len(buf) == 0 {
return nil
}
results := make([]string, len(buf))
var cnt atomic.Uint64
var wg sync.WaitGroup
for i := 0; i < min(len(buf), runtime.NumCPU()); i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
for {
i := cnt.Add(1) - 1
if i >= uint64(len(buf)) {
break
}
l := buf[i]
l.Name += fmt.Sprintf(" f:%d>%d~%d", l.Start, l.End, l.End-l.Start+1)
l.DropCache()
results[i] = l.Encode(duration)
}
}(i)
}
wg.Wait()
return results
}
func (r *Renderer) Flush(frameInfo types.FrameInformation) (result []string) {
result = threadedRenderer(r.RunningBuffer, frameInfo.GetFrameDuration())
r.RunningBuffer = r.RunningBuffer[:0]
return result
}
func BakeRenderedObjectGradients(o *types.RenderedObject) *types.RenderedObject {
func BakeRenderedObjectsFillables(o *types.RenderedObject) *types.RenderedObject {
var baked bool
drawPathList := make(shapes.DrawPathList, 0, len(o.DrawPathList))
for _, command := range o.DrawPathList {
if fillStyleRecord, ok := command.Style.(*shapes.FillStyleRecord); ok {
if gradient, ok := fillStyleRecord.Fill.(shapes.Gradient); ok {
baked = true
fillClip := types.NewClipPath(command.Commands)
//Convert gradients to many tags
for _, gradientPath := range gradient.GetInterpolatedDrawPaths(settings.GlobalSettings.GradientOverlap, settings.GlobalSettings.GradientBlur, settings.GlobalSettings.GradientSlices) {
gradientClip := types.NewClipPath(gradientPath.Commands)
newPath := shapes.DrawPath{
Style: gradientPath.Style,
Commands: fillClip.Intersect(gradientClip).GetShape(),
}
if len(newPath.Commands.Edges) == 0 {
continue
}
drawPathList = append(drawPathList, newPath)
}
} else {
drawPathList = append(drawPathList, command)
}
if fillStyleRecord, ok := command.Style.(*shapes.FillStyleRecord); ok && !fillStyleRecord.IsFlat() {
baked = true
flattened := fillStyleRecord.Flatten(command.Commands)
drawPathList = append(drawPathList, flattened...)
} else {
drawPathList = append(drawPathList, command)
}

View file

@ -236,13 +236,17 @@ func EventLinesFromRenderObject(frameInfo types.FrameInformation, object *types.
} else {
panic("unsupported")
}
t := tag.ContainerTagFromPathEntry(drawPath, object.Clip, object.ColorTransform, object.MatrixTransform, bakeMatrixTransforms)
if t == nil {
continue
}
out = append(out, &EventLine{
Layer: object.GetDepth(),
ShapeIndex: i,
ObjectId: object.ObjectId,
Start: frameInfo.GetFrameNumber(),
End: frameInfo.GetFrameNumber(),
Tags: []tag.Tag{tag.ContainerTagFromPathEntry(drawPath, object.Clip, object.ColorTransform, object.MatrixTransform, bakeMatrixTransforms)},
Tags: []tag.Tag{t},
Name: fmt.Sprintf("o:%d d:%s", object.ObjectId, object.GetDepth().String()),
Style: style,
})

View file

@ -5,20 +5,18 @@ import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/ass/time"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/settings"
swftypes "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/types"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/records"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
"strings"
)
type ClipTag struct {
BaseDrawingTag
Scale int64
Scale int
IsNull bool
}
func NewClipTag(clip *types.ClipPath, scale int64) *ClipTag {
func NewClipTag(clip *shapes.ClipPath, scale int) *ClipTag {
if clip == nil {
return &ClipTag{
IsNull: true,
@ -52,7 +50,7 @@ func (t *ClipTag) ApplyMatrixTransform(transform math.MatrixTransform, applyTran
}
}
func (t *ClipTag) TransitionClipPath(event Event, clip *types.ClipPath) ClipPathTag {
func (t *ClipTag) TransitionClipPath(event Event, clip *shapes.ClipPath) ClipPathTag {
if clip == nil {
if t.IsNull {
return t
@ -78,10 +76,10 @@ func (t *ClipTag) Encode(event time.EventTime) string {
if t.IsNull {
return ""
}
scaleMultiplier := int64(1 << (t.Scale - 1))
scaleMultiplier := 1 << (t.Scale - 1)
precision := settings.GlobalSettings.ASSDrawingPrecision
if t.Scale >= 5 {
precision = 0
}
return fmt.Sprintf("\\clip(%d,%s)", t.Scale, strings.Join(t.GetCommands(scaleMultiplier, precision), " "))
return fmt.Sprintf("\\clip(%d,%s)", t.Scale, t.GetCommands(scaleMultiplier, precision))
}

View file

@ -4,7 +4,6 @@ import (
"fmt"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/ass/time"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/settings"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
"golang.org/x/exp/maps"
@ -20,7 +19,7 @@ type ContainerTag struct {
}
func (t *ContainerTag) TransitionColor(event Event, transform math.ColorTransform) ColorTag {
container := t.Clone()
container := t.Clone(false)
index := event.GetEnd() - event.GetStart()
@ -47,7 +46,7 @@ func (t *ContainerTag) TransitionMatrixTransform(event Event, transform math.Mat
}
}
container := t.Clone()
container := t.Clone(true)
index := event.GetEnd() - event.GetStart()
@ -71,7 +70,7 @@ func (t *ContainerTag) TransitionMatrixTransform(event Event, transform math.Mat
}
func (t *ContainerTag) TransitionStyleRecord(event Event, record shapes.StyleRecord) StyleTag {
container := t.Clone()
container := t.Clone(false)
index := event.GetEnd() - event.GetStart()
@ -90,7 +89,7 @@ func (t *ContainerTag) TransitionStyleRecord(event Event, record shapes.StyleRec
}
func (t *ContainerTag) TransitionShape(event Event, shape *shapes.Shape) PathTag {
container := t.Clone()
container := t.Clone(false)
index := event.GetEnd() - event.GetStart()
@ -108,8 +107,8 @@ func (t *ContainerTag) TransitionShape(event Event, shape *shapes.Shape) PathTag
return container
}
func (t *ContainerTag) TransitionClipPath(event Event, clip *types.ClipPath) ClipPathTag {
container := t.Clone()
func (t *ContainerTag) TransitionClipPath(event Event, clip *shapes.ClipPath) ClipPathTag {
container := t.Clone(false)
index := event.GetEnd() - event.GetStart()
@ -139,7 +138,7 @@ func (t *ContainerTag) FromStyleRecord(record shapes.StyleRecord) StyleTag {
panic("not supported")
}
func (t *ContainerTag) Clone() *ContainerTag {
func (t *ContainerTag) Clone(cloneTags bool) *ContainerTag {
var transform *math.MatrixTransform
if t.BakeTransforms != nil {
t2 := *t.BakeTransforms
@ -149,15 +148,26 @@ func (t *ContainerTag) Clone() *ContainerTag {
for k := range t.Transitions {
transitions[k] = slices.Clone(t.Transitions[k])
}
return &ContainerTag{
Tags: slices.Clone(t.Tags),
Transitions: transitions,
BakeTransforms: transform,
if cloneTags {
return &ContainerTag{
Tags: slices.Clone(t.Tags),
Transitions: transitions,
BakeTransforms: transform,
}
} else {
return &ContainerTag{
Tags: t.Tags,
Transitions: transitions,
BakeTransforms: transform,
}
}
}
func (t *ContainerTag) Equals(tag Tag) bool {
if o, ok := tag.(*ContainerTag); ok && len(t.Tags) == len(o.Tags) {
if o == t {
return true
}
//TODO: optimize this?
tags := slices.Clone(t.Tags)
otherTags := slices.Clone(o.Tags)
@ -236,16 +246,36 @@ func (t *ContainerTag) TryAppend(tag Tag) {
var identityMatrixTransform = math.IdentityTransform()
func ContainerTagFromPathEntry(path shapes.DrawPath, clip *types.ClipPath, colorTransform math.ColorTransform, matrixTransform math.MatrixTransform, bakeMatrixTransforms bool) *ContainerTag {
func ContainerTagFromPathEntry(path shapes.DrawPath, clip *shapes.ClipPath, colorTransform math.ColorTransform, matrixTransform math.MatrixTransform, bakeMatrixTransforms bool) *ContainerTag {
container := &ContainerTag{
Transitions: make(map[int64][]Tag),
}
if !matrixTransform.EqualsExact(identityMatrixTransform) {
if bakeMatrixTransforms {
path = path.ApplyMatrixTransform(matrixTransform, false)
} else {
/*if path.Clip != nil {
path.Clip = path.Clip.ApplyMatrixTransform(matrixTransform, true)
}*/
}
}
if path.Clip != nil {
if clip != nil {
clip = path.Clip.Intersect(clip)
} else {
clip = path.Clip
}
}
if settings.GlobalSettings.BakeClips {
if clip != nil {
//Clip is given in absolute coordinates. path is relative to translation
translationTransform := math.TranslateTransform(matrixTransform.GetTranslation().Multiply(-1))
path = shapes.DrawPath{
Style: path.Style,
Commands: clip.Intersect(types.NewClipPath(path.Commands)).GetShape(),
Commands: clip.ApplyMatrixTransform(translationTransform, true).ClipShape(path.Commands),
}
}
} else {
@ -259,6 +289,10 @@ func ContainerTagFromPathEntry(path shapes.DrawPath, clip *types.ClipPath, color
}
*/
if len(path.Commands.Edges) == 0 {
return nil
}
container.TryAppend((&BorderTag{}).FromStyleRecord(path.Style))
container.TryAppend((&BlurGaussianTag{}).FromStyleRecord(path.Style))
@ -281,9 +315,6 @@ func ContainerTagFromPathEntry(path shapes.DrawPath, clip *types.ClipPath, color
container.TryAppend((&PositionTag{}).FromMatrixTransform(matrixTransform))
drawTag := DrawingTag(NewDrawTag(path.Commands, settings.GlobalSettings.ASSDrawingScale))
if !matrixTransform.EqualsExact(identityMatrixTransform) {
drawTag = drawTag.ApplyMatrixTransform(matrixTransform, false)
}
container.TryAppend(drawTag)
} else {

View file

@ -6,15 +6,14 @@ import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/settings"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
"strings"
)
type DrawTag struct {
BaseDrawingTag
Scale int64
Scale int
}
func NewDrawTag(shape *shapes.Shape, scale int64) *DrawTag {
func NewDrawTag(shape *shapes.Shape, scale int) *DrawTag {
return &DrawTag{
Scale: scale,
BaseDrawingTag: BaseDrawingTag(*shape),
@ -43,10 +42,10 @@ func (t *DrawTag) Equals(tag Tag) bool {
}
func (t *DrawTag) Encode(event time.EventTime) string {
scaleMultiplier := int64(1 << (t.Scale - 1))
scaleMultiplier := 1 << (t.Scale - 1)
precision := settings.GlobalSettings.ASSDrawingPrecision
if t.Scale >= 5 {
precision = 0
}
return fmt.Sprintf("\\p%d}%s{\\p0", t.Scale, strings.Join(t.GetCommands(scaleMultiplier, precision), " "))
return fmt.Sprintf("\\p%d}%s{\\p0", t.Scale, t.GetCommands(scaleMultiplier, precision))
}

View file

@ -1,64 +1,72 @@
package tag
import (
"fmt"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/records"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
"strings"
"strconv"
)
type DrawingTag interface {
Tag
ApplyMatrixTransform(transform math.MatrixTransform, applyTranslation bool) DrawingTag
AsShape() *shapes.Shape
GetCommands(scale, precision int64) []string
GetCommands(scale, precision int) string
}
type BaseDrawingTag shapes.Shape
func entryToPrecisionAndScaleTag(tag string, scale, precision int64, vectors ...math.Vector2[float64]) string {
result := make([]string, 0, len(vectors)+1)
func entryToPrecisionAndScaleTag(buf []byte, tag string, scale, precision int, vectors ...math.Vector2[float64]) []byte {
if len(buf) > 0 {
buf = append(buf, ' ')
}
if len(tag) > 0 {
result = append(result, tag)
buf = append(buf, tag...)
buf = append(buf, ' ')
}
for _, v := range vectors {
result = append(result, vectorToPrecisionAndScale(scale, precision, v))
for i, v := range vectors {
if i > 0 {
buf = append(buf, ' ')
}
buf = vectorToPrecisionAndScale(buf, scale, precision, v)
}
return strings.Join(result, " ")
return buf
}
func vectorToPrecisionAndScale(scale, precision int64, v math.Vector2[float64]) string {
func vectorToPrecisionAndScale(buf []byte, scale, precision int, v math.Vector2[float64]) []byte {
coords := v.Multiply(float64(scale))
return fmt.Sprintf("%.*f %.*f", precision, coords.X, precision, coords.Y)
buf = strconv.AppendFloat(buf, coords.X, 'f', precision, 64)
buf = append(buf, ' ')
buf = strconv.AppendFloat(buf, coords.Y, 'f', precision, 64)
return buf
}
func (b *BaseDrawingTag) AsShape() *shapes.Shape {
return (*shapes.Shape)(b)
}
func (b *BaseDrawingTag) GetCommands(scale, precision int64) []string {
commands := make([]string, 0, len(b.Edges)*2)
func (b *BaseDrawingTag) GetCommands(scale, precision int) string {
var lastEdge records.Record
commands := make([]byte, 0, len(b.Edges)*2*10)
for _, edge := range b.Edges {
moveRecord, isMoveRecord := edge.(*records.MoveRecord)
if !isMoveRecord {
if lastEdge == nil {
commands = append(commands, entryToPrecisionAndScaleTag("m", scale, precision, edge.GetStart()))
commands = entryToPrecisionAndScaleTag(commands, "m", scale, precision, edge.GetStart())
} else if !lastEdge.GetEnd().Equals(edge.GetStart()) {
commands = append(commands, entryToPrecisionAndScaleTag("m", scale, precision, edge.GetStart()))
commands = entryToPrecisionAndScaleTag(commands, "m", scale, precision, edge.GetStart())
lastEdge = nil
}
}
if isMoveRecord {
commands = append(commands, entryToPrecisionAndScaleTag("m", scale, precision, moveRecord.To))
commands = entryToPrecisionAndScaleTag(commands, "m", scale, precision, moveRecord.To)
} else if lineRecord, ok := edge.(*records.LineRecord); ok {
if _, ok = lastEdge.(*records.LineRecord); ok {
commands = append(commands, entryToPrecisionAndScaleTag("", scale, precision, lineRecord.To))
commands = entryToPrecisionAndScaleTag(commands, "", scale, precision, lineRecord.To)
} else {
commands = append(commands, entryToPrecisionAndScaleTag("l", scale, precision, lineRecord.To))
commands = entryToPrecisionAndScaleTag(commands, "l", scale, precision, lineRecord.To)
}
} else if quadraticRecord, ok := edge.(*records.QuadraticCurveRecord); ok {
edge = records.CubicCurveFromQuadraticRecord(quadraticRecord)
@ -66,9 +74,9 @@ func (b *BaseDrawingTag) GetCommands(scale, precision int64) []string {
if cubicRecord, ok := edge.(*records.CubicCurveRecord); ok {
if _, ok = lastEdge.(*records.CubicCurveRecord); ok {
commands = append(commands, entryToPrecisionAndScaleTag("", scale, precision, cubicRecord.Control1, cubicRecord.Control2, cubicRecord.Anchor))
commands = entryToPrecisionAndScaleTag(commands, "", scale, precision, cubicRecord.Control1, cubicRecord.Control2, cubicRecord.Anchor)
} else {
commands = append(commands, entryToPrecisionAndScaleTag("b", scale, precision, cubicRecord.Control1, cubicRecord.Control2, cubicRecord.Anchor))
commands = entryToPrecisionAndScaleTag(commands, "b", scale, precision, cubicRecord.Control1, cubicRecord.Control2, cubicRecord.Anchor)
}
} else if cubicSplineRecord, ok := edge.(*records.CubicSplineCurveRecord); ok {
_ = cubicSplineRecord
@ -83,5 +91,5 @@ func (b *BaseDrawingTag) GetCommands(scale, precision int64) []string {
$commands[] = "n " . round($coords->x, $precision) . " " . round($coords->y, $precision);
}*/
return commands
return string(commands)
}

View file

@ -17,13 +17,8 @@ func (t *FillColorTag) FromStyleRecord(record shapes.StyleRecord) StyleTag {
if color, ok := fillStyleRecord.Fill.(math.Color); ok {
t.Color = &color
t.OriginalColor = &color
} else if gradient, ok := fillStyleRecord.Fill.(shapes.Gradient); ok {
items := gradient.GetItems()
t.Color = &items[0].Color
t.OriginalColor = &items[0].Color
panic("Gradient fill not supported here")
} else {
panic("not implemented")
panic("not supported")
}
} else {
t.OriginalColor = nil

View file

@ -6,6 +6,7 @@ import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/settings"
math2 "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"math"
"strconv"
)
type PositionTag struct {
@ -89,12 +90,10 @@ func (t *PositionTag) Encode(event time.EventTime) string {
start = event.GetDurationFromStartOffset(t.Start).Milliseconds() - 1
end = event.GetDurationFromStartOffset(t.Start).Milliseconds()
}
//TODO: precision?
return fmt.Sprintf("\\move(%f,%f,%f,%f,%d,%d)", t.From.X, t.From.Y, t.To.X, t.To.Y, start, end)
return fmt.Sprintf("\\move(%s,%s,%s,%s,%d,%d)", strconv.FormatFloat(t.From.X, 'f', 0, 64), strconv.FormatFloat(t.From.Y, 'f', 0, 64), strconv.FormatFloat(t.To.X, 'f', 0, 64), strconv.FormatFloat(t.To.Y, 'f', 0, 64), start, end)
}
//TODO: precision?
return fmt.Sprintf("\\pos(%f,%f)", t.From.X, t.From.Y)
return fmt.Sprintf("\\pos(%s,%s)", strconv.FormatFloat(t.From.X, 'f', 0, 64), strconv.FormatFloat(t.From.Y, 'f', 0, 64))
}
func (t *PositionTag) Equals(tag Tag) bool {

View file

@ -2,7 +2,6 @@ package tag
import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/ass/time"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
)
@ -36,7 +35,7 @@ type PathTag interface {
type ClipPathTag interface {
Tag
TransitionClipPath(event Event, clip *types.ClipPath) ClipPathTag
TransitionClipPath(event Event, clip *shapes.ClipPath) ClipPathTag
}
type ColorTag interface {

1
go.mod
View file

@ -10,6 +10,7 @@ require (
require (
github.com/ctessum/geom v0.2.12
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
golang.org/x/text v0.14.0
gonum.org/v1/gonum v0.14.0

2
go.sum
View file

@ -36,6 +36,8 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/llgcode/draw2d v0.0.0-20180817132918-587a55234ca2/go.mod h1:mVa0dA29Db2S4LVqDYLlsePDzRJLDfdhVZiI15uY0FA=
github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/paulmach/orb v0.1.6/go.mod h1:pPwxxs3zoAyosNSbNKn1jiXV2+oovRDObDKfTvRegDI=
github.com/paulmach/osm v0.1.1/go.mod h1:/UEV7XqKKTG3/46W+MtSmIl81yjV7cGoLkpol3S094I=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=

View file

@ -2,8 +2,11 @@ package main
import (
"errors"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/ass"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/tag"
swftag "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/tag"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
"io"
"os"
"testing"
@ -21,13 +24,15 @@ func TestParser(t *testing.T) {
t.Fatal(err)
}
var tags []swftag.Tag
for {
readTag, err := swfReader.Tag()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
t.Fatal(err)
panic(err)
}
if readTag == nil {
@ -35,7 +40,9 @@ func TestParser(t *testing.T) {
continue
}
if readTag.Code() == tag.RecordEnd {
tags = append(tags, readTag)
if readTag.Code() == swftag.RecordEnd {
break
}
@ -45,4 +52,85 @@ func TestParser(t *testing.T) {
_ = t
}
}
var frameOffset int64
processor := types.NewSWFProcessor(tags, shapes.RectangleFromSWF(swfReader.Header().FrameSize), swfReader.Header().FrameRate.Float64(), int64(swfReader.Header().FrameCount))
assRenderer := ass.NewRenderer(processor.FrameRate, processor.ViewPort)
const KeyFrameEveryNSeconds = 10
keyframeInterval := int64(KeyFrameEveryNSeconds * processor.FrameRate)
var lastFrame *types.FrameInformation
for {
frame := processor.NextFrameOutput()
if frame == nil {
break
}
lastFrame = frame
if !processor.Playing || processor.Loops > 0 {
break
}
if processor.Audio != nil && frameOffset == 0 {
if processor.Audio.Start == nil {
continue
}
frameOffset = *processor.Audio.Start
}
frame.FrameOffset = frameOffset
rendered := frame.Frame.Render(0, nil, nil, nil)
if frame.GetFrameNumber() == 0 {
for _, object := range rendered {
t.Logf("frame 0: object %d depth: %s\n", object.ObjectId, object.Depth.String())
}
}
filteredRendered := make(types.RenderedFrame, 0, len(rendered))
var drawCalls, drawItems, filteredObjects, clipCalls, clipItems int
for _, object := range rendered {
if object.Clip != nil {
clipCalls++
clipItems += len(object.Clip.GetShape().Edges)
}
for _, p := range object.DrawPathList {
drawCalls++
drawItems += len(p.Commands.Edges)
}
filteredRendered = append(filteredRendered, object)
}
t.Logf("=== frame %d/%d ~ %d : Depth count: %d :: Object count: %d :: Paths: %d draw calls, %d items :: Filtered: %d :: Clips %d draw calls, %d items\n",
frame.GetFrameNumber(),
processor.ExpectedFrameCount,
frameOffset,
len(frame.Frame.DepthMap),
len(filteredRendered),
drawCalls,
drawItems,
filteredObjects,
clipCalls,
clipItems,
)
assRenderer.RenderFrame(*frame, filteredRendered)
//TODO: do this per object transition? GlobalSettings?
if frame.GetFrameNumber() > 0 && frame.GetFrameNumber()%keyframeInterval == 0 {
assRenderer.Flush(*frame)
}
}
if lastFrame == nil {
t.Fatal("no frames generated")
}
assRenderer.Flush(*lastFrame)
}

View file

@ -1,16 +1,12 @@
package settings
import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
)
type Settings struct {
// ASSDrawingScale Scale that ASS drawing override tags will use. Coordinates will be multiplied by 2^(ASSDrawingScale-1) to enhance precision
ASSDrawingScale int64
ASSDrawingScale int
// ASSDrawingPrecision Number of decimals that ASS drawing override tags will produce
// Note that at high ASSDrawingScale >= 5 this will be brought down to 0 regardless
ASSDrawingPrecision int64
ASSDrawingPrecision int
// VideoRateMultiplier Adjusts the viewport scale. All operations and transforms will be adjusted accordingly
// For example, VideoScaleMultiplier = 2 will make a 640x480 viewport become 1280x960
@ -41,8 +37,12 @@ type Settings struct {
// GradientBlur Amount of blur to apply to gradients
GradientBlur float64
BitmapPaletteSize int
BitmapMaxDimension int
}
const GradientAutoSlices = -1
const DefaultASSDrawingScale = 6
const DefaultASSDrawingPrecision = 2
@ -58,7 +58,10 @@ var GlobalSettings = Settings{
SmoothTransitions: false,
GradientSlices: shapes.GradientAutoSlices,
GradientSlices: GradientAutoSlices,
GradientOverlap: 2,
GradientBlur: 0.1,
BitmapPaletteSize: 32,
BitmapMaxDimension: 256,
}

View file

@ -1,7 +1,10 @@
package tag
import (
"bytes"
"compress/zlib"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/types"
"io"
)
type DefineBitsJPEG3 struct {
@ -12,6 +15,22 @@ type DefineBitsJPEG3 struct {
BitmapAlphaData types.Bytes
}
func (t *DefineBitsJPEG3) GetAlphaData() []byte {
if len(t.BitmapAlphaData) == 0 {
return nil
}
r, err := zlib.NewReader(bytes.NewReader(t.BitmapAlphaData))
if err != nil {
return nil
}
defer r.Close()
buf, err := io.ReadAll(r)
if err != nil {
return nil
}
return buf
}
func (t *DefineBitsJPEG3) Code() Code {
return RecordDefineBitsJPEG3
}

View file

@ -1,7 +1,10 @@
package tag
import (
"bytes"
"compress/zlib"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/types"
"io"
)
type DefineBitsJPEG4 struct {
@ -13,6 +16,22 @@ type DefineBitsJPEG4 struct {
BitmapAlphaData types.Bytes
}
func (t *DefineBitsJPEG4) GetAlphaData() []byte {
if len(t.BitmapAlphaData) == 0 {
return nil
}
r, err := zlib.NewReader(bytes.NewReader(t.BitmapAlphaData))
if err != nil {
return nil
}
defer r.Close()
buf, err := io.ReadAll(r)
if err != nil {
return nil
}
return buf
}
func (t *DefineBitsJPEG4) Code() Code {
return RecordDefineBitsJPEG4
}

View file

@ -0,0 +1,22 @@
package tag
import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/types"
)
type DefineBitsLossless struct {
_ struct{} `swfFlags:"root"`
CharacterId uint16
Format uint8
Width, Height uint16
ColorTableSize uint8 `swfCondition:"HasColorTableSize()"`
ZlibData types.Bytes
}
func (t *DefineBitsLossless) HasColorTableSize(ctx types.ReaderContext) bool {
return t.Format == 3
}
func (t *DefineBitsLossless) Code() Code {
return RecordDefineBitsLossless
}

View file

@ -0,0 +1,83 @@
package tag
import (
"bytes"
"compress/zlib"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/types"
"image"
"image/color"
"io"
)
type DefineBitsLossless2 struct {
_ struct{} `swfFlags:"root"`
CharacterId uint16
Format uint8
Width, Height uint16
ColorTableSize uint8 `swfCondition:"HasColorTableSize()"`
ZlibData types.Bytes
}
func (t *DefineBitsLossless2) HasColorTableSize(ctx types.ReaderContext) bool {
return t.Format == 3
}
func (t *DefineBitsLossless2) GetImage() image.Image {
r, err := zlib.NewReader(bytes.NewReader(t.ZlibData))
if err != nil {
return nil
}
defer r.Close()
var buf [4]byte
switch t.Format {
case 3: // 8-bit colormapped image
var palette color.Palette
for i := 0; i < (int(t.ColorTableSize) + 1); i++ {
_, err = io.ReadFull(r, buf[:])
if err != nil {
return nil
}
palette = append(palette, color.RGBA{R: buf[0], G: buf[1], B: buf[2], A: buf[3]})
}
im := image.NewPaletted(image.Rectangle{
Min: image.Point{},
Max: image.Point{X: int(t.Width), Y: int(t.Height)},
}, palette)
for x := 0; x < int(t.Width); x++ {
for y := 0; y < int(t.Height); y++ {
_, err = io.ReadFull(r, buf[:1])
if err != nil {
return nil
}
im.SetColorIndex(x, y, buf[0])
}
}
return im
case 5: // 32-bit ARGB image
im := image.NewRGBA(image.Rectangle{
Min: image.Point{},
Max: image.Point{X: int(t.Width), Y: int(t.Height)},
})
for x := 0; x < int(t.Width); x++ {
for y := 0; y < int(t.Height); y++ {
_, err = io.ReadFull(r, buf[:])
if err != nil {
return nil
}
im.SetRGBA(x, y, color.RGBA{R: buf[1], G: buf[2], B: buf[3], A: buf[0]})
}
}
return im
default:
panic("not supported")
}
}
func (t *DefineBitsLossless2) Code() Code {
return RecordDefineBitsLossless2
}

View file

@ -92,6 +92,10 @@ func (r *Record) Decode() (readTag Tag, err error) {
readTag = &DefineMorphShape2{}
case RecordDefineBitsJPEG4:
readTag = &DefineBitsJPEG4{}
case RecordDefineBitsLossless:
readTag = &DefineBitsLossless{}
case RecordDefineBitsLossless2:
readTag = &DefineBitsLossless2{}
case RecordDefineSprite:
readTag = &DefineSprite{}
case RecordDefineSound:
@ -107,8 +111,6 @@ func (r *Record) Decode() (readTag Tag, err error) {
case RecordStartSound2:
readTag = &StartSound2{}
case RecordDefineBitsLossless:
case RecordDefineBitsLossless2:
case RecordExportAssets:
case RecordImportAssets:
case RecordImportAssets2:

View file

@ -39,6 +39,23 @@ const (
FillStyleNonSmoothedClippedBitmap = FillStyleType(0x43)
)
func (t *FillStyleType) SWFRead(r types.DataReader, ctx types.ReaderContext) (err error) {
err = types.ReadU8(r, t)
if err != nil {
return err
}
// Bitmap smoothing only occurs in SWF version 8+.
if ctx.Version < 8 {
switch *t {
case FillStyleClippedBitmap:
*t = FillStyleNonSmoothedClippedBitmap
case FillStyleRepeatingBitmap:
*t = FillStyleNonSmoothedRepeatingBitmap
}
}
return nil
}
type FILLSTYLE struct {
_ struct{} `swfFlags:"root,alignend"`
FillStyleType FillStyleType

View file

@ -106,7 +106,7 @@ func main() {
const KeyFrameEveryNSeconds = 10
keyframeInterval := int64(KeyFrameEveryNSeconds * processor.FrameRate)
keyframeInterval := int64(-1) //int64(KeyFrameEveryNSeconds * processor.FrameRate)
var ks KnownSignature
@ -216,7 +216,7 @@ func main() {
outputLines(assRenderer.RenderFrame(*frame, filteredRendered)...)
//TODO: do this per object transition? GlobalSettings?
if frame.GetFrameNumber() > 0 && frame.GetFrameNumber()%keyframeInterval == 0 {
if frame.GetFrameNumber() > 0 && keyframeInterval != -1 && frame.GetFrameNumber()%keyframeInterval == 0 {
outputLines(assRenderer.Flush(*frame)...)
}

45
types/BitmapDefinition.go Normal file
View file

@ -0,0 +1,45 @@
package types
import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
"image"
)
type BitmapDefinition struct {
ObjectId uint16
ShapeList shapes.DrawPathList
}
func (d *BitmapDefinition) GetObjectId() uint16 {
return d.ObjectId
}
func (d *BitmapDefinition) GetShapeList(ratio float64) (list shapes.DrawPathList) {
return d.ShapeList
}
func (d *BitmapDefinition) GetSafeObject() shapes.ObjectDefinition {
return d
}
func BitmapDefinitionFromSWF(bitmapId uint16, imageData []byte, alphaData []byte) (*BitmapDefinition, error) {
l, err := shapes.ConvertBitmapBytesToDrawPathList(imageData, alphaData)
if err != nil {
return nil, err
}
return &BitmapDefinition{
ObjectId: bitmapId,
ShapeList: l,
}, nil
}
func BitmapDefinitionFromSWFLossless(bitmapId uint16, im image.Image) *BitmapDefinition {
if im == nil {
return nil
}
return &BitmapDefinition{
ObjectId: bitmapId,
ShapeList: shapes.ConvertBitmapToDrawPathList(im),
}
}

View file

@ -78,13 +78,13 @@ func (d *MorphShapeDefinition) GetShapeList(ratio float64) (list shapes.DrawPath
list = append(list, shapes.DrawPathFill(&shapes.FillStyleRecord{
Fill: math2.LerpColor(c1Color, c2FillStyle.Fill.(math2.Color), ratio),
Border: c1FillStyle.Border,
}, &shape))
}, &shape, nil))
} else if c1Gradient, ok := c1FillStyle.Fill.(shapes.Gradient); ok {
//TODO: proper gradients
list = append(list, shapes.DrawPathFill(&shapes.FillStyleRecord{
Fill: math2.LerpColor(c1Gradient.GetItems()[0].Color, c2FillStyle.Fill.(shapes.Gradient).GetItems()[0].Color, ratio),
Border: c1FillStyle.Border,
}, &shape))
}, &shape, nil))
} else {
panic("unsupported")
}
@ -92,7 +92,7 @@ func (d *MorphShapeDefinition) GetShapeList(ratio float64) (list shapes.DrawPath
list = append(list, shapes.DrawPathStroke(&shapes.LineStyleRecord{
Width: math2.Lerp(c1LineStyle.Width, c2LineStyle.Width, ratio),
Color: math2.LerpColor(c1LineStyle.Color, c2LineStyle.Color, ratio),
}, &shape))
}, &shape, nil))
} else {
panic("unsupported")
}
@ -101,17 +101,17 @@ func (d *MorphShapeDefinition) GetShapeList(ratio float64) (list shapes.DrawPath
return list
}
func (d *MorphShapeDefinition) GetSafeObject() ObjectDefinition {
func (d *MorphShapeDefinition) GetSafeObject() shapes.ObjectDefinition {
return d
}
func MorphShapeDefinitionFromSWF(shapeId uint16, startBounds, endBounds shapes.Rectangle[float64], startRecords, endRecords subtypes.SHAPERECORDS, fillStyles subtypes.MORPHFILLSTYLEARRAY, lineStyles subtypes.MORPHLINESTYLEARRAY) *MorphShapeDefinition {
startStyles, endStyles := shapes.StyleListFromSWFMorphItems(fillStyles, lineStyles)
func MorphShapeDefinitionFromSWF(collection shapes.ObjectCollection, shapeId uint16, startBounds, endBounds shapes.Rectangle[float64], startRecords, endRecords subtypes.SHAPERECORDS, fillStyles subtypes.MORPHFILLSTYLEARRAY, lineStyles subtypes.MORPHLINESTYLEARRAY) *MorphShapeDefinition {
startStyles, endStyles := shapes.StyleListFromSWFMorphItems(collection, fillStyles, lineStyles)
start := shapes.DrawPathListFromSWFMorph(startRecords, endRecords, startStyles, false)
start := shapes.DrawPathListFromSWFMorph(collection, startRecords, endRecords, startStyles, false)
//TODO: morph styles properly
_ = endStyles
end := shapes.DrawPathListFromSWFMorph(startRecords, endRecords, startStyles, true)
end := shapes.DrawPathListFromSWFMorph(collection, startRecords, endRecords, startStyles, true)
if len(start) != len(end) {
panic("length does not match")

View file

@ -0,0 +1,8 @@
package types
import "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
type MultiFrameObjectDefinition interface {
shapes.ObjectDefinition
NextFrame() *ViewFrame
}

View file

@ -1,14 +0,0 @@
package types
import "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
type ObjectDefinition interface {
GetObjectId() uint16
GetSafeObject() ObjectDefinition
GetShapeList(ratio float64) shapes.DrawPathList
}
type MultiFrameObjectDefinition interface {
ObjectDefinition
NextFrame() *ViewFrame
}

View file

@ -11,7 +11,7 @@ type RenderedObject struct {
DrawPathList shapes.DrawPathList
Clip *ClipPath
Clip *shapes.ClipPath
ColorTransform math.ColorTransform
MatrixTransform math.MatrixTransform

View file

@ -23,7 +23,7 @@ type SWFProcessor struct {
func NewSWFProcessor(tags []swftag.Tag, viewPort shapes.Rectangle[float64], frameRate float64, frameCount int64) *SWFProcessor {
p := &SWFProcessor{
SWFTreeProcessor: *NewSWFTreeProcessor(0, tags, make(ObjectCollection)),
SWFTreeProcessor: *NewSWFTreeProcessor(0, tags, make(shapes.ObjectCollection)),
Background: &shapes.FillStyleRecord{
Fill: math.Color{
R: 255,
@ -94,7 +94,7 @@ func (p *SWFProcessor) NextFrameOutput() *FrameInformation {
//TODO: actions?
frame.AddChild(BackgroundObjectDepth, NewViewFrame(BackgroundObjectId, &shapes.DrawPathList{shapes.DrawPathFill(p.Background, shapes.NewShape(p.ViewPort.Draw()))}))
frame.AddChild(BackgroundObjectDepth, NewViewFrame(BackgroundObjectId, &shapes.DrawPathList{shapes.DrawPathFill(p.Background, shapes.NewShape(p.ViewPort.Draw()), nil)}))
return &FrameInformation{
FrameNumber: p.Frame - 1,
FrameRate: p.FrameRate,

View file

@ -13,7 +13,7 @@ import (
type SWFTreeProcessor struct {
Layout *ViewLayout
Objects ObjectCollection
Objects shapes.ObjectCollection
Tags []swftag.Tag
@ -28,7 +28,7 @@ type SWFTreeProcessor struct {
processFunc func(actions ActionList) (tag swftag.Tag, newActions ActionList)
}
func NewSWFTreeProcessor(objectId uint16, tags []swftag.Tag, objects ObjectCollection) *SWFTreeProcessor {
func NewSWFTreeProcessor(objectId uint16, tags []swftag.Tag, objects shapes.ObjectCollection) *SWFTreeProcessor {
return &SWFTreeProcessor{
Objects: objects,
Frame: 0,
@ -56,7 +56,7 @@ func (p *SWFTreeProcessor) Process(actions ActionList) (tag swftag.Tag, newActio
return p.process(actions)
}
func (p *SWFTreeProcessor) placeObject(object ObjectDefinition, depth, clipDepth uint16, isMove, hasRatio, hasClipDepth bool, ratio float64, transform *math.MatrixTransform, colorTransform *math.ColorTransform) {
func (p *SWFTreeProcessor) placeObject(object shapes.ObjectDefinition, depth, clipDepth uint16, isMove, hasRatio, hasClipDepth bool, ratio float64, transform *math.MatrixTransform, colorTransform *math.ColorTransform) {
if object == nil {
//TODO: place bogus element
fmt.Printf("Object at depth:%d not found\n", depth)
@ -106,32 +106,32 @@ func (p *SWFTreeProcessor) process(actions ActionList) (tag swftag.Tag, newActio
if p.Loops > 0 {
break
}
p.Objects.Add(MorphShapeDefinitionFromSWF(node.CharacterId, shapes.RectangleFromSWF(node.StartBounds), shapes.RectangleFromSWF(node.EndBounds), node.StartEdges.Records, node.EndEdges.Records, node.MorphFillStyles, node.MorphLineStyles))
p.Objects.Add(MorphShapeDefinitionFromSWF(p.Objects, node.CharacterId, shapes.RectangleFromSWF(node.StartBounds), shapes.RectangleFromSWF(node.EndBounds), node.StartEdges.Records, node.EndEdges.Records, node.MorphFillStyles, node.MorphLineStyles))
case *swftag.DefineMorphShape2:
if p.Loops > 0 {
break
}
p.Objects.Add(MorphShapeDefinitionFromSWF(node.CharacterId, shapes.RectangleFromSWF(node.StartBounds), shapes.RectangleFromSWF(node.EndBounds), node.StartEdges.Records, node.EndEdges.Records, node.MorphFillStyles, node.MorphLineStyles))
p.Objects.Add(MorphShapeDefinitionFromSWF(p.Objects, node.CharacterId, shapes.RectangleFromSWF(node.StartBounds), shapes.RectangleFromSWF(node.EndBounds), node.StartEdges.Records, node.EndEdges.Records, node.MorphFillStyles, node.MorphLineStyles))
case *swftag.DefineShape:
if p.Loops > 0 {
break
}
p.Objects.Add(ShapeDefinitionFromSWF(node.ShapeId, shapes.RectangleFromSWF(node.ShapeBounds), node.Shapes.Records, node.Shapes.FillStyles, node.Shapes.LineStyles))
p.Objects.Add(ShapeDefinitionFromSWF(p.Objects, node.ShapeId, shapes.RectangleFromSWF(node.ShapeBounds), node.Shapes.Records, node.Shapes.FillStyles, node.Shapes.LineStyles))
case *swftag.DefineShape2:
if p.Loops > 0 {
break
}
p.Objects.Add(ShapeDefinitionFromSWF(node.ShapeId, shapes.RectangleFromSWF(node.ShapeBounds), node.Shapes.Records, node.Shapes.FillStyles, node.Shapes.LineStyles))
p.Objects.Add(ShapeDefinitionFromSWF(p.Objects, node.ShapeId, shapes.RectangleFromSWF(node.ShapeBounds), node.Shapes.Records, node.Shapes.FillStyles, node.Shapes.LineStyles))
case *swftag.DefineShape3:
if p.Loops > 0 {
break
}
p.Objects.Add(ShapeDefinitionFromSWF(node.ShapeId, shapes.RectangleFromSWF(node.ShapeBounds), node.Shapes.Records, node.Shapes.FillStyles, node.Shapes.LineStyles))
p.Objects.Add(ShapeDefinitionFromSWF(p.Objects, node.ShapeId, shapes.RectangleFromSWF(node.ShapeBounds), node.Shapes.Records, node.Shapes.FillStyles, node.Shapes.LineStyles))
case *swftag.DefineShape4:
if p.Loops > 0 {
break
}
p.Objects.Add(ShapeDefinitionFromSWF(node.ShapeId, shapes.RectangleFromSWF(node.ShapeBounds), node.Shapes.Records, node.Shapes.FillStyles, node.Shapes.LineStyles))
p.Objects.Add(ShapeDefinitionFromSWF(p.Objects, node.ShapeId, shapes.RectangleFromSWF(node.ShapeBounds), node.Shapes.Records, node.Shapes.FillStyles, node.Shapes.LineStyles))
//TODO: case *swftag.DefineShape5:
case *swftag.DefineSprite:
if p.Loops > 0 {
@ -141,7 +141,51 @@ func (p *SWFTreeProcessor) process(actions ActionList) (tag swftag.Tag, newActio
ObjectId: node.SpriteId,
Processor: NewSWFTreeProcessor(node.SpriteId, node.ControlTags, p.Objects),
})
case *swftag.DefineBits:
if p.Loops > 0 {
break
}
fmt.Printf("Unsupported image: DefineBits\n")
case *swftag.DefineBitsLossless2:
if p.Loops > 0 {
break
}
bitDef := BitmapDefinitionFromSWFLossless(node.CharacterId, node.GetImage())
if bitDef == nil {
fmt.Printf("Unsupported lossless bitmap\n")
break
}
p.Objects.Add(bitDef)
case *swftag.DefineBitsJPEG2:
if p.Loops > 0 {
break
}
bitDef, err := BitmapDefinitionFromSWF(node.CharacterId, node.Data, nil)
if err != nil {
fmt.Printf("Unsupported bitmap: %s\n", err)
break
}
p.Objects.Add(bitDef)
case *swftag.DefineBitsJPEG3:
if p.Loops > 0 {
break
}
bitDef, err := BitmapDefinitionFromSWF(node.CharacterId, node.ImageData, node.GetAlphaData())
if err != nil {
fmt.Printf("Unsupported bitmap: %s\n", err)
break
}
p.Objects.Add(bitDef)
case *swftag.DefineBitsJPEG4:
if p.Loops > 0 {
break
}
bitDef, err := BitmapDefinitionFromSWF(node.CharacterId, node.ImageData, node.GetAlphaData())
if err != nil {
fmt.Printf("Unsupported bitmap: %s\n", err)
break
}
p.Objects.Add(bitDef)
case *swftag.RemoveObject:
//TODO: maybe replicate swftag.RemoveObject2 behavior?
if o := p.Layout.Get(node.Depth); o != nil && o.GetObjectId() == node.CharacterId {
@ -153,7 +197,7 @@ func (p *SWFTreeProcessor) process(actions ActionList) (tag swftag.Tag, newActio
p.Layout.Remove(node.Depth)
case *swftag.PlaceObject:
var object ObjectDefinition
var object shapes.ObjectDefinition
if vl := p.Layout.Get(node.Depth); vl != nil {
object = vl.Object
}
@ -171,7 +215,7 @@ func (p *SWFTreeProcessor) process(actions ActionList) (tag swftag.Tag, newActio
p.placeObject(object, node.Depth, 0, false, false, false, 0, transform, colorTransform)
case *swftag.PlaceObject2:
var object ObjectDefinition
var object shapes.ObjectDefinition
if node.Flag.HasCharacter {
object = p.Objects[node.CharacterId]
} else if vl := p.Layout.Get(node.Depth); vl != nil {
@ -193,7 +237,7 @@ func (p *SWFTreeProcessor) process(actions ActionList) (tag swftag.Tag, newActio
p.placeObject(object, node.Depth, node.ClipDepth, node.Flag.Move, node.Flag.HasRatio, node.Flag.HasClipDepth, float64(node.Ratio)/math2.MaxUint16, transform, colorTransform)
case *swftag.PlaceObject3:
//TODO: handle extra properties
var object ObjectDefinition
var object shapes.ObjectDefinition
if node.Flag.HasCharacter {
object = p.Objects[node.CharacterId]
} else {

View file

@ -19,14 +19,14 @@ func (d *ShapeDefinition) GetShapeList(ratio float64) (list shapes.DrawPathList)
return d.ShapeList
}
func (d *ShapeDefinition) GetSafeObject() ObjectDefinition {
func (d *ShapeDefinition) GetSafeObject() shapes.ObjectDefinition {
return d
}
func ShapeDefinitionFromSWF(shapeId uint16, bounds shapes.Rectangle[float64], records subtypes.SHAPERECORDS, fillStyles subtypes.FILLSTYLEARRAY, lineStyles subtypes.LINESTYLEARRAY) *ShapeDefinition {
styles := shapes.StyleListFromSWFItems(fillStyles, lineStyles)
func ShapeDefinitionFromSWF(collection shapes.ObjectCollection, shapeId uint16, bounds shapes.Rectangle[float64], records subtypes.SHAPERECORDS, fillStyles subtypes.FILLSTYLEARRAY, lineStyles subtypes.LINESTYLEARRAY) *ShapeDefinition {
styles := shapes.StyleListFromSWFItems(collection, fillStyles, lineStyles)
drawPathList := shapes.DrawPathListFromSWF(records, styles)
drawPathList := shapes.DrawPathListFromSWF(collection, records, styles)
return &ShapeDefinition{
ObjectId: shapeId,

View file

@ -33,7 +33,7 @@ func (d *SpriteDefinition) NextFrame() *ViewFrame {
return d.CurrentFrame
}
func (d *SpriteDefinition) GetSafeObject() ObjectDefinition {
func (d *SpriteDefinition) GetSafeObject() shapes.ObjectDefinition {
return &SpriteDefinition{
ObjectId: d.ObjectId,
Processor: NewSWFTreeProcessor(d.ObjectId, d.Processor.Tags, d.Processor.Objects),

View file

@ -85,7 +85,7 @@ func (f *ViewFrame) Render(baseDepth uint16, depthChain Depth, parentColor *math
})
} else {
clipMap := make(map[uint16]*ViewFrame)
clipPaths := make(map[uint16]*ClipPath)
clipPaths := make(map[uint16]*shapes.ClipPath)
matrixTransform := &matrixTransform
if matrixTransform.IsIdentity() {
@ -102,12 +102,16 @@ func (f *ViewFrame) Render(baseDepth uint16, depthChain Depth, parentColor *math
frame := f.DepthMap[depth]
if frame.IsClipping { //Process clips as they come
clipMap[depth] = frame
var clipPath *ClipPath
var clipPath *shapes.ClipPath
for _, clipObject := range frame.Render(depth, depthChain, colorTransform, matrixTransform) {
clipShape := NewClipPath(nil)
clipShape := shapes.NewClipPath(nil)
for _, p := range clipObject.DrawPathList {
if _, ok := p.Style.(*shapes.FillStyleRecord); ok { //Only clip with fills TODO: is this correct?
clipShape.AddShape(p.Commands)
if p.Clip != nil {
clipShape.AddShape(p.Clip.ClipShape(p.Commands))
} else {
clipShape.AddShape(p.Commands)
}
}
}
@ -137,7 +141,7 @@ func (f *ViewFrame) Render(baseDepth uint16, depthChain Depth, parentColor *math
if frame.IsClipping { //Already processed
continue
}
var clipPath *ClipPath
var clipPath *shapes.ClipPath
for _, clipDepth := range clipMapKeys {
clip := clipMap[clipDepth]

View file

@ -3,6 +3,7 @@ package types
import (
"fmt"
math2 "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
"golang.org/x/exp/maps"
"math"
"slices"
@ -13,7 +14,7 @@ type ViewLayout struct {
DepthMap map[uint16]*ViewLayout
Object ObjectDefinition
Object shapes.ObjectDefinition
ColorTransform *math2.ColorTransform
MatrixTransform *math2.MatrixTransform
@ -24,14 +25,14 @@ type ViewLayout struct {
ClipDepth uint16
}
func NewClippingViewLayout(objectId, clipDepth uint16, object ObjectDefinition, parent *ViewLayout) *ViewLayout {
func NewClippingViewLayout(objectId, clipDepth uint16, object shapes.ObjectDefinition, parent *ViewLayout) *ViewLayout {
l := NewViewLayout(objectId, object, parent)
l.IsClipping = true
l.ClipDepth = clipDepth
return l
}
func NewViewLayout(objectId uint16, object ObjectDefinition, parent *ViewLayout) *ViewLayout {
func NewViewLayout(objectId uint16, object shapes.ObjectDefinition, parent *ViewLayout) *ViewLayout {
if object != nil && object.GetObjectId() != objectId {
panic("logic error")
}

View file

@ -5,10 +5,45 @@ import (
"math"
)
type PackedColor uint32
func NewPackedColor(r, g, b, a uint8) PackedColor {
return PackedColor((uint64(a) << 24) | (uint64(r) << 16) | (uint64(g) << 8) | uint64(b))
}
func (c PackedColor) Alpha() uint8 {
return uint8((c >> 24) & 0xFF)
}
func (c PackedColor) R() uint8 {
return uint8((c >> 16) & 0xFF)
}
func (c PackedColor) G() uint8 {
return uint8((c >> 8) & 0xFF)
}
func (c PackedColor) B() uint8 {
return uint8(c & 0xFF)
}
func (c PackedColor) Color() Color {
return Color{
R: c.R(),
G: c.G(),
B: c.B(),
Alpha: c.Alpha(),
}
}
type Color struct {
R, G, B, Alpha uint8
}
func (c Color) Packed() PackedColor {
return NewPackedColor(c.R, c.G, c.B, c.Alpha)
}
func (c Color) Equals(o Color, alpha bool) bool {
if !alpha {
return c.R == o.R && c.G == o.G && c.B == o.B

View file

@ -128,6 +128,17 @@ func (m MatrixTransform) GetTranslation() Vector2[float64] {
return m.ApplyToVector(NewVector2[float64](0, 0), true)
}
func (m MatrixTransform) Inverse() *MatrixTransform {
var r mat.Dense
err := r.Inverse(m.matrix)
if err != nil {
return nil
}
return &MatrixTransform{
matrix: &r,
}
}
func MatrixTransformApplyToVector[T ~int64 | ~float64](m MatrixTransform, v Vector2[T], applyTranslation bool) Vector2[T] {
return Vector2ToType[float64, T](m.ApplyToVector(v.Float64(), applyTranslation))
}

View file

@ -0,0 +1,395 @@
package shapes
import (
"bytes"
"encoding/binary"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/settings"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"github.com/ctessum/geom"
"github.com/nfnt/resize"
"golang.org/x/exp/maps"
"image"
"image/color"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"runtime"
"slices"
"sync"
"sync/atomic"
)
func ConvertBitmapBytesToDrawPathList(imageData []byte, alphaData []byte) (DrawPathList, error) {
var im image.Image
var err error
for i, s := range bitmapHeaderFormats {
if bytes.Compare(s, imageData[:len(s)]) == 0 {
if i == 0 || i == 1 {
//jpeg
//remove invalid data
jpegData := removeInvalidJPEGData(imageData)
im, _, err = image.Decode(bytes.NewReader(jpegData))
if im != nil {
size := im.Bounds().Size()
if len(alphaData) == size.X*size.Y {
newIm := image.NewRGBA(im.Bounds())
for x := 0; x < size.X; x++ {
for y := 0; y < size.Y; y++ {
rI, gI, bI, _ := im.At(x, y).RGBA()
// The JPEG data should be premultiplied alpha, but it isn't in some incorrect SWFs.
// This means 0% alpha pixels may have color and incorrectly show as visible.
// Flash Player clamps color to the alpha value to fix this case.
// Only applies to DefineBitsJPEG3; DefineBitsLossless does not seem to clamp.
a := alphaData[y*size.X+x]
if a != 0 {
runtime.KeepAlive(a)
}
r := min(uint8(rI>>8), a)
g := min(uint8(gI>>8), a)
b := min(uint8(bI>>8), a)
newIm.SetRGBA(x, y, color.RGBA{
R: r,
G: g,
B: b,
A: a,
})
}
}
im = newIm
}
}
} else if i == 2 {
//png
im, _, err = image.Decode(bytes.NewReader(imageData))
} else if i == 3 {
//gif
im, _, err = image.Decode(bytes.NewReader(imageData))
}
break
}
}
if err != nil {
return nil, err
}
drawPathList := ConvertBitmapToDrawPathList(im)
return drawPathList, nil
}
func QuantizeBitmap(i image.Image) image.Image {
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 {
newIm.SetRGBA(x, y, color.RGBA{
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: uint8((a >> 12) << 4),
})
}
}
}
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 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 x atomic.Uint64
results := make([]map[math.PackedColor]geom.Polygonal, runtime.NumCPU())
for n := 0; n < runtime.NumCPU(); n++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
myResults := make(map[math.PackedColor]geom.Polygonal)
for {
iX := x.Add(1) - 1
if iX >= uint64(size.X) {
break
}
for y := 0; y < size.Y; y++ {
r, g, b, a := i.At(int(iX), y).RGBA()
p := math.NewPackedColor(uint8(r>>8), uint8(g>>8), uint8(b>>8), uint8(a>>8))
poly := geom.Polygon{{
{float64(iX), float64(y)},
{float64(iX), float64(y + 1)},
{float64(iX + 1), float64(y + 1)},
{float64(iX + 1), float64(y)},
}}
if existingColor, ok := myResults[p]; ok {
u := existingColor.Union(poly).Simplify(0.01).(geom.Polygonal)
myResults[p] = u
} else {
myResults[p] = poly
}
}
}
results[n] = myResults
}(n)
}
wg.Wait()
var hasAlpha bool
colors := make(map[math.PackedColor]geom.Polygonal)
for _, r := range results {
for k, c := range r {
if k.Alpha() < 255 {
hasAlpha = true
}
if k.Alpha() == 0 {
//Skip fully transparent pixels
continue
}
if existingColor, ok := colors[k]; ok {
u := existingColor.Union(c).Simplify(0.01).(geom.Polygonal)
colors[k] = u
} else {
colors[k] = c.Simplify(0.01).(geom.Polygonal)
}
}
}
//Sort from the highest area to lowest
keys := maps.Keys(colors)
slices.SortFunc(keys, func(a, b math.PackedColor) int {
areaA := colors[a].Area()
areaB := colors[b].Area()
if areaA > areaB {
return -1
} else if areaB > areaA {
return 1
} else {
return 0
}
})
// Full shape optimizations when alpha is not in use
if !hasAlpha {
/*
for i, k := range keys {
pol := colors[k]
pol1 := pol
//Iterate through all previous layers and merge
for _, k2 := range keys[:i] {
//Check each sub-polygon of the shape to see if it is within previous indicative of a good merge, merge only those
for _, pol4 := range colors[k2].Union(pol).Polygons() {
if pol4.Bounds().Within(pol) == geom.Inside {
pol = pol.Union(pol4)
}
}
}
//Draw resulting shape
r = append(r, DrawPathFill(&FillStyleRecord{
Fill: k.Color(),
}, ComplexPolygon{
Pol: pol.Simplify(0.01).(geom.Polygonal),
}.GetShape(), nil))
}*/
//make a rectangle covering the whole first area to optimize this case
r = append(r, DrawPathFill(&FillStyleRecord{
Fill: keys[0].Color(),
}, NewShape(Rectangle[float64]{
TopLeft: math.NewVector2[float64](0, 0),
BottomRight: math.NewVector2(float64(size.X+1), float64(size.Y+1)),
}.Draw()), nil))
for _, k := range keys[1:] {
pol := colors[k]
//Draw resulting shape
r = append(r, DrawPathFill(&FillStyleRecord{
Fill: k.Color(),
}, ComplexPolygon{
Pol: pol,
}.GetShape(), nil))
}
} else {
for _, k := range keys {
pol := colors[k]
//Draw resulting shape
r = append(r, DrawPathFill(&FillStyleRecord{
Fill: k.Color(),
}, ComplexPolygon{
Pol: pol,
}.GetShape(), nil))
}
}
scale := math.ScaleTransform(math.NewVector2(ratioX, ratioY))
r2 := r.ApplyMatrixTransform(scale, true)
return r2
}
var bitmapHeaderJPEG = []byte{0xff, 0xd8}
var bitmapHeaderJPEGInvalid = []byte{0xff, 0xd9, 0xff, 0xd8}
var bitmapHeaderPNG = []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}
var bitmapHeaderGIF = []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61}
var bitmapHeaderFormats = [][]byte{
bitmapHeaderJPEG,
bitmapHeaderJPEGInvalid,
bitmapHeaderPNG,
bitmapHeaderGIF,
}
// removeInvalidJPEGData
// SWF19 errata p.138:
// "Before version 8 of the SWF file format, SWF files could contain an erroneous header of 0xFF, 0xD9, 0xFF, 0xD8
// before the JPEG SOI marker."
// 0xFFD9FFD8 is a JPEG EOI+SOI marker pair. Contrary to the spec, this invalid marker sequence can actually appear
// at any time before the 0xFFC0 SOF marker, not only at the beginning of the data. I believe this is a relic from
// the SWF JPEGTables tag, which stores encoding tables separately from the DefineBits image data, encased in its
// own SOI+EOI pair. When these data are glued together, an interior EOI+SOI sequence is produced. The Flash JPEG
// decoder expects this pair and ignores it, despite standard JPEG decoders stopping at the EOI.
// When DefineBitsJPEG2 etc. were introduced, the Flash encoders/decoders weren't properly adjusted, resulting in
// this sequence persisting. Also, despite what the spec says, this doesn't appear to be version checked (e.g., a
// v9 SWF can contain one of these malformed JPEGs and display correctly).
// See https://github.com/ruffle-rs/ruffle/issues/8775 for various examples.
func removeInvalidJPEGData(data []byte) (buf []byte) {
const SOF0 uint8 = 0xC0 // Start of frame
const RST0 uint8 = 0xD0 // Restart (we shouldn't see this before SOS, but just in case)
const RST1 uint8 = 0xD0
const RST2 uint8 = 0xD0
const RST3 uint8 = 0xD0
const RST4 uint8 = 0xD0
const RST5 uint8 = 0xD0
const RST6 uint8 = 0xD0
const RST7 uint8 = 0xD7
const SOI uint8 = 0xD8 // Start of image
const EOI uint8 = 0xD9 // End of image
if bytes.HasPrefix(data, bitmapHeaderJPEGInvalid) {
data = bytes.TrimPrefix(data, bitmapHeaderJPEGInvalid)
} else {
// Parse the JPEG markers searching for the 0xFFD9FFD8 marker sequence to splice out.
// We only have to search up to the SOF0 marker.
// This might be another case where eventually we want to write our own full JPEG decoder to match Flash's decoder.
jpegData := data
var pos int
for {
if len(jpegData) < 4 {
break
}
var payloadLength int
if bytes.Compare([]byte{0xFF, EOI, 0xFF, SOI}, jpegData[:4]) == 0 {
// Invalid EOI+SOI sequence found, splice it out.
data = slices.Delete(slices.Clone(data), pos, pos+4)
break
} else if bytes.Compare([]byte{0xFF, EOI}, jpegData[:2]) == 0 { // EOI, SOI, RST markers do not include a size.
} else if bytes.Compare([]byte{0xFF, SOI}, jpegData[:2]) == 0 {
} else if bytes.Compare([]byte{0xFF, RST0}, jpegData[:2]) == 0 {
} else if bytes.Compare([]byte{0xFF, RST1}, jpegData[:2]) == 0 {
} else if bytes.Compare([]byte{0xFF, RST2}, jpegData[:2]) == 0 {
} else if bytes.Compare([]byte{0xFF, RST3}, jpegData[:2]) == 0 {
} else if bytes.Compare([]byte{0xFF, RST4}, jpegData[:2]) == 0 {
} else if bytes.Compare([]byte{0xFF, RST5}, jpegData[:2]) == 0 {
} else if bytes.Compare([]byte{0xFF, RST6}, jpegData[:2]) == 0 {
} else if bytes.Compare([]byte{0xFF, RST7}, jpegData[:2]) == 0 {
} else if bytes.Compare([]byte{0xFF, SOF0}, jpegData[:2]) == 0 {
// No invalid sequence found before SOF marker, return data as-is.
break
} else if jpegData[0] == 0xFF {
// Other tags include a length.
payloadLength = int(binary.BigEndian.Uint16(jpegData[2:]))
} else {
// All JPEG markers should start with 0xFF.
// So this is either not a JPEG, or we screwed up parsing the markers. Bail out.
break
}
if len(jpegData) < payloadLength+2 {
break
}
jpegData = jpegData[payloadLength+2:]
pos += payloadLength + 2
}
}
// Some JPEGs are missing the final EOI marker (JPEG optimizers truncate it?)
// Flash and most image decoders will still display these images, but jpeg-decoder errors.
// Glue on an EOI marker if its not already there and hope for the best.
if bytes.HasSuffix(data, []byte{0xff, EOI}) {
return data
} else {
//JPEG is missing EOI marker and may not decode properly
return append(slices.Clone(data), []byte{0xff, EOI}...)
}
}

View file

@ -1,35 +1,34 @@
package types
package shapes
import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
"github.com/ctessum/geom"
)
type ClipPath struct {
Clip shapes.ComplexPolygon
Clip ComplexPolygon
}
func NewClipPath(shape *shapes.Shape) *ClipPath {
func NewClipPath(shape *Shape) *ClipPath {
if shape == nil {
return &ClipPath{
Clip: shapes.ComplexPolygon{
Pol: shapes.NewPolygonFromShape(&shapes.Shape{}),
Clip: ComplexPolygon{
Pol: NewPolygonFromShape(&Shape{}),
},
}
}
return &ClipPath{
Clip: shapes.ComplexPolygon{
Pol: shapes.NewPolygonFromShape(shape),
Clip: ComplexPolygon{
Pol: NewPolygonFromShape(shape),
},
}
}
func (c *ClipPath) AddShape(shape *shapes.Shape) {
c.Clip.Pol = c.Clip.Pol.Union(shapes.NewPolygonFromShape(shape))
func (c *ClipPath) AddShape(shape *Shape) {
c.Clip.Pol = c.Clip.Pol.Union(NewPolygonFromShape(shape))
}
func (c *ClipPath) GetShape() *shapes.Shape {
func (c *ClipPath) GetShape() *Shape {
return c.Clip.GetShape()
}
@ -45,7 +44,7 @@ func (c *ClipPath) ApplyMatrixTransform(transform math.MatrixTransform, applyTra
panic("invalid result")
} else {
return &ClipPath{
Clip: shapes.ComplexPolygon{
Clip: ComplexPolygon{
Pol: newPol,
},
}
@ -58,6 +57,12 @@ func (c *ClipPath) Merge(o *ClipPath) *ClipPath {
}
}
func (c *ClipPath) ClipShape(o *Shape) *Shape {
return c.Clip.Intersect(ComplexPolygon{
Pol: NewPolygonFromShape(o),
}).GetShape()
}
func (c *ClipPath) Intersect(o *ClipPath) *ClipPath {
return &ClipPath{
Clip: c.Clip.Intersect(o.Clip),

View file

@ -27,7 +27,7 @@ const PolygonSimplifyTolerance = 0.01
func (p ComplexPolygon) GetShape() (r *Shape) {
var edges []records.Record
for _, pol := range p.Pol.Polygons() {
for _, path := range pol {
for _, path := range pol.Simplify(0.01).(geom.Polygon) {
//pol = pol.Simplify(PolygonSimplifyTolerance).(geom.Polygon)
edges = append(edges, &records.LineRecord{
To: math.NewVector2(path[1].X, path[1].Y),

View file

@ -1,20 +1,39 @@
package shapes
import "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
type DrawPath struct {
Style StyleRecord
Clip *ClipPath
Commands *Shape
}
func DrawPathFill(record *FillStyleRecord, shape *Shape) DrawPath {
func (p DrawPath) ApplyMatrixTransform(transform math.MatrixTransform, applyTranslation bool) (r DrawPath) {
if p.Clip == nil {
return DrawPath{
Style: p.Style,
Commands: p.Commands.ApplyMatrixTransform(transform, applyTranslation),
}
}
return DrawPath{
Style: record,
Commands: shape,
Style: p.Style,
Commands: p.Commands.ApplyMatrixTransform(transform, applyTranslation),
Clip: p.Clip.ApplyMatrixTransform(transform, applyTranslation),
}
}
func DrawPathStroke(record *LineStyleRecord, shape *Shape) DrawPath {
func DrawPathFill(record *FillStyleRecord, shape *Shape, clip *ClipPath) DrawPath {
return DrawPath{
Style: record,
Commands: shape,
Clip: clip,
}
}
func DrawPathStroke(record *LineStyleRecord, shape *Shape, clip *ClipPath) DrawPath {
return DrawPath{
Style: record,
Commands: shape,
Clip: clip,
}
}

View file

@ -2,6 +2,7 @@ package shapes
import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/tag/subtypes"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
)
type DrawPathList []DrawPath
@ -13,15 +14,60 @@ func (l DrawPathList) Merge(b DrawPathList) DrawPathList {
return newList
}
func DrawPathListFromSWF(records subtypes.SHAPERECORDS, styles StyleList) DrawPathList {
converter := NewShapeConverter(records, styles)
func (l DrawPathList) ApplyFunction(f func(p DrawPath) DrawPath) (r DrawPathList) {
r = make(DrawPathList, 0, len(l))
for _, e := range l {
r = append(r, f(e))
}
return r
}
func (l DrawPathList) Fill(shape *Shape) (r DrawPathList) {
clipShape := NewClipPath(shape)
//Convert paths to many tags using intersections
for _, innerPath := range l {
newPath := DrawPath{
Style: innerPath.Style,
Commands: innerPath.Commands,
Clip: clipShape,
}
if len(newPath.Commands.Edges) == 0 {
continue
}
r = append(r, newPath)
}
return r
}
func (l DrawPathList) ApplyColorTransform(transform math.ColorTransform) Fillable {
r := make(DrawPathList, 0, len(l))
for _, e := range l {
r = append(r, DrawPath{
Style: e.Style.ApplyColorTransform(transform),
Commands: e.Commands,
})
}
return r
}
func (l DrawPathList) ApplyMatrixTransform(transform math.MatrixTransform, applyTranslation bool) (r DrawPathList) {
r = make(DrawPathList, 0, len(l))
for i := range l {
r = append(r, l[i].ApplyMatrixTransform(transform, applyTranslation))
}
return r
}
func DrawPathListFromSWF(collection ObjectCollection, records subtypes.SHAPERECORDS, styles StyleList) DrawPathList {
converter := NewShapeConverter(collection, records, styles)
converter.Convert(false)
return converter.Commands
}
func DrawPathListFromSWFMorph(startRecords, endRecords subtypes.SHAPERECORDS, styles StyleList, flip bool) DrawPathList {
converter := NewMorphShapeConverter(startRecords, endRecords, styles)
func DrawPathListFromSWFMorph(collection ObjectCollection, startRecords, endRecords subtypes.SHAPERECORDS, styles StyleList, flip bool) DrawPathList {
converter := NewMorphShapeConverter(collection, startRecords, endRecords, styles)
converter.Convert(flip)
return converter.Commands

View file

@ -0,0 +1,24 @@
package shapes
import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/types"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
)
type DrawPathListFill DrawPathList
func (f DrawPathListFill) ApplyColorTransform(transform math.ColorTransform) Fillable {
return DrawPathListFill(DrawPathList(f).ApplyColorTransform(transform).(DrawPathList))
}
func (f DrawPathListFill) Fill(shape *Shape) DrawPathList {
return DrawPathList(f)
}
func DrawPathListFillFromSWF(l DrawPathList, transform types.MATRIX) DrawPathListFill {
// shape is already in pixel world, but matrix comes as twip
baseScale := math.ScaleTransform(math.NewVector2[float64](1./types.TwipFactor, 1./types.TwipFactor))
t := math.MatrixTransformFromSWF(transform).Multiply(baseScale)
return DrawPathListFill(l.ApplyMatrixTransform(t, true))
}

View file

@ -1,6 +1,7 @@
package shapes
import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/settings"
swfsubtypes "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/tag/subtypes"
swftypes "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/types"
math2 "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
@ -8,15 +9,14 @@ import (
"slices"
)
const GradientAutoSlices = -1
type Gradient interface {
Fillable
GetSpreadMode() swfsubtypes.GradientSpreadMode
GetInterpolationMode() swfsubtypes.GradientInterpolationMode
GetItems() []GradientItem
GetInterpolatedDrawPaths(overlap, blur float64, gradientSlices int) DrawPathList
GetMatrixTransform() math2.MatrixTransform
ApplyColorTransform(transform math2.ColorTransform) Gradient
ApplyColorTransform(transform math2.ColorTransform) Fillable
}
type GradientItem struct {
@ -82,7 +82,7 @@ func LerpGradient(gradient Gradient, gradientSlices int) (result []GradientSlice
var partitions int
if maxColorDistance < math.SmallestNonzeroFloat64 {
partitions = 1
} else if gradientSlices == GradientAutoSlices {
} else if gradientSlices == settings.GradientAutoSlices {
partitions = max(1, int(math.Ceil(min(GradientRatioDivisor/float64(len(items)+1), max(1, math.Ceil(maxColorDistance))))))
} else {
partitions = max(1, int(math.Ceil((distance/GradientRatioDivisor)*float64(gradientSlices))))

View file

@ -1,6 +1,7 @@
package shapes
import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/settings"
swfsubtypes "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/tag/subtypes"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/types"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
@ -15,19 +16,19 @@ type LinearGradient struct {
InterpolationMode swfsubtypes.GradientInterpolationMode
}
func (g *LinearGradient) GetSpreadMode() swfsubtypes.GradientSpreadMode {
func (g LinearGradient) GetSpreadMode() swfsubtypes.GradientSpreadMode {
return g.SpreadMode
}
func (g *LinearGradient) GetInterpolationMode() swfsubtypes.GradientInterpolationMode {
func (g LinearGradient) GetInterpolationMode() swfsubtypes.GradientInterpolationMode {
return g.InterpolationMode
}
func (g *LinearGradient) GetItems() []GradientItem {
func (g LinearGradient) GetItems() []GradientItem {
return g.Colors
}
func (g *LinearGradient) GetInterpolatedDrawPaths(overlap, blur float64, gradientSlices int) DrawPathList {
func (g LinearGradient) GetInterpolatedDrawPaths(overlap, blur float64, gradientSlices int) DrawPathList {
//items is max size 8 to 15 depending on SWF version
size := GradientBounds.Width()
@ -43,18 +44,19 @@ func (g *LinearGradient) GetInterpolatedDrawPaths(overlap, blur float64, gradien
NewShape(Rectangle[float64]{
TopLeft: math.NewVector2(GradientBounds.TopLeft.X+item.Start*size-overlap/2, GradientBounds.TopLeft.Y),
BottomRight: math.NewVector2(GradientBounds.TopLeft.X+item.End*size+overlap/2, GradientBounds.BottomRight.Y),
}.Draw()).ApplyMatrixTransform(g.Transform, true),
))
}.Draw()),
nil, //TODO: clip here instead of outside
).ApplyMatrixTransform(g.Transform, true))
}
return paths
}
func (g *LinearGradient) GetMatrixTransform() math.MatrixTransform {
func (g LinearGradient) GetMatrixTransform() math.MatrixTransform {
return g.Transform
}
func (g *LinearGradient) ApplyColorTransform(transform math.ColorTransform) Gradient {
g2 := *g
func (g LinearGradient) ApplyColorTransform(transform math.ColorTransform) Fillable {
g2 := g
g2.Colors = slices.Clone(g2.Colors)
for i, g := range g2.Colors {
g2.Colors[i] = GradientItem{
@ -65,7 +67,11 @@ func (g *LinearGradient) ApplyColorTransform(transform math.ColorTransform) Grad
return &g2
}
func LinearGradientFromSWF(records []swfsubtypes.GRADRECORD, transform types.MATRIX, spreadMode swfsubtypes.GradientSpreadMode, interpolationMode swfsubtypes.GradientInterpolationMode) *LinearGradient {
func (g LinearGradient) Fill(shape *Shape) DrawPathList {
return g.GetInterpolatedDrawPaths(settings.GlobalSettings.GradientOverlap, settings.GlobalSettings.GradientBlur, settings.GlobalSettings.GradientSlices).Fill(shape)
}
func LinearGradientFromSWF(records []swfsubtypes.GRADRECORD, transform types.MATRIX, spreadMode swfsubtypes.GradientSpreadMode, interpolationMode swfsubtypes.GradientInterpolationMode) DrawPathListFill {
items := make([]GradientItem, 0, len(records))
for _, r := range records {
items = append(items, GradientItemFromSWF(r.Ratio, r.Color))
@ -73,10 +79,11 @@ func LinearGradientFromSWF(records []swfsubtypes.GRADRECORD, transform types.MAT
//TODO: interpolationMode, spreadMode
return &LinearGradient{
Colors: items,
return DrawPathListFill(LinearGradient{
Colors: items,
//TODO: do we need to scale this to pixel world from twips?
Transform: math.MatrixTransformFromSWF(transform),
SpreadMode: spreadMode,
InterpolationMode: interpolationMode,
}
}.GetInterpolatedDrawPaths(settings.GlobalSettings.GradientOverlap, settings.GlobalSettings.GradientBlur, settings.GlobalSettings.GradientSlices))
}

View file

@ -1,6 +1,8 @@
package types
package shapes
import "golang.org/x/exp/maps"
import (
"golang.org/x/exp/maps"
)
type ObjectCollection map[uint16]ObjectDefinition
@ -10,6 +12,10 @@ func (o ObjectCollection) Clone() ObjectCollection {
return m
}
func (o ObjectCollection) Get(objectId uint16) ObjectDefinition {
return o[objectId]
}
func (o ObjectCollection) Add(def ObjectDefinition) {
if _, ok := o[def.GetObjectId()]; ok {
panic("object already exists")

View file

@ -0,0 +1,7 @@
package shapes
type ObjectDefinition interface {
GetObjectId() uint16
GetSafeObject() ObjectDefinition
GetShapeList(ratio float64) DrawPathList
}

View file

@ -1,6 +1,7 @@
package shapes
import (
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/settings"
swfsubtypes "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/tag/subtypes"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/types"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
@ -15,19 +16,19 @@ type RadialGradient struct {
InterpolationMode swfsubtypes.GradientInterpolationMode
}
func (g *RadialGradient) GetSpreadMode() swfsubtypes.GradientSpreadMode {
func (g RadialGradient) GetSpreadMode() swfsubtypes.GradientSpreadMode {
return g.SpreadMode
}
func (g *RadialGradient) GetInterpolationMode() swfsubtypes.GradientInterpolationMode {
func (g RadialGradient) GetInterpolationMode() swfsubtypes.GradientInterpolationMode {
return g.InterpolationMode
}
func (g *RadialGradient) GetItems() []GradientItem {
func (g RadialGradient) GetItems() []GradientItem {
return g.Colors
}
func (g *RadialGradient) GetInterpolatedDrawPaths(overlap, blur float64, gradientSlices int) DrawPathList {
func (g RadialGradient) GetInterpolatedDrawPaths(overlap, blur float64, gradientSlices int) DrawPathList {
//items is max size 8 to 15 depending on SWF version
size := GradientBounds.Width()
@ -45,17 +46,18 @@ func (g *RadialGradient) GetInterpolatedDrawPaths(overlap, blur float64, gradien
Blur: blur,
},
shape.ApplyMatrixTransform(g.Transform, true),
))
nil, //TODO: clip here instead of outside
).ApplyMatrixTransform(g.Transform, true))
}
return paths
}
func (g *RadialGradient) GetMatrixTransform() math.MatrixTransform {
func (g RadialGradient) GetMatrixTransform() math.MatrixTransform {
return g.Transform
}
func (g *RadialGradient) ApplyColorTransform(transform math.ColorTransform) Gradient {
g2 := *g
func (g RadialGradient) ApplyColorTransform(transform math.ColorTransform) Fillable {
g2 := g
g2.Colors = slices.Clone(g2.Colors)
for i, g := range g2.Colors {
g2.Colors[i] = GradientItem{
@ -66,7 +68,11 @@ func (g *RadialGradient) ApplyColorTransform(transform math.ColorTransform) Grad
return &g2
}
func RadialGradientFromSWF(records []swfsubtypes.GRADRECORD, transform types.MATRIX, spreadMode swfsubtypes.GradientSpreadMode, interpolationMode swfsubtypes.GradientInterpolationMode) *RadialGradient {
func (g RadialGradient) Fill(shape *Shape) DrawPathList {
return g.GetInterpolatedDrawPaths(settings.GlobalSettings.GradientOverlap, settings.GlobalSettings.GradientBlur, settings.GlobalSettings.GradientSlices).Fill(shape)
}
func RadialGradientFromSWF(records []swfsubtypes.GRADRECORD, transform types.MATRIX, spreadMode swfsubtypes.GradientSpreadMode, interpolationMode swfsubtypes.GradientInterpolationMode) DrawPathListFill {
items := make([]GradientItem, 0, len(records))
for _, r := range records {
items = append(items, GradientItemFromSWF(r.Ratio, r.Color))
@ -74,10 +80,11 @@ func RadialGradientFromSWF(records []swfsubtypes.GRADRECORD, transform types.MAT
//TODO: interpolationMode, spreadMode
return &RadialGradient{
Colors: items,
return DrawPathListFill(RadialGradient{
Colors: items,
//TODO: do we need to scale this to pixel world from twips?
Transform: math.MatrixTransformFromSWF(transform),
SpreadMode: spreadMode,
InterpolationMode: interpolationMode,
}
}.GetInterpolatedDrawPaths(settings.GlobalSettings.GradientOverlap, settings.GlobalSettings.GradientBlur, settings.GlobalSettings.GradientSlices))
}

View file

@ -9,7 +9,8 @@ import (
)
type ShapeConverter struct {
Styles StyleList
Collection ObjectCollection
Styles StyleList
FillStyle0 *ActivePath
FillStyle1 *ActivePath
@ -26,8 +27,9 @@ type ShapeConverter struct {
FirstElement, SecondElement subtypes.SHAPERECORDS
}
func NewShapeConverter(element subtypes.SHAPERECORDS, styles StyleList) *ShapeConverter {
func NewShapeConverter(collection ObjectCollection, element subtypes.SHAPERECORDS, styles StyleList) *ShapeConverter {
return &ShapeConverter{
Collection: collection,
Styles: styles,
Position: math.NewVector2[types.Twip](0, 0),
Fills: make(PendingPathMap),
@ -36,8 +38,9 @@ func NewShapeConverter(element subtypes.SHAPERECORDS, styles StyleList) *ShapeCo
}
}
func NewMorphShapeConverter(firstElement, secondElement subtypes.SHAPERECORDS, styles StyleList) *ShapeConverter {
func NewMorphShapeConverter(collection ObjectCollection, firstElement, secondElement subtypes.SHAPERECORDS, styles StyleList) *ShapeConverter {
return &ShapeConverter{
Collection: collection,
Styles: styles,
Position: math.NewVector2[types.Twip](0, 0),
Fills: make(PendingPathMap),
@ -186,7 +189,7 @@ func (c *ShapeConverter) HandleNode(node subtypes.SHAPERECORD) {
if node.Flag.NewStyles {
c.FlushLayer()
c.Styles = StyleListFromSWFItems(node.FillStyles, node.LineStyles)
c.Styles = StyleListFromSWFItems(c.Collection, node.FillStyles, node.LineStyles)
}
if node.Flag.FillStyle1 {
@ -300,7 +303,7 @@ func (c *ShapeConverter) FlushLayer() {
if style == nil {
panic("should not happen")
}
c.Commands = append(c.Commands, DrawPathFill(style, path.GetShape()))
c.Commands = append(c.Commands, DrawPathFill(style, path.GetShape(), nil))
}
clear(c.Fills)
@ -333,7 +336,7 @@ func (c *ShapeConverter) FlushLayer() {
//TODO: using custom line borders later using fills this can be removed
fixedStyle := *style
fixedStyle.Width /= 2
c.Commands = append(c.Commands, DrawPathStroke(&fixedStyle, newSegments.GetShape()))
c.Commands = append(c.Commands, DrawPathStroke(&fixedStyle, newSegments.GetShape(), nil))
}
//TODO: leave this as-is and create a fill in renderer
}

View file

@ -25,9 +25,9 @@ func (l *StyleList) GetLineStyle(i int) *LineStyleRecord {
return nil
}
func StyleListFromSWFItems(fillStyles subtypes.FILLSTYLEARRAY, lineStyles subtypes.LINESTYLEARRAY) (r StyleList) {
func StyleListFromSWFItems(collection ObjectCollection, fillStyles subtypes.FILLSTYLEARRAY, lineStyles subtypes.LINESTYLEARRAY) (r StyleList) {
for _, s := range fillStyles.FillStyles {
r.FillStyles = append(r.FillStyles, FillStyleRecordFromSWF(s.FillStyleType, s.Color, s.Gradient, s.GradientMatrix))
r.FillStyles = append(r.FillStyles, FillStyleRecordFromSWF(collection, s.FillStyleType, s.Color, s.Gradient, s.GradientMatrix, s.BitmapMatrix, s.BitmapId))
}
if len(lineStyles.LineStyles) > 0 {
@ -57,7 +57,7 @@ func StyleListFromSWFItems(fillStyles subtypes.FILLSTYLEARRAY, lineStyles subtyp
},
})
} else {
fill := FillStyleRecordFromSWF(s.FillType.FillStyleType, s.FillType.Color, s.FillType.Gradient, s.FillType.GradientMatrix)
fill := FillStyleRecordFromSWF(collection, s.FillType.FillStyleType, s.FillType.Color, s.FillType.Gradient, s.FillType.GradientMatrix, s.FillType.BitmapMatrix, s.FillType.BitmapId)
switch fillEntry := fill.Fill.(type) {
case types.Color:
r.LineStyles = append(r.LineStyles, &LineStyleRecord{
@ -86,10 +86,10 @@ func StyleListFromSWFItems(fillStyles subtypes.FILLSTYLEARRAY, lineStyles subtyp
return r
}
func StyleListFromSWFMorphItems(fillStyles subtypes.MORPHFILLSTYLEARRAY, lineStyles subtypes.MORPHLINESTYLEARRAY) (start, end StyleList) {
func StyleListFromSWFMorphItems(collection ObjectCollection, fillStyles subtypes.MORPHFILLSTYLEARRAY, lineStyles subtypes.MORPHLINESTYLEARRAY) (start, end StyleList) {
for _, s := range fillStyles.FillStyles {
start.FillStyles = append(start.FillStyles, FillStyleRecordFromSWFMORPHFILLSTYLEStart(s))
end.FillStyles = append(end.FillStyles, FillStyleRecordFromSWFMORPHFILLSTYLEEnd(s))
start.FillStyles = append(start.FillStyles, FillStyleRecordFromSWFMORPHFILLSTYLEStart(collection, s))
end.FillStyles = append(end.FillStyles, FillStyleRecordFromSWFMORPHFILLSTYLEEnd(collection, s))
}
if len(lineStyles.LineStyles) > 0 {
@ -140,8 +140,8 @@ func StyleListFromSWFMorphItems(fillStyles subtypes.MORPHFILLSTYLEARRAY, lineSty
},
})
} else {
fillStart := FillStyleRecordFromSWFMORPHFILLSTYLEStart(s.FillType)
fillEnd := FillStyleRecordFromSWFMORPHFILLSTYLEEnd(s.FillType)
fillStart := FillStyleRecordFromSWFMORPHFILLSTYLEStart(collection, s.FillType)
fillEnd := FillStyleRecordFromSWFMORPHFILLSTYLEEnd(collection, s.FillType)
switch fillEntry := fillStart.Fill.(type) {
case types.Color:
start.LineStyles = append(start.LineStyles, &LineStyleRecord{

View file

@ -1,9 +1,12 @@
package shapes
import (
"fmt"
swfsubtypes "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/tag/subtypes"
swftypes "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/swf/types"
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
math2 "math"
"slices"
)
type StyleRecord interface {
@ -24,19 +27,61 @@ func (r *LineStyleRecord) ApplyColorTransform(transform math.ColorTransform) Sty
}
}
type Fillable interface {
Fill(shape *Shape) DrawPathList
ApplyColorTransform(transform math.ColorTransform) Fillable
}
type FillStyleRecord struct {
// Fill can be a math.Color or Gradient
Fill any
Border *LineStyleRecord
Blur float64
// Fill can be math.Color or Fillable
Fill any
Border *LineStyleRecord
Blur float64
fillCache struct {
Shape *Shape
List DrawPathList
}
}
func (r *FillStyleRecord) IsFlat() bool {
_, ok := r.Fill.(math.Color)
return ok
}
// Flatten Creates a fill that is only composed of FillStyleRecord with Fill being math.Color
func (r *FillStyleRecord) Flatten(s *Shape) DrawPathList {
if _, ok := r.Fill.(math.Color); ok {
return DrawPathList{
{
Style: r,
Commands: s,
},
}
} else if fillable, ok := r.Fill.(Fillable); ok {
//TODO: inherit blur/border
if r.fillCache.Shape != nil && r.fillCache.Shape.Equals(s) {
return r.fillCache.List
}
fill := fillable.Fill(s)
r.fillCache.List = fill
r.fillCache.Shape = &Shape{
Edges: slices.Clone(s.Edges),
IsFlat: s.IsFlat,
}
return fill
} else {
panic("not supported")
}
}
func (r *FillStyleRecord) ApplyColorTransform(transform math.ColorTransform) StyleRecord {
fill := r.Fill
if color, ok := fill.(math.Color); ok {
fill = transform.ApplyToColor(color)
} else if gradient, ok := fill.(Gradient); ok {
fill = gradient.ApplyColorTransform(transform)
} else if fillable, ok := r.Fill.(Fillable); ok {
fill = fillable.ApplyColorTransform(transform)
} else {
panic("not supported")
}
return &FillStyleRecord{
Border: r.Border,
@ -45,7 +90,7 @@ func (r *FillStyleRecord) ApplyColorTransform(transform math.ColorTransform) Sty
}
}
func FillStyleRecordFromSWF(fillType swfsubtypes.FillStyleType, color swftypes.Color, gradient swfsubtypes.GRADIENT, gradientMatrix swftypes.MATRIX) (r *FillStyleRecord) {
func FillStyleRecordFromSWF(collection ObjectCollection, fillType swfsubtypes.FillStyleType, color swftypes.Color, gradient swfsubtypes.GRADIENT, gradientMatrix, bitmapMatrix swftypes.MATRIX, bitmapId uint16) (r *FillStyleRecord) {
switch fillType {
case swfsubtypes.FillStyleSolid:
return &FillStyleRecord{
@ -74,6 +119,72 @@ func FillStyleRecordFromSWF(fillType swfsubtypes.FillStyleType, color swftypes.C
Alpha: gradient.Records[0].Color.A(),
},
}
case swfsubtypes.FillStyleClippedBitmap, swfsubtypes.FillStyleRepeatingBitmap:
if bitmapId == math2.MaxUint16 { //Special case, TODO:???
return &FillStyleRecord{
Fill: math.Color{
R: 0,
G: 0,
B: 0,
Alpha: 0,
},
}
}
bitmap := collection.Get(bitmapId)
if bitmap == nil {
fmt.Printf("bitmap %d not found!\n", bitmapId)
return &FillStyleRecord{
Fill: math.Color{
R: 0,
G: 0,
B: 0,
Alpha: 0,
},
}
}
//TODO: what blur factor should it pick
blurFactor := 1.0
//TODO: extend color
return &FillStyleRecord{
Fill: DrawPathListFillFromSWF(bitmap.GetShapeList(0).ApplyFunction(func(p DrawPath) DrawPath {
if fillStyle, ok := p.Style.(*FillStyleRecord); ok {
return DrawPathFill(&FillStyleRecord{
Fill: fillStyle.Fill,
Border: fillStyle.Border,
Blur: blurFactor,
}, p.Commands, p.Clip)
}
return p
}), bitmapMatrix),
}
case swfsubtypes.FillStyleNonSmoothedClippedBitmap, swfsubtypes.FillStyleNonSmoothedRepeatingBitmap:
if bitmapId == math2.MaxUint16 { //Special case, TODO:???
return &FillStyleRecord{
Fill: math.Color{
R: 0,
G: 0,
B: 0,
Alpha: 0,
},
}
}
bitmap := collection.Get(bitmapId)
if bitmap == nil {
fmt.Printf("bitmap %d not found!\n", bitmapId)
return &FillStyleRecord{
Fill: math.Color{
R: 0,
G: 0,
B: 0,
Alpha: 0,
},
}
}
//TODO: extend color
return &FillStyleRecord{
Fill: DrawPathListFillFromSWF(bitmap.GetShapeList(0), bitmapMatrix),
}
//TODO other styles
}
@ -87,10 +198,10 @@ func FillStyleRecordFromSWF(fillType swfsubtypes.FillStyleType, color swftypes.C
}
}
func FillStyleRecordFromSWFMORPHFILLSTYLEStart(fillStyle swfsubtypes.MORPHFILLSTYLE) (r *FillStyleRecord) {
return FillStyleRecordFromSWF(fillStyle.FillStyleType, fillStyle.StartColor, fillStyle.Gradient.StartGradient(), fillStyle.StartGradientMatrix)
func FillStyleRecordFromSWFMORPHFILLSTYLEStart(collection ObjectCollection, fillStyle swfsubtypes.MORPHFILLSTYLE) (r *FillStyleRecord) {
return FillStyleRecordFromSWF(collection, fillStyle.FillStyleType, fillStyle.StartColor, fillStyle.Gradient.StartGradient(), fillStyle.StartGradientMatrix, fillStyle.StartBitmapMatrix, fillStyle.BitmapId)
}
func FillStyleRecordFromSWFMORPHFILLSTYLEEnd(fillStyle swfsubtypes.MORPHFILLSTYLE) (r *FillStyleRecord) {
return FillStyleRecordFromSWF(fillStyle.FillStyleType, fillStyle.EndColor, fillStyle.Gradient.EndGradient(), fillStyle.EndGradientMatrix)
func FillStyleRecordFromSWFMORPHFILLSTYLEEnd(collection ObjectCollection, fillStyle swfsubtypes.MORPHFILLSTYLE) (r *FillStyleRecord) {
return FillStyleRecordFromSWF(collection, fillStyle.FillStyleType, fillStyle.EndColor, fillStyle.Gradient.EndGradient(), fillStyle.EndGradientMatrix, fillStyle.EndBitmapMatrix, fillStyle.BitmapId)
}

263
types/shapes/mediancut.go Normal file
View file

@ -0,0 +1,263 @@
package shapes
import (
"container/heap"
"image"
"image/color"
"sort"
)
// Copyright 2013 Andrew Bonventre. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Copyright (c) 2013, Andrew Bonventre.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import (
"image/draw"
)
const (
numDimensions = 3
)
type point [numDimensions]int
type block struct {
minCorner, maxCorner point
points []point
// The index is needed by update and is maintained by the heap.Interface methods.
index int // The index of the item in the heap.
}
func newBlock(p []point) *block {
return &block{
minCorner: point{0x00, 0x00, 0x00},
maxCorner: point{0xFF, 0xFF, 0xFF},
points: p,
}
}
func (b *block) longestSideIndex() int {
m := b.maxCorner[0] - b.minCorner[0]
maxIndex := 0
for i := 1; i < numDimensions; i++ {
diff := b.maxCorner[i] - b.minCorner[i]
if diff > m {
m = diff
maxIndex = i
}
}
return maxIndex
}
func (b *block) longestSideLength() int {
i := b.longestSideIndex()
return b.maxCorner[i] - b.minCorner[i]
}
func (b *block) shrink() {
for j := 0; j < numDimensions; j++ {
b.minCorner[j] = b.points[0][j]
b.maxCorner[j] = b.points[0][j]
}
for i := 1; i < len(b.points); i++ {
for j := 0; j < numDimensions; j++ {
b.minCorner[j] = min(b.minCorner[j], b.points[i][j])
b.maxCorner[j] = max(b.maxCorner[j], b.points[i][j])
}
}
}
type pointSorter struct {
points []point
by func(p1, p2 *point) bool
}
func (p *pointSorter) Len() int {
return len(p.points)
}
func (p *pointSorter) Swap(i, j int) {
p.points[i], p.points[j] = p.points[j], p.points[i]
}
func (p *pointSorter) Less(i, j int) bool {
return p.by(&p.points[i], &p.points[j])
}
// A priorityQueue implements heap.Interface and holds blocks.
type priorityQueue []*block
func (pq priorityQueue) Len() int { return len(pq) }
func (pq priorityQueue) Less(i, j int) bool {
return pq[i].longestSideLength() > pq[j].longestSideLength()
}
func (pq priorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (pq *priorityQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*block)
item.index = n
*pq = append(*pq, item)
}
func (pq *priorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
item.index = -1 // for safety
*pq = old[:n-1]
return item
}
func (pq *priorityQueue) top() interface{} {
n := len(*pq)
if n == 0 {
return nil
}
return (*pq)[n-1]
}
// clip clips r against each image's bounds (after translating into
// the destination image's co-ordinate space) and shifts the point
// sp by the same amount as the change in r.Min.
func clip(dst draw.Image, r *image.Rectangle, src image.Image, sp *image.Point) {
orig := r.Min
*r = r.Intersect(dst.Bounds())
*r = r.Intersect(src.Bounds().Add(orig.Sub(*sp)))
dx := r.Min.X - orig.X
dy := r.Min.Y - orig.Y
if dx == 0 && dy == 0 {
return
}
(*sp).X += dx
(*sp).Y += dy
}
// MedianCutQuantizer constructs a palette with a maximum of
// NumColor colors by iteratively splitting clusters of color
// points mapped on a three-dimensional (RGB) Euclidian space.
// Once the number of clusters is within the specified bounds,
// the resulting color is computed by averaging those within
// each grouping.
type MedianCutQuantizer struct {
NumColor int
}
func (q *MedianCutQuantizer) medianCut(points []point) color.Palette {
if q.NumColor == 0 {
return color.Palette{}
}
initialBlock := newBlock(points)
initialBlock.shrink()
pq := &priorityQueue{}
heap.Init(pq)
heap.Push(pq, initialBlock)
for pq.Len() < q.NumColor && len(pq.top().(*block).points) > 1 {
longestBlock := heap.Pop(pq).(*block)
points := longestBlock.points
li := longestBlock.longestSideIndex()
// TODO: Instead of sorting the entire slice, finding the median using an
// algorithm like introselect would give much better performance.
sort.Sort(&pointSorter{
points: points,
by: func(p1, p2 *point) bool { return p1[li] < p2[li] },
})
median := len(points) / 2
block1 := newBlock(points[:median])
block2 := newBlock(points[median:])
block1.shrink()
block2.shrink()
heap.Push(pq, block1)
heap.Push(pq, block2)
}
palette := make(color.Palette, q.NumColor)
var n int
for n = 0; pq.Len() > 0; n++ {
block := heap.Pop(pq).(*block)
var sum [numDimensions]int
for i := 0; i < len(block.points); i++ {
for j := 0; j < numDimensions; j++ {
sum[j] += block.points[i][j]
}
}
palette[n] = color.RGBA64{
R: uint16(sum[0] / len(block.points)),
G: uint16(sum[1] / len(block.points)),
B: uint16(sum[2] / len(block.points)),
A: 0xFFFF,
}
}
// Trim to only the colors present in the image, which
// could be less than NumColor.
return palette[:n]
}
func (q *MedianCutQuantizer) Quantize(dst *image.Paletted, r image.Rectangle, src image.Image, sp image.Point) {
clip(dst, &r, src, &sp)
if r.Empty() {
return
}
points := make([]point, r.Dx()*r.Dy())
colorSet := make(map[uint32]color.Color, q.NumColor)
i := 0
for y := r.Min.Y; y < r.Max.Y; y++ {
for x := r.Min.X; x < r.Max.X; x++ {
c := src.At(x, y)
r, g, b, _ := c.RGBA()
colorSet[(r>>8)<<16|(g>>8)<<8|b>>8] = c
points[i][0] = int(r)
points[i][1] = int(g)
points[i][2] = int(b)
i++
}
}
if len(colorSet) <= q.NumColor {
// No need to quantize since the total number of colors
// fits within the palette.
dst.Palette = make(color.Palette, len(colorSet))
i := 0
for _, c := range colorSet {
dst.Palette[i] = c
i++
}
} else {
dst.Palette = q.medianCut(points)
}
for y := 0; y < r.Dy(); y++ {
for x := 0; x < r.Dx(); x++ {
// TODO: this should be done more efficiently.
dst.Set(sp.X+x, sp.Y+y, src.At(r.Min.X+x, r.Min.Y+y))
}
}
}