From 19b0ec55b5621d799ee885073e7556da31ac56c7 Mon Sep 17 00:00:00 2001 From: Jonathan Feinberg Date: Mon, 27 May 2013 14:53:53 -0400 Subject: [PATCH] Initial import of new freetype font handling. --- cmd/testdraw2d.go | 9 +- draw2d/font.go | 3 +- draw2d/gc.go | 1 + draw2d/image.go | 181 ++++++++++++++++++++++------- draw2d/stack_gc.go | 11 +- resource/result/TestFillString.png | Bin 1813 -> 2823 bytes 6 files changed, 156 insertions(+), 49 deletions(-) diff --git a/cmd/testdraw2d.go b/cmd/testdraw2d.go index 09ffe4e..e6f3be6 100644 --- a/cmd/testdraw2d.go +++ b/cmd/testdraw2d.go @@ -31,7 +31,7 @@ func initGc(w, h int) (image.Image, draw2d.GraphicContext) { gc.SetStrokeColor(image.Black) gc.SetFillColor(image.White) - // fill the background + // fill the background //gc.Clear() return i, gc @@ -488,12 +488,13 @@ func TestFillString() { i, gc := initGc(100, 100) draw2d.RoundRect(gc, 5, 5, 95, 95, 10, 10) gc.FillStroke() + gc.SetFillColor(image.Black) gc.SetFontSize(18) - gc.MoveTo(10, 52) + gc.Translate(6, 52) gc.SetFontData(draw2d.FontData{"luxi", draw2d.FontFamilyMono, draw2d.FontStyleBold | draw2d.FontStyleItalic}) width := gc.FillString("cou") - gc.RMoveTo(width+1, 0) - gc.FillString("cou") + gc.Translate(width+1, 0) + gc.StrokeString("cou") saveToPngFile("TestFillString", i) } diff --git a/draw2d/font.go b/draw2d/font.go index 61c39ff..eb0b532 100644 --- a/draw2d/font.go +++ b/draw2d/font.go @@ -4,7 +4,6 @@ package draw2d import ( - "code.google.com/p/freetype-go/freetype" "code.google.com/p/freetype-go/freetype/truetype" "io/ioutil" "log" @@ -89,7 +88,7 @@ func loadFont(fontFileName string) *truetype.Font { log.Println(err) return nil } - font, err := freetype.ParseFont(fontBytes) + font, err := truetype.Parse(fontBytes) if err != nil { log.Println(err) return nil diff --git a/draw2d/gc.go b/draw2d/gc.go index 8ced348..cf7a22b 100644 --- a/draw2d/gc.go +++ b/draw2d/gc.go @@ -44,6 +44,7 @@ type GraphicContext interface { SetDPI(dpi int) GetDPI() int FillString(text string) (cursor float64) + StrokeString(text string) (cursor float64) Stroke(paths ...*PathStorage) Fill(paths ...*PathStorage) FillStroke(paths ...*PathStorage) diff --git a/draw2d/image.go b/draw2d/image.go index 6a6dbd4..3eebb03 100644 --- a/draw2d/image.go +++ b/draw2d/image.go @@ -4,8 +4,9 @@ package draw2d import ( - "code.google.com/p/freetype-go/freetype" "code.google.com/p/freetype-go/freetype/raster" + "code.google.com/p/freetype-go/freetype/truetype" + "errors" "image" "image/color" "image/draw" @@ -27,7 +28,7 @@ type ImageGraphicContext struct { painter Painter fillRasterizer *raster.Rasterizer strokeRasterizer *raster.Rasterizer - freetype *freetype.Context + glyphBuf *truetype.GlyphBuf DPI int } @@ -42,49 +43,25 @@ func NewGraphicContext(img draw.Image) *ImageGraphicContext { default: panic("Image type not supported") } - width, height := img.Bounds().Dx(), img.Bounds().Dy() - dpi := 92 - ftContext := freetype.NewContext() - ftContext.SetDPI(float64(dpi)) - ftContext.SetClip(img.Bounds()) - ftContext.SetDst(img) - gc := &ImageGraphicContext{ - NewStackGraphicContext(), - img, - painter, - raster.NewRasterizer(width, height), - raster.NewRasterizer(width, height), - ftContext, - dpi, - } - return gc + return NewGraphicContextWithPainter(img, painter) } // Create a new Graphic context from an image and a Painter (see Freetype-go) func NewGraphicContextWithPainter(img draw.Image, painter Painter) *ImageGraphicContext { width, height := img.Bounds().Dx(), img.Bounds().Dy() dpi := 92 - ftContext := freetype.NewContext() - ftContext.SetDPI(float64(dpi)) - ftContext.SetClip(img.Bounds()) - ftContext.SetDst(img) gc := &ImageGraphicContext{ NewStackGraphicContext(), img, painter, raster.NewRasterizer(width, height), raster.NewRasterizer(width, height), - ftContext, + truetype.NewGlyphBuf(), dpi, } return gc } -func (gc *ImageGraphicContext) SetDPI(dpi int) { - gc.DPI = dpi - gc.freetype.SetDPI(float64(dpi)) -} - func (gc *ImageGraphicContext) GetDPI() int { return gc.DPI } @@ -104,12 +81,32 @@ func (gc *ImageGraphicContext) DrawImage(img image.Image) { } func (gc *ImageGraphicContext) FillString(text string) (cursor float64) { - gc.freetype.SetSrc(image.NewUniform(gc.Current.StrokeColor)) - // Draw the text. - x, y := gc.Current.Path.LastPoint() - gc.Current.Tr.Transform(&x, &y) - x0, fontSize := 0.0, gc.Current.FontSize - gc.Current.Tr.VectorTransform(&x0, &fontSize) + return gc.FillStringAt(text, 0, 0) +} + +func (gc *ImageGraphicContext) FillStringAt(text string, x, y float64) (cursor float64) { + width := gc.CreateStringPath(text, x, y) + gc.Fill() + return width +} + +func (gc *ImageGraphicContext) StrokeString(text string) (cursor float64) { + return gc.StrokeStringAt(text, 0, 0) +} + +func (gc *ImageGraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) { + width := gc.CreateStringPath(text, x, y) + gc.Stroke() + return width +} + +// 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 *ImageGraphicContext) CreateStringPath(text string, x, y float64) (width float64) { font := GetFont(gc.Current.FontData) if font == nil { font = GetFont(defaultFontData) @@ -117,20 +114,120 @@ func (gc *ImageGraphicContext) FillString(text string) (cursor float64) { if font == nil { return 0 } - gc.freetype.SetFont(font) - gc.freetype.SetFontSize(fontSize) - pt := freetype.Pt(int(x), int(y)) - p, err := gc.freetype.DrawString(text, pt) + gc.SetFont(font) + gc.SetFontSize(gc.Current.FontSize) + width, err := gc._createStringPath(text, 0, 0) if err != nil { log.Println(err) } - x1, _ := gc.Current.Path.LastPoint() - x2, y2 := float64(p.X)/256, float64(p.Y)/256 - gc.Current.Tr.InverseTransform(&x2, &y2) - width := x2 - x1 return width } +func fUnitsToFloat64(x int32) float64 { + scaled := x << 2 + return float64(scaled/256) + float64(scaled%256)/256.0 +} + +// p is a truetype.Point measured in FUnits and positive Y going upwards. +// The returned value is the same thing measured in floating point and positive Y +// going downwards. +func pointToF64Point(p truetype.Point) (x, y float64) { + return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y) +} + +// drawContour draws the given closed contour at the given sub-pixel offset. +func (gc *ImageGraphicContext) drawContour(ps []truetype.Point, dx, dy float64) { + if len(ps) == 0 { + return + } + startX, startY := pointToF64Point(ps[0]) + gc.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 { + gc.LineTo(qX+dx, qY+dy) + } else { + gc.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy) + } + } else { + if on0 { + // No-op. + } else { + midX := (q0X + qX) / 2 + midY := (q0Y + qY) / 2 + gc.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy) + } + } + q0X, q0Y, on0 = qX, qY, on + } + // Close the curve. + if on0 { + gc.LineTo(startX+dx, startY+dy) + } else { + gc.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy) + } +} + +func (gc *ImageGraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error { + if err := gc.glyphBuf.Load(gc.Current.font, gc.Current.scale, glyph, nil); err != nil { + return err + } + e0 := 0 + for _, e1 := range gc.glyphBuf.End { + gc.drawContour(gc.glyphBuf.Point[e0:e1], dx, dy) + e0 = e1 + } + return nil +} + +func (gc *ImageGraphicContext) _createStringPath(s string, x, y float64) (float64, error) { + font := gc.Current.font + if font == nil { + return 0.0, errors.New("draw2d: CreateStringPath called with a nil font") + } + startx := x + prev, hasPrev := truetype.Index(0), false + for _, rune := range s { + index := font.Index(rune) + if hasPrev { + x += fUnitsToFloat64(font.Kerning(gc.Current.scale, prev, index)) + } + err := gc.drawGlyph(index, x, y) + if err != nil { + return startx - x, err + } + x += fUnitsToFloat64(font.HMetric(gc.Current.scale, index).AdvanceWidth) + prev, hasPrev = index, true + } + return x - startx, nil +} + +// recalc recalculates scale and bounds values from the font size, screen +// resolution and font metrics, and invalidates the glyph cache. +func (gc *ImageGraphicContext) recalc() { + gc.Current.scale = int32(gc.Current.FontSize * float64(gc.DPI) * (64.0 / 72.0)) +} + +// SetDPI sets the screen resolution in dots per inch. +func (gc *ImageGraphicContext) SetDPI(dpi int) { + gc.DPI = dpi + gc.recalc() +} + +// SetFont sets the font used to draw text. +func (gc *ImageGraphicContext) SetFont(font *truetype.Font) { + gc.Current.font = font +} + +// SetFontSize sets the font size in points (as in ``a 12 point font''). +func (gc *ImageGraphicContext) SetFontSize(fontSize float64) { + gc.Current.FontSize = fontSize + gc.recalc() +} + func (gc *ImageGraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) { gc.painter.SetColor(color) rasterizer.Rasterize(gc.painter) diff --git a/draw2d/stack_gc.go b/draw2d/stack_gc.go index 8c2cda6..b2cf63f 100644 --- a/draw2d/stack_gc.go +++ b/draw2d/stack_gc.go @@ -4,6 +4,7 @@ package draw2d import ( + "code.google.com/p/freetype-go/freetype/truetype" "image" "image/color" ) @@ -25,7 +26,13 @@ type ContextStack struct { Join Join FontSize float64 FontData FontData - previous *ContextStack + + font *truetype.Font + // fontSize and dpi are used to calculate scale. scale is the number of + // 26.6 fixed point units in 1 em. + scale int32 + + previous *ContextStack } /** @@ -185,6 +192,8 @@ func (gc *StackGraphicContext) Save() { context.Cap = gc.Current.Cap context.Join = gc.Current.Join context.Path = gc.Current.Path.Copy() + context.font = gc.Current.font + context.scale = gc.Current.scale copy(context.Tr[:], gc.Current.Tr[:]) context.previous = gc.Current gc.Current = context diff --git a/resource/result/TestFillString.png b/resource/result/TestFillString.png index c3a699272616ae19b673ce1c9eec5fb15f7c8d92..dfdc7a1f996fd19230a20be769abeb6b84b3a471 100644 GIT binary patch literal 2823 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 literal 1813 zcmb`|X*3&%764!!YDtkQ2BC`aP^Kbct4KU#YNyszW344rEn^KMQAx#8x-qRrsuV%B z6rpynrD@05qnOx6wFn}%XoJLf=biKZy+7~Fk9+Tr`{SN_&OP7Fx$I&q1ponrgoLE* z?U3%rHRRtRE_$3}KmF<{BqTaxkF@f{Qr0Qxa1Td?_o(TqLM<%6Gt%qDwJwY7v2Gn& z?zNLLGFG^)cVf5^u}n^>4%^LbJ;j{R4agNIXyj6!Wh-2k^y)iRJ(8M}ed%SX7MWHP zKS2&AbrskbgvSjrllXxvCFs5WL(IgBKa4t5M7*=r22NWQsS^x(AC)6+)+l*gpgtcf z-f@(DKc2$^J2l+MZv>nX7ZKQL6`(;NP^y7faJTh&ukT>#xc>frweN3pGp)Euua(9eM%BxPH+X~LURBjMGV-|k>I$`&z zl|{Nh1(EhGaGNLwBr%y+ar}*07?!tPSsG8nHgT*Tv_>#`T*gD z{P8q!Tq>ish>>r|{! zU7yw+;raXW6ZHW=%y7v^-fsokz0BrF?hSbAu8!sziK&icu&1~8+d99F1ZDCYFE6jt zci$)C1&2I3X?r`ay0(^RdsmfZF*pYareog@1sW86y|V19zC85AgD5IB=793_tp36F zPHWhy49Bqn((w>hqjBU!Q6q^Y%4IRM86{zu=hA+6_78bY`WzN(6l&--IXb$~K3oF- z#c4KIUe6*}J0Ehz+wKkFo};2+NH%X%SWG%3%9h)k1)Su`YP%F6%$mj-Rjz()&TPl> z)iG3^@939g&YW?mQLCA-AK$yqH)0iyM#F$~0l+ zax0$S?n6GOc9v4;E|@v{ zHw4PXc)BmxrxAJJru^AeLEpE!9tMNuA8E$6gwV8)){D;@ZQuBxwR_ zo#u8=O^$i;yAuUH39K_+h8h@4zz(G|WrzN2PZAsC+lk#eM>IfI+kdqp=-Q_HC+ zu+Fu_LP89vYLtR0V#&jHD(7`hnVO>tN2jK$qi0&n43Z|favJWR8{(H;n$vi3ek6Tl zS-mmNBxddg0*Gq?S>ijnv6q5scl^<>qrIwJpC)Bi)+7iHQ(XV>p}jIyk=CP}P%_ok z)AKOX^9!v-)tm5-FMa8GZ)NR6eZS{6zk8-HKhoSBdC2wby8clYr8;}NJ&o6hS^tr& zCZZqO9Kq1aR4@N14iYDqg;dM>c3=P0ms8gw*Tj%?wzQK;2uF8f8*1TPOZ;JLd+dTW z9mkT%L9GvHWwXCPHSJER`p4;JRr#;1u8ws-G%-mvvxW4q)t@v0dA_SQ5I!w}u}f?f zl!_5(5C(?PYm++brXEWxpnpvk8AI}Fs=8WRwiOGC_;(dqOntME+gEj7ro#=DBd zccG!PUBVgvL>oSyaSsTXzg=UlDCu1|J{F+Dgnn>NOi1v2rkMT-!7qO4R~MT*_cKkB z;pw#ZXaT@`z=Sd(U+}2He+geyH2e?0c|F2HLPtks)JjL