fontisan 0.2.11 → 0.2.13
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 +294 -52
- data/Gemfile +5 -0
- data/README.adoc +163 -2
- data/docs/CONVERSION_GUIDE.adoc +633 -0
- data/docs/TYPE1_FONTS.adoc +445 -0
- data/lib/fontisan/cli.rb +177 -6
- data/lib/fontisan/commands/convert_command.rb +32 -1
- data/lib/fontisan/commands/info_command.rb +83 -2
- data/lib/fontisan/config/conversion_matrix.yml +132 -4
- data/lib/fontisan/constants.rb +12 -0
- data/lib/fontisan/conversion_options.rb +378 -0
- data/lib/fontisan/converters/collection_converter.rb +45 -10
- data/lib/fontisan/converters/format_converter.rb +17 -5
- data/lib/fontisan/converters/outline_converter.rb +78 -4
- data/lib/fontisan/converters/type1_converter.rb +1234 -0
- data/lib/fontisan/font_loader.rb +46 -3
- data/lib/fontisan/hints/hint_converter.rb +4 -1
- data/lib/fontisan/type1/afm_generator.rb +436 -0
- data/lib/fontisan/type1/afm_parser.rb +298 -0
- data/lib/fontisan/type1/agl.rb +456 -0
- data/lib/fontisan/type1/cff_to_type1_converter.rb +302 -0
- data/lib/fontisan/type1/charstring_converter.rb +240 -0
- data/lib/fontisan/type1/charstrings.rb +408 -0
- data/lib/fontisan/type1/conversion_options.rb +243 -0
- data/lib/fontisan/type1/decryptor.rb +183 -0
- data/lib/fontisan/type1/encodings.rb +697 -0
- data/lib/fontisan/type1/font_dictionary.rb +576 -0
- data/lib/fontisan/type1/generator.rb +220 -0
- data/lib/fontisan/type1/inf_generator.rb +332 -0
- data/lib/fontisan/type1/pfa_generator.rb +369 -0
- data/lib/fontisan/type1/pfa_parser.rb +159 -0
- data/lib/fontisan/type1/pfb_generator.rb +314 -0
- data/lib/fontisan/type1/pfb_parser.rb +166 -0
- data/lib/fontisan/type1/pfm_generator.rb +610 -0
- data/lib/fontisan/type1/pfm_parser.rb +433 -0
- data/lib/fontisan/type1/private_dict.rb +342 -0
- data/lib/fontisan/type1/seac_expander.rb +501 -0
- data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
- data/lib/fontisan/type1/upm_scaler.rb +118 -0
- data/lib/fontisan/type1.rb +75 -0
- data/lib/fontisan/type1_font.rb +318 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +2 -0
- metadata +30 -3
- data/docs/DOCUMENTATION_SUMMARY.md +0 -141
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
# Conversion options for font format transformations
|
|
5
|
+
#
|
|
6
|
+
# Defines all options for opening (reading) and generating (writing) fonts
|
|
7
|
+
# during conversion operations. Includes validation, defaults, and presets.
|
|
8
|
+
#
|
|
9
|
+
# @example Using options directly
|
|
10
|
+
# options = ConversionOptions.new(
|
|
11
|
+
# from: :ttf,
|
|
12
|
+
# to: :otf,
|
|
13
|
+
# opening: { convert_curves: true, scale_to_1000: true },
|
|
14
|
+
# generating: { hinting_mode: "auto" }
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# @example Using recommended options
|
|
18
|
+
# options = ConversionOptions.recommended(from: :ttf, to: :otf)
|
|
19
|
+
#
|
|
20
|
+
# @example Using a preset
|
|
21
|
+
# options = ConversionOptions.from_preset(:type1_to_modern)
|
|
22
|
+
class ConversionOptions
|
|
23
|
+
# Opening options (input processing)
|
|
24
|
+
OPENING_OPTIONS = %i[
|
|
25
|
+
decompose_composites
|
|
26
|
+
convert_curves
|
|
27
|
+
scale_to_1000
|
|
28
|
+
scale_from_1000
|
|
29
|
+
autohint
|
|
30
|
+
generate_unicode
|
|
31
|
+
store_custom_tables
|
|
32
|
+
store_native_hinting
|
|
33
|
+
interpret_ot
|
|
34
|
+
read_all_records
|
|
35
|
+
preserve_encoding
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
# Generating options (output processing)
|
|
39
|
+
GENERATING_OPTIONS = %i[
|
|
40
|
+
write_pfm
|
|
41
|
+
write_afm
|
|
42
|
+
write_inf
|
|
43
|
+
select_encoding_automatically
|
|
44
|
+
hinting_mode
|
|
45
|
+
decompose_on_output
|
|
46
|
+
write_custom_tables
|
|
47
|
+
optimize_tables
|
|
48
|
+
reencode_first_256
|
|
49
|
+
encoding_vector
|
|
50
|
+
compression
|
|
51
|
+
transform_tables
|
|
52
|
+
preserve_metadata
|
|
53
|
+
strip_metadata
|
|
54
|
+
target_format
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
# Valid hinting modes
|
|
58
|
+
HINTING_MODES = %w[preserve auto none full].freeze
|
|
59
|
+
|
|
60
|
+
# Valid compression modes
|
|
61
|
+
COMPRESSION_MODES = %w[zlib brotli none].freeze
|
|
62
|
+
|
|
63
|
+
attr_reader :from, :to, :opening, :generating
|
|
64
|
+
|
|
65
|
+
# Initialize conversion options
|
|
66
|
+
#
|
|
67
|
+
# @param from [String, Symbol] Source format
|
|
68
|
+
# @param to [String, Symbol] Target format
|
|
69
|
+
# @param opening [Hash] Opening options
|
|
70
|
+
# @param generating [Hash] Generating options
|
|
71
|
+
def initialize(from:, to:, opening: {}, generating: {})
|
|
72
|
+
@from = self.class.normalize_format(from)
|
|
73
|
+
@to = self.class.normalize_format(to)
|
|
74
|
+
@opening = apply_opening_defaults(opening)
|
|
75
|
+
@generating = apply_generating_defaults(generating)
|
|
76
|
+
validate!
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get recommended options for a conversion pair
|
|
80
|
+
#
|
|
81
|
+
# @param from [String, Symbol] Source format
|
|
82
|
+
# @param to [String, Symbol] Target format
|
|
83
|
+
# @return [ConversionOptions] Pre-configured options
|
|
84
|
+
def self.recommended(from:, to:)
|
|
85
|
+
from_sym = normalize_format(from)
|
|
86
|
+
to_sym = normalize_format(to)
|
|
87
|
+
|
|
88
|
+
options = RECOMMENDED_OPTIONS.dig(from_sym, to_sym) || {}
|
|
89
|
+
new(
|
|
90
|
+
from: from_sym,
|
|
91
|
+
to: to_sym,
|
|
92
|
+
opening: options[:opening] || {},
|
|
93
|
+
generating: options[:generating] || {},
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Load options from a named preset
|
|
98
|
+
#
|
|
99
|
+
# @param preset_name [Symbol, String] Preset name
|
|
100
|
+
# @return [ConversionOptions] Pre-configured options
|
|
101
|
+
def self.from_preset(preset_name)
|
|
102
|
+
preset_key = preset_name.to_sym
|
|
103
|
+
preset = PRESETS.fetch(preset_key) do
|
|
104
|
+
raise ArgumentError, "Unknown preset: #{preset_name}. " \
|
|
105
|
+
"Available: #{PRESETS.keys.join(', ')}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
new(
|
|
109
|
+
from: preset[:from],
|
|
110
|
+
to: preset[:to],
|
|
111
|
+
opening: preset[:opening] || {},
|
|
112
|
+
generating: preset[:generating] || {},
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check if an opening option is set
|
|
117
|
+
#
|
|
118
|
+
# @param key [Symbol] Option key
|
|
119
|
+
# @return [Boolean] true if option is truthy
|
|
120
|
+
def opening_option?(key)
|
|
121
|
+
!!@opening[key]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if a generating option has a specific value
|
|
125
|
+
#
|
|
126
|
+
# @param key [Symbol] Option key
|
|
127
|
+
# @param value [Object] Value to check
|
|
128
|
+
# @return [Boolean] true if option equals value
|
|
129
|
+
def generating_option?(key, value = true)
|
|
130
|
+
@generating[key] == value
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get list of available presets
|
|
134
|
+
#
|
|
135
|
+
# @return [Array<Symbol>] Available preset names
|
|
136
|
+
def self.available_presets
|
|
137
|
+
PRESETS.keys
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
# Normalize format symbol
|
|
143
|
+
#
|
|
144
|
+
# @param format [String, Symbol] Format identifier
|
|
145
|
+
# @return [Symbol] Normalized format symbol
|
|
146
|
+
def self.normalize_format(format)
|
|
147
|
+
case format.to_s.downcase
|
|
148
|
+
when "ttf", "truetype", "truetype-tt", "ot-tt" then :ttf
|
|
149
|
+
when "otf", "cff", "opentype", "ot-ps", "opentype-ps" then :otf
|
|
150
|
+
when "type1", "type-1", "t1", "pfb", "pfa" then :type1
|
|
151
|
+
when "ttc" then :ttc
|
|
152
|
+
when "otc" then :otc
|
|
153
|
+
when "dfont", "suitcase" then :dfont
|
|
154
|
+
when "woff" then :woff
|
|
155
|
+
when "woff2" then :woff2
|
|
156
|
+
when "svg" then :svg
|
|
157
|
+
else
|
|
158
|
+
raise ArgumentError, "Unknown format: #{format}. " \
|
|
159
|
+
"Supported: ttf, otf, type1, ttc, otc, dfont, woff, woff2, svg"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Apply default values for opening options
|
|
164
|
+
#
|
|
165
|
+
# @param options [Hash] User-provided options
|
|
166
|
+
# @return [Hash] Options with defaults applied
|
|
167
|
+
def apply_opening_defaults(options)
|
|
168
|
+
defaults = OPENING_DEFAULTS.dig(@from, @to) || {}
|
|
169
|
+
defaults.merge(options)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Apply default values for generating options
|
|
173
|
+
#
|
|
174
|
+
# @param options [Hash] User-provided options
|
|
175
|
+
# @return [Hash] Options with defaults applied
|
|
176
|
+
def apply_generating_defaults(options)
|
|
177
|
+
defaults = GENERATING_DEFAULTS.dig(@from, @to) || {}
|
|
178
|
+
defaults.merge(options)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Validate options
|
|
182
|
+
#
|
|
183
|
+
# @raise [ArgumentError] If options are invalid
|
|
184
|
+
def validate!
|
|
185
|
+
validate_opening_options!
|
|
186
|
+
validate_generating_options!
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Validate opening options
|
|
190
|
+
def validate_opening_options!
|
|
191
|
+
@opening.each_key do |key|
|
|
192
|
+
unless OPENING_OPTIONS.include?(key)
|
|
193
|
+
raise ArgumentError, "Unknown opening option: #{key}. " \
|
|
194
|
+
"Available: #{OPENING_OPTIONS.join(', ')}"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Validate generating options
|
|
200
|
+
def validate_generating_options!
|
|
201
|
+
@generating.each_key do |key|
|
|
202
|
+
unless GENERATING_OPTIONS.include?(key)
|
|
203
|
+
raise ArgumentError, "Unknown generating option: #{key}. " \
|
|
204
|
+
"Available: #{GENERATING_OPTIONS.join(', ')}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Validate hinting_mode
|
|
209
|
+
if @generating[:hinting_mode]
|
|
210
|
+
mode = @generating[:hinting_mode].to_s
|
|
211
|
+
unless HINTING_MODES.include?(mode)
|
|
212
|
+
raise ArgumentError, "Invalid hinting_mode: #{mode}. " \
|
|
213
|
+
"Available: #{HINTING_MODES.join(', ')}"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Validate compression mode
|
|
218
|
+
if @generating[:compression]
|
|
219
|
+
comp = @generating[:compression].to_s
|
|
220
|
+
unless COMPRESSION_MODES.include?(comp)
|
|
221
|
+
raise ArgumentError, "Invalid compression: #{comp}. " \
|
|
222
|
+
"Available: #{COMPRESSION_MODES.join(', ')}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Validate target_format for collection conversions
|
|
227
|
+
if @generating[:target_format]
|
|
228
|
+
target = @generating[:target_format].to_s
|
|
229
|
+
unless ["ttf", "otf", "preserve"].include?(target)
|
|
230
|
+
raise ArgumentError, "Invalid target_format: #{target}"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Default opening options per conversion pair
|
|
236
|
+
OPENING_DEFAULTS = {
|
|
237
|
+
ttf: {
|
|
238
|
+
ttf: { decompose_composites: false, convert_curves: false,
|
|
239
|
+
store_custom_tables: true },
|
|
240
|
+
otf: { convert_curves: true, scale_to_1000: true,
|
|
241
|
+
autohint: true, decompose_composites: false },
|
|
242
|
+
type1: { convert_curves: true, scale_to_1000: true,
|
|
243
|
+
autohint: true, decompose_composites: false },
|
|
244
|
+
},
|
|
245
|
+
otf: {
|
|
246
|
+
ttf: { convert_curves: true, decompose_composites: false,
|
|
247
|
+
interpret_ot: true },
|
|
248
|
+
otf: { decompose_composites: false, store_custom_tables: true },
|
|
249
|
+
type1: { decompose_composites: false },
|
|
250
|
+
},
|
|
251
|
+
type1: {
|
|
252
|
+
ttf: { decompose_composites: false, generate_unicode: true,
|
|
253
|
+
read_all_records: true },
|
|
254
|
+
otf: { decompose_composites: false, generate_unicode: true,
|
|
255
|
+
read_all_records: true },
|
|
256
|
+
},
|
|
257
|
+
}.freeze
|
|
258
|
+
|
|
259
|
+
# Default generating options per conversion pair
|
|
260
|
+
GENERATING_DEFAULTS = {
|
|
261
|
+
ttf: {
|
|
262
|
+
ttf: { hinting_mode: "preserve", write_custom_tables: true,
|
|
263
|
+
optimize_tables: true },
|
|
264
|
+
otf: { hinting_mode: "auto", decompose_on_output: false,
|
|
265
|
+
write_custom_tables: true },
|
|
266
|
+
type1: { write_pfm: true, write_afm: true, write_inf: true,
|
|
267
|
+
select_encoding_automatically: true, hinting_mode: "auto" },
|
|
268
|
+
},
|
|
269
|
+
otf: {
|
|
270
|
+
ttf: { hinting_mode: "auto", decompose_on_output: false,
|
|
271
|
+
write_custom_tables: true },
|
|
272
|
+
otf: { hinting_mode: "preserve", decompose_on_output: false,
|
|
273
|
+
write_custom_tables: true, optimize_tables: true },
|
|
274
|
+
type1: { write_pfm: true, write_afm: true, write_inf: true,
|
|
275
|
+
select_encoding_automatically: true, hinting_mode: "preserve",
|
|
276
|
+
decompose_on_output: false },
|
|
277
|
+
},
|
|
278
|
+
type1: {
|
|
279
|
+
ttf: { hinting_mode: "auto", decompose_on_output: true },
|
|
280
|
+
otf: { hinting_mode: "preserve", decompose_on_output: true },
|
|
281
|
+
type1: { write_pfm: true, write_afm: true, write_inf: true,
|
|
282
|
+
select_encoding_automatically: true },
|
|
283
|
+
},
|
|
284
|
+
}.freeze
|
|
285
|
+
|
|
286
|
+
# Recommended options from TypeTool 3 manual
|
|
287
|
+
# Based on "Options for Converting Fonts" table (lines 6735-6803)
|
|
288
|
+
RECOMMENDED_OPTIONS = {
|
|
289
|
+
ttf: {
|
|
290
|
+
ttf: {
|
|
291
|
+
opening: { convert_curves: false, scale_to_1000: false,
|
|
292
|
+
decompose_composites: false, autohint: false,
|
|
293
|
+
store_custom_tables: true, store_native_hinting: true },
|
|
294
|
+
generating: { hinting_mode: "full", write_custom_tables: true },
|
|
295
|
+
},
|
|
296
|
+
otf: {
|
|
297
|
+
opening: { convert_curves: true, scale_to_1000: true,
|
|
298
|
+
autohint: true, decompose_composites: false,
|
|
299
|
+
store_custom_tables: true },
|
|
300
|
+
generating: { hinting_mode: "auto", decompose_on_output: true },
|
|
301
|
+
},
|
|
302
|
+
type1: {
|
|
303
|
+
opening: { convert_curves: true, scale_to_1000: true,
|
|
304
|
+
autohint: true, decompose_composites: false,
|
|
305
|
+
store_custom_tables: false },
|
|
306
|
+
generating: { write_pfm: true, write_afm: true, write_inf: true,
|
|
307
|
+
select_encoding_automatically: true },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
otf: {
|
|
311
|
+
ttf: {
|
|
312
|
+
opening: { decompose_composites: false, read_all_records: true,
|
|
313
|
+
interpret_ot: true, store_custom_tables: true,
|
|
314
|
+
store_native_hinting: false },
|
|
315
|
+
generating: { hinting_mode: "full", reencode_first_256: false },
|
|
316
|
+
},
|
|
317
|
+
otf: {
|
|
318
|
+
opening: { decompose_composites: false, store_custom_tables: true },
|
|
319
|
+
generating: { hinting_mode: "none", decompose_on_output: false,
|
|
320
|
+
write_custom_tables: true },
|
|
321
|
+
},
|
|
322
|
+
type1: {
|
|
323
|
+
opening: { decompose_composites: false },
|
|
324
|
+
generating: { write_pfm: true, write_afm: true, write_inf: true,
|
|
325
|
+
select_encoding_automatically: true,
|
|
326
|
+
hinting_mode: "none" },
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
type1: {
|
|
330
|
+
ttf: {
|
|
331
|
+
opening: { decompose_composites: false, generate_unicode: true },
|
|
332
|
+
generating: { hinting_mode: "full" },
|
|
333
|
+
},
|
|
334
|
+
otf: {
|
|
335
|
+
opening: { decompose_composites: false, generate_unicode: true },
|
|
336
|
+
generating: { hinting_mode: "none", decompose_on_output: true },
|
|
337
|
+
},
|
|
338
|
+
type1: {
|
|
339
|
+
opening: { decompose_composites: false, generate_unicode: true },
|
|
340
|
+
generating: { write_pfm: true, write_afm: true, write_inf: true,
|
|
341
|
+
select_encoding_automatically: true },
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
}.freeze
|
|
345
|
+
|
|
346
|
+
# Named presets for common conversion scenarios
|
|
347
|
+
PRESETS = {
|
|
348
|
+
type1_to_modern: {
|
|
349
|
+
from: :type1,
|
|
350
|
+
to: :otf,
|
|
351
|
+
opening: { generate_unicode: true, decompose_composites: false },
|
|
352
|
+
generating: { hinting_mode: "preserve", decompose_on_output: true },
|
|
353
|
+
},
|
|
354
|
+
modern_to_type1: {
|
|
355
|
+
from: :otf,
|
|
356
|
+
to: :type1,
|
|
357
|
+
opening: { convert_curves: true, scale_to_1000: true,
|
|
358
|
+
autohint: true, decompose_composites: false,
|
|
359
|
+
store_custom_tables: false },
|
|
360
|
+
generating: { write_pfm: true, write_afm: true, write_inf: true,
|
|
361
|
+
select_encoding_automatically: true, hinting_mode: "preserve" },
|
|
362
|
+
},
|
|
363
|
+
web_optimized: {
|
|
364
|
+
from: :otf,
|
|
365
|
+
to: :woff2,
|
|
366
|
+
opening: {},
|
|
367
|
+
generating: { compression: "brotli", transform_tables: true,
|
|
368
|
+
optimize_tables: true, preserve_metadata: true },
|
|
369
|
+
},
|
|
370
|
+
archive_to_modern: {
|
|
371
|
+
from: :ttc,
|
|
372
|
+
to: :otf,
|
|
373
|
+
opening: { convert_curves: true, decompose_composites: false },
|
|
374
|
+
generating: { target_format: "otf", hinting_mode: "preserve" },
|
|
375
|
+
},
|
|
376
|
+
}.freeze
|
|
377
|
+
end
|
|
378
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../conversion_options"
|
|
3
4
|
require_relative "format_converter"
|
|
4
5
|
require_relative "../collection/builder"
|
|
5
6
|
require_relative "../collection/dfont_builder"
|
|
@@ -27,14 +28,15 @@ module Fontisan
|
|
|
27
28
|
# converter = CollectionConverter.new
|
|
28
29
|
# result = converter.convert(ttc_path, target_type: :otc, output: 'family.otc')
|
|
29
30
|
#
|
|
30
|
-
# @example Convert
|
|
31
|
+
# @example Convert with ConversionOptions
|
|
32
|
+
# options = ConversionOptions.recommended(from: :ttc, to: :otc)
|
|
31
33
|
# converter = CollectionConverter.new
|
|
32
|
-
# result = converter.convert(ttc_path, target_type: :otc,
|
|
33
|
-
# options: { output: 'family.otc', convert_outlines: true })
|
|
34
|
+
# result = converter.convert(ttc_path, target_type: :otc, options: { output: 'family.otc', options: options })
|
|
34
35
|
#
|
|
35
|
-
# @example Convert
|
|
36
|
+
# @example Convert TTC to OTC with outline conversion
|
|
36
37
|
# converter = CollectionConverter.new
|
|
37
|
-
# result = converter.convert(
|
|
38
|
+
# result = converter.convert(ttc_path, target_type: :otc,
|
|
39
|
+
# options: { output: 'family.otc', target_format: 'otf' })
|
|
38
40
|
class CollectionConverter
|
|
39
41
|
# Convert collection to target format
|
|
40
42
|
#
|
|
@@ -45,6 +47,7 @@ module Fontisan
|
|
|
45
47
|
# @option options [String] :target_format Target outline format: 'preserve' (default), 'ttf', or 'otf'
|
|
46
48
|
# @option options [Boolean] :optimize Enable table sharing (default: true, TTC/OTC only)
|
|
47
49
|
# @option options [Boolean] :verbose Enable verbose output (default: false)
|
|
50
|
+
# @option options [ConversionOptions] :options ConversionOptions object
|
|
48
51
|
# @return [Hash] Conversion result with:
|
|
49
52
|
# - :input [String] - Input collection path
|
|
50
53
|
# - :output [String] - Output collection path
|
|
@@ -57,9 +60,19 @@ module Fontisan
|
|
|
57
60
|
def convert(collection_path, target_type:, options: {})
|
|
58
61
|
validate_parameters!(collection_path, target_type, options)
|
|
59
62
|
|
|
63
|
+
# Extract ConversionOptions if provided
|
|
64
|
+
conv_options = extract_conversion_options(options)
|
|
65
|
+
|
|
60
66
|
verbose = options.fetch(:verbose, false)
|
|
61
67
|
output_path = options[:output]
|
|
62
|
-
|
|
68
|
+
|
|
69
|
+
# Determine target format from ConversionOptions or options hash
|
|
70
|
+
target_format = if conv_options&.generating_option?(:target_format,
|
|
71
|
+
"otf")
|
|
72
|
+
conv_options.generating[:target_format]
|
|
73
|
+
else
|
|
74
|
+
options.fetch(:target_format, "preserve").to_s
|
|
75
|
+
end
|
|
63
76
|
|
|
64
77
|
# Validate target_format
|
|
65
78
|
unless %w[preserve ttf otf].include?(target_format)
|
|
@@ -84,7 +97,8 @@ module Fontisan
|
|
|
84
97
|
# Step 2: Convert - transform fonts if requested
|
|
85
98
|
puts " Converting #{fonts.size} font(s)..." if verbose
|
|
86
99
|
converted_fonts, conversions = convert_fonts(fonts, source_type,
|
|
87
|
-
target_type, options.merge(target_format: target_format)
|
|
100
|
+
target_type, options.merge(target_format: target_format),
|
|
101
|
+
conv_options)
|
|
88
102
|
|
|
89
103
|
# Step 3: Repack - build target collection
|
|
90
104
|
puts " Repacking into #{target_type.to_s.upcase} format..." if verbose
|
|
@@ -206,13 +220,20 @@ module Fontisan
|
|
|
206
220
|
# @param source_type [Symbol] Source collection type
|
|
207
221
|
# @param target_type [Symbol] Target collection type
|
|
208
222
|
# @param options [Hash] Conversion options
|
|
223
|
+
# @param conv_options [ConversionOptions, nil] Conversion options object
|
|
209
224
|
# @return [Array<(Array<Font>, Array<Hash>)>] [converted_fonts, conversions]
|
|
210
|
-
def convert_fonts(fonts, _source_type, target_type, options
|
|
225
|
+
def convert_fonts(fonts, _source_type, target_type, options,
|
|
226
|
+
conv_options = nil)
|
|
211
227
|
converted_fonts = []
|
|
212
228
|
conversions = []
|
|
213
229
|
|
|
214
230
|
# Determine if outline conversion is needed
|
|
215
|
-
target_format =
|
|
231
|
+
target_format = if conv_options&.generating_option?(:target_format,
|
|
232
|
+
"otf")
|
|
233
|
+
conv_options.generating[:target_format]
|
|
234
|
+
else
|
|
235
|
+
options.fetch(:target_format, "preserve").to_s
|
|
236
|
+
end
|
|
216
237
|
|
|
217
238
|
fonts.each_with_index do |font, index|
|
|
218
239
|
source_format = detect_font_format(font)
|
|
@@ -224,8 +245,12 @@ module Fontisan
|
|
|
224
245
|
desired_format = target_format == "preserve" ? source_format : target_format.to_sym
|
|
225
246
|
converter = FormatConverter.new
|
|
226
247
|
|
|
248
|
+
# Build converter options, including ConversionOptions if provided
|
|
249
|
+
converter_opts = options.dup
|
|
250
|
+
converter_opts[:options] = conv_options if conv_options
|
|
251
|
+
|
|
227
252
|
begin
|
|
228
|
-
tables = converter.convert(font, desired_format,
|
|
253
|
+
tables = converter.convert(font, desired_format, converter_opts)
|
|
229
254
|
converted_font = build_font_from_tables(tables, desired_format)
|
|
230
255
|
converted_fonts << converted_font
|
|
231
256
|
|
|
@@ -442,6 +467,16 @@ conversions)
|
|
|
442
467
|
|
|
443
468
|
puts ""
|
|
444
469
|
end
|
|
470
|
+
|
|
471
|
+
# Extract ConversionOptions from options hash
|
|
472
|
+
#
|
|
473
|
+
# @param options [Hash, ConversionOptions] Options or hash containing :options key
|
|
474
|
+
# @return [ConversionOptions, nil] Extracted ConversionOptions or nil
|
|
475
|
+
def extract_conversion_options(options)
|
|
476
|
+
return options if options.is_a?(ConversionOptions)
|
|
477
|
+
|
|
478
|
+
options[:options] if options.is_a?(Hash)
|
|
479
|
+
end
|
|
445
480
|
end
|
|
446
481
|
end
|
|
447
482
|
end
|
|
@@ -6,6 +6,7 @@ require_relative "outline_converter"
|
|
|
6
6
|
require_relative "woff_writer"
|
|
7
7
|
require_relative "woff2_encoder"
|
|
8
8
|
require_relative "svg_generator"
|
|
9
|
+
require_relative "type1_converter"
|
|
9
10
|
require "yaml"
|
|
10
11
|
|
|
11
12
|
module Fontisan
|
|
@@ -54,6 +55,7 @@ module Fontisan
|
|
|
54
55
|
@strategies = [
|
|
55
56
|
TableCopier.new,
|
|
56
57
|
OutlineConverter.new,
|
|
58
|
+
Type1Converter.new,
|
|
57
59
|
WoffWriter.new,
|
|
58
60
|
Woff2Encoder.new,
|
|
59
61
|
SvgGenerator.new,
|
|
@@ -191,9 +193,12 @@ module Fontisan
|
|
|
191
193
|
|
|
192
194
|
# Check if font is a variable font
|
|
193
195
|
#
|
|
194
|
-
# @param font [TrueTypeFont, OpenTypeFont] Font to check
|
|
196
|
+
# @param font [TrueTypeFont, OpenTypeFont, Type1Font] Font to check
|
|
195
197
|
# @return [Boolean] True if font has fvar table
|
|
196
198
|
def variable_font?(font)
|
|
199
|
+
# Type 1 fonts are never variable fonts
|
|
200
|
+
return false if font.is_a?(Type1Font)
|
|
201
|
+
|
|
197
202
|
font.has_table?("fvar")
|
|
198
203
|
end
|
|
199
204
|
|
|
@@ -341,8 +346,12 @@ _options)
|
|
|
341
346
|
def validate_parameters!(font, target_format)
|
|
342
347
|
raise ArgumentError, "Font cannot be nil" if font.nil?
|
|
343
348
|
|
|
344
|
-
|
|
345
|
-
|
|
349
|
+
# Type1Font uses a different interface (font_dictionary, charstrings, etc.)
|
|
350
|
+
# rather than the SFNT table interface
|
|
351
|
+
is_type1 = font.is_a?(Type1Font)
|
|
352
|
+
|
|
353
|
+
unless is_type1 || font.respond_to?(:table)
|
|
354
|
+
raise ArgumentError, "Font must respond to :table method or be a Type1Font"
|
|
346
355
|
end
|
|
347
356
|
|
|
348
357
|
unless target_format.is_a?(Symbol)
|
|
@@ -392,10 +401,13 @@ _options)
|
|
|
392
401
|
|
|
393
402
|
# Detect font format from tables
|
|
394
403
|
#
|
|
395
|
-
# @param font [TrueTypeFont, OpenTypeFont] Font to detect
|
|
396
|
-
# @return [Symbol] Format (:ttf or :
|
|
404
|
+
# @param font [TrueTypeFont, OpenTypeFont, Type1Font] Font to detect
|
|
405
|
+
# @return [Symbol] Format (:ttf, :otf, or :type1)
|
|
397
406
|
# @raise [Error] If format cannot be detected
|
|
398
407
|
def detect_format(font)
|
|
408
|
+
# Check for Type1Font first (uses different interface)
|
|
409
|
+
return :type1 if font.is_a?(Type1Font)
|
|
410
|
+
|
|
399
411
|
# Check for CFF/CFF2 tables (OpenType/CFF)
|
|
400
412
|
if font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
401
413
|
:otf
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../conversion_options"
|
|
3
4
|
require_relative "conversion_strategy"
|
|
4
5
|
require_relative "../outline_extractor"
|
|
5
6
|
require_relative "../models/outline"
|
|
@@ -54,9 +55,10 @@ module Fontisan
|
|
|
54
55
|
# converter = Fontisan::Converters::OutlineConverter.new
|
|
55
56
|
# otf_font = converter.convert(ttf_font, target_format: :otf)
|
|
56
57
|
#
|
|
57
|
-
# @example Converting
|
|
58
|
+
# @example Converting with ConversionOptions
|
|
59
|
+
# options = ConversionOptions.recommended(from: :ttf, to: :otf)
|
|
58
60
|
# converter = Fontisan::Converters::OutlineConverter.new
|
|
59
|
-
#
|
|
61
|
+
# otf_font = converter.convert(ttf_font, options: options)
|
|
60
62
|
#
|
|
61
63
|
# @example Converting with hint preservation
|
|
62
64
|
# converter = Fontisan::Converters::OutlineConverter.new
|
|
@@ -89,19 +91,31 @@ module Fontisan
|
|
|
89
91
|
# @option options [Boolean] :preserve_variations Keep variation data during conversion (default: true)
|
|
90
92
|
# @option options [Boolean] :generate_instance Generate static instance instead of variable font (default: false)
|
|
91
93
|
# @option options [Hash] :instance_coordinates Axis coordinates for instance generation (default: {})
|
|
94
|
+
# @option options [ConversionOptions] :options ConversionOptions object
|
|
92
95
|
# @return [Hash<String, String>] Map of table tags to binary data
|
|
93
96
|
def convert(font, options = {})
|
|
94
97
|
@font = font
|
|
95
98
|
@options = options
|
|
99
|
+
|
|
100
|
+
# Extract ConversionOptions if provided
|
|
101
|
+
conv_options = extract_conversion_options(options)
|
|
102
|
+
|
|
96
103
|
@optimize_cff = options.fetch(:optimize_cff, false)
|
|
97
|
-
@preserve_hints = options.fetch(:preserve_hints,
|
|
104
|
+
@preserve_hints = options.fetch(:preserve_hints,
|
|
105
|
+
conv_options&.generating_option?(
|
|
106
|
+
:hinting_mode, "preserve"
|
|
107
|
+
) || false)
|
|
98
108
|
@preserve_variations = options.fetch(:preserve_variations, true)
|
|
99
109
|
@generate_instance = options.fetch(:generate_instance, false)
|
|
100
110
|
@instance_coordinates = options.fetch(:instance_coordinates, {})
|
|
101
|
-
|
|
111
|
+
|
|
112
|
+
target_format = options[:target_format] || conv_options&.to ||
|
|
102
113
|
detect_target_format(font)
|
|
103
114
|
validate(font, target_format)
|
|
104
115
|
|
|
116
|
+
# Apply opening options to source font
|
|
117
|
+
apply_opening_options(font, conv_options) if conv_options
|
|
118
|
+
|
|
105
119
|
source_format = detect_format(font)
|
|
106
120
|
|
|
107
121
|
# Check if we should generate a static instance instead
|
|
@@ -664,6 +678,66 @@ module Fontisan
|
|
|
664
678
|
def variable_font?(font)
|
|
665
679
|
font.has_table?("fvar")
|
|
666
680
|
end
|
|
681
|
+
|
|
682
|
+
# Extract ConversionOptions from options hash
|
|
683
|
+
#
|
|
684
|
+
# @param options [Hash, ConversionOptions] Options or hash containing :options key
|
|
685
|
+
# @return [ConversionOptions, nil] Extracted ConversionOptions or nil
|
|
686
|
+
def extract_conversion_options(options)
|
|
687
|
+
return options if options.is_a?(ConversionOptions)
|
|
688
|
+
|
|
689
|
+
options[:options] if options.is_a?(Hash)
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Apply opening options to source font
|
|
693
|
+
#
|
|
694
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
695
|
+
# @param conv_options [ConversionOptions] Conversion options with opening options
|
|
696
|
+
def apply_opening_options(font, conv_options)
|
|
697
|
+
return unless conv_options
|
|
698
|
+
|
|
699
|
+
# Decompose composites if requested
|
|
700
|
+
if conv_options.opening_option?(:decompose_composites)
|
|
701
|
+
decompose_composite_glyphs(font)
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
# Interpret OpenType tables if requested
|
|
705
|
+
if conv_options.opening_option?(:interpret_ot)
|
|
706
|
+
# Ensure GSUB/GPOS tables are fully loaded
|
|
707
|
+
interpret_opentype_features(font)
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# Note: scale_to_1000 and convert_curves are handled during conversion
|
|
711
|
+
# These options affect the conversion process itself
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
# Decompose composite glyphs in font
|
|
715
|
+
#
|
|
716
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
717
|
+
def decompose_composite_glyphs(_font)
|
|
718
|
+
# Placeholder: Decompose composite glyphs
|
|
719
|
+
# A full implementation would:
|
|
720
|
+
# 1. Identify composite glyphs (TrueType: flags & 0x0020, CFF: seac operator)
|
|
721
|
+
# 2. Extract component outlines
|
|
722
|
+
# 3. Merge into single glyph
|
|
723
|
+
#
|
|
724
|
+
# For now, this is a no-op placeholder
|
|
725
|
+
nil
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# Interpret OpenType layout features
|
|
729
|
+
#
|
|
730
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
731
|
+
def interpret_opentype_features(_font)
|
|
732
|
+
# Placeholder: Interpret GSUB/GPOS tables
|
|
733
|
+
# A full implementation would:
|
|
734
|
+
# 1. Parse GSUB table for substitution rules
|
|
735
|
+
# 2. Parse GPOS table for positioning rules
|
|
736
|
+
# 3. Store interpreted features for later use
|
|
737
|
+
#
|
|
738
|
+
# For now, this is a no-op placeholder
|
|
739
|
+
nil
|
|
740
|
+
end
|
|
667
741
|
end
|
|
668
742
|
end
|
|
669
743
|
end
|