fontisan 0.2.12 → 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.
@@ -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
@@ -153,12 +153,18 @@ module Fontisan
153
153
  "truetype"
154
154
  when OpenTypeFont
155
155
  "cff"
156
+ when Type1Font
157
+ "type1"
156
158
  else
157
159
  "unknown"
158
160
  end
159
161
 
160
- # Check if variable font
161
- info.is_variable = font.has_table?(Constants::FVAR_TAG)
162
+ # Check if variable font (Type1 fonts are never variable)
163
+ info.is_variable = if font.is_a?(Type1Font)
164
+ false
165
+ else
166
+ font.has_table?(Constants::FVAR_TAG)
167
+ end
162
168
  end
163
169
 
164
170
  # Populate essential fields for brief mode (metadata tables only).
@@ -169,6 +175,17 @@ module Fontisan
169
175
  #
170
176
  # @param info [Models::FontInfo] FontInfo instance to populate
171
177
  def populate_brief_fields(info)
178
+ if font.is_a?(Type1Font)
179
+ populate_type1_brief_fields(info)
180
+ else
181
+ populate_sfnt_brief_fields(info)
182
+ end
183
+ end
184
+
185
+ # Populate SFNT font brief fields (name, head, OS/2 tables).
186
+ #
187
+ # @param info [Models::FontInfo] FontInfo instance to populate
188
+ def populate_sfnt_brief_fields(info)
172
189
  # Essential names from name table
173
190
  if font.has_table?(Constants::NAME_TAG)
174
191
  name_table = font.table(Constants::NAME_TAG)
@@ -193,12 +210,45 @@ module Fontisan
193
210
  end
194
211
  end
195
212
 
213
+ # Populate Type 1 font brief fields.
214
+ #
215
+ # @param info [Models::FontInfo] FontInfo instance to populate
216
+ def populate_type1_brief_fields(info)
217
+ # Get Type 1 font metadata
218
+ font_dict = font.font_dictionary
219
+ font_info = font_dict&.font_info if font_dict
220
+
221
+ return unless font_info
222
+
223
+ # Essential names from Type 1 font
224
+ info.family_name = font_info.family_name
225
+ info.full_name = font_info.full_name
226
+ info.postscript_name = font.font_name
227
+ info.version = font_info.version
228
+
229
+ # Metrics from font dictionary
230
+ if font_dict&.font_b_box
231
+ info.bounding_box = font_dict.font_b_box
232
+ end
233
+ end
234
+
196
235
  # Populate all fields for full mode.
197
236
  #
198
237
  # Full mode extracts comprehensive metadata from all available tables.
199
238
  #
200
239
  # @param info [Models::FontInfo] FontInfo instance to populate
201
240
  def populate_full_fields(info)
241
+ if font.is_a?(Type1Font)
242
+ populate_type1_full_fields(info)
243
+ else
244
+ populate_sfnt_full_fields(info)
245
+ end
246
+ end
247
+
248
+ # Populate SFNT font full fields.
249
+ #
250
+ # @param info [Models::FontInfo] FontInfo instance to populate
251
+ def populate_sfnt_full_fields(info)
202
252
  populate_from_name_table(info) if font.has_table?(Constants::NAME_TAG)
203
253
  populate_from_os2_table(info) if font.has_table?(Constants::OS2_TAG)
204
254
  populate_from_head_table(info) if font.has_table?(Constants::HEAD_TAG)
@@ -207,6 +257,37 @@ module Fontisan
207
257
  populate_bitmap_info(info) if font.has_table?("CBLC") || font.has_table?("sbix")
208
258
  end
209
259
 
260
+ # Populate Type 1 font full fields.
261
+ #
262
+ # @param info [Models::FontInfo] FontInfo instance to populate
263
+ def populate_type1_full_fields(info)
264
+ font_dict = font.font_dictionary
265
+ font_info = font_dict&.font_info if font_dict
266
+
267
+ return unless font_info
268
+
269
+ # Names from Type 1 font
270
+ info.family_name = font_info.family_name
271
+ info.full_name = font_info.full_name
272
+ info.postscript_name = font.font_name
273
+ info.version = font_info.version
274
+ info.copyright = font_info.copyright
275
+ info.description = font_info.notice
276
+ info.designer = nil # Type 1 fonts may not have designer info
277
+
278
+ # Metrics
279
+ if font_dict&.font_b_box
280
+ info.bounding_box = font_dict.font_b_box
281
+ end
282
+
283
+ if font_dict&.font_matrix
284
+ info.font_matrix = font_dict.font_matrix
285
+ end
286
+
287
+ # Glyph count
288
+ info.glyph_count = font.charstrings&.count || 0
289
+ end
290
+
210
291
  # Populate FontInfo from the name table.
211
292
  #
212
293
  # @param info [Models::FontInfo] FontInfo instance to populate
@@ -193,9 +193,12 @@ module Fontisan
193
193
 
194
194
  # Check if font is a variable font
195
195
  #
196
- # @param font [TrueTypeFont, OpenTypeFont] Font to check
196
+ # @param font [TrueTypeFont, OpenTypeFont, Type1Font] Font to check
197
197
  # @return [Boolean] True if font has fvar table
198
198
  def variable_font?(font)
199
+ # Type 1 fonts are never variable fonts
200
+ return false if font.is_a?(Type1Font)
201
+
199
202
  font.has_table?("fvar")
200
203
  end
201
204
 
@@ -343,8 +346,12 @@ _options)
343
346
  def validate_parameters!(font, target_format)
344
347
  raise ArgumentError, "Font cannot be nil" if font.nil?
345
348
 
346
- unless font.respond_to?(:table)
347
- raise ArgumentError, "Font must respond to :table method"
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"
348
355
  end
349
356
 
350
357
  unless target_format.is_a?(Symbol)
@@ -394,10 +401,13 @@ _options)
394
401
 
395
402
  # Detect font format from tables
396
403
  #
397
- # @param font [TrueTypeFont, OpenTypeFont] Font to detect
398
- # @return [Symbol] Format (:ttf or :otf)
404
+ # @param font [TrueTypeFont, OpenTypeFont, Type1Font] Font to detect
405
+ # @return [Symbol] Format (:ttf, :otf, or :type1)
399
406
  # @raise [Error] If format cannot be detected
400
407
  def detect_format(font)
408
+ # Check for Type1Font first (uses different interface)
409
+ return :type1 if font.is_a?(Type1Font)
410
+
401
411
  # Check for CFF/CFF2 tables (OpenType/CFF)
402
412
  if font.has_table?("CFF ") || font.has_table?("CFF2")
403
413
  :otf