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,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# Decryption utilities for Type 1 fonts
|
|
6
|
+
#
|
|
7
|
+
# [`Decryptor`](lib/fontisan/type1/decryptor.rb) handles the two types of
|
|
8
|
+
# encryption used in Adobe Type 1 fonts:
|
|
9
|
+
#
|
|
10
|
+
# 1. **eexec encryption**: Protects the font's private dictionary and
|
|
11
|
+
# CharStrings with key 55665
|
|
12
|
+
# 2. **CharString encryption**: Individual CharStrings within the eexec
|
|
13
|
+
# portion with key 4330
|
|
14
|
+
#
|
|
15
|
+
# Both use the same cipher algorithm but with different keys.
|
|
16
|
+
# The algorithm uses cipher feedback, where the ciphertext is used
|
|
17
|
+
# to modify the key for subsequent bytes.
|
|
18
|
+
#
|
|
19
|
+
# Algorithm (from Adobe Type 1 Font Format):
|
|
20
|
+
# plain = (cipher XOR (R >> 8)) AND 0xFF
|
|
21
|
+
# R = ((cipher + R) * 52845 + 22719) AND 0xFFFF
|
|
22
|
+
#
|
|
23
|
+
# @example Decrypt eexec portion
|
|
24
|
+
# encrypted = Fontisan::Type1::PFAParser.new.parse(data).encrypted_binary
|
|
25
|
+
# decrypted = Fontisan::Type1::Decryptor.eexec_decrypt(encrypted)
|
|
26
|
+
#
|
|
27
|
+
# @example Decrypt CharString
|
|
28
|
+
# encrypted_charstring = "\x80\x00\x01..."
|
|
29
|
+
# decrypted = Fontisan::Type1::Decryptor.charstring_decrypt(encrypted_charstring, len_iv: 4)
|
|
30
|
+
#
|
|
31
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
32
|
+
module Decryptor
|
|
33
|
+
# Default lenIV value (number of random bytes at start of encrypted CharString)
|
|
34
|
+
DEFAULT_LEN_IV = 4
|
|
35
|
+
|
|
36
|
+
# eexec encryption key
|
|
37
|
+
EEXEC_KEY = 55665
|
|
38
|
+
|
|
39
|
+
# CharString encryption key
|
|
40
|
+
CHARSTRING_KEY = 4330
|
|
41
|
+
|
|
42
|
+
# Cipher update constants
|
|
43
|
+
CIPHER_MULT = 52845
|
|
44
|
+
CIPHER_ADD = 22719
|
|
45
|
+
CIPHER_MASK = 0xFFFF
|
|
46
|
+
|
|
47
|
+
# Decrypt eexec encrypted data
|
|
48
|
+
#
|
|
49
|
+
# The eexec cipher is used to encrypt the private dictionary and
|
|
50
|
+
# CharStrings portion of a Type 1 font.
|
|
51
|
+
#
|
|
52
|
+
# @param data [String] Encrypted binary data
|
|
53
|
+
# @return [String] Decrypted data
|
|
54
|
+
#
|
|
55
|
+
# @example Decrypt eexec portion
|
|
56
|
+
# decrypted = Fontisan::Type1::Decryptor.eexec_decrypt(encrypted_data)
|
|
57
|
+
def self.eexec_decrypt(data)
|
|
58
|
+
return "" if data.nil? || data.empty?
|
|
59
|
+
|
|
60
|
+
decrypt(data, EEXEC_KEY)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Encrypt data using eexec cipher
|
|
64
|
+
#
|
|
65
|
+
# @param data [String] Plain text data
|
|
66
|
+
# @return [String] Encrypted data
|
|
67
|
+
def self.eexec_encrypt(data)
|
|
68
|
+
return "" if data.nil? || data.empty?
|
|
69
|
+
|
|
70
|
+
encrypt(data, EEXEC_KEY)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Decrypt Type 1 CharString
|
|
74
|
+
#
|
|
75
|
+
# CharStrings are encrypted within the eexec portion using a different
|
|
76
|
+
# key (4330). The lenIV parameter specifies how many random bytes
|
|
77
|
+
# precede the actual CharString data (default is 4).
|
|
78
|
+
#
|
|
79
|
+
# @param data [String] Encrypted CharString data
|
|
80
|
+
# @param len_iv [Integer] Number of bytes to skip after decryption
|
|
81
|
+
# @return [String] Decrypted CharString data
|
|
82
|
+
#
|
|
83
|
+
# @example Decrypt CharString with default lenIV
|
|
84
|
+
# decrypted = Fontisan::Type1::Decryptor.charstring_decrypt(encrypted)
|
|
85
|
+
#
|
|
86
|
+
# @example Decrypt CharString with custom lenIV
|
|
87
|
+
# decrypted = Fontisan::Type1::Decryptor.charstring_decrypt(encrypted, len_iv: 0)
|
|
88
|
+
def self.charstring_decrypt(data, len_iv: DEFAULT_LEN_IV)
|
|
89
|
+
return "" if data.nil? || data.empty?
|
|
90
|
+
|
|
91
|
+
decrypted = decrypt(data, CHARSTRING_KEY)
|
|
92
|
+
|
|
93
|
+
# Skip lenIV bytes (random bytes added during encryption)
|
|
94
|
+
if len_iv.positive?
|
|
95
|
+
if len_iv >= decrypted.length
|
|
96
|
+
# lenIV is larger than data - return empty
|
|
97
|
+
return ""
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
decrypted = decrypted.byteslice(len_iv..-1)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
decrypted
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Encrypt data using CharString cipher
|
|
107
|
+
#
|
|
108
|
+
# @param data [String] Plain CharString data
|
|
109
|
+
# @param len_iv [Integer] Number of random bytes to prepend
|
|
110
|
+
# @return [String] Encrypted CharString data
|
|
111
|
+
def self.charstring_encrypt(data, len_iv: DEFAULT_LEN_IV)
|
|
112
|
+
return "" if data.nil? || data.empty?
|
|
113
|
+
|
|
114
|
+
# Ensure binary encoding before concatenation
|
|
115
|
+
data = data.b if data.respond_to?(:b)
|
|
116
|
+
|
|
117
|
+
# Prepend lenIV random bytes
|
|
118
|
+
if len_iv.positive?
|
|
119
|
+
random_bytes = Array.new(len_iv) { rand(256) }.pack("C*")
|
|
120
|
+
data = random_bytes + data
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
encrypt(data, CHARSTRING_KEY)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Decrypt using Type 1 cipher
|
|
127
|
+
#
|
|
128
|
+
# The cipher processes data byte by byte, maintaining state
|
|
129
|
+
# across iterations using cipher feedback.
|
|
130
|
+
#
|
|
131
|
+
# Algorithm:
|
|
132
|
+
# plain = (cipher XOR (R >> 8)) AND 0xFF
|
|
133
|
+
# R = ((cipher + R) * 52845 + 22719) AND 0xFFFF
|
|
134
|
+
#
|
|
135
|
+
# @param data [String] Encrypted data
|
|
136
|
+
# @param key [Integer] Cipher key (55665 for eexec, 4330 for CharString)
|
|
137
|
+
# @return [String] Decrypted data
|
|
138
|
+
def self.decrypt(data, key)
|
|
139
|
+
result = String.new(capacity: data.length)
|
|
140
|
+
r = key
|
|
141
|
+
|
|
142
|
+
data.each_byte do |cipher|
|
|
143
|
+
# plain = (cipher XOR (R >> 8)) AND 0xFF
|
|
144
|
+
plain = (cipher ^ (r >> 8)) & 0xFF
|
|
145
|
+
result << plain
|
|
146
|
+
|
|
147
|
+
# R = ((cipher + R) * 52845 + 22719) AND 0xFFFF
|
|
148
|
+
r = ((cipher + r) * CIPHER_MULT + CIPHER_ADD) & CIPHER_MASK
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
result
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Encrypt using Type 1 cipher
|
|
155
|
+
#
|
|
156
|
+
# The encryption algorithm is symmetric with decryption,
|
|
157
|
+
# producing the ciphertext that decrypts to the original plaintext.
|
|
158
|
+
#
|
|
159
|
+
# Algorithm:
|
|
160
|
+
# cipher = (plain XOR (R >> 8)) AND 0xFF
|
|
161
|
+
# R = ((cipher + R) * 52845 + 22719) AND 0xFFFF
|
|
162
|
+
#
|
|
163
|
+
# @param data [String] Plain data
|
|
164
|
+
# @param key [Integer] Cipher key
|
|
165
|
+
# @return [String] Encrypted data
|
|
166
|
+
def self.encrypt(data, key)
|
|
167
|
+
result = String.new(capacity: data.length)
|
|
168
|
+
r = key
|
|
169
|
+
|
|
170
|
+
data.each_byte do |plain|
|
|
171
|
+
# cipher = (plain XOR (R >> 8)) AND 0xFF
|
|
172
|
+
cipher = (plain ^ (r >> 8)) & 0xFF
|
|
173
|
+
result << cipher
|
|
174
|
+
|
|
175
|
+
# R = ((cipher + R) * 52845 + 22719) AND 0xFFFF
|
|
176
|
+
r = ((cipher + r) * CIPHER_MULT + CIPHER_ADD) & CIPHER_MASK
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
result
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|