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,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../outline_extractor"
|
|
4
|
+
require_relative "../tables/cff/charstring_builder"
|
|
5
|
+
require_relative "../tables/glyf/glyph_builder"
|
|
6
|
+
require_relative "../tables/glyf/compound_glyph_resolver"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
module Converters
|
|
10
|
+
# Extracts all glyph outlines from a font for conversion purposes
|
|
11
|
+
#
|
|
12
|
+
# Unlike [`OutlineExtractor`](../outline_extractor.rb) which extracts
|
|
13
|
+
# single glyphs, this module extracts ALL glyphs from a font for
|
|
14
|
+
# bulk conversion operations.
|
|
15
|
+
#
|
|
16
|
+
# @see OutlineExtractor for single glyph extraction
|
|
17
|
+
module OutlineExtraction
|
|
18
|
+
# Extract all outlines from TrueType font
|
|
19
|
+
#
|
|
20
|
+
# @param font [TrueTypeFont] Source font
|
|
21
|
+
# @return [Array<Outline>] Array of outline objects
|
|
22
|
+
def extract_ttf_outlines(font)
|
|
23
|
+
# Get required tables
|
|
24
|
+
head = font.table("head")
|
|
25
|
+
maxp = font.table("maxp")
|
|
26
|
+
loca = font.table("loca")
|
|
27
|
+
glyf = font.table("glyf")
|
|
28
|
+
|
|
29
|
+
# Parse loca with context
|
|
30
|
+
loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
|
|
31
|
+
|
|
32
|
+
# Create resolver for compound glyphs
|
|
33
|
+
resolver = Tables::CompoundGlyphResolver.new(glyf, loca, head)
|
|
34
|
+
|
|
35
|
+
# Extract all glyphs
|
|
36
|
+
outlines = []
|
|
37
|
+
maxp.num_glyphs.times do |glyph_id|
|
|
38
|
+
glyph = glyf.glyph_for(glyph_id, loca, head)
|
|
39
|
+
|
|
40
|
+
outlines << if glyph.nil? || glyph.empty?
|
|
41
|
+
# Empty glyph - create empty outline
|
|
42
|
+
Models::Outline.new(
|
|
43
|
+
glyph_id: glyph_id,
|
|
44
|
+
commands: [],
|
|
45
|
+
bbox: { x_min: 0, y_min: 0, x_max: 0, y_max: 0 },
|
|
46
|
+
)
|
|
47
|
+
elsif glyph.simple?
|
|
48
|
+
# Convert simple glyph to outline
|
|
49
|
+
Models::Outline.from_truetype(glyph, glyph_id)
|
|
50
|
+
else
|
|
51
|
+
# Compound glyph - resolve to simple outline
|
|
52
|
+
resolver.resolve(glyph)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
outlines
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Extract all outlines from CFF font
|
|
60
|
+
#
|
|
61
|
+
# @param font [OpenTypeFont] Source font
|
|
62
|
+
# @return [Array<Outline>] Array of outline objects
|
|
63
|
+
def extract_cff_outlines(font)
|
|
64
|
+
# Get CFF table
|
|
65
|
+
cff = font.table("CFF ")
|
|
66
|
+
raise Fontisan::Error, "CFF table not found" unless cff
|
|
67
|
+
|
|
68
|
+
# Get number of glyphs
|
|
69
|
+
num_glyphs = cff.glyph_count
|
|
70
|
+
|
|
71
|
+
# Extract all glyphs
|
|
72
|
+
outlines = []
|
|
73
|
+
num_glyphs.times do |glyph_id|
|
|
74
|
+
charstring = cff.charstring_for_glyph(glyph_id)
|
|
75
|
+
|
|
76
|
+
outlines << if charstring.nil? || charstring.path.empty?
|
|
77
|
+
# Empty glyph
|
|
78
|
+
Models::Outline.new(
|
|
79
|
+
glyph_id: glyph_id,
|
|
80
|
+
commands: [],
|
|
81
|
+
bbox: { x_min: 0, y_min: 0, x_max: 0, y_max: 0 },
|
|
82
|
+
)
|
|
83
|
+
else
|
|
84
|
+
# Convert CharString to outline
|
|
85
|
+
Models::Outline.from_cff(charstring, glyph_id)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
outlines
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../optimizers/pattern_analyzer"
|
|
4
|
+
require_relative "../optimizers/subroutine_optimizer"
|
|
5
|
+
require_relative "../optimizers/subroutine_builder"
|
|
6
|
+
require_relative "../optimizers/charstring_rewriter"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
module Converters
|
|
10
|
+
# Optimizes CFF CharStrings using subroutine extraction
|
|
11
|
+
#
|
|
12
|
+
# This module analyzes CharStrings for repeated patterns, extracts
|
|
13
|
+
# them as subroutines, and rewrites the CharStrings to call the
|
|
14
|
+
# subroutines instead of repeating the code.
|
|
15
|
+
#
|
|
16
|
+
# The optimization process:
|
|
17
|
+
# 1. Analyze patterns across all CharStrings
|
|
18
|
+
# 2. Select optimal set of patterns for subroutines
|
|
19
|
+
# 3. Optimize subroutine ordering
|
|
20
|
+
# 4. Build subroutines from selected patterns
|
|
21
|
+
# 5. Rewrite CharStrings to call subroutines
|
|
22
|
+
module OutlineOptimizer
|
|
23
|
+
# Optimize CharStrings using subroutine extraction
|
|
24
|
+
#
|
|
25
|
+
# @param charstrings [Array<String>] Original CharString bytes
|
|
26
|
+
# @return [Array<Array<String>, Array<String>>] [optimized_charstrings, local_subrs]
|
|
27
|
+
def optimize_charstrings(charstrings)
|
|
28
|
+
# Convert to hash format expected by PatternAnalyzer
|
|
29
|
+
charstrings_hash = {}
|
|
30
|
+
charstrings.each_with_index do |cs, index|
|
|
31
|
+
charstrings_hash[index] = cs
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Analyze patterns
|
|
35
|
+
analyzer = Optimizers::PatternAnalyzer.new(
|
|
36
|
+
min_length: 10,
|
|
37
|
+
stack_aware: true,
|
|
38
|
+
)
|
|
39
|
+
patterns = analyzer.analyze(charstrings_hash)
|
|
40
|
+
|
|
41
|
+
# Return original if no patterns found
|
|
42
|
+
return [charstrings, []] if patterns.empty?
|
|
43
|
+
|
|
44
|
+
# Optimize selection
|
|
45
|
+
optimizer = Optimizers::SubroutineOptimizer.new(patterns,
|
|
46
|
+
max_subrs: 65_535)
|
|
47
|
+
selected_patterns = optimizer.optimize_selection
|
|
48
|
+
|
|
49
|
+
# Optimize ordering
|
|
50
|
+
selected_patterns = optimizer.optimize_ordering(selected_patterns)
|
|
51
|
+
|
|
52
|
+
# Return original if no patterns selected
|
|
53
|
+
return [charstrings, []] if selected_patterns.empty?
|
|
54
|
+
|
|
55
|
+
# Build subroutines
|
|
56
|
+
builder = Optimizers::SubroutineBuilder.new(selected_patterns,
|
|
57
|
+
type: :local)
|
|
58
|
+
local_subrs = builder.build
|
|
59
|
+
|
|
60
|
+
# Build subroutine map
|
|
61
|
+
subroutine_map = {}
|
|
62
|
+
selected_patterns.each_with_index do |pattern, index|
|
|
63
|
+
subroutine_map[pattern.bytes] = index
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Rewrite CharStrings
|
|
67
|
+
rewriter = Optimizers::CharstringRewriter.new(subroutine_map, builder)
|
|
68
|
+
optimized_charstrings = charstrings.map.with_index do |charstring, glyph_id|
|
|
69
|
+
# Find patterns for this glyph
|
|
70
|
+
glyph_patterns = selected_patterns.select do |p|
|
|
71
|
+
p.glyphs.include?(glyph_id)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if glyph_patterns.empty?
|
|
75
|
+
charstring
|
|
76
|
+
else
|
|
77
|
+
rewriter.rewrite(charstring, glyph_patterns, glyph_id)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
[optimized_charstrings, local_subrs]
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
# If optimization fails for any reason, return original CharStrings
|
|
84
|
+
warn "Optimization warning: #{e.message}"
|
|
85
|
+
[charstrings, []]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|