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
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ # Wraps hb_unicode_funcs_t — Unicode property callbacks
5
+ class UnicodeFuncs
6
+ attr_reader :ptr
7
+
8
+ # Creates a new UnicodeFuncs, optionally inheriting from a parent
9
+ # @param parent [UnicodeFuncs, nil] Parent funcs (nil = use default)
10
+ def initialize(parent = nil)
11
+ parent_ptr = parent ? parent.ptr : C.hb_unicode_funcs_get_default
12
+ @ptr = C.hb_unicode_funcs_create(parent_ptr)
13
+ raise AllocationError, "Failed to create unicode_funcs" if @ptr.null?
14
+
15
+ HarfBuzz::UnicodeFuncs.define_finalizer(self, @ptr)
16
+ end
17
+
18
+ # Returns the default Unicode functions
19
+ # @return [UnicodeFuncs]
20
+ def self.default
21
+ wrap_borrowed(C.hb_unicode_funcs_get_default)
22
+ end
23
+
24
+ # Returns the empty Unicode functions
25
+ # @return [UnicodeFuncs]
26
+ def self.empty
27
+ wrap_borrowed(C.hb_unicode_funcs_get_empty)
28
+ end
29
+
30
+ # Returns the parent functions
31
+ # @return [UnicodeFuncs]
32
+ def parent
33
+ self.class.wrap_borrowed(C.hb_unicode_funcs_get_parent(@ptr))
34
+ end
35
+
36
+ # @return [Boolean] true if immutable
37
+ def immutable?
38
+ C.from_hb_bool(C.hb_unicode_funcs_is_immutable(@ptr))
39
+ end
40
+
41
+ # Makes immutable
42
+ # @return [self]
43
+ def make_immutable!
44
+ C.hb_unicode_funcs_make_immutable(@ptr)
45
+ self
46
+ end
47
+
48
+ # Sets a custom general_category function
49
+ # @yield [codepoint] Returns general category symbol
50
+ def on_general_category(&block)
51
+ @general_category_callback = block
52
+ cb = FFI::Function.new(:int, [:pointer, :uint32, :pointer]) do |_ufuncs, cp, _user_data|
53
+ result = block.call(cp)
54
+ C::UnicodeGeneralCategory[result] || result
55
+ end
56
+ @general_category_ffi = cb
57
+ C.hb_unicode_funcs_set_general_category_func(@ptr, cb, nil, nil)
58
+ end
59
+
60
+ # Sets a custom combining_class function
61
+ # @yield [codepoint] Returns combining class integer
62
+ def on_combining_class(&block)
63
+ @combining_class_callback = block
64
+ cb = FFI::Function.new(:uint, [:pointer, :uint32, :pointer]) do |_ufuncs, cp, _user_data|
65
+ block.call(cp)
66
+ end
67
+ @combining_class_ffi = cb
68
+ C.hb_unicode_funcs_set_combining_class_func(@ptr, cb, nil, nil)
69
+ end
70
+
71
+ # Sets a custom mirroring function
72
+ # @yield [codepoint] Returns mirrored codepoint
73
+ def on_mirroring(&block)
74
+ @mirroring_callback = block
75
+ cb = FFI::Function.new(:uint32, [:pointer, :uint32, :pointer]) do |_ufuncs, cp, _user_data|
76
+ block.call(cp)
77
+ end
78
+ @mirroring_ffi = cb
79
+ C.hb_unicode_funcs_set_mirroring_func(@ptr, cb, nil, nil)
80
+ end
81
+
82
+ # Sets a custom script function
83
+ # @yield [codepoint] Returns script value
84
+ def on_script(&block)
85
+ @script_callback = block
86
+ cb = FFI::Function.new(:uint32, [:pointer, :uint32, :pointer]) do |_ufuncs, cp, _user_data|
87
+ block.call(cp)
88
+ end
89
+ @script_ffi = cb
90
+ C.hb_unicode_funcs_set_script_func(@ptr, cb, nil, nil)
91
+ end
92
+
93
+ # Sets a custom compose function
94
+ # @yield [a, b] Returns composed codepoint or nil
95
+ def on_compose(&block)
96
+ @compose_callback = block
97
+ cb = FFI::Function.new(:int, [:pointer, :uint32, :uint32, :pointer, :pointer]) do
98
+ |_ufuncs, a, b, ab_ptr, _user_data|
99
+ result = block.call(a, b)
100
+ if result
101
+ ab_ptr.write_uint32(result)
102
+ 1
103
+ else
104
+ 0
105
+ end
106
+ end
107
+ @compose_ffi = cb
108
+ C.hb_unicode_funcs_set_compose_func(@ptr, cb, nil, nil)
109
+ end
110
+
111
+ # Sets a custom decompose function
112
+ # @yield [ab] Returns [a, b] array or nil
113
+ def on_decompose(&block)
114
+ @decompose_callback = block
115
+ cb = FFI::Function.new(:int, [:pointer, :uint32, :pointer, :pointer, :pointer]) do
116
+ |_ufuncs, ab, a_ptr, b_ptr, _user_data|
117
+ result = block.call(ab)
118
+ if result
119
+ a_ptr.write_uint32(result[0])
120
+ b_ptr.write_uint32(result[1])
121
+ 1
122
+ else
123
+ 0
124
+ end
125
+ end
126
+ @decompose_ffi = cb
127
+ C.hb_unicode_funcs_set_decompose_func(@ptr, cb, nil, nil)
128
+ end
129
+
130
+ # Queries the general category of a codepoint
131
+ # @param cp [Integer] Unicode codepoint
132
+ # @return [Symbol] General category
133
+ def general_category(cp)
134
+ C.hb_unicode_general_category(@ptr, cp)
135
+ end
136
+
137
+ # Queries the combining class of a codepoint
138
+ # @param cp [Integer] Unicode codepoint
139
+ # @return [Integer] Combining class
140
+ def combining_class(cp)
141
+ C.hb_unicode_combining_class(@ptr, cp)
142
+ end
143
+
144
+ # Queries the mirror codepoint
145
+ # @param cp [Integer] Unicode codepoint
146
+ # @return [Integer] Mirrored codepoint (or same if no mirror)
147
+ def mirroring(cp)
148
+ C.hb_unicode_mirroring(@ptr, cp)
149
+ end
150
+
151
+ # Queries the script of a codepoint
152
+ # @param cp [Integer] Unicode codepoint
153
+ # @return [Integer] Script value
154
+ def script(cp)
155
+ C.hb_unicode_script(@ptr, cp)
156
+ end
157
+
158
+ # Composes two codepoints into one
159
+ # @param a [Integer] First codepoint
160
+ # @param b [Integer] Second codepoint
161
+ # @return [Integer, nil] Composed codepoint or nil
162
+ def compose(a, b)
163
+ ab_ptr = FFI::MemoryPointer.new(:uint32)
164
+ ok = C.hb_unicode_compose(@ptr, a, b, ab_ptr)
165
+ ok.zero? ? nil : ab_ptr.read_uint32
166
+ end
167
+
168
+ # Decomposes a codepoint into two
169
+ # @param cp [Integer] Codepoint to decompose
170
+ # @return [Array<Integer>, nil] [a, b] or nil
171
+ def decompose(cp)
172
+ a_ptr = FFI::MemoryPointer.new(:uint32)
173
+ b_ptr = FFI::MemoryPointer.new(:uint32)
174
+ ok = C.hb_unicode_decompose(@ptr, cp, a_ptr, b_ptr)
175
+ ok.zero? ? nil : [a_ptr.read_uint32, b_ptr.read_uint32]
176
+ end
177
+
178
+ def inspect
179
+ "#<HarfBuzz::UnicodeFuncs immutable=#{immutable?}>"
180
+ end
181
+
182
+ def self.wrap_owned(ptr)
183
+ obj = allocate
184
+ obj.instance_variable_set(:@ptr, ptr)
185
+ define_finalizer(obj, ptr)
186
+ obj
187
+ end
188
+
189
+ def self.wrap_borrowed(ptr)
190
+ obj = allocate
191
+ obj.instance_variable_set(:@ptr, ptr)
192
+ obj.instance_variable_set(:@borrowed, true)
193
+ obj
194
+ end
195
+
196
+ def self.define_finalizer(obj, ptr)
197
+ destroy = C.method(:hb_unicode_funcs_destroy)
198
+ ObjectSpace.define_finalizer(obj, proc { destroy.call(ptr) })
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ # Represents an OpenType variation axis value (e.g., "wght=700")
5
+ class Variation
6
+ def initialize(struct)
7
+ @struct = struct
8
+ end
9
+
10
+ # @return [Integer] Variation axis tag as uint32
11
+ def tag
12
+ @struct[:tag]
13
+ end
14
+
15
+ # @return [Float] Variation axis value
16
+ def value
17
+ @struct[:value]
18
+ end
19
+
20
+ # Parses a variation string such as "wght=700" or "ital=1"
21
+ # @param str [String] Variation string
22
+ # @return [Variation] Parsed variation
23
+ # @raise [VariationParseError] If the string cannot be parsed
24
+ def self.from_string(str)
25
+ struct = C::HbVariationT.new
26
+ result = C.hb_variation_from_string(str, str.bytesize, struct)
27
+ raise VariationParseError, "Invalid variation string: #{str.inspect}" if result.zero?
28
+
29
+ new(struct)
30
+ end
31
+
32
+ # Returns the FFI struct for passing to C functions
33
+ # @return [C::HbVariationT]
34
+ def to_struct
35
+ @struct
36
+ end
37
+
38
+ # @return [String] Variation string representation
39
+ def to_s
40
+ buf = FFI::MemoryPointer.new(:char, 64)
41
+ C.hb_variation_to_string(@struct, buf, 64)
42
+ buf.read_string
43
+ end
44
+
45
+ def inspect
46
+ "#<HarfBuzz::Variation #{to_s}>"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Compatibility alias: require "harfbuzz-ffi" is equivalent to require "harfbuzz"
4
+ require_relative "harfbuzz"
data/lib/harfbuzz.rb ADDED
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "harfbuzz/version"
4
+ require_relative "harfbuzz/error"
5
+ require_relative "harfbuzz/library"
6
+
7
+ # Load FFI low-level layer
8
+ require_relative "harfbuzz/c/base"
9
+ require_relative "harfbuzz/c/enums"
10
+ require_relative "harfbuzz/c/structs"
11
+ require_relative "harfbuzz/c/version"
12
+ require_relative "harfbuzz/c/common"
13
+ require_relative "harfbuzz/c/blob"
14
+ require_relative "harfbuzz/c/buffer"
15
+ require_relative "harfbuzz/c/face"
16
+ require_relative "harfbuzz/c/font"
17
+ require_relative "harfbuzz/c/font_funcs"
18
+ require_relative "harfbuzz/c/shape"
19
+ require_relative "harfbuzz/c/map"
20
+ require_relative "harfbuzz/c/set"
21
+ require_relative "harfbuzz/c/shape_plan"
22
+ require_relative "harfbuzz/c/unicode"
23
+ require_relative "harfbuzz/c/draw"
24
+ require_relative "harfbuzz/c/paint"
25
+ require_relative "harfbuzz/c/ot/layout"
26
+ require_relative "harfbuzz/c/ot/var"
27
+ require_relative "harfbuzz/c/ot/color"
28
+ require_relative "harfbuzz/c/ot/math"
29
+ require_relative "harfbuzz/c/ot/meta"
30
+ require_relative "harfbuzz/c/ot/metrics"
31
+ require_relative "harfbuzz/c/ot/name"
32
+ require_relative "harfbuzz/c/ot/shape"
33
+ require_relative "harfbuzz/c/ot/font"
34
+ require_relative "harfbuzz/c/aat/layout"
35
+
36
+ # Load high-level Ruby layer
37
+ require_relative "harfbuzz/glyph_info"
38
+ require_relative "harfbuzz/glyph_position"
39
+ require_relative "harfbuzz/feature"
40
+ require_relative "harfbuzz/variation"
41
+ require_relative "harfbuzz/blob"
42
+ require_relative "harfbuzz/buffer"
43
+ require_relative "harfbuzz/face"
44
+ require_relative "harfbuzz/font"
45
+ require_relative "harfbuzz/font_funcs"
46
+ require_relative "harfbuzz/flags"
47
+ require_relative "harfbuzz/map"
48
+ require_relative "harfbuzz/set"
49
+ require_relative "harfbuzz/shape_plan"
50
+ require_relative "harfbuzz/unicode_funcs"
51
+ require_relative "harfbuzz/draw_funcs"
52
+ require_relative "harfbuzz/paint_funcs"
53
+ require_relative "harfbuzz/shaping_result"
54
+
55
+ # OT namespace
56
+ module HarfBuzz
57
+ module OT; end
58
+ end
59
+ require_relative "harfbuzz/ot/layout"
60
+ require_relative "harfbuzz/ot/var"
61
+ require_relative "harfbuzz/ot/color"
62
+ require_relative "harfbuzz/ot/math"
63
+ require_relative "harfbuzz/ot/meta"
64
+ require_relative "harfbuzz/ot/metrics"
65
+ require_relative "harfbuzz/ot/name"
66
+ require_relative "harfbuzz/ot/shape"
67
+ require_relative "harfbuzz/ot/font"
68
+
69
+ # AAT namespace
70
+ module HarfBuzz
71
+ module AAT; end
72
+ end
73
+ require_relative "harfbuzz/aat/layout"
74
+
75
+ # Subset (loads libharfbuzz-subset if available)
76
+ require_relative "harfbuzz/c/subset"
77
+ require_relative "harfbuzz/subset"
78
+
79
+ # HarfBuzz Ruby FFI Bindings
80
+ #
81
+ # This module provides complete Ruby bindings for the HarfBuzz text shaping engine.
82
+ # It uses FFI to provide both low-level C API access (HarfBuzz::C) and high-level
83
+ # Ruby-friendly interfaces.
84
+ #
85
+ # @example Basic shaping
86
+ # blob = HarfBuzz::Blob.from_file!("font.ttf")
87
+ # face = HarfBuzz::Face.new(blob, 0)
88
+ # font = HarfBuzz::Font.new(face)
89
+ # buffer = HarfBuzz::Buffer.new
90
+ # buffer.add_utf8("Hello")
91
+ # buffer.guess_segment_properties
92
+ # HarfBuzz.shape(font, buffer)
93
+ module HarfBuzz
94
+ # Returns the HarfBuzz library version as an array [major, minor, micro]
95
+ # @return [Array<Integer>] Version array
96
+ def self.version
97
+ major_ptr = FFI::MemoryPointer.new(:uint)
98
+ minor_ptr = FFI::MemoryPointer.new(:uint)
99
+ micro_ptr = FFI::MemoryPointer.new(:uint)
100
+
101
+ C.hb_version(major_ptr, minor_ptr, micro_ptr)
102
+
103
+ [major_ptr.read_uint, minor_ptr.read_uint, micro_ptr.read_uint]
104
+ end
105
+
106
+ # Returns the HarfBuzz library version as a string
107
+ # @return [String] Version string (e.g., "8.3.0")
108
+ def self.version_string
109
+ C.hb_version_string
110
+ end
111
+
112
+ # Checks if the library version is at least the specified version
113
+ # @param major [Integer] Major version
114
+ # @param minor [Integer] Minor version
115
+ # @param micro [Integer] Micro version
116
+ # @return [Boolean] true if version >= specified
117
+ def self.version_atleast?(major, minor, micro)
118
+ C.from_hb_bool(C.hb_version_atleast(major, minor, micro))
119
+ end
120
+
121
+ class << self
122
+ alias version_at_least? version_atleast?
123
+ end
124
+
125
+ # Converts a 4-character string, Symbol, or Integer to an OpenType tag (uint32)
126
+ # @param str [String, Symbol, Integer] Tag as string (e.g., "GSUB"), symbol, or uint32 integer
127
+ # @return [Integer] Tag as uint32
128
+ def self.tag(str)
129
+ case str
130
+ when Integer then str
131
+ when Symbol then tag(str.to_s)
132
+ when String then C.hb_tag_from_string(str, str.bytesize)
133
+ else raise InvalidArgumentError, "Cannot convert #{str.class} to tag"
134
+ end
135
+ end
136
+
137
+ # Converts an OpenType tag (uint32) to a 4-character string
138
+ # @param tag [Integer] Tag as uint32
139
+ # @return [String] 4-character tag string
140
+ def self.tag_to_s(tag)
141
+ buf = FFI::MemoryPointer.new(:char, 4)
142
+ C.hb_tag_to_string(tag, buf)
143
+ buf.read_bytes(4)
144
+ end
145
+
146
+ # Converts a direction string to a direction enum value
147
+ # @param str [String] Direction string (e.g., "ltr")
148
+ # @return [Symbol] Direction symbol
149
+ def self.direction(str)
150
+ C.hb_direction_from_string(str, str.bytesize)
151
+ end
152
+
153
+ # Returns a language opaque pointer from a BCP 47 language tag string
154
+ # @param str [String] BCP 47 language tag (e.g., "en", "ar")
155
+ # @return [FFI::Pointer] Language pointer
156
+ def self.language(str)
157
+ C.hb_language_from_string(str, str.bytesize)
158
+ end
159
+
160
+ # Returns the default language
161
+ # @return [FFI::Pointer] Default language pointer
162
+ def self.default_language
163
+ C.hb_language_get_default
164
+ end
165
+
166
+ # Returns the BCP 47 string representation of a language pointer
167
+ # @param lang [FFI::Pointer] Language pointer (from HarfBuzz.language)
168
+ # @return [String] BCP 47 language tag string
169
+ def self.language_to_s(lang)
170
+ C.hb_language_to_string(lang)
171
+ end
172
+
173
+ # Checks if two language tags match (BCP 47 prefix comparison)
174
+ # @param lang1 [FFI::Pointer] First language pointer
175
+ # @param lang2 [FFI::Pointer] Second language pointer
176
+ # @return [Boolean] true if languages match
177
+ def self.language_matches?(lang1, lang2)
178
+ C.from_hb_bool(C.hb_language_matches(lang1, lang2))
179
+ end
180
+
181
+ # Converts a script name string to a script value
182
+ # @param str [String] Script name (e.g., "Arab", "Latn")
183
+ # @return [Integer] Script value
184
+ def self.script(str)
185
+ C.hb_script_from_string(str, str.bytesize)
186
+ end
187
+
188
+ # Converts an ISO 15924 OpenType tag to a script value
189
+ # @param tag [Integer] ISO 15924 tag (e.g., HarfBuzz.tag("Arab"))
190
+ # @return [Integer] Script value
191
+ def self.script_from_tag(tag)
192
+ C.hb_script_from_iso15924_tag(tag)
193
+ end
194
+
195
+ # Extracts the alpha component from an RGBA color value
196
+ # @param color [Integer] RGBA color as uint32
197
+ # @return [Integer] Alpha (0-255)
198
+ def self.color_alpha(color)
199
+ C.hb_color_get_alpha(color)
200
+ end
201
+
202
+ # Extracts the red component from an RGBA color value
203
+ # @param color [Integer] RGBA color as uint32
204
+ # @return [Integer] Red (0-255)
205
+ def self.color_red(color)
206
+ C.hb_color_get_red(color)
207
+ end
208
+
209
+ # Extracts the green component from an RGBA color value
210
+ # @param color [Integer] RGBA color as uint32
211
+ # @return [Integer] Green (0-255)
212
+ def self.color_green(color)
213
+ C.hb_color_get_green(color)
214
+ end
215
+
216
+ # Extracts the blue component from an RGBA color value
217
+ # @param color [Integer] RGBA color as uint32
218
+ # @return [Integer] Blue (0-255)
219
+ def self.color_blue(color)
220
+ C.hb_color_get_blue(color)
221
+ end
222
+
223
+ # Returns the horizontal direction for a script
224
+ # @param script [Integer] Script value
225
+ # @return [Symbol] Direction symbol (:ltr or :rtl)
226
+ def self.script_horizontal_direction(script)
227
+ C.hb_script_get_horizontal_direction(script)
228
+ end
229
+
230
+ # Shapes text in the buffer using the font
231
+ # @param font [Font] Font to use for shaping
232
+ # @param buffer [Buffer] Buffer containing text to shape
233
+ # @param features [Array<Feature>] Optional features to apply
234
+ # @param shapers [Array<String>, nil] Optional list of shapers to try
235
+ def self.shape(font, buffer, features = [], shapers: nil)
236
+ features_ptr = build_features_ptr(features)
237
+
238
+ if shapers
239
+ shapers_ptrs = shapers.map { |s| FFI::MemoryPointer.from_string(s) }
240
+ shapers_ptrs << FFI::Pointer::NULL
241
+ shapers_ptr = FFI::MemoryPointer.new(:pointer, shapers_ptrs.size)
242
+ shapers_ptrs.each_with_index { |p, i| shapers_ptr[i].put_pointer(0, p) }
243
+ C.hb_shape_full(font.ptr, buffer.ptr, features_ptr, features.size, shapers_ptr)
244
+ else
245
+ C.hb_shape(font.ptr, buffer.ptr, features_ptr, features.size)
246
+ end
247
+ end
248
+
249
+ # Returns the list of available shapers
250
+ # @return [Array<String>] List of shaper names
251
+ def self.shapers
252
+ ptr = C.hb_shape_list_shapers
253
+ result = []
254
+ i = 0
255
+ loop do
256
+ p = ptr.get_pointer(i * FFI::Pointer.size)
257
+ break if p.null?
258
+
259
+ result << p.read_string
260
+ i += 1
261
+ end
262
+ result
263
+ end
264
+
265
+ # High-level shaping convenience method
266
+ #
267
+ # @param text [String] Text to shape
268
+ # @param font_path [String] Path to font file
269
+ # @param features [Array<Feature, String>, Hash] Features to apply
270
+ # @param direction [Symbol, nil] Text direction (:ltr, :rtl, etc.)
271
+ # @param script [Integer, nil] Script value
272
+ # @param language [String, nil] BCP 47 language tag (e.g., "en")
273
+ # @return [ShapingResult]
274
+ def self.shape_text(text, font_path:, features: [], direction: nil, script: nil, language: nil)
275
+ blob = Blob.from_file!(font_path)
276
+ face = Face.new(blob, 0)
277
+ font = Font.new(face)
278
+
279
+ buffer = Buffer.new
280
+ buffer.add_utf8(text)
281
+ buffer.direction = direction if direction
282
+ buffer.script = script if script
283
+ buffer.language = self.language(language) if language
284
+ buffer.guess_segment_properties
285
+
286
+ parsed_features = case features
287
+ when Array
288
+ features.map { |f| f.is_a?(Feature) ? f : Feature.from_string(f.to_s) }
289
+ when Hash
290
+ Feature.from_hash(features)
291
+ else
292
+ []
293
+ end
294
+
295
+ shape(font, buffer, parsed_features)
296
+ ShapingResult.new(buffer: buffer, font: font)
297
+ end
298
+
299
+ def self.build_features_ptr(features)
300
+ return FFI::Pointer::NULL if features.empty?
301
+
302
+ ptr = FFI::MemoryPointer.new(C::HbFeatureT, features.size)
303
+ features.each_with_index do |f, i|
304
+ s = f.to_struct
305
+ ptr.put_bytes(i * C::HbFeatureT.size, s.to_ptr.read_bytes(C::HbFeatureT.size))
306
+ end
307
+ ptr
308
+ end
309
+ private_class_method :build_features_ptr
310
+ end
311
+
312
+ # Alias for compatibility (Harfbuzz::VERSION was previously defined here)
313
+ Harfbuzz = HarfBuzz unless defined?(Harfbuzz)