srwiley / rasterx

Rasterx is an SVG 2.0 path compliant rasterizer that can use either scany, the golang vector or a derivative of the freetype anti-aliaser.
BSD 3-Clause "New" or "Revised" License
133 stars 11 forks source link

incorrect rasterization of open paths #1

Closed djadala closed 6 years ago

djadala commented 6 years ago

Hi, while testing rasterx, i write this example:

package main

import (
    "image"
    "image/color"
    "image/draw"
    "log"

    "golang.org/x/exp/shiny/driver"
    "golang.org/x/exp/shiny/screen"
    "golang.org/x/exp/shiny/widget"

    "golang.org/x/image/math/fixed"

    "github.com/srwiley/rasterx"
)

const (
    S = 512
)

func main() {
    log.SetFlags(0)
    driver.Main(func(s screen.Screen) {
        src := TestMultiFunctionGV() // image.NewRGBA(image.Rect(0, 0, sw, sh))

        w := widget.NewSheet(widget.NewImage(src, src.Bounds()))
        if err := widget.RunWindow(s, w, &widget.RunWindowOptions{
            NewWindowOptions: screen.NewWindowOptions{
                Title:  "Rasterx Example",
                Width:  S,
                Height: S,
            },
        }); err != nil {
            log.Fatal(err)
        }
    })
}

//////////////////////////////////////////////////////

func toFixedP(x, y float64) (p fixed.Point26_6) {
    p.X = fixed.Int26_6(x * 64)
    p.Y = fixed.Int26_6(y * 64)
    return
}

func GetTestPath() (testPath rasterx.Path) {
    //Path for Q
    //M210.08,222.97
    testPath.Start(toFixedP(210.08, 222.97))
    //L192.55,244.95
    testPath.Line(toFixedP(192.55, 244.95))
    //Q146.53,229.95,115.55,209.55
    testPath.QuadBezier(toFixedP(146.53, 229.95), toFixedP(115.55, 209.55))
    //Q102.50,211.00,95.38,211.00
    testPath.QuadBezier(toFixedP(102.50, 211.00), toFixedP(95.38, 211.00))
    //Q56.09,211.00,31.17,182.33
    testPath.QuadBezier(toFixedP(56.09, 211.00), toFixedP(31.17, 182.33))
    //Q6.27,153.66,6.27,108.44
    testPath.QuadBezier(toFixedP(6.27, 153.66), toFixedP(6.27, 108.44))
    //Q6.27,61.89,31.44,33.94
    testPath.QuadBezier(toFixedP(6.27, 61.89), toFixedP(31.44, 33.94))
    //Q56.62,6.00,98.55,6.00
    testPath.QuadBezier(toFixedP(56.62, 6.00), toFixedP(98.55, 6.00))
    //Q141.27,6.00,166.64,33.88
    testPath.QuadBezier(toFixedP(141.27, 6.00), toFixedP(166.64, 33.88))
    //Q192.02,61.77,192.02,108.70
    testPath.QuadBezier(toFixedP(192.02, 61.77), toFixedP(192.02, 108.70))
    //Q192.02,175.67,140.86,202.05
    testPath.QuadBezier(toFixedP(192.02, 175.67), toFixedP(140.86, 202.05))
    //Q173.42,216.66,210.08,222.97
    testPath.QuadBezier(toFixedP(173.42, 216.66), toFixedP(210.08, 222.97))
    //z
    testPath.Stop(false)
    //M162.22,109.69 M162.22,109.69
    testPath.Start(toFixedP(162.22, 109.69))
    //Q162.22,70.11,145.61,48.55
    testPath.QuadBezier(toFixedP(162.22, 70.11), toFixedP(145.61, 48.55))
    //Q129.00,27.00,98.42,27.00
    testPath.QuadBezier(toFixedP(129.00, 27.00), toFixedP(98.42, 27.00))
    //Q69.14,27.00,52.53,48.62
    testPath.QuadBezier(toFixedP(69.14, 27.00), toFixedP(52.53, 48.62))
    //Q35.92,70.25,35.92,108.50
    testPath.QuadBezier(toFixedP(35.92, 70.25), toFixedP(35.92, 108.50))
    //Q35.92,146.75,52.53,168.38
    testPath.QuadBezier(toFixedP(35.92, 146.75), toFixedP(52.53, 168.38))
    //Q69.14,190.00,98.42,190.00
    testPath.QuadBezier(toFixedP(69.14, 190.00), toFixedP(98.42, 190.00))
    //Q128.34,190.00,145.28,168.70
    testPath.QuadBezier(toFixedP(128.34, 190.00), toFixedP(145.28, 168.70))
    //Q162.22,147.41,162.22,109.69
    testPath.QuadBezier(toFixedP(162.22, 147.41), toFixedP(162.22, 109.69))
    //z
    testPath.Stop(false)

    return testPath
}

// TestMultiFunction tests a Dasher's ability to function
// as a filler, stroker, and dasher by invoking the corresponding anonymous structs
func TestMultiFunctionGV() image.Image {
    img := image.NewRGBA(image.Rect(0, 0, S, S))
    src := image.NewUniform(color.NRGBA{255, 0, 0, 255})
    scannerGV := rasterx.NewScannerGV(S, S, img, img.Bounds(), src, image.ZP)

    draw.Draw(img, img.Bounds(), image.White, image.ZP, draw.Src)

    d := rasterx.NewDasher(S, S, scannerGV)
    d.SetStroke(10*64, 4*64, rasterx.RoundCap, nil, rasterx.RoundGap, rasterx.ArcClip, []float64{33, 12}, 0)
    p := GetTestPath()

    p.AddTo(d)
    d.Draw()
    d.Clear()
    return img
}

Note testPath.Stop(false) in GetTestPath. Result is in attached file: screenshot

i able to resolve issue by adding new PathCommand:

// Human readable path constants
const (
    PathMoveTo PathCommand = iota
    PathLineTo
    PathQuadTo
    PathCubicTo
    PathClose
    PathCloseF
)

...

// Close joins the ends of the path
func (p *Path) Stop(closeLoop bool) {
    if closeLoop {
        *p = append(*p, fixed.Int26_6(PathClose))
    } else {
        *p = append(*p, fixed.Int26_6(PathCloseF))
    }
}

// AddPath adds the Path p to q. This bridges the path and adder interface.
func (p Path) AddTo(q Adder) {
    for i := 0; i < len(p); {
        switch PathCommand(p[i]) {
        case PathMoveTo:
            q.Start(fixed.Point26_6{p[i+1], p[i+2]})
            i += 3
        case PathLineTo:
            q.Line(fixed.Point26_6{p[i+1], p[i+2]})
            i += 3
        case PathQuadTo:
            q.QuadBezier(fixed.Point26_6{p[i+1], p[i+2]}, fixed.Point26_6{p[i+3], p[i+4]})
            i += 5
        case PathCubicTo:
            q.CubeBezier(fixed.Point26_6{p[i+1], p[i+2]},
                fixed.Point26_6{p[i+3], p[i+4]}, fixed.Point26_6{p[i+5], p[i+6]})
            i += 7
        case PathClose:
            q.Stop(true)
            i += 1
        case PathCloseF:
            q.Stop(false)
            i += 1
        default:
            panic("adder geom: bad path")
        }
    }
    q.Stop(false)
}

but i am not sure if this is correct fix.

Regards.

srwiley commented 6 years ago

Thank you Jamil. Good catch!

It is a bug, I was able to reproduce and fix it. What happened is that there is no explicit "close open" command in the path command set. However, rather than add that as a separate command, what I was trying to do was, for the sake of simplicity, make the "close open" implicit, either when the path stops, or if a "moveto" is called while a path is open. It is the latter case that was not being handled correctly, so I needed to add another q.Stop(false) right at the beginning of the PathMoveto case in the AddTo function. If a path is currently not being drawn, Stop should not do anything. However, to make sure of that, upon reviewing my code I also added a simple Clear command to the Filler struct to be sure that a stray line is not drawn if the filler does not stop cleanly due to an error or something like that.

Specifically:

Added a Stop(false) in AddTo func of geomx.go:

func (p Path) AddTo(q Adder) {
    for i := 0; i < len(p); {
        switch PathCommand(p[i]) {
        case PathMoveTo:
            q.Stop(false) // Fixes issues #1 by described by Djadala; implict close if currently in path.
            q.Start(fixed.Point26_6{p[i+1], p[i+2]})
            i +=
 ...

Added this in filler.go in case the filler does not stop cleanly:

func (r *Filler) Clear() {
    r.a = fixed.Point26_6{}
    r.first = r.a
    r.Scanner.Clear()
}

So, I am going to close this issue since I think this fixes it. Please let me know if it does not work for you.

-Steve