Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

21 changed files with 44 additions and 923 deletions

View File

@ -6,16 +6,11 @@
## Installation ## Installation
go get -u github.com/fogleman/gg go get github.com/fogleman/gg
Alternatively, you may use gopkg.in to grab a specific major-version: ## GoDoc
go get -u gopkg.in/fogleman/gg.v1 https://godoc.org/github.com/fogleman/gg
## Documentation
- godoc: https://godoc.org/github.com/fogleman/gg
- pkg.go.dev: https://pkg.go.dev/github.com/fogleman/gg?tab=doc
## Hello, Circle! ## Hello, Circle!
@ -96,7 +91,6 @@ DrawString(s string, x, y float64)
DrawStringAnchored(s string, x, y, ax, ay float64) DrawStringAnchored(s string, x, y, ax, ay float64)
DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align)
MeasureString(s string) (w, h float64) MeasureString(s string) (w, h float64)
MeasureMultilineString(s string, lineSpacing float64) (w, h float64)
WordWrap(s string, w float64) []string WordWrap(s string, w float64) []string
SetFontFace(fontFace font.Face) SetFontFace(fontFace font.Face)
LoadFontFace(path string, points float64) error LoadFontFace(path string, points float64) error
@ -122,13 +116,12 @@ SetLineWidth(lineWidth float64)
SetLineCap(lineCap LineCap) SetLineCap(lineCap LineCap)
SetLineJoin(lineJoin LineJoin) SetLineJoin(lineJoin LineJoin)
SetDash(dashes ...float64) SetDash(dashes ...float64)
SetDashOffset(offset float64)
SetFillRule(fillRule FillRule) SetFillRule(fillRule FillRule)
``` ```
## Gradients & Patterns ## Gradients & Patterns
`gg` supports linear, radial and conic gradients and surface patterns. You can also implement your own patterns. `gg` supports linear and radial gradients and surface patterns. You can also implement your own patterns.
```go ```go
SetFillStyle(pattern Pattern) SetFillStyle(pattern Pattern)
@ -136,7 +129,6 @@ SetStrokeStyle(pattern Pattern)
NewSolidPattern(color color.Color) NewSolidPattern(color color.Color)
NewLinearGradient(x0, y0, x1, y1 float64) NewLinearGradient(x0, y0, x1, y1 float64)
NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) NewRadialGradient(x0, y0, r0, x1, y1, r1 float64)
NewConicGradient(cx, cy, deg float64)
NewSurfacePattern(im image.Image, op RepeatOp) NewSurfacePattern(im image.Image, op RepeatOp)
``` ```
@ -159,6 +151,8 @@ It is often desired to rotate or scale about a point that is not the origin. The
`InvertY` is provided in case Y should increase from bottom to top vs. the default top to bottom. `InvertY` is provided in case Y should increase from bottom to top vs. the default top to bottom.
Note: transforms do not currently affect `DrawImage` or `DrawString`.
## Stack Functions ## Stack Functions
Save and restore the state of the context. These can be nested. Save and restore the state of the context. These can be nested.
@ -177,9 +171,6 @@ defined using paths.
Clip() Clip()
ClipPreserve() ClipPreserve()
ResetClip() ResetClip()
AsMask() *image.Alpha
SetMask(mask *image.Alpha)
InvertMask()
``` ```
## Helper Functions ## Helper Functions
@ -196,6 +187,12 @@ SavePNG(path string, im image.Image) error
![Separator](http://i.imgur.com/fsUvnPB.png) ![Separator](http://i.imgur.com/fsUvnPB.png)
## How Do it Do?
`gg` is mostly a wrapper around `github.com/golang/freetype/raster`. The goal
is to provide some more functionality and a nicer API that will suffice for
most use cases.
## Another Example ## Another Example
See the output of this example below. See the output of this example below.

View File

@ -2,20 +2,16 @@
package gg package gg
import ( import (
"errors"
"image" "image"
"image/color" "image/color"
"image/jpeg" "image/draw"
"image/png" "image/png"
"io" "io"
"math" "math"
"strings"
"github.com/golang/freetype/raster" "github.com/golang/freetype/raster"
"golang.org/x/image/draw"
"golang.org/x/image/font" "golang.org/x/image/font"
"golang.org/x/image/font/basicfont" "golang.org/x/image/font/basicfont"
"golang.org/x/image/math/f64"
) )
type LineCap int type LineCap int
@ -56,7 +52,6 @@ var (
type Context struct { type Context struct {
width int width int
height int height int
rasterizer *raster.Rasterizer
im *image.RGBA im *image.RGBA
mask *image.Alpha mask *image.Alpha
color color.Color color color.Color
@ -68,7 +63,6 @@ type Context struct {
current Point current Point
hasCurrent bool hasCurrent bool
dashes []float64 dashes []float64
dashOffset float64
lineWidth float64 lineWidth float64
lineCap LineCap lineCap LineCap
lineJoin LineJoin lineJoin LineJoin
@ -94,12 +88,9 @@ func NewContextForImage(im image.Image) *Context {
// NewContextForRGBA prepares a context for rendering onto the specified image. // NewContextForRGBA prepares a context for rendering onto the specified image.
// No copy is made. // No copy is made.
func NewContextForRGBA(im *image.RGBA) *Context { func NewContextForRGBA(im *image.RGBA) *Context {
w := im.Bounds().Size().X
h := im.Bounds().Size().Y
return &Context{ return &Context{
width: w, width: im.Bounds().Size().X,
height: h, height: im.Bounds().Size().Y,
rasterizer: raster.NewRasterizer(w, h),
im: im, im: im,
color: color.Transparent, color: color.Transparent,
fillPattern: defaultFillStyle, fillPattern: defaultFillStyle,
@ -112,15 +103,6 @@ func NewContextForRGBA(im *image.RGBA) *Context {
} }
} }
// GetCurrentPoint will return the current point and if there is a current point.
// The point will have been transformed by the context's transformation matrix.
func (dc *Context) GetCurrentPoint() (Point, bool) {
if dc.hasCurrent {
return dc.current, true
}
return Point{}, false
}
// Image returns the image that has been drawn by this context. // Image returns the image that has been drawn by this context.
func (dc *Context) Image() image.Image { func (dc *Context) Image() image.Image {
return dc.im return dc.im
@ -141,23 +123,11 @@ func (dc *Context) SavePNG(path string) error {
return SavePNG(path, dc.im) return SavePNG(path, dc.im)
} }
// SaveJPG encodes the image as a JPG and writes it to disk.
func (dc *Context) SaveJPG(path string, quality int) error {
return SaveJPG(path, dc.im, quality)
}
// EncodePNG encodes the image as a PNG and writes it to the provided io.Writer. // EncodePNG encodes the image as a PNG and writes it to the provided io.Writer.
func (dc *Context) EncodePNG(w io.Writer) error { func (dc *Context) EncodePNG(w io.Writer) error {
return png.Encode(w, dc.im) return png.Encode(w, dc.im)
} }
// EncodeJPG encodes the image as a JPG and writes it to the provided io.Writer
// in JPEG 4:2:0 baseline format with the given options.
// Default parameters are used if a nil *jpeg.Options is passed.
func (dc *Context) EncodeJPG(w io.Writer, o *jpeg.Options) error {
return jpeg.Encode(w, dc.im, o)
}
// SetDash sets the current dash pattern to use. Call with zero arguments to // SetDash sets the current dash pattern to use. Call with zero arguments to
// disable dashes. The values specify the lengths of each dash, with // disable dashes. The values specify the lengths of each dash, with
// alternating on and off lengths. // alternating on and off lengths.
@ -165,12 +135,6 @@ func (dc *Context) SetDash(dashes ...float64) {
dc.dashes = dashes dc.dashes = dashes
} }
// SetDashOffset sets the initial offset into the dash pattern to use when
// stroking dashed paths.
func (dc *Context) SetDashOffset(offset float64) {
dc.dashOffset = offset
}
func (dc *Context) SetLineWidth(lineWidth float64) { func (dc *Context) SetLineWidth(lineWidth float64) {
dc.lineWidth = lineWidth dc.lineWidth = lineWidth
} }
@ -409,16 +373,14 @@ func (dc *Context) joiner() raster.Joiner {
func (dc *Context) stroke(painter raster.Painter) { func (dc *Context) stroke(painter raster.Painter) {
path := dc.strokePath path := dc.strokePath
if len(dc.dashes) > 0 { if len(dc.dashes) > 0 {
path = dashed(path, dc.dashes, dc.dashOffset) path = dashed(path, dc.dashes)
} else { } else {
// TODO: this is a temporary workaround to remove tiny segments // TODO: this is a temporary workaround to remove tiny segments
// that result in rendering issues // that result in rendering issues
path = rasterPath(flattenPath(path)) path = rasterPath(flattenPath(path))
} }
r := dc.rasterizer r := raster.NewRasterizer(dc.width, dc.height)
r.UseNonZeroWinding = true r.UseNonZeroWinding = true
r.Clear()
r.Dx, r.Dy = dc.im.Bounds().Min.X, dc.im.Bounds().Min.Y
r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner()) r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner())
r.Rasterize(painter) r.Rasterize(painter)
} }
@ -430,10 +392,8 @@ func (dc *Context) fill(painter raster.Painter) {
copy(path, dc.fillPath) copy(path, dc.fillPath)
path.Add1(dc.start.Fixed()) path.Add1(dc.start.Fixed())
} }
r := dc.rasterizer r := raster.NewRasterizer(dc.width, dc.height)
r.UseNonZeroWinding = dc.fillRule == FillRuleWinding r.UseNonZeroWinding = dc.fillRule == FillRuleWinding
r.Clear()
r.Dx, r.Dy = dc.im.Bounds().Min.X, dc.im.Bounds().Min.Y
r.AddPath(path) r.AddPath(path)
r.Rasterize(painter) r.Rasterize(painter)
} }
@ -442,19 +402,7 @@ func (dc *Context) fill(painter raster.Painter) {
// line cap, line join and dash settings. The path is preserved after this // line cap, line join and dash settings. The path is preserved after this
// operation. // operation.
func (dc *Context) StrokePreserve() { func (dc *Context) StrokePreserve() {
var painter raster.Painter painter := newPatternPainter(dc.im, dc.mask, dc.strokePattern)
if dc.mask == nil {
if pattern, ok := dc.strokePattern.(*solidPattern); ok {
// with a nil mask and a solid color pattern, we can be more efficient
// TODO: refactor so we don't have to do this type assertion stuff?
p := raster.NewRGBAPainter(dc.im)
p.SetColor(pattern.color)
painter = p
}
}
if painter == nil {
painter = newPatternPainter(dc.im, dc.mask, dc.strokePattern)
}
dc.stroke(painter) dc.stroke(painter)
} }
@ -469,19 +417,7 @@ func (dc *Context) Stroke() {
// FillPreserve fills the current path with the current color. Open subpaths // FillPreserve fills the current path with the current color. Open subpaths
// are implicity closed. The path is preserved after this operation. // are implicity closed. The path is preserved after this operation.
func (dc *Context) FillPreserve() { func (dc *Context) FillPreserve() {
var painter raster.Painter painter := newPatternPainter(dc.im, dc.mask, dc.fillPattern)
if dc.mask == nil {
if pattern, ok := dc.fillPattern.(*solidPattern); ok {
// with a nil mask and a solid color pattern, we can be more efficient
// TODO: refactor so we don't have to do this type assertion stuff?
p := raster.NewRGBAPainter(dc.im)
p.SetColor(pattern.color)
painter = p
}
}
if painter == nil {
painter = newPatternPainter(dc.im, dc.mask, dc.fillPattern)
}
dc.fill(painter) dc.fill(painter)
} }
@ -508,38 +444,6 @@ func (dc *Context) ClipPreserve() {
} }
} }
// SetMask allows you to directly set the *image.Alpha to be used as a clipping
// mask. It must be the same size as the context, else an error is returned
// and the mask is unchanged.
func (dc *Context) SetMask(mask *image.Alpha) error {
if mask.Bounds().Size() != dc.im.Bounds().Size() {
return errors.New("mask size must match context size")
}
dc.mask = mask
return nil
}
// AsMask returns an *image.Alpha representing the alpha channel of this
// context. This can be useful for advanced clipping operations where you first
// render the mask geometry and then use it as a mask.
func (dc *Context) AsMask() *image.Alpha {
mask := image.NewAlpha(dc.im.Bounds())
draw.Draw(mask, dc.im.Bounds(), dc.im, image.ZP, draw.Src)
return mask
}
// InvertMask inverts the alpha values in the current clipping mask such that
// a fully transparent region becomes fully opaque and vice versa.
func (dc *Context) InvertMask() {
if dc.mask == nil {
dc.mask = image.NewAlpha(dc.im.Bounds())
} else {
for i, a := range dc.mask.Pix {
dc.mask.Pix[i] = 255 - a
}
}
}
// Clip updates the clipping region by intersecting the current // Clip updates the clipping region by intersecting the current
// clipping region with the current path as it would be filled by dc.Fill(). // clipping region with the current path as it would be filled by dc.Fill().
// The path is cleared after this operation. // The path is cleared after this operation.
@ -616,18 +520,14 @@ func (dc *Context) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) {
a2 := angle1 + (angle2-angle1)*p2 a2 := angle1 + (angle2-angle1)*p2
x0 := x + rx*math.Cos(a1) x0 := x + rx*math.Cos(a1)
y0 := y + ry*math.Sin(a1) y0 := y + ry*math.Sin(a1)
x1 := x + rx*math.Cos((a1+a2)/2) x1 := x + rx*math.Cos(a1+(a2-a1)/2)
y1 := y + ry*math.Sin((a1+a2)/2) y1 := y + ry*math.Sin(a1+(a2-a1)/2)
x2 := x + rx*math.Cos(a2) x2 := x + rx*math.Cos(a2)
y2 := y + ry*math.Sin(a2) y2 := y + ry*math.Sin(a2)
cx := 2*x1 - x0/2 - x2/2 cx := 2*x1 - x0/2 - x2/2
cy := 2*y1 - y0/2 - y2/2 cy := 2*y1 - y0/2 - y2/2
if i == 0 { if i == 0 && !dc.hasCurrent {
if dc.hasCurrent { dc.MoveTo(x0, y0)
dc.LineTo(x0, y0)
} else {
dc.MoveTo(x0, y0)
}
} }
dc.QuadraticTo(cx, cy, x2, y2) dc.QuadraticTo(cx, cy, x2, y2)
} }
@ -664,6 +564,7 @@ func (dc *Context) DrawRegularPolygon(n int, x, y, r, rotation float64) {
} }
// DrawImage draws the specified image at the specified point. // DrawImage draws the specified image at the specified point.
// Currently, rotation and scaling transforms are not supported.
func (dc *Context) DrawImage(im image.Image, x, y int) { func (dc *Context) DrawImage(im image.Image, x, y int) {
dc.DrawImageAnchored(im, x, y, 0, 0) dc.DrawImageAnchored(im, x, y, 0, 0)
} }
@ -675,17 +576,12 @@ func (dc *Context) DrawImageAnchored(im image.Image, x, y int, ax, ay float64) {
s := im.Bounds().Size() s := im.Bounds().Size()
x -= int(ax * float64(s.X)) x -= int(ax * float64(s.X))
y -= int(ay * float64(s.Y)) y -= int(ay * float64(s.Y))
transformer := draw.BiLinear p := image.Pt(x, y)
fx, fy := float64(x), float64(y) r := image.Rectangle{p, p.Add(s)}
m := dc.matrix.Translate(fx, fy)
s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}
if dc.mask == nil { if dc.mask == nil {
transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, nil) draw.Draw(dc.im, r, im, image.ZP, draw.Over)
} else { } else {
transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, &draw.Options{ draw.DrawMask(dc.im, r, im, image.ZP, dc.mask, p, draw.Over)
DstMask: dc.mask,
DstMaskP: image.ZP,
})
} }
} }
@ -705,10 +601,6 @@ func (dc *Context) LoadFontFace(path string, points float64) error {
return err return err
} }
func (dc *Context) FontHeight() float64 {
return dc.fontHeight
}
func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) { func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
d := &font.Drawer{ d := &font.Drawer{
Dst: im, Dst: im,
@ -716,34 +608,11 @@ func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
Face: dc.fontFace, Face: dc.fontFace,
Dot: fixp(x, y), Dot: fixp(x, y),
} }
// based on Drawer.DrawString() in golang.org/x/image/font/font.go d.DrawString(s)
prevC := rune(-1)
for _, c := range s {
if prevC >= 0 {
d.Dot.X += d.Face.Kern(prevC, c)
}
dr, mask, maskp, advance, ok := d.Face.Glyph(d.Dot, c)
if !ok {
// TODO: is falling back on the U+FFFD glyph the responsibility of
// the Drawer or the Face?
// TODO: set prevC = '\ufffd'?
continue
}
sr := dr.Sub(dr.Min)
transformer := draw.BiLinear
fx, fy := float64(dr.Min.X), float64(dr.Min.Y)
m := dc.matrix.Translate(fx, fy)
s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}
transformer.Transform(d.Dst, s2d, d.Src, sr, draw.Over, &draw.Options{
SrcMask: mask,
SrcMaskP: maskp,
})
d.Dot.X += advance
prevC = c
}
} }
// DrawString draws the specified text at the specified point. // DrawString draws the specified text at the specified point.
// Currently, rotation and scaling transforms are not supported.
func (dc *Context) DrawString(s string, x, y float64) { func (dc *Context) DrawString(s string, x, y float64) {
dc.DrawStringAnchored(s, x, y, 0, 0) dc.DrawStringAnchored(s, x, y, 0, 0)
} }
@ -753,10 +622,11 @@ func (dc *Context) DrawString(s string, x, y float64) {
// text. Use ax=0.5, ay=0.5 to center the text at the specified point. // text. Use ax=0.5, ay=0.5 to center the text at the specified point.
func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) { func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
w, h := dc.MeasureString(s) w, h := dc.MeasureString(s)
x, y = dc.TransformPoint(x, y)
x -= ax * w x -= ax * w
y += ay * h y += ay * h
if dc.mask == nil { if dc.mask == nil {
dc.drawString(dc.im, s, x+float64(dc.im.Bounds().Min.X), y+float64(dc.im.Bounds().Min.Y)) dc.drawString(dc.im, s, x, y)
} else { } else {
im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height)) im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height))
dc.drawString(im, s, x, y) dc.drawString(im, s, x, y)
@ -769,11 +639,8 @@ func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
// spacing and text alignment. // spacing and text alignment.
func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) { func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) {
lines := dc.WordWrap(s, width) lines := dc.WordWrap(s, width)
// sync h formula with MeasureMultilineString
h := float64(len(lines)) * dc.fontHeight * lineSpacing h := float64(len(lines)) * dc.fontHeight * lineSpacing
h -= (lineSpacing - 1) * dc.fontHeight h -= (lineSpacing - 1) * dc.fontHeight
x -= ax * width x -= ax * width
y -= ay * h y -= ay * h
switch align { switch align {
@ -793,29 +660,6 @@ func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing
} }
} }
func (dc *Context) MeasureMultilineString(s string, lineSpacing float64) (width, height float64) {
lines := strings.Split(s, "\n")
// sync h formula with DrawStringWrapped
height = float64(len(lines)) * dc.fontHeight * lineSpacing
height -= (lineSpacing - 1) * dc.fontHeight
d := &font.Drawer{
Face: dc.fontFace,
}
// max width from lines
for _, line := range lines {
adv := d.MeasureString(line)
currentWidth := float64(adv >> 6) // from gg.Context.MeasureString
if currentWidth > width {
width = currentWidth
}
}
return width, height
}
// MeasureString returns the rendered width and height of the specified text // MeasureString returns the rendered width and height of the specified text
// given the current font face. // given the current font face.
func (dc *Context) MeasureString(s string) (w, h float64) { func (dc *Context) MeasureString(s string) (w, h float64) {
@ -859,13 +703,13 @@ func (dc *Context) ScaleAbout(sx, sy, x, y float64) {
dc.Translate(-x, -y) dc.Translate(-x, -y)
} }
// Rotate updates the current matrix with a anticlockwise rotation. // Rotate updates the current matrix with a clockwise rotation.
// Rotation occurs about the origin. Angle is specified in radians. // Rotation occurs about the origin. Angle is specified in radians.
func (dc *Context) Rotate(angle float64) { func (dc *Context) Rotate(angle float64) {
dc.matrix = dc.matrix.Rotate(angle) dc.matrix = dc.matrix.Rotate(angle)
} }
// RotateAbout updates the current matrix with a anticlockwise rotation. // RotateAbout updates the current matrix with a clockwise rotation.
// Rotation occurs about the specified point. Angle is specified in radians. // Rotation occurs about the specified point. Angle is specified in radians.
func (dc *Context) RotateAbout(angle, x, y float64) { func (dc *Context) RotateAbout(angle, x, y float64) {
dc.Translate(x, y) dc.Translate(x, y)

View File

@ -1,323 +0,0 @@
package gg
import (
"crypto/md5"
"flag"
"fmt"
"image/color"
"math/rand"
"testing"
)
var save bool
func init() {
flag.BoolVar(&save, "save", false, "save PNG output for each test case")
flag.Parse()
}
func hash(dc *Context) string {
return fmt.Sprintf("%x", md5.Sum(dc.im.Pix))
}
func checkHash(t *testing.T, dc *Context, expected string) {
actual := hash(dc)
if actual != expected {
t.Fatalf("expected hash: %s != actual hash: %s", expected, actual)
}
}
func saveImage(dc *Context, name string) error {
if save {
return SavePNG(name+".png", dc.Image())
}
return nil
}
func TestBlank(t *testing.T) {
dc := NewContext(100, 100)
saveImage(dc, "TestBlank")
checkHash(t, dc, "4e0a293a5b638f0aba2c4fe2c3418d0e")
}
func TestGrid(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
for i := 10; i < 100; i += 10 {
x := float64(i) + 0.5
dc.DrawLine(x, 0, x, 100)
dc.DrawLine(0, x, 100, x)
}
dc.SetRGB(0, 0, 0)
dc.Stroke()
saveImage(dc, "TestGrid")
checkHash(t, dc, "78606adda71d8abfbd8bb271087e4d69")
}
func TestLines(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(0.5, 0.5, 0.5)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 100; i++ {
x1 := rnd.Float64() * 100
y1 := rnd.Float64() * 100
x2 := rnd.Float64() * 100
y2 := rnd.Float64() * 100
dc.DrawLine(x1, y1, x2, y2)
dc.SetLineWidth(rnd.Float64() * 3)
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Stroke()
}
saveImage(dc, "TestLines")
checkHash(t, dc, "036bd220e2529955cc48425dd72bb686")
}
func TestCircles(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 10; i++ {
x := rnd.Float64() * 100
y := rnd.Float64() * 100
r := rnd.Float64()*10 + 5
dc.DrawCircle(x, y, r)
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.FillPreserve()
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.SetLineWidth(rnd.Float64() * 3)
dc.Stroke()
}
saveImage(dc, "TestCircles")
checkHash(t, dc, "c52698000df96fabafe7863701afe922")
}
func TestQuadratic(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(0.25, 0.25, 0.25)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 100; i++ {
x1 := rnd.Float64() * 100
y1 := rnd.Float64() * 100
x2 := rnd.Float64() * 100
y2 := rnd.Float64() * 100
x3 := rnd.Float64() * 100
y3 := rnd.Float64() * 100
dc.MoveTo(x1, y1)
dc.QuadraticTo(x2, y2, x3, y3)
dc.SetLineWidth(rnd.Float64() * 3)
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Stroke()
}
saveImage(dc, "TestQuadratic")
checkHash(t, dc, "56b842d814aee94b52495addae764a77")
}
func TestCubic(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(0.75, 0.75, 0.75)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 100; i++ {
x1 := rnd.Float64() * 100
y1 := rnd.Float64() * 100
x2 := rnd.Float64() * 100
y2 := rnd.Float64() * 100
x3 := rnd.Float64() * 100
y3 := rnd.Float64() * 100
x4 := rnd.Float64() * 100
y4 := rnd.Float64() * 100
dc.MoveTo(x1, y1)
dc.CubicTo(x2, y2, x3, y3, x4, y4)
dc.SetLineWidth(rnd.Float64() * 3)
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Stroke()
}
saveImage(dc, "TestCubic")
checkHash(t, dc, "4a7960fc4eaaa33ce74131c5ce0afca8")
}
func TestFill(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 10; i++ {
dc.NewSubPath()
for j := 0; j < 10; j++ {
x := rnd.Float64() * 100
y := rnd.Float64() * 100
dc.LineTo(x, y)
}
dc.ClosePath()
dc.SetRGBA(rnd.Float64(), rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Fill()
}
saveImage(dc, "TestFill")
checkHash(t, dc, "7ccb3a2443906a825e57ab94db785467")
}
func TestClip(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.DrawCircle(50, 50, 40)
dc.Clip()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 1000; i++ {
x := rnd.Float64() * 100
y := rnd.Float64() * 100
r := rnd.Float64()*10 + 5
dc.DrawCircle(x, y, r)
dc.SetRGBA(rnd.Float64(), rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Fill()
}
saveImage(dc, "TestClip")
checkHash(t, dc, "762c32374d529fd45ffa038b05be7865")
}
func TestPushPop(t *testing.T) {
const S = 100
dc := NewContext(S, S)
dc.SetRGBA(0, 0, 0, 0.1)
for i := 0; i < 360; i += 15 {
dc.Push()
dc.RotateAbout(Radians(float64(i)), S/2, S/2)
dc.DrawEllipse(S/2, S/2, S*7/16, S/8)
dc.Fill()
dc.Pop()
}
saveImage(dc, "TestPushPop")
checkHash(t, dc, "31e908ee1c2ea180da98fd5681a89d05")
}
func TestDrawStringWrapped(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetRGB(0, 0, 0)
dc.DrawStringWrapped("Hello, world! How are you?", 50, 50, 0.5, 0.5, 90, 1.5, AlignCenter)
saveImage(dc, "TestDrawStringWrapped")
checkHash(t, dc, "8d92f6aae9e8b38563f171abd00893f8")
}
func TestDrawImage(t *testing.T) {
src := NewContext(100, 100)
src.SetRGB(1, 1, 1)
src.Clear()
for i := 10; i < 100; i += 10 {
x := float64(i) + 0.5
src.DrawLine(x, 0, x, 100)
src.DrawLine(0, x, 100, x)
}
src.SetRGB(0, 0, 0)
src.Stroke()
dc := NewContext(200, 200)
dc.SetRGB(0, 0, 0)
dc.Clear()
dc.DrawImage(src.Image(), 50, 50)
saveImage(dc, "TestDrawImage")
checkHash(t, dc, "282afbc134676722960b6bec21305b15")
}
func TestSetPixel(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(0, 0, 0)
dc.Clear()
dc.SetRGB(0, 1, 0)
i := 0
for y := 0; y < 100; y++ {
for x := 0; x < 100; x++ {
if i%31 == 0 {
dc.SetPixel(x, y)
}
i++
}
}
saveImage(dc, "TestSetPixel")
checkHash(t, dc, "27dda6b4b1d94f061018825b11982793")
}
func TestDrawPoint(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(0, 0, 0)
dc.Clear()
dc.SetRGB(0, 1, 0)
dc.Scale(10, 10)
for y := 0; y <= 10; y++ {
for x := 0; x <= 10; x++ {
dc.DrawPoint(float64(x), float64(y), 3)
dc.Fill()
}
}
saveImage(dc, "TestDrawPoint")
checkHash(t, dc, "55af8874531947ea6eeb62222fb33e0e")
}
func TestLinearGradient(t *testing.T) {
dc := NewContext(100, 100)
g := NewLinearGradient(0, 0, 100, 100)
g.AddColorStop(0, color.RGBA{0, 255, 0, 255})
g.AddColorStop(1, color.RGBA{0, 0, 255, 255})
g.AddColorStop(0.5, color.RGBA{255, 0, 0, 255})
dc.SetFillStyle(g)
dc.DrawRectangle(0, 0, 100, 100)
dc.Fill()
saveImage(dc, "TestLinearGradient")
checkHash(t, dc, "75eb9385c1219b1d5bb6f4c961802c7a")
}
func TestRadialGradient(t *testing.T) {
dc := NewContext(100, 100)
g := NewRadialGradient(30, 50, 0, 70, 50, 50)
g.AddColorStop(0, color.RGBA{0, 255, 0, 255})
g.AddColorStop(1, color.RGBA{0, 0, 255, 255})
g.AddColorStop(0.5, color.RGBA{255, 0, 0, 255})
dc.SetFillStyle(g)
dc.DrawRectangle(0, 0, 100, 100)
dc.Fill()
saveImage(dc, "TestRadialGradient")
checkHash(t, dc, "f170f39c3f35c29de11e00428532489d")
}
func TestDashes(t *testing.T) {
dc := NewContext(100, 100)
dc.SetRGB(1, 1, 1)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < 100; i++ {
x1 := rnd.Float64() * 100
y1 := rnd.Float64() * 100
x2 := rnd.Float64() * 100
y2 := rnd.Float64() * 100
dc.SetDash(rnd.Float64()*3+1, rnd.Float64()*3+3)
dc.DrawLine(x1, y1, x2, y2)
dc.SetLineWidth(rnd.Float64() * 3)
dc.SetRGB(rnd.Float64(), rnd.Float64(), rnd.Float64())
dc.Stroke()
}
saveImage(dc, "TestDashes")
checkHash(t, dc, "d188069c69dcc3970edfac80f552b53c")
}
func BenchmarkCircles(b *testing.B) {
dc := NewContext(1000, 1000)
dc.SetRGB(1, 1, 1)
dc.Clear()
rnd := rand.New(rand.NewSource(99))
for i := 0; i < b.N; i++ {
x := rnd.Float64() * 1000
y := rnd.Float64() * 1000
dc.DrawCircle(x, y, 10)
if i%2 == 0 {
dc.SetRGB(0, 0, 0)
} else {
dc.SetRGB(1, 1, 1)
}
dc.Fill()
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 622 KiB

View File

@ -1,30 +0,0 @@
package main
import (
"math"
"github.com/fogleman/gg"
)
func main() {
im1, err := gg.LoadPNG("examples/baboon.png")
if err != nil {
panic(err)
}
im2, err := gg.LoadPNG("examples/gopher.png")
if err != nil {
panic(err)
}
s1 := im1.Bounds().Size()
s2 := im2.Bounds().Size()
width := int(math.Max(float64(s1.X), float64(s2.X)))
height := s1.Y + s2.Y
dc := gg.NewContext(width, height)
dc.DrawImage(im1, 0, 0)
dc.DrawImage(im2, 0, s1.Y)
dc.SavePNG("out.png")
}

View File

@ -1,44 +0,0 @@
package main
import (
"github.com/fogleman/gg"
)
func main() {
const W = 1000
const H = 1000
const Minor = 10
const Major = 100
dc := gg.NewContext(W, H)
dc.SetRGB(1, 1, 1)
dc.Clear()
// minor grid
for x := Minor; x < W; x += Minor {
fx := float64(x) + 0.5
dc.DrawLine(fx, 0, fx, H)
}
for y := Minor; y < H; y += Minor {
fy := float64(y) + 0.5
dc.DrawLine(0, fy, W, fy)
}
dc.SetLineWidth(1)
dc.SetRGBA(0, 0, 0, 0.25)
dc.Stroke()
// major grid
for x := Major; x < W; x += Major {
fx := float64(x) + 0.5
dc.DrawLine(fx, 0, fx, H)
}
for y := Major; y < H; y += Major {
fy := float64(y) + 0.5
dc.DrawLine(0, fy, W, fy)
}
dc.SetLineWidth(1)
dc.SetRGBA(0, 0, 0, 0.5)
dc.Stroke()
dc.SavePNG("out.png")
}

View File

@ -1,26 +0,0 @@
package main
import (
"log"
"github.com/fogleman/gg"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font/gofont/goregular"
)
func main() {
font, err := truetype.Parse(goregular.TTF)
if err != nil {
log.Fatal(err)
}
face := truetype.NewFace(font, &truetype.Options{Size: 48})
dc := gg.NewContext(1024, 1024)
dc.SetFontFace(face)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetRGB(0, 0, 0)
dc.DrawStringAnchored("Hello, world!", 512, 512, 0.5, 0.5)
dc.SavePNG("out.png")
}

View File

@ -1,38 +0,0 @@
// +build ignore
package main
import (
"image/color"
"github.com/fogleman/gg"
)
func main() {
dc := gg.NewContext(400, 400)
grad1 := gg.NewConicGradient(200, 200, 0)
grad1.AddColorStop(0.0, color.Black)
grad1.AddColorStop(0.5, color.RGBA{255, 215, 0, 255})
grad1.AddColorStop(1.0, color.RGBA{255, 0, 0, 255})
grad2 := gg.NewConicGradient(200, 200, 90)
grad2.AddColorStop(0.00, color.RGBA{255, 0, 0, 255})
grad2.AddColorStop(0.16, color.RGBA{255, 255, 0, 255})
grad2.AddColorStop(0.33, color.RGBA{0, 255, 0, 255})
grad2.AddColorStop(0.50, color.RGBA{0, 255, 255, 255})
grad2.AddColorStop(0.66, color.RGBA{0, 0, 255, 255})
grad2.AddColorStop(0.83, color.RGBA{255, 0, 255, 255})
grad2.AddColorStop(1.00, color.RGBA{255, 0, 0, 255})
dc.SetStrokeStyle(grad1)
dc.SetLineWidth(20)
dc.DrawCircle(200, 200, 180)
dc.Stroke()
dc.SetFillStyle(grad2)
dc.DrawCircle(200, 200, 150)
dc.Fill()
dc.SavePNG("gradient-conic.png")
}

View File

@ -1,41 +0,0 @@
package main
import (
"image/color"
"github.com/fogleman/gg"
)
const (
W = 1024
H = 512
)
func main() {
dc := gg.NewContext(W, H)
// draw text
dc.SetRGB(0, 0, 0)
dc.LoadFontFace("/Library/Fonts/Impact.ttf", 128)
dc.DrawStringAnchored("Gradient Text", W/2, H/2, 0.5, 0.5)
// get the context as an alpha mask
mask := dc.AsMask()
// clear the context
dc.SetRGB(1, 1, 1)
dc.Clear()
// set a gradient
g := gg.NewLinearGradient(0, 0, W, H)
g.AddColorStop(0, color.RGBA{255, 0, 0, 255})
g.AddColorStop(1, color.RGBA{0, 0, 255, 255})
dc.SetFillStyle(g)
// using the mask, fill the context with the gradient
dc.SetMask(mask)
dc.DrawRectangle(0, 0, W, H)
dc.Fill()
dc.SavePNG("out.png")
}

View File

@ -1,14 +0,0 @@
package main
import "github.com/fogleman/gg"
func main() {
dc := gg.NewContext(1024, 1024)
dc.DrawCircle(512, 512, 384)
dc.Clip()
dc.InvertMask()
dc.DrawRectangle(0, 0, 1024, 1024)
dc.SetRGB(0, 0, 0)
dc.Fill()
dc.SavePNG("out.png")
}

BIN
examples/lenna.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

View File

@ -7,7 +7,7 @@ import (
) )
func main() { func main() {
im, err := gg.LoadImage("examples/baboon.png") im, err := gg.LoadImage("examples/lenna.png")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -3,7 +3,7 @@ package main
import "github.com/fogleman/gg" import "github.com/fogleman/gg"
func main() { func main() {
im, err := gg.LoadPNG("examples/baboon.png") im, err := gg.LoadPNG("examples/lenna.png")
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -1,34 +0,0 @@
package main
import "github.com/fogleman/gg"
func main() {
const W = 400
const H = 500
im, err := gg.LoadPNG("examples/gopher.png")
if err != nil {
panic(err)
}
iw, ih := im.Bounds().Dx(), im.Bounds().Dy()
dc := gg.NewContext(W, H)
// draw outline
dc.SetHexColor("#ff0000")
dc.SetLineWidth(1)
dc.DrawRectangle(0, 0, float64(W), float64(H))
dc.Stroke()
// draw full image
dc.SetHexColor("#0000ff")
dc.SetLineWidth(2)
dc.DrawRectangle(100, 210, float64(iw), float64(ih))
dc.Stroke()
dc.DrawImage(im, 100, 210)
// draw image with current matrix applied
dc.SetHexColor("#0000ff")
dc.SetLineWidth(2)
dc.Rotate(gg.Radians(10))
dc.DrawRectangle(100, 0, float64(iw), float64(ih)/2+20.0)
dc.StrokePreserve()
dc.Clip()
dc.DrawImageAnchored(im, 100, 0, 0.0, 0.0)
dc.SavePNG("out.png")
}

View File

@ -1,30 +0,0 @@
package main
import (
"github.com/fogleman/gg"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font/gofont/goregular"
)
func main() {
const S = 400
dc := gg.NewContext(S, S)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetRGB(0, 0, 0)
font, err := truetype.Parse(goregular.TTF)
if err != nil {
panic("")
}
face := truetype.NewFace(font, &truetype.Options{
Size: 40,
})
dc.SetFontFace(face)
text := "Hello, world!"
w, h := dc.MeasureString(text)
dc.Rotate(gg.Radians(10))
dc.DrawRectangle(100, 180, w, h)
dc.Stroke()
dc.DrawStringAnchored(text, 100, 180, 0.0, 0.0)
dc.SavePNG("out.png")
}

View File

@ -1,25 +0,0 @@
package main
import "github.com/fogleman/gg"
func main() {
const S = 4096 * 2
const T = 16 * 2
const F = 28
dc := gg.NewContext(S, S)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetRGB(0, 0, 0)
if err := dc.LoadFontFace("Xolonium-Regular.ttf", F); err != nil {
panic(err)
}
for r := 0; r < 256; r++ {
for c := 0; c < 256; c++ {
i := r*256 + c
x := float64(c*T) + T/2
y := float64(r*T) + T/2
dc.DrawStringAnchored(string(rune(i)), x, y, 0.5, 0.5)
}
}
dc.SavePNG("out.png")
}

9
go.mod
View File

@ -1,9 +0,0 @@
module git.milar.in/milarin/gg
go 1.18
require (
github.com/fogleman/gg v1.3.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9
)

8
go.sum
View File

@ -1,8 +0,0 @@
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -164,52 +164,6 @@ func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) Gradient {
return g return g
} }
// Conic Gradient
type conicGradient struct {
cx, cy float64
rotation float64
stops stops
}
func (g *conicGradient) ColorAt(x, y int) color.Color {
if len(g.stops) == 0 {
return color.Transparent
}
a := math.Atan2(float64(y)-g.cy, float64(x)-g.cx)
t := norm(a, -math.Pi, math.Pi) - g.rotation
if t < 0 {
t += 1
}
return getColor(t, g.stops)
}
func (g *conicGradient) AddColorStop(offset float64, color color.Color) {
g.stops = append(g.stops, stop{pos: offset, color: color})
sort.Sort(g.stops)
}
func NewConicGradient(cx, cy, deg float64) Gradient {
g := &conicGradient{
cx: cx,
cy: cy,
rotation: normalizeAngle(deg) / 360,
}
return g
}
func normalizeAngle(t float64) float64 {
t = math.Mod(t, 360)
if t < 0 {
t += 360
}
return t
}
// Map value which is in range [a..b] to range [0..1]
func norm(value, a, b float64) float64 {
return (value - a) * (1.0 / (b - a))
}
func getColor(pos float64, stops stops) color.Color { func getColor(pos float64, stops stops) color.Color {
if pos <= 0.0 || len(stops) == 1 { if pos <= 0.0 || len(stops) == 1 {
return stops[0].color return stops[0].color
@ -235,7 +189,7 @@ func colorLerp(c0, c1 color.Color, t float64) color.Color {
r0, g0, b0, a0 := c0.RGBA() r0, g0, b0, a0 := c0.RGBA()
r1, g1, b1, a1 := c1.RGBA() r1, g1, b1, a1 := c1.RGBA()
return color.RGBA{ return color.NRGBA{
lerp(r0, r1, t), lerp(r0, r1, t),
lerp(g0, g1, t), lerp(g0, g1, t),
lerp(b0, b1, t), lerp(b0, b1, t),

29
path.go
View File

@ -1,8 +1,6 @@
package gg package gg
import ( import (
"math"
"github.com/golang/freetype/raster" "github.com/golang/freetype/raster"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
) )
@ -59,7 +57,7 @@ func flattenPath(p raster.Path) [][]Point {
return result return result
} }
func dashPath(paths [][]Point, dashes []float64, offset float64) [][]Point { func dashPath(paths [][]Point, dashes []float64) [][]Point {
var result [][]Point var result [][]Point
if len(dashes) == 0 { if len(dashes) == 0 {
return paths return paths
@ -75,27 +73,6 @@ func dashPath(paths [][]Point, dashes []float64, offset float64) [][]Point {
pathIndex := 1 pathIndex := 1
dashIndex := 0 dashIndex := 0
segmentLength := 0.0 segmentLength := 0.0
// offset
if offset != 0 {
var totalLength float64
for _, dashLength := range dashes {
totalLength += dashLength
}
offset = math.Mod(offset, totalLength)
if offset < 0 {
offset += totalLength
}
for i, dashLength := range dashes {
offset -= dashLength
if offset < 0 {
dashIndex = i
segmentLength = dashLength + offset
break
}
}
}
var segment []Point var segment []Point
segment = append(segment, previous) segment = append(segment, previous)
for pathIndex < len(path) { for pathIndex < len(path) {
@ -158,6 +135,6 @@ func rasterPath(paths [][]Point) raster.Path {
return result return result
} }
func dashed(path raster.Path, dashes []float64, offset float64) raster.Path { func dashed(path raster.Path, dashes []float64) raster.Path {
return rasterPath(dashPath(flattenPath(path), dashes, offset)) return rasterPath(dashPath(flattenPath(path), dashes))
} }

35
util.go
View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"image" "image"
"image/draw" "image/draw"
"image/jpeg"
_ "image/jpeg" _ "image/jpeg"
"image/png" "image/png"
"io/ioutil" "io/ioutil"
@ -54,32 +53,9 @@ func SavePNG(path string, im image.Image) error {
return png.Encode(file, im) return png.Encode(file, im)
} }
func LoadJPG(path string) (image.Image, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return jpeg.Decode(file)
}
func SaveJPG(path string, im image.Image, quality int) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
var opt jpeg.Options
opt.Quality = quality
return jpeg.Encode(file, im, &opt)
}
func imageToRGBA(src image.Image) *image.RGBA { func imageToRGBA(src image.Image) *image.RGBA {
bounds := src.Bounds() dst := image.NewRGBA(src.Bounds())
dst := image.NewRGBA(bounds) draw.Draw(dst, dst.Rect, src, image.ZP, draw.Src)
draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
return dst return dst
} }
@ -109,7 +85,7 @@ func fixp(x, y float64) fixed.Point26_6 {
} }
func fix(x float64) fixed.Int26_6 { func fix(x float64) fixed.Int26_6 {
return fixed.Int26_6(math.Round(x * 64)) return fixed.Int26_6(x * 64)
} }
func unfix(x fixed.Int26_6) float64 { func unfix(x fixed.Int26_6) float64 {
@ -124,11 +100,6 @@ func unfix(x fixed.Int26_6) float64 {
return 0 return 0
} }
// LoadFontFace is a helper function to load the specified font file with
// the specified point size. Note that the returned `font.Face` objects
// are not thread safe and cannot be used in parallel across goroutines.
// You can usually just use the Context.LoadFontFace function instead of
// this package-level function.
func LoadFontFace(path string, points float64) (font.Face, error) { func LoadFontFace(path string, points float64) (font.Face, error) {
fontBytes, err := ioutil.ReadFile(path) fontBytes, err := ioutil.ReadFile(path)
if err != nil { if err != nil {