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,514 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# Type 1 Font Dictionary model
|
|
6
|
+
#
|
|
7
|
+
# [`FontDictionary`](lib/fontisan/type1/font_dictionary.rb) parses and stores
|
|
8
|
+
# the font dictionary from a Type 1 font, which contains metadata about
|
|
9
|
+
# the font including FontInfo, FontName, Encoding, and other properties.
|
|
10
|
+
#
|
|
11
|
+
# The font dictionary is the top-level PostScript dictionary that defines
|
|
12
|
+
# the font's properties and contains references to the Private dictionary
|
|
13
|
+
# and CharStrings.
|
|
14
|
+
#
|
|
15
|
+
# @example Parse font dictionary from decrypted font data
|
|
16
|
+
# dict = Fontisan::Type1::FontDictionary.parse(decrypted_data)
|
|
17
|
+
# puts dict.font_name
|
|
18
|
+
# puts dict.font_info.full_name
|
|
19
|
+
# puts dict.font_b_box
|
|
20
|
+
#
|
|
21
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
22
|
+
class FontDictionary
|
|
23
|
+
# @return [FontInfo] Font information
|
|
24
|
+
attr_reader :font_info
|
|
25
|
+
|
|
26
|
+
# @return [String] Font name
|
|
27
|
+
attr_reader :font_name
|
|
28
|
+
|
|
29
|
+
# @return [Encoding] Font encoding
|
|
30
|
+
attr_reader :encoding
|
|
31
|
+
|
|
32
|
+
# @return [Hash] Font bounding box [x_min, y_min, x_max, y_max]
|
|
33
|
+
attr_reader :font_b_box
|
|
34
|
+
|
|
35
|
+
# Alias for font_b_box (camelCase compatibility)
|
|
36
|
+
alias font_bbox font_b_box
|
|
37
|
+
|
|
38
|
+
# @return [Array<Float>] Font matrix [xx, xy, yx, yy, tx, ty]
|
|
39
|
+
attr_reader :font_matrix
|
|
40
|
+
|
|
41
|
+
# @return [Integer] Paint type (0=symbol, 1=character)
|
|
42
|
+
attr_reader :paint_type
|
|
43
|
+
|
|
44
|
+
# @return [Integer] Font type (always 1 for Type 1)
|
|
45
|
+
attr_reader :font_type
|
|
46
|
+
|
|
47
|
+
# @return [Hash] Raw dictionary data
|
|
48
|
+
attr_reader :raw_data
|
|
49
|
+
|
|
50
|
+
# Parse font dictionary from decrypted Type 1 font data
|
|
51
|
+
#
|
|
52
|
+
# @param data [String] Decrypted Type 1 font data
|
|
53
|
+
# @return [FontDictionary] Parsed font dictionary
|
|
54
|
+
# @raise [Fontisan::Error] If dictionary cannot be parsed
|
|
55
|
+
#
|
|
56
|
+
# @example Parse from decrypted font data
|
|
57
|
+
# dict = Fontisan::Type1::FontDictionary.parse(decrypted_data)
|
|
58
|
+
def self.parse(data)
|
|
59
|
+
new.parse(data)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Initialize a new FontDictionary
|
|
63
|
+
def initialize
|
|
64
|
+
@font_info = FontInfo.new
|
|
65
|
+
@encoding = Encoding.new
|
|
66
|
+
@raw_data = {}
|
|
67
|
+
@parsed = false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Parse font dictionary from decrypted Type 1 font data
|
|
71
|
+
#
|
|
72
|
+
# @param data [String] Decrypted Type 1 font data
|
|
73
|
+
# @return [FontDictionary] Self for method chaining
|
|
74
|
+
def parse(data)
|
|
75
|
+
extract_font_dictionary(data)
|
|
76
|
+
extract_font_info
|
|
77
|
+
extract_encoding
|
|
78
|
+
extract_properties
|
|
79
|
+
@parsed = true
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check if dictionary was successfully parsed
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean] True if dictionary has been parsed
|
|
86
|
+
def parsed?
|
|
87
|
+
@parsed
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get full name from FontInfo
|
|
91
|
+
#
|
|
92
|
+
# @return [String, nil] Full font name
|
|
93
|
+
def full_name
|
|
94
|
+
@font_info&.full_name
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get family name from FontInfo
|
|
98
|
+
#
|
|
99
|
+
# @return [String, nil] Family name
|
|
100
|
+
def family_name
|
|
101
|
+
@font_info&.family_name
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get version from FontInfo
|
|
105
|
+
#
|
|
106
|
+
# @return [String, nil] Font version
|
|
107
|
+
def version
|
|
108
|
+
@font_info&.version
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get copyright from FontInfo
|
|
112
|
+
#
|
|
113
|
+
# @return [String, nil] Copyright notice
|
|
114
|
+
def copyright
|
|
115
|
+
@font_info&.copyright
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get notice from FontInfo
|
|
119
|
+
#
|
|
120
|
+
# @return [String, nil] Notice string
|
|
121
|
+
def notice
|
|
122
|
+
@font_info&.notice
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get weight from FontInfo
|
|
126
|
+
#
|
|
127
|
+
# @return [String, nil] Font weight (Thin, Light, Regular, Bold, etc.)
|
|
128
|
+
def weight
|
|
129
|
+
@font_info&.weight
|
|
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
|
+
private
|
|
141
|
+
|
|
142
|
+
# Extract font dictionary from data
|
|
143
|
+
#
|
|
144
|
+
# @param data [String] Decrypted Type 1 font data
|
|
145
|
+
def extract_font_dictionary(data)
|
|
146
|
+
# Find the font dictionary definition
|
|
147
|
+
# Type 1 fonts use PostScript dictionary syntax
|
|
148
|
+
# Format: /FontName dict def ... end
|
|
149
|
+
#
|
|
150
|
+
# We need to extract the dictionary between "dict def" and "end"
|
|
151
|
+
|
|
152
|
+
# Look for dictionary pattern
|
|
153
|
+
# The font dict typically starts after the version comment
|
|
154
|
+
# and contains key-value pairs
|
|
155
|
+
|
|
156
|
+
@raw_data = parse_dictionary(data)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Parse PostScript dictionary from text
|
|
160
|
+
#
|
|
161
|
+
# @param text [String] PostScript text
|
|
162
|
+
# @return [Hash] Parsed key-value pairs
|
|
163
|
+
def parse_dictionary(text)
|
|
164
|
+
result = {}
|
|
165
|
+
|
|
166
|
+
# Find dict def ... end blocks
|
|
167
|
+
# This is a simplified parser for Type 1 font dictionaries
|
|
168
|
+
|
|
169
|
+
# Extract key-value pairs using regex
|
|
170
|
+
# Patterns:
|
|
171
|
+
# /key value def
|
|
172
|
+
# /key (string) def
|
|
173
|
+
# /key [array] def
|
|
174
|
+
# /key number def
|
|
175
|
+
|
|
176
|
+
# Parse FontName - use bounded pattern to prevent ReDoS
|
|
177
|
+
# Font names are typically 1-64 characters of alphanumeric, dash, underscore
|
|
178
|
+
if (match = text.match(%r{/FontName\s+/([A-Za-z0-9_-]{1,64})\s+def}m))
|
|
179
|
+
result[:font_name] = match[1]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Parse FontInfo entries
|
|
183
|
+
# These are typically at the top level or in a FontInfo sub-dictionary
|
|
184
|
+
# Format: /FullName (value) readonly def
|
|
185
|
+
# Use safer patterns with bounded content and optional readonly keyword
|
|
186
|
+
if (match = text.match(%r{/FullName\s+\(([^)]{1,128}?)\)\s+(?:readonly\s+)?def}m))
|
|
187
|
+
result[:full_name] = match[1]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
if (match = text.match(%r{/FamilyName\s+\(([^)]{1,128}?)\)\s+(?:readonly\s+)?def}m))
|
|
191
|
+
result[:family_name] = match[1]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
if (match = text.match(%r{/version\s+\(([^)]{1,128}?)\)\s+(?:readonly\s+)?def}m))
|
|
195
|
+
result[:version] = match[1]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
if (match = text.match(%r{/Copyright\s+\(([^)]{1,128}?)\)\s+(?:readonly\s+)?def}m))
|
|
199
|
+
result[:copyright] = match[1]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
if (match = text.match(%r{/Notice\s+\(([^)]{1,128}?)\)\s+(?:readonly\s+)?def}m))
|
|
203
|
+
result[:notice] = match[1]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if (match = text.match(%r{/Weight\s+\(([^)]{1,128}?)\)\s+(?:readonly\s+)?def}m))
|
|
207
|
+
result[:weight] = match[1]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
if (match = text.match(%r{/isFixedPitch\s+(true|false)\s+def}m))
|
|
211
|
+
result[:is_fixed_pitch] = match[1] == "true"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if (match = text.match(%r{/UnderlinePosition\s+(-?\d+)\s+def}m))
|
|
215
|
+
result[:underline_position] = match[1].to_i
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
if (match = text.match(%r{/UnderlineThickness\s+(-?\d+)\s+def}m))
|
|
219
|
+
result[:underline_thickness] = match[1].to_i
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
if (match = text.match(%r{/ItalicAngle\s+(-?\d+)\s+def}m))
|
|
223
|
+
result[:italic_angle] = match[1].to_i
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Parse FontBBox - use safer patterns
|
|
227
|
+
if (match = text.match(%r{/FontBBox\s*\{([^\}]{1,100}?)\}\s+(?:readonly\s+)?def}m))
|
|
228
|
+
bbox_str = match[1].gsub(/[{}]/, "").strip.split
|
|
229
|
+
result[:font_b_box] = bbox_str.map(&:to_i) if bbox_str.length >= 4
|
|
230
|
+
elsif (match = text.match(%r{/FontBBox\s*\[([^\]]{1,100}?)\]\s+(?:readonly\s+)?def}m))
|
|
231
|
+
bbox_str = match[1].strip.split
|
|
232
|
+
result[:font_b_box] = bbox_str.map(&:to_i) if bbox_str.length >= 4
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Parse FontMatrix
|
|
236
|
+
if (match = text.match(%r{/FontMatrix\s*\[([^\]]{1,100}?)\]\s+(?:readonly\s+)?def}m))
|
|
237
|
+
matrix_str = match[1].strip.split
|
|
238
|
+
result[:font_matrix] = matrix_str.map(&:to_f)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Parse PaintType
|
|
242
|
+
if (match = text.match(%r{/PaintType\s+(\d+)\s+def}m))
|
|
243
|
+
result[:paint_type] = match[1].to_i
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Parse FontType
|
|
247
|
+
if (match = text.match(%r{/FontType\s+(\d+)\s+def}m))
|
|
248
|
+
result[:font_type] = match[1].to_i
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
result
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Extract FontInfo sub-dictionary
|
|
255
|
+
def extract_font_info
|
|
256
|
+
@font_info.parse(@raw_data)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Extract encoding
|
|
260
|
+
def extract_encoding
|
|
261
|
+
@encoding.parse(@raw_data)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Extract standard properties
|
|
265
|
+
def extract_properties
|
|
266
|
+
@font_name = @raw_data[:font_name]
|
|
267
|
+
@font_b_box = @raw_data[:font_b_box] || [0, 0, 0, 0]
|
|
268
|
+
@font_matrix = @raw_data[:font_matrix] || [0.001, 0, 0, 0.001, 0, 0]
|
|
269
|
+
@paint_type = @raw_data[:paint_type] || 0
|
|
270
|
+
@font_type = @raw_data[:font_type] || 1
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# FontInfo sub-dictionary
|
|
274
|
+
#
|
|
275
|
+
# Contains font metadata such as FullName, FamilyName, version, etc.
|
|
276
|
+
class FontInfo
|
|
277
|
+
# @return [String, nil] Full font name
|
|
278
|
+
attr_accessor :full_name
|
|
279
|
+
|
|
280
|
+
# @return [String, nil] Family name
|
|
281
|
+
attr_accessor :family_name
|
|
282
|
+
|
|
283
|
+
# @return [String, nil] Font version
|
|
284
|
+
attr_accessor :version
|
|
285
|
+
|
|
286
|
+
# @return [String, nil] Copyright notice
|
|
287
|
+
attr_accessor :copyright
|
|
288
|
+
|
|
289
|
+
# @return [String, nil] Notice string
|
|
290
|
+
attr_accessor :notice
|
|
291
|
+
|
|
292
|
+
# @return [String, nil] Font weight (Thin, Light, Regular, Bold, etc.)
|
|
293
|
+
attr_accessor :weight
|
|
294
|
+
|
|
295
|
+
# @return [String, nil] Fixed pitch (monospace) indicator
|
|
296
|
+
attr_accessor :is_fixed_pitch
|
|
297
|
+
|
|
298
|
+
# @return [String, nil] Underline position
|
|
299
|
+
attr_accessor :underline_position
|
|
300
|
+
|
|
301
|
+
# @return [String, nil] Underline thickness
|
|
302
|
+
attr_accessor :underline_thickness
|
|
303
|
+
|
|
304
|
+
# @return [String, nil] Italic angle
|
|
305
|
+
attr_accessor :italic_angle
|
|
306
|
+
|
|
307
|
+
# Parse FontInfo from dictionary data
|
|
308
|
+
#
|
|
309
|
+
# @param dict_data [Hash] Raw dictionary data
|
|
310
|
+
def parse(dict_data)
|
|
311
|
+
# FontInfo can be embedded in the main dict or as a sub-dict
|
|
312
|
+
# Try to extract from various patterns
|
|
313
|
+
@full_name = extract_string_value(dict_data, "FullName")
|
|
314
|
+
@family_name = extract_string_value(dict_data, "FamilyName")
|
|
315
|
+
@version = extract_string_value(dict_data, "version")
|
|
316
|
+
@copyright = extract_string_value(dict_data, "Copyright")
|
|
317
|
+
@notice = extract_string_value(dict_data, "Notice")
|
|
318
|
+
@weight = extract_string_value(dict_data, "Weight")
|
|
319
|
+
@is_fixed_pitch = extract_value(dict_data, "isFixedPitch")
|
|
320
|
+
@underline_position = extract_value(dict_data, "UnderlinePosition")
|
|
321
|
+
@underline_thickness = extract_value(dict_data, "UnderlineThickness")
|
|
322
|
+
@italic_angle = extract_value(dict_data, "ItalicAngle")
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private
|
|
326
|
+
|
|
327
|
+
# Extract string value from dictionary
|
|
328
|
+
#
|
|
329
|
+
# @param dict_data [Hash] Dictionary data
|
|
330
|
+
# @param key [String] Key to extract
|
|
331
|
+
# @return [String, nil] String value or nil
|
|
332
|
+
def extract_string_value(dict_data, key)
|
|
333
|
+
val = extract_value(dict_data, key)
|
|
334
|
+
return nil if val.nil?
|
|
335
|
+
|
|
336
|
+
# Remove parentheses if present
|
|
337
|
+
val = val.to_s
|
|
338
|
+
val = val[1..-2] if val.start_with?("(") && val.end_with?(")")
|
|
339
|
+
val
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Extract value from dictionary
|
|
343
|
+
#
|
|
344
|
+
# @param dict_data [Hash] Dictionary data
|
|
345
|
+
# @param key [String] Key to extract
|
|
346
|
+
# @return [Object, nil] Value or nil
|
|
347
|
+
def extract_value(dict_data, key)
|
|
348
|
+
sym_key = key.to_sym
|
|
349
|
+
return dict_data[sym_key] if dict_data.key?(sym_key)
|
|
350
|
+
|
|
351
|
+
# Try underscore version (e.g., FullName => full_name)
|
|
352
|
+
underscore_key = key.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/,
|
|
353
|
+
"").to_sym
|
|
354
|
+
dict_data[underscore_key]
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Encoding vector
|
|
359
|
+
#
|
|
360
|
+
# Maps character codes to glyph names in the font.
|
|
361
|
+
class Encoding
|
|
362
|
+
# @return [Hash] Character code to glyph name mapping
|
|
363
|
+
attr_reader :encoding_map
|
|
364
|
+
|
|
365
|
+
# @return [Symbol] Encoding type (:standard, :custom, :identity)
|
|
366
|
+
attr_reader :encoding_type
|
|
367
|
+
|
|
368
|
+
def initialize
|
|
369
|
+
@encoding_map = {}
|
|
370
|
+
@encoding_type = :standard
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Parse encoding from dictionary data
|
|
374
|
+
#
|
|
375
|
+
# @param dict_data [Hash] Dictionary data
|
|
376
|
+
def parse(dict_data)
|
|
377
|
+
# Type 1 fonts typically use StandardEncoding by default
|
|
378
|
+
# Custom encodings are specified as an array
|
|
379
|
+
|
|
380
|
+
@encoding_type = if dict_data[:encoding]
|
|
381
|
+
:custom
|
|
382
|
+
else
|
|
383
|
+
:standard
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Populate with StandardEncoding if standard
|
|
387
|
+
populate_standard_encoding if @encoding_type == :standard
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Get glyph name for character code
|
|
391
|
+
#
|
|
392
|
+
# @param char_code [Integer] Character code
|
|
393
|
+
# @return [String, nil] Glyph name or nil
|
|
394
|
+
def [](char_code)
|
|
395
|
+
@encoding_map[char_code]
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Check if encoding is standard
|
|
399
|
+
#
|
|
400
|
+
# @return [Boolean] True if using StandardEncoding
|
|
401
|
+
def standard?
|
|
402
|
+
@encoding_type == :standard
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
private
|
|
406
|
+
|
|
407
|
+
# Populate with Adobe StandardEncoding
|
|
408
|
+
def populate_standard_encoding
|
|
409
|
+
# A subset of Adobe StandardEncoding
|
|
410
|
+
# This is a simplified version for common characters
|
|
411
|
+
standard_mapping = {
|
|
412
|
+
32 => "space",
|
|
413
|
+
33 => "exclam",
|
|
414
|
+
34 => "quotedbl",
|
|
415
|
+
35 => "numbersign",
|
|
416
|
+
36 => "dollar",
|
|
417
|
+
37 => "percent",
|
|
418
|
+
38 => "ampersand",
|
|
419
|
+
39 => "quoteright",
|
|
420
|
+
40 => "parenleft",
|
|
421
|
+
41 => "parenright",
|
|
422
|
+
42 => "asterisk",
|
|
423
|
+
43 => "plus",
|
|
424
|
+
44 => "comma",
|
|
425
|
+
45 => "hyphen",
|
|
426
|
+
46 => "period",
|
|
427
|
+
47 => "slash",
|
|
428
|
+
48 => "zero",
|
|
429
|
+
49 => "one",
|
|
430
|
+
50 => "two",
|
|
431
|
+
51 => "three",
|
|
432
|
+
52 => "four",
|
|
433
|
+
53 => "five",
|
|
434
|
+
54 => "six",
|
|
435
|
+
55 => "seven",
|
|
436
|
+
56 => "eight",
|
|
437
|
+
57 => "nine",
|
|
438
|
+
58 => "colon",
|
|
439
|
+
59 => "semicolon",
|
|
440
|
+
60 => "less",
|
|
441
|
+
61 => "equal",
|
|
442
|
+
62 => "greater",
|
|
443
|
+
63 => "question",
|
|
444
|
+
64 => "at",
|
|
445
|
+
65 => "A",
|
|
446
|
+
66 => "B",
|
|
447
|
+
67 => "C",
|
|
448
|
+
68 => "D",
|
|
449
|
+
69 => "E",
|
|
450
|
+
70 => "F",
|
|
451
|
+
71 => "G",
|
|
452
|
+
72 => "H",
|
|
453
|
+
73 => "I",
|
|
454
|
+
74 => "J",
|
|
455
|
+
75 => "K",
|
|
456
|
+
76 => "L",
|
|
457
|
+
77 => "M",
|
|
458
|
+
78 => "N",
|
|
459
|
+
79 => "O",
|
|
460
|
+
80 => "P",
|
|
461
|
+
81 => "Q",
|
|
462
|
+
82 => "R",
|
|
463
|
+
83 => "S",
|
|
464
|
+
84 => "T",
|
|
465
|
+
85 => "U",
|
|
466
|
+
86 => "V",
|
|
467
|
+
87 => "W",
|
|
468
|
+
88 => "X",
|
|
469
|
+
89 => "Y",
|
|
470
|
+
90 => "Z",
|
|
471
|
+
91 => "bracketleft",
|
|
472
|
+
92 => "backslash",
|
|
473
|
+
93 => "bracketright",
|
|
474
|
+
94 => "asciicircum",
|
|
475
|
+
95 => "underscore",
|
|
476
|
+
96 => "quoteleft",
|
|
477
|
+
97 => "a",
|
|
478
|
+
98 => "b",
|
|
479
|
+
99 => "c",
|
|
480
|
+
100 => "d",
|
|
481
|
+
101 => "e",
|
|
482
|
+
102 => "f",
|
|
483
|
+
103 => "g",
|
|
484
|
+
104 => "h",
|
|
485
|
+
105 => "i",
|
|
486
|
+
106 => "j",
|
|
487
|
+
107 => "k",
|
|
488
|
+
108 => "l",
|
|
489
|
+
109 => "m",
|
|
490
|
+
110 => "n",
|
|
491
|
+
111 => "o",
|
|
492
|
+
112 => "p",
|
|
493
|
+
113 => "q",
|
|
494
|
+
114 => "r",
|
|
495
|
+
115 => "s",
|
|
496
|
+
116 => "t",
|
|
497
|
+
117 => "u",
|
|
498
|
+
118 => "v",
|
|
499
|
+
119 => "w",
|
|
500
|
+
120 => "x",
|
|
501
|
+
121 => "y",
|
|
502
|
+
122 => "z",
|
|
503
|
+
123 => "braceleft",
|
|
504
|
+
124 => "bar",
|
|
505
|
+
125 => "braceright",
|
|
506
|
+
126 => "asciitilde",
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
@encoding_map = standard_mapping
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
end
|