fontisan 0.2.0 → 0.2.1
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 +270 -131
- data/README.adoc +158 -4
- data/Rakefile +44 -47
- data/lib/fontisan/cli.rb +84 -33
- data/lib/fontisan/collection/builder.rb +81 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +16 -0
- data/lib/fontisan/commands/convert_command.rb +97 -170
- data/lib/fontisan/commands/instance_command.rb +71 -80
- data/lib/fontisan/commands/validate_command.rb +25 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +10 -0
- data/lib/fontisan/converters/format_converter.rb +150 -1
- data/lib/fontisan/converters/outline_converter.rb +80 -18
- data/lib/fontisan/converters/woff_writer.rb +1 -1
- data/lib/fontisan/font_loader.rb +3 -5
- data/lib/fontisan/font_writer.rb +7 -6
- data/lib/fontisan/hints/hint_converter.rb +133 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +221 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +100 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +127 -0
- data/lib/fontisan/loading_modes.rb +2 -0
- data/lib/fontisan/models/font_export.rb +2 -2
- data/lib/fontisan/models/hint.rb +173 -1
- data/lib/fontisan/models/validation_report.rb +1 -1
- data/lib/fontisan/open_type_font.rb +25 -9
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +154 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/tables/cff/charstring.rb +33 -4
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +15 -0
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +9 -4
- data/lib/fontisan/tables/cvar.rb +2 -41
- data/lib/fontisan/tables/gvar.rb +2 -41
- data/lib/fontisan/true_type_font.rb +24 -9
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/validation/table_validator.rb +1 -1
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variation/converter.rb +120 -13
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_preserver.rb +288 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +475 -470
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +12 -0
- metadata +31 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "charstring_parser"
|
|
4
|
+
require_relative "charstring_builder"
|
|
5
|
+
require_relative "index_builder"
|
|
6
|
+
|
|
7
|
+
module Fontisan
|
|
8
|
+
module Tables
|
|
9
|
+
class Cff
|
|
10
|
+
# Rebuilds CharStrings INDEX with modified CharStrings
|
|
11
|
+
#
|
|
12
|
+
# CharStringRebuilder provides high-level interface for modifying
|
|
13
|
+
# CharStrings in a CFF font. It extracts all CharStrings from the source
|
|
14
|
+
# INDEX, allows modifications through a callback, and rebuilds the INDEX
|
|
15
|
+
# with updated CharString data.
|
|
16
|
+
#
|
|
17
|
+
# Use Cases:
|
|
18
|
+
# - Per-glyph hint injection
|
|
19
|
+
# - CharString optimization
|
|
20
|
+
# - Subroutine insertion
|
|
21
|
+
# - Any operation requiring CharString modification
|
|
22
|
+
#
|
|
23
|
+
# @example Inject hints into specific glyphs
|
|
24
|
+
# rebuilder = CharStringRebuilder.new(charstrings_index)
|
|
25
|
+
# rebuilder.modify_charstring(42) do |operations|
|
|
26
|
+
# # Insert hint operations at beginning
|
|
27
|
+
# hint_ops = [
|
|
28
|
+
# { type: :operator, name: :hstem, operands: [10, 20] }
|
|
29
|
+
# ]
|
|
30
|
+
# hint_ops + operations
|
|
31
|
+
# end
|
|
32
|
+
# new_index_data = rebuilder.rebuild
|
|
33
|
+
class CharStringRebuilder
|
|
34
|
+
# @return [CharstringsIndex] Source CharStrings INDEX
|
|
35
|
+
attr_reader :source_index
|
|
36
|
+
|
|
37
|
+
# @return [Hash] Modified CharString data by glyph index
|
|
38
|
+
attr_reader :modifications
|
|
39
|
+
|
|
40
|
+
# Initialize rebuilder with source CharStrings INDEX
|
|
41
|
+
#
|
|
42
|
+
# @param source_index [CharstringsIndex] Source CharStrings INDEX
|
|
43
|
+
# @param stem_count [Integer] Number of stem hints (for parsing hintmask)
|
|
44
|
+
def initialize(source_index, stem_count: 0)
|
|
45
|
+
@source_index = source_index
|
|
46
|
+
@stem_count = stem_count
|
|
47
|
+
@modifications = {}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Modify a CharString by glyph index
|
|
51
|
+
#
|
|
52
|
+
# The block receives the parsed operations for the glyph and should
|
|
53
|
+
# return modified operations.
|
|
54
|
+
#
|
|
55
|
+
# @param glyph_index [Integer] Glyph index (0 = .notdef)
|
|
56
|
+
# @yield [operations] Block to modify operations
|
|
57
|
+
# @yieldparam operations [Array<Hash>] Parsed operations
|
|
58
|
+
# @yieldreturn [Array<Hash>] Modified operations
|
|
59
|
+
def modify_charstring(glyph_index, &block)
|
|
60
|
+
# Get original CharString data
|
|
61
|
+
original_data = @source_index[glyph_index]
|
|
62
|
+
return unless original_data
|
|
63
|
+
|
|
64
|
+
# Parse to operations
|
|
65
|
+
parser = CharStringParser.new(original_data, stem_count: @stem_count)
|
|
66
|
+
operations = parser.parse
|
|
67
|
+
|
|
68
|
+
# Apply modification
|
|
69
|
+
modified_operations = block.call(operations)
|
|
70
|
+
|
|
71
|
+
# Build new CharString
|
|
72
|
+
new_data = CharStringBuilder.build_from_operations(modified_operations)
|
|
73
|
+
|
|
74
|
+
# Store modification
|
|
75
|
+
@modifications[glyph_index] = new_data
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Rebuild CharStrings INDEX with modifications
|
|
79
|
+
#
|
|
80
|
+
# Creates new INDEX with modified CharStrings, keeping unmodified
|
|
81
|
+
# CharStrings unchanged.
|
|
82
|
+
#
|
|
83
|
+
# @return [String] Binary CharStrings INDEX data
|
|
84
|
+
def rebuild
|
|
85
|
+
# Collect all CharString data (modified and unmodified)
|
|
86
|
+
charstrings = []
|
|
87
|
+
|
|
88
|
+
(0...@source_index.count).each do |i|
|
|
89
|
+
if @modifications.key?(i)
|
|
90
|
+
# Use modified CharString
|
|
91
|
+
charstrings << @modifications[i]
|
|
92
|
+
else
|
|
93
|
+
# Use original CharString
|
|
94
|
+
charstrings << @source_index[i]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Build INDEX
|
|
99
|
+
IndexBuilder.build(charstrings)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Batch modify multiple CharStrings
|
|
103
|
+
#
|
|
104
|
+
# More efficient than calling modify_charstring multiple times.
|
|
105
|
+
#
|
|
106
|
+
# @param glyph_indices [Array<Integer>] Glyph indices to modify
|
|
107
|
+
# @yield [glyph_index, operations] Block to modify each glyph
|
|
108
|
+
# @yieldparam glyph_index [Integer] Current glyph index
|
|
109
|
+
# @yieldparam operations [Array<Hash>] Parsed operations
|
|
110
|
+
# @yieldreturn [Array<Hash>] Modified operations
|
|
111
|
+
def batch_modify(glyph_indices, &block)
|
|
112
|
+
glyph_indices.each do |glyph_index|
|
|
113
|
+
modify_charstring(glyph_index) do |operations|
|
|
114
|
+
block.call(glyph_index, operations)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Modify all CharStrings
|
|
120
|
+
#
|
|
121
|
+
# Applies the same modification to every glyph.
|
|
122
|
+
#
|
|
123
|
+
# @yield [glyph_index, operations] Block to modify each glyph
|
|
124
|
+
# @yieldparam glyph_index [Integer] Current glyph index
|
|
125
|
+
# @yieldparam operations [Array<Hash>] Parsed operations
|
|
126
|
+
# @yieldreturn [Array<Hash>] Modified operations
|
|
127
|
+
def modify_all(&block)
|
|
128
|
+
(0...@source_index.count).each do |i|
|
|
129
|
+
modify_charstring(i) do |operations|
|
|
130
|
+
block.call(i, operations)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get CharString data (modified or original)
|
|
136
|
+
#
|
|
137
|
+
# @param glyph_index [Integer] Glyph index
|
|
138
|
+
# @return [String] CharString binary data
|
|
139
|
+
def charstring_data(glyph_index)
|
|
140
|
+
@modifications[glyph_index] || @source_index[glyph_index]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Check if glyph has been modified
|
|
144
|
+
#
|
|
145
|
+
# @param glyph_index [Integer] Glyph index
|
|
146
|
+
# @return [Boolean] True if modified
|
|
147
|
+
def modified?(glyph_index)
|
|
148
|
+
@modifications.key?(glyph_index)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Get count of modified glyphs
|
|
152
|
+
#
|
|
153
|
+
# @return [Integer] Number of modified glyphs
|
|
154
|
+
def modification_count
|
|
155
|
+
@modifications.size
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Clear all modifications
|
|
159
|
+
def clear_modifications
|
|
160
|
+
@modifications.clear
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Update stem count (needed for hintmask parsing)
|
|
164
|
+
#
|
|
165
|
+
# @param count [Integer] Number of stem hints
|
|
166
|
+
def stem_count=(count)
|
|
167
|
+
@stem_count = count
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -52,6 +52,7 @@ module Fontisan
|
|
|
52
52
|
charstring_type: [12, 6],
|
|
53
53
|
font_matrix: [12, 7],
|
|
54
54
|
stroke_width: [12, 8],
|
|
55
|
+
font_bbox: 5,
|
|
55
56
|
synthetic_base: [12, 20],
|
|
56
57
|
postscript: [12, 21],
|
|
57
58
|
base_font_name: [12, 22],
|
|
@@ -60,6 +61,20 @@ module Fontisan
|
|
|
60
61
|
subrs: 19,
|
|
61
62
|
default_width_x: 20,
|
|
62
63
|
nominal_width_x: 21,
|
|
64
|
+
# Hint-related Private DICT operators
|
|
65
|
+
blue_values: 6,
|
|
66
|
+
other_blues: 7,
|
|
67
|
+
family_blues: 8,
|
|
68
|
+
family_other_blues: 9,
|
|
69
|
+
std_hw: 10,
|
|
70
|
+
std_vw: 11,
|
|
71
|
+
stem_snap_h: [12, 12],
|
|
72
|
+
stem_snap_v: [12, 13],
|
|
73
|
+
blue_scale: [12, 9],
|
|
74
|
+
blue_shift: [12, 10],
|
|
75
|
+
blue_fuzz: [12, 11],
|
|
76
|
+
force_bold: [12, 14],
|
|
77
|
+
language_group: [12, 17],
|
|
63
78
|
}.freeze
|
|
64
79
|
|
|
65
80
|
# Build DICT structure from hash
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../models/hint"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# Injects hint operations into CharString operation lists
|
|
9
|
+
#
|
|
10
|
+
# HintOperationInjector converts abstract Hint objects into CFF CharString
|
|
11
|
+
# operations and injects them at the appropriate position. It handles:
|
|
12
|
+
# - Stem hints (hstem, vstem, hstemhm, vstemhm)
|
|
13
|
+
# - Hint masks (hintmask with mask data)
|
|
14
|
+
# - Counter masks (cntrmask with mask data)
|
|
15
|
+
# - Stack management (hints are stack-neutral)
|
|
16
|
+
#
|
|
17
|
+
# **Position Rules:**
|
|
18
|
+
# - Hints must appear BEFORE any path construction operators
|
|
19
|
+
# - Width (if present) comes first
|
|
20
|
+
# - Stem hints come before hintmask/cntrmask
|
|
21
|
+
# - Once path construction begins, no more hints allowed
|
|
22
|
+
#
|
|
23
|
+
# **Stack Neutrality:**
|
|
24
|
+
# - Hint operators consume their operands
|
|
25
|
+
# - They don't leave anything on the stack
|
|
26
|
+
# - Path construction starts with clean stack
|
|
27
|
+
#
|
|
28
|
+
# Reference: Type 2 CharString Format Section 4
|
|
29
|
+
# Adobe Technical Note #5177
|
|
30
|
+
#
|
|
31
|
+
# @example Inject hints into a glyph
|
|
32
|
+
# injector = HintOperationInjector.new
|
|
33
|
+
# hints = [
|
|
34
|
+
# Hint.new(type: :stem, data: { position: 100, width: 50, orientation: :horizontal })
|
|
35
|
+
# ]
|
|
36
|
+
# modified_ops = injector.inject(hints, original_operations)
|
|
37
|
+
class HintOperationInjector
|
|
38
|
+
# Initialize injector
|
|
39
|
+
def initialize
|
|
40
|
+
@stem_count = 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Inject hint operations into operation list
|
|
44
|
+
#
|
|
45
|
+
# @param hints [Array<Models::Hint>] Hints to inject
|
|
46
|
+
# @param operations [Array<Hash>] Original CharString operations
|
|
47
|
+
# @return [Array<Hash>] Modified operations with hints injected
|
|
48
|
+
def inject(hints, operations)
|
|
49
|
+
return operations if hints.nil? || hints.empty?
|
|
50
|
+
|
|
51
|
+
# Convert hints to operations
|
|
52
|
+
hint_ops = convert_hints_to_operations(hints)
|
|
53
|
+
return operations if hint_ops.empty?
|
|
54
|
+
|
|
55
|
+
# Find injection point (before first path operator)
|
|
56
|
+
inject_index = find_injection_point(operations)
|
|
57
|
+
|
|
58
|
+
# Insert hint operations
|
|
59
|
+
operations.dup.insert(inject_index, *hint_ops)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get stem count after injection (needed for hintmask)
|
|
63
|
+
#
|
|
64
|
+
# @return [Integer] Number of stem hints
|
|
65
|
+
attr_reader :stem_count
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Convert Hint objects to CharString operations
|
|
70
|
+
#
|
|
71
|
+
# @param hints [Array<Models::Hint>] Hints to convert
|
|
72
|
+
# @return [Array<Hash>] CharString operations
|
|
73
|
+
def convert_hints_to_operations(hints)
|
|
74
|
+
operations = []
|
|
75
|
+
@stem_count = 0
|
|
76
|
+
|
|
77
|
+
hints.each do |hint|
|
|
78
|
+
ops = hint_to_operations(hint)
|
|
79
|
+
operations.concat(ops)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
operations
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Convert single Hint to operations
|
|
86
|
+
#
|
|
87
|
+
# @param hint [Models::Hint] Hint object
|
|
88
|
+
# @return [Array<Hash>] CharString operations
|
|
89
|
+
def hint_to_operations(hint)
|
|
90
|
+
ps_hint = hint.to_postscript
|
|
91
|
+
return [] if ps_hint.empty?
|
|
92
|
+
|
|
93
|
+
case ps_hint[:operator]
|
|
94
|
+
when :hstem, :vstem
|
|
95
|
+
stem_operation(ps_hint)
|
|
96
|
+
when :hstemhm, :vstemhm
|
|
97
|
+
stem_operation(ps_hint)
|
|
98
|
+
when :hintmask
|
|
99
|
+
hintmask_operation(ps_hint)
|
|
100
|
+
when :counter, :cntrmask
|
|
101
|
+
# :counter from Hint model maps to :cntrmask in CharStrings
|
|
102
|
+
cntrmask_operation(ps_hint)
|
|
103
|
+
else
|
|
104
|
+
[]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Create stem hint operation
|
|
109
|
+
#
|
|
110
|
+
# @param ps_hint [Hash] PostScript hint with :operator and :args
|
|
111
|
+
# @return [Array<Hash>] CharString operations
|
|
112
|
+
def stem_operation(ps_hint)
|
|
113
|
+
operator = ps_hint[:operator]
|
|
114
|
+
args = ps_hint[:args] || []
|
|
115
|
+
|
|
116
|
+
# Each pair of args is one stem
|
|
117
|
+
@stem_count += args.length / 2
|
|
118
|
+
|
|
119
|
+
[{
|
|
120
|
+
type: :operator,
|
|
121
|
+
name: operator,
|
|
122
|
+
operands: args,
|
|
123
|
+
hint_data: nil
|
|
124
|
+
}]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Create hintmask operation
|
|
128
|
+
#
|
|
129
|
+
# @param ps_hint [Hash] PostScript hint with :operator and :args (mask)
|
|
130
|
+
# @return [Array<Hash>] CharString operations
|
|
131
|
+
def hintmask_operation(ps_hint)
|
|
132
|
+
mask_bytes = ps_hint[:args] || []
|
|
133
|
+
|
|
134
|
+
# Convert mask array to binary string
|
|
135
|
+
hint_data = if mask_bytes.is_a?(Array)
|
|
136
|
+
mask_bytes.pack("C*")
|
|
137
|
+
elsif mask_bytes.is_a?(String)
|
|
138
|
+
mask_bytes
|
|
139
|
+
else
|
|
140
|
+
""
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
[{
|
|
144
|
+
type: :operator,
|
|
145
|
+
name: :hintmask,
|
|
146
|
+
operands: [],
|
|
147
|
+
hint_data: hint_data
|
|
148
|
+
}]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Create cntrmask operation
|
|
152
|
+
#
|
|
153
|
+
# @param ps_hint [Hash] PostScript hint with :operator and :args (zones)
|
|
154
|
+
# @return [Array<Hash>] CharString operations
|
|
155
|
+
def cntrmask_operation(ps_hint)
|
|
156
|
+
zones = ps_hint[:args] || []
|
|
157
|
+
|
|
158
|
+
# Convert zones to binary string
|
|
159
|
+
hint_data = if zones.is_a?(Array)
|
|
160
|
+
zones.pack("C*")
|
|
161
|
+
elsif zones.is_a?(String)
|
|
162
|
+
zones
|
|
163
|
+
else
|
|
164
|
+
""
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
[{
|
|
168
|
+
type: :operator,
|
|
169
|
+
name: :cntrmask,
|
|
170
|
+
operands: [],
|
|
171
|
+
hint_data: hint_data
|
|
172
|
+
}]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Find injection point for hints
|
|
176
|
+
#
|
|
177
|
+
# Hints must go before first path construction operator.
|
|
178
|
+
# Path operators: moveto, lineto, curveto, etc.
|
|
179
|
+
#
|
|
180
|
+
# @param operations [Array<Hash>] CharString operations
|
|
181
|
+
# @return [Integer] Index to insert hints
|
|
182
|
+
def find_injection_point(operations)
|
|
183
|
+
# Path construction operators
|
|
184
|
+
path_operators = %i[
|
|
185
|
+
rmoveto hmoveto vmoveto
|
|
186
|
+
rlineto hlineto vlineto
|
|
187
|
+
rrcurveto rcurveline rlinecurve
|
|
188
|
+
vvcurveto hhcurveto vhcurveto hvcurveto
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
# Find first path operator
|
|
192
|
+
operations.each_with_index do |op, index|
|
|
193
|
+
return index if path_operators.include?(op[:name])
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# No path operators found - hints go before endchar
|
|
197
|
+
operations.each_with_index do |op, index|
|
|
198
|
+
return index if op[:name] == :endchar
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Empty or malformed - inject at start
|
|
202
|
+
0
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
class Cff
|
|
6
|
+
# Recalculates CFF table offsets after structure modifications
|
|
7
|
+
#
|
|
8
|
+
# When the Private DICT size changes (e.g., adding hint parameters),
|
|
9
|
+
# all offsets in the CFF table must be recalculated. This class
|
|
10
|
+
# computes new offsets based on section sizes.
|
|
11
|
+
#
|
|
12
|
+
# CFF Structure (sequential layout):
|
|
13
|
+
# - Header (fixed size)
|
|
14
|
+
# - Name INDEX
|
|
15
|
+
# - Top DICT INDEX (contains offsets to CharStrings and Private DICT)
|
|
16
|
+
# - String INDEX
|
|
17
|
+
# - Global Subr INDEX
|
|
18
|
+
# - CharStrings INDEX
|
|
19
|
+
# - Private DICT (variable size)
|
|
20
|
+
# - Local Subr INDEX (optional, within Private DICT)
|
|
21
|
+
#
|
|
22
|
+
# Key offsets to recalculate:
|
|
23
|
+
# - charstrings: Offset from CFF start to CharStrings INDEX
|
|
24
|
+
# - private: [size, offset] in Top DICT pointing to Private DICT
|
|
25
|
+
class OffsetRecalculator
|
|
26
|
+
# Calculate offsets for all CFF sections
|
|
27
|
+
#
|
|
28
|
+
# @param sections [Hash] Hash of section_name => binary_data
|
|
29
|
+
# @return [Hash] Hash of offset information
|
|
30
|
+
def self.calculate_offsets(sections)
|
|
31
|
+
offsets = {}
|
|
32
|
+
pos = 0
|
|
33
|
+
|
|
34
|
+
# Track position through CFF structure
|
|
35
|
+
pos += sections[:header].bytesize
|
|
36
|
+
pos += sections[:name_index].bytesize
|
|
37
|
+
|
|
38
|
+
# Top DICT INDEX starts here
|
|
39
|
+
offsets[:top_dict_start] = pos
|
|
40
|
+
pos += sections[:top_dict_index].bytesize
|
|
41
|
+
|
|
42
|
+
pos += sections[:string_index].bytesize
|
|
43
|
+
pos += sections[:global_subr_index].bytesize
|
|
44
|
+
|
|
45
|
+
# CharStrings INDEX offset (referenced in Top DICT)
|
|
46
|
+
offsets[:charstrings] = pos
|
|
47
|
+
pos += sections[:charstrings_index].bytesize
|
|
48
|
+
|
|
49
|
+
# Private DICT offset and size (referenced in Top DICT)
|
|
50
|
+
offsets[:private] = pos
|
|
51
|
+
offsets[:private_size] = sections[:private_dict].bytesize
|
|
52
|
+
|
|
53
|
+
offsets
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Update Top DICT with new offsets
|
|
57
|
+
#
|
|
58
|
+
# @param top_dict [Hash] Top DICT data
|
|
59
|
+
# @param offsets [Hash] Calculated offsets
|
|
60
|
+
# @return [Hash] Updated Top DICT
|
|
61
|
+
def self.update_top_dict(top_dict, offsets)
|
|
62
|
+
updated = top_dict.dup
|
|
63
|
+
updated[:charstrings] = offsets[:charstrings]
|
|
64
|
+
updated[:private] = [offsets[:private_size], offsets[:private]]
|
|
65
|
+
updated
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dict_builder"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# Builds CFF Private DICT with hint parameters
|
|
9
|
+
#
|
|
10
|
+
# Private DICT contains font-level hint information used for rendering quality.
|
|
11
|
+
# This writer validates hint parameters against CFF spec limits and serializes
|
|
12
|
+
# them into binary DICT format.
|
|
13
|
+
#
|
|
14
|
+
# Supported hint parameters:
|
|
15
|
+
# - blue_values: Alignment zones (max 14 values, pairs)
|
|
16
|
+
# - other_blues: Additional zones (max 10 values, pairs)
|
|
17
|
+
# - family_blues: Family alignment zones (max 14 values, pairs)
|
|
18
|
+
# - family_other_blues: Family zones (max 10 values, pairs)
|
|
19
|
+
# - std_hw: Standard horizontal stem width
|
|
20
|
+
# - std_vw: Standard vertical stem width
|
|
21
|
+
# - stem_snap_h: Horizontal stem snap widths (max 12)
|
|
22
|
+
# - stem_snap_v: Vertical stem snap widths (max 12)
|
|
23
|
+
# - blue_scale, blue_shift, blue_fuzz: Overshoot parameters
|
|
24
|
+
# - force_bold: Bold flag
|
|
25
|
+
# - language_group: 0=Latin, 1=CJK
|
|
26
|
+
class PrivateDictWriter
|
|
27
|
+
# CFF specification limits for hint parameters
|
|
28
|
+
HINT_LIMITS = {
|
|
29
|
+
blue_values: { max: 14, pairs: true },
|
|
30
|
+
other_blues: { max: 10, pairs: true },
|
|
31
|
+
family_blues: { max: 14, pairs: true },
|
|
32
|
+
family_other_blues: { max: 10, pairs: true },
|
|
33
|
+
stem_snap_h: { max: 12 },
|
|
34
|
+
stem_snap_v: { max: 12 },
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# Initialize writer with optional source Private DICT
|
|
38
|
+
#
|
|
39
|
+
# @param source_dict [PrivateDict, nil] Source to copy non-hint params from
|
|
40
|
+
def initialize(source_dict = nil)
|
|
41
|
+
@params = {}
|
|
42
|
+
parse_source(source_dict) if source_dict
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Update hint parameters
|
|
46
|
+
#
|
|
47
|
+
# @param hint_params [Hash] Hint parameters to add/update
|
|
48
|
+
# @raise [ArgumentError] If parameters are invalid
|
|
49
|
+
def update_hints(hint_params)
|
|
50
|
+
validate!(hint_params)
|
|
51
|
+
@params.merge!(hint_params.transform_keys(&:to_sym))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Serialize to binary DICT format
|
|
55
|
+
#
|
|
56
|
+
# @return [String] Binary DICT data
|
|
57
|
+
def serialize
|
|
58
|
+
DictBuilder.build(@params)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get serialized size in bytes
|
|
62
|
+
#
|
|
63
|
+
# @return [Integer] Size in bytes
|
|
64
|
+
def size
|
|
65
|
+
serialize.bytesize
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Parse non-hint parameters from source Private DICT
|
|
71
|
+
#
|
|
72
|
+
# @param source_dict [PrivateDict] Source dictionary
|
|
73
|
+
def parse_source(source_dict)
|
|
74
|
+
return unless source_dict.respond_to?(:to_h)
|
|
75
|
+
|
|
76
|
+
# Extract only non-hint params (subrs, widths)
|
|
77
|
+
@params = source_dict.to_h.select do |k, _|
|
|
78
|
+
%i[subrs default_width_x nominal_width_x].include?(k)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Validate hint parameters against CFF spec
|
|
83
|
+
#
|
|
84
|
+
# @param params [Hash] Hint parameters
|
|
85
|
+
# @raise [ArgumentError] If validation fails
|
|
86
|
+
def validate!(params)
|
|
87
|
+
params.each do |key, value|
|
|
88
|
+
k = key.to_sym
|
|
89
|
+
validate_hint_param(k, value)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Validate individual hint parameter
|
|
94
|
+
#
|
|
95
|
+
# @param key [Symbol] Parameter name
|
|
96
|
+
# @param value [Object] Parameter value
|
|
97
|
+
# @raise [ArgumentError] If validation fails
|
|
98
|
+
def validate_hint_param(key, value)
|
|
99
|
+
# Check array limits
|
|
100
|
+
if HINT_LIMITS[key]
|
|
101
|
+
raise ArgumentError, "#{key} invalid" unless value.is_a?(Array)
|
|
102
|
+
raise ArgumentError, "#{key} too long" if value.length > HINT_LIMITS[key][:max]
|
|
103
|
+
if HINT_LIMITS[key][:pairs] && value.length.odd?
|
|
104
|
+
raise ArgumentError, "#{key} must be pairs"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check value-specific constraints
|
|
109
|
+
case key
|
|
110
|
+
when :std_hw, :std_vw
|
|
111
|
+
raise ArgumentError, "#{key} negative" if value.negative?
|
|
112
|
+
when :blue_scale
|
|
113
|
+
raise ArgumentError, "#{key} not positive" if value <= 0
|
|
114
|
+
when :blue_shift, :blue_fuzz
|
|
115
|
+
raise ArgumentError, "#{key} invalid" unless value.is_a?(Numeric)
|
|
116
|
+
when :force_bold
|
|
117
|
+
raise ArgumentError, "#{key} must be 0 or 1" unless [0, 1].include?(value)
|
|
118
|
+
when :language_group
|
|
119
|
+
raise ArgumentError, "#{key} must be 0 or 1" unless [0, 1].include?(value)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|