8a4428169d
This is more consistent with the standard library's image/color package. The benchmarks didn't seem to change significantly.
601 lines
15 KiB
Go
601 lines
15 KiB
Go
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
|
// Use of this source code is governed by your choice of either the
|
|
// FreeType License or the GNU General Public License version 2 (or
|
|
// any later version), both of which can be found in the LICENSE file.
|
|
|
|
// Package raster provides an anti-aliasing 2-D rasterizer.
|
|
//
|
|
// It is part of the larger Freetype suite of font-related packages, but the
|
|
// raster package is not specific to font rasterization, and can be used
|
|
// standalone without any other Freetype package.
|
|
//
|
|
// Rasterization is done by the same area/coverage accumulation algorithm as
|
|
// the Freetype "smooth" module, and the Anti-Grain Geometry library. A
|
|
// description of the area/coverage algorithm is at
|
|
// http://projects.tuxee.net/cl-vectors/section-the-cl-aa-algorithm
|
|
package raster // import "github.com/golang/freetype/raster"
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
// A cell is part of a linked list (for a given yi co-ordinate) of accumulated
|
|
// area/coverage for the pixel at (xi, yi).
|
|
type cell struct {
|
|
xi int
|
|
area, cover int
|
|
next int
|
|
}
|
|
|
|
type Rasterizer struct {
|
|
// If false, the default behavior is to use the even-odd winding fill
|
|
// rule during Rasterize.
|
|
UseNonZeroWinding bool
|
|
// An offset (in pixels) to the painted spans.
|
|
Dx, Dy int
|
|
|
|
// The width of the Rasterizer. The height is implicit in len(cellIndex).
|
|
width int
|
|
// splitScaleN is the scaling factor used to determine how many times
|
|
// to decompose a quadratic or cubic segment into a linear approximation.
|
|
splitScale2, splitScale3 int
|
|
|
|
// The current pen position.
|
|
a fixed.Point26_6
|
|
// The current cell and its area/coverage being accumulated.
|
|
xi, yi int
|
|
area, cover int
|
|
|
|
// Saved cells.
|
|
cell []cell
|
|
// Linked list of cells, one per row.
|
|
cellIndex []int
|
|
// Buffers.
|
|
cellBuf [256]cell
|
|
cellIndexBuf [64]int
|
|
spanBuf [64]Span
|
|
}
|
|
|
|
// findCell returns the index in r.cell for the cell corresponding to
|
|
// (r.xi, r.yi). The cell is created if necessary.
|
|
func (r *Rasterizer) findCell() int {
|
|
if r.yi < 0 || r.yi >= len(r.cellIndex) {
|
|
return -1
|
|
}
|
|
xi := r.xi
|
|
if xi < 0 {
|
|
xi = -1
|
|
} else if xi > r.width {
|
|
xi = r.width
|
|
}
|
|
i, prev := r.cellIndex[r.yi], -1
|
|
for i != -1 && r.cell[i].xi <= xi {
|
|
if r.cell[i].xi == xi {
|
|
return i
|
|
}
|
|
i, prev = r.cell[i].next, i
|
|
}
|
|
c := len(r.cell)
|
|
if c == cap(r.cell) {
|
|
buf := make([]cell, c, 4*c)
|
|
copy(buf, r.cell)
|
|
r.cell = buf[0 : c+1]
|
|
} else {
|
|
r.cell = r.cell[0 : c+1]
|
|
}
|
|
r.cell[c] = cell{xi, 0, 0, i}
|
|
if prev == -1 {
|
|
r.cellIndex[r.yi] = c
|
|
} else {
|
|
r.cell[prev].next = c
|
|
}
|
|
return c
|
|
}
|
|
|
|
// saveCell saves any accumulated r.area/r.cover for (r.xi, r.yi).
|
|
func (r *Rasterizer) saveCell() {
|
|
if r.area != 0 || r.cover != 0 {
|
|
i := r.findCell()
|
|
if i != -1 {
|
|
r.cell[i].area += r.area
|
|
r.cell[i].cover += r.cover
|
|
}
|
|
r.area = 0
|
|
r.cover = 0
|
|
}
|
|
}
|
|
|
|
// setCell sets the (xi, yi) cell that r is accumulating area/coverage for.
|
|
func (r *Rasterizer) setCell(xi, yi int) {
|
|
if r.xi != xi || r.yi != yi {
|
|
r.saveCell()
|
|
r.xi, r.yi = xi, yi
|
|
}
|
|
}
|
|
|
|
// scan accumulates area/coverage for the yi'th scanline, going from
|
|
// x0 to x1 in the horizontal direction (in 26.6 fixed point co-ordinates)
|
|
// and from y0f to y1f fractional vertical units within that scanline.
|
|
func (r *Rasterizer) scan(yi int, x0, y0f, x1, y1f fixed.Int26_6) {
|
|
// Break the 26.6 fixed point X co-ordinates into integral and fractional parts.
|
|
x0i := int(x0) / 64
|
|
x0f := x0 - fixed.Int26_6(64*x0i)
|
|
x1i := int(x1) / 64
|
|
x1f := x1 - fixed.Int26_6(64*x1i)
|
|
|
|
// A perfectly horizontal scan.
|
|
if y0f == y1f {
|
|
r.setCell(x1i, yi)
|
|
return
|
|
}
|
|
dx, dy := x1-x0, y1f-y0f
|
|
// A single cell scan.
|
|
if x0i == x1i {
|
|
r.area += int((x0f + x1f) * dy)
|
|
r.cover += int(dy)
|
|
return
|
|
}
|
|
// There are at least two cells. Apart from the first and last cells,
|
|
// all intermediate cells go through the full width of the cell,
|
|
// or 64 units in 26.6 fixed point format.
|
|
var (
|
|
p, q, edge0, edge1 fixed.Int26_6
|
|
xiDelta int
|
|
)
|
|
if dx > 0 {
|
|
p, q = (64-x0f)*dy, dx
|
|
edge0, edge1, xiDelta = 0, 64, 1
|
|
} else {
|
|
p, q = x0f*dy, -dx
|
|
edge0, edge1, xiDelta = 64, 0, -1
|
|
}
|
|
yDelta, yRem := p/q, p%q
|
|
if yRem < 0 {
|
|
yDelta -= 1
|
|
yRem += q
|
|
}
|
|
// Do the first cell.
|
|
xi, y := x0i, y0f
|
|
r.area += int((x0f + edge1) * yDelta)
|
|
r.cover += int(yDelta)
|
|
xi, y = xi+xiDelta, y+yDelta
|
|
r.setCell(xi, yi)
|
|
if xi != x1i {
|
|
// Do all the intermediate cells.
|
|
p = 64 * (y1f - y + yDelta)
|
|
fullDelta, fullRem := p/q, p%q
|
|
if fullRem < 0 {
|
|
fullDelta -= 1
|
|
fullRem += q
|
|
}
|
|
yRem -= q
|
|
for xi != x1i {
|
|
yDelta = fullDelta
|
|
yRem += fullRem
|
|
if yRem >= 0 {
|
|
yDelta += 1
|
|
yRem -= q
|
|
}
|
|
r.area += int(64 * yDelta)
|
|
r.cover += int(yDelta)
|
|
xi, y = xi+xiDelta, y+yDelta
|
|
r.setCell(xi, yi)
|
|
}
|
|
}
|
|
// Do the last cell.
|
|
yDelta = y1f - y
|
|
r.area += int((edge0 + x1f) * yDelta)
|
|
r.cover += int(yDelta)
|
|
}
|
|
|
|
// Start starts a new curve at the given point.
|
|
func (r *Rasterizer) Start(a fixed.Point26_6) {
|
|
r.setCell(int(a.X/64), int(a.Y/64))
|
|
r.a = a
|
|
}
|
|
|
|
// Add1 adds a linear segment to the current curve.
|
|
func (r *Rasterizer) Add1(b fixed.Point26_6) {
|
|
x0, y0 := r.a.X, r.a.Y
|
|
x1, y1 := b.X, b.Y
|
|
dx, dy := x1-x0, y1-y0
|
|
// Break the 26.6 fixed point Y co-ordinates into integral and fractional
|
|
// parts.
|
|
y0i := int(y0) / 64
|
|
y0f := y0 - fixed.Int26_6(64*y0i)
|
|
y1i := int(y1) / 64
|
|
y1f := y1 - fixed.Int26_6(64*y1i)
|
|
|
|
if y0i == y1i {
|
|
// There is only one scanline.
|
|
r.scan(y0i, x0, y0f, x1, y1f)
|
|
|
|
} else if dx == 0 {
|
|
// This is a vertical line segment. We avoid calling r.scan and instead
|
|
// manipulate r.area and r.cover directly.
|
|
var (
|
|
edge0, edge1 fixed.Int26_6
|
|
yiDelta int
|
|
)
|
|
if dy > 0 {
|
|
edge0, edge1, yiDelta = 0, 64, 1
|
|
} else {
|
|
edge0, edge1, yiDelta = 64, 0, -1
|
|
}
|
|
x0i, yi := int(x0)/64, y0i
|
|
x0fTimes2 := (int(x0) - (64 * x0i)) * 2
|
|
// Do the first pixel.
|
|
dcover := int(edge1 - y0f)
|
|
darea := int(x0fTimes2 * dcover)
|
|
r.area += darea
|
|
r.cover += dcover
|
|
yi += yiDelta
|
|
r.setCell(x0i, yi)
|
|
// Do all the intermediate pixels.
|
|
dcover = int(edge1 - edge0)
|
|
darea = int(x0fTimes2 * dcover)
|
|
for yi != y1i {
|
|
r.area += darea
|
|
r.cover += dcover
|
|
yi += yiDelta
|
|
r.setCell(x0i, yi)
|
|
}
|
|
// Do the last pixel.
|
|
dcover = int(y1f - edge0)
|
|
darea = int(x0fTimes2 * dcover)
|
|
r.area += darea
|
|
r.cover += dcover
|
|
|
|
} else {
|
|
// There are at least two scanlines. Apart from the first and last
|
|
// scanlines, all intermediate scanlines go through the full height of
|
|
// the row, or 64 units in 26.6 fixed point format.
|
|
var (
|
|
p, q, edge0, edge1 fixed.Int26_6
|
|
yiDelta int
|
|
)
|
|
if dy > 0 {
|
|
p, q = (64-y0f)*dx, dy
|
|
edge0, edge1, yiDelta = 0, 64, 1
|
|
} else {
|
|
p, q = y0f*dx, -dy
|
|
edge0, edge1, yiDelta = 64, 0, -1
|
|
}
|
|
xDelta, xRem := p/q, p%q
|
|
if xRem < 0 {
|
|
xDelta -= 1
|
|
xRem += q
|
|
}
|
|
// Do the first scanline.
|
|
x, yi := x0, y0i
|
|
r.scan(yi, x, y0f, x+xDelta, edge1)
|
|
x, yi = x+xDelta, yi+yiDelta
|
|
r.setCell(int(x)/64, yi)
|
|
if yi != y1i {
|
|
// Do all the intermediate scanlines.
|
|
p = 64 * dx
|
|
fullDelta, fullRem := p/q, p%q
|
|
if fullRem < 0 {
|
|
fullDelta -= 1
|
|
fullRem += q
|
|
}
|
|
xRem -= q
|
|
for yi != y1i {
|
|
xDelta = fullDelta
|
|
xRem += fullRem
|
|
if xRem >= 0 {
|
|
xDelta += 1
|
|
xRem -= q
|
|
}
|
|
r.scan(yi, x, edge0, x+xDelta, edge1)
|
|
x, yi = x+xDelta, yi+yiDelta
|
|
r.setCell(int(x)/64, yi)
|
|
}
|
|
}
|
|
// Do the last scanline.
|
|
r.scan(yi, x, edge0, x1, y1f)
|
|
}
|
|
// The next lineTo starts from b.
|
|
r.a = b
|
|
}
|
|
|
|
// Add2 adds a quadratic segment to the current curve.
|
|
func (r *Rasterizer) Add2(b, c fixed.Point26_6) {
|
|
// Calculate nSplit (the number of recursive decompositions) based on how
|
|
// 'curvy' it is. Specifically, how much the middle point b deviates from
|
|
// (a+c)/2.
|
|
dev := maxAbs(r.a.X-2*b.X+c.X, r.a.Y-2*b.Y+c.Y) / fixed.Int26_6(r.splitScale2)
|
|
nsplit := 0
|
|
for dev > 0 {
|
|
dev /= 4
|
|
nsplit++
|
|
}
|
|
// dev is 32-bit, and nsplit++ every time we shift off 2 bits, so maxNsplit
|
|
// is 16.
|
|
const maxNsplit = 16
|
|
if nsplit > maxNsplit {
|
|
panic("freetype/raster: Add2 nsplit too large: " + strconv.Itoa(nsplit))
|
|
}
|
|
// Recursively decompose the curve nSplit levels deep.
|
|
var (
|
|
pStack [2*maxNsplit + 3]fixed.Point26_6
|
|
sStack [maxNsplit + 1]int
|
|
i int
|
|
)
|
|
sStack[0] = nsplit
|
|
pStack[0] = c
|
|
pStack[1] = b
|
|
pStack[2] = r.a
|
|
for i >= 0 {
|
|
s := sStack[i]
|
|
p := pStack[2*i:]
|
|
if s > 0 {
|
|
// Split the quadratic curve p[:3] into an equivalent set of two
|
|
// shorter curves: p[:3] and p[2:5]. The new p[4] is the old p[2],
|
|
// and p[0] is unchanged.
|
|
mx := p[1].X
|
|
p[4].X = p[2].X
|
|
p[3].X = (p[4].X + mx) / 2
|
|
p[1].X = (p[0].X + mx) / 2
|
|
p[2].X = (p[1].X + p[3].X) / 2
|
|
my := p[1].Y
|
|
p[4].Y = p[2].Y
|
|
p[3].Y = (p[4].Y + my) / 2
|
|
p[1].Y = (p[0].Y + my) / 2
|
|
p[2].Y = (p[1].Y + p[3].Y) / 2
|
|
// The two shorter curves have one less split to do.
|
|
sStack[i] = s - 1
|
|
sStack[i+1] = s - 1
|
|
i++
|
|
} else {
|
|
// Replace the level-0 quadratic with a two-linear-piece
|
|
// approximation.
|
|
midx := (p[0].X + 2*p[1].X + p[2].X) / 4
|
|
midy := (p[0].Y + 2*p[1].Y + p[2].Y) / 4
|
|
r.Add1(fixed.Point26_6{midx, midy})
|
|
r.Add1(p[0])
|
|
i--
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add3 adds a cubic segment to the current curve.
|
|
func (r *Rasterizer) Add3(b, c, d fixed.Point26_6) {
|
|
// Calculate nSplit (the number of recursive decompositions) based on how
|
|
// 'curvy' it is.
|
|
dev2 := maxAbs(r.a.X-3*(b.X+c.X)+d.X, r.a.Y-3*(b.Y+c.Y)+d.Y) / fixed.Int26_6(r.splitScale2)
|
|
dev3 := maxAbs(r.a.X-2*b.X+d.X, r.a.Y-2*b.Y+d.Y) / fixed.Int26_6(r.splitScale3)
|
|
nsplit := 0
|
|
for dev2 > 0 || dev3 > 0 {
|
|
dev2 /= 8
|
|
dev3 /= 4
|
|
nsplit++
|
|
}
|
|
// devN is 32-bit, and nsplit++ every time we shift off 2 bits, so
|
|
// maxNsplit is 16.
|
|
const maxNsplit = 16
|
|
if nsplit > maxNsplit {
|
|
panic("freetype/raster: Add3 nsplit too large: " + strconv.Itoa(nsplit))
|
|
}
|
|
// Recursively decompose the curve nSplit levels deep.
|
|
var (
|
|
pStack [3*maxNsplit + 4]fixed.Point26_6
|
|
sStack [maxNsplit + 1]int
|
|
i int
|
|
)
|
|
sStack[0] = nsplit
|
|
pStack[0] = d
|
|
pStack[1] = c
|
|
pStack[2] = b
|
|
pStack[3] = r.a
|
|
for i >= 0 {
|
|
s := sStack[i]
|
|
p := pStack[3*i:]
|
|
if s > 0 {
|
|
// Split the cubic curve p[:4] into an equivalent set of two
|
|
// shorter curves: p[:4] and p[3:7]. The new p[6] is the old p[3],
|
|
// and p[0] is unchanged.
|
|
m01x := (p[0].X + p[1].X) / 2
|
|
m12x := (p[1].X + p[2].X) / 2
|
|
m23x := (p[2].X + p[3].X) / 2
|
|
p[6].X = p[3].X
|
|
p[5].X = m23x
|
|
p[1].X = m01x
|
|
p[2].X = (m01x + m12x) / 2
|
|
p[4].X = (m12x + m23x) / 2
|
|
p[3].X = (p[2].X + p[4].X) / 2
|
|
m01y := (p[0].Y + p[1].Y) / 2
|
|
m12y := (p[1].Y + p[2].Y) / 2
|
|
m23y := (p[2].Y + p[3].Y) / 2
|
|
p[6].Y = p[3].Y
|
|
p[5].Y = m23y
|
|
p[1].Y = m01y
|
|
p[2].Y = (m01y + m12y) / 2
|
|
p[4].Y = (m12y + m23y) / 2
|
|
p[3].Y = (p[2].Y + p[4].Y) / 2
|
|
// The two shorter curves have one less split to do.
|
|
sStack[i] = s - 1
|
|
sStack[i+1] = s - 1
|
|
i++
|
|
} else {
|
|
// Replace the level-0 cubic with a two-linear-piece approximation.
|
|
midx := (p[0].X + 3*(p[1].X+p[2].X) + p[3].X) / 8
|
|
midy := (p[0].Y + 3*(p[1].Y+p[2].Y) + p[3].Y) / 8
|
|
r.Add1(fixed.Point26_6{midx, midy})
|
|
r.Add1(p[0])
|
|
i--
|
|
}
|
|
}
|
|
}
|
|
|
|
// AddPath adds the given Path.
|
|
func (r *Rasterizer) AddPath(p Path) {
|
|
for i := 0; i < len(p); {
|
|
switch p[i] {
|
|
case 0:
|
|
r.Start(
|
|
fixed.Point26_6{p[i+1], p[i+2]},
|
|
)
|
|
i += 4
|
|
case 1:
|
|
r.Add1(
|
|
fixed.Point26_6{p[i+1], p[i+2]},
|
|
)
|
|
i += 4
|
|
case 2:
|
|
r.Add2(
|
|
fixed.Point26_6{p[i+1], p[i+2]},
|
|
fixed.Point26_6{p[i+3], p[i+4]},
|
|
)
|
|
i += 6
|
|
case 3:
|
|
r.Add3(
|
|
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 += 8
|
|
default:
|
|
panic("freetype/raster: bad path")
|
|
}
|
|
}
|
|
}
|
|
|
|
// AddStroke adds a stroked Path.
|
|
func (r *Rasterizer) AddStroke(q Path, width fixed.Int26_6, cr Capper, jr Joiner) {
|
|
Stroke(r, q, width, cr, jr)
|
|
}
|
|
|
|
// areaToAlpha converts an area value to a uint32 alpha value. A completely
|
|
// filled pixel corresponds to an area of 64*64*2, and an alpha of 0xffff. The
|
|
// conversion of area values greater than this depends on the winding rule:
|
|
// even-odd or non-zero.
|
|
func (r *Rasterizer) areaToAlpha(area int) uint32 {
|
|
// The C Freetype implementation (version 2.3.12) does "alpha := area>>1"
|
|
// without the +1. Round-to-nearest gives a more symmetric result than
|
|
// round-down. The C implementation also returns 8-bit alpha, not 16-bit
|
|
// alpha.
|
|
a := (area + 1) >> 1
|
|
if a < 0 {
|
|
a = -a
|
|
}
|
|
alpha := uint32(a)
|
|
if r.UseNonZeroWinding {
|
|
if alpha > 0x0fff {
|
|
alpha = 0x0fff
|
|
}
|
|
} else {
|
|
alpha &= 0x1fff
|
|
if alpha > 0x1000 {
|
|
alpha = 0x2000 - alpha
|
|
} else if alpha == 0x1000 {
|
|
alpha = 0x0fff
|
|
}
|
|
}
|
|
// alpha is now in the range [0x0000, 0x0fff]. Convert that 12-bit alpha to
|
|
// 16-bit alpha.
|
|
return alpha<<4 | alpha>>8
|
|
}
|
|
|
|
// Rasterize converts r's accumulated curves into Spans for p. The Spans passed
|
|
// to p are non-overlapping, and sorted by Y and then X. They all have non-zero
|
|
// width (and 0 <= X0 < X1 <= r.width) and non-zero A, except for the final
|
|
// Span, which has Y, X0, X1 and A all equal to zero.
|
|
func (r *Rasterizer) Rasterize(p Painter) {
|
|
r.saveCell()
|
|
s := 0
|
|
for yi := 0; yi < len(r.cellIndex); yi++ {
|
|
xi, cover := 0, 0
|
|
for c := r.cellIndex[yi]; c != -1; c = r.cell[c].next {
|
|
if cover != 0 && r.cell[c].xi > xi {
|
|
alpha := r.areaToAlpha(cover * 64 * 2)
|
|
if alpha != 0 {
|
|
xi0, xi1 := xi, r.cell[c].xi
|
|
if xi0 < 0 {
|
|
xi0 = 0
|
|
}
|
|
if xi1 >= r.width {
|
|
xi1 = r.width
|
|
}
|
|
if xi0 < xi1 {
|
|
r.spanBuf[s] = Span{yi + r.Dy, xi0 + r.Dx, xi1 + r.Dx, alpha}
|
|
s++
|
|
}
|
|
}
|
|
}
|
|
cover += r.cell[c].cover
|
|
alpha := r.areaToAlpha(cover*64*2 - r.cell[c].area)
|
|
xi = r.cell[c].xi + 1
|
|
if alpha != 0 {
|
|
xi0, xi1 := r.cell[c].xi, xi
|
|
if xi0 < 0 {
|
|
xi0 = 0
|
|
}
|
|
if xi1 >= r.width {
|
|
xi1 = r.width
|
|
}
|
|
if xi0 < xi1 {
|
|
r.spanBuf[s] = Span{yi + r.Dy, xi0 + r.Dx, xi1 + r.Dx, alpha}
|
|
s++
|
|
}
|
|
}
|
|
if s > len(r.spanBuf)-2 {
|
|
p.Paint(r.spanBuf[:s], false)
|
|
s = 0
|
|
}
|
|
}
|
|
}
|
|
p.Paint(r.spanBuf[:s], true)
|
|
}
|
|
|
|
// Clear cancels any previous calls to r.Start or r.AddXxx.
|
|
func (r *Rasterizer) Clear() {
|
|
r.a = fixed.Point26_6{}
|
|
r.xi = 0
|
|
r.yi = 0
|
|
r.area = 0
|
|
r.cover = 0
|
|
r.cell = r.cell[:0]
|
|
for i := 0; i < len(r.cellIndex); i++ {
|
|
r.cellIndex[i] = -1
|
|
}
|
|
}
|
|
|
|
// SetBounds sets the maximum width and height of the rasterized image and
|
|
// calls Clear. The width and height are in pixels, not fixed.Int26_6 units.
|
|
func (r *Rasterizer) SetBounds(width, height int) {
|
|
if width < 0 {
|
|
width = 0
|
|
}
|
|
if height < 0 {
|
|
height = 0
|
|
}
|
|
// Use the same ssN heuristic as the C Freetype (version 2.4.0)
|
|
// implementation.
|
|
ss2, ss3 := 32, 16
|
|
if width > 24 || height > 24 {
|
|
ss2, ss3 = 2*ss2, 2*ss3
|
|
if width > 120 || height > 120 {
|
|
ss2, ss3 = 2*ss2, 2*ss3
|
|
}
|
|
}
|
|
r.width = width
|
|
r.splitScale2 = ss2
|
|
r.splitScale3 = ss3
|
|
r.cell = r.cellBuf[:0]
|
|
if height > len(r.cellIndexBuf) {
|
|
r.cellIndex = make([]int, height)
|
|
} else {
|
|
r.cellIndex = r.cellIndexBuf[:height]
|
|
}
|
|
r.Clear()
|
|
}
|
|
|
|
// NewRasterizer creates a new Rasterizer with the given bounds.
|
|
func NewRasterizer(width, height int) *Rasterizer {
|
|
r := new(Rasterizer)
|
|
r.SetBounds(width, height)
|
|
return r
|
|
}
|