VideoTools/vendor/github.com/srwiley/oksvg/path_cursor.go
Stu Leak 68df790d27 Fix player frame generation and video playback
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
2026-01-07 22:20:00 -05:00

398 lines
9.7 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 (
"errors"
"log"
"math"
"unicode"
"github.com/srwiley/rasterx"
"golang.org/x/image/math/fixed"
)
type (
// ErrorMode is the for setting how the parser reacts to unparsed elements
ErrorMode uint8
// PathCursor is used to parse SVG format path strings into a rasterx Path
PathCursor struct {
rasterx.Path
placeX, placeY float64
cntlPtX, cntlPtY float64
pathStartX, pathStartY float64
points []float64
lastKey uint8
ErrorMode ErrorMode
inPath bool
}
)
const (
// IgnoreErrorMode skips un-parsed SVG elements.
IgnoreErrorMode ErrorMode = iota
// WarnErrorMode outputs a warning when an un-parsed SVG element is found.
WarnErrorMode
// StrictErrorMode causes an error when an un-parsed SVG element is found.
StrictErrorMode
)
var (
errParamMismatch = errors.New("param mismatch")
errCommandUnknown = errors.New("unknown command")
errZeroLengthID = errors.New("zero length id")
)
// ReadFloat reads a floating point value and adds it to the cursor's points slice.
func (c *PathCursor) ReadFloat(numStr string) error {
last := 0
isFirst := true
for i, n := range numStr {
if n == '.' {
if isFirst {
isFirst = false
continue
}
f, err := parseFloat(numStr[last:i], 64)
if err != nil {
return err
}
c.points = append(c.points, f)
last = i
}
}
f, err := parseFloat(numStr[last:], 64)
if err != nil {
return err
}
c.points = append(c.points, f)
return nil
}
// GetPoints reads a set of floating point values from the SVG format number string,
// and add them to the cursor's points slice.
func (c *PathCursor) GetPoints(dataPoints string) error {
lastIndex := -1
c.points = c.points[0:0]
lr := ' '
for i, r := range dataPoints {
if !unicode.IsNumber(r) && r != '.' && !(r == '-' && lr == 'e') && r != 'e' {
if lastIndex != -1 {
if err := c.ReadFloat(dataPoints[lastIndex:i]); err != nil {
return err
}
}
if r == '-' {
lastIndex = i
} else {
lastIndex = -1
}
} else if lastIndex == -1 {
lastIndex = i
}
lr = r
}
if lastIndex != -1 && lastIndex != len(dataPoints) {
if err := c.ReadFloat(dataPoints[lastIndex:]); err != nil {
return err
}
}
return nil
}
// EllipseAt adds a path of an elipse centered at cx, cy of radius rx and ry
// to the PathCursor
func (c *PathCursor) EllipseAt(cx, cy, rx, ry float64) {
c.placeX, c.placeY = cx+rx, cy
c.points = c.points[0:0]
c.points = append(c.points, rx, ry, 0.0, 1.0, 0.0, c.placeX, c.placeY)
c.Path.Start(fixed.Point26_6{
X: fixed.Int26_6(c.placeX * 64),
Y: fixed.Int26_6(c.placeY * 64)})
c.placeX, c.placeY = rasterx.AddArc(c.points, cx, cy, c.placeX, c.placeY, &c.Path)
c.Path.Stop(true)
}
// AddArcFromA adds a path of an arc element to the cursor path to the PathCursor
func (c *PathCursor) AddArcFromA(points []float64) {
cx, cy := rasterx.FindEllipseCenter(&points[0], &points[1], points[2]*math.Pi/180, c.placeX,
c.placeY, points[5], points[6], points[4] == 0, points[3] == 0)
c.placeX, c.placeY = rasterx.AddArc(c.points, cx, cy, c.placeX, c.placeY, &c.Path)
}
// CompilePath translates the svgPath description string into a rasterx path.
// All valid SVG path elements are interpreted to rasterx equivalents.
// The resulting path element is stored in the PathCursor.
func (c *PathCursor) CompilePath(svgPath string) error {
c.init()
lastIndex := -1
for i, v := range svgPath {
if unicode.IsLetter(v) && v != 'e' {
if lastIndex != -1 {
if err := c.addSeg(svgPath[lastIndex:i]); err != nil {
return err
}
}
lastIndex = i
}
}
if lastIndex != -1 {
if err := c.addSeg(svgPath[lastIndex:]); err != nil {
return err
}
}
return nil
}
func reflect(px, py, rx, ry float64) (x, y float64) {
return px*2 - rx, py*2 - ry
}
func (c *PathCursor) valsToAbs(last float64) {
for i := 0; i < len(c.points); i++ {
last += c.points[i]
c.points[i] = last
}
}
func (c *PathCursor) pointsToAbs(sz int) {
lastX := c.placeX
lastY := c.placeY
for j := 0; j < len(c.points); j += sz {
for i := 0; i < sz; i += 2 {
c.points[i+j] += lastX
c.points[i+1+j] += lastY
}
lastX = c.points[(j+sz)-2]
lastY = c.points[(j+sz)-1]
}
}
func (c *PathCursor) hasSetsOrMore(sz int, rel bool) bool {
if !(len(c.points) >= sz && len(c.points)%sz == 0) {
return false
}
if rel {
c.pointsToAbs(sz)
}
return true
}
func (c *PathCursor) reflectControlQuad() {
switch c.lastKey {
case 'q', 'Q', 'T', 't':
c.cntlPtX, c.cntlPtY = reflect(c.placeX, c.placeY, c.cntlPtX, c.cntlPtY)
default:
c.cntlPtX, c.cntlPtY = c.placeX, c.placeY
}
}
func (c *PathCursor) reflectControlCube() {
switch c.lastKey {
case 'c', 'C', 's', 'S':
c.cntlPtX, c.cntlPtY = reflect(c.placeX, c.placeY, c.cntlPtX, c.cntlPtY)
default:
c.cntlPtX, c.cntlPtY = c.placeX, c.placeY
}
}
// addSeg decodes an SVG seqment string into equivalent raster path commands saved
// in the cursor's Path
func (c *PathCursor) addSeg(segString string) error {
// Parse the string describing the numeric points in SVG format
if err := c.GetPoints(segString[1:]); err != nil {
return err
}
l := len(c.points)
k := segString[0]
rel := false
switch k {
case 'z':
fallthrough
case 'Z':
if len(c.points) != 0 {
return errParamMismatch
}
if c.inPath {
c.Path.Stop(true)
c.placeX = c.pathStartX
c.placeY = c.pathStartY
c.inPath = false
}
case 'm':
rel = true
fallthrough
case 'M':
if !c.hasSetsOrMore(2, rel) {
return errParamMismatch
}
c.pathStartX, c.pathStartY = c.points[0], c.points[1]
c.inPath = true
c.Path.Start(fixed.Point26_6{X: fixed.Int26_6((c.pathStartX) * 64), Y: fixed.Int26_6((c.pathStartY) * 64)})
for i := 2; i < l-1; i += 2 {
c.Path.Line(fixed.Point26_6{
X: fixed.Int26_6((c.points[i]) * 64),
Y: fixed.Int26_6((c.points[i+1]) * 64)})
}
c.placeX = c.points[l-2]
c.placeY = c.points[l-1]
case 'l':
rel = true
fallthrough
case 'L':
if !c.hasSetsOrMore(2, rel) {
return errParamMismatch
}
for i := 0; i < l-1; i += 2 {
c.Path.Line(fixed.Point26_6{
X: fixed.Int26_6((c.points[i]) * 64),
Y: fixed.Int26_6((c.points[i+1]) * 64)})
}
c.placeX = c.points[l-2]
c.placeY = c.points[l-1]
case 'v':
c.valsToAbs(c.placeY)
fallthrough
case 'V':
if !c.hasSetsOrMore(1, false) {
return errParamMismatch
}
for _, p := range c.points {
c.Path.Line(fixed.Point26_6{
X: fixed.Int26_6((c.placeX) * 64),
Y: fixed.Int26_6((p) * 64)})
}
c.placeY = c.points[l-1]
case 'h':
c.valsToAbs(c.placeX)
fallthrough
case 'H':
if !c.hasSetsOrMore(1, false) {
return errParamMismatch
}
for _, p := range c.points {
c.Path.Line(fixed.Point26_6{
X: fixed.Int26_6((p) * 64),
Y: fixed.Int26_6((c.placeY) * 64)})
}
c.placeX = c.points[l-1]
case 'q':
rel = true
fallthrough
case 'Q':
if !c.hasSetsOrMore(4, rel) {
return errParamMismatch
}
for i := 0; i < l-3; i += 4 {
c.Path.QuadBezier(
fixed.Point26_6{
X: fixed.Int26_6((c.points[i]) * 64),
Y: fixed.Int26_6((c.points[i+1]) * 64)},
fixed.Point26_6{
X: fixed.Int26_6((c.points[i+2]) * 64),
Y: fixed.Int26_6((c.points[i+3]) * 64)})
}
c.cntlPtX, c.cntlPtY = c.points[l-4], c.points[l-3]
c.placeX = c.points[l-2]
c.placeY = c.points[l-1]
case 't':
rel = true
fallthrough
case 'T':
if !c.hasSetsOrMore(2, rel) {
return errParamMismatch
}
for i := 0; i < l-1; i += 2 {
c.reflectControlQuad()
c.Path.QuadBezier(
fixed.Point26_6{
X: fixed.Int26_6((c.cntlPtX) * 64),
Y: fixed.Int26_6((c.cntlPtY) * 64)},
fixed.Point26_6{
X: fixed.Int26_6((c.points[i]) * 64),
Y: fixed.Int26_6((c.points[i+1]) * 64)})
c.lastKey = k
c.placeX = c.points[i]
c.placeY = c.points[i+1]
}
case 'c':
rel = true
fallthrough
case 'C':
if !c.hasSetsOrMore(6, rel) {
return errParamMismatch
}
for i := 0; i < l-5; i += 6 {
c.Path.CubeBezier(
fixed.Point26_6{
X: fixed.Int26_6((c.points[i]) * 64),
Y: fixed.Int26_6((c.points[i+1]) * 64)},
fixed.Point26_6{
X: fixed.Int26_6((c.points[i+2]) * 64),
Y: fixed.Int26_6((c.points[i+3]) * 64)},
fixed.Point26_6{
X: fixed.Int26_6((c.points[i+4]) * 64),
Y: fixed.Int26_6((c.points[i+5]) * 64)})
}
c.cntlPtX, c.cntlPtY = c.points[l-4], c.points[l-3]
c.placeX = c.points[l-2]
c.placeY = c.points[l-1]
case 's':
rel = true
fallthrough
case 'S':
if !c.hasSetsOrMore(4, rel) {
return errParamMismatch
}
for i := 0; i < l-3; i += 4 {
c.reflectControlCube()
c.Path.CubeBezier(fixed.Point26_6{
X: fixed.Int26_6((c.cntlPtX) * 64), Y: fixed.Int26_6((c.cntlPtY) * 64)},
fixed.Point26_6{
X: fixed.Int26_6((c.points[i]) * 64), Y: fixed.Int26_6((c.points[i+1]) * 64)},
fixed.Point26_6{
X: fixed.Int26_6((c.points[i+2]) * 64), Y: fixed.Int26_6((c.points[i+3]) * 64)})
c.lastKey = k
c.cntlPtX, c.cntlPtY = c.points[i], c.points[i+1]
c.placeX = c.points[i+2]
c.placeY = c.points[i+3]
}
case 'a', 'A':
if !c.hasSetsOrMore(7, false) {
return errParamMismatch
}
for i := 0; i < l-6; i += 7 {
if k == 'a' {
c.points[i+5] += c.placeX
c.points[i+6] += c.placeY
}
c.AddArcFromA(c.points[i:])
}
default:
if c.ErrorMode == StrictErrorMode {
return errCommandUnknown
}
if c.ErrorMode == WarnErrorMode {
log.Println("Ignoring svg command " + string(k))
}
}
// So we know how to extend some segment types
c.lastKey = k
return nil
}
func (c *PathCursor) init() {
c.placeX = 0.0
c.placeY = 0.0
c.points = c.points[0:0]
c.lastKey = ' '
c.Path.Clear()
c.inPath = false
}