fontisan 0.2.11 → 0.2.12

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +214 -51
  3. data/README.adoc +160 -0
  4. data/lib/fontisan/cli.rb +177 -6
  5. data/lib/fontisan/commands/convert_command.rb +32 -1
  6. data/lib/fontisan/config/conversion_matrix.yml +132 -4
  7. data/lib/fontisan/constants.rb +12 -0
  8. data/lib/fontisan/conversion_options.rb +378 -0
  9. data/lib/fontisan/converters/collection_converter.rb +45 -10
  10. data/lib/fontisan/converters/format_converter.rb +2 -0
  11. data/lib/fontisan/converters/outline_converter.rb +78 -4
  12. data/lib/fontisan/converters/type1_converter.rb +559 -0
  13. data/lib/fontisan/font_loader.rb +46 -3
  14. data/lib/fontisan/type1/afm_generator.rb +436 -0
  15. data/lib/fontisan/type1/afm_parser.rb +298 -0
  16. data/lib/fontisan/type1/agl.rb +456 -0
  17. data/lib/fontisan/type1/charstring_converter.rb +240 -0
  18. data/lib/fontisan/type1/charstrings.rb +408 -0
  19. data/lib/fontisan/type1/conversion_options.rb +243 -0
  20. data/lib/fontisan/type1/decryptor.rb +183 -0
  21. data/lib/fontisan/type1/encodings.rb +697 -0
  22. data/lib/fontisan/type1/font_dictionary.rb +514 -0
  23. data/lib/fontisan/type1/generator.rb +220 -0
  24. data/lib/fontisan/type1/inf_generator.rb +332 -0
  25. data/lib/fontisan/type1/pfa_generator.rb +343 -0
  26. data/lib/fontisan/type1/pfa_parser.rb +158 -0
  27. data/lib/fontisan/type1/pfb_generator.rb +291 -0
  28. data/lib/fontisan/type1/pfb_parser.rb +166 -0
  29. data/lib/fontisan/type1/pfm_generator.rb +610 -0
  30. data/lib/fontisan/type1/pfm_parser.rb +433 -0
  31. data/lib/fontisan/type1/private_dict.rb +285 -0
  32. data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
  33. data/lib/fontisan/type1/upm_scaler.rb +118 -0
  34. data/lib/fontisan/type1.rb +73 -0
  35. data/lib/fontisan/type1_font.rb +331 -0
  36. data/lib/fontisan/version.rb +1 -1
  37. data/lib/fontisan.rb +2 -0
  38. metadata +26 -2
@@ -0,0 +1,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Type1
5
+ # TTF to Type 1 CharString Converter
6
+ #
7
+ # [`TTFToType1Converter`](lib/fontisan/type1/ttf_to_type1_converter.rb) converts
8
+ # TrueType glyphs to Type 1 CharStrings.
9
+ #
10
+ # The conversion involves:
11
+ # - Converting quadratic curves (TrueType) to cubic curves (Type 1)
12
+ # - Scaling coordinates if needed
13
+ # - Generating Type 1 CharString commands
14
+ #
15
+ # @example Convert TTF font to Type 1 CharStrings
16
+ # scaler = Fontisan::Type1::UPMScaler.type1_standard(font)
17
+ # converter = Fontisan::Type1::TTFToType1Converter.new(font, scaler, encoding)
18
+ # charstrings = converter.convert
19
+ #
20
+ # @see https://www.adobe.com/devnet/font/pdfs/5178.Type1.pdf
21
+ class TTFToType1Converter
22
+ # Type 1 CharString command codes
23
+ HSTEM = 1
24
+ VSTEM = 3
25
+ VMOVETO = 4
26
+ RLINETO = 5
27
+ HLINETO = 6
28
+ VLINETO = 7
29
+ RRCURVETO = 8
30
+ CLOSEPATH = 9
31
+ CALLSUBR = 10
32
+ RETURN = 11
33
+ ESCAPE = 12
34
+ HSBW = 13
35
+ ENDCHAR = 14
36
+ RMOVETO = 21
37
+ HMOVETO = 22
38
+ VHCURVETO = 30
39
+ HVCURVETO = 31
40
+
41
+ # Convert TTF font to Type 1 CharStrings
42
+ #
43
+ # @param font [Fontisan::Font] Source TTF font
44
+ # @param scaler [UPMScaler] UPM scaler
45
+ # @param encoding [Class] Encoding class
46
+ # @return [Hash<Integer, String>] Glyph ID to CharString mapping
47
+ def self.convert(font, scaler, encoding)
48
+ new(font, scaler, encoding).convert
49
+ end
50
+
51
+ def initialize(font, scaler, encoding)
52
+ @font = font
53
+ @scaler = scaler
54
+ @encoding = encoding
55
+ @charstrings = {}
56
+ end
57
+
58
+ # Convert all glyphs to CharStrings
59
+ #
60
+ # @return [Hash<Integer, String>] Glyph ID to CharString mapping
61
+ def convert
62
+ glyf_table = @font.table(Constants::GLYF_TAG)
63
+ return {} unless glyf_table
64
+
65
+ loca_table = @font.table(Constants::LOCA_TAG)
66
+ head_table = @font.table(Constants::HEAD_TAG)
67
+
68
+ maxp = @font.table(Constants::MAXP_TAG)
69
+ num_glyphs = maxp&.num_glyphs || 0
70
+
71
+ num_glyphs.times do |gid|
72
+ @charstrings[gid] =
73
+ convert_glyph(glyf_table, loca_table, head_table, gid)
74
+ end
75
+
76
+ @charstrings
77
+ end
78
+
79
+ private
80
+
81
+ # Convert a single glyph to CharString
82
+ #
83
+ # @param glyf_table [Tables::Glyf] TTF glyf table
84
+ # @param loca_table [Tables::Loca] TTF loca table
85
+ # @param head_table [Tables::Head] TTF head table
86
+ # @param gid [Integer] Glyph ID
87
+ # @return [String] Type 1 CharString data
88
+ def convert_glyph(glyf_table, loca_table, head_table, gid)
89
+ glyph = glyf_table.glyph_for(gid, loca_table, head_table)
90
+
91
+ # Handle empty glyph
92
+ return empty_charstring if glyph.nil?
93
+
94
+ # Handle compound glyphs
95
+ if glyph.compound?
96
+ return convert_composite_glyph(glyph, glyf_table)
97
+ end
98
+
99
+ # Convert simple glyph
100
+ convert_simple_glyph(glyph)
101
+ end
102
+
103
+ # Convert a simple glyph to CharString
104
+ #
105
+ # @param glyph [Object] TTF simple glyph
106
+ # @return [String] Type 1 CharString data
107
+ def convert_simple_glyph(glyph)
108
+ commands = []
109
+ points = extract_points(glyph)
110
+
111
+ return empty_charstring if points.empty?
112
+
113
+ # Start with hsbw (horizontal side bearing and width)
114
+ lsb = @scaler.scale(glyph.left_side_bearing || 0)
115
+ width = @scaler.scale(glyph.advance_width || 500)
116
+ commands << [HSBW, lsb, width]
117
+
118
+ # Convert contours to Type 1 commands
119
+ contour_commands = convert_contours(points)
120
+ commands.concat(contour_commands)
121
+
122
+ # End character
123
+ commands << [ENDCHAR]
124
+
125
+ # Encode to CharString format
126
+ encode_charstring(commands)
127
+ end
128
+
129
+ # Extract points from a simple glyph
130
+ #
131
+ # @param glyph [Object] TTF simple glyph
132
+ # @return [Array<Hash>] Array of points with on_curve flag
133
+ def extract_points(glyph)
134
+ return [] unless glyph.respond_to?(:points)
135
+
136
+ points = []
137
+ glyph.points.each do |point|
138
+ points << {
139
+ x: @scaler.scale(point.x),
140
+ y: @scaler.scale(point.y),
141
+ on_curve: point.on_curve?,
142
+ }
143
+ end
144
+
145
+ points
146
+ end
147
+
148
+ # Convert contours to Type 1 commands
149
+ #
150
+ # @param points [Array<Hash>] Array of points
151
+ # @return [Array<Array<Integer>>] Array of command arrays
152
+ def convert_contours(points)
153
+ commands = []
154
+ return commands if points.empty?
155
+
156
+ # Start at first point
157
+ start_point = points[0]
158
+ current_point = { x: start_point[:x], y: start_point[:y] }
159
+
160
+ # Process remaining points in runs
161
+ i = 1
162
+ while i < points.length
163
+ point = points[i]
164
+
165
+ if point[:on_curve]
166
+ # On-curve point - draw line or curve from previous
167
+ if i.positive? && !points[i - 1][:on_curve]
168
+ # Previous was off-curve, this is end of quadratic curve
169
+ prev_point = points[i - 1]
170
+ curve_commands = convert_quadratic_to_cubic(
171
+ current_point,
172
+ prev_point,
173
+ point,
174
+ )
175
+ commands.concat(curve_commands)
176
+ else
177
+ # Line to this point
178
+ dx = point[:x] - current_point[:x]
179
+ dy = point[:y] - current_point[:y]
180
+ commands << [RLINETO, dx, dy]
181
+ end
182
+ current_point = { x: point[:x], y: point[:y] }
183
+ elsif i + 1 < points.length && !points[i + 1][:on_curve]
184
+ # Off-curve control point
185
+ # Check if next point is also off-curve (implicit on-curve midpoint)
186
+ next_point = points[i + 1]
187
+ implicit_on = {
188
+ x: ((point[:x] + next_point[:x]).to_f / 2).round,
189
+ y: ((point[:y] + next_point[:y]).to_f / 2).round,
190
+ }
191
+
192
+ curve_commands = convert_quadratic_to_cubic(
193
+ current_point,
194
+ point,
195
+ implicit_on,
196
+ )
197
+ commands.concat(curve_commands)
198
+ current_point = implicit_on
199
+ # Both are off-curve, implicit on-curve at midpoint
200
+ # If next point is on-curve, we'll handle it in next iteration
201
+ end
202
+
203
+ i += 1
204
+ end
205
+
206
+ # Close contour if needed (implicit close path for Type 1)
207
+ # Type 1 implicitly closes paths, so we don't need explicit close
208
+
209
+ commands
210
+ end
211
+
212
+ # Convert quadratic Bézier curve to cubic Bézier curve
213
+ #
214
+ # TrueType uses quadratic curves with one control point:
215
+ # P0 (on) -> P1 (off) -> P2 (on)
216
+ #
217
+ # Type 1 uses cubic curves with two control points:
218
+ # P0 (on) -> C1 (off) -> C2 (off) -> P2 (on)
219
+ #
220
+ # Conversion formula:
221
+ # C1 = P0 + (2/3)(P1 - P0) = (1/3)P0 + (2/3)P1
222
+ # C2 = P2 + (2/3)(P1 - P2) = (2/3)P1 + (1/3)P2
223
+ #
224
+ # @param p0 [Hash] Start point {x, y}
225
+ # @param p1 [Hash] Control point {x, y}
226
+ # @param p2 [Hash] End point {x, y}
227
+ # @return [Array<Array<Integer>>] Array of command arrays
228
+ def convert_quadratic_to_cubic(p0, p1, p2)
229
+ # Calculate cubic control points
230
+ c1_x = p0[:x] + ((2 * (p1[:x] - p0[:x])).to_f / 3).round
231
+ c1_y = p0[:y] + ((2 * (p1[:y] - p0[:y])).to_f / 3).round
232
+
233
+ c2_x = p2[:x] + ((2 * (p1[:x] - p2[:x])).to_f / 3).round
234
+ c2_y = p2[:y] + ((2 * (p1[:y] - p2[:y])).to_f / 3).round
235
+
236
+ # Calculate deltas
237
+ dc1x = c1_x - p0[:x]
238
+ dc1y = c1_y - p0[:y]
239
+ dc2x = c2_x - c1_x
240
+ dc2y = c2_y - c1_y
241
+ dx = p2[:x] - c2_x
242
+ dy = p2[:y] - c2_y
243
+
244
+ [
245
+ [RRCURVETO, dc1x, dc1y, dc2x, dc2y, dx, dy],
246
+ ]
247
+ end
248
+
249
+ # Convert a composite glyph to CharString
250
+ #
251
+ # @param glyph [Object] TTF composite glyph
252
+ # @param glyf_table [Object] TTF glyf table
253
+ # @return [String] Type 1 CharString data
254
+ def convert_composite_glyph(_glyph, _glyf_table)
255
+ # For composite glyphs, we need to decompose or use seac
256
+ # TODO: Implement proper composite handling with seac or decomposition
257
+
258
+ # For now, return a simple placeholder
259
+ # In a full implementation, we would:
260
+ # 1. Extract component glyphs
261
+ # 2. Transform and merge their outlines
262
+ # 3. Generate combined CharString
263
+
264
+ empty_charstring
265
+ end
266
+
267
+ # Encode commands to Type 1 CharString binary format
268
+ #
269
+ # @param commands [Array<Array<Integer>>] Array of command arrays
270
+ # @return [String] Binary CharString data
271
+ def encode_charstring(commands)
272
+ bytes = []
273
+
274
+ commands.each do |cmd|
275
+ cmd.each do |value|
276
+ if value.is_a?(Integer)
277
+ bytes.concat(encode_number(value))
278
+ end
279
+ end
280
+ end
281
+
282
+ bytes.pack("C*")
283
+ end
284
+
285
+ # Encode a number for CharString
286
+ #
287
+ # Type 1 CharStrings use a variable-length encoding for integers
288
+ #
289
+ # @param value [Integer] Number to encode
290
+ # @return [Array<Integer>] Array of bytes
291
+ def encode_number(value)
292
+ if value >= -107 && value <= 107
293
+ # Single byte encoding: value + 139
294
+ [value + 139]
295
+ elsif value >= 108 && value <= 1131
296
+ # Two byte encoding
297
+ byte1 = ((value - 108) >> 8) + 247
298
+ byte2 = (value - 108) & 0xFF
299
+ [byte1, byte2]
300
+ elsif value >= -1131 && value <= -108
301
+ # Two byte encoding for negative
302
+ byte1 = ((-value - 108) >> 8) + 251
303
+ byte2 = (-value - 108) & 0xFF
304
+ [byte1, byte2]
305
+ elsif value >= -32768 && value <= 32767
306
+ # Three byte encoding (16-bit signed)
307
+ [255, value & 0xFF, (value >> 8) & 0xFF]
308
+ else
309
+ # Four byte encoding (32-bit)
310
+ bytes = []
311
+ 4.times do |i|
312
+ bytes << ((value >> (8 * i)) & 0xFF)
313
+ end
314
+ [255] + bytes
315
+ end
316
+ end
317
+
318
+ # Generate empty CharString
319
+ #
320
+ # @return [String] Empty CharString data
321
+ def empty_charstring
322
+ # hsbw with width 0, then endchar
323
+ [0, 500, ENDCHAR].pack("C*")
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Type1
5
+ # UPM (Units Per Em) Scaler
6
+ #
7
+ # [`UPMScaler`](lib/fontisan/type1/upm_scaler.rb) handles scaling of font metrics
8
+ # from the source font's UPM to a target UPM.
9
+ #
10
+ # Traditional Type 1 fonts use 1000 UPM, while modern TTF fonts typically use
11
+ # 2048 or other values. This scaler converts metrics appropriately.
12
+ #
13
+ # @example Scale to Type 1 standard
14
+ # scaler = Fontisan::Type1::UPMScaler.type1_standard(font)
15
+ # scaled_width = scaler.scale(1024) # => 500 for 2048 UPM font
16
+ #
17
+ # @example Keep native UPM
18
+ # scaler = Fontisan::Type1::UPMScaler.native(font)
19
+ # scaled_width = scaler.scale(1024) # => 1024 (no scaling)
20
+ class UPMScaler
21
+ # @return [Integer] Source font's units per em
22
+ attr_reader :source_upm
23
+
24
+ # @return [Integer] Target units per em
25
+ attr_reader :target_upm
26
+
27
+ # @return [Rational] Scale factor (target / source)
28
+ attr_reader :scale_factor
29
+
30
+ # Initialize a new UPM scaler
31
+ #
32
+ # @param font [Fontisan::Font] Source font
33
+ # @param target_upm [Integer] Target UPM (default: 1000 for Type 1)
34
+ def initialize(font, target_upm: 1000)
35
+ @font = font
36
+ @source_upm = font.units_per_em
37
+ @target_upm = target_upm
38
+ @scale_factor = Rational(target_upm, source_upm)
39
+ end
40
+
41
+ # Scale a single value
42
+ #
43
+ # @param value [Integer, Float] Value to scale
44
+ # @return [Integer] Scaled value (rounded)
45
+ def scale(value)
46
+ return 0 if value.nil? || value.zero?
47
+
48
+ (value * scale_factor).round
49
+ end
50
+
51
+ # Scale an array of values
52
+ #
53
+ # @param values [Array<Integer, Float>] Values to scale
54
+ # @return [Array<Integer>] Scaled values
55
+ def scale_array(values)
56
+ values.map { |v| scale(v) }
57
+ end
58
+
59
+ # Scale a coordinate pair [x, y]
60
+ #
61
+ # @param value [Array<Integer, Float>] Coordinate pair
62
+ # @return [Array<Integer>] Scaled coordinates
63
+ def scale_pair(value)
64
+ [scale(value[0]), scale(value[1])]
65
+ end
66
+
67
+ # Scale a bounding box [llx, lly, urx, ury]
68
+ #
69
+ # @param bbox [Array<Integer, Float>] Bounding box
70
+ # @return [Array<Integer>] Scaled bounding box
71
+ def scale_bbox(bbox)
72
+ return nil if bbox.nil?
73
+
74
+ bbox.map { |v| scale(v) }
75
+ end
76
+
77
+ # Scale a character width
78
+ #
79
+ # @param width [Integer, Float] Character width
80
+ # @return [Integer] Scaled width
81
+ def scale_width(width)
82
+ scale(width)
83
+ end
84
+
85
+ # Check if scaling is needed
86
+ #
87
+ # @return [Boolean] True if source and target UPM differ
88
+ def scaling_needed?
89
+ @source_upm != @target_upm
90
+ end
91
+
92
+ # Create scaler with native UPM (no scaling)
93
+ #
94
+ # @param font [Fontisan::Font] Source font
95
+ # @return [UPMScaler] Scaler with native UPM
96
+ def self.native(font)
97
+ new(font, target_upm: font.units_per_em)
98
+ end
99
+
100
+ # Create scaler with Type 1 standard UPM (1000)
101
+ #
102
+ # @param font [Fontisan::Font] Source font
103
+ # @return [UPMScaler] Scaler with 1000 UPM
104
+ def self.type1_standard(font)
105
+ new(font, target_upm: 1000)
106
+ end
107
+
108
+ # Create scaler with custom UPM
109
+ #
110
+ # @param font [Fontisan::Font] Source font
111
+ # @param upm [Integer] Target UPM
112
+ # @return [UPMScaler] Scaler with custom UPM
113
+ def self.custom(font, upm:)
114
+ new(font, target_upm: upm)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ # Adobe Type 1 Font support
5
+ #
6
+ # [`Type1`](lib/fontisan/type1.rb) provides parsing and conversion
7
+ # capabilities for Adobe Type 1 fonts in PFB (Printer Font Binary)
8
+ # and PFA (Printer Font ASCII) formats.
9
+ #
10
+ # Type 1 fonts were the standard for digital typography in the 1980s-1990s
11
+ # and are still encountered in legacy systems and design workflows.
12
+ #
13
+ # Key features:
14
+ # - PFB and PFA format parsing
15
+ # - eexec decryption for encrypted font portions
16
+ # - CharString decryption with lenIV handling
17
+ # - Font dictionary parsing (FontInfo, Private dict)
18
+ # - Conversion from TTF/OTF to Type 1 formats
19
+ # - UPM scaling for Type 1 compatibility (1000 UPM)
20
+ # - Multiple encoding support (AdobeStandard, ISOLatin1, Unicode)
21
+ #
22
+ # @example Generate Type 1 formats from TTF
23
+ # font = Fontisan::FontLoader.load("font.ttf")
24
+ # result = Fontisan::Type1::Generator.generate(font)
25
+ # result[:afm] # => AFM file content
26
+ # result[:pfm] # => PFM file content
27
+ # result[:pfb] # => PFB file content
28
+ # result[:inf] # => INF file content
29
+ #
30
+ # @example Generate with specific options
31
+ # options = Fontisan::Type1::ConversionOptions.windows_type1
32
+ # result = Fontisan::Type1::Generator.generate(font, options)
33
+ #
34
+ # @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
35
+ module Type1
36
+ end
37
+ end
38
+
39
+ # Parsers
40
+ require_relative "type1/pfb_parser"
41
+ require_relative "type1/pfa_parser"
42
+
43
+ # Core components
44
+ require_relative "type1/decryptor"
45
+ require_relative "type1/font_dictionary"
46
+ require_relative "type1/private_dict"
47
+ require_relative "type1/charstrings"
48
+ require_relative "type1/charstring_converter"
49
+
50
+ # Infrastructure
51
+ require_relative "type1/upm_scaler"
52
+ require_relative "type1/agl"
53
+ require_relative "type1/encodings"
54
+ require_relative "type1/conversion_options"
55
+
56
+ # TTF to Type 1 conversion
57
+ require_relative "type1/ttf_to_type1_converter"
58
+
59
+ # Metrics parsers
60
+ require_relative "type1/afm_parser"
61
+ require_relative "type1/pfm_parser"
62
+
63
+ # Metrics generators
64
+ require_relative "type1/afm_generator"
65
+ require_relative "type1/pfm_generator"
66
+
67
+ # Type 1 font generators
68
+ require_relative "type1/pfa_generator"
69
+ require_relative "type1/pfb_generator"
70
+ require_relative "type1/inf_generator"
71
+
72
+ # Unified generator interface
73
+ require_relative "type1/generator"