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,343 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pfb_generator"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Type1
|
|
7
|
+
# PFA (Printer Font ASCII) Generator
|
|
8
|
+
#
|
|
9
|
+
# [`PFAGenerator`](lib/fontisan/type1/pfa_generator.rb) generates Type 1 PFA files
|
|
10
|
+
# from TrueType fonts.
|
|
11
|
+
#
|
|
12
|
+
# PFA files are ASCII-encoded Type 1 fonts used by Unix systems.
|
|
13
|
+
# They are the same as PFB files but with binary data hex-encoded.
|
|
14
|
+
#
|
|
15
|
+
# @example Generate PFA from TTF
|
|
16
|
+
# font = Fontisan::FontLoader.load("font.ttf")
|
|
17
|
+
# pfa_data = Fontisan::Type1::PFAGenerator.generate(font)
|
|
18
|
+
# File.write("font.pfa", pfa_data)
|
|
19
|
+
#
|
|
20
|
+
# @example Generate PFA with custom options
|
|
21
|
+
# options = { upm_scale: 1000, format: :pfa }
|
|
22
|
+
# pfa_data = Fontisan::Type1::PFAGenerator.generate(font, options)
|
|
23
|
+
#
|
|
24
|
+
# @see https://www.adobe.com/devnet/font/pdfs/5178.Type1.pdf
|
|
25
|
+
class PFAGenerator
|
|
26
|
+
# Hex line length for ASCII encoding
|
|
27
|
+
HEX_LINE_LENGTH = 64
|
|
28
|
+
|
|
29
|
+
# Generate PFA from TTF font
|
|
30
|
+
#
|
|
31
|
+
# @param font [Fontisan::Font] Source TTF font
|
|
32
|
+
# @param options [Hash] Generation options
|
|
33
|
+
# @option options [Integer, :native] :upm_scale Target UPM (default: 1000)
|
|
34
|
+
# @option options [Class] :encoding Encoding class (default: Encodings::AdobeStandard)
|
|
35
|
+
# @option options [Boolean] :convert_curves Convert quadratic to cubic (default: true)
|
|
36
|
+
# @return [String] PFA file content (ASCII text)
|
|
37
|
+
def self.generate(font, options = {})
|
|
38
|
+
new(font, options).generate
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(font, options = {})
|
|
42
|
+
@font = font
|
|
43
|
+
@options = options
|
|
44
|
+
@metrics = MetricsCalculator.new(font)
|
|
45
|
+
|
|
46
|
+
# Set up scaler
|
|
47
|
+
upm_scale = options[:upm_scale] || 1000
|
|
48
|
+
@scaler = if upm_scale == :native
|
|
49
|
+
UPMScaler.native(font)
|
|
50
|
+
else
|
|
51
|
+
UPMScaler.new(font, target_upm: upm_scale)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Set up encoding
|
|
55
|
+
@encoding = options[:encoding] || Encodings::AdobeStandard
|
|
56
|
+
|
|
57
|
+
# Set up converter options
|
|
58
|
+
@convert_curves = options.fetch(:convert_curves, true)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Generate PFA file content
|
|
62
|
+
#
|
|
63
|
+
# @return [String] PFA ASCII content
|
|
64
|
+
def generate
|
|
65
|
+
lines = []
|
|
66
|
+
|
|
67
|
+
# Header (ASCII section 1)
|
|
68
|
+
lines << build_pfa_header
|
|
69
|
+
lines << build_font_dict
|
|
70
|
+
lines << build_private_dict
|
|
71
|
+
lines << ""
|
|
72
|
+
lines << "currentdict end"
|
|
73
|
+
lines << "dup /FontName get exch definefont pop"
|
|
74
|
+
lines << ""
|
|
75
|
+
|
|
76
|
+
# Binary section (hex-encoded)
|
|
77
|
+
lines << "%--Data to be hex-encoded:"
|
|
78
|
+
hex_data = build_hex_encoded_charstrings
|
|
79
|
+
lines.concat(hex_data)
|
|
80
|
+
lines << ""
|
|
81
|
+
|
|
82
|
+
# Trailer
|
|
83
|
+
lines << build_pfa_trailer
|
|
84
|
+
|
|
85
|
+
lines.join("\n")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Build PFA header
|
|
91
|
+
#
|
|
92
|
+
# @return [String] PFA header comment
|
|
93
|
+
def build_pfa_header
|
|
94
|
+
format("%%!PS-AdobeFont-1.0: %s 1.0\n", @font.post_script_name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Build font dictionary
|
|
98
|
+
#
|
|
99
|
+
# @return [String] Font dictionary in PostScript
|
|
100
|
+
def build_font_dict
|
|
101
|
+
dict = []
|
|
102
|
+
dict << "25 dict begin"
|
|
103
|
+
|
|
104
|
+
# Font type
|
|
105
|
+
dict << "/FontType 1 def"
|
|
106
|
+
dict << "/FontMatrix [0.001 0 0 0.001 0 0] def"
|
|
107
|
+
|
|
108
|
+
# Font info
|
|
109
|
+
name_table = @font.table(Constants::NAME_TAG)
|
|
110
|
+
if name_table
|
|
111
|
+
font_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME) || @font.post_script_name
|
|
112
|
+
dict << "/FontName /#{font_name} def"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Bounding box
|
|
116
|
+
head = @font.table(Constants::HEAD_TAG)
|
|
117
|
+
if head
|
|
118
|
+
bbox = [
|
|
119
|
+
@scaler.scale(head.x_min || 0),
|
|
120
|
+
@scaler.scale(head.y_min || 0),
|
|
121
|
+
@scaler.scale(head.x_max || 1000),
|
|
122
|
+
@scaler.scale(head.y_max || 1000),
|
|
123
|
+
]
|
|
124
|
+
dict << "/FontBBox {#{bbox.join(' ')}} def"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Paint type
|
|
128
|
+
dict << "/PaintType 0 def"
|
|
129
|
+
|
|
130
|
+
# Encoding
|
|
131
|
+
if @encoding == Encodings::AdobeStandard
|
|
132
|
+
dict << "/Encoding StandardEncoding def"
|
|
133
|
+
elsif @encoding == Encodings::ISOLatin1
|
|
134
|
+
dict << "/Encoding ISOLatin1Encoding def"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Font info
|
|
138
|
+
if name_table
|
|
139
|
+
if name_table.respond_to?(:version_string)
|
|
140
|
+
version = name_table.version_string(1) || name_table.version_string(3)
|
|
141
|
+
dict << "/Version (#{version}) def" if version
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if name_table.respond_to?(:copyright)
|
|
145
|
+
copyright = name_table.copyright(1) || name_table.copyright(3)
|
|
146
|
+
dict << "/Notice (#{copyright}) def" if copyright
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
dict << "currentdict end"
|
|
151
|
+
dict << "begin"
|
|
152
|
+
|
|
153
|
+
dict.join("\n")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Build Private dictionary
|
|
157
|
+
#
|
|
158
|
+
# @return [String] Private dictionary in PostScript
|
|
159
|
+
def build_private_dict
|
|
160
|
+
dict = []
|
|
161
|
+
dict << "/Private 15 dict begin"
|
|
162
|
+
|
|
163
|
+
# Blue values (for hinting)
|
|
164
|
+
# These are typically derived from the font's alignment zones
|
|
165
|
+
os2 = @font.table(Constants::OS2_TAG)
|
|
166
|
+
if os2.respond_to?(:typo_ascender) && os2.typo_ascender
|
|
167
|
+
blue_values = [
|
|
168
|
+
@scaler.scale(os2.typo_descender || -200),
|
|
169
|
+
@scaler.scale(os2.typo_descender || -200) + 20,
|
|
170
|
+
@scaler.scale(os2.typo_ascender),
|
|
171
|
+
@scaler.scale(os2.typo_ascender) + 10,
|
|
172
|
+
]
|
|
173
|
+
dict << "/BlueValues {#{blue_values.join(' ')}} def"
|
|
174
|
+
else
|
|
175
|
+
dict << "/BlueValues [-20 0 500 510] def"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
dict << "/BlueScale 0.039625 def"
|
|
179
|
+
dict << "/BlueShift 7 def"
|
|
180
|
+
dict << "/BlueFuzz 1 def"
|
|
181
|
+
|
|
182
|
+
# Stem snap hints
|
|
183
|
+
if os2.respond_to?(:weight_class) && os2.weight_class
|
|
184
|
+
stem_width = @scaler.scale([100, 80,
|
|
185
|
+
90][os2.weight_class / 100] || 80)
|
|
186
|
+
dict << "/StemSnapH [#{stem_width}] def"
|
|
187
|
+
dict << "/StemSnapV [#{stem_width}] def"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Force bold flag
|
|
191
|
+
dict << if os2.respond_to?(:weight_class) && os2.weight_class && os2.weight_class >= 700
|
|
192
|
+
"/ForceBold true def"
|
|
193
|
+
else
|
|
194
|
+
"/ForceBold false def"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Language group
|
|
198
|
+
dict << "/LanguageGroup 0 def"
|
|
199
|
+
|
|
200
|
+
# Unique ID (random)
|
|
201
|
+
dict << "/UniqueID #{rand(1000000..9999999)} def"
|
|
202
|
+
|
|
203
|
+
# Subrs (empty for now)
|
|
204
|
+
dict << "/Subrs 0 array def"
|
|
205
|
+
|
|
206
|
+
dict << "private dict begin"
|
|
207
|
+
dict << "end"
|
|
208
|
+
|
|
209
|
+
dict.join("\n")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Build hex-encoded CharStrings section
|
|
213
|
+
#
|
|
214
|
+
# @return [Array<String>] Array of hex-encoded lines
|
|
215
|
+
def build_hex_encoded_charstrings
|
|
216
|
+
# Generate CharStrings
|
|
217
|
+
charstrings = if @convert_curves
|
|
218
|
+
TTFToType1Converter.convert(@font, @scaler, @encoding)
|
|
219
|
+
else
|
|
220
|
+
generate_simple_charstrings
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Combine all charstrings
|
|
224
|
+
binary_data = charstrings.values.join
|
|
225
|
+
|
|
226
|
+
# Convert to hex representation
|
|
227
|
+
hex_lines = []
|
|
228
|
+
|
|
229
|
+
# Start hex section marker
|
|
230
|
+
hex_lines << "00" # Start binary data marker
|
|
231
|
+
|
|
232
|
+
# Encode binary data as hex with line breaks
|
|
233
|
+
hex_string = binary_data.bytes.map { |b| format("%02x", b) }.join
|
|
234
|
+
|
|
235
|
+
# Split into lines of HEX_LINE_LENGTH characters
|
|
236
|
+
hex_string.scan(/.{#{HEX_LINE_LENGTH}}/o) do |line|
|
|
237
|
+
hex_lines << line
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# End hex section marker
|
|
241
|
+
hex_lines << "00" # End binary data marker
|
|
242
|
+
|
|
243
|
+
hex_lines
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Generate simple CharStrings (without curve conversion)
|
|
247
|
+
#
|
|
248
|
+
# @return [Hash<Integer, String>] Glyph ID to CharString mapping
|
|
249
|
+
def generate_simple_charstrings
|
|
250
|
+
glyf_table = @font.table(Constants::GLYF_TAG)
|
|
251
|
+
return {} unless glyf_table
|
|
252
|
+
|
|
253
|
+
maxp = @font.table(Constants::MAXP_TAG)
|
|
254
|
+
num_glyphs = maxp&.num_glyphs || 0
|
|
255
|
+
|
|
256
|
+
charstrings = {}
|
|
257
|
+
num_glyphs.times do |gid|
|
|
258
|
+
charstrings[gid] = simple_charstring(glyf_table, gid)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
charstrings
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Generate a simple CharString for a glyph
|
|
265
|
+
#
|
|
266
|
+
# @param glyf_table [Object] TTF glyf table
|
|
267
|
+
# @param gid [Integer] Glyph ID
|
|
268
|
+
# @return [String] Type 1 CharString data
|
|
269
|
+
def simple_charstring(glyf_table, gid)
|
|
270
|
+
glyph = glyf_table.glyph(gid)
|
|
271
|
+
|
|
272
|
+
# Empty or compound glyph
|
|
273
|
+
if glyph.nil? || glyph.contour_count.zero? || glyph.compound?
|
|
274
|
+
# Return empty charstring (hsbw + endchar)
|
|
275
|
+
return [0, 500, 14].pack("C*")
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# For simple glyphs without curve conversion, generate minimal charstring
|
|
279
|
+
lsb = @scaler.scale(glyph.left_side_bearing || 0)
|
|
280
|
+
width = @scaler.scale(glyph.advance_width || 500)
|
|
281
|
+
bytes = [13, lsb, width] # hsbw command (13)
|
|
282
|
+
|
|
283
|
+
# Add simple line commands (very basic)
|
|
284
|
+
if glyph.respond_to?(:points) && glyph.points && !glyph.points.empty?
|
|
285
|
+
# Just draw lines between consecutive on-curve points
|
|
286
|
+
prev_point = nil
|
|
287
|
+
glyph.points.each do |point|
|
|
288
|
+
next unless point.on_curve?
|
|
289
|
+
|
|
290
|
+
if prev_point
|
|
291
|
+
dx = @scaler.scale(point.x) - @scaler.scale(prev_point.x)
|
|
292
|
+
dy = @scaler.scale(point.y) - @scaler.scale(prev_point.y)
|
|
293
|
+
bytes << 5 # rlineto
|
|
294
|
+
bytes.concat(encode_number(dx))
|
|
295
|
+
bytes.concat(encode_number(dy))
|
|
296
|
+
end
|
|
297
|
+
prev_point = point
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
bytes << 14 # endchar
|
|
302
|
+
bytes.pack("C*")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Encode a number for Type 1 CharString
|
|
306
|
+
#
|
|
307
|
+
# @param value [Integer] Number to encode
|
|
308
|
+
# @return [Array<Integer>] Array of bytes
|
|
309
|
+
def encode_number(value)
|
|
310
|
+
if value >= -107 && value <= 107
|
|
311
|
+
[value + 139]
|
|
312
|
+
elsif value >= 108 && value <= 1131
|
|
313
|
+
byte1 = ((value - 108) >> 8) + 247
|
|
314
|
+
byte2 = (value - 108) & 0xFF
|
|
315
|
+
[byte1, byte2]
|
|
316
|
+
elsif value >= -1131 && value <= -108
|
|
317
|
+
byte1 = ((-value - 108) >> 8) + 251
|
|
318
|
+
byte2 = (-value - 108) & 0xFF
|
|
319
|
+
[byte1, byte2]
|
|
320
|
+
elsif value >= -32768 && value <= 32767
|
|
321
|
+
[255, value & 0xFF, (value >> 8) & 0xFF]
|
|
322
|
+
else
|
|
323
|
+
bytes = []
|
|
324
|
+
4.times do |i|
|
|
325
|
+
bytes << ((value >> (8 * i)) & 0xFF)
|
|
326
|
+
end
|
|
327
|
+
[255] + bytes
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Build PFA trailer
|
|
332
|
+
#
|
|
333
|
+
# @return [String] PFA trailer
|
|
334
|
+
def build_pfa_trailer
|
|
335
|
+
lines = []
|
|
336
|
+
lines << "currentdict end"
|
|
337
|
+
lines << "dup /FontName get exch definefont pop"
|
|
338
|
+
lines << "% cleartomark"
|
|
339
|
+
lines.join("\n")
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# Parser for PFA (Printer Font ASCII) format
|
|
6
|
+
#
|
|
7
|
+
# [`PFAparser`](lib/fontisan/type1/pfa_parser.rb) parses the ASCII PFA format
|
|
8
|
+
# used for storing Adobe Type 1 fonts, primarily on Unix/Linux systems.
|
|
9
|
+
#
|
|
10
|
+
# The PFA format is pure ASCII text with encrypted portions marked by
|
|
11
|
+
# `currentfile eexec` and terminated by 512 ASCII zeros.
|
|
12
|
+
#
|
|
13
|
+
# Format structure:
|
|
14
|
+
# - Clear text: Font dictionary and initial data
|
|
15
|
+
# - Encrypted portion: Starts with `currentfile eexec`
|
|
16
|
+
# - Encrypted data: Binary data encoded as hexadecimal
|
|
17
|
+
# - End marker: 512 ASCII zeros ('0')
|
|
18
|
+
# - Cleartext again: Font dictionary closing
|
|
19
|
+
#
|
|
20
|
+
# @example Parse a PFA file
|
|
21
|
+
# parser = Fontisan::Type1::PFAparser.new
|
|
22
|
+
# result = parser.parse(File.read('font.pfa'))
|
|
23
|
+
# puts result.clear_text # => "!PS-AdobeFont-1.0..."
|
|
24
|
+
# puts result.encrypted_hex # => Encrypted hex string
|
|
25
|
+
#
|
|
26
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
27
|
+
class PFAParser
|
|
28
|
+
# Markers in PFA format
|
|
29
|
+
EEXEC_MARKER = "currentfile eexec"
|
|
30
|
+
# 512 ASCII zeros mark the end of encrypted portion
|
|
31
|
+
ZERO_MARKER = "0" * 512
|
|
32
|
+
|
|
33
|
+
# @return [String] Clear text portion (before eexec)
|
|
34
|
+
attr_reader :clear_text
|
|
35
|
+
|
|
36
|
+
# @return [String] Encrypted portion as hex string
|
|
37
|
+
attr_reader :encrypted_hex
|
|
38
|
+
|
|
39
|
+
# @return [String] Trailing text after zeros (if any)
|
|
40
|
+
attr_reader :trailing_text
|
|
41
|
+
|
|
42
|
+
# Parse PFA format data
|
|
43
|
+
#
|
|
44
|
+
# @param data [String] ASCII PFA data
|
|
45
|
+
# @return [PFAParser] Self for method chaining
|
|
46
|
+
# @raise [ArgumentError] If data is nil or empty
|
|
47
|
+
# @raise [Fontisan::Error] If PFA format is invalid
|
|
48
|
+
def parse(data)
|
|
49
|
+
raise ArgumentError, "Data cannot be nil" if data.nil?
|
|
50
|
+
raise ArgumentError, "Data cannot be empty" if data.empty?
|
|
51
|
+
|
|
52
|
+
# Normalize line endings
|
|
53
|
+
data = normalize_line_endings(data)
|
|
54
|
+
|
|
55
|
+
# Find eexec marker
|
|
56
|
+
eexec_index = data.index(EEXEC_MARKER)
|
|
57
|
+
if eexec_index.nil?
|
|
58
|
+
# No eexec marker - entire file is clear text
|
|
59
|
+
@clear_text = data
|
|
60
|
+
@encrypted_hex = ""
|
|
61
|
+
@trailing_text = ""
|
|
62
|
+
return self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Clear text is everything before and including eexec marker
|
|
66
|
+
@clear_text = data[0...eexec_index + EEXEC_MARKER.length]
|
|
67
|
+
|
|
68
|
+
# Look for zeros after eexec marker
|
|
69
|
+
after_eexec = data[eexec_index + EEXEC_MARKER.length..]
|
|
70
|
+
|
|
71
|
+
# Skip whitespace after eexec marker
|
|
72
|
+
encrypted_start = skip_whitespace(after_eexec, 0)
|
|
73
|
+
encrypted_data = after_eexec[encrypted_start..]
|
|
74
|
+
|
|
75
|
+
# Find zero marker
|
|
76
|
+
zero_index = encrypted_data.index(ZERO_MARKER)
|
|
77
|
+
if zero_index.nil?
|
|
78
|
+
raise Fontisan::Error,
|
|
79
|
+
"Invalid PFA: cannot find zero marker after eexec"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Extract encrypted hex data (before zeros)
|
|
83
|
+
@encrypted_hex = encrypted_data[0...zero_index].strip
|
|
84
|
+
|
|
85
|
+
# Extract trailing text (after zeros)
|
|
86
|
+
trailing_start = zero_index + ZERO_MARKER.length
|
|
87
|
+
trailing_start = skip_whitespace(encrypted_data, trailing_start)
|
|
88
|
+
|
|
89
|
+
@trailing_text = if trailing_start < encrypted_data.length
|
|
90
|
+
encrypted_data[trailing_start..]
|
|
91
|
+
else
|
|
92
|
+
""
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if parser has parsed data
|
|
99
|
+
#
|
|
100
|
+
# @return [Boolean] True if data has been parsed
|
|
101
|
+
def parsed?
|
|
102
|
+
!@clear_text.nil?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if this appears to be a PFA file
|
|
106
|
+
#
|
|
107
|
+
# @param data [String] Text data to check
|
|
108
|
+
# @return [Boolean] True if data appears to be PFA format
|
|
109
|
+
#
|
|
110
|
+
# @example Check if file is PFA format
|
|
111
|
+
# if Fontisan::Type1::PFAParser.pfa_file?(data)
|
|
112
|
+
# # Handle PFA format
|
|
113
|
+
# end
|
|
114
|
+
def self.pfa_file?(data)
|
|
115
|
+
return false if data.nil?
|
|
116
|
+
return false if data.length < 15
|
|
117
|
+
|
|
118
|
+
# Check for Adobe Type 1 font header
|
|
119
|
+
data.include?("%!PS-AdobeFont-1.0") ||
|
|
120
|
+
data.include?("%!PS-Adobe-3.0 Resource-Font")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get encrypted hex as binary data
|
|
124
|
+
#
|
|
125
|
+
# Decodes the hexadecimal encrypted portion to binary.
|
|
126
|
+
#
|
|
127
|
+
# @return [String] Binary encrypted data
|
|
128
|
+
def encrypted_binary
|
|
129
|
+
return "" if @encrypted_hex.nil? || @encrypted_hex.empty?
|
|
130
|
+
|
|
131
|
+
# Convert hex string to binary
|
|
132
|
+
[@encrypted_hex.gsub(/\s/, "")].pack("H*")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# Normalize line endings to LF
|
|
138
|
+
#
|
|
139
|
+
# @param data [String] Input data
|
|
140
|
+
# @return [String] Data with normalized line endings
|
|
141
|
+
def normalize_line_endings(data)
|
|
142
|
+
data.gsub("\r\n", "\n").gsub("\r", "\n")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Skip whitespace in string
|
|
146
|
+
#
|
|
147
|
+
# @param str [String] Input string
|
|
148
|
+
# @param offset [Integer] Starting offset
|
|
149
|
+
# @return [Integer] Offset after skipping whitespace
|
|
150
|
+
def skip_whitespace(str, offset)
|
|
151
|
+
while offset < str.length && str[offset].match?(/\s/)
|
|
152
|
+
offset += 1
|
|
153
|
+
end
|
|
154
|
+
offset
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|