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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +294 -52
  3. data/Gemfile +5 -0
  4. data/README.adoc +163 -2
  5. data/docs/CONVERSION_GUIDE.adoc +633 -0
  6. data/docs/TYPE1_FONTS.adoc +445 -0
  7. data/lib/fontisan/cli.rb +177 -6
  8. data/lib/fontisan/commands/convert_command.rb +32 -1
  9. data/lib/fontisan/commands/info_command.rb +83 -2
  10. data/lib/fontisan/config/conversion_matrix.yml +132 -4
  11. data/lib/fontisan/constants.rb +12 -0
  12. data/lib/fontisan/conversion_options.rb +378 -0
  13. data/lib/fontisan/converters/collection_converter.rb +45 -10
  14. data/lib/fontisan/converters/format_converter.rb +17 -5
  15. data/lib/fontisan/converters/outline_converter.rb +78 -4
  16. data/lib/fontisan/converters/type1_converter.rb +1234 -0
  17. data/lib/fontisan/font_loader.rb +46 -3
  18. data/lib/fontisan/hints/hint_converter.rb +4 -1
  19. data/lib/fontisan/type1/afm_generator.rb +436 -0
  20. data/lib/fontisan/type1/afm_parser.rb +298 -0
  21. data/lib/fontisan/type1/agl.rb +456 -0
  22. data/lib/fontisan/type1/cff_to_type1_converter.rb +302 -0
  23. data/lib/fontisan/type1/charstring_converter.rb +240 -0
  24. data/lib/fontisan/type1/charstrings.rb +408 -0
  25. data/lib/fontisan/type1/conversion_options.rb +243 -0
  26. data/lib/fontisan/type1/decryptor.rb +183 -0
  27. data/lib/fontisan/type1/encodings.rb +697 -0
  28. data/lib/fontisan/type1/font_dictionary.rb +576 -0
  29. data/lib/fontisan/type1/generator.rb +220 -0
  30. data/lib/fontisan/type1/inf_generator.rb +332 -0
  31. data/lib/fontisan/type1/pfa_generator.rb +369 -0
  32. data/lib/fontisan/type1/pfa_parser.rb +159 -0
  33. data/lib/fontisan/type1/pfb_generator.rb +314 -0
  34. data/lib/fontisan/type1/pfb_parser.rb +166 -0
  35. data/lib/fontisan/type1/pfm_generator.rb +610 -0
  36. data/lib/fontisan/type1/pfm_parser.rb +433 -0
  37. data/lib/fontisan/type1/private_dict.rb +342 -0
  38. data/lib/fontisan/type1/seac_expander.rb +501 -0
  39. data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
  40. data/lib/fontisan/type1/upm_scaler.rb +118 -0
  41. data/lib/fontisan/type1.rb +75 -0
  42. data/lib/fontisan/type1_font.rb +318 -0
  43. data/lib/fontisan/version.rb +1 -1
  44. data/lib/fontisan.rb +2 -0
  45. metadata +30 -3
  46. data/docs/DOCUMENTATION_SUMMARY.md +0 -141
@@ -3,6 +3,7 @@
3
3
  require_relative "base_command"
4
4
  require_relative "../pipeline/transformation_pipeline"
5
5
  require_relative "../converters/collection_converter"
6
+ require_relative "../conversion_options"
6
7
  require_relative "../font_loader"
7
8
 
8
9
  module Fontisan
@@ -36,6 +37,16 @@ module Fontisan
36
37
  # coordinates: 'wght=700,wdth=100'
37
38
  # )
38
39
  # command.run
40
+ #
41
+ # @example Convert with ConversionOptions
42
+ # options = ConversionOptions.recommended(from: :ttf, to: :otf)
43
+ # command = ConvertCommand.new(
44
+ # 'input.ttf',
45
+ # to: 'otf',
46
+ # output: 'output.otf',
47
+ # options: options
48
+ # )
49
+ # command.run
39
50
  class ConvertCommand < BaseCommand
40
51
  # Initialize convert command
41
52
  #
@@ -52,6 +63,7 @@ module Fontisan
52
63
  # @option options [String] :target_format Target outline format for collections: 'preserve' (default), 'ttf', or 'otf'
53
64
  # @option options [Boolean] :no_validate Skip output validation
54
65
  # @option options [Boolean] :verbose Verbose output
66
+ # @option options [ConversionOptions] :options ConversionOptions object
55
67
  def initialize(font_path, options = {})
56
68
  super(font_path, options)
57
69
 
@@ -63,6 +75,9 @@ module Fontisan
63
75
  # Parse target format
64
76
  @target_format = parse_target_format(opts[:to])
65
77
 
78
+ # Extract ConversionOptions if provided
79
+ @conv_options = extract_conversion_options(opts)
80
+
66
81
  # Parse coordinates if string provided
67
82
  @coordinates = if opts[:coordinates]
68
83
  parse_coordinates(opts[:coordinates])
@@ -123,6 +138,9 @@ module Fontisan
123
138
  verbose: @options[:verbose],
124
139
  }
125
140
 
141
+ # Add ConversionOptions if available
142
+ pipeline_options[:conversion_options] = @conv_options if @conv_options
143
+
126
144
  # Add variation options if specified
127
145
  pipeline_options[:coordinates] = @coordinates if @coordinates
128
146
  pipeline_options[:instance_index] = @instance_index if @instance_index
@@ -196,6 +214,7 @@ module Fontisan
196
214
  output: @output_path,
197
215
  target_format: @collection_target_format,
198
216
  verbose: @options[:verbose],
217
+ options: @conv_options, # Pass ConversionOptions
199
218
  },
200
219
  )
201
220
 
@@ -309,10 +328,12 @@ module Fontisan
309
328
  :woff
310
329
  when "woff2"
311
330
  :woff2
331
+ when "type1", "type-1", "t1", "pfb", "pfa"
332
+ :type1
312
333
  else
313
334
  raise ArgumentError,
314
335
  "Unknown target format: #{format}. " \
315
- "Supported: ttf, otf, ttc, otc, dfont, svg, woff, woff2"
336
+ "Supported: ttf, otf, type1, t1, ttc, otc, dfont, svg, woff, woff2"
316
337
  end
317
338
  end
318
339
 
@@ -329,6 +350,16 @@ module Fontisan
329
350
  "#{(bytes / (1024.0 * 1024)).round(1)} MB"
330
351
  end
331
352
  end
353
+
354
+ # Extract ConversionOptions from options hash
355
+ #
356
+ # @param options [Hash, ConversionOptions] Options or hash containing :options key
357
+ # @return [ConversionOptions, nil] Extracted ConversionOptions or nil
358
+ def extract_conversion_options(options)
359
+ return options if options.is_a?(ConversionOptions)
360
+
361
+ options[:options] if options.is_a?(Hash)
362
+ end
332
363
  end
333
364
  end
334
365
  end
@@ -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
@@ -6,9 +6,13 @@
6
6
  # Format identifiers:
7
7
  # - ttf: TrueType Font (with glyf/loca tables)
8
8
  # - otf: OpenType Font with CFF outlines (with CFF table)
9
- # - woff: Web Open Font Format (not yet implemented)
10
- # - woff2: Web Open Font Format 2 (implemented in Phase 2)
11
- # - svg: SVG Font (implemented in Phase 2)
9
+ # - type1: Adobe Type 1 Font (PFB/PFA format)
10
+ # - woff: Web Open Font Format (zlib compression)
11
+ # - woff2: Web Open Font Format 2 (Brotli compression)
12
+ # - svg: SVG Font (deprecated, for conversion/inspection)
13
+ # - ttc: TrueType Collection
14
+ # - otc: OpenType Collection
15
+ # - dfont: Apple dfont suitcase
12
16
  #
13
17
  # Phase 1 (Milestone 1.3): Basic conversions
14
18
  # - Same format (copy/optimize): ttf→ttf, otf→otf
@@ -20,8 +24,11 @@
20
24
  # Phase 2 (Milestone 2.2): SVG font generation
21
25
  # - SVG export: ttf to svg, otf to svg
22
26
  #
27
+ # Phase 3: Type 1 font support
28
+ # - Type 1 conversions: type1↔otf, type1↔ttf
29
+ #
23
30
  # Future phases:
24
- # - Phase 3: Variable font conversions
31
+ # - Phase 4: Variable font conversions
25
32
 
26
33
  conversions:
27
34
  # Same-format conversions (copy/optimize)
@@ -58,6 +65,47 @@ conversions:
58
65
  table generation including cubic-to-quadratic curve conversion and
59
66
  proper TrueType glyph structure encoding.
60
67
 
68
+ # Phase 3 conversions - Type 1 fonts
69
+ - from: type1
70
+ to: otf
71
+ strategy: type1_converter
72
+ description: "Convert Type 1 to OpenType/CFF format"
73
+ status: foundation
74
+ notes: >
75
+ Type 1 to OpenType conversion using CharStringConverter. Converts Type 1
76
+ CharStrings to CFF format and builds CFF table with proper DICT structures.
77
+ seac composites are expanded. Some hinting may not be preserved.
78
+
79
+ - from: otf
80
+ to: type1
81
+ strategy: type1_converter
82
+ description: "Convert OpenType/CFF to Type 1 format"
83
+ status: foundation
84
+ notes: >
85
+ OpenType to Type 1 conversion. Reverse conversion from CFF CharStrings to
86
+ Type 1 format. Builds PFB output with eexec encryption. Some modern
87
+ OpenType features may be lost in conversion.
88
+
89
+ - from: type1
90
+ to: ttf
91
+ strategy: type1_converter
92
+ description: "Convert Type 1 to TrueType format"
93
+ status: foundation
94
+ notes: >
95
+ Two-step conversion: Type 1 → OTF → TTF. First converts to OpenType/CFF,
96
+ then to TrueType with cubic-to-quadratic curve approximation. Hinting may
97
+ not be preserved in either step.
98
+
99
+ - from: ttf
100
+ to: type1
101
+ strategy: type1_converter
102
+ description: "Convert TrueType to Type 1 format"
103
+ status: foundation
104
+ notes: >
105
+ Two-step conversion: TTF → OTF → Type 1. First converts to OpenType/CFF
106
+ with quadratic-to-cubic curve conversion, then to Type 1. Significant
107
+ approximation artifacts may occur due to curve conversions.
108
+
61
109
  # Phase 2 conversions (Milestone 2.1) - WOFF2
62
110
  - from: ttf
63
111
  to: woff2
@@ -147,6 +195,35 @@ conversions:
147
195
  transformation. Note: SVG fonts are deprecated in favor of WOFF2, but
148
196
  useful for fallback, conversion, and inspection purposes.
149
197
 
198
+ # Type 1 to web formats (via OTF intermediate)
199
+ - from: type1
200
+ to: woff
201
+ strategy: type1_converter
202
+ description: "Convert Type 1 to WOFF format"
203
+ status: foundation
204
+ notes: >
205
+ Two-step conversion: Type 1 → OTF → WOFF. Converts Type 1 to OpenType/CFF,
206
+ then compresses with zlib. Useful for web delivery of legacy Type 1 fonts.
207
+
208
+ - from: type1
209
+ to: woff2
210
+ strategy: type1_converter
211
+ description: "Convert Type 1 to WOFF2 format"
212
+ status: foundation
213
+ notes: >
214
+ Two-step conversion: Type 1 → OTF → WOFF2. Converts Type 1 to OpenType/CFF,
215
+ then compresses with Brotli. Best for web delivery of legacy Type 1 fonts.
216
+
217
+ - from: type1
218
+ to: svg
219
+ strategy: type1_converter
220
+ description: "Generate SVG font from Type 1"
221
+ status: foundation
222
+ notes: >
223
+ Type 1 to SVG conversion. Extracts outlines from Type 1 CharStrings and
224
+ generates SVG font format. Note: SVG fonts are deprecated but useful for
225
+ inspection and conversion workflows.
226
+
150
227
  # WOFF2 decompression (reverse conversions)
151
228
  - from: woff2
152
229
  to: ttf
@@ -296,6 +373,57 @@ compatibility:
296
373
  - hinting_loss: >
297
374
  CFF hints are not converted to TrueType hinting instructions.
298
375
 
376
+ type1_to_otf:
377
+ preserves:
378
+ - glyph_outlines: Type 1 CharStrings are similar to CFF
379
+ - font_metrics: FontBBox, metrics preserved
380
+ - name_table: FontInfo converted to name table
381
+ - hints: Some hints preserved (BlueValues, OtherBlues)
382
+ limitations:
383
+ - seac_expansion: >
384
+ seac composite glyphs are expanded to base glyphs.
385
+ - hinting_approximation: >
386
+ Some Type 1 hints may not translate exactly to CFF hints.
387
+ - lost_features: >
388
+ Type 1-specific features (Flex, multiple master) not preserved.
389
+
390
+ otf_to_type1:
391
+ preserves:
392
+ - glyph_outlines: CFF outlines converted to Type 1
393
+ - font_metrics: Basic metrics preserved
394
+ - name_table: name table converted to FontInfo
395
+ limitations:
396
+ - reverse_conversion: >
397
+ CFF to Type 1 is reverse conversion with potential data loss.
398
+ - modern_features_lost: >
399
+ Modern OpenType features (GPOS, GSUB variations) lost in Type 1.
400
+ - hinting_approximation: >
401
+ CFF hints may not translate exactly to Type 1 hints.
402
+
403
+ type1_to_ttf:
404
+ preserves:
405
+ - glyph_outlines: Via OTF intermediate
406
+ - font_metrics: Basic metrics preserved
407
+ limitations:
408
+ - multi_step_conversion: >
409
+ Type 1 → OTF → TTF, compounding approximation errors.
410
+ - curve_approximation: >
411
+ CFF cubic to TrueType quadratic approximation.
412
+ - significant_data_loss: >
413
+ Most Type 1 and OpenType features lost.
414
+
415
+ ttf_to_type1:
416
+ preserves:
417
+ - glyph_outlines: Via OTF intermediate
418
+ - font_metrics: Basic metrics preserved
419
+ limitations:
420
+ - multi_step_conversion: >
421
+ TTF → OTF → Type 1, compounding approximation errors.
422
+ - curve_approximation: >
423
+ Quadratic to cubic, then back to Type 1 curves.
424
+ - extreme_data_loss: >
425
+ Nearly all TrueType and Type 1 features lost.
426
+
299
427
  same_format:
300
428
  preserves:
301
429
  - all_tables
@@ -95,6 +95,18 @@ module Fontisan
95
95
  # CFF2 table tag identifier (CFF version 2 with variations)
96
96
  CFF2_TAG = "CFF2"
97
97
 
98
+ # Adobe Type 1 font format constants
99
+ # PFB (Printer Font Binary) chunk markers
100
+ PFB_ASCII_CHUNK = 0x8001
101
+ PFB_BINARY_CHUNK = 0x8002
102
+
103
+ # PFA (Printer Font ASCII) file signatures
104
+ PFA_SIGNATURE_ADOBE_1_0 = "%!PS-AdobeFont-1.0"
105
+ PFA_SIGNATURE_ADOBE_3_0 = "%!PS-Adobe-3.0 Resource-Font"
106
+
107
+ # Type 1 CharString operators
108
+ TYPE1_SEAC_ESCAPE = 6 # seac operator is escape byte 12 + 6
109
+
98
110
  # TrueType hinting tables
99
111
  # Font Program table (TrueType bytecode executed once at font load)
100
112
  FPGM_TAG = "fpgm"