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,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
|