fontisan 0.2.12 → 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 +185 -106
- data/Gemfile +5 -0
- data/README.adoc +3 -2
- data/docs/CONVERSION_GUIDE.adoc +633 -0
- data/docs/TYPE1_FONTS.adoc +445 -0
- data/lib/fontisan/commands/info_command.rb +83 -2
- data/lib/fontisan/converters/format_converter.rb +15 -5
- data/lib/fontisan/converters/type1_converter.rb +734 -59
- data/lib/fontisan/font_loader.rb +1 -1
- data/lib/fontisan/hints/hint_converter.rb +4 -1
- data/lib/fontisan/type1/cff_to_type1_converter.rb +302 -0
- data/lib/fontisan/type1/font_dictionary.rb +62 -0
- data/lib/fontisan/type1/pfa_generator.rb +31 -5
- data/lib/fontisan/type1/pfa_parser.rb +31 -30
- data/lib/fontisan/type1/pfb_generator.rb +28 -5
- data/lib/fontisan/type1/private_dict.rb +57 -0
- data/lib/fontisan/type1/seac_expander.rb +501 -0
- data/lib/fontisan/type1.rb +2 -0
- data/lib/fontisan/type1_font.rb +21 -34
- data/lib/fontisan/version.rb +1 -1
- metadata +6 -3
- data/docs/DOCUMENTATION_SUMMARY.md +0 -141
data/lib/fontisan/font_loader.rb
CHANGED
|
@@ -274,7 +274,10 @@ module Fontisan
|
|
|
274
274
|
end
|
|
275
275
|
|
|
276
276
|
# Merge all extracted hints (prep_hints and fpgm_hints override stem widths if present)
|
|
277
|
-
|
|
277
|
+
# Note: fpgm_hints contains metadata (fpgm_size, has_functions, complexity)
|
|
278
|
+
# which we must filter out before merging into PostScript dict hints
|
|
279
|
+
fpgm_dict_hints = fpgm_hints.reject { |k, _| %i[fpgm_size has_functions complexity].include?(k) }
|
|
280
|
+
hints.merge!(prep_hints).merge!(fpgm_dict_hints).merge!(blue_zones)
|
|
278
281
|
|
|
279
282
|
# Provide default blue_values if none were detected
|
|
280
283
|
# These are standard values that work for most Latin fonts
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# Converter for CFF CharStrings to Type 1 CharStrings
|
|
6
|
+
#
|
|
7
|
+
# [`CffToType1Converter`](lib/fontisan/type1/cff_to_type1_converter.rb) converts
|
|
8
|
+
# CFF (Compact Font Format) Type 2 CharStrings to Type 1 CharStrings.
|
|
9
|
+
#
|
|
10
|
+
# CFF and Type 1 use similar stack-based languages but have different operator
|
|
11
|
+
# codes and some structural differences:
|
|
12
|
+
# - Operator codes differ between formats
|
|
13
|
+
# - Type 1 uses hsbw/sbw for width selection; CFF uses initial operand
|
|
14
|
+
# - Hint operators need to be preserved with code translation
|
|
15
|
+
#
|
|
16
|
+
# @example Convert a CFF CharString to Type 1
|
|
17
|
+
# converter = Fontisan::Type1::CffToType1Converter.new
|
|
18
|
+
# type1_charstring = converter.convert(cff_charstring)
|
|
19
|
+
#
|
|
20
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
21
|
+
# @see https://www.microsoft.com/typography/otspec/cff.htm
|
|
22
|
+
class CffToType1Converter
|
|
23
|
+
# CFF to Type 1 operator mapping
|
|
24
|
+
#
|
|
25
|
+
# Maps CFF operator codes to Type 1 operator codes.
|
|
26
|
+
# Most operators are the same, but some differ.
|
|
27
|
+
CFF_TO_TYPE1 = {
|
|
28
|
+
# Path construction operators
|
|
29
|
+
hmoveto: 22, # CFF: 22, Type 1: 22
|
|
30
|
+
vmoveto: 4, # CFF: 4, Type 1: 4
|
|
31
|
+
rlineto: 5, # CFF: 5, Type 1: 5
|
|
32
|
+
hlineto: 6, # CFF: 6, Type 1: 6
|
|
33
|
+
vlineto: 7, # CFF: 7, Type 1: 7
|
|
34
|
+
rrcurveto: 8, # CFF: 8, Type 1: 8
|
|
35
|
+
hhcurveto: 27, # CFF: 27, Type 1: 27
|
|
36
|
+
hvcurveto: 31, # CFF: 31, Type 1: 31
|
|
37
|
+
vhcurveto: 30, # CFF: 30, Type 1: 30
|
|
38
|
+
rcurveline: 24, # CFF: 24, Type 1: 24
|
|
39
|
+
rlinecurve: 25, # CFF: 25, Type 1: 25
|
|
40
|
+
|
|
41
|
+
# Hint operators
|
|
42
|
+
hstem: 1, # CFF: 1, Type 1: 1
|
|
43
|
+
vstem: 3, # CFF: 3, Type 1: 3
|
|
44
|
+
hstemhm: 18, # CFF: 18, Type 1: 18
|
|
45
|
+
vstemhm: 23, # CFF: 23, Type 1: 23
|
|
46
|
+
|
|
47
|
+
# Hint substitution (preserve for compatibility)
|
|
48
|
+
hintmask: 19, # CFF: 19, Type 1: Not supported (skip)
|
|
49
|
+
cntrmask: 20, # CFF: 20, Type 1: Not supported (skip)
|
|
50
|
+
|
|
51
|
+
# End char
|
|
52
|
+
endchar: 14, # CFF: 14, Type 1: 14
|
|
53
|
+
|
|
54
|
+
# Miscellaneous
|
|
55
|
+
callsubr: 10, # CFF: 10, Type 1: 10
|
|
56
|
+
return: 11, # CFF: 11, Type 1: 11
|
|
57
|
+
rmoveto: 21, # CFF: 21, Type 1: 21
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
# Escape code for two-byte operators
|
|
61
|
+
ESCAPE_BYTE = 12
|
|
62
|
+
|
|
63
|
+
# Initialize a new CffToType1Converter
|
|
64
|
+
#
|
|
65
|
+
# @param nominal_width [Integer] Nominal width from CFF Private dict (default: 0)
|
|
66
|
+
# @param default_width [Integer] Default width from CFF Private dict (default: 0)
|
|
67
|
+
def initialize(nominal_width: 0, default_width: 0)
|
|
68
|
+
@nominal_width = nominal_width
|
|
69
|
+
@default_width = default_width
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Convert CFF CharString to Type 1 CharString
|
|
73
|
+
#
|
|
74
|
+
# Takes binary CFF CharString data and converts it to Type 1 format.
|
|
75
|
+
#
|
|
76
|
+
# @param cff_charstring [String] CFF CharString bytecode
|
|
77
|
+
# @param private_dict [Hash] CFF Private dict for context (optional)
|
|
78
|
+
# @return [String] Type 1 CharString bytecode
|
|
79
|
+
#
|
|
80
|
+
# @example Convert a CharString
|
|
81
|
+
# converter = Fontisan::Type1::CffToType1Converter.new
|
|
82
|
+
# type1_bytes = converter.convert(cff_bytes)
|
|
83
|
+
def convert(cff_charstring, private_dict: {})
|
|
84
|
+
# Parse CFF CharString into operations
|
|
85
|
+
parser = Tables::Cff::CharStringParser.new(cff_charstring,
|
|
86
|
+
stem_count: private_dict[:stem_count]&.to_i || 0)
|
|
87
|
+
operations = parser.parse
|
|
88
|
+
|
|
89
|
+
# Extract width from operations (CFF spec: odd stack before first move = width)
|
|
90
|
+
width = extract_width(operations)
|
|
91
|
+
|
|
92
|
+
# Convert operations to Type 1 format
|
|
93
|
+
convert_operations(operations, width)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Extract width from CFF operations
|
|
97
|
+
#
|
|
98
|
+
# In CFF, if there's an odd number of arguments before the first move
|
|
99
|
+
# operator (rmoveto, hmoveto, vmoveto, rcurveline, rrcurveline, vvcurveto,
|
|
100
|
+
# hhcurveto), the first argument is the width.
|
|
101
|
+
#
|
|
102
|
+
# @param operations [Array<Hash>] Parsed CFF operations
|
|
103
|
+
# @return [Integer, nil] Width value or nil if using default
|
|
104
|
+
def extract_width(operations)
|
|
105
|
+
return @default_width if operations.empty?
|
|
106
|
+
|
|
107
|
+
# Find first move operator
|
|
108
|
+
first_move_idx = operations.index do |op|
|
|
109
|
+
%i[rmoveto hmoveto vmoveto rcurveline rrcurveline vvcurveto hhcurveto].include?(op[:name])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
return @default_width unless first_move_idx
|
|
113
|
+
|
|
114
|
+
# Count operands before first move
|
|
115
|
+
operand_count = operations[0...first_move_idx].sum(0) do |op|
|
|
116
|
+
op[:operands]&.length || 0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# If odd, first operand of first move is width
|
|
120
|
+
if operand_count.odd?
|
|
121
|
+
first_move = operations[first_move_idx]
|
|
122
|
+
if first_move[:operands] && !first_move[:operands].empty?
|
|
123
|
+
return first_move[:operands].first
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
@default_width
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Convert parsed CFF operations to Type 1 CharString
|
|
131
|
+
#
|
|
132
|
+
# @param operations [Array<Hash>] Parsed CFF operations
|
|
133
|
+
# @param width [Integer, nil] Glyph width from CFF CharString
|
|
134
|
+
# @return [String] Type 1 CharString bytecode
|
|
135
|
+
def convert_operations(operations, width = nil)
|
|
136
|
+
result = String.new(encoding: Encoding::ASCII_8BIT)
|
|
137
|
+
|
|
138
|
+
# Determine width: use provided width or default/nominal
|
|
139
|
+
glyph_width = width || @default_width
|
|
140
|
+
|
|
141
|
+
# Add hsbw (horizontal sidebearing and width) at start
|
|
142
|
+
# This is the standard width operator for horizontal fonts
|
|
143
|
+
result << encode_number(0) # left sidebearing (usually 0 for CFF)
|
|
144
|
+
result << encode_number(glyph_width)
|
|
145
|
+
result << ESCAPE_BYTE
|
|
146
|
+
result << 34 # hsbw operator (two-byte: 12 34)
|
|
147
|
+
|
|
148
|
+
x = 0
|
|
149
|
+
y = 0
|
|
150
|
+
first_move = true
|
|
151
|
+
skip_first_operand = false
|
|
152
|
+
|
|
153
|
+
# Check if width was extracted (odd stack before first move)
|
|
154
|
+
if width && operations.any?
|
|
155
|
+
# Count operands before first move to determine if width was in stack
|
|
156
|
+
first_move_idx = operations.index do |op|
|
|
157
|
+
%i[rmoveto hmoveto vmoveto rcurveline rrcurveline vvcurveto hhcurveto].include?(op[:name])
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if first_move_idx
|
|
161
|
+
operand_count = operations[0...first_move_idx].sum(0) do |op|
|
|
162
|
+
op[:operands]&.length || 0
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
skip_first_operand = operand_count.odd?
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
operations.each do |op|
|
|
170
|
+
case op[:name]
|
|
171
|
+
when :hstem, :vstem, :hstemhm, :vstemhm
|
|
172
|
+
# Hint operators - preserve
|
|
173
|
+
op[:operands].each { |val| result << encode_number(val) }
|
|
174
|
+
result << CFF_TO_TYPE1[op[:name]]
|
|
175
|
+
when :rmoveto
|
|
176
|
+
# rmoveto dx dy (or width dx dy if first move with odd stack)
|
|
177
|
+
operands = op[:operands]
|
|
178
|
+
if first_move && skip_first_operand && !operands.empty?
|
|
179
|
+
# Skip first operand (it was the width)
|
|
180
|
+
operands = operands[1..]
|
|
181
|
+
skip_first_operand = false
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if operands.length >= 2
|
|
185
|
+
dx, dy = operands[0], operands[1]
|
|
186
|
+
x += dx
|
|
187
|
+
y += dy
|
|
188
|
+
result << encode_number(dx)
|
|
189
|
+
result << encode_number(dy)
|
|
190
|
+
result << 21 # rmoveto
|
|
191
|
+
elsif operands.length == 1
|
|
192
|
+
# Only dy (hmoveto/vmoveto style)
|
|
193
|
+
result << encode_number(operands.first)
|
|
194
|
+
result << 4 # vmoveto (closest approximation)
|
|
195
|
+
end
|
|
196
|
+
first_move = false
|
|
197
|
+
when :hmoveto
|
|
198
|
+
# hmoveto dx (or width dx if first move)
|
|
199
|
+
operands = op[:operands]
|
|
200
|
+
if first_move && skip_first_operand && !operands.empty?
|
|
201
|
+
operands = [operands[0]] if operands.length > 1
|
|
202
|
+
skip_first_operand = false
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
dx = operands.first
|
|
206
|
+
x += dx if dx
|
|
207
|
+
result << encode_number(dx)
|
|
208
|
+
result << 22 # hmoveto
|
|
209
|
+
first_move = false
|
|
210
|
+
when :vmoveto
|
|
211
|
+
# vmoveto dy
|
|
212
|
+
dy = op[:operands].first
|
|
213
|
+
y += dy if dy
|
|
214
|
+
result << encode_number(dy)
|
|
215
|
+
result << 4 # vmoveto
|
|
216
|
+
first_move = false
|
|
217
|
+
when :rlineto
|
|
218
|
+
# rlineto dx dy
|
|
219
|
+
dx, dy = op[:operands]
|
|
220
|
+
x += dx
|
|
221
|
+
y += dy
|
|
222
|
+
result << encode_number(dx)
|
|
223
|
+
result << encode_number(dy)
|
|
224
|
+
result << 5 # rlineto
|
|
225
|
+
first_move = false
|
|
226
|
+
when :hlineto
|
|
227
|
+
# hlineto dx
|
|
228
|
+
dx = op[:operands].first
|
|
229
|
+
x += dx
|
|
230
|
+
result << encode_number(dx)
|
|
231
|
+
result << 6 # hlineto
|
|
232
|
+
first_move = false
|
|
233
|
+
when :vlineto
|
|
234
|
+
# vlineto dy
|
|
235
|
+
dy = op[:operands].first
|
|
236
|
+
y += dy
|
|
237
|
+
result << encode_number(dy)
|
|
238
|
+
result << 7 # vlineto
|
|
239
|
+
first_move = false
|
|
240
|
+
when :rrcurveto
|
|
241
|
+
# rrcurveto dx1 dy1 dx2 dy2 dx3 dy3
|
|
242
|
+
dx1, dy1, dx2, dy2, dx3, dy3 = op[:operands]
|
|
243
|
+
x += dx1 + dx2 + dx3
|
|
244
|
+
y += dy1 + dy2 + dy3
|
|
245
|
+
[dx1, dy1, dx2, dy2, dx3, dy3].each { |val| result << encode_number(val) }
|
|
246
|
+
result << 8 # rrcurveto
|
|
247
|
+
first_move = false
|
|
248
|
+
when :hhcurveto, :hvcurveto, :vhcurveto
|
|
249
|
+
# Flexible curve operators
|
|
250
|
+
op[:operands].each { |val| result << encode_number(val) }
|
|
251
|
+
result << CFF_TO_TYPE1[op[:name]]
|
|
252
|
+
first_move = false
|
|
253
|
+
when :rcurveline, :rlinecurve
|
|
254
|
+
# Flexible curve operators
|
|
255
|
+
op[:operands].each { |val| result << encode_number(val) }
|
|
256
|
+
result << CFF_TO_TYPE1[op[:name]]
|
|
257
|
+
first_move = false
|
|
258
|
+
when :callsubr, :return, :endchar
|
|
259
|
+
# Control operators
|
|
260
|
+
result << CFF_TO_TYPE1[op[:name]]
|
|
261
|
+
first_move = false
|
|
262
|
+
when :hintmask, :cntrmask
|
|
263
|
+
# Hint mask operators - Type 1 doesn't support these
|
|
264
|
+
# Skip them
|
|
265
|
+
first_move = false
|
|
266
|
+
when :shortint
|
|
267
|
+
# Short integer push - handled by operand encoding
|
|
268
|
+
first_move = false
|
|
269
|
+
else
|
|
270
|
+
# Unknown operator - skip
|
|
271
|
+
first_move = false
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Add endchar
|
|
276
|
+
result << 14
|
|
277
|
+
|
|
278
|
+
result
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private
|
|
282
|
+
|
|
283
|
+
# Encode integer for Type 1 CharString
|
|
284
|
+
#
|
|
285
|
+
# Type 1 CharStrings use a variable-length integer encoding:
|
|
286
|
+
# - Numbers from -107 to 107: single byte (byte + 139)
|
|
287
|
+
# - Larger numbers: escaped with 255, then 2-byte value
|
|
288
|
+
#
|
|
289
|
+
# @param num [Integer] Number to encode
|
|
290
|
+
# @return [String] Encoded bytes
|
|
291
|
+
def encode_number(num)
|
|
292
|
+
if num >= -107 && num <= 107
|
|
293
|
+
[num + 139].pack("C")
|
|
294
|
+
else
|
|
295
|
+
# Use escape sequence (255) followed by 2-byte signed integer
|
|
296
|
+
num += 32768 if num < 0
|
|
297
|
+
[255, num % 256, num >> 8].pack("C*")
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
@@ -137,6 +137,34 @@ module Fontisan
|
|
|
137
137
|
@raw_data[key]
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
+
# Convert FontDictionary to Type 1 text format
|
|
141
|
+
#
|
|
142
|
+
# Generates the PostScript code for the Font Dictionary section
|
|
143
|
+
# of a Type 1 font.
|
|
144
|
+
#
|
|
145
|
+
# @return [String] Type 1 Font Dictionary text
|
|
146
|
+
#
|
|
147
|
+
# @example Generate Type 1 format
|
|
148
|
+
# dict = FontDictionary.new
|
|
149
|
+
# puts dict.to_type1_format
|
|
150
|
+
def to_type1_format
|
|
151
|
+
result = []
|
|
152
|
+
result << "% Type 1 Font Dictionary"
|
|
153
|
+
result << "12 dict begin"
|
|
154
|
+
result << ""
|
|
155
|
+
result << "/FontType 1 def"
|
|
156
|
+
result << "/PaintType #{@paint_type} def"
|
|
157
|
+
result << font_matrix_to_type1
|
|
158
|
+
result << font_bbox_to_type1
|
|
159
|
+
result << ""
|
|
160
|
+
result << font_info_to_type1
|
|
161
|
+
result << ""
|
|
162
|
+
result << "currentdict end"
|
|
163
|
+
result << "/FontName #{@font_name} def"
|
|
164
|
+
|
|
165
|
+
result.join("\n")
|
|
166
|
+
end
|
|
167
|
+
|
|
140
168
|
private
|
|
141
169
|
|
|
142
170
|
# Extract font dictionary from data
|
|
@@ -270,6 +298,40 @@ module Fontisan
|
|
|
270
298
|
@font_type = @raw_data[:font_type] || 1
|
|
271
299
|
end
|
|
272
300
|
|
|
301
|
+
# Format FontMatrix for Type 1 output
|
|
302
|
+
#
|
|
303
|
+
# @return [String] Formatted FontMatrix definition
|
|
304
|
+
def font_matrix_to_type1
|
|
305
|
+
"/FontMatrix [#{@font_matrix.join(' ')}] def"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Format FontBBox for Type 1 output
|
|
309
|
+
#
|
|
310
|
+
# @return [String] Formatted FontBBox definition
|
|
311
|
+
def font_bbox_to_type1
|
|
312
|
+
"/FontBBox {#{@font_b_box.join(' ')}} def"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Format FontInfo for Type 1 output
|
|
316
|
+
#
|
|
317
|
+
# @return [String] Formatted FontInfo definitions
|
|
318
|
+
def font_info_to_type1
|
|
319
|
+
info = @font_info
|
|
320
|
+
result = []
|
|
321
|
+
result << "/FullName (#{info.full_name || 'Untitled'}) readonly def" if info.full_name
|
|
322
|
+
result << "/FamilyName (#{info.family_name || 'Untitled'}) readonly def" if info.family_name
|
|
323
|
+
result << "/version (#{info.version || '001.000'}) readonly def" if info.version
|
|
324
|
+
result << "/Copyright (#{info.copyright || ''}) readonly def" if info.copyright
|
|
325
|
+
result << "/Notice (#{info.notice || ''}) readonly def" if info.notice
|
|
326
|
+
result << "/Weight (#{info.weight || 'Medium'}) readonly def" if info.weight
|
|
327
|
+
result << "/isFixedPitch #{info.is_fixed_pitch || false} def" if info.is_fixed_pitch
|
|
328
|
+
result << "/UnderlinePosition #{info.underline_position || -100} def" if info.underline_position
|
|
329
|
+
result << "/UnderlineThickness #{info.underline_thickness || 50} def" if info.underline_thickness
|
|
330
|
+
result << "/ItalicAngle #{info.italic_angle || 0} def" if info.italic_angle
|
|
331
|
+
|
|
332
|
+
result.join("\n")
|
|
333
|
+
end
|
|
334
|
+
|
|
273
335
|
# FontInfo sub-dictionary
|
|
274
336
|
#
|
|
275
337
|
# Contains font metadata such as FullName, FamilyName, version, etc.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "pfb_generator"
|
|
4
|
+
require_relative "decryptor"
|
|
4
5
|
|
|
5
6
|
module Fontisan
|
|
6
7
|
module Type1
|
|
@@ -220,17 +221,20 @@ module Fontisan
|
|
|
220
221
|
generate_simple_charstrings
|
|
221
222
|
end
|
|
222
223
|
|
|
223
|
-
#
|
|
224
|
-
|
|
224
|
+
# Build CharStrings dictionary text
|
|
225
|
+
charstrings_dict = build_charstrings_dict_text(charstrings)
|
|
225
226
|
|
|
226
|
-
#
|
|
227
|
+
# Encrypt with eexec
|
|
228
|
+
encrypted_data = Decryptor.eexec_encrypt(charstrings_dict)
|
|
229
|
+
|
|
230
|
+
# Convert encrypted data to hex representation
|
|
227
231
|
hex_lines = []
|
|
228
232
|
|
|
229
233
|
# Start hex section marker
|
|
230
234
|
hex_lines << "00" # Start binary data marker
|
|
231
235
|
|
|
232
|
-
# Encode
|
|
233
|
-
hex_string =
|
|
236
|
+
# Encode encrypted data as hex with line breaks
|
|
237
|
+
hex_string = encrypted_data.bytes.map { |b| format("%02x", b) }.join
|
|
234
238
|
|
|
235
239
|
# Split into lines of HEX_LINE_LENGTH characters
|
|
236
240
|
hex_string.scan(/.{#{HEX_LINE_LENGTH}}/o) do |line|
|
|
@@ -243,6 +247,28 @@ module Fontisan
|
|
|
243
247
|
hex_lines
|
|
244
248
|
end
|
|
245
249
|
|
|
250
|
+
# Build CharStrings dictionary text for eexec encryption
|
|
251
|
+
#
|
|
252
|
+
# @param charstrings [Hash] Glyph ID to CharString mapping
|
|
253
|
+
# @return [String] CharStrings dictionary text
|
|
254
|
+
def build_charstrings_dict_text(charstrings)
|
|
255
|
+
lines = []
|
|
256
|
+
lines << "dup /FontName get exch definefont pop"
|
|
257
|
+
lines << "begin"
|
|
258
|
+
lines << "/CharStrings #{charstrings.size} dict dup begin"
|
|
259
|
+
|
|
260
|
+
charstrings.each do |gid, charstring|
|
|
261
|
+
# Convert charstring bytes to hex representation for PostScript
|
|
262
|
+
hex_data = charstring.unpack1("H*")
|
|
263
|
+
lines << "#{gid} #{charstring.bytesize} #{hex_data} RD"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
lines << "end"
|
|
267
|
+
lines << "end"
|
|
268
|
+
lines << "put"
|
|
269
|
+
lines.join("\n")
|
|
270
|
+
end
|
|
271
|
+
|
|
246
272
|
# Generate simple CharStrings (without curve conversion)
|
|
247
273
|
#
|
|
248
274
|
# @return [Hash<Integer, String>] Glyph ID to CharString mapping
|
|
@@ -36,12 +36,18 @@ module Fontisan
|
|
|
36
36
|
# @return [String] Encrypted portion as hex string
|
|
37
37
|
attr_reader :encrypted_hex
|
|
38
38
|
|
|
39
|
+
# @return [String] Encrypted portion as binary data
|
|
40
|
+
attr_reader :encrypted_binary
|
|
41
|
+
|
|
39
42
|
# @return [String] Trailing text after zeros (if any)
|
|
40
43
|
attr_reader :trailing_text
|
|
41
44
|
|
|
42
45
|
# Parse PFA format data
|
|
43
46
|
#
|
|
44
|
-
#
|
|
47
|
+
# Handles both standard PFA (hex-encoded encrypted data with zero marker)
|
|
48
|
+
# and .t1 format (binary encrypted data without zero marker).
|
|
49
|
+
#
|
|
50
|
+
# @param data [String] ASCII PFA data or .t1 format data
|
|
45
51
|
# @return [PFAParser] Self for method chaining
|
|
46
52
|
# @raise [ArgumentError] If data is nil or empty
|
|
47
53
|
# @raise [Fontisan::Error] If PFA format is invalid
|
|
@@ -58,6 +64,7 @@ module Fontisan
|
|
|
58
64
|
# No eexec marker - entire file is clear text
|
|
59
65
|
@clear_text = data
|
|
60
66
|
@encrypted_hex = ""
|
|
67
|
+
@encrypted_binary = ""
|
|
61
68
|
@trailing_text = ""
|
|
62
69
|
return self
|
|
63
70
|
end
|
|
@@ -72,25 +79,31 @@ module Fontisan
|
|
|
72
79
|
encrypted_start = skip_whitespace(after_eexec, 0)
|
|
73
80
|
encrypted_data = after_eexec[encrypted_start..]
|
|
74
81
|
|
|
75
|
-
# Find zero marker
|
|
82
|
+
# Find zero marker (optional for .t1 format)
|
|
76
83
|
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
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
if zero_index
|
|
86
|
+
# Standard PFA format with zero marker
|
|
87
|
+
# Extract encrypted hex data (before zeros)
|
|
88
|
+
@encrypted_hex = encrypted_data[0...zero_index].strip
|
|
89
|
+
@encrypted_binary = [@encrypted_hex.gsub(/\s/, "")].pack("H*")
|
|
90
|
+
|
|
91
|
+
# Extract trailing text (after zeros)
|
|
92
|
+
trailing_start = zero_index + ZERO_MARKER.length
|
|
93
|
+
trailing_start = skip_whitespace(encrypted_data, trailing_start)
|
|
94
|
+
|
|
95
|
+
@trailing_text = if trailing_start < encrypted_data.length
|
|
96
|
+
encrypted_data[trailing_start..]
|
|
97
|
+
else
|
|
98
|
+
""
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
# .t1 format - binary encrypted data without zero marker
|
|
102
|
+
# Treat everything after eexec as binary encrypted data
|
|
103
|
+
@encrypted_binary = encrypted_data.lstrip
|
|
104
|
+
@encrypted_hex = @encrypted_binary.unpack1("H*")
|
|
105
|
+
@trailing_text = ""
|
|
106
|
+
end
|
|
94
107
|
|
|
95
108
|
self
|
|
96
109
|
end
|
|
@@ -120,18 +133,6 @@ module Fontisan
|
|
|
120
133
|
data.include?("%!PS-Adobe-3.0 Resource-Font")
|
|
121
134
|
end
|
|
122
135
|
|
|
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
136
|
private
|
|
136
137
|
|
|
137
138
|
# Normalize line endings to LF
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "upm_scaler"
|
|
4
4
|
require_relative "ttf_to_type1_converter"
|
|
5
|
+
require_relative "decryptor"
|
|
5
6
|
require_relative "../tables/name"
|
|
6
7
|
|
|
7
8
|
module Fontisan
|
|
@@ -197,9 +198,31 @@ module Fontisan
|
|
|
197
198
|
"/CharStrings #{@charstrings&.size || 0} dict dup begin\nend"
|
|
198
199
|
end
|
|
199
200
|
|
|
201
|
+
# Build CharStrings dictionary text for eexec encryption
|
|
202
|
+
#
|
|
203
|
+
# @param charstrings [Hash] Glyph ID to CharString mapping
|
|
204
|
+
# @return [String] CharStrings dictionary text
|
|
205
|
+
def build_charstrings_dict_text(charstrings)
|
|
206
|
+
lines = []
|
|
207
|
+
lines << "dup /FontName get exch definefont pop"
|
|
208
|
+
lines << "begin"
|
|
209
|
+
lines << "/CharStrings #{charstrings.size} dict dup begin"
|
|
210
|
+
|
|
211
|
+
charstrings.each do |gid, charstring|
|
|
212
|
+
# Convert charstring bytes to hex representation for PostScript
|
|
213
|
+
hex_data = charstring.unpack1("H*")
|
|
214
|
+
lines << "#{gid} #{charstring.bytesize} #{hex_data} RD"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
lines << "end"
|
|
218
|
+
lines << "end"
|
|
219
|
+
lines << "put"
|
|
220
|
+
lines.join("\n")
|
|
221
|
+
end
|
|
222
|
+
|
|
200
223
|
# Build binary segment (CharStrings)
|
|
201
224
|
#
|
|
202
|
-
# @return [String] Binary CharString data
|
|
225
|
+
# @return [String] Binary CharString data (encrypted with eexec)
|
|
203
226
|
def build_binary_segment
|
|
204
227
|
# Convert glyphs to Type 1 CharStrings
|
|
205
228
|
charstrings = if @convert_curves
|
|
@@ -209,11 +232,11 @@ module Fontisan
|
|
|
209
232
|
generate_simple_charstrings
|
|
210
233
|
end
|
|
211
234
|
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
# TODO: Implement eexec encryption
|
|
235
|
+
# Build CharStrings dictionary text
|
|
236
|
+
charstrings_dict = build_charstrings_dict_text(charstrings)
|
|
215
237
|
|
|
216
|
-
|
|
238
|
+
# Encrypt with eexec
|
|
239
|
+
Decryptor.eexec_encrypt(charstrings_dict)
|
|
217
240
|
end
|
|
218
241
|
|
|
219
242
|
# Generate simple CharStrings (without curve conversion)
|
|
@@ -163,6 +163,63 @@ module Fontisan
|
|
|
163
163
|
!@stem_snap_h.empty? || !@stem_snap_v.empty?
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
+
# Convert PrivateDict to Type 1 text format
|
|
167
|
+
#
|
|
168
|
+
# Generates the PostScript code for the Private dictionary section
|
|
169
|
+
# of a Type 1 font.
|
|
170
|
+
#
|
|
171
|
+
# @return [String] Type 1 Private dictionary text
|
|
172
|
+
#
|
|
173
|
+
# @example Generate Type 1 format
|
|
174
|
+
# priv = PrivateDict.new
|
|
175
|
+
# priv.blue_values = [-10, 0, 470, 480]
|
|
176
|
+
# puts priv.to_type1_format
|
|
177
|
+
def to_type1_format
|
|
178
|
+
result = []
|
|
179
|
+
result << array_to_type1(:BlueValues, @blue_values) unless @blue_values.empty?
|
|
180
|
+
result << array_to_type1(:OtherBlues, @other_blues) unless @other_blues.empty?
|
|
181
|
+
result << array_to_type1(:FamilyBlues, @family_blues) unless @family_blues.empty?
|
|
182
|
+
result << array_to_type1(:FamilyOtherBlues, @family_other_blues) unless @family_other_blues.empty?
|
|
183
|
+
result << scalar_to_type1(:BlueScale, @blue_scale)
|
|
184
|
+
result << scalar_to_type1(:BlueShift, @blue_shift)
|
|
185
|
+
result << scalar_to_type1(:BlueFuzz, @blue_fuzz)
|
|
186
|
+
result << array_to_type1(:StdHW, @std_hw) unless @std_hw.empty?
|
|
187
|
+
result << array_to_type1(:StdVW, @std_vw) unless @std_vw.empty?
|
|
188
|
+
result << array_to_type1(:StemSnapH, @stem_snap_h) unless @stem_snap_h.empty?
|
|
189
|
+
result << array_to_type1(:StemSnapV, @stem_snap_v) unless @stem_snap_v.empty?
|
|
190
|
+
result << boolean_to_type1(:ForceBold, @force_bold) unless @force_bold == false
|
|
191
|
+
result << scalar_to_type1(:lenIV, @len_iv)
|
|
192
|
+
|
|
193
|
+
result.join("\n")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Format an array value for Type 1 output
|
|
197
|
+
#
|
|
198
|
+
# @param name [Symbol] Array name
|
|
199
|
+
# @param value [Array] Array value
|
|
200
|
+
# @return [String] Formatted Type 1 array definition
|
|
201
|
+
def array_to_type1(name, value)
|
|
202
|
+
"/#{name} [#{value.join(' ')}] def"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Format a scalar value for Type 1 output
|
|
206
|
+
#
|
|
207
|
+
# @param name [Symbol] Value name
|
|
208
|
+
# @param value [Numeric] Numeric value
|
|
209
|
+
# @return [String] Formatted Type 1 scalar definition
|
|
210
|
+
def scalar_to_type1(name, value)
|
|
211
|
+
"/#{name} #{value} def"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Format a boolean value for Type 1 output
|
|
215
|
+
#
|
|
216
|
+
# @param name [Symbol] Value name
|
|
217
|
+
# @param value [Boolean] Boolean value
|
|
218
|
+
# @return [String] Formatted Type 1 boolean definition
|
|
219
|
+
def boolean_to_type1(name, value)
|
|
220
|
+
"/#{name} #{value} def"
|
|
221
|
+
end
|
|
222
|
+
|
|
166
223
|
private
|
|
167
224
|
|
|
168
225
|
# Extract private dictionary from data
|