mirror of
https://github.com/unidoc/unipdf.git
synced 2025-05-01 22:17:29 +08:00

* Add render package * Add text state * Add more text operators * Remove unnecessary files * Add text font * Add custom text render method * Improve text rendering method * Rename text state methods * Refactor and document context interface * Refact text begin/end operators * Fix graphics state transformations * Keep original font when doing font substitution * Take page cropbox into account * Revert to substitution font if original font measurement is 0 * Add font substitution package * Implement addition transform.Point methods * Use transform.Point in the image context package * Remove unneeded functionality from the render image package * Fix golint notices in the image rendering package * Fix go vet notices in the render package * Fix golint notices in the top-level render package * Improve render context package documentation * Document context text state struct. * Document context text font struct. * Minor logging improvements * Add license disclaimer to the render package files * Avoid using package aliases where possible * Change style of section comments * Adapt render package import style to follow the developer guide * Improve documentation for the internal matrix implementation * Update render package dependency versions * Apply crop box post render * Account for offseted media boxes * Improve metrics of rendered characters * Fix text matrix translation * Change priority of fonts used for measuring rendered characters * Skip invalid m and l operators on image rendering * Small fix for v operator * Fix rendered characters spacing issues * Refactor naming of internal render packages
835 lines
23 KiB
Go
835 lines
23 KiB
Go
/*
|
|
* This file is subject to the terms and conditions defined in
|
|
* file 'LICENSE.md', which is part of this source code package.
|
|
*/
|
|
|
|
package imagerender
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
|
|
"github.com/golang/freetype/raster"
|
|
"golang.org/x/image/draw"
|
|
"golang.org/x/image/font"
|
|
"golang.org/x/image/math/f64"
|
|
|
|
"github.com/unidoc/unipdf/v3/internal/transform"
|
|
"github.com/unidoc/unipdf/v3/render/internal/context"
|
|
)
|
|
|
|
var (
|
|
defaultFillStyle = newSolidPattern(color.White)
|
|
defaultStrokeStyle = newSolidPattern(color.Black)
|
|
)
|
|
|
|
// Context represents an image rendering context.
|
|
type Context struct {
|
|
width int
|
|
height int
|
|
rasterizer *raster.Rasterizer
|
|
im *image.RGBA
|
|
mask *image.Alpha
|
|
color color.Color
|
|
fillPattern context.Pattern
|
|
strokePattern context.Pattern
|
|
strokePath raster.Path
|
|
fillPath raster.Path
|
|
start transform.Point
|
|
current transform.Point
|
|
hasCurrent bool
|
|
dashes []float64
|
|
dashOffset float64
|
|
lineWidth float64
|
|
lineCap context.LineCap
|
|
lineJoin context.LineJoin
|
|
fillRule context.FillRule
|
|
matrix transform.Matrix
|
|
textState *context.TextState
|
|
stack []*Context
|
|
}
|
|
|
|
// NewContext creates a new image.RGBA with the specified width and height
|
|
// and prepares a context for rendering onto that image.
|
|
func NewContext(width, height int) *Context {
|
|
return NewContextForRGBA(image.NewRGBA(image.Rect(0, 0, width, height)))
|
|
}
|
|
|
|
// NewContextForImage copies the specified image into a new image.RGBA
|
|
// and prepares a context for rendering onto that image.
|
|
func NewContextForImage(im image.Image) *Context {
|
|
return NewContextForRGBA(imageToRGBA(im))
|
|
}
|
|
|
|
// 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: w,
|
|
height: h,
|
|
rasterizer: raster.NewRasterizer(w, h),
|
|
im: im,
|
|
color: color.Transparent,
|
|
fillPattern: defaultFillStyle,
|
|
strokePattern: defaultStrokeStyle,
|
|
lineWidth: 1,
|
|
fillRule: context.FillRuleWinding,
|
|
matrix: transform.IdentityMatrix(),
|
|
textState: context.NewTextState(),
|
|
}
|
|
}
|
|
|
|
// Image returns the image that has been drawn by this context.
|
|
func (dc *Context) Image() image.Image {
|
|
return dc.im
|
|
}
|
|
|
|
// Width returns the width of the image in pixels.
|
|
func (dc *Context) Width() int {
|
|
return dc.width
|
|
}
|
|
|
|
// Height returns the height of the image in pixels.
|
|
func (dc *Context) Height() int {
|
|
return dc.height
|
|
}
|
|
|
|
// 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.
|
|
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
|
|
}
|
|
|
|
// LineWidth returns the line width of the context.
|
|
func (dc *Context) LineWidth() float64 {
|
|
return dc.lineWidth
|
|
}
|
|
|
|
// SetLineWidth sets the line width of the context.
|
|
func (dc *Context) SetLineWidth(lineWidth float64) {
|
|
dc.lineWidth = lineWidth
|
|
}
|
|
|
|
// SetLineCap sets the line cap style.
|
|
func (dc *Context) SetLineCap(lineCap context.LineCap) {
|
|
dc.lineCap = lineCap
|
|
}
|
|
|
|
// SetLineJoin sets the line join style.
|
|
func (dc *Context) SetLineJoin(lineJoin context.LineJoin) {
|
|
dc.lineJoin = lineJoin
|
|
}
|
|
|
|
// SetFillRule sets the fill rule.
|
|
func (dc *Context) SetFillRule(fillRule context.FillRule) {
|
|
dc.fillRule = fillRule
|
|
}
|
|
|
|
//
|
|
// Color setters
|
|
//
|
|
|
|
func (dc *Context) setFillAndStrokeColor(c color.Color) {
|
|
dc.color = c
|
|
dc.fillPattern = newSolidPattern(c)
|
|
dc.strokePattern = newSolidPattern(c)
|
|
}
|
|
|
|
// SetFillStyle sets current fill style
|
|
func (dc *Context) SetFillStyle(pattern context.Pattern) {
|
|
// if pattern is SolidPattern, also change dc.color(for dc.Clear, dc.drawString)
|
|
if fillStyle, ok := pattern.(*solidPattern); ok {
|
|
dc.color = fillStyle.color
|
|
}
|
|
dc.fillPattern = pattern
|
|
}
|
|
|
|
// SetStrokeStyle sets current stroke style
|
|
func (dc *Context) SetStrokeStyle(pattern context.Pattern) {
|
|
dc.strokePattern = pattern
|
|
}
|
|
|
|
// SetColor sets the current color(for both fill and stroke).
|
|
func (dc *Context) SetColor(c color.Color) {
|
|
dc.setFillAndStrokeColor(c)
|
|
}
|
|
|
|
// SetStrokeRGBA sets the current color for stroking operations.
|
|
// r, g, b, a values must be in range 0-1.
|
|
func (dc *Context) SetStrokeRGBA(r, g, b, a float64) {
|
|
color := color.NRGBA{
|
|
uint8(r * 255),
|
|
uint8(g * 255),
|
|
uint8(b * 255),
|
|
uint8(a * 255),
|
|
}
|
|
dc.strokePattern = newSolidPattern(color)
|
|
}
|
|
|
|
// SetFillRGBA sets the current color for fill operations.
|
|
// r, g, b, a values must be in range 0-1.
|
|
func (dc *Context) SetFillRGBA(r, g, b, a float64) {
|
|
color := color.NRGBA{
|
|
uint8(r * 255),
|
|
uint8(g * 255),
|
|
uint8(b * 255),
|
|
uint8(a * 255),
|
|
}
|
|
dc.color = color
|
|
dc.fillPattern = newSolidPattern(color)
|
|
}
|
|
|
|
// SetHexColor sets the current color using a hex string. The leading pound
|
|
// sign (#) is optional. Both 3- and 6-digit variations are supported. 8 digits
|
|
// may be provided to set the alpha value as well.
|
|
func (dc *Context) SetHexColor(x string) {
|
|
r, g, b, a := parseHexColor(x)
|
|
dc.SetRGBA255(r, g, b, a)
|
|
}
|
|
|
|
// SetRGBA255 sets the current color. r, g, b, a values should be between 0 and
|
|
// 255, inclusive.
|
|
func (dc *Context) SetRGBA255(r, g, b, a int) {
|
|
dc.color = color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)}
|
|
dc.setFillAndStrokeColor(dc.color)
|
|
}
|
|
|
|
// SetRGB255 sets the current color. r, g, b values should be between 0 and 255,
|
|
// inclusive. Alpha will be set to 255 (fully opaque).
|
|
func (dc *Context) SetRGB255(r, g, b int) {
|
|
dc.SetRGBA255(r, g, b, 255)
|
|
}
|
|
|
|
// SetRGBA sets the current color. r, g, b, a values should be between 0 and 1,
|
|
// inclusive.
|
|
func (dc *Context) SetRGBA(r, g, b, a float64) {
|
|
dc.color = color.NRGBA{
|
|
uint8(r * 255),
|
|
uint8(g * 255),
|
|
uint8(b * 255),
|
|
uint8(a * 255),
|
|
}
|
|
dc.setFillAndStrokeColor(dc.color)
|
|
}
|
|
|
|
// SetRGB sets the current color. r, g, b values should be between 0 and 1,
|
|
// inclusive. Alpha will be set to 1 (fully opaque).
|
|
func (dc *Context) SetRGB(r, g, b float64) {
|
|
dc.SetRGBA(r, g, b, 1)
|
|
}
|
|
|
|
//
|
|
// Path manipulation
|
|
//
|
|
|
|
// MoveTo starts a new subpath within the current path starting at the
|
|
// specified point.
|
|
func (dc *Context) MoveTo(x, y float64) {
|
|
if dc.hasCurrent {
|
|
dc.fillPath.Add1(fixedPoint(dc.start))
|
|
}
|
|
|
|
x, y = dc.Transform(x, y)
|
|
p := transform.NewPoint(x, y)
|
|
fp := fixedPoint(p)
|
|
|
|
dc.strokePath.Start(fp)
|
|
dc.fillPath.Start(fp)
|
|
dc.start = p
|
|
dc.current = p
|
|
dc.hasCurrent = true
|
|
}
|
|
|
|
// LineTo adds a line segment to the current path starting at the current
|
|
// point. If there is no current point, it is equivalent to MoveTo(x, y)
|
|
func (dc *Context) LineTo(x, y float64) {
|
|
if !dc.hasCurrent {
|
|
dc.MoveTo(x, y)
|
|
} else {
|
|
x, y = dc.Transform(x, y)
|
|
p := transform.NewPoint(x, y)
|
|
fp := fixedPoint(p)
|
|
|
|
dc.strokePath.Add1(fp)
|
|
dc.fillPath.Add1(fp)
|
|
dc.current = p
|
|
}
|
|
}
|
|
|
|
// QuadraticTo adds a quadratic bezier curve to the current path starting at
|
|
// the current point. If there is no current point, it first performs
|
|
// MoveTo(x1, y1)
|
|
func (dc *Context) QuadraticTo(x1, y1, x2, y2 float64) {
|
|
if !dc.hasCurrent {
|
|
dc.MoveTo(x1, y1)
|
|
}
|
|
|
|
x1, y1 = dc.Transform(x1, y1)
|
|
x2, y2 = dc.Transform(x2, y2)
|
|
p1 := transform.NewPoint(x1, y1)
|
|
p2 := transform.NewPoint(x2, y2)
|
|
fp1 := fixedPoint(p1)
|
|
fp2 := fixedPoint(p2)
|
|
|
|
dc.strokePath.Add2(fp1, fp2)
|
|
dc.fillPath.Add2(fp1, fp2)
|
|
dc.current = p2
|
|
}
|
|
|
|
// CubicTo adds a cubic bezier curve to the current path starting at the
|
|
// current point. If there is no current point, it first performs
|
|
// MoveTo(x1, y1). Because freetype/raster does not support cubic beziers,
|
|
// this is emulated with many small line segments.
|
|
func (dc *Context) CubicTo(x1, y1, x2, y2, x3, y3 float64) {
|
|
if !dc.hasCurrent {
|
|
dc.MoveTo(x1, y1)
|
|
}
|
|
x0, y0 := dc.current.X, dc.current.Y
|
|
x1, y1 = dc.Transform(x1, y1)
|
|
x2, y2 = dc.Transform(x2, y2)
|
|
x3, y3 = dc.Transform(x3, y3)
|
|
points := cubicBezier(x0, y0, x1, y1, x2, y2, x3, y3)
|
|
previous := fixedPoint(dc.current)
|
|
for _, p := range points[1:] {
|
|
f := fixedPoint(p)
|
|
if f == previous {
|
|
// TODO: this fixes some rendering issues but not all
|
|
continue
|
|
}
|
|
previous = f
|
|
dc.strokePath.Add1(f)
|
|
dc.fillPath.Add1(f)
|
|
dc.current = p
|
|
}
|
|
}
|
|
|
|
// ClosePath adds a line segment from the current point to the beginning
|
|
// of the current subpath. If there is no current point, this is a no-op.
|
|
func (dc *Context) ClosePath() {
|
|
if dc.hasCurrent {
|
|
fp := fixedPoint(dc.start)
|
|
dc.strokePath.Add1(fp)
|
|
dc.fillPath.Add1(fp)
|
|
dc.current = dc.start
|
|
}
|
|
}
|
|
|
|
// ClearPath clears the current path. There is no current point after this
|
|
// operation.
|
|
func (dc *Context) ClearPath() {
|
|
dc.strokePath.Clear()
|
|
dc.fillPath.Clear()
|
|
dc.hasCurrent = false
|
|
}
|
|
|
|
// NewSubPath starts a new subpath within the current path. There is no current
|
|
// point after this operation.
|
|
func (dc *Context) NewSubPath() {
|
|
if dc.hasCurrent {
|
|
dc.fillPath.Add1(fixedPoint(dc.start))
|
|
}
|
|
dc.hasCurrent = false
|
|
}
|
|
|
|
//
|
|
// Path drawing
|
|
//
|
|
|
|
func (dc *Context) capper() raster.Capper {
|
|
switch dc.lineCap {
|
|
case context.LineCapButt:
|
|
return raster.ButtCapper
|
|
case context.LineCapRound:
|
|
return raster.RoundCapper
|
|
case context.LineCapSquare:
|
|
return raster.SquareCapper
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (dc *Context) joiner() raster.Joiner {
|
|
switch dc.lineJoin {
|
|
case context.LineJoinBevel:
|
|
return raster.BevelJoiner
|
|
case context.LineJoinRound:
|
|
return raster.RoundJoiner
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (dc *Context) stroke(painter raster.Painter) {
|
|
path := dc.strokePath
|
|
if len(dc.dashes) > 0 {
|
|
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 := dc.rasterizer
|
|
r.UseNonZeroWinding = true
|
|
r.Clear()
|
|
r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner())
|
|
r.Rasterize(painter)
|
|
}
|
|
|
|
func (dc *Context) fill(painter raster.Painter) {
|
|
path := dc.fillPath
|
|
if dc.hasCurrent {
|
|
path = make(raster.Path, len(dc.fillPath))
|
|
copy(path, dc.fillPath)
|
|
path.Add1(fixedPoint(dc.start))
|
|
}
|
|
r := dc.rasterizer
|
|
r.UseNonZeroWinding = dc.fillRule == context.FillRuleWinding
|
|
r.Clear()
|
|
r.AddPath(path)
|
|
r.Rasterize(painter)
|
|
}
|
|
|
|
// StrokePreserve strokes the current path with the current color, line width,
|
|
// line cap, line join and dash settings. The path is preserved after this
|
|
// operation.
|
|
func (dc *Context) StrokePreserve() {
|
|
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)
|
|
}
|
|
|
|
// Stroke strokes the current path with the current color, line width,
|
|
// line cap, line join and dash settings. The path is cleared after this
|
|
// operation.
|
|
func (dc *Context) Stroke() {
|
|
dc.StrokePreserve()
|
|
dc.ClearPath()
|
|
}
|
|
|
|
// 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() {
|
|
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)
|
|
}
|
|
|
|
// Fill fills the current path with the current color. Open subpaths
|
|
// are implicity closed. The path is cleared after this operation.
|
|
func (dc *Context) Fill() {
|
|
dc.FillPreserve()
|
|
dc.ClearPath()
|
|
}
|
|
|
|
// ClipPreserve 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 preserved after this operation.
|
|
func (dc *Context) ClipPreserve() {
|
|
clip := image.NewAlpha(image.Rect(0, 0, dc.width, dc.height))
|
|
painter := raster.NewAlphaOverPainter(clip)
|
|
dc.fill(painter)
|
|
if dc.mask == nil {
|
|
dc.mask = clip
|
|
} else {
|
|
mask := image.NewAlpha(image.Rect(0, 0, dc.width, dc.height))
|
|
draw.DrawMask(mask, mask.Bounds(), clip, image.ZP, dc.mask, image.ZP, draw.Over)
|
|
dc.mask = mask
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
func (dc *Context) Clip() {
|
|
dc.ClipPreserve()
|
|
dc.ClearPath()
|
|
}
|
|
|
|
// ResetClip clears the clipping region.
|
|
func (dc *Context) ResetClip() {
|
|
dc.mask = nil
|
|
}
|
|
|
|
//
|
|
// Drawing operations
|
|
//
|
|
|
|
// Clear fills the entire image with the current color.
|
|
func (dc *Context) Clear() {
|
|
src := image.NewUniform(dc.color)
|
|
draw.Draw(dc.im, dc.im.Bounds(), src, image.ZP, draw.Src)
|
|
}
|
|
|
|
// SetPixel sets the color of the specified pixel using the current color.
|
|
func (dc *Context) SetPixel(x, y int) {
|
|
dc.im.Set(x, y, dc.color)
|
|
}
|
|
|
|
// DrawPoint is like DrawCircle but ensures that a circle of the specified
|
|
// size is drawn regardless of the current transformation matrix. The position
|
|
// is still transformed, but not the shape of the point.
|
|
func (dc *Context) DrawPoint(x, y, r float64) {
|
|
dc.Push()
|
|
tx, ty := dc.Transform(x, y)
|
|
dc.Identity()
|
|
dc.DrawCircle(tx, ty, r)
|
|
dc.Pop()
|
|
}
|
|
|
|
// DrawLine draws the line described by points x1,y1 and x2,y2.
|
|
func (dc *Context) DrawLine(x1, y1, x2, y2 float64) {
|
|
dc.MoveTo(x1, y1)
|
|
dc.LineTo(x2, y2)
|
|
}
|
|
|
|
// DrawRectangle draws a rectangle of size w,h at position x,y.
|
|
func (dc *Context) DrawRectangle(x, y, w, h float64) {
|
|
dc.NewSubPath()
|
|
dc.MoveTo(x, y)
|
|
dc.LineTo(x+w, y)
|
|
dc.LineTo(x+w, y+h)
|
|
dc.LineTo(x, y+h)
|
|
dc.ClosePath()
|
|
}
|
|
|
|
// DrawRoundedRectangle draws a rounded rectangle of size w,h at position x,y.
|
|
func (dc *Context) DrawRoundedRectangle(x, y, w, h, r float64) {
|
|
x0, x1, x2, x3 := x, x+r, x+w-r, x+w
|
|
y0, y1, y2, y3 := y, y+r, y+h-r, y+h
|
|
dc.NewSubPath()
|
|
dc.MoveTo(x1, y0)
|
|
dc.LineTo(x2, y0)
|
|
dc.DrawArc(x2, y1, r, degreesToRadians(270), degreesToRadians(360))
|
|
dc.LineTo(x3, y2)
|
|
dc.DrawArc(x2, y2, r, degreesToRadians(0), degreesToRadians(90))
|
|
dc.LineTo(x1, y3)
|
|
dc.DrawArc(x1, y2, r, degreesToRadians(90), degreesToRadians(180))
|
|
dc.LineTo(x0, y1)
|
|
dc.DrawArc(x1, y1, r, degreesToRadians(180), degreesToRadians(270))
|
|
dc.ClosePath()
|
|
}
|
|
|
|
// DrawArc draws an arc described by r, angle1, angle2 at position x,y.
|
|
func (dc *Context) DrawArc(x, y, r, angle1, angle2 float64) {
|
|
dc.DrawEllipticalArc(x, y, r, r, angle1, angle2)
|
|
}
|
|
|
|
// DrawEllipticalArc draws an elliptical arc described by r, angle1, angle2 at
|
|
// position x,y.
|
|
func (dc *Context) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) {
|
|
const n = 16
|
|
for i := 0; i < n; i++ {
|
|
p1 := float64(i+0) / n
|
|
p2 := float64(i+1) / n
|
|
a1 := angle1 + (angle2-angle1)*p1
|
|
a2 := angle1 + (angle2-angle1)*p2
|
|
x0 := x + rx*math.Cos(a1)
|
|
y0 := y + ry*math.Sin(a1)
|
|
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 {
|
|
if dc.hasCurrent {
|
|
dc.LineTo(x0, y0)
|
|
} else {
|
|
dc.MoveTo(x0, y0)
|
|
}
|
|
}
|
|
dc.QuadraticTo(cx, cy, x2, y2)
|
|
}
|
|
}
|
|
|
|
// DrawEllipse draws an ellipse of size rx,ry at position x,y.
|
|
func (dc *Context) DrawEllipse(x, y, rx, ry float64) {
|
|
dc.NewSubPath()
|
|
dc.DrawEllipticalArc(x, y, rx, ry, 0, 2*math.Pi)
|
|
dc.ClosePath()
|
|
}
|
|
|
|
// DrawCircle draws a circle of radius r at position x,y.
|
|
func (dc *Context) DrawCircle(x, y, r float64) {
|
|
dc.NewSubPath()
|
|
dc.DrawEllipticalArc(x, y, r, r, 0, 2*math.Pi)
|
|
dc.ClosePath()
|
|
}
|
|
|
|
func (dc *Context) drawRegularPolygon(n int, x, y, r, rotation float64) {
|
|
angle := 2 * math.Pi / float64(n)
|
|
rotation -= math.Pi / 2
|
|
if n%2 == 0 {
|
|
rotation += angle / 2
|
|
}
|
|
dc.NewSubPath()
|
|
for i := 0; i < n; i++ {
|
|
a := rotation + angle*float64(i)
|
|
dc.LineTo(x+r*math.Cos(a), y+r*math.Sin(a))
|
|
}
|
|
dc.ClosePath()
|
|
}
|
|
|
|
// DrawImage draws the specified image at the specified point.
|
|
func (dc *Context) DrawImage(im image.Image, x, y int) {
|
|
dc.DrawImageAnchored(im, x, y, 0, 0)
|
|
}
|
|
|
|
// DrawImageAnchored draws the specified image at the specified anchor point.
|
|
// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the
|
|
// image. Use ax=0.5, ay=0.5 to center the image at the specified point.
|
|
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))
|
|
transformer := draw.BiLinear
|
|
m := dc.matrix.Clone()
|
|
m.Translate(float64(x), float64(y))
|
|
s2d := f64.Aff3{m[0], m[3], m[6], m[1], m[4], m[7]}
|
|
if dc.mask == nil {
|
|
transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, nil)
|
|
} else {
|
|
transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, &draw.Options{
|
|
DstMask: dc.mask,
|
|
DstMaskP: image.ZP,
|
|
})
|
|
}
|
|
}
|
|
|
|
//
|
|
// Text operations
|
|
//
|
|
|
|
// TextState returns the current text state.
|
|
func (dc *Context) TextState() *context.TextState {
|
|
return dc.textState
|
|
}
|
|
|
|
func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
|
|
d := &font.Drawer{
|
|
Dst: im,
|
|
Src: image.NewUniform(dc.color),
|
|
Face: dc.textState.Tf.Face,
|
|
Dot: fixedPoint(transform.NewPoint(x, y)),
|
|
}
|
|
// 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
|
|
m := dc.matrix.Clone()
|
|
m.Translate(float64(dr.Min.X), float64(dr.Min.Y))
|
|
s2d := f64.Aff3{m[0], m[3], m[6], m[1], m[4], m[7]}
|
|
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.
|
|
func (dc *Context) DrawString(s string, x, y float64) {
|
|
dc.DrawStringAnchored(s, x, y, 0, 0)
|
|
}
|
|
|
|
// DrawStringAnchored draws the specified text at the specified anchor point.
|
|
// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the
|
|
// 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 -= ax * w
|
|
y += ay * h
|
|
if dc.mask == nil {
|
|
dc.drawString(dc.im, s, x, y)
|
|
} else {
|
|
im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height))
|
|
dc.drawString(im, s, x, y)
|
|
draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over)
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
d := &font.Drawer{
|
|
Face: dc.textState.Tf.Face,
|
|
}
|
|
a := d.MeasureString(s)
|
|
return float64(a >> 6), dc.textState.Tf.Size
|
|
}
|
|
|
|
//
|
|
// Transformation matrix operations
|
|
//
|
|
|
|
// Matrix returns the current transformation matrix.
|
|
func (dc *Context) Matrix() transform.Matrix {
|
|
return dc.matrix
|
|
}
|
|
|
|
// SetMatrix modifies the transformation matrix.
|
|
func (dc *Context) SetMatrix(m transform.Matrix) {
|
|
dc.matrix = m
|
|
}
|
|
|
|
// Identity resets the current transformation matrix to the identity matrix.
|
|
// This results in no translating, scaling, rotating, or shearing.
|
|
func (dc *Context) Identity() {
|
|
dc.matrix = transform.IdentityMatrix()
|
|
}
|
|
|
|
// Translate updates the current matrix with a translation.
|
|
func (dc *Context) Translate(x, y float64) {
|
|
dc.matrix.Translate(x, y)
|
|
}
|
|
|
|
// Scale updates the current matrix with a scaling factor.
|
|
// Scaling occurs about the origin.
|
|
func (dc *Context) Scale(x, y float64) {
|
|
dc.matrix.Scale(x, y)
|
|
}
|
|
|
|
// ScaleAbout updates the current matrix with a scaling factor.
|
|
// Scaling occurs about the specified point.
|
|
func (dc *Context) ScaleAbout(sx, sy, x, y float64) {
|
|
dc.Translate(x, y)
|
|
dc.Scale(sx, sy)
|
|
dc.Translate(-x, -y)
|
|
}
|
|
|
|
// 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.Rotate(angle)
|
|
}
|
|
|
|
// 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)
|
|
dc.Rotate(angle)
|
|
dc.Translate(-x, -y)
|
|
}
|
|
|
|
// Shear updates the current matrix with a shearing angle.
|
|
// Shearing occurs about the origin.
|
|
func (dc *Context) Shear(x, y float64) {
|
|
dc.matrix.Shear(x, y)
|
|
}
|
|
|
|
// ShearAbout updates the current matrix with a shearing angle.
|
|
// Shearing occurs about the specified point.
|
|
func (dc *Context) ShearAbout(sx, sy, x, y float64) {
|
|
dc.Translate(x, y)
|
|
dc.Shear(sx, sy)
|
|
dc.Translate(-x, -y)
|
|
}
|
|
|
|
// Transform multiplies the specified point by the current matrix,
|
|
// returning a transformed position.
|
|
func (dc *Context) Transform(x, y float64) (tx, ty float64) {
|
|
return dc.matrix.Transform(x, y)
|
|
}
|
|
|
|
//
|
|
// Stack operations
|
|
//
|
|
|
|
// Push saves the current state of the context for later retrieval. These
|
|
// can be nested.
|
|
func (dc *Context) Push() {
|
|
x := *dc
|
|
dc.stack = append(dc.stack, &x)
|
|
}
|
|
|
|
// Pop restores the last saved context state from the stack.
|
|
func (dc *Context) Pop() {
|
|
before := *dc
|
|
s := dc.stack
|
|
x, s := s[len(s)-1], s[:len(s)-1]
|
|
*dc = *x
|
|
//dc.mask = before.mask
|
|
dc.strokePath = before.strokePath
|
|
dc.fillPath = before.fillPath
|
|
dc.start = before.start
|
|
dc.current = before.current
|
|
dc.hasCurrent = before.hasCurrent
|
|
dc.textState = before.textState
|
|
}
|