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
|
@@ -6,6 +6,7 @@ require_relative "outline_converter"
|
|
|
6
6
|
require_relative "woff_writer"
|
|
7
7
|
require_relative "woff2_encoder"
|
|
8
8
|
require_relative "svg_generator"
|
|
9
|
+
require_relative "type1_converter"
|
|
9
10
|
require "yaml"
|
|
10
11
|
|
|
11
12
|
module Fontisan
|
|
@@ -54,6 +55,7 @@ module Fontisan
|
|
|
54
55
|
@strategies = [
|
|
55
56
|
TableCopier.new,
|
|
56
57
|
OutlineConverter.new,
|
|
58
|
+
Type1Converter.new,
|
|
57
59
|
WoffWriter.new,
|
|
58
60
|
Woff2Encoder.new,
|
|
59
61
|
SvgGenerator.new,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../tables/glyf/glyph_builder"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Converters
|
|
7
|
+
# Builds glyf and loca tables from glyph outlines
|
|
8
|
+
#
|
|
9
|
+
# This module handles the construction of TrueType glyph tables:
|
|
10
|
+
# - glyf table: Contains actual glyph outline data
|
|
11
|
+
# - loca table: Contains offsets to glyph data in glyf table
|
|
12
|
+
#
|
|
13
|
+
# The loca table format depends on the maximum offset:
|
|
14
|
+
# - Short format (offsets/2) if max offset <= 0x1FFFE
|
|
15
|
+
# - Long format (raw offsets) if max offset > 0x1FFFE
|
|
16
|
+
module GlyfTableBuilder
|
|
17
|
+
# Build glyf and loca tables from outlines
|
|
18
|
+
#
|
|
19
|
+
# @param outlines [Array<Outline>] Glyph outlines
|
|
20
|
+
# @return [Array<String, String, Integer>] [glyf_data, loca_data, loca_format]
|
|
21
|
+
def build_glyf_loca_tables(outlines)
|
|
22
|
+
glyf_data = "".b
|
|
23
|
+
offsets = []
|
|
24
|
+
|
|
25
|
+
# Build each glyph
|
|
26
|
+
outlines.each do |outline|
|
|
27
|
+
offsets << glyf_data.bytesize
|
|
28
|
+
|
|
29
|
+
if outline.empty?
|
|
30
|
+
# Empty glyph - no data
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Build glyph data using GlyphBuilder class method
|
|
35
|
+
glyph_data = Fontisan::Tables::GlyphBuilder.build_simple_glyph(outline)
|
|
36
|
+
glyf_data << glyph_data
|
|
37
|
+
|
|
38
|
+
# Add padding to 4-byte boundary
|
|
39
|
+
padding = (4 - (glyf_data.bytesize % 4)) % 4
|
|
40
|
+
glyf_data << ("\x00" * padding) if padding.positive?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Add final offset
|
|
44
|
+
offsets << glyf_data.bytesize
|
|
45
|
+
|
|
46
|
+
# Build loca table
|
|
47
|
+
# Determine format based on max offset
|
|
48
|
+
max_offset = offsets.max
|
|
49
|
+
if max_offset <= 0x1FFFE
|
|
50
|
+
# Short format (offsets / 2)
|
|
51
|
+
loca_format = 0
|
|
52
|
+
loca_data = offsets.map { |off| off / 2 }.pack("n*")
|
|
53
|
+
else
|
|
54
|
+
# Long format
|
|
55
|
+
loca_format = 1
|
|
56
|
+
loca_data = offsets.pack("N*")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
[glyf_data, loca_data, loca_format]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../conversion_options"
|
|
3
4
|
require_relative "conversion_strategy"
|
|
4
5
|
require_relative "../outline_extractor"
|
|
5
6
|
require_relative "../models/outline"
|
|
6
7
|
require_relative "../tables/cff/charstring_builder"
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "
|
|
9
|
-
require_relative "
|
|
10
|
-
require_relative "
|
|
11
|
-
require_relative "../optimizers/pattern_analyzer"
|
|
12
|
-
require_relative "../optimizers/subroutine_optimizer"
|
|
13
|
-
require_relative "../optimizers/subroutine_builder"
|
|
14
|
-
require_relative "../optimizers/charstring_rewriter"
|
|
8
|
+
require_relative "outline_extraction"
|
|
9
|
+
require_relative "cff_table_builder"
|
|
10
|
+
require_relative "glyf_table_builder"
|
|
11
|
+
require_relative "outline_optimizer"
|
|
15
12
|
require_relative "../hints/truetype_hint_extractor"
|
|
16
13
|
require_relative "../hints/postscript_hint_extractor"
|
|
17
14
|
require_relative "../hints/hint_converter"
|
|
@@ -58,15 +55,20 @@ module Fontisan
|
|
|
58
55
|
# converter = Fontisan::Converters::OutlineConverter.new
|
|
59
56
|
# otf_font = converter.convert(ttf_font, target_format: :otf)
|
|
60
57
|
#
|
|
61
|
-
# @example Converting
|
|
58
|
+
# @example Converting with ConversionOptions
|
|
59
|
+
# options = ConversionOptions.recommended(from: :ttf, to: :otf)
|
|
62
60
|
# converter = Fontisan::Converters::OutlineConverter.new
|
|
63
|
-
#
|
|
61
|
+
# otf_font = converter.convert(ttf_font, options: options)
|
|
64
62
|
#
|
|
65
63
|
# @example Converting with hint preservation
|
|
66
64
|
# converter = Fontisan::Converters::OutlineConverter.new
|
|
67
65
|
# otf_font = converter.convert(ttf_font, target_format: :otf, preserve_hints: true)
|
|
68
66
|
class OutlineConverter
|
|
69
67
|
include ConversionStrategy
|
|
68
|
+
include OutlineExtraction
|
|
69
|
+
include CffTableBuilder
|
|
70
|
+
include GlyfTableBuilder
|
|
71
|
+
include OutlineOptimizer
|
|
70
72
|
|
|
71
73
|
# Supported outline formats
|
|
72
74
|
SUPPORTED_FORMATS = %i[ttf otf cff2].freeze
|
|
@@ -89,19 +91,31 @@ module Fontisan
|
|
|
89
91
|
# @option options [Boolean] :preserve_variations Keep variation data during conversion (default: true)
|
|
90
92
|
# @option options [Boolean] :generate_instance Generate static instance instead of variable font (default: false)
|
|
91
93
|
# @option options [Hash] :instance_coordinates Axis coordinates for instance generation (default: {})
|
|
94
|
+
# @option options [ConversionOptions] :options ConversionOptions object
|
|
92
95
|
# @return [Hash<String, String>] Map of table tags to binary data
|
|
93
96
|
def convert(font, options = {})
|
|
94
97
|
@font = font
|
|
95
98
|
@options = options
|
|
99
|
+
|
|
100
|
+
# Extract ConversionOptions if provided
|
|
101
|
+
conv_options = extract_conversion_options(options)
|
|
102
|
+
|
|
96
103
|
@optimize_cff = options.fetch(:optimize_cff, false)
|
|
97
|
-
@preserve_hints = options.fetch(:preserve_hints,
|
|
104
|
+
@preserve_hints = options.fetch(:preserve_hints,
|
|
105
|
+
conv_options&.generating_option?(
|
|
106
|
+
:hinting_mode, "preserve"
|
|
107
|
+
) || false)
|
|
98
108
|
@preserve_variations = options.fetch(:preserve_variations, true)
|
|
99
109
|
@generate_instance = options.fetch(:generate_instance, false)
|
|
100
110
|
@instance_coordinates = options.fetch(:instance_coordinates, {})
|
|
101
|
-
|
|
111
|
+
|
|
112
|
+
target_format = options[:target_format] || conv_options&.to ||
|
|
102
113
|
detect_target_format(font)
|
|
103
114
|
validate(font, target_format)
|
|
104
115
|
|
|
116
|
+
# Apply opening options to source font
|
|
117
|
+
apply_opening_options(font, conv_options) if conv_options
|
|
118
|
+
|
|
105
119
|
source_format = detect_format(font)
|
|
106
120
|
|
|
107
121
|
# Check if we should generate a static instance instead
|
|
@@ -138,8 +152,30 @@ module Fontisan
|
|
|
138
152
|
# Extract hints if preservation is enabled
|
|
139
153
|
hints_per_glyph = @preserve_hints ? extract_ttf_hints(font) : {}
|
|
140
154
|
|
|
141
|
-
# Build
|
|
142
|
-
|
|
155
|
+
# Build CharStrings from outlines
|
|
156
|
+
charstrings = outlines.map do |outline|
|
|
157
|
+
builder = Tables::Cff::CharStringBuilder.new
|
|
158
|
+
if outline.empty?
|
|
159
|
+
builder.build_empty
|
|
160
|
+
else
|
|
161
|
+
builder.build(outline)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Apply subroutine optimization if enabled
|
|
166
|
+
local_subrs = []
|
|
167
|
+
if @optimize_cff
|
|
168
|
+
begin
|
|
169
|
+
charstrings, local_subrs = optimize_charstrings(charstrings)
|
|
170
|
+
rescue StandardError => e
|
|
171
|
+
# If optimization fails, fall back to unoptimized CharStrings
|
|
172
|
+
warn "CFF optimization failed: #{e.message}, using unoptimized CharStrings"
|
|
173
|
+
local_subrs = []
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Build CFF table from charstrings and local subrs
|
|
178
|
+
cff_data = build_cff_table(charstrings, local_subrs, font)
|
|
143
179
|
|
|
144
180
|
# Copy all tables except glyf/loca
|
|
145
181
|
tables = copy_tables(font, %w[glyf loca])
|
|
@@ -184,8 +220,7 @@ module Fontisan
|
|
|
184
220
|
hints_per_glyph = @preserve_hints ? extract_cff_hints(font) : {}
|
|
185
221
|
|
|
186
222
|
# Build glyf and loca tables
|
|
187
|
-
glyf_data, loca_data, loca_format = build_glyf_loca_tables(outlines
|
|
188
|
-
hints_per_glyph)
|
|
223
|
+
glyf_data, loca_data, loca_format = build_glyf_loca_tables(outlines)
|
|
189
224
|
|
|
190
225
|
# Copy all tables except CFF
|
|
191
226
|
tables = copy_tables(font, ["CFF ", "CFF2"])
|
|
@@ -279,285 +314,6 @@ module Fontisan
|
|
|
279
314
|
true
|
|
280
315
|
end
|
|
281
316
|
|
|
282
|
-
# Extract outlines from TrueType font
|
|
283
|
-
#
|
|
284
|
-
# @param font [TrueTypeFont] Source font
|
|
285
|
-
# @return [Array<Outline>] Array of outline objects
|
|
286
|
-
def extract_ttf_outlines(font)
|
|
287
|
-
# Get required tables
|
|
288
|
-
head = font.table("head")
|
|
289
|
-
maxp = font.table("maxp")
|
|
290
|
-
loca = font.table("loca")
|
|
291
|
-
glyf = font.table("glyf")
|
|
292
|
-
|
|
293
|
-
# Parse loca with context
|
|
294
|
-
loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
|
|
295
|
-
|
|
296
|
-
# Create resolver for compound glyphs
|
|
297
|
-
resolver = Tables::CompoundGlyphResolver.new(glyf, loca, head)
|
|
298
|
-
|
|
299
|
-
# Extract all glyphs
|
|
300
|
-
outlines = []
|
|
301
|
-
maxp.num_glyphs.times do |glyph_id|
|
|
302
|
-
glyph = glyf.glyph_for(glyph_id, loca, head)
|
|
303
|
-
|
|
304
|
-
outlines << if glyph.nil? || glyph.empty?
|
|
305
|
-
# Empty glyph - create empty outline
|
|
306
|
-
Models::Outline.new(
|
|
307
|
-
glyph_id: glyph_id,
|
|
308
|
-
commands: [],
|
|
309
|
-
bbox: { x_min: 0, y_min: 0, x_max: 0, y_max: 0 },
|
|
310
|
-
)
|
|
311
|
-
elsif glyph.simple?
|
|
312
|
-
# Convert simple glyph to outline
|
|
313
|
-
Models::Outline.from_truetype(glyph, glyph_id)
|
|
314
|
-
else
|
|
315
|
-
# Compound glyph - resolve to simple outline
|
|
316
|
-
resolver.resolve(glyph)
|
|
317
|
-
end
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
outlines
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
# Extract outlines from CFF font
|
|
324
|
-
#
|
|
325
|
-
# @param font [OpenTypeFont] Source font
|
|
326
|
-
# @return [Array<Outline>] Array of outline objects
|
|
327
|
-
def extract_cff_outlines(font)
|
|
328
|
-
# Get CFF table
|
|
329
|
-
cff = font.table("CFF ")
|
|
330
|
-
raise Fontisan::Error, "CFF table not found" unless cff
|
|
331
|
-
|
|
332
|
-
# Get number of glyphs
|
|
333
|
-
num_glyphs = cff.glyph_count
|
|
334
|
-
|
|
335
|
-
# Extract all glyphs
|
|
336
|
-
outlines = []
|
|
337
|
-
num_glyphs.times do |glyph_id|
|
|
338
|
-
charstring = cff.charstring_for_glyph(glyph_id)
|
|
339
|
-
|
|
340
|
-
outlines << if charstring.nil? || charstring.path.empty?
|
|
341
|
-
# Empty glyph
|
|
342
|
-
Models::Outline.new(
|
|
343
|
-
glyph_id: glyph_id,
|
|
344
|
-
commands: [],
|
|
345
|
-
bbox: { x_min: 0, y_min: 0, x_max: 0, y_max: 0 },
|
|
346
|
-
)
|
|
347
|
-
else
|
|
348
|
-
# Convert CharString to outline
|
|
349
|
-
Models::Outline.from_cff(charstring, glyph_id)
|
|
350
|
-
end
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
outlines
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
# Build CFF table from outlines
|
|
357
|
-
#
|
|
358
|
-
# @param outlines [Array<Outline>] Glyph outlines
|
|
359
|
-
# @param font [TrueTypeFont] Source font (for metadata)
|
|
360
|
-
# @return [String] CFF table binary data
|
|
361
|
-
def build_cff_table(outlines, font, _hints_per_glyph)
|
|
362
|
-
# Build CharStrings INDEX from outlines
|
|
363
|
-
begin
|
|
364
|
-
charstrings = outlines.map do |outline|
|
|
365
|
-
builder = Tables::Cff::CharStringBuilder.new
|
|
366
|
-
if outline.empty?
|
|
367
|
-
builder.build_empty
|
|
368
|
-
else
|
|
369
|
-
builder.build(outline)
|
|
370
|
-
end
|
|
371
|
-
end
|
|
372
|
-
rescue StandardError => e
|
|
373
|
-
raise Fontisan::Error, "Failed to build CharStrings: #{e.message}"
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# Apply subroutine optimization if enabled
|
|
377
|
-
local_subrs = []
|
|
378
|
-
|
|
379
|
-
if @optimize_cff
|
|
380
|
-
begin
|
|
381
|
-
charstrings, local_subrs = optimize_charstrings(charstrings)
|
|
382
|
-
rescue StandardError => e
|
|
383
|
-
# If optimization fails, fall back to unoptimized CharStrings
|
|
384
|
-
warn "CFF optimization failed: #{e.message}, using unoptimized CharStrings"
|
|
385
|
-
local_subrs = []
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
# Build font metadata
|
|
390
|
-
begin
|
|
391
|
-
font_name = extract_font_name(font)
|
|
392
|
-
rescue StandardError => e
|
|
393
|
-
raise Fontisan::Error, "Failed to extract font name: #{e.message}"
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
# Build all INDEXes
|
|
397
|
-
begin
|
|
398
|
-
header_size = 4
|
|
399
|
-
name_index_data = Tables::Cff::IndexBuilder.build([font_name])
|
|
400
|
-
string_index_data = Tables::Cff::IndexBuilder.build([]) # Empty strings
|
|
401
|
-
global_subr_index_data = Tables::Cff::IndexBuilder.build([]) # Empty global subrs
|
|
402
|
-
charstrings_index_data = Tables::Cff::IndexBuilder.build(charstrings)
|
|
403
|
-
local_subrs_index_data = Tables::Cff::IndexBuilder.build(local_subrs)
|
|
404
|
-
rescue StandardError => e
|
|
405
|
-
raise Fontisan::Error, "Failed to build CFF indexes: #{e.message}"
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
# Build Private DICT with Subrs offset if we have local subroutines
|
|
409
|
-
begin
|
|
410
|
-
private_dict_hash = {
|
|
411
|
-
default_width_x: 1000,
|
|
412
|
-
nominal_width_x: 0,
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
# If we have local subroutines, add Subrs offset
|
|
416
|
-
# Subrs offset is relative to Private DICT start
|
|
417
|
-
if local_subrs.any?
|
|
418
|
-
# Add a placeholder Subrs offset first to get accurate size
|
|
419
|
-
private_dict_hash[:subrs] = 0
|
|
420
|
-
|
|
421
|
-
# Calculate size of Private DICT with Subrs entry
|
|
422
|
-
temp_private_dict_data = Tables::Cff::DictBuilder.build(private_dict_hash)
|
|
423
|
-
subrs_offset = temp_private_dict_data.bytesize
|
|
424
|
-
|
|
425
|
-
# Update with actual Subrs offset
|
|
426
|
-
private_dict_hash[:subrs] = subrs_offset
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
# Build final Private DICT
|
|
430
|
-
private_dict_data = Tables::Cff::DictBuilder.build(private_dict_hash)
|
|
431
|
-
private_dict_size = private_dict_data.bytesize
|
|
432
|
-
rescue StandardError => e
|
|
433
|
-
raise Fontisan::Error, "Failed to build Private DICT: #{e.message}"
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
# Calculate offsets with iterative refinement
|
|
437
|
-
begin
|
|
438
|
-
# Initial pass
|
|
439
|
-
top_dict_index_start = header_size + name_index_data.bytesize
|
|
440
|
-
string_index_start = top_dict_index_start + 100 # Approximate
|
|
441
|
-
global_subr_index_start = string_index_start + string_index_data.bytesize
|
|
442
|
-
charstrings_offset = global_subr_index_start + global_subr_index_data.bytesize
|
|
443
|
-
|
|
444
|
-
# Build Top DICT
|
|
445
|
-
top_dict_hash = {
|
|
446
|
-
charset: 0,
|
|
447
|
-
encoding: 0,
|
|
448
|
-
charstrings: charstrings_offset,
|
|
449
|
-
}
|
|
450
|
-
top_dict_data = Tables::Cff::DictBuilder.build(top_dict_hash)
|
|
451
|
-
top_dict_index_data = Tables::Cff::IndexBuilder.build([top_dict_data])
|
|
452
|
-
|
|
453
|
-
# Recalculate with actual Top DICT size
|
|
454
|
-
string_index_start = top_dict_index_start + top_dict_index_data.bytesize
|
|
455
|
-
global_subr_index_start = string_index_start + string_index_data.bytesize
|
|
456
|
-
charstrings_offset = global_subr_index_start + global_subr_index_data.bytesize
|
|
457
|
-
private_dict_offset = charstrings_offset + charstrings_index_data.bytesize
|
|
458
|
-
|
|
459
|
-
# Update Top DICT with Private DICT info
|
|
460
|
-
top_dict_hash = {
|
|
461
|
-
charset: 0,
|
|
462
|
-
encoding: 0,
|
|
463
|
-
charstrings: charstrings_offset,
|
|
464
|
-
private: [private_dict_size, private_dict_offset],
|
|
465
|
-
}
|
|
466
|
-
top_dict_data = Tables::Cff::DictBuilder.build(top_dict_hash)
|
|
467
|
-
top_dict_index_data = Tables::Cff::IndexBuilder.build([top_dict_data])
|
|
468
|
-
|
|
469
|
-
# Final recalculation
|
|
470
|
-
string_index_start = top_dict_index_start + top_dict_index_data.bytesize
|
|
471
|
-
global_subr_index_start = string_index_start + string_index_data.bytesize
|
|
472
|
-
charstrings_offset = global_subr_index_start + global_subr_index_data.bytesize
|
|
473
|
-
private_dict_offset = charstrings_offset + charstrings_index_data.bytesize
|
|
474
|
-
|
|
475
|
-
# Final Top DICT
|
|
476
|
-
top_dict_hash = {
|
|
477
|
-
charset: 0,
|
|
478
|
-
encoding: 0,
|
|
479
|
-
charstrings: charstrings_offset,
|
|
480
|
-
private: [private_dict_size, private_dict_offset],
|
|
481
|
-
}
|
|
482
|
-
top_dict_data = Tables::Cff::DictBuilder.build(top_dict_hash)
|
|
483
|
-
top_dict_index_data = Tables::Cff::IndexBuilder.build([top_dict_data])
|
|
484
|
-
rescue StandardError => e
|
|
485
|
-
raise Fontisan::Error,
|
|
486
|
-
"Failed to calculate CFF table offsets: #{e.message}"
|
|
487
|
-
end
|
|
488
|
-
|
|
489
|
-
# Build CFF Header
|
|
490
|
-
begin
|
|
491
|
-
header = [
|
|
492
|
-
1, # major version
|
|
493
|
-
0, # minor version
|
|
494
|
-
4, # header size
|
|
495
|
-
4, # offSize (will be in INDEX)
|
|
496
|
-
].pack("C4")
|
|
497
|
-
rescue StandardError => e
|
|
498
|
-
raise Fontisan::Error, "Failed to build CFF header: #{e.message}"
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
# Assemble complete CFF table
|
|
502
|
-
begin
|
|
503
|
-
header +
|
|
504
|
-
name_index_data +
|
|
505
|
-
top_dict_index_data +
|
|
506
|
-
string_index_data +
|
|
507
|
-
global_subr_index_data +
|
|
508
|
-
charstrings_index_data +
|
|
509
|
-
private_dict_data +
|
|
510
|
-
local_subrs_index_data
|
|
511
|
-
rescue StandardError => e
|
|
512
|
-
raise Fontisan::Error, "Failed to assemble CFF table: #{e.message}"
|
|
513
|
-
end
|
|
514
|
-
end
|
|
515
|
-
|
|
516
|
-
# Build glyf and loca tables from outlines
|
|
517
|
-
#
|
|
518
|
-
# @param outlines [Array<Outline>] Glyph outlines
|
|
519
|
-
# @return [Array<String, String, Integer>] [glyf_data, loca_data, loca_format]
|
|
520
|
-
def build_glyf_loca_tables(outlines, _hints_per_glyph)
|
|
521
|
-
glyf_data = "".b
|
|
522
|
-
offsets = []
|
|
523
|
-
|
|
524
|
-
# Build each glyph
|
|
525
|
-
outlines.each do |outline|
|
|
526
|
-
offsets << glyf_data.bytesize
|
|
527
|
-
|
|
528
|
-
if outline.empty?
|
|
529
|
-
# Empty glyph - no data
|
|
530
|
-
next
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
# Build glyph data using GlyphBuilder class method
|
|
534
|
-
glyph_data = Fontisan::Tables::GlyphBuilder.build_simple_glyph(outline)
|
|
535
|
-
glyf_data << glyph_data
|
|
536
|
-
|
|
537
|
-
# Add padding to 4-byte boundary
|
|
538
|
-
padding = (4 - (glyf_data.bytesize % 4)) % 4
|
|
539
|
-
glyf_data << ("\x00" * padding) if padding.positive?
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
# Add final offset
|
|
543
|
-
offsets << glyf_data.bytesize
|
|
544
|
-
|
|
545
|
-
# Build loca table
|
|
546
|
-
# Determine format based on max offset
|
|
547
|
-
max_offset = offsets.max
|
|
548
|
-
if max_offset <= 0x1FFFE
|
|
549
|
-
# Short format (offsets / 2)
|
|
550
|
-
loca_format = 0
|
|
551
|
-
loca_data = offsets.map { |off| off / 2 }.pack("n*")
|
|
552
|
-
else
|
|
553
|
-
# Long format
|
|
554
|
-
loca_format = 1
|
|
555
|
-
loca_data = offsets.pack("N*")
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
[glyf_data, loca_data, loca_format]
|
|
559
|
-
end
|
|
560
|
-
|
|
561
317
|
# Copy non-outline tables from source to target
|
|
562
318
|
#
|
|
563
319
|
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
@@ -664,85 +420,6 @@ module Fontisan
|
|
|
664
420
|
head_data
|
|
665
421
|
end
|
|
666
422
|
|
|
667
|
-
# Extract font name from name table
|
|
668
|
-
#
|
|
669
|
-
# @param font [TrueTypeFont, OpenTypeFont] Font
|
|
670
|
-
# @return [String] Font name
|
|
671
|
-
def extract_font_name(font)
|
|
672
|
-
name_table = font.table("name")
|
|
673
|
-
if name_table
|
|
674
|
-
font_name = name_table.english_name(Tables::Name::FAMILY)
|
|
675
|
-
return font_name.dup.force_encoding("ASCII-8BIT") if font_name
|
|
676
|
-
end
|
|
677
|
-
|
|
678
|
-
"UnnamedFont"
|
|
679
|
-
end
|
|
680
|
-
|
|
681
|
-
# Optimize CharStrings using subroutine extraction
|
|
682
|
-
#
|
|
683
|
-
# @param charstrings [Array<String>] Original CharString bytes
|
|
684
|
-
# @return [Array<Array<String>, Array<String>>] [optimized_charstrings, local_subrs]
|
|
685
|
-
def optimize_charstrings(charstrings)
|
|
686
|
-
# Convert to hash format expected by PatternAnalyzer
|
|
687
|
-
charstrings_hash = {}
|
|
688
|
-
charstrings.each_with_index do |cs, index|
|
|
689
|
-
charstrings_hash[index] = cs
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
# Analyze patterns
|
|
693
|
-
analyzer = Optimizers::PatternAnalyzer.new(
|
|
694
|
-
min_length: 10,
|
|
695
|
-
stack_aware: true,
|
|
696
|
-
)
|
|
697
|
-
patterns = analyzer.analyze(charstrings_hash)
|
|
698
|
-
|
|
699
|
-
# Return original if no patterns found
|
|
700
|
-
return [charstrings, []] if patterns.empty?
|
|
701
|
-
|
|
702
|
-
# Optimize selection
|
|
703
|
-
optimizer = Optimizers::SubroutineOptimizer.new(patterns,
|
|
704
|
-
max_subrs: 65_535)
|
|
705
|
-
selected_patterns = optimizer.optimize_selection
|
|
706
|
-
|
|
707
|
-
# Optimize ordering
|
|
708
|
-
selected_patterns = optimizer.optimize_ordering(selected_patterns)
|
|
709
|
-
|
|
710
|
-
# Return original if no patterns selected
|
|
711
|
-
return [charstrings, []] if selected_patterns.empty?
|
|
712
|
-
|
|
713
|
-
# Build subroutines
|
|
714
|
-
builder = Optimizers::SubroutineBuilder.new(selected_patterns,
|
|
715
|
-
type: :local)
|
|
716
|
-
local_subrs = builder.build
|
|
717
|
-
|
|
718
|
-
# Build subroutine map
|
|
719
|
-
subroutine_map = {}
|
|
720
|
-
selected_patterns.each_with_index do |pattern, index|
|
|
721
|
-
subroutine_map[pattern.bytes] = index
|
|
722
|
-
end
|
|
723
|
-
|
|
724
|
-
# Rewrite CharStrings
|
|
725
|
-
rewriter = Optimizers::CharstringRewriter.new(subroutine_map, builder)
|
|
726
|
-
optimized_charstrings = charstrings.map.with_index do |charstring, glyph_id|
|
|
727
|
-
# Find patterns for this glyph
|
|
728
|
-
glyph_patterns = selected_patterns.select do |p|
|
|
729
|
-
p.glyphs.include?(glyph_id)
|
|
730
|
-
end
|
|
731
|
-
|
|
732
|
-
if glyph_patterns.empty?
|
|
733
|
-
charstring
|
|
734
|
-
else
|
|
735
|
-
rewriter.rewrite(charstring, glyph_patterns, glyph_id)
|
|
736
|
-
end
|
|
737
|
-
end
|
|
738
|
-
|
|
739
|
-
[optimized_charstrings, local_subrs]
|
|
740
|
-
rescue StandardError => e
|
|
741
|
-
# If optimization fails for any reason, return original CharStrings
|
|
742
|
-
warn "Optimization warning: #{e.message}"
|
|
743
|
-
[charstrings, []]
|
|
744
|
-
end
|
|
745
|
-
|
|
746
423
|
# Generate static instance from variable font
|
|
747
424
|
#
|
|
748
425
|
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
@@ -1001,6 +678,66 @@ module Fontisan
|
|
|
1001
678
|
def variable_font?(font)
|
|
1002
679
|
font.has_table?("fvar")
|
|
1003
680
|
end
|
|
681
|
+
|
|
682
|
+
# Extract ConversionOptions from options hash
|
|
683
|
+
#
|
|
684
|
+
# @param options [Hash, ConversionOptions] Options or hash containing :options key
|
|
685
|
+
# @return [ConversionOptions, nil] Extracted ConversionOptions or nil
|
|
686
|
+
def extract_conversion_options(options)
|
|
687
|
+
return options if options.is_a?(ConversionOptions)
|
|
688
|
+
|
|
689
|
+
options[:options] if options.is_a?(Hash)
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Apply opening options to source font
|
|
693
|
+
#
|
|
694
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
695
|
+
# @param conv_options [ConversionOptions] Conversion options with opening options
|
|
696
|
+
def apply_opening_options(font, conv_options)
|
|
697
|
+
return unless conv_options
|
|
698
|
+
|
|
699
|
+
# Decompose composites if requested
|
|
700
|
+
if conv_options.opening_option?(:decompose_composites)
|
|
701
|
+
decompose_composite_glyphs(font)
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
# Interpret OpenType tables if requested
|
|
705
|
+
if conv_options.opening_option?(:interpret_ot)
|
|
706
|
+
# Ensure GSUB/GPOS tables are fully loaded
|
|
707
|
+
interpret_opentype_features(font)
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# Note: scale_to_1000 and convert_curves are handled during conversion
|
|
711
|
+
# These options affect the conversion process itself
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
# Decompose composite glyphs in font
|
|
715
|
+
#
|
|
716
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
717
|
+
def decompose_composite_glyphs(_font)
|
|
718
|
+
# Placeholder: Decompose composite glyphs
|
|
719
|
+
# A full implementation would:
|
|
720
|
+
# 1. Identify composite glyphs (TrueType: flags & 0x0020, CFF: seac operator)
|
|
721
|
+
# 2. Extract component outlines
|
|
722
|
+
# 3. Merge into single glyph
|
|
723
|
+
#
|
|
724
|
+
# For now, this is a no-op placeholder
|
|
725
|
+
nil
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# Interpret OpenType layout features
|
|
729
|
+
#
|
|
730
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
731
|
+
def interpret_opentype_features(_font)
|
|
732
|
+
# Placeholder: Interpret GSUB/GPOS tables
|
|
733
|
+
# A full implementation would:
|
|
734
|
+
# 1. Parse GSUB table for substitution rules
|
|
735
|
+
# 2. Parse GPOS table for positioning rules
|
|
736
|
+
# 3. Store interpreted features for later use
|
|
737
|
+
#
|
|
738
|
+
# For now, this is a no-op placeholder
|
|
739
|
+
nil
|
|
740
|
+
end
|
|
1004
741
|
end
|
|
1005
742
|
end
|
|
1006
743
|
end
|