Optimize performance, implement bitmap shapes, implement clipping settings
This commit is contained in:
parent
c115d9d2f8
commit
159b72e22b
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
22
swf/tag/DefineBitsLossless.go
Normal file
22
swf/tag/DefineBitsLossless.go
Normal 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
|
||||
}
|
83
swf/tag/DefineBitsLossless2.go
Normal file
83
swf/tag/DefineBitsLossless2.go
Normal 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
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
45
types/BitmapDefinition.go
Normal 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),
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
8
types/MultiFrameObjectDefinition.go
Normal file
8
types/MultiFrameObjectDefinition.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package types
|
||||
|
||||
import "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes"
|
||||
|
||||
type MultiFrameObjectDefinition interface {
|
||||
shapes.ObjectDefinition
|
||||
NextFrame() *ViewFrame
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -11,7 +11,7 @@ type RenderedObject struct {
|
|||
|
||||
DrawPathList shapes.DrawPathList
|
||||
|
||||
Clip *ClipPath
|
||||
Clip *shapes.ClipPath
|
||||
|
||||
ColorTransform math.ColorTransform
|
||||
MatrixTransform math.MatrixTransform
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
395
types/shapes/BitmapConverter.go
Normal file
395
types/shapes/BitmapConverter.go
Normal 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}...)
|
||||
}
|
||||
}
|
|
@ -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),
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
24
types/shapes/DrawPathListFill.go
Normal file
24
types/shapes/DrawPathListFill.go
Normal 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))
|
||||
}
|
|
@ -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))))
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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")
|
7
types/shapes/ObjectDefinition.go
Normal file
7
types/shapes/ObjectDefinition.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package shapes
|
||||
|
||||
type ObjectDefinition interface {
|
||||
GetObjectId() uint16
|
||||
GetSafeObject() ObjectDefinition
|
||||
GetShapeList(ratio float64) DrawPathList
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
263
types/shapes/mediancut.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue