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,664 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Models
|
|
5
|
+
# Universal outline representation for format-agnostic glyph outlines
|
|
6
|
+
#
|
|
7
|
+
# [`Outline`](lib/fontisan/models/outline.rb) provides a format-independent
|
|
8
|
+
# representation of glyph outlines that can be converted to/from both
|
|
9
|
+
# TrueType (quadratic) and CFF (cubic) formats. This enables bidirectional
|
|
10
|
+
# TTF ↔ OTF conversion.
|
|
11
|
+
#
|
|
12
|
+
# The outline stores paths as a sequence of drawing commands:
|
|
13
|
+
# - **move_to**: Start a new contour at (x, y)
|
|
14
|
+
# - **line_to**: Draw a line to (x, y)
|
|
15
|
+
# - **quad_to**: Quadratic Bézier curve with control point (cx, cy) to (x, y)
|
|
16
|
+
# - **curve_to**: Cubic Bézier curve with control points (cx1, cy1), (cx2, cy2) to (x, y)
|
|
17
|
+
# - **close_path**: Close the current contour
|
|
18
|
+
#
|
|
19
|
+
# This command-based representation:
|
|
20
|
+
# - Is format-agnostic (works for both TrueType and CFF)
|
|
21
|
+
# - Preserves curve type information
|
|
22
|
+
# - Makes conversion logic clear and testable
|
|
23
|
+
# - Enables easy validation and manipulation
|
|
24
|
+
#
|
|
25
|
+
# @example Creating an outline from commands
|
|
26
|
+
# outline = Fontisan::Models::Outline.new(
|
|
27
|
+
# glyph_id: 65,
|
|
28
|
+
# commands: [
|
|
29
|
+
# { type: :move_to, x: 100, y: 0 },
|
|
30
|
+
# { type: :line_to, x: 200, y: 700 },
|
|
31
|
+
# { type: :line_to, x: 300, y: 0 },
|
|
32
|
+
# { type: :close_path }
|
|
33
|
+
# ],
|
|
34
|
+
# bbox: { x_min: 100, y_min: 0, x_max: 300, y_max: 700 },
|
|
35
|
+
# width: 400
|
|
36
|
+
# )
|
|
37
|
+
#
|
|
38
|
+
# @example Converting from TrueType
|
|
39
|
+
# outline = Fontisan::Models::Outline.from_truetype(glyph, glyph_id)
|
|
40
|
+
#
|
|
41
|
+
# @example Converting from CFF
|
|
42
|
+
# outline = Fontisan::Models::Outline.from_cff(charstring, glyph_id)
|
|
43
|
+
class Outline
|
|
44
|
+
# @return [Integer] Glyph identifier
|
|
45
|
+
attr_reader :glyph_id
|
|
46
|
+
|
|
47
|
+
# @return [Array<Hash>] Array of drawing commands
|
|
48
|
+
# Each command is a hash with :type and coordinate keys
|
|
49
|
+
attr_reader :commands
|
|
50
|
+
|
|
51
|
+
# @return [Hash] Bounding box {:x_min, :y_min, :x_max, :y_max}
|
|
52
|
+
attr_reader :bbox
|
|
53
|
+
|
|
54
|
+
# @return [Integer, nil] Advance width (optional)
|
|
55
|
+
attr_reader :width
|
|
56
|
+
|
|
57
|
+
# Initialize a new universal outline
|
|
58
|
+
#
|
|
59
|
+
# @param glyph_id [Integer] Glyph identifier
|
|
60
|
+
# @param commands [Array<Hash>] Drawing commands
|
|
61
|
+
# @param bbox [Hash] Bounding box
|
|
62
|
+
# @param width [Integer, nil] Advance width (optional)
|
|
63
|
+
# @raise [ArgumentError] If parameters are invalid
|
|
64
|
+
def initialize(glyph_id:, commands:, bbox:, width: nil)
|
|
65
|
+
validate_parameters!(glyph_id, commands, bbox)
|
|
66
|
+
|
|
67
|
+
@glyph_id = glyph_id
|
|
68
|
+
@commands = commands.freeze
|
|
69
|
+
@bbox = bbox.freeze
|
|
70
|
+
@width = width
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Create outline from TrueType glyph
|
|
74
|
+
#
|
|
75
|
+
# TrueType glyphs use quadratic Bézier curves. This method extracts
|
|
76
|
+
# the contours and converts them to our universal command format.
|
|
77
|
+
#
|
|
78
|
+
# @param glyph [SimpleGlyph, CompoundGlyph] TrueType glyph object
|
|
79
|
+
# @param glyph_id [Integer] Glyph identifier
|
|
80
|
+
# @return [Outline] Universal outline instance
|
|
81
|
+
# @raise [ArgumentError] If glyph is invalid
|
|
82
|
+
def self.from_truetype(glyph, glyph_id)
|
|
83
|
+
raise ArgumentError, "glyph cannot be nil" if glyph.nil?
|
|
84
|
+
raise ArgumentError, "glyph must be simple glyph" unless glyph.simple?
|
|
85
|
+
|
|
86
|
+
commands = []
|
|
87
|
+
bbox = {
|
|
88
|
+
x_min: glyph.x_min,
|
|
89
|
+
y_min: glyph.y_min,
|
|
90
|
+
x_max: glyph.x_max,
|
|
91
|
+
y_max: glyph.y_max,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Process each contour
|
|
95
|
+
glyph.num_contours.times do |contour_index|
|
|
96
|
+
points = glyph.points_for_contour(contour_index)
|
|
97
|
+
next if points.nil? || points.empty?
|
|
98
|
+
|
|
99
|
+
contour_commands = convert_truetype_contour_to_commands(points)
|
|
100
|
+
commands.concat(contour_commands)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
new(
|
|
104
|
+
glyph_id: glyph_id,
|
|
105
|
+
commands: commands,
|
|
106
|
+
bbox: bbox,
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Create outline from CFF CharString
|
|
111
|
+
#
|
|
112
|
+
# CFF uses cubic Bézier curves. This method executes the CharString
|
|
113
|
+
# and converts the path to our universal command format.
|
|
114
|
+
#
|
|
115
|
+
# @param charstring [CharString] CFF CharString object
|
|
116
|
+
# @param glyph_id [Integer] Glyph identifier
|
|
117
|
+
# @return [Outline] Universal outline instance
|
|
118
|
+
# @raise [ArgumentError] If charstring is invalid
|
|
119
|
+
def self.from_cff(charstring, glyph_id)
|
|
120
|
+
raise ArgumentError, "charstring cannot be nil" if charstring.nil?
|
|
121
|
+
|
|
122
|
+
# Get path from CharString
|
|
123
|
+
path = charstring.path
|
|
124
|
+
raise ArgumentError, "CharString has no path data" if path.nil? || path.empty?
|
|
125
|
+
|
|
126
|
+
commands = convert_cff_path_to_commands(path)
|
|
127
|
+
|
|
128
|
+
# Get bounding box
|
|
129
|
+
bbox_array = charstring.bounding_box
|
|
130
|
+
raise ArgumentError, "CharString has no bounding box" unless bbox_array
|
|
131
|
+
|
|
132
|
+
bbox = {
|
|
133
|
+
x_min: bbox_array[0],
|
|
134
|
+
y_min: bbox_array[1],
|
|
135
|
+
x_max: bbox_array[2],
|
|
136
|
+
y_max: bbox_array[3],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
new(
|
|
140
|
+
glyph_id: glyph_id,
|
|
141
|
+
commands: commands,
|
|
142
|
+
bbox: bbox,
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Convert to TrueType contour format
|
|
147
|
+
#
|
|
148
|
+
# Converts universal commands to TrueType contour format with
|
|
149
|
+
# quadratic curves. Cubic curves are approximated as quadratics.
|
|
150
|
+
#
|
|
151
|
+
# @return [Array<Array<Hash>>] Array of contours
|
|
152
|
+
def to_truetype_contours
|
|
153
|
+
contours = []
|
|
154
|
+
current_contour = []
|
|
155
|
+
|
|
156
|
+
commands.each do |cmd|
|
|
157
|
+
case cmd[:type]
|
|
158
|
+
when :move_to
|
|
159
|
+
# Start new contour
|
|
160
|
+
contours << current_contour unless current_contour.empty?
|
|
161
|
+
current_contour = []
|
|
162
|
+
current_contour << {
|
|
163
|
+
x: cmd[:x].round,
|
|
164
|
+
y: cmd[:y].round,
|
|
165
|
+
on_curve: true,
|
|
166
|
+
}
|
|
167
|
+
when :line_to
|
|
168
|
+
current_contour << {
|
|
169
|
+
x: cmd[:x].round,
|
|
170
|
+
y: cmd[:y].round,
|
|
171
|
+
on_curve: true,
|
|
172
|
+
}
|
|
173
|
+
when :quad_to
|
|
174
|
+
# Quadratic curve - add control point and end point
|
|
175
|
+
current_contour << {
|
|
176
|
+
x: cmd[:cx].round,
|
|
177
|
+
y: cmd[:cy].round,
|
|
178
|
+
on_curve: false,
|
|
179
|
+
}
|
|
180
|
+
current_contour << {
|
|
181
|
+
x: cmd[:x].round,
|
|
182
|
+
y: cmd[:y].round,
|
|
183
|
+
on_curve: true,
|
|
184
|
+
}
|
|
185
|
+
when :curve_to
|
|
186
|
+
# Cubic curve - approximate as quadratic
|
|
187
|
+
# Convert cubic Bézier to quadratic (may need multiple segments)
|
|
188
|
+
# For now, use simple midpoint approximation
|
|
189
|
+
control_x = ((cmd[:cx1] + cmd[:cx2]) / 2.0).round
|
|
190
|
+
control_y = ((cmd[:cy1] + cmd[:cy2]) / 2.0).round
|
|
191
|
+
|
|
192
|
+
current_contour << {
|
|
193
|
+
x: control_x,
|
|
194
|
+
y: control_y,
|
|
195
|
+
on_curve: false,
|
|
196
|
+
}
|
|
197
|
+
current_contour << {
|
|
198
|
+
x: cmd[:x].round,
|
|
199
|
+
y: cmd[:y].round,
|
|
200
|
+
on_curve: true,
|
|
201
|
+
}
|
|
202
|
+
when :close_path
|
|
203
|
+
# Close contour
|
|
204
|
+
contours << current_contour unless current_contour.empty?
|
|
205
|
+
current_contour = []
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Add final contour if not closed
|
|
210
|
+
contours << current_contour unless current_contour.empty?
|
|
211
|
+
|
|
212
|
+
contours
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Convert to CFF drawing commands
|
|
216
|
+
#
|
|
217
|
+
# Converts universal commands to CFF CharString format with
|
|
218
|
+
# cubic curves. Quadratic curves are elevation to cubic (exact).
|
|
219
|
+
#
|
|
220
|
+
# @return [Array<Hash>] Array of CFF command hashes
|
|
221
|
+
def to_cff_commands
|
|
222
|
+
cff_commands = []
|
|
223
|
+
|
|
224
|
+
commands.each do |cmd|
|
|
225
|
+
case cmd[:type]
|
|
226
|
+
when :move_to
|
|
227
|
+
cff_commands << {
|
|
228
|
+
type: :move_to,
|
|
229
|
+
x: cmd[:x].round,
|
|
230
|
+
y: cmd[:y].round,
|
|
231
|
+
}
|
|
232
|
+
when :line_to
|
|
233
|
+
cff_commands << {
|
|
234
|
+
type: :line_to,
|
|
235
|
+
x: cmd[:x].round,
|
|
236
|
+
y: cmd[:y].round,
|
|
237
|
+
}
|
|
238
|
+
when :quad_to
|
|
239
|
+
# Quadratic to cubic (degree elevation - exact conversion)
|
|
240
|
+
# For quadratic: P0 (current), P1 (control), P2 (end)
|
|
241
|
+
# Cubic control points: CP1 = P0 + 2/3*(P1 - P0), CP2 = P2 + 2/3*(P1 - P2)
|
|
242
|
+
# We need the previous point (P0)
|
|
243
|
+
prev = find_previous_point(cff_commands)
|
|
244
|
+
|
|
245
|
+
cx1 = (prev[:x] + (2.0 / 3.0) * (cmd[:cx] - prev[:x])).round
|
|
246
|
+
cy1 = (prev[:y] + (2.0 / 3.0) * (cmd[:cy] - prev[:y])).round
|
|
247
|
+
|
|
248
|
+
cx2 = (cmd[:x] + (2.0 / 3.0) * (cmd[:cx] - cmd[:x])).round
|
|
249
|
+
cy2 = (cmd[:y] + (2.0 / 3.0) * (cmd[:cy] - cmd[:y])).round
|
|
250
|
+
|
|
251
|
+
cff_commands << {
|
|
252
|
+
type: :curve_to,
|
|
253
|
+
x1: cx1,
|
|
254
|
+
y1: cy1,
|
|
255
|
+
x2: cx2,
|
|
256
|
+
y2: cy2,
|
|
257
|
+
x: cmd[:x].round,
|
|
258
|
+
y: cmd[:y].round,
|
|
259
|
+
}
|
|
260
|
+
when :curve_to
|
|
261
|
+
# Already cubic - direct mapping
|
|
262
|
+
cff_commands << {
|
|
263
|
+
type: :curve_to,
|
|
264
|
+
x1: cmd[:cx1].round,
|
|
265
|
+
y1: cmd[:cy1].round,
|
|
266
|
+
x2: cmd[:cx2].round,
|
|
267
|
+
y2: cmd[:cy2].round,
|
|
268
|
+
x: cmd[:x].round,
|
|
269
|
+
y: cmd[:y].round,
|
|
270
|
+
}
|
|
271
|
+
when :close_path
|
|
272
|
+
# CFF doesn't have explicit close - handled by move_to
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
cff_commands
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Check if outline is empty
|
|
280
|
+
#
|
|
281
|
+
# @return [Boolean] True if no drawing commands
|
|
282
|
+
def empty?
|
|
283
|
+
commands.empty? || commands.all? { |cmd| cmd[:type] == :close_path }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Get number of commands
|
|
287
|
+
#
|
|
288
|
+
# @return [Integer] Number of commands
|
|
289
|
+
def command_count
|
|
290
|
+
commands.length
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Get number of contours
|
|
294
|
+
#
|
|
295
|
+
# @return [Integer] Number of contours
|
|
296
|
+
def contour_count
|
|
297
|
+
commands.count { |cmd| cmd[:type] == :move_to }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# String representation
|
|
301
|
+
#
|
|
302
|
+
# @return [String] Human-readable representation
|
|
303
|
+
def to_s
|
|
304
|
+
"#<#{self.class.name} glyph_id=#{glyph_id} " \
|
|
305
|
+
"commands=#{command_count} contours=#{contour_count} " \
|
|
306
|
+
"bbox=#{bbox.inspect}>"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
alias inspect to_s
|
|
310
|
+
|
|
311
|
+
# Apply affine transformation to outline
|
|
312
|
+
#
|
|
313
|
+
# Applies a 2x3 affine transformation matrix to all points in the outline.
|
|
314
|
+
# The matrix is in the format [a, b, c, d, e, f] representing:
|
|
315
|
+
# x' = a*x + c*y + e
|
|
316
|
+
# y' = b*x + d*y + f
|
|
317
|
+
#
|
|
318
|
+
# @param matrix [Array<Float>] Transformation matrix [a, b, c, d, e, f]
|
|
319
|
+
# @return [Outline] New outline with transformed commands
|
|
320
|
+
def transform(matrix)
|
|
321
|
+
a, b, c, d, e, f = matrix
|
|
322
|
+
|
|
323
|
+
# Transform all commands
|
|
324
|
+
transformed_commands = commands.map do |cmd|
|
|
325
|
+
case cmd[:type]
|
|
326
|
+
when :move_to, :line_to
|
|
327
|
+
{
|
|
328
|
+
type: cmd[:type],
|
|
329
|
+
x: (a * cmd[:x] + c * cmd[:y] + e),
|
|
330
|
+
y: (b * cmd[:x] + d * cmd[:y] + f),
|
|
331
|
+
}
|
|
332
|
+
when :quad_to
|
|
333
|
+
{
|
|
334
|
+
type: :quad_to,
|
|
335
|
+
cx: (a * cmd[:cx] + c * cmd[:cy] + e),
|
|
336
|
+
cy: (b * cmd[:cx] + d * cmd[:cy] + f),
|
|
337
|
+
x: (a * cmd[:x] + c * cmd[:y] + e),
|
|
338
|
+
y: (b * cmd[:x] + d * cmd[:y] + f),
|
|
339
|
+
}
|
|
340
|
+
when :curve_to
|
|
341
|
+
{
|
|
342
|
+
type: :curve_to,
|
|
343
|
+
cx1: (a * cmd[:cx1] + c * cmd[:cy1] + e),
|
|
344
|
+
cy1: (b * cmd[:cx1] + d * cmd[:cy1] + f),
|
|
345
|
+
cx2: (a * cmd[:cx2] + c * cmd[:cy2] + e),
|
|
346
|
+
cy2: (b * cmd[:cx2] + d * cmd[:cy2] + f),
|
|
347
|
+
x: (a * cmd[:x] + c * cmd[:y] + e),
|
|
348
|
+
y: (b * cmd[:x] + d * cmd[:y] + f),
|
|
349
|
+
}
|
|
350
|
+
when :close_path
|
|
351
|
+
cmd
|
|
352
|
+
else
|
|
353
|
+
cmd
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Calculate transformed bounding box
|
|
358
|
+
# Apply transformation to all four corners
|
|
359
|
+
corners = [
|
|
360
|
+
[bbox[:x_min], bbox[:y_min]],
|
|
361
|
+
[bbox[:x_max], bbox[:y_min]],
|
|
362
|
+
[bbox[:x_min], bbox[:y_max]],
|
|
363
|
+
[bbox[:x_max], bbox[:y_max]],
|
|
364
|
+
].map do |x, y|
|
|
365
|
+
[a * x + c * y + e, b * x + d * y + f]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
x_coords = corners.map(&:first)
|
|
369
|
+
y_coords = corners.map(&:last)
|
|
370
|
+
|
|
371
|
+
transformed_bbox = {
|
|
372
|
+
x_min: x_coords.min.round,
|
|
373
|
+
y_min: y_coords.min.round,
|
|
374
|
+
x_max: x_coords.max.round,
|
|
375
|
+
y_max: y_coords.max.round,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
Outline.new(
|
|
379
|
+
glyph_id: glyph_id,
|
|
380
|
+
commands: transformed_commands,
|
|
381
|
+
bbox: transformed_bbox,
|
|
382
|
+
width: width,
|
|
383
|
+
)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Merge another outline into this one
|
|
387
|
+
#
|
|
388
|
+
# Combines the commands from another outline with this one,
|
|
389
|
+
# creating a composite outline. The bounding box is recalculated
|
|
390
|
+
# to encompass both outlines.
|
|
391
|
+
#
|
|
392
|
+
# @param other [Outline] Outline to merge
|
|
393
|
+
# @return [void]
|
|
394
|
+
def merge!(other)
|
|
395
|
+
return if other.empty?
|
|
396
|
+
|
|
397
|
+
# Merge commands (skip close_path before adding new contours)
|
|
398
|
+
merged_commands = commands.dup
|
|
399
|
+
merged_commands.pop if merged_commands.last && merged_commands.last[:type] == :close_path
|
|
400
|
+
|
|
401
|
+
# Add other's commands
|
|
402
|
+
merged_commands.concat(other.commands)
|
|
403
|
+
|
|
404
|
+
# Recalculate bounding box
|
|
405
|
+
merged_bbox = {
|
|
406
|
+
x_min: [bbox[:x_min], other.bbox[:x_min]].min,
|
|
407
|
+
y_min: [bbox[:y_min], other.bbox[:y_min]].min,
|
|
408
|
+
x_max: [bbox[:x_max], other.bbox[:x_max]].max,
|
|
409
|
+
y_max: [bbox[:y_max], other.bbox[:y_max]].max,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
# Update instance variables
|
|
413
|
+
@commands = merged_commands.freeze
|
|
414
|
+
@bbox = merged_bbox.freeze
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
private
|
|
418
|
+
|
|
419
|
+
# Validate initialization parameters
|
|
420
|
+
#
|
|
421
|
+
# @param glyph_id [Integer] Glyph ID
|
|
422
|
+
# @param commands [Array] Commands array
|
|
423
|
+
# @param bbox [Hash] Bounding box
|
|
424
|
+
# @raise [ArgumentError] If validation fails
|
|
425
|
+
def validate_parameters!(glyph_id, commands, bbox)
|
|
426
|
+
if glyph_id.nil? || !glyph_id.is_a?(Integer) || glyph_id.negative?
|
|
427
|
+
raise ArgumentError,
|
|
428
|
+
"glyph_id must be non-negative Integer, got: #{glyph_id.inspect}"
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
unless commands.is_a?(Array)
|
|
432
|
+
raise ArgumentError,
|
|
433
|
+
"commands must be Array, got: #{commands.class}"
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
unless bbox.is_a?(Hash)
|
|
437
|
+
raise ArgumentError,
|
|
438
|
+
"bbox must be Hash, got: #{bbox.class}"
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
required_keys = %i[x_min y_min x_max y_max]
|
|
442
|
+
missing_keys = required_keys - bbox.keys
|
|
443
|
+
unless missing_keys.empty?
|
|
444
|
+
raise ArgumentError,
|
|
445
|
+
"bbox missing keys: #{missing_keys.join(', ')}"
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Validate commands
|
|
449
|
+
commands.each_with_index do |cmd, i|
|
|
450
|
+
unless cmd.is_a?(Hash) && cmd.key?(:type)
|
|
451
|
+
raise ArgumentError,
|
|
452
|
+
"command #{i} must be Hash with :type key"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
validate_command!(cmd, i)
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Validate individual command
|
|
460
|
+
#
|
|
461
|
+
# @param cmd [Hash] Command to validate
|
|
462
|
+
# @param index [Integer] Command index (for error messages)
|
|
463
|
+
# @raise [ArgumentError] If command is invalid
|
|
464
|
+
def validate_command!(cmd, index)
|
|
465
|
+
case cmd[:type]
|
|
466
|
+
when :move_to, :line_to
|
|
467
|
+
unless cmd.key?(:x) && cmd.key?(:y)
|
|
468
|
+
raise ArgumentError,
|
|
469
|
+
"command #{index} (#{cmd[:type]}) missing :x or :y"
|
|
470
|
+
end
|
|
471
|
+
when :quad_to
|
|
472
|
+
unless cmd.key?(:cx) && cmd.key?(:cy) && cmd.key?(:x) && cmd.key?(:y)
|
|
473
|
+
raise ArgumentError,
|
|
474
|
+
"command #{index} (quad_to) missing required keys"
|
|
475
|
+
end
|
|
476
|
+
when :curve_to
|
|
477
|
+
required = %i[cx1 cy1 cx2 cy2 x y]
|
|
478
|
+
missing = required - cmd.keys
|
|
479
|
+
unless missing.empty?
|
|
480
|
+
raise ArgumentError,
|
|
481
|
+
"command #{index} (curve_to) missing keys: #{missing.join(', ')}"
|
|
482
|
+
end
|
|
483
|
+
when :close_path
|
|
484
|
+
# No additional validation needed
|
|
485
|
+
else
|
|
486
|
+
raise ArgumentError,
|
|
487
|
+
"command #{index} has invalid type: #{cmd[:type]}"
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Convert TrueType contour points to commands
|
|
492
|
+
#
|
|
493
|
+
# @param points [Array<Hash>] Array of points with :x, :y, :on_curve
|
|
494
|
+
# @return [Array<Hash>] Array of commands
|
|
495
|
+
def self.convert_truetype_contour_to_commands(points)
|
|
496
|
+
return [] if points.empty?
|
|
497
|
+
|
|
498
|
+
commands = []
|
|
499
|
+
i = 0
|
|
500
|
+
|
|
501
|
+
# Move to first point
|
|
502
|
+
first = points[i]
|
|
503
|
+
commands << { type: :move_to, x: first[:x], y: first[:y] }
|
|
504
|
+
i += 1
|
|
505
|
+
|
|
506
|
+
# Process remaining points
|
|
507
|
+
while i < points.length
|
|
508
|
+
point = points[i]
|
|
509
|
+
|
|
510
|
+
if point[:on_curve]
|
|
511
|
+
# Line to on-curve point
|
|
512
|
+
commands << { type: :line_to, x: point[:x], y: point[:y] }
|
|
513
|
+
i += 1
|
|
514
|
+
else
|
|
515
|
+
# Off-curve point - quadratic curve control point
|
|
516
|
+
control = point
|
|
517
|
+
i += 1
|
|
518
|
+
|
|
519
|
+
if i < points.length && !points[i][:on_curve]
|
|
520
|
+
# Two consecutive off-curve points - implied on-curve at midpoint
|
|
521
|
+
next_control = points[i]
|
|
522
|
+
implied_x = (control[:x] + next_control[:x]) / 2.0
|
|
523
|
+
implied_y = (control[:y] + next_control[:y]) / 2.0
|
|
524
|
+
|
|
525
|
+
commands << {
|
|
526
|
+
type: :quad_to,
|
|
527
|
+
cx: control[:x],
|
|
528
|
+
cy: control[:y],
|
|
529
|
+
x: implied_x,
|
|
530
|
+
y: implied_y,
|
|
531
|
+
}
|
|
532
|
+
elsif i < points.length
|
|
533
|
+
# Next point is on-curve - end of quadratic curve
|
|
534
|
+
end_point = points[i]
|
|
535
|
+
commands << {
|
|
536
|
+
type: :quad_to,
|
|
537
|
+
cx: control[:x],
|
|
538
|
+
cy: control[:y],
|
|
539
|
+
x: end_point[:x],
|
|
540
|
+
y: end_point[:y],
|
|
541
|
+
}
|
|
542
|
+
i += 1
|
|
543
|
+
else
|
|
544
|
+
# Curves back to first point
|
|
545
|
+
commands << {
|
|
546
|
+
type: :quad_to,
|
|
547
|
+
cx: control[:x],
|
|
548
|
+
cy: control[:y],
|
|
549
|
+
x: first[:x],
|
|
550
|
+
y: first[:y],
|
|
551
|
+
}
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Close path
|
|
557
|
+
commands << { type: :close_path }
|
|
558
|
+
|
|
559
|
+
commands
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Convert CFF path to universal commands
|
|
563
|
+
#
|
|
564
|
+
# CFF doesn't have explicit closepath operators - contours are implicitly
|
|
565
|
+
# closed when a new moveto starts or at endchar. We add explicit
|
|
566
|
+
# close_path commands only when the contour is geometrically closed
|
|
567
|
+
# (last point equals first point), to preserve open contours from TTF.
|
|
568
|
+
#
|
|
569
|
+
# @param path [Array<Hash>] CFF path data
|
|
570
|
+
# @return [Array<Hash>] Universal commands
|
|
571
|
+
def self.convert_cff_path_to_commands(path)
|
|
572
|
+
commands = []
|
|
573
|
+
contour_start = nil # Track the start point of current contour
|
|
574
|
+
|
|
575
|
+
path.each_with_index do |cmd, _index|
|
|
576
|
+
case cmd[:type]
|
|
577
|
+
when :move_to
|
|
578
|
+
# Before starting new contour, close previous one if it was geometrically closed
|
|
579
|
+
if contour_start && !commands.empty? && commands.last[:type] != :close_path
|
|
580
|
+
# Check if last point equals start point (contour is closed)
|
|
581
|
+
last_cmd = commands.last
|
|
582
|
+
last_point = case last_cmd[:type]
|
|
583
|
+
when :line_to
|
|
584
|
+
{ x: last_cmd[:x], y: last_cmd[:y] }
|
|
585
|
+
when :curve_to
|
|
586
|
+
{ x: last_cmd[:x], y: last_cmd[:y] }
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
if last_point &&
|
|
590
|
+
(last_point[:x] - contour_start[:x]).abs <= 1 &&
|
|
591
|
+
(last_point[:y] - contour_start[:y]).abs <= 1
|
|
592
|
+
# Contour is geometrically closed
|
|
593
|
+
commands << { type: :close_path }
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# Start new contour
|
|
598
|
+
contour_start = { x: cmd[:x].round, y: cmd[:y].round }
|
|
599
|
+
commands << {
|
|
600
|
+
type: :move_to,
|
|
601
|
+
x: cmd[:x].round,
|
|
602
|
+
y: cmd[:y].round,
|
|
603
|
+
}
|
|
604
|
+
when :line_to
|
|
605
|
+
commands << {
|
|
606
|
+
type: :line_to,
|
|
607
|
+
x: cmd[:x].round,
|
|
608
|
+
y: cmd[:y].round,
|
|
609
|
+
}
|
|
610
|
+
when :curve_to
|
|
611
|
+
# CFF cubic curve
|
|
612
|
+
commands << {
|
|
613
|
+
type: :curve_to,
|
|
614
|
+
cx1: cmd[:x1].round,
|
|
615
|
+
cy1: cmd[:y1].round,
|
|
616
|
+
cx2: cmd[:x2].round,
|
|
617
|
+
cy2: cmd[:y2].round,
|
|
618
|
+
x: cmd[:x].round,
|
|
619
|
+
y: cmd[:y].round,
|
|
620
|
+
}
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Close the final contour if it was geometrically closed
|
|
625
|
+
if contour_start && !commands.empty? && commands.last[:type] != :close_path
|
|
626
|
+
last_cmd = commands.last
|
|
627
|
+
last_point = case last_cmd[:type]
|
|
628
|
+
when :line_to
|
|
629
|
+
{ x: last_cmd[:x], y: last_cmd[:y] }
|
|
630
|
+
when :curve_to
|
|
631
|
+
{ x: last_cmd[:x], y: last_cmd[:y] }
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
if last_point &&
|
|
635
|
+
(last_point[:x] - contour_start[:x]).abs <= 1 &&
|
|
636
|
+
(last_point[:y] - contour_start[:y]).abs <= 1
|
|
637
|
+
# Contour is geometrically closed
|
|
638
|
+
commands << { type: :close_path }
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
commands
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# Find previous point from commands
|
|
646
|
+
#
|
|
647
|
+
# @param commands [Array<Hash>] CFF commands
|
|
648
|
+
# @return [Hash] Previous point {:x, :y}
|
|
649
|
+
def find_previous_point(commands)
|
|
650
|
+
commands.reverse_each do |cmd|
|
|
651
|
+
case cmd[:type]
|
|
652
|
+
when :move_to, :line_to
|
|
653
|
+
return { x: cmd[:x], y: cmd[:y] }
|
|
654
|
+
when :curve_to
|
|
655
|
+
return { x: cmd[:x], y: cmd[:y] }
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# Default to origin if no previous point found
|
|
660
|
+
{ x: 0, y: 0 }
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Models
|
|
7
|
+
# Model for table sharing statistics
|
|
8
|
+
#
|
|
9
|
+
# Represents table deduplication information in a TTC/OTC collection.
|
|
10
|
+
# Shows which tables are shared between fonts.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating table sharing info
|
|
13
|
+
# sharing = TableSharingInfo.new(
|
|
14
|
+
# shared_tables: 12,
|
|
15
|
+
# unique_tables: 48,
|
|
16
|
+
# sharing_percentage: 20.0,
|
|
17
|
+
# space_saved_bytes: 156300
|
|
18
|
+
# )
|
|
19
|
+
class TableSharingInfo < Lutaml::Model::Serializable
|
|
20
|
+
attribute :shared_tables, :integer
|
|
21
|
+
attribute :unique_tables, :integer
|
|
22
|
+
attribute :sharing_percentage, :float
|
|
23
|
+
attribute :space_saved_bytes, :integer
|
|
24
|
+
|
|
25
|
+
yaml do
|
|
26
|
+
map "shared_tables", to: :shared_tables
|
|
27
|
+
map "unique_tables", to: :unique_tables
|
|
28
|
+
map "sharing_percentage", to: :sharing_percentage
|
|
29
|
+
map "space_saved_bytes", to: :space_saved_bytes
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
json do
|
|
33
|
+
map "shared_tables", to: :shared_tables
|
|
34
|
+
map "unique_tables", to: :unique_tables
|
|
35
|
+
map "sharing_percentage", to: :sharing_percentage
|
|
36
|
+
map "space_saved_bytes", to: :space_saved_bytes
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|