fontisan 0.2.11 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +214 -51
- data/README.adoc +160 -0
- data/lib/fontisan/cli.rb +177 -6
- data/lib/fontisan/commands/convert_command.rb +32 -1
- data/lib/fontisan/config/conversion_matrix.yml +132 -4
- data/lib/fontisan/constants.rb +12 -0
- data/lib/fontisan/conversion_options.rb +378 -0
- data/lib/fontisan/converters/collection_converter.rb +45 -10
- data/lib/fontisan/converters/format_converter.rb +2 -0
- data/lib/fontisan/converters/outline_converter.rb +78 -4
- data/lib/fontisan/converters/type1_converter.rb +559 -0
- data/lib/fontisan/font_loader.rb +46 -3
- data/lib/fontisan/type1/afm_generator.rb +436 -0
- data/lib/fontisan/type1/afm_parser.rb +298 -0
- data/lib/fontisan/type1/agl.rb +456 -0
- data/lib/fontisan/type1/charstring_converter.rb +240 -0
- data/lib/fontisan/type1/charstrings.rb +408 -0
- data/lib/fontisan/type1/conversion_options.rb +243 -0
- data/lib/fontisan/type1/decryptor.rb +183 -0
- data/lib/fontisan/type1/encodings.rb +697 -0
- data/lib/fontisan/type1/font_dictionary.rb +514 -0
- data/lib/fontisan/type1/generator.rb +220 -0
- data/lib/fontisan/type1/inf_generator.rb +332 -0
- data/lib/fontisan/type1/pfa_generator.rb +343 -0
- data/lib/fontisan/type1/pfa_parser.rb +158 -0
- data/lib/fontisan/type1/pfb_generator.rb +291 -0
- data/lib/fontisan/type1/pfb_parser.rb +166 -0
- data/lib/fontisan/type1/pfm_generator.rb +610 -0
- data/lib/fontisan/type1/pfm_parser.rb +433 -0
- data/lib/fontisan/type1/private_dict.rb +285 -0
- data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
- data/lib/fontisan/type1/upm_scaler.rb +118 -0
- data/lib/fontisan/type1.rb +73 -0
- data/lib/fontisan/type1_font.rb +331 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +2 -0
- metadata +26 -2
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# TTF to Type 1 CharString Converter
|
|
6
|
+
#
|
|
7
|
+
# [`TTFToType1Converter`](lib/fontisan/type1/ttf_to_type1_converter.rb) converts
|
|
8
|
+
# TrueType glyphs to Type 1 CharStrings.
|
|
9
|
+
#
|
|
10
|
+
# The conversion involves:
|
|
11
|
+
# - Converting quadratic curves (TrueType) to cubic curves (Type 1)
|
|
12
|
+
# - Scaling coordinates if needed
|
|
13
|
+
# - Generating Type 1 CharString commands
|
|
14
|
+
#
|
|
15
|
+
# @example Convert TTF font to Type 1 CharStrings
|
|
16
|
+
# scaler = Fontisan::Type1::UPMScaler.type1_standard(font)
|
|
17
|
+
# converter = Fontisan::Type1::TTFToType1Converter.new(font, scaler, encoding)
|
|
18
|
+
# charstrings = converter.convert
|
|
19
|
+
#
|
|
20
|
+
# @see https://www.adobe.com/devnet/font/pdfs/5178.Type1.pdf
|
|
21
|
+
class TTFToType1Converter
|
|
22
|
+
# Type 1 CharString command codes
|
|
23
|
+
HSTEM = 1
|
|
24
|
+
VSTEM = 3
|
|
25
|
+
VMOVETO = 4
|
|
26
|
+
RLINETO = 5
|
|
27
|
+
HLINETO = 6
|
|
28
|
+
VLINETO = 7
|
|
29
|
+
RRCURVETO = 8
|
|
30
|
+
CLOSEPATH = 9
|
|
31
|
+
CALLSUBR = 10
|
|
32
|
+
RETURN = 11
|
|
33
|
+
ESCAPE = 12
|
|
34
|
+
HSBW = 13
|
|
35
|
+
ENDCHAR = 14
|
|
36
|
+
RMOVETO = 21
|
|
37
|
+
HMOVETO = 22
|
|
38
|
+
VHCURVETO = 30
|
|
39
|
+
HVCURVETO = 31
|
|
40
|
+
|
|
41
|
+
# Convert TTF font to Type 1 CharStrings
|
|
42
|
+
#
|
|
43
|
+
# @param font [Fontisan::Font] Source TTF font
|
|
44
|
+
# @param scaler [UPMScaler] UPM scaler
|
|
45
|
+
# @param encoding [Class] Encoding class
|
|
46
|
+
# @return [Hash<Integer, String>] Glyph ID to CharString mapping
|
|
47
|
+
def self.convert(font, scaler, encoding)
|
|
48
|
+
new(font, scaler, encoding).convert
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(font, scaler, encoding)
|
|
52
|
+
@font = font
|
|
53
|
+
@scaler = scaler
|
|
54
|
+
@encoding = encoding
|
|
55
|
+
@charstrings = {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Convert all glyphs to CharStrings
|
|
59
|
+
#
|
|
60
|
+
# @return [Hash<Integer, String>] Glyph ID to CharString mapping
|
|
61
|
+
def convert
|
|
62
|
+
glyf_table = @font.table(Constants::GLYF_TAG)
|
|
63
|
+
return {} unless glyf_table
|
|
64
|
+
|
|
65
|
+
loca_table = @font.table(Constants::LOCA_TAG)
|
|
66
|
+
head_table = @font.table(Constants::HEAD_TAG)
|
|
67
|
+
|
|
68
|
+
maxp = @font.table(Constants::MAXP_TAG)
|
|
69
|
+
num_glyphs = maxp&.num_glyphs || 0
|
|
70
|
+
|
|
71
|
+
num_glyphs.times do |gid|
|
|
72
|
+
@charstrings[gid] =
|
|
73
|
+
convert_glyph(glyf_table, loca_table, head_table, gid)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@charstrings
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Convert a single glyph to CharString
|
|
82
|
+
#
|
|
83
|
+
# @param glyf_table [Tables::Glyf] TTF glyf table
|
|
84
|
+
# @param loca_table [Tables::Loca] TTF loca table
|
|
85
|
+
# @param head_table [Tables::Head] TTF head table
|
|
86
|
+
# @param gid [Integer] Glyph ID
|
|
87
|
+
# @return [String] Type 1 CharString data
|
|
88
|
+
def convert_glyph(glyf_table, loca_table, head_table, gid)
|
|
89
|
+
glyph = glyf_table.glyph_for(gid, loca_table, head_table)
|
|
90
|
+
|
|
91
|
+
# Handle empty glyph
|
|
92
|
+
return empty_charstring if glyph.nil?
|
|
93
|
+
|
|
94
|
+
# Handle compound glyphs
|
|
95
|
+
if glyph.compound?
|
|
96
|
+
return convert_composite_glyph(glyph, glyf_table)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Convert simple glyph
|
|
100
|
+
convert_simple_glyph(glyph)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Convert a simple glyph to CharString
|
|
104
|
+
#
|
|
105
|
+
# @param glyph [Object] TTF simple glyph
|
|
106
|
+
# @return [String] Type 1 CharString data
|
|
107
|
+
def convert_simple_glyph(glyph)
|
|
108
|
+
commands = []
|
|
109
|
+
points = extract_points(glyph)
|
|
110
|
+
|
|
111
|
+
return empty_charstring if points.empty?
|
|
112
|
+
|
|
113
|
+
# Start with hsbw (horizontal side bearing and width)
|
|
114
|
+
lsb = @scaler.scale(glyph.left_side_bearing || 0)
|
|
115
|
+
width = @scaler.scale(glyph.advance_width || 500)
|
|
116
|
+
commands << [HSBW, lsb, width]
|
|
117
|
+
|
|
118
|
+
# Convert contours to Type 1 commands
|
|
119
|
+
contour_commands = convert_contours(points)
|
|
120
|
+
commands.concat(contour_commands)
|
|
121
|
+
|
|
122
|
+
# End character
|
|
123
|
+
commands << [ENDCHAR]
|
|
124
|
+
|
|
125
|
+
# Encode to CharString format
|
|
126
|
+
encode_charstring(commands)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Extract points from a simple glyph
|
|
130
|
+
#
|
|
131
|
+
# @param glyph [Object] TTF simple glyph
|
|
132
|
+
# @return [Array<Hash>] Array of points with on_curve flag
|
|
133
|
+
def extract_points(glyph)
|
|
134
|
+
return [] unless glyph.respond_to?(:points)
|
|
135
|
+
|
|
136
|
+
points = []
|
|
137
|
+
glyph.points.each do |point|
|
|
138
|
+
points << {
|
|
139
|
+
x: @scaler.scale(point.x),
|
|
140
|
+
y: @scaler.scale(point.y),
|
|
141
|
+
on_curve: point.on_curve?,
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
points
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Convert contours to Type 1 commands
|
|
149
|
+
#
|
|
150
|
+
# @param points [Array<Hash>] Array of points
|
|
151
|
+
# @return [Array<Array<Integer>>] Array of command arrays
|
|
152
|
+
def convert_contours(points)
|
|
153
|
+
commands = []
|
|
154
|
+
return commands if points.empty?
|
|
155
|
+
|
|
156
|
+
# Start at first point
|
|
157
|
+
start_point = points[0]
|
|
158
|
+
current_point = { x: start_point[:x], y: start_point[:y] }
|
|
159
|
+
|
|
160
|
+
# Process remaining points in runs
|
|
161
|
+
i = 1
|
|
162
|
+
while i < points.length
|
|
163
|
+
point = points[i]
|
|
164
|
+
|
|
165
|
+
if point[:on_curve]
|
|
166
|
+
# On-curve point - draw line or curve from previous
|
|
167
|
+
if i.positive? && !points[i - 1][:on_curve]
|
|
168
|
+
# Previous was off-curve, this is end of quadratic curve
|
|
169
|
+
prev_point = points[i - 1]
|
|
170
|
+
curve_commands = convert_quadratic_to_cubic(
|
|
171
|
+
current_point,
|
|
172
|
+
prev_point,
|
|
173
|
+
point,
|
|
174
|
+
)
|
|
175
|
+
commands.concat(curve_commands)
|
|
176
|
+
else
|
|
177
|
+
# Line to this point
|
|
178
|
+
dx = point[:x] - current_point[:x]
|
|
179
|
+
dy = point[:y] - current_point[:y]
|
|
180
|
+
commands << [RLINETO, dx, dy]
|
|
181
|
+
end
|
|
182
|
+
current_point = { x: point[:x], y: point[:y] }
|
|
183
|
+
elsif i + 1 < points.length && !points[i + 1][:on_curve]
|
|
184
|
+
# Off-curve control point
|
|
185
|
+
# Check if next point is also off-curve (implicit on-curve midpoint)
|
|
186
|
+
next_point = points[i + 1]
|
|
187
|
+
implicit_on = {
|
|
188
|
+
x: ((point[:x] + next_point[:x]).to_f / 2).round,
|
|
189
|
+
y: ((point[:y] + next_point[:y]).to_f / 2).round,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
curve_commands = convert_quadratic_to_cubic(
|
|
193
|
+
current_point,
|
|
194
|
+
point,
|
|
195
|
+
implicit_on,
|
|
196
|
+
)
|
|
197
|
+
commands.concat(curve_commands)
|
|
198
|
+
current_point = implicit_on
|
|
199
|
+
# Both are off-curve, implicit on-curve at midpoint
|
|
200
|
+
# If next point is on-curve, we'll handle it in next iteration
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
i += 1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Close contour if needed (implicit close path for Type 1)
|
|
207
|
+
# Type 1 implicitly closes paths, so we don't need explicit close
|
|
208
|
+
|
|
209
|
+
commands
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Convert quadratic Bézier curve to cubic Bézier curve
|
|
213
|
+
#
|
|
214
|
+
# TrueType uses quadratic curves with one control point:
|
|
215
|
+
# P0 (on) -> P1 (off) -> P2 (on)
|
|
216
|
+
#
|
|
217
|
+
# Type 1 uses cubic curves with two control points:
|
|
218
|
+
# P0 (on) -> C1 (off) -> C2 (off) -> P2 (on)
|
|
219
|
+
#
|
|
220
|
+
# Conversion formula:
|
|
221
|
+
# C1 = P0 + (2/3)(P1 - P0) = (1/3)P0 + (2/3)P1
|
|
222
|
+
# C2 = P2 + (2/3)(P1 - P2) = (2/3)P1 + (1/3)P2
|
|
223
|
+
#
|
|
224
|
+
# @param p0 [Hash] Start point {x, y}
|
|
225
|
+
# @param p1 [Hash] Control point {x, y}
|
|
226
|
+
# @param p2 [Hash] End point {x, y}
|
|
227
|
+
# @return [Array<Array<Integer>>] Array of command arrays
|
|
228
|
+
def convert_quadratic_to_cubic(p0, p1, p2)
|
|
229
|
+
# Calculate cubic control points
|
|
230
|
+
c1_x = p0[:x] + ((2 * (p1[:x] - p0[:x])).to_f / 3).round
|
|
231
|
+
c1_y = p0[:y] + ((2 * (p1[:y] - p0[:y])).to_f / 3).round
|
|
232
|
+
|
|
233
|
+
c2_x = p2[:x] + ((2 * (p1[:x] - p2[:x])).to_f / 3).round
|
|
234
|
+
c2_y = p2[:y] + ((2 * (p1[:y] - p2[:y])).to_f / 3).round
|
|
235
|
+
|
|
236
|
+
# Calculate deltas
|
|
237
|
+
dc1x = c1_x - p0[:x]
|
|
238
|
+
dc1y = c1_y - p0[:y]
|
|
239
|
+
dc2x = c2_x - c1_x
|
|
240
|
+
dc2y = c2_y - c1_y
|
|
241
|
+
dx = p2[:x] - c2_x
|
|
242
|
+
dy = p2[:y] - c2_y
|
|
243
|
+
|
|
244
|
+
[
|
|
245
|
+
[RRCURVETO, dc1x, dc1y, dc2x, dc2y, dx, dy],
|
|
246
|
+
]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Convert a composite glyph to CharString
|
|
250
|
+
#
|
|
251
|
+
# @param glyph [Object] TTF composite glyph
|
|
252
|
+
# @param glyf_table [Object] TTF glyf table
|
|
253
|
+
# @return [String] Type 1 CharString data
|
|
254
|
+
def convert_composite_glyph(_glyph, _glyf_table)
|
|
255
|
+
# For composite glyphs, we need to decompose or use seac
|
|
256
|
+
# TODO: Implement proper composite handling with seac or decomposition
|
|
257
|
+
|
|
258
|
+
# For now, return a simple placeholder
|
|
259
|
+
# In a full implementation, we would:
|
|
260
|
+
# 1. Extract component glyphs
|
|
261
|
+
# 2. Transform and merge their outlines
|
|
262
|
+
# 3. Generate combined CharString
|
|
263
|
+
|
|
264
|
+
empty_charstring
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Encode commands to Type 1 CharString binary format
|
|
268
|
+
#
|
|
269
|
+
# @param commands [Array<Array<Integer>>] Array of command arrays
|
|
270
|
+
# @return [String] Binary CharString data
|
|
271
|
+
def encode_charstring(commands)
|
|
272
|
+
bytes = []
|
|
273
|
+
|
|
274
|
+
commands.each do |cmd|
|
|
275
|
+
cmd.each do |value|
|
|
276
|
+
if value.is_a?(Integer)
|
|
277
|
+
bytes.concat(encode_number(value))
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
bytes.pack("C*")
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Encode a number for CharString
|
|
286
|
+
#
|
|
287
|
+
# Type 1 CharStrings use a variable-length encoding for integers
|
|
288
|
+
#
|
|
289
|
+
# @param value [Integer] Number to encode
|
|
290
|
+
# @return [Array<Integer>] Array of bytes
|
|
291
|
+
def encode_number(value)
|
|
292
|
+
if value >= -107 && value <= 107
|
|
293
|
+
# Single byte encoding: value + 139
|
|
294
|
+
[value + 139]
|
|
295
|
+
elsif value >= 108 && value <= 1131
|
|
296
|
+
# Two byte encoding
|
|
297
|
+
byte1 = ((value - 108) >> 8) + 247
|
|
298
|
+
byte2 = (value - 108) & 0xFF
|
|
299
|
+
[byte1, byte2]
|
|
300
|
+
elsif value >= -1131 && value <= -108
|
|
301
|
+
# Two byte encoding for negative
|
|
302
|
+
byte1 = ((-value - 108) >> 8) + 251
|
|
303
|
+
byte2 = (-value - 108) & 0xFF
|
|
304
|
+
[byte1, byte2]
|
|
305
|
+
elsif value >= -32768 && value <= 32767
|
|
306
|
+
# Three byte encoding (16-bit signed)
|
|
307
|
+
[255, value & 0xFF, (value >> 8) & 0xFF]
|
|
308
|
+
else
|
|
309
|
+
# Four byte encoding (32-bit)
|
|
310
|
+
bytes = []
|
|
311
|
+
4.times do |i|
|
|
312
|
+
bytes << ((value >> (8 * i)) & 0xFF)
|
|
313
|
+
end
|
|
314
|
+
[255] + bytes
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Generate empty CharString
|
|
319
|
+
#
|
|
320
|
+
# @return [String] Empty CharString data
|
|
321
|
+
def empty_charstring
|
|
322
|
+
# hsbw with width 0, then endchar
|
|
323
|
+
[0, 500, ENDCHAR].pack("C*")
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# UPM (Units Per Em) Scaler
|
|
6
|
+
#
|
|
7
|
+
# [`UPMScaler`](lib/fontisan/type1/upm_scaler.rb) handles scaling of font metrics
|
|
8
|
+
# from the source font's UPM to a target UPM.
|
|
9
|
+
#
|
|
10
|
+
# Traditional Type 1 fonts use 1000 UPM, while modern TTF fonts typically use
|
|
11
|
+
# 2048 or other values. This scaler converts metrics appropriately.
|
|
12
|
+
#
|
|
13
|
+
# @example Scale to Type 1 standard
|
|
14
|
+
# scaler = Fontisan::Type1::UPMScaler.type1_standard(font)
|
|
15
|
+
# scaled_width = scaler.scale(1024) # => 500 for 2048 UPM font
|
|
16
|
+
#
|
|
17
|
+
# @example Keep native UPM
|
|
18
|
+
# scaler = Fontisan::Type1::UPMScaler.native(font)
|
|
19
|
+
# scaled_width = scaler.scale(1024) # => 1024 (no scaling)
|
|
20
|
+
class UPMScaler
|
|
21
|
+
# @return [Integer] Source font's units per em
|
|
22
|
+
attr_reader :source_upm
|
|
23
|
+
|
|
24
|
+
# @return [Integer] Target units per em
|
|
25
|
+
attr_reader :target_upm
|
|
26
|
+
|
|
27
|
+
# @return [Rational] Scale factor (target / source)
|
|
28
|
+
attr_reader :scale_factor
|
|
29
|
+
|
|
30
|
+
# Initialize a new UPM scaler
|
|
31
|
+
#
|
|
32
|
+
# @param font [Fontisan::Font] Source font
|
|
33
|
+
# @param target_upm [Integer] Target UPM (default: 1000 for Type 1)
|
|
34
|
+
def initialize(font, target_upm: 1000)
|
|
35
|
+
@font = font
|
|
36
|
+
@source_upm = font.units_per_em
|
|
37
|
+
@target_upm = target_upm
|
|
38
|
+
@scale_factor = Rational(target_upm, source_upm)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Scale a single value
|
|
42
|
+
#
|
|
43
|
+
# @param value [Integer, Float] Value to scale
|
|
44
|
+
# @return [Integer] Scaled value (rounded)
|
|
45
|
+
def scale(value)
|
|
46
|
+
return 0 if value.nil? || value.zero?
|
|
47
|
+
|
|
48
|
+
(value * scale_factor).round
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Scale an array of values
|
|
52
|
+
#
|
|
53
|
+
# @param values [Array<Integer, Float>] Values to scale
|
|
54
|
+
# @return [Array<Integer>] Scaled values
|
|
55
|
+
def scale_array(values)
|
|
56
|
+
values.map { |v| scale(v) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Scale a coordinate pair [x, y]
|
|
60
|
+
#
|
|
61
|
+
# @param value [Array<Integer, Float>] Coordinate pair
|
|
62
|
+
# @return [Array<Integer>] Scaled coordinates
|
|
63
|
+
def scale_pair(value)
|
|
64
|
+
[scale(value[0]), scale(value[1])]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Scale a bounding box [llx, lly, urx, ury]
|
|
68
|
+
#
|
|
69
|
+
# @param bbox [Array<Integer, Float>] Bounding box
|
|
70
|
+
# @return [Array<Integer>] Scaled bounding box
|
|
71
|
+
def scale_bbox(bbox)
|
|
72
|
+
return nil if bbox.nil?
|
|
73
|
+
|
|
74
|
+
bbox.map { |v| scale(v) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Scale a character width
|
|
78
|
+
#
|
|
79
|
+
# @param width [Integer, Float] Character width
|
|
80
|
+
# @return [Integer] Scaled width
|
|
81
|
+
def scale_width(width)
|
|
82
|
+
scale(width)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if scaling is needed
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean] True if source and target UPM differ
|
|
88
|
+
def scaling_needed?
|
|
89
|
+
@source_upm != @target_upm
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Create scaler with native UPM (no scaling)
|
|
93
|
+
#
|
|
94
|
+
# @param font [Fontisan::Font] Source font
|
|
95
|
+
# @return [UPMScaler] Scaler with native UPM
|
|
96
|
+
def self.native(font)
|
|
97
|
+
new(font, target_upm: font.units_per_em)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Create scaler with Type 1 standard UPM (1000)
|
|
101
|
+
#
|
|
102
|
+
# @param font [Fontisan::Font] Source font
|
|
103
|
+
# @return [UPMScaler] Scaler with 1000 UPM
|
|
104
|
+
def self.type1_standard(font)
|
|
105
|
+
new(font, target_upm: 1000)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Create scaler with custom UPM
|
|
109
|
+
#
|
|
110
|
+
# @param font [Fontisan::Font] Source font
|
|
111
|
+
# @param upm [Integer] Target UPM
|
|
112
|
+
# @return [UPMScaler] Scaler with custom UPM
|
|
113
|
+
def self.custom(font, upm:)
|
|
114
|
+
new(font, target_upm: upm)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
# Adobe Type 1 Font support
|
|
5
|
+
#
|
|
6
|
+
# [`Type1`](lib/fontisan/type1.rb) provides parsing and conversion
|
|
7
|
+
# capabilities for Adobe Type 1 fonts in PFB (Printer Font Binary)
|
|
8
|
+
# and PFA (Printer Font ASCII) formats.
|
|
9
|
+
#
|
|
10
|
+
# Type 1 fonts were the standard for digital typography in the 1980s-1990s
|
|
11
|
+
# and are still encountered in legacy systems and design workflows.
|
|
12
|
+
#
|
|
13
|
+
# Key features:
|
|
14
|
+
# - PFB and PFA format parsing
|
|
15
|
+
# - eexec decryption for encrypted font portions
|
|
16
|
+
# - CharString decryption with lenIV handling
|
|
17
|
+
# - Font dictionary parsing (FontInfo, Private dict)
|
|
18
|
+
# - Conversion from TTF/OTF to Type 1 formats
|
|
19
|
+
# - UPM scaling for Type 1 compatibility (1000 UPM)
|
|
20
|
+
# - Multiple encoding support (AdobeStandard, ISOLatin1, Unicode)
|
|
21
|
+
#
|
|
22
|
+
# @example Generate Type 1 formats from TTF
|
|
23
|
+
# font = Fontisan::FontLoader.load("font.ttf")
|
|
24
|
+
# result = Fontisan::Type1::Generator.generate(font)
|
|
25
|
+
# result[:afm] # => AFM file content
|
|
26
|
+
# result[:pfm] # => PFM file content
|
|
27
|
+
# result[:pfb] # => PFB file content
|
|
28
|
+
# result[:inf] # => INF file content
|
|
29
|
+
#
|
|
30
|
+
# @example Generate with specific options
|
|
31
|
+
# options = Fontisan::Type1::ConversionOptions.windows_type1
|
|
32
|
+
# result = Fontisan::Type1::Generator.generate(font, options)
|
|
33
|
+
#
|
|
34
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
35
|
+
module Type1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Parsers
|
|
40
|
+
require_relative "type1/pfb_parser"
|
|
41
|
+
require_relative "type1/pfa_parser"
|
|
42
|
+
|
|
43
|
+
# Core components
|
|
44
|
+
require_relative "type1/decryptor"
|
|
45
|
+
require_relative "type1/font_dictionary"
|
|
46
|
+
require_relative "type1/private_dict"
|
|
47
|
+
require_relative "type1/charstrings"
|
|
48
|
+
require_relative "type1/charstring_converter"
|
|
49
|
+
|
|
50
|
+
# Infrastructure
|
|
51
|
+
require_relative "type1/upm_scaler"
|
|
52
|
+
require_relative "type1/agl"
|
|
53
|
+
require_relative "type1/encodings"
|
|
54
|
+
require_relative "type1/conversion_options"
|
|
55
|
+
|
|
56
|
+
# TTF to Type 1 conversion
|
|
57
|
+
require_relative "type1/ttf_to_type1_converter"
|
|
58
|
+
|
|
59
|
+
# Metrics parsers
|
|
60
|
+
require_relative "type1/afm_parser"
|
|
61
|
+
require_relative "type1/pfm_parser"
|
|
62
|
+
|
|
63
|
+
# Metrics generators
|
|
64
|
+
require_relative "type1/afm_generator"
|
|
65
|
+
require_relative "type1/pfm_generator"
|
|
66
|
+
|
|
67
|
+
# Type 1 font generators
|
|
68
|
+
require_relative "type1/pfa_generator"
|
|
69
|
+
require_relative "type1/pfb_generator"
|
|
70
|
+
require_relative "type1/inf_generator"
|
|
71
|
+
|
|
72
|
+
# Unified generator interface
|
|
73
|
+
require_relative "type1/generator"
|