diff --git a/draw2d.go b/draw2d.go index 92a7344..590a97e 100644 --- a/draw2d.go +++ b/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 { diff --git a/draw2dgl/gc.go b/draw2dgl/gc.go index 7253170..c744ee6 100644 --- a/draw2dgl/gc.go +++ b/draw2dgl/gc.go @@ -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 diff --git a/draw2dsvg/converters.go b/draw2dsvg/converters.go new file mode 100644 index 0000000..b4440a1 --- /dev/null +++ b/draw2dsvg/converters.go @@ -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 +} diff --git a/draw2dsvg/doc.go b/draw2dsvg/doc.go new file mode 100644 index 0000000..033aaa2 --- /dev/null +++ b/draw2dsvg/doc.go @@ -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 diff --git a/draw2dsvg/fileutil.go b/draw2dsvg/fileutil.go new file mode 100644 index 0000000..2ade34b --- /dev/null +++ b/draw2dsvg/fileutil.go @@ -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 +} diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go new file mode 100644 index 0000000..30fba26 --- /dev/null +++ b/draw2dsvg/gc.go @@ -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) +} diff --git a/draw2dsvg/samples_test.go b/draw2dsvg/samples_test.go new file mode 100644 index 0000000..aec6573 --- /dev/null +++ b/draw2dsvg/samples_test.go @@ -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) +} diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go new file mode 100644 index 0000000..2f15bd6 --- /dev/null +++ b/draw2dsvg/svg.go @@ -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"` +} diff --git a/draw2dsvg/test_test.go b/draw2dsvg/test_test.go new file mode 100644 index 0000000..d1bd328 --- /dev/null +++ b/draw2dsvg/test_test.go @@ -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) + } +} diff --git a/draw2dsvg/text.go b/draw2dsvg/text.go new file mode 100644 index 0000000..30959ae --- /dev/null +++ b/draw2dsvg/text.go @@ -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, + } +} diff --git a/draw2dsvg/xml_test.go b/draw2dsvg/xml_test.go new file mode 100644 index 0000000..8a7d11e --- /dev/null +++ b/draw2dsvg/xml_test.go @@ -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 := ` + + + + + + + Hello + world + +` + + 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, + ) + } +} diff --git a/font.go b/font.go index 90ef9a1..f5c9a04 100644 --- a/font.go +++ b/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), diff --git a/output/samples/geometry.png b/output/samples/geometry.png index b68bb32..b46ec89 100644 Binary files a/output/samples/geometry.png and b/output/samples/geometry.png differ diff --git a/path.go b/path.go index 04a6572..bd24c9b 100644 --- a/path.go +++ b/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 +} diff --git a/samples/geometry/geometry.go b/samples/geometry/geometry.go index 99f1731..05d9bdb 100644 --- a/samples/geometry/geometry.go +++ b/samples/geometry/geometry.go @@ -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() } diff --git a/samples/samples.go b/samples/samples.go index ff30559..c76b608 100644 --- a/samples/samples.go +++ b/samples/samples.go @@ -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) diff --git a/sync_test.go b/sync_test.go index 6c5afd8..8580a81 100644 --- a/sync_test.go +++ b/sync_test.go @@ -2,7 +2,6 @@ package draw2d_test - import ( "fmt" "github.com/llgcode/draw2d"