├── .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 --------------------------------------------------------------------------------