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,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
# Glyph ID mapping management
|
|
6
|
+
#
|
|
7
|
+
# This class manages the mapping between original glyph IDs (GIDs) in the
|
|
8
|
+
# source font and new GIDs in the subset font. It supports two modes:
|
|
9
|
+
#
|
|
10
|
+
# 1. Compact mode (retain_gids: false): Glyphs are renumbered sequentially,
|
|
11
|
+
# eliminating gaps from removed glyphs. This produces smaller fonts.
|
|
12
|
+
#
|
|
13
|
+
# 2. Retain mode (retain_gids: true): Original glyph IDs are preserved,
|
|
14
|
+
# with removed glyphs leaving empty slots. This maintains glyph
|
|
15
|
+
# references but produces larger fonts.
|
|
16
|
+
#
|
|
17
|
+
# @example Compact mode (default)
|
|
18
|
+
# mapping = Fontisan::Subset::GlyphMapping.new([0, 5, 10, 15])
|
|
19
|
+
# mapping.new_id(5) # => 1
|
|
20
|
+
# mapping.new_id(10) # => 2
|
|
21
|
+
# mapping.size # => 4
|
|
22
|
+
#
|
|
23
|
+
# @example Retain mode
|
|
24
|
+
# mapping = Fontisan::Subset::GlyphMapping.new([0, 5, 10, 15], retain_gids: true)
|
|
25
|
+
# mapping.new_id(5) # => 5
|
|
26
|
+
# mapping.new_id(10) # => 10
|
|
27
|
+
# mapping.size # => 16 (0..15)
|
|
28
|
+
#
|
|
29
|
+
# @example Reverse lookup
|
|
30
|
+
# mapping = Fontisan::Subset::GlyphMapping.new([0, 5, 10])
|
|
31
|
+
# mapping.old_id(1) # => 5
|
|
32
|
+
class GlyphMapping
|
|
33
|
+
# @return [Hash<Integer, Integer>] mapping from old GIDs to new GIDs
|
|
34
|
+
attr_reader :old_to_new
|
|
35
|
+
|
|
36
|
+
# @return [Hash<Integer, Integer>] mapping from new GIDs to old GIDs
|
|
37
|
+
attr_reader :new_to_old
|
|
38
|
+
|
|
39
|
+
# @return [Boolean] whether original GIDs are retained
|
|
40
|
+
attr_reader :retain_gids
|
|
41
|
+
|
|
42
|
+
# Initialize glyph mapping
|
|
43
|
+
#
|
|
44
|
+
# @param old_glyph_ids [Array<Integer>] array of glyph IDs to include
|
|
45
|
+
# in the subset, typically sorted
|
|
46
|
+
# @param retain_gids [Boolean] whether to preserve original glyph IDs
|
|
47
|
+
#
|
|
48
|
+
# @example Create compact mapping
|
|
49
|
+
# mapping = GlyphMapping.new([0, 3, 5, 10])
|
|
50
|
+
#
|
|
51
|
+
# @example Create mapping that retains GIDs
|
|
52
|
+
# mapping = GlyphMapping.new([0, 3, 5, 10], retain_gids: true)
|
|
53
|
+
def initialize(old_glyph_ids, retain_gids: false)
|
|
54
|
+
@old_to_new = {}
|
|
55
|
+
@new_to_old = {}
|
|
56
|
+
@retain_gids = retain_gids
|
|
57
|
+
|
|
58
|
+
build_mappings(old_glyph_ids)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get new glyph ID for an old glyph ID
|
|
62
|
+
#
|
|
63
|
+
# @param old_id [Integer] original glyph ID
|
|
64
|
+
# @return [Integer, nil] new glyph ID, or nil if not in subset
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# mapping = GlyphMapping.new([0, 5, 10])
|
|
68
|
+
# mapping.new_id(5) # => 1
|
|
69
|
+
# mapping.new_id(99) # => nil
|
|
70
|
+
def new_id(old_id)
|
|
71
|
+
old_to_new[old_id]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get old glyph ID for a new glyph ID
|
|
75
|
+
#
|
|
76
|
+
# @param new_id [Integer] new glyph ID in subset
|
|
77
|
+
# @return [Integer, nil] original glyph ID, or nil if invalid
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# mapping = GlyphMapping.new([0, 5, 10])
|
|
81
|
+
# mapping.old_id(1) # => 5
|
|
82
|
+
# mapping.old_id(99) # => nil
|
|
83
|
+
def old_id(new_id)
|
|
84
|
+
new_to_old[new_id]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get number of glyphs in the subset
|
|
88
|
+
#
|
|
89
|
+
# In compact mode, this is the number of included glyphs.
|
|
90
|
+
# In retain mode, this is the highest old GID + 1.
|
|
91
|
+
#
|
|
92
|
+
# @return [Integer] number of glyphs
|
|
93
|
+
#
|
|
94
|
+
# @example Compact mode
|
|
95
|
+
# mapping = GlyphMapping.new([0, 5, 10])
|
|
96
|
+
# mapping.size # => 3
|
|
97
|
+
#
|
|
98
|
+
# @example Retain mode
|
|
99
|
+
# mapping = GlyphMapping.new([0, 5, 10], retain_gids: true)
|
|
100
|
+
# mapping.size # => 11 (0..10)
|
|
101
|
+
def size
|
|
102
|
+
new_to_old.size
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if a glyph is included in the subset
|
|
106
|
+
#
|
|
107
|
+
# @param old_id [Integer] original glyph ID to check
|
|
108
|
+
# @return [Boolean] true if glyph is in subset
|
|
109
|
+
#
|
|
110
|
+
# @example
|
|
111
|
+
# mapping = GlyphMapping.new([0, 5, 10])
|
|
112
|
+
# mapping.include?(5) # => true
|
|
113
|
+
# mapping.include?(99) # => false
|
|
114
|
+
def include?(old_id)
|
|
115
|
+
old_to_new.key?(old_id)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get array of all old glyph IDs in subset
|
|
119
|
+
#
|
|
120
|
+
# @return [Array<Integer>] sorted array of old glyph IDs
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# mapping = GlyphMapping.new([10, 0, 5])
|
|
124
|
+
# mapping.old_ids # => [0, 5, 10]
|
|
125
|
+
def old_ids
|
|
126
|
+
old_to_new.keys.sort
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Get array of all new glyph IDs in subset
|
|
130
|
+
#
|
|
131
|
+
# @return [Array<Integer>] sorted array of new glyph IDs
|
|
132
|
+
#
|
|
133
|
+
# @example
|
|
134
|
+
# mapping = GlyphMapping.new([0, 5, 10])
|
|
135
|
+
# mapping.new_ids # => [0, 1, 2]
|
|
136
|
+
def new_ids
|
|
137
|
+
new_to_old.keys.sort
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Iterate over all glyph mappings
|
|
141
|
+
#
|
|
142
|
+
# Yields old_id and new_id pairs in order of old glyph IDs.
|
|
143
|
+
#
|
|
144
|
+
# @yield [old_id, new_id] each glyph mapping
|
|
145
|
+
# @yieldparam old_id [Integer] original glyph ID
|
|
146
|
+
# @yieldparam new_id [Integer] new glyph ID
|
|
147
|
+
#
|
|
148
|
+
# @example
|
|
149
|
+
# mapping = GlyphMapping.new([0, 5, 10])
|
|
150
|
+
# mapping.each do |old_id, new_id|
|
|
151
|
+
# puts "#{old_id} => #{new_id}"
|
|
152
|
+
# end
|
|
153
|
+
# # Output:
|
|
154
|
+
# # 0 => 0
|
|
155
|
+
# # 5 => 1
|
|
156
|
+
# # 10 => 2
|
|
157
|
+
def each
|
|
158
|
+
return enum_for(:each) unless block_given?
|
|
159
|
+
|
|
160
|
+
old_ids.each do |old_id|
|
|
161
|
+
yield old_id, old_to_new[old_id]
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
# Build the bidirectional mapping tables
|
|
168
|
+
#
|
|
169
|
+
# @param old_glyph_ids [Array<Integer>] glyph IDs to map
|
|
170
|
+
def build_mappings(old_glyph_ids)
|
|
171
|
+
if retain_gids
|
|
172
|
+
build_retained_mappings(old_glyph_ids)
|
|
173
|
+
else
|
|
174
|
+
build_compact_mappings(old_glyph_ids)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Build mappings in compact mode
|
|
179
|
+
#
|
|
180
|
+
# Assigns sequential new GIDs starting from 0, preserving the order
|
|
181
|
+
# of old GIDs.
|
|
182
|
+
#
|
|
183
|
+
# @param old_glyph_ids [Array<Integer>] glyph IDs to map
|
|
184
|
+
def build_compact_mappings(old_glyph_ids)
|
|
185
|
+
sorted_ids = old_glyph_ids.sort.uniq
|
|
186
|
+
sorted_ids.each_with_index do |old_id, new_id|
|
|
187
|
+
old_to_new[old_id] = new_id
|
|
188
|
+
new_to_old[new_id] = old_id
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Build mappings in retain GID mode
|
|
193
|
+
#
|
|
194
|
+
# Preserves original GIDs, creating empty slots for removed glyphs.
|
|
195
|
+
#
|
|
196
|
+
# @param old_glyph_ids [Array<Integer>] glyph IDs to map
|
|
197
|
+
def build_retained_mappings(old_glyph_ids)
|
|
198
|
+
sorted_ids = old_glyph_ids.sort.uniq
|
|
199
|
+
max_id = sorted_ids.max || 0
|
|
200
|
+
|
|
201
|
+
# Map each glyph to itself
|
|
202
|
+
sorted_ids.each do |old_id|
|
|
203
|
+
old_to_new[old_id] = old_id
|
|
204
|
+
new_to_old[old_id] = old_id
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Fill in empty slots for removed glyphs with nil mappings
|
|
208
|
+
# This ensures size calculation includes the empty slots
|
|
209
|
+
(0..max_id).each do |gid|
|
|
210
|
+
new_to_old[gid] ||= nil
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Subset
|
|
7
|
+
# Subsetting configuration class
|
|
8
|
+
#
|
|
9
|
+
# This class defines all available options for font subsetting operations.
|
|
10
|
+
# It provides sensible defaults for various subsetting scenarios and uses
|
|
11
|
+
# Lutaml::Model for serialization support.
|
|
12
|
+
#
|
|
13
|
+
# @example Create default PDF subsetting options
|
|
14
|
+
# options = Fontisan::Subset::Options.new
|
|
15
|
+
# options.profile # => "pdf"
|
|
16
|
+
# options.drop_hints # => false
|
|
17
|
+
#
|
|
18
|
+
# @example Create custom web subsetting options
|
|
19
|
+
# options = Fontisan::Subset::Options.new(
|
|
20
|
+
# profile: "web",
|
|
21
|
+
# drop_hints: true,
|
|
22
|
+
# unicode_ranges: false
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# @example Retain original glyph IDs
|
|
26
|
+
# options = Fontisan::Subset::Options.new(retain_gids: true)
|
|
27
|
+
class Options < Lutaml::Model::Serializable
|
|
28
|
+
# Subsetting profile name (pdf, web, minimal, or custom)
|
|
29
|
+
#
|
|
30
|
+
# @return [String] the profile name
|
|
31
|
+
attribute :profile, :string, default: -> { "pdf" }
|
|
32
|
+
|
|
33
|
+
# Whether to drop hinting instructions
|
|
34
|
+
#
|
|
35
|
+
# Hinting improves text rendering at small sizes but increases file size.
|
|
36
|
+
# Web fonts typically don't need hints due to modern rendering engines.
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean] true to drop hints, false to retain them
|
|
39
|
+
attribute :drop_hints, :boolean, default: -> { false }
|
|
40
|
+
|
|
41
|
+
# Whether to drop glyph names from the post table
|
|
42
|
+
#
|
|
43
|
+
# Glyph names are useful for debugging but not required for rendering.
|
|
44
|
+
# Dropping them reduces file size.
|
|
45
|
+
#
|
|
46
|
+
# @return [Boolean] true to drop names, false to retain them
|
|
47
|
+
attribute :drop_names, :boolean, default: -> { false }
|
|
48
|
+
|
|
49
|
+
# Whether to prune OS/2 Unicode ranges
|
|
50
|
+
#
|
|
51
|
+
# Updates the OS/2 table's Unicode range bits to reflect only the
|
|
52
|
+
# glyphs present in the subset.
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean] true to prune ranges, false to keep original
|
|
55
|
+
attribute :unicode_ranges, :boolean, default: -> { true }
|
|
56
|
+
|
|
57
|
+
# Whether to retain original glyph IDs
|
|
58
|
+
#
|
|
59
|
+
# When true, removed glyphs leave empty slots in the glyf table,
|
|
60
|
+
# preserving original GID assignments. When false, glyphs are
|
|
61
|
+
# compacted to eliminate gaps.
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean] true to retain GIDs, false to compact
|
|
64
|
+
attribute :retain_gids, :boolean, default: -> { false }
|
|
65
|
+
|
|
66
|
+
# Whether to include the .notdef glyph
|
|
67
|
+
#
|
|
68
|
+
# The .notdef glyph is displayed for missing characters. It is
|
|
69
|
+
# typically required by font specifications.
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean] true to include .notdef, false to exclude
|
|
72
|
+
attribute :include_notdef, :boolean, default: -> { true }
|
|
73
|
+
|
|
74
|
+
# Whether to include the .null glyph
|
|
75
|
+
#
|
|
76
|
+
# The .null glyph (U+0000) is sometimes used for control purposes.
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean] true to include .null, false to exclude
|
|
79
|
+
attribute :include_null, :boolean, default: -> { false }
|
|
80
|
+
|
|
81
|
+
# OpenType features to retain in the subset
|
|
82
|
+
#
|
|
83
|
+
# An empty array means all features are retained. Specify feature
|
|
84
|
+
# tags (e.g., ['liga', 'kern']) to keep only those features.
|
|
85
|
+
#
|
|
86
|
+
# @return [Array<String>] array of feature tags to retain
|
|
87
|
+
attribute :features, :string, collection: true, default: -> { [] }
|
|
88
|
+
|
|
89
|
+
# Script tags to retain in the subset
|
|
90
|
+
#
|
|
91
|
+
# An array containing "*" means all scripts are retained. Specify
|
|
92
|
+
# script tags (e.g., ['latn', 'arab']) to keep only those scripts.
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<String>] array of script tags to retain
|
|
95
|
+
attribute :scripts, :string, collection: true, default: -> { ["*"] }
|
|
96
|
+
|
|
97
|
+
# Initialize options with custom values
|
|
98
|
+
#
|
|
99
|
+
# @param attributes [Hash] hash of attribute values
|
|
100
|
+
# @option attributes [String] :profile ("pdf") subsetting profile
|
|
101
|
+
# @option attributes [Boolean] :drop_hints (false) drop hinting
|
|
102
|
+
# @option attributes [Boolean] :drop_names (false) drop glyph names
|
|
103
|
+
# @option attributes [Boolean] :unicode_ranges (true) prune OS/2 ranges
|
|
104
|
+
# @option attributes [Boolean] :retain_gids (false) retain glyph IDs
|
|
105
|
+
# @option attributes [Boolean] :include_notdef (true) include .notdef
|
|
106
|
+
# @option attributes [Boolean] :include_null (false) include .null
|
|
107
|
+
# @option attributes [Array<String>] :features ([]) features to keep
|
|
108
|
+
# @option attributes [Array<String>] :scripts (["*"]) scripts to keep
|
|
109
|
+
def initialize(attributes = {})
|
|
110
|
+
super
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if all features should be retained
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean] true if features array is empty
|
|
116
|
+
def all_features?
|
|
117
|
+
features.empty?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if all scripts should be retained
|
|
121
|
+
#
|
|
122
|
+
# @return [Boolean] true if scripts contains "*"
|
|
123
|
+
def all_scripts?
|
|
124
|
+
scripts.include?("*")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Validate the options configuration
|
|
128
|
+
#
|
|
129
|
+
# @raise [ArgumentError] if profile is invalid
|
|
130
|
+
# @return [Boolean] true if valid
|
|
131
|
+
def validate!
|
|
132
|
+
valid_profiles = %w[pdf web minimal custom]
|
|
133
|
+
unless valid_profiles.include?(profile)
|
|
134
|
+
raise ArgumentError,
|
|
135
|
+
"Invalid profile '#{profile}'. Must be one of: #{valid_profiles.join(', ')}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
true
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Subset
|
|
7
|
+
# Subsetting profiles
|
|
8
|
+
#
|
|
9
|
+
# This class manages font subsetting profiles that specify which
|
|
10
|
+
# font tables should be included in the subset. Profiles are loaded from
|
|
11
|
+
# an external YAML configuration file for flexibility and maintainability.
|
|
12
|
+
#
|
|
13
|
+
# @example Get tables for PDF profile
|
|
14
|
+
# tables = Fontisan::Subset::Profile.for_name("pdf")
|
|
15
|
+
# # => ["cmap", "head", "hhea", "hmtx", "maxp", "name", "post", "loca", "glyf"]
|
|
16
|
+
#
|
|
17
|
+
# @example Get tables for web profile
|
|
18
|
+
# tables = Fontisan::Subset::Profile.for_name("web")
|
|
19
|
+
# # => ["cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post", "loca", "glyf"]
|
|
20
|
+
#
|
|
21
|
+
# @example Create custom profile
|
|
22
|
+
# tables = Fontisan::Subset::Profile.custom(["cmap", "head", "hhea"])
|
|
23
|
+
# # => ["cmap", "head", "hhea"]
|
|
24
|
+
class Profile
|
|
25
|
+
# All known font table tags
|
|
26
|
+
#
|
|
27
|
+
# Comprehensive list of all standard TrueType/OpenType tables
|
|
28
|
+
KNOWN_TABLES = %w[
|
|
29
|
+
cmap head hhea hmtx maxp name OS/2 post
|
|
30
|
+
loca glyf cvt fpgm prep gasp
|
|
31
|
+
GSUB GPOS GDEF BASE JSTF
|
|
32
|
+
CFF CFF2 VORG
|
|
33
|
+
EBDT EBLC EBSC
|
|
34
|
+
CBDT CBLC sbix
|
|
35
|
+
kern vhea vmtx
|
|
36
|
+
LTSH PCLT VDMX hdmx
|
|
37
|
+
fvar gvar avar cvar HVAR VVAR MVAR STAT
|
|
38
|
+
DSIG
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# Get table list for a named profile
|
|
43
|
+
#
|
|
44
|
+
# @param name [String] profile name (pdf, web, minimal, full)
|
|
45
|
+
# @raise [ArgumentError] if profile name is unknown
|
|
46
|
+
# @return [Array<String>] array of table tags
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# Profile.for_name("pdf")
|
|
50
|
+
# # => ["cmap", "head", "hhea", "hmtx", "maxp", "name", "post", "loca", "glyf"]
|
|
51
|
+
def for_name(name)
|
|
52
|
+
profiles = load_profiles
|
|
53
|
+
profile_config = profiles[name.to_s.downcase]
|
|
54
|
+
|
|
55
|
+
unless profile_config
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"Unknown profile '#{name}'. Valid profiles: #{valid_names.join(', ')}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
profile_config["tables"].dup
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Create a custom profile with specified tables
|
|
64
|
+
#
|
|
65
|
+
# Validates that all provided table tags are recognized and returns
|
|
66
|
+
# the list of tables in a consistent format.
|
|
67
|
+
#
|
|
68
|
+
# @param tables [Array<String>] array of table tags
|
|
69
|
+
# @raise [ArgumentError] if any table tag is unknown
|
|
70
|
+
# @return [Array<String>] validated array of table tags
|
|
71
|
+
#
|
|
72
|
+
# @example Create custom profile
|
|
73
|
+
# Profile.custom(["cmap", "head", "glyf"])
|
|
74
|
+
# # => ["cmap", "head", "glyf"]
|
|
75
|
+
#
|
|
76
|
+
# @example Invalid table raises error
|
|
77
|
+
# Profile.custom(["cmap", "invalid"])
|
|
78
|
+
# # => ArgumentError: Unknown table tags: invalid
|
|
79
|
+
def custom(tables)
|
|
80
|
+
tables = Array(tables)
|
|
81
|
+
unknown = tables - KNOWN_TABLES
|
|
82
|
+
|
|
83
|
+
unless unknown.empty?
|
|
84
|
+
raise ArgumentError,
|
|
85
|
+
"Unknown table tags: #{unknown.join(', ')}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
tables.dup
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check if a table tag is recognized
|
|
92
|
+
#
|
|
93
|
+
# @param table [String] table tag to check
|
|
94
|
+
# @return [Boolean] true if table is known
|
|
95
|
+
#
|
|
96
|
+
# @example
|
|
97
|
+
# Profile.known_table?("cmap") # => true
|
|
98
|
+
# Profile.known_table?("invalid") # => false
|
|
99
|
+
def known_table?(table)
|
|
100
|
+
KNOWN_TABLES.include?(table.to_s)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get list of all valid profile names
|
|
104
|
+
#
|
|
105
|
+
# @return [Array<String>] array of profile names
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# Profile.valid_names # => ["pdf", "web", "minimal", "full"]
|
|
109
|
+
def valid_names
|
|
110
|
+
load_profiles.keys.sort
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get profile description
|
|
114
|
+
#
|
|
115
|
+
# @param name [String] profile name
|
|
116
|
+
# @return [String, nil] profile description or nil if not found
|
|
117
|
+
#
|
|
118
|
+
# @example
|
|
119
|
+
# Profile.description("pdf")
|
|
120
|
+
# # => "Minimal tables required for PDF font embedding"
|
|
121
|
+
def description(name)
|
|
122
|
+
profiles = load_profiles
|
|
123
|
+
profile_config = profiles[name.to_s.downcase]
|
|
124
|
+
profile_config&.dig("description")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
# Load profiles from YAML configuration file
|
|
130
|
+
#
|
|
131
|
+
# @return [Hash] Hash of profile configurations
|
|
132
|
+
def load_profiles
|
|
133
|
+
@load_profiles ||= begin
|
|
134
|
+
config_path = File.join(__dir__, "../config/subset_profiles.yml")
|
|
135
|
+
YAML.load_file(config_path)
|
|
136
|
+
rescue Errno::ENOENT
|
|
137
|
+
raise Fontisan::Error,
|
|
138
|
+
"Profile configuration file not found: #{config_path}"
|
|
139
|
+
rescue Psych::SyntaxError => e
|
|
140
|
+
raise Fontisan::Error,
|
|
141
|
+
"Invalid YAML in profile configuration: #{e.message}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Clear cached profiles (useful for testing)
|
|
146
|
+
def clear_cache!
|
|
147
|
+
@load_profiles = nil
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|