fontisan 0.1.0 → 0.2.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 +4 -4
- data/.rubocop_todo.yml +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +309 -0
- data/lib/fontisan/collection/builder.rb +260 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +295 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +178 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +121 -12
- data/lib/fontisan/font_writer.rb +301 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +233 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +296 -10
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -0
- data/lib/fontisan/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
- data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
- data/lib/fontisan/tables/cff2.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +270 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +268 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../models/outline"
|
|
4
|
+
require_relative "curve_converter"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# Builds binary TrueType glyph data from universal outline representation
|
|
9
|
+
#
|
|
10
|
+
# [`GlyphBuilder`](lib/fontisan/tables/glyf/glyph_builder.rb) converts the format-agnostic
|
|
11
|
+
# [`Outline`](lib/fontisan/models/outline.rb) model into binary TrueType glyph format.
|
|
12
|
+
# It handles both simple and compound glyphs with proper encoding:
|
|
13
|
+
#
|
|
14
|
+
# **Simple Glyphs**:
|
|
15
|
+
# - Converts universal outline to TrueType contours
|
|
16
|
+
# - Uses [`CurveConverter`](lib/fontisan/tables/glyf/curve_converter.rb) for cubic→quadratic conversion
|
|
17
|
+
# - Delta-encodes coordinates for compact storage
|
|
18
|
+
# - Applies flag compression with run-length encoding
|
|
19
|
+
# - Calculates accurate bounding box
|
|
20
|
+
#
|
|
21
|
+
# **Compound Glyphs**:
|
|
22
|
+
# - Encodes component references
|
|
23
|
+
# - Supports transformation matrices
|
|
24
|
+
# - Handles positioning via points or offsets
|
|
25
|
+
#
|
|
26
|
+
# @example Building a simple glyph from outline
|
|
27
|
+
# outline = Fontisan::Models::Outline.new(...)
|
|
28
|
+
# binary_data = Fontisan::Tables::GlyphBuilder.build_simple_glyph(outline)
|
|
29
|
+
#
|
|
30
|
+
# @example Building a compound glyph
|
|
31
|
+
# components = [
|
|
32
|
+
# { glyph_index: 10, x_offset: 100, y_offset: 0 },
|
|
33
|
+
# { glyph_index: 20, x_offset: 300, y_offset: 0 }
|
|
34
|
+
# ]
|
|
35
|
+
# bbox = { x_min: 0, y_min: 0, x_max: 500, y_max: 700 }
|
|
36
|
+
# binary_data = Fontisan::Tables::GlyphBuilder.build_compound_glyph(components, bbox)
|
|
37
|
+
class GlyphBuilder
|
|
38
|
+
# Flag constants (matching SimpleGlyph)
|
|
39
|
+
ON_CURVE_POINT = 0x01
|
|
40
|
+
X_SHORT_VECTOR = 0x02
|
|
41
|
+
Y_SHORT_VECTOR = 0x04
|
|
42
|
+
REPEAT_FLAG = 0x08
|
|
43
|
+
X_IS_SAME_OR_POSITIVE_X_SHORT = 0x10
|
|
44
|
+
Y_IS_SAME_OR_POSITIVE_Y_SHORT = 0x20
|
|
45
|
+
|
|
46
|
+
# Component flag constants (matching CompoundGlyph)
|
|
47
|
+
ARG_1_AND_2_ARE_WORDS = 0x0001
|
|
48
|
+
ARGS_ARE_XY_VALUES = 0x0002
|
|
49
|
+
ROUND_XY_TO_GRID = 0x0004
|
|
50
|
+
WE_HAVE_A_SCALE = 0x0008
|
|
51
|
+
MORE_COMPONENTS = 0x0020
|
|
52
|
+
WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
|
|
53
|
+
WE_HAVE_A_TWO_BY_TWO = 0x0080
|
|
54
|
+
WE_HAVE_INSTRUCTIONS = 0x0100
|
|
55
|
+
USE_MY_METRICS = 0x0200
|
|
56
|
+
OVERLAP_COMPOUND = 0x0400
|
|
57
|
+
|
|
58
|
+
# Build a simple TrueType glyph from universal outline
|
|
59
|
+
#
|
|
60
|
+
# Converts the universal outline to TrueType format with:
|
|
61
|
+
# - Quadratic curves (cubic curves converted via [`CurveConverter`](lib/fontisan/tables/glyf/curve_converter.rb))
|
|
62
|
+
# - Delta-encoded coordinates
|
|
63
|
+
# - Flag compression
|
|
64
|
+
# - Accurate bounding box
|
|
65
|
+
#
|
|
66
|
+
# @param outline [Fontisan::Models::Outline] Universal outline
|
|
67
|
+
# @param instructions [String] Optional TrueType instructions (default: empty)
|
|
68
|
+
# @return [String] Binary glyph data
|
|
69
|
+
# @raise [ArgumentError] If outline is invalid or empty
|
|
70
|
+
def self.build_simple_glyph(outline, instructions: "".b)
|
|
71
|
+
raise ArgumentError, "outline cannot be nil" if outline.nil?
|
|
72
|
+
raise ArgumentError, "outline must be Outline" unless outline.is_a?(Fontisan::Models::Outline)
|
|
73
|
+
raise ArgumentError, "outline cannot be empty" if outline.empty?
|
|
74
|
+
|
|
75
|
+
# Convert outline to TrueType contours
|
|
76
|
+
contours = outline.to_truetype_contours
|
|
77
|
+
raise ArgumentError, "no contours in outline" if contours.empty?
|
|
78
|
+
|
|
79
|
+
# Calculate bounding box from contours
|
|
80
|
+
bbox = calculate_bounding_box(contours)
|
|
81
|
+
|
|
82
|
+
# Build binary data
|
|
83
|
+
build_simple_glyph_data(contours, bbox, instructions)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Build a compound TrueType glyph
|
|
87
|
+
#
|
|
88
|
+
# Creates a compound glyph by referencing other glyphs with optional
|
|
89
|
+
# transformations. Each component can specify positioning and scaling.
|
|
90
|
+
#
|
|
91
|
+
# @param components [Array<Hash>] Component descriptions
|
|
92
|
+
# Each component hash can contain:
|
|
93
|
+
# - `:glyph_index` (Integer, required): Referenced glyph ID
|
|
94
|
+
# - `:x_offset` (Integer): X offset (default: 0)
|
|
95
|
+
# - `:y_offset` (Integer): Y offset (default: 0)
|
|
96
|
+
# - `:scale` (Float): Uniform scale (optional)
|
|
97
|
+
# - `:scale_x` (Float): X-axis scale (optional)
|
|
98
|
+
# - `:scale_y` (Float): Y-axis scale (optional)
|
|
99
|
+
# - `:scale_01` (Float): Matrix element (0,1) (optional)
|
|
100
|
+
# - `:scale_10` (Float): Matrix element (1,0) (optional)
|
|
101
|
+
# - `:use_my_metrics` (Boolean): Use component's metrics (default: false)
|
|
102
|
+
# - `:overlap` (Boolean): Mark as overlapping (default: false)
|
|
103
|
+
# @param bbox [Hash] Bounding box {:x_min, :y_min, :x_max, :y_max}
|
|
104
|
+
# @param instructions [String] Optional TrueType instructions (default: empty)
|
|
105
|
+
# @return [String] Binary glyph data
|
|
106
|
+
# @raise [ArgumentError] If parameters are invalid
|
|
107
|
+
def self.build_compound_glyph(components, bbox, instructions: "".b)
|
|
108
|
+
raise ArgumentError, "components cannot be nil" if components.nil?
|
|
109
|
+
raise ArgumentError, "components must be Array" unless components.is_a?(Array)
|
|
110
|
+
raise ArgumentError, "components cannot be empty" if components.empty?
|
|
111
|
+
|
|
112
|
+
validate_bbox!(bbox)
|
|
113
|
+
|
|
114
|
+
build_compound_glyph_data(components, bbox, instructions)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private_class_method def self.build_simple_glyph_data(contours, bbox, instructions)
|
|
118
|
+
num_contours = contours.length
|
|
119
|
+
|
|
120
|
+
# Build endPtsOfContours array
|
|
121
|
+
end_pts_of_contours = []
|
|
122
|
+
total_points = 0
|
|
123
|
+
contours.each do |contour|
|
|
124
|
+
total_points += contour.length
|
|
125
|
+
end_pts_of_contours << (total_points - 1)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Flatten all points
|
|
129
|
+
all_points = contours.flatten
|
|
130
|
+
|
|
131
|
+
# Encode flags and coordinates
|
|
132
|
+
flags_data, x_coords_data, y_coords_data = encode_coordinates(all_points)
|
|
133
|
+
|
|
134
|
+
# Build binary data
|
|
135
|
+
data = (+"").force_encoding(Encoding::BINARY)
|
|
136
|
+
|
|
137
|
+
# Header (10 bytes)
|
|
138
|
+
data << [num_contours].pack("n") # numberOfContours
|
|
139
|
+
data << [bbox[:x_min], bbox[:y_min], bbox[:x_max], bbox[:y_max]].pack("n4")
|
|
140
|
+
|
|
141
|
+
# endPtsOfContours
|
|
142
|
+
data << end_pts_of_contours.pack("n*")
|
|
143
|
+
|
|
144
|
+
# Instructions
|
|
145
|
+
data << [instructions.bytesize].pack("n")
|
|
146
|
+
data << instructions if instructions.bytesize.positive?
|
|
147
|
+
|
|
148
|
+
# Flags
|
|
149
|
+
data << flags_data
|
|
150
|
+
|
|
151
|
+
# Coordinates
|
|
152
|
+
data << x_coords_data
|
|
153
|
+
data << y_coords_data
|
|
154
|
+
|
|
155
|
+
data
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private_class_method def self.build_compound_glyph_data(components, bbox, instructions)
|
|
159
|
+
data = (+"").force_encoding(Encoding::BINARY)
|
|
160
|
+
|
|
161
|
+
# Header (10 bytes) - numberOfContours = -1 for compound
|
|
162
|
+
data << [-1].pack("n") # Use signed pack, will convert to 0xFFFF
|
|
163
|
+
data << [bbox[:x_min], bbox[:y_min], bbox[:x_max], bbox[:y_max]].pack("n4")
|
|
164
|
+
|
|
165
|
+
# Encode components
|
|
166
|
+
has_instructions = instructions.bytesize.positive?
|
|
167
|
+
components.each_with_index do |component, index|
|
|
168
|
+
is_last = (index == components.length - 1)
|
|
169
|
+
component_data = encode_component(component, is_last, has_instructions)
|
|
170
|
+
data << component_data
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Instructions (if any)
|
|
174
|
+
if has_instructions
|
|
175
|
+
data << [instructions.bytesize].pack("n")
|
|
176
|
+
data << instructions
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
data
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private_class_method def self.encode_component(component, is_last, has_instructions)
|
|
183
|
+
validate_component!(component)
|
|
184
|
+
|
|
185
|
+
glyph_index = component[:glyph_index]
|
|
186
|
+
x_offset = component[:x_offset] || 0
|
|
187
|
+
y_offset = component[:y_offset] || 0
|
|
188
|
+
|
|
189
|
+
# Build flags
|
|
190
|
+
flags = ARGS_ARE_XY_VALUES # Always use x,y offsets
|
|
191
|
+
|
|
192
|
+
# Determine if we need 16-bit arguments
|
|
193
|
+
if x_offset.abs > 127 || y_offset.abs > 127
|
|
194
|
+
flags |= ARG_1_AND_2_ARE_WORDS
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Add transformation flags
|
|
198
|
+
if component[:scale_01] || component[:scale_10]
|
|
199
|
+
# 2x2 matrix
|
|
200
|
+
flags |= WE_HAVE_A_TWO_BY_TWO
|
|
201
|
+
elsif component[:scale_x] && component[:scale_y]
|
|
202
|
+
# Separate x,y scale
|
|
203
|
+
flags |= WE_HAVE_AN_X_AND_Y_SCALE
|
|
204
|
+
elsif component[:scale]
|
|
205
|
+
# Uniform scale
|
|
206
|
+
flags |= WE_HAVE_A_SCALE
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Add more components flag if not last
|
|
210
|
+
flags |= MORE_COMPONENTS unless is_last
|
|
211
|
+
|
|
212
|
+
# Add instructions flag if last and has instructions
|
|
213
|
+
flags |= WE_HAVE_INSTRUCTIONS if is_last && has_instructions
|
|
214
|
+
|
|
215
|
+
# Add optional flags
|
|
216
|
+
flags |= USE_MY_METRICS if component[:use_my_metrics]
|
|
217
|
+
flags |= OVERLAP_COMPOUND if component[:overlap]
|
|
218
|
+
|
|
219
|
+
# Build binary data
|
|
220
|
+
data = (+"").force_encoding(Encoding::BINARY)
|
|
221
|
+
data << [flags, glyph_index].pack("n2")
|
|
222
|
+
|
|
223
|
+
# Encode arguments
|
|
224
|
+
data << if (flags & ARG_1_AND_2_ARE_WORDS).zero?
|
|
225
|
+
# 8-bit signed
|
|
226
|
+
[x_offset, y_offset].pack("c2")
|
|
227
|
+
else
|
|
228
|
+
# 16-bit signed
|
|
229
|
+
[x_offset, y_offset].pack("n2")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Encode transformation
|
|
233
|
+
if (flags & WE_HAVE_A_TWO_BY_TWO) != 0
|
|
234
|
+
# 2x2 matrix (4 F2DOT14 values)
|
|
235
|
+
scale_x = component[:scale_x] || 1.0
|
|
236
|
+
scale_y = component[:scale_y] || 1.0
|
|
237
|
+
scale_01 = component[:scale_01] || 0.0
|
|
238
|
+
scale_10 = component[:scale_10] || 0.0
|
|
239
|
+
data << [
|
|
240
|
+
float_to_f2dot14(scale_x),
|
|
241
|
+
float_to_f2dot14(scale_01),
|
|
242
|
+
float_to_f2dot14(scale_10),
|
|
243
|
+
float_to_f2dot14(scale_y),
|
|
244
|
+
].pack("n4")
|
|
245
|
+
elsif (flags & WE_HAVE_AN_X_AND_Y_SCALE) != 0
|
|
246
|
+
# Separate x,y scale (2 F2DOT14 values)
|
|
247
|
+
scale_x = component[:scale_x] || 1.0
|
|
248
|
+
scale_y = component[:scale_y] || 1.0
|
|
249
|
+
data << [
|
|
250
|
+
float_to_f2dot14(scale_x),
|
|
251
|
+
float_to_f2dot14(scale_y),
|
|
252
|
+
].pack("n2")
|
|
253
|
+
elsif (flags & WE_HAVE_A_SCALE) != 0
|
|
254
|
+
# Uniform scale (1 F2DOT14 value)
|
|
255
|
+
scale = component[:scale] || 1.0
|
|
256
|
+
data << [float_to_f2dot14(scale)].pack("n")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
data
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
private_class_method def self.encode_coordinates(points)
|
|
263
|
+
flags = []
|
|
264
|
+
x_deltas = []
|
|
265
|
+
y_deltas = []
|
|
266
|
+
|
|
267
|
+
prev_x = 0
|
|
268
|
+
prev_y = 0
|
|
269
|
+
|
|
270
|
+
# Calculate deltas and determine flags
|
|
271
|
+
points.each do |point|
|
|
272
|
+
x = point[:x]
|
|
273
|
+
y = point[:y]
|
|
274
|
+
on_curve = point[:on_curve]
|
|
275
|
+
|
|
276
|
+
dx = x - prev_x
|
|
277
|
+
dy = y - prev_y
|
|
278
|
+
|
|
279
|
+
flag = 0
|
|
280
|
+
flag |= ON_CURVE_POINT if on_curve
|
|
281
|
+
|
|
282
|
+
# X coordinate encoding
|
|
283
|
+
if dx.zero?
|
|
284
|
+
flag |= X_IS_SAME_OR_POSITIVE_X_SHORT
|
|
285
|
+
elsif dx >= -255 && dx <= 255
|
|
286
|
+
flag |= X_SHORT_VECTOR
|
|
287
|
+
flag |= X_IS_SAME_OR_POSITIVE_X_SHORT if dx.positive?
|
|
288
|
+
x_deltas << dx.abs
|
|
289
|
+
else
|
|
290
|
+
x_deltas << dx
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Y coordinate encoding
|
|
294
|
+
if dy.zero?
|
|
295
|
+
flag |= Y_IS_SAME_OR_POSITIVE_Y_SHORT
|
|
296
|
+
elsif dy >= -255 && dy <= 255
|
|
297
|
+
flag |= Y_SHORT_VECTOR
|
|
298
|
+
flag |= Y_IS_SAME_OR_POSITIVE_Y_SHORT if dy.positive?
|
|
299
|
+
y_deltas << dy.abs
|
|
300
|
+
else
|
|
301
|
+
y_deltas << dy
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
flags << flag
|
|
305
|
+
prev_x = x
|
|
306
|
+
prev_y = y
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Apply RLE compression to flags
|
|
310
|
+
flags_data = compress_flags(flags)
|
|
311
|
+
|
|
312
|
+
# Encode coordinates
|
|
313
|
+
x_coords_data = encode_coordinate_values(flags, x_deltas, :x)
|
|
314
|
+
y_coords_data = encode_coordinate_values(flags, y_deltas, :y)
|
|
315
|
+
|
|
316
|
+
[flags_data, x_coords_data, y_coords_data]
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
private_class_method def self.compress_flags(flags)
|
|
320
|
+
data = (+"").force_encoding(Encoding::BINARY)
|
|
321
|
+
i = 0
|
|
322
|
+
|
|
323
|
+
while i < flags.length
|
|
324
|
+
flag = flags[i]
|
|
325
|
+
count = 1
|
|
326
|
+
|
|
327
|
+
# Count consecutive identical flags
|
|
328
|
+
while i + count < flags.length && flags[i + count] == flag && count < 256
|
|
329
|
+
count += 1
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
if count > 1
|
|
333
|
+
# Use repeat flag
|
|
334
|
+
data << [flag | REPEAT_FLAG].pack("C")
|
|
335
|
+
data << [count - 1].pack("C") # Repeat count (0 means repeat once more)
|
|
336
|
+
i += count
|
|
337
|
+
else
|
|
338
|
+
# Single flag
|
|
339
|
+
data << [flag].pack("C")
|
|
340
|
+
i += 1
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
data
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
private_class_method def self.encode_coordinate_values(flags, deltas, axis)
|
|
348
|
+
data = (+"").force_encoding(Encoding::BINARY)
|
|
349
|
+
short_flag = axis == :x ? X_SHORT_VECTOR : Y_SHORT_VECTOR
|
|
350
|
+
same_flag = axis == :x ? X_IS_SAME_OR_POSITIVE_X_SHORT : Y_IS_SAME_OR_POSITIVE_Y_SHORT
|
|
351
|
+
|
|
352
|
+
delta_index = 0
|
|
353
|
+
|
|
354
|
+
flags.each do |flag|
|
|
355
|
+
if (flag & short_flag) != 0
|
|
356
|
+
# 1-byte coordinate (already absolute value in deltas)
|
|
357
|
+
data << [deltas[delta_index]].pack("C")
|
|
358
|
+
delta_index += 1
|
|
359
|
+
elsif (flag & same_flag) != 0
|
|
360
|
+
# Same as previous (delta = 0), no data
|
|
361
|
+
else
|
|
362
|
+
# 2-byte signed coordinate
|
|
363
|
+
delta = deltas[delta_index]
|
|
364
|
+
# Pack as signed 16-bit big-endian
|
|
365
|
+
data << [delta].pack("n") # Will need to convert to signed
|
|
366
|
+
delta_index += 1
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
data
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
private_class_method def self.calculate_bounding_box(contours)
|
|
374
|
+
x_min = Float::INFINITY
|
|
375
|
+
y_min = Float::INFINITY
|
|
376
|
+
x_max = -Float::INFINITY
|
|
377
|
+
y_max = -Float::INFINITY
|
|
378
|
+
|
|
379
|
+
contours.each do |contour|
|
|
380
|
+
contour.each do |point|
|
|
381
|
+
x = point[:x]
|
|
382
|
+
y = point[:y]
|
|
383
|
+
|
|
384
|
+
x_min = x if x < x_min
|
|
385
|
+
y_min = y if y < y_min
|
|
386
|
+
x_max = x if x > x_max
|
|
387
|
+
y_max = y if y > y_max
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
{
|
|
392
|
+
x_min: x_min.round,
|
|
393
|
+
y_min: y_min.round,
|
|
394
|
+
x_max: x_max.round,
|
|
395
|
+
y_max: y_max.round,
|
|
396
|
+
}
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
private_class_method def self.float_to_f2dot14(value)
|
|
400
|
+
# Convert float to F2DOT14 fixed-point format
|
|
401
|
+
# F2DOT14: 2 bits integer, 14 bits fractional
|
|
402
|
+
# Range: -2.0 to ~1.99993896484375
|
|
403
|
+
raise ArgumentError, "value out of F2DOT14 range" if value < -2.0 || value > 2.0
|
|
404
|
+
|
|
405
|
+
fixed = (value * 16_384.0).round
|
|
406
|
+
# Convert to unsigned 16-bit
|
|
407
|
+
fixed.negative? ? fixed + 65_536 : fixed
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
private_class_method def self.validate_bbox!(bbox)
|
|
411
|
+
raise ArgumentError, "bbox cannot be nil" if bbox.nil?
|
|
412
|
+
raise ArgumentError, "bbox must be Hash" unless bbox.is_a?(Hash)
|
|
413
|
+
|
|
414
|
+
required = %i[x_min y_min x_max y_max]
|
|
415
|
+
missing = required - bbox.keys
|
|
416
|
+
unless missing.empty?
|
|
417
|
+
raise ArgumentError, "bbox missing keys: #{missing.join(', ')}"
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
required.each do |key|
|
|
421
|
+
value = bbox[key]
|
|
422
|
+
unless value.is_a?(Numeric)
|
|
423
|
+
raise ArgumentError, "bbox[:#{key}] must be Numeric"
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
if bbox[:x_min] > bbox[:x_max]
|
|
428
|
+
raise ArgumentError, "bbox x_min must be <= x_max"
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
if bbox[:y_min] > bbox[:y_max]
|
|
432
|
+
raise ArgumentError, "bbox y_min must be <= y_max"
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
private_class_method def self.validate_component!(component)
|
|
437
|
+
raise ArgumentError, "component must be Hash" unless component.is_a?(Hash)
|
|
438
|
+
unless component[:glyph_index]
|
|
439
|
+
raise ArgumentError, "component must have :glyph_index"
|
|
440
|
+
end
|
|
441
|
+
unless component[:glyph_index].is_a?(Integer)
|
|
442
|
+
raise ArgumentError, "component :glyph_index must be Integer"
|
|
443
|
+
end
|
|
444
|
+
if component[:glyph_index].negative?
|
|
445
|
+
raise ArgumentError, "component :glyph_index must be non-negative"
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|