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,1234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../conversion_options"
|
|
4
|
+
require_relative "../type1/charstring_converter"
|
|
5
|
+
require_relative "../type1/cff_to_type1_converter"
|
|
6
|
+
require_relative "../type1/font_dictionary"
|
|
7
|
+
require_relative "../type1/charstrings"
|
|
8
|
+
require_relative "../type1/seac_expander"
|
|
9
|
+
require_relative "../type1_font"
|
|
10
|
+
require_relative "cff_table_builder"
|
|
11
|
+
|
|
12
|
+
module Fontisan
|
|
13
|
+
module Converters
|
|
14
|
+
# Converter for Adobe Type 1 fonts to/from SFNT formats.
|
|
15
|
+
#
|
|
16
|
+
# [`Type1Converter`](lib/fontisan/converters/type1_converter.rb) handles
|
|
17
|
+
# bidirectional conversion between Type 1 fonts (PFB/PFA) and SFNT-based
|
|
18
|
+
# formats (TTF, OTF, WOFF, WOFF2).
|
|
19
|
+
#
|
|
20
|
+
# == Conversion Strategy
|
|
21
|
+
#
|
|
22
|
+
# Type 1 fonts use PostScript CharStrings that are similar to CFF CharStrings
|
|
23
|
+
# used in OpenType fonts. The conversion uses CharStringConverter for the
|
|
24
|
+
# CharString translation.
|
|
25
|
+
#
|
|
26
|
+
# * Type 1 → OTF: Convert Type 1 CharStrings to CFF format, build CFF table
|
|
27
|
+
# * OTF → Type 1: Convert CFF CharStrings to Type 1 format, build PFB/PFA
|
|
28
|
+
# * Type 1 → TTF: Type 1 → OTF → TTF (via OutlineConverter)
|
|
29
|
+
# * TTF → Type 1: TTF → OTF → Type 1
|
|
30
|
+
#
|
|
31
|
+
# == Conversion Options
|
|
32
|
+
#
|
|
33
|
+
# The converter accepts [`ConversionOptions`](../conversion_options) with
|
|
34
|
+
# opening and generating options:
|
|
35
|
+
#
|
|
36
|
+
# * Opening options: decompose_composites, generate_unicode, read_all_records
|
|
37
|
+
# * Generating options: decompose_on_output, hinting_mode, write_pfm, write_afm
|
|
38
|
+
#
|
|
39
|
+
# @example Convert Type 1 to OTF with options
|
|
40
|
+
# font = FontLoader.load("font.pfb")
|
|
41
|
+
# options = ConversionOptions.recommended(from: :type1, to: :otf)
|
|
42
|
+
# converter = Type1Converter.new
|
|
43
|
+
# tables = converter.convert(font, options: options)
|
|
44
|
+
#
|
|
45
|
+
# @example Convert with preset
|
|
46
|
+
# options = ConversionOptions.from_preset(:type1_to_modern)
|
|
47
|
+
# tables = converter.convert(font, options: options)
|
|
48
|
+
#
|
|
49
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
50
|
+
# @see CharStringConverter
|
|
51
|
+
class Type1Converter
|
|
52
|
+
include ConversionStrategy
|
|
53
|
+
include CffTableBuilder
|
|
54
|
+
|
|
55
|
+
# Initialize a new Type1Converter
|
|
56
|
+
#
|
|
57
|
+
# @param options [Hash] Converter options
|
|
58
|
+
# @option options [Boolean] :optimize_cff Enable CFF optimization (default: false)
|
|
59
|
+
# @option options [Boolean] :preserve_hints Preserve hinting (default: true)
|
|
60
|
+
# @option options [Symbol] :target_format Target format for conversion
|
|
61
|
+
def initialize(options = {})
|
|
62
|
+
@optimize_cff = options.fetch(:optimize_cff, false)
|
|
63
|
+
@preserve_hints = options.fetch(:preserve_hints, true)
|
|
64
|
+
@target_format = options[:target_format]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Convert font to target format
|
|
68
|
+
#
|
|
69
|
+
# @param font [Type1Font, OpenTypeFont, TrueTypeFont] Source font
|
|
70
|
+
# @param options [Hash, ConversionOptions] Conversion options
|
|
71
|
+
# @option options [Symbol] :target_format Target format override
|
|
72
|
+
# @option options [ConversionOptions] :options ConversionOptions object
|
|
73
|
+
# @return [Hash<String, String>] Map of table tags to binary data
|
|
74
|
+
def convert(font, options = {})
|
|
75
|
+
# Extract ConversionOptions if provided
|
|
76
|
+
conv_options = extract_conversion_options(options)
|
|
77
|
+
|
|
78
|
+
target_format = options[:target_format] || conv_options&.to || @target_format ||
|
|
79
|
+
detect_target_format(font)
|
|
80
|
+
validate(font, target_format)
|
|
81
|
+
|
|
82
|
+
# Apply opening options to source font
|
|
83
|
+
apply_opening_options(font, conv_options) if conv_options
|
|
84
|
+
|
|
85
|
+
source_format = detect_format(font)
|
|
86
|
+
|
|
87
|
+
case [source_format, target_format]
|
|
88
|
+
when %i[type1 otf]
|
|
89
|
+
convert_type1_to_otf(font, conv_options)
|
|
90
|
+
when %i[otf type1]
|
|
91
|
+
convert_otf_to_type1(font, conv_options)
|
|
92
|
+
when %i[type1 ttf]
|
|
93
|
+
convert_type1_to_ttf(font, conv_options)
|
|
94
|
+
when %i[ttf type1]
|
|
95
|
+
convert_ttf_to_type1(font, conv_options)
|
|
96
|
+
else
|
|
97
|
+
raise Fontisan::Error,
|
|
98
|
+
"Unsupported conversion: #{source_format} → #{target_format}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get supported conversions
|
|
103
|
+
#
|
|
104
|
+
# @return [Array<Array<Symbol>>] Supported conversion pairs
|
|
105
|
+
def supported_conversions
|
|
106
|
+
[
|
|
107
|
+
%i[type1 otf],
|
|
108
|
+
%i[otf type1],
|
|
109
|
+
%i[type1 ttf],
|
|
110
|
+
%i[ttf type1],
|
|
111
|
+
]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Validate font for conversion
|
|
115
|
+
#
|
|
116
|
+
# @param font [Type1Font, OpenTypeFont, TrueTypeFont] Font to validate
|
|
117
|
+
# @param target_format [Symbol] Target format
|
|
118
|
+
# @return [Boolean] True if valid
|
|
119
|
+
# @raise [ArgumentError] If font is invalid
|
|
120
|
+
# @raise [Error] If conversion is not supported
|
|
121
|
+
def validate(font, target_format)
|
|
122
|
+
raise ArgumentError, "Font cannot be nil" if font.nil?
|
|
123
|
+
|
|
124
|
+
unless font.respond_to?(:font_dictionary) || font.respond_to?(:tables)
|
|
125
|
+
raise ArgumentError,
|
|
126
|
+
"Font must be Type1Font or have :tables method"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
source_format = detect_format(font)
|
|
130
|
+
unless supports?(source_format, target_format)
|
|
131
|
+
raise Fontisan::Error,
|
|
132
|
+
"Conversion #{source_format} → #{target_format} not supported"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# Extract ConversionOptions from options hash
|
|
141
|
+
#
|
|
142
|
+
# @param options [Hash, ConversionOptions] Options or hash containing :options key
|
|
143
|
+
# @return [ConversionOptions, nil] Extracted ConversionOptions or nil
|
|
144
|
+
def extract_conversion_options(options)
|
|
145
|
+
return options if options.is_a?(ConversionOptions)
|
|
146
|
+
|
|
147
|
+
options[:options] if options.is_a?(Hash)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Apply opening options to source font
|
|
151
|
+
#
|
|
152
|
+
# @param font [Type1Font] Source font
|
|
153
|
+
# @param conv_options [ConversionOptions] Conversion options with opening options
|
|
154
|
+
def apply_opening_options(font, conv_options)
|
|
155
|
+
return unless font.is_a?(Type1Font)
|
|
156
|
+
return unless conv_options
|
|
157
|
+
|
|
158
|
+
# Generate Unicode codepoints if requested
|
|
159
|
+
if conv_options.opening_option?(:generate_unicode)
|
|
160
|
+
generate_unicode_mappings(font)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Decompose seac composites if requested
|
|
164
|
+
if conv_options.opening_option?(:decompose_composites)
|
|
165
|
+
decompose_seac_glyphs(font)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Read all font dictionary records if requested
|
|
169
|
+
if conv_options.opening_option?(:read_all_records) && font.font_dictionary.respond_to?(:reload)
|
|
170
|
+
# Ensure full font dictionary is loaded
|
|
171
|
+
font.font_dictionary.reload
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Generate Unicode codepoints from glyph names/encoding
|
|
176
|
+
#
|
|
177
|
+
# @param font [Type1Font] Source Type 1 font
|
|
178
|
+
def generate_unicode_mappings(_font)
|
|
179
|
+
# Placeholder: Generate Unicode mappings from glyph names
|
|
180
|
+
# A full implementation would:
|
|
181
|
+
# 1. Parse the Adobe Glyph List
|
|
182
|
+
# 2. Map glyph names to Unicode codepoints
|
|
183
|
+
# 3. Update the charstrings encoding
|
|
184
|
+
#
|
|
185
|
+
# For now, this is a no-op placeholder
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Decompose seac composite glyphs to base glyphs
|
|
190
|
+
#
|
|
191
|
+
# @param font [Type1Font] Source Type 1 font
|
|
192
|
+
def decompose_seac_glyphs(font)
|
|
193
|
+
return unless font.charstrings
|
|
194
|
+
|
|
195
|
+
# Create SeacExpander to decompose composite glyphs
|
|
196
|
+
expander = Type1::SeacExpander.new(font.charstrings, font.private_dict)
|
|
197
|
+
|
|
198
|
+
# Get all composite glyphs
|
|
199
|
+
composites = expander.composite_glyphs
|
|
200
|
+
return if composites.empty?
|
|
201
|
+
|
|
202
|
+
# Decompose each composite glyph
|
|
203
|
+
composites.each do |glyph_name|
|
|
204
|
+
decomposed = expander.decompose(glyph_name)
|
|
205
|
+
next if decomposed.nil? || decomposed.empty?
|
|
206
|
+
|
|
207
|
+
# Update the CharString with decomposed version
|
|
208
|
+
# Access the charstrings hash directly and update
|
|
209
|
+
charstrings_hash = font.charstrings.charstrings
|
|
210
|
+
charstrings_hash[glyph_name] = decomposed
|
|
211
|
+
|
|
212
|
+
# Mark as decomposed (no longer a seac composite)
|
|
213
|
+
# The decomposed CharString no longer contains the seac operator
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Detect font format
|
|
218
|
+
#
|
|
219
|
+
# @param font [Type1Font, OpenTypeFont, TrueTypeFont] Font to detect
|
|
220
|
+
# @return [Symbol] Font format (:type1, :ttf, :otf)
|
|
221
|
+
def detect_format(font)
|
|
222
|
+
case font
|
|
223
|
+
when Type1Font
|
|
224
|
+
:type1
|
|
225
|
+
when TrueTypeFont
|
|
226
|
+
:ttf
|
|
227
|
+
when OpenTypeFont
|
|
228
|
+
:otf
|
|
229
|
+
else
|
|
230
|
+
# Try to detect from tables
|
|
231
|
+
if font.respond_to?(:tables)
|
|
232
|
+
if font.tables.key?("glyf")
|
|
233
|
+
:ttf
|
|
234
|
+
elsif font.tables.key?("CFF ") || font.tables.key?("CFF2")
|
|
235
|
+
:otf
|
|
236
|
+
else
|
|
237
|
+
raise Fontisan::Error, "Cannot detect font format"
|
|
238
|
+
end
|
|
239
|
+
else
|
|
240
|
+
raise Fontisan::Error, "Unknown font type: #{font.class}"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Detect target format from font class or options
|
|
246
|
+
#
|
|
247
|
+
# @param font [Type1Font, OpenTypeFont, TrueTypeFont] Source font
|
|
248
|
+
# @return [Symbol] Target format
|
|
249
|
+
def detect_target_format(font)
|
|
250
|
+
case font
|
|
251
|
+
when Type1Font
|
|
252
|
+
:otf # Default: Type 1 → OTF
|
|
253
|
+
when TrueTypeFont
|
|
254
|
+
:type1 # TTF → Type 1
|
|
255
|
+
when OpenTypeFont
|
|
256
|
+
:type1 # OTF → Type 1
|
|
257
|
+
else
|
|
258
|
+
:otf
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Convert Type 1 font to OpenType/CFF
|
|
263
|
+
#
|
|
264
|
+
# @param font [Type1Font] Source Type 1 font
|
|
265
|
+
# @param options [Hash] Conversion options
|
|
266
|
+
# @return [Hash<String, String>] Target tables including CFF table
|
|
267
|
+
def convert_type1_to_otf(font, _options = {})
|
|
268
|
+
# Convert Type 1 CharStrings to CFF format
|
|
269
|
+
converter = Type1::CharStringConverter.new(font.charstrings)
|
|
270
|
+
cff_charstrings = {}
|
|
271
|
+
|
|
272
|
+
font.charstrings.each_charstring do |glyph_name, charstring|
|
|
273
|
+
cff_charstrings[glyph_name] = converter.convert(charstring)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Build font dictionary for CFF
|
|
277
|
+
font_dict = build_cff_font_dict(font)
|
|
278
|
+
|
|
279
|
+
# Build private dictionary for CFF
|
|
280
|
+
private_dict = build_cff_private_dict(font)
|
|
281
|
+
|
|
282
|
+
# Build CFF table
|
|
283
|
+
# Note: This is a simplified implementation
|
|
284
|
+
# A full implementation would build proper CFF INDEX structures
|
|
285
|
+
cff_data = build_cff_table_data(font, cff_charstrings, font_dict,
|
|
286
|
+
private_dict)
|
|
287
|
+
|
|
288
|
+
# Build other required SFNT tables
|
|
289
|
+
tables = {}
|
|
290
|
+
|
|
291
|
+
# Build head table
|
|
292
|
+
tables["head"] = build_head_table(font)
|
|
293
|
+
|
|
294
|
+
# Build hhea table
|
|
295
|
+
tables["hhea"] = build_hhea_table(font)
|
|
296
|
+
|
|
297
|
+
# Build maxp table
|
|
298
|
+
tables["maxp"] = build_maxp_table(font)
|
|
299
|
+
|
|
300
|
+
# Build name table
|
|
301
|
+
tables["name"] = build_name_table(font)
|
|
302
|
+
|
|
303
|
+
# Build OS/2 table
|
|
304
|
+
tables["OS/2"] = build_os2_table(font)
|
|
305
|
+
|
|
306
|
+
# Build post table
|
|
307
|
+
tables["post"] = build_post_table(font)
|
|
308
|
+
|
|
309
|
+
# Build cmap table
|
|
310
|
+
tables["cmap"] = build_cmap_table(font)
|
|
311
|
+
|
|
312
|
+
# Add CFF table
|
|
313
|
+
tables["CFF "] = cff_data
|
|
314
|
+
|
|
315
|
+
tables
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Convert OpenType/CFF font to Type 1
|
|
319
|
+
#
|
|
320
|
+
# @param font [OpenTypeFont] Source OpenType font
|
|
321
|
+
# @param options [Hash] Conversion options
|
|
322
|
+
# @return [Hash<String, String>] Type 1 font data as PFB
|
|
323
|
+
def convert_otf_to_type1(font, _options = {})
|
|
324
|
+
# Extract CFF table
|
|
325
|
+
cff_table = font.table("CFF ")
|
|
326
|
+
raise Fontisan::Error, "CFF table not found" unless cff_table
|
|
327
|
+
|
|
328
|
+
# Get CharStrings INDEX from CFF
|
|
329
|
+
charstrings_index = cff_table.charstrings_index(0)
|
|
330
|
+
raise Fontisan::Error, "CharStrings INDEX not found" unless charstrings_index
|
|
331
|
+
|
|
332
|
+
# Get Private DICT for context
|
|
333
|
+
private_dict = cff_table.private_dict(0)
|
|
334
|
+
|
|
335
|
+
# Create CFF to Type 1 converter
|
|
336
|
+
converter = Type1::CffToType1Converter.new(
|
|
337
|
+
nominal_width: private_dict&.nominal_width || 0,
|
|
338
|
+
default_width: private_dict&.default_width || 0
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Convert each CFF CharString to Type 1 format
|
|
342
|
+
type1_charstrings = {}
|
|
343
|
+
glyph_count = charstrings_index.count
|
|
344
|
+
|
|
345
|
+
glyph_count.times do |glyph_index|
|
|
346
|
+
# Get raw CFF CharString data
|
|
347
|
+
cff_charstring = charstrings_index[glyph_index]
|
|
348
|
+
next unless cff_charstring
|
|
349
|
+
|
|
350
|
+
# Get glyph name
|
|
351
|
+
glyph_name = font.glyph_name(glyph_index) || "glyph#{glyph_index}"
|
|
352
|
+
|
|
353
|
+
# Convert CFF CharString to Type 1 format
|
|
354
|
+
private_dict_hash = build_private_dict_hash(private_dict)
|
|
355
|
+
type1_charstrings[glyph_name] = converter.convert(
|
|
356
|
+
cff_charstring,
|
|
357
|
+
private_dict: private_dict_hash
|
|
358
|
+
)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Build Type 1 font data
|
|
362
|
+
build_type1_data(font, type1_charstrings, cff_table)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Convert Type 1 font to TrueType (via OTF)
|
|
366
|
+
#
|
|
367
|
+
# @param font [Type1Font] Source Type 1 font
|
|
368
|
+
# @param options [Hash] Conversion options
|
|
369
|
+
# @return [Hash<String, String>] Target tables including glyf table
|
|
370
|
+
def convert_type1_to_ttf(font, options = {})
|
|
371
|
+
# First convert to OTF
|
|
372
|
+
otf_tables = convert_type1_to_otf(font, options)
|
|
373
|
+
|
|
374
|
+
# Then use OutlineConverter to convert OTF to TTF
|
|
375
|
+
# Create a temporary OTF font object
|
|
376
|
+
temp_otf = OpenTypeFont.new
|
|
377
|
+
otf_tables.each do |tag, data|
|
|
378
|
+
temp_otf.tables[tag] = data
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Use OutlineConverter for OTF → TTF
|
|
382
|
+
outline_converter = OutlineConverter.new(
|
|
383
|
+
optimize_cff: @optimize_cff,
|
|
384
|
+
preserve_hints: @preserve_hints,
|
|
385
|
+
target_format: :ttf,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
outline_converter.convert(temp_otf, target_format: :ttf)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Convert TrueType font to Type 1 (via OTF)
|
|
392
|
+
#
|
|
393
|
+
# @param font [TrueTypeFont] Source TrueType font
|
|
394
|
+
# @return [Hash<String, String>] Type 1 font data as PFB
|
|
395
|
+
def convert_ttf_to_type1(font)
|
|
396
|
+
# First use OutlineConverter to convert TTF to OTF
|
|
397
|
+
outline_converter = OutlineConverter.new(
|
|
398
|
+
optimize_cff: @optimize_cff,
|
|
399
|
+
preserve_hints: @preserve_hints,
|
|
400
|
+
target_format: :otf,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
otf_tables = outline_converter.convert(font, target_format: :otf)
|
|
404
|
+
|
|
405
|
+
# Create a temporary OTF font object
|
|
406
|
+
temp_otf = OpenTypeFont.new
|
|
407
|
+
otf_tables.each do |tag, data|
|
|
408
|
+
temp_otf.tables[tag] = data
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Then convert OTF to Type 1
|
|
412
|
+
convert_otf_to_type1(temp_otf)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Build CFF font dictionary from Type 1 font
|
|
416
|
+
#
|
|
417
|
+
# @param font [Type1Font] Source Type 1 font
|
|
418
|
+
# @return [Hash] CFF font dictionary data
|
|
419
|
+
def build_cff_font_dict(font)
|
|
420
|
+
{
|
|
421
|
+
version: font.font_dictionary.version || "001.000",
|
|
422
|
+
notice: font.font_dictionary.notice || "",
|
|
423
|
+
copyright: font.font_dictionary.copyright || "",
|
|
424
|
+
full_name: font.font_dictionary.full_name || font.font_name,
|
|
425
|
+
family_name: font.font_dictionary.family_name || font.font_name,
|
|
426
|
+
weight: font.font_dictionary.weight || "Medium",
|
|
427
|
+
font_b_box: font.font_dictionary.font_bbox || [0, 0, 0, 0],
|
|
428
|
+
font_matrix: font.font_dictionary.font_matrix || [0.001, 0, 0, 0.001,
|
|
429
|
+
0, 0],
|
|
430
|
+
charset: font.charstrings.encoding.keys,
|
|
431
|
+
encoding: font.charstrings.encoding,
|
|
432
|
+
}
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Build CFF private dictionary from Type 1 font
|
|
436
|
+
#
|
|
437
|
+
# @param font [Type1Font] Source Type 1 font
|
|
438
|
+
# @return [Hash] CFF private dictionary data
|
|
439
|
+
def build_cff_private_dict(font)
|
|
440
|
+
private_dict = font.private_dict
|
|
441
|
+
{
|
|
442
|
+
blue_values: private_dict.blue_values || [],
|
|
443
|
+
other_blues: private_dict.other_blues || [],
|
|
444
|
+
family_blues: private_dict.family_blues || [],
|
|
445
|
+
family_other_blues: private_dict.family_other_blues || [],
|
|
446
|
+
blue_scale: private_dict.blue_scale || 0.039625,
|
|
447
|
+
blue_shift: private_dict.blue_shift || 7,
|
|
448
|
+
blue_fuzz: private_dict.blue_fuzz || 1,
|
|
449
|
+
std_hw: private_dict.std_hw || 0,
|
|
450
|
+
std_vw: private_dict.std_vw || 0,
|
|
451
|
+
stem_snap_h: private_dict.stem_snap_h || [],
|
|
452
|
+
stem_snap_v: private_dict.stem_snap_v || [],
|
|
453
|
+
force_bold: private_dict.force_bold || false,
|
|
454
|
+
language_group: private_dict.language_group || 0,
|
|
455
|
+
expansion_factor: private_dict.expansion_factor || 0.06,
|
|
456
|
+
initial_random_seed: private_dict.initial_random_seed || 0,
|
|
457
|
+
}
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Build CFF table data
|
|
461
|
+
#
|
|
462
|
+
# @param font [Type1Font] Source Type 1 font
|
|
463
|
+
# @param charstrings [Hash] CFF CharStrings (glyph_name => data)
|
|
464
|
+
# @param font_dict [Hash] CFF font dictionary (not used, kept for compatibility)
|
|
465
|
+
# @param private_dict [Hash] CFF private dictionary (not used, kept for compatibility)
|
|
466
|
+
# @return [String] CFF table binary data
|
|
467
|
+
def build_cff_table_data(font, charstrings, _font_dict, _private_dict)
|
|
468
|
+
# Convert charstrings hash to array (build_cff_table expects array)
|
|
469
|
+
charstrings_array = charstrings.values
|
|
470
|
+
|
|
471
|
+
# Build CFF table using CffTableBuilder
|
|
472
|
+
# We need to pass the Type1Font as-is for metadata extraction
|
|
473
|
+
build_cff_table(charstrings_array, [], font)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Override extract_font_name to handle Type1Font
|
|
477
|
+
#
|
|
478
|
+
# @param font [Type1Font, TrueTypeFont, OpenTypeFont] Font
|
|
479
|
+
# @return [String] Font name
|
|
480
|
+
def extract_font_name(font)
|
|
481
|
+
if font.is_a?(Type1Font)
|
|
482
|
+
# Get font name from Type1Font
|
|
483
|
+
name = font.font_name || font.font_dictionary&.font_name
|
|
484
|
+
return name.dup.force_encoding("ASCII-8BIT") if name
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Fall back to original implementation for TrueTypeFont/OpenTypeFont
|
|
488
|
+
super
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Build Type 1 Private dictionary hash from CFF Private dict
|
|
492
|
+
#
|
|
493
|
+
# @param private_dict [Tables::Cff::PrivateDict] CFF Private dict
|
|
494
|
+
# @return [Hash] Private dictionary as hash for Type 1
|
|
495
|
+
def build_private_dict_hash(private_dict)
|
|
496
|
+
return {} unless private_dict
|
|
497
|
+
|
|
498
|
+
{
|
|
499
|
+
nominal_width: private_dict.nominal_width,
|
|
500
|
+
default_width: private_dict.default_width,
|
|
501
|
+
blue_values: private_dict.blue_values || [],
|
|
502
|
+
other_blues: private_dict.other_blues || [],
|
|
503
|
+
family_blues: private_dict.family_blues || [],
|
|
504
|
+
family_other_blues: private_dict.family_other_blues || [],
|
|
505
|
+
blue_scale: private_dict.blue_scale || 0.039625,
|
|
506
|
+
blue_shift: private_dict.blue_shift || 7,
|
|
507
|
+
blue_fuzz: private_dict.blue_fuzz || 1,
|
|
508
|
+
std_hw: private_dict.std_hw || 0,
|
|
509
|
+
std_vw: private_dict.std_vw || 0,
|
|
510
|
+
stem_snap_h: private_dict.stem_snap_h || [],
|
|
511
|
+
stem_snap_v: private_dict.stem_snap_v || [],
|
|
512
|
+
force_bold: private_dict.force_bold || false,
|
|
513
|
+
language_group: private_dict.language_group || 0,
|
|
514
|
+
expansion_factor: private_dict.expansion_factor || 0.06,
|
|
515
|
+
initial_random_seed: private_dict.initial_random_seed || 0,
|
|
516
|
+
}
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Build Type 1 font data
|
|
520
|
+
#
|
|
521
|
+
# @param font [OpenTypeFont] Source OpenType font
|
|
522
|
+
# @param charstrings [Hash] Type 1 CharStrings
|
|
523
|
+
# @param cff_table [Tables::Cff] CFF table for metadata
|
|
524
|
+
# @return [Hash] Type 1 font data with :pfb key
|
|
525
|
+
def build_type1_data(_font, _charstrings, _cff_table)
|
|
526
|
+
# Build PFB format
|
|
527
|
+
# This is a placeholder implementation
|
|
528
|
+
# Full implementation requires:
|
|
529
|
+
# 1. Build Font Dictionary
|
|
530
|
+
# 2. Build Private Dictionary
|
|
531
|
+
# 3. Build CharStrings
|
|
532
|
+
# 4. Encrypt with eexec
|
|
533
|
+
# 5. Format as PFB chunks
|
|
534
|
+
|
|
535
|
+
pfb_data = String.new(encoding: Encoding::ASCII_8BIT)
|
|
536
|
+
|
|
537
|
+
{ pfb: pfb_data }
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Build head table from Type 1 font
|
|
541
|
+
#
|
|
542
|
+
# @param font [Type1Font] Source Type 1 font
|
|
543
|
+
# @return [String] head table binary data
|
|
544
|
+
def build_head_table(font)
|
|
545
|
+
data = (+"").b
|
|
546
|
+
|
|
547
|
+
# Get font metadata from Type1Font
|
|
548
|
+
font_bbox = font.font_dictionary&.font_bbox || [0, 0, 1000, 1000]
|
|
549
|
+
version_str = font.version || "001.000"
|
|
550
|
+
|
|
551
|
+
# Parse version (e.g., "001.000" => 1.0)
|
|
552
|
+
version_parts = version_str.split(".")
|
|
553
|
+
major = version_parts[0].to_i
|
|
554
|
+
minor = version_parts[1]&.to_i || 0
|
|
555
|
+
version = major + (minor / 1000.0)
|
|
556
|
+
|
|
557
|
+
# Version (Fixed 16.16) - stored as int32
|
|
558
|
+
integer_part = version.to_i
|
|
559
|
+
fractional_part = ((version - integer_part) * 65_536).to_i
|
|
560
|
+
version_raw = (integer_part << 16) | fractional_part
|
|
561
|
+
data << [version_raw].pack("N")
|
|
562
|
+
|
|
563
|
+
# Font Revision (Fixed 16.16) - default to 1.0
|
|
564
|
+
font_revision_raw = 0x00010000
|
|
565
|
+
data << [font_revision_raw].pack("N")
|
|
566
|
+
|
|
567
|
+
# Checksum Adjustment (uint32) - will be calculated later
|
|
568
|
+
data << [0].pack("N")
|
|
569
|
+
|
|
570
|
+
# Magic Number (uint32)
|
|
571
|
+
data << [0x5F0F3CF5].pack("N")
|
|
572
|
+
|
|
573
|
+
# Flags (uint16) - bit 0 indicates y direction (0 = mixed)
|
|
574
|
+
data << [0].pack("n")
|
|
575
|
+
|
|
576
|
+
# Units Per Em (uint16) - Type 1 standard is 1000
|
|
577
|
+
data << [1000].pack("n")
|
|
578
|
+
|
|
579
|
+
# Created (LONGDATETIME) - use current time
|
|
580
|
+
created_seconds = Time.now.to_i + 2_082_844_800
|
|
581
|
+
data << [created_seconds].pack("Q>")
|
|
582
|
+
|
|
583
|
+
# Modified (LONGDATETIME) - use current time
|
|
584
|
+
modified_seconds = Time.now.to_i + 2_082_844_800
|
|
585
|
+
data << [modified_seconds].pack("Q>")
|
|
586
|
+
|
|
587
|
+
# Bounding box (int16 each)
|
|
588
|
+
data << [font_bbox[0]].pack("s>") # x_min
|
|
589
|
+
data << [font_bbox[1]].pack("s>") # y_min
|
|
590
|
+
data << [font_bbox[2]].pack("s>") # x_max
|
|
591
|
+
data << [font_bbox[3]].pack("s>") # y_max
|
|
592
|
+
|
|
593
|
+
# Mac Style (uint16) - no style bits set
|
|
594
|
+
data << [0].pack("n")
|
|
595
|
+
|
|
596
|
+
# Lowest Rec PPEM (uint16) - readable size
|
|
597
|
+
data << [8].pack("n")
|
|
598
|
+
|
|
599
|
+
# Font Direction Hint (int16)
|
|
600
|
+
# 2 = Left to right, mixed glyphs
|
|
601
|
+
data << [2].pack("s>")
|
|
602
|
+
|
|
603
|
+
# Index To Loc Format (int16)
|
|
604
|
+
# 0 = short offsets (for CFF fonts we use this)
|
|
605
|
+
data << [0].pack("s>")
|
|
606
|
+
|
|
607
|
+
# Glyph Data Format (int16)
|
|
608
|
+
data << [0].pack("s>")
|
|
609
|
+
|
|
610
|
+
data
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# Build hhea table from Type 1 font
|
|
614
|
+
#
|
|
615
|
+
# @param font [Type1Font] Source Type 1 font
|
|
616
|
+
# @return [String] hhea table binary data
|
|
617
|
+
def build_hhea_table(font)
|
|
618
|
+
data = (+"").b
|
|
619
|
+
|
|
620
|
+
# Get font metrics from Type1Font
|
|
621
|
+
font_bbox = font.font_dictionary&.font_bbox || [0, 0, 1000, 1000]
|
|
622
|
+
blue_values = font.private_dict&.blue_values || []
|
|
623
|
+
|
|
624
|
+
# Version (Fixed 16.16) - 0x00010000 (1.0)
|
|
625
|
+
data << [0x00010000].pack("N")
|
|
626
|
+
|
|
627
|
+
# Ascent (int16) - Distance from baseline to highest ascender
|
|
628
|
+
# Use BlueValues[2] or [3] if available, otherwise font_bbox[3]
|
|
629
|
+
if blue_values.length >= 4
|
|
630
|
+
ascent = blue_values[3] # Top zone top
|
|
631
|
+
elsif blue_values.length >= 3
|
|
632
|
+
ascent = blue_values[2] # Top zone bottom
|
|
633
|
+
else
|
|
634
|
+
ascent = font_bbox[3] # y_max
|
|
635
|
+
end
|
|
636
|
+
data << [ascent].pack("s>")
|
|
637
|
+
|
|
638
|
+
# Descent (int16) - Distance from baseline to lowest descender (negative)
|
|
639
|
+
# Use BlueValues[0] or [1] if available, otherwise font_bbox[1]
|
|
640
|
+
if blue_values.length >= 2
|
|
641
|
+
descent = blue_values[0] # Bottom zone bottom (negative)
|
|
642
|
+
elsif blue_values.length >= 1
|
|
643
|
+
descent = blue_values[0]
|
|
644
|
+
else
|
|
645
|
+
descent = font_bbox[1] # y_min (should be negative)
|
|
646
|
+
end
|
|
647
|
+
data << [descent].pack("s>")
|
|
648
|
+
|
|
649
|
+
# Line Gap (int16) - Additional space between lines
|
|
650
|
+
# Use typical value of 0 for Type 1 fonts
|
|
651
|
+
data << [0].pack("s>")
|
|
652
|
+
|
|
653
|
+
# Advance Width Max (uint16)
|
|
654
|
+
# Type 1 standard is typically 1000, use font_bbox width + padding
|
|
655
|
+
advance_max = (font_bbox[2] - font_bbox[0]) + 100
|
|
656
|
+
data << [advance_max].pack("n")
|
|
657
|
+
|
|
658
|
+
# Min Left Side Bearing (int16)
|
|
659
|
+
# Use font_bbox[0] (x_min) as reasonable default
|
|
660
|
+
data << [font_bbox[0]].pack("s>")
|
|
661
|
+
|
|
662
|
+
# Min Right Side Bearing (int16)
|
|
663
|
+
# Estimate as 0 (will be updated if actual metrics available)
|
|
664
|
+
data << [0].pack("s>")
|
|
665
|
+
|
|
666
|
+
# x Max Extent (int16) - Max(lsb + xMax)
|
|
667
|
+
# Use font_bbox[2] (x_max) as reasonable default
|
|
668
|
+
data << [font_bbox[2]].pack("s>")
|
|
669
|
+
|
|
670
|
+
# Caret Slope Rise (int16)
|
|
671
|
+
# 1 for upright fonts (not italic)
|
|
672
|
+
data << [1].pack("s>")
|
|
673
|
+
|
|
674
|
+
# Caret Slope Run (int16)
|
|
675
|
+
# 0 for upright fonts
|
|
676
|
+
data << [0].pack("s>")
|
|
677
|
+
|
|
678
|
+
# Caret Offset (int16)
|
|
679
|
+
# Set to 0 for standard fonts
|
|
680
|
+
data << [0].pack("s>")
|
|
681
|
+
|
|
682
|
+
# Reserved (int64) - 8 bytes of zeros
|
|
683
|
+
data << [0, 0].pack("Q>")
|
|
684
|
+
|
|
685
|
+
# Metric Data Format (int16)
|
|
686
|
+
# 0 for current format
|
|
687
|
+
data << [0].pack("s>")
|
|
688
|
+
|
|
689
|
+
# Number of HMetrics (uint16)
|
|
690
|
+
# Number of glyphs with explicit metrics (typically all glyphs)
|
|
691
|
+
num_glyphs = font.charstrings&.count || 1
|
|
692
|
+
data << [[num_glyphs, 1].max].pack("n")
|
|
693
|
+
|
|
694
|
+
data
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# Build maxp table from Type 1 font
|
|
698
|
+
#
|
|
699
|
+
# @param font [Type1Font] Source Type 1 font
|
|
700
|
+
# @return [String] maxp table binary data
|
|
701
|
+
def build_maxp_table(font)
|
|
702
|
+
data = (+"").b
|
|
703
|
+
|
|
704
|
+
# Get number of glyphs from Type1Font
|
|
705
|
+
num_glyphs = font.charstrings&.count || 1
|
|
706
|
+
|
|
707
|
+
# Version (Fixed 16.16)
|
|
708
|
+
# For CFF fonts (OTF output), use version 0.5 (0x00005000)
|
|
709
|
+
# For TrueType fonts (TTF output), would use version 1.0 (0x00010000)
|
|
710
|
+
# Type 1 fonts convert to CFF-based OTF, so use version 0.5
|
|
711
|
+
data << [0x00005000].pack("N")
|
|
712
|
+
|
|
713
|
+
# Number of Glyphs (uint16)
|
|
714
|
+
# Must be >= 1 (at minimum, .notdef must be present)
|
|
715
|
+
data << [[num_glyphs, 1].max].pack("n")
|
|
716
|
+
|
|
717
|
+
data
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Build name table from Type 1 font
|
|
721
|
+
#
|
|
722
|
+
# @param font [Type1Font] Source Type 1 font
|
|
723
|
+
# @return [String] name table binary data
|
|
724
|
+
def build_name_table(font)
|
|
725
|
+
# Get font metadata from Type1Font
|
|
726
|
+
font_dict = font.font_dictionary
|
|
727
|
+
font_info = font_dict&.font_info
|
|
728
|
+
|
|
729
|
+
# Extract font names with fallbacks
|
|
730
|
+
font_name = font.font_name || font_dict&.font_name || "Unnamed"
|
|
731
|
+
family_name = if font_info&.respond_to?(:family_name)
|
|
732
|
+
font_info.family_name || font_dict&.family_name || font_name
|
|
733
|
+
else
|
|
734
|
+
font_dict&.family_name || font_name
|
|
735
|
+
end
|
|
736
|
+
full_name = if font_info&.respond_to?(:full_name)
|
|
737
|
+
font_info.full_name || font_dict&.full_name || family_name
|
|
738
|
+
else
|
|
739
|
+
font_dict&.full_name || family_name
|
|
740
|
+
end
|
|
741
|
+
version = if font_info&.respond_to?(:version)
|
|
742
|
+
font_info.version || font.version || "001.000"
|
|
743
|
+
else
|
|
744
|
+
font.version || "001.000"
|
|
745
|
+
end
|
|
746
|
+
copyright = if font_info&.respond_to?(:copyright)
|
|
747
|
+
font_info.copyright || font_dict&.raw_data&.dig(:copyright) || ""
|
|
748
|
+
else
|
|
749
|
+
font_dict&.raw_data&.dig(:copyright) || ""
|
|
750
|
+
end
|
|
751
|
+
postscript_name = font_name
|
|
752
|
+
weight = if font_info&.respond_to?(:weight)
|
|
753
|
+
font_info.weight
|
|
754
|
+
else
|
|
755
|
+
"Regular"
|
|
756
|
+
end
|
|
757
|
+
notice = if font_info&.respond_to?(:notice)
|
|
758
|
+
font_info.notice
|
|
759
|
+
else
|
|
760
|
+
""
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
# Build name records (Windows Unicode, English US)
|
|
764
|
+
# Platform ID 3 (Windows), Encoding ID 1 (Unicode BMP), Language ID 0x0409 (US English)
|
|
765
|
+
name_records = [
|
|
766
|
+
# Copyright (name ID 0)
|
|
767
|
+
{ name_id: 0, string: copyright },
|
|
768
|
+
# Family Name (name ID 1)
|
|
769
|
+
{ name_id: 1, string: family_name },
|
|
770
|
+
# Subfamily Name (name ID 2) - derive from weight or default to Regular
|
|
771
|
+
{ name_id: 2, string: weight || "Regular" },
|
|
772
|
+
# Unique ID (name ID 3) - format: version;copyright;postscript_name
|
|
773
|
+
{ name_id: 3, string: "#{version};#{copyright};#{postscript_name}" },
|
|
774
|
+
# Full Name (name ID 4)
|
|
775
|
+
{ name_id: 4, string: full_name },
|
|
776
|
+
# Version (name ID 5)
|
|
777
|
+
{ name_id: 5, string: version },
|
|
778
|
+
# PostScript Name (name ID 6)
|
|
779
|
+
{ name_id: 6, string: postscript_name },
|
|
780
|
+
# Trademark (name ID 7) - use notice if available
|
|
781
|
+
{ name_id: 7, string: notice || "" },
|
|
782
|
+
]
|
|
783
|
+
|
|
784
|
+
# Filter out empty strings and build string storage
|
|
785
|
+
name_records = name_records.select { |r| !r[:string].nil? && !r[:string].empty? }
|
|
786
|
+
|
|
787
|
+
# Build string storage (UTF-16BE encoded for Windows platform)
|
|
788
|
+
string_storage = (+"").b
|
|
789
|
+
name_records.each do |record|
|
|
790
|
+
encoded_string = record[:string].encode("UTF-16BE").force_encoding("ASCII-8BIT")
|
|
791
|
+
record[:encoded] = encoded_string
|
|
792
|
+
record[:offset] = string_storage.bytesize
|
|
793
|
+
string_storage << encoded_string
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# Build name table
|
|
797
|
+
data = (+"").b
|
|
798
|
+
|
|
799
|
+
# Format selector (uint16) - 0 for basic
|
|
800
|
+
data << [0].pack("n")
|
|
801
|
+
|
|
802
|
+
# Count (uint16) - number of name records
|
|
803
|
+
data << [name_records.size].pack("n")
|
|
804
|
+
|
|
805
|
+
# String offset (uint16) - offset to string storage from start of table
|
|
806
|
+
# Header is 6 bytes, each name record is 12 bytes
|
|
807
|
+
string_data_offset = 6 + (name_records.size * 12)
|
|
808
|
+
data << [string_data_offset].pack("n")
|
|
809
|
+
|
|
810
|
+
# Write name records
|
|
811
|
+
platform_id = 3 # Windows
|
|
812
|
+
encoding_id = 1 # Unicode BMP
|
|
813
|
+
language_id = 0x0409 # US English
|
|
814
|
+
|
|
815
|
+
name_records.each do |record|
|
|
816
|
+
data << [platform_id].pack("n") # platform ID
|
|
817
|
+
data << [encoding_id].pack("n") # encoding ID
|
|
818
|
+
data << [language_id].pack("n") # language ID
|
|
819
|
+
data << [record[:name_id]].pack("n") # name ID
|
|
820
|
+
data << [record[:encoded].bytesize].pack("n") # string length
|
|
821
|
+
data << [record[:offset]].pack("n") # string offset
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Write string storage
|
|
825
|
+
data << string_storage
|
|
826
|
+
|
|
827
|
+
data
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
# Build OS/2 table from Type 1 font
|
|
831
|
+
#
|
|
832
|
+
# @param font [Type1Font] Source Type 1 font
|
|
833
|
+
# @return [String] OS/2 table binary data
|
|
834
|
+
def build_os2_table(font)
|
|
835
|
+
data = (+"").b
|
|
836
|
+
|
|
837
|
+
# Get font metadata from Type1Font
|
|
838
|
+
font_bbox = font.font_dictionary&.font_bbox || [0, 0, 1000, 1000]
|
|
839
|
+
blue_values = font.private_dict&.blue_values || []
|
|
840
|
+
font_info = font.font_dictionary&.font_info || {}
|
|
841
|
+
weight = font_info.weight || "Medium"
|
|
842
|
+
|
|
843
|
+
# Determine weight class (100-900)
|
|
844
|
+
# Order matters - more specific patterns must come first
|
|
845
|
+
weight_class = case weight.to_s.downcase
|
|
846
|
+
when /thin/ then 100
|
|
847
|
+
when /extralight/ then 200
|
|
848
|
+
when /light/ then 300
|
|
849
|
+
when /regular|normal/ then 400
|
|
850
|
+
when /medium/ then 400
|
|
851
|
+
when /semibold|semib/ then 600
|
|
852
|
+
when /extrabold/ then 800
|
|
853
|
+
when /bold/ then 700
|
|
854
|
+
when /black|heavy/ then 900
|
|
855
|
+
else 400
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# Version (uint16) - Use version 4 for modern fonts
|
|
859
|
+
data << [4].pack("n")
|
|
860
|
+
|
|
861
|
+
# xAvgCharWidth (int16) - Average character width
|
|
862
|
+
# Use font width estimate
|
|
863
|
+
avg_width = ((font_bbox[2] - font_bbox[0]) * 0.5).to_i
|
|
864
|
+
data << [avg_width].pack("s>")
|
|
865
|
+
|
|
866
|
+
# usWeightClass (uint16)
|
|
867
|
+
data << [weight_class].pack("n")
|
|
868
|
+
|
|
869
|
+
# usWidthClass (uint16) - 1 = Ultra-condensed to 9 = Ultra-expanded
|
|
870
|
+
# Default to 5 (Medium)
|
|
871
|
+
data << [5].pack("n")
|
|
872
|
+
|
|
873
|
+
# fsType (uint16) - Embedding permissions
|
|
874
|
+
# 0 = Installable embedding, 8 = Restricted (use 0 as default)
|
|
875
|
+
data << [0].pack("n")
|
|
876
|
+
|
|
877
|
+
# ySubscriptXSize (int16)
|
|
878
|
+
data << [650].pack("s>")
|
|
879
|
+
|
|
880
|
+
# ySubscriptYSize (int16)
|
|
881
|
+
data << [600].pack("s>")
|
|
882
|
+
|
|
883
|
+
# ySubscriptXOffset (int16)
|
|
884
|
+
data << [0].pack("s>")
|
|
885
|
+
|
|
886
|
+
# ySubscriptYOffset (int16)
|
|
887
|
+
data << [75].pack("s>")
|
|
888
|
+
|
|
889
|
+
# ySuperscriptXSize (int16)
|
|
890
|
+
data << [650].pack("s>")
|
|
891
|
+
|
|
892
|
+
# ySuperscriptYSize (int16)
|
|
893
|
+
data << [600].pack("s>")
|
|
894
|
+
|
|
895
|
+
# ySuperscriptXOffset (int16)
|
|
896
|
+
data << [0].pack("s>")
|
|
897
|
+
|
|
898
|
+
# ySuperscriptYOffset (int16)
|
|
899
|
+
data << [350].pack("s>")
|
|
900
|
+
|
|
901
|
+
# yStrikeoutSize (int16)
|
|
902
|
+
data << [50].pack("s>")
|
|
903
|
+
|
|
904
|
+
# yStrikeoutPosition (int16)
|
|
905
|
+
data << [300].pack("s>")
|
|
906
|
+
|
|
907
|
+
# sFamilyClass (int16) - Family class and subclass
|
|
908
|
+
# 0 = No classification
|
|
909
|
+
data << [0].pack("s>")
|
|
910
|
+
|
|
911
|
+
# PANOSE (10 bytes) - Use default Latin Text family
|
|
912
|
+
# Family: 2 (Text and Display), Serif Style: 11 (Normal Sans)
|
|
913
|
+
panose = [
|
|
914
|
+
2, # Family kind: Latin Text
|
|
915
|
+
11, # Serif style: Normal Sans
|
|
916
|
+
5, # Weight: Medium
|
|
917
|
+
5, # Proportion: Modern
|
|
918
|
+
2, # Contrast: Medium Low
|
|
919
|
+
5, # Stroke variation: Medium
|
|
920
|
+
5, # Arm style: Straight arms/serifs
|
|
921
|
+
5, # Letter form: Normal
|
|
922
|
+
4, # Midline: Standard
|
|
923
|
+
3, # X-height: Medium
|
|
924
|
+
]
|
|
925
|
+
data << panose.pack("C*")
|
|
926
|
+
|
|
927
|
+
# Unicode ranges (4 x uint32) - Basic Latin + Latin-1
|
|
928
|
+
# Bits 0-31: Basic Latin, Latin-1, Latin Extended-A/B, etc.
|
|
929
|
+
data << [0x00000001].pack("N") # Basic Latin (0-7F)
|
|
930
|
+
data << [0x00000000].pack("N")
|
|
931
|
+
data << [0x00000000].pack("N")
|
|
932
|
+
data << [0x00000000].pack("N")
|
|
933
|
+
|
|
934
|
+
# achVendID (4 bytes) - Vendor ID
|
|
935
|
+
data << "UKWN" # Unknown
|
|
936
|
+
|
|
937
|
+
# fsSelection (uint16) - Font selection flags
|
|
938
|
+
# Bit 6 (0x40) = Regular weight if 400-500
|
|
939
|
+
fs_selection = if weight_class >= 400 && weight_class <= 500
|
|
940
|
+
0x40 # REGULAR
|
|
941
|
+
elsif weight_class >= 700
|
|
942
|
+
0x20 # BOLD
|
|
943
|
+
else
|
|
944
|
+
0
|
|
945
|
+
end
|
|
946
|
+
data << [fs_selection].pack("n")
|
|
947
|
+
|
|
948
|
+
# usFirstCharIndex (uint16) - First Unicode character
|
|
949
|
+
data << [32].pack("n") # Space
|
|
950
|
+
|
|
951
|
+
# usLastCharIndex (uint16) - Last Unicode character
|
|
952
|
+
data << [0xFFFD].pack("n") # Replacement character
|
|
953
|
+
|
|
954
|
+
# sTypoAscender (int16) - Use BlueValues or font bbox
|
|
955
|
+
if blue_values.length >= 4
|
|
956
|
+
typo_ascender = blue_values[3]
|
|
957
|
+
else
|
|
958
|
+
typo_ascender = font_bbox[3]
|
|
959
|
+
end
|
|
960
|
+
data << [typo_ascender].pack("s>")
|
|
961
|
+
|
|
962
|
+
# sTypoDescender (int16) - Use BlueValues or font bbox (negative)
|
|
963
|
+
if blue_values.length >= 2
|
|
964
|
+
typo_descender = blue_values[0]
|
|
965
|
+
else
|
|
966
|
+
typo_descender = font_bbox[1]
|
|
967
|
+
end
|
|
968
|
+
data << [typo_descender].pack("s>")
|
|
969
|
+
|
|
970
|
+
# sTypoLineGap (int16)
|
|
971
|
+
data << [0].pack("s>")
|
|
972
|
+
|
|
973
|
+
# usWinAscent (uint16)
|
|
974
|
+
data << [[font_bbox[3], 1000].max].pack("n")
|
|
975
|
+
|
|
976
|
+
# usWinDescent (uint16)
|
|
977
|
+
data << [[-font_bbox[1], 200].max].pack("n")
|
|
978
|
+
|
|
979
|
+
# ulCodePageRange1 (uint32) - Latin 1
|
|
980
|
+
data << [0x00000001].pack("N")
|
|
981
|
+
|
|
982
|
+
# ulCodePageRange2 (uint32)
|
|
983
|
+
data << [0x00000000].pack("N")
|
|
984
|
+
|
|
985
|
+
# sxHeight (int16) - x-height, approximate as 500 for 1000 UPM
|
|
986
|
+
data << [500].pack("s>")
|
|
987
|
+
|
|
988
|
+
# sCapHeight (int16) - Cap height, approximate as 700 for 1000 UPM
|
|
989
|
+
data << [700].pack("s>")
|
|
990
|
+
|
|
991
|
+
# usDefaultChar (uint16)
|
|
992
|
+
data << [0].pack("n")
|
|
993
|
+
|
|
994
|
+
# usBreakChar (uint16) - Space
|
|
995
|
+
data << [32].pack("n")
|
|
996
|
+
|
|
997
|
+
# usMaxContext (uint16)
|
|
998
|
+
data << [0].pack("n")
|
|
999
|
+
|
|
1000
|
+
data
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
# Build post table from Type 1 font
|
|
1004
|
+
#
|
|
1005
|
+
# @param font [Type1Font] Source Type 1 font
|
|
1006
|
+
# @return [String] post table binary data
|
|
1007
|
+
def build_post_table(font)
|
|
1008
|
+
data = (+"").b
|
|
1009
|
+
|
|
1010
|
+
# Get font metadata from Type1Font
|
|
1011
|
+
font_info = font.font_dictionary&.font_info || {}
|
|
1012
|
+
|
|
1013
|
+
# Version (Fixed 16.16) - Use version 3.0 for CFF fonts (no glyph names)
|
|
1014
|
+
# Version 2.0 would include glyph names, but for OTF output version 3.0 is fine
|
|
1015
|
+
# since CFF table contains the glyph names
|
|
1016
|
+
data << [0x00030000].pack("N") # Version 3.0
|
|
1017
|
+
|
|
1018
|
+
# Italic Angle (Fixed 16.16)
|
|
1019
|
+
# Get from FontInfo if available, otherwise default to 0
|
|
1020
|
+
italic_angle = font_info.italic_angle || 0
|
|
1021
|
+
angle_raw = (italic_angle * 65_536).to_i
|
|
1022
|
+
data << [angle_raw].pack("N")
|
|
1023
|
+
|
|
1024
|
+
# Underline Position (int16)
|
|
1025
|
+
underline_position = font_info.underline_position || -100
|
|
1026
|
+
data << [underline_position].pack("s>")
|
|
1027
|
+
|
|
1028
|
+
# Underline Thickness (int16)
|
|
1029
|
+
underline_thickness = font_info.underline_thickness || 50
|
|
1030
|
+
data << [underline_thickness].pack("s>")
|
|
1031
|
+
|
|
1032
|
+
# Fixed Pitch (uint32) - Boolean for monospace
|
|
1033
|
+
is_fixed_pitch = (font_info.is_fixed_pitch || false) ? 1 : 0
|
|
1034
|
+
data << [is_fixed_pitch].pack("N")
|
|
1035
|
+
|
|
1036
|
+
# Min/Max Memory for Type 42 (uint32 each) - Not used for CFF, set to 0
|
|
1037
|
+
data << [0].pack("N") # min_mem_type42
|
|
1038
|
+
data << [0].pack("N") # max_mem_type42
|
|
1039
|
+
|
|
1040
|
+
# Min/Max Memory for Type 1 (uint32 each) - Not used for CFF, set to 0
|
|
1041
|
+
data << [0].pack("N") # min_mem_type1
|
|
1042
|
+
data << [0].pack("N") # max_mem_type1
|
|
1043
|
+
|
|
1044
|
+
data
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
# Build cmap table from Type 1 font
|
|
1048
|
+
#
|
|
1049
|
+
# @param font [Type1Font] Source Type 1 font
|
|
1050
|
+
# @return [String] cmap table binary data
|
|
1051
|
+
def build_cmap_table(font)
|
|
1052
|
+
require_relative "../type1/agl"
|
|
1053
|
+
|
|
1054
|
+
data = (+"").b
|
|
1055
|
+
|
|
1056
|
+
# Get encoding from Type1Font
|
|
1057
|
+
encoding = font.charstrings&.encoding || {}
|
|
1058
|
+
glyph_names = font.charstrings&.glyph_names || encoding.keys
|
|
1059
|
+
|
|
1060
|
+
# Build Unicode mapping from glyph names using AGL
|
|
1061
|
+
unicode_to_glyph = {}
|
|
1062
|
+
glyph_index = 0
|
|
1063
|
+
|
|
1064
|
+
glyph_names.each do |glyph_name|
|
|
1065
|
+
# Get Unicode code point from AGL
|
|
1066
|
+
unicode = Type1::AGL.unicode_for_glyph_name(glyph_name)
|
|
1067
|
+
|
|
1068
|
+
# If no Unicode mapping, try to derive from encoding position
|
|
1069
|
+
if unicode.nil?
|
|
1070
|
+
# For standard encoding, try to map from position
|
|
1071
|
+
# This is a simplified approach - real implementation would be more robust
|
|
1072
|
+
unicode = glyph_index if glyph_index < 128
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
# Map Unicode to glyph index
|
|
1076
|
+
if unicode && unicode <= 0xFFFF
|
|
1077
|
+
unicode_to_glyph[unicode] ||= glyph_index
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
glyph_index += 1
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
# Ensure at least .notdef (glyph 0) maps to something
|
|
1084
|
+
unicode_to_glyph[0x0000] ||= 0
|
|
1085
|
+
|
|
1086
|
+
# Build Format 4 subtable (Segment mapping to delta values)
|
|
1087
|
+
# This is the most common format for BMP Unicode fonts
|
|
1088
|
+
subtable_data = build_cmap_format_4(unicode_to_glyph)
|
|
1089
|
+
|
|
1090
|
+
# Calculate offsets
|
|
1091
|
+
encoding_records_offset = 4 # After version (2) + num_tables (2)
|
|
1092
|
+
subtable_offset = encoding_records_offset + 8 # After one encoding record (8 bytes)
|
|
1093
|
+
|
|
1094
|
+
# Build cmap table header
|
|
1095
|
+
# Version (uint16)
|
|
1096
|
+
data << [0].pack("n")
|
|
1097
|
+
|
|
1098
|
+
# Number of encoding records (uint16)
|
|
1099
|
+
data << [1].pack("n") # One encoding record
|
|
1100
|
+
|
|
1101
|
+
# Encoding record: Platform ID (uint16), Encoding ID (uint16), Subtable offset (uint32)
|
|
1102
|
+
# Platform 3 (Windows), Encoding 1 (Unicode BMP)
|
|
1103
|
+
data << [3].pack("n") # Platform ID: Windows
|
|
1104
|
+
data << [1].pack("n") # Encoding ID: Unicode BMP
|
|
1105
|
+
data << [subtable_offset].pack("N") # Subtable offset
|
|
1106
|
+
|
|
1107
|
+
# Append subtable data
|
|
1108
|
+
data << subtable_data
|
|
1109
|
+
|
|
1110
|
+
data
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
# Build cmap format 4 subtable
|
|
1114
|
+
#
|
|
1115
|
+
# @param unicode_to_glyph [Hash<Integer, Integer>] Unicode to glyph index mapping
|
|
1116
|
+
# @return [String] Format 4 subtable binary data
|
|
1117
|
+
def build_cmap_format_4(unicode_to_glyph)
|
|
1118
|
+
data = (+"").b
|
|
1119
|
+
|
|
1120
|
+
# Get sorted Unicode values
|
|
1121
|
+
unicode_values = unicode_to_glyph.keys.sort
|
|
1122
|
+
return data if unicode_values.empty?
|
|
1123
|
+
|
|
1124
|
+
# For simplicity, create segments for continuous ranges
|
|
1125
|
+
# A more sophisticated implementation would optimize this
|
|
1126
|
+
segments = []
|
|
1127
|
+
current_segment = nil
|
|
1128
|
+
|
|
1129
|
+
unicode_values.each do |unicode|
|
|
1130
|
+
glyph_id = unicode_to_glyph[unicode]
|
|
1131
|
+
|
|
1132
|
+
if current_segment.nil?
|
|
1133
|
+
current_segment = {
|
|
1134
|
+
start: unicode,
|
|
1135
|
+
end: unicode,
|
|
1136
|
+
start_glyph: glyph_id,
|
|
1137
|
+
glyphs: [glyph_id],
|
|
1138
|
+
}
|
|
1139
|
+
elsif unicode == current_segment[:end] + 1 && glyph_id == current_segment[:glyphs].last + 1
|
|
1140
|
+
# Continue current segment (sequential)
|
|
1141
|
+
current_segment[:end] = unicode
|
|
1142
|
+
current_segment[:glyphs] << glyph_id
|
|
1143
|
+
else
|
|
1144
|
+
# Start new segment
|
|
1145
|
+
segments << current_segment
|
|
1146
|
+
current_segment = {
|
|
1147
|
+
start: unicode,
|
|
1148
|
+
end: unicode,
|
|
1149
|
+
start_glyph: glyph_id,
|
|
1150
|
+
glyphs: [glyph_id],
|
|
1151
|
+
}
|
|
1152
|
+
end
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
segments << current_segment if current_segment
|
|
1156
|
+
|
|
1157
|
+
# Add end segment marker (0xFFFF)
|
|
1158
|
+
segments << { start: 0xFFFF, end: 0xFFFF, start_glyph: 0, glyphs: [0] }
|
|
1159
|
+
|
|
1160
|
+
# Calculate segment count and related values
|
|
1161
|
+
seg_count = segments.length
|
|
1162
|
+
seg_count_x2 = seg_count * 2
|
|
1163
|
+
search_range = 2 ** (Math.log2(seg_count).to_i) * 2
|
|
1164
|
+
entry_selector = Math.log2(search_range / 2).to_i
|
|
1165
|
+
range_shift = (seg_count - search_range / 2) * 2
|
|
1166
|
+
|
|
1167
|
+
# Build format 4 subtable header (14 bytes)
|
|
1168
|
+
data << [4].pack("n") # Format
|
|
1169
|
+
data << [calculate_cmap4_length(segments)].pack("n") # Length (placeholder)
|
|
1170
|
+
data << [0].pack("n") # Language (0 = independent)
|
|
1171
|
+
data << [seg_count_x2].pack("n") # segCountX2
|
|
1172
|
+
data << [search_range].pack("n") # searchRange
|
|
1173
|
+
data << [entry_selector].pack("n") # entrySelector
|
|
1174
|
+
data << [range_shift].pack("n") # rangeShift
|
|
1175
|
+
|
|
1176
|
+
# Build segment arrays
|
|
1177
|
+
end_codes = []
|
|
1178
|
+
start_codes = []
|
|
1179
|
+
id_deltas = []
|
|
1180
|
+
id_range_offsets = []
|
|
1181
|
+
glyph_id_array = []
|
|
1182
|
+
|
|
1183
|
+
segments.each do |seg|
|
|
1184
|
+
end_codes << seg[:end]
|
|
1185
|
+
start_codes << seg[:start]
|
|
1186
|
+
|
|
1187
|
+
# For sequential glyphs, use delta
|
|
1188
|
+
if seg[:start] == 0xFFFF
|
|
1189
|
+
# End segment marker
|
|
1190
|
+
id_deltas << 1
|
|
1191
|
+
id_range_offsets << 0
|
|
1192
|
+
elsif seg[:end] - seg[:start] == seg[:glyphs].length - 1
|
|
1193
|
+
# Sequential: use delta
|
|
1194
|
+
id_deltas << (seg[:start_glyph] - seg[:start])
|
|
1195
|
+
id_range_offsets << 0
|
|
1196
|
+
else
|
|
1197
|
+
# Non-sequential: use glyph ID array
|
|
1198
|
+
id_deltas << 0
|
|
1199
|
+
id_range_offsets << (glyph_id_array.length * 2 + 2)
|
|
1200
|
+
glyph_id_array.concat(seg[:glyphs])
|
|
1201
|
+
end
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1204
|
+
# Write arrays (padded to even length)
|
|
1205
|
+
end_codes.each { |code| data << [code].pack("n") }
|
|
1206
|
+
data << [0].pack("n") # Reserved padding
|
|
1207
|
+
start_codes.each { |code| data << [code].pack("n") }
|
|
1208
|
+
id_deltas.each { |delta| data << [delta].pack("s>") } # Signed
|
|
1209
|
+
id_range_offsets.each { |offset| data << [offset].pack("n") }
|
|
1210
|
+
glyph_id_array.each { |gid| data << [gid].pack("n") }
|
|
1211
|
+
|
|
1212
|
+
# Update length in header
|
|
1213
|
+
length = data.bytesize
|
|
1214
|
+
data[2..3] = [length].pack("n")
|
|
1215
|
+
|
|
1216
|
+
data
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1219
|
+
# Calculate length for format 4 subtable
|
|
1220
|
+
#
|
|
1221
|
+
# @param segments [Array<Hash>] Segment definitions
|
|
1222
|
+
# @return [Integer] Estimated length
|
|
1223
|
+
def calculate_cmap4_length(segments)
|
|
1224
|
+
# Header: 14 bytes
|
|
1225
|
+
# Arrays: seg_count * 2 bytes each
|
|
1226
|
+
# Glyph ID array: variable
|
|
1227
|
+
seg_count = segments.length
|
|
1228
|
+
|
|
1229
|
+
# Rough estimate (actual calculation done during construction)
|
|
1230
|
+
14 + (seg_count * 8) + (seg_count * 2) + 100 # 100 for glyph ID array estimate
|
|
1231
|
+
end
|
|
1232
|
+
end
|
|
1233
|
+
end
|
|
1234
|
+
end
|