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
|
@@ -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
|
data/lib/fontisan/type1.rb
CHANGED
|
@@ -46,6 +46,8 @@ require_relative "type1/font_dictionary"
|
|
|
46
46
|
require_relative "type1/private_dict"
|
|
47
47
|
require_relative "type1/charstrings"
|
|
48
48
|
require_relative "type1/charstring_converter"
|
|
49
|
+
require_relative "type1/cff_to_type1_converter"
|
|
50
|
+
require_relative "type1/seac_expander"
|
|
49
51
|
|
|
50
52
|
# Infrastructure
|
|
51
53
|
require_relative "type1/upm_scaler"
|
data/lib/fontisan/type1_font.rb
CHANGED
|
@@ -37,27 +37,41 @@ module Fontisan
|
|
|
37
37
|
# @return [Symbol] Format type (:pfb or :pfa)
|
|
38
38
|
attr_reader :format
|
|
39
39
|
|
|
40
|
+
# @return [Symbol] Loading mode (:metadata or :full)
|
|
41
|
+
attr_reader :loading_mode
|
|
42
|
+
|
|
40
43
|
# @return [String, nil] Decrypted font data
|
|
41
44
|
attr_reader :decrypted_data
|
|
42
45
|
|
|
43
46
|
# @return [FontDictionary, nil] Font dictionary
|
|
44
|
-
|
|
47
|
+
def font_dictionary
|
|
48
|
+
parse_dictionaries! unless @font_dictionary
|
|
49
|
+
@font_dictionary
|
|
50
|
+
end
|
|
45
51
|
|
|
46
52
|
# @return [PrivateDict, nil] Private dictionary
|
|
47
|
-
|
|
53
|
+
def private_dict
|
|
54
|
+
parse_dictionaries! unless @private_dict
|
|
55
|
+
@private_dict
|
|
56
|
+
end
|
|
48
57
|
|
|
49
58
|
# @return [CharStrings, nil] CharStrings dictionary
|
|
50
|
-
|
|
59
|
+
def charstrings
|
|
60
|
+
parse_dictionaries! unless @charstrings
|
|
61
|
+
@charstrings
|
|
62
|
+
end
|
|
51
63
|
|
|
52
64
|
# Initialize a new Type1Font instance
|
|
53
65
|
#
|
|
54
66
|
# @param data [String] Font file data (binary or text)
|
|
55
67
|
# @param format [Symbol] Format type (:pfb or :pfa, auto-detected if nil)
|
|
56
68
|
# @param file_path [String, nil] Optional file path for reference
|
|
57
|
-
|
|
69
|
+
# @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
|
|
70
|
+
def initialize(data, format: nil, file_path: nil, mode: :full)
|
|
58
71
|
@file_path = file_path
|
|
59
72
|
@format = format || detect_format(data)
|
|
60
73
|
@data = data
|
|
74
|
+
@loading_mode = mode
|
|
61
75
|
|
|
62
76
|
parse_font_data
|
|
63
77
|
end
|
|
@@ -65,6 +79,7 @@ module Fontisan
|
|
|
65
79
|
# Load Type 1 font from file
|
|
66
80
|
#
|
|
67
81
|
# @param file_path [String] Path to PFB or PFA file
|
|
82
|
+
# @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
|
|
68
83
|
# @return [Type1Font] Loaded font instance
|
|
69
84
|
# @raise [ArgumentError] If file_path is nil
|
|
70
85
|
# @raise [Fontisan::Error] If file cannot be read or parsed
|
|
@@ -74,7 +89,7 @@ module Fontisan
|
|
|
74
89
|
#
|
|
75
90
|
# @example Load PFA file
|
|
76
91
|
# font = Fontisan::Type1Font.from_file('font.pfa')
|
|
77
|
-
def self.from_file(file_path)
|
|
92
|
+
def self.from_file(file_path, mode: :full)
|
|
78
93
|
raise ArgumentError, "File path cannot be nil" if file_path.nil?
|
|
79
94
|
|
|
80
95
|
unless File.exist?(file_path)
|
|
@@ -84,7 +99,7 @@ module Fontisan
|
|
|
84
99
|
# Read file
|
|
85
100
|
data = File.binread(file_path)
|
|
86
101
|
|
|
87
|
-
new(data, file_path: file_path)
|
|
102
|
+
new(data, file_path: file_path, mode: mode)
|
|
88
103
|
end
|
|
89
104
|
|
|
90
105
|
# Get clear text portion (before eexec)
|
|
@@ -101,34 +116,6 @@ module Fontisan
|
|
|
101
116
|
@encrypted_portion ||= ""
|
|
102
117
|
end
|
|
103
118
|
|
|
104
|
-
# Get font name from font dictionary
|
|
105
|
-
#
|
|
106
|
-
# @return [String, nil] Font name or nil if not found
|
|
107
|
-
def font_name
|
|
108
|
-
extract_dictionary_value("/FontName")
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Get full name from FontInfo
|
|
112
|
-
#
|
|
113
|
-
# @return [String, nil] Full name or nil if not found
|
|
114
|
-
def full_name
|
|
115
|
-
extract_fontinfo_value("FullName")
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Get family name from FontInfo
|
|
119
|
-
#
|
|
120
|
-
# @return [String, nil] Family name or nil if not found
|
|
121
|
-
def family_name
|
|
122
|
-
extract_fontinfo_value("FamilyName")
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Get version from FontInfo
|
|
126
|
-
#
|
|
127
|
-
# @return [String, nil] Version or nil if not found
|
|
128
|
-
def version
|
|
129
|
-
extract_fontinfo_value("version")
|
|
130
|
-
end
|
|
131
|
-
|
|
132
119
|
# Check if font has been decrypted
|
|
133
120
|
#
|
|
134
121
|
# @return [Boolean] True if font data has been decrypted
|
data/lib/fontisan/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -120,9 +120,10 @@ files:
|
|
|
120
120
|
- docs/APPLE_LEGACY_FONTS.adoc
|
|
121
121
|
- docs/COLLECTION_VALIDATION.adoc
|
|
122
122
|
- docs/COLOR_FONTS.adoc
|
|
123
|
-
- docs/
|
|
123
|
+
- docs/CONVERSION_GUIDE.adoc
|
|
124
124
|
- docs/EXTRACT_TTC_MIGRATION.md
|
|
125
125
|
- docs/FONT_HINTING.adoc
|
|
126
|
+
- docs/TYPE1_FONTS.adoc
|
|
126
127
|
- docs/VALIDATION.adoc
|
|
127
128
|
- docs/VARIABLE_FONT_OPERATIONS.adoc
|
|
128
129
|
- docs/WOFF_WOFF2_FORMATS.adoc
|
|
@@ -354,6 +355,7 @@ files:
|
|
|
354
355
|
- lib/fontisan/type1/afm_generator.rb
|
|
355
356
|
- lib/fontisan/type1/afm_parser.rb
|
|
356
357
|
- lib/fontisan/type1/agl.rb
|
|
358
|
+
- lib/fontisan/type1/cff_to_type1_converter.rb
|
|
357
359
|
- lib/fontisan/type1/charstring_converter.rb
|
|
358
360
|
- lib/fontisan/type1/charstrings.rb
|
|
359
361
|
- lib/fontisan/type1/conversion_options.rb
|
|
@@ -369,6 +371,7 @@ files:
|
|
|
369
371
|
- lib/fontisan/type1/pfm_generator.rb
|
|
370
372
|
- lib/fontisan/type1/pfm_parser.rb
|
|
371
373
|
- lib/fontisan/type1/private_dict.rb
|
|
374
|
+
- lib/fontisan/type1/seac_expander.rb
|
|
372
375
|
- lib/fontisan/type1/ttf_to_type1_converter.rb
|
|
373
376
|
- lib/fontisan/type1/upm_scaler.rb
|
|
374
377
|
- lib/fontisan/type1_font.rb
|