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,445 @@
|
|
|
1
|
+
= Type 1 Font Support
|
|
2
|
+
|
|
3
|
+
Fontisan provides comprehensive support for Adobe Type 1 fonts (PFB/PFA), including reading, converting, and generating Type 1 format fonts.
|
|
4
|
+
|
|
5
|
+
== Implementation Status
|
|
6
|
+
|
|
7
|
+
*Complete* features:
|
|
8
|
+
* Reading PFB (binary) and PFA (ASCII) format files
|
|
9
|
+
* eexec decryption for encrypted font portions (key: 55665)
|
|
10
|
+
* CharString decryption with lenIV support (key: 4330)
|
|
11
|
+
* Font dictionary and private dictionary parsing
|
|
12
|
+
* CharString parsing with `seac` composite expansion
|
|
13
|
+
* Type 1 → OTF conversion with CFF CharString generation
|
|
14
|
+
* Type 1 → TTF conversion (via OTF intermediate)
|
|
15
|
+
* OTF → Type 1 conversion with CFF → Type 1 CharString conversion
|
|
16
|
+
* TTF → Type 1 conversion (via OTF intermediate)
|
|
17
|
+
* Adobe Glyph List (AGL) integration for Unicode mapping
|
|
18
|
+
* SFNT table generation (head, hhea, maxp, name, OS/2, post, cmap)
|
|
19
|
+
* PFM, AFM, INF file generation for Type 1 fonts
|
|
20
|
+
|
|
21
|
+
*Preview/Planned* features:
|
|
22
|
+
* Multiple master Type 1 fonts
|
|
23
|
+
* Subroutine optimization in CharStrings
|
|
24
|
+
|
|
25
|
+
== Overview
|
|
26
|
+
|
|
27
|
+
Type 1 is a font format developed by Adobe Systems that uses PostScript outline descriptions. Type 1 fonts were widely used in professional typography and desktop publishing before being largely superseded by OpenType.
|
|
28
|
+
|
|
29
|
+
Fontisan's Type 1 support includes:
|
|
30
|
+
|
|
31
|
+
* Reading PFB and PFA files
|
|
32
|
+
* Converting Type 1 to modern formats (OTF, TTF, WOFF, WOFF2)
|
|
33
|
+
* Converting modern formats back to Type 1
|
|
34
|
+
* Preserving Type 1 metadata and hinting information
|
|
35
|
+
* Generating Unicode mappings from glyph names
|
|
36
|
+
|
|
37
|
+
== Type 1 Font Structure
|
|
38
|
+
|
|
39
|
+
=== Encryption
|
|
40
|
+
|
|
41
|
+
Type 1 fonts typically use two levels of encryption to protect font data:
|
|
42
|
+
|
|
43
|
+
* *eexec encryption* - Protects the private dictionary and CharStrings with key 55665
|
|
44
|
+
* *CharString encryption* - Encrypts individual CharStrings with key 4330
|
|
45
|
+
|
|
46
|
+
Fontisan automatically decrypts both levels when loading Type 1 fonts using the Rabin-Miller cipher with byte shuffle:
|
|
47
|
+
|
|
48
|
+
[source,ruby]
|
|
49
|
+
----
|
|
50
|
+
# Automatic decryption on load
|
|
51
|
+
font = Fontisan::FontLoader.from_file('font.pfb')
|
|
52
|
+
puts font.decrypted? # => true
|
|
53
|
+
|
|
54
|
+
# Access decrypted data
|
|
55
|
+
decrypted = font.decrypted_data
|
|
56
|
+
----
|
|
57
|
+
|
|
58
|
+
=== PFB vs PFA Formats
|
|
59
|
+
|
|
60
|
+
*PFB (Printer Font Binary)*::
|
|
61
|
+
Binary format with segmented structure using chunk markers:
|
|
62
|
+
* `0x8001` - ASCII text chunk
|
|
63
|
+
* `0x8002` - Binary data chunk (encrypted)
|
|
64
|
+
* `0x8003` - End of file marker
|
|
65
|
+
|
|
66
|
+
Each chunk is prefixed with a 4-byte little-endian length. PFB is most common on Windows systems.
|
|
67
|
+
|
|
68
|
+
*PFA (Printer Font ASCII)*::
|
|
69
|
+
Pure ASCII text format with encrypted portions marked by `currentfile eexec`. The encrypted data is represented as hexadecimal strings, followed by 512 ASCII zeros as a separator marker. PFA is most common on Unix/Linux systems.
|
|
70
|
+
|
|
71
|
+
=== Type 1 Font Structure
|
|
72
|
+
|
|
73
|
+
A Type 1 font consists of three main parts:
|
|
74
|
+
|
|
75
|
+
. *Font Dictionary* - Contains font-level metadata (font name, version, bounding box, etc.)
|
|
76
|
+
. *Private Dictionary* - Contains hinting parameters (blue values, stem snap, etc.)
|
|
77
|
+
. *CharStrings* - Contains glyph outline descriptions in PostScript format
|
|
78
|
+
|
|
79
|
+
=== Font Dictionary
|
|
80
|
+
|
|
81
|
+
The font dictionary contains essential font information:
|
|
82
|
+
|
|
83
|
+
[source,ruby]
|
|
84
|
+
----
|
|
85
|
+
{
|
|
86
|
+
version: "001.000",
|
|
87
|
+
notice: "Copyright notice",
|
|
88
|
+
copyright: "Copyright string",
|
|
89
|
+
full_name: "Font Full Name",
|
|
90
|
+
family_name: "Font Family",
|
|
91
|
+
weight: "Medium",
|
|
92
|
+
font_bbox: [x_min, y_min, x_max, y_max],
|
|
93
|
+
font_matrix: [0.001, 0, 0, 0.001, 0, 0]
|
|
94
|
+
}
|
|
95
|
+
----
|
|
96
|
+
|
|
97
|
+
Where,
|
|
98
|
+
|
|
99
|
+
`version`:: Version string in the format "XXX.YYY"
|
|
100
|
+
`notice`:: Copyright and license information
|
|
101
|
+
`copyright`:: Copyright string
|
|
102
|
+
`full_name`:: Full font name including style
|
|
103
|
+
`family_name`:: Font family name
|
|
104
|
+
`weight`:: Font weight (e.g., "Medium", "Bold", "Light")
|
|
105
|
+
`font_bbox`:: Font bounding box as [x_min, y_min, x_max, y_max]
|
|
106
|
+
`font_matrix`:: Transformation matrix for scaling coordinates
|
|
107
|
+
|
|
108
|
+
=== Private Dictionary
|
|
109
|
+
|
|
110
|
+
The private dictionary contains hinting parameters:
|
|
111
|
+
|
|
112
|
+
[source,ruby]
|
|
113
|
+
----
|
|
114
|
+
{
|
|
115
|
+
blue_values: [-10, 0, 470, 480],
|
|
116
|
+
other_blues: [250, 260],
|
|
117
|
+
blue_scale: 0.039625,
|
|
118
|
+
blue_shift: 7,
|
|
119
|
+
blue_fuzz: 1,
|
|
120
|
+
std_hw: 50,
|
|
121
|
+
std_vw: 80,
|
|
122
|
+
stem_snap_h: [50, 52],
|
|
123
|
+
stem_snap_v: [80, 82],
|
|
124
|
+
force_bold: false
|
|
125
|
+
}
|
|
126
|
+
----
|
|
127
|
+
|
|
128
|
+
Where,
|
|
129
|
+
|
|
130
|
+
`blue_values`:: Alignment zones for baseline and cap heights
|
|
131
|
+
`other_blues`:: Additional alignment zones
|
|
132
|
+
`blue_scale`:: Scaling factor for alignment zones
|
|
133
|
+
`blue_shift`:: Maximum deviation from alignment zone
|
|
134
|
+
`blue_fuzz`:: Fuzz factor for alignment
|
|
135
|
+
`std_hw`:: Standard horizontal stem width
|
|
136
|
+
`std_vw`:: Standard vertical stem width
|
|
137
|
+
`stem_snap_h`:: Array of horizontal stem widths
|
|
138
|
+
`stem_snap_v`:: Array of vertical stem widths
|
|
139
|
+
`force_bold`:: Whether to force bold rendering
|
|
140
|
+
|
|
141
|
+
=== CharStrings
|
|
142
|
+
|
|
143
|
+
CharStrings contain glyph outline descriptions using PostScript commands:
|
|
144
|
+
|
|
145
|
+
[source]
|
|
146
|
+
----
|
|
147
|
+
hsbw 0 0 # Horizontal sidebearings and width
|
|
148
|
+
rmoveto 100 0 # Relative move to
|
|
149
|
+
rlineto 50 0 # Relative line to
|
|
150
|
+
rlineto 0 50 # Relative line to
|
|
151
|
+
rlineto -50 0 # Relative line to (close path)
|
|
152
|
+
rlineto 0 -50 # Relative line to (close path)
|
|
153
|
+
endchar # End of glyph
|
|
154
|
+
----
|
|
155
|
+
|
|
156
|
+
=== Seac Composite Glyphs
|
|
157
|
+
|
|
158
|
+
Type 1 fonts include composite glyphs using the `seac` operator for accented characters. For example, "À" (A grave) might be defined as a base "A" with a combining grave accent.
|
|
159
|
+
|
|
160
|
+
Fontisan provides automatic `seac` expansion:
|
|
161
|
+
|
|
162
|
+
[source,ruby]
|
|
163
|
+
----
|
|
164
|
+
# Enable seac decomposition
|
|
165
|
+
options = Fontisan::ConversionOptions.new(
|
|
166
|
+
from: :type1,
|
|
167
|
+
to: :otf,
|
|
168
|
+
opening: { decompose_composites: true }
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
converter = Fontisan::Converters::Type1Converter.new
|
|
172
|
+
tables = converter.convert(font, options: options)
|
|
173
|
+
----
|
|
174
|
+
|
|
175
|
+
The `SeacExpander` resolves composite glyphs by:
|
|
176
|
+
1. Extracting the base character and accent from the encoding
|
|
177
|
+
2. Recursively retrieving component CharStrings
|
|
178
|
+
3. Merging the outlines into a single decomposed glyph
|
|
179
|
+
|
|
180
|
+
This is necessary because CFF fonts (used in OTF) do not support the `seac` operator.
|
|
181
|
+
|
|
182
|
+
== Loading Type 1 Fonts
|
|
183
|
+
|
|
184
|
+
Use `FontLoader.load` to load Type 1 fonts:
|
|
185
|
+
|
|
186
|
+
[source,ruby]
|
|
187
|
+
----
|
|
188
|
+
require 'fontisan'
|
|
189
|
+
|
|
190
|
+
# Load PFB file
|
|
191
|
+
font = Fontisan::FontLoader.load('font.pfb')
|
|
192
|
+
|
|
193
|
+
# Load PFA file
|
|
194
|
+
font = Fontisan::FontLoader.load('font.pfa')
|
|
195
|
+
|
|
196
|
+
# Access font dictionary
|
|
197
|
+
dict = font.font_dictionary
|
|
198
|
+
puts "Font: #{dict.font_name}"
|
|
199
|
+
puts "Family: #{dict.family_name}"
|
|
200
|
+
----
|
|
201
|
+
|
|
202
|
+
== Converting Type 1 Fonts
|
|
203
|
+
|
|
204
|
+
=== Type 1 to OpenType (OTF)
|
|
205
|
+
|
|
206
|
+
Convert Type 1 fonts to modern OpenType format with CFF outlines:
|
|
207
|
+
|
|
208
|
+
[source,ruby]
|
|
209
|
+
----
|
|
210
|
+
converter = Fontisan::Converters::Type1Converter.new
|
|
211
|
+
tables = converter.convert(font, target_format: :otf)
|
|
212
|
+
|
|
213
|
+
# Write to file
|
|
214
|
+
Fontisan::FontWriter.write(tables, 'output.otf')
|
|
215
|
+
----
|
|
216
|
+
|
|
217
|
+
Using the CLI:
|
|
218
|
+
|
|
219
|
+
[source,shell]
|
|
220
|
+
----
|
|
221
|
+
fontisan convert font.pfb --to otf --output font.otf
|
|
222
|
+
----
|
|
223
|
+
|
|
224
|
+
=== Type 1 to TrueType (TTF)
|
|
225
|
+
|
|
226
|
+
Convert Type 1 fonts to TrueType format with quadratic curves:
|
|
227
|
+
|
|
228
|
+
[source,ruby]
|
|
229
|
+
----
|
|
230
|
+
converter = Fontisan::Converters::Type1Converter.new
|
|
231
|
+
tables = converter.convert(font, target_format: :ttf)
|
|
232
|
+
|
|
233
|
+
# Write to file
|
|
234
|
+
Fontisan::FontWriter.write(tables, 'output.ttf')
|
|
235
|
+
----
|
|
236
|
+
|
|
237
|
+
Using the CLI:
|
|
238
|
+
|
|
239
|
+
[source,shell]
|
|
240
|
+
----
|
|
241
|
+
fontisan convert font.pfb --to ttf --output font.ttf
|
|
242
|
+
----
|
|
243
|
+
|
|
244
|
+
=== Type 1 to Web Fonts
|
|
245
|
+
|
|
246
|
+
Convert Type 1 fonts directly to web font formats:
|
|
247
|
+
|
|
248
|
+
[source,shell]
|
|
249
|
+
----
|
|
250
|
+
# Convert to WOFF
|
|
251
|
+
fontisan convert font.pfb --to woff --output font.woff
|
|
252
|
+
|
|
253
|
+
# Convert to WOFF2
|
|
254
|
+
fontisan convert font.pfb --to woff2 --output font.woff2
|
|
255
|
+
----
|
|
256
|
+
|
|
257
|
+
=== Conversion with Options
|
|
258
|
+
|
|
259
|
+
Use ConversionOptions for advanced control:
|
|
260
|
+
|
|
261
|
+
[source,ruby]
|
|
262
|
+
----
|
|
263
|
+
options = Fontisan::ConversionOptions.recommended(from: :type1, to: :otf)
|
|
264
|
+
converter = Fontisan::Converters::Type1Converter.new
|
|
265
|
+
tables = converter.convert(font, options: options)
|
|
266
|
+
----
|
|
267
|
+
|
|
268
|
+
Using presets:
|
|
269
|
+
|
|
270
|
+
[source,ruby]
|
|
271
|
+
----
|
|
272
|
+
options = Fontisan::ConversionOptions.from_preset(:type1_to_modern)
|
|
273
|
+
tables = converter.convert(font, options: options)
|
|
274
|
+
----
|
|
275
|
+
|
|
276
|
+
Using the CLI:
|
|
277
|
+
|
|
278
|
+
[source,shell]
|
|
279
|
+
----
|
|
280
|
+
fontisan convert font.pfb --to otf --output font.otf --preset type1_to_modern
|
|
281
|
+
----
|
|
282
|
+
|
|
283
|
+
== Converting to Type 1
|
|
284
|
+
|
|
285
|
+
=== OpenType to Type 1
|
|
286
|
+
|
|
287
|
+
Convert OpenType/CFF fonts back to Type 1 format:
|
|
288
|
+
|
|
289
|
+
[source,ruby]
|
|
290
|
+
----
|
|
291
|
+
converter = Fontisan::Converters::Type1Converter.new
|
|
292
|
+
type1_data = converter.convert(otf_font, target_format: :type1)
|
|
293
|
+
|
|
294
|
+
# Write PFB file
|
|
295
|
+
File.write('output.pfb', type1_data[:pfb])
|
|
296
|
+
----
|
|
297
|
+
|
|
298
|
+
Using the CLI:
|
|
299
|
+
|
|
300
|
+
[source,shell]
|
|
301
|
+
----
|
|
302
|
+
fontisan convert font.otf --to type1 --output font.pfb --preset modern_to_type1
|
|
303
|
+
----
|
|
304
|
+
|
|
305
|
+
=== TrueType to Type 1
|
|
306
|
+
|
|
307
|
+
Convert TrueType fonts to Type 1 (via OpenType intermediate):
|
|
308
|
+
|
|
309
|
+
[source,shell]
|
|
310
|
+
----
|
|
311
|
+
fontisan convert font.ttf --to type1 --output font.pfb
|
|
312
|
+
----
|
|
313
|
+
|
|
314
|
+
== Conversion Options
|
|
315
|
+
|
|
316
|
+
=== Opening Options
|
|
317
|
+
|
|
318
|
+
Options applied when reading Type 1 fonts:
|
|
319
|
+
|
|
320
|
+
`generate_unicode`:: Generate Unicode codepoints from glyph names using the Adobe Glyph List
|
|
321
|
+
`decompose_composites`:: Decompose seac composite glyphs into base glyphs
|
|
322
|
+
`read_all_records`:: Force loading of all font dictionary records
|
|
323
|
+
|
|
324
|
+
=== Generating Options
|
|
325
|
+
|
|
326
|
+
Options applied when writing Type 1 fonts:
|
|
327
|
+
|
|
328
|
+
`write_pfm`:: Generate PFM (Printer Font Metrics) file (default: true)
|
|
329
|
+
`write_afm`:: Generate AFM (Adobe Font Metrics) file (default: true)
|
|
330
|
+
`write_inf`:: Generate INF file for installation (default: true)
|
|
331
|
+
`select_encoding_automatically`:: Automatically select encoding (default: true)
|
|
332
|
+
`hinting_mode`:: Hint preservation mode: preserve, auto, or none
|
|
333
|
+
`decompose_on_output`:: Decompose composite glyphs on output
|
|
334
|
+
|
|
335
|
+
=== Presets
|
|
336
|
+
|
|
337
|
+
Fontisan includes predefined presets for common Type 1 workflows:
|
|
338
|
+
|
|
339
|
+
* `type1_to_modern` - Optimize Type 1 fonts for modern use (generates Unicode, preserves hints)
|
|
340
|
+
* `modern_to_type1` - Convert modern fonts back to Type 1 format with proper metrics
|
|
341
|
+
|
|
342
|
+
[source,ruby]
|
|
343
|
+
----
|
|
344
|
+
# Using preset programmatically
|
|
345
|
+
options = Fontisan::ConversionOptions.from_preset(:type1_to_modern)
|
|
346
|
+
|
|
347
|
+
# Using preset via CLI
|
|
348
|
+
fontisan convert font.pfb --to otf --preset type1_to_modern --output font.otf
|
|
349
|
+
----
|
|
350
|
+
|
|
351
|
+
== Working with Glyphs
|
|
352
|
+
|
|
353
|
+
=== Accessing Glyph Outlines
|
|
354
|
+
|
|
355
|
+
[source,ruby]
|
|
356
|
+
----
|
|
357
|
+
font = Fontisan::FontLoader.load('font.pfb')
|
|
358
|
+
|
|
359
|
+
# Iterate over glyphs
|
|
360
|
+
font.charstrings.each_charstring do |glyph_name, charstring|
|
|
361
|
+
puts "Glyph: #{glyph_name}"
|
|
362
|
+
|
|
363
|
+
# Convert to universal outline format
|
|
364
|
+
outline = charstring.to_outline
|
|
365
|
+
|
|
366
|
+
# Access contour points
|
|
367
|
+
outline.contours.each do |contour|
|
|
368
|
+
contour.points.each do |point|
|
|
369
|
+
puts " Point: #{point.x}, #{point.y} (#{point.on_curve? ? 'on' : 'off'} curve)"
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
----
|
|
374
|
+
|
|
375
|
+
=== Glyph Names to Unicode
|
|
376
|
+
|
|
377
|
+
Type 1 fonts use glyph names rather than Unicode codepoints. Use the Adobe Glyph List to map names to codepoints:
|
|
378
|
+
|
|
379
|
+
[source,ruby]
|
|
380
|
+
----
|
|
381
|
+
# Enable Unicode generation
|
|
382
|
+
options = Fontisan::ConversionOptions.new(
|
|
383
|
+
from: :type1,
|
|
384
|
+
to: :otf,
|
|
385
|
+
opening: { generate_unicode: true }
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
converter = Fontisan::Converters::Type1Converter.new
|
|
389
|
+
tables = converter.convert(font, options: options)
|
|
390
|
+
----
|
|
391
|
+
|
|
392
|
+
== Limitations
|
|
393
|
+
|
|
394
|
+
Current limitations of Type 1 support:
|
|
395
|
+
|
|
396
|
+
* Subroutines in CharStrings are not fully optimized for space efficiency
|
|
397
|
+
* Multiple master Type 1 fonts are not supported (only single master fonts)
|
|
398
|
+
* Some hinting parameters may not convert perfectly between Type 1 and CFF formats
|
|
399
|
+
* Converting modern fonts back to Type 1 may lose some OpenType features (GSUB/GPOS)
|
|
400
|
+
|
|
401
|
+
== Technical Details
|
|
402
|
+
|
|
403
|
+
=== Decryption Algorithm
|
|
404
|
+
|
|
405
|
+
Fontisan uses the Rabin-Miller cipher with byte shuffle for decryption:
|
|
406
|
+
|
|
407
|
+
[source,ruby]
|
|
408
|
+
----
|
|
409
|
+
# eexec decryption (key: 55665)
|
|
410
|
+
cipher = key
|
|
411
|
+
data.each_byte do |byte|
|
|
412
|
+
cipher = ((cipher << 8) & 0xFFFFFFFF) | byte
|
|
413
|
+
plain = cipher >> 8
|
|
414
|
+
result << plain.chr
|
|
415
|
+
cipher = plain ^ (cipher >> 16)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# CharString decryption (key: 4330)
|
|
419
|
+
# Same cipher, but skips lenIV bytes at start
|
|
420
|
+
decrypted = decrypt(charstring_data, 4330)
|
|
421
|
+
charstring = lenIV > 0 ? decrypted[lenIV..-1] : decrypted
|
|
422
|
+
----
|
|
423
|
+
|
|
424
|
+
=== Command Mapping
|
|
425
|
+
|
|
426
|
+
Type 1 CharString commands map directly to CFF commands for conversion:
|
|
427
|
+
|
|
428
|
+
| Type 1 Command | CFF Command | Notes |
|
|
429
|
+
|----------------|-------------|-------|
|
|
430
|
+
| hsbw | hsbw | Identical |
|
|
431
|
+
| sbw | sbw | Identical |
|
|
432
|
+
| endchar | endchar | Identical |
|
|
433
|
+
| hstem/vstem | hstem/vstem | Identical |
|
|
434
|
+
| rmoveto/hmoveto/vmoveto | rmoveto/hmoveto/vmoveto | Identical |
|
|
435
|
+
| rlineto/hlineto/vlineto | rlineto/hlineto/vlineto | Identical |
|
|
436
|
+
| rrcurveto/hhcurveto/vvcurveto/hvhcurveto/vhcurveto | rrcurveto/hhcurveto/vvcurveto/hvhcurveto/vhcurveto | Identical |
|
|
437
|
+
| seac | - | Decomposed (not in CFF) |
|
|
438
|
+
| closepath | endchar | Mapped |
|
|
439
|
+
| callsubr/callgsubr | callsubr/callgsubr | Identical |
|
|
440
|
+
|
|
441
|
+
== See Also
|
|
442
|
+
|
|
443
|
+
* https://www.adobe.com/devnet/font/pdfs/Type1.pdf[Adobe Type 1 Font Format Specification]
|
|
444
|
+
* link:CONVERSION_GUIDE.adoc[Conversion Guide] - Comprehensive conversion options reference
|
|
445
|
+
* link:README.adoc[README] - Main Fontisan documentation
|
data/lib/fontisan/cli.rb
CHANGED
|
@@ -203,10 +203,10 @@ module Fontisan
|
|
|
203
203
|
|
|
204
204
|
desc "convert FONT_FILE", "Convert font to different format"
|
|
205
205
|
option :to, type: :string, required: true,
|
|
206
|
-
desc: "Target format (ttf, otf, woff, woff2)",
|
|
206
|
+
desc: "Target format (ttf, otf, type1, t1, woff, woff2)",
|
|
207
207
|
aliases: "-t"
|
|
208
|
-
option :output, type: :string,
|
|
209
|
-
desc: "Output file path",
|
|
208
|
+
option :output, type: :string,
|
|
209
|
+
desc: "Output file path (required unless --show-options)",
|
|
210
210
|
aliases: "-o"
|
|
211
211
|
option :coordinates, type: :string,
|
|
212
212
|
desc: "Instance coordinates (e.g., wght=700,wdth=100)",
|
|
@@ -232,10 +232,34 @@ module Fontisan
|
|
|
232
232
|
desc: "Italic axis value (alternative to --coordinates)"
|
|
233
233
|
option :opsz, type: :numeric,
|
|
234
234
|
desc: "Optical size axis value (alternative to --coordinates)"
|
|
235
|
+
# Conversion options
|
|
236
|
+
option :preset, type: :string,
|
|
237
|
+
desc: "Use named preset (type1_to_modern, modern_to_type1, web_optimized, archive_to_modern)"
|
|
238
|
+
option :show_options, type: :boolean, default: false,
|
|
239
|
+
desc: "Show recommended options for the conversion and exit"
|
|
240
|
+
option :decompose, type: :boolean,
|
|
241
|
+
desc: "Decompose composite glyphs (opening option)"
|
|
242
|
+
option :convert_curves, type: :boolean,
|
|
243
|
+
desc: "Convert curves during conversion (opening option)"
|
|
244
|
+
option :scale_to_1000, type: :boolean,
|
|
245
|
+
desc: "Scale to 1000 units per em (opening option)"
|
|
246
|
+
option :autohint, type: :boolean,
|
|
247
|
+
desc: "Auto-hint the font (opening option)"
|
|
248
|
+
option :generate_unicode, type: :boolean,
|
|
249
|
+
desc: "Generate Unicode mappings (Type 1 opening option)"
|
|
250
|
+
option :hinting_mode, type: :string,
|
|
251
|
+
desc: "Hinting mode: preserve, auto, none, or full"
|
|
252
|
+
option :optimize_cff, type: :boolean,
|
|
253
|
+
desc: "Enable CFF subroutine optimization"
|
|
254
|
+
option :optimize_tables, type: :boolean,
|
|
255
|
+
desc: "Enable table optimization"
|
|
256
|
+
option :decompose_on_output, type: :boolean,
|
|
257
|
+
desc: "Decompose on output (generating option)"
|
|
235
258
|
# Convert a font to a different format using the universal transformation pipeline.
|
|
236
259
|
#
|
|
237
260
|
# Supported conversions:
|
|
238
261
|
# - TTF ↔ OTF: Outline format conversion
|
|
262
|
+
# - Type 1 ↔ TTF/OTF: Adobe Type 1 font conversion
|
|
239
263
|
# - WOFF/WOFF2: Web font packaging
|
|
240
264
|
# - Variable fonts: Automatic variation preservation or instance generation
|
|
241
265
|
# - Collections (TTC/OTC/dfont): Preserve mixed TTF+OTF by default, or standardize with --target-format
|
|
@@ -261,6 +285,12 @@ module Fontisan
|
|
|
261
285
|
# @example Convert TTF to OTF
|
|
262
286
|
# fontisan convert font.ttf --to otf --output font.otf
|
|
263
287
|
#
|
|
288
|
+
# @example Convert Type 1 to OTF
|
|
289
|
+
# fontisan convert font.pfb --to otf --output font.otf
|
|
290
|
+
#
|
|
291
|
+
# @example Convert OTF to Type 1
|
|
292
|
+
# fontisan convert font.otf --to type1 --output font.pfb
|
|
293
|
+
#
|
|
264
294
|
# @example Convert TTC to OTC (preserves mixed formats by default)
|
|
265
295
|
# fontisan convert family.ttc --to otc --output family.otc
|
|
266
296
|
#
|
|
@@ -284,17 +314,46 @@ module Fontisan
|
|
|
284
314
|
#
|
|
285
315
|
# @example Convert without validation
|
|
286
316
|
# fontisan convert font.ttf --to otf --output font.otf --no-validate
|
|
317
|
+
#
|
|
318
|
+
# @example Use named preset
|
|
319
|
+
# fontisan convert font.pfb --to otf --output font.otf --preset type1_to_modern
|
|
320
|
+
#
|
|
321
|
+
# @example Show recommended options for conversion
|
|
322
|
+
# fontisan convert font.ttf --to otf --show-options
|
|
323
|
+
#
|
|
324
|
+
# @example Convert with custom options
|
|
325
|
+
# fontisan convert font.ttf --to otf --output font.otf --autohint --hinting-mode auto
|
|
287
326
|
def convert(font_file)
|
|
327
|
+
# Detect source format from file
|
|
328
|
+
source_format = detect_source_format(font_file)
|
|
329
|
+
|
|
330
|
+
# Handle --show-options
|
|
331
|
+
if options[:show_options]
|
|
332
|
+
show_recommended_options(source_format, options[:to])
|
|
333
|
+
return
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Validate output is provided when not using --show-options
|
|
337
|
+
unless options[:output]
|
|
338
|
+
raise Thor::Error, "Output path is required. Use --output option."
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Build ConversionOptions
|
|
342
|
+
conv_options = build_conversion_options(source_format, options[:to],
|
|
343
|
+
options)
|
|
344
|
+
|
|
288
345
|
# Build instance coordinates from axis options
|
|
289
346
|
instance_coords = build_instance_coordinates(options)
|
|
290
347
|
|
|
291
|
-
# Merge coordinates into
|
|
348
|
+
# Merge coordinates and ConversionOptions into convert_options
|
|
292
349
|
convert_options = options.to_h.dup
|
|
293
350
|
if instance_coords.any?
|
|
294
|
-
convert_options[:instance_coordinates] =
|
|
295
|
-
instance_coords
|
|
351
|
+
convert_options[:instance_coordinates] = instance_coords
|
|
296
352
|
end
|
|
297
353
|
|
|
354
|
+
# Add ConversionOptions if built
|
|
355
|
+
convert_options[:options] = conv_options if conv_options
|
|
356
|
+
|
|
298
357
|
command = Commands::ConvertCommand.new(font_file, convert_options)
|
|
299
358
|
command.run
|
|
300
359
|
rescue Errno::ENOENT, Error => e
|
|
@@ -668,5 +727,117 @@ module Fontisan
|
|
|
668
727
|
puts " #{profile_name.to_s.ljust(20)} - #{config[:description]}"
|
|
669
728
|
end
|
|
670
729
|
end
|
|
730
|
+
|
|
731
|
+
# Detect source format from file extension
|
|
732
|
+
#
|
|
733
|
+
# @param font_file [String] Path to the font file
|
|
734
|
+
# @return [Symbol] Detected format symbol
|
|
735
|
+
def detect_source_format(font_file)
|
|
736
|
+
ext = File.extname(font_file).downcase
|
|
737
|
+
case ext
|
|
738
|
+
when ".ttf"
|
|
739
|
+
:ttf
|
|
740
|
+
when ".otf"
|
|
741
|
+
:otf
|
|
742
|
+
when ".pfb", ".pfa"
|
|
743
|
+
:type1
|
|
744
|
+
when ".ttc"
|
|
745
|
+
:ttc
|
|
746
|
+
when ".otc"
|
|
747
|
+
:otc
|
|
748
|
+
when ".dfont"
|
|
749
|
+
:dfont
|
|
750
|
+
when ".woff"
|
|
751
|
+
:woff
|
|
752
|
+
when ".woff2"
|
|
753
|
+
:woff2
|
|
754
|
+
when ".svg"
|
|
755
|
+
:svg
|
|
756
|
+
else
|
|
757
|
+
# Default to TTF for unknown extensions
|
|
758
|
+
:ttf
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# Show recommended options for a conversion
|
|
763
|
+
#
|
|
764
|
+
# @param source_format [Symbol] Source format
|
|
765
|
+
# @param target_format_str [String] Target format string
|
|
766
|
+
# @return [void]
|
|
767
|
+
def show_recommended_options(source_format, target_format_str)
|
|
768
|
+
target_format = Fontisan::ConversionOptions.normalize_format(target_format_str)
|
|
769
|
+
|
|
770
|
+
puts "\nRecommended options for #{source_format.to_s.upcase} → #{target_format.to_s.upcase} conversion:"
|
|
771
|
+
puts "=" * 70
|
|
772
|
+
|
|
773
|
+
# Show recommended options
|
|
774
|
+
recommended = Fontisan::ConversionOptions.recommended(from: source_format,
|
|
775
|
+
to: target_format)
|
|
776
|
+
puts "\nOpening options:"
|
|
777
|
+
if recommended.opening.any?
|
|
778
|
+
recommended.opening.each do |key, value|
|
|
779
|
+
puts " --#{key.to_s.gsub('_', '-')}: #{value}"
|
|
780
|
+
end
|
|
781
|
+
else
|
|
782
|
+
puts " (none)"
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
puts "\nGenerating options:"
|
|
786
|
+
if recommended.generating.any?
|
|
787
|
+
recommended.generating.each do |key, value|
|
|
788
|
+
puts " --#{key.to_s.gsub('_', '-')}: #{value}"
|
|
789
|
+
end
|
|
790
|
+
else
|
|
791
|
+
puts " (none)"
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
puts "\nAvailable presets:"
|
|
795
|
+
Fontisan::ConversionOptions.available_presets.each do |preset|
|
|
796
|
+
puts " #{preset}"
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
puts "\nTo use preset:"
|
|
800
|
+
puts " fontisan convert #{source_format} --to #{target_format} --preset <name> --output output.ext"
|
|
801
|
+
puts "\n"
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
# Build ConversionOptions from CLI options
|
|
805
|
+
#
|
|
806
|
+
# @param source_format [Symbol] Source format
|
|
807
|
+
# @param target_format_str [String] Target format string
|
|
808
|
+
# @param opts [Hash] CLI options
|
|
809
|
+
# @return [ConversionOptions, nil] Built ConversionOptions or nil
|
|
810
|
+
def build_conversion_options(source_format, target_format_str, opts)
|
|
811
|
+
target_format = Fontisan::ConversionOptions.normalize_format(target_format_str)
|
|
812
|
+
|
|
813
|
+
# Use preset if specified
|
|
814
|
+
if opts[:preset]
|
|
815
|
+
return Fontisan::ConversionOptions.from_preset(opts[:preset])
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# Build opening options from CLI flags
|
|
819
|
+
opening = {}
|
|
820
|
+
opening[:decompose_composites] = true if opts[:decompose]
|
|
821
|
+
opening[:convert_curves] = true if opts[:convert_curves]
|
|
822
|
+
opening[:scale_to_1000] = true if opts[:scale_to_1000]
|
|
823
|
+
opening[:autohint] = true if opts[:autohint]
|
|
824
|
+
opening[:generate_unicode] = true if opts[:generate_unicode]
|
|
825
|
+
|
|
826
|
+
# Build generating options from CLI flags
|
|
827
|
+
generating = {}
|
|
828
|
+
generating[:hinting_mode] = opts[:hinting_mode] if opts[:hinting_mode]
|
|
829
|
+
generating[:decompose_on_output] = true if opts[:decompose_on_output]
|
|
830
|
+
generating[:optimize_tables] = true if opts[:optimize_tables]
|
|
831
|
+
|
|
832
|
+
# Only create ConversionOptions if any options were set
|
|
833
|
+
return nil if opening.empty? && generating.empty?
|
|
834
|
+
|
|
835
|
+
Fontisan::ConversionOptions.new(
|
|
836
|
+
from: source_format,
|
|
837
|
+
to: target_format,
|
|
838
|
+
opening: opening,
|
|
839
|
+
generating: generating,
|
|
840
|
+
)
|
|
841
|
+
end
|
|
671
842
|
end
|
|
672
843
|
end
|