Merge pull request #137 from Drahoslav7/feature/svg-context
Feature/svg context
This commit is contained in:
commit
bc151d5e2c
17 changed files with 1094 additions and 10 deletions
16
draw2d.go
16
draw2d.go
|
@ -128,6 +128,14 @@ const (
|
||||||
SquareCap
|
SquareCap
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (cap LineCap) String() string {
|
||||||
|
return map[LineCap]string{
|
||||||
|
RoundCap: "round",
|
||||||
|
ButtCap: "cap",
|
||||||
|
SquareCap: "square",
|
||||||
|
}[cap]
|
||||||
|
}
|
||||||
|
|
||||||
// LineJoin is the style of segments joint
|
// LineJoin is the style of segments joint
|
||||||
type LineJoin int
|
type LineJoin int
|
||||||
|
|
||||||
|
@ -140,6 +148,14 @@ const (
|
||||||
MiterJoin
|
MiterJoin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (join LineJoin) String() string {
|
||||||
|
return map[LineJoin]string{
|
||||||
|
RoundJoin: "round",
|
||||||
|
BevelJoin: "bevel",
|
||||||
|
MiterJoin: "miter",
|
||||||
|
}[join]
|
||||||
|
}
|
||||||
|
|
||||||
// StrokeStyle keeps stroke style attributes
|
// StrokeStyle keeps stroke style attributes
|
||||||
// that is used by the Stroke method of a Drawer
|
// that is used by the Stroke method of a Drawer
|
||||||
type StrokeStyle struct {
|
type StrokeStyle struct {
|
||||||
|
|
|
@ -124,7 +124,7 @@ type GraphicContext struct {
|
||||||
painter *Painter
|
painter *Painter
|
||||||
fillRasterizer *raster.Rasterizer
|
fillRasterizer *raster.Rasterizer
|
||||||
strokeRasterizer *raster.Rasterizer
|
strokeRasterizer *raster.Rasterizer
|
||||||
FontCache draw2d.FontCache
|
FontCache draw2d.FontCache
|
||||||
glyphCache draw2dbase.GlyphCache
|
glyphCache draw2dbase.GlyphCache
|
||||||
glyphBuf *truetype.GlyphBuf
|
glyphBuf *truetype.GlyphBuf
|
||||||
DPI int
|
DPI int
|
||||||
|
|
175
draw2dsvg/converters.go
Normal file
175
draw2dsvg/converters.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
// Copyright 2015 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 16/12/2017 by Drahoslav Bednářpackage draw2dsvg
|
||||||
|
|
||||||
|
package draw2dsvg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"github.com/llgcode/draw2d"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toSvgRGBA(c color.Color) string {
|
||||||
|
r, g, b, a := c.RGBA()
|
||||||
|
r, g, b, a = r>>8, g>>8, b>>8, a>>8
|
||||||
|
if a == 255 {
|
||||||
|
return optiSprintf("#%02X%02X%02X", r, g, b)
|
||||||
|
}
|
||||||
|
return optiSprintf("rgba(%v,%v,%v,%f)", r, g, b, float64(a)/255)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSvgLength(l float64) string {
|
||||||
|
if math.IsInf(l, 1) {
|
||||||
|
return "100%"
|
||||||
|
}
|
||||||
|
return optiSprintf("%f", l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSvgArray(nums []float64) string {
|
||||||
|
arr := make([]string, len(nums))
|
||||||
|
for i, num := range nums {
|
||||||
|
arr[i] = optiSprintf("%f", num)
|
||||||
|
}
|
||||||
|
return strings.Join(arr, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSvgFillRule(rule draw2d.FillRule) string {
|
||||||
|
return map[draw2d.FillRule]string{
|
||||||
|
draw2d.FillRuleEvenOdd: "evenodd",
|
||||||
|
draw2d.FillRuleWinding: "nonzero",
|
||||||
|
}[rule]
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSvgPathDesc(p *draw2d.Path) string {
|
||||||
|
parts := make([]string, len(p.Components))
|
||||||
|
ps := p.Points
|
||||||
|
for i, cmp := range p.Components {
|
||||||
|
switch cmp {
|
||||||
|
case draw2d.MoveToCmp:
|
||||||
|
parts[i] = optiSprintf("M %f,%f", ps[0], ps[1])
|
||||||
|
ps = ps[2:]
|
||||||
|
case draw2d.LineToCmp:
|
||||||
|
parts[i] = optiSprintf("L %f,%f", ps[0], ps[1])
|
||||||
|
ps = ps[2:]
|
||||||
|
case draw2d.QuadCurveToCmp:
|
||||||
|
parts[i] = optiSprintf("Q %f,%f %f,%f", ps[0], ps[1], ps[2], ps[3])
|
||||||
|
ps = ps[4:]
|
||||||
|
case draw2d.CubicCurveToCmp:
|
||||||
|
parts[i] = optiSprintf("C %f,%f %f,%f %f,%f", ps[0], ps[1], ps[2], ps[3], ps[4], ps[5])
|
||||||
|
ps = ps[6:]
|
||||||
|
case draw2d.ArcToCmp:
|
||||||
|
cx, cy := ps[0], ps[1] // center
|
||||||
|
rx, ry := ps[2], ps[3] // radii
|
||||||
|
fi := ps[4] + ps[5] // startAngle + angle
|
||||||
|
|
||||||
|
// compute endpoint
|
||||||
|
sinfi, cosfi := math.Sincos(fi)
|
||||||
|
nom := math.Hypot(ry*cosfi, rx*sinfi)
|
||||||
|
x := cx + (rx*ry*cosfi)/nom
|
||||||
|
y := cy + (rx*ry*sinfi)/nom
|
||||||
|
|
||||||
|
// compute large and sweep flags
|
||||||
|
large := 0
|
||||||
|
sweep := 0
|
||||||
|
if math.Abs(ps[5]) > math.Pi {
|
||||||
|
large = 1
|
||||||
|
}
|
||||||
|
if !math.Signbit(ps[5]) {
|
||||||
|
sweep = 1
|
||||||
|
}
|
||||||
|
// dirty hack to ensure whole arc is drawn
|
||||||
|
// if start point equals end point
|
||||||
|
if sweep == 1 {
|
||||||
|
x += 0.01 * sinfi
|
||||||
|
y += 0.01 * -cosfi
|
||||||
|
} else {
|
||||||
|
x += 0.01 * sinfi
|
||||||
|
y += 0.01 * cosfi
|
||||||
|
}
|
||||||
|
|
||||||
|
// rx ry x-axis-rotation large-arc-flag sweep-flag x y
|
||||||
|
parts[i] = optiSprintf("A %f %f %v %v %v %F %F",
|
||||||
|
rx, ry, 0, large, sweep, x, y,
|
||||||
|
)
|
||||||
|
ps = ps[6:]
|
||||||
|
case draw2d.CloseCmp:
|
||||||
|
parts[i] = "Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSvgTransform(mat draw2d.Matrix) string {
|
||||||
|
if mat.IsIdentity() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if mat.IsTranslation() {
|
||||||
|
x, y := mat.GetTranslation()
|
||||||
|
return optiSprintf("translate(%f,%f)", x, y)
|
||||||
|
}
|
||||||
|
return optiSprintf("matrix(%f,%f,%f,%f,%f,%f)",
|
||||||
|
mat[0], mat[1], mat[2], mat[3], mat[4], mat[5],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageToSvgHref(image image.Image) string {
|
||||||
|
out := "data:image/png;base64,"
|
||||||
|
pngBuf := &bytes.Buffer{}
|
||||||
|
png.Encode(pngBuf, image)
|
||||||
|
out += base64.RawStdEncoding.EncodeToString(pngBuf.Bytes())
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the same thing as fmt.Sprintf
|
||||||
|
// except it uses the optimal precition for floats: (0-3) for f and (0-6) for F
|
||||||
|
// eg.:
|
||||||
|
// optiSprintf("%f", 3.0) => fmt.Sprintf("%.0f", 3.0)
|
||||||
|
// optiSprintf("%f", 3.33) => fmt.Sprintf("%.2f", 3.33)
|
||||||
|
// optiSprintf("%f", 3.3001) => fmt.Sprintf("%.1f", 3.3001)
|
||||||
|
// optiSprintf("%f", 3.333333333333333) => fmt.Sprintf("%.3f", 3.333333333333333)
|
||||||
|
// optiSprintf("%F", 3.333333333333333) => fmt.Sprintf("%.6f", 3.333333333333333)
|
||||||
|
func optiSprintf(format string, a ...interface{}) string {
|
||||||
|
chunks := strings.Split(format, "%")
|
||||||
|
newChunks := make([]string, len(chunks))
|
||||||
|
for i, chunk := range chunks {
|
||||||
|
if i != 0 {
|
||||||
|
verb := chunk[0]
|
||||||
|
if verb == 'f' || verb == 'F' {
|
||||||
|
num := a[i-1].(float64)
|
||||||
|
p := strconv.Itoa(getPrec(num, verb == 'F'))
|
||||||
|
chunk = strings.Replace(chunk, string(verb), "."+p+"f", 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newChunks[i] = chunk
|
||||||
|
}
|
||||||
|
format = strings.Join(newChunks, "%")
|
||||||
|
return fmt.Sprintf(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO needs test, since it is not quiet right
|
||||||
|
func getPrec(num float64, better bool) int {
|
||||||
|
max := 3
|
||||||
|
eps := 0.0005
|
||||||
|
if better {
|
||||||
|
max = 6
|
||||||
|
eps = 0.0000005
|
||||||
|
}
|
||||||
|
prec := 0
|
||||||
|
for math.Mod(num, 1) > eps {
|
||||||
|
num *= 10
|
||||||
|
eps *= 10
|
||||||
|
prec++
|
||||||
|
}
|
||||||
|
|
||||||
|
if max < prec {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return prec
|
||||||
|
}
|
11
draw2dsvg/doc.go
Normal file
11
draw2dsvg/doc.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright 2015 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 16/12/2017 by Drahoslav Bednář
|
||||||
|
|
||||||
|
// Package draw2svg provides a graphic context that can draw
|
||||||
|
// vector graphics and text on svg file.
|
||||||
|
//
|
||||||
|
// Quick Start
|
||||||
|
// The following Go code geneartes a simple drawing and saves it
|
||||||
|
// to a svg document:
|
||||||
|
// TODO
|
||||||
|
package draw2dsvg
|
22
draw2dsvg/fileutil.go
Normal file
22
draw2dsvg/fileutil.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package draw2dsvg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
_ "errors"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SaveToSvgFile(filePath string, svg *Svg) error {
|
||||||
|
f, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
f.Write([]byte(xml.Header))
|
||||||
|
encoder := xml.NewEncoder(f)
|
||||||
|
encoder.Indent("", "\t")
|
||||||
|
err = encoder.Encode(svg)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
412
draw2dsvg/gc.go
Normal file
412
draw2dsvg/gc.go
Normal file
|
@ -0,0 +1,412 @@
|
||||||
|
// Copyright 2015 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 16/12/2017 by Drahoslav Bednář
|
||||||
|
|
||||||
|
package draw2dsvg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/llgcode/draw2d"
|
||||||
|
"github.com/llgcode/draw2d/draw2dbase"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
"image"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type drawType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
filled drawType = 1 << iota
|
||||||
|
stroked
|
||||||
|
)
|
||||||
|
|
||||||
|
// GraphicContext implements the draw2d.GraphicContext interface
|
||||||
|
// It provides draw2d with a svg backend
|
||||||
|
type GraphicContext struct {
|
||||||
|
*draw2dbase.StackGraphicContext
|
||||||
|
FontCache draw2d.FontCache
|
||||||
|
glyphCache draw2dbase.GlyphCache
|
||||||
|
glyphBuf *truetype.GlyphBuf
|
||||||
|
svg *Svg
|
||||||
|
DPI int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGraphicContext(svg *Svg) *GraphicContext {
|
||||||
|
gc := &GraphicContext{
|
||||||
|
draw2dbase.NewStackGraphicContext(),
|
||||||
|
draw2d.GetGlobalFontCache(),
|
||||||
|
draw2dbase.NewGlyphCache(),
|
||||||
|
&truetype.GlyphBuf{},
|
||||||
|
svg,
|
||||||
|
92,
|
||||||
|
}
|
||||||
|
return gc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear fills the current canvas with a default transparent color
|
||||||
|
func (gc *GraphicContext) Clear() {
|
||||||
|
gc.svg.Groups = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stroke strokes the paths with the color specified by SetStrokeColor
|
||||||
|
func (gc *GraphicContext) Stroke(paths ...*draw2d.Path) {
|
||||||
|
gc.drawPaths(stroked, paths...)
|
||||||
|
gc.Current.Path.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill fills the paths with the color specified by SetFillColor
|
||||||
|
func (gc *GraphicContext) Fill(paths ...*draw2d.Path) {
|
||||||
|
gc.drawPaths(filled, paths...)
|
||||||
|
gc.Current.Path.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillStroke first fills the paths and than strokes them
|
||||||
|
func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) {
|
||||||
|
gc.drawPaths(filled|stroked, paths...)
|
||||||
|
gc.Current.Path.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillString draws the text at point (0, 0)
|
||||||
|
func (gc *GraphicContext) FillString(text string) (cursor float64) {
|
||||||
|
return gc.FillStringAt(text, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillStringAt draws the text at the specified point (x, y)
|
||||||
|
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) {
|
||||||
|
return gc.drawString(text, filled, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrokeString draws the contour of the text at point (0, 0)
|
||||||
|
func (gc *GraphicContext) StrokeString(text string) (cursor float64) {
|
||||||
|
return gc.StrokeStringAt(text, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||||
|
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) {
|
||||||
|
return gc.drawString(text, stroked, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the context and push it to the context stack
|
||||||
|
func (gc *GraphicContext) Save() {
|
||||||
|
gc.StackGraphicContext.Save()
|
||||||
|
// TODO use common transformation group for multiple elements
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore remove the current context and restore the last one
|
||||||
|
func (gc *GraphicContext) Restore() {
|
||||||
|
gc.StackGraphicContext.Restore()
|
||||||
|
// TODO use common transformation group for multiple elements
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GraphicContext) SetDPI(dpi int) {
|
||||||
|
gc.DPI = dpi
|
||||||
|
gc.recalc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GraphicContext) GetDPI() int {
|
||||||
|
return gc.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFont sets the font used to draw text.
|
||||||
|
func (gc *GraphicContext) SetFont(font *truetype.Font) {
|
||||||
|
gc.Current.Font = font
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFontSize sets the font size in points (as in “a 12 point font”).
|
||||||
|
func (gc *GraphicContext) SetFontSize(fontSize float64) {
|
||||||
|
gc.Current.FontSize = fontSize
|
||||||
|
gc.recalc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawImage draws the raster image in the current canvas
|
||||||
|
func (gc *GraphicContext) DrawImage(image image.Image) {
|
||||||
|
bounds := image.Bounds()
|
||||||
|
|
||||||
|
svgImage := &Image{Href: imageToSvgHref(image)}
|
||||||
|
svgImage.X = float64(bounds.Min.X)
|
||||||
|
svgImage.Y = float64(bounds.Min.Y)
|
||||||
|
svgImage.Width = toSvgLength(float64(bounds.Max.X - bounds.Min.X))
|
||||||
|
svgImage.Height = toSvgLength(float64(bounds.Max.Y - bounds.Min.Y))
|
||||||
|
gc.newGroup(0).Image = svgImage
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearRect fills the specified rectangle with a default transparent color
|
||||||
|
func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) {
|
||||||
|
mask := gc.newMask(x1, y1, x2-x1, y2-y1)
|
||||||
|
|
||||||
|
newGroup := &Group{
|
||||||
|
Groups: gc.svg.Groups,
|
||||||
|
Mask: "url(#" + mask.Id + ")",
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace groups with new masked group
|
||||||
|
gc.svg.Groups = []*Group{newGroup}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE following two functions and soe other further below copied from dwra2d{img|gl}
|
||||||
|
// TODO move them all to common draw2dbase?
|
||||||
|
|
||||||
|
// 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 (gc *GraphicContext) CreateStringPath(s string, x, y float64) (cursor float64) {
|
||||||
|
f, err := gc.loadCurrentFont()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
startx := x
|
||||||
|
prev, hasPrev := truetype.Index(0), false
|
||||||
|
for _, rune := range s {
|
||||||
|
index := f.Index(rune)
|
||||||
|
if hasPrev {
|
||||||
|
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
||||||
|
}
|
||||||
|
err := gc.drawGlyph(index, x, y)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return startx - x
|
||||||
|
}
|
||||||
|
x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth)
|
||||||
|
prev, hasPrev = index, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return x - startx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStringBounds returns the approximate pixel bounds of the string s at x, y.
|
||||||
|
// 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.
|
||||||
|
// Therefore the top and left coordinates may well be negative.
|
||||||
|
func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) {
|
||||||
|
f, err := gc.loadCurrentFont()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 0, 0, 0, 0
|
||||||
|
}
|
||||||
|
if gc.Current.Scale == 0 {
|
||||||
|
panic("zero scale")
|
||||||
|
}
|
||||||
|
top, left, bottom, right = 10e6, 10e6, -10e6, -10e6
|
||||||
|
cursor := 0.0
|
||||||
|
prev, hasPrev := truetype.Index(0), false
|
||||||
|
for _, rune := range s {
|
||||||
|
index := f.Index(rune)
|
||||||
|
if hasPrev {
|
||||||
|
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
||||||
|
}
|
||||||
|
if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), index, font.HintingNone); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 0, 0, 0, 0
|
||||||
|
}
|
||||||
|
e0 := 0
|
||||||
|
for _, e1 := range gc.glyphBuf.Ends {
|
||||||
|
ps := gc.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth)
|
||||||
|
prev, hasPrev = index, true
|
||||||
|
}
|
||||||
|
return left, top, right, bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
// private funcitons
|
||||||
|
|
||||||
|
func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) {
|
||||||
|
// create elements
|
||||||
|
svgPath := Path{}
|
||||||
|
group := gc.newGroup(drawType)
|
||||||
|
|
||||||
|
// set attrs to path element
|
||||||
|
paths = append(paths, gc.Current.Path)
|
||||||
|
svgPathsDesc := make([]string, len(paths))
|
||||||
|
// multiple pathes has to be joined to single svg path description
|
||||||
|
// because fill-rule wont work for whole group as excepted
|
||||||
|
for i, path := range paths {
|
||||||
|
svgPathsDesc[i] = toSvgPathDesc(path)
|
||||||
|
}
|
||||||
|
svgPath.Desc = strings.Join(svgPathsDesc, " ")
|
||||||
|
|
||||||
|
// attach to group
|
||||||
|
group.Paths = []*Path{&svgPath}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add text element to svg and returns its expected width
|
||||||
|
func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float64) float64 {
|
||||||
|
switch gc.svg.FontMode {
|
||||||
|
case PathFontMode:
|
||||||
|
w := gc.CreateStringPath(text, x, y)
|
||||||
|
gc.drawPaths(drawType)
|
||||||
|
gc.Current.Path.Clear()
|
||||||
|
return w
|
||||||
|
case SvgFontMode:
|
||||||
|
gc.embedSvgFont(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create elements
|
||||||
|
svgText := Text{}
|
||||||
|
group := gc.newGroup(drawType)
|
||||||
|
|
||||||
|
// set attrs to text element
|
||||||
|
svgText.Text = text
|
||||||
|
svgText.FontSize = gc.Current.FontSize
|
||||||
|
svgText.X = x
|
||||||
|
svgText.Y = y
|
||||||
|
svgText.FontFamily = gc.Current.FontData.Name
|
||||||
|
|
||||||
|
// attach to group
|
||||||
|
group.Texts = []*Text{&svgText}
|
||||||
|
left, _, right, _ := gc.GetStringBounds(text)
|
||||||
|
return right - left
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates new group from current context
|
||||||
|
// attach it to svg and return
|
||||||
|
func (gc *GraphicContext) newGroup(drawType drawType) *Group {
|
||||||
|
group := Group{}
|
||||||
|
// set attrs to group
|
||||||
|
if drawType&stroked == stroked {
|
||||||
|
group.Stroke = toSvgRGBA(gc.Current.StrokeColor)
|
||||||
|
group.StrokeWidth = toSvgLength(gc.Current.LineWidth)
|
||||||
|
group.StrokeLinecap = gc.Current.Cap.String()
|
||||||
|
group.StrokeLinejoin = gc.Current.Join.String()
|
||||||
|
if len(gc.Current.Dash) > 0 {
|
||||||
|
group.StrokeDasharray = toSvgArray(gc.Current.Dash)
|
||||||
|
group.StrokeDashoffset = toSvgLength(gc.Current.DashOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if drawType&filled == filled {
|
||||||
|
group.Fill = toSvgRGBA(gc.Current.FillColor)
|
||||||
|
group.FillRule = toSvgFillRule(gc.Current.FillRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Transform = toSvgTransform(gc.Current.Tr)
|
||||||
|
|
||||||
|
// attach
|
||||||
|
gc.svg.Groups = append(gc.svg.Groups, &group)
|
||||||
|
|
||||||
|
return &group
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates new mask attached to svg
|
||||||
|
func (gc *GraphicContext) newMask(x, y, width, height int) *Mask {
|
||||||
|
mask := &Mask{}
|
||||||
|
mask.X = float64(x)
|
||||||
|
mask.Y = float64(y)
|
||||||
|
mask.Width = toSvgLength(float64(width))
|
||||||
|
mask.Height = toSvgLength(float64(height))
|
||||||
|
|
||||||
|
// attach mask
|
||||||
|
gc.svg.Masks = append(gc.svg.Masks, mask)
|
||||||
|
mask.Id = "mask-" + strconv.Itoa(len(gc.svg.Masks))
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embed svg font definition to svg tree itself
|
||||||
|
// Or update existing if already exists for curent font data
|
||||||
|
func (gc *GraphicContext) embedSvgFont(text string) *Font {
|
||||||
|
fontName := gc.Current.FontData.Name
|
||||||
|
gc.loadCurrentFont()
|
||||||
|
|
||||||
|
// find or create font Element
|
||||||
|
svgFont := (*Font)(nil)
|
||||||
|
for _, font := range gc.svg.Fonts {
|
||||||
|
if font.Name == fontName {
|
||||||
|
svgFont = font
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if svgFont == nil {
|
||||||
|
// create new
|
||||||
|
svgFont = &Font{}
|
||||||
|
// and attach
|
||||||
|
gc.svg.Fonts = append(gc.svg.Fonts, svgFont)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill with glyphs
|
||||||
|
|
||||||
|
gc.Save()
|
||||||
|
defer gc.Restore()
|
||||||
|
gc.SetFontSize(2048)
|
||||||
|
defer gc.SetDPI(gc.GetDPI())
|
||||||
|
gc.SetDPI(92)
|
||||||
|
filling:
|
||||||
|
for _, rune := range text {
|
||||||
|
for _, g := range svgFont.Glyphs {
|
||||||
|
if g.Rune == Rune(rune) {
|
||||||
|
continue filling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glyph := gc.glyphCache.Fetch(gc, gc.GetFontName(), rune)
|
||||||
|
// glyphCache.Load indirectly calls CreateStringPath for single rune string
|
||||||
|
|
||||||
|
glypPath := glyph.Path.VerticalFlip() // svg font glyphs have oposite y axe
|
||||||
|
svgFont.Glyphs = append(svgFont.Glyphs, &Glyph{
|
||||||
|
Rune: Rune(rune),
|
||||||
|
Desc: toSvgPathDesc(glypPath),
|
||||||
|
HorizAdvX: glyph.Width,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// set attrs
|
||||||
|
svgFont.Id = "font-" + strconv.Itoa(len(gc.svg.Fonts))
|
||||||
|
svgFont.Name = fontName
|
||||||
|
|
||||||
|
// TODO use css @font-face with id instead of this
|
||||||
|
svgFont.Face = &Face{Family: fontName, Units: 2048, HorizAdvX: 2048}
|
||||||
|
return svgFont
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) {
|
||||||
|
font, err := gc.FontCache.Load(gc.Current.FontData)
|
||||||
|
if err != nil {
|
||||||
|
font, err = gc.FontCache.Load(draw2dbase.DefaultFontData)
|
||||||
|
}
|
||||||
|
if font != nil {
|
||||||
|
gc.SetFont(font)
|
||||||
|
gc.SetFontSize(gc.Current.FontSize)
|
||||||
|
}
|
||||||
|
return font, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *GraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error {
|
||||||
|
if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), glyph, font.HintingNone); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e0 := 0
|
||||||
|
for _, e1 := range gc.glyphBuf.Ends {
|
||||||
|
DrawContour(gc, gc.glyphBuf.Points[e0:e1], dx, dy)
|
||||||
|
e0 = e1
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recalc recalculates scale and bounds values from the font size, screen
|
||||||
|
// resolution and font metrics, and invalidates the glyph cache.
|
||||||
|
func (gc *GraphicContext) recalc() {
|
||||||
|
gc.Current.Scale = gc.Current.FontSize * float64(gc.DPI) * (64.0 / 72.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////
|
||||||
|
// TODO implement following methods (or remove if not neccesary)
|
||||||
|
|
||||||
|
// GetFontName gets the current FontData with fontSize as a string
|
||||||
|
func (gc *GraphicContext) GetFontName() string {
|
||||||
|
fontData := gc.Current.FontData
|
||||||
|
return fmt.Sprintf("%s:%d:%d:%d", fontData.Name, fontData.Family, fontData.Style, gc.Current.FontSize)
|
||||||
|
}
|
65
draw2dsvg/samples_test.go
Normal file
65
draw2dsvg/samples_test.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright 2015 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 26/06/2015 by Stani Michiels
|
||||||
|
// See also test_test.go
|
||||||
|
|
||||||
|
package draw2dsvg_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/llgcode/draw2d"
|
||||||
|
"github.com/llgcode/draw2d/samples/android"
|
||||||
|
"github.com/llgcode/draw2d/samples/frameimage"
|
||||||
|
"github.com/llgcode/draw2d/samples/geometry"
|
||||||
|
"github.com/llgcode/draw2d/samples/gopher"
|
||||||
|
"github.com/llgcode/draw2d/samples/gopher2"
|
||||||
|
"github.com/llgcode/draw2d/samples/helloworld"
|
||||||
|
"github.com/llgcode/draw2d/samples/line"
|
||||||
|
"github.com/llgcode/draw2d/samples/linecapjoin"
|
||||||
|
"github.com/llgcode/draw2d/samples/postscript"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSampleAndroid(t *testing.T) {
|
||||||
|
test(t, android.Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: FillString: w (width) is incorrect
|
||||||
|
func TestSampleGeometry(t *testing.T) {
|
||||||
|
// Set the global folder for searching fonts
|
||||||
|
// The pdf backend needs for every ttf file its corresponding
|
||||||
|
// json/.z file which is generated by gofpdf/makefont.
|
||||||
|
draw2d.SetFontFolder("../resource/font")
|
||||||
|
test(t, geometry.Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSampleGopher(t *testing.T) {
|
||||||
|
test(t, gopher.Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSampleGopher2(t *testing.T) {
|
||||||
|
test(t, gopher2.Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSampleHelloWorld(t *testing.T) {
|
||||||
|
// Set the global folder for searching fonts
|
||||||
|
// The pdf backend needs for every ttf file its corresponding
|
||||||
|
// json/.z file which is generated by gofpdf/makefont.
|
||||||
|
draw2d.SetFontFolder("../resource/font")
|
||||||
|
test(t, helloworld.Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSampleFrameImage(t *testing.T) {
|
||||||
|
test(t, frameimage.Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSampleLine(t *testing.T) {
|
||||||
|
test(t, line.Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSampleLineCap(t *testing.T) {
|
||||||
|
test(t, linecapjoin.Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSamplePostscript(t *testing.T) {
|
||||||
|
test(t, postscript.Main)
|
||||||
|
}
|
172
draw2dsvg/svg.go
Normal file
172
draw2dsvg/svg.go
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
// Copyright 2015 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 16/12/2017 by Drahoslav Bednář
|
||||||
|
|
||||||
|
package draw2dsvg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* svg elements */
|
||||||
|
|
||||||
|
type FontMode int
|
||||||
|
|
||||||
|
// Modes of font handling in svg
|
||||||
|
const (
|
||||||
|
// Does nothing special
|
||||||
|
// Makes sense only for common system fonts
|
||||||
|
SysFontMode FontMode = 1 << iota
|
||||||
|
|
||||||
|
// Links font files in css def
|
||||||
|
// Requires distribution of font files with outputed svg
|
||||||
|
LinkFontMode // TODO implement
|
||||||
|
|
||||||
|
// Embeds glyphs definition in svg file itself in svg font format
|
||||||
|
// Has poor browser support
|
||||||
|
SvgFontMode
|
||||||
|
|
||||||
|
// Embeds font definiton in svg file itself in woff format as part of css def
|
||||||
|
CssFontMode // TODO implement
|
||||||
|
|
||||||
|
// Converts texts to paths
|
||||||
|
PathFontMode
|
||||||
|
)
|
||||||
|
|
||||||
|
type Svg struct {
|
||||||
|
XMLName xml.Name `xml:"svg"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
Fonts []*Font `xml:"defs>font"`
|
||||||
|
Masks []*Mask `xml:"defs>mask"`
|
||||||
|
Groups []*Group `xml:"g"`
|
||||||
|
FontMode FontMode `xml:"-"`
|
||||||
|
FillStroke
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSvg() *Svg {
|
||||||
|
return &Svg{
|
||||||
|
Xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
FillStroke: FillStroke{Fill: "none", Stroke: "none"},
|
||||||
|
FontMode: PathFontMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
FillStroke
|
||||||
|
Transform string `xml:"transform,attr,omitempty"`
|
||||||
|
Groups []*Group `xml:"g"`
|
||||||
|
Paths []*Path `xml:"path"`
|
||||||
|
Texts []*Text `xml:"text"`
|
||||||
|
Image *Image `xml:"image"`
|
||||||
|
Mask string `xml:"mask,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Path struct {
|
||||||
|
FillStroke
|
||||||
|
Desc string `xml:"d,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Text struct {
|
||||||
|
FillStroke
|
||||||
|
Position
|
||||||
|
FontSize float64 `xml:"font-size,attr,omitempty"`
|
||||||
|
FontFamily string `xml:"font-family,attr,omitempty"`
|
||||||
|
Text string `xml:",innerxml"`
|
||||||
|
Style string `xml:"style,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
Position
|
||||||
|
Dimension
|
||||||
|
Href string `xml:"href,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mask struct {
|
||||||
|
Identity
|
||||||
|
Position
|
||||||
|
Dimension
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rect struct {
|
||||||
|
Position
|
||||||
|
Dimension
|
||||||
|
FillStroke
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Mask) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
bigRect := Rect{}
|
||||||
|
bigRect.X, bigRect.Y = 0, 0
|
||||||
|
bigRect.Width, bigRect.Height = "100%", "100%"
|
||||||
|
bigRect.Fill = "#fff"
|
||||||
|
rect := Rect{}
|
||||||
|
rect.X, rect.Y = m.X, m.Y
|
||||||
|
rect.Width, rect.Height = m.Width, m.Height
|
||||||
|
rect.Fill = "#000"
|
||||||
|
|
||||||
|
return e.EncodeElement(struct {
|
||||||
|
XMLName xml.Name `xml:"mask"`
|
||||||
|
Rects [2]Rect `xml:"rect"`
|
||||||
|
Id string `xml:"id,attr"`
|
||||||
|
}{
|
||||||
|
Rects: [2]Rect{bigRect, rect},
|
||||||
|
Id: m.Id,
|
||||||
|
}, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* font related elements */
|
||||||
|
|
||||||
|
type Font struct {
|
||||||
|
Identity
|
||||||
|
Face *Face `xml:"font-face"`
|
||||||
|
Glyphs []*Glyph `xml:"glyph"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Face struct {
|
||||||
|
Family string `xml:"font-family,attr"`
|
||||||
|
Units int `xml:"units-per-em,attr"`
|
||||||
|
HorizAdvX float64 `xml:"horiz-adv-x,attr"`
|
||||||
|
// TODO add other attrs, like style, variant, weight...
|
||||||
|
}
|
||||||
|
|
||||||
|
type Glyph struct {
|
||||||
|
Rune Rune `xml:"unicode,attr"`
|
||||||
|
Desc string `xml:"d,attr"`
|
||||||
|
HorizAdvX float64 `xml:"horiz-adv-x,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rune rune
|
||||||
|
|
||||||
|
func (r Rune) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
|
||||||
|
return xml.Attr{
|
||||||
|
Name: name,
|
||||||
|
Value: string(rune(r)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* shared attrs */
|
||||||
|
|
||||||
|
type Identity struct {
|
||||||
|
Id string `xml:"id,attr"`
|
||||||
|
Name string `xml:"name,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Position struct {
|
||||||
|
X float64 `xml:"x,attr,omitempty"`
|
||||||
|
Y float64 `xml:"y,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dimension struct {
|
||||||
|
Width string `xml:"width,attr"`
|
||||||
|
Height string `xml:"height,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FillStroke struct {
|
||||||
|
Fill string `xml:"fill,attr,omitempty"`
|
||||||
|
FillRule string `xml:"fill-rule,attr,omitempty"`
|
||||||
|
|
||||||
|
Stroke string `xml:"stroke,attr,omitempty"`
|
||||||
|
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
|
||||||
|
StrokeLinecap string `xml:"stroke-linecap,attr,omitempty"`
|
||||||
|
StrokeLinejoin string `xml:"stroke-linejoin,attr,omitempty"`
|
||||||
|
StrokeDasharray string `xml:"stroke-dasharray,attr,omitempty"`
|
||||||
|
StrokeDashoffset string `xml:"stroke-dashoffset,attr,omitempty"`
|
||||||
|
}
|
32
draw2dsvg/test_test.go
Normal file
32
draw2dsvg/test_test.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright 2015 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 16/12/2017 by Drahoslav Bednář
|
||||||
|
|
||||||
|
// Package draw2dsvg_test gives test coverage with the command:
|
||||||
|
// go test -cover ./... | grep -v "no test"
|
||||||
|
// (It should be run from its parent draw2d directory.)
|
||||||
|
package draw2dsvg_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/llgcode/draw2d"
|
||||||
|
"github.com/llgcode/draw2d/draw2dsvg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sample func(gc draw2d.GraphicContext, ext string) (string, error)
|
||||||
|
|
||||||
|
func test(t *testing.T, draw sample) {
|
||||||
|
// Initialize the graphic context on an pdf document
|
||||||
|
dest := draw2dsvg.NewSvg()
|
||||||
|
gc := draw2dsvg.NewGraphicContext(dest)
|
||||||
|
// Draw sample
|
||||||
|
output, err := draw(gc, "svg")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Drawing %q failed: %v", output, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = draw2dsvg.SaveToSvgFile(output, dest)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Saving %q failed: %v", output, err)
|
||||||
|
}
|
||||||
|
}
|
83
draw2dsvg/text.go
Normal file
83
draw2dsvg/text.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// NOTE that this is identical copy of draw2dgl/text.go and draw2dimg/text.go
|
||||||
|
package draw2dsvg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/llgcode/draw2d"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DrawContour draws the given closed contour at the given sub-pixel offset.
|
||||||
|
func DrawContour(path draw2d.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
58
draw2dsvg/xml_test.go
Normal file
58
draw2dsvg/xml_test.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright 2015 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 16/12/2017 by Drahoslav Bednář
|
||||||
|
|
||||||
|
// Package draw2dsvg_test gives test coverage with the command:
|
||||||
|
// go test -cover ./... | grep -v "no test"
|
||||||
|
// (It should be run from its parent draw2d directory.)
|
||||||
|
package draw2dsvg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test basic encoding of svg/xml elements
|
||||||
|
func TestXml(t *testing.T) {
|
||||||
|
|
||||||
|
svg := NewSvg()
|
||||||
|
svg.Groups = []*Group{&Group{
|
||||||
|
Groups: []*Group{
|
||||||
|
&Group{}, // nested groups
|
||||||
|
&Group{},
|
||||||
|
},
|
||||||
|
Texts: []*Text{
|
||||||
|
&Text{Text: "Hello"}, // text
|
||||||
|
&Text{Text: "world", Style: "opacity: 0.5"}, // text with style
|
||||||
|
},
|
||||||
|
Paths: []*Path{
|
||||||
|
&Path{Desc: "M100,200 C100,100 250,100 250,200 S400,300 400,200"}, // simple path
|
||||||
|
&Path{}, // empty path
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
expectedOut := `<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="none">
|
||||||
|
<defs></defs>
|
||||||
|
<g>
|
||||||
|
<g></g>
|
||||||
|
<g></g>
|
||||||
|
<path d="M100,200 C100,100 250,100 250,200 S400,300 400,200"></path>
|
||||||
|
<path d=""></path>
|
||||||
|
<text>Hello</text>
|
||||||
|
<text style="opacity: 0.5">world</text>
|
||||||
|
</g>
|
||||||
|
</svg>`
|
||||||
|
|
||||||
|
out, err := xml.MarshalIndent(svg, "", " ")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if string(out) != expectedOut {
|
||||||
|
t.Errorf("svg output is not as expected\n"+
|
||||||
|
"got:\n%s\n\n"+
|
||||||
|
"want:\n%s\n",
|
||||||
|
string(out),
|
||||||
|
expectedOut,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
8
font.go
8
font.go
|
@ -8,8 +8,8 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"sync"
|
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FontStyle defines bold and italic styles for the font
|
// FontStyle defines bold and italic styles for the font
|
||||||
|
@ -125,7 +125,7 @@ type FolderFontCache struct {
|
||||||
namer FontFileNamer
|
namer FontFileNamer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFolderFontCache creates FolderFontCache
|
// NewFolderFontCache creates FolderFontCache
|
||||||
func NewFolderFontCache(folder string) *FolderFontCache {
|
func NewFolderFontCache(folder string) *FolderFontCache {
|
||||||
return &FolderFontCache{
|
return &FolderFontCache{
|
||||||
fonts: make(map[string]*truetype.Font),
|
fonts: make(map[string]*truetype.Font),
|
||||||
|
@ -168,9 +168,7 @@ type SyncFolderFontCache struct {
|
||||||
namer FontFileNamer
|
namer FontFileNamer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSyncFolderFontCache creates SyncFolderFontCache
|
||||||
|
|
||||||
// NewSyncFolderFontCache creates SyncFolderFontCache
|
|
||||||
func NewSyncFolderFontCache(folder string) *SyncFolderFontCache {
|
func NewSyncFolderFontCache(folder string) *SyncFolderFontCache {
|
||||||
return &SyncFolderFontCache{
|
return &SyncFolderFontCache{
|
||||||
fonts: make(map[string]*truetype.Font),
|
fonts: make(map[string]*truetype.Font),
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
31
path.go
31
path.go
|
@ -190,3 +190,34 @@ func (p *Path) String() string {
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns new Path with flipped y axes
|
||||||
|
func (path *Path) VerticalFlip() *Path {
|
||||||
|
p := path.Copy()
|
||||||
|
j := 0
|
||||||
|
for _, cmd := range p.Components {
|
||||||
|
switch cmd {
|
||||||
|
case MoveToCmp, LineToCmp:
|
||||||
|
p.Points[j+1] = -p.Points[j+1]
|
||||||
|
j = j + 2
|
||||||
|
case QuadCurveToCmp:
|
||||||
|
p.Points[j+1] = -p.Points[j+1]
|
||||||
|
p.Points[j+3] = -p.Points[j+3]
|
||||||
|
j = j + 4
|
||||||
|
case CubicCurveToCmp:
|
||||||
|
p.Points[j+1] = -p.Points[j+1]
|
||||||
|
p.Points[j+3] = -p.Points[j+3]
|
||||||
|
p.Points[j+5] = -p.Points[j+5]
|
||||||
|
j = j + 6
|
||||||
|
case ArcToCmp:
|
||||||
|
p.Points[j+1] = -p.Points[j+1]
|
||||||
|
p.Points[j+3] = -p.Points[j+3]
|
||||||
|
p.Points[j+4] = -p.Points[j+4] // start angle
|
||||||
|
p.Points[j+5] = -p.Points[j+5] // angle
|
||||||
|
j = j + 6
|
||||||
|
case CloseCmp:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.y = -p.y
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
|
@ -189,6 +189,7 @@ func CubicCurve(gc draw2d.GraphicContext, x, y, width, height float64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// FillString draws a filled and stroked string.
|
// FillString draws a filled and stroked string.
|
||||||
|
// And filles/stroked path created from string. Which may have different - unselectable - output in non raster gc implementations.
|
||||||
func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
|
func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
|
||||||
sx, sy := width/100, height/100
|
sx, sy := width/100, height/100
|
||||||
gc.Save()
|
gc.Save()
|
||||||
|
@ -202,7 +203,8 @@ func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
|
||||||
gc.SetFontData(draw2d.FontData{
|
gc.SetFontData(draw2d.FontData{
|
||||||
Name: "luxi",
|
Name: "luxi",
|
||||||
Family: draw2d.FontFamilyMono,
|
Family: draw2d.FontFamilyMono,
|
||||||
Style: draw2d.FontStyleBold | draw2d.FontStyleItalic})
|
Style: draw2d.FontStyleBold | draw2d.FontStyleItalic,
|
||||||
|
})
|
||||||
w := gc.FillString("Hug")
|
w := gc.FillString("Hug")
|
||||||
gc.Translate(w+sx, 0)
|
gc.Translate(w+sx, 0)
|
||||||
left, top, right, bottom := gc.GetStringBounds("cou")
|
left, top, right, bottom := gc.GetStringBounds("cou")
|
||||||
|
@ -214,6 +216,14 @@ func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
|
||||||
gc.SetStrokeColor(color.NRGBA{0x33, 0x33, 0xff, 0xff})
|
gc.SetStrokeColor(color.NRGBA{0x33, 0x33, 0xff, 0xff})
|
||||||
gc.SetLineWidth(height / 100)
|
gc.SetLineWidth(height / 100)
|
||||||
gc.StrokeString("Hug")
|
gc.StrokeString("Hug")
|
||||||
|
|
||||||
|
gc.Translate(-(w + sx), sy*24)
|
||||||
|
w = gc.CreateStringPath("Hug", 0, 0)
|
||||||
|
gc.Fill()
|
||||||
|
gc.Translate(w+sx, 0)
|
||||||
|
gc.CreateStringPath("Hug", 0, 0)
|
||||||
|
path := gc.GetPath()
|
||||||
|
gc.Stroke((&path).VerticalFlip())
|
||||||
gc.Restore()
|
gc.Restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import "fmt"
|
||||||
// Resource returns a resource filename for testing.
|
// Resource returns a resource filename for testing.
|
||||||
func Resource(folder, filename, ext string) string {
|
func Resource(folder, filename, ext string) string {
|
||||||
var root string
|
var root string
|
||||||
if ext == "pdf" {
|
if ext == "pdf" || ext == "svg" {
|
||||||
root = "../"
|
root = "../"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%sresource/%s/%s", root, folder, filename)
|
return fmt.Sprintf("%sresource/%s/%s", root, folder, filename)
|
||||||
|
@ -17,7 +17,7 @@ func Resource(folder, filename, ext string) string {
|
||||||
// Output returns the output filename for testing.
|
// Output returns the output filename for testing.
|
||||||
func Output(name, ext string) string {
|
func Output(name, ext string) string {
|
||||||
var root string
|
var root string
|
||||||
if ext == "pdf" {
|
if ext == "pdf" || ext == "svg" {
|
||||||
root = "../"
|
root = "../"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%soutput/samples/%s.%s", root, name, ext)
|
return fmt.Sprintf("%soutput/samples/%s.%s", root, name, ext)
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
package draw2d_test
|
package draw2d_test
|
||||||
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/llgcode/draw2d"
|
"github.com/llgcode/draw2d"
|
||||||
|
|
Loading…
Reference in a new issue