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,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
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# Converter for Type 1 CharStrings to CFF CharStrings
|
|
6
|
+
#
|
|
7
|
+
# [`CharStringConverter`](lib/fontisan/type1/charstring_converter.rb) converts
|
|
8
|
+
# Type 1 CharString bytecode to CFF (Compact Font Format) CharString bytecode.
|
|
9
|
+
#
|
|
10
|
+
# Type 1 and CFF use similar stack-based languages but have different operator
|
|
11
|
+
# codes and some structural differences:
|
|
12
|
+
# - Operator codes differ between formats
|
|
13
|
+
# - Type 1 has seac operator for composites; CFF doesn't support it
|
|
14
|
+
# - Hint operators need to be preserved with code translation
|
|
15
|
+
#
|
|
16
|
+
# @example Convert a Type 1 CharString to CFF
|
|
17
|
+
# converter = Fontisan::Type1::CharStringConverter.new
|
|
18
|
+
# cff_charstring = converter.convert(type1_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 CharStringConverter
|
|
23
|
+
# Type 1 to CFF operator mapping
|
|
24
|
+
#
|
|
25
|
+
# Maps Type 1 operator codes to CFF operator codes.
|
|
26
|
+
# Some operators have the same code, others differ.
|
|
27
|
+
TYPE1_TO_CFF = {
|
|
28
|
+
# Path construction operators
|
|
29
|
+
hmoveto: 22, # Type 1: 22, CFF: 22
|
|
30
|
+
vmoveto: 4, # Type 1: 4, CFF: 4
|
|
31
|
+
rlineto: 5, # Type 1: 5, CFF: 5
|
|
32
|
+
hlineto: 6, # Type 1: 6, CFF: 6
|
|
33
|
+
vlineto: 7, # Type 1: 7, CFF: 7
|
|
34
|
+
rrcurveto: 8, # Type 1: 8, CFF: 8
|
|
35
|
+
hhcurveto: 27, # Type 1: 27, CFF: 27
|
|
36
|
+
hvcurveto: 31, # Type 1: 31, CFF: 31
|
|
37
|
+
vhcurveto: 30, # Type 1: 30, CFF: 30
|
|
38
|
+
rcurveline: 24, # Type 1: 24, CFF: 24
|
|
39
|
+
rlinecurve: 25, # Type 1: 25, CFF: 25
|
|
40
|
+
|
|
41
|
+
# Hint operators
|
|
42
|
+
hstem: 1, # Type 1: 1, CFF: 1
|
|
43
|
+
vstem: 3, # Type 1: 3, CFF: 3
|
|
44
|
+
hstemhm: 18, # Type 1: 18, CFF: 18
|
|
45
|
+
vstemhm: 23, # Type 1: 23, CFF: 23
|
|
46
|
+
|
|
47
|
+
# Hint substitution (not in Type 1, but we preserve for compatibility)
|
|
48
|
+
hintmask: 19, # Type 1: N/A, CFF: 19
|
|
49
|
+
cntrmask: 20, # Type 1: N/A, CFF: 20
|
|
50
|
+
|
|
51
|
+
# End char
|
|
52
|
+
endchar: 14, # Type 1: 14 (or 11 in some specs), CFF: 14
|
|
53
|
+
|
|
54
|
+
# Miscellaneous
|
|
55
|
+
callsubr: 10, # Type 1: 10, CFF: 10
|
|
56
|
+
return: 11, # Type 1: 11, CFF: 11
|
|
57
|
+
|
|
58
|
+
# Deprecated operators (preserve for compatibility)
|
|
59
|
+
hstem3: 12, # Type 1: 12 (escape 0), CFF: 12 (escape 0)
|
|
60
|
+
vstem3: 13, # Type 1: 13 (escape 1), CFF: 13 (escape 1)
|
|
61
|
+
seac: 12, # Type 1: 12 (escape 6), CFF: Not supported
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
# Escape code for two-byte operators
|
|
65
|
+
ESCAPE_BYTE = 12
|
|
66
|
+
|
|
67
|
+
# seac operator escape code (second byte)
|
|
68
|
+
SEAC_ESCAPE_CODE = 6
|
|
69
|
+
|
|
70
|
+
# Initialize a new CharStringConverter
|
|
71
|
+
#
|
|
72
|
+
# @param charstrings [CharStrings, nil] CharStrings dictionary for seac expansion
|
|
73
|
+
def initialize(charstrings = nil)
|
|
74
|
+
@charstrings = charstrings
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Convert Type 1 CharString to CFF CharString
|
|
78
|
+
#
|
|
79
|
+
# @param type1_charstring [String] Type 1 CharString bytecode
|
|
80
|
+
# @return [String] CFF CharString bytecode
|
|
81
|
+
#
|
|
82
|
+
# @example Convert a CharString
|
|
83
|
+
# converter = Fontisan::Type1::CharStringConverter.new
|
|
84
|
+
# cff_bytes = converter.convert(type1_bytes)
|
|
85
|
+
def convert(type1_charstring)
|
|
86
|
+
# Parse Type 1 CharString into commands
|
|
87
|
+
parser = Type1::CharStrings::CharStringParser.new
|
|
88
|
+
commands = parser.parse(type1_charstring)
|
|
89
|
+
|
|
90
|
+
# Check for seac operator and expand if needed
|
|
91
|
+
if parser.seac_components
|
|
92
|
+
return expand_seac(parser.seac_components)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Convert commands to CFF format
|
|
96
|
+
convert_commands(commands)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Convert parsed commands to CFF CharString
|
|
100
|
+
#
|
|
101
|
+
# @param commands [Array<Array>] Parsed Type 1 commands
|
|
102
|
+
# @return [String] CFF CharString bytecode
|
|
103
|
+
def convert_commands(commands)
|
|
104
|
+
result = String.new(encoding: Encoding::ASCII_8BIT)
|
|
105
|
+
|
|
106
|
+
commands.each do |command|
|
|
107
|
+
case command[0]
|
|
108
|
+
when :number
|
|
109
|
+
# Encode number in CFF format
|
|
110
|
+
result << encode_cff_number(command[1])
|
|
111
|
+
when :seac
|
|
112
|
+
# seac should be expanded before this point
|
|
113
|
+
raise Fontisan::Error,
|
|
114
|
+
"seac operator not supported in CFF, must be expanded first"
|
|
115
|
+
else
|
|
116
|
+
# Convert operator
|
|
117
|
+
op_code = TYPE1_TO_CFF[command[0]]
|
|
118
|
+
if op_code.nil?
|
|
119
|
+
# Unknown operator, skip or raise error
|
|
120
|
+
next
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
result << encode_cff_operator(op_code)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
result
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Expand seac composite glyph
|
|
131
|
+
#
|
|
132
|
+
# The seac operator in Type 1 creates composite glyphs (like À = A + `).
|
|
133
|
+
# CFF doesn't support seac, so we need to expand it into the base glyphs
|
|
134
|
+
# with appropriate positioning.
|
|
135
|
+
#
|
|
136
|
+
# @param seac_data [Hash] seac component data
|
|
137
|
+
# @return [String] CFF CharString bytecode with expanded seac
|
|
138
|
+
def expand_seac(seac_data)
|
|
139
|
+
# seac format: adx ady bchar achar seac
|
|
140
|
+
# adx, ady: accent offset
|
|
141
|
+
# bchar: base character code
|
|
142
|
+
# achar: accent character code
|
|
143
|
+
# The accent is positioned at (adx, ady) relative to the base
|
|
144
|
+
|
|
145
|
+
seac_data[:base]
|
|
146
|
+
seac_data[:accent]
|
|
147
|
+
seac_data[:adx]
|
|
148
|
+
seac_data[:ady]
|
|
149
|
+
|
|
150
|
+
# For now, we'll create a simple placeholder that indicates seac expansion
|
|
151
|
+
# In a full implementation, we would:
|
|
152
|
+
# 1. Parse the base glyph's CharString
|
|
153
|
+
# 2. Parse the accent glyph's CharString
|
|
154
|
+
# 3. Merge them with the appropriate offset
|
|
155
|
+
# 4. Convert to CFF format
|
|
156
|
+
|
|
157
|
+
# This is a simplified implementation that creates a composite reference
|
|
158
|
+
# CFF doesn't have native seac, so we need to actually merge the outlines
|
|
159
|
+
|
|
160
|
+
# For now, return endchar as placeholder
|
|
161
|
+
# TODO: Implement full seac expansion by merging glyph outlines
|
|
162
|
+
encode_cff_operator(TYPE1_TO_CFF[:endchar])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Check if CharString contains seac operator
|
|
166
|
+
#
|
|
167
|
+
# @param type1_charstring [String] Type 1 CharString bytecode
|
|
168
|
+
# @return [Boolean] True if CharString contains seac
|
|
169
|
+
def seac?(type1_charstring)
|
|
170
|
+
parser = Type1::CharStrings::CharStringParser.new
|
|
171
|
+
parser.parse(type1_charstring)
|
|
172
|
+
!parser.seac_components.nil?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
# Encode number in CFF format
|
|
178
|
+
#
|
|
179
|
+
# CFF uses a variable-length encoding for numbers:
|
|
180
|
+
# - 32-246: 1 byte (value - 139)
|
|
181
|
+
# - 247-250: 2 bytes (first byte indicates format)
|
|
182
|
+
# - 251-254: 3 bytes (first byte indicates format)
|
|
183
|
+
# - 255: 5 bytes (signed 16-bit integer)
|
|
184
|
+
# - 28: 2 bytes (signed 16.16 fixed point, not used for CharStrings)
|
|
185
|
+
#
|
|
186
|
+
# @param value [Integer] Number to encode
|
|
187
|
+
# @return [String] Encoded number bytes
|
|
188
|
+
def encode_cff_number(value)
|
|
189
|
+
result = String.new(encoding: Encoding::ASCII_8BIT)
|
|
190
|
+
|
|
191
|
+
if value >= -107 && value <= 107
|
|
192
|
+
# 1-byte number: value + 139
|
|
193
|
+
result << (value + 139).chr
|
|
194
|
+
elsif value >= 108 && value <= 1131
|
|
195
|
+
# 2-byte positive number
|
|
196
|
+
value -= 108
|
|
197
|
+
result << ((value >> 8) + 247).chr
|
|
198
|
+
result << (value & 0xFF).chr
|
|
199
|
+
elsif value >= -1131 && value <= -108
|
|
200
|
+
# 2-byte negative number
|
|
201
|
+
value = -value - 108
|
|
202
|
+
result << ((value >> 8) + 251).chr
|
|
203
|
+
result << (value & 0xFF).chr
|
|
204
|
+
elsif value >= -32768 && value <= 32767
|
|
205
|
+
# 5-byte number (16-bit integer)
|
|
206
|
+
result << 255.chr
|
|
207
|
+
result << [(value >> 8) & 0xFF, value & 0xFF].pack("CC")
|
|
208
|
+
result << [0, 0].pack("CC") # Pad to 5 bytes
|
|
209
|
+
else
|
|
210
|
+
raise Fontisan::Error,
|
|
211
|
+
"Number out of range for CFF encoding: #{value}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
result
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Encode operator in CFF format
|
|
218
|
+
#
|
|
219
|
+
# Most operators are single-byte. Some use escape byte (12) followed
|
|
220
|
+
# by a second byte.
|
|
221
|
+
#
|
|
222
|
+
# @param op_code [Integer] Operator code
|
|
223
|
+
# @return [String] Encoded operator bytes
|
|
224
|
+
def encode_cff_operator(op_code)
|
|
225
|
+
result = String.new(encoding: Encoding::ASCII_8BIT)
|
|
226
|
+
|
|
227
|
+
if op_code > 31 && op_code != ESCAPE_BYTE
|
|
228
|
+
# Two-byte operator (escape + code)
|
|
229
|
+
result << ESCAPE_BYTE.chr
|
|
230
|
+
result << (op_code - ESCAPE_BYTE).chr
|
|
231
|
+
else
|
|
232
|
+
# Single-byte operator
|
|
233
|
+
result << op_code.chr
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
result
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|