Skip to content

Commit

Permalink
Use Scanx rasterizer, fixes #334
Browse files Browse the repository at this point in the history
  • Loading branch information
tdewolff committed Feb 6, 2025
1 parent 59be125 commit e419d36
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 82 deletions.
9 changes: 4 additions & 5 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -2341,10 +2341,8 @@ func (p *Path) ToPDF() string {
return sb.String()[1:] // remove the first space
}

// ToRasterizer rasterizes the path using the given rasterizer and resolution.
func (p *Path) ToRasterizer(ras *vector.Rasterizer, resolution Resolution) {
// TODO: smoothen path using Ramer-...

// ToVectorRasterizer rasterizes the path to *vector.Rasterizer using the given rasterizer and resolution.
func (p *Path) ToVectorRasterizer(ras *vector.Rasterizer, resolution Resolution) {
dpmm := resolution.DPMM()
tolerance := PixelTolerance / dpmm // tolerance of 1/10 of a pixel
dy := float64(ras.Bounds().Size().Y)
Expand Down Expand Up @@ -2401,7 +2399,8 @@ func fixedPoint26_6(x, y float64) fixed.Point26_6 {
}
}

func (p *Path) ToScanx(ras *scanx.Scanner, dy float64, resolution Resolution) {
// ToScanxScanner rasterizes the path to *scanx.Scanner using the given rasterizer, height, and resolution.
func (p *Path) ToScanxScanner(ras *scanx.Scanner, dy float64, resolution Resolution) {
dpmm := resolution.DPMM()
tolerance := PixelTolerance / dpmm // tolerance of 1/10 of a pixel
for i := 0; i < len(p.d); {
Expand Down
12 changes: 6 additions & 6 deletions path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,7 @@ func TestPathLengthParametrization(t *testing.T) {
}

func BenchmarkToRasterizer(b *testing.B) {
res := DPMM(0.5)
res := DPMM(10.0)
data, err := os.ReadFile("resources/netherlands.path")
if err != nil {
panic(err)
Expand All @@ -969,7 +969,7 @@ func BenchmarkToRasterizer(b *testing.B) {
ras.Reset(int(w), int(h))

src := image.NewUniform(image.Black)
p.ToRasterizer(ras, res)
p.ToVectorRasterizer(ras, res)
ras.Draw(img, rect, src, image.Point{0, 0})

f, _ := os.Create("ToRasterizer.png")
Expand All @@ -979,13 +979,13 @@ func BenchmarkToRasterizer(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
ras.Reset(int(w), int(h))
p.ToRasterizer(ras, res)
p.ToVectorRasterizer(ras, res)
ras.Draw(img, rect, src, image.Point{0, 0})
}
}

func BenchmarkToScanx(b *testing.B) {
res := DPMM(0.5)
res := DPMM(10.0)
data, err := os.ReadFile("resources/netherlands.path")
if err != nil {
panic(err)
Expand All @@ -1003,7 +1003,7 @@ func BenchmarkToScanx(b *testing.B) {
scanner := scanx.NewScanner(spanner, int(w), int(h))

scanner.SetColor(image.Black)
p.ToScanx(scanner, h, res)
p.ToScanxScanner(scanner, h, res)
scanner.Draw()

f, _ := os.Create("ToScanx.png")
Expand All @@ -1013,7 +1013,7 @@ func BenchmarkToScanx(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
scanner.Clear()
p.ToScanx(scanner, h, res)
p.ToScanxScanner(scanner, h, res)
scanner.Draw()
}
}
6 changes: 3 additions & 3 deletions patterns.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
type Pattern interface {
SetView(Matrix) Pattern
SetColorSpace(ColorSpace) Pattern
ClipTo(Renderer, *Path)
RenderTo(Renderer, *Path)
}

//type CanvasPattern struct {
Expand Down Expand Up @@ -126,8 +126,8 @@ func (p *HatchPattern) Tile(clip *Path) *Path {
return hatch
}

// ClipTo tiles the hatch pattern to the clipping path and renders it to the renderer.
func (p *HatchPattern) ClipTo(r Renderer, clip *Path) {
// RenderTo tiles the hatch pattern to the clipping path and renders it to the renderer.
func (p *HatchPattern) RenderTo(r Renderer, clip *Path) {
hatch := p.Tile(clip)
r.RenderPath(hatch, Style{Fill: p.Fill}, Identity)
}
Expand Down
110 changes: 42 additions & 68 deletions renderers/rasterizer/rasterizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package rasterizer

import (
"image"
"image/color"
"math"

"github.com/srwiley/rasterx"
"github.com/srwiley/scanx"
"github.com/tdewolff/canvas"
"golang.org/x/image/draw"
"golang.org/x/image/math/f64"
"golang.org/x/image/vector"
)

// TODO: add ASM optimized version for NRGBA images, since those are much faster to write as PNG
Expand All @@ -27,7 +29,8 @@ type Rasterizer struct {
resolution canvas.Resolution
colorSpace canvas.ColorSpace

ras *vector.Rasterizer // reuse
spanner *scanx.ImgSpanner
scanner *scanx.Scanner
}

// New returns a renderer that draws to a rasterized image. The final width and height of the image is the width and height (mm) multiplied by the resolution (px/mm), thus a higher resolution results in larger images. By default the linear color space is used, which assumes input and output colors are in linearRGB. If the sRGB color space is used for drawing with an average of gamma=2.2, the input and output colors are assumed to be in sRGB (a common assumption) and blending happens in linearRGB. Be aware that for text this results in thin stems for black-on-white (but wide stems for white-on-black).
Expand All @@ -48,12 +51,14 @@ func FromImage(img draw.Image, resolution canvas.Resolution, colorSpace canvas.C
if colorSpace == nil {
colorSpace = canvas.DefaultColorSpace
}
spanner := scanx.NewImgSpanner(img)
return &Rasterizer{
Image: img,
resolution: resolution,
colorSpace: colorSpace,

ras: &vector.Rasterizer{},
spanner: spanner,
scanner: scanx.NewScanner(spanner, bounds.Dx(), bounds.Dy()),
}
}

Expand Down Expand Up @@ -95,90 +100,59 @@ func (r *Rasterizer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.
}
}

padding := 2
dx, dy := 0, 0
origin := r.Bounds().Min
size := r.Bounds().Size()
dpmm := r.resolution.DPMM()
x := int(bounds.X0*dpmm) - padding
y := size.Y - int((bounds.Y1)*dpmm) - padding
w := int(bounds.W()*dpmm) + 2*padding
h := int(bounds.H()*dpmm) + 2*padding
if (x+w <= origin.X || origin.X+size.X <= x) && (y+h <= origin.Y || origin.Y+size.Y <= y) {
return // outside canvas
}

zp := image.Point{x, y}
if x < origin.X {
dx = -x
x = origin.X
}
if y < origin.Y {
dy = -y
y = origin.Y
}
if origin.X+size.X <= x+w {
w = origin.X + size.X - x
}
if origin.Y+size.Y <= y+h {
h = origin.Y + size.Y - y
}
if w <= 0 || h <= 0 {
return // has no size
}

if style.HasFill() {
if style.Fill.IsPattern() {
if hatch, ok := style.Fill.Pattern.(*canvas.HatchPattern); ok {
style.Fill = hatch.Fill
fill = hatch.Tile(fill)
} else {
pattern := style.Fill.Pattern.SetColorSpace(r.colorSpace)
pattern.RenderTo(r, fill)
}
}

var src image.Image
if style.Fill.IsColor() {
c := r.colorSpace.ToLinear(style.Fill.Color)
src = image.NewUniform(r.Image.ColorModel().Convert(c))
} else if style.Fill.IsGradient() {
if style.Fill.IsGradient() {
gradient := style.Fill.Gradient.SetColorSpace(r.colorSpace)
src = NewGradientImage(gradient, zp, size, r.resolution)
// TODO: convert to dst color model
} else if style.Fill.IsPattern() {
pattern := style.Fill.Pattern.SetColorSpace(r.colorSpace)
pattern.ClipTo(r, fill)
}
if src != nil {
r.ras.Reset(w, h)
fill = fill.Translate(-float64(x)/dpmm, -float64(size.Y-y-h)/dpmm)
fill.ToRasterizer(r.ras, r.resolution)
r.ras.Draw(r.Image, image.Rect(x, y, x+w, y+h), src, image.Point{dx, dy})
r.scanner.Clear()
r.scanner.SetColor(rasterx.ColorFunc(func(x, y int) color.Color {
// TODO: convert to dst color model
return gradient.At(float64(x), float64(y))
}))
fill.ToScanxScanner(r.scanner, float64(size.Y), r.resolution)
r.scanner.Draw()
} else if style.Fill.IsColor() {
c := r.colorSpace.ToLinear(style.Fill.Color)
r.scanner.Clear()
r.scanner.SetColor(color.Color(r.Image.ColorModel().Convert(c)))
fill.ToScanxScanner(r.scanner, float64(size.Y), r.resolution)
r.scanner.Draw()
}
}
if style.HasStroke() {
if style.Stroke.IsPattern() {
if hatch, ok := style.Stroke.Pattern.(*canvas.HatchPattern); ok {
style.Stroke = hatch.Fill
stroke = hatch.Tile(stroke)
} else {
pattern := style.Stroke.Pattern.SetColorSpace(r.colorSpace)
pattern.RenderTo(r, stroke)
}
}

var src image.Image
if style.Stroke.IsColor() {
c := r.colorSpace.ToLinear(style.Stroke.Color)
src = image.NewUniform(r.Image.ColorModel().Convert(c))
} else if style.Stroke.IsGradient() {
if style.Stroke.IsGradient() {
gradient := style.Stroke.Gradient.SetColorSpace(r.colorSpace)
src = NewGradientImage(gradient, zp, size, r.resolution)
// TODO: convert to dst color model
} else if style.Stroke.IsPattern() {
pattern := style.Stroke.Pattern.SetColorSpace(r.colorSpace)
pattern.ClipTo(r, stroke)
}
if src != nil {
r.ras.Reset(w, h)
stroke = stroke.Translate(-float64(x)/dpmm, -float64(size.Y-y-h)/dpmm)
stroke.ToRasterizer(r.ras, r.resolution)
r.ras.Draw(r.Image, image.Rect(x, y, x+w, y+h), src, image.Point{dx, dy})
r.scanner.Clear()
r.scanner.SetColor(rasterx.ColorFunc(func(x, y int) color.Color {
// TODO: convert to dst color model
return gradient.At(float64(x), float64(y))
}))
stroke.ToScanxScanner(r.scanner, float64(size.Y), r.resolution)
r.scanner.Draw()
} else if style.Stroke.IsColor() {
c := r.colorSpace.ToLinear(style.Stroke.Color)
r.scanner.Clear()
r.scanner.SetColor(color.Color(r.Image.ColorModel().Convert(c)))
stroke.ToScanxScanner(r.scanner, float64(size.Y), r.resolution)
r.scanner.Draw()
}
}
}
Expand Down

0 comments on commit e419d36

Please sign in to comment.