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