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
|
||||
)
|
||||
|
||||
func (cap LineCap) String() string {
|
||||
return map[LineCap]string{
|
||||
RoundCap: "round",
|
||||
ButtCap: "cap",
|
||||
SquareCap: "square",
|
||||
}[cap]
|
||||
}
|
||||
|
||||
// LineJoin is the style of segments joint
|
||||
type LineJoin int
|
||||
|
||||
|
@ -140,6 +148,14 @@ const (
|
|||
MiterJoin
|
||||
)
|
||||
|
||||
func (join LineJoin) String() string {
|
||||
return map[LineJoin]string{
|
||||
RoundJoin: "round",
|
||||
BevelJoin: "bevel",
|
||||
MiterJoin: "miter",
|
||||
}[join]
|
||||
}
|
||||
|
||||
// StrokeStyle keeps stroke style attributes
|
||||
// that is used by the Stroke method of a Drawer
|
||||
type StrokeStyle struct {
|
||||
|
|
|
@ -124,7 +124,7 @@ type GraphicContext struct {
|
|||
painter *Painter
|
||||
fillRasterizer *raster.Rasterizer
|
||||
strokeRasterizer *raster.Rasterizer
|
||||
FontCache draw2d.FontCache
|
||||
FontCache draw2d.FontCache
|
||||
glyphCache draw2dbase.GlyphCache
|
||||
glyphBuf *truetype.GlyphBuf
|
||||
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"
|
||||
"path/filepath"
|
||||
|
||||
"sync"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// FontStyle defines bold and italic styles for the font
|
||||
|
@ -125,7 +125,7 @@ type FolderFontCache struct {
|
|||
namer FontFileNamer
|
||||
}
|
||||
|
||||
// NewFolderFontCache creates FolderFontCache
|
||||
// NewFolderFontCache creates FolderFontCache
|
||||
func NewFolderFontCache(folder string) *FolderFontCache {
|
||||
return &FolderFontCache{
|
||||
fonts: make(map[string]*truetype.Font),
|
||||
|
@ -168,9 +168,7 @@ type SyncFolderFontCache struct {
|
|||
namer FontFileNamer
|
||||
}
|
||||
|
||||
|
||||
|
||||
// NewSyncFolderFontCache creates SyncFolderFontCache
|
||||
// NewSyncFolderFontCache creates SyncFolderFontCache
|
||||
func NewSyncFolderFontCache(folder string) *SyncFolderFontCache {
|
||||
return &SyncFolderFontCache{
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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) {
|
||||
sx, sy := width/100, height/100
|
||||
gc.Save()
|
||||
|
@ -202,7 +203,8 @@ func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
|
|||
gc.SetFontData(draw2d.FontData{
|
||||
Name: "luxi",
|
||||
Family: draw2d.FontFamilyMono,
|
||||
Style: draw2d.FontStyleBold | draw2d.FontStyleItalic})
|
||||
Style: draw2d.FontStyleBold | draw2d.FontStyleItalic,
|
||||
})
|
||||
w := gc.FillString("Hug")
|
||||
gc.Translate(w+sx, 0)
|
||||
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.SetLineWidth(height / 100)
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import "fmt"
|
|||
// Resource returns a resource filename for testing.
|
||||
func Resource(folder, filename, ext string) string {
|
||||
var root string
|
||||
if ext == "pdf" {
|
||||
if ext == "pdf" || ext == "svg" {
|
||||
root = "../"
|
||||
}
|
||||
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.
|
||||
func Output(name, ext string) string {
|
||||
var root string
|
||||
if ext == "pdf" {
|
||||
if ext == "pdf" || ext == "svg" {
|
||||
root = "../"
|
||||
}
|
||||
return fmt.Sprintf("%soutput/samples/%s.%s", root, name, ext)
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
package draw2d_test
|
||||
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/llgcode/draw2d"
|
||||
|
|
Loading…
Reference in a new issue