Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@ -0,0 +1,65 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"dao.go",
"draw.go",
"freetype.go",
"gaussian.go",
"imgutil.go",
],
importpath = "go-common/app/interface/main/creative/dao/drawimg",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/interface/main/creative/conf:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/golang/freetype/raster:go_default_library",
"//vendor/github.com/golang/freetype/truetype:go_default_library",
"//vendor/golang.org/x/image/font:go_default_library",
"//vendor/golang.org/x/image/math/fixed:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"dao_test.go",
"draw_test.go",
"freetype_test.go",
"imgutil_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/interface/main/creative/conf:go_default_library",
"//vendor/github.com/bouk/monkey:go_default_library",
"//vendor/github.com/golang/freetype/raster:go_default_library",
"//vendor/github.com/golang/freetype/truetype:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
"//vendor/golang.org/x/image/font:go_default_library",
"//vendor/golang.org/x/image/math/fixed:go_default_library",
],
)

View File

@ -0,0 +1,75 @@
package drawimg
import (
"context"
"go-common/app/interface/main/creative/conf"
"go-common/library/log"
"os"
"strconv"
"time"
)
// Dao define
type Dao struct {
// conf
c *conf.Config
// watermark
dw *DrawImg
}
// New init dao
func New(c *conf.Config) (d *Dao) {
if !isExist(c.WaterMark.FontFile) {
log.Error("font file not exist")
return
}
if !isExist(c.WaterMark.UnameMark) {
log.Error("uname image file not exist")
return
}
if !isExist(c.WaterMark.UIDMark) {
log.Error("uid image file not exist")
return
}
d = &Dao{
c: c,
dw: NewDrawImg(c.WaterMark.FontFile, c.WaterMark.FontSize),
}
return
}
// Make create watermark.
func (d *Dao) Make(c context.Context, mid int64, text string, isUname bool) (dw *DrawImg, err error) {
var src string
if isUname {
src = d.c.WaterMark.UnameMark
} else {
src = d.c.WaterMark.UIDMark
}
img, err := d.dw.ReadSrcImg(src)
if err != nil {
return
}
if img == nil {
return
}
d.dw.srcImg = img
midStr := strconv.FormatInt(mid, 10)
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
file := d.c.WaterMark.SaveImg + midStr + "-" + timestamp + ".png"
if err = d.dw.Draw(text, file, isUname); err != nil {
log.Error("d.dw.Draw error(%v)", err)
return
}
dw = &DrawImg{
CanvasWidth: d.dw.CanvasWidth,
CanvasHeight: d.dw.CanvasHeight,
File: file,
}
return
}
func isExist(path string) bool {
_, err := os.Stat(path)
return err == nil || os.IsExist(err)
}

View File

@ -0,0 +1,58 @@
package drawimg
import (
"context"
"flag"
"github.com/smartystreets/goconvey/convey"
"go-common/app/interface/main/creative/conf"
"os"
"testing"
)
var (
d *Dao
)
func TestMain(m *testing.M) {
if os.Getenv("DEPLOY_ENV") != "" {
flag.Set("app_id", "main.archive.creative")
flag.Set("conf_token", "96b6a6c10bb311e894c14a552f48fef8")
flag.Set("tree_id", "2305")
flag.Set("conf_version", "docker-1")
flag.Set("deploy_env", "uat")
flag.Set("conf_host", "config.bilibili.co")
flag.Set("conf_path", "/tmp")
flag.Set("region", "sh")
flag.Set("zone", "sh001")
} else {
flag.Set("conf", "../../cmd/creative.toml")
}
flag.Parse()
if err := conf.Init(); err != nil {
panic(err)
}
d = New(conf.Conf)
d = &Dao{
c: conf.Conf,
dw: &di,
}
m.Run()
os.Exit(0)
}
func TestDrawimgMake(t *testing.T) {
convey.Convey("Make", t, func(ctx convey.C) {
var (
c = context.Background()
mid = int64(1)
text = "123"
isUname = true
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
_, err := d.Make(c, mid, text, isUname)
ctx.Convey("Then err should be nil.dw should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldNotBeNil)
})
})
})
}

View File

@ -0,0 +1,171 @@
package drawimg
import (
"errors"
"image"
"image/color"
"image/draw"
"io/ioutil"
"strconv"
"strings"
"go-common/library/log"
"github.com/golang/freetype/truetype"
"golang.org/x/image/math/fixed"
)
// DrawImg create img info.
type DrawImg struct {
size int
CanvasWidth int
CanvasHeight int
File string
txtWidth int
srcImg image.Image
Canvas *image.NRGBA
c *Context
f *truetype.Font
}
// NewDrawImg create new img.
func NewDrawImg(fontfile string, size int) (w *DrawImg) {
w = &DrawImg{
size: size,
}
f, err := w.readFont(fontfile, size)
if err != nil {
return
}
if f == nil {
return
}
w.f = f
return
}
//ReadSrcImg read an image
func (w *DrawImg) ReadSrcImg(path string) (img image.Image, err error) {
if img, err = Open(path); err != nil {
log.Error("readSrcImg error(%v)", err)
}
return
}
func (w *DrawImg) readFont(path string, size int) (f *truetype.Font, err error) {
fbs, err := ioutil.ReadFile(path)
if err != nil {
log.Error("readFont error(%v)", err)
return
}
f, err = ParseFont(fbs)
if err != nil {
log.Error("error(%v)", err)
}
cxt := NewContext()
if cxt == nil {
return
}
w.c = cxt
w.c.SetFont(f)
w.c.SetFontSize(float64(size))
return
}
func (w *DrawImg) imgWidth() int {
return w.srcImg.Bounds().Max.X
}
func (w *DrawImg) imgHeight() int {
return w.srcImg.Bounds().Max.Y
}
func (w *DrawImg) newImgWidth() int {
return w.txtWidth + w.imgWidth()
}
func (w *DrawImg) newCanvas(width, height int) *image.NRGBA {
return NewNRGBA(width, height, color.RGBA{255, 0, 0, 0})
}
func (w *DrawImg) fillColor(r, g, b, a int32) image.Image {
return &image.Uniform{color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)}}
}
func (w *DrawImg) textWidth(text string) (err error) {
defer func() {
if err := recover(); err != nil {
log.Error("draw set textWidth text(%s)|error(%v)", text, err)
}
}()
box, err := w.c.FontBox(text, w.pt(3, 5))
if err != nil {
log.Error("set textWidth text(%s)|error(%v)", text, err)
return
}
widthStr := strings.Split(Int26_6ToString(box.X), ":")[0]
wid, err := strconv.ParseInt(widthStr, 10, 64)
if err != nil {
log.Error("strconv.ParseInt widthStr(%+v)|error(%v)", widthStr, err)
return
}
w.txtWidth = int(wid)
return
}
func (w *DrawImg) pt(x, y int) fixed.Point26_6 {
return Pt(x, y+int(w.c.PointToFixed(float64(w.size))>>6))
}
func (w *DrawImg) setFont(text string, dstRgba *image.NRGBA, fsrc image.Image, pt fixed.Point26_6) (err error) {
w.c.SetClip(w.Canvas.Bounds())
w.c.SetDst(dstRgba)
w.c.SetSrc(fsrc)
_, err = w.c.DrawString(text, pt)
if err != nil {
log.Error("setFont error(%v)", err)
}
return
}
func (w *DrawImg) composite(dstCanvas *image.NRGBA, src image.Image, isLeft bool) {
var p image.Point
if isLeft {
p = image.Point{-int(w.txtWidth), 0}
} else {
p = image.ZP
}
draw.Draw(dstCanvas, image.Rect(0, 0, w.newImgWidth(), w.imgHeight()), src, p, draw.Over)
}
// Draw write text to the left or right of img.
func (w *DrawImg) Draw(text, savepath string, isLeft bool) (err error) {
if text == "" {
err = errors.New("draw: DrawText called with a empty text")
return
}
w.textWidth(text)
w.CanvasWidth = w.newImgWidth()
w.CanvasHeight = w.imgHeight()
w.Canvas = w.newCanvas(w.CanvasWidth, w.CanvasHeight)
if w.c == nil || w.Canvas == nil {
err = errors.New("draw: DrawText w.c or w.Canvas is nil")
return
}
draw.Draw(w.Canvas, w.Canvas.Bounds(), w.Canvas, image.ZP, draw.Src)
var pt fixed.Point26_6
if isLeft {
pt = w.pt(3, 5)
} else {
pt = w.pt(w.imgWidth(), 8)
}
black := w.fillColor(0, 0, 0, 125)
w.setFont(text, w.Canvas, black, pt)
blurRgba := Blur(w.Canvas, 6, 3.5)
white := w.fillColor(255, 255, 255, 180)
w.setFont(text, blurRgba, white, pt)
w.composite(blurRgba, w.srcImg, isLeft)
Save(blurRgba, savepath)
return
}

View File

@ -0,0 +1,257 @@
package drawimg
import (
"image"
"image/draw"
"io/ioutil"
"reflect"
"testing"
"github.com/bouk/monkey"
"github.com/golang/freetype/raster"
"github.com/golang/freetype/truetype"
"github.com/smartystreets/goconvey/convey"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
var (
di = DrawImg{
size: 100,
CanvasWidth: 20,
CanvasHeight: 20,
File: "",
txtWidth: 20,
srcImg: image.NewAlpha(imgRectangle),
Canvas: &image.NRGBA{},
c: &c,
f: &truetype.Font{},
}
c = Context{
r: nil,
f: &truetype.Font{},
glyphBuf: truetype.GlyphBuf{},
clip: image.Rectangle{},
dst: nil,
src: nil,
fontSize: 0,
dpi: 0,
scale: 0,
hinting: 0,
cache: [1024]cacheEntry{},
}
imgRectangle = image.Rectangle{
Min: image.Point{
X: 0,
Y: 0,
},
Max: image.Point{
X: 1,
Y: 1,
},
}
)
func TestDrawimgNewDrawImg(t *testing.T) {
convey.Convey("NewDrawImg", t, func(ctx convey.C) {
var (
fontfile = ""
size = int(10)
)
monkeyReadFile([]byte{}, nil)
monkeyTrueTypeParser(&truetype.Font{}, nil)
monkeyFreeTypeSetFont()
monkeySetFontSize()
ctx.Convey("When everything gose positive", func(ctx convey.C) {
w := NewDrawImg(fontfile, size)
ctx.Convey("Then w should not be nil.", func(ctx convey.C) {
ctx.So(w, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgreadFont(t *testing.T) {
convey.Convey("readFont", t, func(ctx convey.C) {
var (
path = ""
size = int(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
monkeyReadFile([]byte{}, nil)
monkeyTrueTypeParser(&truetype.Font{}, nil)
monkeyFreeTypeSetFont()
monkeySetFontSize()
f, err := di.readFont(path, size)
ctx.Convey("Then err should be nil.f should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(f, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgnewCanvas(t *testing.T) {
convey.Convey("newCanvas", t, func(ctx convey.C) {
var (
width = int(0)
height = int(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := di.newCanvas(width, height)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgfillColor(t *testing.T) {
convey.Convey("fillColor", t, func(ctx convey.C) {
var (
r = int32(0)
g = int32(0)
b = int32(0)
a = int32(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := di.fillColor(r, g, b, a)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgtextWidth(t *testing.T) {
convey.Convey("textWidth", t, func(ctx convey.C) {
var (
text = "12"
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
monkeyFontBox(fixed.Point26_6{}, nil)
err := di.textWidth(text)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDrawimgpt(t *testing.T) {
convey.Convey("pt", t, func(ctx convey.C) {
var (
x = int(0)
y = int(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := di.pt(x, y)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgsetFont(t *testing.T) {
convey.Convey("setFont", t, func(ctx convey.C) {
var (
text = ""
dstRgba = &image.NRGBA{}
fsrc image.Image
pt fixed.Point26_6
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
monkeyDrawString(fixed.Point26_6{}, nil)
err := di.setFont(text, dstRgba, fsrc, pt)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDrawimgcomposite(t *testing.T) {
convey.Convey("composite", t, func(ctx convey.C) {
var (
dstCanvas = &image.NRGBA{}
src image.Image
isLeft bool
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
monkeyDraw()
//monkeybounds(imgRectangle)
di.composite(dstCanvas, src, isLeft)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgDraw(t *testing.T) {
convey.Convey("Draw", t, func(ctx convey.C) {
var (
text = "123"
savepath = ""
isLeft bool
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
//monkeybounds(imgRectangle)
monkeyFreeTypeSetFont()
err := di.Draw(text, savepath, isLeft)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func monkeyReadFile(file []byte, err error) {
monkey.Patch(ioutil.ReadFile, func(_ string) ([]byte, error) {
return file, err
})
}
func monkeyTrueTypeParser(font *truetype.Font, err error) {
monkey.Patch(truetype.Parse, func(_ []byte) (*truetype.Font, error) {
return font, err
})
}
func monkeyFreeTypeSetFont() {
monkey.PatchInstanceMethod(reflect.TypeOf(di.c), "SetFont", func(_ *Context, _ *truetype.Font) {})
}
func monkeySetFontSize() {
monkey.PatchInstanceMethod(reflect.TypeOf(di.c), "SetFontSize", func(_ *Context, _ float64) {})
}
func monkeyDrawString(p fixed.Point26_6, err error) {
monkey.PatchInstanceMethod(reflect.TypeOf(di.c), "DrawString", func(_ *Context, _ string, _ fixed.Point26_6) (fixed.Point26_6, error) {
return p, err
})
}
func monkeyDraw() {
monkey.Patch(draw.Draw, func(_ draw.Image, _ image.Rectangle, _ image.Image, _ image.Point, _ draw.Op) {})
}
func monkeyFontBox(p fixed.Point26_6, err error) {
monkey.PatchInstanceMethod(reflect.TypeOf(di.c), "FontBox", func(_ *Context, _ string, _ fixed.Point26_6) (fixed.Point26_6, error) {
return p, err
})
}
func monkeyLoad() {
monkey.PatchInstanceMethod(reflect.TypeOf(&di.c.glyphBuf), "Load", func(_ *truetype.GlyphBuf, _ *truetype.Font, _ fixed.Int26_6, _ truetype.Index, _ font.Hinting) error {
return nil
})
}
func monkeyClear() {
monkey.PatchInstanceMethod(reflect.TypeOf(di.c.r), "Clear", func(_ *raster.Rasterizer) {})
}
func monkeyRasterizer() {
monkey.PatchInstanceMethod(reflect.TypeOf(di.c.r), "Rasterize", func(_ *raster.Rasterizer, _ raster.Painter) {})
}

View File

@ -0,0 +1,316 @@
package drawimg
import (
"errors"
"fmt"
"image"
"image/draw"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
"github.com/golang/freetype/raster"
"github.com/golang/freetype/truetype"
)
const (
nGlyphs = 256
nXFractions = 4
nYFractions = 1
)
type cacheEntry struct {
valid bool
glyph truetype.Index
advanceWidth fixed.Int26_6
mask *image.Alpha
offset image.Point
}
// ParseFont just calls the Parse function from the freetype/truetype package.
func ParseFont(b []byte) (*truetype.Font, error) {
return truetype.Parse(b)
}
// Pt converts from a co-ordinate pair measured in pixels to a fixed.Point26_6 co-ordinate pair measured in fixed.Int26_6 units.
func Pt(x, y int) fixed.Point26_6 {
return fixed.Point26_6{
X: fixed.Int26_6(x << 6),
Y: fixed.Int26_6(y << 6),
}
}
// A Context holds the state for drawing text in a given font and size.
type Context struct {
r *raster.Rasterizer
f *truetype.Font
glyphBuf truetype.GlyphBuf
clip image.Rectangle
dst draw.Image
src image.Image
fontSize, dpi float64
scale fixed.Int26_6
hinting font.Hinting
cache [nGlyphs * nXFractions * nYFractions]cacheEntry
}
// PointToFixed converts the given number of points (as in "a 12 point font") into a 26.6 fixed point number of pixels.
func (c *Context) PointToFixed(x float64) fixed.Int26_6 {
return fixed.Int26_6(x * float64(c.dpi) * (64.0 / 72.0))
}
// drawContour draws the given closed contour with the given offset.
func (c *Context) drawContour(ps []truetype.Point, dx, dy fixed.Int26_6) {
if len(ps) == 0 {
return
}
start := fixed.Point26_6{
X: dx + ps[0].X,
Y: dy - ps[0].Y,
}
others := []truetype.Point(nil)
if ps[0].Flags&0x01 != 0 {
others = ps[1:]
} else {
last := fixed.Point26_6{
X: dx + ps[len(ps)-1].X,
Y: dy - 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
}
}
c.r.Start(start)
q0, on0 := start, true
for _, p := range others {
q := fixed.Point26_6{
X: dx + p.X,
Y: dy - p.Y,
}
on := p.Flags&0x01 != 0
if on {
if on0 {
c.r.Add1(q)
} else {
c.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,
}
c.r.Add2(q0, mid)
}
}
q0, on0 = q, on
}
if on0 {
c.r.Add1(start)
} else {
c.r.Add2(q0, start)
}
}
// 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 (c *Context) rasterize(glyph truetype.Index, fx, fy fixed.Int26_6) (
fixed.Int26_6, *image.Alpha, image.Point, error) {
if err := c.glyphBuf.Load(c.f, c.scale, glyph, c.hinting); err != nil {
return 0, nil, image.Point{}, err
}
xmin := int(fx+c.glyphBuf.Bounds.Min.X) >> 6
ymin := int(fy-c.glyphBuf.Bounds.Max.Y) >> 6
xmax := int(fx+c.glyphBuf.Bounds.Max.X+0x3f) >> 6
ymax := int(fy-c.glyphBuf.Bounds.Min.Y+0x3f) >> 6
if xmin > xmax || ymin > ymax {
return 0, nil, image.Point{}, errors.New("freetype: negative sized glyph")
}
fx -= fixed.Int26_6(xmin << 6)
fy -= fixed.Int26_6(ymin << 6)
c.r.Clear()
e0 := 0
for _, e1 := range c.glyphBuf.Ends {
c.drawContour(c.glyphBuf.Points[e0:e1], fx, fy)
e0 = e1
}
a := image.NewAlpha(image.Rect(0, 0, xmax-xmin, ymax-ymin))
c.r.Rasterize(raster.NewAlphaSrcPainter(a))
return c.glyphBuf.AdvanceWidth, a, image.Point{xmin, ymin}, nil
}
// glyph returns the advance width, glyph mask and integer-pixel offset to render the given glyph at the given sub-pixel point.
func (c *Context) glyph(glyph truetype.Index, p fixed.Point26_6) (fixed.Int26_6, *image.Alpha, image.Point, error) {
ix, fx := int(p.X>>6), p.X&0x3f
iy, fy := int(p.Y>>6), p.Y&0x3f
tg := int(glyph) % nGlyphs
tx := int(fx) / (64 / nXFractions)
ty := int(fy) / (64 / nYFractions)
t := ((tg*nXFractions)+tx)*nYFractions + ty
if e := c.cache[t]; e.valid && e.glyph == glyph {
return e.advanceWidth, e.mask, e.offset.Add(image.Point{ix, iy}), nil
}
advanceWidth, mask, offset, err := c.rasterize(glyph, fx, fy)
if err != nil {
return 0, nil, image.Point{}, err
}
c.cache[t] = cacheEntry{true, glyph, advanceWidth, mask, offset}
return advanceWidth, mask, offset.Add(image.Point{ix, iy}), nil
}
// DrawString draws s at p and returns p advanced by the text extent.
func (c *Context) DrawString(s string, p fixed.Point26_6) (fixed.Point26_6, error) {
if c.f == nil {
return fixed.Point26_6{}, errors.New("freetype: DrawText called with a nil font")
}
prev, hasPrev := truetype.Index(0), false
for _, rune := range s {
index := c.f.Index(rune)
if hasPrev {
kern := c.f.Kern(c.scale, prev, index)
if c.hinting != font.HintingNone {
kern = (kern + 32) &^ 63
}
p.X += kern
}
advanceWidth, mask, offset, err := c.glyph(index, p)
if err != nil {
return fixed.Point26_6{}, err
}
p.X += advanceWidth
glyphRect := mask.Bounds().Add(offset)
dr := c.clip.Intersect(glyphRect)
if !dr.Empty() {
mp := image.Point{0, dr.Min.Y - glyphRect.Min.Y}
draw.DrawMask(c.dst, dr, c.src, image.ZP, mask, mp, draw.Over)
}
prev, hasPrev = index, true
}
return p, nil
}
// recalc recalculates scale and bounds values from the font size, screen resolution and font metrics, and invalidates the glyph cache.
func (c *Context) recalc() {
c.scale = fixed.Int26_6(c.fontSize * c.dpi * (64.0 / 72.0))
if c.f == nil {
c.r.SetBounds(0, 0)
} else {
b := c.f.Bounds(c.scale)
xmin := +int(b.Min.X) >> 6
ymin := -int(b.Max.Y) >> 6
xmax := +int(b.Max.X+63) >> 6
ymax := -int(b.Min.Y-63) >> 6
c.r.SetBounds(xmax-xmin, ymax-ymin)
}
for i := range c.cache {
c.cache[i] = cacheEntry{}
}
}
// SetDPI sets the screen resolution in dots per inch.
func (c *Context) SetDPI(dpi float64) {
if c.dpi == dpi {
return
}
c.dpi = dpi
c.recalc()
}
// SetFont sets the font used to draw text.
func (c *Context) SetFont(f *truetype.Font) {
if c.f == f {
return
}
c.f = f
c.recalc()
}
// SetFontSize sets the font size in points (as in "a 12 point font").
func (c *Context) SetFontSize(fontSize float64) {
if c.fontSize == fontSize {
return
}
c.fontSize = fontSize
c.recalc()
}
// SetHinting sets the hinting policy.
func (c *Context) SetHinting(hinting font.Hinting) {
c.hinting = hinting
for i := range c.cache {
c.cache[i] = cacheEntry{}
}
}
// SetDst sets the destination image for draw operations.
func (c *Context) SetDst(dst draw.Image) {
c.dst = dst
}
// SetSrc sets the source image for draw operations. This is typically an image.Uniform.
func (c *Context) SetSrc(src image.Image) {
c.src = src
}
// SetClip sets the clip rectangle for drawing.
func (c *Context) SetClip(clip image.Rectangle) {
c.clip = clip
}
// NewContext creates a new Context.
func NewContext() *Context {
return &Context{
r: raster.NewRasterizer(0, 0),
fontSize: 12,
dpi: 72,
scale: 12 << 6,
}
}
// FontBox get textbox.
func (c *Context) FontBox(s string, p fixed.Point26_6) (fixed.Point26_6, error) {
if c.f == nil {
return fixed.Point26_6{}, errors.New("freetype: DrawText called with a nil font")
}
prev, hasPrev := truetype.Index(0), false
for _, rune := range s {
index := c.f.Index(rune)
if hasPrev {
kern := c.f.Kern(c.scale, prev, index)
if c.hinting != font.HintingNone {
kern = (kern + 32) &^ 63
}
p.X += kern
}
advanceWidth, _, _, err := c.glyph(index, p)
if err != nil {
return fixed.Point26_6{}, err
}
p.X += advanceWidth
}
return p, nil
}
// Int26_6ToString convert Int26_6 to string.
func Int26_6ToString(x fixed.Int26_6) string {
const shift, mask = 6, 1<<6 - 1
if x >= 0 {
return fmt.Sprintf("%d:%02d", int32(x>>shift), int32(x&mask))
}
x = -x
if x >= 0 {
return fmt.Sprintf("-%d:%02d", int32(x>>shift), int32(x&mask))
}
return "-33554432:00" // The minimum value is -(1<<25).
}

View File

@ -0,0 +1,228 @@
package drawimg
import (
"image"
"image/draw"
"testing"
"github.com/golang/freetype/truetype"
"github.com/smartystreets/goconvey/convey"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
func TestDrawimgParseFont(t *testing.T) {
convey.Convey("ParseFont", t, func(ctx convey.C) {
var (
b = []byte("")
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1, err := ParseFont(b)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgPt(t *testing.T) {
convey.Convey("Pt", t, func(ctx convey.C) {
var (
x = int(0)
y = int(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := Pt(x, y)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgPointToFixed(t *testing.T) {
convey.Convey("PointToFixed", t, func(ctx convey.C) {
var (
x = float64(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := di.c.PointToFixed(x)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgdrawContour(t *testing.T) {
convey.Convey("drawContour", t, func(ctx convey.C) {
var (
ps = []truetype.Point{
{
X: 2,
Y: 2,
Flags: 1,
},
{
X: 2,
Y: 2,
Flags: 1,
},
}
dx fixed.Int26_6
dy fixed.Int26_6
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
di.c.drawContour(ps, dx, dy)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgrecalc(t *testing.T) {
convey.Convey("recalc", t, func(ctx convey.C) {
ctx.Convey("When everything gose positive", func(ctx convey.C) {
di.c.recalc()
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgSetDPI(t *testing.T) {
convey.Convey("SetDPI", t, func(ctx convey.C) {
var (
dpi = float64(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
di.c.SetDPI(dpi)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgSetFont(t *testing.T) {
convey.Convey("SetFont", t, func(ctx convey.C) {
var (
f = &truetype.Font{}
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
di.c.SetFont(f)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgSetFontSize(t *testing.T) {
convey.Convey("SetFontSize", t, func(ctx convey.C) {
var (
fontSize = float64(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
di.c.SetFontSize(fontSize)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgSetHinting(t *testing.T) {
convey.Convey("SetHinting", t, func(ctx convey.C) {
var (
hinting font.Hinting
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
di.c.SetHinting(hinting)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgSetDst(t *testing.T) {
convey.Convey("SetDst", t, func(ctx convey.C) {
var (
dst draw.Image
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
di.c.SetDst(dst)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgSetSrc(t *testing.T) {
convey.Convey("SetSrc", t, func(ctx convey.C) {
var (
src image.Image
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
di.c.SetSrc(src)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgSetClip(t *testing.T) {
convey.Convey("SetClip", t, func(ctx convey.C) {
var (
clip image.Rectangle
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
di.c.SetClip(clip)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgNewContext(t *testing.T) {
convey.Convey("NewContext", t, func(ctx convey.C) {
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := NewContext()
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgInt26_6ToString(t *testing.T) {
convey.Convey("Int26_6ToString", t, func(ctx convey.C) {
var (
x fixed.Int26_6
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := Int26_6ToString(x)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgDrawString(t *testing.T) {
convey.Convey("DrawString", t, func(ctx convey.C) {
var (
s = "mock"
p fixed.Point26_6
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
di.c = &c
monkeyLoad()
monkeyClear()
monkeyRasterizer()
p1, err := di.c.DrawString(s, p)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}

View File

@ -0,0 +1,158 @@
package drawimg
import (
"image"
"image/color"
"math"
)
func gaussianBlurKernel(x, sigma float64) float64 {
return math.Exp(-(x*x)/(2*sigma*sigma)) / (sigma * math.Sqrt(2*math.Pi))
}
func blurHorizontal(src *image.NRGBA, kernel []float64) *image.NRGBA {
radius := len(kernel) - 1
width := src.Bounds().Max.X
height := src.Bounds().Max.Y
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
parallel(width, func(partStart, partEnd int) {
for x := partStart; x < partEnd; x++ {
start := x - radius
if start < 0 {
start = 0
}
end := x + radius
if end > width-1 {
end = width - 1
}
weightSum := 0.0
for ix := start; ix <= end; ix++ {
weightSum += kernel[absint(x-ix)]
}
for y := 0; y < height; y++ {
var r, g, b, a float64
for ix := start; ix <= end; ix++ {
weight := kernel[absint(x-ix)]
i := y*src.Stride + ix*4
wa := float64(src.Pix[i+3]) * weight
r += float64(src.Pix[i+0]) * wa
g += float64(src.Pix[i+1]) * wa
b += float64(src.Pix[i+2]) * wa
a += wa
}
j := y*dst.Stride + x*4
dst.Pix[j+0] = clamp(r / a)
dst.Pix[j+1] = clamp(g / a)
dst.Pix[j+2] = clamp(b / a)
dst.Pix[j+3] = clamp(a / weightSum)
}
}
})
return dst
}
func blurVertical(src *image.NRGBA, kernel []float64) *image.NRGBA {
radius := len(kernel) - 1
width := src.Bounds().Max.X
height := src.Bounds().Max.Y
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
parallel(height, func(partStart, partEnd int) {
for y := partStart; y < partEnd; y++ {
start := y - radius
if start < 0 {
start = 0
}
end := y + radius
if end > height-1 {
end = height - 1
}
weightSum := 0.0
for iy := start; iy <= end; iy++ {
weightSum += kernel[absint(y-iy)]
}
for x := 0; x < width; x++ {
var r, g, b, a float64
for iy := start; iy <= end; iy++ {
weight := kernel[absint(y-iy)]
i := iy*src.Stride + x*4
wa := float64(src.Pix[i+3]) * weight
r += float64(src.Pix[i+0]) * wa
g += float64(src.Pix[i+1]) * wa
b += float64(src.Pix[i+2]) * wa
a += wa
}
j := y*dst.Stride + x*4
dst.Pix[j+0] = clamp(r / a)
dst.Pix[j+1] = clamp(g / a)
dst.Pix[j+2] = clamp(b / a)
dst.Pix[j+3] = clamp(a / weightSum)
}
}
})
return dst
}
// Sharpen produces a sharpened version of the image.
func Sharpen(img image.Image, radius int, sigma float64) *image.NRGBA {
if sigma <= 0 {
// sigma parameter must be positive!
return Clone(img)
}
src := toNRGBA(img)
blurred := Blur(img, radius, sigma)
width := src.Bounds().Max.X
height := src.Bounds().Max.Y
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
parallel(height, func(partStart, partEnd int) {
for y := partStart; y < partEnd; y++ {
for x := 0; x < width; x++ {
i := y*src.Stride + x*4
for j := 0; j < 4; j++ {
k := i + j
val := int(src.Pix[k])<<1 - int(blurred.Pix[k])
if val < 0 {
val = 0
} else if val > 255 {
val = 255
}
dst.Pix[k] = uint8(val)
}
}
}
})
return dst
}
// Blur produces a blurred version of the image using a Gaussian function.
func Blur(img image.Image, radius int, sigma float64) *image.NRGBA {
var dst *image.NRGBA
if sigma <= 0 {
// sigma parameter must be positive!
return Clone(img)
}
src := toNRGBA(img)
kernel := make([]float64, radius+1)
for i := 0; i <= radius; i++ {
kernel[i] = gaussianBlurKernel(float64(i), sigma)
}
dst = blurHorizontal(src, kernel)
dst = blurVertical(dst, kernel)
return dst
}
// IsTooBright assume that average brightness higher than 100 is too bright.
func IsTooBright(img image.Image) bool {
var (
pixCount, totalBrightness float64
)
pixCount = 0
totalBrightness = 0
AdjustFunc(img, func(c color.NRGBA) color.NRGBA {
brightness := 0.2126*float64(c.R) + 0.7152*float64(c.G) + 0.0722*float64(c.B)
totalBrightness += brightness
pixCount++
return c
})
averBrightness := totalBrightness / pixCount
return averBrightness > 100
}

View File

@ -0,0 +1,558 @@
package drawimg
import (
"errors"
"image"
"image/color"
"image/gif"
"image/jpeg"
"image/png"
"io"
"math"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"sync/atomic"
)
// Format is an image file format.
type Format int
// Image file formats.
const (
JPEG Format = iota
PNG
GIF
)
func (f Format) String() string {
switch f {
case JPEG:
return "JPEG"
case PNG:
return "PNG"
case GIF:
return "GIF"
default:
return "Unsupported"
}
}
var (
// ErrUnsupportedFormat means the given image format (or file extension) is unsupported.
ErrUnsupportedFormat = errors.New("imaging: unsupported image format")
)
// Decode reads an image from r.
func Decode(r io.Reader) (image.Image, error) {
img, _, err := image.Decode(r)
if err != nil {
return nil, err
}
return toNRGBA(img), nil
}
// Open loads an image from file
func Open(filename string) (image.Image, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
img, err := Decode(file)
return img, err
}
// Encode writes the image img to w in the specified format (JPEG, PNG, GIF, TIFF or BMP).
func Encode(w io.Writer, img image.Image, format Format) error {
var err error
switch format {
case JPEG:
var rgba *image.RGBA
if nrgba, ok := img.(*image.NRGBA); ok {
if nrgba.Opaque() {
rgba = &image.RGBA{
Pix: nrgba.Pix,
Stride: nrgba.Stride,
Rect: nrgba.Rect,
}
}
}
if rgba != nil {
err = jpeg.Encode(w, rgba, &jpeg.Options{Quality: 95})
} else {
err = jpeg.Encode(w, img, &jpeg.Options{Quality: 95})
}
case PNG:
err = png.Encode(w, img)
case GIF:
err = gif.Encode(w, img, &gif.Options{NumColors: 256})
default:
err = ErrUnsupportedFormat
}
return err
}
// Save saves the image to file with the specified filename.
// The format is determined from the filename extension: "jpg" (or "jpeg"), "png", "gif" are supported.
func Save(img image.Image, filename string) (err error) {
formats := map[string]Format{
".jpg": JPEG,
".jpeg": JPEG,
".png": PNG,
".gif": GIF,
}
ext := strings.ToLower(filepath.Ext(filename))
f, ok := formats[ext]
if !ok {
return ErrUnsupportedFormat
}
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
return Encode(file, img, f)
}
// NewNRGBA creates a new image with the specified width and height, and fills it with the specified color.
func NewNRGBA(width, height int, fillColor color.Color) *image.NRGBA {
if width <= 0 || height <= 0 {
return &image.NRGBA{}
}
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
c := color.NRGBAModel.Convert(fillColor).(color.NRGBA)
if c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0 {
return dst
}
cs := []uint8{c.R, c.G, c.B, c.A}
// fill the first row
for x := 0; x < width; x++ {
copy(dst.Pix[x*4:(x+1)*4], cs)
}
// copy the first row to other rows
for y := 1; y < height; y++ {
copy(dst.Pix[y*dst.Stride:y*dst.Stride+width*4], dst.Pix[0:width*4])
}
return dst
}
// Clone returns a copy of the given image.
func Clone(img image.Image) *image.NRGBA {
srcBounds := img.Bounds()
srcMinX := srcBounds.Min.X
srcMinY := srcBounds.Min.Y
dstBounds := srcBounds.Sub(srcBounds.Min)
dstW := dstBounds.Dx()
dstH := dstBounds.Dy()
dst := image.NewNRGBA(dstBounds)
switch src := img.(type) {
case *image.NRGBA:
rowSize := srcBounds.Dx() * 4
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize])
}
})
case *image.NRGBA64:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
dst.Pix[di+0] = src.Pix[si+0]
dst.Pix[di+1] = src.Pix[si+2]
dst.Pix[di+2] = src.Pix[si+4]
dst.Pix[di+3] = src.Pix[si+6]
di += 4
si += 8
}
}
})
case *image.RGBA:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
a := src.Pix[si+3]
dst.Pix[di+3] = a
switch a {
case 0:
dst.Pix[di+0] = 0
dst.Pix[di+1] = 0
dst.Pix[di+2] = 0
case 0xff:
dst.Pix[di+0] = src.Pix[si+0]
dst.Pix[di+1] = src.Pix[si+1]
dst.Pix[di+2] = src.Pix[si+2]
default:
var tmp uint16
tmp = uint16(src.Pix[si+0]) * 0xff / uint16(a)
dst.Pix[di+0] = uint8(tmp)
tmp = uint16(src.Pix[si+1]) * 0xff / uint16(a)
dst.Pix[di+1] = uint8(tmp)
tmp = uint16(src.Pix[si+2]) * 0xff / uint16(a)
dst.Pix[di+2] = uint8(tmp)
}
di += 4
si += 4
}
}
})
case *image.RGBA64:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
a := src.Pix[si+6]
dst.Pix[di+3] = a
switch a {
case 0:
dst.Pix[di+0] = 0
dst.Pix[di+1] = 0
dst.Pix[di+2] = 0
case 0xff:
dst.Pix[di+0] = src.Pix[si+0]
dst.Pix[di+1] = src.Pix[si+2]
dst.Pix[di+2] = src.Pix[si+4]
default:
var tmp uint16
tmp = uint16(src.Pix[si+0]) * 0xff / uint16(a)
dst.Pix[di+0] = uint8(tmp)
tmp = uint16(src.Pix[si+2]) * 0xff / uint16(a)
dst.Pix[di+1] = uint8(tmp)
tmp = uint16(src.Pix[si+4]) * 0xff / uint16(a)
dst.Pix[di+2] = uint8(tmp)
}
di += 4
si += 8
}
}
})
case *image.Gray:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := src.Pix[si]
dst.Pix[di+0] = c
dst.Pix[di+1] = c
dst.Pix[di+2] = c
dst.Pix[di+3] = 0xff
di += 4
si++
}
}
})
case *image.Gray16:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := src.Pix[si]
dst.Pix[di+0] = c
dst.Pix[di+1] = c
dst.Pix[di+2] = c
dst.Pix[di+3] = 0xff
di += 4
si += 2
}
}
})
case *image.YCbCr:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
for dstX := 0; dstX < dstW; dstX++ {
srcX := srcMinX + dstX
srcY := srcMinY + dstY
siy := src.YOffset(srcX, srcY)
sic := src.COffset(srcX, srcY)
r, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic])
dst.Pix[di+0] = r
dst.Pix[di+1] = g
dst.Pix[di+2] = b
dst.Pix[di+3] = 0xff
di += 4
}
}
})
case *image.Paletted:
plen := len(src.Palette)
pnew := make([]color.NRGBA, plen)
for i := 0; i < plen; i++ {
pnew[i] = color.NRGBAModel.Convert(src.Palette[i]).(color.NRGBA)
}
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := pnew[src.Pix[si]]
dst.Pix[di+0] = c.R
dst.Pix[di+1] = c.G
dst.Pix[di+2] = c.B
dst.Pix[di+3] = c.A
di += 4
si++
}
}
})
default:
parallel(dstH, func(partStart, partEnd int) {
for dstY := partStart; dstY < partEnd; dstY++ {
di := dst.PixOffset(0, dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)
dst.Pix[di+0] = c.R
dst.Pix[di+1] = c.G
dst.Pix[di+2] = c.B
dst.Pix[di+3] = c.A
di += 4
}
}
})
}
return dst
}
// toNRGBA converts any image type to *image.NRGBA with min-point at (0, 0).
func toNRGBA(img image.Image) *image.NRGBA {
srcBounds := img.Bounds()
if srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 {
if src0, ok := img.(*image.NRGBA); ok {
return src0
}
}
return Clone(img)
}
// AdjustFunc performs a gamma correction on the image and returns the adjusted image.
func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGBA {
src := toNRGBA(img)
width := src.Bounds().Max.X
height := src.Bounds().Max.Y
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
parallel(height, func(partStart, partEnd int) {
for y := partStart; y < partEnd; y++ {
for x := 0; x < width; x++ {
i := y*src.Stride + x*4
j := y*dst.Stride + x*4
r := src.Pix[i+0]
g := src.Pix[i+1]
b := src.Pix[i+2]
a := src.Pix[i+3]
c := fn(color.NRGBA{r, g, b, a})
dst.Pix[j+0] = c.R
dst.Pix[j+1] = c.G
dst.Pix[j+2] = c.B
dst.Pix[j+3] = c.A
}
}
})
return dst
}
// AdjustGamma performs a gamma correction on the image and returns the adjusted image.
// Gamma parameter must be positive. Gamma = 1.0 gives the original image.
// Gamma less than 1.0 darkens the image and gamma greater than 1.0 lightens it.
//
// Example:
//
// dstImage = imaging.AdjustGamma(srcImage, 0.7)
//
func AdjustGamma(img image.Image, gamma float64) *image.NRGBA {
e := 1.0 / math.Max(gamma, 0.0001)
lut := make([]uint8, 256)
for i := 0; i < 256; i++ {
lut[i] = clamp(math.Pow(float64(i)/255.0, e) * 255.0)
}
fn := func(c color.NRGBA) color.NRGBA {
return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
}
return AdjustFunc(img, fn)
}
func sigmoid(a, b, x float64) float64 {
return 1 / (1 + math.Exp(b*(a-x)))
}
// AdjustSigmoid changes the contrast of the image using a sigmoidal function and returns the adjusted image.
// It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail.
// The midpoint parameter is the midpoint of contrast that must be between 0 and 1, typically 0.5.
// The factor parameter indicates how much to increase or decrease the contrast, typically in range (-10, 10).
// If the factor parameter is positive the image contrast is increased otherwise the contrast is decreased.
//
// Examples:
//
// dstImage = imaging.AdjustSigmoid(srcImage, 0.5, 3.0) // increase the contrast
// dstImage = imaging.AdjustSigmoid(srcImage, 0.5, -3.0) // decrease the contrast
//
func AdjustSigmoid(img image.Image, midpoint, factor float64) *image.NRGBA {
if factor == 0 {
return Clone(img)
}
lut := make([]uint8, 256)
a := math.Min(math.Max(midpoint, 0.0), 1.0)
b := math.Abs(factor)
sig0 := sigmoid(a, b, 0)
sig1 := sigmoid(a, b, 1)
e := 1.0e-6
if factor > 0 {
for i := 0; i < 256; i++ {
x := float64(i) / 255.0
sigX := sigmoid(a, b, x)
f := (sigX - sig0) / (sig1 - sig0)
lut[i] = clamp(f * 255.0)
}
} else {
for i := 0; i < 256; i++ {
x := float64(i) / 255.0
arg := math.Min(math.Max((sig1-sig0)*x+sig0, e), 1.0-e)
f := a - math.Log(1.0/arg-1.0)/b
lut[i] = clamp(f * 255.0)
}
}
fn := func(c color.NRGBA) color.NRGBA {
return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
}
return AdjustFunc(img, fn)
}
// AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
// The percentage = -100 gives solid grey image.
//
// Examples:
//
// dstImage = imaging.AdjustContrast(srcImage, -10) // decrease image contrast by 10%
// dstImage = imaging.AdjustContrast(srcImage, 20) // increase image contrast by 20%
//
func AdjustContrast(img image.Image, percentage float64) *image.NRGBA {
percentage = math.Min(math.Max(percentage, -100.0), 100.0)
lut := make([]uint8, 256)
v := (100.0 + percentage) / 100.0
for i := 0; i < 256; i++ {
if 0 <= v && v <= 1 {
lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*v) * 255.0)
} else if 1 < v && v < 2 {
lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*(1/(2.0-v))) * 255.0)
} else {
lut[i] = uint8(float64(i)/255.0+0.5) * 255
}
}
fn := func(c color.NRGBA) color.NRGBA {
return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
}
return AdjustFunc(img, fn)
}
// AdjustBrightness changes the brightness of the image using the percentage parameter and returns the adjusted image.
// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
// The percentage = -100 gives solid black image. The percentage = 100 gives solid white image.
//
// Examples:
//
// dstImage = imaging.AdjustBrightness(srcImage, -15) // decrease image brightness by 15%
// dstImage = imaging.AdjustBrightness(srcImage, 10) // increase image brightness by 10%
//
func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA {
percentage = math.Min(math.Max(percentage, -100.0), 100.0)
lut := make([]uint8, 256)
shift := 255.0 * percentage / 100.0
for i := 0; i < 256; i++ {
lut[i] = clamp(float64(i) + shift)
}
fn := func(c color.NRGBA) color.NRGBA {
return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
}
return AdjustFunc(img, fn)
}
// Grayscale produces grayscale version of the image.
func Grayscale(img image.Image) *image.NRGBA {
fn := func(c color.NRGBA) color.NRGBA {
f := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
y := uint8(f + 0.5)
return color.NRGBA{y, y, y, c.A}
}
return AdjustFunc(img, fn)
}
// Invert produces inverted (negated) version of the image.
func Invert(img image.Image) *image.NRGBA {
fn := func(c color.NRGBA) color.NRGBA {
return color.NRGBA{255 - c.R, 255 - c.G, 255 - c.B, c.A}
}
return AdjustFunc(img, fn)
}
// parallel starts parallel image processing based on the current GOMAXPROCS value.
// If GOMAXPROCS = 1 it uses no parallelization.
// If GOMAXPROCS > 1 it spawns N=GOMAXPROCS workers in separate goroutines.
func parallel(dataSize int, fn func(partStart, partEnd int)) {
numGoroutines := 1
partSize := dataSize
numProcs := runtime.GOMAXPROCS(0)
if numProcs > 1 {
numGoroutines = numProcs
partSize = dataSize / (numGoroutines * 10)
if partSize < 1 {
partSize = 1
}
}
if numGoroutines == 1 {
fn(0, dataSize)
} else {
var wg sync.WaitGroup
wg.Add(numGoroutines)
idx := uint64(0)
for p := 0; p < numGoroutines; p++ {
go func() {
defer wg.Done()
for {
partStart := int(atomic.AddUint64(&idx, uint64(partSize))) - partSize
if partStart >= dataSize {
break
}
partEnd := partStart + partSize
if partEnd > dataSize {
partEnd = dataSize
}
fn(partStart, partEnd)
}
}()
}
wg.Wait()
}
}
// absint returns the absolute value of i.
func absint(i int) int {
if i < 0 {
return -i
}
return i
}
// clamp rounds and clamps float64 value to fit into uint8.
func clamp(x float64) uint8 {
v := int64(x + 0.5)
if v > 255 {
return 255
}
if v > 0 {
return uint8(v)
}
return 0
}

View File

@ -0,0 +1,290 @@
package drawimg
import (
"image"
"image/color"
"image/jpeg"
"io"
"testing"
"github.com/bouk/monkey"
"github.com/smartystreets/goconvey/convey"
)
func TestDrawimgString(t *testing.T) {
convey.Convey("String", t, func(ctx convey.C) {
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := JPEG.String()
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgOpen(t *testing.T) {
convey.Convey("Open", t, func(ctx convey.C) {
var (
filename = ""
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1, err := Open(filename)
ctx.Convey("Then err should be nil.p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldBeNil)
ctx.So(err, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgEncode(t *testing.T) {
convey.Convey("Encode", t, func(ctx convey.C) {
var (
w io.Writer
img = image.NewRGBA(imgRectangle)
format = JPEG
)
monkeyJpegEncode()
ctx.Convey("When everything gose positive", func(ctx convey.C) {
err := Encode(w, img, format)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestDrawimgSave(t *testing.T) {
convey.Convey("Save", t, func(ctx convey.C) {
var (
img image.Image
filename = ""
)
ctx.Convey("When everything goes not positive", func(ctx convey.C) {
err := Save(img, filename)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgNewNRGBA(t *testing.T) {
convey.Convey("NewNRGBA", t, func(ctx convey.C) {
var (
width = int(0)
height = int(0)
fillColor color.Color
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := NewNRGBA(width, height, fillColor)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgClone(t *testing.T) {
convey.Convey("Clone", t, func(ctx convey.C) {
var (
rgba = image.NewRGBA(imgRectangle)
rgba64 = image.NewNRGBA64(imgRectangle)
)
ctx.Convey("RGBA", func(ctx convey.C) {
p1 := Clone(rgba)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
ctx.Convey("RGBA64", func(ctx convey.C) {
p1 := Clone(rgba64)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgtoNRGBA(t *testing.T) {
convey.Convey("toNRGBA", t, func(ctx convey.C) {
var (
img = image.NewRGBA64(imgRectangle)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := toNRGBA(img)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgAdjustFunc(t *testing.T) {
convey.Convey("AdjustFunc", t, func(ctx convey.C) {
var (
img = image.NewRGBA64(imgRectangle)
fn = func(c color.NRGBA) color.NRGBA { return c }
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := AdjustFunc(img, fn)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgAdjustGamma(t *testing.T) {
convey.Convey("AdjustGamma", t, func(ctx convey.C) {
var (
img = image.NewRGBA64(imgRectangle)
gamma = float64(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := AdjustGamma(img, gamma)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgsigmoid(t *testing.T) {
convey.Convey("sigmoid", t, func(ctx convey.C) {
var (
a = float64(0)
b = float64(0)
x = float64(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := sigmoid(a, b, x)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgAdjustSigmoid(t *testing.T) {
convey.Convey("AdjustSigmoid", t, func(ctx convey.C) {
var (
img = image.NewRGBA64(imgRectangle)
midpoint = float64(0)
factor = float64(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := AdjustSigmoid(img, midpoint, factor)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgAdjustContrast(t *testing.T) {
convey.Convey("AdjustContrast", t, func(ctx convey.C) {
var (
img = image.NewRGBA64(imgRectangle)
percentage = float64(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := AdjustContrast(img, percentage)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgAdjustBrightness(t *testing.T) {
convey.Convey("AdjustBrightness", t, func(ctx convey.C) {
var (
img = image.NewRGBA64(imgRectangle)
percentage = float64(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := AdjustBrightness(img, percentage)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgGrayscale(t *testing.T) {
convey.Convey("Grayscale", t, func(ctx convey.C) {
var (
img = image.NewRGBA64(imgRectangle)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := Grayscale(img)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgInvert(t *testing.T) {
convey.Convey("Invert", t, func(ctx convey.C) {
var (
img = image.NewRGBA64(imgRectangle)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := Invert(img)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgparallel(t *testing.T) {
convey.Convey("parallel", t, func(ctx convey.C) {
var (
dataSize = int(0)
fn func(partStart int, partEnd int)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
parallel(dataSize, fn)
ctx.Convey("No return values", func(ctx convey.C) {
})
})
})
}
func TestDrawimgabsint(t *testing.T) {
convey.Convey("absint", t, func(ctx convey.C) {
var (
i = int(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := absint(i)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func TestDrawimgclamp(t *testing.T) {
convey.Convey("clamp", t, func(ctx convey.C) {
var (
x = float64(0)
)
ctx.Convey("When everything gose positive", func(ctx convey.C) {
p1 := clamp(x)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldNotBeNil)
})
})
})
}
func monkeyJpegEncode() {
monkey.Patch(jpeg.Encode, func(_ io.Writer, _ image.Image, _ *jpeg.Options) error {
return nil
})
}