From 8bc8b1087ca0a74ec6b5de852b709b4f27228b6b Mon Sep 17 00:00:00 2001 From: Will Charczuk Date: Fri, 8 Jul 2016 17:57:14 -0700 Subject: [PATCH] removing 3rd party deps. --- chart.go | 26 ++- defaults.go | 2 +- drawing/constants.go | 9 + drawing/curve.go | 158 +++++++++++++++++ drawing/dasher.go | 89 ++++++++++ drawing/demux_flattener.go | 41 +++++ drawing/drawing.go | 148 ++++++++++++++++ drawing/flattener.go | 90 ++++++++++ drawing/free_type_path.go | 30 ++++ drawing/graphic_context.go | 82 +++++++++ drawing/image_filter.go | 13 ++ drawing/line.go | 48 +++++ drawing/matrix.go | 219 +++++++++++++++++++++++ drawing/painter.go | 31 ++++ drawing/path.go | 186 ++++++++++++++++++++ drawing/raster_graphic_context.go | 282 ++++++++++++++++++++++++++++++ drawing/stack_graphic_context.go | 183 +++++++++++++++++++ drawing/stroker.go | 85 +++++++++ drawing/text.go | 74 ++++++++ drawing/transformer.go | 39 +++++ drawing/util.go | 56 ++++++ raster_renderer.go | 56 +++--- renderer.go | 7 +- style.go | 2 +- testserver/main.go | 2 + util.go | 4 + vector_renderer.go | 87 +++++++-- 27 files changed, 1995 insertions(+), 54 deletions(-) create mode 100644 drawing/constants.go create mode 100644 drawing/curve.go create mode 100644 drawing/dasher.go create mode 100644 drawing/demux_flattener.go create mode 100644 drawing/drawing.go create mode 100644 drawing/flattener.go create mode 100644 drawing/free_type_path.go create mode 100644 drawing/graphic_context.go create mode 100644 drawing/image_filter.go create mode 100644 drawing/line.go create mode 100644 drawing/matrix.go create mode 100644 drawing/painter.go create mode 100644 drawing/path.go create mode 100644 drawing/raster_graphic_context.go create mode 100644 drawing/stack_graphic_context.go create mode 100644 drawing/stroker.go create mode 100644 drawing/text.go create mode 100644 drawing/transformer.go create mode 100644 drawing/util.go diff --git a/chart.go b/chart.go index 4e867bd..5846bb9 100644 --- a/chart.go +++ b/chart.go @@ -15,6 +15,7 @@ type Chart struct { Width int Height int + DPI float64 Background Style Canvas Style @@ -28,6 +29,17 @@ type Chart struct { Series []Series } +// GetDPI returns the dpi for the chart. +func (c Chart) GetDPI(defaults ...float64) float64 { + if c.DPI == 0 { + if len(defaults) > 0 { + return defaults[0] + } + return DefaultDPI + } + return c.DPI +} + // GetFont returns the text font. func (c Chart) GetFont() (*truetype.Font, error) { if c.Font == nil { @@ -35,7 +47,7 @@ func (c Chart) GetFont() (*truetype.Font, error) { if err != nil { return nil, err } - c.Font = f + return f, nil } return c.Font, nil } @@ -45,7 +57,10 @@ func (c *Chart) Render(provider RendererProvider, w io.Writer) error { if len(c.Series) == 0 { return errors.New("Please provide at least one series") } - r := provider(c.Width, c.Height) + r, err := provider(c.Width, c.Height) + if err != nil { + return err + } if c.hasText() { font, err := c.GetFont() if err != nil { @@ -53,6 +68,7 @@ func (c *Chart) Render(provider RendererProvider, w io.Writer) error { } r.SetFont(font) } + r.SetDPI(c.GetDPI(DefaultDPI)) canvasBox := c.calculateCanvasBox(r) xrange, yrange := c.initRanges(canvasBox) @@ -122,7 +138,7 @@ func (c Chart) calculateFinalLabelWidth(r Renderer) int { } r.SetFontSize(c.FinalValueLabel.GetFontSize(DefaultFinalLabelFontSize)) - textWidth := r.MeasureText(finalLabelText) + textWidth, _ := r.MeasureText(finalLabelText) asw := c.getAxisWidth() pl := c.FinalValueLabel.Padding.GetLeft(DefaultFinalLabelPadding.Left) @@ -355,7 +371,7 @@ func (c Chart) drawFinalValueLabel(r Renderer, canvasBox Box, index int, s Serie ly := yrange.Translate(lv) + py r.SetFontSize(c.FinalValueLabel.GetFontSize(DefaultFinalLabelFontSize)) - textWidth := r.MeasureText(ll) + textWidth, _ := r.MeasureText(ll) textHeight := int(math.Floor(DefaultFinalLabelFontSize)) halfTextHeight := textHeight >> 1 @@ -409,7 +425,7 @@ func (c Chart) drawTitle(r Renderer) error { r.SetFontColor(c.Canvas.GetFontColor(DefaultTextColor)) titleFontSize := c.Canvas.GetFontSize(DefaultTitleFontSize) r.SetFontSize(titleFontSize) - textWidth := r.MeasureText(c.Title) + textWidth, _ := r.MeasureText(c.Title) titleX := (c.Width >> 1) - (textWidth >> 1) titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + int(titleFontSize) r.Text(c.Title, titleX, titleY) diff --git a/defaults.go b/defaults.go index a3d1e8b..52cbc55 100644 --- a/defaults.go +++ b/defaults.go @@ -17,7 +17,7 @@ const ( // DefaultAxisLineWidth is the line width of the axis lines. DefaultAxisLineWidth = 1.0 //DefaultDPI is the default dots per inch for the chart. - DefaultDPI = 120.0 + DefaultDPI = 92.0 // DefaultMinimumFontSize is the default minimum font size. DefaultMinimumFontSize = 8.0 // DefaultFontSize is the default font size. diff --git a/drawing/constants.go b/drawing/constants.go new file mode 100644 index 0000000..7639b29 --- /dev/null +++ b/drawing/constants.go @@ -0,0 +1,9 @@ +package drawing + +const ( + // DefaultDPI is the default image DPI. + DefaultDPI = 96.0 + + // EMRatio is the ratio of something to something else. + EMRatio = 64.0 / 72.0 +) diff --git a/drawing/curve.go b/drawing/curve.go new file mode 100644 index 0000000..304be1c --- /dev/null +++ b/drawing/curve.go @@ -0,0 +1,158 @@ +package drawing + +import ( + "math" +) + +const ( + // CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines + CurveRecursionLimit = 32 +) + +// Cubic +// x1, y1, cpx1, cpy1, cpx2, cpy2, x2, y2 float64 + +// SubdivideCubic a Bezier cubic curve in 2 equivalents Bezier cubic curves. +// c1 and c2 parameters are the resulting curves +func SubdivideCubic(c, c1, c2 []float64) { + // First point of c is the first point of c1 + c1[0], c1[1] = c[0], c[1] + // Last point of c is the last point of c2 + c2[6], c2[7] = c[6], c[7] + + // Subdivide segment using midpoints + c1[2] = (c[0] + c[2]) / 2 + c1[3] = (c[1] + c[3]) / 2 + + midX := (c[2] + c[4]) / 2 + midY := (c[3] + c[5]) / 2 + + c2[4] = (c[4] + c[6]) / 2 + c2[5] = (c[5] + c[7]) / 2 + + c1[4] = (c1[2] + midX) / 2 + c1[5] = (c1[3] + midY) / 2 + + c2[2] = (midX + c2[4]) / 2 + c2[3] = (midY + c2[5]) / 2 + + c1[6] = (c1[4] + c2[2]) / 2 + c1[7] = (c1[5] + c2[3]) / 2 + + // Last Point of c1 is equal to the first point of c2 + c2[0], c2[1] = c1[6], c1[7] +} + +// TraceCubic generate lines subdividing the cubic curve using a Liner +// flattening_threshold helps determines the flattening expectation of the curve +func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) { + // Allocation curves + var curves [CurveRecursionLimit * 8]float64 + copy(curves[0:8], cubic[0:8]) + i := 0 + + // current curve + var c []float64 + + var dx, dy, d2, d3 float64 + + for i >= 0 { + c = curves[i*8:] + dx = c[6] - c[0] + dy = c[7] - c[1] + + d2 = math.Abs((c[2]-c[6])*dy - (c[3]-c[7])*dx) + d3 = math.Abs((c[4]-c[6])*dy - (c[5]-c[7])*dx) + + // if it's flat then trace a line + if (d2+d3)*(d2+d3) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 { + t.LineTo(c[6], c[7]) + i-- + } else { + // second half of bezier go lower onto the stack + SubdivideCubic(c, curves[(i+1)*8:], curves[i*8:]) + i++ + } + } +} + +// Quad +// x1, y1, cpx1, cpy2, x2, y2 float64 + +// SubdivideQuad a Bezier quad curve in 2 equivalents Bezier quad curves. +// c1 and c2 parameters are the resulting curves +func SubdivideQuad(c, c1, c2 []float64) { + // First point of c is the first point of c1 + c1[0], c1[1] = c[0], c[1] + // Last point of c is the last point of c2 + c2[4], c2[5] = c[4], c[5] + + // Subdivide segment using midpoints + c1[2] = (c[0] + c[2]) / 2 + c1[3] = (c[1] + c[3]) / 2 + c2[2] = (c[2] + c[4]) / 2 + c2[3] = (c[3] + c[5]) / 2 + c1[4] = (c1[2] + c2[2]) / 2 + c1[5] = (c1[3] + c2[3]) / 2 + c2[0], c2[1] = c1[4], c1[5] + return +} + +// TraceQuad generate lines subdividing the curve using a Liner +// flattening_threshold helps determines the flattening expectation of the curve +func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) { + // Allocates curves stack + var curves [CurveRecursionLimit * 6]float64 + copy(curves[0:6], quad[0:6]) + i := 0 + // current curve + var c []float64 + var dx, dy, d float64 + + for i >= 0 { + c = curves[i*6:] + dx = c[4] - c[0] + dy = c[5] - c[1] + + d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx)) + + // if it's flat then trace a line + if (d*d) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 { + t.LineTo(c[4], c[5]) + i-- + } else { + // second half of bezier go lower onto the stack + SubdivideQuad(c, curves[(i+1)*6:], curves[i*6:]) + i++ + } + } +} + +// TraceArc trace an arc using a Liner +func TraceArc(t Liner, x, y, rx, ry, start, angle, scale float64) (lastX, lastY float64) { + end := start + angle + clockWise := true + if angle < 0 { + clockWise = false + } + ra := (math.Abs(rx) + math.Abs(ry)) / 2 + da := math.Acos(ra/(ra+0.125/scale)) * 2 + //normalize + if !clockWise { + da = -da + } + angle = start + da + var curX, curY float64 + for { + if (angle < end-da/4) != clockWise { + curX = x + math.Cos(end)*rx + curY = y + math.Sin(end)*ry + return curX, curY + } + curX = x + math.Cos(angle)*rx + curY = y + math.Sin(angle)*ry + + angle += da + t.LineTo(curX, curY) + } +} diff --git a/drawing/dasher.go b/drawing/dasher.go new file mode 100644 index 0000000..20a5a6b --- /dev/null +++ b/drawing/dasher.go @@ -0,0 +1,89 @@ +package drawing + +// NewDashVertexConverter creates a new dash converter. +func NewDashVertexConverter(dash []float64, dashOffset float64, flattener Flattener) *DashVertexConverter { + var dasher DashVertexConverter + dasher.dash = dash + dasher.currentDash = 0 + dasher.dashOffset = dashOffset + dasher.next = flattener + return &dasher +} + +// DashVertexConverter is a converter for dash vertexes. +type DashVertexConverter struct { + next Flattener + x, y, distance float64 + dash []float64 + currentDash int + dashOffset float64 +} + +// LineTo implements the pathbuilder interface. +func (dasher *DashVertexConverter) LineTo(x, y float64) { + dasher.lineTo(x, y) +} + +// MoveTo implements the pathbuilder interface. +func (dasher *DashVertexConverter) MoveTo(x, y float64) { + dasher.next.MoveTo(x, y) + dasher.x, dasher.y = x, y + dasher.distance = dasher.dashOffset + dasher.currentDash = 0 +} + +// LineJoin implements the pathbuilder interface. +func (dasher *DashVertexConverter) LineJoin() { + dasher.next.LineJoin() +} + +// Close implements the pathbuilder interface. +func (dasher *DashVertexConverter) Close() { + dasher.next.Close() +} + +// End implements the pathbuilder interface. +func (dasher *DashVertexConverter) End() { + dasher.next.End() +} + +func (dasher *DashVertexConverter) lineTo(x, y float64) { + rest := dasher.dash[dasher.currentDash] - dasher.distance + for rest < 0 { + dasher.distance = dasher.distance - dasher.dash[dasher.currentDash] + dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash) + rest = dasher.dash[dasher.currentDash] - dasher.distance + } + d := distance(dasher.x, dasher.y, x, y) + for d >= rest { + k := rest / d + lx := dasher.x + k*(x-dasher.x) + ly := dasher.y + k*(y-dasher.y) + if dasher.currentDash%2 == 0 { + // line + dasher.next.LineTo(lx, ly) + } else { + // gap + dasher.next.End() + dasher.next.MoveTo(lx, ly) + } + d = d - rest + dasher.x, dasher.y = lx, ly + dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash) + rest = dasher.dash[dasher.currentDash] + } + dasher.distance = d + if dasher.currentDash%2 == 0 { + // line + dasher.next.LineTo(x, y) + } else { + // gap + dasher.next.End() + dasher.next.MoveTo(x, y) + } + if dasher.distance >= dasher.dash[dasher.currentDash] { + dasher.distance = dasher.distance - dasher.dash[dasher.currentDash] + dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash) + } + dasher.x, dasher.y = x, y +} diff --git a/drawing/demux_flattener.go b/drawing/demux_flattener.go new file mode 100644 index 0000000..43b8646 --- /dev/null +++ b/drawing/demux_flattener.go @@ -0,0 +1,41 @@ +package drawing + +// DemuxFlattener is a flattener +type DemuxFlattener struct { + Flatteners []Flattener +} + +// MoveTo implements the path builder interface. +func (dc DemuxFlattener) MoveTo(x, y float64) { + for _, flattener := range dc.Flatteners { + flattener.MoveTo(x, y) + } +} + +// LineTo implements the path builder interface. +func (dc DemuxFlattener) LineTo(x, y float64) { + for _, flattener := range dc.Flatteners { + flattener.LineTo(x, y) + } +} + +// LineJoin implements the path builder interface. +func (dc DemuxFlattener) LineJoin() { + for _, flattener := range dc.Flatteners { + flattener.LineJoin() + } +} + +// Close implements the path builder interface. +func (dc DemuxFlattener) Close() { + for _, flattener := range dc.Flatteners { + flattener.Close() + } +} + +// End implements the path builder interface. +func (dc DemuxFlattener) End() { + for _, flattener := range dc.Flatteners { + flattener.End() + } +} diff --git a/drawing/drawing.go b/drawing/drawing.go new file mode 100644 index 0000000..8a1da6a --- /dev/null +++ b/drawing/drawing.go @@ -0,0 +1,148 @@ +package drawing + +import ( + "image/color" + + "github.com/golang/freetype/truetype" +) + +// FillRule defines the type for fill rules +type FillRule int + +const ( + // FillRuleEvenOdd determines the "insideness" of a point in the shape + // by drawing a ray from that point to infinity in any direction + // and counting the number of path segments from the given shape that the ray crosses. + // If this number is odd, the point is inside; if even, the point is outside. + FillRuleEvenOdd FillRule = iota + // FillRuleWinding determines the "insideness" of a point in the shape + // by drawing a ray from that point to infinity in any direction + // and then examining the places where a segment of the shape crosses the ray. + // Starting with a count of zero, add one each time a path segment crosses + // the ray from left to right and subtract one each time + // a path segment crosses the ray from right to left. After counting the crossings, + // if the result is zero then the point is outside the path. Otherwise, it is inside. + FillRuleWinding +) + +// LineCap is the style of line extremities +type LineCap int + +const ( + // RoundCap defines a rounded shape at the end of the line + RoundCap LineCap = iota + // ButtCap defines a squared shape exactly at the end of the line + ButtCap + // SquareCap defines a squared shape at the end of the line + SquareCap +) + +// LineJoin is the style of segments joint +type LineJoin int + +const ( + // BevelJoin represents cut segments joint + BevelJoin LineJoin = iota + // RoundJoin represents rounded segments joint + RoundJoin + // MiterJoin represents peaker segments joint + MiterJoin +) + +// StrokeStyle keeps stroke style attributes +// that is used by the Stroke method of a Drawer +type StrokeStyle struct { + // Color defines the color of stroke + Color color.Color + // Line width + Width float64 + // Line cap style rounded, butt or square + LineCap LineCap + // Line join style bevel, round or miter + LineJoin LineJoin + // offset of the first dash + DashOffset float64 + // array represented dash length pair values are plain dash and impair are space between dash + // if empty display plain line + Dash []float64 +} + +// SolidFillStyle define style attributes for a solid fill style +type SolidFillStyle struct { + // Color defines the line color + Color color.Color + // FillRule defines the file rule to used + FillRule FillRule +} + +// Valign Vertical Alignment of the text +type Valign int + +const ( + // ValignTop top align text + ValignTop Valign = iota + // ValignCenter centered text + ValignCenter + // ValignBottom bottom aligned text + ValignBottom + // ValignBaseline align text with the baseline of the font + ValignBaseline +) + +// Halign Horizontal Alignment of the text +type Halign int + +const ( + // HalignLeft Horizontally align to left + HalignLeft = iota + // HalignCenter Horizontally align to center + HalignCenter + // HalignRight Horizontally align to right + HalignRight +) + +// TextStyle describe text property +type TextStyle struct { + // Color defines the color of text + Color color.Color + // Size font size + Size float64 + // The font to use + Font *truetype.Font + // Horizontal Alignment of the text + Halign Halign + // Vertical Alignment of the text + Valign Valign +} + +// ScalingPolicy is a constant to define how to scale an image +type ScalingPolicy int + +const ( + // ScalingNone no scaling applied + ScalingNone ScalingPolicy = iota + // ScalingStretch the image is stretched so that its width and height are exactly the given width and height + ScalingStretch + // ScalingWidth the image is scaled so that its width is exactly the given width + ScalingWidth + // ScalingHeight the image is scaled so that its height is exactly the given height + ScalingHeight + // ScalingFit the image is scaled to the largest scale that allow the image to fit within a rectangle width x height + ScalingFit + // ScalingSameArea the image is scaled so that its area is exactly the area of the given rectangle width x height + ScalingSameArea + // ScalingFill the image is scaled to the smallest scale that allow the image to fully cover a rectangle width x height + ScalingFill +) + +// ImageScaling style attributes used to display the image +type ImageScaling struct { + // Horizontal Alignment of the image + Halign Halign + // Vertical Alignment of the image + Valign Valign + // Width Height used by scaling policy + Width, Height float64 + // ScalingPolicy defines the scaling policy to applied to the image + ScalingPolicy ScalingPolicy +} diff --git a/drawing/flattener.go b/drawing/flattener.go new file mode 100644 index 0000000..e3fef9a --- /dev/null +++ b/drawing/flattener.go @@ -0,0 +1,90 @@ +package drawing + +// Liner receive segment definition +type Liner interface { + // LineTo Draw a line from the current position to the point (x, y) + LineTo(x, y float64) +} + +// Flattener receive segment definition +type Flattener interface { + // MoveTo Start a New line from the point (x, y) + MoveTo(x, y float64) + // LineTo Draw a line from the current position to the point (x, y) + LineTo(x, y float64) + // LineJoin add the most recent starting point to close the path to create a polygon + LineJoin() + // Close add the most recent starting point to close the path to create a polygon + Close() + // End mark the current line as finished so we can draw caps + End() +} + +// Flatten convert curves into straight segments keeping join segments info +func Flatten(path *Path, flattener Flattener, scale float64) { + // First Point + var startX, startY float64 = 0, 0 + // Current Point + var x, y float64 = 0, 0 + i := 0 + for _, cmp := range path.Components { + switch cmp { + case MoveToComponent: + x, y = path.Points[i], path.Points[i+1] + startX, startY = x, y + if i != 0 { + flattener.End() + } + flattener.MoveTo(x, y) + i += 2 + case LineToComponent: + x, y = path.Points[i], path.Points[i+1] + flattener.LineTo(x, y) + flattener.LineJoin() + i += 2 + case QuadCurveToComponent: + TraceQuad(flattener, path.Points[i-2:], 0.5) + x, y = path.Points[i+2], path.Points[i+3] + flattener.LineTo(x, y) + i += 4 + case CubicCurveToComponent: + TraceCubic(flattener, path.Points[i-2:], 0.5) + x, y = path.Points[i+4], path.Points[i+5] + flattener.LineTo(x, y) + i += 6 + case ArcToComponent: + x, y = TraceArc(flattener, path.Points[i], path.Points[i+1], path.Points[i+2], path.Points[i+3], path.Points[i+4], path.Points[i+5], scale) + flattener.LineTo(x, y) + i += 6 + case CloseComponent: + flattener.LineTo(startX, startY) + flattener.Close() + } + } + flattener.End() +} + +type SegmentedPath struct { + Points []float64 +} + +func (p *SegmentedPath) MoveTo(x, y float64) { + p.Points = append(p.Points, x, y) + // TODO need to mark this point as moveto +} + +func (p *SegmentedPath) LineTo(x, y float64) { + p.Points = append(p.Points, x, y) +} + +func (p *SegmentedPath) LineJoin() { + // TODO need to mark the current point as linejoin +} + +func (p *SegmentedPath) Close() { + // TODO Close +} + +func (p *SegmentedPath) End() { + // Nothing to do +} diff --git a/drawing/free_type_path.go b/drawing/free_type_path.go new file mode 100644 index 0000000..5c3c266 --- /dev/null +++ b/drawing/free_type_path.go @@ -0,0 +1,30 @@ +package drawing + +import ( + "github.com/golang/freetype/raster" + "golang.org/x/image/math/fixed" +) + +// FtLineBuilder is a builder for freetype raster glyphs. +type FtLineBuilder struct { + Adder raster.Adder +} + +// MoveTo implements the path builder interface. +func (liner FtLineBuilder) MoveTo(x, y float64) { + liner.Adder.Start(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)}) +} + +// LineTo implements the path builder interface. +func (liner FtLineBuilder) LineTo(x, y float64) { + liner.Adder.Add1(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)}) +} + +// LineJoin implements the path builder interface. +func (liner FtLineBuilder) LineJoin() {} + +// Close implements the path builder interface. +func (liner FtLineBuilder) Close() {} + +// End implements the path builder interface. +func (liner FtLineBuilder) End() {} diff --git a/drawing/graphic_context.go b/drawing/graphic_context.go new file mode 100644 index 0000000..553c1eb --- /dev/null +++ b/drawing/graphic_context.go @@ -0,0 +1,82 @@ +package drawing + +import ( + "image" + "image/color" + + "github.com/golang/freetype/truetype" +) + +// GraphicContext describes the interface for the various backends (images, pdf, opengl, ...) +type GraphicContext interface { + // PathBuilder describes the interface for path drawing + PathBuilder + // BeginPath creates a new path + BeginPath() + // GetMatrixTransform returns the current transformation matrix + GetMatrixTransform() Matrix + // SetMatrixTransform sets the current transformation matrix + SetMatrixTransform(tr Matrix) + // ComposeMatrixTransform composes the current transformation matrix with tr + ComposeMatrixTransform(tr Matrix) + // Rotate applies a rotation to the current transformation matrix. angle is in radian. + Rotate(angle float64) + // Translate applies a translation to the current transformation matrix. + Translate(tx, ty float64) + // Scale applies a scale to the current transformation matrix. + Scale(sx, sy float64) + // SetStrokeColor sets the current stroke color + SetStrokeColor(c color.Color) + // SetFillColor sets the current fill color + SetFillColor(c color.Color) + // SetFillRule sets the current fill rule + SetFillRule(f FillRule) + // SetLineWidth sets the current line width + SetLineWidth(lineWidth float64) + // SetLineCap sets the current line cap + SetLineCap(cap LineCap) + // SetLineJoin sets the current line join + SetLineJoin(join LineJoin) + // SetLineDash sets the current dash + SetLineDash(dash []float64, dashOffset float64) + // SetFontSize sets the current font size + SetFontSize(fontSize float64) + // GetFontSize gets the current font size + GetFontSize() float64 + // SetFont sets the font for the context + SetFont(f *truetype.Font) + // GetFont returns the current font + GetFont() *truetype.Font + // DrawImage draws the raster image in the current canvas + DrawImage(image image.Image) + // Save the context and push it to the context stack + Save() + // Restore remove the current context and restore the last one + Restore() + // Clear fills the current canvas with a default transparent color + Clear() + // ClearRect fills the specified rectangle with a default transparent color + ClearRect(x1, y1, x2, y2 int) + // SetDPI sets the current DPI + SetDPI(dpi int) + // GetDPI gets the current DPI + GetDPI() int + // GetStringBounds gets pixel bounds(dimensions) of given string + GetStringBounds(s string) (left, top, right, bottom float64) + // CreateStringPath creates a path from the string s at x, y + CreateStringPath(text string, x, y float64) (cursor float64) + // FillString draws the text at point (0, 0) + FillString(text string) (cursor float64) + // FillStringAt draws the text at the specified point (x, y) + FillStringAt(text string, x, y float64) (cursor float64) + // StrokeString draws the contour of the text at point (0, 0) + StrokeString(text string) (cursor float64) + // StrokeStringAt draws the contour of the text at point (x, y) + StrokeStringAt(text string, x, y float64) (cursor float64) + // Stroke strokes the paths with the color specified by SetStrokeColor + Stroke(paths ...*Path) + // Fill fills the paths with the color specified by SetFillColor + Fill(paths ...*Path) + // FillStroke first fills the paths and than strokes them + FillStroke(paths ...*Path) +} diff --git a/drawing/image_filter.go b/drawing/image_filter.go new file mode 100644 index 0000000..3cf0309 --- /dev/null +++ b/drawing/image_filter.go @@ -0,0 +1,13 @@ +package drawing + +// ImageFilter defines the type of filter to use +type ImageFilter int + +const ( + // LinearFilter defines a linear filter + LinearFilter ImageFilter = iota + // BilinearFilter defines a bilinear filter + BilinearFilter + // BicubicFilter defines a bicubic filter + BicubicFilter +) diff --git a/drawing/line.go b/drawing/line.go new file mode 100644 index 0000000..b4e28e7 --- /dev/null +++ b/drawing/line.go @@ -0,0 +1,48 @@ +package drawing + +import ( + "image/color" + "image/draw" +) + +// PolylineBresenham draws a polyline to an image +func PolylineBresenham(img draw.Image, c color.Color, s ...float64) { + for i := 2; i < len(s); i += 2 { + Bresenham(img, c, int(s[i-2]+0.5), int(s[i-1]+0.5), int(s[i]+0.5), int(s[i+1]+0.5)) + } +} + +// Bresenham draws a line between (x0, y0) and (x1, y1) +func Bresenham(img draw.Image, color color.Color, x0, y0, x1, y1 int) { + dx := abs(x1 - x0) + dy := abs(y1 - y0) + var sx, sy int + if x0 < x1 { + sx = 1 + } else { + sx = -1 + } + if y0 < y1 { + sy = 1 + } else { + sy = -1 + } + err := dx - dy + + var e2 int + for { + img.Set(x0, y0, color) + if x0 == x1 && y0 == y1 { + return + } + e2 = 2 * err + if e2 > -dy { + err = err - dy + x0 = x0 + sx + } + if e2 < dx { + err = err + dx + y0 = y0 + sy + } + } +} diff --git a/drawing/matrix.go b/drawing/matrix.go new file mode 100644 index 0000000..b9a4128 --- /dev/null +++ b/drawing/matrix.go @@ -0,0 +1,219 @@ +package drawing + +import ( + "math" +) + +// Matrix represents an affine transformation +type Matrix [6]float64 + +const ( + epsilon = 1e-6 +) + +// Determinant compute the determinant of the matrix +func (tr Matrix) Determinant() float64 { + return tr[0]*tr[3] - tr[1]*tr[2] +} + +// Transform applies the transformation matrix to points. It modify the points passed in parameter. +func (tr Matrix) Transform(points []float64) { + for i, j := 0, 1; j < len(points); i, j = i+2, j+2 { + x := points[i] + y := points[j] + points[i] = x*tr[0] + y*tr[2] + tr[4] + points[j] = x*tr[1] + y*tr[3] + tr[5] + } +} + +// TransformPoint applies the transformation matrix to point. It returns the point the transformed point. +func (tr Matrix) TransformPoint(x, y float64) (xres, yres float64) { + xres = x*tr[0] + y*tr[2] + tr[4] + yres = x*tr[1] + y*tr[3] + tr[5] + return xres, yres +} + +func minMax(x, y float64) (min, max float64) { + if x > y { + return y, x + } + return x, y +} + +// Transform applies the transformation matrix to the rectangle represented by the min and the max point of the rectangle +func (tr Matrix) TransformRectangle(x0, y0, x2, y2 float64) (nx0, ny0, nx2, ny2 float64) { + points := []float64{x0, y0, x2, y0, x2, y2, x0, y2} + tr.Transform(points) + points[0], points[2] = minMax(points[0], points[2]) + points[4], points[6] = minMax(points[4], points[6]) + points[1], points[3] = minMax(points[1], points[3]) + points[5], points[7] = minMax(points[5], points[7]) + + nx0 = math.Min(points[0], points[4]) + ny0 = math.Min(points[1], points[5]) + nx2 = math.Max(points[2], points[6]) + ny2 = math.Max(points[3], points[7]) + return nx0, ny0, nx2, ny2 +} + +// InverseTransform applies the transformation inverse matrix to the rectangle represented by the min and the max point of the rectangle +func (tr Matrix) InverseTransform(points []float64) { + d := tr.Determinant() // matrix determinant + for i, j := 0, 1; j < len(points); i, j = i+2, j+2 { + x := points[i] + y := points[j] + points[i] = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d + points[j] = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d + } +} + +// InverseTransformPoint applies the transformation inverse matrix to point. It returns the point the transformed point. +func (tr Matrix) InverseTransformPoint(x, y float64) (xres, yres float64) { + d := tr.Determinant() // matrix determinant + xres = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d + yres = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d + return xres, yres +} + +// VectorTransform applies the transformation matrix to points without using the translation parameter of the affine matrix. +// It modify the points passed in parameter. +func (tr Matrix) VectorTransform(points []float64) { + for i, j := 0, 1; j < len(points); i, j = i+2, j+2 { + x := points[i] + y := points[j] + points[i] = x*tr[0] + y*tr[2] + points[j] = x*tr[1] + y*tr[3] + } +} + +// NewIdentityMatrix creates an identity transformation matrix. +func NewIdentityMatrix() Matrix { + return Matrix{1, 0, 0, 1, 0, 0} +} + +// NewTranslationMatrix creates a transformation matrix with a translation tx and ty translation parameter +func NewTranslationMatrix(tx, ty float64) Matrix { + return Matrix{1, 0, 0, 1, tx, ty} +} + +// NewScaleMatrix creates a transformation matrix with a sx, sy scale factor +func NewScaleMatrix(sx, sy float64) Matrix { + return Matrix{sx, 0, 0, sy, 0, 0} +} + +// NewRotationMatrix creates a rotation transformation matrix. angle is in radian +func NewRotationMatrix(angle float64) Matrix { + c := math.Cos(angle) + s := math.Sin(angle) + return Matrix{c, s, -s, c, 0, 0} +} + +// NewMatrixFromRects creates a transformation matrix, combining a scale and a translation, that transform rectangle1 into rectangle2. +func NewMatrixFromRects(rectangle1, rectangle2 [4]float64) Matrix { + xScale := (rectangle2[2] - rectangle2[0]) / (rectangle1[2] - rectangle1[0]) + yScale := (rectangle2[3] - rectangle2[1]) / (rectangle1[3] - rectangle1[1]) + xOffset := rectangle2[0] - (rectangle1[0] * xScale) + yOffset := rectangle2[1] - (rectangle1[1] * yScale) + return Matrix{xScale, 0, 0, yScale, xOffset, yOffset} +} + +// Inverse computes the inverse matrix +func (tr *Matrix) Inverse() { + d := tr.Determinant() // matrix determinant + tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5] + tr[0] = tr3 / d + tr[1] = -tr1 / d + tr[2] = -tr2 / d + tr[3] = tr0 / d + tr[4] = (tr2*tr5 - tr3*tr4) / d + tr[5] = (tr1*tr4 - tr0*tr5) / d +} + +func (tr Matrix) Copy() Matrix { + var result Matrix + copy(result[:], tr[:]) + return result +} + +// Compose multiplies trToConcat x tr +func (tr *Matrix) Compose(trToCompose Matrix) { + tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5] + tr[0] = trToCompose[0]*tr0 + trToCompose[1]*tr2 + tr[1] = trToCompose[1]*tr3 + trToCompose[0]*tr1 + tr[2] = trToCompose[2]*tr0 + trToCompose[3]*tr2 + tr[3] = trToCompose[3]*tr3 + trToCompose[2]*tr1 + tr[4] = trToCompose[4]*tr0 + trToCompose[5]*tr2 + tr4 + tr[5] = trToCompose[5]*tr3 + trToCompose[4]*tr1 + tr5 +} + +// Scale adds a scale to the matrix +func (tr *Matrix) Scale(sx, sy float64) { + tr[0] = sx * tr[0] + tr[1] = sx * tr[1] + tr[2] = sy * tr[2] + tr[3] = sy * tr[3] +} + +// Translate adds a translation to the matrix +func (tr *Matrix) Translate(tx, ty float64) { + tr[4] = tx*tr[0] + ty*tr[2] + tr[4] + tr[5] = ty*tr[3] + tx*tr[1] + tr[5] +} + +// Rotate adds a rotation to the matrix. angle is in radian +func (tr *Matrix) Rotate(angle float64) { + c := math.Cos(angle) + s := math.Sin(angle) + t0 := c*tr[0] + s*tr[2] + t1 := s*tr[3] + c*tr[1] + t2 := c*tr[2] - s*tr[0] + t3 := c*tr[3] - s*tr[1] + tr[0] = t0 + tr[1] = t1 + tr[2] = t2 + tr[3] = t3 +} + +// GetTranslation +func (tr Matrix) GetTranslation() (x, y float64) { + return tr[4], tr[5] +} + +// GetScaling +func (tr Matrix) GetScaling() (x, y float64) { + return tr[0], tr[3] +} + +// GetScale computes a scale for the matrix +func (tr Matrix) GetScale() float64 { + x := 0.707106781*tr[0] + 0.707106781*tr[1] + y := 0.707106781*tr[2] + 0.707106781*tr[3] + return math.Sqrt(x*x + y*y) +} + +// ******************** Testing ******************** + +// Equals tests if a two transformation are equal. A tolerance is applied when comparing matrix elements. +func (tr1 Matrix) Equals(tr2 Matrix) bool { + for i := 0; i < 6; i = i + 1 { + if !fequals(tr1[i], tr2[i]) { + return false + } + } + return true +} + +// IsIdentity tests if a transformation is the identity transformation. A tolerance is applied when comparing matrix elements. +func (tr Matrix) IsIdentity() bool { + return fequals(tr[4], 0) && fequals(tr[5], 0) && tr.IsTranslation() +} + +// IsTranslation tests if a transformation is is a pure translation. A tolerance is applied when comparing matrix elements. +func (tr Matrix) IsTranslation() bool { + return fequals(tr[0], 1) && fequals(tr[1], 0) && fequals(tr[2], 0) && fequals(tr[3], 1) +} + +// fequals compares two floats. return true if the distance between the two floats is less than epsilon, false otherwise +func fequals(float1, float2 float64) bool { + return math.Abs(float1-float2) <= epsilon +} diff --git a/drawing/painter.go b/drawing/painter.go new file mode 100644 index 0000000..1353449 --- /dev/null +++ b/drawing/painter.go @@ -0,0 +1,31 @@ +package drawing + +import ( + "image" + "image/color" + + "golang.org/x/image/draw" + "golang.org/x/image/math/f64" + + "github.com/golang/freetype/raster" +) + +// Painter implements the freetype raster.Painter and has a SetColor method like the RGBAPainter +type Painter interface { + raster.Painter + SetColor(color color.Color) +} + +// DrawImage draws an image into dest using an affine transformation matrix, an op and a filter +func DrawImage(src image.Image, dest draw.Image, tr Matrix, op draw.Op, filter ImageFilter) { + var transformer draw.Transformer + switch filter { + case LinearFilter: + transformer = draw.NearestNeighbor + case BilinearFilter: + transformer = draw.BiLinear + case BicubicFilter: + transformer = draw.CatmullRom + } + transformer.Transform(dest, f64.Aff3{tr[0], tr[1], tr[4], tr[2], tr[3], tr[5]}, src, src.Bounds(), op, nil) +} diff --git a/drawing/path.go b/drawing/path.go new file mode 100644 index 0000000..979a0d5 --- /dev/null +++ b/drawing/path.go @@ -0,0 +1,186 @@ +package drawing + +import ( + "fmt" + "math" +) + +// PathBuilder describes the interface for path drawing. +type PathBuilder interface { + // LastPoint returns the current point of the current sub path + LastPoint() (x, y float64) + // MoveTo creates a new subpath that start at the specified point + MoveTo(x, y float64) + // LineTo adds a line to the current subpath + LineTo(x, y float64) + // QuadCurveTo adds a quadratic Bézier curve to the current subpath + QuadCurveTo(cx, cy, x, y float64) + // CubicCurveTo adds a cubic Bézier curve to the current subpath + CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) + // ArcTo adds an arc to the current subpath + ArcTo(cx, cy, rx, ry, startAngle, angle float64) + // Close creates a line from the current point to the last MoveTo + // point (if not the same) and mark the path as closed so the + // first and last lines join nicely. + Close() +} + +// PathComponent represents component of a path +type PathComponent int + +const ( + // MoveToComponent is a MoveTo component in a Path + MoveToComponent PathComponent = iota + // LineToComponent is a LineTo component in a Path + LineToComponent + // QuadCurveToComponent is a QuadCurveTo component in a Path + QuadCurveToComponent + // CubicCurveToComponent is a CubicCurveTo component in a Path + CubicCurveToComponent + // ArcToComponent is a ArcTo component in a Path + ArcToComponent + // CloseComponent is a ArcTo component in a Path + CloseComponent +) + +// Path stores points +type Path struct { + // Components is a slice of PathComponent in a Path and mark the role of each points in the Path + Components []PathComponent + // Points are combined with Components to have a specific role in the path + Points []float64 + // Last Point of the Path + x, y float64 +} + +func (p *Path) appendToPath(cmd PathComponent, points ...float64) { + p.Components = append(p.Components, cmd) + p.Points = append(p.Points, points...) +} + +// LastPoint returns the current point of the current path +func (p *Path) LastPoint() (x, y float64) { + return p.x, p.y +} + +// MoveTo starts a new path at (x, y) position +func (p *Path) MoveTo(x, y float64) { + p.appendToPath(MoveToComponent, x, y) + p.x = x + p.y = y +} + +// LineTo adds a line to the current path +func (p *Path) LineTo(x, y float64) { + if len(p.Components) == 0 { //special case when no move has been done + p.MoveTo(0, 0) + } + p.appendToPath(LineToComponent, x, y) + p.x = x + p.y = y +} + +// QuadCurveTo adds a quadratic bezier curve to the current path +func (p *Path) QuadCurveTo(cx, cy, x, y float64) { + if len(p.Components) == 0 { //special case when no move has been done + p.MoveTo(0, 0) + } + p.appendToPath(QuadCurveToComponent, cx, cy, x, y) + p.x = x + p.y = y +} + +// CubicCurveTo adds a cubic bezier curve to the current path +func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) { + if len(p.Components) == 0 { //special case when no move has been done + p.MoveTo(0, 0) + } + p.appendToPath(CubicCurveToComponent, cx1, cy1, cx2, cy2, x, y) + p.x = x + p.y = y +} + +// ArcTo adds an arc to the path +func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, angle float64) { + endAngle := startAngle + angle + clockWise := true + if angle < 0 { + clockWise = false + } + // normalize + if clockWise { + for endAngle < startAngle { + endAngle += math.Pi * 2.0 + } + } else { + for startAngle < endAngle { + startAngle += math.Pi * 2.0 + } + } + startX := cx + math.Cos(startAngle)*rx + startY := cy + math.Sin(startAngle)*ry + if len(p.Components) > 0 { + p.LineTo(startX, startY) + } else { + p.MoveTo(startX, startY) + } + p.appendToPath(ArcToComponent, cx, cy, rx, ry, startAngle, angle) + p.x = cx + math.Cos(endAngle)*rx + p.y = cy + math.Sin(endAngle)*ry +} + +// Close closes the current path +func (p *Path) Close() { + p.appendToPath(CloseComponent) +} + +// Copy make a clone of the current path and return it +func (p *Path) Copy() (dest *Path) { + dest = new(Path) + dest.Components = make([]PathComponent, len(p.Components)) + copy(dest.Components, p.Components) + dest.Points = make([]float64, len(p.Points)) + copy(dest.Points, p.Points) + dest.x, dest.y = p.x, p.y + return dest +} + +// Clear reset the path +func (p *Path) Clear() { + p.Components = p.Components[0:0] + p.Points = p.Points[0:0] + return +} + +// IsEmpty returns true if the path is empty +func (p *Path) IsEmpty() bool { + return len(p.Components) == 0 +} + +// String returns a debug text view of the path +func (p *Path) String() string { + s := "" + j := 0 + for _, cmd := range p.Components { + switch cmd { + case MoveToComponent: + s += fmt.Sprintf("MoveTo: %f, %f\n", p.Points[j], p.Points[j+1]) + j = j + 2 + case LineToComponent: + s += fmt.Sprintf("LineTo: %f, %f\n", p.Points[j], p.Points[j+1]) + j = j + 2 + case QuadCurveToComponent: + s += fmt.Sprintf("QuadCurveTo: %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3]) + j = j + 4 + case CubicCurveToComponent: + s += fmt.Sprintf("CubicCurveTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5]) + j = j + 6 + case ArcToComponent: + s += fmt.Sprintf("ArcTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5]) + j = j + 6 + case CloseComponent: + s += "Close\n" + } + } + return s +} diff --git a/drawing/raster_graphic_context.go b/drawing/raster_graphic_context.go new file mode 100644 index 0000000..a967775 --- /dev/null +++ b/drawing/raster_graphic_context.go @@ -0,0 +1,282 @@ +package drawing + +import ( + "errors" + "image" + "image/color" + "math" + + "github.com/golang/freetype/raster" + "github.com/golang/freetype/truetype" + "golang.org/x/image/draw" + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" +) + +// NewRasterGraphicContext creates a new Graphic context from an image. +func NewRasterGraphicContext(img draw.Image) (*RasterGraphicContext, error) { + var painter Painter + switch selectImage := img.(type) { + case *image.RGBA: + painter = raster.NewRGBAPainter(selectImage) + default: + return nil, errors.New("NewRasterGraphicContext() :: invalid image type") + } + return NewRasterGraphicContextWithPainter(img, painter), nil +} + +// NewRasterGraphicContextWithPainter creates a new Graphic context from an image and a Painter (see Freetype-go) +func NewRasterGraphicContextWithPainter(img draw.Image, painter Painter) *RasterGraphicContext { + width, height := img.Bounds().Dx(), img.Bounds().Dy() + return &RasterGraphicContext{ + NewStackGraphicContext(), + img, + painter, + raster.NewRasterizer(width, height), + raster.NewRasterizer(width, height), + &truetype.GlyphBuf{}, + DefaultDPI, + } +} + +// RasterGraphicContext is the implementation of GraphicContext for a raster image +type RasterGraphicContext struct { + *StackGraphicContext + img draw.Image + painter Painter + fillRasterizer *raster.Rasterizer + strokeRasterizer *raster.Rasterizer + glyphBuf *truetype.GlyphBuf + DPI float64 +} + +// SetDPI sets the screen resolution in dots per inch. +func (rgc *RasterGraphicContext) SetDPI(dpi float64) { + rgc.DPI = dpi + rgc.recalc() +} + +// GetDPI returns the resolution of the Image GraphicContext +func (rgc *RasterGraphicContext) GetDPI() float64 { + return rgc.DPI +} + +// Clear fills the current canvas with a default transparent color +func (rgc *RasterGraphicContext) Clear() { + width, height := rgc.img.Bounds().Dx(), rgc.img.Bounds().Dy() + rgc.ClearRect(0, 0, width, height) +} + +// ClearRect fills the current canvas with a default transparent color at the specified rectangle +func (rgc *RasterGraphicContext) ClearRect(x1, y1, x2, y2 int) { + imageColor := image.NewUniform(rgc.current.FillColor) + draw.Draw(rgc.img, image.Rect(x1, y1, x2, y2), imageColor, image.ZP, draw.Over) +} + +// DrawImage draws the raster image in the current canvas +func (rgc *RasterGraphicContext) DrawImage(img image.Image) { + DrawImage(img, rgc.img, rgc.current.Tr, draw.Over, BilinearFilter) +} + +// FillString draws the text at point (0, 0) +func (rgc *RasterGraphicContext) FillString(text string) (cursor float64, err error) { + cursor, err = rgc.FillStringAt(text, 0, 0) + return +} + +// FillStringAt draws the text at the specified point (x, y) +func (rgc *RasterGraphicContext) FillStringAt(text string, x, y float64) (cursor float64, err error) { + cursor, err = rgc.CreateStringPath(text, x, y) + rgc.Fill() + return +} + +// StrokeString draws the contour of the text at point (0, 0) +func (rgc *RasterGraphicContext) StrokeString(text string) (cursor float64, err error) { + cursor, err = rgc.StrokeStringAt(text, 0, 0) + return +} + +// StrokeStringAt draws the contour of the text at point (x, y) +func (rgc *RasterGraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64, err error) { + cursor, err = rgc.CreateStringPath(text, x, y) + rgc.Stroke() + return +} + +func (rgc *RasterGraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error { + if err := rgc.glyphBuf.Load(rgc.current.Font, fixed.Int26_6(rgc.current.Scale), glyph, font.HintingNone); err != nil { + return err + } + e0 := 0 + for _, e1 := range rgc.glyphBuf.Ends { + DrawContour(rgc, rgc.glyphBuf.Points[e0:e1], dx, dy) + e0 = e1 + } + return nil +} + +// CreateStringPath creates a path from the string s at x, y, and returns the string width. +// The text is placed so that the left edge of the em square of the first character of s +// and the baseline intersect at x, y. The majority of the affected pixels will be +// above and to the right of the point, but some may be below or to the left. +// For example, drawing a string that starts with a 'J' in an italic font may +// affect pixels below and left of the point. +func (rgc *RasterGraphicContext) CreateStringPath(s string, x, y float64) (cursor float64, err error) { + f := rgc.GetFont() + if f == nil { + err = errors.New("No font loaded, cannot continue") + return + } + rgc.recalc() + + startx := x + prev, hasPrev := truetype.Index(0), false + for _, rc := range s { + index := f.Index(rc) + if hasPrev { + x += fUnitsToFloat64(f.Kern(fixed.Int26_6(rgc.current.Scale), prev, index)) + } + err = rgc.drawGlyph(index, x, y) + if err != nil { + cursor = x - startx + return + } + x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(rgc.current.Scale), index).AdvanceWidth) + prev, hasPrev = index, true + } + cursor = x - startx + return +} + +// GetStringBounds returns the approximate pixel bounds of a string. +// The the left edge of the em square of the first character of s +// and the baseline intersect at 0, 0 in the returned coordinates. +func (rgc *RasterGraphicContext) GetStringBounds(s string) (left, top, right, bottom float64, err error) { + f := rgc.GetFont() + if f == nil { + err = errors.New("No font loaded, cannot continue") + return + } + rgc.recalc() + + cursor := 0.0 + prev, hasPrev := truetype.Index(0), false + for _, rc := range s { + index := f.Index(rc) + if hasPrev { + cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(rgc.current.Scale), prev, index)) + } + + if err = rgc.glyphBuf.Load(rgc.current.Font, fixed.Int26_6(rgc.current.Scale), index, font.HintingNone); err != nil { + return + } + e0 := 0 + for _, e1 := range rgc.glyphBuf.Ends { + ps := rgc.glyphBuf.Points[e0:e1] + for _, p := range ps { + x, y := pointToF64Point(p) + top = math.Min(top, y) + bottom = math.Max(bottom, y) + left = math.Min(left, x+cursor) + right = math.Max(right, x+cursor) + } + e0 = e1 + } + cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(rgc.current.Scale), index).AdvanceWidth) + prev, hasPrev = index, true + } + return +} + +// recalc recalculates scale and bounds values from the font size, screen +// resolution and font metrics, and invalidates the glyph cache. +func (rgc *RasterGraphicContext) recalc() { + rgc.current.Scale = rgc.current.FontSize * float64(rgc.DPI) * EMRatio +} + +// SetFont sets the font used to draw text. +func (rgc *RasterGraphicContext) SetFont(font *truetype.Font) { + rgc.current.Font = font +} + +// GetFont returns the font used to draw text. +func (rgc *RasterGraphicContext) GetFont() *truetype.Font { + return rgc.current.Font +} + +// SetFontSize sets the font size in points (as in ``a 12 point font''). +func (rgc *RasterGraphicContext) SetFontSize(fontSize float64) { + rgc.current.FontSize = fontSize + rgc.recalc() +} + +func (rgc *RasterGraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) { + rgc.painter.SetColor(color) + rasterizer.Rasterize(rgc.painter) + rasterizer.Clear() + rgc.current.Path.Clear() +} + +// Stroke strokes the paths with the color specified by SetStrokeColor +func (rgc *RasterGraphicContext) Stroke(paths ...*Path) { + paths = append(paths, rgc.current.Path) + rgc.strokeRasterizer.UseNonZeroWinding = true + + stroker := NewLineStroker(rgc.current.Cap, rgc.current.Join, Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.strokeRasterizer}}) + stroker.HalfLineWidth = rgc.current.LineWidth / 2 + + var liner Flattener + if rgc.current.Dash != nil && len(rgc.current.Dash) > 0 { + liner = NewDashVertexConverter(rgc.current.Dash, rgc.current.DashOffset, stroker) + } else { + liner = stroker + } + for _, p := range paths { + Flatten(p, liner, rgc.current.Tr.GetScale()) + } + + rgc.paint(rgc.strokeRasterizer, rgc.current.StrokeColor) +} + +// Fill fills the paths with the color specified by SetFillColor +func (rgc *RasterGraphicContext) Fill(paths ...*Path) { + paths = append(paths, rgc.current.Path) + rgc.fillRasterizer.UseNonZeroWinding = rgc.current.FillRule == FillRuleWinding + + flattener := Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.fillRasterizer}} + for _, p := range paths { + Flatten(p, flattener, rgc.current.Tr.GetScale()) + } + + rgc.paint(rgc.fillRasterizer, rgc.current.FillColor) +} + +// FillStroke first fills the paths and than strokes them +func (rgc *RasterGraphicContext) FillStroke(paths ...*Path) { + paths = append(paths, rgc.current.Path) + rgc.fillRasterizer.UseNonZeroWinding = rgc.current.FillRule == FillRuleWinding + rgc.strokeRasterizer.UseNonZeroWinding = true + + flattener := Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.fillRasterizer}} + + stroker := NewLineStroker(rgc.current.Cap, rgc.current.Join, Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.strokeRasterizer}}) + stroker.HalfLineWidth = rgc.current.LineWidth / 2 + + var liner Flattener + if rgc.current.Dash != nil && len(rgc.current.Dash) > 0 { + liner = NewDashVertexConverter(rgc.current.Dash, rgc.current.DashOffset, stroker) + } else { + liner = stroker + } + + demux := DemuxFlattener{Flatteners: []Flattener{flattener, liner}} + for _, p := range paths { + Flatten(p, demux, rgc.current.Tr.GetScale()) + } + + // Fill + rgc.paint(rgc.fillRasterizer, rgc.current.FillColor) + // Stroke + rgc.paint(rgc.strokeRasterizer, rgc.current.StrokeColor) +} diff --git a/drawing/stack_graphic_context.go b/drawing/stack_graphic_context.go new file mode 100644 index 0000000..b03a37d --- /dev/null +++ b/drawing/stack_graphic_context.go @@ -0,0 +1,183 @@ +package drawing + +import ( + "image" + "image/color" + + "github.com/golang/freetype/truetype" +) + +// StackGraphicContext is a context that does thngs. +type StackGraphicContext struct { + current *ContextStack +} + +// ContextStack is a graphic context implementation. +type ContextStack struct { + Tr Matrix + Path *Path + LineWidth float64 + Dash []float64 + DashOffset float64 + StrokeColor color.Color + FillColor color.Color + FillRule FillRule + Cap LineCap + Join LineJoin + + FontSize float64 + Font *truetype.Font + + Scale float64 + + Previous *ContextStack +} + +// NewStackGraphicContext Create a new Graphic context from an image +func NewStackGraphicContext() *StackGraphicContext { + gc := &StackGraphicContext{} + gc.current = new(ContextStack) + gc.current.Tr = NewIdentityMatrix() + gc.current.Path = new(Path) + gc.current.LineWidth = 1.0 + gc.current.StrokeColor = image.Black + gc.current.FillColor = image.White + gc.current.Cap = RoundCap + gc.current.FillRule = FillRuleEvenOdd + gc.current.Join = RoundJoin + gc.current.FontSize = 10 + return gc +} + +func (gc *StackGraphicContext) GetMatrixTransform() Matrix { + return gc.current.Tr +} + +func (gc *StackGraphicContext) SetMatrixTransform(Tr Matrix) { + gc.current.Tr = Tr +} + +func (gc *StackGraphicContext) ComposeMatrixTransform(Tr Matrix) { + gc.current.Tr.Compose(Tr) +} + +func (gc *StackGraphicContext) Rotate(angle float64) { + gc.current.Tr.Rotate(angle) +} + +func (gc *StackGraphicContext) Translate(tx, ty float64) { + gc.current.Tr.Translate(tx, ty) +} + +func (gc *StackGraphicContext) Scale(sx, sy float64) { + gc.current.Tr.Scale(sx, sy) +} + +func (gc *StackGraphicContext) SetStrokeColor(c color.Color) { + gc.current.StrokeColor = c +} + +func (gc *StackGraphicContext) SetFillColor(c color.Color) { + gc.current.FillColor = c +} + +func (gc *StackGraphicContext) SetFillRule(f FillRule) { + gc.current.FillRule = f +} + +func (gc *StackGraphicContext) SetLineWidth(lineWidth float64) { + gc.current.LineWidth = lineWidth +} + +func (gc *StackGraphicContext) SetLineCap(cap LineCap) { + gc.current.Cap = cap +} + +func (gc *StackGraphicContext) SetLineJoin(join LineJoin) { + gc.current.Join = join +} + +func (gc *StackGraphicContext) SetLineDash(dash []float64, dashOffset float64) { + gc.current.Dash = dash + gc.current.DashOffset = dashOffset +} + +func (gc *StackGraphicContext) SetFontSize(fontSize float64) { + gc.current.FontSize = fontSize +} + +func (gc *StackGraphicContext) GetFontSize() float64 { + return gc.current.FontSize +} + +func (gc *StackGraphicContext) SetFont(f *truetype.Font) { + gc.current.Font = f +} + +func (gc *StackGraphicContext) GetFont() *truetype.Font { + return gc.current.Font +} + +func (gc *StackGraphicContext) BeginPath() { + gc.current.Path.Clear() +} + +func (gc *StackGraphicContext) IsEmpty() bool { + return gc.current.Path.IsEmpty() +} + +func (gc *StackGraphicContext) LastPoint() (float64, float64) { + return gc.current.Path.LastPoint() +} + +func (gc *StackGraphicContext) MoveTo(x, y float64) { + gc.current.Path.MoveTo(x, y) +} + +func (gc *StackGraphicContext) LineTo(x, y float64) { + gc.current.Path.LineTo(x, y) +} + +func (gc *StackGraphicContext) QuadCurveTo(cx, cy, x, y float64) { + gc.current.Path.QuadCurveTo(cx, cy, x, y) +} + +func (gc *StackGraphicContext) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) { + gc.current.Path.CubicCurveTo(cx1, cy1, cx2, cy2, x, y) +} + +func (gc *StackGraphicContext) ArcTo(cx, cy, rx, ry, startAngle, angle float64) { + gc.current.Path.ArcTo(cx, cy, rx, ry, startAngle, angle) +} + +func (gc *StackGraphicContext) Close() { + gc.current.Path.Close() +} + +func (gc *StackGraphicContext) Save() { + context := new(ContextStack) + context.FontSize = gc.current.FontSize + context.Font = gc.current.Font + context.LineWidth = gc.current.LineWidth + context.StrokeColor = gc.current.StrokeColor + context.FillColor = gc.current.FillColor + context.FillRule = gc.current.FillRule + context.Dash = gc.current.Dash + context.DashOffset = gc.current.DashOffset + context.Cap = gc.current.Cap + context.Join = gc.current.Join + context.Path = gc.current.Path.Copy() + context.Font = gc.current.Font + context.Scale = gc.current.Scale + copy(context.Tr[:], gc.current.Tr[:]) + context.Previous = gc.current + gc.current = context +} + +func (gc *StackGraphicContext) Restore() { + if gc.current.Previous != nil { + oldContext := gc.current + gc.current = gc.current.Previous + oldContext.Previous = nil + } +} diff --git a/drawing/stroker.go b/drawing/stroker.go new file mode 100644 index 0000000..e3ae070 --- /dev/null +++ b/drawing/stroker.go @@ -0,0 +1,85 @@ +// Copyright 2010 The draw2d Authors. All rights reserved. +// created: 13/12/2010 by Laurent Le Goff + +package drawing + +// NewLineStroker creates a new line stroker. +func NewLineStroker(c LineCap, j LineJoin, flattener Flattener) *LineStroker { + l := new(LineStroker) + l.Flattener = flattener + l.HalfLineWidth = 0.5 + l.Cap = c + l.Join = j + return l +} + +// LineStroker draws the stroke portion of a line. +type LineStroker struct { + Flattener Flattener + HalfLineWidth float64 + Cap LineCap + Join LineJoin + vertices []float64 + rewind []float64 + x, y, nx, ny float64 +} + +// MoveTo implements the path builder interface. +func (l *LineStroker) MoveTo(x, y float64) { + l.x, l.y = x, y +} + +// LineTo implements the path builder interface. +func (l *LineStroker) LineTo(x, y float64) { + l.line(l.x, l.y, x, y) +} + +// LineJoin implements the path builder interface. +func (l *LineStroker) LineJoin() {} + +func (l *LineStroker) line(x1, y1, x2, y2 float64) { + dx := (x2 - x1) + dy := (y2 - y1) + d := vectorDistance(dx, dy) + if d != 0 { + nx := dy * l.HalfLineWidth / d + ny := -(dx * l.HalfLineWidth / d) + l.appendVertex(x1+nx, y1+ny, x2+nx, y2+ny, x1-nx, y1-ny, x2-nx, y2-ny) + l.x, l.y, l.nx, l.ny = x2, y2, nx, ny + } +} + +// Close implements the path builder interface. +func (l *LineStroker) Close() { + if len(l.vertices) > 1 { + l.appendVertex(l.vertices[0], l.vertices[1], l.rewind[0], l.rewind[1]) + } +} + +// End implements the path builder interface. +func (l *LineStroker) End() { + if len(l.vertices) > 1 { + l.Flattener.MoveTo(l.vertices[0], l.vertices[1]) + for i, j := 2, 3; j < len(l.vertices); i, j = i+2, j+2 { + l.Flattener.LineTo(l.vertices[i], l.vertices[j]) + } + } + for i, j := len(l.rewind)-2, len(l.rewind)-1; j > 0; i, j = i-2, j-2 { + l.Flattener.LineTo(l.rewind[i], l.rewind[j]) + } + if len(l.vertices) > 1 { + l.Flattener.LineTo(l.vertices[0], l.vertices[1]) + } + l.Flattener.End() + // reinit vertices + l.vertices = l.vertices[0:0] + l.rewind = l.rewind[0:0] + l.x, l.y, l.nx, l.ny = 0, 0, 0, 0 + +} + +func (l *LineStroker) appendVertex(vertices ...float64) { + s := len(vertices) / 2 + l.vertices = append(l.vertices, vertices[:s]...) + l.rewind = append(l.rewind, vertices[s:]...) +} diff --git a/drawing/text.go b/drawing/text.go new file mode 100644 index 0000000..52f6349 --- /dev/null +++ b/drawing/text.go @@ -0,0 +1,74 @@ +// Copyright 2010 The draw2d Authors. All rights reserved. +// created: 13/12/2010 by Laurent Le Goff + +package drawing + +import ( + "github.com/golang/freetype/truetype" + "golang.org/x/image/math/fixed" +) + +// DrawContour draws the given closed contour at the given sub-pixel offset. +func DrawContour(path PathBuilder, ps []truetype.Point, dx, dy float64) { + if len(ps) == 0 { + return + } + startX, startY := pointToF64Point(ps[0]) + path.MoveTo(startX+dx, startY+dy) + q0X, q0Y, on0 := startX, startY, true + for _, p := range ps[1:] { + qX, qY := pointToF64Point(p) + on := p.Flags&0x01 != 0 + if on { + if on0 { + path.LineTo(qX+dx, qY+dy) + } else { + path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy) + } + } else { + if on0 { + // No-op. + } else { + midX := (q0X + qX) / 2 + midY := (q0Y + qY) / 2 + path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy) + } + } + q0X, q0Y, on0 = qX, qY, on + } + // Close the curve. + if on0 { + path.LineTo(startX+dx, startY+dy) + } else { + path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy) + } +} + +// FontExtents contains font metric information. +type FontExtents struct { + // Ascent is the distance that the text + // extends above the baseline. + Ascent float64 + + // Descent is the distance that the text + // extends below the baseline. The descent + // is given as a negative value. + Descent float64 + + // Height is the distance from the lowest + // descending point to the highest ascending + // point. + Height float64 +} + +// Extents returns the FontExtents for a font. +// TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro +func Extents(font *truetype.Font, size float64) FontExtents { + bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm())) + scale := size / float64(font.FUnitsPerEm()) + return FontExtents{ + Ascent: float64(bounds.Max.Y) * scale, + Descent: float64(bounds.Min.Y) * scale, + Height: float64(bounds.Max.Y-bounds.Min.Y) * scale, + } +} diff --git a/drawing/transformer.go b/drawing/transformer.go new file mode 100644 index 0000000..4e31720 --- /dev/null +++ b/drawing/transformer.go @@ -0,0 +1,39 @@ +// Copyright 2010 The draw2d Authors. All rights reserved. +// created: 13/12/2010 by Laurent Le Goff + +package drawing + +// Transformer apply the Matrix transformation tr +type Transformer struct { + Tr Matrix + Flattener Flattener +} + +// MoveTo implements the path builder interface. +func (t Transformer) MoveTo(x, y float64) { + u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4] + v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5] + t.Flattener.MoveTo(u, v) +} + +// LineTo implements the path builder interface. +func (t Transformer) LineTo(x, y float64) { + u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4] + v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5] + t.Flattener.LineTo(u, v) +} + +// LineJoin implements the path builder interface. +func (t Transformer) LineJoin() { + t.Flattener.LineJoin() +} + +// Close implements the path builder interface. +func (t Transformer) Close() { + t.Flattener.Close() +} + +// End implements the path builder interface. +func (t Transformer) End() { + t.Flattener.End() +} diff --git a/drawing/util.go b/drawing/util.go new file mode 100644 index 0000000..c4c49c1 --- /dev/null +++ b/drawing/util.go @@ -0,0 +1,56 @@ +package drawing + +import ( + "math" + + "golang.org/x/image/math/fixed" + + "github.com/golang/freetype/raster" + "github.com/golang/freetype/truetype" +) + +func abs(i int) int { + if i < 0 { + return -i + } + return i +} + +func distance(x1, y1, x2, y2 float64) float64 { + return vectorDistance(x2-x1, y2-y1) +} + +func vectorDistance(dx, dy float64) float64 { + return float64(math.Sqrt(dx*dx + dy*dy)) +} + +func toFtCap(c LineCap) raster.Capper { + switch c { + case RoundCap: + return raster.RoundCapper + case ButtCap: + return raster.ButtCapper + case SquareCap: + return raster.SquareCapper + } + return raster.RoundCapper +} + +func toFtJoin(j LineJoin) raster.Joiner { + switch j { + case RoundJoin: + return raster.RoundJoiner + case BevelJoin: + return raster.BevelJoiner + } + return raster.RoundJoiner +} + +func pointToF64Point(p truetype.Point) (x, y float64) { + return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y) +} + +func fUnitsToFloat64(x fixed.Int26_6) float64 { + scaled := x << 2 + return float64(scaled/256) + float64(scaled%256)/256.0 +} diff --git a/raster_renderer.go b/raster_renderer.go index 331c0a7..97af3f8 100644 --- a/raster_renderer.go +++ b/raster_renderer.go @@ -5,31 +5,38 @@ import ( "image/color" "image/png" "io" - - "golang.org/x/image/font" + "math" "github.com/golang/freetype/truetype" - drawing "github.com/llgcode/draw2d/draw2dimg" + "github.com/wcharczuk/go-chart/drawing" ) // PNG returns a new png/raster renderer. -func PNG(width, height int) Renderer { +func PNG(width, height int) (Renderer, error) { i := image.NewRGBA(image.Rect(0, 0, width, height)) - return &rasterRenderer{ - i: i, - gc: drawing.NewGraphicContext(i), + gc, err := drawing.NewRasterGraphicContext(i) + if err == nil { + return &rasterRenderer{ + i: i, + gc: gc, + }, nil } + return nil, err } // rasterRenderer renders chart commands to a bitmap. type rasterRenderer struct { i *image.RGBA - gc *drawing.GraphicContext - fc *font.Drawer + gc *drawing.RasterGraphicContext - font *truetype.Font - fontColor color.RGBA fontSize float64 + fontColor color.RGBA + f *truetype.Font +} + +// SetDPI implements the interface method. +func (rr *rasterRenderer) SetDPI(dpi float64) { + rr.gc.SetDPI(dpi) } // SetStrokeColor implements the interface method. @@ -92,7 +99,7 @@ func (rr *rasterRenderer) Circle(radius float64, x, y int) { // SetFont implements the interface method. func (rr *rasterRenderer) SetFont(f *truetype.Font) { - rr.font = f + rr.f = f rr.gc.SetFont(f) } @@ -115,25 +122,20 @@ func (rr *rasterRenderer) Text(body string, x, y int) { rr.gc.Fill() } -// MeasureText uses the truetype font drawer to measure the width of text. -func (rr *rasterRenderer) MeasureText(body string) int { - if rr.fc == nil && rr.font != nil { - rr.fc = &font.Drawer{ - Face: truetype.NewFace(rr.font, &truetype.Options{ - DPI: DefaultDPI, - Size: rr.fontSize, - }), - } +// MeasureText returns the height and width in pixels of a string. +func (rr *rasterRenderer) MeasureText(body string) (width int, height int) { + l, t, r, b, err := rr.gc.GetStringBounds(body) + if err != nil { + return } - if rr.fc != nil { - dimensions := rr.fc.MeasureString(body) - return dimensions.Floor() - } - return 0 + dw := r - l + dh := b - t + width = int(math.Ceil(dw * (4.0 / 3.0))) + height = int(math.Ceil(dh * (4.0 / 3.0))) + return } // Save implements the interface method. func (rr *rasterRenderer) Save(w io.Writer) error { - return png.Encode(w, rr.i) } diff --git a/renderer.go b/renderer.go index 48473af..9ad8a17 100644 --- a/renderer.go +++ b/renderer.go @@ -8,10 +8,13 @@ import ( ) // RendererProvider is a function that returns a renderer. -type RendererProvider func(int, int) Renderer +type RendererProvider func(int, int) (Renderer, error) // Renderer represents the basic methods required to draw a chart. type Renderer interface { + // SetDPI sets the DPI for the renderer. + SetDPI(dpi float64) + // SetStrokeColor sets the current stroke color. SetStrokeColor(color.RGBA) @@ -56,7 +59,7 @@ type Renderer interface { Text(body string, x, y int) // MeasureText measures text. - MeasureText(body string) int + MeasureText(body string) (width int, height int) // Save writes the image to the given writer. Save(w io.Writer) error diff --git a/style.go b/style.go index cfdbd6f..d3b6d5b 100644 --- a/style.go +++ b/style.go @@ -102,7 +102,7 @@ func (s Style) SVG() string { fontSizeText := "" if fs != 0 { - fontSizeText = "font-size:" + fmt.Sprintf("%.1f", fs) + fontSizeText = "font-size:" + fmt.Sprintf("%.1fpx", fs) } if !ColorIsZero(fnc) { diff --git a/testserver/main.go b/testserver/main.go index 0d26299..a428135 100644 --- a/testserver/main.go +++ b/testserver/main.go @@ -2,6 +2,7 @@ package main import ( "log" + "net/http" "github.com/wcharczuk/go-chart" "github.com/wcharczuk/go-web" @@ -49,6 +50,7 @@ func main() { if err != nil { return rc.API().InternalError(err) } + rc.Response.WriteHeader(http.StatusOK) return nil }) log.Fatal(app.Start()) diff --git a/util.go b/util.go index dae980d..4aab867 100644 --- a/util.go +++ b/util.go @@ -66,3 +66,7 @@ func Slices(count int, total float64) []float64 { } return values } + +func flf(v float64) string { + return fmt.Sprintf("%.2f", v) +} diff --git a/vector_renderer.go b/vector_renderer.go index be3ed8f..83f1705 100644 --- a/vector_renderer.go +++ b/vector_renderer.go @@ -5,37 +5,44 @@ import ( "fmt" "image/color" "io" + "math" "strings" "golang.org/x/image/font" - "github.com/ajstarks/svgo" "github.com/golang/freetype/truetype" ) // SVG returns a new png/raster renderer. -func SVG(width, height int) Renderer { +func SVG(width, height int) (Renderer, error) { buffer := bytes.NewBuffer([]byte{}) - canvas := svg.New(buffer) + canvas := newCanvas(buffer) canvas.Start(width, height) return &vectorRenderer{ b: buffer, c: canvas, s: &Style{}, p: []string{}, - } + }, nil } // vectorRenderer renders chart commands to a bitmap. type vectorRenderer struct { - b *bytes.Buffer - c *svg.SVG - s *Style - f *truetype.Font - p []string - fc *font.Drawer + dpi float64 + b *bytes.Buffer + c *canvas + s *Style + f *truetype.Font + p []string + fc *font.Drawer } +// SetDPI implements the interface method. +func (vr *vectorRenderer) SetDPI(dpi float64) { + vr.dpi = dpi +} + +// SetStrokeColor implements the interface method. func (vr *vectorRenderer) SetStrokeColor(c color.RGBA) { vr.s.StrokeColor = c } @@ -130,20 +137,18 @@ func (vr *vectorRenderer) Text(body string, x, y int) { } // MeasureText uses the truetype font drawer to measure the width of text. -func (vr *vectorRenderer) MeasureText(body string) int { - if vr.fc == nil && vr.f != nil { +func (vr *vectorRenderer) MeasureText(body string) (width, height int) { + if vr.f != nil { vr.fc = &font.Drawer{ Face: truetype.NewFace(vr.f, &truetype.Options{ - DPI: DefaultDPI, + DPI: vr.dpi, Size: vr.s.FontSize, }), } + width = vr.fc.MeasureString(body).Ceil() + height = int(math.Ceil(vr.s.FontSize)) } - if vr.fc != nil { - dimensions := vr.fc.MeasureString(body) - return dimensions.Floor() - } - return 0 + return } func (vr *vectorRenderer) Save(w io.Writer) error { @@ -151,3 +156,49 @@ func (vr *vectorRenderer) Save(w io.Writer) error { _, err := w.Write(vr.b.Bytes()) return err } + +func newCanvas(w io.Writer) *canvas { + return &canvas{ + w: w, + } +} + +type canvas struct { + w io.Writer + width int + height int +} + +func (c *canvas) Start(width, height int) { + c.width = width + c.height = height + c.w.Write([]byte(fmt.Sprintf(`\n`, c.width, c.height))) +} + +func (c *canvas) Path(d string, style ...string) { + if len(style) > 0 { + c.w.Write([]byte(fmt.Sprintf(`\n`, d, style[0]))) + } else { + c.w.Write([]byte(fmt.Sprintf(`\n`, d))) + } +} + +func (c *canvas) Text(x, y int, body string, style ...string) { + if len(style) > 0 { + c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, style[0], body))) + } else { + c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, body))) + } +} + +func (c *canvas) Circle(x, y, r int, style ...string) { + if len(style) > 0 { + c.w.Write([]byte(fmt.Sprintf(``, x, y, r, style[0]))) + } else { + c.w.Write([]byte(fmt.Sprintf(``, x, y, r))) + } +} + +func (c *canvas) End() { + c.w.Write([]byte("")) +}