├── .github └── workflows │ ├── analysis.yml │ └── test.yml ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── di ├── README.md ├── direction.go └── direction_test.go ├── font ├── README.md ├── aat_layout_kern_kerx.go ├── aat_layout_morx.go ├── aat_layout_test.go ├── bitmaps.go ├── bitmaps_test.go ├── cache.go ├── cff │ ├── cff2.go │ ├── cff_gen.go │ ├── cff_src.go │ ├── charsets.go │ ├── charstring.go │ ├── interpreter │ │ ├── charstrings.go │ │ └── interpreter.go │ ├── parser.go │ └── parser_test.go ├── cmap.go ├── cmap_arabic_pua_table.go ├── cmap_test.go ├── font.go ├── font_test.go ├── glyphs.go ├── metadata.go ├── metadata_test.go ├── metrics.go ├── opentype │ ├── opentype.go │ ├── reader.go │ ├── reader_otf.go │ ├── reader_test.go │ ├── reader_woff.go │ ├── tables │ │ ├── aat_ankr_gen.go │ │ ├── aat_ankr_src.go │ │ ├── aat_common.go │ │ ├── aat_common_test.go │ │ ├── aat_feat_gen.go │ │ ├── aat_feat_src.go │ │ ├── aat_kerx_gen.go │ │ ├── aat_kerx_src.go │ │ ├── aat_ltag_gen.go │ │ ├── aat_ltag_src.go │ │ ├── aat_mortx_gen.go │ │ ├── aat_mortx_src.go │ │ ├── aat_properties.go │ │ ├── aat_trak_gen.go │ │ ├── aat_trak_src.go │ │ ├── cmap_gen.go │ │ ├── cmap_src.go │ │ ├── glyphs.go │ │ ├── glyphs_bitmap_gen.go │ │ ├── glyphs_bitmap_src.go │ │ ├── glyphs_glyf_gen.go │ │ ├── glyphs_glyf_src.go │ │ ├── glyphs_misc_gen.go │ │ ├── glyphs_misc_src.go │ │ ├── glyphs_misc_test.go │ │ ├── glyphs_sbix_gen.go │ │ ├── glyphs_sbix_src.go │ │ ├── glyphs_test.go │ │ ├── head_gen.go │ │ ├── head_src.go │ │ ├── hhea_vhea_gen.go │ │ ├── hhea_vhea_src.go │ │ ├── hmtx_vmtx_gen.go │ │ ├── hmtx_vmtx_src.go │ │ ├── kern.go │ │ ├── kern_gen.go │ │ ├── kern_src.go │ │ ├── kern_test.go │ │ ├── maxp_gen.go │ │ ├── maxp_src.go │ │ ├── name_gen.go │ │ ├── name_src.go │ │ ├── name_test.go │ │ ├── os2_gen.go │ │ ├── os2_src.go │ │ ├── ot_gdef_gen.go │ │ ├── ot_gdef_src.go │ │ ├── ot_gpos_gen.go │ │ ├── ot_gpos_src.go │ │ ├── ot_gsub_gen.go │ │ ├── ot_gsub_src.go │ │ ├── ot_layout.go │ │ ├── ot_layout_gen.go │ │ ├── ot_layout_src.go │ │ ├── ot_layout_test.go │ │ ├── ot_properties.go │ │ ├── post_gen.go │ │ ├── post_src.go │ │ ├── tables.go │ │ ├── tables_test.go │ │ ├── xvar_gen.go │ │ ├── xvar_src.go │ │ └── xvar_test.go │ ├── writer.go │ └── writer_test.go ├── os2.go ├── ot_layout.go ├── ot_layout_test.go ├── post.go ├── renderer.go ├── renderer_test.go ├── svg.go ├── testdata │ ├── APACHE.txt │ ├── Amiri-Regular.ttf │ ├── OFL.txt │ ├── Roboto-Regular.ttf │ ├── Selawik-VF-Subset.ttf │ ├── UbuntuMono-R.ttf │ └── readme.md ├── variations.go └── variations_test.go ├── fontscan ├── fontconfig.go ├── fontconfig_test.go ├── fontconfig_test │ ├── conf.d │ │ ├── 10-antialias.conf │ │ ├── 99-custom.conf │ │ ├── other-child.conf │ │ └── other.conf │ ├── fonts.conf │ └── invalid.conf ├── fontmap.go ├── fontmap_sample_test.go ├── fontmap_test.go ├── footprint.go ├── langset.go ├── langset_gen.go ├── langset_test.go ├── lru.go ├── match.go ├── match_test.go ├── readme.md ├── rune_coverage.go ├── rune_coverage_test.go ├── scan.go ├── scan_test.go ├── scandir.go ├── serialize.go ├── serialize_test.go ├── substitutions.go ├── substitutions_table.go └── substitutions_test.go ├── go.mod ├── go.sum ├── harfbuzz ├── LICENSE ├── buffer.go ├── buffer_test.go ├── buffer_verify_test.go ├── emojis_list_test.go ├── emojis_test.go ├── fonts.go ├── fonts_test.go ├── glyph.go ├── harbuzz_test.go ├── harfbuzz.go ├── harfbuzz_shape_input_test.go ├── harfbuzz_shape_test.go ├── ot_aat_layout.go ├── ot_aat_layout_test.go ├── ot_aat_map.go ├── ot_arabic.go ├── ot_arabic_pua_table.go ├── ot_arabic_table.go ├── ot_arabic_test.go ├── ot_arabic_win1256.go ├── ot_hangul.go ├── ot_hebrew.go ├── ot_indic.go ├── ot_indic_machine.go ├── ot_indic_machine.rl ├── ot_indic_table.go ├── ot_indic_test.go ├── ot_kern.go ├── ot_khmer.go ├── ot_khmer_machine.go ├── ot_khmer_machine.rl ├── ot_language.go ├── ot_language_table.go ├── ot_language_test.go ├── ot_layout.go ├── ot_layout_gpos.go ├── ot_layout_gsub.go ├── ot_layout_gsubgpos.go ├── ot_map.go ├── ot_map_test.go ├── ot_myanmar.go ├── ot_myanmar_machine.go ├── ot_myanmar_machine.rl ├── ot_shape_complex.go ├── ot_shape_fallback.go ├── ot_shape_fallback_test.go ├── ot_shape_normalize.go ├── ot_shaper.go ├── ot_tag.go ├── ot_tag_test.go ├── ot_thai.go ├── ot_use.go ├── ot_use_machine.go ├── ot_use_machine.rl ├── ot_use_machine_defs.go ├── ot_use_table.go ├── ot_use_test.go ├── ot_vowels_constraints.go ├── set_digest.go ├── set_digest_test.go ├── shape.go ├── shaper_perf_test.go ├── unicode.go └── unicode_test.go ├── language ├── language.go ├── language_table_gen.go ├── language_test.go ├── scripts.go ├── scripts_table.go └── scripts_test.go ├── segmenter ├── segmenter.go ├── segmenter_test.go ├── test │ ├── GraphemeBreakTest.txt │ ├── LineBreakTest.txt │ └── WordBreakTest.txt ├── unicode14_rules.go └── unicode29_rules.go ├── shaping ├── README.md ├── fuzz_test.go ├── input.go ├── input_test.go ├── lru.go ├── output.go ├── output_test.go ├── paired_delims_table.go ├── render_test.go ├── shaping.go ├── shaping_test.go ├── spacing.go ├── spacing_test.go ├── wrapping.go └── wrapping_test.go ├── testutils └── utils.go └── unicodedata ├── combining_classes.go ├── decomposition.go ├── east_asian_width.go ├── emojis.go ├── general_category.go ├── grapheme_break.go ├── indic.go ├── linebreak.go ├── mirroring.go ├── sentence_break.go ├── unicode.go ├── unicode_test.go ├── vertical_orientation.go └── word_break.go /.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.6.0 19 | 20 | - name: Vet 21 | run: go vet ./... 22 | 23 | - name: Staticcheck 24 | run: staticcheck ./... 25 | -------------------------------------------------------------------------------- /.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, windows-latest, macos-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 | 25 | - name: Build without tests 26 | run: go build ./... 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | -------------------------------------------------------------------------------- /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 | # typesetting 2 | 3 | This library provides typesetting capabilities in pure Go. It is appropriate for use in GUI applications, and is shared by multiple Go UI toolkits including [Fyne](https://fyne.io), [Gio](https://gioui.org), and [Ebitengine](https://ebitengine.org). 4 | 5 | ## Development cycle 6 | 7 | This project, although already used in production by UI toolkits, still evolves rapidly. As such, the library uses unstable versions v0.x.y : the required breaking changes will bump the minor version number (x); the bug fixes and performance improvements the patch number (y). 8 | 9 | ## Review guidelines 10 | 11 | Go-text is a collaboration between many individuals and projects, it is important to us that 12 | designs and decisions are right for the broadest possible audience. 13 | As a result the project will always have 3 maintainers that represent different projects 14 | (currently Fyne.io, Gio and an independent developer). 15 | 16 | ### API and Architectural decisions 17 | 18 | Changes to any core go-text repositories (not including utility or generator repos) will require 19 | sign-off from at least 2 of these 3 maintainers to be approved. 20 | "typesetting" and "render" are currently considered core. 21 | Upon approval the second thumbs up will typically merge the change into the repository. 22 | 23 | Decisions or API discussions are best carried out within the context of a GitHub issue or 24 | pull request for greatest visibility in the future. 25 | 26 | ### Smaller changes and quality of life improvements 27 | 28 | For speed of acceptance on smaller issues it is not always required to have complete consensus. 29 | When a change is deemed to be of minor impact (for example documentation corrections, trivial 30 | bug fixes and straight forward refactoring of content) an expedited review is supported. 31 | In this situation the contribution only requires a single approval (not from the individual 32 | proposing the change). 33 | 34 | If in doubt please seek approval of two maintainers - and feel free to ask questions in the 35 | #go-text channel of gophers Slack server. 36 | -------------------------------------------------------------------------------- /di/README.md: -------------------------------------------------------------------------------- 1 | # di 2 | 3 | di is a library that converts bi-directional text into uni-directional text 4 | -------------------------------------------------------------------------------- /di/direction.go: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import ( 4 | "github.com/go-text/typesetting/harfbuzz" 5 | ) 6 | 7 | // Direction indicates the layout direction of a piece of text. 8 | type Direction uint8 9 | 10 | const ( 11 | // DirectionLTR is for Left-to-Right text. 12 | DirectionLTR Direction = iota 13 | // DirectionRTL is for Right-to-Left text. 14 | DirectionRTL 15 | // DirectionTTB is for Top-to-Bottom text. 16 | DirectionTTB 17 | // DirectionBTT is for Bottom-to-Top text. 18 | DirectionBTT 19 | ) 20 | 21 | const ( 22 | progression Direction = 1 << iota 23 | // axisVertical is the bit for the axis, 0 for horizontal, 1 for vertical 24 | axisVertical 25 | 26 | // If this flag is set, the orientation is chosen 27 | // using the [verticalSideways] flag. 28 | // Otherwise, the segmenter will resolve the orientation based 29 | // on unicode properties 30 | verticalOrientationSet 31 | // verticalSideways is set for 'sideways', unset for 'upright' 32 | // It implies BVerticalOrientationSet is set 33 | verticalSideways 34 | ) 35 | 36 | // IsVertical returns whether d is laid out on a vertical 37 | // axis. If the return value is false, d is on the horizontal 38 | // axis. 39 | func (d Direction) IsVertical() bool { return d&axisVertical != 0 } 40 | 41 | // Axis returns the layout axis for d. 42 | func (d Direction) Axis() Axis { 43 | if d.IsVertical() { 44 | return Vertical 45 | } 46 | return Horizontal 47 | } 48 | 49 | // SwitchAxis switches from horizontal to vertical (and vice versa), preserving 50 | // the progression. 51 | func (d Direction) SwitchAxis() Direction { return d ^ axisVertical } 52 | 53 | // Progression returns the text layout progression for d. 54 | func (d Direction) Progression() Progression { 55 | if d&progression == 0 { 56 | return FromTopLeft 57 | } 58 | return TowardTopLeft 59 | } 60 | 61 | // SetProgression sets the progression, preserving the others bits. 62 | func (d *Direction) SetProgression(p Progression) { 63 | if p == FromTopLeft { 64 | *d &= ^progression 65 | } else { 66 | *d |= progression 67 | } 68 | } 69 | 70 | // Axis indicates the axis of layout for a piece of text. 71 | type Axis bool 72 | 73 | const ( 74 | Horizontal Axis = false 75 | Vertical Axis = true 76 | ) 77 | 78 | // Progression indicates how text is read within its Axis relative 79 | // to the top left corner. 80 | type Progression bool 81 | 82 | const ( 83 | // FromTopLeft indicates text in which a reader starts reading 84 | // at the top left corner of the text and moves away from it. 85 | // DirectionLTR and DirectionTTB are examples of FromTopLeft 86 | // Progression. 87 | FromTopLeft Progression = false 88 | // TowardTopLeft indicates text in which a reader starts reading 89 | // at the opposite end of the text's Axis from the top left corner 90 | // and moves towards it. DirectionRTL and DirectionBTT are examples 91 | // of TowardTopLeft progression. 92 | TowardTopLeft Progression = true 93 | ) 94 | 95 | // HasVerticalOrientation returns true if the direction has set up 96 | // an orientation for vertical text (typically using [SetSideways] or [SetUpright]) 97 | func (d Direction) HasVerticalOrientation() bool { return d&verticalOrientationSet != 0 } 98 | 99 | // IsSideways returns true if the direction is vertical with a 'sideways' 100 | // orientation. 101 | // 102 | // When shaping vertical text, 'sideways' means that the glyphs are rotated 103 | // by 90°, clock-wise. This flag should be used by renderers to properly 104 | // rotate the glyphs when drawing. 105 | func (d Direction) IsSideways() bool { return d.IsVertical() && d&verticalSideways != 0 } 106 | 107 | // SetSideways makes d vertical with 'sideways' or 'upright' orientation, preserving only the 108 | // progression. 109 | func (d *Direction) SetSideways(sideways bool) { 110 | *d |= axisVertical | verticalOrientationSet 111 | if sideways { 112 | *d |= verticalSideways 113 | } else { 114 | *d &= ^verticalSideways 115 | } 116 | } 117 | 118 | // Harfbuzz returns the equivalent direction used by harfbuzz. 119 | func (d Direction) Harfbuzz() harfbuzz.Direction { 120 | switch d & (progression | axisVertical) { 121 | case DirectionRTL: 122 | return harfbuzz.RightToLeft 123 | case DirectionBTT: 124 | return harfbuzz.BottomToTop 125 | case DirectionTTB: 126 | return harfbuzz.TopToBottom 127 | default: 128 | return harfbuzz.LeftToRight 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /di/direction_test.go: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-text/typesetting/harfbuzz" 7 | tu "github.com/go-text/typesetting/testutils" 8 | ) 9 | 10 | func TestDirection(t *testing.T) { 11 | tu.Assert(t, DirectionLTR.Axis() == Horizontal) 12 | tu.Assert(t, DirectionRTL.Axis() == Horizontal) 13 | tu.Assert(t, DirectionTTB.Axis() == Vertical) 14 | tu.Assert(t, DirectionBTT.Axis() == Vertical) 15 | tu.Assert(t, !DirectionLTR.IsVertical()) 16 | tu.Assert(t, !DirectionRTL.IsVertical()) 17 | tu.Assert(t, DirectionTTB.IsVertical()) 18 | tu.Assert(t, DirectionBTT.IsVertical()) 19 | 20 | tu.Assert(t, DirectionLTR.Progression() == FromTopLeft) 21 | tu.Assert(t, DirectionRTL.Progression() == TowardTopLeft) 22 | tu.Assert(t, DirectionTTB.Progression() == FromTopLeft) 23 | tu.Assert(t, DirectionBTT.Progression() == TowardTopLeft) 24 | 25 | tu.Assert(t, !DirectionTTB.IsSideways()) 26 | tu.Assert(t, !DirectionBTT.IsSideways()) 27 | 28 | tu.Assert(t, DirectionLTR.SwitchAxis() == DirectionTTB) 29 | tu.Assert(t, DirectionRTL.SwitchAxis() == DirectionBTT) 30 | tu.Assert(t, DirectionTTB.SwitchAxis() == DirectionLTR) 31 | tu.Assert(t, DirectionBTT.SwitchAxis() == DirectionRTL) 32 | 33 | tu.Assert(t, DirectionLTR.Harfbuzz() == harfbuzz.LeftToRight) 34 | tu.Assert(t, DirectionRTL.Harfbuzz() == harfbuzz.RightToLeft) 35 | tu.Assert(t, DirectionTTB.Harfbuzz() == harfbuzz.TopToBottom) 36 | tu.Assert(t, DirectionBTT.Harfbuzz() == harfbuzz.BottomToTop) 37 | 38 | tu.Assert(t, !DirectionLTR.HasVerticalOrientation()) 39 | tu.Assert(t, !DirectionRTL.HasVerticalOrientation()) 40 | tu.Assert(t, !DirectionTTB.HasVerticalOrientation()) 41 | tu.Assert(t, !DirectionBTT.HasVerticalOrientation()) 42 | 43 | for _, test := range []struct { 44 | sideways bool 45 | progression Progression 46 | hb harfbuzz.Direction 47 | }{ 48 | {true, FromTopLeft, harfbuzz.TopToBottom}, 49 | {true, TowardTopLeft, harfbuzz.BottomToTop}, 50 | {false, FromTopLeft, harfbuzz.TopToBottom}, 51 | {false, TowardTopLeft, harfbuzz.BottomToTop}, 52 | } { 53 | d := axisVertical 54 | d.SetProgression(test.progression) 55 | 56 | tu.Assert(t, !d.HasVerticalOrientation()) 57 | d.SetSideways(test.sideways) 58 | 59 | tu.Assert(t, d.HasVerticalOrientation()) 60 | tu.Assert(t, d.IsSideways() == test.sideways) 61 | tu.Assert(t, d.Axis() == Vertical) 62 | tu.Assert(t, d.Progression() == test.progression) 63 | tu.Assert(t, d.Harfbuzz() == test.hb) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /font/README.md: -------------------------------------------------------------------------------- 1 | # font 2 | 3 | font is a library that handles loading and utilizing Opentype fonts. 4 | 5 | `font/opentype` implements the low level parsing of a font file and its tables, 6 | and `font` provides an higher level API usable by shapers and renderers. 7 | -------------------------------------------------------------------------------- /font/aat_layout_morx.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package font 4 | 5 | import "github.com/go-text/typesetting/font/opentype/tables" 6 | 7 | type Morx []MorxChain 8 | 9 | func newMorx(table tables.Morx) Morx { 10 | if len(table.Chains) == 0 { 11 | return nil 12 | } 13 | out := make(Morx, len(table.Chains)) 14 | for i, c := range table.Chains { 15 | out[i] = newMorxChain(c) 16 | } 17 | return out 18 | } 19 | 20 | type MorxChain struct { 21 | Features []tables.AATFeature 22 | Subtables []MorxSubtable 23 | DefaultFlags uint32 24 | } 25 | 26 | func newMorxChain(table tables.MorxChain) (out MorxChain) { 27 | out.DefaultFlags = table.Flags 28 | out.Features = table.Features 29 | out.Subtables = make([]MorxSubtable, len(table.Subtables)) 30 | for i, s := range table.Subtables { 31 | out.Subtables[i] = newMorxSubtable(s) 32 | } 33 | return out 34 | } 35 | 36 | type MorxSubtable struct { 37 | Data interface{ isMorxSubtable() } 38 | Coverage uint8 // high byte of the coverage flag 39 | Flags uint32 // Mask identifying which subtable this is. 40 | } 41 | 42 | func (MorxRearrangementSubtable) isMorxSubtable() {} 43 | func (MorxContextualSubtable) isMorxSubtable() {} 44 | func (MorxLigatureSubtable) isMorxSubtable() {} 45 | func (MorxNonContextualSubtable) isMorxSubtable() {} 46 | func (MorxInsertionSubtable) isMorxSubtable() {} 47 | 48 | func newMorxSubtable(table tables.MorxChainSubtable) (out MorxSubtable) { 49 | out.Coverage = table.Coverage 50 | out.Flags = table.SubFeatureFlags 51 | switch data := table.Data.(type) { 52 | case tables.MorxSubtableRearrangement: 53 | out.Data = MorxRearrangementSubtable(newAATStableTable(data.AATStateTableExt)) 54 | case tables.MorxSubtableContextual: 55 | out.Data = MorxContextualSubtable{ 56 | Machine: newAATStableTable(data.AATStateTableExt), 57 | Substitutions: data.Substitutions.Substitutions, 58 | } 59 | case tables.MorxSubtableLigature: 60 | s := MorxLigatureSubtable{ 61 | Machine: newAATStableTable(data.AATStateTableExt), 62 | LigatureAction: data.LigActions, 63 | Components: data.Components, 64 | Ligatures: make([]GID, len(data.Ligatures)), 65 | } 66 | for i, g := range data.Ligatures { 67 | s.Ligatures[i] = GID(g) 68 | } 69 | out.Data = s 70 | case tables.MorxSubtableNonContextual: 71 | out.Data = MorxNonContextualSubtable{Class: data.Class} 72 | case tables.MorxSubtableInsertion: 73 | s := MorxInsertionSubtable{ 74 | Machine: newAATStableTable(data.AATStateTableExt), 75 | Insertions: make([]GID, len(data.Insertions)), 76 | } 77 | for i, g := range data.Insertions { 78 | s.Insertions[i] = GID(g) 79 | } 80 | out.Data = s 81 | } 82 | return out 83 | } 84 | 85 | type MorxRearrangementSubtable AATStateTable 86 | 87 | type MorxContextualSubtable struct { 88 | Substitutions []tables.AATLookup 89 | Machine AATStateTable 90 | } 91 | 92 | type MorxLigatureSubtable struct { 93 | LigatureAction []uint32 94 | Components []uint16 95 | Ligatures []GID 96 | Machine AATStateTable 97 | } 98 | 99 | type MorxNonContextualSubtable struct { 100 | Class tables.AATLookup // the lookup value is interpreted as a GlyphIndex 101 | } 102 | 103 | type MorxInsertionSubtable struct { 104 | // After successul parsing, this array may be safely 105 | // indexed by the indexes and counts from Machine entries. 106 | Insertions []GID 107 | Machine AATStateTable 108 | } 109 | -------------------------------------------------------------------------------- /font/bitmaps_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package font 4 | 5 | import ( 6 | "testing" 7 | 8 | td "github.com/go-text/typesetting-utils/opentype" 9 | "github.com/go-text/typesetting/font/opentype/tables" 10 | tu "github.com/go-text/typesetting/testutils" 11 | ) 12 | 13 | func TestBloc(t *testing.T) { 14 | blocT, err := td.Files.ReadFile("toys/tables/bloc.bin") 15 | tu.AssertNoErr(t, err) 16 | bloc, _, err := tables.ParseCBLC(blocT) 17 | tu.AssertNoErr(t, err) 18 | 19 | bdatT, err := td.Files.ReadFile("toys/tables/bdat.bin") 20 | tu.AssertNoErr(t, err) 21 | 22 | bt, err := newBitmap(bloc, bdatT) 23 | tu.AssertNoErr(t, err) 24 | tu.Assert(t, len(bt) == 1) 25 | tu.Assert(t, len(bt[0].subTables) == 4) 26 | } 27 | 28 | func TestCBLC(t *testing.T) { 29 | for _, file := range td.WithCBLC { 30 | fp := readFontFile(t, file.Path) 31 | 32 | cblc, _, err := tables.ParseCBLC(readTable(t, fp, "CBLC")) 33 | tu.AssertNoErr(t, err) 34 | cbdt := readTable(t, fp, "CBDT") 35 | 36 | _, err = newBitmap(cblc, cbdt) 37 | tu.AssertNoErr(t, err) 38 | } 39 | } 40 | 41 | func TestEBLC(t *testing.T) { 42 | for _, file := range td.WithEBLC { 43 | fp := readFontFile(t, file.Path) 44 | 45 | eblc, _, err := tables.ParseCBLC(readTable(t, fp, "EBLC")) 46 | tu.AssertNoErr(t, err) 47 | ebdt := readTable(t, fp, "EBDT") 48 | 49 | _, err = newBitmap(eblc, ebdt) 50 | tu.AssertNoErr(t, err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /font/cache.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | type glyphExtents struct { 4 | valid bool 5 | extents GlyphExtents 6 | } 7 | 8 | type extentsCache []glyphExtents 9 | 10 | func (ec extentsCache) get(gid GID) (GlyphExtents, bool) { 11 | if int(gid) >= len(ec) { 12 | return GlyphExtents{}, false 13 | } 14 | ge := ec[gid] 15 | return ge.extents, ge.valid 16 | } 17 | 18 | func (ec extentsCache) set(gid GID, extents GlyphExtents) { 19 | if int(gid) >= len(ec) { 20 | return 21 | } 22 | ec[gid].valid = true 23 | ec[gid].extents = extents 24 | } 25 | 26 | func (ec extentsCache) reset() { 27 | for i := range ec { 28 | ec[i] = glyphExtents{} 29 | } 30 | } 31 | 32 | func (f *Face) GlyphExtents(glyph GID) (GlyphExtents, bool) { 33 | if e, ok := f.extentsCache.get(glyph); ok { 34 | return e, ok 35 | } 36 | e, ok := f.glyphExtentsRaw(glyph) 37 | if ok { 38 | f.extentsCache.set(glyph, e) 39 | } 40 | return e, ok 41 | } 42 | -------------------------------------------------------------------------------- /font/cff/cff_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package cff 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from cff2_src.go. DO NOT EDIT 11 | 12 | func (item *header2) mustParse(src []byte) { 13 | _ = src[4] // early bound checking 14 | item.majorVersion = src[0] 15 | item.minorVersion = src[1] 16 | item.headerSize = src[2] 17 | item.topDictLength = binary.BigEndian.Uint16(src[3:]) 18 | } 19 | 20 | func (item *indexStart) mustParse(src []byte) { 21 | _ = src[4] // early bound checking 22 | item.count = binary.BigEndian.Uint32(src[0:]) 23 | item.offSize = src[4] 24 | } 25 | 26 | func parseFdSelect(src []byte, fdsCount int) (fdSelect, int, error) { 27 | var item fdSelect 28 | 29 | if L := len(src); L < 1 { 30 | return item, 0, fmt.Errorf("reading fdSelect: "+"EOF: expected length: 1, got %d", L) 31 | } 32 | format := uint8(src[0]) 33 | var ( 34 | read int 35 | err error 36 | ) 37 | switch format { 38 | case 0: 39 | item, read, err = parseFdSelect0(src[0:], fdsCount) 40 | case 3: 41 | item, read, err = parseFdSelect3(src[0:]) 42 | case 4: 43 | item, read, err = parseFdSelect4(src[0:]) 44 | default: 45 | err = fmt.Errorf("unsupported fdSelect format %d", format) 46 | } 47 | if err != nil { 48 | return item, 0, fmt.Errorf("reading fdSelect: %s", err) 49 | } 50 | 51 | return item, read, nil 52 | } 53 | 54 | func parseFdSelect0(src []byte, fdsCount int) (fdSelect0, int, error) { 55 | var item fdSelect0 56 | n := 0 57 | if L := len(src); L < 1 { 58 | return item, 0, fmt.Errorf("reading fdSelect0: "+"EOF: expected length: 1, got %d", L) 59 | } 60 | item.format = src[0] 61 | n += 1 62 | 63 | { 64 | 65 | L := int(1 + fdsCount) 66 | if len(src) < L { 67 | return item, 0, fmt.Errorf("reading fdSelect0: "+"EOF: expected length: %d, got %d", L, len(src)) 68 | } 69 | item.fds = src[1:L] 70 | n = L 71 | } 72 | return item, n, nil 73 | } 74 | 75 | func parseFdSelect3(src []byte) (fdSelect3, int, error) { 76 | var item fdSelect3 77 | n := 0 78 | if L := len(src); L < 3 { 79 | return item, 0, fmt.Errorf("reading fdSelect3: "+"EOF: expected length: 3, got %d", L) 80 | } 81 | _ = src[2] // early bound checking 82 | item.format = src[0] 83 | item.nRanges = binary.BigEndian.Uint16(src[1:]) 84 | n += 3 85 | 86 | { 87 | arrayLength := int(item.nRanges) 88 | 89 | if L := len(src); L < 3+arrayLength*3 { 90 | return item, 0, fmt.Errorf("reading fdSelect3: "+"EOF: expected length: %d, got %d", 3+arrayLength*3, L) 91 | } 92 | 93 | item.ranges = make([]range3, arrayLength) // allocation guarded by the previous check 94 | for i := range item.ranges { 95 | item.ranges[i].mustParse(src[3+i*3:]) 96 | } 97 | n += arrayLength * 3 98 | } 99 | if L := len(src); L < n+2 { 100 | return item, 0, fmt.Errorf("reading fdSelect3: "+"EOF: expected length: n + 2, got %d", L) 101 | } 102 | item.sentinel = binary.BigEndian.Uint16(src[n:]) 103 | n += 2 104 | 105 | return item, n, nil 106 | } 107 | 108 | func parseFdSelect4(src []byte) (fdSelect4, int, error) { 109 | var item fdSelect4 110 | n := 0 111 | if L := len(src); L < 5 { 112 | return item, 0, fmt.Errorf("reading fdSelect4: "+"EOF: expected length: 5, got %d", L) 113 | } 114 | _ = src[4] // early bound checking 115 | item.format = src[0] 116 | item.nRanges = binary.BigEndian.Uint32(src[1:]) 117 | n += 5 118 | 119 | { 120 | arrayLength := int(item.nRanges) 121 | 122 | if L := len(src); L < 5+arrayLength*6 { 123 | return item, 0, fmt.Errorf("reading fdSelect4: "+"EOF: expected length: %d, got %d", 5+arrayLength*6, L) 124 | } 125 | 126 | item.ranges = make([]range4, arrayLength) // allocation guarded by the previous check 127 | for i := range item.ranges { 128 | item.ranges[i].mustParse(src[5+i*6:]) 129 | } 130 | n += arrayLength * 6 131 | } 132 | if L := len(src); L < n+4 { 133 | return item, 0, fmt.Errorf("reading fdSelect4: "+"EOF: expected length: n + 4, got %d", L) 134 | } 135 | item.sentinel = binary.BigEndian.Uint32(src[n:]) 136 | n += 4 137 | 138 | return item, n, nil 139 | } 140 | 141 | func (item *range3) mustParse(src []byte) { 142 | _ = src[2] // early bound checking 143 | item.first = binary.BigEndian.Uint16(src[0:]) 144 | item.fd = src[2] 145 | } 146 | 147 | func (item *range4) mustParse(src []byte) { 148 | _ = src[5] // early bound checking 149 | item.first = binary.BigEndian.Uint32(src[0:]) 150 | item.fd = binary.BigEndian.Uint16(src[4:]) 151 | } 152 | -------------------------------------------------------------------------------- /font/cff/cff_src.go: -------------------------------------------------------------------------------- 1 | package cff 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/go-text/typesetting/font/opentype/tables" 7 | ) 8 | 9 | //go:generate ../../../../../typesetting-utils/generators/binarygen/cmd/generator . _src.go 10 | 11 | type header2 struct { 12 | majorVersion uint8 // Format major version. Set to 2. 13 | minorVersion uint8 // Format minor version. Set to zero. 14 | headerSize uint8 // Header size (bytes). 15 | topDictLength uint16 // Length of Top DICT structure in bytes. 16 | } 17 | 18 | type indexStart struct { 19 | count uint32 // Number of objects stored in INDEX 20 | offSize uint8 // Offset array element size 21 | // then 22 | // offset []Offset 23 | // data []byte 24 | } 25 | 26 | //lint:ignore U1000 this type is required so that the code generator add a ParseFdSelect function 27 | type dummy struct { 28 | fd fdSelect 29 | } 30 | 31 | // fdSelect holds a CFF font's Font Dict Select data. 32 | type fdSelect interface { 33 | isFdSelect() 34 | 35 | fontDictIndex(glyph tables.GlyphID) (byte, error) 36 | // return the maximum index + 1 (it's the length of an array 37 | // which can be safely indexed by the indexes) 38 | extent() int 39 | } 40 | 41 | func (fdSelect0) isFdSelect() {} 42 | func (fdSelect3) isFdSelect() {} 43 | func (fdSelect4) isFdSelect() {} 44 | 45 | type fdSelect0 struct { 46 | format uint8 `unionTag:"0"` // Set to 0 47 | fds []uint8 // [nGlyphs] FD selector array 48 | } 49 | 50 | var errGlyph = errors.New("invalid glyph index") 51 | 52 | func (fds fdSelect0) fontDictIndex(glyph tables.GlyphID) (byte, error) { 53 | if int(glyph) >= len(fds.fds) { 54 | return 0, errGlyph 55 | } 56 | return fds.fds[glyph], nil 57 | } 58 | 59 | func (fds fdSelect0) extent() int { 60 | max := -1 61 | for _, b := range fds.fds { 62 | if int(b) > max { 63 | max = int(b) 64 | } 65 | } 66 | return max + 1 67 | } 68 | 69 | type fdSelect3 struct { 70 | format uint8 `unionTag:"3"` // Set to 3 71 | nRanges uint16 // Number of ranges 72 | ranges []range3 `arrayCount:"ComputedField-nRanges"` // [nRanges] Array of Range3 records (see below) 73 | sentinel uint16 // Sentinel GID 74 | } 75 | 76 | type range3 struct { 77 | first tables.GlyphID // First glyph index in range 78 | fd uint8 // FD index for all glyphs in range 79 | } 80 | 81 | func (fds fdSelect3) fontDictIndex(x tables.GlyphID) (byte, error) { 82 | lo, hi := 0, len(fds.ranges) 83 | for lo < hi { 84 | i := (lo + hi) / 2 85 | r := fds.ranges[i] 86 | xlo := r.first 87 | if x < xlo { 88 | hi = i 89 | continue 90 | } 91 | xhi := fds.sentinel 92 | if i < len(fds.ranges)-1 { 93 | xhi = fds.ranges[i+1].first 94 | } 95 | if xhi <= x { 96 | lo = i + 1 97 | continue 98 | } 99 | return r.fd, nil 100 | } 101 | return 0, errGlyph 102 | } 103 | 104 | func (fds fdSelect3) extent() int { 105 | max := -1 106 | for _, b := range fds.ranges { 107 | if int(b.fd) > max { 108 | max = int(b.fd) 109 | } 110 | } 111 | return max + 1 112 | } 113 | 114 | type fdSelect4 struct { 115 | format uint8 `unionTag:"4"` // Set to 4 116 | nRanges uint32 // Number of ranges 117 | ranges []range4 `arrayCount:"ComputedField-nRanges"` // [nRanges] Array of Range4 records (see below) 118 | sentinel uint32 // Sentinel GID 119 | } 120 | 121 | type range4 struct { 122 | first uint32 // First glyph index in range 123 | fd uint16 // FD index for all glyphs in range 124 | } 125 | 126 | func (fds fdSelect4) fontDictIndex(x tables.GlyphID) (byte, error) { 127 | fd, err := fds.fontDictIndex32(uint32(x)) 128 | return byte(fd), err 129 | } 130 | 131 | func (fds fdSelect4) fontDictIndex32(x uint32) (uint16, error) { 132 | lo, hi := 0, len(fds.ranges) 133 | for lo < hi { 134 | i := (lo + hi) / 2 135 | r := fds.ranges[i] 136 | xlo := r.first 137 | if x < xlo { 138 | hi = i 139 | continue 140 | } 141 | xhi := fds.sentinel 142 | if i < len(fds.ranges)-1 { 143 | xhi = fds.ranges[i+1].first 144 | } 145 | if xhi <= x { 146 | lo = i + 1 147 | continue 148 | } 149 | return r.fd, nil 150 | } 151 | return 0, errGlyph 152 | } 153 | 154 | func (fds fdSelect4) extent() int { 155 | max := -1 156 | for _, b := range fds.ranges { 157 | if int(b.fd) > max { 158 | max = int(b.fd) 159 | } 160 | } 161 | return max + 1 162 | } 163 | -------------------------------------------------------------------------------- /font/metadata_test.go: -------------------------------------------------------------------------------- 1 | package font 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | td "github.com/go-text/typesetting-utils/opentype" 9 | ot "github.com/go-text/typesetting/font/opentype" 10 | tu "github.com/go-text/typesetting/testutils" 11 | ) 12 | 13 | func TestMetadata(t *testing.T) { 14 | tests := []struct { 15 | fontPath string 16 | aspect Aspect 17 | family string 18 | }{ 19 | { 20 | "common/Roboto-BoldItalic.ttf", 21 | Aspect{StyleItalic, WeightBold, StretchNormal}, 22 | "Roboto", 23 | }, 24 | { 25 | "common/NotoSansArabic.ttf", 26 | Aspect{StyleNormal, WeightNormal, StretchNormal}, 27 | "Noto Sans Arabic", 28 | }, 29 | { 30 | "common/DejaVuSans.ttf", 31 | Aspect{StyleNormal, WeightNormal, StretchNormal}, 32 | "DejaVu Sans", 33 | }, 34 | } 35 | 36 | for _, test := range tests { 37 | f, err := td.Files.ReadFile(test.fontPath) 38 | tu.AssertNoErr(t, err) 39 | 40 | ld, err := ot.NewLoader(bytes.NewReader(f)) 41 | tu.AssertNoErr(t, err) 42 | 43 | got, _ := Describe(ld, nil) 44 | tu.AssertC(t, got.Aspect == test.aspect, fmt.Sprint(got.Aspect)) 45 | tu.AssertC(t, got.Family == test.family, got.Family) 46 | 47 | // check the two APIs are consistent 48 | ft, err := NewFont(ld) 49 | tu.AssertNoErr(t, err) 50 | tu.Assert(t, ft.Describe() == got) 51 | } 52 | } 53 | 54 | func Test_IsMonospace(t *testing.T) { 55 | for _, file := range tu.Filenames(t, "common") { 56 | f, err := td.Files.ReadFile(file) 57 | tu.AssertNoErr(t, err) 58 | 59 | ld, err := ot.NewLoader(bytes.NewReader(f)) 60 | tu.AssertNoErr(t, err) 61 | 62 | fd, err := NewFont(ld) 63 | tu.AssertNoErr(t, err) 64 | tu.AssertC(t, td.Monospace[file] == fd.IsMonospace(), file) 65 | } 66 | 67 | tu.Assert(t, !(&Font{}).IsMonospace()) // check it does not crash 68 | } 69 | 70 | func TestAspect_inferFromStyle(t *testing.T) { 71 | styn, wn, sten := StyleNormal, WeightNormal, StretchNormal 72 | tests := []struct { 73 | args string 74 | fields Aspect 75 | want Aspect 76 | }{ 77 | { 78 | "", Aspect{styn, wn, sten}, Aspect{styn, wn, sten}, // no op 79 | }, 80 | { 81 | "Black", Aspect{0, 0, 0}, Aspect{0, WeightBlack, 0}, 82 | }, 83 | { 84 | "conDensed", Aspect{0, 0, 0}, Aspect{0, 0, StretchCondensed}, 85 | }, 86 | { 87 | "ITALIC", Aspect{0, 0, 0}, Aspect{StyleItalic, 0, 0}, 88 | }, 89 | { 90 | "black", Aspect{0, WeightNormal, 0}, Aspect{0, WeightNormal, 0}, // respect initial value 91 | }, 92 | { 93 | "black oblique", Aspect{0, 0, 0}, Aspect{StyleItalic, WeightBlack, 0}, 94 | }, 95 | } 96 | for _, tt := range tests { 97 | as := tt.fields 98 | as.inferFromStyle(tt.args) 99 | tu.AssertC(t, as == tt.want, tt.args) 100 | } 101 | } 102 | 103 | func TestAspectFromOS2(t *testing.T) { 104 | // This font has two different weight values : 105 | // 400, in the OS/2 table and 380, in the style description 106 | f, err := td.Files.ReadFile("common/DejaVuSans.ttf") 107 | tu.AssertNoErr(t, err) 108 | 109 | ld, err := ot.NewLoader(bytes.NewReader(f)) 110 | tu.AssertNoErr(t, err) 111 | 112 | fd, _ := newFontDescriptor(ld, nil) 113 | 114 | raw := fd.rawAspect() 115 | tu.Assert(t, raw.Weight == WeightNormal) 116 | 117 | var inferred Aspect 118 | inferred.inferFromStyle(fd.additionalStyle()) 119 | tu.Assert(t, inferred.Weight == 380) 120 | } 121 | 122 | func TestFamily(t *testing.T) { 123 | f, err := td.Files.ReadFile("collections/msgothic.ttc") 124 | tu.AssertNoErr(t, err) 125 | 126 | faces, err := ParseTTC(bytes.NewReader(f)) 127 | tu.AssertNoErr(t, err) 128 | 129 | tu.Assert(t, len(faces) == 3) 130 | tu.Assert(t, faces[0].Describe().Family == "MS Gothic") 131 | } 132 | -------------------------------------------------------------------------------- /font/opentype/opentype.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | // Package opentype provides the low level routines 4 | // required to read and write Opentype font files, including collections. 5 | // 6 | // This package is designed to provide an efficient, lazy, reading API. 7 | // 8 | // For the parsing of the various tables, see package [tables]. 9 | package opentype 10 | 11 | type Tag uint32 12 | 13 | // NewTag returns the tag for . 14 | func NewTag(a, b, c, d byte) Tag { 15 | return Tag(uint32(d) | uint32(c)<<8 | uint32(b)<<16 | uint32(a)<<24) 16 | } 17 | 18 | // MustNewTag gives you the Tag corresponding to the acronym. 19 | // This function will panic if the string passed in is not 4 bytes long. 20 | func MustNewTag(str string) Tag { 21 | if len(str) != 4 { 22 | panic("invalid tag: must be exactly 4 bytes") 23 | } 24 | _ = str[3] 25 | return NewTag(str[0], str[1], str[2], str[3]) 26 | } 27 | 28 | // String return the ASCII form of the tag. 29 | func (t Tag) String() string { 30 | return string([]byte{ 31 | byte(t >> 24), 32 | byte(t >> 16), 33 | byte(t >> 8), 34 | byte(t), 35 | }) 36 | } 37 | 38 | type GID uint32 39 | 40 | type GlyphExtents struct { 41 | XBearing float32 // Left side of glyph from origin 42 | YBearing float32 // Top side of glyph from origin 43 | Width float32 // Distance from left to right side 44 | Height float32 // Distance from top to bottom side 45 | } 46 | 47 | type SegmentOp uint8 48 | 49 | const ( 50 | SegmentOpMoveTo SegmentOp = iota 51 | SegmentOpLineTo 52 | SegmentOpQuadTo 53 | SegmentOpCubeTo 54 | ) 55 | 56 | type SegmentPoint struct { 57 | X, Y float32 // expressed in fonts units 58 | } 59 | 60 | // Move translates the point. 61 | func (pt *SegmentPoint) Move(dx, dy float32) { 62 | pt.X += dx 63 | pt.Y += dy 64 | } 65 | 66 | type Segment struct { 67 | Op SegmentOp 68 | // Args is up to three (x, y) coordinates, depending on the 69 | // operation. 70 | // The Y axis increases up. 71 | Args [3]SegmentPoint 72 | } 73 | 74 | // ArgsSlice returns the effective slice of points 75 | // used (whose length is between 1 and 3). 76 | func (s *Segment) ArgsSlice() []SegmentPoint { 77 | switch s.Op { 78 | case SegmentOpMoveTo, SegmentOpLineTo: 79 | return s.Args[0:1] 80 | case SegmentOpQuadTo: 81 | return s.Args[0:2] 82 | case SegmentOpCubeTo: 83 | return s.Args[0:3] 84 | default: 85 | panic("unreachable") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /font/opentype/reader_otf.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package opentype 4 | 5 | import ( 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "io" 10 | ) 11 | 12 | // An Entry in an OpenType table. 13 | type otfEntry struct { 14 | Tag Tag 15 | CheckSum uint32 16 | Offset uint32 17 | Length uint32 18 | } 19 | 20 | const ( 21 | otfHeaderSize = 12 22 | otfEntrySize = 16 23 | ) 24 | 25 | func readOTFHeader(r io.Reader) (flavor Tag, numTables uint16, err error) { 26 | var buf [otfHeaderSize]byte 27 | if _, err := r.Read(buf[:]); err != nil { 28 | return 0, 0, fmt.Errorf("invalid OpenType header: %s", err) 29 | } 30 | 31 | return NewTag(buf[0], buf[1], buf[2], buf[3]), binary.BigEndian.Uint16(buf[4:6]), nil 32 | } 33 | 34 | func readOTFEntry(r io.Reader) (otfEntry, error) { 35 | var ( 36 | buf [otfEntrySize]byte 37 | entry otfEntry 38 | ) 39 | if _, err := io.ReadFull(r, buf[:]); err != nil { 40 | return entry, fmt.Errorf("invalid directory entry: %s", err) 41 | } 42 | 43 | entry.Tag = Tag(binary.BigEndian.Uint32(buf[0:4])) 44 | entry.CheckSum = binary.BigEndian.Uint32(buf[4:8]) 45 | entry.Offset = binary.BigEndian.Uint32(buf[8:12]) 46 | entry.Length = binary.BigEndian.Uint32(buf[12:16]) 47 | 48 | return entry, nil 49 | } 50 | 51 | // parseOTF reads an OpenTyp (.otf) or TrueType (.ttf) file and returns a Font. 52 | // If the parsing fails, then an error is returned and Font will be nil. 53 | // `offset` is the beginning of the ressource in the file (non zero for collections) 54 | // `relativeOffset` is true when the table offset are expresed relatively to the ressource start 55 | // (that is, `offset`) rather than to the file start. 56 | func parseOTF(file Resource, offset uint32, relativeOffset bool) (*Loader, error) { 57 | _, err := file.Seek(int64(offset), io.SeekStart) 58 | if err != nil { 59 | return nil, fmt.Errorf("invalid offset: %s", err) 60 | } 61 | 62 | flavor, numTables, err := readOTFHeader(file) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | pr := &Loader{ 68 | file: file, 69 | tables: make(map[Tag]tableSection, numTables), 70 | Type: flavor, 71 | } 72 | 73 | for i := 0; i < int(numTables); i++ { 74 | entry, err := readOTFEntry(file) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | if _, found := pr.tables[entry.Tag]; found { 80 | // ignore duplicate tables – the first one wins 81 | continue 82 | } 83 | 84 | sec := tableSection{ 85 | offset: entry.Offset, 86 | length: entry.Length, 87 | } 88 | // adapt the relative offsets 89 | if relativeOffset { 90 | sec.offset += offset 91 | if sec.offset < offset { // check for overflow 92 | return nil, errors.New("unsupported table offset or length") 93 | } 94 | } 95 | pr.tables[entry.Tag] = sec 96 | } 97 | 98 | return pr, nil 99 | } 100 | -------------------------------------------------------------------------------- /font/opentype/reader_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package opentype 4 | 5 | import ( 6 | "bytes" 7 | "math/rand" 8 | "testing" 9 | 10 | td "github.com/go-text/typesetting-utils/opentype" 11 | tu "github.com/go-text/typesetting/testutils" 12 | ) 13 | 14 | func TestParseCrashers(t *testing.T) { 15 | font, err := NewLoader(bytes.NewReader([]byte{})) 16 | tu.Assert(t, font == nil) 17 | tu.Assert(t, err != nil) 18 | 19 | for range [50]int{} { 20 | L := rand.Intn(100) 21 | input := make([]byte, L) 22 | rand.Read(input) 23 | 24 | _, err = NewLoader(bytes.NewReader(input)) 25 | tu.Assert(t, err != nil) 26 | 27 | _, err = NewLoaders(bytes.NewReader(input)) 28 | tu.Assert(t, err != nil) 29 | } 30 | } 31 | 32 | func TestCollection(t *testing.T) { 33 | for _, filename := range tu.Filenames(t, "collections") { 34 | f, err := td.Files.ReadFile(filename) 35 | tu.AssertNoErr(t, err) 36 | 37 | fonts, err := NewLoaders(bytes.NewReader(f)) 38 | tu.AssertC(t, err == nil, filename) 39 | 40 | for _, font := range fonts { 41 | tu.Assert(t, len(font.tables) != 0) 42 | } 43 | 44 | // check that NewLoader indeed fail on collections 45 | _, err = NewLoader(bytes.NewReader(f)) 46 | tu.Assert(t, err != nil) 47 | } 48 | 49 | // check that it also works for single font files 50 | for _, filename := range tu.Filenames(t, "common") { 51 | f, err := td.Files.ReadFile(filename) 52 | tu.AssertNoErr(t, err) 53 | 54 | fonts, err := NewLoaders(bytes.NewReader(f)) 55 | tu.AssertC(t, err == nil, filename) 56 | 57 | if len(fonts) != 1 { 58 | tu.Assert(t, len(fonts) == 1) 59 | } 60 | } 61 | } 62 | 63 | func TestRawTable(t *testing.T) { 64 | for _, filename := range tu.Filenames(t, "common") { 65 | f, err := td.Files.ReadFile(filename) 66 | tu.AssertNoErr(t, err) 67 | 68 | font, err := NewLoader(bytes.NewReader(f)) 69 | tu.AssertC(t, err == nil, filename) 70 | 71 | _, err = font.RawTable(MustNewTag("xxxx")) 72 | tu.Assert(t, err != nil) 73 | 74 | _, err = font.RawTable(MustNewTag("head")) 75 | tu.AssertC(t, err == nil, filename) 76 | 77 | _, err = font.RawTable(MustNewTag("OS/2")) 78 | tu.AssertC(t, err == nil, filename) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /font/opentype/reader_woff.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package opentype 4 | 5 | import ( 6 | "encoding/binary" 7 | "errors" 8 | "io" 9 | ) 10 | 11 | type woffEntry struct { 12 | Tag Tag 13 | Offset uint32 14 | CompLength uint32 15 | OrigLength uint32 16 | OrigChecksum uint32 17 | } 18 | 19 | const ( 20 | woffHeaderSize = 44 // for the full header, but we only read Flavor and NumTables 21 | woffEntrySize = 20 22 | ) 23 | 24 | func readWOFFHeader(r io.Reader) (flavor Tag, numTables uint16, err error) { 25 | var buf [woffHeaderSize]byte 26 | if _, err := io.ReadFull(r, buf[:]); err != nil { 27 | return 0, 0, err 28 | } 29 | 30 | return NewTag(buf[4], buf[5], buf[6], buf[7]), binary.BigEndian.Uint16(buf[12:14]), nil 31 | } 32 | 33 | func readWOFFEntry(r io.Reader) (woffEntry, error) { 34 | var ( 35 | buf [woffEntrySize]byte 36 | entry woffEntry 37 | ) 38 | if _, err := io.ReadFull(r, buf[:]); err != nil { 39 | return entry, err 40 | } 41 | entry.Tag = NewTag(buf[0], buf[1], buf[2], buf[3]) 42 | entry.Offset = binary.BigEndian.Uint32(buf[4:8]) 43 | entry.CompLength = binary.BigEndian.Uint32(buf[8:12]) 44 | entry.OrigLength = binary.BigEndian.Uint32(buf[12:16]) 45 | entry.OrigChecksum = binary.BigEndian.Uint32(buf[16:20]) 46 | return entry, nil 47 | } 48 | 49 | // `offset` is the beginning of the ressource in the file (non zero for collections) 50 | // `relativeOffset` is true when the table offset are expresed relatively ot the ressource 51 | // (that is, `offset`) rather than to the file 52 | func parseWOFF(file Resource, offset uint32, relativeOffset bool) (*Loader, error) { 53 | _, err := file.Seek(int64(offset), io.SeekStart) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | flavor, numTables, err := readWOFFHeader(file) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | fontParser := &Loader{ 64 | file: file, 65 | tables: make(map[Tag]tableSection, numTables), 66 | Type: flavor, 67 | } 68 | for i := 0; i < int(numTables); i++ { 69 | entry, err := readWOFFEntry(file) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | if _, found := fontParser.tables[entry.Tag]; found { 75 | // ignore duplicate tables – the first one wins 76 | continue 77 | } 78 | 79 | sec := tableSection{ 80 | offset: entry.Offset, 81 | length: entry.CompLength, 82 | zLength: entry.OrigLength, 83 | } 84 | // adapt the relative offsets 85 | if relativeOffset { 86 | sec.offset += offset 87 | if sec.offset < offset { // check for overflow 88 | return nil, errors.New("unsupported table offset or length") 89 | } 90 | } 91 | 92 | fontParser.tables[entry.Tag] = sec 93 | } 94 | 95 | return fontParser, nil 96 | } 97 | -------------------------------------------------------------------------------- /font/opentype/tables/aat_ankr_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import "encoding/binary" 6 | 7 | // Ankr is the anchor point table 8 | // See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ankr.html 9 | type Ankr struct { 10 | version uint16 // Version number (set to zero) 11 | flags uint16 // Flags (currently unused; set to zero) 12 | // Offset to the table's lookup table; currently this is always 0x0000000C 13 | // The lookup table returns uint16 offset from the beginning of the glyph data table, not indices. 14 | lookupTable AATLookup `offsetSize:"Offset32"` 15 | // Offset to the glyph data table 16 | glyphDataTable []byte `offsetSize:"Offset32" arrayCount:"ToEnd"` 17 | } 18 | 19 | // GetAnchor return the i-th anchor for `glyph`, or {0,0} if not found. 20 | func (ank Ankr) GetAnchor(glyph GlyphID, index int) (anchor AnkrAnchor) { 21 | offset, ok := ank.lookupTable.Class(glyph) 22 | if !ok || int(offset)+4 >= len(ank.glyphDataTable) { 23 | return anchor 24 | } 25 | 26 | count := int(binary.BigEndian.Uint32(ank.glyphDataTable[offset:])) 27 | if index >= count { 28 | return anchor // invalid index 29 | } 30 | 31 | indexStart := int(offset) + 4 + 4*index 32 | if len(ank.glyphDataTable) < indexStart+4 { 33 | return anchor // invalid table 34 | } 35 | anchor.X = int16(binary.BigEndian.Uint16(ank.glyphDataTable[indexStart:])) 36 | anchor.Y = int16(binary.BigEndian.Uint16(ank.glyphDataTable[indexStart+2:])) 37 | return anchor 38 | } 39 | 40 | // AnkrAnchor is a point within the coordinate space of a given glyph 41 | // independent of the control points used to render the glyph 42 | type AnkrAnchor struct { 43 | X, Y int16 44 | } 45 | -------------------------------------------------------------------------------- /font/opentype/tables/aat_feat_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from aat_feat_src.go. DO NOT EDIT 11 | 12 | func (item *FeatureSettingName) mustParse(src []byte) { 13 | _ = src[3] // early bound checking 14 | item.Setting = binary.BigEndian.Uint16(src[0:]) 15 | item.NameIndex = binary.BigEndian.Uint16(src[2:]) 16 | } 17 | 18 | func ParseFeat(src []byte) (Feat, int, error) { 19 | var item Feat 20 | n := 0 21 | if L := len(src); L < 12 { 22 | return item, 0, fmt.Errorf("reading Feat: "+"EOF: expected length: 12, got %d", L) 23 | } 24 | _ = src[11] // early bound checking 25 | item.version = binary.BigEndian.Uint32(src[0:]) 26 | item.featureNameCount = binary.BigEndian.Uint16(src[4:]) 27 | item.none1 = binary.BigEndian.Uint16(src[6:]) 28 | item.none2 = binary.BigEndian.Uint32(src[8:]) 29 | n += 12 30 | 31 | { 32 | arrayLength := int(item.featureNameCount) 33 | 34 | offset := 12 35 | for i := 0; i < arrayLength; i++ { 36 | elem, read, err := ParseFeatureName(src[offset:], src) 37 | if err != nil { 38 | return item, 0, fmt.Errorf("reading Feat: %s", err) 39 | } 40 | item.Names = append(item.Names, elem) 41 | offset += read 42 | } 43 | n = offset 44 | } 45 | return item, n, nil 46 | } 47 | 48 | func ParseFeatureName(src []byte, parentSrc []byte) (FeatureName, int, error) { 49 | var item FeatureName 50 | n := 0 51 | if L := len(src); L < 12 { 52 | return item, 0, fmt.Errorf("reading FeatureName: "+"EOF: expected length: 12, got %d", L) 53 | } 54 | _ = src[11] // early bound checking 55 | item.Feature = binary.BigEndian.Uint16(src[0:]) 56 | item.nSettings = binary.BigEndian.Uint16(src[2:]) 57 | offsetSettingTable := int(binary.BigEndian.Uint32(src[4:])) 58 | item.FeatureFlags = binary.BigEndian.Uint16(src[8:]) 59 | item.NameIndex = binary.BigEndian.Uint16(src[10:]) 60 | n += 12 61 | 62 | { 63 | 64 | if offsetSettingTable != 0 { // ignore null offset 65 | if L := len(parentSrc); L < offsetSettingTable { 66 | return item, 0, fmt.Errorf("reading FeatureName: "+"EOF: expected length: %d, got %d", offsetSettingTable, L) 67 | } 68 | 69 | arrayLength := int(item.nSettings) 70 | 71 | if L := len(parentSrc); L < offsetSettingTable+arrayLength*4 { 72 | return item, 0, fmt.Errorf("reading FeatureName: "+"EOF: expected length: %d, got %d", offsetSettingTable+arrayLength*4, L) 73 | } 74 | 75 | item.SettingTable = make([]FeatureSettingName, arrayLength) // allocation guarded by the previous check 76 | for i := range item.SettingTable { 77 | item.SettingTable[i].mustParse(parentSrc[offsetSettingTable+i*4:]) 78 | } 79 | offsetSettingTable += arrayLength * 4 80 | } 81 | } 82 | return item, n, nil 83 | } 84 | -------------------------------------------------------------------------------- /font/opentype/tables/aat_feat_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | // Feat is the feature name table. 6 | // See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6feat.html 7 | type Feat struct { 8 | version uint32 // Version number of the feature name table (0x00010000 for the current version). 9 | featureNameCount uint16 // The number of entries in the feature name array. 10 | none1 uint16 // Reserved (set to zero). 11 | none2 uint32 // Reserved (set to zero). 12 | Names []FeatureName `arrayCount:"ComputedField-featureNameCount"` // The feature name array. 13 | } 14 | 15 | type FeatureName struct { 16 | Feature uint16 // Feature type. 17 | nSettings uint16 // The number of records in the setting name array. 18 | SettingTable []FeatureSettingName `offsetSize:"Offset32" offsetRelativeTo:"Parent" arrayCount:"ComputedField-nSettings"` // Offset in bytes from the beginning of the 'feat' table to this feature's setting name array. The actual type of record this offset refers to will depend on the exclusivity value, as described below. 19 | FeatureFlags uint16 // Single-bit flags associated with the feature type. 20 | NameIndex uint16 // The name table index for the feature's name. This index has values greater than 255 and less than 32768. 21 | } 22 | 23 | type FeatureSettingName struct { 24 | Setting uint16 // The setting. 25 | NameIndex uint16 // The name table index for the setting's name. The nameIndex must be greater than 255 and less than 32768. 26 | } 27 | -------------------------------------------------------------------------------- /font/opentype/tables/aat_ltag_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from aat_ltag_src.go. DO NOT EDIT 11 | 12 | func ParseLtag(src []byte) (Ltag, int, error) { 13 | var item Ltag 14 | n := 0 15 | if L := len(src); L < 12 { 16 | return item, 0, fmt.Errorf("reading Ltag: "+"EOF: expected length: 12, got %d", L) 17 | } 18 | _ = src[11] // early bound checking 19 | item.version = binary.BigEndian.Uint32(src[0:]) 20 | item.flags = binary.BigEndian.Uint32(src[4:]) 21 | item.numTags = binary.BigEndian.Uint32(src[8:]) 22 | n += 12 23 | 24 | { 25 | arrayLength := int(item.numTags) 26 | 27 | if L := len(src); L < 12+arrayLength*4 { 28 | return item, 0, fmt.Errorf("reading Ltag: "+"EOF: expected length: %d, got %d", 12+arrayLength*4, L) 29 | } 30 | 31 | item.tagRange = make([]stringRange, arrayLength) // allocation guarded by the previous check 32 | for i := range item.tagRange { 33 | item.tagRange[i].mustParse(src[12+i*4:]) 34 | } 35 | n += arrayLength * 4 36 | } 37 | { 38 | 39 | item.stringData = src[0:] 40 | n = len(src) 41 | } 42 | return item, n, nil 43 | } 44 | 45 | func (item *stringRange) mustParse(src []byte) { 46 | _ = src[3] // early bound checking 47 | item.offset = binary.BigEndian.Uint16(src[0:]) 48 | item.length = binary.BigEndian.Uint16(src[2:]) 49 | } 50 | -------------------------------------------------------------------------------- /font/opentype/tables/aat_ltag_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import "github.com/go-text/typesetting/language" 6 | 7 | // Ltag is the language tags table 8 | // See https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ltag.html 9 | type Ltag struct { 10 | version uint32 // Table version; currently 1 11 | flags uint32 // Table flags; currently none defined 12 | numTags uint32 // Number of language tags which follow 13 | tagRange []stringRange `arrayCount:"ComputedField-numTags"` // Range for each tag's string 14 | stringData []byte `subsliceStart:"AtStart" arrayCount:"ToEnd"` 15 | } 16 | 17 | type stringRange struct { 18 | offset uint16 // Offset from the start of the table to the beginning of the string 19 | length uint16 // String length (in bytes) 20 | } 21 | 22 | func (lt Ltag) Language(i uint16) language.Language { 23 | r := lt.tagRange[i] 24 | return language.NewLanguage(string(lt.stringData[r.offset : r.offset+r.length])) 25 | } 26 | -------------------------------------------------------------------------------- /font/opentype/tables/aat_trak_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | // Trak is the tracking table. 6 | // See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html 7 | type Trak struct { 8 | version uint32 // Version number of the tracking table (0x00010000 for the current version). 9 | format uint16 // Format of the tracking table (set to 0). 10 | Horiz TrackData `offsetSize:"Offset16"` // Offset from start of tracking table to TrackData for horizontal text (or 0 if none). 11 | Vert TrackData `offsetSize:"Offset16"` // Offset from start of tracking table to TrackData for vertical text (or 0 if none). 12 | reserved uint16 // Reserved. Set to 0. 13 | } 14 | 15 | // IsEmpty return `true` it the table has no entries. 16 | func (t Trak) IsEmpty() bool { 17 | return len(t.Horiz.TrackTable)+len(t.Vert.TrackTable) == 0 18 | } 19 | 20 | type TrackData struct { 21 | nTracks uint16 // Number of separate tracks included in this table. 22 | nSizes uint16 // Number of point sizes included in this table. 23 | SizeTable []Float1616 `offsetSize:"Offset32" offsetRelativeTo:"Parent" arrayCount:"ComputedField-nSizes"` // Offset from start of the tracking table to the start of the size subtable. 24 | TrackTable []TrackTableEntry `arrayCount:"ComputedField-nTracks" arguments:"perSizeTrackingCount=.nSizes"` // Array[nTracks] of TrackTableEntry records. 25 | } 26 | 27 | type TrackTableEntry struct { 28 | Track Float1616 // Track value for this record. 29 | NameIndex uint16 // The 'name' table index for this track (a short word or phrase like "loose" or "very tight"). NameIndex has a value greater than 255 and less than 32768. 30 | PerSizeTracking []int16 `offsetSize:"Offset16" offsetRelativeTo:"GrandParent"` // in font units, with length len(SizeTable) 31 | } 32 | -------------------------------------------------------------------------------- /font/opentype/tables/glyphs.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | type BitmapSubtable struct { 6 | FirstGlyph GlyphID // First glyph ID of this range. 7 | LastGlyph GlyphID // Last glyph ID of this range (inclusive). 8 | IndexSubHeader 9 | } 10 | 11 | // EBLC is the Embedded Bitmap Location Table 12 | // See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/eblc 13 | type EBLC = CBLC 14 | 15 | // Bloc is the bitmap location table 16 | // See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bloc.html 17 | type Bloc = CBLC 18 | -------------------------------------------------------------------------------- /font/opentype/tables/glyphs_glyf_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from glyphs_glyf_src.go. DO NOT EDIT 11 | 12 | func (item *CompositeGlyphPart) mustParse(src []byte) { 13 | _ = src[23] // early bound checking 14 | item.Flags = binary.BigEndian.Uint16(src[0:]) 15 | item.GlyphIndex = binary.BigEndian.Uint16(src[2:]) 16 | item.arg1 = binary.BigEndian.Uint16(src[4:]) 17 | item.arg2 = binary.BigEndian.Uint16(src[6:]) 18 | item.Scale[0] = float32(binary.BigEndian.Uint32(src[8:])) 19 | item.Scale[1] = float32(binary.BigEndian.Uint32(src[12:])) 20 | item.Scale[2] = float32(binary.BigEndian.Uint32(src[16:])) 21 | item.Scale[3] = float32(binary.BigEndian.Uint32(src[20:])) 22 | } 23 | 24 | func (item *GlyphContourPoint) mustParse(src []byte) { 25 | _ = src[4] // early bound checking 26 | item.Flag = src[0] 27 | item.X = int16(binary.BigEndian.Uint16(src[1:])) 28 | item.Y = int16(binary.BigEndian.Uint16(src[3:])) 29 | } 30 | 31 | func ParseCompositeGlyph(src []byte) (CompositeGlyph, int, error) { 32 | var item CompositeGlyph 33 | n := 0 34 | { 35 | 36 | err := item.parseGlyphs(src[:]) 37 | if err != nil { 38 | return item, 0, fmt.Errorf("reading CompositeGlyph: %s", err) 39 | } 40 | } 41 | { 42 | 43 | err := item.parseInstructions(src[:]) 44 | if err != nil { 45 | return item, 0, fmt.Errorf("reading CompositeGlyph: %s", err) 46 | } 47 | } 48 | return item, n, nil 49 | } 50 | 51 | func ParseCompositeGlyphPart(src []byte) (CompositeGlyphPart, int, error) { 52 | var item CompositeGlyphPart 53 | n := 0 54 | if L := len(src); L < 24 { 55 | return item, 0, fmt.Errorf("reading CompositeGlyphPart: "+"EOF: expected length: 24, got %d", L) 56 | } 57 | item.mustParse(src) 58 | n += 24 59 | return item, n, nil 60 | } 61 | 62 | func ParseGlyph(src []byte) (Glyph, int, error) { 63 | var item Glyph 64 | n := 0 65 | if L := len(src); L < 10 { 66 | return item, 0, fmt.Errorf("reading Glyph: "+"EOF: expected length: 10, got %d", L) 67 | } 68 | _ = src[9] // early bound checking 69 | item.numberOfContours = int16(binary.BigEndian.Uint16(src[0:])) 70 | item.XMin = int16(binary.BigEndian.Uint16(src[2:])) 71 | item.YMin = int16(binary.BigEndian.Uint16(src[4:])) 72 | item.XMax = int16(binary.BigEndian.Uint16(src[6:])) 73 | item.YMax = int16(binary.BigEndian.Uint16(src[8:])) 74 | n += 10 75 | 76 | { 77 | 78 | err := item.parseData(src[10:]) 79 | if err != nil { 80 | return item, 0, fmt.Errorf("reading Glyph: %s", err) 81 | } 82 | } 83 | return item, n, nil 84 | } 85 | 86 | func ParseGlyphContourPoint(src []byte) (GlyphContourPoint, int, error) { 87 | var item GlyphContourPoint 88 | n := 0 89 | if L := len(src); L < 5 { 90 | return item, 0, fmt.Errorf("reading GlyphContourPoint: "+"EOF: expected length: 5, got %d", L) 91 | } 92 | item.mustParse(src) 93 | n += 5 94 | return item, n, nil 95 | } 96 | 97 | func ParseSimpleGlyph(src []byte, endPtsOfContoursCount int) (SimpleGlyph, int, error) { 98 | var item SimpleGlyph 99 | n := 0 100 | { 101 | 102 | if L := len(src); L < endPtsOfContoursCount*2 { 103 | return item, 0, fmt.Errorf("reading SimpleGlyph: "+"EOF: expected length: %d, got %d", endPtsOfContoursCount*2, L) 104 | } 105 | 106 | item.EndPtsOfContours = make([]uint16, endPtsOfContoursCount) // allocation guarded by the previous check 107 | for i := range item.EndPtsOfContours { 108 | item.EndPtsOfContours[i] = binary.BigEndian.Uint16(src[i*2:]) 109 | } 110 | n += endPtsOfContoursCount * 2 111 | } 112 | if L := len(src); L < n+2 { 113 | return item, 0, fmt.Errorf("reading SimpleGlyph: "+"EOF: expected length: n + 2, got %d", L) 114 | } 115 | arrayLengthInstructions := int(binary.BigEndian.Uint16(src[n:])) 116 | n += 2 117 | 118 | { 119 | 120 | L := int(n + arrayLengthInstructions) 121 | if len(src) < L { 122 | return item, 0, fmt.Errorf("reading SimpleGlyph: "+"EOF: expected length: %d, got %d", L, len(src)) 123 | } 124 | item.Instructions = src[n:L] 125 | n = L 126 | } 127 | { 128 | 129 | err := item.parsePoints(src[n:], endPtsOfContoursCount) 130 | if err != nil { 131 | return item, 0, fmt.Errorf("reading SimpleGlyph: %s", err) 132 | } 133 | } 134 | return item, n, nil 135 | } 136 | -------------------------------------------------------------------------------- /font/opentype/tables/glyphs_misc_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from glyphs_misc_src.go. DO NOT EDIT 11 | 12 | func ParseSVG(src []byte) (SVG, int, error) { 13 | var item SVG 14 | n := 0 15 | if L := len(src); L < 10 { 16 | return item, 0, fmt.Errorf("reading SVG: "+"EOF: expected length: 10, got %d", L) 17 | } 18 | _ = src[9] // early bound checking 19 | item.version = binary.BigEndian.Uint16(src[0:]) 20 | offsetSVGDocumentList := int(binary.BigEndian.Uint32(src[2:])) 21 | item.reserved = binary.BigEndian.Uint32(src[6:]) 22 | n += 10 23 | 24 | { 25 | 26 | if offsetSVGDocumentList != 0 { // ignore null offset 27 | if L := len(src); L < offsetSVGDocumentList { 28 | return item, 0, fmt.Errorf("reading SVG: "+"EOF: expected length: %d, got %d", offsetSVGDocumentList, L) 29 | } 30 | 31 | var err error 32 | item.SVGDocumentList, _, err = ParseSVGDocumentList(src[offsetSVGDocumentList:]) 33 | if err != nil { 34 | return item, 0, fmt.Errorf("reading SVG: %s", err) 35 | } 36 | 37 | } 38 | } 39 | return item, n, nil 40 | } 41 | 42 | func ParseSVGDocumentList(src []byte) (SVGDocumentList, int, error) { 43 | var item SVGDocumentList 44 | n := 0 45 | if L := len(src); L < 2 { 46 | return item, 0, fmt.Errorf("reading SVGDocumentList: "+"EOF: expected length: 2, got %d", L) 47 | } 48 | arrayLengthDocumentRecords := int(binary.BigEndian.Uint16(src[0:])) 49 | n += 2 50 | 51 | { 52 | 53 | if L := len(src); L < 2+arrayLengthDocumentRecords*12 { 54 | return item, 0, fmt.Errorf("reading SVGDocumentList: "+"EOF: expected length: %d, got %d", 2+arrayLengthDocumentRecords*12, L) 55 | } 56 | 57 | item.DocumentRecords = make([]SVGDocumentRecord, arrayLengthDocumentRecords) // allocation guarded by the previous check 58 | for i := range item.DocumentRecords { 59 | item.DocumentRecords[i].mustParse(src[2+i*12:]) 60 | } 61 | n += arrayLengthDocumentRecords * 12 62 | } 63 | { 64 | 65 | item.SVGRawData = src[0:] 66 | n = len(src) 67 | } 68 | return item, n, nil 69 | } 70 | 71 | func ParseVORG(src []byte) (VORG, int, error) { 72 | var item VORG 73 | n := 0 74 | if L := len(src); L < 8 { 75 | return item, 0, fmt.Errorf("reading VORG: "+"EOF: expected length: 8, got %d", L) 76 | } 77 | _ = src[7] // early bound checking 78 | item.majorVersion = binary.BigEndian.Uint16(src[0:]) 79 | item.minorVersion = binary.BigEndian.Uint16(src[2:]) 80 | item.DefaultVertOriginY = int16(binary.BigEndian.Uint16(src[4:])) 81 | arrayLengthVertOriginYMetrics := int(binary.BigEndian.Uint16(src[6:])) 82 | n += 8 83 | 84 | { 85 | 86 | if L := len(src); L < 8+arrayLengthVertOriginYMetrics*4 { 87 | return item, 0, fmt.Errorf("reading VORG: "+"EOF: expected length: %d, got %d", 8+arrayLengthVertOriginYMetrics*4, L) 88 | } 89 | 90 | item.VertOriginYMetrics = make([]VertOriginYMetric, arrayLengthVertOriginYMetrics) // allocation guarded by the previous check 91 | for i := range item.VertOriginYMetrics { 92 | item.VertOriginYMetrics[i].mustParse(src[8+i*4:]) 93 | } 94 | n += arrayLengthVertOriginYMetrics * 4 95 | } 96 | return item, n, nil 97 | } 98 | 99 | func (item *SVGDocumentRecord) mustParse(src []byte) { 100 | _ = src[11] // early bound checking 101 | item.StartGlyphID = binary.BigEndian.Uint16(src[0:]) 102 | item.EndGlyphID = binary.BigEndian.Uint16(src[2:]) 103 | item.SvgDocOffset = Offset32(binary.BigEndian.Uint32(src[4:])) 104 | item.SvgDocLength = binary.BigEndian.Uint32(src[8:]) 105 | } 106 | 107 | func (item *VertOriginYMetric) mustParse(src []byte) { 108 | _ = src[3] // early bound checking 109 | item.GlyphIndex = binary.BigEndian.Uint16(src[0:]) 110 | item.VertOriginY = int16(binary.BigEndian.Uint16(src[2:])) 111 | } 112 | -------------------------------------------------------------------------------- /font/opentype/tables/glyphs_misc_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | // SVG is the SVG (Scalable Vector Graphics) table. 6 | // See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/svg 7 | type SVG struct { 8 | version uint16 // Table version (starting at 0). Set to 0. 9 | SVGDocumentList SVGDocumentList `offsetSize:"Offset32"` // Offset to the SVG Document List, from the start of the SVG table. Must be non-zero. 10 | reserved uint32 // Set to 0. 11 | } 12 | 13 | type SVGDocumentList struct { 14 | DocumentRecords []SVGDocumentRecord `arrayCount:"FirstUint16"` // [numEntries] Array of SVG document records. 15 | SVGRawData []byte `subsliceStart:"AtStart" arrayCount:"ToEnd"` 16 | } 17 | 18 | // Each SVG document record specifies a range of glyph IDs (from startGlyphID to endGlyphID, inclusive), and the location of its associated SVG document in the SVG table. 19 | type SVGDocumentRecord struct { 20 | StartGlyphID GlyphID // The first glyph ID for the range covered by this record. 21 | EndGlyphID GlyphID // The last glyph ID for the range covered by this record. 22 | SvgDocOffset Offset32 // Offset from the beginning of the SVGDocumentList to an SVG document. Must be non-zero. 23 | SvgDocLength uint32 // Length of the SVG document data. Must be non-zero. 24 | } 25 | 26 | // CFF is the Compact Font Format Table. 27 | // Since it used its own format, quite different from the regular Opentype format, 28 | // its interpretation is handled externally (see font/cff). 29 | // See also https://learn.microsoft.com/fr-fr/typography/opentype/spec/cff 30 | type CFF = []byte 31 | 32 | // VORG is the Vertical Origin Table 33 | // See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/vorg 34 | type VORG struct { 35 | majorVersion uint16 // Major version (starting at 1). Set to 1. 36 | minorVersion uint16 // Minor version (starting at 0). Set to 0. 37 | DefaultVertOriginY int16 // The y coordinate of a glyph’s vertical origin, in the font’s design coordinate system, to be used if no entry is present for the glyph in the vertOriginYMetrics array. 38 | VertOriginYMetrics []VertOriginYMetric `arrayCount:"FirstUint16"` 39 | } 40 | 41 | // YOrigin returns the vertical origin for [glyph]. 42 | func (t *VORG) YOrigin(glyph GlyphID) int16 { 43 | // binary search 44 | for i, j := 0, len(t.VertOriginYMetrics); i < j; { 45 | h := i + (j-i)/2 46 | entry := t.VertOriginYMetrics[h] 47 | if glyph < entry.GlyphIndex { 48 | j = h 49 | } else if entry.GlyphIndex < glyph { 50 | i = h + 1 51 | } else { 52 | return entry.VertOriginY 53 | } 54 | } 55 | return t.DefaultVertOriginY 56 | } 57 | 58 | type VertOriginYMetric struct { 59 | GlyphIndex GlyphID // Glyph index. 60 | VertOriginY int16 // Y coordinate, in the font’s design coordinate system, of the vertical origin of glyph with index glyphIndex. 61 | } 62 | -------------------------------------------------------------------------------- /font/opentype/tables/glyphs_misc_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "testing" 7 | 8 | tu "github.com/go-text/typesetting/testutils" 9 | ) 10 | 11 | func TestParseSVG(t *testing.T) { 12 | fp := readFontFile(t, "toys/chromacheck-svg.ttf") 13 | _, _, err := ParseSVG(readTable(t, fp, "SVG ")) 14 | tu.AssertNoErr(t, err) 15 | } 16 | 17 | func TestParseCFF(t *testing.T) { 18 | fp := readFontFile(t, "toys/CFFTest.otf") 19 | readTable(t, fp, "CFF ") 20 | } 21 | -------------------------------------------------------------------------------- /font/opentype/tables/glyphs_sbix_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from glyphs_sbix_src.go. DO NOT EDIT 11 | 12 | func ParseBitmapGlyphData(src []byte) (BitmapGlyphData, int, error) { 13 | var item BitmapGlyphData 14 | n := 0 15 | if L := len(src); L < 8 { 16 | return item, 0, fmt.Errorf("reading BitmapGlyphData: "+"EOF: expected length: 8, got %d", L) 17 | } 18 | _ = src[7] // early bound checking 19 | item.OriginOffsetX = int16(binary.BigEndian.Uint16(src[0:])) 20 | item.OriginOffsetY = int16(binary.BigEndian.Uint16(src[2:])) 21 | item.GraphicType = Tag(binary.BigEndian.Uint32(src[4:])) 22 | n += 8 23 | 24 | { 25 | 26 | item.Data = src[8:] 27 | n = len(src) 28 | } 29 | return item, n, nil 30 | } 31 | 32 | func ParseSbix(src []byte, numGlyphs int) (Sbix, int, error) { 33 | var item Sbix 34 | n := 0 35 | if L := len(src); L < 8 { 36 | return item, 0, fmt.Errorf("reading Sbix: "+"EOF: expected length: 8, got %d", L) 37 | } 38 | _ = src[7] // early bound checking 39 | item.version = binary.BigEndian.Uint16(src[0:]) 40 | item.Flags = binary.BigEndian.Uint16(src[2:]) 41 | arrayLengthStrikes := int(binary.BigEndian.Uint32(src[4:])) 42 | n += 8 43 | 44 | { 45 | 46 | if L := len(src); L < 8+arrayLengthStrikes*4 { 47 | return item, 0, fmt.Errorf("reading Sbix: "+"EOF: expected length: %d, got %d", 8+arrayLengthStrikes*4, L) 48 | } 49 | 50 | item.Strikes = make([]Strike, arrayLengthStrikes) // allocation guarded by the previous check 51 | for i := range item.Strikes { 52 | offset := int(binary.BigEndian.Uint32(src[8+i*4:])) 53 | // ignore null offsets 54 | if offset == 0 { 55 | continue 56 | } 57 | 58 | if L := len(src); L < offset { 59 | return item, 0, fmt.Errorf("reading Sbix: "+"EOF: expected length: %d, got %d", offset, L) 60 | } 61 | 62 | var err error 63 | item.Strikes[i], _, err = ParseStrike(src[offset:], numGlyphs) 64 | if err != nil { 65 | return item, 0, fmt.Errorf("reading Sbix: %s", err) 66 | } 67 | } 68 | n += arrayLengthStrikes * 4 69 | } 70 | return item, n, nil 71 | } 72 | 73 | func ParseStrike(src []byte, numGlyphs int) (Strike, int, error) { 74 | var item Strike 75 | n := 0 76 | if L := len(src); L < 4 { 77 | return item, 0, fmt.Errorf("reading Strike: "+"EOF: expected length: 4, got %d", L) 78 | } 79 | _ = src[3] // early bound checking 80 | item.Ppem = binary.BigEndian.Uint16(src[0:]) 81 | item.Ppi = binary.BigEndian.Uint16(src[2:]) 82 | n += 4 83 | 84 | { 85 | 86 | err := item.parseGlyphDatas(src[:], numGlyphs) 87 | if err != nil { 88 | return item, 0, fmt.Errorf("reading Strike: %s", err) 89 | } 90 | } 91 | return item, n, nil 92 | } 93 | -------------------------------------------------------------------------------- /font/opentype/tables/glyphs_sbix_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | // Sbix is the Standard Bitmap Graphics Table 10 | // See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/sbix 11 | type Sbix struct { 12 | version uint16 // Table version number — set to 1 13 | // Bit 0: Set to 1. 14 | // Bit 1: Draw outlines. 15 | // Bits 2 to 15: reserved (set to 0). 16 | Flags uint16 17 | Strikes []Strike `arrayCount:"FirstUint32" offsetsArray:"Offset32"` // [numStrikes] Offsets from the beginning of the 'sbix' table to data for each individual bitmap strike. 18 | } 19 | 20 | // Strike stores one size of bitmap glyphs in the 'sbix' table. 21 | // binarygen: argument=numGlyphs int 22 | type Strike struct { 23 | Ppem uint16 // The PPEM size for which this strike was designed. 24 | Ppi uint16 // The device pixel density (in PPI) for which this strike was designed. (E.g., 96 PPI, 192 PPI.) 25 | GlyphDatas []BitmapGlyphData `isOpaque:""` //[numGlyphs+1] Offset from the beginning of the strike data header to bitmap data for an individual glyph ID. 26 | } 27 | 28 | func (st *Strike) parseGlyphDatas(src []byte, numGlyphs int) error { 29 | const headerSize = 4 30 | offsets, err := ParseLoca(src[headerSize:], numGlyphs, true) 31 | if err != nil { 32 | return err 33 | } 34 | st.GlyphDatas = make([]BitmapGlyphData, numGlyphs) 35 | for i := range st.GlyphDatas { 36 | start, end := offsets[i], offsets[i+1] 37 | if start == end { // no data 38 | continue 39 | } 40 | 41 | if start > end { 42 | return fmt.Errorf("invalid strike offsets %d > %d", start, end) 43 | } 44 | 45 | if L := len(src); L < int(end) { 46 | return fmt.Errorf("EOF: expected length: %d, got %d", end, L) 47 | } 48 | 49 | st.GlyphDatas[i], _, err = ParseBitmapGlyphData(src[start:end]) 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | type BitmapGlyphData struct { 58 | OriginOffsetX int16 // The horizontal (x-axis) position of the left edge of the bitmap graphic in relation to the glyph design space origin. 59 | OriginOffsetY int16 // The vertical (y-axis) position of the bottom edge of the bitmap graphic in relation to the glyph design space origin. 60 | GraphicType Tag // Indicates the format of the embedded graphic data: one of 'jpg ', 'png ' or 'tiff', or the special format 'dupe'. 61 | Data []byte `arrayCount:"ToEnd"` // The actual embedded graphic data. The total length is inferred from sequential entries in the glyphDataOffsets array and the fixed size (8 bytes) of the preceding fields. 62 | } 63 | -------------------------------------------------------------------------------- /font/opentype/tables/head_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from head_src.go. DO NOT EDIT 11 | 12 | func (item *Head) mustParse(src []byte) { 13 | _ = src[53] // early bound checking 14 | item.majorVersion = binary.BigEndian.Uint16(src[0:]) 15 | item.minorVersion = binary.BigEndian.Uint16(src[2:]) 16 | item.fontRevision = binary.BigEndian.Uint32(src[4:]) 17 | item.checksumAdjustment = binary.BigEndian.Uint32(src[8:]) 18 | item.magicNumber = binary.BigEndian.Uint32(src[12:]) 19 | item.flags = binary.BigEndian.Uint16(src[16:]) 20 | item.UnitsPerEm = binary.BigEndian.Uint16(src[18:]) 21 | item.created = binary.BigEndian.Uint64(src[20:]) 22 | item.modified = binary.BigEndian.Uint64(src[28:]) 23 | item.XMin = int16(binary.BigEndian.Uint16(src[36:])) 24 | item.YMin = int16(binary.BigEndian.Uint16(src[38:])) 25 | item.XMax = int16(binary.BigEndian.Uint16(src[40:])) 26 | item.YMax = int16(binary.BigEndian.Uint16(src[42:])) 27 | item.MacStyle = binary.BigEndian.Uint16(src[44:]) 28 | item.lowestRecPPEM = binary.BigEndian.Uint16(src[46:]) 29 | item.fontDirectionHint = int16(binary.BigEndian.Uint16(src[48:])) 30 | item.IndexToLocFormat = int16(binary.BigEndian.Uint16(src[50:])) 31 | item.glyphDataFormat = int16(binary.BigEndian.Uint16(src[52:])) 32 | } 33 | 34 | func ParseHead(src []byte) (Head, int, error) { 35 | var item Head 36 | n := 0 37 | if L := len(src); L < 54 { 38 | return item, 0, fmt.Errorf("reading Head: "+"EOF: expected length: 54, got %d", L) 39 | } 40 | item.mustParse(src) 41 | n += 54 42 | return item, n, nil 43 | } 44 | -------------------------------------------------------------------------------- /font/opentype/tables/head_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | // TableHead contains critical information about the rest of the font. 6 | // https://learn.microsoft.com/en-us/typography/opentype/spec/head 7 | // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html 8 | // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bhed.html 9 | type Head struct { 10 | majorVersion uint16 11 | minorVersion uint16 12 | fontRevision uint32 13 | checksumAdjustment uint32 14 | magicNumber uint32 15 | flags uint16 16 | UnitsPerEm uint16 17 | created longdatetime 18 | modified longdatetime 19 | XMin int16 20 | YMin int16 21 | XMax int16 22 | YMax int16 23 | MacStyle uint16 24 | lowestRecPPEM uint16 25 | fontDirectionHint int16 26 | IndexToLocFormat int16 27 | glyphDataFormat int16 28 | } 29 | 30 | // Upem returns a sanitize version of the 'UnitsPerEm' field. 31 | func (head *Head) Upem() uint16 { 32 | if head.UnitsPerEm < 16 || head.UnitsPerEm > 16384 { 33 | return 1000 34 | } 35 | return head.UnitsPerEm 36 | } 37 | -------------------------------------------------------------------------------- /font/opentype/tables/hhea_vhea_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from hhea_vhea_src.go. DO NOT EDIT 11 | 12 | func (item *Hhea) mustParse(src []byte) { 13 | _ = src[35] // early bound checking 14 | item.majorVersion = binary.BigEndian.Uint16(src[0:]) 15 | item.minorVersion = binary.BigEndian.Uint16(src[2:]) 16 | item.Ascender = int16(binary.BigEndian.Uint16(src[4:])) 17 | item.Descender = int16(binary.BigEndian.Uint16(src[6:])) 18 | item.LineGap = int16(binary.BigEndian.Uint16(src[8:])) 19 | item.AdvanceMax = binary.BigEndian.Uint16(src[10:]) 20 | item.MinFirstSideBearing = int16(binary.BigEndian.Uint16(src[12:])) 21 | item.MinSecondSideBearing = int16(binary.BigEndian.Uint16(src[14:])) 22 | item.MaxExtent = int16(binary.BigEndian.Uint16(src[16:])) 23 | item.CaretSlopeRise = int16(binary.BigEndian.Uint16(src[18:])) 24 | item.CaretSlopeRun = int16(binary.BigEndian.Uint16(src[20:])) 25 | item.CaretOffset = int16(binary.BigEndian.Uint16(src[22:])) 26 | item.reserved[0] = binary.BigEndian.Uint16(src[24:]) 27 | item.reserved[1] = binary.BigEndian.Uint16(src[26:]) 28 | item.reserved[2] = binary.BigEndian.Uint16(src[28:]) 29 | item.reserved[3] = binary.BigEndian.Uint16(src[30:]) 30 | item.metricDataformat = int16(binary.BigEndian.Uint16(src[32:])) 31 | item.NumOfLongMetrics = binary.BigEndian.Uint16(src[34:]) 32 | } 33 | 34 | func ParseHhea(src []byte) (Hhea, int, error) { 35 | var item Hhea 36 | n := 0 37 | if L := len(src); L < 36 { 38 | return item, 0, fmt.Errorf("reading Hhea: "+"EOF: expected length: 36, got %d", L) 39 | } 40 | item.mustParse(src) 41 | n += 36 42 | return item, n, nil 43 | } 44 | -------------------------------------------------------------------------------- /font/opentype/tables/hhea_vhea_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | // https://learn.microsoft.com/en-us/typography/opentype/spec/hhea 6 | type Hhea struct { 7 | majorVersion uint16 8 | minorVersion uint16 9 | Ascender int16 10 | Descender int16 11 | LineGap int16 12 | AdvanceMax uint16 13 | MinFirstSideBearing int16 14 | MinSecondSideBearing int16 15 | MaxExtent int16 16 | CaretSlopeRise int16 17 | CaretSlopeRun int16 18 | CaretOffset int16 19 | reserved [4]uint16 20 | metricDataformat int16 21 | NumOfLongMetrics uint16 22 | } 23 | 24 | // https://learn.microsoft.com/en-us/typography/opentype/spec/vhea 25 | type Vhea = Hhea 26 | -------------------------------------------------------------------------------- /font/opentype/tables/hmtx_vmtx_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from hmtx_vmtx_src.go. DO NOT EDIT 11 | 12 | func (item *LongHorMetric) mustParse(src []byte) { 13 | _ = src[3] // early bound checking 14 | item.AdvanceWidth = int16(binary.BigEndian.Uint16(src[0:])) 15 | item.LeftSideBearing = int16(binary.BigEndian.Uint16(src[2:])) 16 | } 17 | 18 | func ParseHmtx(src []byte, metricsCount int, leftSideBearingsCount int) (Hmtx, int, error) { 19 | var item Hmtx 20 | n := 0 21 | { 22 | 23 | if L := len(src); L < metricsCount*4 { 24 | return item, 0, fmt.Errorf("reading Hmtx: "+"EOF: expected length: %d, got %d", metricsCount*4, L) 25 | } 26 | 27 | item.Metrics = make([]LongHorMetric, metricsCount) // allocation guarded by the previous check 28 | for i := range item.Metrics { 29 | item.Metrics[i].mustParse(src[i*4:]) 30 | } 31 | n += metricsCount * 4 32 | } 33 | { 34 | 35 | if L := len(src); L < n+leftSideBearingsCount*2 { 36 | return item, 0, fmt.Errorf("reading Hmtx: "+"EOF: expected length: %d, got %d", n+leftSideBearingsCount*2, L) 37 | } 38 | 39 | item.LeftSideBearings = make([]int16, leftSideBearingsCount) // allocation guarded by the previous check 40 | for i := range item.LeftSideBearings { 41 | item.LeftSideBearings[i] = int16(binary.BigEndian.Uint16(src[n+i*2:])) 42 | } 43 | n += leftSideBearingsCount * 2 44 | } 45 | return item, n, nil 46 | } 47 | -------------------------------------------------------------------------------- /font/opentype/tables/hmtx_vmtx_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | // https://learn.microsoft.com/en-us/typography/opentype/spec/hmtx 6 | type Hmtx struct { 7 | Metrics []LongHorMetric `arrayCount:""` 8 | // avances are padded with the last value 9 | // and side bearings are given 10 | LeftSideBearings []int16 `arrayCount:""` 11 | } 12 | 13 | func (table Hmtx) IsEmpty() bool { 14 | return len(table.Metrics)+len(table.LeftSideBearings) == 0 15 | } 16 | 17 | func (table Hmtx) Advance(gid GlyphID) int16 { 18 | LM, LS := len(table.Metrics), len(table.LeftSideBearings) 19 | index := int(gid) 20 | if index < LM { 21 | return table.Metrics[index].AdvanceWidth 22 | } else if index < LS+LM { // return the last value 23 | return table.Metrics[len(table.Metrics)-1].AdvanceWidth 24 | } 25 | return 0 26 | } 27 | 28 | type LongHorMetric struct { 29 | AdvanceWidth, LeftSideBearing int16 30 | } 31 | 32 | // https://learn.microsoft.com/en-us/typography/opentype/spec/vmtx 33 | type Vmtx = Hmtx 34 | -------------------------------------------------------------------------------- /font/opentype/tables/kern.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | // Kern is the kern table. It has multiple header format, defined in Apple AAT and Microsoft OT 12 | // specs, but the subtable data actually are the same. 13 | // 14 | // Microsoft (OT) format 15 | // 16 | // version uint16 : Table version number (0) 17 | // nTables uint16 : Number of subtables in the kerning table. 18 | // 19 | // Apple (AAT) old format 20 | // 21 | // version uint16 : The version number of the kerning table (0x0001 for the current version). 22 | // nTables uint16 : The number of subtables included in the kerning table. 23 | // 24 | // Apple (AAT) new format 25 | // 26 | // version uint32 : The version number of the kerning table (0x00010000 for the current version). 27 | // nTables uint32 : The number of subtables included in the kerning table. 28 | // 29 | // See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html 30 | // and - https://learn.microsoft.com/fr-fr/typography/opentype/spec/kern 31 | type Kern struct { 32 | version uint16 33 | Tables []KernSubtable 34 | } 35 | 36 | // We apply the following logic: 37 | // - read the first uint16 -> it's always the major version 38 | // - if it's 0, we have a Miscrosoft table 39 | // - if it's 1, we have an Apple table. We read the next uint16, 40 | // to differentiate between the old and the new Apple format. 41 | func ParseKern(src []byte) (Kern, int, error) { 42 | if L := len(src); L < 4 { 43 | return Kern{}, 0, fmt.Errorf("reading Kern: "+"EOF: expected length: 4, got %d", L) 44 | } 45 | 46 | var numTables uint32 47 | 48 | major := binary.BigEndian.Uint16(src) 49 | switch major { 50 | case 0: 51 | numTables = uint32(binary.BigEndian.Uint16(src[2:])) 52 | src = src[4:] 53 | case 1: 54 | nextUint16 := binary.BigEndian.Uint16(src[2:]) 55 | if nextUint16 == 0 { 56 | // either new format or old format with 0 subtables, the later being invalid (or at least useless) 57 | if len(src) < 8 { 58 | return Kern{}, 0, errors.New("invalid kern table version 1 (EOF)") 59 | } 60 | numTables = binary.BigEndian.Uint32(src[4:]) 61 | src = src[8:] 62 | } else { 63 | // old format 64 | numTables = uint32(nextUint16) 65 | src = src[4:] 66 | } 67 | 68 | default: 69 | return Kern{}, 0, fmt.Errorf("unsupported kern table version: %d", major) 70 | } 71 | 72 | out := make([]KernSubtable, numTables) 73 | var ( 74 | err error 75 | nbRead int 76 | isOT = major == 0 77 | ) 78 | for i := range out { 79 | if L := len(src); L < nbRead { 80 | return Kern{}, 0, fmt.Errorf("reading Kern: "+"EOF: expected length: %d, got %d", nbRead, L) 81 | } 82 | src = src[nbRead:] 83 | if isOT { 84 | out[i], nbRead, err = ParseOTKernSubtableHeader(src) 85 | } else { 86 | out[i], nbRead, err = ParseAATKernSubtableHeader(src) 87 | } 88 | if err != nil { 89 | return Kern{}, 0, err 90 | } 91 | } 92 | 93 | return Kern{ 94 | version: major, 95 | Tables: out, 96 | }, 0, nil 97 | } 98 | 99 | func (k AATKernSubtableHeader) Data() KernData { return k.data } 100 | 101 | func (k OTKernSubtableHeader) Data() KernData { return k.data } 102 | -------------------------------------------------------------------------------- /font/opentype/tables/kern_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "testing" 7 | 8 | td "github.com/go-text/typesetting-utils/opentype" 9 | tu "github.com/go-text/typesetting/testutils" 10 | ) 11 | 12 | func TestParseKern(t *testing.T) { 13 | filepath := "common/FreeSerif.ttf" 14 | fp := readFontFile(t, filepath) 15 | _, _, err := ParseKern(readTable(t, fp, "kern")) 16 | tu.AssertNoErr(t, err) 17 | 18 | filepath = "toys/Kern2.ttf" 19 | fp = readFontFile(t, filepath) 20 | _, _, err = ParseKern(readTable(t, fp, "kern")) 21 | tu.AssertNoErr(t, err) 22 | 23 | for _, filepath := range []string{ 24 | "toys/tables/kern0Exp.bin", 25 | "toys/tables/kern1.bin", 26 | "toys/tables/kern02.bin", 27 | "toys/tables/kern3.bin", 28 | } { 29 | table, err := td.Files.ReadFile(filepath) 30 | tu.AssertNoErr(t, err) 31 | 32 | kern, _, err := ParseKern(table) 33 | tu.AssertNoErr(t, err) 34 | tu.Assert(t, len(kern.Tables) > 0) 35 | 36 | for _, subtable := range kern.Tables { 37 | tu.Assert(t, subtable.Data() != nil) 38 | } 39 | } 40 | } 41 | 42 | func TestKern3(t *testing.T) { 43 | table, err := td.Files.ReadFile("toys/tables/kern3.bin") 44 | tu.AssertNoErr(t, err) 45 | 46 | kern, _, err := ParseKern(table) 47 | tu.AssertNoErr(t, err) 48 | tu.Assert(t, len(kern.Tables) == 5) 49 | 50 | expectedsLengths := [...][3]int{ 51 | {570, 5688, 92}, 52 | {570, 6557, 104}, 53 | {570, 5832, 107}, 54 | {570, 6083, 106}, 55 | {570, 4828, 82}, 56 | } 57 | for i := range kern.Tables { 58 | data, ok := kern.Tables[i].Data().(KernData3) 59 | tu.Assert(t, ok) 60 | exp := expectedsLengths[i] 61 | tu.Assert(t, len(data.LeftClass) == exp[0]) 62 | tu.Assert(t, len(data.KernIndex) == exp[1]) 63 | tu.Assert(t, len(data.Kernings) == exp[2]) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /font/opentype/tables/maxp_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from maxp_src.go. DO NOT EDIT 11 | 12 | func ParseMaxp(src []byte) (Maxp, int, error) { 13 | var item Maxp 14 | n := 0 15 | if L := len(src); L < 6 { 16 | return item, 0, fmt.Errorf("reading Maxp: "+"EOF: expected length: 6, got %d", L) 17 | } 18 | _ = src[5] // early bound checking 19 | item.version = maxpVersion(binary.BigEndian.Uint32(src[0:])) 20 | item.NumGlyphs = binary.BigEndian.Uint16(src[4:]) 21 | n += 6 22 | 23 | { 24 | var ( 25 | read int 26 | err error 27 | ) 28 | switch item.version { 29 | case maxpVersion05: 30 | item.data, read, err = parseMaxpData05(src[6:]) 31 | case maxpVersion1: 32 | item.data, read, err = parseMaxpData1(src[6:]) 33 | default: 34 | err = fmt.Errorf("unsupported maxpDataVersion %d", item.version) 35 | } 36 | if err != nil { 37 | return item, 0, fmt.Errorf("reading Maxp: %s", err) 38 | } 39 | n += read 40 | } 41 | return item, n, nil 42 | } 43 | 44 | func (item *maxpData1) mustParse(src []byte) { 45 | item.rawData[0] = binary.BigEndian.Uint16(src[0:]) 46 | item.rawData[1] = binary.BigEndian.Uint16(src[2:]) 47 | item.rawData[2] = binary.BigEndian.Uint16(src[4:]) 48 | item.rawData[3] = binary.BigEndian.Uint16(src[6:]) 49 | item.rawData[4] = binary.BigEndian.Uint16(src[8:]) 50 | item.rawData[5] = binary.BigEndian.Uint16(src[10:]) 51 | item.rawData[6] = binary.BigEndian.Uint16(src[12:]) 52 | item.rawData[7] = binary.BigEndian.Uint16(src[14:]) 53 | item.rawData[8] = binary.BigEndian.Uint16(src[16:]) 54 | item.rawData[9] = binary.BigEndian.Uint16(src[18:]) 55 | item.rawData[10] = binary.BigEndian.Uint16(src[20:]) 56 | item.rawData[11] = binary.BigEndian.Uint16(src[22:]) 57 | item.rawData[12] = binary.BigEndian.Uint16(src[24:]) 58 | } 59 | 60 | func parseMaxpData05([]byte) (maxpData05, int, error) { 61 | var item maxpData05 62 | n := 0 63 | return item, n, nil 64 | } 65 | 66 | func parseMaxpData1(src []byte) (maxpData1, int, error) { 67 | var item maxpData1 68 | n := 0 69 | if L := len(src); L < 26 { 70 | return item, 0, fmt.Errorf("reading maxpData1: "+"EOF: expected length: 26, got %d", L) 71 | } 72 | item.mustParse(src) 73 | n += 26 74 | return item, n, nil 75 | } 76 | -------------------------------------------------------------------------------- /font/opentype/tables/maxp_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | // https://learn.microsoft.com/en-us/typography/opentype/spec/Maxp 6 | type Maxp struct { 7 | version maxpVersion 8 | NumGlyphs uint16 9 | data maxpData `unionField:"version"` 10 | } 11 | 12 | type maxpVersion uint32 13 | 14 | const ( 15 | maxpVersion05 maxpVersion = 0x00005000 16 | maxpVersion1 maxpVersion = 0x00010000 17 | ) 18 | 19 | type maxpData interface { 20 | isMaxpVersion() 21 | } 22 | 23 | func (maxpData05) isMaxpVersion() {} 24 | func (maxpData1) isMaxpVersion() {} 25 | 26 | type maxpData05 struct{} 27 | 28 | type maxpData1 struct { 29 | rawData [13]uint16 30 | } 31 | -------------------------------------------------------------------------------- /font/opentype/tables/name_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from name_src.go. DO NOT EDIT 11 | 12 | func ParseName(src []byte) (Name, int, error) { 13 | var item Name 14 | n := 0 15 | if L := len(src); L < 6 { 16 | return item, 0, fmt.Errorf("reading Name: "+"EOF: expected length: 6, got %d", L) 17 | } 18 | _ = src[5] // early bound checking 19 | item.version = binary.BigEndian.Uint16(src[0:]) 20 | item.count = binary.BigEndian.Uint16(src[2:]) 21 | offsetStringData := int(binary.BigEndian.Uint16(src[4:])) 22 | n += 6 23 | 24 | { 25 | 26 | if offsetStringData != 0 { // ignore null offset 27 | if L := len(src); L < offsetStringData { 28 | return item, 0, fmt.Errorf("reading Name: "+"EOF: expected length: %d, got %d", offsetStringData, L) 29 | } 30 | 31 | item.stringData = src[offsetStringData:] 32 | } 33 | } 34 | { 35 | arrayLength := int(item.count) 36 | 37 | if L := len(src); L < 6+arrayLength*12 { 38 | return item, 0, fmt.Errorf("reading Name: "+"EOF: expected length: %d, got %d", 6+arrayLength*12, L) 39 | } 40 | 41 | item.nameRecords = make([]nameRecord, arrayLength) // allocation guarded by the previous check 42 | for i := range item.nameRecords { 43 | item.nameRecords[i].mustParse(src[6+i*12:]) 44 | } 45 | n += arrayLength * 12 46 | } 47 | return item, n, nil 48 | } 49 | 50 | func (item *nameRecord) mustParse(src []byte) { 51 | _ = src[11] // early bound checking 52 | item.platformID = PlatformID(binary.BigEndian.Uint16(src[0:])) 53 | item.encodingID = EncodingID(binary.BigEndian.Uint16(src[2:])) 54 | item.languageID = LanguageID(binary.BigEndian.Uint16(src[4:])) 55 | item.nameID = NameID(binary.BigEndian.Uint16(src[6:])) 56 | item.length = binary.BigEndian.Uint16(src[8:]) 57 | item.stringOffset = binary.BigEndian.Uint16(src[10:]) 58 | } 59 | -------------------------------------------------------------------------------- /font/opentype/tables/os2_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from os2_src.go. DO NOT EDIT 11 | 12 | func ParseOs2(src []byte) (Os2, int, error) { 13 | var item Os2 14 | n := 0 15 | if L := len(src); L < 78 { 16 | return item, 0, fmt.Errorf("reading Os2: "+"EOF: expected length: 78, got %d", L) 17 | } 18 | _ = src[77] // early bound checking 19 | item.Version = binary.BigEndian.Uint16(src[0:]) 20 | item.XAvgCharWidth = binary.BigEndian.Uint16(src[2:]) 21 | item.USWeightClass = binary.BigEndian.Uint16(src[4:]) 22 | item.USWidthClass = binary.BigEndian.Uint16(src[6:]) 23 | item.fSType = binary.BigEndian.Uint16(src[8:]) 24 | item.YSubscriptXSize = int16(binary.BigEndian.Uint16(src[10:])) 25 | item.YSubscriptYSize = int16(binary.BigEndian.Uint16(src[12:])) 26 | item.YSubscriptXOffset = int16(binary.BigEndian.Uint16(src[14:])) 27 | item.YSubscriptYOffset = int16(binary.BigEndian.Uint16(src[16:])) 28 | item.YSuperscriptXSize = int16(binary.BigEndian.Uint16(src[18:])) 29 | item.YSuperscriptYSize = int16(binary.BigEndian.Uint16(src[20:])) 30 | item.YSuperscriptXOffset = int16(binary.BigEndian.Uint16(src[22:])) 31 | item.ySuperscriptYOffset = int16(binary.BigEndian.Uint16(src[24:])) 32 | item.YStrikeoutSize = int16(binary.BigEndian.Uint16(src[26:])) 33 | item.YStrikeoutPosition = int16(binary.BigEndian.Uint16(src[28:])) 34 | item.sFamilyClass = int16(binary.BigEndian.Uint16(src[30:])) 35 | item.panose[0] = src[32] 36 | item.panose[1] = src[33] 37 | item.panose[2] = src[34] 38 | item.panose[3] = src[35] 39 | item.panose[4] = src[36] 40 | item.panose[5] = src[37] 41 | item.panose[6] = src[38] 42 | item.panose[7] = src[39] 43 | item.panose[8] = src[40] 44 | item.panose[9] = src[41] 45 | item.ulCharRange[0] = binary.BigEndian.Uint32(src[42:]) 46 | item.ulCharRange[1] = binary.BigEndian.Uint32(src[46:]) 47 | item.ulCharRange[2] = binary.BigEndian.Uint32(src[50:]) 48 | item.ulCharRange[3] = binary.BigEndian.Uint32(src[54:]) 49 | item.achVendID = Tag(binary.BigEndian.Uint32(src[58:])) 50 | item.FsSelection = binary.BigEndian.Uint16(src[62:]) 51 | item.USFirstCharIndex = binary.BigEndian.Uint16(src[64:]) 52 | item.USLastCharIndex = binary.BigEndian.Uint16(src[66:]) 53 | item.STypoAscender = int16(binary.BigEndian.Uint16(src[68:])) 54 | item.STypoDescender = int16(binary.BigEndian.Uint16(src[70:])) 55 | item.STypoLineGap = int16(binary.BigEndian.Uint16(src[72:])) 56 | item.usWinAscent = binary.BigEndian.Uint16(src[74:]) 57 | item.usWinDescent = binary.BigEndian.Uint16(src[76:]) 58 | n += 78 59 | 60 | { 61 | 62 | item.HigherVersionData = src[78:] 63 | n = len(src) 64 | } 65 | return item, n, nil 66 | } 67 | -------------------------------------------------------------------------------- /font/opentype/tables/os2_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | // OS/2 and Windows Metrics Table 6 | // See https://learn.microsoft.com/en-us/typography/opentype/spec/os2 7 | type Os2 struct { 8 | Version uint16 9 | XAvgCharWidth uint16 10 | USWeightClass uint16 11 | USWidthClass uint16 12 | fSType uint16 13 | YSubscriptXSize int16 14 | YSubscriptYSize int16 15 | YSubscriptXOffset int16 16 | YSubscriptYOffset int16 17 | YSuperscriptXSize int16 18 | YSuperscriptYSize int16 19 | YSuperscriptXOffset int16 20 | ySuperscriptYOffset int16 21 | YStrikeoutSize int16 22 | YStrikeoutPosition int16 23 | sFamilyClass int16 24 | panose [10]byte 25 | ulCharRange [4]uint32 26 | achVendID Tag 27 | FsSelection uint16 28 | USFirstCharIndex uint16 29 | USLastCharIndex uint16 30 | STypoAscender int16 31 | STypoDescender int16 32 | STypoLineGap int16 33 | usWinAscent uint16 34 | usWinDescent uint16 35 | HigherVersionData []byte `arrayCount:"ToEnd"` 36 | } 37 | 38 | func (os *Os2) FontPage() FontPage { 39 | if os.Version == 0 { 40 | return FontPage(os.FsSelection & 0xFF00) 41 | } 42 | return FPNone 43 | } 44 | 45 | // See https://docs.microsoft.com/en-us/typography/legacy/legacy_arabic_fonts 46 | // https://github.com/Microsoft/Font-Validator/blob/520aaae/OTFontFileVal/val_OS2.cs#L644-L681 47 | type FontPage uint16 48 | 49 | const ( 50 | FPNone FontPage = 0 51 | FPHebrew FontPage = 0xB100 /* Hebrew Windows 3.1 font page */ 52 | FPSimpArabic FontPage = 0xB200 /* Simplified Arabic Windows 3.1 font page */ 53 | FPTradArabic FontPage = 0xB300 /* Traditional Arabic Windows 3.1 font page */ 54 | FPOemArabic FontPage = 0xB400 /* OEM Arabic Windows 3.1 font page */ 55 | FPSimpFarsi FontPage = 0xBA00 /* Simplified Farsi Windows 3.1 font page */ 56 | FPTradFarsi FontPage = 0xBB00 /* Traditional Farsi Windows 3.1 font page */ 57 | FPThai FontPage = 0xDE00 /* Thai Windows 3.1 font page */ 58 | ) 59 | -------------------------------------------------------------------------------- /font/opentype/tables/post_gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | ) 9 | 10 | // Code generated by binarygen from post_src.go. DO NOT EDIT 11 | 12 | func ParsePost(src []byte) (Post, int, error) { 13 | var item Post 14 | n := 0 15 | if L := len(src); L < 32 { 16 | return item, 0, fmt.Errorf("reading Post: "+"EOF: expected length: 32, got %d", L) 17 | } 18 | _ = src[31] // early bound checking 19 | item.version = postVersion(binary.BigEndian.Uint32(src[0:])) 20 | item.italicAngle = binary.BigEndian.Uint32(src[4:]) 21 | item.UnderlinePosition = int16(binary.BigEndian.Uint16(src[8:])) 22 | item.UnderlineThickness = int16(binary.BigEndian.Uint16(src[10:])) 23 | item.IsFixedPitch = binary.BigEndian.Uint32(src[12:]) 24 | item.memoryUsage[0] = binary.BigEndian.Uint32(src[16:]) 25 | item.memoryUsage[1] = binary.BigEndian.Uint32(src[20:]) 26 | item.memoryUsage[2] = binary.BigEndian.Uint32(src[24:]) 27 | item.memoryUsage[3] = binary.BigEndian.Uint32(src[28:]) 28 | n += 32 29 | 30 | { 31 | var ( 32 | read int 33 | err error 34 | ) 35 | switch item.version { 36 | case postVersion10: 37 | item.Names, read, err = ParsePostNames10(src[32:]) 38 | case postVersion20: 39 | item.Names, read, err = ParsePostNames20(src[32:]) 40 | case postVersion30: 41 | item.Names, read, err = ParsePostNames30(src[32:]) 42 | default: 43 | err = fmt.Errorf("unsupported PostNamesVersion %d", item.version) 44 | } 45 | if err != nil { 46 | return item, 0, fmt.Errorf("reading Post: %s", err) 47 | } 48 | n += read 49 | } 50 | return item, n, nil 51 | } 52 | 53 | func ParsePostNames10([]byte) (PostNames10, int, error) { 54 | var item PostNames10 55 | n := 0 56 | return item, n, nil 57 | } 58 | 59 | func ParsePostNames20(src []byte) (PostNames20, int, error) { 60 | var item PostNames20 61 | n := 0 62 | if L := len(src); L < 2 { 63 | return item, 0, fmt.Errorf("reading PostNames20: "+"EOF: expected length: 2, got %d", L) 64 | } 65 | arrayLengthGlyphNameIndexes := int(binary.BigEndian.Uint16(src[0:])) 66 | n += 2 67 | 68 | { 69 | 70 | if L := len(src); L < 2+arrayLengthGlyphNameIndexes*2 { 71 | return item, 0, fmt.Errorf("reading PostNames20: "+"EOF: expected length: %d, got %d", 2+arrayLengthGlyphNameIndexes*2, L) 72 | } 73 | 74 | item.GlyphNameIndexes = make([]uint16, arrayLengthGlyphNameIndexes) // allocation guarded by the previous check 75 | for i := range item.GlyphNameIndexes { 76 | item.GlyphNameIndexes[i] = binary.BigEndian.Uint16(src[2+i*2:]) 77 | } 78 | n += arrayLengthGlyphNameIndexes * 2 79 | } 80 | { 81 | 82 | err := item.parseStrings(src[n:]) 83 | if err != nil { 84 | return item, 0, fmt.Errorf("reading PostNames20: %s", err) 85 | } 86 | } 87 | return item, n, nil 88 | } 89 | 90 | func ParsePostNames30([]byte) (PostNames30, int, error) { 91 | var item PostNames30 92 | n := 0 93 | return item, n, nil 94 | } 95 | -------------------------------------------------------------------------------- /font/opentype/tables/post_src.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import "fmt" 6 | 7 | // PostScript table 8 | // See https://learn.microsoft.com/en-us/typography/opentype/spec/post 9 | type Post struct { 10 | version postVersion 11 | italicAngle uint32 12 | // UnderlinePosition is the suggested distance of the top of the 13 | // underline from the baseline (negative values indicate below baseline). 14 | UnderlinePosition int16 15 | // Suggested values for the underline thickness. 16 | UnderlineThickness int16 17 | // IsFixedPitch indicates that the font is not proportionally spaced 18 | // (i.e. monospaced). 19 | IsFixedPitch uint32 20 | memoryUsage [4]uint32 21 | Names PostNames `unionField:"version"` 22 | } 23 | 24 | type PostNames interface { 25 | isPostNames() 26 | } 27 | 28 | func (PostNames10) isPostNames() {} 29 | func (PostNames20) isPostNames() {} 30 | func (PostNames30) isPostNames() {} 31 | 32 | type postVersion uint32 33 | 34 | const ( 35 | postVersion10 postVersion = 0x00010000 36 | postVersion20 postVersion = 0x00020000 37 | postVersion30 postVersion = 0x00030000 38 | ) 39 | 40 | type PostNames10 struct{} 41 | 42 | type PostNames20 struct { 43 | GlyphNameIndexes []uint16 `arrayCount:"FirstUint16"` // size numGlyph 44 | Strings []string `isOpaque:"" subsliceStart:"AtCurrent"` 45 | } 46 | 47 | // see https://learn.microsoft.com/en-us/typography/opentype/spec/post#version-20 48 | func (ps *PostNames20) parseStrings(src []byte) error { 49 | // "Strings are in Pascal string format, meaning that the first byte of 50 | // a given string is a length: the number of characters in that string. 51 | // The length byte is not included; for example, a length byte of 8 indicates 52 | // that the 8 bytes following the length byte comprise the string character data." 53 | for i := 0; i < len(src); { 54 | length := int(src[i]) // read the length 55 | end := i + 1 + length 56 | if L := len(src); L < end { 57 | return fmt.Errorf("invalid Postscript names tables format 20: EOF: expected %d, got %d", end, L) 58 | } 59 | ps.Strings = append(ps.Strings, string(src[i+1:end])) 60 | i = end 61 | } 62 | return nil 63 | } 64 | 65 | type PostNames30 PostNames10 66 | -------------------------------------------------------------------------------- /font/opentype/tables/tables.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import "github.com/go-text/typesetting/font/opentype" 6 | 7 | //go:generate ../../../../typesetting-utils/generators/binarygen/cmd/generator . _src.go 8 | 9 | type GlyphID = uint16 10 | 11 | // NameID is the ID for entries in the font table. 12 | type NameID uint16 13 | 14 | type Tag = opentype.Tag 15 | 16 | // Float1616 is a float32, represented in 17 | // fixed 16.16 format in font files. 18 | type Float1616 = float32 19 | 20 | func Float1616FromUint(v uint32) Float1616 { 21 | // value are actually signed integers 22 | return Float1616(int32(v)) / (1 << 16) 23 | } 24 | 25 | func Float1616ToUint(f Float1616) uint32 { 26 | return uint32(int32(f * (1 << 16))) 27 | } 28 | 29 | func Float214FromUint(v uint16) float32 { 30 | // value are actually signed integers 31 | return float32(int16(v)) / (1 << 14) 32 | } 33 | 34 | // Coord is real number in [-1;1], stored as a fixed 2.14 integer 35 | type Coord int16 36 | 37 | func NewCoord(c float64) Coord { 38 | return Coord(c * (1 << 14)) 39 | } 40 | 41 | // Number of seconds since 12:00 midnight that started January 1st 1904 in GMT/UTC time zone. 42 | type longdatetime = uint64 43 | 44 | // PlatformID represents the platform id for entries in the name table. 45 | type PlatformID uint16 46 | 47 | // EncodingID represents the platform specific id for entries in the name table. 48 | // The most common values are provided as constants. 49 | type EncodingID uint16 50 | 51 | // LanguageID represents the language used by an entry in the name table 52 | type LanguageID uint16 53 | 54 | // Offset16 is an offset into the input byte slice 55 | type Offset16 uint16 56 | 57 | // Offset32 is an offset into the input byte slice 58 | type Offset32 uint32 59 | -------------------------------------------------------------------------------- /font/opentype/tables/tables_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package tables 4 | 5 | import ( 6 | "bytes" 7 | "testing" 8 | 9 | td "github.com/go-text/typesetting-utils/opentype" 10 | ot "github.com/go-text/typesetting/font/opentype" 11 | tu "github.com/go-text/typesetting/testutils" 12 | ) 13 | 14 | // wrap td.Files.ReadFile 15 | func readFontFile(t testing.TB, filepath string) *ot.Loader { 16 | t.Helper() 17 | 18 | file, err := td.Files.ReadFile(filepath) 19 | tu.AssertNoErr(t, err) 20 | 21 | fp, err := ot.NewLoader(bytes.NewReader(file)) 22 | tu.AssertNoErr(t, err) 23 | 24 | return fp 25 | } 26 | 27 | func readTable(t testing.TB, fl *ot.Loader, tag string) []byte { 28 | t.Helper() 29 | 30 | table, err := fl.RawTable(ot.MustNewTag(tag)) 31 | tu.AssertNoErr(t, err) 32 | 33 | return table 34 | } 35 | 36 | func numGlyphs(t *testing.T, fp *ot.Loader) int { 37 | t.Helper() 38 | 39 | table := readTable(t, fp, "maxp") 40 | maxp, _, err := ParseMaxp(table) 41 | tu.AssertNoErr(t, err) 42 | 43 | return int(maxp.NumGlyphs) 44 | } 45 | 46 | func TestParseBasicTables(t *testing.T) { 47 | for _, filename := range append(tu.Filenames(t, "morx"), tu.Filenames(t, "common")...) { 48 | fp := readFontFile(t, filename) 49 | _, _, err := ParseOs2(readTable(t, fp, "OS/2")) 50 | tu.AssertNoErr(t, err) 51 | 52 | _, _, err = ParseHead(readTable(t, fp, "head")) 53 | tu.AssertNoErr(t, err) 54 | 55 | _, _, err = ParseMaxp(readTable(t, fp, "maxp")) 56 | tu.AssertNoErr(t, err) 57 | 58 | _, _, err = ParseName(readTable(t, fp, "name")) 59 | tu.AssertNoErr(t, err) 60 | 61 | _, _, err = ParsePost(readTable(t, fp, "post")) 62 | tu.AssertNoErr(t, err) 63 | } 64 | } 65 | 66 | func TestParseCmap(t *testing.T) { 67 | // general parsing 68 | for _, filename := range tu.Filenames(t, "common") { 69 | fp := readFontFile(t, filename) 70 | _, _, err := ParseCmap(readTable(t, fp, "cmap")) 71 | tu.AssertNoErr(t, err) 72 | } 73 | 74 | // specialized tests for each format 75 | for _, filename := range tu.Filenames(t, "cmap") { 76 | fp := readFontFile(t, filename) 77 | _, _, err := ParseCmap(readTable(t, fp, "cmap")) 78 | tu.AssertNoErr(t, err) 79 | } 80 | 81 | // tests through a single table 82 | for _, filepath := range tu.Filenames(t, "cmap/table") { 83 | table, err := td.Files.ReadFile(filepath) 84 | tu.AssertNoErr(t, err) 85 | 86 | cmap, _, err := ParseCmap(table) 87 | tu.AssertNoErr(t, err) 88 | tu.Assert(t, len(cmap.Records) > 0) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /font/opentype/writer.go: -------------------------------------------------------------------------------- 1 | package opentype 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | ) 7 | 8 | // Table is one opentype binary table and its tag. 9 | type Table struct { 10 | Content []byte 11 | Tag Tag 12 | } 13 | 14 | // WriteTTF creates a single Truetype font file (.ttf) from the given [tables] slice, 15 | // which must be sorted by Tag 16 | func WriteTTF(tables []Table) []byte { 17 | introLength := uint32(otfHeaderSize + len(tables)*otfEntrySize) 18 | buffer := make([]byte, introLength) 19 | 20 | writeTTFHeader(len(tables), buffer) 21 | 22 | tableOffset := introLength // the actual content will start after the header + table directory 23 | for i, table := range tables { 24 | cs := checksum(table.Content) 25 | tableLength := uint32(len(table.Content)) 26 | 27 | slice := buffer[otfHeaderSize+i*otfEntrySize:] 28 | binary.BigEndian.PutUint32(slice, uint32(table.Tag)) 29 | binary.BigEndian.PutUint32(slice[4:], cs) 30 | binary.BigEndian.PutUint32(slice[8:], tableOffset) 31 | binary.BigEndian.PutUint32(slice[12:], tableLength) 32 | 33 | // update the offset 34 | tableOffset = tableOffset + tableLength 35 | } 36 | 37 | // append the actual table content : 38 | // allocate only once 39 | buffer = append(buffer, make([]byte, tableOffset-introLength)...) 40 | tableOffset = introLength 41 | for _, table := range tables { 42 | copy(buffer[tableOffset:], table.Content) 43 | tableOffset = tableOffset + uint32(len(table.Content)) 44 | } 45 | 46 | return buffer 47 | } 48 | 49 | // out is assumed to have a length >= ttfHeaderSize 50 | func writeTTFHeader(nTables int, out []byte) { 51 | log2 := math.Floor(math.Log2(float64(nTables))) 52 | // Maximum power of 2 less than or equal to numTables, times 16 ((2**floor(log2(numTables))) * 16, where “**” is an exponentiation operator). 53 | searchRange := math.Pow(2, log2) * 16 54 | // Log2 of the maximum power of 2 less than or equal to numTables (log2(searchRange/16), which is equal to floor(log2(numTables))). 55 | entrySelector := log2 56 | // numTables times 16, minus searchRange ((numTables * 16) - searchRange). 57 | rangeShift := nTables*16 - int(searchRange) 58 | 59 | binary.BigEndian.PutUint32(out[:], uint32(TrueType)) 60 | binary.BigEndian.PutUint16(out[4:], uint16(nTables)) 61 | binary.BigEndian.PutUint16(out[6:], uint16(searchRange)) 62 | binary.BigEndian.PutUint16(out[8:], uint16(entrySelector)) 63 | binary.BigEndian.PutUint16(out[10:], uint16(rangeShift)) 64 | } 65 | 66 | func checksum(table []byte) uint32 { 67 | // "To accommodate data with a length that is not a multiple of four, 68 | // the above algorithm must be modified to treat the data as though 69 | // it contains zero padding to a length that is a multiple of four." 70 | if r := len(table) % 4; r != 0 { 71 | table = append(table, make([]byte, r)...) 72 | } 73 | 74 | var sum uint32 75 | for i := 0; i < len(table)/4; i++ { 76 | sum += binary.BigEndian.Uint32(table[i*4:]) 77 | } 78 | 79 | return sum 80 | } 81 | -------------------------------------------------------------------------------- /font/opentype/writer_test.go: -------------------------------------------------------------------------------- 1 | package opentype 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | td "github.com/go-text/typesetting-utils/opentype" 8 | tu "github.com/go-text/typesetting/testutils" 9 | ) 10 | 11 | func TestWrite(t *testing.T) { 12 | for _, filename := range tu.Filenames(t, "common") { 13 | f, err := td.Files.ReadFile(filename) 14 | tu.AssertNoErr(t, err) 15 | 16 | font, err := NewLoader(bytes.NewReader(f)) 17 | tu.AssertNoErr(t, err) 18 | 19 | tags := font.Tables() 20 | tables := make([]Table, len(tags)) 21 | for i, tag := range tags { 22 | tables[i].Tag = tag 23 | tables[i].Content, err = font.RawTable(tag) 24 | tu.AssertNoErr(t, err) 25 | } 26 | 27 | content := WriteTTF(tables) 28 | font2, err := NewLoader(bytes.NewReader(content)) 29 | tu.AssertNoErr(t, err) 30 | 31 | for _, table := range tables { 32 | t2, err := font2.RawTable(table.Tag) 33 | tu.AssertNoErr(t, err) 34 | 35 | tu.Assert(t, bytes.Equal(table.Content, t2)) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /font/os2.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package font 4 | 5 | import ( 6 | "encoding/binary" 7 | "errors" 8 | 9 | "github.com/go-text/typesetting/font/opentype/tables" 10 | ) 11 | 12 | type os2 struct { 13 | version uint16 14 | xAvgCharWidth uint16 15 | 16 | *os2Desc 17 | 18 | useTypoMetrics bool // true if the field sTypoAscender, sTypoDescender and sTypoLineGap are valid. 19 | 20 | ySubscriptXSize float32 21 | ySubscriptYSize float32 22 | ySubscriptXOffset float32 23 | ySubscriptYOffset float32 24 | ySuperscriptXSize float32 25 | ySuperscriptYSize float32 26 | ySuperscriptXOffset float32 27 | yStrikeoutSize float32 28 | yStrikeoutPosition float32 29 | sTypoAscender float32 30 | sTypoDescender float32 31 | sTypoLineGap float32 32 | sxHeigh float32 33 | sCapHeight float32 34 | } 35 | 36 | func newOs2(os tables.Os2) (os2, error) { 37 | out := os2{ 38 | version: os.Version, 39 | xAvgCharWidth: os.XAvgCharWidth, 40 | os2Desc: newOS2Desc(os), 41 | ySubscriptXSize: float32(os.YSubscriptXSize), 42 | ySubscriptYSize: float32(os.YSubscriptYSize), 43 | ySubscriptXOffset: float32(os.YSubscriptXOffset), 44 | ySubscriptYOffset: float32(os.YSubscriptYOffset), 45 | ySuperscriptXSize: float32(os.YSuperscriptXSize), 46 | ySuperscriptYSize: float32(os.YSuperscriptYSize), 47 | ySuperscriptXOffset: float32(os.YSuperscriptXOffset), 48 | yStrikeoutSize: float32(os.YStrikeoutSize), 49 | yStrikeoutPosition: float32(os.YStrikeoutPosition), 50 | sTypoAscender: float32(os.STypoAscender), 51 | sTypoDescender: float32(os.STypoDescender), 52 | sTypoLineGap: float32(os.STypoLineGap), 53 | } 54 | // add addition info for version >= 2 55 | if os.Version >= 2 { 56 | if len(os.HigherVersionData) < 12 { 57 | return os2{}, errors.New("invalid table os2") 58 | } 59 | out.sxHeigh = float32(binary.BigEndian.Uint16(os.HigherVersionData[8:])) 60 | out.sCapHeight = float32(binary.BigEndian.Uint16(os.HigherVersionData[10:])) 61 | } 62 | 63 | const useTypoMetrics = 1 << 7 64 | use := os.FsSelection&useTypoMetrics != 0 65 | hasData := os.USWeightClass != 0 || os.USWidthClass != 0 || os.USFirstCharIndex != 0 || os.USLastCharIndex != 0 66 | out.useTypoMetrics = use && hasData 67 | 68 | return out, nil 69 | } 70 | -------------------------------------------------------------------------------- /font/ot_layout_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package font 4 | 5 | import ( 6 | "reflect" 7 | "sort" 8 | "testing" 9 | 10 | "github.com/go-text/typesetting/font/opentype/tables" 11 | tu "github.com/go-text/typesetting/testutils" 12 | ) 13 | 14 | func TestGetProps(t *testing.T) { 15 | file := readFontFile(t, "common/Raleway-v4020-Regular.otf") 16 | 17 | gpos, _, err := tables.ParseLayout(readTable(t, file, "GPOS")) 18 | tu.AssertNoErr(t, err) 19 | gsub, _, err := tables.ParseLayout(readTable(t, file, "GSUB")) 20 | tu.AssertNoErr(t, err) 21 | 22 | for _, table := range []Layout{newLayout(gpos), newLayout(gsub)} { 23 | var tags []int 24 | for _, s := range table.Scripts { 25 | tags = append(tags, int(s.Tag)) 26 | } 27 | tu.Assert(t, sort.IntsAreSorted(tags)) 28 | 29 | for i, s := range table.Scripts { 30 | ptr := table.FindScript(s.Tag) 31 | tu.Assert(t, ptr == i) 32 | } 33 | 34 | s := table.FindScript(Tag(0)) // invalid 35 | tu.Assert(t, s == -1) 36 | 37 | for _, feat := range table.Features { 38 | _, ok := table.FindFeatureIndex(feat.Tag) 39 | tu.Assert(t, ok) 40 | } 41 | _, ok := table.FindFeatureIndex(Tag(0)) // invalid 42 | tu.Assert(t, !ok) 43 | 44 | // now check the languages 45 | 46 | for _, script := range table.Scripts { 47 | var tags []int 48 | for _, s := range script.LangSysRecords { 49 | tags = append(tags, int(s.Tag)) 50 | } 51 | tu.Assert(t, sort.IntsAreSorted(tags)) 52 | 53 | for i, l := range script.LangSysRecords { 54 | ptr := script.FindLanguage(l.Tag) 55 | tu.Assert(t, ptr == i) 56 | } 57 | 58 | s := script.FindLanguage(Tag(0)) // invalid 59 | tu.Assert(t, s == -1) 60 | 61 | tu.Assert(t, script.DefaultLangSys != nil) 62 | tu.Assert(t, reflect.DeepEqual(script.GetLangSys(0xFFFF), *script.DefaultLangSys)) 63 | } 64 | } 65 | } 66 | 67 | func TestOTFeatureVariation(t *testing.T) { 68 | ft := readFontFile(t, "common/Commissioner-VF.ttf") 69 | 70 | gsubT, _, err := tables.ParseLayout(readTable(t, ft, "GSUB")) 71 | tu.AssertNoErr(t, err) 72 | 73 | gsub := newLayout(gsubT) 74 | tu.Assert(t, gsub.FindVariationIndex([]VarCoord{tables.NewCoord(0.8)}) == 0) 75 | tu.Assert(t, gsub.FindVariationIndex([]VarCoord{tables.NewCoord(0.4)}) == -1) 76 | } 77 | -------------------------------------------------------------------------------- /font/svg.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package font 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/go-text/typesetting/font/opentype/tables" 9 | ) 10 | 11 | type svg []svgDocument 12 | 13 | func newSvg(table tables.SVG) (svg, error) { 14 | rawData := table.SVGDocumentList.SVGRawData 15 | out := make(svg, len(table.SVGDocumentList.DocumentRecords)) 16 | for i, rec := range table.SVGDocumentList.DocumentRecords { 17 | start, end := rec.SvgDocOffset, rec.SvgDocOffset+tables.Offset32(rec.SvgDocLength) 18 | if len(rawData) < int(end) { 19 | return nil, fmt.Errorf("invalid svg table (EOF: expected %d, got %d)", end, len(rawData)) 20 | } 21 | out[i] = svgDocument{ 22 | first: rec.StartGlyphID, 23 | last: rec.EndGlyphID, 24 | svg: rawData[start:end], 25 | } 26 | } 27 | return out, nil 28 | } 29 | 30 | type svgDocument struct { 31 | // svg document 32 | // each glyph description must be written 33 | // in an element with id=glyphXXX 34 | svg []byte 35 | first gID // The first glyph ID in the range described by this index entry. 36 | last gID // The last glyph ID in the range described by this index entry. Must be >= startGlyphID. 37 | } 38 | 39 | // rawGlyphData returns the SVG document for [gid], or false. 40 | func (s svg) rawGlyphData(gid gID) ([]byte, bool) { 41 | // binary search 42 | for i, j := 0, len(s); i < j; { 43 | h := i + (j-i)/2 44 | entry := s[h] 45 | if gid < entry.first { 46 | j = h 47 | } else if entry.last < gid { 48 | i = h + 1 49 | } else { 50 | return entry.svg, true 51 | } 52 | } 53 | return nil, false 54 | } 55 | -------------------------------------------------------------------------------- /font/testdata/Amiri-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-text/typesetting/acb59e3cfb986d6f64635fb7d80cb94506cb0496/font/testdata/Amiri-Regular.ttf -------------------------------------------------------------------------------- /font/testdata/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010-2020 The Amiri Project Authors (https://github.com/alif-type/amiri). 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 | -------------------------------------------------------------------------------- /font/testdata/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-text/typesetting/acb59e3cfb986d6f64635fb7d80cb94506cb0496/font/testdata/Roboto-Regular.ttf -------------------------------------------------------------------------------- /font/testdata/Selawik-VF-Subset.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-text/typesetting/acb59e3cfb986d6f64635fb7d80cb94506cb0496/font/testdata/Selawik-VF-Subset.ttf -------------------------------------------------------------------------------- /font/testdata/UbuntuMono-R.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-text/typesetting/acb59e3cfb986d6f64635fb7d80cb94506cb0496/font/testdata/UbuntuMono-R.ttf -------------------------------------------------------------------------------- /font/testdata/readme.md: -------------------------------------------------------------------------------- 1 | # Font samples for testing 2 | 3 | This directory includes font files used for testing. 4 | 5 | ### Licenses 6 | 7 | - Roboto-Regular.ttf: APACHE (https://fonts.google.com/specimen/Roboto) 8 | - Amiri-Regular.ttf: OFL (https://fonts.google.com/specimen/Amiri) 9 | - UbuntuMono-R.ttf : Ubuntu Font License (http://font.ubuntu.com/ufl/) 10 | -------------------------------------------------------------------------------- /fontscan/fontconfig_test.go: -------------------------------------------------------------------------------- 1 | package fontscan 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "sort" 10 | "testing" 11 | 12 | tu "github.com/go-text/typesetting/testutils" 13 | ) 14 | 15 | func TestParseFontconfig(t *testing.T) { 16 | cwd, err := os.Getwd() 17 | tu.AssertNoErr(t, err) 18 | fc := fcVars{ 19 | xdgDataHome: filepath.Clean("/xdgData"), 20 | xdgConfigHome: filepath.Clean("/xdgConfig"), 21 | userHome: filepath.Clean("/home/me"), 22 | configFile: "fonts.conf", 23 | paths: []string{filepath.Join(cwd, "fontconfig_test")}, 24 | sysroot: "", 25 | } 26 | logger := log.New(io.Discard, "", 0) 27 | 28 | dirs, includes, err := fc.parseFcFile(logger, "fontconfig_test/fonts.conf", cwd) 29 | tu.AssertNoErr(t, err) 30 | tu.Assert(t, len(dirs) == 4) 31 | tu.Assert(t, len(includes) == 1) 32 | 33 | dirs, includes, err = fc.parseFcDir(logger, "fontconfig_test/conf.d", cwd, map[string]bool{}) 34 | tu.AssertNoErr(t, err) 35 | tu.Assert(t, len(dirs) == 1) 36 | tu.Assert(t, len(includes) == 2) 37 | 38 | dirs, err = fc.parseFcConfig(logger) 39 | for i, s := range dirs { 40 | dirs[i] = filepath.ToSlash(s) 41 | } 42 | sort.Strings(dirs) 43 | tu.AssertNoErr(t, err) 44 | expected := []string{ 45 | "/usr/share/fonts", 46 | "/usr/local/share/fonts", 47 | "/xdgData/fonts", 48 | "~/.fonts", 49 | "my_Custom_Font_Dir", 50 | filepath.Join(cwd, "fontconfig_test/conf.d/relative_font_dir"), 51 | filepath.Join(cwd, "cwd_font_dir"), 52 | } 53 | for i, s := range expected { 54 | expected[i] = filepath.ToSlash(s) 55 | } 56 | sort.Strings(expected) 57 | if !reflect.DeepEqual(expected, dirs) { 58 | t.Errorf("expected %q\ngot %q", expected, dirs) 59 | } 60 | } 61 | 62 | func TestParseFontconfigErrors(t *testing.T) { 63 | fc := fcVars{ 64 | xdgDataHome: "/xdgData", 65 | xdgConfigHome: "/xdgConfig", 66 | userHome: "", 67 | configFile: "fonts.conf", 68 | paths: []string{""}, 69 | sysroot: "", 70 | } 71 | 72 | logger := log.New(io.Discard, "", 0) 73 | _, _, err := fc.parseFcFile(logger, "fontconfig_test/invalid.conf", "") 74 | tu.Assert(t, err != nil) 75 | } 76 | -------------------------------------------------------------------------------- /fontscan/fontconfig_test/conf.d/10-antialias.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | 8 | 9 | -------------------------------------------------------------------------------- /fontscan/fontconfig_test/conf.d/99-custom.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | conf.d/other.conf 6 | 7 | conf.d/99-custom.conf 8 | 9 | my_Custom_Font_Dir 10 | 11 | -------------------------------------------------------------------------------- /fontscan/fontconfig_test/conf.d/other-child.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Use lcddefault as default for LCD filter 9 | 10 | 11 | 17 | 18 | lcddefault 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /fontscan/fontconfig_test/conf.d/other.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | other-child.conf 6 | 7 | XXX 8 | ~/.XXX 9 | relative_font_dir 10 | cwd_font_dir 11 | 12 | XXX 13 | 14 | 15 | -------------------------------------------------------------------------------- /fontscan/fontconfig_test/fonts.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Default configuration file 10 | 11 | 28 | 29 | 30 | 31 | /usr/share/fonts 32 | /usr/local/share/fonts 33 | fonts 34 | 35 | ~/.fonts 36 | 37 | 40 | 41 | 42 | mono 43 | 44 | 45 | monospace 46 | 47 | 48 | 49 | 52 | 53 | 54 | sans serif 55 | 56 | 57 | sans-serif 58 | 59 | 60 | 61 | 64 | 65 | 66 | sans 67 | 68 | 69 | sans-serif 70 | 71 | 72 | 73 | 76 | 77 | 78 | *.dpkg-tmp 79 | 80 | 81 | 82 | 83 | *.dpkg-new 84 | 85 | 86 | 87 | 90 | conf.d 91 | 92 | 93 | 94 | /var/cache/fontconfig 95 | fontconfig 96 | 97 | ~/.fontconfig 98 | 99 | 100 | 103 | 104 | 30 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /fontscan/fontconfig_test/invalid.conf: -------------------------------------------------------------------------------- 1 | not an xml file -------------------------------------------------------------------------------- /fontscan/footprint.go: -------------------------------------------------------------------------------- 1 | package fontscan 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/go-text/typesetting/font" 10 | ot "github.com/go-text/typesetting/font/opentype" 11 | "github.com/go-text/typesetting/font/opentype/tables" 12 | ) 13 | 14 | // Location identifies where a font.Face is stored. 15 | type Location = font.FontID 16 | 17 | // Footprint is a condensed summary of the main information 18 | // about a font, serving as a lightweight surrogate 19 | // for the original font file. 20 | type Footprint struct { 21 | // Location stores the adress of the font resource. 22 | Location Location 23 | 24 | // Family is the general nature of the font, like 25 | // "Arial" 26 | // Note that, for performance reason, we store the 27 | // normalized version of the family name. 28 | Family string 29 | 30 | // Runes is the set of runes supported by the font. 31 | Runes RuneSet 32 | 33 | // Scripts is the set of scripts deduced from [Runes] 34 | Scripts ScriptSet 35 | 36 | // Langs is the set of languages deduced from [Runes] 37 | Langs LangSet 38 | 39 | // Aspect precises the visual characteristics 40 | // of the font among a family, like "Bold Italic" 41 | Aspect font.Aspect 42 | 43 | // isUserProvided is set to true for fonts add manually to 44 | // a FontMap 45 | // User fonts will always be tried if no other fonts match, 46 | // and will have priority among font with same family name. 47 | // 48 | // This field is not serialized in the index, since it is always false 49 | // for system fonts. 50 | isUserProvided bool 51 | } 52 | 53 | func newFootprintFromFont(f *font.Font, location Location, md font.Description) (out Footprint) { 54 | out.Runes, out.Scripts, _ = newCoveragesFromCmap(f.Cmap, nil) 55 | out.Langs = newLangsetFromCoverage(out.Runes) 56 | out.Family = font.NormalizeFamily(md.Family) 57 | out.Aspect = md.Aspect 58 | out.Location = location 59 | out.isUserProvided = true 60 | return out 61 | } 62 | 63 | func newFootprintFromLoader(ld *ot.Loader, isUserProvided bool, buffer scanBuffer) (out Footprint, _ scanBuffer, err error) { 64 | raw := buffer.tableBuffer 65 | 66 | // since raw is shared, special car must be taken in the parsing order 67 | 68 | raw, _ = ld.RawTableTo(ot.MustNewTag("OS/2"), raw) 69 | fp := tables.FPNone 70 | if os2, _, err := tables.ParseOs2(raw); err != nil { 71 | fp = os2.FontPage() 72 | } 73 | 74 | // we can use the buffer since ProcessCmap do not keep any reference on 75 | // the input slice 76 | raw, err = ld.RawTableTo(ot.MustNewTag("cmap"), raw) 77 | if err != nil { 78 | return Footprint{}, buffer, err 79 | } 80 | tb, _, err := tables.ParseCmap(raw) 81 | if err != nil { 82 | return Footprint{}, buffer, err 83 | } 84 | cmap, _, err := font.ProcessCmap(tb, fp) 85 | if err != nil { 86 | return Footprint{}, buffer, err 87 | } 88 | 89 | out.Runes, out.Scripts, buffer.cmapBuffer = newCoveragesFromCmap(cmap, buffer.cmapBuffer) // ... and build the corresponding rune set 90 | 91 | out.Langs = newLangsetFromCoverage(out.Runes) 92 | 93 | desc, raw := font.Describe(ld, raw) 94 | out.Family = font.NormalizeFamily(desc.Family) 95 | out.Aspect = desc.Aspect 96 | out.isUserProvided = isUserProvided 97 | 98 | buffer.tableBuffer = raw 99 | 100 | return out, buffer, nil 101 | } 102 | 103 | // returns true for .ttf and .ttc font files 104 | func (fp *Footprint) isTruetypeHint() bool { 105 | switch strings.ToLower(filepath.Ext(fp.Location.File)) { 106 | case ".ttf", ".ttc": 107 | return true 108 | default: 109 | return false 110 | } 111 | } 112 | 113 | // isMonoHint returns true if "mono" is included in the family name 114 | // this is not very precise but much more efficient than using [font.Font.IsMonospace] 115 | func (fp *Footprint) isMonoHint() bool { 116 | return strings.Contains(fp.Family, "mono") 117 | } 118 | 119 | // loadFromDisk assume the footprint location refers to the file system 120 | func (fp *Footprint) loadFromDisk() (*font.Face, error) { 121 | location := fp.Location 122 | 123 | file, err := os.Open(location.File) 124 | if err != nil { 125 | return nil, err 126 | } 127 | defer file.Close() 128 | 129 | loaders, err := ot.NewLoaders(file) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | if index := int(location.Index); len(loaders) <= index { 135 | // this should only happen if the font file as changed 136 | // since the last scan (very unlikely) 137 | return nil, fmt.Errorf("invalid font index in collection: %d >= %d", index, len(loaders)) 138 | } 139 | 140 | ft, err := font.NewFont(loaders[location.Index]) 141 | if err != nil { 142 | return nil, fmt.Errorf("reading font at %s: %s", location.File, err) 143 | } 144 | 145 | return font.NewFace(ft), nil 146 | } 147 | -------------------------------------------------------------------------------- /fontscan/langset.go: -------------------------------------------------------------------------------- 1 | package fontscan 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/go-text/typesetting/language" 9 | ) 10 | 11 | // LangID is a compact representation of a language 12 | // this package has orthographic knowledge of. 13 | type LangID = language.LangID 14 | 15 | // NewLangID is [language.NewLangID]. 16 | // 17 | // Deprecated: use [language.NewLangID] instead. 18 | var NewLangID = language.NewLangID 19 | 20 | // LangSet is a bit set for 512 languages 21 | // 22 | // It works as a map[LangID]bool, with the limitation 23 | // that only the 9 low bits of a LangID are used. 24 | // More precisely, the page of a LangID l is given by its 3 "higher" bits : 8-6 25 | // and the bit position by its 6 lower bits : 5-0 26 | type LangSet [8]uint64 27 | 28 | // newLangsetFromCoverage compile the languages supported by the given 29 | // rune coverage 30 | func newLangsetFromCoverage(rs RuneSet) (out LangSet) { 31 | for id, runes := range languagesRunes { 32 | if rs.includes(runes) { 33 | out.Add(LangID(id)) 34 | } 35 | } 36 | return out 37 | } 38 | 39 | func (ls LangSet) String() string { 40 | var chunks []string 41 | for pageN, page := range ls { 42 | for bit := 0; bit < 64; bit++ { 43 | if page&(1<> 6) 54 | bit := l & 0b111111 55 | ls[page] |= 1 << bit 56 | } 57 | 58 | func (ls LangSet) Contains(l LangID) bool { 59 | page := (l & 0b111111111 >> 6) 60 | bit := l & 0b111111 61 | return ls[page]&(1< l.maxSize { 98 | oldest := l.tail.next 99 | l.remove(oldest) 100 | delete(l.m, oldest.key) 101 | } 102 | } 103 | 104 | // remove cuts e out of the lru linked list. 105 | func (l *runeLRU) remove(e *runeLRUEntry) { 106 | e.next.prev = e.prev 107 | e.prev.next = e.next 108 | } 109 | 110 | // insert adds e to the lru linked list. 111 | func (l *runeLRU) insert(e *runeLRUEntry) { 112 | e.next = l.head 113 | e.prev = l.head.prev 114 | e.prev.next = e 115 | e.next.prev = e 116 | } 117 | -------------------------------------------------------------------------------- /fontscan/readme.md: -------------------------------------------------------------------------------- 1 | # Description and purpose of the package 2 | 3 | This package provides a way to locate and load a `font.Font`, which is the 4 | fundamental object needed by `go-text` for shaping and text rendering. 5 | 6 | ## Use case 7 | 8 | This package may be used by UI toolkits and markup language renderers. Both use-cases may need to display large quantities of text of varying languages and writing systems, and want to make use of all available fonts, both packaged within the application and installed on the system. In both cases, content/UI authors provide hints about the fonts that they want chosen (family names, weights, styles, etc...) and want the closest available match to the requested properties. 9 | 10 | ## Overview of the API 11 | 12 | The entry point of the library is the `FontMap` type. It should be created for each text shaping task and be filled either with system fonts (by calling `UseSystemFonts`) or with user-provided font files (using `AddFont`, `AddFace`), or both. 13 | To leverage all the system fonts, the first usage of `UseSystemFonts` triggers a scan which builds a font index. Its content is saved on disk so that subsequent usage by the same app are not slowed down by this step. 14 | 15 | Once initialized, the font map is used to select fonts matching a `Query` with `SetQuery`. A query is defined by one or several families and an `Aspect`, containining style, weight, stretchiness. `SetScript` may be called to 16 | influence fallback font resolution. 17 | 18 | Finally, the font map satisfies the `shaping.Fontmap` interface, so that is may be used with `shaping.SplitByFace`. 19 | 20 | ## Zoom on the implementation 21 | 22 | ### Font directories 23 | 24 | Fonts are searched by walking the file system, in the folders returned by `DefaultFontDirectories`, which are platform dependent. 25 | The current list is copied from [fontconfig](https://gitlab.freedesktop.org/fontconfig/fontconfig) and [go-findfont](github.com/flopp/go-findfont). 26 | 27 | ### Font family substitutions 28 | 29 | A key concept of the implementation (inspired by [fontconfig](https://gitlab.freedesktop.org/fontconfig/fontconfig)) is the idea to enlarge the requested family with similar known families. 30 | This ensure that suitable font fallbacks may be provided even if the required font is not available. 31 | It is implemented by a list of susbtitutions, each of them having a test and a list of additions. 32 | 33 | Simplified example : if the list of susbtitutions is 34 | 35 | - Test: the input family is Arial, Addition: Arimo 36 | - Test: the input family is Arimo, Addition: sans-serif 37 | - Test: the input family is sans-serif, Addition: DejaVu Sans et Verdana 38 | 39 | then, 40 | 41 | - for the Arimo input family, [Arimo, sans-serif, DejaVu Sans, Verdana] would be matched 42 | - for the Arial input family, [Arial, Arimo, sans-serif, DejaVu Sans, Verdana] would be matched 43 | 44 | To respect the user request, the order of the list is significant (first entries have higher priority). 45 | 46 | `FontMap.SetQuery` apply a list of hard-coded subsitutions, extracted from 47 | Fontconfig configurations files. 48 | 49 | ### Style matching 50 | 51 | `FontMap.SetQuery` takes an optional argument describing the style of 52 | the required font (style, weight, stretchiness). 53 | 54 | When no exact match is found, the [CSS font selection rules](https://drafts.csswg.org/css-fonts/#font-prop) are applied to return the closest match. 55 | As an example, if the user asks for `(Italic, ExtraBold)` but only `(Normal, Bold)` and `(Oblique, Bold)` 56 | are available, the `(Oblique, Bold)` would be returned. 57 | 58 | ### System font index 59 | 60 | The `FontMap` type requires more information than the font paths to be able to quickly and accurately 61 | match a font against family, aspect, and rune coverage query. This information is provided by a list of font summaries, 62 | which are lightweight enough to be loaded and queried efficiently. 63 | 64 | The initial scan required to build this index has a significant latency (say between 0.2 and 0.5 sec on a laptop). 65 | Once the first scan has been done, however, the subsequent launches are fast : at the first call of `UseSystemFonts`, the index is loaded from an on-disk cache, and its integrity is checked against the 66 | current file system state to detect font installation or suppression. 67 | -------------------------------------------------------------------------------- /fontscan/scan_test.go: -------------------------------------------------------------------------------- 1 | package fontscan 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | "time" 11 | 12 | tu "github.com/go-text/typesetting/testutils" 13 | ) 14 | 15 | func TestDefaultDirs(t *testing.T) { 16 | logger := log.New(io.Discard, "", 0) 17 | dirs, err := DefaultFontDirectories(logger) 18 | tu.AssertNoErr(t, err) 19 | fmt.Printf("Valid font directories:\n%v\n", dirs) 20 | } 21 | 22 | func TestScanFontFootprints(t *testing.T) { 23 | ti := time.Now() 24 | 25 | logger := log.New(io.Discard, "", 0) 26 | directories, err := DefaultFontDirectories(logger) 27 | tu.AssertNoErr(t, err) 28 | 29 | fontset, err := scanFontFootprints(logger, nil, directories...) 30 | tu.AssertNoErr(t, err) 31 | 32 | // Show some basic stats 33 | families := map[string]bool{} 34 | for _, font := range fontset.flatten() { 35 | if font.Runes.Len() == 0 { 36 | t.Fatalf("unexpected empty rune coverage for %s", font.Location.File) 37 | } 38 | families[font.Family] = true 39 | } 40 | 41 | fmt.Printf("Found %d fonts (%d families) in %s\n", 42 | len(fontset), len(families), time.Since(ti)) 43 | } 44 | 45 | func BenchmarkScanFonts(b *testing.B) { 46 | logger := log.New(io.Discard, "", 0) 47 | directories, err := DefaultFontDirectories(logger) 48 | tu.AssertNoErr(b, err) 49 | 50 | for i := 0; i < b.N; i++ { 51 | _, _ = scanFontFootprints(logger, nil, directories...) 52 | } 53 | } 54 | 55 | func TestScanIncrementalNoOp(t *testing.T) { 56 | ti := time.Now() 57 | 58 | logger := log.New(io.Discard, "", 0) 59 | directories, err := DefaultFontDirectories(logger) 60 | tu.AssertNoErr(t, err) 61 | 62 | // first scan 63 | fontset, err := scanFontFootprints(logger, nil, directories...) 64 | tu.AssertNoErr(t, err) 65 | fmt.Printf("Initial scan time: %s\n", time.Since(ti)) 66 | 67 | ti = time.Now() 68 | incremental, err := scanFontFootprints(logger, fontset, directories...) 69 | tu.AssertNoErr(t, err) 70 | fmt.Printf("Second scan time: %s\n", time.Since(ti)) 71 | 72 | if err = assertFontsetEquals(fontset.flatten(), incremental.flatten()); err != nil { 73 | t.Fatalf("incremental scan not consistent with initial scan: %s", err) 74 | } 75 | } 76 | 77 | func copyFile(t *testing.T, srcName, dstName string) { 78 | t.Helper() 79 | 80 | dst, err := os.Create(dstName) 81 | tu.AssertNoErr(t, err) 82 | 83 | src, err := os.Open(srcName) 84 | tu.AssertNoErr(t, err) 85 | defer src.Close() 86 | 87 | _, err = io.Copy(dst, src) 88 | tu.AssertNoErr(t, err) 89 | if err = dst.Sync(); err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | if err = dst.Close(); err != nil { 94 | t.Fatal(err) 95 | } 96 | } 97 | 98 | func TestScanIncrementalUpdate(t *testing.T) { 99 | dir := t.TempDir() 100 | copyFile(t, filepath.Join("..", "font", "testdata", "Amiri-Regular.ttf"), filepath.Join(dir, "font1.ttf")) 101 | 102 | // first scan 103 | logger := log.New(io.Discard, "", 0) 104 | fontset, err := scanFontFootprints(logger, nil, dir) 105 | tu.AssertNoErr(t, err) 106 | if len(fontset) != 1 { 107 | t.Fatalf("unexpected font set: %v", fontset) 108 | } 109 | 110 | time.Sleep(time.Millisecond * 10) 111 | 112 | // test adding a new file 113 | copyFile(t, filepath.Join("..", "font", "testdata", "Roboto-Regular.ttf"), filepath.Join(dir, "font2.ttf")) 114 | 115 | fontset2, err := scanFontFootprints(logger, fontset, dir) 116 | tu.AssertNoErr(t, err) 117 | if len(fontset2) != 2 { 118 | t.Fatalf("unexpected font set: %v", fontset) 119 | } 120 | 121 | time.Sleep(time.Millisecond * 10) 122 | 123 | // test updating an existing file 124 | copyFile(t, filepath.Join("..", "font", "testdata", "Roboto-Regular.ttf"), filepath.Join(dir, "font1.ttf")) 125 | 126 | fontset3, err := scanFontFootprints(logger, nil, dir) 127 | tu.AssertNoErr(t, err) 128 | if len(fontset3) != 2 { 129 | t.Fatalf("unexpected font set: %v", fontset) 130 | } 131 | if family := fontset3.flatten()[0].Family; family != "roboto" { 132 | t.Fatalf("unexpected family %s", family) 133 | } 134 | 135 | time.Sleep(time.Millisecond * 10) 136 | 137 | incremental, err := scanFontFootprints(logger, fontset2, dir) 138 | tu.AssertNoErr(t, err) 139 | if err = assertFontsetEquals(fontset3.flatten(), incremental.flatten()); err != nil { 140 | t.Fatalf("incremental scan not consistent with initial scan: %s", err) 141 | } 142 | 143 | // test removing a file 144 | if err = os.Remove(filepath.Join(dir, "font1.ttf")); err != nil { 145 | t.Fatal(err) 146 | } 147 | fontset4, err := scanFontFootprints(logger, fontset3, dir) 148 | tu.AssertNoErr(t, err) 149 | if len(fontset4) != 1 { 150 | t.Fatalf("unexpected font set: %v", fontset) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /fontscan/scandir.go: -------------------------------------------------------------------------------- 1 | package fontscan 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // recursively walk through the given directory, scanning font files and calling dst.consume 10 | // for each valid file found. 11 | func (dst *footprintScanner) scanDirectory(logger Logger, dir string, visited map[string]bool) error { 12 | walkFn := func(path string, d fs.DirEntry, err error) error { 13 | if err != nil { 14 | logger.Printf("error walking font directory %q: %v", path, err) 15 | return filepath.SkipDir 16 | } 17 | 18 | if d.IsDir() { // keep going 19 | return nil 20 | } 21 | 22 | if visited[path] { 23 | return nil // skip the path 24 | } 25 | visited[path] = true 26 | 27 | // load the information, following potential symoblic links 28 | info, err := os.Stat(path) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | // always ignore files which should never be font files 34 | if ignoreFontFile(info.Name()) { 35 | return nil 36 | } 37 | 38 | err = dst.consume(path, info) 39 | 40 | return err 41 | } 42 | 43 | err := filepath.WalkDir(dir, walkFn) 44 | 45 | return err 46 | } 47 | 48 | type dirEntry = fs.DirEntry 49 | 50 | func readDir(name string) ([]dirEntry, error) { 51 | return os.ReadDir(name) 52 | } 53 | -------------------------------------------------------------------------------- /fontscan/serialize_test.go: -------------------------------------------------------------------------------- 1 | package fontscan 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "log" 9 | "math/rand" 10 | "reflect" 11 | "testing" 12 | "time" 13 | 14 | "github.com/go-text/typesetting/font" 15 | "github.com/go-text/typesetting/language" 16 | ) 17 | 18 | func Test_serializeFootprints(t *testing.T) { 19 | input := []Footprint{ 20 | { 21 | Family: "a strange one", 22 | Runes: newRuneSet(1, 0, 2, 0x789, 0xfffee), 23 | Scripts: ScriptSet{0, 1, 5, 0xffffff, language.Nabataean, language.Unknown}, 24 | Aspect: font.Aspect{Style: 1, Weight: 200, Stretch: 0.45}, 25 | }, 26 | { 27 | Runes: RuneSet{}, 28 | Scripts: ScriptSet{}, 29 | }, 30 | } 31 | dump := serializeFootprintsTo(input, nil) 32 | 33 | got, err := deserializeFootprints(dump) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | if !reflect.DeepEqual(input, got) { 39 | t.Fatalf("expected %v, got %v", input, got) 40 | } 41 | } 42 | 43 | // Test_serializeEmpty ensures that serializing an empty index is safe. 44 | func Test_serializeEmpty(t *testing.T) { 45 | input := []Footprint{} 46 | dump := serializeFootprintsTo(input, nil) 47 | 48 | got, err := deserializeFootprints(dump) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | if len(input) != len(got) { 54 | t.Errorf("expected %d footprints, got %d", len(input), len(got)) 55 | } 56 | } 57 | 58 | func assertFontsetEquals(expected, got []Footprint) error { 59 | if len(expected) != len(got) { 60 | return fmt.Errorf("invalid length: expected %d, got %d", len(expected), len(got)) 61 | } 62 | for i := range got { 63 | expectedFootprint, gotFootprint := expected[i], got[i] 64 | if !reflect.DeepEqual(expectedFootprint, gotFootprint) { 65 | return fmt.Errorf("expected Footprint \n %v \n got \n %v", expectedFootprint, gotFootprint) 66 | } 67 | } 68 | return nil 69 | } 70 | 71 | func TestSerializeDeserialize(t *testing.T) { 72 | for _, fp := range []Footprint{ 73 | { 74 | Family: "a strange one", 75 | Runes: newRuneSet(1, 0, 2, 0x789, 0xfffee), 76 | Scripts: ScriptSet{0, 1, 5, 0xffffff}, 77 | Aspect: font.Aspect{Style: 1, Weight: 200, Stretch: 0.45}, 78 | }, 79 | { 80 | Runes: RuneSet{}, 81 | Scripts: ScriptSet{}, 82 | }, 83 | } { 84 | b := fp.serializeTo(nil) 85 | 86 | var got Footprint 87 | n, err := got.deserializeFrom(b) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | if n != len(b) { 92 | t.Fatalf("unexpected number of bytes read: %d", n) 93 | } 94 | 95 | if !reflect.DeepEqual(got, fp) { 96 | t.Fatalf("unexepected Footprint: %v, expected %v", got, fp) 97 | } 98 | } 99 | } 100 | 101 | func randomBytes() []byte { 102 | out := make([]byte, 1000) 103 | rand.Read(out) 104 | return out 105 | } 106 | 107 | func TestDeserializeInvalid(t *testing.T) { 108 | for range [50]int{} { 109 | src := randomBytes() 110 | if rand.Intn(2) == 0 { // indicate a small string 111 | binary.BigEndian.PutUint16(src, 10) 112 | } 113 | if rand.Intn(2) == 0 { // indicate no string and no rune set 114 | binary.BigEndian.PutUint16(src, 0) 115 | binary.BigEndian.PutUint32(src[2:], 0) 116 | src = src[:8] // truncate to simulate a broken input 117 | } 118 | var fp Footprint 119 | _, err := fp.deserializeFrom(src) 120 | if err == nil { 121 | t.Fatal("expected error on random input") 122 | } 123 | } 124 | } 125 | 126 | func TestSerializeSystemFonts(t *testing.T) { 127 | logger := log.New(io.Discard, "", 0) 128 | directories, err := DefaultFontDirectories(logger) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | fontset, err := scanFontFootprints(logger, nil, directories...) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | 138 | ti := time.Now() 139 | var b bytes.Buffer 140 | err = fontset.serializeTo(&b) 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | fmt.Printf("%d fonts serialized (into memory) in %s; size: %dKB\n", len(fontset), time.Since(ti), b.Len()/1000) 145 | 146 | fontset2, err := deserializeIndex(&b) 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | if err = assertFontsetEquals(fontset.flatten(), fontset2.flatten()); err != nil { 151 | t.Fatalf("inconsistent serialization %s", err) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-text/typesetting 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-text/typesetting-utils v0.0.0-20250527170436-63e4acdcf075 7 | golang.org/x/image v0.23.0 8 | golang.org/x/text v0.21.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-text/typesetting-utils v0.0.0-20250527170436-63e4acdcf075 h1:zRaPuzKe/+Euzz3WwE0YwjXAX4IwvGPRH6abtmRI/4M= 2 | github.com/go-text/typesetting-utils v0.0.0-20250527170436-63e4acdcf075/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= 3 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 4 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 5 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 6 | golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= 7 | golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= 8 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 9 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 10 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 11 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 12 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 13 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 14 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 15 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 16 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 17 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 18 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 19 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 20 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 26 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 27 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 28 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 29 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 30 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 31 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 32 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 33 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 34 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 35 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 36 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 37 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 38 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 39 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 40 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 41 | -------------------------------------------------------------------------------- /harfbuzz/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 B. KUGLER 4 | Copyright © 2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020 Google, Inc. 5 | Copyright © 2018,2019,2020 Ebrahim Byagowi 6 | Copyright © 2019,2020 Facebook, Inc. 7 | Copyright © 2012 Mozilla Foundation 8 | Copyright © 2011 Codethink Limited 9 | Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) 10 | Copyright © 2009 Keith Stribley 11 | Copyright © 2009 Martin Hosken and SIL International 12 | Copyright © 2007 Chris Wilson 13 | Copyright © 2006 Behdad Esfahbod 14 | Copyright © 2005 David Turner 15 | Copyright © 2004,2007,2008,2009,2010 Red Hat, Inc. 16 | Copyright © 1998-2004 David Turner and Werner Lemberg 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to deal 20 | in the Software without restriction, including without limitation the rights 21 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in all 26 | copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | SOFTWARE. -------------------------------------------------------------------------------- /harfbuzz/buffer_verify_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Ported from src/hb-buffer-verify.cc 9 | 10 | func (b *Buffer) verifyValidGID(font *Font) error { 11 | for _, glyph := range b.Info { 12 | _, ok := font.GlyphExtents(glyph.Glyph) 13 | if !ok { 14 | return fmt.Errorf("Unknow glyph %d in font", glyph.Glyph) 15 | } 16 | } 17 | return nil 18 | } 19 | 20 | // check that clusters are monotone. 21 | func (b *Buffer) verifyMonotone() error { 22 | if b.ClusterLevel == MonotoneGraphemes || b.ClusterLevel == MonotoneCharacters { 23 | isForward := b.Props.Direction.isForward() 24 | 25 | info := b.Info 26 | 27 | for i := 1; i < len(info); i++ { 28 | if info[i-1].Cluster != info[i].Cluster && (info[i-1].Cluster < info[i].Cluster) != isForward { 29 | return fmt.Errorf("cluster at index %d is not monotone", i) 30 | } 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | 37 | func (b *Buffer) showRunes() string { 38 | var s strings.Builder 39 | for _, r := range b.Info { 40 | fmt.Fprintf(&s, "U+%04X(at:%d),", r.codepoint, r.Cluster) 41 | } 42 | return s.String() 43 | } 44 | 45 | func (b *Buffer) showGIDs() string { 46 | var s strings.Builder 47 | for _, r := range b.Info { 48 | fmt.Fprintf(&s, "%d,", r.Glyph) 49 | } 50 | return s.String() 51 | } 52 | 53 | func (b *Buffer) verifyUnsafeToBreak(textBuffer *Buffer, font *Font, features []Feature) error { 54 | if b.ClusterLevel != MonotoneGraphemes && b.ClusterLevel != MonotoneCharacters { 55 | /* Cannot perform this check without monotone clusters. */ 56 | return nil 57 | } 58 | 59 | /* Check that breaking up shaping at safe-to-break is indeed safe. */ 60 | 61 | fragment, reconstruction := NewBuffer(), NewBuffer() 62 | copyBufferProperties(reconstruction, b) 63 | 64 | info := b.Info 65 | text := textBuffer.Info 66 | 67 | /* Chop text and shape fragments. */ 68 | forward := b.Props.Direction.isForward() 69 | start := 0 70 | textStart := len(textBuffer.Info) 71 | if forward { 72 | textStart = 0 73 | } 74 | textEnd := textStart 75 | for end := 1; end < len(b.Info)+1; end++ { 76 | offset := 1 77 | if forward { 78 | offset = 0 79 | } 80 | if end < len(b.Info) && (info[end].Cluster == info[end-1].Cluster || 81 | info[end-offset].Mask&GlyphUnsafeToBreak != 0) { 82 | continue 83 | } 84 | 85 | /* Shape segment corresponding to glyphs start..end. */ 86 | if end == len(b.Info) { 87 | if forward { 88 | textEnd = len(textBuffer.Info) 89 | } else { 90 | textStart = 0 91 | } 92 | } else { 93 | if forward { 94 | cluster := info[end].Cluster 95 | for textEnd < len(textBuffer.Info) && text[textEnd].Cluster < cluster { 96 | textEnd++ 97 | } 98 | } else { 99 | cluster := info[end-1].Cluster 100 | for textStart != 0 && text[textStart-1].Cluster >= cluster { 101 | textStart-- 102 | } 103 | } 104 | } 105 | if !(textStart < textEnd) { 106 | return fmt.Errorf("unexpected %d >= %d", textStart, textEnd) 107 | } 108 | 109 | if debugMode { 110 | fmt.Println() 111 | fmt.Printf("VERIFY SAFE TO BREAK : start %d end %d text start %d end %d\n", start, end, textStart, textEnd) 112 | fmt.Println() 113 | } 114 | 115 | fragment.Clear() 116 | copyBufferProperties(fragment, b) 117 | 118 | flags := fragment.Flags 119 | if 0 < textStart { 120 | flags = (flags & ^Bot) 121 | } 122 | if textEnd < len(textBuffer.Info) { 123 | flags = (flags & ^Eot) 124 | } 125 | fragment.Flags = flags 126 | 127 | appendBuffer(fragment, textBuffer, textStart, textEnd) 128 | fragment.Shape(font, features) 129 | appendBuffer(reconstruction, fragment, 0, len(fragment.Info)) 130 | 131 | start = end 132 | if forward { 133 | textStart = textEnd 134 | } else { 135 | textEnd = textStart 136 | } 137 | } 138 | 139 | diff := bufferDiff(reconstruction, b, ^GID(0), 0) 140 | if diff & ^bdfGlyphFlagsMismatch != 0 { 141 | return fmt.Errorf("unsafe-to-break test failed: %b (%s -> %s)", diff, b.showRunes(), b.showGIDs()) 142 | } 143 | 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /harfbuzz/emojis_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestEmojisSequences(t *testing.T) { 10 | for _, sequence := range emojisSequences { 11 | var runes []string 12 | for _, r := range sequence { 13 | runes = append(runes, fmt.Sprintf("U+%X", r)) 14 | } 15 | clusters := strings.Repeat("|1=0", len(sequence))[1:] 16 | test := fmt.Sprintf("fonts/AdobeBlank2.ttf;--no-glyph-names --no-positions;%s;[%s]", strings.Join(runes, ","), clusters) 17 | 18 | testD := newTestData(t, ".", test) 19 | runShapingTest(t, testD, false) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /harfbuzz/ot_aat_layout_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/go-text/typesetting/font/opentype/tables" 8 | tu "github.com/go-text/typesetting/testutils" 9 | ) 10 | 11 | // ported from harfbuzz/test/api/test-aat-layout.c Copyright © 2018 Ebrahim Byagowi 12 | 13 | func TestAATFeaturesSorted(t *testing.T) { 14 | var tags []int 15 | for _, f := range featureMappings { 16 | tags = append(tags, int(f.otFeatureTag)) 17 | } 18 | if !sort.IntsAreSorted(tags) { 19 | t.Fatalf("expected sorted tags, got %v", tags) 20 | } 21 | } 22 | 23 | func aatLayoutGetFeatureTypes(feat tables.Feat) []aatLayoutFeatureType { 24 | out := make([]aatLayoutFeatureType, len(feat.Names)) 25 | for i, f := range feat.Names { 26 | out[i] = f.Feature 27 | } 28 | return out 29 | } 30 | 31 | func aatLayoutFeatureTypeGetNameID(feat tables.Feat, feature uint16) int { 32 | if f := feat.GetFeature(feature); f != nil { 33 | return int(f.NameIndex) 34 | } 35 | return -1 36 | } 37 | 38 | func TestAatGetFeatureTypes(t *testing.T) { 39 | feat := openFontFile(t, "fonts/aat-feat.ttf").Feat 40 | 41 | features := aatLayoutGetFeatureTypes(feat) 42 | assertEqualInt(t, 11, len(feat.Names)) 43 | 44 | assertEqualInt(t, 1, int(features[0])) 45 | assertEqualInt(t, 3, int(features[1])) 46 | assertEqualInt(t, 6, int(features[2])) 47 | 48 | assertEqualInt(t, 258, aatLayoutFeatureTypeGetNameID(feat, features[0])) 49 | assertEqualInt(t, 261, aatLayoutFeatureTypeGetNameID(feat, features[1])) 50 | assertEqualInt(t, 265, aatLayoutFeatureTypeGetNameID(feat, features[2])) 51 | } 52 | 53 | func TestAatHas(t *testing.T) { 54 | morx := openFontFile(t, "fonts/aat-morx.ttf") 55 | 56 | tu.Assert(t, len(morx.Morx) != 0) 57 | 58 | trak := openFontFile(t, "fonts/aat-trak.ttf") 59 | tu.Assert(t, !trak.Trak.IsEmpty()) 60 | } 61 | -------------------------------------------------------------------------------- /harfbuzz/ot_arabic_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-text/typesetting/language" 7 | ) 8 | 9 | func TestNumArabicLookup(t *testing.T) { 10 | if len(arabicFallbackFeatures) > arabicFallbackMaxLookups { 11 | t.Error() 12 | } 13 | } 14 | 15 | func TestHasArabicJoining(t *testing.T) { 16 | if !hasArabicJoining(language.Arabic) { 17 | t.Fatal() 18 | } 19 | if hasArabicJoining(language.Linear_A) { 20 | t.Fatal() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /harfbuzz/ot_arabic_win1256.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "github.com/go-text/typesetting/font" 5 | ot "github.com/go-text/typesetting/font/opentype" 6 | "github.com/go-text/typesetting/font/opentype/tables" 7 | ) 8 | 9 | // ported from harfbuzz/src/hb-ot-shape-complex-arabic-win1256.hh Copyright © 2014 Google, Inc. Behdad Esfahbod 10 | 11 | type manifest struct { 12 | lookup *lookupGSUB 13 | tag tables.Tag 14 | } 15 | 16 | var arabicWin1256GsubLookups = [...]manifest{ 17 | {&rligLookup, ot.NewTag('r', 'l', 'i', 'g')}, 18 | {&initLookup, ot.NewTag('i', 'n', 'i', 't')}, 19 | {&mediLookup, ot.NewTag('m', 'e', 'd', 'i')}, 20 | {&finaLookup, ot.NewTag('f', 'i', 'n', 'a')}, 21 | {&rligMarksLookup, ot.NewTag('r', 'l', 'i', 'g')}, 22 | } 23 | 24 | // Lookups 25 | var ( 26 | initLookup = lookupGSUB{ 27 | LookupOptions: font.LookupOptions{Flag: otIgnoreMarks}, 28 | Subtables: []tables.GSUBLookup{ 29 | initmediSubLookup, 30 | initSubLookup, 31 | }, 32 | } 33 | mediLookup = lookupGSUB{ 34 | LookupOptions: font.LookupOptions{Flag: otIgnoreMarks}, 35 | Subtables: []tables.GSUBLookup{ 36 | initmediSubLookup, 37 | mediSubLookup, 38 | medifinaLamAlefSubLookup, 39 | }, 40 | } 41 | finaLookup = lookupGSUB{ 42 | LookupOptions: font.LookupOptions{Flag: otIgnoreMarks}, 43 | Subtables: []tables.GSUBLookup{ 44 | finaSubLookup, 45 | /* We don't need this one currently as the sequence inherits masks 46 | * from the first item. Just in case we change that in the future 47 | * to be smart about Arabic masks when ligating... */ 48 | medifinaLamAlefSubLookup, 49 | }, 50 | } 51 | rligLookup = lookupGSUB{ 52 | LookupOptions: font.LookupOptions{Flag: otIgnoreMarks}, 53 | Subtables: []tables.GSUBLookup{lamAlefLigaturesSubLookup}, 54 | } 55 | rligMarksLookup = lookupGSUB{ 56 | Subtables: []tables.GSUBLookup{shaddaLigaturesSubLookup}, 57 | } 58 | ) 59 | 60 | // init/medi/fina forms 61 | var ( 62 | initmediSubLookup = tables.SingleSubs{Data: tables.SingleSubstData2{ 63 | Coverage: tables.Coverage1{Glyphs: []gID{198, 200, 201, 202, 203, 204, 205, 206, 211, 212, 213, 214, 223, 225, 227, 228, 236, 237}}, 64 | SubstituteGlyphIDs: []gID{162, 4, 5, 5, 6, 7, 9, 11, 13, 14, 15, 26, 140, 141, 142, 143, 154, 154}, 65 | }} 66 | initSubLookup = tables.SingleSubs{Data: tables.SingleSubstData2{ 67 | Coverage: tables.Coverage1{Glyphs: []gID{218, 219, 221, 222, 229}}, 68 | SubstituteGlyphIDs: []gID{27, 30, 128, 131, 144}, 69 | }} 70 | mediSubLookup = tables.SingleSubs{Data: tables.SingleSubstData2{ 71 | Coverage: tables.Coverage1{Glyphs: []gID{218, 219, 221, 222, 229}}, 72 | SubstituteGlyphIDs: []gID{28, 31, 129, 138, 149}, 73 | }} 74 | finaSubLookup = tables.SingleSubs{Data: tables.SingleSubstData2{ 75 | Coverage: tables.Coverage1{Glyphs: []gID{194, 195, 197, 198, 199, 201, 204, 205, 206, 218, 219, 229, 236, 237}}, 76 | SubstituteGlyphIDs: []gID{2, 1, 3, 181, 0, 159, 8, 10, 12, 29, 127, 152, 160, 156}, 77 | }} 78 | medifinaLamAlefSubLookup = tables.SingleSubs{Data: tables.SingleSubstData2{ 79 | Coverage: tables.Coverage1{Glyphs: []gID{165, 178, 180, 252}}, 80 | SubstituteGlyphIDs: []gID{170, 179, 185, 255}, 81 | }} 82 | ) 83 | 84 | type ligs = []tables.Ligature 85 | 86 | var ( 87 | // Lam+Alef ligatures 88 | lamAlefLigaturesSubLookup = tables.LigatureSubs{ 89 | Coverage: tables.Coverage1{Glyphs: []gID{225}}, 90 | LigatureSets: []tables.LigatureSet{{Ligatures: lamLigatureSet}}, 91 | } 92 | lamLigatureSet = ligs{ 93 | { 94 | LigatureGlyph: 199, 95 | ComponentGlyphIDs: []uint16{165}, 96 | }, 97 | { 98 | LigatureGlyph: 195, 99 | ComponentGlyphIDs: []uint16{178}, 100 | }, 101 | { 102 | LigatureGlyph: 194, 103 | ComponentGlyphIDs: []uint16{180}, 104 | }, 105 | { 106 | LigatureGlyph: 197, 107 | ComponentGlyphIDs: []uint16{252}, 108 | }, 109 | } 110 | 111 | // Shadda ligatures 112 | shaddaLigaturesSubLookup = tables.LigatureSubs{ 113 | Coverage: tables.Coverage1{Glyphs: []gID{248}}, 114 | LigatureSets: []tables.LigatureSet{{Ligatures: shaddaLigatureSet}}, 115 | } 116 | shaddaLigatureSet = ligs{ 117 | { 118 | LigatureGlyph: 243, 119 | ComponentGlyphIDs: []uint16{172}, 120 | }, 121 | { 122 | LigatureGlyph: 245, 123 | ComponentGlyphIDs: []uint16{173}, 124 | }, 125 | { 126 | LigatureGlyph: 246, 127 | ComponentGlyphIDs: []uint16{175}, 128 | }, 129 | } 130 | ) 131 | -------------------------------------------------------------------------------- /harfbuzz/ot_hebrew.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | ot "github.com/go-text/typesetting/font/opentype" 5 | "github.com/go-text/typesetting/font/opentype/tables" 6 | ) 7 | 8 | // ported from harfbuzz/src/hb-ot-shape-complex-hebrew.cc Copyright © 2010,2012 Google, Inc. Behdad Esfahbod 9 | 10 | var _ otComplexShaper = complexShaperHebrew{} 11 | 12 | type complexShaperHebrew struct { 13 | complexShaperNil 14 | } 15 | 16 | /* Hebrew presentation-form shaping. 17 | * https://bugzilla.mozilla.org/show_bug.cgi?id=728866 18 | * Hebrew presentation forms with dagesh, for characters U+05D0..05EA; 19 | * Note that some letters do not have a dagesh presForm encoded. */ 20 | var sDageshForms = [0x05EA - 0x05D0 + 1]rune{ 21 | 0xFB30, /* ALEF */ 22 | 0xFB31, /* BET */ 23 | 0xFB32, /* GIMEL */ 24 | 0xFB33, /* DALET */ 25 | 0xFB34, /* HE */ 26 | 0xFB35, /* VAV */ 27 | 0xFB36, /* ZAYIN */ 28 | 0x0000, /* HET */ 29 | 0xFB38, /* TET */ 30 | 0xFB39, /* YOD */ 31 | 0xFB3A, /* FINAL KAF */ 32 | 0xFB3B, /* KAF */ 33 | 0xFB3C, /* LAMED */ 34 | 0x0000, /* FINAL MEM */ 35 | 0xFB3E, /* MEM */ 36 | 0x0000, /* FINAL NUN */ 37 | 0xFB40, /* NUN */ 38 | 0xFB41, /* SAMEKH */ 39 | 0x0000, /* AYIN */ 40 | 0xFB43, /* FINAL PE */ 41 | 0xFB44, /* PE */ 42 | 0x0000, /* FINAL TSADI */ 43 | 0xFB46, /* TSADI */ 44 | 0xFB47, /* QOF */ 45 | 0xFB48, /* RESH */ 46 | 0xFB49, /* SHIN */ 47 | 0xFB4A, /* TAV */ 48 | } 49 | 50 | func (complexShaperHebrew) compose(c *otNormalizeContext, a, b rune) (rune, bool) { 51 | ab, found := uni.compose(a, b) 52 | 53 | if !found && !c.plan.hasGposMark { 54 | /* Special-case Hebrew presentation forms that are excluded from 55 | * standard normalization, but wanted for old fonts. */ 56 | switch b { 57 | case 0x05B4: /* HIRIQ */ 58 | if a == 0x05D9 { /* YOD */ 59 | return 0xFB1D, true 60 | } 61 | case 0x05B7: /* PATAH */ 62 | if a == 0x05F2 { /* YIDDISH YOD YOD */ 63 | return 0xFB1F, true 64 | } else if a == 0x05D0 { /* ALEF */ 65 | return 0xFB2E, true 66 | } 67 | case 0x05B8: /* QAMATS */ 68 | if a == 0x05D0 { /* ALEF */ 69 | return 0xFB2F, true 70 | } 71 | case 0x05B9: /* HOLAM */ 72 | if a == 0x05D5 { /* VAV */ 73 | return 0xFB4B, true 74 | } 75 | case 0x05BC: /* DAGESH */ 76 | if a >= 0x05D0 && a <= 0x05EA { 77 | ab = sDageshForms[a-0x05D0] 78 | return ab, ab != 0 79 | } else if a == 0xFB2A { /* SHIN WITH SHIN DOT */ 80 | return 0xFB2C, true 81 | } else if a == 0xFB2B { /* SHIN WITH SIN DOT */ 82 | return 0xFB2D, true 83 | } 84 | case 0x05BF: /* RAFE */ 85 | switch a { 86 | case 0x05D1: /* BET */ 87 | return 0xFB4C, true 88 | case 0x05DB: /* KAF */ 89 | return 0xFB4D, true 90 | case 0x05E4: /* PE */ 91 | return 0xFB4E, true 92 | } 93 | case 0x05C1: /* SHIN DOT */ 94 | if a == 0x05E9 { /* SHIN */ 95 | return 0xFB2A, true 96 | } else if a == 0xFB49 { /* SHIN WITH DAGESH */ 97 | return 0xFB2C, true 98 | } 99 | case 0x05C2: /* SIN DOT */ 100 | if a == 0x05E9 { /* SHIN */ 101 | return 0xFB2B, true 102 | } else if a == 0xFB49 { /* SHIN WITH DAGESH */ 103 | return 0xFB2D, true 104 | } 105 | } 106 | } 107 | 108 | return ab, found 109 | } 110 | 111 | func (complexShaperHebrew) marksBehavior() (zeroWidthMarks, bool) { 112 | return zeroWidthMarksByGdefLate, true 113 | } 114 | 115 | func (complexShaperHebrew) normalizationPreference() normalizationMode { 116 | return nmDefault 117 | } 118 | 119 | func (complexShaperHebrew) gposTag() tables.Tag { 120 | // https://github.com/harfbuzz/harfbuzz/issues/347#issuecomment-267838368 121 | return ot.NewTag('h', 'e', 'b', 'r') 122 | } 123 | 124 | func (complexShaperHebrew) reorderMarks(_ *otShapePlan, buffer *Buffer, start, end int) { 125 | info := buffer.Info 126 | 127 | for i := start + 2; i < end; i++ { 128 | c0 := info[i-2].getModifiedCombiningClass() 129 | c1 := info[i-1].getModifiedCombiningClass() 130 | c2 := info[i-0].getModifiedCombiningClass() 131 | 132 | if (c0 == mcc17 || c0 == mcc18) /* patach or qamats */ && 133 | (c1 == mcc10 || c1 == mcc14) /* sheva or hiriq */ && 134 | (c2 == mcc22 || c2 == combiningClassBelow) /* meteg or below */ { 135 | buffer.mergeClusters(i-1, i+1) 136 | info[i-1], info[i] = info[i], info[i-1] // swap 137 | break 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /harfbuzz/ot_indic_machine.rl: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | // Code generated with ragel -Z -o ot_indic_machine.go ot_indic_machine.rl ; sed -i '/^\/\/line/ d' ot_indic_machine.go ; goimports -w ot_indic_machine.go DO NOT EDIT. 4 | 5 | // ported from harfbuzz/src/hb-ot-shape-complex-indic-machine.rl Copyright © 2015 Google, Inc. Behdad Esfahbod 6 | 7 | // indic_syllable_type_t 8 | const ( 9 | indicConsonantSyllable = iota 10 | indicVowelSyllable 11 | indicStandaloneCluster 12 | indicSymbolCluster 13 | indicBrokenCluster 14 | indicNonIndicCluster 15 | ) 16 | 17 | %%{ 18 | machine indSM; 19 | alphtype byte; 20 | write exports; 21 | write data; 22 | }%% 23 | 24 | %%{ 25 | 26 | 27 | export X = 0; 28 | export C = 1; 29 | export V = 2; 30 | export N = 3; 31 | export H = 4; 32 | export ZWNJ = 5; 33 | export ZWJ = 6; 34 | export M = 7; 35 | export SM = 8; 36 | export A = 9; 37 | export VD = 9; 38 | export PLACEHOLDER = 10; 39 | export DOTTEDCIRCLE = 11; 40 | export RS = 12; 41 | export MPst = 13; 42 | export Repha = 14; 43 | export Ra = 15; 44 | export CM = 16; 45 | export Symbol= 17; 46 | export CS = 18; 47 | 48 | c = (C | Ra); # is_consonant 49 | n = ((ZWNJ?.RS)? (N.N?)?); # is_consonant_modifier 50 | z = ZWJ|ZWNJ; # is_joiner 51 | reph = (Ra H | Repha); # possible reph 52 | 53 | cn = c.ZWJ?.n?; 54 | symbol = Symbol.N?; 55 | matra_group = z*.(M | SM? MPst).N?.H?; 56 | syllable_tail = (z?.SM.SM?.ZWNJ?)? (A | VD)*; 57 | halant_group = (z?.H.(ZWJ.N?)?); 58 | final_halant_group = halant_group | H.ZWNJ; 59 | medial_group = CM?; 60 | halant_or_matra_group = (final_halant_group | matra_group*); 61 | 62 | complex_syllable_tail = (halant_group.cn)* medial_group halant_or_matra_group syllable_tail; 63 | 64 | consonant_syllable = (Repha|CS)? cn complex_syllable_tail; 65 | vowel_syllable = reph? V.n? (ZWJ | complex_syllable_tail); 66 | standalone_cluster = ((Repha|CS)? PLACEHOLDER | reph? DOTTEDCIRCLE).n? complex_syllable_tail; 67 | symbol_cluster = symbol syllable_tail; 68 | broken_cluster = reph? n? complex_syllable_tail; 69 | other = any; 70 | 71 | main := |* 72 | consonant_syllable => { foundSyllableIndic (indicConsonantSyllable,ts, te, info, &syllableSerial); }; 73 | vowel_syllable => { foundSyllableIndic (indicVowelSyllable,ts, te, info, &syllableSerial); }; 74 | standalone_cluster => { foundSyllableIndic (indicStandaloneCluster,ts, te, info, &syllableSerial); }; 75 | symbol_cluster => { foundSyllableIndic (indicSymbolCluster,ts, te, info, &syllableSerial); }; 76 | broken_cluster => { foundSyllableIndic (indicBrokenCluster,ts, te, info, &syllableSerial); buffer.scratchFlags |= bsfHasBrokenSyllable; }; 77 | other => { foundSyllableIndic (indicNonIndicCluster,ts, te, info, &syllableSerial); }; 78 | *|; 79 | 80 | }%% 81 | 82 | func findSyllablesIndic (buffer * Buffer) { 83 | var p, ts, te, act, cs int 84 | info := buffer.Info; 85 | %%{ 86 | write init; 87 | getkey info[p].complexCategory; 88 | }%% 89 | 90 | pe := len(info) 91 | eof := pe 92 | 93 | var syllableSerial uint8 = 1; 94 | %%{ 95 | write exec; 96 | }%% 97 | _ = act // needed by Ragel, but unused 98 | } 99 | 100 | -------------------------------------------------------------------------------- /harfbuzz/ot_indic_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | tu "github.com/go-text/typesetting/testutils" 8 | ) 9 | 10 | func TestIndicGetCategories(t *testing.T) { 11 | expecteds := map[rune]struct{ syllable, position uint8 }{ 12 | 0x0b55: {indSM_ex_N, posEnd}, 13 | 0x103A: {myaSM_ex_As, posEnd}, 14 | 0x103B: {myaSM_ex_MY, posEnd}, 15 | 0x17D0: {khmSM_ex_Xgroup, posEnd}, 16 | 0x17E0: {myaSM_ex_GB, posBaseC}, 17 | // myanmar 18 | 4100: {indSM_ex_Ra, posBaseC}, 19 | 4123: {indSM_ex_Ra, posBaseC}, 20 | 4141: {myaSM_ex_VAbv, posAboveC}, 21 | 4153: {indSM_ex_H, posEnd}, 22 | 4157: {myaSM_ex_MW, posEnd}, 23 | } 24 | for u, exp := range expecteds { 25 | got := indicGetCategories(u) 26 | syl, pos := uint8(got&0xFF), uint8(got>>8) 27 | tu.AssertC(t, syl == exp.syllable, fmt.Sprint("rune ", u, syl)) 28 | tu.AssertC(t, pos == exp.position, fmt.Sprint("rune ", u, pos)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /harfbuzz/ot_kern.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import fontP "github.com/go-text/typesetting/font" 4 | 5 | func simpleKern(kernTable fontP.Kernx) fontP.SimpleKerns { 6 | for _, subtable := range kernTable { 7 | if simple, ok := subtable.Data.(fontP.SimpleKerns); ok { 8 | return simple 9 | } 10 | } 11 | return nil 12 | } 13 | 14 | func kern(driver fontP.SimpleKerns, crossStream bool, font *Font, buffer *Buffer, kernMask GlyphMask, scale bool) { 15 | buffer.unsafeToConcat(0, maxInt) 16 | 17 | var c otApplyContext 18 | 19 | c.reset(1, font, buffer) 20 | c.setLookupMask(kernMask) 21 | c.setLookupProps(uint32(otIgnoreMarks)) 22 | skippyIter := &c.iterInput 23 | horizontal := buffer.Props.Direction.isHorizontal() 24 | info := buffer.Info 25 | pos := buffer.Pos 26 | for idx := 0; idx < len(pos); { 27 | if info[idx].Mask&kernMask == 0 { 28 | idx++ 29 | continue 30 | } 31 | 32 | skippyIter.reset(idx, 1) 33 | if ok, _ := skippyIter.next(); !ok { 34 | idx++ 35 | continue 36 | } 37 | 38 | i := idx 39 | j := skippyIter.idx 40 | 41 | rawKern := driver.KernPair(info[i].Glyph, info[j].Glyph) 42 | kern := Position(rawKern) 43 | 44 | if rawKern == 0 { 45 | goto skip 46 | } 47 | 48 | if horizontal { 49 | if scale { 50 | kern = font.emScaleX(rawKern) 51 | } 52 | if crossStream { 53 | pos[j].YOffset = kern 54 | buffer.scratchFlags |= bsfHasGPOSAttachment 55 | } else { 56 | kern1 := kern >> 1 57 | kern2 := kern - kern1 58 | pos[i].XAdvance += kern1 59 | pos[j].XAdvance += kern2 60 | pos[j].XOffset += kern2 61 | } 62 | } else { 63 | if scale { 64 | kern = font.emScaleY(rawKern) 65 | } 66 | if crossStream { 67 | pos[j].XOffset = kern 68 | buffer.scratchFlags |= bsfHasGPOSAttachment 69 | } else { 70 | kern1 := kern >> 1 71 | kern2 := kern - kern1 72 | pos[i].YAdvance += kern1 73 | pos[j].YAdvance += kern2 74 | pos[j].YOffset += kern2 75 | } 76 | } 77 | 78 | buffer.unsafeToBreak(i, j+1) 79 | 80 | skip: 81 | idx = skippyIter.idx 82 | } 83 | } 84 | 85 | func (sp *otShapePlan) otApplyFallbackKern(font *Font, buffer *Buffer) { 86 | reverse := buffer.Props.Direction.isBackward() 87 | 88 | if reverse { 89 | buffer.Reverse() 90 | } 91 | 92 | if driver := simpleKern(font.face.Kern); driver != nil { 93 | kern(driver, false, font, buffer, sp.kernMask, false) 94 | } 95 | 96 | if reverse { 97 | buffer.Reverse() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /harfbuzz/ot_khmer_machine.rl: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | // Code generated with ragel -Z -o ot_khmer_machine.go ot_khmer_machine.rl ; sed -i '/^\/\/line/ d' ot_khmer_machine.go ; goimports -w ot_khmer_machine.go DO NOT EDIT. 4 | 5 | // ported from harfbuzz/src/hb-ot-shape-complex-khmer-machine.rl Copyright © 2015 Google, Inc. Behdad Esfahbod 6 | 7 | 8 | const ( 9 | khmerConsonantSyllable = iota 10 | khmerBrokenCluster 11 | khmerNonKhmerCluster 12 | ) 13 | 14 | %%{ 15 | machine khmSM; 16 | alphtype byte; 17 | write exports; 18 | write data; 19 | }%% 20 | 21 | %%{ 22 | 23 | # We use category H for spec category Coeng 24 | 25 | export C = 1; 26 | export V = 2; 27 | export H = 4; 28 | export ZWNJ = 5; 29 | export ZWJ = 6; 30 | export PLACEHOLDER = 10; 31 | export DOTTEDCIRCLE = 11; 32 | export Ra = 15; 33 | 34 | export VAbv = 20; 35 | export VBlw = 21; 36 | export VPre = 22; 37 | export VPst = 23; 38 | 39 | export Robatic = 25; 40 | export Xgroup = 26; 41 | export Ygroup = 27; 42 | 43 | c = (C | Ra | V); 44 | cn = c.((ZWJ|ZWNJ)?.Robatic)?; 45 | joiner = (ZWJ | ZWNJ); 46 | xgroup = (joiner*.Xgroup)*; 47 | ygroup = Ygroup*; 48 | 49 | # This grammar was experimentally extracted from what Uniscribe allows. 50 | 51 | matra_group = VPre? xgroup VBlw? xgroup (joiner?.VAbv)? xgroup VPst?; 52 | syllable_tail = xgroup matra_group xgroup (H.c)? ygroup; 53 | 54 | 55 | broken_cluster = Robatic? (H.cn)* (H | syllable_tail); 56 | consonant_syllable = (cn|PLACEHOLDER|DOTTEDCIRCLE) broken_cluster; 57 | other = any; 58 | 59 | main := |* 60 | consonant_syllable => { foundSyllableKhmer (khmerConsonantSyllable, ts, te, info, &syllableSerial); }; 61 | broken_cluster => { foundSyllableKhmer (khmerBrokenCluster, ts, te, info, &syllableSerial); buffer.scratchFlags |= bsfHasBrokenSyllable; }; 62 | other => { foundSyllableKhmer (khmerNonKhmerCluster, ts, te, info, &syllableSerial); }; 63 | *|; 64 | 65 | 66 | }%% 67 | 68 | 69 | func findSyllablesKhmer (buffer * Buffer) { 70 | var p, ts, te, act, cs int 71 | info := buffer.Info; 72 | %%{ 73 | write init; 74 | getkey info[p].complexCategory; 75 | }%% 76 | 77 | pe := len(info) 78 | eof := pe 79 | 80 | var syllableSerial uint8 = 1; 81 | %%{ 82 | write exec; 83 | }%% 84 | _ = act // needed by Ragel, but unused 85 | } 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /harfbuzz/ot_language.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/go-text/typesetting/font/opentype/tables" 7 | ) 8 | 9 | type langTag struct { 10 | language string 11 | tag tables.Tag 12 | } 13 | 14 | // return -1 if `a` < `l` 15 | func (l *langTag) compare(a string) int { 16 | b := l.language 17 | 18 | p := strings.IndexByte(a, '-') 19 | // da := len(a) 20 | if p != -1 { 21 | // da = p 22 | a = a[:p] 23 | } 24 | 25 | p = strings.IndexByte(b, '-') 26 | // db := len(b) 27 | if p != -1 { 28 | // db = p 29 | b = b[:p] 30 | } 31 | // L := min(min(len(a), len(b)), max(da, db)) 32 | return strings.Compare(a, b) 33 | } 34 | 35 | func bfindLanguage(lang string) int { 36 | low, high := 0, len(otLanguages) 37 | for low <= high { 38 | mid := (low + high) / 2 39 | p := &otLanguages[mid] 40 | cmp := p.compare(lang) 41 | if cmp < 0 { 42 | high = mid - 1 43 | } else if cmp > 0 { 44 | low = mid + 1 45 | } else { 46 | return mid 47 | } 48 | } 49 | return -1 50 | } 51 | 52 | func subtagMatches(langStr string, subtag string) bool { 53 | LS := len(subtag) 54 | if len(langStr) < LS { 55 | return false 56 | } 57 | 58 | for { 59 | s := strings.Index(langStr, subtag) 60 | if s == -1 { 61 | return false 62 | } 63 | if s+LS >= len(langStr) || !isAlnum(langStr[s+LS]) { 64 | return true 65 | } 66 | langStr = langStr[s+LS:] 67 | } 68 | } 69 | 70 | func langMatches(langStr, spec string) bool { 71 | l := len(spec) 72 | return strings.HasPrefix(langStr, spec) && (len(langStr) == l || langStr[l] == '-') 73 | } 74 | -------------------------------------------------------------------------------- /harfbuzz/ot_language_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLanguageOrder(t *testing.T) { 8 | for i, l := range otLanguages { 9 | if i == 0 { 10 | continue 11 | } 12 | c := l.compare(otLanguages[i-1].language) 13 | if c > 0 { 14 | t.Fatalf("ot_languages not sorted at index %d: %s %d %s\n", 15 | i, otLanguages[i-1].language, c, l.language) 16 | } 17 | } 18 | } 19 | 20 | func TestFindLanguage(t *testing.T) { 21 | for _, l := range otLanguages { 22 | j := bfindLanguage(l.language) 23 | if j == -1 { 24 | t.Errorf("can't find back language %v", l) 25 | } 26 | // since there is some duplicate, we won't have i == j 27 | if otLanguages[j].language != l.language { 28 | t.Errorf("unexpected %s", otLanguages[j].language) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /harfbuzz/ot_map_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "testing" 5 | 6 | ot "github.com/go-text/typesetting/font/opentype" 7 | ) 8 | 9 | func TestOTFeature(t *testing.T) { 10 | face := openFontFile(t, "fonts/cv01.otf") 11 | 12 | cv01 := ot.NewTag('c', 'v', '0', '1') 13 | 14 | featureIndex := findFeatureForLang(&face.GSUB.Layout, 0, DefaultLanguageIndex, cv01) 15 | if featureIndex == NoFeatureIndex { 16 | t.Fatal("failed to find feature index") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /harfbuzz/ot_myanmar_machine.rl: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | // Code generated with ragel -Z -o ot_myanmar_machine.go ot_myanmar_machine.rl ; sed -i '/^\/\/line/ d' ot_myanmar_machine.go ; goimports -w ot_myanmar_machine.go DO NOT EDIT. 4 | 5 | // ported from harfbuzz/src/hb-ot-shape-complex-myanmar-machine.rl Copyright © 2015 Mozilla Foundation. Google, Inc. Behdad Esfahbod 6 | 7 | // myanmar_syllable_type_t 8 | const ( 9 | myanmarConsonantSyllable = iota 10 | myanmarBrokenCluster 11 | myanmarNonMyanmarCluster 12 | ) 13 | 14 | %%{ 15 | machine myaSM; 16 | alphtype byte; 17 | write exports; 18 | write data; 19 | }%% 20 | 21 | %%{ 22 | 23 | # Spec category D is folded into GB; D0 is not implemented by Uniscribe and as such folded into D 24 | # Spec category P is folded into GB 25 | 26 | export C = 1; 27 | export IV = 2; 28 | export DB = 3; # Dot below = OT_N 29 | export H = 4; 30 | export ZWNJ = 5; 31 | export ZWJ = 6; 32 | export SM = 8; # Visarga and Shan tones 33 | export GB = 10; # = OT_PLACEHOLDER 34 | export DOTTEDCIRCLE = 11; 35 | export A = 9; 36 | export Ra = 15; 37 | export CS = 18; 38 | 39 | export VAbv = 20; 40 | export VBlw = 21; 41 | export VPre = 22; 42 | export VPst = 23; 43 | 44 | # 32+ are for Myanmar-specific values 45 | export As = 32; # Asat 46 | export MH = 35; # Medial Ha 47 | export MR = 36; # Medial Ra 48 | export MW = 37; # Medial Wa, Shan Wa 49 | export MY = 38; # Medial Ya, Mon Na, Mon Ma 50 | export PT = 39; # Pwo and other tones 51 | export VS = 40; # Variation selectors 52 | export ML = 41; # Medial Mon La 53 | 54 | j = ZWJ|ZWNJ; # Joiners 55 | k = (Ra As H); # Kinzi 56 | 57 | c = C|Ra; # is_consonant 58 | 59 | medial_group = MY? As? MR? ((MW MH? ML? | MH ML? | ML) As?)?; 60 | main_vowel_group = (VPre.VS?)* VAbv* VBlw* A* (DB As?)?; 61 | post_vowel_group = VPst MH? ML? As* VAbv* A* (DB As?)?; 62 | pwo_tone_group = PT A* DB? As?; 63 | 64 | complex_syllable_tail = As* medial_group main_vowel_group post_vowel_group* pwo_tone_group* SM* j?; 65 | syllable_tail = (H (c|IV).VS?)* (H | complex_syllable_tail); 66 | 67 | consonant_syllable = (k|CS)? (c|IV|GB|DOTTEDCIRCLE).VS? syllable_tail; 68 | broken_cluster = k? VS? syllable_tail; 69 | other = any; 70 | 71 | main := |* 72 | consonant_syllable => { foundSyllableMyanmar (myanmarConsonantSyllable, ts, te, info, &syllableSerial); }; 73 | j => { foundSyllableMyanmar (myanmarNonMyanmarCluster, ts, te, info, &syllableSerial); }; 74 | broken_cluster => { foundSyllableMyanmar (myanmarBrokenCluster, ts, te, info, &syllableSerial); buffer.scratchFlags |= bsfHasBrokenSyllable }; 75 | other => { foundSyllableMyanmar (myanmarNonMyanmarCluster, ts, te, info, &syllableSerial); }; 76 | *|; 77 | 78 | 79 | }%% 80 | 81 | 82 | func findSyllablesMyanmar (buffer *Buffer){ 83 | var p, ts, te, act, cs int 84 | info := buffer.Info; 85 | %%{ 86 | write init; 87 | getkey info[p].complexCategory; 88 | }%% 89 | 90 | pe := len(info) 91 | eof := pe 92 | 93 | var syllableSerial uint8 = 1; 94 | %%{ 95 | write exec; 96 | }%% 97 | _ = act // needed by Ragel, but unused 98 | } 99 | 100 | -------------------------------------------------------------------------------- /harfbuzz/ot_shape_fallback_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import "testing" 4 | 5 | func TestRecategorize(t *testing.T) { 6 | runes := []rune{1615, 1617, 1614, 1616} 7 | ccc := []uint8{32, 27, 31, 33} 8 | exps := []uint8{230, 230, 230, 220} 9 | for i, r := range runes { 10 | exp := exps[i] 11 | got := recategorizeCombiningClass(r, ccc[i]) 12 | if exp != got { 13 | t.Fatalf("for rune %d and class %d, expected %d, got %d", r, ccc[i], exp, got) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /harfbuzz/ot_use_machine_defs.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | // logic needed by the USE rl parser 4 | 5 | func notCCSDefaultIgnorable(i GlyphInfo) bool { 6 | return i.complexCategory != useSM_ex_CGJ 7 | } 8 | 9 | type pairUSE struct { 10 | i int // index in the original info slice 11 | v GlyphInfo 12 | } 13 | 14 | type machineIndexUSE struct { 15 | j int // index in the filtered slice 16 | p pairUSE 17 | } 18 | 19 | func preprocessInfoUSE(info []GlyphInfo) []machineIndexUSE { 20 | filterMark := func(p pairUSE) bool { 21 | if p.v.complexCategory == useSM_ex_ZWNJ { 22 | for i := p.i + 1; i < len(info); i++ { 23 | if notCCSDefaultIgnorable(info[i]) { 24 | return !info[i].isUnicodeMark() 25 | } 26 | } 27 | } 28 | return true 29 | } 30 | var tmp []pairUSE 31 | for i, v := range info { 32 | if notCCSDefaultIgnorable(v) { 33 | p := pairUSE{i, v} 34 | if filterMark(p) { 35 | tmp = append(tmp, p) 36 | } 37 | } 38 | } 39 | data := make([]machineIndexUSE, len(tmp)) 40 | for j, p := range tmp { 41 | data[j] = machineIndexUSE{j: j, p: p} 42 | } 43 | return data 44 | } 45 | 46 | func foundSyllableUSE(syllableType uint8, data []machineIndexUSE, ts, te int, info []GlyphInfo, syllableSerial *uint8) { 47 | start := data[ts].p.i 48 | end := len(info) // te might right after the end of data 49 | if te < len(data) { 50 | end = data[te].p.i 51 | } 52 | for i := start; i < end; i++ { 53 | info[i].syllable = (*syllableSerial << 4) | syllableType 54 | } 55 | *syllableSerial++ 56 | if *syllableSerial == 16 { 57 | *syllableSerial = 1 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /harfbuzz/ot_use_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import "testing" 4 | 5 | func TestUSE(t *testing.T) { 6 | if !(joiningFormInit < 4 && joiningFormIsol < 4 && joiningFormMedi < 4 && joiningFormFina < 4) { 7 | t.Error() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /harfbuzz/set_digest.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "github.com/go-text/typesetting/font/opentype/tables" 5 | ) 6 | 7 | // ported from src/hb-set-digest.hh Copyright © 2012 Google, Inc. Behdad Esfahbod 8 | 9 | const maskBits = 4 * 8 // 4 = size(setDigestLowestBits) 10 | 11 | type setType = gID 12 | 13 | type setBits uint32 14 | 15 | func maskFor(g setType, shift uint) setBits { 16 | return 1 << ((g >> shift) & (maskBits - 1)) 17 | } 18 | 19 | func (sd *setBits) add(g setType, shift uint) { *sd |= maskFor(g, shift) } 20 | 21 | func (sd *setBits) addRange(a, b setType, shift uint) { 22 | if (b>>shift)-(a>>shift) >= maskBits-1 { 23 | *sd = ^setBits(0) 24 | } else { 25 | mb := maskFor(b, shift) 26 | ma := maskFor(a, shift) 27 | var op setBits 28 | if mb < ma { 29 | op = 1 30 | } 31 | *sd |= mb + (mb - ma) - op 32 | } 33 | } 34 | 35 | func (sd *setBits) addArray(arr []setType, shift uint) { 36 | for _, v := range arr { 37 | sd.add(v, shift) 38 | } 39 | } 40 | 41 | func (sd setBits) mayHave(g setType, shift uint) bool { 42 | return sd&maskFor(g, shift) != 0 43 | } 44 | 45 | func (sd setBits) mayHaveSet(g setBits) bool { 46 | return sd&g != 0 47 | } 48 | 49 | /* This is a combination of digests that performs "best". 50 | * There is not much science to this: it's a result of intuition 51 | * and testing. */ 52 | const ( 53 | shift0 = 4 54 | shift1 = 0 55 | shift2 = 9 56 | ) 57 | 58 | // setDigest implement various "filters" that support 59 | // "approximate member query". Conceptually these are like Bloom 60 | // Filter and Quotient Filter, however, much smaller, faster, and 61 | // designed to fit the requirements of our uses for glyph coverage 62 | // queries. 63 | // 64 | // Our filters are highly accurate if the lookup covers fairly local 65 | // set of glyphs, but fully flooded and ineffective if coverage is 66 | // all over the place. 67 | // 68 | // The frozen-set can be used instead of a digest, to trade more 69 | // memory for 100% accuracy, but in practice, that doesn't look like 70 | // an attractive trade-off. 71 | type setDigest [3]setBits 72 | 73 | // add adds the given rune to the set. 74 | func (sd *setDigest) add(g setType) { 75 | sd[0].add(g, shift0) 76 | sd[1].add(g, shift1) 77 | sd[2].add(g, shift2) 78 | } 79 | 80 | // addRange adds the given, inclusive range to the set, 81 | // in an efficient manner. 82 | func (sd *setDigest) addRange(a, b setType) { 83 | sd[0].addRange(a, b, shift0) 84 | sd[1].addRange(a, b, shift1) 85 | sd[2].addRange(a, b, shift2) 86 | } 87 | 88 | // addArray is a convenience method to add 89 | // many runes. 90 | func (sd *setDigest) addArray(arr []setType) { 91 | sd[0].addArray(arr, shift0) 92 | sd[1].addArray(arr, shift1) 93 | sd[2].addArray(arr, shift2) 94 | } 95 | 96 | // mayHave performs an "approximate member query": if the return value 97 | // is `false`, then it is certain that `g` is not in the set. 98 | // Otherwise, we don't kwow, it might be a false positive. 99 | // Note that runes in the set are certain to return `true`. 100 | func (sd setDigest) mayHave(g setType) bool { 101 | return sd[0].mayHave(g, shift0) && sd[1].mayHave(g, shift1) && sd[2].mayHave(g, shift2) 102 | } 103 | 104 | func (sd setDigest) mayHaveDigest(o setDigest) bool { 105 | return sd[0].mayHaveSet(o[0]) && sd[1].mayHaveSet(o[1]) && sd[2].mayHaveSet(o[2]) 106 | } 107 | 108 | func (sd *setDigest) collectCoverage(cov tables.Coverage) { 109 | switch cov := cov.(type) { 110 | case tables.Coverage1: 111 | sd.addArray(cov.Glyphs) 112 | case tables.Coverage2: 113 | for _, r := range cov.Ranges { 114 | sd.addRange(r.StartGlyphID, r.EndGlyphID) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /harfbuzz/set_digest_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import "testing" 4 | 5 | func TestDigest(t *testing.T) { 6 | const ( 7 | setTypeSize = 2 8 | numBits = 3 + 1 + 1 9 | ) 10 | if shift0 >= setTypeSize*8 { 11 | t.Error() 12 | } 13 | if shift0+numBits > setTypeSize*8 { 14 | t.Error() 15 | } 16 | if shift1 >= setTypeSize*8 { 17 | t.Error() 18 | } 19 | if shift1+numBits > setTypeSize*8 { 20 | t.Error() 21 | } 22 | if shift2 >= setTypeSize*8 { 23 | t.Error() 24 | } 25 | if shift2+numBits > setTypeSize*8 { 26 | t.Error() 27 | } 28 | } 29 | 30 | func TestDigestHas(t *testing.T) { 31 | var d setDigest 32 | for i := setType(10); i < 65_000; i += 7 { 33 | d.add(i) 34 | } 35 | for i := setType(10); i < 65_000; i += 7 { 36 | if !d.mayHave(i) { 37 | t.Errorf("expected for %d", i) 38 | } 39 | } 40 | for i := setType(0); i < 0xFFFF; i++ { // care with overflow 41 | // if the filter is negative, then the glyph must not be in the set 42 | if !d.mayHave(i) { 43 | if (i-10)%7 == 0 { 44 | t.Errorf(" for glyph %d present in set", i) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /harfbuzz/shaper_perf_test.go: -------------------------------------------------------------------------------- 1 | package harfbuzz 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/go-text/typesetting/font" 8 | "github.com/go-text/typesetting/language" 9 | tu "github.com/go-text/typesetting/testutils" 10 | ) 11 | 12 | // ported from harfbuzz/perf 13 | 14 | func BenchmarkShaping(b *testing.B) { 15 | runs := []struct { 16 | name string 17 | textFile string 18 | fontFile string 19 | script language.Script 20 | direction Direction 21 | }{ 22 | { 23 | "fa-thelittleprince.txt - Amiri", 24 | "erf_reference/texts/fa-thelittleprince.txt", 25 | "perf_reference/fonts/Amiri-Regular.ttf", 26 | language.Arabic, 27 | RightToLeft, 28 | }, 29 | { 30 | "fa-thelittleprince.txt - NotoNastaliqUrdu", 31 | "perf_reference/texts/fa-thelittleprince.txt", 32 | "perf_reference/fonts/NotoNastaliqUrdu-Regular.ttf", 33 | language.Arabic, 34 | RightToLeft, 35 | }, 36 | 37 | { 38 | "fa-monologue.txt - Amiri", 39 | "perf_reference/texts/fa-monologue.txt", 40 | "perf_reference/fonts/Amiri-Regular.ttf", 41 | language.Arabic, 42 | RightToLeft, 43 | }, 44 | { 45 | "fa-monologue.txt - NotoNastaliqUrdu", 46 | "perf_reference/texts/fa-monologue.txt", 47 | "perf_reference/fonts/NotoNastaliqUrdu-Regular.ttf", 48 | language.Arabic, 49 | RightToLeft, 50 | }, 51 | 52 | { 53 | "en-thelittleprince.txt - Roboto", 54 | "perf_reference/texts/en-thelittleprince.txt", 55 | "perf_reference/fonts/Roboto-Regular.ttf", 56 | language.Latin, 57 | LeftToRight, 58 | }, 59 | 60 | { 61 | "en-words.txt - Roboto", 62 | "perf_reference/texts/en-words.txt", 63 | "perf_reference/fonts/Roboto-Regular.ttf", 64 | language.Latin, 65 | LeftToRight, 66 | }, 67 | } 68 | 69 | for _, run := range runs { 70 | b.Run(run.name, func(b *testing.B) { 71 | shapeOne(b, run.textFile, run.fontFile, run.direction, run.script) 72 | }) 73 | } 74 | } 75 | 76 | func shapeOne(b *testing.B, textFile, fontFile string, direction Direction, script language.Script) { 77 | ft := openFontFile(b, fontFile) 78 | 79 | font := NewFont(font.NewFace(ft)) 80 | 81 | textB, err := os.ReadFile(textFile) 82 | tu.AssertNoErr(b, err) 83 | 84 | text := []rune(string(textB)) 85 | 86 | buf := NewBuffer() 87 | 88 | b.ResetTimer() 89 | for i := 0; i < b.N; i++ { 90 | buf.AddRunes(text, 0, -1) 91 | buf.Props.Direction = direction 92 | buf.Props.Script = script 93 | buf.Shape(font, nil) 94 | buf.Clear() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /language/scripts.go: -------------------------------------------------------------------------------- 1 | package language 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | // Script identifies different writing systems. 9 | // It is represented as the binary encoding of a script tag of 4 (case sensitive) letters, 10 | // as specified by ISO 15924. 11 | // Note that the default value is usually the Unknown script, not the 0 value (which is invalid) 12 | type Script uint32 13 | 14 | // ParseScript converts a 4 bytes string into its binary encoding, 15 | // enforcing the conventional capitalized case. 16 | // If [script] is longer, only its 4 first bytes are used. 17 | func ParseScript(script string) (Script, error) { 18 | if len(script) < 4 { 19 | return 0, fmt.Errorf("invalid script string: %s", script) 20 | } 21 | s := binary.BigEndian.Uint32([]byte(script)) 22 | // ensure capitalized case : make first letter upper, others lower 23 | const mask uint32 = 0x20000000 24 | return Script(s & ^mask | 0x00202020), nil 25 | } 26 | 27 | // LookupScript looks up the script for a particular character (as defined by 28 | // Unicode Standard Annex #24), and returns Unknown if not found. 29 | func LookupScript(r rune) Script { 30 | // binary search 31 | for i, j := 0, len(ScriptRanges); i < j; { 32 | h := i + (j-i)/2 33 | entry := ScriptRanges[h] 34 | if r < entry.Start { 35 | j = h 36 | } else if entry.End < r { 37 | i = h + 1 38 | } else { 39 | return entry.Script 40 | } 41 | } 42 | return Unknown 43 | } 44 | 45 | // String returns the ISO 4 lower letters code of the script 46 | func (s Script) String() string { 47 | var buf [4]byte 48 | binary.BigEndian.PutUint32(buf[:], uint32(s)) 49 | return string(buf[:]) 50 | } 51 | 52 | // Strong returns true if the script is not Common or Inherited 53 | func (s Script) Strong() bool { 54 | return s != Common && s != Inherited 55 | } 56 | -------------------------------------------------------------------------------- /shaping/README.md: -------------------------------------------------------------------------------- 1 | # shaping 2 | 3 | This text shaping library is shared by multiple Go UI toolkits including Fyne, and GIO. 4 | -------------------------------------------------------------------------------- /shaping/lru.go: -------------------------------------------------------------------------------- 1 | package shaping 2 | 3 | import ( 4 | "github.com/go-text/typesetting/font" 5 | "github.com/go-text/typesetting/harfbuzz" 6 | ) 7 | 8 | // fontEntry holds a single key-value pair for an LRU cache. 9 | type fontEntry struct { 10 | next, prev *fontEntry 11 | key *font.Font 12 | v *harfbuzz.Font 13 | } 14 | 15 | // fontLRU is a least-recently-used cache for harfbuzz fonts built from 16 | // font.Fonts. It uses a doubly-linked list to track how recently elements have 17 | // been used and a map to store element data for quick access. 18 | type fontLRU struct { 19 | // This implementation is derived from the one here under the terms of the UNLICENSE: 20 | // 21 | // https://git.sr.ht/~eliasnaur/gio/tree/e768fe347a732056031100f2c66987d6db258ea4/item/text/lru.go 22 | m map[*font.Font]*fontEntry 23 | head, tail *fontEntry 24 | maxSize int 25 | } 26 | 27 | // Get fetches the value associated with the given key, if any. 28 | func (l *fontLRU) Get(k *font.Font) (*harfbuzz.Font, bool) { 29 | if lt, ok := l.m[k]; ok { 30 | l.remove(lt) 31 | l.insert(lt) 32 | return lt.v, true 33 | } 34 | return nil, false 35 | } 36 | 37 | // Put inserts the given value with the given key, evicting old 38 | // cache entries if necessary. 39 | func (l *fontLRU) Put(k *font.Font, v *harfbuzz.Font) { 40 | if l.m == nil { 41 | l.m = make(map[*font.Font]*fontEntry) 42 | l.head = new(fontEntry) 43 | l.tail = new(fontEntry) 44 | l.head.prev = l.tail 45 | l.tail.next = l.head 46 | } 47 | val := &fontEntry{key: k, v: v} 48 | l.m[k] = val 49 | l.insert(val) 50 | if len(l.m) > l.maxSize { 51 | oldest := l.tail.next 52 | l.remove(oldest) 53 | delete(l.m, oldest.key) 54 | } 55 | } 56 | 57 | // remove cuts e out of the lru linked list. 58 | func (l *fontLRU) remove(e *fontEntry) { 59 | e.next.prev = e.prev 60 | e.prev.next = e.next 61 | } 62 | 63 | // insert adds e to the lru linked list. 64 | func (l *fontLRU) insert(e *fontEntry) { 65 | e.next = l.head 66 | e.prev = l.head.prev 67 | e.prev.next = e 68 | e.next.prev = e 69 | } 70 | -------------------------------------------------------------------------------- /shaping/paired_delims_table.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package shaping 4 | 5 | // Code generated by typesettings-utils/generators/unicodedata/cmd/main.go DO NOT EDIT. 6 | 7 | var pairedDelims = [...]rune{ 8 | 0x0028, 0x0029, 9 | 0x003c, 0x003e, 10 | 0x005b, 0x005d, 11 | 0x007b, 0x007d, 12 | 0x00ab, 0x00bb, 13 | 0x2018, 0x2019, 14 | 0x201a, 0x201b, 15 | 0x201c, 0x201d, 16 | 0x201e, 0x201f, 17 | 0x2039, 0x203a, 18 | 0x2045, 0x2046, 19 | 0x207d, 0x207e, 20 | 0x208d, 0x208e, 21 | 0x2308, 0x2309, 22 | 0x230a, 0x230b, 23 | 0x2329, 0x232a, 24 | 0x2768, 0x2769, 25 | 0x276a, 0x276b, 26 | 0x276c, 0x276d, 27 | 0x276e, 0x276f, 28 | 0x2770, 0x2771, 29 | 0x2772, 0x2773, 30 | 0x2774, 0x2775, 31 | 0x27c5, 0x27c6, 32 | 0x27e6, 0x27e7, 33 | 0x27e8, 0x27e9, 34 | 0x27ea, 0x27eb, 35 | 0x27ec, 0x27ed, 36 | 0x27ee, 0x27ef, 37 | 0x2983, 0x2984, 38 | 0x2985, 0x2986, 39 | 0x2987, 0x2988, 40 | 0x2989, 0x298a, 41 | 0x298b, 0x298c, 42 | 0x298d, 0x298e, 43 | 0x298f, 0x2990, 44 | 0x2991, 0x2992, 45 | 0x2993, 0x2994, 46 | 0x2995, 0x2996, 47 | 0x2997, 0x2998, 48 | 0x29d8, 0x29d9, 49 | 0x29da, 0x29db, 50 | 0x29fc, 0x29fd, 51 | 0x2e02, 0x2e03, 52 | 0x2e04, 0x2e05, 53 | 0x2e09, 0x2e0a, 54 | 0x2e0c, 0x2e0d, 55 | 0x2e1c, 0x2e1d, 56 | 0x2e20, 0x2e21, 57 | 0x2e22, 0x2e23, 58 | 0x2e24, 0x2e25, 59 | 0x2e26, 0x2e27, 60 | 0x2e28, 0x2e29, 61 | 0x2e42, 0x2e55, 62 | 0x2e56, 0x2e57, 63 | 0x2e58, 0x2e59, 64 | 0x2e5a, 0x2e5b, 65 | 0x2e5c, 0x3008, 66 | 0x3009, 0x300a, 67 | 0x300b, 0x300c, 68 | 0x300d, 0x300e, 69 | 0x300f, 0x3010, 70 | 0x3011, 0x3014, 71 | 0x3015, 0x3016, 72 | 0x3017, 0x3018, 73 | 0x3019, 0x301a, 74 | 0x301b, 0x301d, 75 | 0x301e, 0x301f, 76 | 0xfd3e, 0xfd3f, 77 | 0xfe17, 0xfe18, 78 | 0xfe35, 0xfe36, 79 | 0xfe37, 0xfe38, 80 | 0xfe39, 0xfe3a, 81 | 0xfe3b, 0xfe3c, 82 | 0xfe3d, 0xfe3e, 83 | 0xfe3f, 0xfe40, 84 | 0xfe41, 0xfe42, 85 | 0xfe43, 0xfe44, 86 | 0xfe47, 0xfe48, 87 | 0xfe59, 0xfe5a, 88 | 0xfe5b, 0xfe5c, 89 | 0xfe5d, 0xfe5e, 90 | 0xff08, 0xff09, 91 | 0xff3b, 0xff3d, 92 | 0xff5b, 0xff5d, 93 | 0xff5f, 0xff60, 94 | 0xff62, 0xff63, 95 | } 96 | -------------------------------------------------------------------------------- /shaping/spacing.go: -------------------------------------------------------------------------------- 1 | package shaping 2 | 3 | import ( 4 | "golang.org/x/image/math/fixed" 5 | ) 6 | 7 | // AddWordSpacing alters the run, adding [additionalSpacing] on each 8 | // word separator. 9 | // [text] is the input slice used to create the run. 10 | // Note that space is always added, even on boundaries. 11 | // 12 | // See also the convenience function [AddSpacing] to handle a slice of runs. 13 | // 14 | // See also https://www.w3.org/TR/css-text-3/#word-separator 15 | func (run *Output) AddWordSpacing(text []rune, additionalSpacing fixed.Int26_6) { 16 | isVertical := run.Direction.IsVertical() 17 | for i, g := range run.Glyphs { 18 | // find the corresponding runes : 19 | // to simplify, we assume a simple one to one rune/glyph mapping 20 | // which should be common in practice for word separators 21 | if !(g.RuneCount == 1 && g.GlyphCount == 1) { 22 | continue 23 | } 24 | r := text[g.ClusterIndex] 25 | switch r { 26 | case '\u0020', // space 27 | '\u00A0', // no-break space 28 | '\u1361', // Ethiopic word space 29 | '\U00010100', '\U00010101', // Aegean word separators 30 | '\U0001039F', // Ugaritic word divider 31 | '\U0001091F': // Phoenician word separator 32 | default: 33 | continue 34 | } 35 | // we have a word separator: add space 36 | // we do it by enlarging the separator glyph advance 37 | // and distributing space around the glyph content 38 | if isVertical { 39 | run.Glyphs[i].YAdvance += additionalSpacing 40 | run.Glyphs[i].YOffset += additionalSpacing / 2 41 | } else { 42 | run.Glyphs[i].XAdvance += additionalSpacing 43 | run.Glyphs[i].XOffset += additionalSpacing / 2 44 | } 45 | } 46 | run.RecomputeAdvance() 47 | } 48 | 49 | // AddLetterSpacing alters the run, adding [additionalSpacing] between 50 | // each Harfbuzz clusters. 51 | // 52 | // Space is added at the boundaries if and only if there is an adjacent run, as specified by [isStartRun] and [isEndRun]. 53 | // 54 | // See also the convenience function [AddSpacing] to handle a slice of runs. 55 | // 56 | // See also https://www.w3.org/TR/css-text-3/#letter-spacing-property 57 | func (run *Output) AddLetterSpacing(additionalSpacing fixed.Int26_6, isStartRun, isEndRun bool) { 58 | isVertical := run.Direction.IsVertical() 59 | 60 | halfSpacing := additionalSpacing / 2 61 | for startGIdx := 0; startGIdx < len(run.Glyphs); { 62 | startGlyph := run.Glyphs[startGIdx] 63 | endGIdx := startGIdx + startGlyph.GlyphCount - 1 64 | 65 | // start : apply spacing at boundary only if the run is not the first 66 | if startGIdx > 0 || !isStartRun { 67 | if isVertical { 68 | run.Glyphs[startGIdx].YAdvance += halfSpacing 69 | run.Glyphs[startGIdx].YOffset += halfSpacing 70 | } else { 71 | run.Glyphs[startGIdx].XAdvance += halfSpacing 72 | run.Glyphs[startGIdx].XOffset += halfSpacing 73 | } 74 | run.Glyphs[startGIdx].startLetterSpacing += halfSpacing 75 | } 76 | 77 | // end : apply spacing at boundary only if the run is not the last 78 | isLastCluster := startGIdx+startGlyph.GlyphCount >= len(run.Glyphs) 79 | if !isLastCluster || !isEndRun { 80 | if isVertical { 81 | run.Glyphs[endGIdx].YAdvance += halfSpacing 82 | } else { 83 | run.Glyphs[endGIdx].XAdvance += halfSpacing 84 | } 85 | run.Glyphs[endGIdx].endLetterSpacing += halfSpacing 86 | } 87 | 88 | // go to next cluster 89 | startGIdx += startGlyph.GlyphCount 90 | } 91 | 92 | run.RecomputeAdvance() 93 | } 94 | 95 | // does not run RecomputeAdvance 96 | func (run *Output) trimStartLetterSpacing() { 97 | if len(run.Glyphs) == 0 { 98 | return 99 | } 100 | firstG := &run.Glyphs[0] 101 | halfSpacing := firstG.startLetterSpacing 102 | if run.Direction.IsVertical() { 103 | firstG.YAdvance -= halfSpacing 104 | firstG.YOffset -= halfSpacing 105 | } else { 106 | firstG.XAdvance -= halfSpacing 107 | firstG.XOffset -= halfSpacing 108 | } 109 | firstG.startLetterSpacing = 0 110 | } 111 | 112 | // AddSpacing adds additionnal spacing between words and letters, mutating the given [runs]. 113 | // [text] is the input slice the [runs] refer to. 114 | // 115 | // See the method [Output.AddWordSpacing] and [Output.AddLetterSpacing] for details 116 | // about what spacing actually is. 117 | func AddSpacing(runs []Output, text []rune, wordSpacing, letterSpacing fixed.Int26_6) { 118 | for i := range runs { 119 | isStartRun, isEndRun := i == 0, i == len(runs)-1 120 | if wordSpacing != 0 { 121 | runs[i].AddWordSpacing(text, wordSpacing) 122 | } 123 | if letterSpacing != 0 { 124 | runs[i].AddLetterSpacing(letterSpacing, isStartRun, isEndRun) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /testutils/utils.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package testutils 4 | 5 | import ( 6 | "embed" 7 | "path" 8 | "testing" 9 | 10 | "github.com/go-text/typesetting-utils/opentype" 11 | ) 12 | 13 | func Assert(t testing.TB, b bool) { 14 | t.Helper() 15 | AssertC(t, b, "assertion error") 16 | } 17 | 18 | func AssertNoErr(t testing.TB, err error) { 19 | t.Helper() 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | } 24 | 25 | func AssertC(t testing.TB, b bool, context string) { 26 | t.Helper() 27 | if !b { 28 | t.Fatal(context) 29 | } 30 | } 31 | 32 | // Filenames return the "absolute" file names of the given directory 33 | // excluding directories, and not recursing. 34 | // It uses the opentype embed file system. 35 | func Filenames(t testing.TB, dir string) []string { 36 | return FilenamesFS(t, &opentype.Files, dir) 37 | } 38 | 39 | func FilenamesFS(t testing.TB, fs *embed.FS, dir string) []string { 40 | t.Helper() 41 | 42 | files, err := fs.ReadDir(dir) 43 | AssertNoErr(t, err) 44 | 45 | var out []string 46 | for _, entry := range files { 47 | if entry.IsDir() { 48 | continue 49 | } 50 | // We should not use filepath.Join here because embed.FS still uses 51 | // unix-style paths on Windows. 52 | filename := path.Join(dir, entry.Name()) 53 | out = append(out, filename) 54 | } 55 | return out 56 | } 57 | -------------------------------------------------------------------------------- /unicodedata/emojis.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package unicodedata 4 | 5 | import "unicode" 6 | 7 | // Code generated by typesettings-utils/generators/unicodedata/cmd/main.go DO NOT EDIT. 8 | 9 | var Extended_Pictographic = &unicode.RangeTable{ 10 | R16: []unicode.Range16{ 11 | {Lo: 0x00a9, Hi: 0x00ae, Stride: 5}, 12 | {Lo: 0x203c, Hi: 0x2049, Stride: 13}, 13 | {Lo: 0x2122, Hi: 0x2139, Stride: 23}, 14 | {Lo: 0x2194, Hi: 0x2199, Stride: 1}, 15 | {Lo: 0x21a9, Hi: 0x21aa, Stride: 1}, 16 | {Lo: 0x231a, Hi: 0x231b, Stride: 1}, 17 | {Lo: 0x2328, Hi: 0x2388, Stride: 96}, 18 | {Lo: 0x23cf, Hi: 0x23e9, Stride: 26}, 19 | {Lo: 0x23ea, Hi: 0x23f3, Stride: 1}, 20 | {Lo: 0x23f8, Hi: 0x23fa, Stride: 1}, 21 | {Lo: 0x24c2, Hi: 0x25aa, Stride: 232}, 22 | {Lo: 0x25ab, Hi: 0x25b6, Stride: 11}, 23 | {Lo: 0x25c0, Hi: 0x25fb, Stride: 59}, 24 | {Lo: 0x25fc, Hi: 0x25fe, Stride: 1}, 25 | {Lo: 0x2600, Hi: 0x2605, Stride: 1}, 26 | {Lo: 0x2607, Hi: 0x2612, Stride: 1}, 27 | {Lo: 0x2614, Hi: 0x2685, Stride: 1}, 28 | {Lo: 0x2690, Hi: 0x2705, Stride: 1}, 29 | {Lo: 0x2708, Hi: 0x2712, Stride: 1}, 30 | {Lo: 0x2714, Hi: 0x2716, Stride: 2}, 31 | {Lo: 0x271d, Hi: 0x2721, Stride: 4}, 32 | {Lo: 0x2728, Hi: 0x2733, Stride: 11}, 33 | {Lo: 0x2734, Hi: 0x2744, Stride: 16}, 34 | {Lo: 0x2747, Hi: 0x274c, Stride: 5}, 35 | {Lo: 0x274e, Hi: 0x2753, Stride: 5}, 36 | {Lo: 0x2754, Hi: 0x2755, Stride: 1}, 37 | {Lo: 0x2757, Hi: 0x2763, Stride: 12}, 38 | {Lo: 0x2764, Hi: 0x2767, Stride: 1}, 39 | {Lo: 0x2795, Hi: 0x2797, Stride: 1}, 40 | {Lo: 0x27a1, Hi: 0x27bf, Stride: 15}, 41 | {Lo: 0x2934, Hi: 0x2935, Stride: 1}, 42 | {Lo: 0x2b05, Hi: 0x2b07, Stride: 1}, 43 | {Lo: 0x2b1b, Hi: 0x2b1c, Stride: 1}, 44 | {Lo: 0x2b50, Hi: 0x2b55, Stride: 5}, 45 | {Lo: 0x3030, Hi: 0x303d, Stride: 13}, 46 | {Lo: 0x3297, Hi: 0x3299, Stride: 2}, 47 | }, 48 | R32: []unicode.Range32{ 49 | {Lo: 0x1f000, Hi: 0x1f0ff, Stride: 1}, 50 | {Lo: 0x1f10d, Hi: 0x1f10f, Stride: 1}, 51 | {Lo: 0x1f12f, Hi: 0x1f16c, Stride: 61}, 52 | {Lo: 0x1f16d, Hi: 0x1f171, Stride: 1}, 53 | {Lo: 0x1f17e, Hi: 0x1f17f, Stride: 1}, 54 | {Lo: 0x1f18e, Hi: 0x1f191, Stride: 3}, 55 | {Lo: 0x1f192, Hi: 0x1f19a, Stride: 1}, 56 | {Lo: 0x1f1ad, Hi: 0x1f1e5, Stride: 1}, 57 | {Lo: 0x1f201, Hi: 0x1f20f, Stride: 1}, 58 | {Lo: 0x1f21a, Hi: 0x1f22f, Stride: 21}, 59 | {Lo: 0x1f232, Hi: 0x1f23a, Stride: 1}, 60 | {Lo: 0x1f23c, Hi: 0x1f23f, Stride: 1}, 61 | {Lo: 0x1f249, Hi: 0x1f3fa, Stride: 1}, 62 | {Lo: 0x1f400, Hi: 0x1f53d, Stride: 1}, 63 | {Lo: 0x1f546, Hi: 0x1f64f, Stride: 1}, 64 | {Lo: 0x1f680, Hi: 0x1f6ff, Stride: 1}, 65 | {Lo: 0x1f774, Hi: 0x1f77f, Stride: 1}, 66 | {Lo: 0x1f7d5, Hi: 0x1f7ff, Stride: 1}, 67 | {Lo: 0x1f80c, Hi: 0x1f80f, Stride: 1}, 68 | {Lo: 0x1f848, Hi: 0x1f84f, Stride: 1}, 69 | {Lo: 0x1f85a, Hi: 0x1f85f, Stride: 1}, 70 | {Lo: 0x1f888, Hi: 0x1f88f, Stride: 1}, 71 | {Lo: 0x1f8ae, Hi: 0x1f8ff, Stride: 1}, 72 | {Lo: 0x1f90c, Hi: 0x1f93a, Stride: 1}, 73 | {Lo: 0x1f93c, Hi: 0x1f945, Stride: 1}, 74 | {Lo: 0x1f947, Hi: 0x1faff, Stride: 1}, 75 | {Lo: 0x1fc00, Hi: 0x1fffd, Stride: 1}, 76 | }, 77 | LatinOffset: 1, 78 | } 79 | -------------------------------------------------------------------------------- /unicodedata/sentence_break.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package unicodedata 4 | 5 | import "unicode" 6 | 7 | // Code generated by typesettings-utils/generators/unicodedata/cmd/main.go DO NOT EDIT. 8 | 9 | // SentenceBreakProperty: STerm 10 | var STerm = &unicode.RangeTable{ 11 | R16: []unicode.Range16{ 12 | {Lo: 0x0021, Hi: 0x003f, Stride: 30}, 13 | {Lo: 0x0589, Hi: 0x061d, Stride: 148}, 14 | {Lo: 0x061e, Hi: 0x061f, Stride: 1}, 15 | {Lo: 0x06d4, Hi: 0x0700, Stride: 44}, 16 | {Lo: 0x0701, Hi: 0x0702, Stride: 1}, 17 | {Lo: 0x07f9, Hi: 0x0837, Stride: 62}, 18 | {Lo: 0x0839, Hi: 0x083d, Stride: 4}, 19 | {Lo: 0x083e, Hi: 0x0964, Stride: 294}, 20 | {Lo: 0x0965, Hi: 0x104a, Stride: 1765}, 21 | {Lo: 0x104b, Hi: 0x1362, Stride: 791}, 22 | {Lo: 0x1367, Hi: 0x1368, Stride: 1}, 23 | {Lo: 0x166e, Hi: 0x1735, Stride: 199}, 24 | {Lo: 0x1736, Hi: 0x17d4, Stride: 158}, 25 | {Lo: 0x17d5, Hi: 0x1803, Stride: 46}, 26 | {Lo: 0x1809, Hi: 0x1944, Stride: 315}, 27 | {Lo: 0x1945, Hi: 0x1aa8, Stride: 355}, 28 | {Lo: 0x1aa9, Hi: 0x1aab, Stride: 1}, 29 | {Lo: 0x1b5a, Hi: 0x1b5b, Stride: 1}, 30 | {Lo: 0x1b5e, Hi: 0x1b5f, Stride: 1}, 31 | {Lo: 0x1b7d, Hi: 0x1b7e, Stride: 1}, 32 | {Lo: 0x1c3b, Hi: 0x1c3c, Stride: 1}, 33 | {Lo: 0x1c7e, Hi: 0x1c7f, Stride: 1}, 34 | {Lo: 0x203c, Hi: 0x203d, Stride: 1}, 35 | {Lo: 0x2047, Hi: 0x2049, Stride: 1}, 36 | {Lo: 0x2e2e, Hi: 0x2e3c, Stride: 14}, 37 | {Lo: 0x2e53, Hi: 0x2e54, Stride: 1}, 38 | {Lo: 0x3002, Hi: 0xa4ff, Stride: 29949}, 39 | {Lo: 0xa60e, Hi: 0xa60f, Stride: 1}, 40 | {Lo: 0xa6f3, Hi: 0xa6f7, Stride: 4}, 41 | {Lo: 0xa876, Hi: 0xa877, Stride: 1}, 42 | {Lo: 0xa8ce, Hi: 0xa8cf, Stride: 1}, 43 | {Lo: 0xa92f, Hi: 0xa9c8, Stride: 153}, 44 | {Lo: 0xa9c9, Hi: 0xaa5d, Stride: 148}, 45 | {Lo: 0xaa5e, Hi: 0xaa5f, Stride: 1}, 46 | {Lo: 0xaaf0, Hi: 0xaaf1, Stride: 1}, 47 | {Lo: 0xabeb, Hi: 0xfe56, Stride: 21099}, 48 | {Lo: 0xfe57, Hi: 0xff01, Stride: 170}, 49 | {Lo: 0xff1f, Hi: 0xff61, Stride: 66}, 50 | }, 51 | R32: []unicode.Range32{ 52 | {Lo: 0x10a56, Hi: 0x10a57, Stride: 1}, 53 | {Lo: 0x10f55, Hi: 0x10f59, Stride: 1}, 54 | {Lo: 0x10f86, Hi: 0x10f89, Stride: 1}, 55 | {Lo: 0x11047, Hi: 0x11048, Stride: 1}, 56 | {Lo: 0x110be, Hi: 0x110c1, Stride: 1}, 57 | {Lo: 0x11141, Hi: 0x11143, Stride: 1}, 58 | {Lo: 0x111c5, Hi: 0x111c6, Stride: 1}, 59 | {Lo: 0x111cd, Hi: 0x111de, Stride: 17}, 60 | {Lo: 0x111df, Hi: 0x11238, Stride: 89}, 61 | {Lo: 0x11239, Hi: 0x1123b, Stride: 2}, 62 | {Lo: 0x1123c, Hi: 0x112a9, Stride: 109}, 63 | {Lo: 0x1144b, Hi: 0x1144c, Stride: 1}, 64 | {Lo: 0x115c2, Hi: 0x115c3, Stride: 1}, 65 | {Lo: 0x115c9, Hi: 0x115d7, Stride: 1}, 66 | {Lo: 0x11641, Hi: 0x11642, Stride: 1}, 67 | {Lo: 0x1173c, Hi: 0x1173e, Stride: 1}, 68 | {Lo: 0x11944, Hi: 0x11946, Stride: 2}, 69 | {Lo: 0x11a42, Hi: 0x11a43, Stride: 1}, 70 | {Lo: 0x11a9b, Hi: 0x11a9c, Stride: 1}, 71 | {Lo: 0x11c41, Hi: 0x11c42, Stride: 1}, 72 | {Lo: 0x11ef7, Hi: 0x11ef8, Stride: 1}, 73 | {Lo: 0x11f43, Hi: 0x11f44, Stride: 1}, 74 | {Lo: 0x16a6e, Hi: 0x16a6f, Stride: 1}, 75 | {Lo: 0x16af5, Hi: 0x16b37, Stride: 66}, 76 | {Lo: 0x16b38, Hi: 0x16b44, Stride: 12}, 77 | {Lo: 0x16e98, Hi: 0x1bc9f, Stride: 19975}, 78 | {Lo: 0x1da88, Hi: 0x1da88, Stride: 1}, 79 | }, 80 | LatinOffset: 1, 81 | } 82 | -------------------------------------------------------------------------------- /unicodedata/vertical_orientation.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR BSD-3-Clause 2 | 3 | package unicodedata 4 | 5 | import ( 6 | "unicode" 7 | 8 | "github.com/go-text/typesetting/language" 9 | ) 10 | 11 | // Code generated by typesettings-utils/generators/unicodedata/cmd/main.go DO NOT EDIT. 12 | 13 | // ScriptVerticalOrientation provides the glyph oriention 14 | // to use for vertical text. 15 | type ScriptVerticalOrientation struct { 16 | exceptions *unicode.RangeTable 17 | script language.Script 18 | isMainSideways bool 19 | } 20 | 21 | // uprightOrMixedScripts is the list of scripts 22 | // which may use both mode ("upright" or "sideways") for vertical text orientation 23 | var uprightOrMixedScripts = [...]ScriptVerticalOrientation{ 24 | {nil, language.Anatolian_Hieroglyphs, false}, 25 | {nil, language.Bopomofo, false}, 26 | { 27 | &unicode.RangeTable{ 28 | R16: []unicode.Range16{ 29 | {Lo: 0x1400, Hi: 0x1400, Stride: 1}, 30 | }, 31 | }, language.Canadian_Aboriginal, false, 32 | }, 33 | {nil, language.Egyptian_Hieroglyphs, false}, 34 | {nil, language.Han, false}, 35 | { 36 | &unicode.RangeTable{ 37 | R16: []unicode.Range16{ 38 | {Lo: 0xffa0, Hi: 0xffbe, Stride: 1}, 39 | {Lo: 0xffc2, Hi: 0xffc7, Stride: 1}, 40 | {Lo: 0xffca, Hi: 0xffcf, Stride: 1}, 41 | {Lo: 0xffd2, Hi: 0xffd7, Stride: 1}, 42 | {Lo: 0xffda, Hi: 0xffdc, Stride: 1}, 43 | }, 44 | }, language.Hangul, false, 45 | }, 46 | {nil, language.Hiragana, false}, 47 | { 48 | &unicode.RangeTable{ 49 | R16: []unicode.Range16{ 50 | {Lo: 0xff66, Hi: 0xff6f, Stride: 1}, 51 | {Lo: 0xff71, Hi: 0xff9d, Stride: 1}, 52 | }, 53 | }, language.Katakana, false, 54 | }, 55 | {nil, language.Khitan_Small_Script, false}, 56 | { 57 | &unicode.RangeTable{ 58 | R16: []unicode.Range16{ 59 | {Lo: 0x2160, Hi: 0x2188, Stride: 1}, 60 | {Lo: 0xff21, Hi: 0xff3a, Stride: 1}, 61 | {Lo: 0xff41, Hi: 0xff5a, Stride: 1}, 62 | }, 63 | }, language.Latin, true, 64 | }, 65 | {nil, language.Meroitic_Hieroglyphs, false}, 66 | {nil, language.Nushu, false}, 67 | {nil, language.Siddham, false}, 68 | {nil, language.SignWriting, false}, 69 | {nil, language.Soyombo, false}, 70 | {nil, language.Tangut, false}, 71 | {nil, language.Yi, false}, 72 | {nil, language.Zanabazar_Square, false}, 73 | } 74 | --------------------------------------------------------------------------------