diff --git a/example/drawer/main.go b/example/drawer/main.go new file mode 100644 index 0000000..402c277 --- /dev/null +++ b/example/drawer/main.go @@ -0,0 +1,147 @@ +// Copyright 2015 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. + +// +build ignore +// +// This build tag means that "go install github.com/golang/freetype/..." +// doesn't install this example program. Use "go run main.go" to run it. + +package main + +import ( + "bufio" + "flag" + "fmt" + "image" + "image/color" + "image/draw" + "image/png" + "io/ioutil" + "log" + "math" + "os" + + "github.com/golang/freetype/truetype" + "golang.org/x/exp/shiny/font" + "golang.org/x/image/math/fixed" +) + +var ( + dpi = flag.Float64("dpi", 72, "screen resolution in Dots Per Inch") + fontfile = flag.String("fontfile", "../../testdata/luxisr.ttf", "filename of the ttf font") + hinting = flag.String("hinting", "none", "none | full") + size = flag.Float64("size", 12, "font size in points") + spacing = flag.Float64("spacing", 1.5, "line spacing (e.g. 2 means double spaced)") + wonb = flag.Bool("whiteonblack", false, "white text on a black background") +) + +var text = []string{ + "’Twas brillig, and the slithy toves", + "Did gyre and gimble in the wabe;", + "All mimsy were the borogoves,", + "And the mome raths outgrabe.", + "", + "“Beware the Jabberwock, my son!", + "The jaws that bite, the claws that catch!", + "Beware the Jubjub bird, and shun", + "The frumious Bandersnatch!”", + "", + "He took his vorpal sword in hand:", + "Long time the manxome foe he sought—", + "So rested he by the Tumtum tree,", + "And stood awhile in thought.", + "", + "And as in uffish thought he stood,", + "The Jabberwock, with eyes of flame,", + "Came whiffling through the tulgey wood,", + "And burbled as it came!", + "", + "One, two! One, two! and through and through", + "The vorpal blade went snicker-snack!", + "He left it dead, and with its head", + "He went galumphing back.", + "", + "“And hast thou slain the Jabberwock?", + "Come to my arms, my beamish boy!", + "O frabjous day! Callooh! Callay!”", + "He chortled in his joy.", + "", + "’Twas brillig, and the slithy toves", + "Did gyre and gimble in the wabe;", + "All mimsy were the borogoves,", + "And the mome raths outgrabe.", +} + +func main() { + flag.Parse() + + // Read the font data. + fontBytes, err := ioutil.ReadFile(*fontfile) + if err != nil { + log.Println(err) + return + } + f, err := truetype.Parse(fontBytes) + if err != nil { + log.Println(err) + return + } + + // Draw the background and the guidelines. + fg, bg := image.Black, image.White + ruler := color.RGBA{0xdd, 0xdd, 0xdd, 0xff} + if *wonb { + fg, bg = image.White, image.Black + ruler = color.RGBA{0x22, 0x22, 0x22, 0xff} + } + rgba := image.NewRGBA(image.Rect(0, 0, 640, 480)) + draw.Draw(rgba, rgba.Bounds(), bg, image.ZP, draw.Src) + for i := 0; i < 200; i++ { + rgba.Set(10, 10+i, ruler) + rgba.Set(10+i, 10, ruler) + } + + // Draw the text. + h := font.HintingNone + switch *hinting { + case "full": + h = font.HintingFull + } + d := &font.Drawer{ + Dst: rgba, + Src: fg, + Face: truetype.NewFace(f, truetype.Options{ + Size: *size, + DPI: *dpi, + Hinting: h, + }), + } + dy0 := int(math.Ceil(*size * *dpi / 72)) + dy := int(math.Ceil(*size * *spacing * *dpi / 72)) + for i, s := range text { + d.Dot = fixed.P(10, 10+dy0+i*dy) + d.DrawString(s) + } + + // Save that RGBA image to disk. + outFile, err := os.Create("out.png") + if err != nil { + log.Println(err) + os.Exit(1) + } + defer outFile.Close() + b := bufio.NewWriter(outFile) + err = png.Encode(b, rgba) + if err != nil { + log.Println(err) + os.Exit(1) + } + err = b.Flush() + if err != nil { + log.Println(err) + os.Exit(1) + } + fmt.Println("Wrote out.png OK.") +} diff --git a/truetype/face.go b/truetype/face.go new file mode 100644 index 0000000..a677c81 --- /dev/null +++ b/truetype/face.go @@ -0,0 +1,237 @@ +// Copyright 2015 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 truetype + +import ( + "image" + + "github.com/golang/freetype/raster" + "golang.org/x/exp/shiny/font" + "golang.org/x/image/math/fixed" +) + +// Options are optional arguments to NewFace. +type Options struct { + // Size is the font size in points, as in "a 10 point font size". + // + // A zero value means to use a 12 point font size. + Size float64 + + // DPI is the dots-per-inch resolution. + // + // A zero value means to use 72 DPI. + DPI float64 + + // Hinting is how to quantize the glyph nodes. + // + // A zero value means to use no hinting. + Hinting font.Hinting +} + +func (o *Options) size() float64 { + if o.Size > 0 { + return o.Size + } + return 12 +} + +func (o *Options) dpi() float64 { + if o.DPI > 0 { + return o.DPI + } + return 72 +} + +func (o *Options) hinting() font.Hinting { + switch o.Hinting { + case font.HintingVertical, font.HintingFull: + // TODO: support vertical hinting. + return font.HintingFull + } + return font.HintingNone +} + +// NewFace returns a new font.Face for the given Font. +func NewFace(f *Font, opts Options) font.Face { + a := &face{ + f: f, + hinting: opts.hinting(), + scale: fixed.Int26_6(0.5 + (opts.size() * opts.dpi() * 64 / 72)), + } + + // Set the rasterizer's bounds to be big enough to handle the largest glyph. + b := f.Bounds(a.scale) + xmin := +int(b.XMin) >> 6 + ymin := -int(b.YMax) >> 6 + xmax := +int(b.XMax+63) >> 6 + ymax := -int(b.YMin-63) >> 6 + a.r.SetBounds(xmax-xmin, ymax-ymin) + + return a +} + +type face struct { + f *Font + hinting font.Hinting + scale fixed.Int26_6 + r raster.Rasterizer + glyphBuf GlyphBuf + + // TODO: clip rectangle? +} + +// Close satisfies the font.Face interface. +func (a *face) Close() error { return nil } + +// Kern satisfies the font.Face interface. +func (a *face) Kern(r0, r1 rune) fixed.Int26_6 { + i0 := a.f.Index(r0) + i1 := a.f.Index(r1) + kern := fixed.Int26_6(a.f.Kerning(a.scale, i0, i1)) + if a.hinting != font.HintingNone { + kern = (kern + 32) &^ 63 + } + return kern +} + +// Glyph satisfies the font.Face interface. +func (a *face) Glyph(dot fixed.Point26_6, r rune) ( + newDot fixed.Point26_6, dr image.Rectangle, mask image.Image, maskp image.Point, ok bool) { + + // Split p.X and p.Y into their integer and fractional parts. + ix, fx := int(dot.X>>6), dot.X&0x3f + iy, fy := int(dot.Y>>6), dot.Y&0x3f + + advanceWidth, mask, offset, ok := a.rasterize(a.f.Index(r), fx, fy) + if !ok { + return fixed.Point26_6{}, image.Rectangle{}, nil, image.Point{}, false + } + newDot = fixed.Point26_6{ + X: dot.X + advanceWidth, + Y: dot.Y, + } + mb := mask.Bounds() + dr.Min = image.Point{ + X: ix + offset.X, + Y: iy + offset.Y, + } + dr.Max = image.Point{ + X: dr.Min.X + mb.Dx(), + Y: dr.Min.Y + mb.Dy(), + } + return newDot, dr, mask, image.Point{}, true +} + +// rasterize returns the advance width, glyph mask and integer-pixel offset +// to render the given glyph at the given sub-pixel offsets. +// The 26.6 fixed point arguments fx and fy must be in the range [0, 1). +func (a *face) rasterize(index Index, fx, fy fixed.Int26_6) ( + fixed.Int26_6, *image.Alpha, image.Point, bool) { + + if err := a.glyphBuf.Load(a.f, a.scale, index, a.hinting); err != nil { + return 0, nil, image.Point{}, false + } + // Calculate the integer-pixel bounds for the glyph. + xmin := int(fx+fixed.Int26_6(a.glyphBuf.B.XMin)) >> 6 + ymin := int(fy-fixed.Int26_6(a.glyphBuf.B.YMax)) >> 6 + xmax := int(fx+fixed.Int26_6(a.glyphBuf.B.XMax)+0x3f) >> 6 + ymax := int(fy-fixed.Int26_6(a.glyphBuf.B.YMin)+0x3f) >> 6 + if xmin > xmax || ymin > ymax { + return 0, nil, image.Point{}, false + } + // A TrueType's glyph's nodes can have negative co-ordinates, but the + // rasterizer clips anything left of x=0 or above y=0. xmin and ymin are + // the pixel offsets, based on the font's FUnit metrics, that let a + // negative co-ordinate in TrueType space be non-negative in rasterizer + // space. xmin and ymin are typically <= 0. + fx += fixed.Int26_6(-xmin << 6) + fy += fixed.Int26_6(-ymin << 6) + // Rasterize the glyph's vectors. + a.r.Clear() + e0 := 0 + for _, e1 := range a.glyphBuf.End { + a.drawContour(a.glyphBuf.Point[e0:e1], fx, fy) + e0 = e1 + } + // TODO: don't allocate a new mask each time. + mask := image.NewAlpha(image.Rect(0, 0, xmax-xmin, ymax-ymin)) + a.r.Rasterize(raster.NewAlphaSrcPainter(mask)) + return fixed.Int26_6(a.glyphBuf.AdvanceWidth), mask, image.Point{xmin, ymin}, true +} + +// drawContour draws the given closed contour with the given offset. +func (a *face) drawContour(ps []Point, dx, dy fixed.Int26_6) { + if len(ps) == 0 { + return + } + + // The low bit of each point's Flags value is whether the point is on the + // curve. Truetype fonts only have quadratic Bézier curves, not cubics. + // Thus, two consecutive off-curve points imply an on-curve point in the + // middle of those two. + // + // See http://chanae.walon.org/pub/ttf/ttf_glyphs.htm for more details. + + // ps[0] is a truetype.Point measured in FUnits and positive Y going + // upwards. start is the same thing measured in fixed point units and + // positive Y going downwards, and offset by (dx, dy). + start := fixed.Point26_6{ + X: dx + fixed.Int26_6(ps[0].X), + Y: dy - fixed.Int26_6(ps[0].Y), + } + var others []Point + if ps[0].Flags&0x01 != 0 { + others = ps[1:] + } else { + last := fixed.Point26_6{ + X: dx + fixed.Int26_6(ps[len(ps)-1].X), + Y: dy - fixed.Int26_6(ps[len(ps)-1].Y), + } + if ps[len(ps)-1].Flags&0x01 != 0 { + start = last + others = ps[:len(ps)-1] + } else { + start = fixed.Point26_6{ + X: (start.X + last.X) / 2, + Y: (start.Y + last.Y) / 2, + } + others = ps + } + } + a.r.Start(start) + q0, on0 := start, true + for _, p := range others { + q := fixed.Point26_6{ + X: dx + fixed.Int26_6(p.X), + Y: dy - fixed.Int26_6(p.Y), + } + on := p.Flags&0x01 != 0 + if on { + if on0 { + a.r.Add1(q) + } else { + a.r.Add2(q0, q) + } + } else { + if on0 { + // No-op. + } else { + mid := fixed.Point26_6{ + X: (q0.X + q.X) / 2, + Y: (q0.Y + q.Y) / 2, + } + a.r.Add2(q0, mid) + } + } + q0, on0 = q, on + } + // Close the curve. + if on0 { + a.r.Add1(start) + } else { + a.r.Add2(q0, start) + } +}