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,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ # Wraps hb_glyph_info_t, providing access to glyph ID and cluster index
5
+ class GlyphInfo
6
+ def initialize(struct_ptr)
7
+ @struct = C::HbGlyphInfoT.new(struct_ptr)
8
+ end
9
+
10
+ # @return [Integer] Glyph ID (after shaping) or Unicode codepoint (before shaping)
11
+ def codepoint
12
+ @struct[:codepoint]
13
+ end
14
+
15
+ alias glyph_id codepoint
16
+
17
+ # @return [Integer] Cluster index in source text
18
+ def cluster
19
+ @struct[:cluster]
20
+ end
21
+
22
+ # @return [Integer] Glyph flags bitmask
23
+ def mask
24
+ @struct[:mask]
25
+ end
26
+
27
+ def to_h
28
+ { codepoint: codepoint, cluster: cluster, mask: mask }
29
+ end
30
+
31
+ def inspect
32
+ "#<HarfBuzz::GlyphInfo glyph_id=#{codepoint} cluster=#{cluster}>"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ # Wraps hb_glyph_position_t, providing access to advance and offset values
5
+ class GlyphPosition
6
+ def initialize(struct_ptr)
7
+ @struct = C::HbGlyphPositionT.new(struct_ptr)
8
+ end
9
+
10
+ # @return [Integer] Horizontal advance (in font units)
11
+ def x_advance
12
+ @struct[:x_advance]
13
+ end
14
+
15
+ # @return [Integer] Vertical advance (in font units)
16
+ def y_advance
17
+ @struct[:y_advance]
18
+ end
19
+
20
+ # @return [Integer] Horizontal offset (in font units)
21
+ def x_offset
22
+ @struct[:x_offset]
23
+ end
24
+
25
+ # @return [Integer] Vertical offset (in font units)
26
+ def y_offset
27
+ @struct[:y_offset]
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ x_advance: x_advance, y_advance: y_advance,
33
+ x_offset: x_offset, y_offset: y_offset
34
+ }
35
+ end
36
+
37
+ def inspect
38
+ "#<HarfBuzz::GlyphPosition adv=(#{x_advance},#{y_advance}) off=(#{x_offset},#{y_offset})>"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module HarfBuzz
6
+ # Returns the path to the HarfBuzz shared library
7
+ # @return [String, Array<String>] Library path or array of library names for FFI to search
8
+ def self.library_path
9
+ @library_path ||= detect_library
10
+ end
11
+
12
+ # Sets the library path explicitly
13
+ # @param path [String] Path to the HarfBuzz shared library
14
+ def self.library_path=(path)
15
+ @library_path = path
16
+ end
17
+
18
+ # Detects the HarfBuzz shared library location
19
+ # @return [String, Array<String>] Library path or array of library names
20
+ # @raise [LibraryNotFoundError] If library cannot be found
21
+ private_class_method def self.detect_library
22
+ # 1. Environment variable override
23
+ if (env_path = ENV.fetch("HARFBUZZ_LIB_PATH", nil))
24
+ return env_path if File.exist?(env_path)
25
+
26
+ warn "HARFBUZZ_LIB_PATH=#{env_path} does not exist, falling back to auto-detection"
27
+ end
28
+
29
+ # 2. pkg-config (most reliable)
30
+ pkg_config_path = try_pkg_config
31
+ return pkg_config_path if pkg_config_path
32
+
33
+ # 3. Platform-specific default paths
34
+ platform_path = try_platform_paths
35
+ return platform_path if platform_path
36
+
37
+ # 4. Let FFI search via ldconfig / dyld (return array of library names)
38
+ ["harfbuzz", "libharfbuzz", "libharfbuzz-0"]
39
+ end
40
+
41
+ # Tries to find library path using pkg-config
42
+ # @return [String, nil] Library path if found
43
+ private_class_method def self.try_pkg_config
44
+ # Suppress mkmf output
45
+ libs = `pkg-config --libs harfbuzz 2>/dev/null`.strip
46
+ return nil if libs.empty?
47
+
48
+ lib_dir = libs[/-L(\S+)/, 1]
49
+ return nil unless lib_dir
50
+
51
+ ext = lib_extension
52
+ path = File.join(lib_dir, "libharfbuzz.#{ext}")
53
+ File.exist?(path) ? path : nil
54
+ rescue StandardError
55
+ nil
56
+ end
57
+
58
+ # Tries platform-specific library paths
59
+ # @return [String, nil] Library path if found
60
+ private_class_method def self.try_platform_paths
61
+ candidates = case RbConfig::CONFIG["host_os"]
62
+ when /darwin/i
63
+ %w[
64
+ /opt/homebrew/lib/libharfbuzz.dylib
65
+ /usr/local/lib/libharfbuzz.dylib
66
+ /opt/local/lib/libharfbuzz.dylib
67
+ ]
68
+ when /linux/i
69
+ %w[
70
+ /usr/lib/x86_64-linux-gnu/libharfbuzz.so
71
+ /usr/lib/aarch64-linux-gnu/libharfbuzz.so
72
+ /usr/lib64/libharfbuzz.so
73
+ /usr/lib/libharfbuzz.so
74
+ /usr/local/lib/libharfbuzz.so
75
+ ]
76
+ when /mingw|mswin|cygwin/i
77
+ %w[
78
+ C:/msys64/mingw64/bin/libharfbuzz-0.dll
79
+ C:/msys64/ucrt64/bin/libharfbuzz-0.dll
80
+ C:/msys64/clang64/bin/libharfbuzz-0.dll
81
+ ]
82
+ else
83
+ []
84
+ end
85
+
86
+ candidates.find { |p| File.exist?(p) }
87
+ end
88
+
89
+ # Returns the library extension for the current platform
90
+ # @return [String] Library extension (.dylib, .so, or .dll)
91
+ private_class_method def self.lib_extension
92
+ case RbConfig::CONFIG["host_os"]
93
+ when /darwin/i then "dylib"
94
+ when /mingw|mswin|cygwin/i then "dll"
95
+ else "so"
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ # Wraps hb_map_t — an integer-to-integer mapping
5
+ #
6
+ # Behaves similarly to Ruby's Hash with Integer keys and values.
7
+ # Includes Enumerable.
8
+ class Map
9
+ include Enumerable
10
+
11
+ HB_MAP_VALUE_INVALID = 0xFFFFFFFF
12
+
13
+ attr_reader :ptr
14
+
15
+ def initialize
16
+ @ptr = C.hb_map_create
17
+ raise AllocationError, "Failed to create map" if @ptr.null?
18
+
19
+ HarfBuzz::Map.define_finalizer(self, @ptr)
20
+ end
21
+
22
+ # Returns the singleton empty map
23
+ # @return [Map]
24
+ def self.empty
25
+ wrap_borrowed(C.hb_map_get_empty)
26
+ end
27
+
28
+ # @return [Integer] Number of entries
29
+ def size
30
+ C.hb_map_get_population(@ptr)
31
+ end
32
+
33
+ alias length size
34
+
35
+ # @return [Boolean] true if empty
36
+ def empty?
37
+ C.from_hb_bool(C.hb_map_is_empty(@ptr))
38
+ end
39
+
40
+ # @param key [Integer] Key
41
+ # @return [Boolean] true if key exists
42
+ def has_key?(key)
43
+ C.from_hb_bool(C.hb_map_has(@ptr, key))
44
+ end
45
+
46
+ alias include? has_key?
47
+ alias key? has_key?
48
+
49
+ # @param key [Integer] Key
50
+ # @return [Integer] Value (HB_MAP_VALUE_INVALID if not found)
51
+ def [](key)
52
+ val = C.hb_map_get(@ptr, key)
53
+ val == HB_MAP_VALUE_INVALID ? nil : val
54
+ end
55
+
56
+ # @param key [Integer] Key
57
+ # @param value [Integer] Value
58
+ def []=(key, value)
59
+ C.hb_map_set(@ptr, key, value)
60
+ end
61
+
62
+ # Deletes a key
63
+ # @param key [Integer] Key to delete
64
+ def delete(key)
65
+ C.hb_map_del(@ptr, key)
66
+ end
67
+
68
+ # Clears all entries
69
+ # @return [self]
70
+ def clear
71
+ C.hb_map_clear(@ptr)
72
+ self
73
+ end
74
+
75
+ # @param other [Map] Map to compare
76
+ # @return [Boolean] true if equal
77
+ def ==(other)
78
+ return false unless other.is_a?(Map)
79
+
80
+ C.from_hb_bool(C.hb_map_is_equal(@ptr, other.ptr))
81
+ end
82
+
83
+ # @return [Integer] Hash of this map
84
+ def hash
85
+ C.hb_map_hash(@ptr)
86
+ end
87
+
88
+ # Copies entries from another map into this one
89
+ # @param other [Map] Source map
90
+ # @return [self]
91
+ def update(other)
92
+ C.hb_map_update(@ptr, other.ptr)
93
+ self
94
+ end
95
+
96
+ alias merge! update
97
+
98
+ # @return [Boolean] true if the last allocation was successful
99
+ def allocation_successful?
100
+ C.from_hb_bool(C.hb_map_allocation_successful(@ptr))
101
+ end
102
+
103
+ # @return [Set] Set of all keys
104
+ def keys
105
+ set = Set.new
106
+ C.hb_map_keys(@ptr, set.ptr)
107
+ set
108
+ end
109
+
110
+ # @return [Set] Set of all values
111
+ def values
112
+ set = Set.new
113
+ C.hb_map_values(@ptr, set.ptr)
114
+ set
115
+ end
116
+
117
+ # Iterates over key-value pairs
118
+ # @yield [key, value]
119
+ # @return [Enumerator] if no block given
120
+ def each
121
+ return to_enum(:each) unless block_given?
122
+
123
+ idx_ptr = FFI::MemoryPointer.new(:int)
124
+ idx_ptr.write_int(-1)
125
+ key_ptr = FFI::MemoryPointer.new(:uint32)
126
+ val_ptr = FFI::MemoryPointer.new(:uint32)
127
+
128
+ while C.from_hb_bool(C.hb_map_next(@ptr, idx_ptr, key_ptr, val_ptr))
129
+ yield key_ptr.read_uint32, val_ptr.read_uint32
130
+ end
131
+ self
132
+ end
133
+
134
+ def inspect
135
+ "#<HarfBuzz::Map size=#{size}>"
136
+ end
137
+
138
+ def self.wrap_owned(ptr)
139
+ obj = allocate
140
+ obj.instance_variable_set(:@ptr, ptr)
141
+ define_finalizer(obj, ptr)
142
+ obj
143
+ end
144
+
145
+ def self.wrap_borrowed(ptr)
146
+ obj = allocate
147
+ obj.instance_variable_set(:@ptr, ptr)
148
+ obj.instance_variable_set(:@borrowed, true)
149
+ obj
150
+ end
151
+
152
+ def self.define_finalizer(obj, ptr)
153
+ destroy = C.method(:hb_map_destroy)
154
+ ObjectSpace.define_finalizer(obj, proc { destroy.call(ptr) })
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ module OT
5
+ # OpenType Color Fonts API (CPAL, COLR, SVG, CBDT/CBLC)
6
+ module Color
7
+ module_function
8
+
9
+ # @param face [Face] Font face
10
+ # @return [Boolean] true if the face has CPAL color palettes
11
+ def has_palettes?(face)
12
+ C.from_hb_bool(C.hb_ot_color_has_palettes(face.ptr))
13
+ end
14
+
15
+ # @param face [Face] Font face
16
+ # @return [Integer] Number of palettes
17
+ def palette_count(face)
18
+ C.hb_ot_color_palette_get_count(face.ptr)
19
+ end
20
+
21
+ # @param face [Face] Font face
22
+ # @param idx [Integer] Palette index
23
+ # @return [Integer] Name ID
24
+ def palette_name_id(face, idx)
25
+ C.hb_ot_color_palette_get_name_id(face.ptr, idx)
26
+ end
27
+
28
+ # @param face [Face] Font face
29
+ # @param idx [Integer] Color index within palette
30
+ # @return [Integer] Name ID
31
+ def palette_color_name_id(face, idx)
32
+ C.hb_ot_color_palette_color_get_name_id(face.ptr, idx)
33
+ end
34
+
35
+ # @param face [Face] Font face
36
+ # @param idx [Integer] Palette index
37
+ # @return [Integer] Palette flags bitmask
38
+ def palette_flags(face, idx)
39
+ C.hb_ot_color_palette_get_flags(face.ptr, idx)
40
+ end
41
+
42
+ # Returns colors in a palette
43
+ # @param face [Face] Font face
44
+ # @param idx [Integer] Palette index
45
+ # @return [Array<Integer>] Array of RGBA color values
46
+ def palette_colors(face, idx)
47
+ count_ptr = FFI::MemoryPointer.new(:uint)
48
+ count_ptr.write_uint(0)
49
+ C.hb_ot_color_palette_get_colors(face.ptr, idx, 0, count_ptr, nil)
50
+ count = count_ptr.read_uint
51
+ return [] if count.zero?
52
+
53
+ colors_ptr = FFI::MemoryPointer.new(:uint32, count)
54
+ count_ptr.write_uint(count)
55
+ C.hb_ot_color_palette_get_colors(face.ptr, idx, 0, count_ptr, colors_ptr)
56
+ colors_ptr.read_array_of_uint32(count_ptr.read_uint)
57
+ end
58
+
59
+ # @param face [Face] Font face
60
+ # @return [Boolean] true if face has COLR layers
61
+ def has_layers?(face)
62
+ C.from_hb_bool(C.hb_ot_color_has_layers(face.ptr))
63
+ end
64
+
65
+ # Returns COLR layers for a glyph
66
+ # @param face [Face] Font face
67
+ # @param glyph [Integer] Glyph ID
68
+ # @return [Array<C::HbOtColorLayerT>] Layer array
69
+ def glyph_layers(face, glyph)
70
+ count_ptr = FFI::MemoryPointer.new(:uint)
71
+ count_ptr.write_uint(0)
72
+ C.hb_ot_color_glyph_get_layers(face.ptr, glyph, 0, count_ptr, nil)
73
+ count = count_ptr.read_uint
74
+ return [] if count.zero?
75
+
76
+ layers_ptr = FFI::MemoryPointer.new(C::HbOtColorLayerT, count)
77
+ count_ptr.write_uint(count)
78
+ C.hb_ot_color_glyph_get_layers(face.ptr, glyph, 0, count_ptr, layers_ptr)
79
+ actual = count_ptr.read_uint
80
+ actual.times.map { |i| C::HbOtColorLayerT.new(layers_ptr + i * C::HbOtColorLayerT.size) }
81
+ end
82
+
83
+ # @param face [Face] Font face
84
+ # @return [Boolean] true if face has SVG glyphs
85
+ def has_svg?(face)
86
+ C.from_hb_bool(C.hb_ot_color_has_svg(face.ptr))
87
+ end
88
+
89
+ # Returns the SVG blob for a glyph
90
+ # @param face [Face] Font face
91
+ # @param glyph [Integer] Glyph ID
92
+ # @return [Blob] SVG data blob
93
+ def glyph_svg(face, glyph)
94
+ Blob.wrap_owned(C.hb_ot_color_glyph_reference_svg(face.ptr, glyph))
95
+ end
96
+
97
+ # @param face [Face] Font face
98
+ # @return [Boolean] true if face has PNG glyphs
99
+ def has_png?(face)
100
+ C.from_hb_bool(C.hb_ot_color_has_png(face.ptr))
101
+ end
102
+
103
+ # Returns the PNG blob for a glyph at a given size
104
+ # @param font [Font] Sized font
105
+ # @param glyph [Integer] Glyph ID
106
+ # @return [Blob] PNG data blob
107
+ def glyph_png(font, glyph)
108
+ Blob.wrap_owned(C.hb_ot_color_glyph_reference_png(font.ptr, glyph))
109
+ end
110
+
111
+ # @param face [Face] Font face
112
+ # @return [Boolean] true if face has COLRv1 paint data
113
+ def has_paint?(face)
114
+ C.from_hb_bool(C.hb_ot_color_has_paint(face.ptr))
115
+ end
116
+
117
+ # @param face [Face] Font face
118
+ # @param glyph [Integer] Glyph ID
119
+ # @return [Boolean] true if glyph has COLRv1 paint data
120
+ def glyph_has_paint?(face, glyph)
121
+ C.from_hb_bool(C.hb_ot_color_glyph_has_paint(face.ptr, glyph))
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HarfBuzz
4
+ module OT
5
+ # OpenType Font functions
6
+ module Font
7
+ module_function
8
+
9
+ # Sets OpenType font functions on a font
10
+ # @param font [HarfBuzz::Font] Font to configure
11
+ def set_funcs(font)
12
+ C.hb_ot_font_set_funcs(font.ptr)
13
+ end
14
+ end
15
+ end
16
+ end