├── .github
└── workflows
│ ├── analysis.yml
│ └── test.yml
├── .gitignore
├── CODEOWNERS
├── LICENSE
├── README.md
├── bitmap.go
├── go.mod
├── go.sum
├── render.go
├── render_test.go
├── svg.go
└── testdata
├── EmojiOneColor.otf
├── Greybeard-22px.ttf
├── NotoSans-Bold.ttf
├── NotoSans-Regular.ttf
├── OFL.txt
├── bitmap_emoji.png
├── cherry
├── LICENSE
└── cherry-10-r.otb
├── out.png
└── out_hindi.png
/.github/workflows/analysis.yml:
--------------------------------------------------------------------------------
1 | name: Static Analysis
2 | on: [push, pull_request]
3 | permissions:
4 | contents: read
5 |
6 | jobs:
7 | static_analysis:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | with:
12 | persist-credentials: false
13 | - uses: WillAbides/setup-go-faster@v1
14 | with:
15 | go-version: 'stable'
16 |
17 | - name: Install analysis tools
18 | run: go install honnef.co/go/tools/cmd/staticcheck@v0.5.1
19 |
20 | - name: Vet
21 | run: go vet ./...
22 |
23 | - name: Staticcheck
24 | run: staticcheck ./...
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on: [push, pull_request]
3 | permissions:
4 | contents: read
5 |
6 | jobs:
7 | test:
8 | strategy:
9 | matrix:
10 | go-version: [1.19.x, stable]
11 | os: [ubuntu-latest]
12 |
13 | runs-on: ${{ matrix.os }}
14 | steps:
15 | - uses: actions/checkout@v4
16 | with:
17 | persist-credentials: false
18 | - uses: WillAbides/setup-go-faster@v1
19 | with:
20 | go-version: ${{ matrix.go-version }}
21 |
22 | - name: Test
23 | run: go test ./...
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | render.test
3 |
4 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @whereswaldon @benoitkugler @andydotxyz
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This project is provided under the terms of the UNLICENSE or
2 | the BSD license denoted by the following SPDX identifier:
3 |
4 | SPDX-License-Identifier: Unlicense OR BSD-3-Clause
5 |
6 | You may use the project under the terms of either license.
7 |
8 | Both licenses are reproduced below.
9 |
10 | ----
11 | The BSD 3 Clause License
12 |
13 | Copyright 2021 The go-text authors
14 |
15 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
16 |
17 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
18 |
19 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
20 |
21 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
22 |
23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 | ---
25 |
26 |
27 |
28 | ---
29 | The UNLICENSE
30 |
31 | This is free and unencumbered software released into the public domain.
32 |
33 | Anyone is free to copy, modify, publish, use, compile, sell, or
34 | distribute this software, either in source code form or as a compiled
35 | binary, for any purpose, commercial or non-commercial, and by any
36 | means.
37 |
38 | In jurisdictions that recognize copyright laws, the author or authors
39 | of this software dedicate any and all copyright interest in the
40 | software to the public domain. We make this dedication for the benefit
41 | of the public at large and to the detriment of our heirs and
42 | successors. We intend this dedication to be an overt act of
43 | relinquishment in perpetuity of all present and future rights to this
44 | software under copyright law.
45 |
46 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
47 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
48 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
49 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
50 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
51 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
52 | OTHER DEALINGS IN THE SOFTWARE.
53 |
54 | For more information, please refer to
55 | ---
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # render
2 |
3 | A simple rasterising renderer for the go-text project.
4 | Draw your text into a `draw.Image` provided by the developer for caching / overlay opportunities.
5 |
--------------------------------------------------------------------------------
/bitmap.go:
--------------------------------------------------------------------------------
1 | package render
2 |
3 | import (
4 | "bytes"
5 | "image"
6 | "image/color"
7 | "image/draw"
8 | _ "image/jpeg" // load image formats for users of the API
9 | _ "image/png"
10 |
11 | "github.com/go-text/typesetting/font"
12 | "github.com/go-text/typesetting/shaping"
13 | scale "golang.org/x/image/draw"
14 | _ "golang.org/x/image/tiff" // load image formats for users of the API
15 | )
16 |
17 | func (r *Renderer) drawBitmap(g shaping.Glyph, bitmap font.GlyphBitmap, img draw.Image, x, y float32) error {
18 | // scaled glyph rect content
19 | top := y - fixed266ToFloat(g.YBearing)*r.PixScale
20 | bottom := top - fixed266ToFloat(g.Height)*r.PixScale
21 | right := x + fixed266ToFloat(g.Width)*r.PixScale
22 | switch bitmap.Format {
23 | case font.BlackAndWhite:
24 | rec := image.Rect(0, 0, bitmap.Width, bitmap.Height)
25 | sub := image.NewPaletted(rec, color.Palette{color.Transparent, r.Color})
26 |
27 | for i := range sub.Pix {
28 | sub.Pix[i] = bitAt(bitmap.Data, i)
29 | }
30 |
31 | rect := image.Rect(int(x), int(top), int(right), int(bottom))
32 | scale.NearestNeighbor.Scale(img, rect, sub, sub.Bounds(), draw.Over, nil)
33 | case font.JPG, font.PNG, font.TIFF:
34 | pix, _, err := image.Decode(bytes.NewReader(bitmap.Data))
35 | if err != nil {
36 | return err
37 | }
38 |
39 | rect := image.Rect(int(x), int(top), int(right), int(bottom))
40 | scale.BiLinear.Scale(img, rect, pix, pix.Bounds(), draw.Over, nil)
41 | }
42 |
43 | if bitmap.Outline != nil {
44 | r.drawOutline(g, *bitmap.Outline, r.filler, r.fillerScale, x, y)
45 | }
46 | return nil
47 | }
48 |
49 | // bitAt returns the bit at the given index in the byte slice.
50 | func bitAt(b []byte, i int) byte {
51 | return (b[i/8] >> (7 - i%8)) & 1
52 | }
53 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-text/render
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/go-text/typesetting v0.2.0
7 | github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66
8 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
9 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
10 | golang.org/x/image v0.23.0
11 | )
12 |
13 | require (
14 | golang.org/x/net v0.9.0 // indirect
15 | golang.org/x/text v0.21.0 // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/go-text/typesetting v0.2.0 h1:fbzsgbmk04KiWtE+c3ZD4W2nmCRzBqrqQOvYlwAOdho=
2 | github.com/go-text/typesetting v0.2.0/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I=
3 | github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY=
4 | github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
5 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
6 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
7 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
8 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
9 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
11 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
12 | golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
13 | golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
14 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
15 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
16 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
17 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
18 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
19 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
20 | golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
21 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
22 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
23 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
24 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
25 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
26 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
27 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
28 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
29 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
30 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
31 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
32 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
33 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
34 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
35 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
36 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
37 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
38 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
39 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
40 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
41 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
42 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
43 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
44 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
45 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
46 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
47 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
48 |
--------------------------------------------------------------------------------
/render.go:
--------------------------------------------------------------------------------
1 | package render
2 |
3 | import (
4 | "image/color"
5 | "image/draw"
6 | "math"
7 |
8 | "github.com/go-text/typesetting/font"
9 | "github.com/go-text/typesetting/font/opentype"
10 | "github.com/go-text/typesetting/shaping"
11 | "github.com/srwiley/rasterx"
12 | "golang.org/x/image/math/fixed"
13 | )
14 |
15 | // Renderer defines a type that can render strings to a bitmap canvas.
16 | // The size and look of output depends on the various fields in this struct.
17 | // Developers should provide suitable output images for their draw requests.
18 | // This type is not thread safe so instances should be used from only 1 goroutine.
19 | type Renderer struct {
20 | // FontSize defines the point size of output text, commonly between 10 and 14 for regular text
21 | FontSize float32
22 | // PixScale is used to indicate the pixel density of your output target.
23 | // For example on a hi-DPI (or "retina") display this may be 2.0.
24 | // Default value is 1.0, meaning 1 pixel on the image for each render pixel.
25 | PixScale float32
26 | // Color is the pen colour for rendering
27 | Color color.Color
28 |
29 | segmenter shaping.Segmenter
30 | shaper shaping.HarfbuzzShaper
31 | filler *rasterx.Filler
32 | fillerScale float32
33 | }
34 |
35 | func (r *Renderer) shape(str string, face *font.Face) (_ shaping.Line, ascent int) {
36 | text := []rune(str)
37 | in := shaping.Input{
38 | Text: text,
39 | RunStart: 0,
40 | RunEnd: len(text),
41 | Face: face,
42 | Size: fixed.I(int(r.FontSize)),
43 | }
44 |
45 | runs := r.segmenter.Split(in, singleFontMap{face})
46 |
47 | line := make(shaping.Line, len(runs))
48 | for i, run := range runs {
49 | line[i] = r.shaper.Shape(run)
50 | if a := line[i].LineBounds.Ascent.Ceil(); a > ascent {
51 | ascent = a
52 | }
53 | }
54 | return line, ascent
55 | }
56 |
57 | // DrawString will rasterise the given string into the output image using the specified font face.
58 | // The text will be drawn starting at the left edge, down from the image top by the
59 | // font ascent value, so that the text is all visible.
60 | // The return value is the X pixel position of the end of the drawn string.
61 | func (r *Renderer) DrawString(str string, img draw.Image, face *font.Face) int {
62 | line, ascent := r.shape(str, face)
63 | x := 0
64 | for _, run := range line {
65 | x = r.DrawShapedRunAt(run, img, x, ascent)
66 | }
67 | return x
68 | }
69 |
70 | // DrawStringAt will rasterise the given string into the output image using the specified font face.
71 | // The text will be drawn starting at the x, y pixel position.
72 | // Note that x and y are not multiplied by the `PixScale` value as they refer to output coordinates.
73 | // The return value is the X pixel position of the end of the drawn string.
74 | func (r *Renderer) DrawStringAt(str string, img draw.Image, x, y int, face *font.Face) int {
75 | line, _ := r.shape(str, face)
76 | for _, run := range line {
77 | x = r.DrawShapedRunAt(run, img, x, y)
78 | }
79 | return x
80 | }
81 |
82 | // DrawShapedRunAt will rasterise the given shaper run into the output image using font face referenced in the shaping.
83 | // The text will be drawn starting at the startX, startY pixel position.
84 | // Note that startX and startY are not multiplied by the `PixScale` value as they refer to output coordinates.
85 | // The return value is the X pixel position of the end of the drawn string.
86 | func (r *Renderer) DrawShapedRunAt(run shaping.Output, img draw.Image, startX, startY int) int {
87 | if r.PixScale == 0 {
88 | r.PixScale = 1
89 | }
90 | scale := r.FontSize * r.PixScale / float32(run.Face.Upem())
91 | r.fillerScale = scale
92 |
93 | b := img.Bounds()
94 | scanner := rasterx.NewScannerGV(b.Dx(), b.Dy(), img, b)
95 | f := rasterx.NewFiller(b.Dx(), b.Dy(), scanner)
96 | r.filler = f
97 | f.SetColor(r.Color)
98 | x := float32(startX)
99 | y := float32(startY)
100 | for _, g := range run.Glyphs {
101 | xPos := x + fixed266ToFloat(g.XOffset)*r.PixScale
102 | yPos := y - fixed266ToFloat(g.YOffset)*r.PixScale
103 | data := run.Face.GlyphData(g.GlyphID)
104 | switch format := data.(type) {
105 | case font.GlyphOutline:
106 | r.drawOutline(g, format, f, scale, xPos, yPos)
107 | case font.GlyphBitmap:
108 | _ = r.drawBitmap(g, format, img, xPos, yPos)
109 | case font.GlyphSVG:
110 | _ = r.drawSVG(g, format, img, xPos, yPos)
111 | }
112 |
113 | x += fixed266ToFloat(g.XAdvance) * r.PixScale
114 | }
115 | f.Draw()
116 | r.filler = nil
117 | return int(math.Ceil(float64(x)))
118 | }
119 |
120 | func (r *Renderer) drawOutline(g shaping.Glyph, bitmap font.GlyphOutline, f *rasterx.Filler, scale float32, x, y float32) {
121 | for _, s := range bitmap.Segments {
122 | switch s.Op {
123 | case opentype.SegmentOpMoveTo:
124 | f.Start(fixed.Point26_6{X: floatToFixed266(s.Args[0].X*scale + x), Y: floatToFixed266(-s.Args[0].Y*scale + y)})
125 | case opentype.SegmentOpLineTo:
126 | f.Line(fixed.Point26_6{X: floatToFixed266(s.Args[0].X*scale + x), Y: floatToFixed266(-s.Args[0].Y*scale + y)})
127 | case opentype.SegmentOpQuadTo:
128 | f.QuadBezier(fixed.Point26_6{X: floatToFixed266(s.Args[0].X*scale + x), Y: floatToFixed266(-s.Args[0].Y*scale + y)},
129 | fixed.Point26_6{X: floatToFixed266(s.Args[1].X*scale + x), Y: floatToFixed266(-s.Args[1].Y*scale + y)})
130 | case opentype.SegmentOpCubeTo:
131 | f.CubeBezier(fixed.Point26_6{X: floatToFixed266(s.Args[0].X*scale + x), Y: floatToFixed266(-s.Args[0].Y*scale + y)},
132 | fixed.Point26_6{X: floatToFixed266(s.Args[1].X*scale + x), Y: floatToFixed266(-s.Args[1].Y*scale + y)},
133 | fixed.Point26_6{X: floatToFixed266(s.Args[2].X*scale + x), Y: floatToFixed266(-s.Args[2].Y*scale + y)})
134 | }
135 | }
136 | f.Stop(true)
137 | }
138 |
139 | func fixed266ToFloat(i fixed.Int26_6) float32 {
140 | return float32(float64(i) / 64)
141 | }
142 |
143 | func floatToFixed266(f float32) fixed.Int26_6 {
144 | return fixed.Int26_6(int(float64(f) * 64))
145 | }
146 |
147 | type singleFontMap struct {
148 | face *font.Face
149 | }
150 |
151 | func (sf singleFontMap) ResolveFace(rune) *font.Face { return sf.face }
152 |
--------------------------------------------------------------------------------
/render_test.go:
--------------------------------------------------------------------------------
1 | package render_test
2 |
3 | import (
4 | "bytes"
5 | "image"
6 | "image/color"
7 | "image/draw"
8 | "image/png"
9 | "os"
10 | "testing"
11 |
12 | "github.com/go-text/render"
13 | "github.com/go-text/typesetting/font"
14 | "github.com/go-text/typesetting/shaping"
15 |
16 | "golang.org/x/image/math/fixed"
17 |
18 | ot "github.com/go-text/typesetting-utils/opentype"
19 | )
20 |
21 | func Test_Render(t *testing.T) {
22 | img := image.NewNRGBA(image.Rect(0, 0, 425, 250))
23 | draw.Draw(img, img.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
24 |
25 | data, _ := os.Open("testdata/NotoSans-Regular.ttf")
26 | f1, _ := font.ParseTTF(data)
27 |
28 | r := &render.Renderer{
29 | FontSize: 48,
30 | Color: color.Black,
31 | }
32 | str := "Hello! ± ज्या"
33 | r.DrawString(str, img, f1)
34 | r.DrawStringAt(str, img, 0, 100, f1)
35 |
36 | r.PixScale = 2
37 | r.Color = color.Gray{Y: 0xcc}
38 | r.DrawStringAt("baseline", img, 0, 180, f1)
39 |
40 | data, _ = os.Open("testdata/NotoSans-Bold.ttf")
41 | f2, _ := font.ParseTTF(data)
42 | r.FontSize = 36
43 | r.Color = color.NRGBA{R: 0xcc, G: 0, B: 0x33, A: 0x99}
44 | x := r.DrawStringAt("Red", img, 60, 140, f2)
45 | r.DrawStringAt("Bold", img, x, 140, f2)
46 |
47 | // from https://github.com/adobe-fonts/emojione-color, MIT license
48 | data, _ = os.Open("testdata/EmojiOneColor.otf")
49 | f3, _ := font.ParseTTF(data)
50 | r.FontSize = 36
51 | r.DrawStringAt("🚀🖥️", img, 270, 80, f3)
52 |
53 | data, _ = os.Open("testdata/Greybeard-22px.ttf")
54 | f4, _ := font.ParseTTF(data)
55 | r.FontSize = 22
56 | r.Color = color.NRGBA{R: 0xcc, G: 0x66, B: 0x33, A: 0xcc}
57 | r.DrawStringAt("\uE0A2░", img, 366, 164, f4)
58 |
59 | data, _ = os.Open("testdata/cherry/cherry-10-r.otb")
60 | f5, _ := font.ParseTTF(data)
61 | (&render.Renderer{FontSize: 10, PixScale: 1, Color: color.Black}).DrawStringAt("Hello, world!", img, 6, 10, f5)
62 |
63 | str = "Hello ज्या 😀! 🎁 fin."
64 | rs := []rune(str)
65 | sh := &shaping.HarfbuzzShaper{}
66 | in := shaping.Input{
67 | Text: rs,
68 | RunStart: 0,
69 | RunEnd: len(rs),
70 | Size: fixed.I(int(r.FontSize)),
71 | }
72 | seg := shaping.Segmenter{}
73 | runs := seg.Split(in, fixedFontmap([]*font.Face{f1, f2, f3}))
74 |
75 | line := make(shaping.Line, len(runs))
76 | for i, run := range runs {
77 | line[i] = sh.Shape(run)
78 | }
79 |
80 | x = 0
81 | r.Color = color.NRGBA{R: 0x33, G: 0x99, B: 0x33, A: 0xcc}
82 | for _, run := range line {
83 | x = r.DrawShapedRunAt(run, img, x, 232)
84 | }
85 |
86 | w, _ := os.Create("testdata/out.png")
87 | png.Encode(w, img)
88 | w.Close()
89 | }
90 |
91 | func TestRender_PixScaleAdvance(t *testing.T) {
92 | img := image.NewNRGBA(image.Rect(0, 0, 350, 180))
93 |
94 | data, _ := os.Open("testdata/NotoSans-Regular.ttf")
95 | f, _ := font.ParseTTF(data)
96 |
97 | r := &render.Renderer{
98 | FontSize: 48,
99 | Color: color.Black,
100 | }
101 | str := "Testing"
102 | adv0 := r.DrawString(str, img, f)
103 |
104 | r.PixScale = 1 // instead of the zero value
105 | adv1 := r.DrawString(str, img, f)
106 | if adv0 != adv1 {
107 | t.Error("unscaled font did not advance as default")
108 | }
109 |
110 | r.PixScale = 2
111 | adv2 := r.DrawString(str, img, f)
112 | if adv2 <= int(float32(adv1)*1.9) || adv2 >= int(float32(adv1)*2.1) {
113 | t.Error("scaled font did not advance proportionately")
114 | }
115 | }
116 |
117 | func TestRenderHindi(t *testing.T) {
118 | text := "नमस्ते"
119 | r := &render.Renderer{
120 | FontSize: 30,
121 | Color: color.Black,
122 | }
123 |
124 | img := image.NewNRGBA(image.Rect(0, 0, 120, 50))
125 | draw.Draw(img, img.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
126 | data, _ := os.Open("testdata/NotoSans-Regular.ttf")
127 | face, _ := font.ParseTTF(data)
128 |
129 | r.DrawString(text, img, face)
130 |
131 | w, _ := os.Create("testdata/out_hindi.png")
132 | png.Encode(w, img)
133 | w.Close()
134 | }
135 |
136 | type fixedFontmap []*font.Face
137 |
138 | // ResolveFace panics if the slice is empty
139 | func (ff fixedFontmap) ResolveFace(r rune) *font.Face {
140 | for _, f := range ff {
141 | if _, has := f.NominalGlyph(r); has {
142 | return f
143 | }
144 | }
145 | return ff[0]
146 | }
147 |
148 | func TestBitmapBaseline(t *testing.T) {
149 | text := "\U0001F615\U0001F618\U0001F616"
150 | r := &render.Renderer{
151 | FontSize: 40,
152 | Color: color.Black,
153 | }
154 |
155 | img := image.NewNRGBA(image.Rect(0, 0, 150, 100))
156 | draw.Draw(img, img.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
157 | data, _ := ot.Files.ReadFile("bitmap/NotoColorEmoji.ttf")
158 | face, _ := font.ParseTTF(bytes.NewReader(data))
159 |
160 | r.DrawString(text, img, face)
161 |
162 | // w, _ := os.Create("testdata/bitmap_emoji.png")
163 | // png.Encode(w, img)
164 | // w.Close()
165 |
166 | // compare against the reference
167 | var pngBytes bytes.Buffer
168 | png.Encode(&pngBytes, img)
169 |
170 | reference, _ := os.ReadFile("testdata/bitmap_emoji.png")
171 | if !bytes.Equal(pngBytes.Bytes(), reference) {
172 | t.Error("unexpected image output")
173 | }
174 |
175 | }
176 |
--------------------------------------------------------------------------------
/svg.go:
--------------------------------------------------------------------------------
1 | package render
2 |
3 | import (
4 | "bytes"
5 | "image"
6 | "image/draw"
7 | "io"
8 |
9 | "github.com/go-text/typesetting/font"
10 | "github.com/go-text/typesetting/shaping"
11 | "github.com/srwiley/oksvg"
12 | "github.com/srwiley/rasterx"
13 | )
14 |
15 | func (r *Renderer) drawSVG(g shaping.Glyph, svg font.GlyphSVG, img draw.Image, x, y float32) error {
16 | pixWidth := int(fixed266ToFloat(g.Width) * r.PixScale)
17 | pixHeight := int(fixed266ToFloat(-g.Height) * r.PixScale)
18 | pix, err := renderSVGStream(bytes.NewReader(svg.Source), pixWidth, pixHeight)
19 | if err != nil {
20 | return err
21 | }
22 |
23 | rect := image.Rect(int(fixed266ToFloat(g.XBearing)*r.PixScale), int(fixed266ToFloat(-g.YBearing)*r.PixScale),
24 | pixWidth, pixHeight)
25 | draw.Draw(img, rect.Add(image.Point{X: int(x), Y: int(y)}), pix, image.Point{}, draw.Over)
26 |
27 | // ignore the svg.Outline shapes, as they are a fallback which we won't use
28 | return nil
29 | }
30 |
31 | func renderSVGStream(stream io.Reader, width, height int) (*image.NRGBA, error) {
32 | icon, err := oksvg.ReadIconStream(stream)
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | iconAspect := float32(icon.ViewBox.W / icon.ViewBox.H)
38 | viewAspect := float32(width) / float32(height)
39 | imgW, imgH := width, height
40 | if viewAspect > iconAspect {
41 | imgW = int(float32(height) * iconAspect)
42 | } else if viewAspect < iconAspect {
43 | imgH = int(float32(width) / iconAspect)
44 | }
45 |
46 | icon.SetTarget(icon.ViewBox.X, icon.ViewBox.Y, float64(imgW), float64(imgH))
47 |
48 | out := image.NewNRGBA(image.Rect(0, 0, imgW, imgH))
49 | scanner := rasterx.NewScannerGV(int(icon.ViewBox.W), int(icon.ViewBox.H), out, out.Bounds())
50 | raster := rasterx.NewDasher(width, height, scanner)
51 |
52 | icon.Draw(raster, 1)
53 | return out, nil
54 | }
55 |
--------------------------------------------------------------------------------
/testdata/EmojiOneColor.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-text/render/381f3073075071976683eec16ef507d78c4193ce/testdata/EmojiOneColor.otf
--------------------------------------------------------------------------------
/testdata/Greybeard-22px.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-text/render/381f3073075071976683eec16ef507d78c4193ce/testdata/Greybeard-22px.ttf
--------------------------------------------------------------------------------
/testdata/NotoSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-text/render/381f3073075071976683eec16ef507d78c4193ce/testdata/NotoSans-Bold.ttf
--------------------------------------------------------------------------------
/testdata/NotoSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-text/render/381f3073075071976683eec16ef507d78c4193ce/testdata/NotoSans-Regular.ttf
--------------------------------------------------------------------------------
/testdata/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2015-2021 Google LLC. All Rights Reserved.
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/testdata/bitmap_emoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-text/render/381f3073075071976683eec16ef507d78c4193ce/testdata/bitmap_emoji.png
--------------------------------------------------------------------------------
/testdata/cherry/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2023 by camille
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any
4 | purpose with or without fee is hereby granted.
5 |
6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
8 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
10 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
11 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
12 | PERFORMANCE OF THIS SOFTWARE.
13 |
--------------------------------------------------------------------------------
/testdata/cherry/cherry-10-r.otb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-text/render/381f3073075071976683eec16ef507d78c4193ce/testdata/cherry/cherry-10-r.otb
--------------------------------------------------------------------------------
/testdata/out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-text/render/381f3073075071976683eec16ef507d78c4193ce/testdata/out.png
--------------------------------------------------------------------------------
/testdata/out_hindi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-text/render/381f3073075071976683eec16ef507d78c4193ce/testdata/out_hindi.png
--------------------------------------------------------------------------------