fontisan 0.2.0 → 0.2.2
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 +119 -308
- data/README.adoc +1525 -1323
- data/Rakefile +45 -47
- data/benchmark/variation_quick_bench.rb +4 -4
- data/docs/FONT_HINTING.adoc +562 -0
- data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
- data/lib/fontisan/cli.rb +92 -34
- data/lib/fontisan/collection/builder.rb +82 -0
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +21 -2
- data/lib/fontisan/commands/convert_command.rb +96 -165
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +77 -85
- data/lib/fontisan/commands/validate_command.rb +28 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +34 -24
- data/lib/fontisan/converters/format_converter.rb +154 -1
- data/lib/fontisan/converters/outline_converter.rb +101 -34
- data/lib/fontisan/converters/woff_writer.rb +9 -4
- data/lib/fontisan/font_loader.rb +14 -9
- data/lib/fontisan/font_writer.rb +9 -6
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +131 -2
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
- data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
- data/lib/fontisan/loading_modes.rb +6 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +183 -12
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/open_type_font.rb +28 -10
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +159 -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 +416 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +58 -3
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +10 -5
- data/lib/fontisan/tables/cvar.rb +2 -41
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
- data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
- data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
- data/lib/fontisan/tables/gvar.rb +2 -41
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +27 -10
- 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/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +121 -13
- data/lib/fontisan/variation/delta_applier.rb +2 -1
- data/lib/fontisan/variation/inspector.rb +2 -1
- data/lib/fontisan/variation/instance_generator.rb +2 -1
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- 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 +291 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +489 -468
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +54 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +37 -2
|
@@ -1,184 +1,263 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../models/hint"
|
|
4
|
+
require "json"
|
|
4
5
|
|
|
5
6
|
module Fontisan
|
|
6
7
|
module Hints
|
|
7
|
-
# Applies rendering hints to PostScript/CFF
|
|
8
|
+
# Applies rendering hints to PostScript/CFF font tables
|
|
8
9
|
#
|
|
9
|
-
# This applier
|
|
10
|
-
#
|
|
11
|
-
# operator placement and maintains CharString validity.
|
|
10
|
+
# This applier validates and applies PostScript hint data to CFF fonts by
|
|
11
|
+
# rebuilding the entire CFF table structure with updated Private DICT parameters.
|
|
12
12
|
#
|
|
13
|
-
# **
|
|
13
|
+
# **Status**: Fully Operational (Phase 10A Complete)
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
# - Hintmask operators can appear throughout the CharString
|
|
17
|
-
# - Hints affect all subsequent path operations
|
|
15
|
+
# **PostScript Hint Parameters (Private DICT)**:
|
|
18
16
|
#
|
|
19
|
-
#
|
|
17
|
+
# - blue_values: Alignment zones for overshoot suppression
|
|
18
|
+
# - other_blues: Additional alignment zones
|
|
19
|
+
# - std_hw: Standard horizontal stem width
|
|
20
|
+
# - std_vw: Standard vertical stem width
|
|
21
|
+
# - stem_snap_h: Horizontal stem snap widths
|
|
22
|
+
# - stem_snap_v: Vertical stem snap widths
|
|
23
|
+
# - blue_scale, blue_shift, blue_fuzz: Overshoot parameters
|
|
24
|
+
# - force_bold: Force bold flag
|
|
25
|
+
# - language_group: Language group (0=Latin, 1=CJK)
|
|
26
|
+
#
|
|
27
|
+
# @example Apply PostScript hints
|
|
20
28
|
# applier = PostScriptHintApplier.new
|
|
21
|
-
#
|
|
29
|
+
# tables = { "CFF " => cff_table }
|
|
30
|
+
# hint_set = HintSet.new(format: "postscript", private_dict_hints: hints_json)
|
|
31
|
+
# result = applier.apply(hint_set, tables)
|
|
22
32
|
class PostScriptHintApplier
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
HSTEM3 = [12, 2]
|
|
29
|
-
VSTEM3 = [12, 1]
|
|
30
|
-
|
|
31
|
-
# Apply hints to CharString
|
|
33
|
+
# Apply PostScript hints to font tables
|
|
34
|
+
#
|
|
35
|
+
# Validates hint data and rebuilds CFF table with updated Private DICT.
|
|
36
|
+
# Supports arbitrary Private DICT size changes through full table reconstruction.
|
|
37
|
+
# Also supports per-glyph hints injected directly into CharStrings.
|
|
32
38
|
#
|
|
33
|
-
# @param
|
|
34
|
-
# @param
|
|
35
|
-
# @return [
|
|
36
|
-
def apply(
|
|
37
|
-
return
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
# @param hint_set [HintSet] Hint data to apply
|
|
40
|
+
# @param tables [Hash] Font tables (must include "CFF " or "CFF2 ")
|
|
41
|
+
# @return [Hash] Updated font tables with hints applied
|
|
42
|
+
def apply(hint_set, tables)
|
|
43
|
+
return tables if hint_set.nil? || hint_set.empty?
|
|
44
|
+
return tables unless hint_set.format == "postscript"
|
|
45
|
+
|
|
46
|
+
if cff2_table?(tables)
|
|
47
|
+
apply_cff2_hints(hint_set, tables)
|
|
48
|
+
elsif cff_table?(tables)
|
|
49
|
+
apply_cff_hints(hint_set, tables)
|
|
50
|
+
else
|
|
51
|
+
tables
|
|
52
|
+
end
|
|
46
53
|
end
|
|
47
54
|
|
|
48
55
|
private
|
|
49
56
|
|
|
50
|
-
#
|
|
57
|
+
# Check if tables contain CFF2 table
|
|
51
58
|
#
|
|
52
|
-
# @param
|
|
53
|
-
# @return [
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# Group hints by type for proper ordering
|
|
58
|
-
stem_hints = hints.select { |h| h.type == :stem }
|
|
59
|
-
stem3_hints = hints.select { |h| h.type == :stem3 }
|
|
60
|
-
mask_hints = hints.select { |h| %i[hint_replacement counter].include?(h.type) }
|
|
61
|
-
|
|
62
|
-
# Add stem hints first
|
|
63
|
-
stem_hints.each do |hint|
|
|
64
|
-
operators << encode_stem_hint(hint)
|
|
65
|
-
end
|
|
59
|
+
# @param tables [Hash] Font tables
|
|
60
|
+
# @return [Boolean] True if CFF2 table present
|
|
61
|
+
def cff2_table?(tables)
|
|
62
|
+
tables.key?("CFF2") || tables.key?("CFF2 ")
|
|
63
|
+
end
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
# Check if tables contain CFF table
|
|
66
|
+
#
|
|
67
|
+
# @param tables [Hash] Font tables
|
|
68
|
+
# @return [Boolean] True if CFF table present
|
|
69
|
+
def cff_table?(tables)
|
|
70
|
+
tables.key?("CFF ")
|
|
71
|
+
end
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
# Apply hints to CFF2 variable font
|
|
74
|
+
#
|
|
75
|
+
# @param hint_set [HintSet] Hint set with font-level and per-glyph hints
|
|
76
|
+
# @param tables [Hash] Font tables
|
|
77
|
+
# @return [Hash] Updated tables
|
|
78
|
+
def apply_cff2_hints(hint_set, tables)
|
|
79
|
+
# Load CFF2 table
|
|
80
|
+
cff2_data = tables["CFF2"] || tables["CFF2 "]
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
begin
|
|
83
|
+
require_relative "../tables/cff2/table_reader"
|
|
84
|
+
require_relative "../tables/cff2/table_builder"
|
|
85
|
+
|
|
86
|
+
reader = Tables::Cff2::TableReader.new(cff2_data)
|
|
87
|
+
|
|
88
|
+
# Validate CFF2 version
|
|
89
|
+
reader.read_header
|
|
90
|
+
unless reader.header[:major_version] == 2
|
|
91
|
+
warn "Invalid CFF2 table version: #{reader.header[:major_version]}"
|
|
92
|
+
return tables
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Build with hints
|
|
96
|
+
builder = Tables::Cff2::TableBuilder.new(reader, hint_set)
|
|
97
|
+
modified_table = builder.build
|
|
98
|
+
|
|
99
|
+
# Update tables
|
|
100
|
+
table_key = tables.key?("CFF2") ? "CFF2" : "CFF2 "
|
|
101
|
+
tables[table_key] = modified_table
|
|
102
|
+
|
|
103
|
+
tables
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
warn "Error applying CFF2 hints: #{e.message}"
|
|
106
|
+
tables
|
|
107
|
+
end
|
|
78
108
|
end
|
|
79
109
|
|
|
80
|
-
#
|
|
110
|
+
# Apply hints to CFF font
|
|
81
111
|
#
|
|
82
|
-
# @param
|
|
83
|
-
# @
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return ""
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
# @param hint_set [HintSet] Hint set with font-level and per-glyph hints
|
|
113
|
+
# @param tables [Hash] Font tables
|
|
114
|
+
# @return [Hash] Updated tables
|
|
115
|
+
def apply_cff_hints(hint_set, tables)
|
|
116
|
+
return tables unless tables["CFF "]
|
|
117
|
+
|
|
118
|
+
# Validate hint parameters (Private DICT)
|
|
119
|
+
hint_params = parse_hint_parameters(hint_set)
|
|
120
|
+
|
|
121
|
+
# Check if we have per-glyph hints
|
|
122
|
+
has_per_glyph_hints = hint_set.hinted_glyph_count.positive?
|
|
123
|
+
|
|
124
|
+
# If neither font-level nor per-glyph hints, return unchanged
|
|
125
|
+
return tables unless hint_params || has_per_glyph_hints
|
|
126
|
+
|
|
127
|
+
# Validate font-level parameters if present
|
|
128
|
+
if hint_params && !valid_hint_parameters?(hint_params)
|
|
129
|
+
return tables
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Apply hints (both font-level and per-glyph)
|
|
133
|
+
begin
|
|
134
|
+
require_relative "../tables/cff/table_builder"
|
|
135
|
+
require_relative "../tables/cff/charstring_rebuilder"
|
|
136
|
+
require_relative "../tables/cff/hint_operation_injector"
|
|
137
|
+
require_relative "../tables/cff"
|
|
138
|
+
|
|
139
|
+
# Parse CFF binary data into Cff object if needed
|
|
140
|
+
cff_data = tables["CFF "]
|
|
141
|
+
cff_table = if cff_data.is_a?(Tables::Cff)
|
|
142
|
+
cff_data
|
|
143
|
+
else
|
|
144
|
+
Tables::Cff.read(cff_data)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Prepare per-glyph hint data if present
|
|
148
|
+
per_glyph_hints = if has_per_glyph_hints
|
|
149
|
+
extract_per_glyph_hints(hint_set)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
new_cff_data = Tables::Cff::TableBuilder.rebuild(
|
|
153
|
+
cff_table,
|
|
154
|
+
private_dict_hints: hint_params,
|
|
155
|
+
per_glyph_hints: per_glyph_hints,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
tables["CFF "] = new_cff_data
|
|
159
|
+
tables
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
warn "Failed to apply PostScript hints: #{e.message}"
|
|
162
|
+
tables
|
|
163
|
+
end
|
|
102
164
|
end
|
|
103
165
|
|
|
104
|
-
#
|
|
166
|
+
# Parse hint parameters from HintSet
|
|
105
167
|
#
|
|
106
|
-
# @param
|
|
107
|
-
# @return [
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
bytes = args.map { |arg| encode_cff_integer(arg) }.join
|
|
117
|
-
|
|
118
|
-
# Add two-byte operator (12 followed by subop)
|
|
119
|
-
bytes << if operator == :vstem3
|
|
120
|
-
VSTEM3.pack("C*")
|
|
121
|
-
else
|
|
122
|
-
HSTEM3.pack("C*")
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
bytes
|
|
168
|
+
# @param hint_set [HintSet] Hint set with Private dict hints
|
|
169
|
+
# @return [Hash, nil] Parsed hint parameters, or nil if invalid
|
|
170
|
+
def parse_hint_parameters(hint_set)
|
|
171
|
+
return nil unless hint_set.private_dict_hints
|
|
172
|
+
return nil if hint_set.private_dict_hints == "{}"
|
|
173
|
+
|
|
174
|
+
JSON.parse(hint_set.private_dict_hints)
|
|
175
|
+
rescue JSON::ParserError => e
|
|
176
|
+
warn "Failed to parse Private dict hints: #{e.message}"
|
|
177
|
+
nil
|
|
126
178
|
end
|
|
127
179
|
|
|
128
|
-
#
|
|
180
|
+
# Validate hint parameters against CFF specification limits
|
|
129
181
|
#
|
|
130
|
-
# @param
|
|
131
|
-
# @return [
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
-
|
|
182
|
+
# @param params [Hash] Hint parameters
|
|
183
|
+
# @return [Boolean] True if all parameters are valid
|
|
184
|
+
def valid_hint_parameters?(params)
|
|
185
|
+
# Validate blue values (must be pairs, max 7 pairs = 14 values)
|
|
186
|
+
if params["blue_values"] || params[:blue_values]
|
|
187
|
+
values = params["blue_values"] || params[:blue_values]
|
|
188
|
+
return false unless values.is_a?(Array)
|
|
189
|
+
return false if values.length > 14 # Max 7 pairs
|
|
190
|
+
return false if values.length.odd? # Must be pairs
|
|
191
|
+
end
|
|
135
192
|
|
|
136
|
-
#
|
|
137
|
-
|
|
193
|
+
# Validate other_blues (max 5 pairs = 10 values)
|
|
194
|
+
if params["other_blues"] || params[:other_blues]
|
|
195
|
+
values = params["other_blues"] || params[:other_blues]
|
|
196
|
+
return false unless values.is_a?(Array)
|
|
197
|
+
return false if values.length > 10
|
|
198
|
+
return false if values.length.odd?
|
|
199
|
+
end
|
|
138
200
|
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
201
|
+
# Validate stem widths (single values)
|
|
202
|
+
if params["std_hw"] || params[:std_hw]
|
|
203
|
+
value = params["std_hw"] || params[:std_hw]
|
|
204
|
+
return false unless value.is_a?(Numeric)
|
|
205
|
+
return false if value.negative?
|
|
206
|
+
end
|
|
142
207
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
208
|
+
if params["std_vw"] || params[:std_vw]
|
|
209
|
+
value = params["std_vw"] || params[:std_vw]
|
|
210
|
+
return false unless value.is_a?(Numeric)
|
|
211
|
+
return false if value.negative?
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Validate stem snaps (arrays, max 12 values each)
|
|
215
|
+
%w[stem_snap_h stem_snap_v].each do |key|
|
|
216
|
+
next unless params[key] || params[key.to_sym]
|
|
217
|
+
|
|
218
|
+
values = params[key] || params[key.to_sym]
|
|
219
|
+
return false unless values.is_a?(Array)
|
|
220
|
+
return false if values.length > 12
|
|
151
221
|
end
|
|
152
222
|
|
|
153
|
-
#
|
|
154
|
-
if
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return
|
|
223
|
+
# Validate blue_scale (should be positive)
|
|
224
|
+
if params["blue_scale"] || params[:blue_scale]
|
|
225
|
+
value = params["blue_scale"] || params[:blue_scale]
|
|
226
|
+
return false unless value.is_a?(Numeric)
|
|
227
|
+
return false if value <= 0
|
|
158
228
|
end
|
|
159
229
|
|
|
160
|
-
#
|
|
161
|
-
if
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return [b0, b1].pack("C*")
|
|
230
|
+
# Validate language_group (0 or 1 only)
|
|
231
|
+
if params["language_group"] || params[:language_group]
|
|
232
|
+
value = params["language_group"] || params[:language_group]
|
|
233
|
+
return false unless [0, 1].include?(value)
|
|
165
234
|
end
|
|
166
235
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
236
|
+
true
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Extract specific hint parameter with symbol/string key support
|
|
240
|
+
#
|
|
241
|
+
# @param params [Hash] Hint parameters
|
|
242
|
+
# @param key [String] Parameter name
|
|
243
|
+
# @return [Object, nil] Parameter value
|
|
244
|
+
def extract_param(params, key)
|
|
245
|
+
params[key] || params[key.to_sym]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Extract per-glyph hint data from HintSet
|
|
249
|
+
#
|
|
250
|
+
# @param hint_set [HintSet] Hint set with per-glyph hints
|
|
251
|
+
# @return [Hash] Hash mapping glyph_id => Array<Hint>
|
|
252
|
+
def extract_per_glyph_hints(hint_set)
|
|
253
|
+
per_glyph = {}
|
|
254
|
+
|
|
255
|
+
hint_set.hinted_glyph_ids.each do |glyph_id|
|
|
256
|
+
hints = hint_set.get_glyph_hints(glyph_id)
|
|
257
|
+
per_glyph[glyph_id.to_i] = hints unless hints.empty?
|
|
171
258
|
end
|
|
172
259
|
|
|
173
|
-
|
|
174
|
-
bytes = [
|
|
175
|
-
255,
|
|
176
|
-
(num >> 24) & 0xff,
|
|
177
|
-
(num >> 16) & 0xff,
|
|
178
|
-
(num >> 8) & 0xff,
|
|
179
|
-
num & 0xff
|
|
180
|
-
]
|
|
181
|
-
bytes.pack("C*")
|
|
260
|
+
per_glyph
|
|
182
261
|
end
|
|
183
262
|
end
|
|
184
263
|
end
|
|
@@ -29,6 +29,28 @@ module Fontisan
|
|
|
29
29
|
HSTEM3 = 12 << 8 | 2
|
|
30
30
|
VSTEM3 = 12 << 8 | 1
|
|
31
31
|
|
|
32
|
+
# Extract complete hint data from OpenType/CFF font
|
|
33
|
+
#
|
|
34
|
+
# This extracts both font-level hints (CFF Private dict) and
|
|
35
|
+
# per-glyph hints from CharStrings.
|
|
36
|
+
#
|
|
37
|
+
# @param font [OpenTypeFont] OpenType font with CFF table
|
|
38
|
+
# @return [Models::HintSet] Complete hint set
|
|
39
|
+
def extract_from_font(font)
|
|
40
|
+
hint_set = Models::HintSet.new(format: "postscript")
|
|
41
|
+
|
|
42
|
+
# Extract font-level Private dict hints
|
|
43
|
+
hint_set.private_dict_hints = extract_private_dict_hints(font).to_json
|
|
44
|
+
|
|
45
|
+
# Extract per-glyph CharString hints
|
|
46
|
+
extract_charstring_hints(font, hint_set)
|
|
47
|
+
|
|
48
|
+
# Update metadata
|
|
49
|
+
hint_set.has_hints = !hint_set.empty?
|
|
50
|
+
|
|
51
|
+
hint_set
|
|
52
|
+
end
|
|
53
|
+
|
|
32
54
|
# Extract hints from CFF CharString
|
|
33
55
|
#
|
|
34
56
|
# @param charstring [CharString, String] CFF CharString object or bytes
|
|
@@ -69,12 +91,12 @@ module Fontisan
|
|
|
69
91
|
if operator?(byte)
|
|
70
92
|
# Process operator
|
|
71
93
|
operator = if byte == 12
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
# Two-byte operator
|
|
95
|
+
i += 1
|
|
96
|
+
(12 << 8) | bytes[i]
|
|
97
|
+
else
|
|
98
|
+
byte
|
|
99
|
+
end
|
|
78
100
|
|
|
79
101
|
hint = process_operator(operator, stack)
|
|
80
102
|
hints << hint if hint
|
|
@@ -143,7 +165,7 @@ module Fontisan
|
|
|
143
165
|
# 5-byte signed integer
|
|
144
166
|
if index + 4 < bytes.length
|
|
145
167
|
num = (bytes[index + 1] << 24) | (bytes[index + 2] << 16) |
|
|
146
|
-
|
|
168
|
+
(bytes[index + 3] << 8) | bytes[index + 4]
|
|
147
169
|
num = num - 4294967296 if num > 2147483647
|
|
148
170
|
[num, 5]
|
|
149
171
|
else
|
|
@@ -182,7 +204,7 @@ module Fontisan
|
|
|
182
204
|
Models::Hint.new(
|
|
183
205
|
type: :hint_replacement,
|
|
184
206
|
data: { mask: stack.dup },
|
|
185
|
-
source_format: :postscript
|
|
207
|
+
source_format: :postscript,
|
|
186
208
|
)
|
|
187
209
|
|
|
188
210
|
when CNTRMASK
|
|
@@ -190,11 +212,9 @@ module Fontisan
|
|
|
190
212
|
Models::Hint.new(
|
|
191
213
|
type: :counter,
|
|
192
214
|
data: { zones: stack.dup },
|
|
193
|
-
source_format: :postscript
|
|
215
|
+
source_format: :postscript,
|
|
194
216
|
)
|
|
195
217
|
|
|
196
|
-
else
|
|
197
|
-
nil
|
|
198
218
|
end
|
|
199
219
|
end
|
|
200
220
|
|
|
@@ -216,9 +236,9 @@ module Fontisan
|
|
|
216
236
|
data: {
|
|
217
237
|
position: position,
|
|
218
238
|
width: width,
|
|
219
|
-
orientation: orientation
|
|
239
|
+
orientation: orientation,
|
|
220
240
|
},
|
|
221
|
-
source_format: :postscript
|
|
241
|
+
source_format: :postscript,
|
|
222
242
|
)
|
|
223
243
|
end
|
|
224
244
|
|
|
@@ -236,7 +256,7 @@ module Fontisan
|
|
|
236
256
|
pos_idx = i * 2
|
|
237
257
|
stems << {
|
|
238
258
|
position: stack[pos_idx],
|
|
239
|
-
width: stack[pos_idx + 1]
|
|
259
|
+
width: stack[pos_idx + 1],
|
|
240
260
|
}
|
|
241
261
|
end
|
|
242
262
|
|
|
@@ -244,11 +264,126 @@ module Fontisan
|
|
|
244
264
|
type: :stem3,
|
|
245
265
|
data: {
|
|
246
266
|
stems: stems,
|
|
247
|
-
orientation: orientation
|
|
267
|
+
orientation: orientation,
|
|
248
268
|
},
|
|
249
|
-
source_format: :postscript
|
|
269
|
+
source_format: :postscript,
|
|
250
270
|
)
|
|
251
271
|
end
|
|
272
|
+
|
|
273
|
+
# Extract Private dict hints from CFF table
|
|
274
|
+
#
|
|
275
|
+
# Private dict contains font-level hint parameters like BlueValues,
|
|
276
|
+
# StdHW, StdVW, etc.
|
|
277
|
+
#
|
|
278
|
+
# @param font [OpenTypeFont] OpenType font
|
|
279
|
+
# @return [Hash] Private dict hint parameters
|
|
280
|
+
def extract_private_dict_hints(font)
|
|
281
|
+
hints = {}
|
|
282
|
+
|
|
283
|
+
return hints unless font.has_table?("CFF ")
|
|
284
|
+
|
|
285
|
+
cff_table = font.table("CFF ")
|
|
286
|
+
return hints unless cff_table
|
|
287
|
+
|
|
288
|
+
# Get Private DICT for first font (index 0)
|
|
289
|
+
private_dict = cff_table.private_dict(0)
|
|
290
|
+
return hints unless private_dict
|
|
291
|
+
|
|
292
|
+
# Extract hint-related parameters from Private DICT
|
|
293
|
+
# These are the key hinting parameters in CFF
|
|
294
|
+
if private_dict.respond_to?(:blue_values)
|
|
295
|
+
hints[:blue_values] =
|
|
296
|
+
private_dict.blue_values
|
|
297
|
+
end
|
|
298
|
+
if private_dict.respond_to?(:other_blues)
|
|
299
|
+
hints[:other_blues] =
|
|
300
|
+
private_dict.other_blues
|
|
301
|
+
end
|
|
302
|
+
if private_dict.respond_to?(:family_blues)
|
|
303
|
+
hints[:family_blues] =
|
|
304
|
+
private_dict.family_blues
|
|
305
|
+
end
|
|
306
|
+
if private_dict.respond_to?(:family_other_blues)
|
|
307
|
+
hints[:family_other_blues] =
|
|
308
|
+
private_dict.family_other_blues
|
|
309
|
+
end
|
|
310
|
+
if private_dict.respond_to?(:blue_scale)
|
|
311
|
+
hints[:blue_scale] =
|
|
312
|
+
private_dict.blue_scale
|
|
313
|
+
end
|
|
314
|
+
if private_dict.respond_to?(:blue_shift)
|
|
315
|
+
hints[:blue_shift] =
|
|
316
|
+
private_dict.blue_shift
|
|
317
|
+
end
|
|
318
|
+
if private_dict.respond_to?(:blue_fuzz)
|
|
319
|
+
hints[:blue_fuzz] =
|
|
320
|
+
private_dict.blue_fuzz
|
|
321
|
+
end
|
|
322
|
+
if private_dict.respond_to?(:std_hw)
|
|
323
|
+
hints[:std_hw] =
|
|
324
|
+
private_dict.std_hw
|
|
325
|
+
end
|
|
326
|
+
if private_dict.respond_to?(:std_vw)
|
|
327
|
+
hints[:std_vw] =
|
|
328
|
+
private_dict.std_vw
|
|
329
|
+
end
|
|
330
|
+
if private_dict.respond_to?(:stem_snap_h)
|
|
331
|
+
hints[:stem_snap_h] =
|
|
332
|
+
private_dict.stem_snap_h
|
|
333
|
+
end
|
|
334
|
+
if private_dict.respond_to?(:stem_snap_v)
|
|
335
|
+
hints[:stem_snap_v] =
|
|
336
|
+
private_dict.stem_snap_v
|
|
337
|
+
end
|
|
338
|
+
if private_dict.respond_to?(:force_bold)
|
|
339
|
+
hints[:force_bold] =
|
|
340
|
+
private_dict.force_bold
|
|
341
|
+
end
|
|
342
|
+
if private_dict.respond_to?(:language_group)
|
|
343
|
+
hints[:language_group] =
|
|
344
|
+
private_dict.language_group
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
hints.compact
|
|
348
|
+
rescue StandardError => e
|
|
349
|
+
warn "Failed to extract Private dict hints: #{e.message}"
|
|
350
|
+
{}
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Extract per-glyph CharString hints from CFF table
|
|
354
|
+
#
|
|
355
|
+
# @param font [OpenTypeFont] OpenType font
|
|
356
|
+
# @param hint_set [Models::HintSet] Hint set to populate
|
|
357
|
+
# @return [void]
|
|
358
|
+
def extract_charstring_hints(font, hint_set)
|
|
359
|
+
return unless font.has_table?("CFF ")
|
|
360
|
+
|
|
361
|
+
cff_table = font.table("CFF ")
|
|
362
|
+
return unless cff_table
|
|
363
|
+
|
|
364
|
+
# Get CharStrings INDEX
|
|
365
|
+
charstrings_index = cff_table.charstrings_index(0)
|
|
366
|
+
return unless charstrings_index
|
|
367
|
+
|
|
368
|
+
# Iterate through all glyphs
|
|
369
|
+
glyph_count = cff_table.glyph_count(0)
|
|
370
|
+
(0...glyph_count).each do |glyph_id|
|
|
371
|
+
# Get CharString for this glyph
|
|
372
|
+
charstring = cff_table.charstring_for_glyph(glyph_id, 0)
|
|
373
|
+
next unless charstring
|
|
374
|
+
|
|
375
|
+
# Extract hints from CharString
|
|
376
|
+
hints = extract(charstring)
|
|
377
|
+
next if hints.empty?
|
|
378
|
+
|
|
379
|
+
# Store glyph hints
|
|
380
|
+
hint_set.add_glyph_hints(glyph_id, hints)
|
|
381
|
+
rescue StandardError => e
|
|
382
|
+
warn "Failed to extract hints for glyph #{glyph_id}: #{e.message}"
|
|
383
|
+
end
|
|
384
|
+
rescue StandardError => e
|
|
385
|
+
warn "Failed to extract CharString hints: #{e.message}"
|
|
386
|
+
end
|
|
252
387
|
end
|
|
253
388
|
end
|
|
254
389
|
end
|