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,501 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# Expands Type 1 seac composite glyphs into base + accent outlines
|
|
6
|
+
#
|
|
7
|
+
# [`SeacExpander`](lib/fontisan/type1/seac_expander.rb) handles the
|
|
8
|
+
# decomposition of Type 1 composite glyphs that use the `seac` operator.
|
|
9
|
+
# The seac operator combines two glyphs (a base character and an accent)
|
|
10
|
+
# with a positioning offset, which must be decomposed for CFF conversion.
|
|
11
|
+
#
|
|
12
|
+
# The seac operator format is:
|
|
13
|
+
# ```
|
|
14
|
+
# seac asb adx ady bchar achar
|
|
15
|
+
# ```
|
|
16
|
+
#
|
|
17
|
+
# Where:
|
|
18
|
+
# - `asb`: Accent side bearing (not used in decomposition)
|
|
19
|
+
# - `adx`: X offset for accent placement
|
|
20
|
+
# - `ady`: Y offset for accent placement
|
|
21
|
+
# - `bchar`: Character code of base glyph
|
|
22
|
+
# - `achar`: Character code of accent glyph
|
|
23
|
+
#
|
|
24
|
+
# @example Decompose a seac composite
|
|
25
|
+
# expander = Fontisan::Type1::SeacExpander.new(charstrings, private_dict)
|
|
26
|
+
# decomposed = expander.decompose("Agrave")
|
|
27
|
+
# # => Returns merged outline of base 'A' + accent 'grave'
|
|
28
|
+
#
|
|
29
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
30
|
+
class SeacExpander
|
|
31
|
+
# @return [CharStrings] Type 1 CharStrings dictionary
|
|
32
|
+
attr_reader :charstrings
|
|
33
|
+
|
|
34
|
+
# @return [PrivateDict] Private dictionary for hinting
|
|
35
|
+
attr_reader :private_dict
|
|
36
|
+
|
|
37
|
+
# Initialize a new SeacExpander
|
|
38
|
+
#
|
|
39
|
+
# @param charstrings [CharStrings] Type 1 CharStrings dictionary
|
|
40
|
+
# @param private_dict [PrivateDict] Private dictionary for context
|
|
41
|
+
def initialize(charstrings, private_dict)
|
|
42
|
+
@charstrings = charstrings
|
|
43
|
+
@private_dict = private_dict
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Decompose a seac composite glyph into base + accent outlines
|
|
47
|
+
#
|
|
48
|
+
# This method:
|
|
49
|
+
# 1. Parses the seac operator to extract components
|
|
50
|
+
# 2. Gets CharStrings for base and accent glyphs
|
|
51
|
+
# 3. Parses both CharStrings into outline commands
|
|
52
|
+
# 4. Transforms the accent by (adx, ady) offset
|
|
53
|
+
# 5. Merges base and accent outlines into a single path
|
|
54
|
+
# 6. Returns the decomposed CharString data
|
|
55
|
+
#
|
|
56
|
+
# @param glyph_name [String] Name of the composite glyph to decompose
|
|
57
|
+
# @return [String, nil] Decomposed CharString bytecode, or nil if not a seac composite
|
|
58
|
+
# @raise [Fontisan::Error] If base or accent glyphs are not found
|
|
59
|
+
#
|
|
60
|
+
# @example Decompose "Agrave" glyph
|
|
61
|
+
# expander.decompose("Agrave")
|
|
62
|
+
def decompose(glyph_name)
|
|
63
|
+
components = @charstrings.components_for(glyph_name)
|
|
64
|
+
return nil unless components
|
|
65
|
+
|
|
66
|
+
# Use the encoding map to lookup glyph names from character codes
|
|
67
|
+
base_glyph_name = @charstrings.encoding[components[:base]]
|
|
68
|
+
accent_glyph_name = @charstrings.encoding[components[:accent]]
|
|
69
|
+
|
|
70
|
+
if base_glyph_name.nil?
|
|
71
|
+
raise Fontisan::Error, "Base glyph for char code #{components[:base]} not found"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if accent_glyph_name.nil?
|
|
75
|
+
raise Fontisan::Error, "Accent glyph for char code #{components[:accent]} not found"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get CharStrings for base and accent
|
|
79
|
+
base_charstring = @charstrings[base_glyph_name]
|
|
80
|
+
accent_charstring = @charstrings[accent_glyph_name]
|
|
81
|
+
|
|
82
|
+
if base_charstring.nil?
|
|
83
|
+
raise Fontisan::Error, "CharString not found for base glyph #{base_glyph_name}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if accent_charstring.nil?
|
|
87
|
+
raise Fontisan::Error, "CharString not found for accent glyph #{accent_glyph_name}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Parse both CharStrings into command sequences
|
|
91
|
+
base_commands = parse_charstring_to_commands(base_charstring)
|
|
92
|
+
accent_commands = parse_charstring_to_commands(accent_charstring)
|
|
93
|
+
|
|
94
|
+
# Transform accent by (adx, ady) offset
|
|
95
|
+
accent_commands = transform_commands(accent_commands, components[:adx], components[:ady])
|
|
96
|
+
|
|
97
|
+
# Merge base and accent commands
|
|
98
|
+
merged_commands = merge_outline_commands(base_commands, accent_commands)
|
|
99
|
+
|
|
100
|
+
# Convert merged commands back to CharString bytecode
|
|
101
|
+
generate_charstring(merged_commands)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check if a glyph is a seac composite
|
|
105
|
+
#
|
|
106
|
+
# @param glyph_name [String] Glyph name to check
|
|
107
|
+
# @return [Boolean] True if the glyph uses seac operator
|
|
108
|
+
def composite?(glyph_name)
|
|
109
|
+
@charstrings.composite?(glyph_name)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get all seac composite glyphs in the font
|
|
113
|
+
#
|
|
114
|
+
# @return [Array<String>] List of composite glyph names
|
|
115
|
+
def composite_glyphs
|
|
116
|
+
@charstrings.glyph_names.select { |name| composite?(name) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# Parse Type 1 CharString into drawing commands
|
|
122
|
+
#
|
|
123
|
+
# Converts Type 1 CharString bytecode into a list of drawing commands
|
|
124
|
+
# that can be manipulated and transformed.
|
|
125
|
+
#
|
|
126
|
+
# @param charstring [String] Binary CharString data
|
|
127
|
+
# @return [Array<Hash>] Array of command hashes
|
|
128
|
+
def parse_charstring_to_commands(charstring)
|
|
129
|
+
parser = CharStrings::CharStringParser.new(@private_dict)
|
|
130
|
+
parser.parse(charstring)
|
|
131
|
+
# Preprocess to combine numbers with operators
|
|
132
|
+
combined_commands = combine_numbers_with_operators(parser.commands)
|
|
133
|
+
commands_to_outline(combined_commands)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Combine separate number commands with their operators
|
|
137
|
+
#
|
|
138
|
+
# The CharStringParser produces commands like [[:number, 100], [:number, 0], [:hsbw]]
|
|
139
|
+
# This method combines them into [[:hsbw, 100, 0]]
|
|
140
|
+
#
|
|
141
|
+
# @param commands [Array] Parser commands
|
|
142
|
+
# @return [Array] Combined commands
|
|
143
|
+
def combine_numbers_with_operators(commands)
|
|
144
|
+
result = []
|
|
145
|
+
number_stack = []
|
|
146
|
+
|
|
147
|
+
commands.each do |cmd|
|
|
148
|
+
if cmd[0] == :number
|
|
149
|
+
number_stack << cmd[1]
|
|
150
|
+
elsif cmd[0] == :seac
|
|
151
|
+
# seac is special - it uses the number stack for components
|
|
152
|
+
result << cmd
|
|
153
|
+
parse_seac_from_stack(result, number_stack)
|
|
154
|
+
else
|
|
155
|
+
# Operator - pop required numbers from stack
|
|
156
|
+
operator = cmd[0]
|
|
157
|
+
args = get_args_for_operator(operator, number_stack)
|
|
158
|
+
result << [operator, *args] if args
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
result
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Get arguments for an operator from the number stack
|
|
166
|
+
#
|
|
167
|
+
# @param operator [Symbol] Operator symbol
|
|
168
|
+
# @param stack [Array] Number stack
|
|
169
|
+
# @return [Array, nil] Arguments or nil if operator unknown
|
|
170
|
+
def get_args_for_operator(operator, stack)
|
|
171
|
+
case operator
|
|
172
|
+
when :hsbw
|
|
173
|
+
# hsbw takes 2 args: sbw, width
|
|
174
|
+
stack.pop(2).reverse
|
|
175
|
+
when :sbw
|
|
176
|
+
# sbw takes 3 args: sbw, wy, wx
|
|
177
|
+
stack.pop(3).reverse
|
|
178
|
+
when :rmoveto
|
|
179
|
+
# rmoveto takes 2 args: dy, dx
|
|
180
|
+
stack.pop(2).reverse
|
|
181
|
+
when :hmoveto
|
|
182
|
+
# hmoveto takes 1 arg: dx
|
|
183
|
+
stack.pop(1)
|
|
184
|
+
when :vmoveto
|
|
185
|
+
# vmoveto takes 1 arg: dy
|
|
186
|
+
stack.pop(1)
|
|
187
|
+
when :rlineto
|
|
188
|
+
# rlineto takes 2 args: dy, dx
|
|
189
|
+
stack.pop(2).reverse
|
|
190
|
+
when :hlineto
|
|
191
|
+
# hlineto takes 1 arg: dx
|
|
192
|
+
stack.pop(1)
|
|
193
|
+
when :vlineto
|
|
194
|
+
# vlineto takes 1 arg: dy
|
|
195
|
+
stack.pop(1)
|
|
196
|
+
when :rrcurveto
|
|
197
|
+
# rrcurveto takes 6 args: dy3, dx3, dy2, dx2, dy1, dx1
|
|
198
|
+
stack.pop(6).reverse
|
|
199
|
+
when :vhcurveto, :hvcurveto
|
|
200
|
+
# vhcurveto/hvcurveto take 4 args
|
|
201
|
+
stack.pop(4).reverse
|
|
202
|
+
when :hstem, :vstem, :callsubr, :return, :endchar
|
|
203
|
+
# These operators don't need args for outline conversion
|
|
204
|
+
[]
|
|
205
|
+
else
|
|
206
|
+
# Unknown operator - return nil
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Parse seac components from number stack
|
|
212
|
+
#
|
|
213
|
+
# @param result [Array] Result array to append to
|
|
214
|
+
# @param stack [Array] Number stack
|
|
215
|
+
def parse_seac_from_stack(result, stack)
|
|
216
|
+
return if stack.length < 5
|
|
217
|
+
|
|
218
|
+
# Last 5 numbers are: asb, adx, ady, bchar, achar
|
|
219
|
+
args = stack.pop(5).reverse
|
|
220
|
+
result.last[1] = args[1] # adx
|
|
221
|
+
result.last[2] = args[2] # ady
|
|
222
|
+
result.last[3] = args[3] # bchar
|
|
223
|
+
result.last[4] = args[4] # achar
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Convert parser commands to outline format
|
|
227
|
+
#
|
|
228
|
+
# @param commands [Array] Parser command format [type, *args]
|
|
229
|
+
# @return [Array<Hash>] Outline command format
|
|
230
|
+
def commands_to_outline(commands)
|
|
231
|
+
outline_commands = []
|
|
232
|
+
x = 0
|
|
233
|
+
y = 0
|
|
234
|
+
|
|
235
|
+
commands.each do |cmd|
|
|
236
|
+
case cmd[0]
|
|
237
|
+
when :hsbw
|
|
238
|
+
# hsbw x0 sbw: Set horizontal width and left sidebearing
|
|
239
|
+
# This is usually the first command in a CharString
|
|
240
|
+
x = cmd[1]
|
|
241
|
+
y = 0
|
|
242
|
+
outline_commands << { type: :move_to, x: x, y: y }
|
|
243
|
+
when :sbw
|
|
244
|
+
# sbw sbw x0 sbw: Set width and side bearings (vertical)
|
|
245
|
+
y = cmd[1]
|
|
246
|
+
x = cmd[2]
|
|
247
|
+
outline_commands << { type: :move_to, x: x, y: y }
|
|
248
|
+
when :rmoveto
|
|
249
|
+
# rmoveto dx dy: Relative move to
|
|
250
|
+
x += cmd[1]
|
|
251
|
+
y += cmd[2]
|
|
252
|
+
outline_commands << { type: :line_to, x: x, y: y }
|
|
253
|
+
when :hmoveto
|
|
254
|
+
# hmoveto dx: Horizontal move to
|
|
255
|
+
x += cmd[1]
|
|
256
|
+
outline_commands << { type: :line_to, x: x, y: y }
|
|
257
|
+
when :vmoveto
|
|
258
|
+
# vmoveto dy: Vertical move to
|
|
259
|
+
y += cmd[1]
|
|
260
|
+
outline_commands << { type: :line_to, x: x, y: y }
|
|
261
|
+
when :rlineto
|
|
262
|
+
# rlineto dx dy: Relative line to
|
|
263
|
+
x += cmd[1]
|
|
264
|
+
y += cmd[2]
|
|
265
|
+
outline_commands << { type: :line_to, x: x, y: y }
|
|
266
|
+
when :hlineto
|
|
267
|
+
# hlineto dx: Horizontal line to
|
|
268
|
+
x += cmd[1]
|
|
269
|
+
outline_commands << { type: :line_to, x: x, y: y }
|
|
270
|
+
when :vlineto
|
|
271
|
+
# vlineto dy: Vertical line to
|
|
272
|
+
y += cmd[1]
|
|
273
|
+
outline_commands << { type: :line_to, x: x, y: y }
|
|
274
|
+
when :rrcurveto
|
|
275
|
+
# rrcurveto dx1 dy1 dx2 dy2 dx3 dy3: Relative curved line to
|
|
276
|
+
# This is a quadratic curve in Type 1
|
|
277
|
+
# Convert to our outline format (quadratic)
|
|
278
|
+
dx1, dy1, dx2, dy2, dx3, dy3 = cmd[1..6]
|
|
279
|
+
control_x = x + dx1
|
|
280
|
+
control_y = y + dy1
|
|
281
|
+
anchor_x = x + dx1 + dx2
|
|
282
|
+
anchor_y = y + dy1 + dy2
|
|
283
|
+
end_x = x + dx1 + dx2 + dx3
|
|
284
|
+
end_y = y + dy1 + dy2 + dy3
|
|
285
|
+
|
|
286
|
+
outline_commands << {
|
|
287
|
+
type: :quad_to,
|
|
288
|
+
cx: control_x,
|
|
289
|
+
cy: control_y,
|
|
290
|
+
x: end_x,
|
|
291
|
+
y: end_y
|
|
292
|
+
}
|
|
293
|
+
x = end_x
|
|
294
|
+
y = end_y
|
|
295
|
+
when :vhcurveto, :hvcurveto
|
|
296
|
+
# vhcurveto: Vertical-to-horizontal curve
|
|
297
|
+
# hvcurveto: Horizontal-to-vertical curve
|
|
298
|
+
# These are also quadratic curves
|
|
299
|
+
# For now, treat as simple curves
|
|
300
|
+
handle_curve_command(outline_commands, cmd, x, y)
|
|
301
|
+
x = outline_commands.last[:x]
|
|
302
|
+
y = outline_commands.last[:y]
|
|
303
|
+
when :endchar
|
|
304
|
+
# End of CharString
|
|
305
|
+
break
|
|
306
|
+
when :number
|
|
307
|
+
# Number - skip, consumed by operators
|
|
308
|
+
nil
|
|
309
|
+
else
|
|
310
|
+
# Unknown command - skip
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
outline_commands
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Handle curve commands (vhcurveto, hvcurveto)
|
|
319
|
+
#
|
|
320
|
+
# @param commands [Array] Command list to append to
|
|
321
|
+
# @param cmd [Array] The curve command
|
|
322
|
+
# @param x [Integer] Current X position
|
|
323
|
+
# @param y [Integer] Current Y position
|
|
324
|
+
def handle_curve_command(commands, cmd, x, y)
|
|
325
|
+
# Simplified handling - treat as quadratic curve
|
|
326
|
+
# vhcurveto: dy1 dx2 dy2 dx3
|
|
327
|
+
# hvcurveto: dx1 dy2 dx3 dy3
|
|
328
|
+
if cmd[0] == :vhcurveto
|
|
329
|
+
dy1, dx2, dy2, dx3 = cmd[1..4]
|
|
330
|
+
control_x = x
|
|
331
|
+
control_y = y + dy1
|
|
332
|
+
end_x = x + dx2
|
|
333
|
+
end_y = y + dy1 + dy2
|
|
334
|
+
else
|
|
335
|
+
dx1, dy2, dx3, dy3 = cmd[1..4]
|
|
336
|
+
control_x = x + dx1
|
|
337
|
+
control_y = y
|
|
338
|
+
end_x = x + dx1 + dx2
|
|
339
|
+
end_y = y + dy2 + dy3
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
commands << {
|
|
343
|
+
type: :quad_to,
|
|
344
|
+
cx: control_x,
|
|
345
|
+
cy: control_y,
|
|
346
|
+
x: end_x,
|
|
347
|
+
y: end_y
|
|
348
|
+
}
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Transform outline commands by translation
|
|
352
|
+
#
|
|
353
|
+
# @param commands [Array<Hash>] Outline commands
|
|
354
|
+
# @param dx [Integer] X offset
|
|
355
|
+
# @param dy [Integer] Y offset
|
|
356
|
+
# @return [Array<Hash>] Transformed commands
|
|
357
|
+
def transform_commands(commands, dx, dy)
|
|
358
|
+
return commands if dx == 0 && dy == 0
|
|
359
|
+
|
|
360
|
+
commands.map do |cmd|
|
|
361
|
+
case cmd[:type]
|
|
362
|
+
when :move_to, :line_to
|
|
363
|
+
{ type: cmd[:type], x: cmd[:x] + dx, y: cmd[:y] + dy }
|
|
364
|
+
when :quad_to
|
|
365
|
+
{
|
|
366
|
+
type: :quad_to,
|
|
367
|
+
cx: cmd[:cx] + dx,
|
|
368
|
+
cy: cmd[:cy] + dy,
|
|
369
|
+
x: cmd[:x] + dx,
|
|
370
|
+
y: cmd[:y] + dy
|
|
371
|
+
}
|
|
372
|
+
when :curve_to
|
|
373
|
+
{
|
|
374
|
+
type: :curve_to,
|
|
375
|
+
cx1: cmd[:cx1] + dx,
|
|
376
|
+
cy1: cmd[:cy1] + dy,
|
|
377
|
+
cx2: cmd[:cx2] + dx,
|
|
378
|
+
cy2: cmd[:cy2] + dy,
|
|
379
|
+
x: cmd[:x] + dx,
|
|
380
|
+
y: cmd[:y] + dy
|
|
381
|
+
}
|
|
382
|
+
else
|
|
383
|
+
cmd # close_path, etc. pass through
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Merge base and accent outline commands
|
|
389
|
+
#
|
|
390
|
+
# Combines two outline sequences into one. The accent outline is
|
|
391
|
+
# appended to the base outline with proper contour separation.
|
|
392
|
+
#
|
|
393
|
+
# @param base_commands [Array<Hash>] Base glyph commands
|
|
394
|
+
# @param accent_commands [Array<Hash>] Accent glyph commands
|
|
395
|
+
# @return [Array<Hash>] Merged commands
|
|
396
|
+
def merge_outline_commands(base_commands, accent_commands)
|
|
397
|
+
# Remove the final close_path from base (if any)
|
|
398
|
+
# Then append accent commands
|
|
399
|
+
merged = base_commands.dup
|
|
400
|
+
|
|
401
|
+
# If base ends with close_path, remove it (accent will have its own)
|
|
402
|
+
merged.pop if merged.last && merged.last[:type] == :close_path
|
|
403
|
+
|
|
404
|
+
# Add accent commands
|
|
405
|
+
merged.concat(accent_commands)
|
|
406
|
+
|
|
407
|
+
merged
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Generate CharString bytecode from outline commands
|
|
411
|
+
#
|
|
412
|
+
# Converts outline commands back to Type 1 CharString format.
|
|
413
|
+
# This is a simplified implementation that handles common cases.
|
|
414
|
+
#
|
|
415
|
+
# @param commands [Array<Hash>] Outline commands
|
|
416
|
+
# @return [String] Binary CharString data
|
|
417
|
+
def generate_charstring(commands)
|
|
418
|
+
return "" if commands.empty?
|
|
419
|
+
|
|
420
|
+
charstring = String.new(encoding: Encoding::ASCII_8BIT)
|
|
421
|
+
|
|
422
|
+
x = 0
|
|
423
|
+
y = 0
|
|
424
|
+
|
|
425
|
+
commands.each do |cmd|
|
|
426
|
+
case cmd[:type]
|
|
427
|
+
when :move_to
|
|
428
|
+
# Use hsbw to set initial position
|
|
429
|
+
dx = cmd[:x] - x
|
|
430
|
+
# hsbw is a two-byte operator: 12 34
|
|
431
|
+
charstring << encode_number(dx) # sbw value
|
|
432
|
+
charstring << encode_number(0) # width (always 0 for decomposed glyphs)
|
|
433
|
+
charstring << 12 # First byte of two-byte operator
|
|
434
|
+
charstring << 34 # Second byte - hsbw
|
|
435
|
+
x = cmd[:x]
|
|
436
|
+
y = cmd[:y]
|
|
437
|
+
when :line_to
|
|
438
|
+
dx = cmd[:x] - x
|
|
439
|
+
dy = cmd[:y] - y
|
|
440
|
+
if dx != 0 && dy != 0
|
|
441
|
+
charstring << encode_number(dx)
|
|
442
|
+
charstring << encode_number(dy)
|
|
443
|
+
charstring << 5 # rlineto
|
|
444
|
+
elsif dx != 0
|
|
445
|
+
charstring << encode_number(dx)
|
|
446
|
+
charstring << 6 # hlineto
|
|
447
|
+
elsif dy != 0
|
|
448
|
+
charstring << encode_number(dy)
|
|
449
|
+
charstring << 7 # vlineto
|
|
450
|
+
end
|
|
451
|
+
x = cmd[:x]
|
|
452
|
+
y = cmd[:y]
|
|
453
|
+
when :quad_to
|
|
454
|
+
# rrcurveto: dx1 dy1 dx2 dy2 dx3 dy3
|
|
455
|
+
dx1 = cmd[:cx] - x
|
|
456
|
+
dy1 = cmd[:cy] - y
|
|
457
|
+
# For quadratic curves, we need to convert to cubic (Type 1 rrcurveto)
|
|
458
|
+
# This is a simplified conversion
|
|
459
|
+
anchor_dx = (cmd[:x] - x) / 2 - dx1 / 2
|
|
460
|
+
anchor_dy = (cmd[:y] - y) / 2 - dy1 / 2
|
|
461
|
+
end_dx = (cmd[:x] - x) - dx1 - anchor_dx
|
|
462
|
+
end_dy = (cmd[:y] - y) - dy1 - anchor_dy
|
|
463
|
+
|
|
464
|
+
charstring << encode_number(dx1)
|
|
465
|
+
charstring << encode_number(dy1)
|
|
466
|
+
charstring << encode_number(anchor_dx)
|
|
467
|
+
charstring << encode_number(anchor_dy)
|
|
468
|
+
charstring << encode_number(end_dx)
|
|
469
|
+
charstring << encode_number(end_dy)
|
|
470
|
+
charstring << 8 # rrcurveto
|
|
471
|
+
x = cmd[:x]
|
|
472
|
+
y = cmd[:y]
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Add endchar
|
|
477
|
+
charstring << 14
|
|
478
|
+
|
|
479
|
+
charstring
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Encode integer for Type 1 CharString
|
|
483
|
+
#
|
|
484
|
+
# Type 1 CharStrings use a variable-length integer encoding:
|
|
485
|
+
# - Numbers from -107 to 107: single byte (byte + 139)
|
|
486
|
+
# - Larger numbers: escaped with 255, then 2-byte value
|
|
487
|
+
#
|
|
488
|
+
# @param num [Integer] Number to encode
|
|
489
|
+
# @return [String] Encoded bytes
|
|
490
|
+
def encode_number(num)
|
|
491
|
+
if num >= -107 && num <= 107
|
|
492
|
+
[num + 139].pack("C*")
|
|
493
|
+
else
|
|
494
|
+
# Use escape sequence (255) followed by 2-byte signed integer
|
|
495
|
+
num += 32768 if num < 0
|
|
496
|
+
[255, num % 256, num >> 8].pack("C*")
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
end
|