Major improvements to UnifiedPlayer: 1. GetFrameImage() now works when paused for responsive UI updates 2. Play() method properly starts FFmpeg process 3. Frame display loop runs continuously for smooth video display 4. Disabled audio temporarily to fix video playback fundamentals 5. Simplified FFmpeg command to focus on video stream only Player now: - Generates video frames correctly - Shows video when paused - Has responsive progress tracking - Starts playback properly Next steps: Re-enable audio playback once video is stable
392 lines
9.3 KiB
Go
392 lines
9.3 KiB
Go
// Copyright 2017 The oksvg Authors. All rights reserved.
|
|
// created: 2/12/2017 by S.R.Wiley
|
|
//
|
|
// utils.go implements translation of an SVG2.0 path into a rasterx Path.
|
|
|
|
package oksvg
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"image/color"
|
|
"log"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/srwiley/rasterx"
|
|
)
|
|
|
|
// IconCursor is used while parsing SVG files.
|
|
type IconCursor struct {
|
|
PathCursor
|
|
icon *SvgIcon
|
|
StyleStack []PathStyle
|
|
grad *rasterx.Gradient
|
|
inTitleText, inDescText, inGrad, inDefs, inDefsStyle bool
|
|
currentDef []definition
|
|
}
|
|
|
|
// ReadGradURL reads an SVG format gradient url
|
|
// Since the context of the gradient can affect the colors
|
|
// the current fill or line color is passed in and used in
|
|
// the case of a nil stopClor value
|
|
func (c *IconCursor) ReadGradURL(v string, defaultColor interface{}) (grad rasterx.Gradient, ok bool) {
|
|
if strings.HasPrefix(v, "url(") && strings.HasSuffix(v, ")") {
|
|
urlStr := strings.TrimSpace(v[4 : len(v)-1])
|
|
if strings.HasPrefix(urlStr, "#") {
|
|
var g *rasterx.Gradient
|
|
g, ok = c.icon.Grads[urlStr[1:]]
|
|
if ok {
|
|
grad = localizeGradIfStopClrNil(g, defaultColor)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// ReadGradAttr reads an SVG gradient attribute
|
|
func (c *IconCursor) ReadGradAttr(attr xml.Attr) (err error) {
|
|
switch attr.Name.Local {
|
|
case "gradientTransform":
|
|
c.grad.Matrix, err = c.parseTransform(attr.Value)
|
|
case "gradientUnits":
|
|
switch strings.TrimSpace(attr.Value) {
|
|
case "userSpaceOnUse":
|
|
c.grad.Units = rasterx.UserSpaceOnUse
|
|
case "objectBoundingBox":
|
|
c.grad.Units = rasterx.ObjectBoundingBox
|
|
}
|
|
case "spreadMethod":
|
|
switch strings.TrimSpace(attr.Value) {
|
|
case "pad":
|
|
c.grad.Spread = rasterx.PadSpread
|
|
case "reflect":
|
|
c.grad.Spread = rasterx.ReflectSpread
|
|
case "repeat":
|
|
c.grad.Spread = rasterx.RepeatSpread
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// PushStyle parses the style element, and push it on the style stack. Only color and opacity are supported
|
|
// for fill. Note that this parses both the contents of a style attribute plus
|
|
// direct fill and opacity attributes.
|
|
func (c *IconCursor) PushStyle(attrs []xml.Attr) error {
|
|
var pairs []string
|
|
className := ""
|
|
for _, attr := range attrs {
|
|
switch strings.ToLower(attr.Name.Local) {
|
|
case "style":
|
|
pairs = append(pairs, strings.Split(attr.Value, ";")...)
|
|
case "class":
|
|
className = attr.Value
|
|
default:
|
|
pairs = append(pairs, attr.Name.Local+":"+attr.Value)
|
|
}
|
|
}
|
|
// Make a copy of the top style
|
|
curStyle := c.StyleStack[len(c.StyleStack)-1]
|
|
for _, pair := range pairs {
|
|
kv := strings.Split(pair, ":")
|
|
if len(kv) >= 2 {
|
|
k := strings.ToLower(kv[0])
|
|
k = strings.TrimSpace(k)
|
|
v := strings.TrimSpace(kv[1])
|
|
err := c.readStyleAttr(&curStyle, k, v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
c.adaptClasses(&curStyle, className)
|
|
c.StyleStack = append(c.StyleStack, curStyle) // Push style onto stack
|
|
return nil
|
|
}
|
|
|
|
func (c *IconCursor) readTransformAttr(m1 rasterx.Matrix2D, k string) (rasterx.Matrix2D, error) {
|
|
ln := len(c.points)
|
|
switch k {
|
|
case "rotate":
|
|
if ln == 1 {
|
|
m1 = m1.Rotate(c.points[0] * math.Pi / 180)
|
|
} else if ln == 3 {
|
|
m1 = m1.Translate(c.points[1], c.points[2]).
|
|
Rotate(c.points[0]*math.Pi/180).
|
|
Translate(-c.points[1], -c.points[2])
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
case "translate":
|
|
if ln == 1 {
|
|
m1 = m1.Translate(c.points[0], 0)
|
|
} else if ln == 2 {
|
|
m1 = m1.Translate(c.points[0], c.points[1])
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
case "skewx":
|
|
if ln == 1 {
|
|
m1 = m1.SkewX(c.points[0] * math.Pi / 180)
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
case "skewy":
|
|
if ln == 1 {
|
|
m1 = m1.SkewY(c.points[0] * math.Pi / 180)
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
case "scale":
|
|
if ln == 1 {
|
|
m1 = m1.Scale(c.points[0], 0)
|
|
} else if ln == 2 {
|
|
m1 = m1.Scale(c.points[0], c.points[1])
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
case "matrix":
|
|
if ln == 6 {
|
|
m1 = m1.Mult(rasterx.Matrix2D{
|
|
A: c.points[0],
|
|
B: c.points[1],
|
|
C: c.points[2],
|
|
D: c.points[3],
|
|
E: c.points[4],
|
|
F: c.points[5]})
|
|
} else {
|
|
return m1, errParamMismatch
|
|
}
|
|
default:
|
|
return m1, errParamMismatch
|
|
}
|
|
return m1, nil
|
|
}
|
|
|
|
func (c *IconCursor) parseTransform(v string) (rasterx.Matrix2D, error) {
|
|
ts := strings.Split(v, ")")
|
|
m1 := c.StyleStack[len(c.StyleStack)-1].mAdder.M
|
|
for _, t := range ts {
|
|
t = strings.TrimSpace(t)
|
|
if len(t) == 0 {
|
|
continue
|
|
}
|
|
d := strings.Split(t, "(")
|
|
if len(d) != 2 || len(d[1]) < 1 {
|
|
return m1, errParamMismatch // badly formed transformation
|
|
}
|
|
err := c.GetPoints(d[1])
|
|
if err != nil {
|
|
return m1, err
|
|
}
|
|
m1, err = c.readTransformAttr(m1, strings.ToLower(strings.TrimSpace(d[0])))
|
|
if err != nil {
|
|
return m1, err
|
|
}
|
|
}
|
|
return m1, nil
|
|
}
|
|
|
|
func (c *IconCursor) readStyleAttr(curStyle *PathStyle, k, v string) error {
|
|
switch k {
|
|
case "fill":
|
|
gradient, ok := c.ReadGradURL(v, curStyle.fillerColor)
|
|
if ok {
|
|
curStyle.fillerColor = gradient
|
|
break
|
|
}
|
|
var err error
|
|
curStyle.fillerColor, err = ParseSVGColor(v)
|
|
return err
|
|
case "stroke":
|
|
gradient, ok := c.ReadGradURL(v, curStyle.linerColor)
|
|
if ok {
|
|
curStyle.linerColor = gradient
|
|
break
|
|
}
|
|
col, errc := ParseSVGColor(v)
|
|
if errc != nil {
|
|
return errc
|
|
}
|
|
if col != nil {
|
|
curStyle.linerColor = col.(color.NRGBA)
|
|
} else {
|
|
curStyle.linerColor = nil
|
|
}
|
|
case "stroke-linegap":
|
|
switch v {
|
|
case "flat":
|
|
curStyle.LineGap = rasterx.FlatGap
|
|
case "round":
|
|
curStyle.LineGap = rasterx.RoundGap
|
|
case "cubic":
|
|
curStyle.LineGap = rasterx.CubicGap
|
|
case "quadratic":
|
|
curStyle.LineGap = rasterx.QuadraticGap
|
|
}
|
|
case "stroke-leadlinecap":
|
|
switch v {
|
|
case "butt":
|
|
curStyle.LeadLineCap = rasterx.ButtCap
|
|
case "round":
|
|
curStyle.LeadLineCap = rasterx.RoundCap
|
|
case "square":
|
|
curStyle.LeadLineCap = rasterx.SquareCap
|
|
case "cubic":
|
|
curStyle.LeadLineCap = rasterx.CubicCap
|
|
case "quadratic":
|
|
curStyle.LeadLineCap = rasterx.QuadraticCap
|
|
}
|
|
case "stroke-linecap":
|
|
switch v {
|
|
case "butt":
|
|
curStyle.LineCap = rasterx.ButtCap
|
|
case "round":
|
|
curStyle.LineCap = rasterx.RoundCap
|
|
case "square":
|
|
curStyle.LineCap = rasterx.SquareCap
|
|
case "cubic":
|
|
curStyle.LineCap = rasterx.CubicCap
|
|
case "quadratic":
|
|
curStyle.LineCap = rasterx.QuadraticCap
|
|
}
|
|
case "stroke-linejoin":
|
|
switch v {
|
|
case "miter":
|
|
curStyle.LineJoin = rasterx.Miter
|
|
case "miter-clip":
|
|
curStyle.LineJoin = rasterx.MiterClip
|
|
case "arc-clip":
|
|
curStyle.LineJoin = rasterx.ArcClip
|
|
case "round":
|
|
curStyle.LineJoin = rasterx.Round
|
|
case "arc":
|
|
curStyle.LineJoin = rasterx.Arc
|
|
case "bevel":
|
|
curStyle.LineJoin = rasterx.Bevel
|
|
}
|
|
case "stroke-miterlimit":
|
|
mLimit, err := parseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
curStyle.MiterLimit = mLimit
|
|
case "stroke-width":
|
|
width, err := parseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
curStyle.LineWidth = width
|
|
case "stroke-dashoffset":
|
|
dashOffset, err := parseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
curStyle.DashOffset = dashOffset
|
|
case "stroke-dasharray":
|
|
if v != "none" {
|
|
dashes := splitOnCommaOrSpace(v)
|
|
dList := make([]float64, len(dashes))
|
|
for i, dstr := range dashes {
|
|
d, err := parseFloat(strings.TrimSpace(dstr), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dList[i] = d
|
|
}
|
|
curStyle.Dash = dList
|
|
break
|
|
}
|
|
case "opacity", "stroke-opacity", "fill-opacity":
|
|
op, err := parseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if k != "stroke-opacity" {
|
|
curStyle.FillOpacity *= op
|
|
}
|
|
if k != "fill-opacity" {
|
|
curStyle.LineOpacity *= op
|
|
}
|
|
case "transform":
|
|
m, err := c.parseTransform(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
curStyle.mAdder.M = m
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *IconCursor) readStartElement(se xml.StartElement) (err error) {
|
|
var skipDef bool
|
|
if se.Name.Local == "radialGradient" || se.Name.Local == "linearGradient" || c.inGrad {
|
|
skipDef = true
|
|
}
|
|
if c.inDefs && !skipDef {
|
|
ID := ""
|
|
for _, attr := range se.Attr {
|
|
if attr.Name.Local == "id" {
|
|
ID = attr.Value
|
|
}
|
|
}
|
|
if ID != "" && len(c.currentDef) > 0 {
|
|
c.icon.Defs[c.currentDef[0].ID] = c.currentDef
|
|
c.currentDef = make([]definition, 0)
|
|
}
|
|
c.currentDef = append(c.currentDef, definition{
|
|
ID: ID,
|
|
Tag: se.Name.Local,
|
|
Attrs: se.Attr,
|
|
})
|
|
return nil
|
|
}
|
|
df, ok := drawFuncs[se.Name.Local]
|
|
if !ok {
|
|
errStr := "Cannot process svg element " + se.Name.Local
|
|
if c.returnError(errStr) {
|
|
return errors.New(errStr)
|
|
}
|
|
return nil
|
|
}
|
|
err = df(c, se.Attr)
|
|
if err != nil {
|
|
e := fmt.Sprintf("error during processing svg element %s: %s", se.Name.Local, err.Error())
|
|
if c.returnError(e) {
|
|
err = errors.New(e)
|
|
}
|
|
err = nil
|
|
}
|
|
|
|
if len(c.Path) > 0 {
|
|
//The cursor parsed a path from the xml element
|
|
pathCopy := make(rasterx.Path, len(c.Path))
|
|
copy(pathCopy, c.Path)
|
|
c.icon.SVGPaths = append(c.icon.SVGPaths,
|
|
SvgPath{c.StyleStack[len(c.StyleStack)-1], pathCopy})
|
|
c.Path = c.Path[:0]
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *IconCursor) adaptClasses(pathStyle *PathStyle, className string) {
|
|
if className == "" || len(c.icon.classes) == 0 {
|
|
return
|
|
}
|
|
for k, v := range c.icon.classes[className] {
|
|
c.readStyleAttr(pathStyle, k, v)
|
|
}
|
|
}
|
|
|
|
func (c *IconCursor) returnError(errMsg string) bool {
|
|
if c.ErrorMode == StrictErrorMode {
|
|
return true
|
|
}
|
|
if c.ErrorMode == WarnErrorMode {
|
|
log.Println(errMsg)
|
|
}
|
|
|
|
return false
|
|
}
|