diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 7a1b0a2..0133ca3 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -27,10 +27,12 @@ # Please keep the list sorted. Andrew Gerrand +Dmitri Shuralyov Jeff R. Allen Maksim Kochkin Michael Fogleman Nigel Tao +Randall O'Reilly Rémy Oudompheng Rob Pike Roger Peppe diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e9dc81f --- /dev/null +++ b/Makefile @@ -0,0 +1,75 @@ +# Basic Go makefile + +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOGET=$(GOCMD) get + +DIRS=`go list ./...` + +all: build + +build: + @echo "GO111MODULE = $(value GO111MODULE)" + $(GOBUILD) -v $(DIRS) + +test: + @echo "GO111MODULE = $(value GO111MODULE)" + $(GOTEST) -v $(DIRS) + +clean: + @echo "GO111MODULE = $(value GO111MODULE)" + $(GOCLEAN) ./... + +fmts: + gofmt -s -w . + +vet: + @echo "GO111MODULE = $(value GO111MODULE)" + $(GOCMD) vet $(DIRS) | grep -v unkeyed + +tidy: export GO111MODULE = on +tidy: + @echo "GO111MODULE = $(value GO111MODULE)" + go mod tidy + +mod-update: export GO111MODULE = on +mod-update: + @echo "GO111MODULE = $(value GO111MODULE)" + go get -u ./... + go mod tidy + +# gopath-update is for GOPATH to get most things updated. +# need to call it in a target executable directory +gopath-update: export GO111MODULE = off +gopath-update: + @echo "GO111MODULE = $(value GO111MODULE)" + go get -u ./... + +# NOTE: MUST update version number here prior to running 'make release' and edit this file! +VERS=v1.0.1 +PACKAGE=freetype +GIT_COMMIT=`git rev-parse --short HEAD` +VERS_DATE=`date -u +%Y-%m-%d\ %H:%M` +VERS_FILE=version.go + +release: + /bin/rm -f $(VERS_FILE) + @echo "// WARNING: auto-generated by Makefile release target -- run 'make release' to update" > $(VERS_FILE) + @echo "" >> $(VERS_FILE) + @echo "package $(PACKAGE)" >> $(VERS_FILE) + @echo "" >> $(VERS_FILE) + @echo "const (" >> $(VERS_FILE) + @echo " Version = \"$(VERS)\"" >> $(VERS_FILE) + @echo " GitCommit = \"$(GIT_COMMIT)\" // the commit JUST BEFORE the release" >> $(VERS_FILE) + @echo " VersionDate = \"$(VERS_DATE)\" // UTC" >> $(VERS_FILE) + @echo ")" >> $(VERS_FILE) + @echo "" >> $(VERS_FILE) + goimports -w $(VERS_FILE) + /bin/cat $(VERS_FILE) + git commit -am "$(VERS) release" + git tag -a $(VERS) -m "$(VERS) release" + git push + git push origin --tags + diff --git a/README b/README index 39b3d82..a0d70bc 100644 --- a/README +++ b/README @@ -1,7 +1,7 @@ The Freetype font rasterizer in the Go programming language. To download and install from source: -$ go get github.com/golang/freetype +$ go get github.com/tdewolff/freetype It is an incomplete port: * It only supports TrueType fonts, and not Type 1 fonts nor bitmap fonts. diff --git a/example/capjoin/main.go b/example/capjoin/main.go index 71f3356..4295215 100644 --- a/example/capjoin/main.go +++ b/example/capjoin/main.go @@ -3,9 +3,11 @@ // FreeType License or the GNU General Public License version 2 (or // any later version), both of which can be found in the LICENSE file. +//go:build example // +build example + // -// This build tag means that "go install github.com/golang/freetype/..." +// This build tag means that "go install ..." // doesn't install this example program. Use "go run main.go" to run it or "go // install -tags=example" to install it. @@ -21,7 +23,7 @@ import ( "log" "os" - "github.com/golang/freetype/raster" + "github.com/goki/freetype/raster" "golang.org/x/image/math/fixed" ) diff --git a/example/drawer/main.go b/example/drawer/main.go index d26d066..4eda98f 100644 --- a/example/drawer/main.go +++ b/example/drawer/main.go @@ -3,9 +3,11 @@ // FreeType License or the GNU General Public License version 2 (or // any later version), both of which can be found in the LICENSE file. +//go:build example // +build example + // -// This build tag means that "go install github.com/golang/freetype/..." +// This build tag means that "go install ..." // doesn't install this example program. Use "go run main.go" to run it or "go // install -tags=example" to install it. @@ -24,7 +26,7 @@ import ( "math" "os" - "github.com/golang/freetype/truetype" + "github.com/goki/freetype/truetype" "golang.org/x/image/font" "golang.org/x/image/math/fixed" ) diff --git a/example/freetype/main.go b/example/freetype/main.go index dfbde9a..f609ad4 100644 --- a/example/freetype/main.go +++ b/example/freetype/main.go @@ -3,9 +3,11 @@ // FreeType License or the GNU General Public License version 2 (or // any later version), both of which can be found in the LICENSE file. +//go:build example // +build example + // -// This build tag means that "go install github.com/golang/freetype/..." +// This build tag means that "go install ..." // doesn't install this example program. Use "go run main.go" to run it or "go // install -tags=example" to install it. @@ -23,7 +25,7 @@ import ( "log" "os" - "github.com/golang/freetype" + "github.com/goki/freetype" "golang.org/x/image/font" ) diff --git a/example/gamma/main.go b/example/gamma/main.go index cdd50bc..bf1d28a 100644 --- a/example/gamma/main.go +++ b/example/gamma/main.go @@ -3,9 +3,11 @@ // FreeType License or the GNU General Public License version 2 (or // any later version), both of which can be found in the LICENSE file. +//go:build example // +build example + // -// This build tag means that "go install github.com/golang/freetype/..." +// This build tag means that "go install ..." // doesn't install this example program. Use "go run main.go" to run it or "go // install -tags=example" to install it. @@ -20,7 +22,7 @@ import ( "log" "os" - "github.com/golang/freetype/raster" + "github.com/goki/freetype/raster" "golang.org/x/image/math/fixed" ) diff --git a/example/genbasicfont/main.go b/example/genbasicfont/main.go index 5b2f2bc..d106e1f 100644 --- a/example/genbasicfont/main.go +++ b/example/genbasicfont/main.go @@ -3,9 +3,11 @@ // FreeType License or the GNU General Public License version 2 (or // any later version), both of which can be found in the LICENSE file. +//go:build example // +build example + // -// This build tag means that "go install github.com/golang/freetype/..." +// This build tag means that "go install ..." // doesn't install this example program. Use "go run main.go" to run it or "go // install -tags=example" to install it. @@ -26,7 +28,7 @@ import ( "strings" "unicode" - "github.com/golang/freetype/truetype" + "github.com/goki/freetype/truetype" "golang.org/x/image/font" "golang.org/x/image/math/fixed" ) diff --git a/example/raster/main.go b/example/raster/main.go index 3e572e1..359ec39 100644 --- a/example/raster/main.go +++ b/example/raster/main.go @@ -3,9 +3,11 @@ // FreeType License or the GNU General Public License version 2 (or // any later version), both of which can be found in the LICENSE file. +//go:build example // +build example + // -// This build tag means that "go install github.com/golang/freetype/..." +// This build tag means that "go install ..." // doesn't install this example program. Use "go run main.go" to run it or "go // install -tags=example" to install it. @@ -21,7 +23,7 @@ import ( "log" "os" - "github.com/golang/freetype/raster" + "github.com/goki/freetype/raster" "golang.org/x/image/math/fixed" ) diff --git a/example/round/main.go b/example/round/main.go index 2920e83..79468b0 100644 --- a/example/round/main.go +++ b/example/round/main.go @@ -3,9 +3,11 @@ // FreeType License or the GNU General Public License version 2 (or // any later version), both of which can be found in the LICENSE file. +//go:build example // +build example + // -// This build tag means that "go install github.com/golang/freetype/..." +// This build tag means that "go install ..." // doesn't install this example program. Use "go run main.go" to run it or "go // install -tags=example" to install it. @@ -27,7 +29,7 @@ import ( "math" "os" - "github.com/golang/freetype/raster" + "github.com/goki/freetype/raster" "golang.org/x/image/math/fixed" ) diff --git a/example/truetype/main.go b/example/truetype/main.go index e7db2d0..75f9a9c 100644 --- a/example/truetype/main.go +++ b/example/truetype/main.go @@ -3,9 +3,11 @@ // FreeType License or the GNU General Public License version 2 (or // any later version), both of which can be found in the LICENSE file. +//go:build example // +build example + // -// This build tag means that "go install github.com/golang/freetype/..." +// This build tag means that "go install ..." // doesn't install this example program. Use "go run main.go" to run it or "go // install -tags=example" to install it. @@ -17,7 +19,7 @@ import ( "io/ioutil" "log" - "github.com/golang/freetype/truetype" + "github.com/goki/freetype/truetype" "golang.org/x/image/font" "golang.org/x/image/math/fixed" ) diff --git a/freetype.go b/freetype.go index 9603586..193eed0 100644 --- a/freetype.go +++ b/freetype.go @@ -6,15 +6,15 @@ // The freetype package provides a convenient API to draw text onto an image. // Use the freetype/raster and freetype/truetype packages for lower level // control over rasterization and TrueType parsing. -package freetype // import "github.com/golang/freetype" +package freetype import ( "errors" "image" "image/draw" - "github.com/golang/freetype/raster" - "github.com/golang/freetype/truetype" + "github.com/goki/freetype/raster" + "github.com/goki/freetype/truetype" "golang.org/x/image/font" "golang.org/x/image/math/fixed" ) @@ -78,7 +78,7 @@ type Context struct { // PointToFixed converts the given number of points (as in "a 12 point font") // into a 26.6 fixed point number of pixels. func (c *Context) PointToFixed(x float64) fixed.Int26_6 { - return fixed.Int26_6(x * float64(c.dpi) * (64.0 / 72.0)) + return fixed.Int26_6(0.5 + (x * c.dpi * 64 / 72)) } // drawContour draws the given closed contour with the given offset. @@ -260,7 +260,7 @@ func (c *Context) DrawString(s string, p fixed.Point26_6) (fixed.Point26_6, erro // recalc recalculates scale and bounds values from the font size, screen // resolution and font metrics, and invalidates the glyph cache. func (c *Context) recalc() { - c.scale = fixed.Int26_6(c.fontSize * c.dpi * (64.0 / 72.0)) + c.scale = fixed.Int26_6(0.5 + (c.fontSize * c.dpi * 64 / 72)) if c.f == nil { c.r.SetBounds(0, 0) } else { @@ -332,10 +332,12 @@ func (c *Context) SetClip(clip image.Rectangle) { // NewContext creates a new Context. func NewContext() *Context { - return &Context{ + c := &Context{ r: raster.NewRasterizer(0, 0), fontSize: 12, dpi: 72, scale: 12 << 6, } + c.r.UseNonZeroWinding = true // needed for font rendering + return c } diff --git a/freetype_test.go b/freetype_test.go index 348c411..24c8b51 100644 --- a/freetype_test.go +++ b/freetype_test.go @@ -12,6 +12,8 @@ import ( "runtime" "strings" "testing" + + "golang.org/x/image/math/fixed" ) func BenchmarkDrawString(b *testing.B) { @@ -57,3 +59,26 @@ func BenchmarkDrawString(b *testing.B) { mallocs = ms.Mallocs - mallocs b.Logf("%d iterations, %d mallocs per iteration\n", b.N, int(mallocs)/b.N) } + +func TestScaling(t *testing.T) { + c := NewContext() + for _, tc := range [...]struct { + in float64 + want fixed.Int26_6 + }{ + {in: 12, want: fixed.I(12)}, + {in: 11.992, want: fixed.I(12) - 1}, + {in: 11.993, want: fixed.I(12)}, + {in: 12.007, want: fixed.I(12)}, + {in: 12.008, want: fixed.I(12) + 1}, + {in: 86.4, want: fixed.Int26_6(86<<6 + 26)}, // Issue https://github.com/golang/freetype/issues/85. + } { + c.SetFontSize(tc.in) + if got, want := c.scale, tc.want; got != want { + t.Errorf("scale after SetFontSize(%v) = %v, want %v", tc.in, got, want) + } + if got, want := c.PointToFixed(tc.in), tc.want; got != want { + t.Errorf("PointToFixed(%v) = %v, want %v", tc.in, got, want) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e131112 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/goki/freetype + +go 1.18 + +require golang.org/x/image v0.6.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..64dc9f5 --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= +golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/raster/raster.go b/raster/raster.go index 7e6cd4e..995925e 100644 --- a/raster/raster.go +++ b/raster/raster.go @@ -13,7 +13,7 @@ // the Freetype "smooth" module, and the Anti-Grain Geometry library. A // description of the area/coverage algorithm is at // http://projects.tuxee.net/cl-vectors/section-the-cl-aa-algorithm -package raster // import "github.com/golang/freetype/raster" +package raster import ( "strconv" diff --git a/raster/stroke.go b/raster/stroke.go index bcc66b2..84a62f9 100644 --- a/raster/stroke.go +++ b/raster/stroke.go @@ -438,12 +438,20 @@ func (k *stroker) stroke(q Path) { if len(k.r) == 0 { return } - // TODO(nigeltao): if q is a closed curve then we should join the first and - // last segments instead of capping them. - k.cr.Cap(k.p, k.u, q.lastPoint(), pNeg(k.anorm)) + + closed := q.firstPoint() == q.lastPoint() + if !closed { + k.cr.Cap(k.p, k.u, q.lastPoint(), pNeg(k.anorm)) + } else { + pivot := q.firstPoint() + k.jr.Join(k.p, &k.r, k.u, pivot, k.anorm, pivot.Sub(fixed.Point26_6{k.r[1], k.r[2]})) + k.p.Start(fixed.Point26_6{k.r[len(k.r)-3], k.r[len(k.r)-2]}) // reverse path is now separate + } addPathReversed(k.p, k.r) - pivot := q.firstPoint() - k.cr.Cap(k.p, k.u, pivot, pivot.Sub(fixed.Point26_6{k.r[1], k.r[2]})) + if !closed { + pivot := q.firstPoint() + k.cr.Cap(k.p, k.u, pivot, pivot.Sub(fixed.Point26_6{k.r[1], k.r[2]})) + } } // Stroke adds q stroked with the given width to p. The result is typically diff --git a/testdata/luxisr-12pt-sans-hinting.txt b/testdata/luxisr-12pt-sans-hinting.txt index e276164..6509f94 100644 --- a/testdata/luxisr-12pt-sans-hinting.txt +++ b/testdata/luxisr-12pt-sans-hinting.txt @@ -5,7 +5,7 @@ freetype version 2.5.1 213 0 0 0 0; 213 70 0 144 555;70 0 1, 70 74 1, 144 74 1, 144 0 1, 79 148 1, 70 444 1, 70 555 1, 144 555 1, 144 444 1, 135 148 1 273 35 407 238 592;44 407 1, 35 592 1, 108 592 1, 99 407 1, 173 407 1, 164 592 1, 238 592 1, 229 407 1 -427 9 0 418 555;47 0 1, 89 167 1, 9 167 1, 18 213 1, 100 213 1, 133 342 1, 44 342 1, 54 389 1, 144 389 1, 186 555 1, 234 555 1, 192 389 1, 291 389 1, 332 555 1, 380 555 1, 339 389 1, 418 389 1, 409 342 1, 327 342 1, 294 213 1, 383 213 1, 374 167 1, 283 167 1, 242 0 1, 194 0 1, 235 167 1, 137 167 1, 95 0 1, 148 213 1, 247 213 1, 279 342 1, 180 342 1 +427 9 0 418 555;47 0 1, 89 167 1, 9 167 1, 18 213 1, 100 213 1, 133 342 1, 44 342 1, 54 389 1, 144 389 1, 186 555 10000, 234 555 1, 192 389 1, 291 389 1, 332 555 1, 380 555 1, 339 389 1, 418 389 1, 409 342 1, 327 342 1, 294 213 1, 383 213 1, 374 167 1, 283 167 1, 242 0 1, 194 0 1, 235 167 1, 137 167 1, 95 0 1, 148 213 1, 247 213 1, 279 342 1, 180 342 1 427 39 -46 353 602;187 -46 1, 187 0 1, 121 0 0, 39 31 1, 39 95 1, 123 56 0, 187 56 1, 187 255 1, 117 298 0, 88 330 1, 55 368 0, 55 422 1, 55 486 0, 103 524 1, 135 550 0, 187 555 1, 187 602 1, 224 602 1, 224 555 1, 278 555 0, 344 530 1, 344 470 1, 273 501 0, 224 504 1, 224 307 1, 228 304 1, 238 298 0, 247 293 1, 251 290 1, 299 262 0, 322 237 1, 353 205 0, 353 155 1, 353 87 0, 308 42 1, 276 12 0, 224 0 1, 224 -46 1, 224 60 1, 288 85 0, 288 144 1, 288 175 0, 270 195 1, 257 210 0, 224 233 1, 187 331 1, 187 502 1, 120 479 0, 120 425 1, 120 376 0 683 42 -14 641 569;94 -14 1, 531 569 1, 589 569 1, 152 -14 1, 161 555 1, 216 555 0, 248 518 1, 280 480 0, 280 416 1, 280 352 0, 248 315 1, 216 278 0, 161 278 1, 106 278 0, 74 315 1, 42 353 0, 42 418 1, 42 475 0, 68 511 1, 101 555 0, 161 518 1, 134 518 0, 117 491 1, 100 462 0, 100 419 1, 100 375 0, 114 348 1, 131 315 0, 161 315 1, 189 315 0, 206 343 1, 222 371 0, 222 416 1, 222 462 0, 206 490 1, 188 518 0, 522 278 1, 577 278 0, 609 240 1, 641 203 0, 641 139 1, 641 75 0, 609 38 1, 577 0 0, 522 0 1, 467 0 0, 435 38 1, 403 75 0, 403 141 1, 403 198 0, 429 233 1, 462 278 0, 522 241 1, 494 241 0, 477 213 1, 461 185 0, 461 141 1, 461 98 0, 474 71 1, 491 37 0, 522 37 1, 549 37 0, 566 65 1, 583 93 0, 583 139 1, 583 185 0, 566 213 1, 549 241 0 512 21 -14 485 569;384 0 1, 357 33 1, 282 -14 0, 214 -14 1, 132 -14 0, 77 37 1, 21 88 0, 21 166 1, 21 243 0, 69 290 1, 98 318 0, 152 339 1, 119 400 0, 119 445 1, 119 501 0, 153 535 1, 188 569 0, 247 569 1, 303 569 0, 336 539 1, 368 508 0, 368 457 1, 368 401 0, 325 360 1, 298 335 0, 248 312 1, 311 198 0, 373 123 1, 410 171 0, 410 265 1, 410 295 1, 483 295 1, 483 165 0, 408 83 1, 441 41 0, 485 0 1, 325 76 1, 251 160 0, 178 296 1, 141 279 0, 123 257 1, 95 225 0, 95 179 1, 95 122 0, 134 82 1, 172 42 0, 226 42 1, 268 42 0, 220 359 1, 256 374 0, 273 392 1, 299 419 0, 299 456 1, 299 513 0, 246 513 1, 191 513 0, 191 453 1, 191 416 0, 217 365 1 diff --git a/truetype/face.go b/truetype/face.go index 099006f..cd243ae 100644 --- a/truetype/face.go +++ b/truetype/face.go @@ -9,7 +9,7 @@ import ( "image" "math" - "github.com/golang/freetype/raster" + "github.com/goki/freetype/raster" "golang.org/x/image/font" "golang.org/x/image/math/fixed" ) @@ -66,6 +66,11 @@ type Options struct { // // A zero value means to use 1 sub-pixel location. SubPixelsY int + + // Stroke is the number of pixels that the font glyphs are being stroked. + // + // A zero values means no stroke. + Stroke int } func (o *Options) size() float64 { @@ -134,11 +139,12 @@ func (o *Options) subPixelsY() (value uint32, halfQuantum, mask fixed.Int26_6) { // // For example, q == 4 leads to a bias of 8 and a mask of 0xfffffff0, or -16, // because we want to round fractions of fixed.Int26_6 as: -// - 0 to 7 rounds to 0. -// - 8 to 23 rounds to 16. -// - 24 to 39 rounds to 32. -// - 40 to 55 rounds to 48. -// - 56 to 63 rounds to 64. +// - 0 to 7 rounds to 0. +// - 8 to 23 rounds to 16. +// - 24 to 39 rounds to 32. +// - 40 to 55 rounds to 48. +// - 56 to 63 rounds to 64. +// // which means to add 8 and then bitwise-and with -16, in two's complement // representation. // @@ -175,14 +181,32 @@ type indexCacheEntry struct { index Index } +type IndexableFace interface { + font.Face + + // GlyphAtIndex returns the draw.DrawMask parameters (dr, mask, maskp) to draw the + // glyph identified by gid at the sub-pixel destination location dot, and that glyph's + // advance width. + // + // It returns !ok if the face does not contain a glyph for r. + // + // The contents of the mask image returned by one Glyph call may change + // after the next Glyph call. Callers that want to cache the mask must make + // a copy. + GlyphAtIndex(dot fixed.Point26_6, gid Index) ( + dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) +} + // NewFace returns a new font.Face for the given Font. -func NewFace(f *Font, opts *Options) font.Face { +func NewFace(f *Font, opts *Options) IndexableFace { a := &face{ f: f, hinting: opts.hinting(), scale: fixed.Int26_6(0.5 + (opts.size() * opts.dpi() * 64 / 72)), glyphCache: make([]glyphCacheEntry, opts.glyphCacheEntries()), + stroke: fixed.I(opts.Stroke * 2), } + a.r.UseNonZeroWinding = true // key for fonts a.subPixelX, a.subPixelBiasX, a.subPixelMaskX = opts.subPixelsX() a.subPixelY, a.subPixelBiasY, a.subPixelMaskY = opts.subPixelsY() @@ -207,6 +231,10 @@ func NewFace(f *Font, opts *Options) font.Face { a.r.SetBounds(a.maxw, a.maxh) a.p = facePainter{a} + if a.stroke != 0 { + a.r.UseNonZeroWinding = true + } + return a } @@ -220,6 +248,7 @@ type face struct { subPixelY uint32 subPixelBiasY fixed.Int26_6 subPixelMaskY fixed.Int26_6 + stroke fixed.Int26_6 masks *image.Alpha glyphCache []glyphCacheEntry r raster.Rasterizer @@ -229,6 +258,7 @@ type face struct { maxh int glyphBuf GlyphBuf indexCache [indexCacheLen]indexCacheEntry + advanceCache map[rune]fixed.Int26_6 // TODO: clip rectangle? } @@ -255,9 +285,12 @@ func (a *face) Metrics() font.Metrics { scale := float64(a.scale) fupe := float64(a.f.FUnitsPerEm()) return font.Metrics{ - Height: a.scale, + Height: fixed.Int26_6(math.Ceil(scale * float64(a.f.ascent-a.f.descent+a.f.lineGap) / fupe)), Ascent: fixed.Int26_6(math.Ceil(scale * float64(+a.f.ascent) / fupe)), Descent: fixed.Int26_6(math.Ceil(scale * float64(-a.f.descent) / fupe)), + // TODO: Metrics should include LineGap as a separate measure + // TODO: Would also be great to include Ex as in the height of an "x" -- + // used widely for layout } } @@ -276,6 +309,13 @@ func (a *face) Kern(r0, r1 rune) fixed.Int26_6 { func (a *face) Glyph(dot fixed.Point26_6, r rune) ( dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) { + return a.GlyphAtIndex(dot, a.index(r)) +} + +// GlyphAtIndex satisfies the IndexableFace interface. +func (a *face) GlyphAtIndex(dot fixed.Point26_6, index Index) ( + dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) { + // Quantize to the sub-pixel granularity. dotX := (dot.X + a.subPixelBiasX) & a.subPixelMaskX dotY := (dot.Y + a.subPixelBiasY) & a.subPixelMaskY @@ -284,7 +324,6 @@ func (a *face) Glyph(dot fixed.Point26_6, r rune) ( ix, fx := int(dotX>>6), dotX&0x3f iy, fy := int(dotY>>6), dotY&0x3f - index := a.index(r) cIndex := uint32(index) cIndex = cIndex*a.subPixelX - uint32(fx/a.subPixelMaskX) cIndex = cIndex*a.subPixelY - uint32(fy/a.subPixelMaskY) @@ -329,6 +368,10 @@ func (a *face) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.In if xmin > xmax || ymin > ymax { return fixed.Rectangle26_6{}, 0, false } + xmin -= a.stroke + ymin -= a.stroke + xmax += a.stroke + ymax += a.stroke return fixed.Rectangle26_6{ Min: fixed.Point26_6{ X: xmin, @@ -342,10 +385,20 @@ func (a *face) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.In } func (a *face) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) { - if err := a.glyphBuf.Load(a.f, a.scale, a.index(r), a.hinting); err != nil { + if a.advanceCache == nil { + a.advanceCache = make(map[rune]fixed.Int26_6, 1024) + } + advance, ok = a.advanceCache[r] + if ok { + idx := a.index(r) + return advance, (idx != 0) + } + idx := a.index(r) + if err := a.glyphBuf.Load(a.f, a.scale, idx, a.hinting); err != nil { return 0, false } - return a.glyphBuf.AdvanceWidth, true + a.advanceCache[r] = a.glyphBuf.AdvanceWidth + return a.glyphBuf.AdvanceWidth, (idx != 0) } // rasterize returns the advance width, integer-pixel offset to render at, and @@ -364,6 +417,10 @@ func (a *face) rasterize(index Index, fx, fy fixed.Int26_6) (v glyphCacheVal, ok if xmin > xmax || ymin > ymax { return glyphCacheVal{}, false } + xmin -= int(a.stroke) >> 6 + ymin -= int(a.stroke) >> 6 + xmax += int(a.stroke) >> 6 + ymax += int(a.stroke) >> 6 // A TrueType's glyph's nodes can have negative co-ordinates, but the // rasterizer clips anything left of x=0 or above y=0. xmin and ymin are // the pixel offsets, based on the font's FUnit metrics, that let a @@ -434,7 +491,8 @@ func (a *face) drawContour(ps []Point, dx, dy fixed.Int26_6) { others = ps } } - a.r.Start(start) + path := raster.Path{} + path.Start(start) q0, on0 := start, true for _, p := range others { q := fixed.Point26_6{ @@ -444,9 +502,9 @@ func (a *face) drawContour(ps []Point, dx, dy fixed.Int26_6) { on := p.Flags&0x01 != 0 if on { if on0 { - a.r.Add1(q) + path.Add1(q) } else { - a.r.Add2(q0, q) + path.Add2(q0, q) } } else { if on0 { @@ -456,16 +514,22 @@ func (a *face) drawContour(ps []Point, dx, dy fixed.Int26_6) { X: (q0.X + q.X) / 2, Y: (q0.Y + q.Y) / 2, } - a.r.Add2(q0, mid) + path.Add2(q0, mid) } } q0, on0 = q, on } // Close the curve. if on0 { - a.r.Add1(start) + path.Add1(start) + } else { + path.Add2(q0, start) + } + + if a.stroke == 0 { + a.r.AddPath(path) } else { - a.r.Add2(q0, start) + a.r.AddStroke(path, a.stroke, raster.ButtCapper, raster.RoundJoiner) } } diff --git a/truetype/glyph.go b/truetype/glyph.go index 6157ad8..ceafd42 100644 --- a/truetype/glyph.go +++ b/truetype/glyph.go @@ -6,6 +6,8 @@ package truetype import ( + "fmt" + "golang.org/x/image/font" "golang.org/x/image/math/fixed" ) @@ -175,6 +177,9 @@ func (g *GlyphBuf) load(recursion uint32, i Index, useMyMetrics bool) (err error if recursion >= 32 { return UnsupportedError("excessive compound glyph recursion") } + if g.font.loca == nil { + return UnsupportedError("no glyph location data") + } // Find the relevant slice of g.font.glyf. var g0, g1 uint32 if g.font.locaOffsetFormat == locaOffsetFormatShort { @@ -228,7 +233,11 @@ func (g *GlyphBuf) load(recursion uint32, i Index, useMyMetrics bool) (err error np0, ne0 := len(g.Points), len(g.Ends) program := g.loadSimple(glyf, ne) g.addPhantomsAndScale(np0, np0, true, true) - pp1x = g.Points[len(g.Points)-4].X + idx := len(g.Points) - 4 + if idx < 0 { + return fmt.Errorf("points out of range error") + } + pp1x = g.Points[idx].X if g.hinting != font.HintingNone { if len(program) != 0 { err := g.hinter.run( diff --git a/truetype/hint.go b/truetype/hint.go index 13f785b..9fbf411 100644 --- a/truetype/hint.go +++ b/truetype/hint.go @@ -1677,11 +1677,13 @@ func fmul(x, y fixed.Int26_6) fixed.Int26_6 { } // dotProduct returns the dot product of [x, y] and q. It is almost the same as +// // px := int64(x) // py := int64(y) // qx := int64(q[0]) // qy := int64(q[1]) // return fixed.Int26_6((px*qx + py*qy + 1<<13) >> 14) +// // except that the computation is done with 32-bit integers to produce exactly // the same rounding behavior as C Freetype. func dotProduct(x, y fixed.Int26_6, q [2]f2dot14) fixed.Int26_6 { diff --git a/truetype/truetype.go b/truetype/truetype.go index 7270bbf..8892e3b 100644 --- a/truetype/truetype.go +++ b/truetype/truetype.go @@ -15,10 +15,14 @@ // // To measure a TrueType font in ideal FUnit space, use scale equal to // font.FUnitsPerEm(). -package truetype // import "github.com/golang/freetype/truetype" + +package truetype import ( + "bytes" "fmt" + "unicode/utf16" + "unicode/utf8" "golang.org/x/image/math/fixed" ) @@ -133,7 +137,7 @@ func parseSubtables(table []byte, name string, offset, size int, pred func([]byt if len(table) < size*nSubtables+offset { return 0, 0, FormatError(name + " too short") } - ok := false + bestScore := -1 for i := 0; i < nSubtables; i, offset = i+1, offset+size { if pred != nil && !pred(table[offset:]) { continue @@ -143,19 +147,24 @@ func parseSubtables(table []byte, name string, offset, size int, pred func([]byt pidPsid := u32(table, offset) // We prefer the Unicode cmap encoding. Failing to find that, we fall // back onto the Microsoft cmap encoding. + // And we prefer full/UCS4 encoding over BMP/UCS2. So the priority goes: + // unicodeEncodingFull > microsoftUCS4Encoding > unicodeEncodingBMPOnly > microsoftUCS2Encoding > microsoftSymbolEncoding + // It is in accord with the Psid part. + score := int(pidPsid & 0xFFFF) + if score <= bestScore { + continue + } if pidPsid == unicodeEncodingBMPOnly || pidPsid == unicodeEncodingFull { - bestOffset, bestPID, ok = offset, pidPsid>>16, true + bestOffset, bestPID, bestScore = offset, pidPsid>>16, score break } else if pidPsid == microsoftSymbolEncoding || pidPsid == microsoftUCS2Encoding || pidPsid == microsoftUCS4Encoding { - - bestOffset, bestPID, ok = offset, pidPsid>>16, true - // We don't break out of the for loop, so that Unicode can override Microsoft. + bestOffset, bestPID, bestScore = offset, pidPsid>>16, score } } - if !ok { + if bestScore < 0 { return 0, 0, UnsupportedError(name + " encoding") } return bestOffset, bestPID, nil @@ -187,6 +196,7 @@ type Font struct { fUnitsPerEm int32 ascent int32 // In FUnits. descent int32 // In FUnits; typically negative. + lineGap int32 // In FUnits. bounds fixed.Rectangle26_6 // In FUnits. // Values from the maxp section. maxTwilightPoints, maxStorage, maxFunctionDefs, maxStackElements uint16 @@ -294,6 +304,7 @@ func (f *Font) parseHhea() error { } f.ascent = int32(int16(u16(f.hhea, 4))) f.descent = int32(int16(u16(f.hhea, 6))) + f.lineGap = int32(int16(u16(f.hhea, 8))) f.nHMetric = int(u16(f.hhea, 34)) if 4*f.nHMetric+2*(f.nGlyph-f.nHMetric) != len(f.hmtx) { return FormatError(fmt.Sprintf("bad hmtx length: %d", len(f.hmtx))) @@ -312,9 +323,10 @@ func (f *Font) parseKern() error { // Since we expect that almost all fonts aim to be Windows-compatible, we only parse the "older" format, // just like the C Freetype implementation. if len(f.kern) == 0 { - if f.nKern != 0 { - return FormatError("bad kern table length") - } + // if f.nKern != 0 { + // return FormatError("bad kern table length") + // } + f.nKern = 0 // just reset and move on return nil } if len(f.kern) < 18 { @@ -322,7 +334,9 @@ func (f *Font) parseKern() error { } version, offset := u16(f.kern, 0), 2 if version != 0 { - return UnsupportedError(fmt.Sprintf("kern version: %d", version)) + f.nKern = 0 + return nil + // return UnsupportedError(fmt.Sprintf("kern version: %d", version)) } n, offset := u16(f.kern, offset), offset+2 @@ -346,7 +360,9 @@ func (f *Font) parseKern() error { } f.nKern, offset = int(u16(f.kern, offset)), offset+2 if 6*f.nKern != length-14 { - return FormatError("bad kern table length") + f.nKern = 0 + return nil + // return FormatError("bad kern table length") } return nil } @@ -421,20 +437,27 @@ func (f *Font) Name(id NameID) string { // Return the ASCII value of the encoded string. // The string is encoded as UTF-16 on non-Apple platformIDs; Apple is platformID 1. src := f.name[offset : offset+length] - var dst []byte if platformID != 1 { // UTF-16. if len(src)&1 != 0 { return "" } - dst = make([]byte, len(src)/2) - for i := range dst { - dst[i] = printable(u16(src, 2*i)) - } - } else { // ASCII. - dst = make([]byte, len(src)) - for i, c := range src { - dst[i] = printable(uint16(c)) + lb := len(src) / 2 + b8buf := make([]byte, 4) + u16s := make([]uint16, 1) + var buf bytes.Buffer + for i := 0; i < lb; i++ { + u16s[0] = u16(src, i) + r := utf16.Decode(u16s) + n := utf8.EncodeRune(b8buf, r[0]) + buf.Write([]byte(string(b8buf[:n]))) } + return buf.String() + } + // ASCII. + var dst []byte + dst = make([]byte, len(src)) + for i, c := range src { + dst[i] = printable(uint16(c)) } return string(dst) } diff --git a/version.go b/version.go new file mode 100644 index 0000000..9b45b04 --- /dev/null +++ b/version.go @@ -0,0 +1,9 @@ +// WARNING: auto-generated by Makefile release target -- run 'make release' to update + +package freetype + +const ( + Version = "v1.0.1" + GitCommit = "4e73317" // the commit JUST BEFORE the release + VersionDate = "2023-03-29 04:22" // UTC +)