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
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Type1
5
+ # Type 1 Private Dictionary model
6
+ #
7
+ # [`PrivateDict`](lib/fontisan/type1/private_dict.rb) parses and stores
8
+ # the private dictionary from a Type 1 font, which contains hinting
9
+ # and spacing information used by the CharString interpreter.
10
+ #
11
+ # The private dictionary includes:
12
+ # - BlueValues and OtherBlues (alignment zones for optical consistency)
13
+ # - StdHW and StdVW (standard stem widths)
14
+ # - StemSnapH and StemSnapV (stem snap arrays)
15
+ # - Subrs (local subroutines)
16
+ # - lenIV (CharString encryption IV length)
17
+ #
18
+ # @example Parse private dictionary from decrypted font data
19
+ # priv = Fontisan::Type1::PrivateDict.parse(decrypted_data)
20
+ # puts priv.blue_values
21
+ # puts priv.std_hw
22
+ #
23
+ # @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
24
+ class PrivateDict
25
+ # @return [Array<Integer>] BlueValues alignment zones
26
+ attr_accessor :blue_values
27
+
28
+ # @return [Array<Integer>] OtherBlues alignment zones
29
+ attr_accessor :other_blues
30
+
31
+ # @return [Array<Integer>] FamilyBlues alignment zones
32
+ attr_accessor :family_blues
33
+
34
+ # @return [Array<Integer>] FamilyOtherBlues alignment zones
35
+ attr_accessor :family_other_blues
36
+
37
+ # @return [Float] BlueScale
38
+ attr_accessor :blue_scale
39
+
40
+ # @return [Integer] BlueShift
41
+ attr_accessor :blue_shift
42
+
43
+ # @return [Integer] BlueFuzz
44
+ attr_accessor :blue_fuzz
45
+
46
+ # @return [Array<Float>] StdHW (standard horizontal width)
47
+ attr_accessor :std_hw
48
+
49
+ # @return [Array<Float>] StdVW (standard vertical width)
50
+ attr_accessor :std_vw
51
+
52
+ # @return [Array<Float>] StemSnapH (horizontal stem snap array)
53
+ attr_accessor :stem_snap_h
54
+
55
+ # @return [Array<Float>] StemSnapV (vertical stem snap array)
56
+ attr_accessor :stem_snap_v
57
+
58
+ # @return [Boolean] ForceBold flag
59
+ attr_accessor :force_bold
60
+
61
+ # @return [Integer] lenIV (CharString encryption IV length)
62
+ attr_accessor :len_iv
63
+
64
+ # @return [Array<String>] Subrs (local subroutines)
65
+ attr_accessor :subrs
66
+
67
+ # @return [Integer] LanguageGroup (0 for Latin, 1 for Japanese, etc.)
68
+ attr_accessor :language_group
69
+
70
+ # @return [Float] ExpansionFactor for counter widening
71
+ attr_accessor :expansion_factor
72
+
73
+ # @return [Integer] InitialRandomSeed for randomization
74
+ attr_accessor :initial_random_seed
75
+
76
+ # @return [Hash] Raw dictionary data
77
+ attr_reader :raw_data
78
+
79
+ # Parse private dictionary from decrypted Type 1 font data
80
+ #
81
+ # @param data [String] Decrypted Type 1 font data
82
+ # @return [PrivateDict] Parsed private dictionary
83
+ # @raise [Fontisan::Error] If dictionary cannot be parsed
84
+ #
85
+ # @example Parse from decrypted font data
86
+ # priv = Fontisan::Type1::PrivateDict.parse(decrypted_data)
87
+ def self.parse(data)
88
+ new.parse(data)
89
+ end
90
+
91
+ # Initialize a new PrivateDict
92
+ def initialize
93
+ @blue_values = []
94
+ @other_blues = []
95
+ @family_blues = []
96
+ @family_other_blues = []
97
+ @blue_scale = 0.039625
98
+ @blue_shift = 7
99
+ @blue_fuzz = 1
100
+ @std_hw = []
101
+ @std_vw = []
102
+ @stem_snap_h = []
103
+ @stem_snap_v = []
104
+ @force_bold = false
105
+ @len_iv = 4
106
+ @subrs = []
107
+ @language_group = 0
108
+ @expansion_factor = 0.06
109
+ @initial_random_seed = 0
110
+ @raw_data = {}
111
+ @parsed = false
112
+ end
113
+
114
+ # Parse private dictionary from decrypted Type 1 font data
115
+ #
116
+ # @param data [String] Decrypted Type 1 font data
117
+ # @return [PrivateDict] Self for method chaining
118
+ def parse(data)
119
+ extract_private_dict(data)
120
+ extract_properties
121
+ @parsed = true
122
+ self
123
+ end
124
+
125
+ # Check if dictionary was successfully parsed
126
+ #
127
+ # @return [Boolean] True if dictionary has been parsed
128
+ def parsed?
129
+ @parsed
130
+ end
131
+
132
+ # Get raw value from dictionary
133
+ #
134
+ # @param key [String] Dictionary key
135
+ # @return [Object, nil] Value or nil if not found
136
+ def [](key)
137
+ @raw_data[key]
138
+ end
139
+
140
+ # Get effective BlueValues for hinting
141
+ #
142
+ # Returns BlueValues adjusted by BlueScale.
143
+ #
144
+ # @return [Array<Float>] Scaled blue values
145
+ def effective_blue_values
146
+ return [] if @blue_values.empty?
147
+
148
+ @blue_values.map { |v| v * @blue_scale }
149
+ end
150
+
151
+ # Check if font has blues
152
+ #
153
+ # @return [Boolean] True if BlueValues or OtherBlues are defined
154
+ def has_blues?
155
+ !@blue_values.empty? || !@other_blues.empty?
156
+ end
157
+
158
+ # Check if font has stem hints
159
+ #
160
+ # @return [Boolean] True if StdHW, StdVW, or StemSnap arrays are defined
161
+ def has_stem_hints?
162
+ !@std_hw.empty? || !@std_vw.empty? ||
163
+ !@stem_snap_h.empty? || !@stem_snap_v.empty?
164
+ end
165
+
166
+ # Convert PrivateDict to Type 1 text format
167
+ #
168
+ # Generates the PostScript code for the Private dictionary section
169
+ # of a Type 1 font.
170
+ #
171
+ # @return [String] Type 1 Private dictionary text
172
+ #
173
+ # @example Generate Type 1 format
174
+ # priv = PrivateDict.new
175
+ # priv.blue_values = [-10, 0, 470, 480]
176
+ # puts priv.to_type1_format
177
+ def to_type1_format
178
+ result = []
179
+ result << array_to_type1(:BlueValues, @blue_values) unless @blue_values.empty?
180
+ result << array_to_type1(:OtherBlues, @other_blues) unless @other_blues.empty?
181
+ result << array_to_type1(:FamilyBlues, @family_blues) unless @family_blues.empty?
182
+ result << array_to_type1(:FamilyOtherBlues, @family_other_blues) unless @family_other_blues.empty?
183
+ result << scalar_to_type1(:BlueScale, @blue_scale)
184
+ result << scalar_to_type1(:BlueShift, @blue_shift)
185
+ result << scalar_to_type1(:BlueFuzz, @blue_fuzz)
186
+ result << array_to_type1(:StdHW, @std_hw) unless @std_hw.empty?
187
+ result << array_to_type1(:StdVW, @std_vw) unless @std_vw.empty?
188
+ result << array_to_type1(:StemSnapH, @stem_snap_h) unless @stem_snap_h.empty?
189
+ result << array_to_type1(:StemSnapV, @stem_snap_v) unless @stem_snap_v.empty?
190
+ result << boolean_to_type1(:ForceBold, @force_bold) unless @force_bold == false
191
+ result << scalar_to_type1(:lenIV, @len_iv)
192
+
193
+ result.join("\n")
194
+ end
195
+
196
+ # Format an array value for Type 1 output
197
+ #
198
+ # @param name [Symbol] Array name
199
+ # @param value [Array] Array value
200
+ # @return [String] Formatted Type 1 array definition
201
+ def array_to_type1(name, value)
202
+ "/#{name} [#{value.join(' ')}] def"
203
+ end
204
+
205
+ # Format a scalar value for Type 1 output
206
+ #
207
+ # @param name [Symbol] Value name
208
+ # @param value [Numeric] Numeric value
209
+ # @return [String] Formatted Type 1 scalar definition
210
+ def scalar_to_type1(name, value)
211
+ "/#{name} #{value} def"
212
+ end
213
+
214
+ # Format a boolean value for Type 1 output
215
+ #
216
+ # @param name [Symbol] Value name
217
+ # @param value [Boolean] Boolean value
218
+ # @return [String] Formatted Type 1 boolean definition
219
+ def boolean_to_type1(name, value)
220
+ "/#{name} #{value} def"
221
+ end
222
+
223
+ private
224
+
225
+ # Extract private dictionary from data
226
+ #
227
+ # @param data [String] Decrypted Type 1 font data
228
+ def extract_private_dict(data)
229
+ # Find the Private dictionary definition
230
+ # Type 1 fonts have: /Private <dict_size> dict def ... end
231
+
232
+ # Look for /Private dict def pattern - use safer pattern
233
+ # Match until we find the matching 'end' keyword
234
+ private_match = data.match(%r{/Private\s+\d+\s+dict\s+def\b(.*)end}m)
235
+ return if private_match.nil?
236
+
237
+ private_text = private_match[1]
238
+ @raw_data = parse_private_dict_text(private_text)
239
+ end
240
+
241
+ # Parse private dictionary text
242
+ #
243
+ # @param text [String] Private dictionary text
244
+ # @return [Hash] Parsed key-value pairs
245
+ def parse_private_dict_text(text)
246
+ result = {}
247
+
248
+ # Parse BlueValues array
249
+ if (match = text.match(/\/BlueValues\s*\[([^\]]+)\]\s+def/m))
250
+ result[:blue_values] = parse_array(match[1])
251
+ end
252
+
253
+ # Parse OtherBlues array
254
+ if (match = text.match(/\/OtherBlues\s*\[([^\]]+)\]\s+def/m))
255
+ result[:other_blues] = parse_array(match[1])
256
+ end
257
+
258
+ # Parse FamilyBlues array
259
+ if (match = text.match(/\/FamilyBlues\s*\[([^\]]+)\]\s+def/m))
260
+ result[:family_blues] = parse_array(match[1])
261
+ end
262
+
263
+ # Parse FamilyOtherBlues array
264
+ if (match = text.match(/\/FamilyOtherBlues\s*\[([^\]]+)\]\s+def/m))
265
+ result[:family_other_blues] = parse_array(match[1])
266
+ end
267
+
268
+ # Parse BlueScale
269
+ if (match = text.match(/\/BlueScale\s+([0-9.-]+)\s+def/m))
270
+ result[:blue_scale] = match[1].to_f
271
+ end
272
+
273
+ # Parse BlueShift
274
+ if (match = text.match(/\/BlueShift\s+(\d+)\s+def/m))
275
+ result[:blue_shift] = match[1].to_i
276
+ end
277
+
278
+ # Parse BlueFuzz
279
+ if (match = text.match(/\/BlueFuzz\s+(\d+)\s+def/m))
280
+ result[:blue_fuzz] = match[1].to_i
281
+ end
282
+
283
+ # Parse StdHW array
284
+ if (match = text.match(/\/StdHW\s*\[([^\]]+)\]\s+def/m))
285
+ result[:std_hw] = parse_array(match[1]).map(&:to_f)
286
+ end
287
+
288
+ # Parse StdVW array
289
+ if (match = text.match(/\/StdVW\s*\[([^\]]+)\]\s+def/m))
290
+ result[:std_vw] = parse_array(match[1]).map(&:to_f)
291
+ end
292
+
293
+ # Parse StemSnapH array
294
+ if (match = text.match(/\/StemSnapH\s*\[([^\]]+)\]\s+def/m))
295
+ result[:stem_snap_h] = parse_array(match[1]).map(&:to_f)
296
+ end
297
+
298
+ # Parse StemSnapV array
299
+ if (match = text.match(/\/StemSnapV\s*\[([^\]]+)\]\s+def/m))
300
+ result[:stem_snap_v] = parse_array(match[1]).map(&:to_f)
301
+ end
302
+
303
+ # Parse ForceBold
304
+ if (match = text.match(/\/ForceBold\s+(true|false)\s+def/m))
305
+ result[:force_bold] = match[1] == "true"
306
+ end
307
+
308
+ # Parse lenIV
309
+ if (match = text.match(/\/lenIV\s+(\d+)\s+def/m))
310
+ result[:len_iv] = match[1].to_i
311
+ end
312
+
313
+ result
314
+ end
315
+
316
+ # Parse array from string
317
+ #
318
+ # @param str [String] Array string (e.g., "1 2 3 4")
319
+ # @return [Array<Integer>] Parsed integers
320
+ def parse_array(str)
321
+ str.strip.split.map(&:strip).reject(&:empty?).map(&:to_i)
322
+ end
323
+
324
+ # Extract properties from raw data
325
+ def extract_properties
326
+ @blue_values = @raw_data[:blue_values] || []
327
+ @other_blues = @raw_data[:other_blues] || []
328
+ @family_blues = @raw_data[:family_blues] || []
329
+ @family_other_blues = @raw_data[:family_other_blues] || []
330
+ @blue_scale = @raw_data[:blue_scale] || 0.039625
331
+ @blue_shift = @raw_data[:blue_shift] || 7
332
+ @blue_fuzz = @raw_data[:blue_fuzz] || 1
333
+ @std_hw = @raw_data[:std_hw] || []
334
+ @std_vw = @raw_data[:std_vw] || []
335
+ @stem_snap_h = @raw_data[:stem_snap_h] || []
336
+ @stem_snap_v = @raw_data[:stem_snap_v] || []
337
+ @force_bold = @raw_data[:force_bold] || false
338
+ @len_iv = @raw_data[:len_iv] || 4
339
+ end
340
+ end
341
+ end
342
+ end