Compare commits

..

57 Commits
v1.0.0 ... main

Author SHA1 Message Date
d9a531518d migrated module to git.milar.in 2023-01-22 12:06:29 +01:00
Timon Ringwald
cc30f39e11 fixed drawclipping when drawing text on subimages 2022-04-22 11:57:30 +02:00
Timon Ringwald
69923d9438 fixed draw clipping on subimages 2022-04-22 11:49:17 +02:00
Timon Ringwald
2755d666d6 moduling 2022-04-22 11:48:47 +02:00
Michael Fogleman
8febc0f526 Fix rounding when converting to fixed precision 2021-09-28 10:35:35 -04:00
Michael Fogleman
af4cd58078
Merge pull request #124 from mazznoer/conic-gradient
Add conic gradient
2021-01-31 12:28:31 -05:00
Nor Khasyatillah
c26555283c Add conic gradient 2021-01-29 19:00:22 +07:00
Michael Fogleman
ad4d1eafac
Update README.md 2020-05-14 21:10:29 -04:00
Michael Fogleman
8d127f461d
Merge pull request #100 from ajnirp/patch-1
Update documentation link
2020-05-14 21:09:58 -04:00
Rohan Prinja
b89ba07b94
Update documentation link
When visiting the godoc link, a banner at the top informs me that "pkg.go.dev is a new destination for Go discovery and docs". While it doesn't look like godoc is going away, I figured it would be nice to link to both sites.
2020-05-14 17:15:05 -07:00
Michael Fogleman
4dc34561c6
Merge pull request #83 from Xeoncross/master
Add support for JPEG encoding
2019-08-26 15:13:58 -04:00
xeoncross
068da56c91 Added support for JPEG encoding 2019-08-26 14:04:06 -05:00
Michael Fogleman
f194ddec6f
Merge pull request #77 from leexingliang/master
modify comment
2019-06-12 10:10:55 -04:00
lixingliang
6897f9a1a0 modify comment 2019-06-12 11:43:36 +08:00
Michael Fogleman
72436a171b fix test for 1.12+ 2019-04-16 21:30:52 -04:00
Michael Fogleman
5899172fb0
Merge pull request #70 from bevand10/master
Add SaveJPG method
2019-04-16 21:28:22 -04:00
Dave Bevan
1236b6346f Add SaveJPG method 2019-04-12 18:25:28 +01:00
Michael Fogleman
0403632d5b New test image. 2019-02-20 17:12:49 -05:00
Michael Fogleman
64338842c5 New test image. 2019-02-20 17:12:06 -05:00
Michael Fogleman
3bcf9e0320 Add SetDashOffset. Close #64. 2019-02-20 16:47:39 -05:00
Michael Fogleman
fa28a6e1e3
Merge pull request #65 from kortschak/average
Use fewer ops to calculate mid-point
2019-02-20 10:22:23 -05:00
Dan Kortschak
f23d82b106 Use fewer ops to calculate mid-point 2019-02-20 17:08:29 +10:30
Michael Fogleman
0e8122236d
Merge pull request #61 from JamieCrisman/get-current
Allows the ability to get the current point in the context
2019-02-07 15:47:22 -05:00
Jamie Crisman
ccde3a8923 Allows the ability to get the current point in the context 2019-02-07 14:39:14 -06:00
Michael Fogleman
3795562800
Merge pull request #60 from emaele/jpeg-open&save
Load and save jpeg images
2019-01-19 16:22:23 -05:00
Emanuele Rocco Petrone
74a8429cb8 load and save jpeg images 2019-01-19 18:47:38 +01:00
Michael Fogleman
9db508d34a
Update README.md 2019-01-14 11:18:59 -05:00
Michael Fogleman
77d18b88fe
Merge pull request #52 from rekby/multiline-measure
Add MeasureMultilineString method to Context.
2019-01-14 11:16:02 -05:00
Michael Fogleman
cdabe43353 Add InvertMask and an example 2019-01-14 11:04:49 -05:00
Timofey Kulin
16a00d1152 Add MeasureMultilineString method to Context.
Fix #https://github.com/fogleman/gg/issues/49
2018-10-23 09:27:48 +03:00
Michael Fogleman
0e0ff3ade7 Add gofont.go example, close #45 2018-08-22 21:57:45 -04:00
Michael Fogleman
7cc16ce8b2 Add -save test flag 2018-08-17 11:14:38 -04:00
Michael Fogleman
eb261f0bd1 Perf improvement when nil clip and solid color (usual case) 2018-08-16 22:49:05 -04:00
Michael Fogleman
a4f287e211 Fix #23 via doc on gg.LoadFontFace function. 2018-08-16 22:22:39 -04:00
Michael Fogleman
9a34078211 Fix #42. Performance improvement by reusing Rasterizer 2018-08-16 22:18:45 -04:00
Michael Fogleman
6bee5281ff Add BenchmarkCircles for simple benchmarking 2018-08-16 22:18:08 -04:00
Michael Fogleman
b2d255c6f2 Add some tests! (mostly regression tests) 2018-08-16 22:08:53 -04:00
Michael Fogleman
a9ff18eccd
Merge pull request #38 from tschaub/subimage
Start copying at bounds min instead of zero point
2018-03-16 10:14:17 -04:00
Tim Schaub
ca366ba15b Start copying at bounds min instead of zero point 2018-03-15 21:28:47 -06:00
Michael Fogleman
c97f757e6f better DrawEllipticalArc behavior when dc.hasCurrent is true 2018-03-08 13:42:55 -05:00
Michael Fogleman
da3d0863b9 add crisp lines example 2018-02-23 09:52:38 -05:00
Michael Fogleman
363d282ef3
Merge pull request #28 from derekschaab/issue-25
Return alpha-premultiplied color in gradient interpolation
2018-02-12 10:03:43 -05:00
Michael Fogleman
1b3894b028 Add SetMask and AsMask and an example using them 2018-02-11 23:15:32 -05:00
Michael Fogleman
c828b09e4a
Merge pull request #31 from magneticcoffee/get-dc-fontheight
Add a context fontHeight getter
2018-02-08 12:06:39 -05:00
Alexander Kahl
00ed7b79c0 Add a context fontHeight getter 2017-10-31 16:09:26 +01:00
Derek Schaab
ffbeb6b231 Return alpha-premultiplied color in gradient interpolation
Fixes #25.
2017-09-30 21:34:46 -05:00
Michael Fogleman
e611489b86 concat example 2017-07-28 18:55:21 -04:00
Michael Fogleman
ee8994ff90 Update README.md 2017-05-03 21:14:43 -04:00
Michael Fogleman
30f3b1d4ae Update README.md 2017-05-03 21:09:02 -04:00
Michael Fogleman
7fb6ce3c57 Merge pull request #16 from wsw0108/rotated-image-text
Draw image/text with current transformation appied
2017-05-03 21:06:54 -04:00
Michael Fogleman
0a834b0873 unicode table 2017-04-29 14:19:37 -04:00
Michael Fogleman
5e707d618f Update README.md 2017-04-14 13:57:42 -04:00
wsw
2c35caba58 apply current matrix when draw text 2017-01-08 16:23:33 +08:00
wsw
5e5aa69079 update example 'rotated-image' 2017-01-08 16:18:14 +08:00
wsw
b13517ff6f Merge branch 'master' into draw-image-rotated 2016-12-09 15:52:52 +08:00
wsw
3be68eb22e Merge branch 'master' into draw-image-rotated 2016-12-08 13:24:25 +08:00
wsw
f06d3564a1 apply current matrix when draw image 2016-12-08 11:16:19 +08:00
21 changed files with 923 additions and 44 deletions

View File

@ -6,11 +6,16 @@
## Installation
go get github.com/fogleman/gg
go get -u github.com/fogleman/gg
## GoDoc
Alternatively, you may use gopkg.in to grab a specific major-version:
https://godoc.org/github.com/fogleman/gg
go get -u gopkg.in/fogleman/gg.v1
## Documentation
- godoc: https://godoc.org/github.com/fogleman/gg
- pkg.go.dev: https://pkg.go.dev/github.com/fogleman/gg?tab=doc
## Hello, Circle!
@ -91,6 +96,7 @@ DrawString(s string, x, y float64)
DrawStringAnchored(s string, x, y, ax, ay float64)
DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align)
MeasureString(s string) (w, h float64)
MeasureMultilineString(s string, lineSpacing float64) (w, h float64)
WordWrap(s string, w float64) []string
SetFontFace(fontFace font.Face)
LoadFontFace(path string, points float64) error
@ -116,12 +122,13 @@ SetLineWidth(lineWidth float64)
SetLineCap(lineCap LineCap)
SetLineJoin(lineJoin LineJoin)
SetDash(dashes ...float64)
SetDashOffset(offset float64)
SetFillRule(fillRule FillRule)
```
## Gradients & Patterns
`gg` supports linear and radial gradients and surface patterns. You can also implement your own patterns.
`gg` supports linear, radial and conic gradients and surface patterns. You can also implement your own patterns.
```go
SetFillStyle(pattern Pattern)
@ -129,6 +136,7 @@ SetStrokeStyle(pattern Pattern)
NewSolidPattern(color color.Color)
NewLinearGradient(x0, y0, x1, y1 float64)
NewRadialGradient(x0, y0, r0, x1, y1, r1 float64)
NewConicGradient(cx, cy, deg float64)
NewSurfacePattern(im image.Image, op RepeatOp)
```
@ -151,8 +159,6 @@ 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.
Note: transforms do not currently affect `DrawImage` or `DrawString`.
## Stack Functions
Save and restore the state of the context. These can be nested.
@ -171,6 +177,9 @@ defined using paths.
Clip()
ClipPreserve()
ResetClip()
AsMask() *image.Alpha
SetMask(mask *image.Alpha)
InvertMask()
```
## Helper Functions
@ -187,12 +196,6 @@ SavePNG(path string, im image.Image) error
![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
See the output of this example below.

View File

@ -2,16 +2,20 @@
package gg
import (
"errors"
"image"
"image/color"
"image/draw"
"image/jpeg"
"image/png"
"io"
"math"
"strings"
"github.com/golang/freetype/raster"
"golang.org/x/image/draw"
"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
"golang.org/x/image/math/f64"
)
type LineCap int
@ -52,6 +56,7 @@ var (
type Context struct {
width int
height int
rasterizer *raster.Rasterizer
im *image.RGBA
mask *image.Alpha
color color.Color
@ -63,6 +68,7 @@ type Context struct {
current Point
hasCurrent bool
dashes []float64
dashOffset float64
lineWidth float64
lineCap LineCap
lineJoin LineJoin
@ -88,9 +94,12 @@ func NewContextForImage(im image.Image) *Context {
// NewContextForRGBA prepares a context for rendering onto the specified image.
// No copy is made.
func NewContextForRGBA(im *image.RGBA) *Context {
w := im.Bounds().Size().X
h := im.Bounds().Size().Y
return &Context{
width: im.Bounds().Size().X,
height: im.Bounds().Size().Y,
width: w,
height: h,
rasterizer: raster.NewRasterizer(w, h),
im: im,
color: color.Transparent,
fillPattern: defaultFillStyle,
@ -103,6 +112,15 @@ 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.
func (dc *Context) Image() image.Image {
return dc.im
@ -123,11 +141,23 @@ func (dc *Context) SavePNG(path string) error {
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.
func (dc *Context) EncodePNG(w io.Writer) error {
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
// disable dashes. The values specify the lengths of each dash, with
// alternating on and off lengths.
@ -135,6 +165,12 @@ func (dc *Context) SetDash(dashes ...float64) {
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) {
dc.lineWidth = lineWidth
}
@ -373,14 +409,16 @@ func (dc *Context) joiner() raster.Joiner {
func (dc *Context) stroke(painter raster.Painter) {
path := dc.strokePath
if len(dc.dashes) > 0 {
path = dashed(path, dc.dashes)
path = dashed(path, dc.dashes, dc.dashOffset)
} else {
// TODO: this is a temporary workaround to remove tiny segments
// that result in rendering issues
path = rasterPath(flattenPath(path))
}
r := raster.NewRasterizer(dc.width, dc.height)
r := dc.rasterizer
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.Rasterize(painter)
}
@ -392,8 +430,10 @@ func (dc *Context) fill(painter raster.Painter) {
copy(path, dc.fillPath)
path.Add1(dc.start.Fixed())
}
r := raster.NewRasterizer(dc.width, dc.height)
r := dc.rasterizer
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.Rasterize(painter)
}
@ -402,7 +442,19 @@ func (dc *Context) fill(painter raster.Painter) {
// line cap, line join and dash settings. The path is preserved after this
// operation.
func (dc *Context) StrokePreserve() {
painter := newPatternPainter(dc.im, dc.mask, dc.strokePattern)
var painter raster.Painter
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)
}
@ -417,7 +469,19 @@ func (dc *Context) Stroke() {
// FillPreserve fills the current path with the current color. Open subpaths
// are implicity closed. The path is preserved after this operation.
func (dc *Context) FillPreserve() {
painter := newPatternPainter(dc.im, dc.mask, dc.fillPattern)
var painter raster.Painter
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)
}
@ -444,6 +508,38 @@ 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
// clipping region with the current path as it would be filled by dc.Fill().
// The path is cleared after this operation.
@ -520,14 +616,18 @@ func (dc *Context) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) {
a2 := angle1 + (angle2-angle1)*p2
x0 := x + rx*math.Cos(a1)
y0 := y + ry*math.Sin(a1)
x1 := x + rx*math.Cos(a1+(a2-a1)/2)
y1 := y + ry*math.Sin(a1+(a2-a1)/2)
x1 := x + rx*math.Cos((a1+a2)/2)
y1 := y + ry*math.Sin((a1+a2)/2)
x2 := x + rx*math.Cos(a2)
y2 := y + ry*math.Sin(a2)
cx := 2*x1 - x0/2 - x2/2
cy := 2*y1 - y0/2 - y2/2
if i == 0 && !dc.hasCurrent {
dc.MoveTo(x0, y0)
if i == 0 {
if dc.hasCurrent {
dc.LineTo(x0, y0)
} else {
dc.MoveTo(x0, y0)
}
}
dc.QuadraticTo(cx, cy, x2, y2)
}
@ -564,7 +664,6 @@ func (dc *Context) DrawRegularPolygon(n int, x, y, r, rotation float64) {
}
// 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) {
dc.DrawImageAnchored(im, x, y, 0, 0)
}
@ -576,12 +675,17 @@ func (dc *Context) DrawImageAnchored(im image.Image, x, y int, ax, ay float64) {
s := im.Bounds().Size()
x -= int(ax * float64(s.X))
y -= int(ay * float64(s.Y))
p := image.Pt(x, y)
r := image.Rectangle{p, p.Add(s)}
transformer := draw.BiLinear
fx, fy := float64(x), float64(y)
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 {
draw.Draw(dc.im, r, im, image.ZP, draw.Over)
transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, nil)
} else {
draw.DrawMask(dc.im, r, im, image.ZP, dc.mask, p, draw.Over)
transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, &draw.Options{
DstMask: dc.mask,
DstMaskP: image.ZP,
})
}
}
@ -601,6 +705,10 @@ func (dc *Context) LoadFontFace(path string, points float64) error {
return err
}
func (dc *Context) FontHeight() float64 {
return dc.fontHeight
}
func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
d := &font.Drawer{
Dst: im,
@ -608,11 +716,34 @@ func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
Face: dc.fontFace,
Dot: fixp(x, y),
}
d.DrawString(s)
// based on Drawer.DrawString() in golang.org/x/image/font/font.go
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.
// Currently, rotation and scaling transforms are not supported.
func (dc *Context) DrawString(s string, x, y float64) {
dc.DrawStringAnchored(s, x, y, 0, 0)
}
@ -622,11 +753,10 @@ 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.
func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
w, h := dc.MeasureString(s)
x, y = dc.TransformPoint(x, y)
x -= ax * w
y += ay * h
if dc.mask == nil {
dc.drawString(dc.im, s, x, y)
dc.drawString(dc.im, s, x+float64(dc.im.Bounds().Min.X), y+float64(dc.im.Bounds().Min.Y))
} else {
im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height))
dc.drawString(im, s, x, y)
@ -639,8 +769,11 @@ func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
// spacing and text alignment.
func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) {
lines := dc.WordWrap(s, width)
// sync h formula with MeasureMultilineString
h := float64(len(lines)) * dc.fontHeight * lineSpacing
h -= (lineSpacing - 1) * dc.fontHeight
x -= ax * width
y -= ay * h
switch align {
@ -660,6 +793,29 @@ 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
// given the current font face.
func (dc *Context) MeasureString(s string) (w, h float64) {
@ -703,13 +859,13 @@ func (dc *Context) ScaleAbout(sx, sy, x, y float64) {
dc.Translate(-x, -y)
}
// Rotate updates the current matrix with a clockwise rotation.
// Rotate updates the current matrix with a anticlockwise rotation.
// Rotation occurs about the origin. Angle is specified in radians.
func (dc *Context) Rotate(angle float64) {
dc.matrix = dc.matrix.Rotate(angle)
}
// RotateAbout updates the current matrix with a clockwise rotation.
// RotateAbout updates the current matrix with a anticlockwise rotation.
// Rotation occurs about the specified point. Angle is specified in radians.
func (dc *Context) RotateAbout(angle, x, y float64) {
dc.Translate(x, y)

323
context_test.go Normal file
View File

@ -0,0 +1,323 @@
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()
}
}

BIN
examples/baboon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

30
examples/concat.go Normal file
View File

@ -0,0 +1,30 @@
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")
}

44
examples/crisp.go Normal file
View File

@ -0,0 +1,44 @@
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")
}

26
examples/gofont.go Normal file
View File

@ -0,0 +1,26 @@
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

@ -0,0 +1,38 @@
// +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")
}

41
examples/gradient-text.go Normal file
View File

@ -0,0 +1,41 @@
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")
}

14
examples/invert-mask.go Normal file
View File

@ -0,0 +1,14 @@
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")
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

View File

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

View File

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

34
examples/rotated-image.go Normal file
View File

@ -0,0 +1,34 @@
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")
}

30
examples/rotated-text.go Normal file
View File

@ -0,0 +1,30 @@
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")
}

25
examples/unicode.go Normal file
View File

@ -0,0 +1,25 @@
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 Normal file
View File

@ -0,0 +1,9 @@
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 Normal file
View File

@ -0,0 +1,8 @@
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,6 +164,52 @@ func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) Gradient {
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 {
if pos <= 0.0 || len(stops) == 1 {
return stops[0].color
@ -189,7 +235,7 @@ func colorLerp(c0, c1 color.Color, t float64) color.Color {
r0, g0, b0, a0 := c0.RGBA()
r1, g1, b1, a1 := c1.RGBA()
return color.NRGBA{
return color.RGBA{
lerp(r0, r1, t),
lerp(g0, g1, t),
lerp(b0, b1, t),

29
path.go
View File

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

35
util.go
View File

@ -4,6 +4,7 @@ import (
"fmt"
"image"
"image/draw"
"image/jpeg"
_ "image/jpeg"
"image/png"
"io/ioutil"
@ -53,9 +54,32 @@ func SavePNG(path string, im image.Image) error {
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 {
dst := image.NewRGBA(src.Bounds())
draw.Draw(dst, dst.Rect, src, image.ZP, draw.Src)
bounds := src.Bounds()
dst := image.NewRGBA(bounds)
draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
return dst
}
@ -85,7 +109,7 @@ func fixp(x, y float64) fixed.Point26_6 {
}
func fix(x float64) fixed.Int26_6 {
return fixed.Int26_6(x * 64)
return fixed.Int26_6(math.Round(x * 64))
}
func unfix(x fixed.Int26_6) float64 {
@ -100,6 +124,11 @@ func unfix(x fixed.Int26_6) float64 {
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) {
fontBytes, err := ioutil.ReadFile(path)
if err != nil {