fontisan 0.2.10 → 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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +216 -42
  3. data/README.adoc +160 -0
  4. data/lib/fontisan/cli.rb +177 -6
  5. data/lib/fontisan/collection/table_analyzer.rb +88 -3
  6. data/lib/fontisan/commands/convert_command.rb +32 -1
  7. data/lib/fontisan/config/conversion_matrix.yml +132 -4
  8. data/lib/fontisan/constants.rb +12 -0
  9. data/lib/fontisan/conversion_options.rb +378 -0
  10. data/lib/fontisan/converters/cff_table_builder.rb +198 -0
  11. data/lib/fontisan/converters/collection_converter.rb +45 -10
  12. data/lib/fontisan/converters/format_converter.rb +2 -0
  13. data/lib/fontisan/converters/glyf_table_builder.rb +63 -0
  14. data/lib/fontisan/converters/outline_converter.rb +111 -374
  15. data/lib/fontisan/converters/outline_extraction.rb +93 -0
  16. data/lib/fontisan/converters/outline_optimizer.rb +89 -0
  17. data/lib/fontisan/converters/type1_converter.rb +559 -0
  18. data/lib/fontisan/font_loader.rb +46 -3
  19. data/lib/fontisan/glyph_accessor.rb +29 -1
  20. data/lib/fontisan/type1/afm_generator.rb +436 -0
  21. data/lib/fontisan/type1/afm_parser.rb +298 -0
  22. data/lib/fontisan/type1/agl.rb +456 -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 +514 -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 +343 -0
  32. data/lib/fontisan/type1/pfa_parser.rb +158 -0
  33. data/lib/fontisan/type1/pfb_generator.rb +291 -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 +285 -0
  38. data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
  39. data/lib/fontisan/type1/upm_scaler.rb +118 -0
  40. data/lib/fontisan/type1.rb +73 -0
  41. data/lib/fontisan/type1_font.rb +331 -0
  42. data/lib/fontisan/variation/cache.rb +1 -0
  43. data/lib/fontisan/version.rb +1 -1
  44. data/lib/fontisan/woff2_font.rb +3 -3
  45. data/lib/fontisan.rb +2 -0
  46. metadata +30 -2
@@ -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