From 4aa737507256d7d90dd744f668d49a4fe260477e Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Tue, 13 Apr 2010 22:26:10 +1000 Subject: [PATCH] Freetype-Go: batch Spans when painting. Introduce MonochromePainter and GammaCorrectionPainter. R=r, rsc CC=golang-dev http://codereview.appspot.com/904041 --- .hgignore | 1 + example/gamma/main.go | 84 ++++++++++++++++++++ example/raster/main.go | 6 +- freetype/raster/Makefile | 1 + freetype/raster/paint.go | 161 ++++++++++++++++++++++++++++++++++++++ freetype/raster/raster.go | 68 ++++------------ 6 files changed, 266 insertions(+), 55 deletions(-) create mode 100644 example/gamma/main.go create mode 100644 freetype/raster/paint.go diff --git a/.hgignore b/.hgignore index af8f845..86388eb 100644 --- a/.hgignore +++ b/.hgignore @@ -14,6 +14,7 @@ syntax:glob core _obj _test +out.png syntax:regexp ^.*/core.[0-9]*$ diff --git a/example/gamma/main.go b/example/gamma/main.go new file mode 100644 index 0000000..ba5c087 --- /dev/null +++ b/example/gamma/main.go @@ -0,0 +1,84 @@ +// 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, +// both of which can be found in the LICENSE file. + +package main + +import ( + "bufio" + "exp/draw" + "fmt" + "image" + "image/png" + "log" + "os" + + "freetype-go.googlecode.com/hg/freetype/raster" +) + +func p(x, y int) raster.Point { + return raster.Point{raster.Fixed(x * 256), raster.Fixed(y * 256)} +} + +func clear(m *image.Alpha) { + for y := 0; y < m.Height(); y++ { + for x := 0; x < m.Width(); x++ { + m.Pixel[y][x] = image.AlphaColor{0} + } + } +} + +func main() { + // Draw a rounded corner that is one pixel wide. + r := raster.New(50, 50) + r.Start(p(5, 5)) + r.Add1(p(5, 25)) + r.Add2(p(5, 45), p(25, 45)) + r.Add1(p(45, 45)) + r.Add1(p(45, 44)) + r.Add1(p(26, 44)) + r.Add2(p(6, 44), p(6, 24)) + r.Add1(p(6, 5)) + r.Add1(p(5, 5)) + + // Rasterize that curve multiple times at different gammas. + const ( + w = 600 + h = 200 + ) + rgba := image.NewRGBA(w, h) + draw.Draw(rgba, draw.Rect(0, 0, w, h/2), image.Black, draw.ZP) + draw.Draw(rgba, draw.Rect(0, h/2, w, h), image.White, draw.ZP) + mask := image.NewAlpha(50, 50) + painter := raster.AlphaSrcPainter(mask) + gammas := []float64{1.0 / 10.0, 1.0 / 3.0, 1.0 / 2.0, 2.0 / 3.0, 4.0 / 5.0, 1.0, 5.0 / 4.0, 3.0 / 2.0, 2.0, 3.0, 10.0} + for i, g := range gammas { + clear(mask) + r.Rasterize(raster.GammaCorrectionPainter(painter, g)) + x, y := 50*i+25, 25 + draw.DrawMask(rgba, draw.Rect(x, y, x+50, y+50), image.White, draw.ZP, mask, draw.ZP, draw.Over) + y += 100 + draw.DrawMask(rgba, draw.Rect(x, y, x+50, y+50), image.Black, draw.ZP, mask, draw.ZP, draw.Over) + } + + // Save that RGBA image to disk. + f, err := os.Open("out.png", os.O_CREAT|os.O_WRONLY, 0600) + if err != nil { + log.Stderr(err) + os.Exit(1) + } + defer f.Close() + b := bufio.NewWriter(f) + err = png.Encode(b, rgba) + if err != nil { + log.Stderr(err) + os.Exit(1) + } + err = b.Flush() + if err != nil { + log.Stderr(err) + os.Exit(1) + } + fmt.Println("Wrote out.png OK.") +} diff --git a/example/raster/main.go b/example/raster/main.go index 4e16133..6b73211 100644 --- a/example/raster/main.go +++ b/example/raster/main.go @@ -155,19 +155,19 @@ func main() { f, err := os.Open("out.png", os.O_CREAT|os.O_WRONLY, 0600) if err != nil { log.Stderr(err) - return + os.Exit(1) } defer f.Close() b := bufio.NewWriter(f) err = png.Encode(b, rgba) if err != nil { log.Stderr(err) - return + os.Exit(1) } err = b.Flush() if err != nil { log.Stderr(err) - return + os.Exit(1) } fmt.Println("Wrote out.png OK.") } diff --git a/freetype/raster/Makefile b/freetype/raster/Makefile index 3871e7b..c32d15f 100644 --- a/freetype/raster/Makefile +++ b/freetype/raster/Makefile @@ -7,6 +7,7 @@ include $(GOROOT)/src/Make.$(GOARCH) TARG=freetype-go.googlecode.com/hg/freetype/raster GOFILES=\ + paint.go\ raster.go\ include $(GOROOT)/src/Make.pkg diff --git a/freetype/raster/paint.go b/freetype/raster/paint.go new file mode 100644 index 0000000..4d31a10 --- /dev/null +++ b/freetype/raster/paint.go @@ -0,0 +1,161 @@ +// 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, +// both of which can be found in the LICENSE file. + +package raster + +import ( + "image" + "math" +) + +// A Span is a horizontal segment of pixels with constant alpha. X0 is an +// inclusive bound and X1 is exclusive, the same as for slices. A fully +// opaque Span has A == 1<<32 - 1. +type Span struct { + Y, X0, X1 int + A uint32 +} + +// A Painter knows how to paint a batch of Spans. A Span's alpha is non-zero +// until the final Span of the rasterization, which is the zero value Span. +// A rasterization may involve Painting multiple batches, but the final zero +// value Span will occur only once per rasterization, not once per Paint call. +// Paint may use all of ss as scratch space during the call. +type Painter interface { + Paint(ss []Span) +} + +// The PainterFunc type adapts an ordinary function to the Painter interface. +type PainterFunc func(ss []Span) + +// Paint just delegates the call to f. +func (f PainterFunc) Paint(ss []Span) { f(ss) } + +// AlphaOverPainter returns a Painter that paints onto the given Alpha image +// using the "src over dst" Porter-Duff composition operator. +func AlphaOverPainter(m *image.Alpha) Painter { + return PainterFunc(func(ss []Span) { + for _, s := range ss { + a := int(s.A >> 24) + p := m.Pixel[s.Y] + for i := s.X0; i < s.X1; i++ { + ai := int(p[i].A) + ai = (ai*255 + (255-ai)*a) / 255 + p[i] = image.AlphaColor{uint8(ai)} + } + } + }) +} + +// AlphaSrcPainter returns a Painter that paints onto the given Alpha image +// using the "src" Porter-Duff composition operator. +func AlphaSrcPainter(m *image.Alpha) Painter { + return PainterFunc(func(ss []Span) { + for _, s := range ss { + color := image.AlphaColor{uint8(s.A >> 24)} + p := m.Pixel[s.Y] + for i := s.X0; i < s.X1; i++ { + p[i] = color + } + } + }) +} + +// A monochromePainter has a wrapped painter and an accumulator for merging +// adjacent opaque Spans. +type monochromePainter struct { + p Painter + y, x0, x1 int +} + +// Paint delegates to the wrapped Painter after quantizing each Span's alpha +// values and merging adjacent fully opaque Spans. +func (m *monochromePainter) Paint(ss []Span) { + // We compact the ss slice, discarding any Spans whose alpha quantizes to zero. + j := 0 + for _, s := range ss { + if s.A >= 1<<31 { + if m.y == s.Y && m.x1 == s.X0 { + m.x1 = s.X1 + } else { + ss[j] = Span{m.y, m.x0, m.x1, 1<<32 - 1} + j++ + m.y, m.x0, m.x1 = s.Y, s.X0, s.X1 + } + + } else if s.A == 0 { + // The final Span of a rasterization is a zero value. We flush + // our accumulated Span and finish with a zero Span. + ss[j] = Span{m.y, m.x0, m.x1, 1<<32 - 1} + j++ + if j < len(ss) { + ss[j] = Span{} + j++ + m.p.Paint(ss[0:j]) + } else if j == len(ss) { + m.p.Paint(ss) + ss[0] = Span{} + m.p.Paint(ss[0:1]) + } else { + panic("unreachable") + } + // Reset the accumulator, so that this Painter can be re-used. + m.y, m.x0, m.x1 = 0, 0, 0 + return + } + } + m.p.Paint(ss[0:j]) +} + +// A MonochromePainter wraps another Painter, quantizing each Span's alpha to +// be either fully opaque or fully transparent. +func MonochromePainter(p Painter) Painter { + return &monochromePainter{p: p} +} + +// A gammaCorrectionPainter has a wrapped painter and a precomputed linear +// interpolation of the exponential gamma-correction curve. +type gammaCorrectionPainter struct { + p Painter + a [256]uint16 // Alpha values, with fully opaque == 1<<16-1. +} + +// Paint delegates to the wrapped Painter after performing gamma-correction +// on each Span. +func (g *gammaCorrectionPainter) Paint(ss []Span) { + const ( + M = 0x1010101 // 255*M == 1<<32-1 + N = 0x8080 // N = M>>9, and N < 1<<16-1 + ) + for i, _ := range ss { + if ss[i].A == 0 || ss[i].A == 1<<32-1 { + continue + } + p, q := ss[i].A/M, (ss[i].A%M)>>9 + // The resultant alpha is a linear interpolation of g.a[p] and g.a[p+1]. + a := uint32(g.a[p])*(N-q) + uint32(g.a[p+1])*q + a = (a + N/2) / N + // Convert the alpha from 16-bit (which is g.a's range) to 32-bit. + a |= a << 16 + // A non-final Span can't have zero alpha. + if a == 0 { + a = 1 + } + ss[i].A = a + } + g.p.Paint(ss) +} + +// A GammaCorrectionPainter wraps another Painter, performing gamma-correction +// on the alpha values of each Span. +func GammaCorrectionPainter(p Painter, gamma float64) Painter { + g := &gammaCorrectionPainter{p: p} + for i := 0; i < 256; i++ { + a := float64(i) / 0xff + a = math.Pow(a, gamma) + g.a[i] = uint16(0xffff * a) + } + return g +} diff --git a/freetype/raster/raster.go b/freetype/raster/raster.go index 4cd3e8d..f884edc 100644 --- a/freetype/raster/raster.go +++ b/freetype/raster/raster.go @@ -17,28 +17,9 @@ package raster import ( "fmt" - "image" "strconv" ) -// A Painter knows how to paint a span, and a span is a horizontal segment -// of a certain alpha. A fully opaque span has alpha == 1<<32-1. A span's -// alpha is non-zero until the final Paint call of the rasterization, which -// has all arguments zero. -// TODO(nigeltao): Is it worth batching spans, so that Paint takes a []Span -// instead of a single Span? -type Painter interface { - Paint(yi, xi0, xi1 int, alpha uint32) -} - -// The PainterFunc type adapts an ordinary function to the Painter interface. -type PainterFunc func(yi, xi0, xi1 int, alpha uint32) - -// Paint just delegates the call to f. -func (f PainterFunc) Paint(yi, xi0, xi1 int, alpha uint32) { - f(yi, xi0, xi1, alpha) -} - // A 24.8 fixed point number. type Fixed int32 @@ -103,6 +84,7 @@ type Rasterizer struct { // Buffers. cellBuf [256]cell cellIndexBuf [64]int + spanBuf [64]Span } // findCell returns the index in r.cell for the cell corresponding to @@ -461,13 +443,13 @@ func (r *Rasterizer) areaToAlpha(area int) uint32 { return alpha } -// Rasterize converts r's accumulated curves into spans for p. The spans +// 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 <= xi0 < xi1 <= r.width) and non-zero alpha, -// except for the final span, which has yi, xi0, xi1 and alpha all equal -// to zero. +// 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 { @@ -482,7 +464,8 @@ func (r *Rasterizer) Rasterize(p Painter) { xi1 = r.width } if xi0 < xi1 { - p.Paint(yi, xi0, xi1, alpha) + r.spanBuf[s] = Span{yi, xi0, xi1, alpha} + s++ } } } @@ -498,12 +481,19 @@ func (r *Rasterizer) Rasterize(p Painter) { xi1 = r.width } if xi0 < xi1 { - p.Paint(yi, xi0, xi1, alpha) + r.spanBuf[s] = Span{yi, xi0, xi1, alpha} + s++ } } + if s > len(r.spanBuf)-2 { + p.Paint(r.spanBuf[0:s]) + s = 0 + } } } - p.Paint(0, 0, 0, 0) + r.spanBuf[s] = Span{} + s++ + p.Paint(r.spanBuf[0:s]) } // Clear cancels any previous calls to r.Start or r.AddN. @@ -554,29 +544,3 @@ func New(width, height int) *Rasterizer { } return r } - -// AlphaOverPainter returns a Painter that paints onto the given Alpha image -// using the "src over dst" Porter-Duff composition operator. -func AlphaOverPainter(m *image.Alpha) Painter { - return PainterFunc(func(yi, xi0, xi1 int, alpha uint32) { - a := int(alpha >> 24) - p := m.Pixel[yi] - for i := xi0; i < xi1; i++ { - ai := int(p[i].A) - ai = (ai*255 + (255-ai)*a) / 255 - p[i] = image.AlphaColor{uint8(ai)} - } - }) -} - -// AlphaSrcPainter returns a Painter that paints onto the given Alpha image -// using the "src" Porter-Duff composition operator. -func AlphaSrcPainter(m *image.Alpha) Painter { - return PainterFunc(func(yi, xi0, xi1 int, alpha uint32) { - color := image.AlphaColor{uint8(alpha >> 24)} - p := m.Pixel[yi] - for i := xi0; i < xi1; i++ { - p[i] = color - } - }) -}