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,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # examples/render_svg.rb — Render shaped text to an SVG file and open it
4
+ #
5
+ # Usage: ruby examples/render_svg.rb [text] [path/to/font.ttf]
6
+ #
7
+ # Generates an SVG file from shaped text using glyph outlines and opens
8
+ # it in the default browser.
9
+
10
+ require "harfbuzz"
11
+
12
+ text = ARGV[0] || "Hello, HarfBuzz!"
13
+ font_path = ARGV[1] ||
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 font found. Pass font path as argument."
20
+ end
21
+
22
+ blob = HarfBuzz::Blob.from_file!(font_path)
23
+ face = HarfBuzz::Face.new(blob, 0)
24
+ font = HarfBuzz::Font.new(face)
25
+ upem = face.upem
26
+
27
+ # Set up draw funcs for outline extraction
28
+ draw = HarfBuzz::DrawFuncs.new
29
+ draw_commands = []
30
+
31
+ draw.on_move_to { |x, y| draw_commands << "M#{x.round(2)},#{(-y).round(2)}" }
32
+ draw.on_line_to { |x, y| draw_commands << "L#{x.round(2)},#{(-y).round(2)}" }
33
+ draw.on_quadratic_to { |cx, cy, x, y|
34
+ draw_commands << "Q#{cx.round(2)},#{(-cy).round(2)},#{x.round(2)},#{(-y).round(2)}"
35
+ }
36
+ draw.on_cubic_to { |c1x, c1y, c2x, c2y, x, y|
37
+ draw_commands << "C#{c1x.round(2)},#{(-c1y).round(2)},#{c2x.round(2)},#{(-c2y).round(2)},#{x.round(2)},#{(-y).round(2)}"
38
+ }
39
+ draw.on_close_path { draw_commands << "Z" }
40
+ draw.make_immutable!
41
+
42
+ # Shape the text
43
+ buffer = HarfBuzz::Buffer.new
44
+ buffer.add_utf8(text)
45
+ buffer.guess_segment_properties
46
+ HarfBuzz.shape(font, buffer)
47
+
48
+ infos = buffer.glyph_infos
49
+ positions = buffer.glyph_positions
50
+
51
+ # Build SVG path elements for each glyph
52
+ svg_paths = []
53
+ cursor_x = 0
54
+ cursor_y = 0
55
+
56
+ infos.zip(positions).each do |info, pos|
57
+ draw_commands.clear
58
+ font.draw_glyph(info.glyph_id, draw)
59
+
60
+ unless draw_commands.empty?
61
+ ox = cursor_x + pos.x_offset
62
+ oy = cursor_y + pos.y_offset
63
+ svg_paths << %(<path transform="translate(#{ox},#{(-oy)})" d="#{draw_commands.join}" />)
64
+ end
65
+
66
+ cursor_x += pos.x_advance
67
+ cursor_y += pos.y_advance
68
+ end
69
+
70
+ # Get font metrics for viewBox
71
+ extents = font.extents_for_direction(:ltr)
72
+ ascender = extents[:ascender]
73
+ descender = extents[:descender]
74
+ height = ascender - descender
75
+ total_advance = positions.sum(&:x_advance)
76
+
77
+ margin = (upem * 0.1).round
78
+ vb_x = -margin
79
+ vb_y = -(ascender + margin)
80
+ vb_w = total_advance + margin * 2
81
+ vb_h = height + margin * 2
82
+
83
+ svg = <<~SVG
84
+ <?xml version="1.0" encoding="UTF-8"?>
85
+ <svg xmlns="http://www.w3.org/2000/svg"
86
+ viewBox="#{vb_x} #{vb_y} #{vb_w} #{vb_h}"
87
+ width="#{(vb_w.to_f / upem * 64).round}" height="#{(vb_h.to_f / upem * 64).round}">
88
+ <rect x="#{vb_x}" y="#{vb_y}" width="#{vb_w}" height="#{vb_h}" fill="white" />
89
+ <line x1="#{vb_x}" y1="0" x2="#{vb_x + vb_w}" y2="0" stroke="#ccc" stroke-width="#{upem * 0.005}" />
90
+ <g fill="black">
91
+ #{svg_paths.join("\n ")}
92
+ </g>
93
+ </svg>
94
+ SVG
95
+
96
+ output_path = File.expand_path("render_output.svg", __dir__)
97
+ File.write(output_path, svg)
98
+ puts "SVG written to #{output_path}"
99
+ puts " Text: #{text}"
100
+ puts " Font: #{font_path}"
101
+ puts " Glyphs: #{infos.length}"
102
+ puts " Total advance: #{total_advance} (#{upem} upem)"
103
+
104
+ # Open in default browser
105
+ case RUBY_PLATFORM
106
+ when /darwin/
107
+ system("open", output_path)
108
+ when /linux/
109
+ system("xdg-open", output_path)
110
+ when /mingw|mswin/
111
+ system("start", output_path)
112
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ # examples/render_waterfall.rb — Render a variable font waterfall to HTML
4
+ #
5
+ # Usage: ruby examples/render_waterfall.rb /path/to/variable_font.ttf [text]
6
+ #
7
+ # Generates an HTML file with SVG renderings of text at different
8
+ # variable font axis values and opens it in the default browser.
9
+ #
10
+ # Download a variable font from https://fonts.google.com/variablefonts
11
+ # e.g. Roboto Flex, Inter, or Noto Sans
12
+
13
+ require "harfbuzz"
14
+
15
+ font_path = ARGV[0]
16
+ unless font_path && File.exist?(font_path)
17
+ puts "Usage: ruby examples/render_waterfall.rb /path/to/variable_font.ttf [text]"
18
+ puts
19
+ puts "Download a variable font from https://fonts.google.com/variablefonts"
20
+ exit 1
21
+ end
22
+
23
+ text = ARGV[1] || "AaBbCcDdEeFf"
24
+
25
+ blob = HarfBuzz::Blob.from_file!(font_path)
26
+ face = HarfBuzz::Face.new(blob, 0)
27
+ upem = face.upem
28
+
29
+ unless HarfBuzz::OT::Var.has_data?(face)
30
+ puts "Error: #{font_path} is not a variable font."
31
+ exit 1
32
+ end
33
+
34
+ axes = HarfBuzz::OT::Var.axis_infos(face)
35
+ wght_axis = axes.find { |a| HarfBuzz.tag_to_s(a[:tag]).strip == "wght" }
36
+ unless wght_axis
37
+ puts "Error: Font has no 'wght' axis."
38
+ exit 1
39
+ end
40
+
41
+ # Generate weight steps
42
+ min_w = wght_axis[:min_value]
43
+ max_w = wght_axis[:max_value]
44
+ steps = 7
45
+ weights = (0...steps).map { |i| (min_w + (max_w - min_w) * i / (steps - 1)).round }
46
+
47
+ # Set up draw funcs
48
+ draw = HarfBuzz::DrawFuncs.new
49
+ draw_commands = []
50
+
51
+ draw.on_move_to { |x, y| draw_commands << "M#{x.round(2)},#{(-y).round(2)}" }
52
+ draw.on_line_to { |x, y| draw_commands << "L#{x.round(2)},#{(-y).round(2)}" }
53
+ draw.on_quadratic_to { |cx, cy, x, y|
54
+ draw_commands << "Q#{cx.round(2)},#{(-cy).round(2)},#{x.round(2)},#{(-y).round(2)}"
55
+ }
56
+ draw.on_cubic_to { |c1x, c1y, c2x, c2y, x, y|
57
+ draw_commands << "C#{c1x.round(2)},#{(-c1y).round(2)},#{c2x.round(2)},#{(-c2y).round(2)},#{x.round(2)},#{(-y).round(2)}"
58
+ }
59
+ draw.on_close_path { draw_commands << "Z" }
60
+ draw.make_immutable!
61
+
62
+ def shape_to_svg(font, face, text, draw, draw_commands)
63
+ upem = face.upem
64
+ buffer = HarfBuzz::Buffer.new
65
+ buffer.add_utf8(text)
66
+ buffer.guess_segment_properties
67
+ HarfBuzz.shape(font, buffer)
68
+
69
+ infos = buffer.glyph_infos
70
+ positions = buffer.glyph_positions
71
+
72
+ svg_paths = []
73
+ cursor_x = 0
74
+
75
+ infos.zip(positions).each do |info, pos|
76
+ draw_commands.clear
77
+ font.draw_glyph(info.glyph_id, draw)
78
+
79
+ unless draw_commands.empty?
80
+ ox = cursor_x + pos.x_offset
81
+ oy = pos.y_offset
82
+ svg_paths << %(<path transform="translate(#{ox},#{(-oy)})" d="#{draw_commands.join}" />)
83
+ end
84
+
85
+ cursor_x += pos.x_advance
86
+ end
87
+
88
+ extents = font.extents_for_direction(:ltr)
89
+ ascender = extents[:ascender]
90
+ descender = extents[:descender]
91
+ height = ascender - descender
92
+ total_advance = positions.sum(&:x_advance)
93
+
94
+ margin = (upem * 0.08).round
95
+ vb_x = -margin
96
+ vb_y = -(ascender + margin)
97
+ vb_w = total_advance + margin * 2
98
+ vb_h = height + margin * 2
99
+
100
+ pixel_height = 48
101
+ pixel_width = (vb_w.to_f / vb_h * pixel_height).round
102
+
103
+ <<~SVG
104
+ <svg xmlns="http://www.w3.org/2000/svg"
105
+ viewBox="#{vb_x} #{vb_y} #{vb_w} #{vb_h}"
106
+ width="#{pixel_width}" height="#{pixel_height}">
107
+ <g fill="black">#{svg_paths.join}</g>
108
+ </svg>
109
+ SVG
110
+ end
111
+
112
+ # Render each weight
113
+ rows = weights.map do |w|
114
+ font = HarfBuzz::Font.new(face)
115
+ variation = HarfBuzz::Variation.from_string("wght=#{w}")
116
+ font.variations = [variation]
117
+
118
+ svg = shape_to_svg(font, face, text, draw, draw_commands)
119
+ { weight: w, svg: svg }
120
+ end
121
+
122
+ # Build HTML
123
+ tag_name = HarfBuzz.tag_to_s(wght_axis[:tag]).strip
124
+ html = <<~HTML
125
+ <!DOCTYPE html>
126
+ <html lang="en">
127
+ <head>
128
+ <meta charset="UTF-8">
129
+ <title>HarfBuzz Variable Font Waterfall</title>
130
+ <style>
131
+ body {
132
+ font-family: system-ui, sans-serif;
133
+ max-width: 900px;
134
+ margin: 40px auto;
135
+ padding: 0 20px;
136
+ background: #fafafa;
137
+ color: #333;
138
+ }
139
+ h1 { font-size: 1.4em; margin-bottom: 0.3em; }
140
+ p.meta { color: #888; font-size: 0.9em; margin-top: 0; }
141
+ table { border-collapse: collapse; width: 100%; margin-top: 20px; }
142
+ th, td { text-align: left; padding: 10px 16px; border-bottom: 1px solid #eee; }
143
+ th { color: #999; font-weight: normal; font-size: 0.85em; text-transform: uppercase; }
144
+ td.weight { font-variant-numeric: tabular-nums; font-size: 0.95em; width: 80px; }
145
+ td.render svg { display: block; }
146
+ </style>
147
+ </head>
148
+ <body>
149
+ <h1>Variable Font Waterfall</h1>
150
+ <p class="meta">
151
+ Font: #{File.basename(font_path)}<br>
152
+ Axis: #{tag_name} (#{min_w.round}–#{max_w.round})<br>
153
+ Text: "#{text}"
154
+ </p>
155
+ <table>
156
+ <tr><th>#{tag_name}</th><th>Rendered</th></tr>
157
+ #{rows.map { |r| "<tr><td class=\"weight\">#{r[:weight]}</td><td class=\"render\">#{r[:svg]}</td></tr>" }.join("\n ")}
158
+ </table>
159
+ </body>
160
+ </html>
161
+ HTML
162
+
163
+ output_path = File.expand_path("waterfall_output.html", __dir__)
164
+ File.write(output_path, html)
165
+ puts "HTML written to #{output_path}"
166
+ puts " Font: #{font_path}"
167
+ puts " Axis: #{tag_name} (#{min_w.round}–#{max_w.round}), #{steps} steps"
168
+ puts " Text: #{text}"
169
+
170
+ case RUBY_PLATFORM
171
+ when /darwin/
172
+ system("open", output_path)
173
+ when /linux/
174
+ system("xdg-open", output_path)
175
+ when /mingw|mswin/
176
+ system("start", output_path)
177
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # examples/variable_fonts.rb — Variable font axis queries and shaping
4
+ #
5
+ # Usage: ruby examples/variable_fonts.rb /path/to/variable_font.ttf
6
+ #
7
+ # A variable font is required. Download one from Google Fonts, e.g.:
8
+ # https://fonts.google.com/specimen/Roboto+Flex
9
+
10
+ require "harfbuzz"
11
+
12
+ font_path = ARGV[0]
13
+ unless font_path && File.exist?(font_path)
14
+ puts "Usage: ruby examples/variable_fonts.rb /path/to/variable_font.ttf"
15
+ puts
16
+ puts "Download a variable font from https://fonts.google.com/variablefonts"
17
+ exit 1
18
+ end
19
+
20
+ puts "=== Variable Fonts ==="
21
+ puts "Font: #{font_path}"
22
+ puts
23
+
24
+ blob = HarfBuzz::Blob.from_file!(font_path)
25
+ face = HarfBuzz::Face.new(blob, 0)
26
+ font = HarfBuzz::Font.new(face)
27
+
28
+ # --- Check if font has variation data ---
29
+ puts "--- Variation Axes ---"
30
+ if HarfBuzz::OT::Var.has_data?(face)
31
+ axes = HarfBuzz::OT::Var.axis_infos(face)
32
+ puts "#{axes.length} axes found:"
33
+ axes.each do |axis|
34
+ tag_str = HarfBuzz.tag_to_s(axis[:tag])
35
+ puts " #{tag_str}: min=#{axis[:min_value]} default=#{axis[:default_value]} max=#{axis[:max_value]}"
36
+ end
37
+ else
38
+ puts "This font has no variation data."
39
+ exit 0
40
+ end
41
+ puts
42
+
43
+ # --- Named instances ---
44
+ puts "--- Named Instances ---"
45
+ count = HarfBuzz::OT::Var.named_instance_count(face)
46
+ puts "#{count} named instances"
47
+ count.times do |i|
48
+ coords = HarfBuzz::OT::Var.named_instance_design_coords(face, i)
49
+ puts " Instance #{i}: #{coords.inspect}"
50
+ end
51
+ puts
52
+
53
+ # --- Apply variation to font ---
54
+ puts "--- Applying Variations ---"
55
+ axes = HarfBuzz::OT::Var.axis_infos(face)
56
+ if axes.any?
57
+ wght_axis = axes.find { |a| HarfBuzz.tag_to_s(a[:tag]).strip == "wght" }
58
+ if wght_axis
59
+ puts "Setting weight to max (#{wght_axis[:max_value]})"
60
+ variation = HarfBuzz::Variation.from_string("wght=#{wght_axis[:max_value]}")
61
+ font.variations = [variation]
62
+ end
63
+ end
64
+
65
+ # Shape with varied font
66
+ buffer = HarfBuzz::Buffer.new
67
+ buffer.add_utf8("Hello")
68
+ buffer.guess_segment_properties
69
+ HarfBuzz.shape(font, buffer)
70
+ puts "Shaped with variation: #{buffer.length} glyphs"
71
+ buffer.glyph_positions.each do |pos|
72
+ puts " x_advance=#{pos.x_advance}"
73
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ module AAT
5
+ # Apple Advanced Typography Layout queries
6
+ module Layout
7
+ module_function
8
+
9
+ # @param face [Face] Font face
10
+ # @return [Boolean] true if the face has AAT morx substitution
11
+ def has_substitution?(face)
12
+ C.from_hb_bool(C.hb_aat_layout_has_substitution(face.ptr))
13
+ end
14
+
15
+ # @param face [Face] Font face
16
+ # @return [Boolean] true if the face has AAT kerning/kerx positioning
17
+ def has_positioning?(face)
18
+ C.from_hb_bool(C.hb_aat_layout_has_positioning(face.ptr))
19
+ end
20
+
21
+ # @param face [Face] Font face
22
+ # @return [Boolean] true if the face has AAT trak tracking
23
+ def has_tracking?(face)
24
+ C.from_hb_bool(C.hb_aat_layout_has_tracking(face.ptr))
25
+ end
26
+
27
+ # Returns feature types available in the AAT 'feat' table
28
+ # @param face [Face] Font face
29
+ # @return [Array<Integer>] Feature type values
30
+ def feature_types(face)
31
+ count_ptr = FFI::MemoryPointer.new(:uint)
32
+ count_ptr.write_uint(0)
33
+ C.hb_aat_layout_get_feature_types(face.ptr, 0, count_ptr, nil)
34
+ count = count_ptr.read_uint
35
+ return [] if count.zero?
36
+
37
+ types_ptr = FFI::MemoryPointer.new(:uint, count)
38
+ count_ptr.write_uint(count)
39
+ C.hb_aat_layout_get_feature_types(face.ptr, 0, count_ptr, types_ptr)
40
+ types_ptr.read_array_of_uint(count_ptr.read_uint)
41
+ end
42
+
43
+ # Returns the name ID for a feature type
44
+ # @param face [Face] Font face
45
+ # @param type [Integer] Feature type
46
+ # @return [Integer, nil] Name ID or nil
47
+ def feature_type_name_id(face, type)
48
+ name_id_ptr = FFI::MemoryPointer.new(:uint)
49
+ ok = C.hb_aat_layout_feature_type_get_name_id(face.ptr, type, name_id_ptr)
50
+ ok.zero? ? nil : name_id_ptr.read_uint
51
+ end
52
+
53
+ # Returns the selector infos for a feature type
54
+ # @param face [Face] Font face
55
+ # @param type [Integer] Feature type
56
+ # @return [Array<C::HbAatLayoutFeatureSelectorInfoT>] Selector info array
57
+ def selector_infos(face, type)
58
+ count_ptr = FFI::MemoryPointer.new(:uint)
59
+ count_ptr.write_uint(0)
60
+ C.hb_aat_layout_feature_type_get_selector_infos(face.ptr, type, 0, count_ptr, nil, nil)
61
+ count = count_ptr.read_uint
62
+ return [] if count.zero?
63
+
64
+ infos_ptr = FFI::MemoryPointer.new(C::HbAatLayoutFeatureSelectorInfoT, count)
65
+ count_ptr.write_uint(count)
66
+ C.hb_aat_layout_feature_type_get_selector_infos(
67
+ face.ptr, type, 0, count_ptr, infos_ptr, nil
68
+ )
69
+ actual = count_ptr.read_uint
70
+ actual.times.map do |i|
71
+ C::HbAatLayoutFeatureSelectorInfoT.new(
72
+ infos_ptr + i * C::HbAatLayoutFeatureSelectorInfoT.size
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ # Wraps hb_blob_t — binary data container
5
+ class Blob
6
+ attr_reader :ptr
7
+
8
+ # Creates a Blob from binary data
9
+ # @param data [String] Binary data
10
+ # @param mode [Symbol] Memory mode (:duplicate, :readonly, :writable,
11
+ # :readonly_may_make_writable)
12
+ # @return [Blob]
13
+ def initialize(data, mode: :duplicate)
14
+ mem = FFI::MemoryPointer.from_string(data)
15
+ @ptr = C.hb_blob_create(mem, data.bytesize, mode, nil, nil)
16
+ raise AllocationError, "Failed to create blob" if @ptr.null?
17
+
18
+ # Keep mem alive until the blob is destroyed
19
+ @mem = mem
20
+ register_finalizer
21
+ end
22
+
23
+ # Creates a Blob from a file path. Returns an empty blob if file is missing.
24
+ # @param path [String] File path
25
+ # @return [Blob]
26
+ def self.from_file(path)
27
+ ptr = C.hb_blob_create_from_file(path)
28
+ wrap_owned(ptr)
29
+ end
30
+
31
+ # Creates a Blob from a file path. Raises if the file cannot be read.
32
+ # @param path [String] File path
33
+ # @return [Blob]
34
+ # @raise [AllocationError] If file cannot be read
35
+ def self.from_file!(path)
36
+ ptr = C.hb_blob_create_from_file_or_fail(path)
37
+ raise AllocationError, "Failed to create blob from file: #{path}" if ptr.null?
38
+
39
+ wrap_owned(ptr)
40
+ end
41
+
42
+ # Returns the empty (singleton) blob
43
+ # @return [Blob]
44
+ def self.empty
45
+ wrap_borrowed(C.hb_blob_get_empty)
46
+ end
47
+
48
+ # Creates a sub-blob of this blob
49
+ # @param offset [Integer] Byte offset into the blob
50
+ # @param length [Integer] Length in bytes
51
+ # @return [Blob]
52
+ def sub_blob(offset, length)
53
+ ptr = C.hb_blob_create_sub_blob(@ptr, offset, length)
54
+ self.class.wrap_owned(ptr)
55
+ end
56
+
57
+ # Returns a writable copy of this blob, or nil if it fails
58
+ # @return [Blob, nil]
59
+ def writable_copy
60
+ ptr = C.hb_blob_copy_writable_or_fail(@ptr)
61
+ return nil if ptr.null?
62
+
63
+ self.class.wrap_owned(ptr)
64
+ end
65
+
66
+ # @return [Integer] Blob length in bytes
67
+ def length
68
+ C.hb_blob_get_length(@ptr)
69
+ end
70
+
71
+ alias size length
72
+
73
+ # @return [String] Blob data as a Ruby String (read-only)
74
+ def data
75
+ length_ptr = FFI::MemoryPointer.new(:uint)
76
+ data_ptr = C.hb_blob_get_data(@ptr, length_ptr)
77
+ return "".b if data_ptr.null?
78
+
79
+ data_ptr.read_bytes(length_ptr.read_uint)
80
+ end
81
+
82
+ # @return [String, nil] Blob data as a writable Ruby String, or nil if blob is immutable
83
+ def data_writable
84
+ length_ptr = FFI::MemoryPointer.new(:uint)
85
+ data_ptr = C.hb_blob_get_data_writable(@ptr, length_ptr)
86
+ return nil if data_ptr.null?
87
+
88
+ data_ptr.read_bytes(length_ptr.read_uint)
89
+ end
90
+
91
+ # @return [Boolean] true if the blob is immutable
92
+ def immutable?
93
+ C.from_hb_bool(C.hb_blob_is_immutable(@ptr))
94
+ end
95
+
96
+ # Makes the blob immutable
97
+ # @return [self]
98
+ def make_immutable!
99
+ C.hb_blob_make_immutable(@ptr)
100
+ self
101
+ end
102
+
103
+ def inspect
104
+ "#<HarfBuzz::Blob length=#{length} immutable=#{immutable?}>"
105
+ end
106
+
107
+ # Wraps an owned pointer (will be destroyed via finalizer)
108
+ def self.wrap_owned(ptr)
109
+ obj = allocate
110
+ obj.instance_variable_set(:@ptr, ptr)
111
+ obj.send(:register_finalizer)
112
+ obj
113
+ end
114
+
115
+ # Wraps a borrowed pointer (no finalizer — caller owns the lifetime)
116
+ def self.wrap_borrowed(ptr)
117
+ obj = allocate
118
+ obj.instance_variable_set(:@ptr, ptr)
119
+ obj.instance_variable_set(:@borrowed, true)
120
+ obj
121
+ end
122
+
123
+ private
124
+
125
+ def register_finalizer
126
+ return if instance_variable_defined?(:@borrowed) && @borrowed
127
+
128
+ HarfBuzz::Blob.define_finalizer(self, @ptr)
129
+ end
130
+
131
+ def self.define_finalizer(obj, ptr)
132
+ destroy = C.method(:hb_blob_destroy)
133
+ ObjectSpace.define_finalizer(obj, proc { destroy.call(ptr) })
134
+ end
135
+ end
136
+ end