harfbuzz-ruby 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +258 -0
  5. data/Rakefile +8 -0
  6. data/benchmark/shaping_bench.rb +77 -0
  7. data/examples/basic_shaping.rb +67 -0
  8. data/examples/glyph_outlines.rb +79 -0
  9. data/examples/opentype_features.rb +91 -0
  10. data/examples/render_svg.rb +112 -0
  11. data/examples/render_waterfall.rb +177 -0
  12. data/examples/variable_fonts.rb +73 -0
  13. data/lib/harfbuzz/aat/layout.rb +78 -0
  14. data/lib/harfbuzz/blob.rb +136 -0
  15. data/lib/harfbuzz/buffer.rb +497 -0
  16. data/lib/harfbuzz/c/aat/layout.rb +15 -0
  17. data/lib/harfbuzz/c/base.rb +114 -0
  18. data/lib/harfbuzz/c/blob.rb +23 -0
  19. data/lib/harfbuzz/c/buffer.rb +127 -0
  20. data/lib/harfbuzz/c/common.rb +39 -0
  21. data/lib/harfbuzz/c/draw.rb +22 -0
  22. data/lib/harfbuzz/c/enums.rb +146 -0
  23. data/lib/harfbuzz/c/face.rb +37 -0
  24. data/lib/harfbuzz/c/font.rb +88 -0
  25. data/lib/harfbuzz/c/font_funcs.rb +58 -0
  26. data/lib/harfbuzz/c/map.rb +28 -0
  27. data/lib/harfbuzz/c/ot/color.rb +32 -0
  28. data/lib/harfbuzz/c/ot/font.rb +7 -0
  29. data/lib/harfbuzz/c/ot/layout.rb +83 -0
  30. data/lib/harfbuzz/c/ot/math.rb +23 -0
  31. data/lib/harfbuzz/c/ot/meta.rb +10 -0
  32. data/lib/harfbuzz/c/ot/metrics.rb +16 -0
  33. data/lib/harfbuzz/c/ot/name.rb +13 -0
  34. data/lib/harfbuzz/c/ot/shape.rb +10 -0
  35. data/lib/harfbuzz/c/ot/var.rb +22 -0
  36. data/lib/harfbuzz/c/paint.rb +38 -0
  37. data/lib/harfbuzz/c/set.rb +42 -0
  38. data/lib/harfbuzz/c/shape.rb +11 -0
  39. data/lib/harfbuzz/c/shape_plan.rb +24 -0
  40. data/lib/harfbuzz/c/structs.rb +120 -0
  41. data/lib/harfbuzz/c/subset.rb +49 -0
  42. data/lib/harfbuzz/c/unicode.rb +40 -0
  43. data/lib/harfbuzz/c/version.rb +25 -0
  44. data/lib/harfbuzz/draw_funcs.rb +112 -0
  45. data/lib/harfbuzz/error.rb +27 -0
  46. data/lib/harfbuzz/face.rb +186 -0
  47. data/lib/harfbuzz/feature.rb +76 -0
  48. data/lib/harfbuzz/flags.rb +85 -0
  49. data/lib/harfbuzz/font.rb +404 -0
  50. data/lib/harfbuzz/font_funcs.rb +286 -0
  51. data/lib/harfbuzz/glyph_info.rb +35 -0
  52. data/lib/harfbuzz/glyph_position.rb +41 -0
  53. data/lib/harfbuzz/library.rb +98 -0
  54. data/lib/harfbuzz/map.rb +157 -0
  55. data/lib/harfbuzz/ot/color.rb +125 -0
  56. data/lib/harfbuzz/ot/font.rb +16 -0
  57. data/lib/harfbuzz/ot/layout.rb +583 -0
  58. data/lib/harfbuzz/ot/math.rb +111 -0
  59. data/lib/harfbuzz/ot/meta.rb +34 -0
  60. data/lib/harfbuzz/ot/metrics.rb +54 -0
  61. data/lib/harfbuzz/ot/name.rb +81 -0
  62. data/lib/harfbuzz/ot/shape.rb +34 -0
  63. data/lib/harfbuzz/ot/var.rb +116 -0
  64. data/lib/harfbuzz/paint_funcs.rb +134 -0
  65. data/lib/harfbuzz/set.rb +272 -0
  66. data/lib/harfbuzz/shape_plan.rb +115 -0
  67. data/lib/harfbuzz/shaping_result.rb +94 -0
  68. data/lib/harfbuzz/subset.rb +130 -0
  69. data/lib/harfbuzz/unicode_funcs.rb +201 -0
  70. data/lib/harfbuzz/variation.rb +49 -0
  71. data/lib/harfbuzz/version.rb +5 -0
  72. data/lib/harfbuzz-ffi.rb +4 -0
  73. data/lib/harfbuzz.rb +313 -0
  74. data/sig/harfbuzz.rbs +594 -0
  75. metadata +132 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b56e40fbf6e75fcf2cecdcfce3d2dc685281a3283c60b96f6a6bac6c28f16fc1
4
+ data.tar.gz: 6c1137acb54689eedf6a05da99e47e44ea83b000cf0b98a129bdfbc4c60074ad
5
+ SHA512:
6
+ metadata.gz: d4f119a1c98ec67296ec7351bd36943e1a067341305cd7c3ee7e9a1efb7d42c7a8515273951017af8dbe212f3a4252827108c40332297e2dfb2d323595c3ea25
7
+ data.tar.gz: 2bc37ddb7b27ef540fef758b52a2708d5df8ec41fd96763680e85adcbe43c667ede6f3dd9873b57f381847ff129c6a60398e1773fa499498e5933a146a8b9d5d
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## Unreleased
9
+
10
+ ## v1.0.0 (2026-02-27)
11
+
12
+ - Initial release.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # HarfBuzz Ruby
2
+
3
+ Ruby bindings for [HarfBuzz](https://harfbuzz.github.io/) text shaping engine.
4
+
5
+ ## Features
6
+
7
+ - Complete HarfBuzz API bindings using Ruby-FFI
8
+ - Two-layer architecture: low-level `HarfBuzz::C` (1:1 C binding) + high-level Ruby-idiomatic layer
9
+ - Safe memory management with borrow/own distinction
10
+ - Thread-safe immutable font objects
11
+ - Cross-platform support (macOS, Linux)
12
+ - RBS type signatures included
13
+
14
+ ## Requirements
15
+
16
+ - Ruby 3.1+
17
+ - HarfBuzz C library installed on your system
18
+ - Supported platforms:
19
+ - macOS (Homebrew)
20
+ - Ubuntu / Debian
21
+ - Fedora / RHEL
22
+ - Alpine
23
+
24
+ ## Installation
25
+
26
+ Install the HarfBuzz C library:
27
+
28
+ ```bash
29
+ # macOS
30
+ brew install harfbuzz
31
+
32
+ # Ubuntu / Debian
33
+ sudo apt-get install libharfbuzz-dev libharfbuzz-subset0
34
+
35
+ # Fedora / RHEL
36
+ sudo dnf install harfbuzz-devel
37
+
38
+ # Alpine
39
+ apk add harfbuzz-dev
40
+ ```
41
+
42
+ Add to your Gemfile:
43
+
44
+ ```ruby
45
+ gem 'harfbuzz-ruby'
46
+ ```
47
+
48
+ Then run:
49
+
50
+ ```bash
51
+ bundle install
52
+ ```
53
+
54
+ Or install directly:
55
+
56
+ ```bash
57
+ gem install harfbuzz-ruby
58
+ ```
59
+
60
+ ### Custom Library Path
61
+
62
+ To use a custom build of HarfBuzz:
63
+
64
+ ```bash
65
+ export HARFBUZZ_LIB_PATH=/path/to/libharfbuzz.dylib
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ### Basic Setup
71
+
72
+ ```ruby
73
+ require "harfbuzz"
74
+
75
+ result = HarfBuzz.shape_text("Hello, World!", font_path: "/path/to/font.ttf")
76
+ result.each do |info, pos|
77
+ puts "glyph=#{info.glyph_id} x_advance=#{pos.x_advance}"
78
+ end
79
+ puts "Total advance: #{result.total_advance.first}"
80
+ ```
81
+
82
+ ### Text Shaping Example
83
+
84
+ ```ruby
85
+ require "harfbuzz"
86
+
87
+ # Load font
88
+ blob = HarfBuzz::Blob.from_file!("/path/to/font.ttf")
89
+ face = HarfBuzz::Face.new(blob, 0)
90
+ font = HarfBuzz::Font.new(face)
91
+
92
+ # Create buffer and add text
93
+ buffer = HarfBuzz::Buffer.new
94
+ buffer.add_utf8("Hello, World!")
95
+ buffer.guess_segment_properties
96
+
97
+ # Shape
98
+ HarfBuzz.shape(font, buffer)
99
+
100
+ # Read results
101
+ buffer.glyph_infos.zip(buffer.glyph_positions).each do |info, pos|
102
+ puts "glyph_id=#{info.glyph_id} cluster=#{info.cluster} " \
103
+ "x_advance=#{pos.x_advance} x_offset=#{pos.x_offset}"
104
+ end
105
+ ```
106
+
107
+ ### Shaping with Features
108
+
109
+ ```ruby
110
+ features = [
111
+ HarfBuzz::Feature.from_string("liga"), # enable ligatures
112
+ HarfBuzz::Feature.from_string("-kern"), # disable kerning
113
+ ]
114
+ HarfBuzz.shape(font, buffer, features)
115
+
116
+ # Or use a hash
117
+ features = HarfBuzz::Feature.from_hash(liga: true, kern: false, smcp: 2)
118
+ HarfBuzz.shape(font, buffer, features)
119
+ ```
120
+
121
+ ### Variable Fonts
122
+
123
+ ```ruby
124
+ font = HarfBuzz::Font.new(face)
125
+
126
+ # Set weight=700 and width=75
127
+ variations = [
128
+ HarfBuzz::Variation.from_string("wght=700"),
129
+ HarfBuzz::Variation.from_string("wdth=75"),
130
+ ]
131
+ font.variations = variations
132
+ ```
133
+
134
+ ### Glyph Outline Extraction
135
+
136
+ ```ruby
137
+ draw = HarfBuzz::DrawFuncs.new
138
+
139
+ path_commands = []
140
+ draw.on_move_to { |x, y, _| path_commands << "M #{x},#{y}" }
141
+ draw.on_line_to { |x, y, _| path_commands << "L #{x},#{y}" }
142
+ draw.on_quadratic_to { |cx, cy, x, y, _| path_commands << "Q #{cx},#{cy} #{x},#{y}" }
143
+ draw.on_cubic_to { |c1x, c1y, c2x, c2y, x, y, _|
144
+ path_commands << "C #{c1x},#{c1y} #{c2x},#{c2y} #{x},#{y}" }
145
+ draw.on_close_path { |_| path_commands << "Z" }
146
+ draw.make_immutable!
147
+
148
+ font.draw_glyph(36, draw)
149
+ puts path_commands.join(" ")
150
+ ```
151
+
152
+ ### Font Subsetting
153
+
154
+ ```ruby
155
+ input = HarfBuzz::Subset::Input.new
156
+
157
+ unicode_set = input.unicode_set
158
+ "Hello".each_codepoint { |cp| unicode_set.add(cp) }
159
+
160
+ subsetted_face = HarfBuzz::Subset.subset(face, input)
161
+ puts "Subsetted glyph count: #{subsetted_face.glyph_count}"
162
+ ```
163
+
164
+ ## Examples
165
+
166
+ See the `examples/` directory for more complete examples:
167
+
168
+ - `basic_shaping.rb` - Basic text shaping
169
+ - `glyph_outlines.rb` - Glyph outline extraction
170
+ - `opentype_features.rb` - OpenType feature queries
171
+ - `variable_fonts.rb` - Variable font axis manipulation
172
+ - `render_svg.rb` - Render shaped text to SVG and open in browser
173
+ - `render_waterfall.rb` - Variable font weight waterfall (HTML + SVG)
174
+
175
+ Run an example:
176
+
177
+ ```bash
178
+ bundle exec ruby examples/basic_shaping.rb
179
+ ```
180
+
181
+ Rendering examples generate SVG/HTML and open them in the default browser:
182
+
183
+ ```bash
184
+ bundle exec ruby examples/render_svg.rb "Hello, HarfBuzz!"
185
+ bundle exec ruby examples/render_waterfall.rb /path/to/variable_font.ttf
186
+ ```
187
+
188
+ ## API Overview
189
+
190
+ ### Core Objects
191
+
192
+ | Class | Description |
193
+ |-------|-------------|
194
+ | `HarfBuzz::Blob` | Binary data container for font files |
195
+ | `HarfBuzz::Face` | Font face (typeface + index) |
196
+ | `HarfBuzz::Font` | Font instance with metrics and scale |
197
+ | `HarfBuzz::Buffer` | Text buffer for shaping input/output |
198
+
199
+ ### Shaping
200
+
201
+ | Class | Description |
202
+ |-------|-------------|
203
+ | `HarfBuzz::Feature` | OpenType feature toggle |
204
+ | `HarfBuzz::Variation` | Variable font axis setting |
205
+ | `HarfBuzz::ShapePlan` | Reusable shaping plan |
206
+ | `HarfBuzz::ShapingResult` | Shaped text result with glyph info |
207
+ | `HarfBuzz::GlyphInfo` | Glyph ID and cluster info |
208
+ | `HarfBuzz::GlyphPosition` | Glyph advance and offset |
209
+
210
+ ### Drawing & Color
211
+
212
+ | Class | Description |
213
+ |-------|-------------|
214
+ | `HarfBuzz::DrawFuncs` | Glyph outline extraction callbacks |
215
+ | `HarfBuzz::PaintFuncs` | Color font paint callbacks |
216
+
217
+ ### OpenType
218
+
219
+ | Class | Description |
220
+ |-------|-------------|
221
+ | `HarfBuzz::OT::Layout` | GSUB/GPOS script, language, and feature queries |
222
+ | `HarfBuzz::OT::Var` | Variable font axis and instance queries |
223
+ | `HarfBuzz::OT::Metrics` | Font metrics (x-height, cap-height, etc.) |
224
+ | `HarfBuzz::OT::Name` | Name table access |
225
+ | `HarfBuzz::OT::Color` | Color palette and COLR queries |
226
+ | `HarfBuzz::OT::Math` | Math table queries |
227
+ | `HarfBuzz::OT::Meta` | Metadata queries |
228
+
229
+ ### Subsetting
230
+
231
+ | Class | Description |
232
+ |-------|-------------|
233
+ | `HarfBuzz::Subset` | Font subsetting API |
234
+ | `HarfBuzz::Set` | Integer set for codepoints/glyphs |
235
+ | `HarfBuzz::Map` | Integer-to-integer mapping |
236
+
237
+ ## Development
238
+
239
+ ```bash
240
+ git clone https://github.com/ydah/harfbuzz.git
241
+ cd harfbuzz
242
+ bundle install
243
+ bundle exec rake spec
244
+ ```
245
+
246
+ ## Contributing
247
+
248
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ydah/harfbuzz.
249
+
250
+ ## License
251
+
252
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
253
+
254
+ ## References
255
+
256
+ - [HarfBuzz Documentation](https://harfbuzz.github.io/)
257
+ - [HarfBuzz GitHub](https://github.com/harfbuzz/harfbuzz)
258
+ - [OpenType Specification](https://learn.microsoft.com/en-us/typography/opentype/spec/)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ # benchmark/shaping_bench.rb — HarfBuzz Ruby FFI shaping benchmarks
4
+ #
5
+ # Usage:
6
+ # bundle exec ruby benchmark/shaping_bench.rb [path/to/font.ttf]
7
+ #
8
+ # Requires benchmark-ips gem: gem install benchmark-ips
9
+
10
+ require "benchmark/ips"
11
+ require "harfbuzz"
12
+
13
+ font_path = ARGV[0] ||
14
+ if File.exist?("/System/Library/Fonts/Helvetica.ttc")
15
+ "/System/Library/Fonts/Helvetica.ttc"
16
+ elsif File.exist?("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
17
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
18
+ else
19
+ raise "No test font found. Pass font path as argument: ruby benchmark/shaping_bench.rb /path/to/font.ttf"
20
+ end
21
+
22
+ puts "HarfBuzz version: #{HarfBuzz.version_string}"
23
+ puts "Font: #{font_path}"
24
+ puts
25
+
26
+ blob = HarfBuzz::Blob.from_file!(font_path)
27
+ face = HarfBuzz::Face.new(blob, 0)
28
+ font = HarfBuzz::Font.new(face)
29
+ font.make_immutable!
30
+
31
+ Benchmark.ips do |x|
32
+ x.config(time: 5, warmup: 2)
33
+
34
+ x.report("shape Latin 10 chars") do
35
+ buf = HarfBuzz::Buffer.new
36
+ buf.add_utf8("Hello Word")
37
+ buf.guess_segment_properties
38
+ HarfBuzz.shape(font, buf)
39
+ end
40
+
41
+ x.report("shape Latin 100 chars") do
42
+ buf = HarfBuzz::Buffer.new
43
+ buf.add_utf8("a" * 100)
44
+ buf.guess_segment_properties
45
+ HarfBuzz.shape(font, buf)
46
+ end
47
+
48
+ x.report("shape with buffer reuse") do |times|
49
+ buf = HarfBuzz::Buffer.new
50
+ times.times do
51
+ buf.clear
52
+ buf.add_utf8("Hello Word")
53
+ buf.guess_segment_properties
54
+ HarfBuzz.shape(font, buf)
55
+ end
56
+ end
57
+
58
+ x.report("create + destroy buffer") do
59
+ HarfBuzz::Buffer.new
60
+ end
61
+
62
+ x.report("glyph outline extraction") do
63
+ draw = HarfBuzz::DrawFuncs.new
64
+ draw.on_move_to { |_x, _y, _| }
65
+ draw.on_line_to { |_x, _y, _| }
66
+ draw.on_cubic_to { |*_args| }
67
+ draw.on_close_path { |_| }
68
+ draw.make_immutable!
69
+ font.draw_glyph(36, draw)
70
+ end
71
+
72
+ x.report("shape_text one-liner") do
73
+ HarfBuzz.shape_text("Hello", font_path: font_path)
74
+ end
75
+
76
+ x.compare!
77
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # examples/basic_shaping.rb — Basic text shaping with HarfBuzz
4
+ #
5
+ # Usage: ruby examples/basic_shaping.rb [path/to/font.ttf]
6
+
7
+ require "harfbuzz"
8
+
9
+ font_path = ARGV[0] ||
10
+ if File.exist?("/System/Library/Fonts/Helvetica.ttc")
11
+ "/System/Library/Fonts/Helvetica.ttc"
12
+ elsif File.exist?("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
13
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
14
+ else
15
+ raise "No font found. Pass font path as argument."
16
+ end
17
+
18
+ puts "=== Basic Shaping ==="
19
+ puts "HarfBuzz #{HarfBuzz.version_string}"
20
+ puts
21
+
22
+ # --- One-liner API ---
23
+ puts "--- shape_text (one-liner) ---"
24
+ result = HarfBuzz.shape_text("Hello, World!", font_path: font_path)
25
+ puts "Glyphs: #{result.length}"
26
+ result.each do |info, pos|
27
+ puts " glyph=#{info.glyph_id} cluster=#{info.cluster} x_advance=#{pos.x_advance}"
28
+ end
29
+ puts "Total X advance: #{result.total_advance.first}"
30
+ puts
31
+
32
+ # --- Manual API ---
33
+ puts "--- Manual Buffer + Font API ---"
34
+ blob = HarfBuzz::Blob.from_file!(font_path)
35
+ face = HarfBuzz::Face.new(blob, 0)
36
+ font = HarfBuzz::Font.new(face)
37
+ buffer = HarfBuzz::Buffer.new
38
+
39
+ buffer.add_utf8("Ruby")
40
+ buffer.guess_segment_properties
41
+ HarfBuzz.shape(font, buffer)
42
+
43
+ puts "Shaped '#{buffer.serialize}'"
44
+ puts "Glyph count: #{buffer.length}"
45
+ buffer.glyph_infos.zip(buffer.glyph_positions).each do |info, pos|
46
+ puts " glyph_id=#{info.glyph_id} x_advance=#{pos.x_advance} y_advance=#{pos.y_advance}"
47
+ end
48
+ puts
49
+
50
+ # --- Buffer reuse pattern ---
51
+ puts "--- Buffer reuse ---"
52
+ texts = ["Hello", "World", "Ruby", "HarfBuzz"]
53
+ texts.each do |text|
54
+ buffer.reset
55
+ buffer.add_utf8(text)
56
+ buffer.guess_segment_properties
57
+ HarfBuzz.shape(font, buffer)
58
+ total_advance = buffer.glyph_positions.sum(&:x_advance)
59
+ puts " '#{text}': #{buffer.length} glyphs, total_advance=#{total_advance}"
60
+ end
61
+ puts
62
+
63
+ # --- RTL shaping ---
64
+ puts "--- RTL (Arabic) ---"
65
+ arabic_result = HarfBuzz.shape_text("مرحبا", font_path: font_path, direction: :rtl)
66
+ puts "Arabic glyphs: #{arabic_result.length}"
67
+ puts "Direction detected: #{HarfBuzz::Buffer.new.tap { |b| b.add_utf8("مرحبا"); b.guess_segment_properties }.direction}"
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # examples/glyph_outlines.rb — Extract glyph outlines using DrawFuncs
4
+ #
5
+ # Usage: ruby examples/glyph_outlines.rb [path/to/font.ttf]
6
+
7
+ require "harfbuzz"
8
+
9
+ font_path = ARGV[0] ||
10
+ if File.exist?("/System/Library/Fonts/Helvetica.ttc")
11
+ "/System/Library/Fonts/Helvetica.ttc"
12
+ elsif File.exist?("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
13
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
14
+ else
15
+ raise "No font found. Pass font path as argument."
16
+ end
17
+
18
+ puts "=== Glyph Outline Extraction ==="
19
+ puts
20
+
21
+ blob = HarfBuzz::Blob.from_file!(font_path)
22
+ face = HarfBuzz::Face.new(blob, 0)
23
+ font = HarfBuzz::Font.new(face)
24
+
25
+ # --- Set up DrawFuncs ---
26
+ draw = HarfBuzz::DrawFuncs.new
27
+
28
+ commands = []
29
+
30
+ draw.on_move_to do |x, y, _draw_state|
31
+ commands << "M #{x.round(2)},#{y.round(2)}"
32
+ end
33
+
34
+ draw.on_line_to do |x, y, _draw_state|
35
+ commands << "L #{x.round(2)},#{y.round(2)}"
36
+ end
37
+
38
+ draw.on_quadratic_to do |cx, cy, x, y, _draw_state|
39
+ commands << "Q #{cx.round(2)},#{cy.round(2)} #{x.round(2)},#{y.round(2)}"
40
+ end
41
+
42
+ draw.on_cubic_to do |c1x, c1y, c2x, c2y, x, y, _draw_state|
43
+ commands << "C #{c1x.round(2)},#{c1y.round(2)} #{c2x.round(2)},#{c2y.round(2)} #{x.round(2)},#{y.round(2)}"
44
+ end
45
+
46
+ draw.on_close_path do |_draw_state|
47
+ commands << "Z"
48
+ end
49
+
50
+ draw.make_immutable!
51
+
52
+ # --- Shape text and get glyph IDs ---
53
+ buffer = HarfBuzz::Buffer.new
54
+ buffer.add_utf8("AB")
55
+ buffer.guess_segment_properties
56
+ HarfBuzz.shape(font, buffer)
57
+
58
+ # --- Extract outline for each glyph ---
59
+ buffer.glyph_infos.each do |info|
60
+ commands.clear
61
+ font.draw_glyph(info.glyph_id, draw)
62
+ puts "Glyph #{info.glyph_id} (cluster #{info.cluster}):"
63
+ if commands.empty?
64
+ puts " (no outline — space or missing)"
65
+ else
66
+ puts " SVG path: #{commands.join(" ")}"
67
+ puts " Commands: #{commands.length}"
68
+ end
69
+ puts
70
+ end
71
+
72
+ # --- Glyph extents ---
73
+ puts "--- Glyph Extents ---"
74
+ buffer.glyph_infos.each do |info|
75
+ extents = font.glyph_extents(info.glyph_id)
76
+ if extents
77
+ puts "Glyph #{info.glyph_id}: x_bearing=#{extents[:x_bearing]} y_bearing=#{extents[:y_bearing]} width=#{extents[:width]} height=#{extents[:height]}"
78
+ end
79
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # examples/opentype_features.rb — OpenType feature queries and shaping with features
4
+ #
5
+ # Usage: ruby examples/opentype_features.rb [path/to/font.ttf]
6
+
7
+ require "harfbuzz"
8
+
9
+ font_path = ARGV[0] ||
10
+ if File.exist?("/System/Library/Fonts/Helvetica.ttc")
11
+ "/System/Library/Fonts/Helvetica.ttc"
12
+ elsif File.exist?("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
13
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
14
+ else
15
+ raise "No font found. Pass font path as argument."
16
+ end
17
+
18
+ puts "=== OpenType Features ==="
19
+ puts
20
+
21
+ blob = HarfBuzz::Blob.from_file!(font_path)
22
+ face = HarfBuzz::Face.new(blob, 0)
23
+ font = HarfBuzz::Font.new(face)
24
+
25
+ # --- List GSUB scripts ---
26
+ puts "--- GSUB Scripts ---"
27
+ begin
28
+ scripts = HarfBuzz::OT::Layout.script_tags(face, :gsub)
29
+ puts "Scripts: #{scripts.map { |t| HarfBuzz.tag_to_s(t) }.inspect}"
30
+ rescue => e
31
+ puts " (#{e.message})"
32
+ end
33
+ puts
34
+
35
+ # --- List GSUB features for default script ---
36
+ puts "--- GSUB Features (first script/language) ---"
37
+ begin
38
+ features = HarfBuzz::OT::Layout.feature_tags(face, :gsub, 0, 0)
39
+ puts "Features: #{features.map { |t| HarfBuzz.tag_to_s(t) }.first(10).inspect}"
40
+ rescue => e
41
+ puts " (#{e.message})"
42
+ end
43
+ puts
44
+
45
+ # --- Shaping with explicit features ---
46
+ puts "--- Shaping with Features ---"
47
+ font2 = HarfBuzz::Font.new(face)
48
+
49
+ buf_with_liga = HarfBuzz::Buffer.new
50
+ buf_with_liga.add_utf8("fi")
51
+ buf_with_liga.guess_segment_properties
52
+ HarfBuzz.shape(font2, buf_with_liga, [HarfBuzz::Feature.from_string("liga")])
53
+ puts "With liga: #{buf_with_liga.length} glyph(s)"
54
+
55
+ buf_no_liga = HarfBuzz::Buffer.new
56
+ buf_no_liga.add_utf8("fi")
57
+ buf_no_liga.guess_segment_properties
58
+ HarfBuzz.shape(font2, buf_no_liga, [HarfBuzz::Feature.from_string("-liga")])
59
+ puts "Without liga: #{buf_no_liga.length} glyph(s)"
60
+ puts
61
+
62
+ # --- Feature from hash ---
63
+ puts "--- Feature from Hash ---"
64
+ features = HarfBuzz::Feature.from_hash(liga: true, kern: false)
65
+ puts "Features: #{features.map(&:to_s).inspect}"
66
+ puts
67
+
68
+ # --- Glyph class query ---
69
+ puts "--- Glyph Classes ---"
70
+ begin
71
+ has_classes = HarfBuzz::OT::Layout.has_glyph_classes?(face)
72
+ puts "Has glyph classes: #{has_classes}"
73
+ if has_classes
74
+ klass = HarfBuzz::OT::Layout.glyph_class(face, 65) # 'A'
75
+ puts " Glyph class of glyph 65: #{klass}"
76
+ end
77
+ rescue => e
78
+ puts " (#{e.message})"
79
+ end
80
+ puts
81
+
82
+ # --- OT Metrics ---
83
+ puts "--- OT Metrics ---"
84
+ begin
85
+ x_height = HarfBuzz::OT::Metrics.x_height(font)
86
+ puts "x-height: #{x_height}"
87
+ cap_height = HarfBuzz::OT::Metrics.cap_height(font)
88
+ puts "cap-height: #{cap_height}"
89
+ rescue => e
90
+ puts " (#{e.message})"
91
+ end