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,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ # Wraps hb_set_t — a set of Unicode codepoints or glyph IDs
5
+ #
6
+ # Includes Enumerable for Ruby-friendly iteration.
7
+ class Set
8
+ include Enumerable
9
+
10
+ HB_SET_VALUE_INVALID = C::HB_SET_VALUE_INVALID
11
+
12
+ attr_reader :ptr
13
+
14
+ def initialize
15
+ @ptr = C.hb_set_create
16
+ raise AllocationError, "Failed to create set" if @ptr.null?
17
+
18
+ HarfBuzz::Set.define_finalizer(self, @ptr)
19
+ end
20
+
21
+ # Returns the singleton empty set
22
+ # @return [Set]
23
+ def self.empty
24
+ wrap_borrowed(C.hb_set_get_empty)
25
+ end
26
+
27
+ # @return [Integer] Number of elements
28
+ def size
29
+ C.hb_set_get_population(@ptr)
30
+ end
31
+
32
+ alias length size
33
+
34
+ # @return [Boolean] true if empty
35
+ def empty?
36
+ C.from_hb_bool(C.hb_set_is_empty(@ptr))
37
+ end
38
+
39
+ # @param value [Integer] Codepoint to check
40
+ # @return [Boolean] true if value is in the set
41
+ def include?(value)
42
+ C.from_hb_bool(C.hb_set_has(@ptr, value))
43
+ end
44
+
45
+ alias member? include?
46
+
47
+ # Adds a value to the set
48
+ # @param value [Integer] Codepoint to add
49
+ # @return [self]
50
+ def add(value)
51
+ C.hb_set_add(@ptr, value)
52
+ self
53
+ end
54
+
55
+ alias << add
56
+
57
+ # Adds a range of values (inclusive)
58
+ # @param first [Integer] First codepoint
59
+ # @param last [Integer] Last codepoint
60
+ # @return [self]
61
+ def add_range(first, last)
62
+ C.hb_set_add_range(@ptr, first, last)
63
+ self
64
+ end
65
+
66
+ # Adds a sorted array of codepoints
67
+ # @param arr [Array<Integer>] Sorted array of codepoints
68
+ # @return [self]
69
+ def add_sorted_array(arr)
70
+ mem = FFI::MemoryPointer.new(:uint32, arr.size)
71
+ mem.put_array_of_uint32(0, arr)
72
+ C.hb_set_add_sorted_array(@ptr, mem, arr.size)
73
+ self
74
+ end
75
+
76
+ # Removes a value
77
+ # @param value [Integer] Codepoint to remove
78
+ # @return [self]
79
+ def delete(value)
80
+ C.hb_set_del(@ptr, value)
81
+ self
82
+ end
83
+
84
+ # Removes a range of values (inclusive)
85
+ # @param first [Integer] First codepoint
86
+ # @param last [Integer] Last codepoint
87
+ # @return [self]
88
+ def delete_range(first, last)
89
+ C.hb_set_del_range(@ptr, first, last)
90
+ self
91
+ end
92
+
93
+ # Clears all values
94
+ # @return [self]
95
+ def clear
96
+ C.hb_set_clear(@ptr)
97
+ self
98
+ end
99
+
100
+ # @param other [Set] Set to compare
101
+ # @return [Boolean] true if equal
102
+ def ==(other)
103
+ return false unless other.is_a?(Set)
104
+
105
+ C.from_hb_bool(C.hb_set_is_equal(@ptr, other.ptr))
106
+ end
107
+
108
+ # @return [Integer] Hash of this set
109
+ def hash
110
+ C.hb_set_hash(@ptr)
111
+ end
112
+
113
+ # @param other [Set] Potential superset
114
+ # @return [Boolean] true if self is a subset of other
115
+ def subset?(other)
116
+ C.from_hb_bool(C.hb_set_is_subset(@ptr, other.ptr))
117
+ end
118
+
119
+ # Replaces contents with another set
120
+ # @param other [Set] Source set
121
+ # @return [self]
122
+ def replace(other)
123
+ C.hb_set_set(@ptr, other.ptr)
124
+ self
125
+ end
126
+
127
+ # Adds all elements from another set (in-place union)
128
+ # @param other [Set] Set to union with
129
+ # @return [self]
130
+ def union(other)
131
+ result = dup_set
132
+ C.hb_set_union(result.ptr, other.ptr)
133
+ result
134
+ end
135
+
136
+ alias | union
137
+
138
+ # Removes all elements not in another set (in-place intersection)
139
+ # @param other [Set] Set to intersect with
140
+ # @return [self]
141
+ def intersect(other)
142
+ result = dup_set
143
+ C.hb_set_intersect(result.ptr, other.ptr)
144
+ result
145
+ end
146
+
147
+ alias & intersect
148
+
149
+ # Removes all elements that are in another set (in-place subtraction)
150
+ # @param other [Set] Set to subtract
151
+ # @return [self]
152
+ def subtract(other)
153
+ result = dup_set
154
+ C.hb_set_subtract(result.ptr, other.ptr)
155
+ result
156
+ end
157
+
158
+ alias - subtract
159
+
160
+ # Symmetric difference (elements in either but not both)
161
+ # @param other [Set] Other set
162
+ # @return [Set] New set
163
+ def symmetric_difference(other)
164
+ result = dup_set
165
+ C.hb_set_symmetric_difference(result.ptr, other.ptr)
166
+ result
167
+ end
168
+
169
+ alias ^ symmetric_difference
170
+
171
+ # Inverts the set (complements it)
172
+ # @return [self]
173
+ def invert!
174
+ C.hb_set_invert(@ptr)
175
+ self
176
+ end
177
+
178
+ # @return [Integer, nil] Minimum value or nil if empty
179
+ def min
180
+ val = C.hb_set_get_min(@ptr)
181
+ val == HB_SET_VALUE_INVALID ? nil : val
182
+ end
183
+
184
+ # @return [Integer, nil] Maximum value or nil if empty
185
+ def max
186
+ val = C.hb_set_get_max(@ptr)
187
+ val == HB_SET_VALUE_INVALID ? nil : val
188
+ end
189
+
190
+ # Iterates over all values in ascending order
191
+ # @yield [value]
192
+ # @return [Enumerator] if no block given
193
+ def each
194
+ return to_enum(:each) unless block_given?
195
+
196
+ cp_ptr = FFI::MemoryPointer.new(:uint32)
197
+ cp_ptr.write_uint32(HB_SET_VALUE_INVALID)
198
+ yield cp_ptr.read_uint32 while C.from_hb_bool(C.hb_set_next(@ptr, cp_ptr))
199
+ self
200
+ end
201
+
202
+ # Iterates over contiguous ranges in ascending order
203
+ # @yield [first, last] First and last value of each range (inclusive)
204
+ # @return [Enumerator] if no block given
205
+ def each_range
206
+ return to_enum(:each_range) unless block_given?
207
+
208
+ first_ptr = FFI::MemoryPointer.new(:uint32)
209
+ last_ptr = FFI::MemoryPointer.new(:uint32)
210
+ first_ptr.write_uint32(HB_SET_VALUE_INVALID)
211
+ last_ptr.write_uint32(HB_SET_VALUE_INVALID)
212
+ while C.from_hb_bool(C.hb_set_next_range(@ptr, first_ptr, last_ptr))
213
+ yield first_ptr.read_uint32, last_ptr.read_uint32
214
+ end
215
+ self
216
+ end
217
+
218
+ # Iterates over all values in descending order
219
+ # @yield [value]
220
+ # @return [Enumerator] if no block given
221
+ def reverse_each
222
+ return to_enum(:reverse_each) unless block_given?
223
+
224
+ cp_ptr = FFI::MemoryPointer.new(:uint32)
225
+ cp_ptr.write_uint32(HB_SET_VALUE_INVALID)
226
+ yield cp_ptr.read_uint32 while C.from_hb_bool(C.hb_set_previous(@ptr, cp_ptr))
227
+ self
228
+ end
229
+
230
+ # Bulk-retrieves all values as an array
231
+ # @return [Array<Integer>]
232
+ def to_a
233
+ return [] if empty?
234
+
235
+ count = size
236
+ buf = FFI::MemoryPointer.new(:uint32, count)
237
+ actual = C.hb_set_next_many(@ptr, HB_SET_VALUE_INVALID, buf, count)
238
+ buf.read_array_of_uint32(actual)
239
+ end
240
+
241
+ def inspect
242
+ "#<HarfBuzz::Set size=#{size}>"
243
+ end
244
+
245
+ def self.wrap_owned(ptr)
246
+ obj = allocate
247
+ obj.instance_variable_set(:@ptr, ptr)
248
+ define_finalizer(obj, ptr)
249
+ obj
250
+ end
251
+
252
+ def self.wrap_borrowed(ptr)
253
+ obj = allocate
254
+ obj.instance_variable_set(:@ptr, ptr)
255
+ obj.instance_variable_set(:@borrowed, true)
256
+ obj
257
+ end
258
+
259
+ def self.define_finalizer(obj, ptr)
260
+ destroy = C.method(:hb_set_destroy)
261
+ ObjectSpace.define_finalizer(obj, proc { destroy.call(ptr) })
262
+ end
263
+
264
+ private
265
+
266
+ def dup_set
267
+ new_set = self.class.new
268
+ C.hb_set_set(new_set.ptr, @ptr)
269
+ new_set
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ # Wraps hb_shape_plan_t — a cached shaping plan for repeated shaping
5
+ class ShapePlan
6
+ attr_reader :ptr
7
+
8
+ # Creates a new ShapePlan for the given face and properties
9
+ # @param face [Face] Font face
10
+ # @param props [C::HbSegmentPropertiesT] Segment properties
11
+ # @param features [Array<Feature>] Features
12
+ # @param shapers [Array<String>, nil] Shaper list
13
+ # @param coords [Array<Integer>, nil] Normalized variation coordinates (nil = none)
14
+ # @return [ShapePlan]
15
+ def self.new(face, props, features = [], shapers: nil, coords: nil)
16
+ features_ptr = HarfBuzz.send(:build_features_ptr, features)
17
+ shapers_ptr = build_shapers_ptr(shapers)
18
+ if coords
19
+ coords_ptr = FFI::MemoryPointer.new(:int32, coords.size)
20
+ coords_ptr.put_array_of_int32(0, coords)
21
+ ptr = C.hb_shape_plan_create2(
22
+ face.ptr, props, features_ptr, features.size,
23
+ shapers_ptr, coords.size, coords_ptr
24
+ )
25
+ else
26
+ ptr = C.hb_shape_plan_create(
27
+ face.ptr, props, features_ptr, features.size, shapers_ptr
28
+ )
29
+ end
30
+ wrap_owned(ptr)
31
+ end
32
+
33
+ # Creates (or retrieves from cache) a ShapePlan
34
+ # @param face [Face] Font face
35
+ # @param props [C::HbSegmentPropertiesT] Segment properties
36
+ # @param features [Array<Feature>] Features
37
+ # @param shapers [Array<String>, nil] Shaper list
38
+ # @param coords [Array<Integer>, nil] Normalized variation coordinates (nil = none)
39
+ # @return [ShapePlan]
40
+ def self.cached(face, props, features = [], shapers: nil, coords: nil)
41
+ features_ptr = HarfBuzz.send(:build_features_ptr, features)
42
+ shapers_ptr = build_shapers_ptr(shapers)
43
+ if coords
44
+ coords_ptr = FFI::MemoryPointer.new(:int32, coords.size)
45
+ coords_ptr.put_array_of_int32(0, coords)
46
+ ptr = C.hb_shape_plan_create_cached2(
47
+ face.ptr, props, features_ptr, features.size,
48
+ shapers_ptr, coords.size, coords_ptr
49
+ )
50
+ else
51
+ ptr = C.hb_shape_plan_create_cached(
52
+ face.ptr, props, features_ptr, features.size, shapers_ptr
53
+ )
54
+ end
55
+ wrap_owned(ptr)
56
+ end
57
+
58
+ # Returns the singleton empty shape plan
59
+ # @return [ShapePlan]
60
+ def self.empty
61
+ wrap_borrowed(C.hb_shape_plan_get_empty)
62
+ end
63
+
64
+ # Executes the shape plan
65
+ # @param font [Font] Font to use
66
+ # @param buffer [Buffer] Buffer to shape
67
+ # @param features [Array<Feature>] Features
68
+ # @return [Boolean] Success
69
+ def execute(font, buffer, features = [])
70
+ features_ptr = HarfBuzz.send(:build_features_ptr, features)
71
+ C.from_hb_bool(
72
+ C.hb_shape_plan_execute(@ptr, font.ptr, buffer.ptr, features_ptr, features.size)
73
+ )
74
+ end
75
+
76
+ # @return [String] Name of the shaper used
77
+ def shaper
78
+ C.hb_shape_plan_get_shaper(@ptr)
79
+ end
80
+
81
+ def inspect
82
+ "#<HarfBuzz::ShapePlan shaper=#{shaper.inspect}>"
83
+ end
84
+
85
+ def self.wrap_owned(ptr)
86
+ obj = allocate
87
+ obj.instance_variable_set(:@ptr, ptr)
88
+ define_finalizer(obj, ptr)
89
+ obj
90
+ end
91
+
92
+ def self.wrap_borrowed(ptr)
93
+ obj = allocate
94
+ obj.instance_variable_set(:@ptr, ptr)
95
+ obj.instance_variable_set(:@borrowed, true)
96
+ obj
97
+ end
98
+
99
+ def self.define_finalizer(obj, ptr)
100
+ destroy = C.method(:hb_shape_plan_destroy)
101
+ ObjectSpace.define_finalizer(obj, proc { destroy.call(ptr) })
102
+ end
103
+
104
+ def self.build_shapers_ptr(shapers)
105
+ return FFI::Pointer::NULL unless shapers
106
+
107
+ ptrs = shapers.map { |s| FFI::MemoryPointer.from_string(s) }
108
+ ptrs << FFI::Pointer::NULL
109
+ arr = FFI::MemoryPointer.new(:pointer, ptrs.size)
110
+ ptrs.each_with_index { |p, i| arr[i].put_pointer(0, p) }
111
+ arr
112
+ end
113
+ private_class_method :build_shapers_ptr
114
+ end
115
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ # Value object returned by HarfBuzz.shape_text — provides convenient access
5
+ # to shaped glyphs, positions, and rendering helpers.
6
+ class ShapingResult
7
+ include Enumerable
8
+
9
+ attr_reader :buffer, :font
10
+
11
+ # @param buffer [Buffer] Shaped buffer
12
+ # @param font [Font] Font used for shaping
13
+ def initialize(buffer:, font:)
14
+ @buffer = buffer
15
+ @font = font
16
+ @glyph_infos = buffer.glyph_infos
17
+ @glyph_positions = buffer.glyph_positions
18
+ end
19
+
20
+ # @return [Array<GlyphInfo>] Glyph info array
21
+ def glyph_infos
22
+ @glyph_infos
23
+ end
24
+
25
+ # @return [Array<GlyphPosition>] Glyph position array
26
+ def glyph_positions
27
+ @glyph_positions
28
+ end
29
+
30
+ # Iterates over [GlyphInfo, GlyphPosition] pairs
31
+ def each
32
+ @glyph_infos.zip(@glyph_positions).each { |info, pos| yield info, pos }
33
+ end
34
+
35
+ # @return [Integer] Number of glyphs
36
+ def length
37
+ @glyph_infos.length
38
+ end
39
+
40
+ alias size length
41
+
42
+ # Returns the total advance as [x_advance, y_advance]
43
+ # @return [Array<Integer>]
44
+ def total_advance
45
+ @glyph_positions.inject([0, 0]) do |(x, y), pos|
46
+ [x + pos.x_advance, y + pos.y_advance]
47
+ end
48
+ end
49
+
50
+ # Generates an SVG path string for all glyphs at their shaped positions
51
+ # @return [String] SVG path data
52
+ def to_svg_path
53
+ paths = []
54
+ cx = 0
55
+ cy = 0
56
+ each do |info, pos|
57
+ glyph_path = extract_glyph_path(info.glyph_id)
58
+ ox = cx + pos.x_offset
59
+ oy = cy + pos.y_offset
60
+ paths << translate_path(glyph_path, ox, oy) unless glyph_path.empty?
61
+ cx += pos.x_advance
62
+ cy += pos.y_advance
63
+ end
64
+ paths.join(" ")
65
+ end
66
+
67
+ def inspect
68
+ "#<HarfBuzz::ShapingResult length=#{length} total_advance=#{total_advance.inspect}>"
69
+ end
70
+
71
+ private
72
+
73
+ def extract_glyph_path(glyph_id)
74
+ segments = []
75
+ draw = DrawFuncs.new
76
+ draw.on_move_to { |x, y| segments << "M#{x},#{y}" }
77
+ draw.on_line_to { |x, y| segments << "L#{x},#{y}" }
78
+ draw.on_quadratic_to { |cx, cy, x, y| segments << "Q#{cx},#{cy},#{x},#{y}" }
79
+ draw.on_cubic_to { |c1x, c1y, c2x, c2y, x, y|
80
+ segments << "C#{c1x},#{c1y},#{c2x},#{c2y},#{x},#{y}"
81
+ }
82
+ draw.on_close_path { segments << "Z" }
83
+ draw.make_immutable!
84
+ @font.draw_glyph(glyph_id, draw)
85
+ segments.join
86
+ end
87
+
88
+ def translate_path(path, ox, oy)
89
+ return path if ox.zero? && oy.zero?
90
+
91
+ "translate(#{ox},#{oy}) #{path}"
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ # Font subsetting API (requires libharfbuzz-subset)
5
+ module Subset
6
+ # @return [Boolean] true if libharfbuzz-subset is available
7
+ def self.available?
8
+ C.available?
9
+ end
10
+
11
+ # Subsets a font face using the given input specification
12
+ # @param face [HarfBuzz::Face] Source font face
13
+ # @param input [Input] Subset input specification
14
+ # @return [HarfBuzz::Face] Subsetted face
15
+ # @raise [UnavailableError] If subset library is not available
16
+ # @raise [SubsetError] If subsetting fails
17
+ def self.subset(face, input)
18
+ raise UnavailableError, "libharfbuzz-subset is not available" unless available?
19
+
20
+ ptr = C.hb_subset_or_fail(face.ptr, input.ptr)
21
+ raise SubsetError, "Subsetting failed" if ptr.null?
22
+
23
+ HarfBuzz::Face.wrap_owned(ptr)
24
+ end
25
+
26
+ # Wraps hb_subset_input_t — specifies what to include in a subset
27
+ class Input
28
+ attr_reader :ptr
29
+
30
+ # Creates a new subset input
31
+ # @raise [UnavailableError] If subset library is not available
32
+ # @raise [AllocationError] If allocation fails
33
+ def initialize
34
+ raise UnavailableError, "libharfbuzz-subset is not available" unless C.available?
35
+
36
+ @ptr = C.hb_subset_input_create_or_fail
37
+ raise AllocationError, "Failed to create subset input" if @ptr.null?
38
+
39
+ HarfBuzz::Subset::Input.define_finalizer(self, @ptr)
40
+ end
41
+
42
+ # Returns the set of Unicode codepoints to include (borrowed reference)
43
+ # @return [HarfBuzz::Set] Unicode set (do not destroy separately)
44
+ def unicode_set
45
+ HarfBuzz::Set.wrap_borrowed(C.hb_subset_input_unicode_set(@ptr))
46
+ end
47
+
48
+ # Returns the set of glyph IDs to include (borrowed reference)
49
+ # @return [HarfBuzz::Set] Glyph set (do not destroy separately)
50
+ def glyph_set
51
+ HarfBuzz::Set.wrap_borrowed(C.hb_subset_input_glyph_set(@ptr))
52
+ end
53
+
54
+ # @return [Integer] Current flags bitmask
55
+ def flags
56
+ C.hb_subset_input_get_flags(@ptr)
57
+ end
58
+
59
+ # @param flags [Integer] New flags bitmask
60
+ def flags=(flags)
61
+ C.hb_subset_input_set_flags(@ptr, flags)
62
+ end
63
+
64
+ def inspect
65
+ "#<HarfBuzz::Subset::Input flags=#{flags}>"
66
+ end
67
+
68
+ def self.define_finalizer(obj, ptr)
69
+ destroy = C.method(:hb_subset_input_destroy)
70
+ ObjectSpace.define_finalizer(obj, proc { destroy.call(ptr) })
71
+ end
72
+ end
73
+
74
+ # Wraps hb_subset_plan_t — a more fine-grained subsetting plan
75
+ class Plan
76
+ attr_reader :ptr
77
+
78
+ # Creates a subset plan for a face using the given input
79
+ # @param face [HarfBuzz::Face] Source font face
80
+ # @param input [Input] Subset input specification
81
+ # @raise [UnavailableError] If subset library is not available
82
+ # @raise [SubsetError] If plan creation fails
83
+ def initialize(face, input)
84
+ raise UnavailableError, "libharfbuzz-subset is not available" unless C.available?
85
+
86
+ @ptr = C.hb_subset_plan_create_or_fail(face.ptr, input.ptr)
87
+ raise SubsetError, "Failed to create subset plan" if @ptr.null?
88
+
89
+ HarfBuzz::Subset::Plan.define_finalizer(self, @ptr)
90
+ end
91
+
92
+ # Executes the plan and returns the subsetted face
93
+ # @return [HarfBuzz::Face] Subsetted face
94
+ # @raise [SubsetError] If execution fails
95
+ def execute
96
+ ptr = C.hb_subset_plan_execute_or_fail(@ptr)
97
+ raise SubsetError, "Subset plan execution failed" if ptr.null?
98
+
99
+ HarfBuzz::Face.wrap_owned(ptr)
100
+ end
101
+
102
+ # Returns the old→new glyph ID mapping (borrowed)
103
+ # @return [HarfBuzz::Map]
104
+ def old_to_new_glyph_mapping
105
+ HarfBuzz::Map.wrap_borrowed(C.hb_subset_plan_old_to_new_glyph_mapping(@ptr))
106
+ end
107
+
108
+ # Returns the new→old glyph ID mapping (borrowed)
109
+ # @return [HarfBuzz::Map]
110
+ def new_to_old_glyph_mapping
111
+ HarfBuzz::Map.wrap_borrowed(C.hb_subset_plan_new_to_old_glyph_mapping(@ptr))
112
+ end
113
+
114
+ # Returns the unicode→old glyph ID mapping (borrowed)
115
+ # @return [HarfBuzz::Map]
116
+ def unicode_to_old_glyph_mapping
117
+ HarfBuzz::Map.wrap_borrowed(C.hb_subset_plan_unicode_to_old_glyph_mapping(@ptr))
118
+ end
119
+
120
+ def inspect
121
+ "#<HarfBuzz::Subset::Plan>"
122
+ end
123
+
124
+ def self.define_finalizer(obj, ptr)
125
+ destroy = C.method(:hb_subset_plan_destroy)
126
+ ObjectSpace.define_finalizer(obj, proc { destroy.call(ptr) })
127
+ end
128
+ end
129
+ end
130
+ end