fontisan 0.2.10 → 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 +216 -42
- data/README.adoc +160 -0
- data/lib/fontisan/cli.rb +177 -6
- data/lib/fontisan/collection/table_analyzer.rb +88 -3
- 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/cff_table_builder.rb +198 -0
- data/lib/fontisan/converters/collection_converter.rb +45 -10
- data/lib/fontisan/converters/format_converter.rb +2 -0
- data/lib/fontisan/converters/glyf_table_builder.rb +63 -0
- data/lib/fontisan/converters/outline_converter.rb +111 -374
- data/lib/fontisan/converters/outline_extraction.rb +93 -0
- data/lib/fontisan/converters/outline_optimizer.rb +89 -0
- data/lib/fontisan/converters/type1_converter.rb +559 -0
- data/lib/fontisan/font_loader.rb +46 -3
- data/lib/fontisan/glyph_accessor.rb +29 -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/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/variation/cache.rb +1 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2_font.rb +3 -3
- data/lib/fontisan.rb +2 -0
- metadata +30 -2
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../conversion_options"
|
|
4
|
+
require_relative "../type1/charstring_converter"
|
|
5
|
+
require_relative "../type1/font_dictionary"
|
|
6
|
+
require_relative "../type1/charstrings"
|
|
7
|
+
require_relative "../type1_font"
|
|
8
|
+
require_relative "cff_table_builder"
|
|
9
|
+
|
|
10
|
+
module Fontisan
|
|
11
|
+
module Converters
|
|
12
|
+
# Converter for Adobe Type 1 fonts to/from SFNT formats.
|
|
13
|
+
#
|
|
14
|
+
# [`Type1Converter`](lib/fontisan/converters/type1_converter.rb) handles
|
|
15
|
+
# bidirectional conversion between Type 1 fonts (PFB/PFA) and SFNT-based
|
|
16
|
+
# formats (TTF, OTF, WOFF, WOFF2).
|
|
17
|
+
#
|
|
18
|
+
# == Conversion Strategy
|
|
19
|
+
#
|
|
20
|
+
# Type 1 fonts use PostScript CharStrings that are similar to CFF CharStrings
|
|
21
|
+
# used in OpenType fonts. The conversion uses CharStringConverter for the
|
|
22
|
+
# CharString translation.
|
|
23
|
+
#
|
|
24
|
+
# * Type 1 → OTF: Convert Type 1 CharStrings to CFF format, build CFF table
|
|
25
|
+
# * OTF → Type 1: Convert CFF CharStrings to Type 1 format, build PFB/PFA
|
|
26
|
+
# * Type 1 → TTF: Type 1 → OTF → TTF (via OutlineConverter)
|
|
27
|
+
# * TTF → Type 1: TTF → OTF → Type 1
|
|
28
|
+
#
|
|
29
|
+
# == Conversion Options
|
|
30
|
+
#
|
|
31
|
+
# The converter accepts [`ConversionOptions`](../conversion_options) with
|
|
32
|
+
# opening and generating options:
|
|
33
|
+
#
|
|
34
|
+
# * Opening options: decompose_composites, generate_unicode, read_all_records
|
|
35
|
+
# * Generating options: decompose_on_output, hinting_mode, write_pfm, write_afm
|
|
36
|
+
#
|
|
37
|
+
# @example Convert Type 1 to OTF with options
|
|
38
|
+
# font = FontLoader.load("font.pfb")
|
|
39
|
+
# options = ConversionOptions.recommended(from: :type1, to: :otf)
|
|
40
|
+
# converter = Type1Converter.new
|
|
41
|
+
# tables = converter.convert(font, options: options)
|
|
42
|
+
#
|
|
43
|
+
# @example Convert with preset
|
|
44
|
+
# options = ConversionOptions.from_preset(:type1_to_modern)
|
|
45
|
+
# tables = converter.convert(font, options: options)
|
|
46
|
+
#
|
|
47
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
48
|
+
# @see CharStringConverter
|
|
49
|
+
class Type1Converter
|
|
50
|
+
include ConversionStrategy
|
|
51
|
+
include CffTableBuilder
|
|
52
|
+
|
|
53
|
+
# Initialize a new Type1Converter
|
|
54
|
+
#
|
|
55
|
+
# @param options [Hash] Converter options
|
|
56
|
+
# @option options [Boolean] :optimize_cff Enable CFF optimization (default: false)
|
|
57
|
+
# @option options [Boolean] :preserve_hints Preserve hinting (default: true)
|
|
58
|
+
# @option options [Symbol] :target_format Target format for conversion
|
|
59
|
+
def initialize(options = {})
|
|
60
|
+
@optimize_cff = options.fetch(:optimize_cff, false)
|
|
61
|
+
@preserve_hints = options.fetch(:preserve_hints, true)
|
|
62
|
+
@target_format = options[:target_format]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Convert font to target format
|
|
66
|
+
#
|
|
67
|
+
# @param font [Type1Font, OpenTypeFont, TrueTypeFont] Source font
|
|
68
|
+
# @param options [Hash, ConversionOptions] Conversion options
|
|
69
|
+
# @option options [Symbol] :target_format Target format override
|
|
70
|
+
# @option options [ConversionOptions] :options ConversionOptions object
|
|
71
|
+
# @return [Hash<String, String>] Map of table tags to binary data
|
|
72
|
+
def convert(font, options = {})
|
|
73
|
+
# Extract ConversionOptions if provided
|
|
74
|
+
conv_options = extract_conversion_options(options)
|
|
75
|
+
|
|
76
|
+
target_format = options[:target_format] || conv_options&.to || @target_format ||
|
|
77
|
+
detect_target_format(font)
|
|
78
|
+
validate(font, target_format)
|
|
79
|
+
|
|
80
|
+
# Apply opening options to source font
|
|
81
|
+
apply_opening_options(font, conv_options) if conv_options
|
|
82
|
+
|
|
83
|
+
source_format = detect_format(font)
|
|
84
|
+
|
|
85
|
+
case [source_format, target_format]
|
|
86
|
+
when %i[type1 otf]
|
|
87
|
+
convert_type1_to_otf(font, conv_options)
|
|
88
|
+
when %i[otf type1]
|
|
89
|
+
convert_otf_to_type1(font, conv_options)
|
|
90
|
+
when %i[type1 ttf]
|
|
91
|
+
convert_type1_to_ttf(font, conv_options)
|
|
92
|
+
when %i[ttf type1]
|
|
93
|
+
convert_ttf_to_type1(font, conv_options)
|
|
94
|
+
else
|
|
95
|
+
raise Fontisan::Error,
|
|
96
|
+
"Unsupported conversion: #{source_format} → #{target_format}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get supported conversions
|
|
101
|
+
#
|
|
102
|
+
# @return [Array<Array<Symbol>>] Supported conversion pairs
|
|
103
|
+
def supported_conversions
|
|
104
|
+
[
|
|
105
|
+
%i[type1 otf],
|
|
106
|
+
%i[otf type1],
|
|
107
|
+
%i[type1 ttf],
|
|
108
|
+
%i[ttf type1],
|
|
109
|
+
]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Validate font for conversion
|
|
113
|
+
#
|
|
114
|
+
# @param font [Type1Font, OpenTypeFont, TrueTypeFont] Font to validate
|
|
115
|
+
# @param target_format [Symbol] Target format
|
|
116
|
+
# @return [Boolean] True if valid
|
|
117
|
+
# @raise [ArgumentError] If font is invalid
|
|
118
|
+
# @raise [Error] If conversion is not supported
|
|
119
|
+
def validate(font, target_format)
|
|
120
|
+
raise ArgumentError, "Font cannot be nil" if font.nil?
|
|
121
|
+
|
|
122
|
+
unless font.respond_to?(:font_dictionary) || font.respond_to?(:tables)
|
|
123
|
+
raise ArgumentError,
|
|
124
|
+
"Font must be Type1Font or have :tables method"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
source_format = detect_format(font)
|
|
128
|
+
unless supports?(source_format, target_format)
|
|
129
|
+
raise Fontisan::Error,
|
|
130
|
+
"Conversion #{source_format} → #{target_format} not supported"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
# Extract ConversionOptions from options hash
|
|
139
|
+
#
|
|
140
|
+
# @param options [Hash, ConversionOptions] Options or hash containing :options key
|
|
141
|
+
# @return [ConversionOptions, nil] Extracted ConversionOptions or nil
|
|
142
|
+
def extract_conversion_options(options)
|
|
143
|
+
return options if options.is_a?(ConversionOptions)
|
|
144
|
+
|
|
145
|
+
options[:options] if options.is_a?(Hash)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Apply opening options to source font
|
|
149
|
+
#
|
|
150
|
+
# @param font [Type1Font] Source font
|
|
151
|
+
# @param conv_options [ConversionOptions] Conversion options with opening options
|
|
152
|
+
def apply_opening_options(font, conv_options)
|
|
153
|
+
return unless font.is_a?(Type1Font)
|
|
154
|
+
return unless conv_options
|
|
155
|
+
|
|
156
|
+
# Generate Unicode codepoints if requested
|
|
157
|
+
if conv_options.opening_option?(:generate_unicode)
|
|
158
|
+
generate_unicode_mappings(font)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Decompose seac composites if requested
|
|
162
|
+
if conv_options.opening_option?(:decompose_composites)
|
|
163
|
+
decompose_seac_glyphs(font)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Read all font dictionary records if requested
|
|
167
|
+
if conv_options.opening_option?(:read_all_records) && font.font_dictionary.respond_to?(:reload)
|
|
168
|
+
# Ensure full font dictionary is loaded
|
|
169
|
+
font.font_dictionary.reload
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Generate Unicode codepoints from glyph names/encoding
|
|
174
|
+
#
|
|
175
|
+
# @param font [Type1Font] Source Type 1 font
|
|
176
|
+
def generate_unicode_mappings(_font)
|
|
177
|
+
# Placeholder: Generate Unicode mappings from glyph names
|
|
178
|
+
# A full implementation would:
|
|
179
|
+
# 1. Parse the Adobe Glyph List
|
|
180
|
+
# 2. Map glyph names to Unicode codepoints
|
|
181
|
+
# 3. Update the charstrings encoding
|
|
182
|
+
#
|
|
183
|
+
# For now, this is a no-op placeholder
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Decompose seac composite glyphs to base glyphs
|
|
188
|
+
#
|
|
189
|
+
# @param font [Type1Font] Source Type 1 font
|
|
190
|
+
def decompose_seac_glyphs(_font)
|
|
191
|
+
# Placeholder: Decompose seac composites
|
|
192
|
+
# A full implementation would:
|
|
193
|
+
# 1. Identify glyphs using seac operator
|
|
194
|
+
# 2. Resolve the accent character from encoding
|
|
195
|
+
# 3. Extract component outlines recursively
|
|
196
|
+
# 4. Merge into single glyph
|
|
197
|
+
#
|
|
198
|
+
# For now, this is a no-op placeholder
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Detect font format
|
|
203
|
+
#
|
|
204
|
+
# @param font [Type1Font, OpenTypeFont, TrueTypeFont] Font to detect
|
|
205
|
+
# @return [Symbol] Font format (:type1, :ttf, :otf)
|
|
206
|
+
def detect_format(font)
|
|
207
|
+
case font
|
|
208
|
+
when Type1Font
|
|
209
|
+
:type1
|
|
210
|
+
when TrueTypeFont
|
|
211
|
+
:ttf
|
|
212
|
+
when OpenTypeFont
|
|
213
|
+
:otf
|
|
214
|
+
else
|
|
215
|
+
# Try to detect from tables
|
|
216
|
+
if font.respond_to?(:tables)
|
|
217
|
+
if font.tables.key?("glyf")
|
|
218
|
+
:ttf
|
|
219
|
+
elsif font.tables.key?("CFF ") || font.tables.key?("CFF2")
|
|
220
|
+
:otf
|
|
221
|
+
else
|
|
222
|
+
raise Fontisan::Error, "Cannot detect font format"
|
|
223
|
+
end
|
|
224
|
+
else
|
|
225
|
+
raise Fontisan::Error, "Unknown font type: #{font.class}"
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Detect target format from font class or options
|
|
231
|
+
#
|
|
232
|
+
# @param font [Type1Font, OpenTypeFont, TrueTypeFont] Source font
|
|
233
|
+
# @return [Symbol] Target format
|
|
234
|
+
def detect_target_format(font)
|
|
235
|
+
case font
|
|
236
|
+
when Type1Font
|
|
237
|
+
:otf # Default: Type 1 → OTF
|
|
238
|
+
when TrueTypeFont
|
|
239
|
+
:type1 # TTF → Type 1
|
|
240
|
+
when OpenTypeFont
|
|
241
|
+
:type1 # OTF → Type 1
|
|
242
|
+
else
|
|
243
|
+
:otf
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Convert Type 1 font to OpenType/CFF
|
|
248
|
+
#
|
|
249
|
+
# @param font [Type1Font] Source Type 1 font
|
|
250
|
+
# @param options [Hash] Conversion options
|
|
251
|
+
# @return [Hash<String, String>] Target tables including CFF table
|
|
252
|
+
def convert_type1_to_otf(font, _options = {})
|
|
253
|
+
# Convert Type 1 CharStrings to CFF format
|
|
254
|
+
converter = Type1::CharStringConverter.new(font.charstrings)
|
|
255
|
+
cff_charstrings = {}
|
|
256
|
+
|
|
257
|
+
font.charstrings.each_charstring do |glyph_name, charstring|
|
|
258
|
+
cff_charstrings[glyph_name] = converter.convert(charstring)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Build font dictionary for CFF
|
|
262
|
+
font_dict = build_cff_font_dict(font)
|
|
263
|
+
|
|
264
|
+
# Build private dictionary for CFF
|
|
265
|
+
private_dict = build_cff_private_dict(font)
|
|
266
|
+
|
|
267
|
+
# Build CFF table
|
|
268
|
+
# Note: This is a simplified implementation
|
|
269
|
+
# A full implementation would build proper CFF INDEX structures
|
|
270
|
+
cff_data = build_cff_table_data(font, cff_charstrings, font_dict,
|
|
271
|
+
private_dict)
|
|
272
|
+
|
|
273
|
+
# Build other required SFNT tables
|
|
274
|
+
tables = {}
|
|
275
|
+
|
|
276
|
+
# Build head table
|
|
277
|
+
tables["head"] = build_head_table(font)
|
|
278
|
+
|
|
279
|
+
# Build hhea table
|
|
280
|
+
tables["hhea"] = build_hhea_table(font)
|
|
281
|
+
|
|
282
|
+
# Build maxp table
|
|
283
|
+
tables["maxp"] = build_maxp_table(font)
|
|
284
|
+
|
|
285
|
+
# Build name table
|
|
286
|
+
tables["name"] = build_name_table(font)
|
|
287
|
+
|
|
288
|
+
# Build OS/2 table
|
|
289
|
+
tables["OS/2"] = build_os2_table(font)
|
|
290
|
+
|
|
291
|
+
# Build post table
|
|
292
|
+
tables["post"] = build_post_table(font)
|
|
293
|
+
|
|
294
|
+
# Build cmap table
|
|
295
|
+
tables["cmap"] = build_cmap_table(font)
|
|
296
|
+
|
|
297
|
+
# Add CFF table
|
|
298
|
+
tables["CFF "] = cff_data
|
|
299
|
+
|
|
300
|
+
tables
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Convert OpenType/CFF font to Type 1
|
|
304
|
+
#
|
|
305
|
+
# @param font [OpenTypeFont] Source OpenType font
|
|
306
|
+
# @return [Hash<String, String>] Type 1 font data as PFB
|
|
307
|
+
def convert_otf_to_type1(font)
|
|
308
|
+
# Extract CFF table
|
|
309
|
+
cff_table = font.table("CFF ")
|
|
310
|
+
raise Fontisan::Error, "CFF table not found" unless cff_table
|
|
311
|
+
|
|
312
|
+
# Parse CFF table to extract CharStrings
|
|
313
|
+
# Note: This is a simplified implementation
|
|
314
|
+
# A full implementation would parse CFF INDEX structures
|
|
315
|
+
|
|
316
|
+
# Convert CFF CharStrings to Type 1 format
|
|
317
|
+
type1_charstrings = {}
|
|
318
|
+
Type1::CharStringConverter.new
|
|
319
|
+
|
|
320
|
+
# Extract glyph outlines from CFF
|
|
321
|
+
# For each glyph, convert CFF CharString to Type 1
|
|
322
|
+
font.outlines.each_with_index do |outline, index|
|
|
323
|
+
glyph_name = font.glyph_name(index) || "glyph#{index}"
|
|
324
|
+
# Reverse conversion: CFF → Type 1
|
|
325
|
+
# This is a placeholder - full implementation requires CFF parser
|
|
326
|
+
type1_charstrings[glyph_name] = convert_cff_to_type1(outline)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Build Type 1 font data
|
|
330
|
+
build_type1_data(font, type1_charstrings)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Convert Type 1 font to TrueType (via OTF)
|
|
334
|
+
#
|
|
335
|
+
# @param font [Type1Font] Source Type 1 font
|
|
336
|
+
# @param options [Hash] Conversion options
|
|
337
|
+
# @return [Hash<String, String>] Target tables including glyf table
|
|
338
|
+
def convert_type1_to_ttf(font, options = {})
|
|
339
|
+
# First convert to OTF
|
|
340
|
+
otf_tables = convert_type1_to_otf(font, options)
|
|
341
|
+
|
|
342
|
+
# Then use OutlineConverter to convert OTF to TTF
|
|
343
|
+
# Create a temporary OTF font object
|
|
344
|
+
temp_otf = OpenTypeFont.new
|
|
345
|
+
otf_tables.each do |tag, data|
|
|
346
|
+
temp_otf.tables[tag] = data
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Use OutlineConverter for OTF → TTF
|
|
350
|
+
outline_converter = OutlineConverter.new(
|
|
351
|
+
optimize_cff: @optimize_cff,
|
|
352
|
+
preserve_hints: @preserve_hints,
|
|
353
|
+
target_format: :ttf,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
outline_converter.convert(temp_otf, target_format: :ttf)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Convert TrueType font to Type 1 (via OTF)
|
|
360
|
+
#
|
|
361
|
+
# @param font [TrueTypeFont] Source TrueType font
|
|
362
|
+
# @return [Hash<String, String>] Type 1 font data as PFB
|
|
363
|
+
def convert_ttf_to_type1(font)
|
|
364
|
+
# First use OutlineConverter to convert TTF to OTF
|
|
365
|
+
outline_converter = OutlineConverter.new(
|
|
366
|
+
optimize_cff: @optimize_cff,
|
|
367
|
+
preserve_hints: @preserve_hints,
|
|
368
|
+
target_format: :otf,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
otf_tables = outline_converter.convert(font, target_format: :otf)
|
|
372
|
+
|
|
373
|
+
# Create a temporary OTF font object
|
|
374
|
+
temp_otf = OpenTypeFont.new
|
|
375
|
+
otf_tables.each do |tag, data|
|
|
376
|
+
temp_otf.tables[tag] = data
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Then convert OTF to Type 1
|
|
380
|
+
convert_otf_to_type1(temp_otf)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Build CFF font dictionary from Type 1 font
|
|
384
|
+
#
|
|
385
|
+
# @param font [Type1Font] Source Type 1 font
|
|
386
|
+
# @return [Hash] CFF font dictionary data
|
|
387
|
+
def build_cff_font_dict(font)
|
|
388
|
+
{
|
|
389
|
+
version: font.font_dictionary.version || "001.000",
|
|
390
|
+
notice: font.font_dictionary.notice || "",
|
|
391
|
+
copyright: font.font_dictionary.copyright || "",
|
|
392
|
+
full_name: font.font_dictionary.full_name || font.font_name,
|
|
393
|
+
family_name: font.font_dictionary.family_name || font.font_name,
|
|
394
|
+
weight: font.font_dictionary.weight || "Medium",
|
|
395
|
+
font_b_box: font.font_dictionary.font_bbox || [0, 0, 0, 0],
|
|
396
|
+
font_matrix: font.font_dictionary.font_matrix || [0.001, 0, 0, 0.001,
|
|
397
|
+
0, 0],
|
|
398
|
+
charset: font.charstrings.encoding.keys,
|
|
399
|
+
encoding: font.charstrings.encoding,
|
|
400
|
+
}
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Build CFF private dictionary from Type 1 font
|
|
404
|
+
#
|
|
405
|
+
# @param font [Type1Font] Source Type 1 font
|
|
406
|
+
# @return [Hash] CFF private dictionary data
|
|
407
|
+
def build_cff_private_dict(font)
|
|
408
|
+
private_dict = font.private_dict
|
|
409
|
+
{
|
|
410
|
+
blue_values: private_dict.blue_values || [],
|
|
411
|
+
other_blues: private_dict.other_blues || [],
|
|
412
|
+
family_blues: private_dict.family_blues || [],
|
|
413
|
+
family_other_blues: private_dict.family_other_blues || [],
|
|
414
|
+
blue_scale: private_dict.blue_scale || 0.039625,
|
|
415
|
+
blue_shift: private_dict.blue_shift || 7,
|
|
416
|
+
blue_fuzz: private_dict.blue_fuzz || 1,
|
|
417
|
+
std_hw: private_dict.std_hw || 0,
|
|
418
|
+
std_vw: private_dict.std_vw || 0,
|
|
419
|
+
stem_snap_h: private_dict.stem_snap_h || [],
|
|
420
|
+
stem_snap_v: private_dict.stem_snap_v || [],
|
|
421
|
+
force_bold: private_dict.force_bold || false,
|
|
422
|
+
language_group: private_dict.language_group || 0,
|
|
423
|
+
expansion_factor: private_dict.expansion_factor || 0.06,
|
|
424
|
+
initial_random_seed: private_dict.initial_random_seed || 0,
|
|
425
|
+
}
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Build CFF table data
|
|
429
|
+
#
|
|
430
|
+
# @param font [Type1Font] Source Type 1 font
|
|
431
|
+
# @param charstrings [Hash] CFF CharStrings (glyph_name => data)
|
|
432
|
+
# @param font_dict [Hash] CFF font dictionary (not used, kept for compatibility)
|
|
433
|
+
# @param private_dict [Hash] CFF private dictionary (not used, kept for compatibility)
|
|
434
|
+
# @return [String] CFF table binary data
|
|
435
|
+
def build_cff_table_data(font, charstrings, _font_dict, _private_dict)
|
|
436
|
+
# Convert charstrings hash to array (build_cff_table expects array)
|
|
437
|
+
charstrings_array = charstrings.values
|
|
438
|
+
|
|
439
|
+
# Build CFF table using CffTableBuilder
|
|
440
|
+
# We need to pass the Type1Font as-is for metadata extraction
|
|
441
|
+
build_cff_table(charstrings_array, [], font)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Override extract_font_name to handle Type1Font
|
|
445
|
+
#
|
|
446
|
+
# @param font [Type1Font, TrueTypeFont, OpenTypeFont] Font
|
|
447
|
+
# @return [String] Font name
|
|
448
|
+
def extract_font_name(font)
|
|
449
|
+
if font.is_a?(Type1Font)
|
|
450
|
+
# Get font name from Type1Font
|
|
451
|
+
name = font.font_name || font.font_dictionary&.font_name
|
|
452
|
+
return name.dup.force_encoding("ASCII-8BIT") if name
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Fall back to original implementation for TrueTypeFont/OpenTypeFont
|
|
456
|
+
super
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Convert CFF outline to Type 1 CharString
|
|
460
|
+
#
|
|
461
|
+
# @param outline [Outline] Glyph outline
|
|
462
|
+
# @return [String] Type 1 CharString bytecode
|
|
463
|
+
def convert_cff_to_type1(_outline)
|
|
464
|
+
# Reverse conversion from CFF to Type 1
|
|
465
|
+
# This is a placeholder implementation
|
|
466
|
+
# Full implementation requires:
|
|
467
|
+
# 1. Parse CFF CharString to commands
|
|
468
|
+
# 2. Map CFF operators to Type 1 operators
|
|
469
|
+
# 3. Encode numbers in Type 1 format
|
|
470
|
+
# 4. Handle hints and subroutines
|
|
471
|
+
|
|
472
|
+
String.new(encoding: Encoding::ASCII_8BIT)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Build Type 1 font data
|
|
476
|
+
#
|
|
477
|
+
# @param font [OpenTypeFont] Source OpenType font
|
|
478
|
+
# @param charstrings [Hash] Type 1 CharStrings
|
|
479
|
+
# @return [Hash] Type 1 font data with :pfb key
|
|
480
|
+
def build_type1_data(_font, _charstrings)
|
|
481
|
+
# Build PFB format
|
|
482
|
+
# This is a placeholder implementation
|
|
483
|
+
# Full implementation requires:
|
|
484
|
+
# 1. Build Font Dictionary
|
|
485
|
+
# 2. Build Private Dictionary
|
|
486
|
+
# 3. Build CharStrings
|
|
487
|
+
# 4. Encrypt with eexec
|
|
488
|
+
# 5. Format as PFB chunks
|
|
489
|
+
|
|
490
|
+
pfb_data = String.new(encoding: Encoding::ASCII_8BIT)
|
|
491
|
+
|
|
492
|
+
{ pfb: pfb_data }
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Build head table from Type 1 font
|
|
496
|
+
#
|
|
497
|
+
# @param font [Type1Font] Source Type 1 font
|
|
498
|
+
# @return [String] head table binary data
|
|
499
|
+
def build_head_table(_font)
|
|
500
|
+
# Placeholder: Build actual head table
|
|
501
|
+
String.new(encoding: Encoding::ASCII_8BIT)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Build hhea table from Type 1 font
|
|
505
|
+
#
|
|
506
|
+
# @param font [Type1Font] Source Type 1 font
|
|
507
|
+
# @return [String] hhea table binary data
|
|
508
|
+
def build_hhea_table(_font)
|
|
509
|
+
# Placeholder: Build actual hhea table
|
|
510
|
+
String.new(encoding: Encoding::ASCII_8BIT)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Build maxp table from Type 1 font
|
|
514
|
+
#
|
|
515
|
+
# @param font [Type1Font] Source Type 1 font
|
|
516
|
+
# @return [String] maxp table binary data
|
|
517
|
+
def build_maxp_table(_font)
|
|
518
|
+
# Placeholder: Build actual maxp table
|
|
519
|
+
String.new(encoding: Encoding::ASCII_8BIT)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Build name table from Type 1 font
|
|
523
|
+
#
|
|
524
|
+
# @param font [Type1Font] Source Type 1 font
|
|
525
|
+
# @return [String] name table binary data
|
|
526
|
+
def build_name_table(_font)
|
|
527
|
+
# Placeholder: Build actual name table
|
|
528
|
+
String.new(encoding: Encoding::ASCII_8BIT)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Build OS/2 table from Type 1 font
|
|
532
|
+
#
|
|
533
|
+
# @param font [Type1Font] Source Type 1 font
|
|
534
|
+
# @return [String] OS/2 table binary data
|
|
535
|
+
def build_os2_table(_font)
|
|
536
|
+
# Placeholder: Build actual OS/2 table
|
|
537
|
+
String.new(encoding: Encoding::ASCII_8BIT)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Build post table from Type 1 font
|
|
541
|
+
#
|
|
542
|
+
# @param font [Type1Font] Source Type 1 font
|
|
543
|
+
# @return [String] post table binary data
|
|
544
|
+
def build_post_table(_font)
|
|
545
|
+
# Placeholder: Build actual post table
|
|
546
|
+
String.new(encoding: Encoding::ASCII_8BIT)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Build cmap table from Type 1 font
|
|
550
|
+
#
|
|
551
|
+
# @param font [Type1Font] Source Type 1 font
|
|
552
|
+
# @return [String] cmap table binary data
|
|
553
|
+
def build_cmap_table(_font)
|
|
554
|
+
# Placeholder: Build actual cmap table
|
|
555
|
+
String.new(encoding: Encoding::ASCII_8BIT)
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
end
|
data/lib/fontisan/font_loader.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "true_type_collection"
|
|
|
8
8
|
require_relative "open_type_collection"
|
|
9
9
|
require_relative "woff_font"
|
|
10
10
|
require_relative "woff2_font"
|
|
11
|
+
require_relative "type1_font"
|
|
11
12
|
require_relative "error"
|
|
12
13
|
|
|
13
14
|
module Fontisan
|
|
@@ -15,11 +16,13 @@ module Fontisan
|
|
|
15
16
|
#
|
|
16
17
|
# This class is the primary entry point for loading fonts in Fontisan.
|
|
17
18
|
# It automatically detects the font format and returns the appropriate
|
|
18
|
-
# domain object (TrueTypeFont, OpenTypeFont, TrueTypeCollection, or OpenTypeCollection).
|
|
19
|
+
# domain object (TrueTypeFont, OpenTypeFont, Type1Font, TrueTypeCollection, or OpenTypeCollection).
|
|
19
20
|
#
|
|
20
21
|
# @example Load any font type
|
|
21
22
|
# font = FontLoader.load("font.ttf") # => TrueTypeFont
|
|
22
23
|
# font = FontLoader.load("font.otf") # => OpenTypeFont
|
|
24
|
+
# font = FontLoader.load("font.pfb") # => Type1Font
|
|
25
|
+
# font = FontLoader.load("font.pfa") # => Type1Font
|
|
23
26
|
# font = FontLoader.load("fonts.ttc") # => TrueTypeFont (first in collection)
|
|
24
27
|
# font = FontLoader.load("fonts.ttc", font_index: 2) # => TrueTypeFont (third in collection)
|
|
25
28
|
#
|
|
@@ -37,7 +40,7 @@ module Fontisan
|
|
|
37
40
|
# @param font_index [Integer] Index of font in collection (0-based, default: 0)
|
|
38
41
|
# @param mode [Symbol] Loading mode (:metadata or :full, default: from ENV or :full)
|
|
39
42
|
# @param lazy [Boolean] If true, load tables on demand (default: false for eager loading)
|
|
40
|
-
# @return [TrueTypeFont, OpenTypeFont, WoffFont, Woff2Font] The loaded font object
|
|
43
|
+
# @return [TrueTypeFont, OpenTypeFont, Type1Font, WoffFont, Woff2Font] The loaded font object
|
|
41
44
|
# @raise [Errno::ENOENT] if file does not exist
|
|
42
45
|
# @raise [UnsupportedFormatError] for unsupported formats
|
|
43
46
|
# @raise [InvalidFontError] for corrupted or unknown formats
|
|
@@ -55,6 +58,11 @@ module Fontisan
|
|
|
55
58
|
# Validate mode
|
|
56
59
|
LoadingModes.validate_mode!(resolved_mode)
|
|
57
60
|
|
|
61
|
+
# Check for Type 1 format first (PFB/PFA have different signatures)
|
|
62
|
+
if type1_font?(path)
|
|
63
|
+
return Type1Font.from_file(path)
|
|
64
|
+
end
|
|
65
|
+
|
|
58
66
|
File.open(path, "rb") do |io|
|
|
59
67
|
signature = io.read(4)
|
|
60
68
|
io.rewind
|
|
@@ -76,7 +84,7 @@ module Fontisan
|
|
|
76
84
|
resolved_lazy)
|
|
77
85
|
else
|
|
78
86
|
raise InvalidFontError,
|
|
79
|
-
"Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, or
|
|
87
|
+
"Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, WOFF2, PFB, or PFA file."
|
|
80
88
|
end
|
|
81
89
|
end
|
|
82
90
|
end
|
|
@@ -387,5 +395,40 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
387
395
|
end
|
|
388
396
|
|
|
389
397
|
private_class_method :dfont_signature?
|
|
398
|
+
|
|
399
|
+
# Check if file is a Type 1 font (PFB or PFA)
|
|
400
|
+
#
|
|
401
|
+
# Type 1 fonts come in two formats:
|
|
402
|
+
# - PFB (Printer Font Binary): Binary format with chunk markers
|
|
403
|
+
# - PFA (Printer Font ASCII): ASCII text format with hex encoding
|
|
404
|
+
#
|
|
405
|
+
# @param path [String] Path to the font file
|
|
406
|
+
# @return [Boolean] true if Type 1 font
|
|
407
|
+
# @api private
|
|
408
|
+
def self.type1_font?(path)
|
|
409
|
+
# Check file extension first (quick check)
|
|
410
|
+
ext = File.extname(path).downcase
|
|
411
|
+
return true if [".pfb", ".pfa", ".ps"].include?(ext)
|
|
412
|
+
|
|
413
|
+
# Check PFB signature (first byte should be 0x80 or 0x81)
|
|
414
|
+
File.open(path, "rb") do |io|
|
|
415
|
+
first_byte = io.getbyte
|
|
416
|
+
return true if [Constants::PFB_ASCII_CHUNK, Constants::PFB_BINARY_CHUNK].include?(first_byte)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Check PFA signature (text file with Adobe header)
|
|
420
|
+
File.open(path, "rb") do |io|
|
|
421
|
+
# Read first 100 bytes to check for PFA signature
|
|
422
|
+
header = io.read(100)
|
|
423
|
+
return true if header.include?(Constants::PFA_SIGNATURE_ADOBE_1_0) ||
|
|
424
|
+
header.include?(Constants::PFA_SIGNATURE_ADOBE_3_0)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
false
|
|
428
|
+
rescue IOError, Errno::ENOENT
|
|
429
|
+
false
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
private_class_method :type1_font?
|
|
390
433
|
end
|
|
391
434
|
end
|