├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── input.go ├── output.go ├── output_test.go └── shaper.go /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 | # This repo has been merged into [typesetting](https://github.com/go-text/typesetting). 2 | 3 | # shaping 4 | 5 | This text shaping library is shared by multiple Go UI toolkits including Fyne, and GIO. 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-text/shaping 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/benoitkugler/textlayout v0.0.2 7 | github.com/go-text/di v0.0.0-20210512160309-47d7273de543 8 | github.com/go-text/font v0.0.0-20210614180816-1e3a33db90d4 9 | golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE= 2 | github.com/benoitkugler/textlayout v0.0.1 h1:bx6bcjFv4LuoymKjw/bir7S2rSO67TPCURHPbN/RMz8= 3 | github.com/benoitkugler/textlayout v0.0.1/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8= 4 | github.com/benoitkugler/textlayout v0.0.2 h1:3v0UtMl5DiG2qNTKlPah/7x5Or95MVPgpDHLTqS2270= 5 | github.com/benoitkugler/textlayout v0.0.2/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8= 6 | github.com/go-text/di v0.0.0-20210512160309-47d7273de543 h1:KVgLfDiy/b0k9JY2UAFuKyexYNQZm587A529Bp9LuBo= 7 | github.com/go-text/di v0.0.0-20210512160309-47d7273de543/go.mod h1:C0pTBO6161csSadIq6Gt9mYzJH1WNn/bWIPukOezFus= 8 | github.com/go-text/font v0.0.0-20210614180816-1e3a33db90d4 h1:eRr2WXlQSkwSWyF2Pi9sGdHm/1zzz+g7kseMTes4K9o= 9 | github.com/go-text/font v0.0.0-20210614180816-1e3a33db90d4/go.mod h1:wGHyFuuMY20DezAUxuNzc/TR4evOLbVH9eGB9iJ6JOM= 10 | golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 11 | golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9 h1:D0iM1dTCbD5Dg1CbuvLC/v/agLc79efSj/L35Q3Vqhs= 12 | golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 13 | golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 14 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 17 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 18 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 19 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 20 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 21 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package shaping 4 | 5 | import ( 6 | "github.com/benoitkugler/textlayout/language" 7 | "github.com/go-text/di" 8 | "github.com/go-text/font" 9 | "golang.org/x/image/math/fixed" 10 | ) 11 | 12 | type Input struct { 13 | // Text is the body of text being shaped. Only the range Text[RunStart:RunEnd] is considered 14 | // for shaping, with the rest provided as context for the shaper. This helps with, for example, 15 | // cross-run Arabic shaping or handling combining marks at the start of a run. 16 | Text []rune 17 | // RunStart and RunEnd indicate the subslice of Text being shaped. 18 | RunStart, RunEnd int 19 | // Direction is the directionality of the text. 20 | Direction di.Direction 21 | // Face is the font face to render the text in. 22 | Face font.Face 23 | 24 | // Size is the requested size of the font. 25 | // More generally, it is a scale factor applied to the resulting metrics. 26 | // For instance, given a device resolution (in dpi) and a point size (like 14), the `Size` to 27 | // get result in pixels is given by : pointSize * dpi / 72 28 | Size fixed.Int26_6 29 | 30 | // Script is an identifier for the writing system used in the text. 31 | Script language.Script 32 | 33 | // Language is an identifier for the language of the text. 34 | Language language.Language 35 | } 36 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package shaping 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/benoitkugler/textlayout/fonts" 9 | "github.com/benoitkugler/textlayout/harfbuzz" 10 | "github.com/go-text/di" 11 | "golang.org/x/image/math/fixed" 12 | ) 13 | 14 | // Glyph describes the attributes of a single glyph from a single 15 | // font face in a shaped output. 16 | type Glyph struct { 17 | Width fixed.Int26_6 18 | Height fixed.Int26_6 19 | XBearing fixed.Int26_6 20 | YBearing fixed.Int26_6 21 | XAdvance fixed.Int26_6 22 | YAdvance fixed.Int26_6 23 | XOffset fixed.Int26_6 24 | YOffset fixed.Int26_6 25 | Cluster int 26 | Glyph fonts.GID 27 | Mask harfbuzz.GlyphMask 28 | } 29 | 30 | // LeftSideBearing returns the distance from the glyph's X origin to 31 | // its leftmost edge. This value can be negative if the glyph extends 32 | // across the origin. 33 | func (g Glyph) LeftSideBearing() fixed.Int26_6 { 34 | return g.XBearing 35 | } 36 | 37 | // RightSideBearing returns the distance from the glyph's right edge to 38 | // the edge of the glyph's advance. This value can be negative if the glyph's 39 | // right edge is before the end of its advance. 40 | func (g Glyph) RightSideBearing() fixed.Int26_6 { 41 | return g.XAdvance - g.Width - g.XBearing 42 | } 43 | 44 | // Bounds describes the minor-axis bounds of a line of text. In a LTR or RTL 45 | // layout, it describes the vertical axis. In a BTT or TTB layout, it describes 46 | // the horizontal. 47 | // 48 | // For horizontal text: 49 | // 50 | // - Ascent GLYPH 51 | // | GLYPH 52 | // | GLYPH 53 | // | GLYPH 54 | // | GLYPH 55 | // - Baseline GLYPH 56 | // | GLYPH 57 | // | GLYPH 58 | // | GLYPH 59 | // - Descent GLYPH 60 | // | 61 | // - Gap 62 | type Bounds struct { 63 | // Ascent is the maximum ascent away from the baseline. This value is typically 64 | // positive in coordiate systems that grow up. 65 | Ascent fixed.Int26_6 66 | // Descent is the maximum descent away from the baseline. This value is typically 67 | // negative in coordinate systems that grow up. 68 | Descent fixed.Int26_6 69 | // Gap is the height of empty pixels between lines. This value is typically positive 70 | // in coordinate systems that grow up. 71 | Gap fixed.Int26_6 72 | } 73 | 74 | // LineHeight returns the height of a horizontal line of text described by b. 75 | func (b Bounds) LineHeight() fixed.Int26_6 { 76 | return b.Ascent - b.Descent + b.Gap 77 | } 78 | 79 | // Output describes the dimensions and content of shaped text. 80 | type Output struct { 81 | // Advance is the distance the Dot has advanced. 82 | Advance fixed.Int26_6 83 | // Glyphs are the shaped output text. 84 | Glyphs []Glyph 85 | // LineBounds describes the font's suggested line bounding dimensions. The 86 | // dimensions described should contain any glyphs from the given font. 87 | LineBounds Bounds 88 | // GlyphBounds describes a tight bounding box on the specific glyphs contained 89 | // within this output. The dimensions may not be sufficient to contain all 90 | // glyphs within the chosen font. 91 | GlyphBounds Bounds 92 | } 93 | 94 | // UnimplementedDirectionError is returned when a function does not support the 95 | // provided layout direction yet. 96 | type UnimplementedDirectionError struct { 97 | Direction di.Direction 98 | } 99 | 100 | // Error formats the error into a string message. 101 | func (u UnimplementedDirectionError) Error() string { 102 | return fmt.Sprintf("support for text direction %v is not implemented yet", u.Direction) 103 | } 104 | 105 | // RecomputeAdvance updates only the Advance field based on the current 106 | // contents of the Glyphs field. It is faster than RecalculateAll(), 107 | // and can be used to speed up line wrapping logic. 108 | func (o *Output) RecomputeAdvance(dir di.Direction) { 109 | advance := fixed.Int26_6(0) 110 | switch dir { 111 | case di.DirectionLTR, di.DirectionRTL: 112 | for _, g := range o.Glyphs { 113 | advance += g.XAdvance 114 | } 115 | default: // vertical 116 | for _, g := range o.Glyphs { 117 | advance += g.YAdvance 118 | } 119 | } 120 | o.Advance = advance 121 | } 122 | 123 | // RecalculateAll updates the all other fields of the Output 124 | // to match the current contents of the Glyphs field. 125 | // This method will fail with UnimplementedDirectionError if the provided 126 | // direction is unimplemented. 127 | func (o *Output) RecalculateAll(dir di.Direction) error { 128 | var ( 129 | advance fixed.Int26_6 130 | tallest fixed.Int26_6 131 | lowest fixed.Int26_6 132 | ) 133 | 134 | switch dir { 135 | default: 136 | return UnimplementedDirectionError{Direction: dir} 137 | case di.DirectionLTR, di.DirectionRTL: 138 | for i := range o.Glyphs { 139 | g := &o.Glyphs[i] 140 | advance += g.XAdvance 141 | height := g.YBearing + g.YOffset 142 | if height > tallest { 143 | tallest = height 144 | } 145 | depth := height + g.Height 146 | if depth < lowest { 147 | lowest = depth 148 | } 149 | } 150 | } 151 | o.Advance = advance 152 | o.GlyphBounds = Bounds{ 153 | Ascent: tallest, 154 | Descent: lowest, 155 | } 156 | 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /output_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package shaping_test 4 | 5 | import ( 6 | "errors" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/benoitkugler/textlayout/fonts" 11 | "github.com/benoitkugler/textlayout/harfbuzz" 12 | "github.com/go-text/di" 13 | "github.com/go-text/shaping" 14 | "golang.org/x/image/math/fixed" 15 | ) 16 | 17 | const ( 18 | simpleGID fonts.GID = iota 19 | leftExtentGID 20 | rightExtentGID 21 | deepGID 22 | offsetGID 23 | ) 24 | 25 | var ( 26 | expectedFontExtents = shaping.Bounds{ 27 | Ascent: fixed.I(int(15)), 28 | Descent: fixed.I(int(-15)), 29 | Gap: fixed.I(int(0)), 30 | } 31 | simpleGlyph = shaping.Glyph{ 32 | Glyph: simpleGID, 33 | XAdvance: fixed.I(int(10)), 34 | YAdvance: fixed.I(int(10)), 35 | XOffset: fixed.I(int(0)), 36 | YOffset: fixed.I(int(0)), 37 | Width: fixed.I(int(10)), 38 | Height: -fixed.I(int(10)), 39 | YBearing: fixed.I(int(10)), 40 | } 41 | leftExtentGlyph = shaping.Glyph{ 42 | Glyph: leftExtentGID, 43 | XAdvance: fixed.I(int(5)), 44 | YAdvance: fixed.I(int(5)), 45 | XOffset: fixed.I(int(0)), 46 | YOffset: fixed.I(int(0)), 47 | Width: fixed.I(int(10)), 48 | Height: -fixed.I(int(10)), 49 | YBearing: fixed.I(int(10)), 50 | XBearing: fixed.I(int(5)), 51 | } 52 | rightExtentGlyph = shaping.Glyph{ 53 | Glyph: rightExtentGID, 54 | XAdvance: fixed.I(int(5)), 55 | YAdvance: fixed.I(int(5)), 56 | XOffset: fixed.I(int(0)), 57 | YOffset: fixed.I(int(0)), 58 | Width: fixed.I(int(10)), 59 | Height: -fixed.I(int(10)), 60 | YBearing: fixed.I(int(10)), 61 | XBearing: fixed.I(int(0)), 62 | } 63 | deepGlyph = shaping.Glyph{ 64 | Glyph: deepGID, 65 | XAdvance: fixed.I(int(10)), 66 | YAdvance: fixed.I(int(10)), 67 | XOffset: fixed.I(int(0)), 68 | YOffset: fixed.I(int(0)), 69 | Width: fixed.I(int(10)), 70 | Height: -fixed.I(int(10)), 71 | YBearing: fixed.I(int(0)), 72 | XBearing: fixed.I(int(0)), 73 | } 74 | offsetGlyph = shaping.Glyph{ 75 | Glyph: offsetGID, 76 | XAdvance: fixed.I(int(10)), 77 | YAdvance: fixed.I(int(10)), 78 | XOffset: fixed.I(int(2)), 79 | YOffset: fixed.I(int(2)), 80 | Width: fixed.I(int(10)), 81 | Height: -fixed.I(int(10)), 82 | YBearing: fixed.I(int(10)), 83 | XBearing: fixed.I(int(0)), 84 | } 85 | ) 86 | 87 | // TestRecalculate ensures that the Output.Recalculate function correctly 88 | // computes the bounds, advance, and baseline of the output. 89 | func TestRecalculate(t *testing.T) { 90 | type testcase struct { 91 | Name string 92 | Direction di.Direction 93 | Input []shaping.Glyph 94 | Output shaping.Output 95 | Error error 96 | } 97 | for _, tc := range []testcase{ 98 | { 99 | Name: "empty", 100 | Output: shaping.Output{ 101 | LineBounds: expectedFontExtents, 102 | }, 103 | }, 104 | { 105 | Name: "single simple glyph", 106 | Direction: di.DirectionLTR, 107 | Input: []shaping.Glyph{simpleGlyph}, 108 | Output: shaping.Output{ 109 | Glyphs: []shaping.Glyph{simpleGlyph}, 110 | Advance: simpleGlyph.XAdvance, 111 | GlyphBounds: shaping.Bounds{ 112 | Ascent: simpleGlyph.YBearing, 113 | Descent: fixed.I(0), 114 | }, 115 | LineBounds: expectedFontExtents, 116 | }, 117 | }, 118 | { 119 | Name: "glyph below baseline", 120 | Direction: di.DirectionLTR, 121 | Input: []shaping.Glyph{simpleGlyph, deepGlyph}, 122 | Output: shaping.Output{ 123 | Glyphs: []shaping.Glyph{simpleGlyph, deepGlyph}, 124 | Advance: simpleGlyph.XAdvance + deepGlyph.XAdvance, 125 | GlyphBounds: shaping.Bounds{ 126 | Ascent: simpleGlyph.YBearing, 127 | Descent: deepGlyph.YBearing + deepGlyph.Height, 128 | }, 129 | LineBounds: expectedFontExtents, 130 | }, 131 | }, 132 | { 133 | Name: "single complex glyph", 134 | Direction: di.DirectionLTR, 135 | Input: []shaping.Glyph{offsetGlyph}, 136 | Output: shaping.Output{ 137 | Glyphs: []shaping.Glyph{offsetGlyph}, 138 | Advance: offsetGlyph.XAdvance, 139 | GlyphBounds: shaping.Bounds{ 140 | Ascent: offsetGlyph.YBearing + offsetGlyph.YOffset, 141 | Descent: fixed.I(0), 142 | }, 143 | LineBounds: expectedFontExtents, 144 | }, 145 | }, 146 | { 147 | Name: "vertical text not supported", 148 | Direction: di.Direction(harfbuzz.BottomToTop), 149 | Error: shaping.UnimplementedDirectionError{}, 150 | }, 151 | } { 152 | t.Run(tc.Name, func(t *testing.T) { 153 | output := shaping.Output{ 154 | Glyphs: tc.Input, 155 | LineBounds: expectedFontExtents, 156 | } 157 | err := output.RecalculateAll(tc.Direction) 158 | if tc.Error != nil && !errors.As(err, &tc.Error) { 159 | t.Errorf("expected error of type %T, got %T", tc.Error, err) 160 | } else if tc.Error == nil && !reflect.DeepEqual(output, tc.Output) { 161 | t.Errorf("recalculation incorrect: expected %v, got %v", tc.Output, output) 162 | } 163 | }) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /shaper.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package shaping 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/benoitkugler/textlayout/fonts" 9 | "github.com/benoitkugler/textlayout/harfbuzz" 10 | "github.com/go-text/di" 11 | "golang.org/x/image/math/fixed" 12 | ) 13 | 14 | type Shaper interface { 15 | // Shape takes an Input and shapes it into the Output. 16 | Shape(Input) Output 17 | } 18 | 19 | // MissingGlyphError indicates that the font used in shaping did not 20 | // have a glyph needed to complete the shaping. 21 | type MissingGlyphError struct { 22 | fonts.GID 23 | } 24 | 25 | func (m MissingGlyphError) Error() string { 26 | return fmt.Sprintf("missing glyph with id %d", m.GID) 27 | } 28 | 29 | // InvalidRunError represents an invalid run of text, either because 30 | // the end is before the start or because start or end is greater 31 | // than the length. 32 | type InvalidRunError struct { 33 | RunStart, RunEnd, TextLength int 34 | } 35 | 36 | func (i InvalidRunError) Error() string { 37 | return fmt.Sprintf("run from %d to %d is not valid for text len %d", i.RunStart, i.RunEnd, i.TextLength) 38 | } 39 | 40 | const ( 41 | // scaleShift is the power of 2 with which to automatically scale 42 | // up the input coordinate space of the shaper. This factor will 43 | // be removed prior to returning dimensions. This ensures that the 44 | // returned glyph dimensions take advantage of all of the precision 45 | // that a fixed.Int26_6 can provide. 46 | scaleShift = 6 47 | ) 48 | 49 | // Shape turns an input into an output. 50 | func Shape(input Input) (Output, error) { 51 | // Prepare to shape the text. 52 | // TODO: maybe reuse these buffers for performance? 53 | buf := harfbuzz.NewBuffer() 54 | runes, start, end := input.Text, input.RunStart, input.RunEnd 55 | if end < start { 56 | return Output{}, InvalidRunError{RunStart: start, RunEnd: end, TextLength: len(input.Text)} 57 | } 58 | buf.AddRunes(runes, start, end-start) 59 | // TODO: handle vertical text? 60 | switch input.Direction { 61 | case di.DirectionLTR: 62 | buf.Props.Direction = harfbuzz.LeftToRight 63 | case di.DirectionRTL: 64 | buf.Props.Direction = harfbuzz.RightToLeft 65 | default: 66 | return Output{}, UnimplementedDirectionError{ 67 | Direction: input.Direction, 68 | } 69 | } 70 | buf.Props.Language = input.Language 71 | buf.Props.Script = input.Script 72 | // TODO: figure out what (if anything) to do if this type assertion fails. 73 | font := harfbuzz.NewFont(input.Face.(harfbuzz.Face)) 74 | font.XScale = int32(input.Size.Ceil()) << scaleShift 75 | font.YScale = font.XScale 76 | 77 | // Actually use harfbuzz to shape the text. 78 | buf.Shape(font, nil) 79 | 80 | // Convert the shaped text into an Output. 81 | glyphs := make([]Glyph, len(buf.Info)) 82 | for i := range glyphs { 83 | g := buf.Info[i].Glyph 84 | extents, ok := font.GlyphExtents(g) 85 | if !ok { 86 | // TODO: can this error happen? Will harfbuzz return a 87 | // GID for a glyph that isn't in the font? 88 | return Output{}, MissingGlyphError{GID: g} 89 | } 90 | glyphs[i] = Glyph{ 91 | Width: fixed.I(int(extents.Width)) >> scaleShift, 92 | Height: fixed.I(int(extents.Height)) >> scaleShift, 93 | XBearing: fixed.I(int(extents.XBearing)) >> scaleShift, 94 | YBearing: fixed.I(int(extents.YBearing)) >> scaleShift, 95 | XAdvance: fixed.I(int(buf.Pos[i].XAdvance)) >> scaleShift, 96 | YAdvance: fixed.I(int(buf.Pos[i].YAdvance)) >> scaleShift, 97 | XOffset: fixed.I(int(buf.Pos[i].XOffset)) >> scaleShift, 98 | YOffset: fixed.I(int(buf.Pos[i].YOffset)) >> scaleShift, 99 | Cluster: buf.Info[i].Cluster, 100 | Glyph: g, 101 | Mask: buf.Info[i].Mask, 102 | } 103 | } 104 | out := Output{ 105 | Glyphs: glyphs, 106 | } 107 | fontExtents := font.ExtentsForDirection(buf.Props.Direction) 108 | out.LineBounds = Bounds{ 109 | Ascent: fixed.I(int(fontExtents.Ascender)) >> scaleShift, 110 | Descent: fixed.I(int(fontExtents.Descender)) >> scaleShift, 111 | Gap: fixed.I(int(fontExtents.LineGap)) >> scaleShift, 112 | } 113 | return out, out.RecalculateAll(input.Direction) 114 | } 115 | --------------------------------------------------------------------------------