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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +214 -51
- data/README.adoc +160 -0
- data/lib/fontisan/cli.rb +177 -6
- data/lib/fontisan/commands/convert_command.rb +32 -1
- 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 +2 -0
- data/lib/fontisan/converters/outline_converter.rb +78 -4
- data/lib/fontisan/converters/type1_converter.rb +559 -0
- data/lib/fontisan/font_loader.rb +46 -3
- 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/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 +514 -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 +343 -0
- data/lib/fontisan/type1/pfa_parser.rb +158 -0
- data/lib/fontisan/type1/pfb_generator.rb +291 -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 +285 -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 +73 -0
- data/lib/fontisan/type1_font.rb +331 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +2 -0
- metadata +26 -2
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# Type 1 CharStrings parser
|
|
6
|
+
#
|
|
7
|
+
# [`CharStrings`](lib/fontisan/type1/charstrings.rb) parses and stores
|
|
8
|
+
# the CharStrings dictionary from a Type 1 font, which contains
|
|
9
|
+
# glyph outline descriptions.
|
|
10
|
+
#
|
|
11
|
+
# CharStrings in Type 1 fonts use a stack-based language with commands
|
|
12
|
+
# for drawing curves, lines, and composite glyphs (via the seac operator).
|
|
13
|
+
#
|
|
14
|
+
# @example Parse CharStrings from decrypted font data
|
|
15
|
+
# charstrings = Fontisan::Type1::CharStrings.parse(decrypted_data, private_dict)
|
|
16
|
+
# outline = charstrings.outline_for("A")
|
|
17
|
+
#
|
|
18
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
19
|
+
class CharStrings
|
|
20
|
+
# @return [Hash] Glyph name to CharString data mapping
|
|
21
|
+
attr_reader :charstrings
|
|
22
|
+
|
|
23
|
+
# @return [PrivateDict] Private dictionary for decryption
|
|
24
|
+
attr_reader :private_dict
|
|
25
|
+
|
|
26
|
+
# Parse CharStrings dictionary from decrypted Type 1 font data
|
|
27
|
+
#
|
|
28
|
+
# @param data [String] Decrypted Type 1 font data
|
|
29
|
+
# @param private_dict [PrivateDict] Private dictionary for lenIV
|
|
30
|
+
# @return [CharStrings] Parsed CharStrings dictionary
|
|
31
|
+
# @raise [Fontisan::Error] If CharStrings cannot be parsed
|
|
32
|
+
#
|
|
33
|
+
# @example Parse from decrypted font data
|
|
34
|
+
# charstrings = Fontisan::Type1::CharStrings.parse(decrypted_data, private_dict)
|
|
35
|
+
def self.parse(data, private_dict = nil)
|
|
36
|
+
new(private_dict).parse(data)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Initialize a new CharStrings parser
|
|
40
|
+
#
|
|
41
|
+
# @param private_dict [PrivateDict, nil] Private dictionary for lenIV
|
|
42
|
+
def initialize(private_dict = nil)
|
|
43
|
+
@private_dict = private_dict || PrivateDict.new
|
|
44
|
+
@charstrings = {}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Parse CharStrings dictionary from decrypted Type 1 font data
|
|
48
|
+
#
|
|
49
|
+
# @param data [String] Decrypted Type 1 font data
|
|
50
|
+
# @return [CharStrings] Self for method chaining
|
|
51
|
+
def parse(data)
|
|
52
|
+
extract_charstrings(data)
|
|
53
|
+
decrypt_charstrings
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get list of glyph names
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<String>] Glyph names
|
|
60
|
+
def glyph_names
|
|
61
|
+
@charstrings.keys
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get the number of charstrings
|
|
65
|
+
#
|
|
66
|
+
# @return [Integer] Number of charstrings
|
|
67
|
+
def count
|
|
68
|
+
@charstrings.size
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get encoding map
|
|
72
|
+
#
|
|
73
|
+
# @return [Hash] Character code to glyph name mapping
|
|
74
|
+
def encoding
|
|
75
|
+
@encoding ||= build_standard_encoding
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Iterate over all charstrings
|
|
79
|
+
#
|
|
80
|
+
# @yield [glyph_name, charstring_data] Each glyph name and its charstring data
|
|
81
|
+
# @return [Enumerator] If no block given
|
|
82
|
+
def each_charstring(&)
|
|
83
|
+
return enum_for(:each_charstring) unless block_given?
|
|
84
|
+
|
|
85
|
+
@charstrings.each(&)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if glyph exists
|
|
89
|
+
#
|
|
90
|
+
# @param glyph_name [String] Glyph name
|
|
91
|
+
# @return [Boolean] True if glyph exists
|
|
92
|
+
def has_glyph?(glyph_name)
|
|
93
|
+
@charstrings.key?(glyph_name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Get CharString data for glyph
|
|
97
|
+
#
|
|
98
|
+
# @param glyph_name [String] Glyph name
|
|
99
|
+
# @return [String, nil] CharString data or nil if not found
|
|
100
|
+
def [](glyph_name)
|
|
101
|
+
@charstrings[glyph_name]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Alias for #[]
|
|
105
|
+
#
|
|
106
|
+
# @param glyph_name [String] Glyph name
|
|
107
|
+
# @return [String, nil] CharString data or nil if not found
|
|
108
|
+
def charstring(glyph_name)
|
|
109
|
+
@charstrings[glyph_name]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get outline for glyph by name
|
|
113
|
+
#
|
|
114
|
+
# Parses the CharString and returns outline commands.
|
|
115
|
+
#
|
|
116
|
+
# @param glyph_name [String] Glyph name
|
|
117
|
+
# @return [Array] Outline commands
|
|
118
|
+
def outline_for(glyph_name)
|
|
119
|
+
charstring = @charstrings[glyph_name]
|
|
120
|
+
return nil if charstring.nil?
|
|
121
|
+
|
|
122
|
+
parser = CharStringParser.new(@private_dict)
|
|
123
|
+
parser.parse(charstring)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Check if glyph is composite (uses seac)
|
|
127
|
+
#
|
|
128
|
+
# @param glyph_name [String] Glyph name
|
|
129
|
+
# @return [Boolean] True if glyph uses seac operator
|
|
130
|
+
def composite?(glyph_name)
|
|
131
|
+
charstring = @charstrings[glyph_name]
|
|
132
|
+
return false if charstring.nil?
|
|
133
|
+
|
|
134
|
+
charstring.include?(SEAC_OPCODE)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Get components for composite glyph
|
|
138
|
+
#
|
|
139
|
+
# @param glyph_name [String] Glyph name
|
|
140
|
+
# @return [Hash, nil] Component info {:base, :accent} or nil
|
|
141
|
+
def components_for(glyph_name)
|
|
142
|
+
return nil unless composite?(glyph_name)
|
|
143
|
+
|
|
144
|
+
charstring = @charstrings[glyph_name]
|
|
145
|
+
parser = CharStringParser.new(@private_dict)
|
|
146
|
+
parser.parse(charstring)
|
|
147
|
+
|
|
148
|
+
parser.seac_components
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
# Extract CharStrings dictionary from font data
|
|
154
|
+
#
|
|
155
|
+
# @param data [String] Decrypted Type 1 font data
|
|
156
|
+
def extract_charstrings(data)
|
|
157
|
+
# Find CharStrings dictionary
|
|
158
|
+
# Format: /CharStrings <dict_size> dict def begin ... end
|
|
159
|
+
#
|
|
160
|
+
# The CharStrings dict contains entries like:
|
|
161
|
+
# /.notdef <index> CharString_data
|
|
162
|
+
# /A <index> CharString_data
|
|
163
|
+
# etc.
|
|
164
|
+
|
|
165
|
+
# Look for /CharStrings dict def begin ... end pattern
|
|
166
|
+
# Use bounded patterns to prevent ReDoS - the dict size is a number
|
|
167
|
+
# Limit capture to 100KB which is sufficient for CharStrings
|
|
168
|
+
# Use [\s\S] to match any character including newlines
|
|
169
|
+
charstrings_match = data.match(%r{/CharStrings\s+\d+(?:\s+dup)?\s+dict\s+(?:dup\s+)?begin([\s\S]{0,100000}?)end}m)
|
|
170
|
+
return if charstrings_match.nil?
|
|
171
|
+
|
|
172
|
+
charstrings_text = charstrings_match[1]
|
|
173
|
+
parse_charstrings_dict(charstrings_text)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Parse CharStrings dictionary text
|
|
177
|
+
#
|
|
178
|
+
# @param text [String] CharStrings dictionary text
|
|
179
|
+
def parse_charstrings_dict(text)
|
|
180
|
+
# Type 1 CharStrings format:
|
|
181
|
+
# /glyphname <index> RD <binary_data>
|
|
182
|
+
# /glyphname <index> -| <binary_data> |-
|
|
183
|
+
# where RD and -| mark binary data
|
|
184
|
+
|
|
185
|
+
# Use a non-greedy match to capture data between the marker and end marker
|
|
186
|
+
# For -| ... |- format:
|
|
187
|
+
text.scan(/\/([^\s]+)\s+(\d+)\s+-\|(.*?)\|-/m) do |match|
|
|
188
|
+
glyph_name = match[0]
|
|
189
|
+
_index = match[1].to_i
|
|
190
|
+
encrypted_data = match[2]
|
|
191
|
+
|
|
192
|
+
@charstrings[glyph_name] = encrypted_data
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# For RD format (no end marker, data ends at next glyph or end):
|
|
196
|
+
# This is harder to parse, so we'll skip for now and focus on -| format
|
|
197
|
+
# which is what PFB uses
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Decrypt all CharStrings
|
|
201
|
+
def decrypt_charstrings
|
|
202
|
+
len_iv = @private_dict.len_iv || 4
|
|
203
|
+
|
|
204
|
+
@charstrings.transform_values! do |encrypted|
|
|
205
|
+
# Check if data is binary (from PFB) or hex-encoded (from PFA)
|
|
206
|
+
binary_data = if encrypted.ascii_only?
|
|
207
|
+
# Hex-encoded string (PFA format)
|
|
208
|
+
[encrypted.gsub(/\s/, "")].pack("H*")
|
|
209
|
+
else
|
|
210
|
+
# Already binary data (PFB format)
|
|
211
|
+
encrypted
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Decrypt CharString
|
|
215
|
+
Decryptor.charstring_decrypt(binary_data, len_iv: len_iv)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# seac opcode
|
|
220
|
+
SEAC_OPCODE = "\x0C\x06".b
|
|
221
|
+
|
|
222
|
+
# Build standard encoding map
|
|
223
|
+
#
|
|
224
|
+
# @return [Hash] Standard encoding map (character code to glyph name)
|
|
225
|
+
def build_standard_encoding
|
|
226
|
+
# A subset of Adobe StandardEncoding
|
|
227
|
+
{
|
|
228
|
+
32 => "space", 33 => "exclam", 34 => "quotedbl", 35 => "numbersign",
|
|
229
|
+
36 => "dollar", 37 => "percent", 38 => "ampersand", 39 => "quoteright",
|
|
230
|
+
40 => "parenleft", 41 => "parenright", 42 => "asterisk", 43 => "plus",
|
|
231
|
+
44 => "comma", 45 => "hyphen", 46 => "period", 47 => "slash",
|
|
232
|
+
48 => "zero", 49 => "one", 50 => "two", 51 => "three",
|
|
233
|
+
52 => "four", 53 => "five", 54 => "six", 55 => "seven",
|
|
234
|
+
56 => "eight", 57 => "nine", 58 => "colon", 59 => "semicolon",
|
|
235
|
+
60 => "less", 61 => "equal", 62 => "greater", 63 => "question",
|
|
236
|
+
64 => "at",
|
|
237
|
+
65 => "A", 66 => "B", 67 => "C", 68 => "D", 69 => "E",
|
|
238
|
+
70 => "F", 71 => "G", 72 => "H", 73 => "I", 74 => "J",
|
|
239
|
+
75 => "K", 76 => "L", 77 => "M", 78 => "N", 79 => "O",
|
|
240
|
+
80 => "P", 81 => "Q", 82 => "R", 83 => "S", 84 => "T",
|
|
241
|
+
85 => "U", 86 => "V", 87 => "W", 88 => "X", 89 => "Y",
|
|
242
|
+
90 => "Z",
|
|
243
|
+
91 => "bracketleft", 92 => "backslash", 93 => "bracketright",
|
|
244
|
+
94 => "asciicircum", 95 => "underscore", 96 => "quoteleft",
|
|
245
|
+
97 => "a", 98 => "b", 99 => "c", 100 => "d", 101 => "e",
|
|
246
|
+
102 => "f", 103 => "g", 104 => "h", 105 => "i", 106 => "j",
|
|
247
|
+
107 => "k", 108 => "l", 109 => "m", 110 => "n", 111 => "o",
|
|
248
|
+
112 => "p", 113 => "q", 114 => "r", 115 => "s", 116 => "t",
|
|
249
|
+
117 => "u", 118 => "v", 119 => "w", 120 => "x", 121 => "y",
|
|
250
|
+
122 => "z",
|
|
251
|
+
123 => "braceleft", 124 => "bar", 125 => "braceright",
|
|
252
|
+
126 => "asciitilde"
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# CharString parser
|
|
257
|
+
#
|
|
258
|
+
# Parses Type 1 CharString bytecode into commands.
|
|
259
|
+
class CharStringParser
|
|
260
|
+
# @return [Array] Parsed commands
|
|
261
|
+
attr_reader :commands
|
|
262
|
+
|
|
263
|
+
# @return [Hash, nil] seac components if seac found
|
|
264
|
+
attr_reader :seac_components
|
|
265
|
+
|
|
266
|
+
# @return [PrivateDict] Private dictionary
|
|
267
|
+
attr_reader :private_dict
|
|
268
|
+
|
|
269
|
+
# Initialize parser
|
|
270
|
+
#
|
|
271
|
+
# @param private_dict [PrivateDict] Private dictionary
|
|
272
|
+
def initialize(private_dict = nil)
|
|
273
|
+
@private_dict = private_dict || PrivateDict.new
|
|
274
|
+
@commands = []
|
|
275
|
+
@seac_components = nil
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Parse CharString bytecode
|
|
279
|
+
#
|
|
280
|
+
# @param charstring [String] Binary CharString data
|
|
281
|
+
# @return [Array] Parsed commands
|
|
282
|
+
def parse(charstring)
|
|
283
|
+
return [] if charstring.nil? || charstring.empty?
|
|
284
|
+
|
|
285
|
+
@commands = []
|
|
286
|
+
@seac_components = nil
|
|
287
|
+
|
|
288
|
+
i = 0
|
|
289
|
+
while i < charstring.length
|
|
290
|
+
byte = charstring.getbyte(i)
|
|
291
|
+
|
|
292
|
+
if byte <= 31
|
|
293
|
+
# Operator
|
|
294
|
+
parse_operator(charstring, byte, i)
|
|
295
|
+
break if @seac_components # Stop at seac for now
|
|
296
|
+
|
|
297
|
+
i += 1
|
|
298
|
+
elsif byte == 255
|
|
299
|
+
# Escaped number (2 bytes follow)
|
|
300
|
+
num = charstring.getbyte(i + 1) |
|
|
301
|
+
(charstring.getbyte(i + 2) << 8)
|
|
302
|
+
num = num - 32768 if num >= 32768
|
|
303
|
+
@commands << [:number, num]
|
|
304
|
+
i += 3
|
|
305
|
+
elsif byte >= 32 && byte <= 246
|
|
306
|
+
# Small number (-107 to 107)
|
|
307
|
+
num = byte - 139
|
|
308
|
+
@commands << [:number, num]
|
|
309
|
+
i += 1
|
|
310
|
+
else
|
|
311
|
+
# Unknown
|
|
312
|
+
i += 1
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
@commands
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
private
|
|
320
|
+
|
|
321
|
+
# Parse operator
|
|
322
|
+
#
|
|
323
|
+
# @param charstring [String] Full CharString data
|
|
324
|
+
# @param byte [Integer] Operator byte
|
|
325
|
+
# @param offset [Integer] Current offset
|
|
326
|
+
def parse_operator(charstring, byte, offset)
|
|
327
|
+
case byte
|
|
328
|
+
when 12
|
|
329
|
+
# Two-byte operator
|
|
330
|
+
next_byte = charstring.getbyte(offset + 1)
|
|
331
|
+
parse_two_byte_operator(next_byte)
|
|
332
|
+
when 1
|
|
333
|
+
@commands << [:hstem]
|
|
334
|
+
when 3
|
|
335
|
+
@commands << [:vstem]
|
|
336
|
+
when 4
|
|
337
|
+
@commands << [:vmoveto]
|
|
338
|
+
when 5
|
|
339
|
+
@commands << [:rlineto]
|
|
340
|
+
when 6
|
|
341
|
+
@commands << [:hlineto]
|
|
342
|
+
when 7
|
|
343
|
+
@commands << [:vlineto]
|
|
344
|
+
when 8
|
|
345
|
+
@commands << [:rrcurveto]
|
|
346
|
+
when 10
|
|
347
|
+
@commands << [:callsubr]
|
|
348
|
+
when 11
|
|
349
|
+
@commands << [:return]
|
|
350
|
+
when 14
|
|
351
|
+
@commands << [:endchar]
|
|
352
|
+
when 21
|
|
353
|
+
@commands << [:rmoveto]
|
|
354
|
+
when 22
|
|
355
|
+
@commands << [:hmoveto]
|
|
356
|
+
when 30
|
|
357
|
+
@commands << [:vhcurveto]
|
|
358
|
+
when 31
|
|
359
|
+
@commands << [:hvcurveto]
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Parse two-byte operator
|
|
364
|
+
#
|
|
365
|
+
# @param byte [Integer] Second operator byte
|
|
366
|
+
def parse_two_byte_operator(byte)
|
|
367
|
+
case byte
|
|
368
|
+
when 6
|
|
369
|
+
@commands << [:seac]
|
|
370
|
+
parse_seac
|
|
371
|
+
when 7
|
|
372
|
+
@commands << [:sbw]
|
|
373
|
+
when 34
|
|
374
|
+
@commands << [:hsbw]
|
|
375
|
+
when 36
|
|
376
|
+
@commands << [:div]
|
|
377
|
+
when 5
|
|
378
|
+
@commands << [:callgsubr]
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Parse seac composite glyph
|
|
383
|
+
#
|
|
384
|
+
# seac format: asb adx ady bchar achar seac
|
|
385
|
+
def parse_seac
|
|
386
|
+
# seac takes 5 arguments: asb, adx, ady, bchar, achar
|
|
387
|
+
# We need to extract the last 5 numbers from the command stack
|
|
388
|
+
nums = @commands.select { |c| c.first == :number }.map(&:last)
|
|
389
|
+
|
|
390
|
+
return if nums.length < 5
|
|
391
|
+
|
|
392
|
+
# Last two are bchar and achar (character codes)
|
|
393
|
+
bchar = nums[-2]
|
|
394
|
+
achar = nums[-1]
|
|
395
|
+
adx = nums[-3]
|
|
396
|
+
ady = nums[-4]
|
|
397
|
+
|
|
398
|
+
@seac_components = {
|
|
399
|
+
base: bchar,
|
|
400
|
+
accent: achar,
|
|
401
|
+
adx: adx,
|
|
402
|
+
ady: ady,
|
|
403
|
+
}
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "encodings"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Type1
|
|
7
|
+
# Conversion options for Type 1 font generation
|
|
8
|
+
#
|
|
9
|
+
# [`ConversionOptions`](lib/fontisan/type1/conversion_options.rb) provides a unified
|
|
10
|
+
# configuration interface for converting outline fonts (TTF, OTF, TTC, etc.) to
|
|
11
|
+
# Type 1 formats (PFA, PFB, AFM, PFM, INF).
|
|
12
|
+
#
|
|
13
|
+
# @example Create Windows Type 1 options
|
|
14
|
+
# options = Fontisan::Type1::ConversionOptions.windows_type1
|
|
15
|
+
# options.upm_scale # => 1000
|
|
16
|
+
# options.encoding # => Fontisan::Type1::Encodings::AdobeStandard
|
|
17
|
+
# options.format # => :pfb
|
|
18
|
+
#
|
|
19
|
+
# @example Create custom options
|
|
20
|
+
# options = Fontisan::Type1::ConversionOptions.new(
|
|
21
|
+
# upm_scale: 1000,
|
|
22
|
+
# encoding: Fontisan::Type1::Encodings::Unicode,
|
|
23
|
+
# format: :pfa
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
# @see http://www.adobe.com/devnet/font/pdfs/5178.Type1.pdf
|
|
27
|
+
class ConversionOptions
|
|
28
|
+
# @return [Integer, :native] Target UPM (1000 for Type 1, :native to keep source UPM)
|
|
29
|
+
attr_accessor :upm_scale
|
|
30
|
+
|
|
31
|
+
# @return [Class] Encoding class (AdobeStandard, ISOLatin1, Unicode)
|
|
32
|
+
attr_accessor :encoding
|
|
33
|
+
|
|
34
|
+
# @return [Boolean] Decompose composite glyphs into base glyphs
|
|
35
|
+
attr_accessor :decompose_composites
|
|
36
|
+
|
|
37
|
+
# @return [Boolean] Convert quadratic curves to cubic
|
|
38
|
+
attr_accessor :convert_curves
|
|
39
|
+
|
|
40
|
+
# @return [Boolean] Apply autohinting to generated Type 1
|
|
41
|
+
attr_accessor :autohint
|
|
42
|
+
|
|
43
|
+
# @return [Boolean] Preserve native hinting from source font
|
|
44
|
+
attr_accessor :preserve_hinting
|
|
45
|
+
|
|
46
|
+
# @return [Symbol] Output format (:pfb or :pfa)
|
|
47
|
+
attr_accessor :format
|
|
48
|
+
|
|
49
|
+
# Default values based on TypeTool 3 and Adobe Type 1 specifications
|
|
50
|
+
DEFAULTS = {
|
|
51
|
+
upm_scale: 1000,
|
|
52
|
+
encoding: Encodings::AdobeStandard,
|
|
53
|
+
decompose_composites: false,
|
|
54
|
+
convert_curves: true,
|
|
55
|
+
autohint: false,
|
|
56
|
+
preserve_hinting: false,
|
|
57
|
+
format: :pfb,
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
# Initialize conversion options
|
|
61
|
+
#
|
|
62
|
+
# @param options [Hash] Option values
|
|
63
|
+
# @option options [Integer, :native] :upm_scale Target UPM (default: 1000)
|
|
64
|
+
# @option options [Class] :encoding Encoding class (default: AdobeStandard)
|
|
65
|
+
# @option options [Boolean] :decompose_composites Decompose composites (default: false)
|
|
66
|
+
# @option options [Boolean] :convert_curves Convert curves (default: true)
|
|
67
|
+
# @option options [Boolean] :autohint Apply autohinting (default: false)
|
|
68
|
+
# @option options [Boolean] :preserve_hinting Preserve hinting (default: false)
|
|
69
|
+
# @option options [Symbol] :format Output format :pfb or :pfa (default: :pfb)
|
|
70
|
+
def initialize(options = {})
|
|
71
|
+
@upm_scale = options[:upm_scale] || DEFAULTS[:upm_scale]
|
|
72
|
+
@encoding = options[:encoding] || DEFAULTS[:encoding]
|
|
73
|
+
@decompose_composites = options[:decompose_composites] || DEFAULTS[:decompose_composites]
|
|
74
|
+
@convert_curves = options.fetch(:convert_curves,
|
|
75
|
+
DEFAULTS[:convert_curves])
|
|
76
|
+
@autohint = options[:autohint] || DEFAULTS[:autohint]
|
|
77
|
+
@preserve_hinting = options[:preserve_hinting] || DEFAULTS[:preserve_hinting]
|
|
78
|
+
@format = options[:format] || DEFAULTS[:format]
|
|
79
|
+
|
|
80
|
+
validate!
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check if UPM scaling is needed
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean] True if upm_scale is not :native
|
|
86
|
+
def needs_scaling?
|
|
87
|
+
@upm_scale != :native
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if curve conversion is needed
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] True if convert_curves is true
|
|
93
|
+
def needs_curve_conversion?
|
|
94
|
+
@convert_curves
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check if autohinting is requested
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean] True if autohint is true
|
|
100
|
+
def needs_autohinting?
|
|
101
|
+
@autohint
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Convert to hash
|
|
105
|
+
#
|
|
106
|
+
# @return [Hash] Options as hash
|
|
107
|
+
def to_hash
|
|
108
|
+
{
|
|
109
|
+
upm_scale: @upm_scale,
|
|
110
|
+
encoding: @encoding,
|
|
111
|
+
decompose_composites: @decompose_composites,
|
|
112
|
+
convert_curves: @convert_curves,
|
|
113
|
+
autohint: @autohint,
|
|
114
|
+
preserve_hinting: @preserve_hinting,
|
|
115
|
+
format: @format,
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Create options for Windows Type 1 output
|
|
120
|
+
#
|
|
121
|
+
# @return [ConversionOptions] Options configured for Windows Type 1
|
|
122
|
+
def self.windows_type1
|
|
123
|
+
new(
|
|
124
|
+
upm_scale: 1000,
|
|
125
|
+
encoding: Encodings::AdobeStandard,
|
|
126
|
+
format: :pfb,
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Create options for Unix Type 1 output
|
|
131
|
+
#
|
|
132
|
+
# @return [ConversionOptions] Options configured for Unix Type 1
|
|
133
|
+
def self.unix_type1
|
|
134
|
+
new(
|
|
135
|
+
upm_scale: 1000,
|
|
136
|
+
encoding: Encodings::AdobeStandard,
|
|
137
|
+
format: :pfa,
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Create options with native UPM (no scaling)
|
|
142
|
+
#
|
|
143
|
+
# @return [ConversionOptions] Options with native UPM
|
|
144
|
+
def self.native_upm
|
|
145
|
+
new(
|
|
146
|
+
upm_scale: :native,
|
|
147
|
+
encoding: Encodings::Unicode,
|
|
148
|
+
format: :pfb,
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Create options for ISO-8859-1 encoding
|
|
153
|
+
#
|
|
154
|
+
# @return [ConversionOptions] Options with ISO Latin-1 encoding
|
|
155
|
+
def self.iso_latin1
|
|
156
|
+
new(
|
|
157
|
+
upm_scale: 1000,
|
|
158
|
+
encoding: Encodings::ISOLatin1,
|
|
159
|
+
format: :pfb,
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Create options with Unicode encoding
|
|
164
|
+
#
|
|
165
|
+
# @return [ConversionOptions] Options with Unicode encoding
|
|
166
|
+
def self.unicode_encoding
|
|
167
|
+
new(
|
|
168
|
+
upm_scale: 1000,
|
|
169
|
+
encoding: Encodings::Unicode,
|
|
170
|
+
format: :pfb,
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Create options for high-quality output (with curve conversion)
|
|
175
|
+
#
|
|
176
|
+
# @return [ConversionOptions] Options optimized for quality
|
|
177
|
+
def self.high_quality
|
|
178
|
+
new(
|
|
179
|
+
upm_scale: 1000,
|
|
180
|
+
encoding: Encodings::AdobeStandard,
|
|
181
|
+
convert_curves: true,
|
|
182
|
+
decompose_composites: true,
|
|
183
|
+
format: :pfb,
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Create options for minimal file size
|
|
188
|
+
#
|
|
189
|
+
# @return [ConversionOptions] Options optimized for size
|
|
190
|
+
def self.minimal_size
|
|
191
|
+
new(
|
|
192
|
+
upm_scale: 1000,
|
|
193
|
+
encoding: Encodings::AdobeStandard,
|
|
194
|
+
convert_curves: false,
|
|
195
|
+
decompose_composites: false,
|
|
196
|
+
format: :pfa,
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
# Validate options
|
|
203
|
+
#
|
|
204
|
+
# @raise [ArgumentError] If options are invalid
|
|
205
|
+
def validate!
|
|
206
|
+
validate_upm_scale!
|
|
207
|
+
validate_encoding!
|
|
208
|
+
validate_format!
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Validate UPM scale value
|
|
212
|
+
#
|
|
213
|
+
# @raise [ArgumentError] If upm_scale is invalid
|
|
214
|
+
def validate_upm_scale!
|
|
215
|
+
return if @upm_scale == :native
|
|
216
|
+
return if @upm_scale.is_a?(Integer) && @upm_scale.positive?
|
|
217
|
+
|
|
218
|
+
raise ArgumentError,
|
|
219
|
+
"upm_scale must be a positive integer or :native, got: #{@upm_scale.inspect}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Validate encoding class
|
|
223
|
+
#
|
|
224
|
+
# @raise [ArgumentError] If encoding is not a valid encoding class
|
|
225
|
+
def validate_encoding!
|
|
226
|
+
return if @encoding.is_a?(Class) && @encoding < Encodings::Encoding
|
|
227
|
+
|
|
228
|
+
raise ArgumentError,
|
|
229
|
+
"encoding must be an Encoding class (AdobeStandard, ISOLatin1, Unicode), got: #{@encoding.inspect}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Validate format value
|
|
233
|
+
#
|
|
234
|
+
# @raise [ArgumentError] If format is not :pfb or :pfa
|
|
235
|
+
def validate_format!
|
|
236
|
+
return if %i[pfb pfa].include?(@format)
|
|
237
|
+
|
|
238
|
+
raise ArgumentError,
|
|
239
|
+
"format must be :pfb or :pfa, got: #{@format.inspect}"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|