zaniah 0.1.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 +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +70 -0
- data/assets/fonts/Abel-Regular.ttf +0 -0
- data/assets/fonts/OFL.txt +93 -0
- data/docs/adr/000-template.md +24 -0
- data/docs/adr/001-native-gpu-backends.md +36 -0
- data/docs/adr/002-objective-c-aggregates.md +34 -0
- data/docs/adr/003-fiddle-call-boundary.md +33 -0
- data/docs/adr/README.md +10 -0
- data/docs/native.md +66 -0
- data/docs/process_pool.md +67 -0
- data/docs/text.md +90 -0
- data/docs/vector_and_list.md +52 -0
- data/examples/ffi_bench.rb +16 -0
- data/examples/linux_smoke.rb +53 -0
- data/examples/mac_event_check.rb +61 -0
- data/examples/mac_poll_bench.rb +24 -0
- data/examples/metal_state_check.rb +72 -0
- data/examples/native_bench.rb +18 -0
- data/examples/native_drop.rb +82 -0
- data/examples/native_font.rb +15 -0
- data/examples/native_ime.rb +50 -0
- data/examples/native_smoke.rb +75 -0
- data/examples/native_text_bench.rb +83 -0
- data/examples/native_watch.rb +25 -0
- data/examples/vulkan_triangle.rb +20 -0
- data/examples/word_count_worker.rb +8 -0
- data/lib/zaniah/anchored.rb +10 -0
- data/lib/zaniah/app.rb +130 -0
- data/lib/zaniah/bounds.rb +22 -0
- data/lib/zaniah/canvas.rb +15 -0
- data/lib/zaniah/color.rb +35 -0
- data/lib/zaniah/configuration.rb +5 -0
- data/lib/zaniah/corners.rb +7 -0
- data/lib/zaniah/data_compat.rb +25 -0
- data/lib/zaniah/device_pixels.rb +7 -0
- data/lib/zaniah/div.rb +5 -0
- data/lib/zaniah/edges.rb +7 -0
- data/lib/zaniah/element.rb +106 -0
- data/lib/zaniah/entity.rb +9 -0
- data/lib/zaniah/entity_context.rb +17 -0
- data/lib/zaniah/error.rb +5 -0
- data/lib/zaniah/ffi/com.rb +24 -0
- data/lib/zaniah/ffi/library.rb +29 -0
- data/lib/zaniah/ffi/objc.rb +86 -0
- data/lib/zaniah/ffi/struct/signature.rb +46 -0
- data/lib/zaniah/ffi/struct.rb +61 -0
- data/lib/zaniah/ffi/wayland.rb +140 -0
- data/lib/zaniah/frame_context.rb +14 -0
- data/lib/zaniah/geometry.rb +16 -0
- data/lib/zaniah/gpu/buffer.rb +23 -0
- data/lib/zaniah/gpu/frame_encoder.rb +43 -0
- data/lib/zaniah/gpu/instance_packing.rb +53 -0
- data/lib/zaniah/gpu/metal.rb +209 -0
- data/lib/zaniah/gpu/open_gl.rb +210 -0
- data/lib/zaniah/gpu/pipeline.rb +7 -0
- data/lib/zaniah/gpu/software.rb +126 -0
- data/lib/zaniah/gpu/texture.rb +33 -0
- data/lib/zaniah/gpu/vulkan.rb +233 -0
- data/lib/zaniah/gpu.rb +18 -0
- data/lib/zaniah/hsla.rb +14 -0
- data/lib/zaniah/image.rb +19 -0
- data/lib/zaniah/input/action_registry.rb +12 -0
- data/lib/zaniah/input/composition.rb +7 -0
- data/lib/zaniah/input/context_predicate.rb +87 -0
- data/lib/zaniah/input/dispatcher.rb +59 -0
- data/lib/zaniah/input/file_drop.rb +7 -0
- data/lib/zaniah/input/focus_handle.rb +24 -0
- data/lib/zaniah/input/key_down.rb +7 -0
- data/lib/zaniah/input/key_up.rb +7 -0
- data/lib/zaniah/input/keymap/binding.rb +9 -0
- data/lib/zaniah/input/keymap.rb +55 -0
- data/lib/zaniah/input/keystroke.rb +20 -0
- data/lib/zaniah/input/mouse_down.rb +7 -0
- data/lib/zaniah/input/mouse_move.rb +7 -0
- data/lib/zaniah/input/mouse_up.rb +7 -0
- data/lib/zaniah/input/scroll_wheel.rb +7 -0
- data/lib/zaniah/input/text_input.rb +7 -0
- data/lib/zaniah/input.rb +17 -0
- data/lib/zaniah/layout/available_space.rb +11 -0
- data/lib/zaniah/layout/engine.rb +214 -0
- data/lib/zaniah/layout/node.rb +36 -0
- data/lib/zaniah/layout/style.rb +19 -0
- data/lib/zaniah/layout/tree.rb +16 -0
- data/lib/zaniah/layout.rb +11 -0
- data/lib/zaniah/length.rb +14 -0
- data/lib/zaniah/length_units.rb +9 -0
- data/lib/zaniah/list/height_index.rb +68 -0
- data/lib/zaniah/list.rb +101 -0
- data/lib/zaniah/overlay.rb +10 -0
- data/lib/zaniah/platform/display.rb +7 -0
- data/lib/zaniah/platform/file_event.rb +7 -0
- data/lib/zaniah/platform/headless/window.rb +185 -0
- data/lib/zaniah/platform/linux/appearance_aware.rb +60 -0
- data/lib/zaniah/platform/linux/displays.rb +55 -0
- data/lib/zaniah/platform/linux/free_type.rb +80 -0
- data/lib/zaniah/platform/linux/watcher.rb +70 -0
- data/lib/zaniah/platform/linux/wayland_display_discovery.rb +60 -0
- data/lib/zaniah/platform/linux/wayland_window.rb +369 -0
- data/lib/zaniah/platform/linux/window.rb +418 -0
- data/lib/zaniah/platform/linux.rb +3 -0
- data/lib/zaniah/platform/mac/app.rb +67 -0
- data/lib/zaniah/platform/mac/core_text.rb +94 -0
- data/lib/zaniah/platform/mac/displays.rb +23 -0
- data/lib/zaniah/platform/mac/watcher.rb +63 -0
- data/lib/zaniah/platform/mac/window.rb +298 -0
- data/lib/zaniah/platform/mac.rb +19 -0
- data/lib/zaniah/platform/tui/input_decoder.rb +145 -0
- data/lib/zaniah/platform/tui/text_renderer.rb +40 -0
- data/lib/zaniah/platform/tui/window.rb +91 -0
- data/lib/zaniah/platform/windows/terminal.rb +127 -0
- data/lib/zaniah/platform/windows/watcher.rb +91 -0
- data/lib/zaniah/platform/windows/window.rb +407 -0
- data/lib/zaniah/platform/windows.rb +3 -0
- data/lib/zaniah/platform.rb +34 -0
- data/lib/zaniah/png.rb +118 -0
- data/lib/zaniah/point.rb +8 -0
- data/lib/zaniah/process_pool/worker.rb +7 -0
- data/lib/zaniah/process_pool.rb +239 -0
- data/lib/zaniah/process_wire.rb +127 -0
- data/lib/zaniah/quad_packer.rb +15 -0
- data/lib/zaniah/scene/sprite_batch.rb +7 -0
- data/lib/zaniah/scene.rb +167 -0
- data/lib/zaniah/scene_renderer.rb +8 -0
- data/lib/zaniah/size.rb +5 -0
- data/lib/zaniah/subscription.rb +14 -0
- data/lib/zaniah/svg/path.rb +134 -0
- data/lib/zaniah/svg/rasterizable.rb +186 -0
- data/lib/zaniah/svg.rb +295 -0
- data/lib/zaniah/task/cancelled.rb +7 -0
- data/lib/zaniah/task/timeout.rb +7 -0
- data/lib/zaniah/task.rb +51 -0
- data/lib/zaniah/task_executor.rb +105 -0
- data/lib/zaniah/text.rb +24 -0
- data/lib/zaniah/text_system/atlas/entry.rb +9 -0
- data/lib/zaniah/text_system/atlas.rb +102 -0
- data/lib/zaniah/text_system/atlas_cache.rb +146 -0
- data/lib/zaniah/text_system/font_db/face.rb +9 -0
- data/lib/zaniah/text_system/font_db.rb +193 -0
- data/lib/zaniah/text_system/glyph.rb +7 -0
- data/lib/zaniah/text_system/line_layout.rb +20 -0
- data/lib/zaniah/text_system/renderer.rb +196 -0
- data/lib/zaniah/text_system/shaper.rb +430 -0
- data/lib/zaniah/text_system/typesetter/copy_error.rb +9 -0
- data/lib/zaniah/text_system/typesetter.rb +168 -0
- data/lib/zaniah/text_system.rb +9 -0
- data/lib/zaniah/unicode.rb +13 -0
- data/lib/zaniah/uniform_list.rb +30 -0
- data/lib/zaniah/version.rb +5 -0
- data/lib/zaniah/view.rb +13 -0
- data/lib/zaniah.rb +29 -0
- data/sig/app.rbs +80 -0
- data/sig/elements.rbs +129 -0
- data/sig/gpu.rbs +108 -0
- data/sig/input.rbs +114 -0
- data/sig/layout.rbs +53 -0
- data/sig/native.rbs +88 -0
- data/sig/platform.rbs +94 -0
- data/sig/scene.rbs +52 -0
- data/sig/shaper.rbs +8 -0
- data/sig/text.rbs +137 -0
- data/sig/zaniah.rbs +120 -0
- data/tools/check_dependencies.rb +52 -0
- metadata +249 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "atlas"
|
|
4
|
+
require_relative "typesetter"
|
|
5
|
+
|
|
6
|
+
module Zaniah
|
|
7
|
+
module TextSystem
|
|
8
|
+
class Renderer < Typesetter
|
|
9
|
+
MAX_PAINT_BYTES = 32 << 20
|
|
10
|
+
|
|
11
|
+
attr_reader :atlas, :color_atlas, :cache_dir
|
|
12
|
+
attr_accessor :scale_factor
|
|
13
|
+
|
|
14
|
+
def initialize(font: nil, font_db: Zaniah.configuration.font_db, capacity: 2000, font_raster: Zaniah.configuration.font_raster,
|
|
15
|
+
shaper: Zaniah.configuration.shaper, segmenter: Zaniah.configuration.segmenter, cache_dir: nil)
|
|
16
|
+
super(font: font, font_db: font_db, capacity: capacity, shaper: shaper, segmenter: segmenter)
|
|
17
|
+
@rasters = Alhena::Cache.new(capacity: 4096, max_bytes: 32 << 20)
|
|
18
|
+
case font_raster
|
|
19
|
+
when :native, :alhena
|
|
20
|
+
# The default is the bundled pure-Ruby engine.
|
|
21
|
+
when :core_text, :coretext
|
|
22
|
+
require_relative "../platform/mac/core_text"
|
|
23
|
+
@rasters = Platform::Mac::CoreText.new
|
|
24
|
+
when :freetype
|
|
25
|
+
require_relative "../platform/linux/free_type"
|
|
26
|
+
@rasters = Platform::Linux::FreeType.new
|
|
27
|
+
else
|
|
28
|
+
raise ArgumentError, "unknown font rasterizer" unless font_raster.respond_to?(:rasterize)
|
|
29
|
+
@rasters = font_raster
|
|
30
|
+
end
|
|
31
|
+
@atlas = Atlas.new
|
|
32
|
+
@color_atlas, @color_rasters, @scale_factor = Atlas.new(format: :rgba8), {}, 1.0
|
|
33
|
+
@paint_cache, @paint_colors, @paint_span_cache, @paint_bytes = {}, {}, {}, 0
|
|
34
|
+
@paint_lookup = Array.new(6)
|
|
35
|
+
if cache_dir
|
|
36
|
+
raise ArgumentError, "cache_dir must be a nonempty path string" unless cache_dir.is_a?(String) && !cache_dir.empty? && !cache_dir.include?("\0")
|
|
37
|
+
require_relative "atlas_cache"
|
|
38
|
+
@cache_dir = File.expand_path(cache_dir).freeze
|
|
39
|
+
@font_identities = {}.compare_by_identity
|
|
40
|
+
@raster_identity = if [:native, :alhena].include?(font_raster)
|
|
41
|
+
"alhena/#{Alhena::VERSION}"
|
|
42
|
+
elsif @rasters.respond_to?(:cache_key) && (identity = @rasters.cache_key).is_a?(String) && !identity.empty?
|
|
43
|
+
identity
|
|
44
|
+
else
|
|
45
|
+
raise ArgumentError, "disk-cached rasterizers must supply a stable nonempty String cache_key"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def paint_line(scene, line, x:, y:, color: "#ddd", spans: nil)
|
|
51
|
+
scale = @scale_factor.to_f
|
|
52
|
+
raise ArgumentError, "scale factor must be positive" unless scale.positive? && scale.finite?
|
|
53
|
+
raise ArgumentError, "text origin must be finite" unless x.is_a?(Numeric) && x.finite? && y.is_a?(Numeric) && y.finite?
|
|
54
|
+
color = paint_color(color)
|
|
55
|
+
spans = paint_spans(spans)
|
|
56
|
+
refresh_paint_cache
|
|
57
|
+
@paint_lookup[0], @paint_lookup[1], @paint_lookup[2] = line.object_id, x, y
|
|
58
|
+
@paint_lookup[3], @paint_lookup[4], @paint_lookup[5] = color, spans, scale
|
|
59
|
+
cached = @paint_cache.delete(@paint_lookup)
|
|
60
|
+
if cached
|
|
61
|
+
@paint_cache[cached[3]] = cached
|
|
62
|
+
batches = cached[1]
|
|
63
|
+
else
|
|
64
|
+
batches = pack_line(line, x, y, color, spans, scale)
|
|
65
|
+
refresh_paint_cache
|
|
66
|
+
length = batches.sum { |batch| batch.bytes.bytesize }
|
|
67
|
+
if length <= MAX_PAINT_BYTES
|
|
68
|
+
while !@paint_cache.empty? && (@paint_cache.length >= @capacity || @paint_bytes + length > MAX_PAINT_BYTES)
|
|
69
|
+
@paint_bytes -= @paint_cache.shift.last[2]
|
|
70
|
+
end
|
|
71
|
+
# Retaining the line prevents object_id reuse while cached.
|
|
72
|
+
key = [line.object_id, x, y, color, spans, scale].freeze
|
|
73
|
+
@paint_cache[key] = [line, batches, length, key]
|
|
74
|
+
@paint_bytes += length
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
batches.each { |batch| scene.sprite_batch(batch) }
|
|
78
|
+
scene
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def end_frame
|
|
82
|
+
@atlas.end_frame
|
|
83
|
+
@color_atlas.end_frame
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def prewarm(text = (32..126).map(&:chr).join.force_encoding(Encoding::UTF_8), size: 14)
|
|
87
|
+
scale = @scale_factor.to_f
|
|
88
|
+
raise ArgumentError, "scale factor must be positive" unless scale.positive? && scale.finite?
|
|
89
|
+
line = layout_line(text, size: size)
|
|
90
|
+
if @cache_dir
|
|
91
|
+
identity = [Zaniah::VERSION, Alhena::VERSION, @raster_identity, RUBY_PLATFORM, scale, size.to_f, text,
|
|
92
|
+
line.glyphs.map { |glyph| [font_identity(glyph.font), glyph.id, glyph.x, glyph.advance] }]
|
|
93
|
+
key = Digest::SHA256.hexdigest(JSON.generate(identity))
|
|
94
|
+
mono, color = %w[mono color].map { |kind| File.join(@cache_dir, "#{key}-#{kind}.atlas") }
|
|
95
|
+
mono_loaded, color_loaded = @atlas.load_cache(mono, key: key, preserve: true), @color_atlas.load_cache(color, key: key, preserve: true)
|
|
96
|
+
if mono_loaded && color_loaded
|
|
97
|
+
refresh_paint_cache
|
|
98
|
+
return self
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
tint = paint_color("#fff")
|
|
102
|
+
4.times { |bucket| pack_line(line, bucket / (4.0 * scale), 0, tint, nil, scale) }
|
|
103
|
+
if @cache_dir
|
|
104
|
+
@atlas.save_cache(mono, key: key)
|
|
105
|
+
@color_atlas.save_cache(color, key: key)
|
|
106
|
+
end
|
|
107
|
+
refresh_paint_cache
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def close
|
|
112
|
+
return if @closed
|
|
113
|
+
@paint_cache.clear
|
|
114
|
+
@rasters.close if @rasters.respond_to?(:close) && ![@shaper, @segmenter, @font_db].include?(@rasters)
|
|
115
|
+
super
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def font_identity(font)
|
|
121
|
+
return font.object_id unless @cache_dir
|
|
122
|
+
@font_identities[font] ||= begin
|
|
123
|
+
raise ArgumentError, "disk-cached fonts must expose immutable data, index and axis_values" unless font.respond_to?(:data) && font.respond_to?(:index) && font.respond_to?(:axis_values) && font.data.is_a?(String) && font.data.frozen?
|
|
124
|
+
digest = Digest::SHA256.new.update(font.data)
|
|
125
|
+
digest.update(JSON.generate([font.index, font.axis_values.sort])).hexdigest.freeze
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def refresh_paint_cache
|
|
130
|
+
return if @paint_mono_generation == @atlas.generation && @paint_color_generation == @color_atlas.generation
|
|
131
|
+
@paint_cache.clear
|
|
132
|
+
@paint_bytes = 0
|
|
133
|
+
@paint_mono_generation, @paint_color_generation = @atlas.generation, @color_atlas.generation
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def paint_color(value)
|
|
137
|
+
return value if value.is_a?(Color)
|
|
138
|
+
return Color.parse(value) if value.is_a?(Array)
|
|
139
|
+
@paint_colors.clear if @paint_colors.length >= 256
|
|
140
|
+
@paint_colors[value] ||= Color.parse(value)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def paint_spans(spans)
|
|
144
|
+
return unless spans
|
|
145
|
+
cached = @paint_span_cache[spans]
|
|
146
|
+
return cached if cached
|
|
147
|
+
@paint_span_cache.clear if @paint_span_cache.length >= 256
|
|
148
|
+
# Keys are snapshots: callers may update their range/color arrays later.
|
|
149
|
+
snapshot = spans.map do |first, last, tint|
|
|
150
|
+
tint = tint.dup.freeze if tint.is_a?(String) || tint.is_a?(Array)
|
|
151
|
+
[first, last, tint].freeze
|
|
152
|
+
end.freeze
|
|
153
|
+
@paint_span_cache[snapshot] = snapshot.map { |first, last, tint| [first, last, paint_color(tint)].freeze }.freeze
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def pack_line(line, x, y, color, spans, scale)
|
|
157
|
+
span_index, batches, values, texture = 0, [], [], nil
|
|
158
|
+
line.glyphs.each do |glyph|
|
|
159
|
+
span_index += 1 while spans && span_index < spans.length && glyph.start >= spans[span_index][1]
|
|
160
|
+
tint = spans && spans[span_index] && glyph.start >= spans[span_index][0] ? spans[span_index][2] : color
|
|
161
|
+
position = (x + glyph.x) * scale
|
|
162
|
+
bucket = ((position - position.floor) * 4).round % 4
|
|
163
|
+
key = [font_identity(glyph.font), glyph.id, (line.size * scale).to_f, bucket]
|
|
164
|
+
colored = glyph.font.tables.key?("COLR") || glyph.font.tables.key?("sbix") || glyph.font.tables.key?("CBDT")
|
|
165
|
+
entry = colored ? @color_atlas.lookup(key) : nil
|
|
166
|
+
color_glyph = !entry.nil?
|
|
167
|
+
bitmap = if colored && !entry
|
|
168
|
+
unless @color_rasters.key?(key)
|
|
169
|
+
@color_rasters.shift if @color_rasters.length >= 1024
|
|
170
|
+
@color_rasters[key] = glyph.font.color_bitmap(glyph.id, size: line.size * scale, subpixel_x: bucket / 4.0)
|
|
171
|
+
end
|
|
172
|
+
@color_rasters[key]
|
|
173
|
+
end
|
|
174
|
+
if bitmap
|
|
175
|
+
entry = @color_atlas.fetch(key) { bitmap }
|
|
176
|
+
color_glyph = true
|
|
177
|
+
end
|
|
178
|
+
entry ||= @atlas.fetch(key) { @rasters.rasterize(glyph.font, glyph.id, size: line.size * scale, subpixel_x: bucket / 4.0) }
|
|
179
|
+
next if entry.width.zero? || entry.height.zero?
|
|
180
|
+
unless texture.equal?(entry.texture)
|
|
181
|
+
batches << Scene::SpriteBatch.new(values.pack("f*").freeze, texture) unless values.empty?
|
|
182
|
+
values.clear
|
|
183
|
+
texture = entry.texture
|
|
184
|
+
end
|
|
185
|
+
tint = paint_color("#fff") if color_glyph
|
|
186
|
+
values.push((position.floor + entry.left) / scale, ((y * scale).floor - entry.top) / scale, entry.width / scale, entry.height / scale,
|
|
187
|
+
tint.r, tint.g, tint.b, tint.a, 0, 0, 0, 0, 0, 0, 0, 0, 0, texture.format == :r8 ? 1 : 2, 0, 0,
|
|
188
|
+
entry.x.to_f / texture.width, entry.y.to_f / texture.height,
|
|
189
|
+
entry.width.to_f / texture.width, entry.height.to_f / texture.height)
|
|
190
|
+
end
|
|
191
|
+
batches << Scene::SpriteBatch.new(values.pack("f*").freeze, texture) unless values.empty?
|
|
192
|
+
batches.freeze
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module TextSystem
|
|
5
|
+
# Horizontal Latin/CJK/kana shaping, not complex-script reordering.
|
|
6
|
+
# OpenType GSUB 1/4/6/7; GPOS 2/9; GDEF lookup filtering; legacy kern.
|
|
7
|
+
class Shaper
|
|
8
|
+
def initialize = (@lookups, @filters = {}, {})
|
|
9
|
+
|
|
10
|
+
def shape(glyphs, size:, text: nil, script: nil, language: nil)
|
|
11
|
+
raise ArgumentError, "font size must be finite and positive" unless size.is_a?(Numeric) && size.finite? && size.positive?
|
|
12
|
+
runs, previous = [], nil
|
|
13
|
+
inferred = glyphs.map { |glyph| script || (text && script_for(text.byteslice(glyph.start...glyph.finish))) }
|
|
14
|
+
inherited = inferred.compact.first || "latn"
|
|
15
|
+
glyphs.each_with_index do |glyph, i|
|
|
16
|
+
inherited = inferred[i] || inherited
|
|
17
|
+
key = [glyph.font, inherited.to_s]
|
|
18
|
+
runs << [key, []] unless key == previous
|
|
19
|
+
runs.last[1] << glyph
|
|
20
|
+
previous = key
|
|
21
|
+
end
|
|
22
|
+
x = 0.0
|
|
23
|
+
runs.flat_map do |(font, tag), run|
|
|
24
|
+
if font.tables.key?("GSUB")
|
|
25
|
+
data = font.table("GSUB")
|
|
26
|
+
lookups(data, font, "GSUB", %w[liga calt], tag, language).each do |lookup|
|
|
27
|
+
index = 0
|
|
28
|
+
index = substitute(data, lookup, run, index, size, 0, []) || index + 1 while index < run.length
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
placements, advances = positioning(run, size, tag, language)
|
|
32
|
+
run.each_with_index.map do |glyph, i|
|
|
33
|
+
advance = glyph.advance + advances[i]
|
|
34
|
+
placed = Glyph.new(font, glyph.id, glyph.start, glyph.finish, x + placements[i], advance)
|
|
35
|
+
x += advance
|
|
36
|
+
placed
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def script_for(text)
|
|
44
|
+
return unless text
|
|
45
|
+
case text
|
|
46
|
+
when /\p{Latin}/ then "latn"
|
|
47
|
+
when /[\p{Hiragana}\p{Katakana}]/ then "kana"
|
|
48
|
+
when /\p{Han}/ then "hani"
|
|
49
|
+
when /\p{Hangul}/ then "hang"
|
|
50
|
+
when /\p{Greek}/ then "grek"
|
|
51
|
+
when /\p{Cyrillic}/ then "cyrl"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def words(data, offset, count)
|
|
56
|
+
data.check(offset, count * 2)
|
|
57
|
+
Array.new(count) { |i| data.u16(offset + i * 2) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def records(data, offset, base = offset)
|
|
61
|
+
count = data.u16(offset)
|
|
62
|
+
data.check(offset + 2, count * 6)
|
|
63
|
+
Array.new(count) { |i| [data.bytes(offset + 2 + i * 6, 4), base + data.u16(offset + 6 + i * 6)] }.to_h
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def lookups(data, font, tag, features, script, language)
|
|
67
|
+
@lookups[[font, tag, script, language, features]] ||= begin
|
|
68
|
+
raise Error, "unsupported OpenType layout version" unless data.u16(0) == 1 && data.u16(2) <= 1
|
|
69
|
+
scripts = records(data, data.u16(4))
|
|
70
|
+
selected = scripts[script] || scripts["DFLT"]
|
|
71
|
+
indices = []
|
|
72
|
+
if selected
|
|
73
|
+
languages = records(data, selected + 2, selected)
|
|
74
|
+
lang = (language && languages[language.to_s]) || (data.u16(selected).positive? && selected + data.u16(selected))
|
|
75
|
+
if lang
|
|
76
|
+
required = data.u16(lang + 2)
|
|
77
|
+
candidates = words(data, lang + 6, data.u16(lang + 4))
|
|
78
|
+
candidates.unshift(required) unless required == 0xffff
|
|
79
|
+
feature_list = data.u16(6)
|
|
80
|
+
count = data.u16(feature_list)
|
|
81
|
+
data.check(feature_list + 2, count * 6)
|
|
82
|
+
candidates.uniq.each do |index|
|
|
83
|
+
raise Error, "invalid OpenType feature index" if index >= count
|
|
84
|
+
at = feature_list + 2 + index * 6
|
|
85
|
+
next unless index == required || features.include?(data.bytes(at, 4))
|
|
86
|
+
feature = feature_list + data.u16(at + 4)
|
|
87
|
+
indices.concat(words(data, feature + 4, data.u16(feature + 2)))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
list = data.u16(8)
|
|
92
|
+
offsets = words(data, list + 2, data.u16(list))
|
|
93
|
+
indices.uniq.sort.map do |index|
|
|
94
|
+
raise Error, "invalid OpenType lookup index" if index >= offsets.length
|
|
95
|
+
list + offsets[index]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def coverage(data, offset, glyph)
|
|
101
|
+
case data.u16(offset)
|
|
102
|
+
when 1
|
|
103
|
+
count = data.u16(offset + 2)
|
|
104
|
+
data.check(offset + 4, count * 2)
|
|
105
|
+
index = (0...count).bsearch { |i| data.u16(offset + 4 + i * 2) >= glyph }
|
|
106
|
+
index if index && data.u16(offset + 4 + index * 2) == glyph
|
|
107
|
+
when 2
|
|
108
|
+
count = data.u16(offset + 2)
|
|
109
|
+
data.check(offset + 4, count * 6)
|
|
110
|
+
index = (0...count).bsearch { |i| data.u16(offset + 6 + i * 6) >= glyph }
|
|
111
|
+
return unless index
|
|
112
|
+
at = offset + 4 + index * 6
|
|
113
|
+
first, last = data.u16(at), data.u16(at + 2)
|
|
114
|
+
raise Error, "reversed OpenType coverage range" if last < first
|
|
115
|
+
data.u16(at + 4) + glyph - first if glyph.between?(first, last)
|
|
116
|
+
else raise Error, "invalid OpenType coverage format"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def glyph_class(data, at, glyph)
|
|
121
|
+
return 0 if at.zero?
|
|
122
|
+
case data.u16(at)
|
|
123
|
+
when 1
|
|
124
|
+
first, count = data.u16(at + 2), data.u16(at + 4)
|
|
125
|
+
data.check(at + 6, count * 2)
|
|
126
|
+
glyph.between?(first, first + count - 1) ? data.u16(at + 6 + (glyph - first) * 2) : 0
|
|
127
|
+
when 2
|
|
128
|
+
count = data.u16(at + 2)
|
|
129
|
+
data.check(at + 4, count * 6)
|
|
130
|
+
index = (0...count).bsearch { |i| data.u16(at + 6 + i * 6) >= glyph }
|
|
131
|
+
return 0 unless index && data.u16(at + 4 + index * 6) <= glyph
|
|
132
|
+
data.u16(at + 8 + index * 6)
|
|
133
|
+
else raise Error, "invalid OpenType class definition"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def filter(data, lookup, font)
|
|
138
|
+
@filters[[data, lookup, font]] ||= begin
|
|
139
|
+
flags = data.u16(lookup + 2)
|
|
140
|
+
raise Error, "reserved OpenType lookup flags" unless (flags & 0xe0).zero?
|
|
141
|
+
gdef = font.tables.key?("GDEF") && font.table("GDEF")
|
|
142
|
+
if flags & 0xff1e != 0
|
|
143
|
+
raise Error, "lookup filtering requires GDEF glyph classes" unless gdef && gdef.u16(4).positive?
|
|
144
|
+
if flags >> 8 != 0 && flags & 0x18 == 0 && gdef.u16(10).zero?
|
|
145
|
+
raise Error, "lookup filtering requires GDEF mark attachment classes"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
mark_set = nil
|
|
149
|
+
if flags & 0x10 != 0
|
|
150
|
+
raise Error, "missing GDEF mark filtering sets" unless gdef && gdef.u16(0) == 1 && gdef.u16(2) >= 2
|
|
151
|
+
sets = gdef.u16(12)
|
|
152
|
+
raise Error, "missing GDEF mark filtering sets" if sets.zero?
|
|
153
|
+
index = data.u16(lookup + 6 + data.u16(lookup + 4) * 2)
|
|
154
|
+
raise Error, "invalid GDEF mark filtering set" unless gdef.u16(sets) == 1 && index < gdef.u16(sets + 2)
|
|
155
|
+
mark_set = sets + gdef.u32(sets + 4 + index * 4)
|
|
156
|
+
end
|
|
157
|
+
lambda do |glyph|
|
|
158
|
+
next false unless gdef && flags & 0xff1e != 0
|
|
159
|
+
klass = glyph_class(gdef, gdef.u16(4), glyph.id)
|
|
160
|
+
next true if (klass == 1 && flags & 2 != 0) || (klass == 2 && flags & 4 != 0) || (klass == 3 && flags & 8 != 0)
|
|
161
|
+
next false unless klass == 3
|
|
162
|
+
next coverage(gdef, mark_set, glyph.id).nil? if mark_set
|
|
163
|
+
flags >> 8 != 0 && glyph_class(gdef, gdef.u16(10), glyph.id) != flags >> 8
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def next_index(glyphs, index, direction, ignored)
|
|
169
|
+
index += direction
|
|
170
|
+
index += direction while index.between?(0, glyphs.length - 1) && ignored.call(glyphs[index])
|
|
171
|
+
index if index.between?(0, glyphs.length - 1)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def subtables(data, lookup, extension)
|
|
175
|
+
type = data.u16(lookup)
|
|
176
|
+
words(data, lookup + 6, data.u16(lookup + 4)).each do |offset|
|
|
177
|
+
raise Error, "null OpenType subtable" if offset.zero?
|
|
178
|
+
at, actual = lookup + offset, type
|
|
179
|
+
if type == extension
|
|
180
|
+
raise Error, "invalid OpenType extension" unless data.u16(at) == 1
|
|
181
|
+
actual = data.u16(at + 2)
|
|
182
|
+
raise Error, "recursive OpenType extension" if actual == extension
|
|
183
|
+
at += data.u32(at + 4)
|
|
184
|
+
end
|
|
185
|
+
yield actual, at
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def substitute(data, lookup, glyphs, index, size, depth, contexts)
|
|
190
|
+
raise Error, "OpenType contextual lookup recursion limit" if depth > 8
|
|
191
|
+
return unless (glyph = glyphs[index])
|
|
192
|
+
ignored = filter(data, lookup, glyph.font)
|
|
193
|
+
subtables(data, lookup, 7) do |type, at|
|
|
194
|
+
next unless [1, 4, 6].include?(type)
|
|
195
|
+
format = data.u16(at)
|
|
196
|
+
cov = type == 6 && format == 3 ? 0 : coverage(data, at + data.u16(at + 2), glyph.id)
|
|
197
|
+
next unless cov
|
|
198
|
+
case type
|
|
199
|
+
when 1
|
|
200
|
+
id = case format
|
|
201
|
+
when 1 then (glyph.id + data.i16(at + 4)) & 0xffff
|
|
202
|
+
when 2
|
|
203
|
+
substitutes = words(data, at + 6, data.u16(at + 4))
|
|
204
|
+
raise Error, "OpenType substitute coverage exceeds array" if cov >= substitutes.length
|
|
205
|
+
substitutes[cov]
|
|
206
|
+
else raise Error, "invalid single substitution format"
|
|
207
|
+
end
|
|
208
|
+
glyphs[index] = Glyph.new(glyph.font, id, glyph.start, glyph.finish, glyph.x, glyph.font.advance(id, size: size))
|
|
209
|
+
return index + 1
|
|
210
|
+
when 4
|
|
211
|
+
raise Error, "invalid ligature substitution format" unless format == 1
|
|
212
|
+
sets = words(data, at + 6, data.u16(at + 4))
|
|
213
|
+
next if cov >= sets.length || sets[cov].zero?
|
|
214
|
+
set = at + sets[cov]
|
|
215
|
+
words(data, set + 2, data.u16(set)).each do |offset|
|
|
216
|
+
ligature = set + offset
|
|
217
|
+
id, count = data.u16(ligature), data.u16(ligature + 2)
|
|
218
|
+
raise Error, "empty OpenType ligature" if count.zero?
|
|
219
|
+
expected = words(data, ligature + 4, count - 1)
|
|
220
|
+
positions, cursor = [index], index
|
|
221
|
+
matched = expected.all? do |expected_id|
|
|
222
|
+
cursor = next_index(glyphs, cursor, 1, ignored)
|
|
223
|
+
positions << cursor if cursor
|
|
224
|
+
cursor && glyphs[cursor].id == expected_id
|
|
225
|
+
end
|
|
226
|
+
next unless matched
|
|
227
|
+
ending = glyphs[positions.last].finish
|
|
228
|
+
glyphs[index] = Glyph.new(glyph.font, id, glyph.start, ending, glyph.x, glyph.font.advance(id, size: size))
|
|
229
|
+
remove_positions(glyphs, positions.drop(1), contexts)
|
|
230
|
+
return index + 1
|
|
231
|
+
end
|
|
232
|
+
when 6
|
|
233
|
+
ending = contextual(data, at, format, cov, glyphs, index, size, depth, contexts, ignored)
|
|
234
|
+
return ending if ending
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def remove_positions(glyphs, removed, contexts)
|
|
241
|
+
contexts.each do |context|
|
|
242
|
+
context[:positions] = context[:positions].reject { |i| removed.include?(i) }.map { |i| i - removed.count { |j| j < i } }
|
|
243
|
+
context[:end] -= removed.count { |i| i < context[:end] }
|
|
244
|
+
end
|
|
245
|
+
removed.reverse_each { |i| glyphs.delete_at(i) }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def contextual(data, at, format, cov, glyphs, index, size, depth, contexts, ignored)
|
|
249
|
+
if format == 3
|
|
250
|
+
rules, definitions = [at + 2], nil
|
|
251
|
+
elsif [1, 2].include?(format)
|
|
252
|
+
definitions = format == 2 ? words(data, at + 4, 3).map { |offset| at + offset } : nil
|
|
253
|
+
set_index = definitions ? glyph_class(data, definitions[1], glyphs[index].id) : cov
|
|
254
|
+
offset = format == 1 ? 4 : 10
|
|
255
|
+
sets = words(data, at + offset + 2, data.u16(at + offset))
|
|
256
|
+
return if set_index >= sets.length || sets[set_index].zero?
|
|
257
|
+
set = at + sets[set_index]
|
|
258
|
+
rules = words(data, set + 2, data.u16(set)).map { |distance| set + distance }
|
|
259
|
+
else raise Error, "invalid chained context format"
|
|
260
|
+
end
|
|
261
|
+
rules.each do |cursor|
|
|
262
|
+
sequences = []
|
|
263
|
+
3.times do |part|
|
|
264
|
+
count = data.u16(cursor)
|
|
265
|
+
cursor += 2
|
|
266
|
+
raise Error, "empty context input" if part == 1 && count.zero?
|
|
267
|
+
count -= 1 if part == 1 && format != 3
|
|
268
|
+
sequences << words(data, cursor, count)
|
|
269
|
+
cursor += count * 2
|
|
270
|
+
end
|
|
271
|
+
match = lambda do |part, value, position|
|
|
272
|
+
id = glyphs[position].id
|
|
273
|
+
if format == 3
|
|
274
|
+
!coverage(data, at + value, id).nil?
|
|
275
|
+
elsif definitions
|
|
276
|
+
glyph_class(data, definitions[part], id) == value
|
|
277
|
+
else
|
|
278
|
+
id == value
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
previous = index
|
|
282
|
+
next unless sequences[0].all? { |value| previous = next_index(glyphs, previous, -1, ignored); previous && match.call(0, value, previous) }
|
|
283
|
+
positions, current = [index], index
|
|
284
|
+
input = sequences[1].dup
|
|
285
|
+
next if format == 3 && !match.call(1, input.shift, index)
|
|
286
|
+
next unless input.all? do |value|
|
|
287
|
+
current = next_index(glyphs, current, 1, ignored)
|
|
288
|
+
positions << current if current
|
|
289
|
+
current && match.call(1, value, current)
|
|
290
|
+
end
|
|
291
|
+
following = current
|
|
292
|
+
next unless sequences[2].all? { |value| following = next_index(glyphs, following, 1, ignored); following && match.call(2, value, following) }
|
|
293
|
+
count = data.u16(cursor)
|
|
294
|
+
actions = words(data, cursor + 2, count * 2).each_slice(2).to_a
|
|
295
|
+
list = data.u16(8)
|
|
296
|
+
lookups = words(data, list + 2, data.u16(list))
|
|
297
|
+
context = {positions: positions, end: current + 1}
|
|
298
|
+
contexts << context
|
|
299
|
+
actions.each do |sequence, lookup_index|
|
|
300
|
+
raise Error, "invalid contextual lookup index" if lookup_index >= lookups.length
|
|
301
|
+
target = context[:positions][sequence]
|
|
302
|
+
substitute(data, list + lookups[lookup_index], glyphs, target, size, depth + 1, contexts) if target
|
|
303
|
+
end
|
|
304
|
+
contexts.pop
|
|
305
|
+
return context[:end]
|
|
306
|
+
end
|
|
307
|
+
nil
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def positioning(glyphs, size, script, language)
|
|
311
|
+
placements, advances = Array.new(glyphs.length, 0.0), Array.new(glyphs.length, 0.0)
|
|
312
|
+
return [placements, advances] if glyphs.empty?
|
|
313
|
+
font, active = glyphs.first.font, []
|
|
314
|
+
factor = size.to_f / font.units_per_em
|
|
315
|
+
if font.tables.key?("GPOS")
|
|
316
|
+
data = font.table("GPOS")
|
|
317
|
+
active = lookups(data, font, "GPOS", ["kern"], script, language)
|
|
318
|
+
active.each do |lookup|
|
|
319
|
+
ignored = filter(data, lookup, font)
|
|
320
|
+
index = 0
|
|
321
|
+
while index < glyphs.length
|
|
322
|
+
following = next_index(glyphs, index, 1, ignored)
|
|
323
|
+
break unless following
|
|
324
|
+
next_cursor = index + 1
|
|
325
|
+
subtables(data, lookup, 9) do |type, at|
|
|
326
|
+
next unless type == 2
|
|
327
|
+
cov = coverage(data, at + data.u16(at + 2), glyphs[index].id)
|
|
328
|
+
next unless cov
|
|
329
|
+
pair = pair_adjustment(data, at, cov, glyphs[index].id, glyphs[following].id, size, factor)
|
|
330
|
+
next unless pair
|
|
331
|
+
first, second, second_format = pair
|
|
332
|
+
placements[index] += first[0]
|
|
333
|
+
advances[index] += first[1]
|
|
334
|
+
placements[following] += second[0]
|
|
335
|
+
advances[following] += second[1]
|
|
336
|
+
next_cursor = second_format.zero? ? following : following + 1
|
|
337
|
+
break
|
|
338
|
+
end
|
|
339
|
+
index = next_cursor
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
if active.empty?
|
|
344
|
+
glyphs.each_cons(2).with_index { |(left, right), i| advances[i] += legacy_kern(font, left.id, right.id) * factor }
|
|
345
|
+
end
|
|
346
|
+
[placements, advances]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def pair_adjustment(data, at, coverage_index, left, right, size, factor)
|
|
350
|
+
format, first_format, second_format = data.u16(at), data.u16(at + 4), data.u16(at + 6)
|
|
351
|
+
raise Error, "reserved OpenType value format" unless ((first_format | second_format) & 0xff00).zero?
|
|
352
|
+
first_size, second_size = first_format.digits(2).sum * 2, second_format.digits(2).sum * 2
|
|
353
|
+
stride = first_size + second_size
|
|
354
|
+
if format == 1
|
|
355
|
+
sets = words(data, at + 10, data.u16(at + 8))
|
|
356
|
+
return if coverage_index >= sets.length
|
|
357
|
+
parent = at + sets[coverage_index]
|
|
358
|
+
count = data.u16(parent)
|
|
359
|
+
data.check(parent + 2, count * (stride + 2))
|
|
360
|
+
index = (0...count).bsearch { |j| data.u16(parent + 2 + j * (stride + 2)) >= right }
|
|
361
|
+
return unless index && data.u16(parent + 2 + index * (stride + 2)) == right
|
|
362
|
+
record = parent + 4 + index * (stride + 2)
|
|
363
|
+
elsif format == 2
|
|
364
|
+
parent = at
|
|
365
|
+
class1 = glyph_class(data, at + data.u16(at + 8), left)
|
|
366
|
+
class2 = glyph_class(data, at + data.u16(at + 10), right)
|
|
367
|
+
count1, count2 = data.u16(at + 12), data.u16(at + 14)
|
|
368
|
+
data.check(at + 16, count1 * count2 * stride)
|
|
369
|
+
raise Error, "OpenType pair class exceeds matrix" unless class1 < count1 && class2 < count2
|
|
370
|
+
record = at + 16 + (class1 * count2 + class2) * stride
|
|
371
|
+
else raise Error, "invalid pair positioning format"
|
|
372
|
+
end
|
|
373
|
+
[value_record(data, record, first_format, parent, size, factor),
|
|
374
|
+
value_record(data, record + first_size, second_format, parent, size, factor), second_format]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def value_record(data, at, format, parent, size, factor)
|
|
378
|
+
values = Array.new(8, 0)
|
|
379
|
+
8.times do |bit|
|
|
380
|
+
next if format & (1 << bit) == 0
|
|
381
|
+
values[bit] = bit < 4 ? data.i16(at) : data.u16(at)
|
|
382
|
+
at += 2
|
|
383
|
+
end
|
|
384
|
+
# yAdvance is irrelevant to horizontal text; vertical placement needs
|
|
385
|
+
# a native shaper with a two-dimensional glyph attachment model.
|
|
386
|
+
raise Error, "vertical pair placement requires a native shaper" unless values[1].zero? && values[5].zero?
|
|
387
|
+
placement, advance = values[0] * factor, values[2] * factor
|
|
388
|
+
placement += device_delta(data, parent + values[4], size.round) unless values[4].zero?
|
|
389
|
+
advance += device_delta(data, parent + values[6], size.round) unless values[6].zero?
|
|
390
|
+
[placement, advance]
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def device_delta(data, at, ppem)
|
|
394
|
+
first, last, format = data.u16(at), data.u16(at + 2), data.u16(at + 4)
|
|
395
|
+
raise Error, "unsupported OpenType device delta format" unless (1..3).cover?(format)
|
|
396
|
+
raise Error, "reversed device ppem range" if last < first
|
|
397
|
+
bits = 1 << format
|
|
398
|
+
data.check(at + 6, ((last - first + 1) * bits + 15) / 16 * 2)
|
|
399
|
+
return 0 unless ppem.between?(first, last)
|
|
400
|
+
index, mask = ppem - first, (1 << bits) - 1
|
|
401
|
+
value = (data.u16(at + 6 + index * bits / 16 * 2) >> (16 - bits - index * bits % 16)) & mask
|
|
402
|
+
value >= 1 << (bits - 1) ? value - (1 << bits) : value
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def legacy_kern(font, left, right)
|
|
406
|
+
return 0 unless font.tables.key?("kern")
|
|
407
|
+
data = font.table("kern")
|
|
408
|
+
return 0 unless data.u16(0).zero?
|
|
409
|
+
cursor, result = 4, 0
|
|
410
|
+
data.u16(2).times do
|
|
411
|
+
length, flags = data.u16(cursor + 2), data.u16(cursor + 4)
|
|
412
|
+
raise Error, "invalid kern table length" if length < 6
|
|
413
|
+
data.check(cursor, length)
|
|
414
|
+
if flags >> 8 == 0 && flags & 7 == 1
|
|
415
|
+
count = data.u16(cursor + 6)
|
|
416
|
+
raise Error, "kern pairs exceed subtable" if 14 + count * 6 > length
|
|
417
|
+
pair = (left << 16) | right
|
|
418
|
+
index = (0...count).bsearch { |j| data.u32(cursor + 14 + j * 6) >= pair }
|
|
419
|
+
if index && data.u32(cursor + 14 + index * 6) == pair
|
|
420
|
+
value = data.i16(cursor + 18 + index * 6)
|
|
421
|
+
result = flags & 8 == 0 ? result + value : value
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
cursor += length
|
|
425
|
+
end
|
|
426
|
+
result
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|