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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +258 -0
- data/Rakefile +8 -0
- data/benchmark/shaping_bench.rb +77 -0
- data/examples/basic_shaping.rb +67 -0
- data/examples/glyph_outlines.rb +79 -0
- data/examples/opentype_features.rb +91 -0
- data/examples/render_svg.rb +112 -0
- data/examples/render_waterfall.rb +177 -0
- data/examples/variable_fonts.rb +73 -0
- data/lib/harfbuzz/aat/layout.rb +78 -0
- data/lib/harfbuzz/blob.rb +136 -0
- data/lib/harfbuzz/buffer.rb +497 -0
- data/lib/harfbuzz/c/aat/layout.rb +15 -0
- data/lib/harfbuzz/c/base.rb +114 -0
- data/lib/harfbuzz/c/blob.rb +23 -0
- data/lib/harfbuzz/c/buffer.rb +127 -0
- data/lib/harfbuzz/c/common.rb +39 -0
- data/lib/harfbuzz/c/draw.rb +22 -0
- data/lib/harfbuzz/c/enums.rb +146 -0
- data/lib/harfbuzz/c/face.rb +37 -0
- data/lib/harfbuzz/c/font.rb +88 -0
- data/lib/harfbuzz/c/font_funcs.rb +58 -0
- data/lib/harfbuzz/c/map.rb +28 -0
- data/lib/harfbuzz/c/ot/color.rb +32 -0
- data/lib/harfbuzz/c/ot/font.rb +7 -0
- data/lib/harfbuzz/c/ot/layout.rb +83 -0
- data/lib/harfbuzz/c/ot/math.rb +23 -0
- data/lib/harfbuzz/c/ot/meta.rb +10 -0
- data/lib/harfbuzz/c/ot/metrics.rb +16 -0
- data/lib/harfbuzz/c/ot/name.rb +13 -0
- data/lib/harfbuzz/c/ot/shape.rb +10 -0
- data/lib/harfbuzz/c/ot/var.rb +22 -0
- data/lib/harfbuzz/c/paint.rb +38 -0
- data/lib/harfbuzz/c/set.rb +42 -0
- data/lib/harfbuzz/c/shape.rb +11 -0
- data/lib/harfbuzz/c/shape_plan.rb +24 -0
- data/lib/harfbuzz/c/structs.rb +120 -0
- data/lib/harfbuzz/c/subset.rb +49 -0
- data/lib/harfbuzz/c/unicode.rb +40 -0
- data/lib/harfbuzz/c/version.rb +25 -0
- data/lib/harfbuzz/draw_funcs.rb +112 -0
- data/lib/harfbuzz/error.rb +27 -0
- data/lib/harfbuzz/face.rb +186 -0
- data/lib/harfbuzz/feature.rb +76 -0
- data/lib/harfbuzz/flags.rb +85 -0
- data/lib/harfbuzz/font.rb +404 -0
- data/lib/harfbuzz/font_funcs.rb +286 -0
- data/lib/harfbuzz/glyph_info.rb +35 -0
- data/lib/harfbuzz/glyph_position.rb +41 -0
- data/lib/harfbuzz/library.rb +98 -0
- data/lib/harfbuzz/map.rb +157 -0
- data/lib/harfbuzz/ot/color.rb +125 -0
- data/lib/harfbuzz/ot/font.rb +16 -0
- data/lib/harfbuzz/ot/layout.rb +583 -0
- data/lib/harfbuzz/ot/math.rb +111 -0
- data/lib/harfbuzz/ot/meta.rb +34 -0
- data/lib/harfbuzz/ot/metrics.rb +54 -0
- data/lib/harfbuzz/ot/name.rb +81 -0
- data/lib/harfbuzz/ot/shape.rb +34 -0
- data/lib/harfbuzz/ot/var.rb +116 -0
- data/lib/harfbuzz/paint_funcs.rb +134 -0
- data/lib/harfbuzz/set.rb +272 -0
- data/lib/harfbuzz/shape_plan.rb +115 -0
- data/lib/harfbuzz/shaping_result.rb +94 -0
- data/lib/harfbuzz/subset.rb +130 -0
- data/lib/harfbuzz/unicode_funcs.rb +201 -0
- data/lib/harfbuzz/variation.rb +49 -0
- data/lib/harfbuzz/version.rb +5 -0
- data/lib/harfbuzz-ffi.rb +4 -0
- data/lib/harfbuzz.rb +313 -0
- data/sig/harfbuzz.rbs +594 -0
- 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
|