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
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Hints
|
|
5
|
+
# Generates TrueType instruction bytecode from PostScript hint parameters
|
|
6
|
+
#
|
|
7
|
+
# This class is the inverse of TrueTypeInstructionAnalyzer - it takes
|
|
8
|
+
# PostScript hint parameters and generates equivalent TrueType prep/fpgm
|
|
9
|
+
# programs and CVT values.
|
|
10
|
+
#
|
|
11
|
+
# TrueType Instruction Opcodes:
|
|
12
|
+
# - NPUSHB (0x40): Push n bytes
|
|
13
|
+
# - NPUSHW (0x41): Push n words (16-bit)
|
|
14
|
+
# - PUSHB[n] (0xB0-0xB7): Push 1-8 bytes
|
|
15
|
+
# - PUSHW[n] (0xB8-0xBF): Push 1-8 words
|
|
16
|
+
# - SSW (0x1F): Set Single Width
|
|
17
|
+
# - SSWCI (0x1E): Set Single Width Cut-In
|
|
18
|
+
# - SCVTCI (0x1D): Set CVT Cut-In
|
|
19
|
+
# - WCVTP (0x44): Write CVT in Pixels
|
|
20
|
+
# - WCVTF (0x70): Write CVT in FUnits
|
|
21
|
+
#
|
|
22
|
+
# @example Generate TrueType programs
|
|
23
|
+
# generator = TrueTypeInstructionGenerator.new
|
|
24
|
+
# programs = generator.generate({
|
|
25
|
+
# blue_scale: 0.039625,
|
|
26
|
+
# std_hw: 80,
|
|
27
|
+
# std_vw: 90
|
|
28
|
+
# })
|
|
29
|
+
# programs[:prep] # => Binary prep program
|
|
30
|
+
# programs[:fpgm] # => Binary fpgm program (usually empty)
|
|
31
|
+
# programs[:cvt] # => Array of CVT values
|
|
32
|
+
class TrueTypeInstructionGenerator
|
|
33
|
+
# TrueType instruction opcodes
|
|
34
|
+
NPUSHB = 0x40 # Push n bytes
|
|
35
|
+
NPUSHW = 0x41 # Push n words (16-bit)
|
|
36
|
+
PUSHB_BASE = 0xB0 # PUSHB[0] through PUSHB[7]
|
|
37
|
+
PUSHW_BASE = 0xB8 # PUSHW[0] through PUSHW[7]
|
|
38
|
+
SSW = 0x1F # Set Single Width
|
|
39
|
+
SSWCI = 0x1E # Set Single Width Cut-In
|
|
40
|
+
SCVTCI = 0x1D # Set CVT Cut-In
|
|
41
|
+
WCVTP = 0x44 # Write CVT in Pixels
|
|
42
|
+
WCVTF = 0x70 # Write CVT in FUnits
|
|
43
|
+
|
|
44
|
+
# Size thresholds for instruction selection
|
|
45
|
+
MAX_PUSHB_INLINE = 8 # Maximum bytes for PUSHB[n]
|
|
46
|
+
MAX_PUSHW_INLINE = 8 # Maximum words for PUSHW[n]
|
|
47
|
+
BYTE_MAX = 255 # Maximum value for byte
|
|
48
|
+
WORD_MAX = 65535 # Maximum value for word
|
|
49
|
+
|
|
50
|
+
# Generate TrueType programs and CVT from PostScript parameters
|
|
51
|
+
#
|
|
52
|
+
# @param ps_params [Hash] PostScript hint parameters
|
|
53
|
+
# @option ps_params [Float] :blue_scale Blue scale value (0.0-1.0)
|
|
54
|
+
# @option ps_params [Integer] :std_hw Standard horizontal width
|
|
55
|
+
# @option ps_params [Integer] :std_vw Standard vertical width
|
|
56
|
+
# @option ps_params [Array<Integer>] :stem_snap_h Horizontal stem snap values
|
|
57
|
+
# @option ps_params [Array<Integer>] :stem_snap_v Vertical stem snap values
|
|
58
|
+
# @option ps_params [Array<Integer>] :blue_values Blue zone values
|
|
59
|
+
# @option ps_params [Array<Integer>] :other_blues Other blue zone values
|
|
60
|
+
# @return [Hash] Hash with :prep, :fpgm, and :cvt keys
|
|
61
|
+
def generate(ps_params)
|
|
62
|
+
# Normalize keys to symbols
|
|
63
|
+
ps_params = normalize_keys(ps_params)
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
fpgm: generate_fpgm(ps_params),
|
|
67
|
+
prep: generate_prep(ps_params),
|
|
68
|
+
cvt: generate_cvt(ps_params)
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Generate prep (Control Value Program) from PostScript parameters
|
|
73
|
+
#
|
|
74
|
+
# The prep program sets up global hint parameters:
|
|
75
|
+
# - CVT Cut-In (from blue_scale)
|
|
76
|
+
# - Single Width Cut-In (from std_hw/std_vw)
|
|
77
|
+
# - Single Width (from std_hw or std_vw)
|
|
78
|
+
#
|
|
79
|
+
# @param ps_params [Hash] PostScript parameters
|
|
80
|
+
# @return [String] Binary instruction bytes
|
|
81
|
+
def generate_prep(ps_params)
|
|
82
|
+
instructions = []
|
|
83
|
+
|
|
84
|
+
# Set CVT Cut-In from blue_scale if present
|
|
85
|
+
if ps_params[:blue_scale]
|
|
86
|
+
cvt_cut_in = calculate_cvt_cut_in(ps_params[:blue_scale])
|
|
87
|
+
instructions.concat(push_value(cvt_cut_in))
|
|
88
|
+
instructions << SCVTCI
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Set Single Width Cut-In if we have stem widths
|
|
92
|
+
if ps_params[:std_hw] || ps_params[:std_vw]
|
|
93
|
+
sw_cut_in = calculate_sw_cut_in(ps_params)
|
|
94
|
+
instructions.concat(push_value(sw_cut_in))
|
|
95
|
+
instructions << SSWCI
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Set Single Width (prefer horizontal, fall back to vertical)
|
|
99
|
+
single_width = ps_params[:std_hw] || ps_params[:std_vw]
|
|
100
|
+
if single_width
|
|
101
|
+
instructions.concat(push_value(single_width))
|
|
102
|
+
instructions << SSW
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
instructions.pack("C*")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Generate fpgm (Font Program) from PostScript parameters
|
|
109
|
+
#
|
|
110
|
+
# For converted fonts, fpgm is typically empty as font-level
|
|
111
|
+
# functions are not needed for basic hint conversion.
|
|
112
|
+
#
|
|
113
|
+
# @param _ps_params [Hash] PostScript parameters (unused)
|
|
114
|
+
# @return [String] Binary instruction bytes (empty for converted fonts)
|
|
115
|
+
def generate_fpgm(_ps_params)
|
|
116
|
+
# For converted fonts, fpgm is typically empty
|
|
117
|
+
# Advanced implementations might generate function definitions here
|
|
118
|
+
"".b
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Generate CVT (Control Value Table) from PostScript parameters
|
|
122
|
+
#
|
|
123
|
+
# CVT entries are derived from:
|
|
124
|
+
# - stem_snap_h/stem_snap_v: Stem widths
|
|
125
|
+
# - blue_values/other_blues: Alignment zones
|
|
126
|
+
# - std_hw/std_vw: Standard widths
|
|
127
|
+
#
|
|
128
|
+
# Duplicates are removed and values sorted for optimal CVT organization.
|
|
129
|
+
#
|
|
130
|
+
# @param ps_params [Hash] PostScript parameters
|
|
131
|
+
# @return [Array<Integer>] Array of 16-bit signed integers
|
|
132
|
+
def generate_cvt(ps_params)
|
|
133
|
+
cvt = []
|
|
134
|
+
|
|
135
|
+
# Add standard widths to CVT
|
|
136
|
+
cvt << ps_params[:std_hw] if ps_params[:std_hw]
|
|
137
|
+
cvt << ps_params[:std_vw] if ps_params[:std_vw]
|
|
138
|
+
|
|
139
|
+
# Add stem snap values
|
|
140
|
+
if ps_params[:stem_snap_h]
|
|
141
|
+
cvt.concat(ps_params[:stem_snap_h])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if ps_params[:stem_snap_v]
|
|
145
|
+
cvt.concat(ps_params[:stem_snap_v])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Add blue zone values (as pairs: bottom, top)
|
|
149
|
+
if ps_params[:blue_values]
|
|
150
|
+
cvt.concat(ps_params[:blue_values])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if ps_params[:other_blues]
|
|
154
|
+
cvt.concat(ps_params[:other_blues])
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Remove duplicates and sort for optimal CVT organization
|
|
158
|
+
cvt.uniq.sort
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
# Normalize hash keys to symbols
|
|
164
|
+
#
|
|
165
|
+
# @param hash [Hash] Input hash with string or symbol keys
|
|
166
|
+
# @return [Hash] Hash with symbol keys
|
|
167
|
+
def normalize_keys(hash)
|
|
168
|
+
return hash unless hash.is_a?(Hash)
|
|
169
|
+
return hash if hash.empty? || hash.keys.first.is_a?(Symbol)
|
|
170
|
+
|
|
171
|
+
hash.transform_keys(&:to_sym)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Calculate CVT Cut-In from PostScript blue_scale
|
|
175
|
+
#
|
|
176
|
+
# Blue scale controls the threshold at which alignment zones apply.
|
|
177
|
+
# We convert this to TrueType's CVT Cut-In value.
|
|
178
|
+
#
|
|
179
|
+
# @param blue_scale [Float] PostScript blue scale (0.0-1.0)
|
|
180
|
+
# @return [Integer] CVT Cut-In value in pixels
|
|
181
|
+
def calculate_cvt_cut_in(blue_scale)
|
|
182
|
+
# blue_scale of 0.039625 (common default) maps to ~17px cut-in
|
|
183
|
+
# Linear scaling: 0.039625 -> 17, 0.0 -> 0, 1.0 -> 428
|
|
184
|
+
(blue_scale * 428).round.clamp(0, 255)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Calculate Single Width Cut-In from stem widths
|
|
188
|
+
#
|
|
189
|
+
# The cut-in determines when to apply single-width rounding.
|
|
190
|
+
# We use 9 pixels as a sensible default.
|
|
191
|
+
#
|
|
192
|
+
# @param _ps_params [Hash] PostScript parameters (for future use)
|
|
193
|
+
# @return [Integer] Single Width Cut-In in pixels
|
|
194
|
+
def calculate_sw_cut_in(_ps_params)
|
|
195
|
+
9 # Standard value: 9 pixels
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Push a single value onto the TrueType stack
|
|
199
|
+
#
|
|
200
|
+
# Selects the most efficient instruction based on value size.
|
|
201
|
+
#
|
|
202
|
+
# @param value [Integer] Value to push
|
|
203
|
+
# @return [Array<Integer>] Instruction bytes
|
|
204
|
+
def push_value(value)
|
|
205
|
+
if value <= BYTE_MAX
|
|
206
|
+
push_bytes([value])
|
|
207
|
+
else
|
|
208
|
+
push_words([value])
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Push byte values using most efficient instruction
|
|
213
|
+
#
|
|
214
|
+
# Uses PUSHB[n] for 1-8 values, NPUSHB for more.
|
|
215
|
+
#
|
|
216
|
+
# @param values [Array<Integer>] Byte values (0-255)
|
|
217
|
+
# @return [Array<Integer>] Instruction bytes
|
|
218
|
+
def push_bytes(values)
|
|
219
|
+
return [] if values.empty?
|
|
220
|
+
|
|
221
|
+
# Validate all values fit in bytes
|
|
222
|
+
unless values.all? { |v| v >= 0 && v <= BYTE_MAX }
|
|
223
|
+
raise ArgumentError, "Values must be in range 0-255 for PUSHB"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
count = values.size
|
|
227
|
+
|
|
228
|
+
if count <= MAX_PUSHB_INLINE
|
|
229
|
+
# Use PUSHB[n-1] for 1-8 values
|
|
230
|
+
[PUSHB_BASE + count - 1] + values
|
|
231
|
+
else
|
|
232
|
+
# Use NPUSHB for more than 8 values
|
|
233
|
+
[NPUSHB, count] + values
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Push word values using most efficient instruction
|
|
238
|
+
#
|
|
239
|
+
# Uses PUSHW[n] for 1-8 values, NPUSHW for more.
|
|
240
|
+
# Words are encoded big-endian (high byte first).
|
|
241
|
+
#
|
|
242
|
+
# @param values [Array<Integer>] Word values (0-65535)
|
|
243
|
+
# @return [Array<Integer>] Instruction bytes
|
|
244
|
+
def push_words(values)
|
|
245
|
+
return [] if values.empty?
|
|
246
|
+
|
|
247
|
+
# Validate all values fit in words
|
|
248
|
+
unless values.all? { |v| v >= 0 && v <= WORD_MAX }
|
|
249
|
+
raise ArgumentError, "Values must be in range 0-65535 for PUSHW"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
count = values.size
|
|
253
|
+
# Convert words to big-endian byte pairs
|
|
254
|
+
word_bytes = values.flat_map { |v| [(v >> 8) & 0xFF, v & 0xFF] }
|
|
255
|
+
|
|
256
|
+
if count <= MAX_PUSHW_INLINE
|
|
257
|
+
# Use PUSHW[n-1] for 1-8 values
|
|
258
|
+
[PUSHW_BASE + count - 1] + word_bytes
|
|
259
|
+
else
|
|
260
|
+
# Use NPUSHW for more than 8 values
|
|
261
|
+
[NPUSHW, count] + word_bytes
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module Fontisan
|
|
4
6
|
# Loading modes module that defines which tables are loaded in each mode.
|
|
5
7
|
#
|
|
@@ -26,12 +28,12 @@ module Fontisan
|
|
|
26
28
|
MODES = {
|
|
27
29
|
METADATA => {
|
|
28
30
|
tables: %w[name head hhea maxp OS/2 post].freeze,
|
|
29
|
-
description: "Metadata mode - loads only identification and metrics tables (otfinfo-equivalent)"
|
|
31
|
+
description: "Metadata mode - loads only identification and metrics tables (otfinfo-equivalent)",
|
|
30
32
|
}.freeze,
|
|
31
33
|
FULL => {
|
|
32
34
|
tables: :all,
|
|
33
|
-
description: "Full mode - loads all tables in the font"
|
|
34
|
-
}.freeze
|
|
35
|
+
description: "Full mode - loads all tables in the font",
|
|
36
|
+
}.freeze,
|
|
35
37
|
}.freeze
|
|
36
38
|
|
|
37
39
|
# Pre-computed Set for O(1) lookup of metadata tables
|
|
@@ -78,7 +80,7 @@ module Fontisan
|
|
|
78
80
|
# @raise [ArgumentError] if mode is invalid
|
|
79
81
|
def self.default_lazy?(mode)
|
|
80
82
|
validate_mode!(mode)
|
|
81
|
-
true
|
|
83
|
+
true # Lazy loading is recommended for all modes
|
|
82
84
|
end
|
|
83
85
|
|
|
84
86
|
# Get mode description
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
require_relative "font_info"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Models
|
|
8
|
+
# Model for collection brief information
|
|
9
|
+
#
|
|
10
|
+
# Represents collection metadata plus brief info for each font.
|
|
11
|
+
# Used by InfoCommand in brief mode for collections.
|
|
12
|
+
#
|
|
13
|
+
# @example Creating collection brief info
|
|
14
|
+
# info = CollectionBriefInfo.new(
|
|
15
|
+
# collection_path: "fonts.ttc",
|
|
16
|
+
# num_fonts: 3,
|
|
17
|
+
# fonts: [font_info1, font_info2, font_info3]
|
|
18
|
+
# )
|
|
19
|
+
class CollectionBriefInfo < Lutaml::Model::Serializable
|
|
20
|
+
attribute :collection_path, :string
|
|
21
|
+
attribute :num_fonts, :integer
|
|
22
|
+
attribute :fonts, FontInfo, collection: true
|
|
23
|
+
|
|
24
|
+
key_value do
|
|
25
|
+
map "collection_path", to: :collection_path
|
|
26
|
+
map "num_fonts", to: :num_fonts
|
|
27
|
+
map "fonts", to: :fonts
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -36,37 +36,9 @@ module Fontisan
|
|
|
36
36
|
attribute :font_revision, :float
|
|
37
37
|
attribute :permissions, :string
|
|
38
38
|
attribute :units_per_em, :integer
|
|
39
|
+
attribute :collection_offset, :integer
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
map "font_format", to: :font_format
|
|
42
|
-
map "is_variable", to: :is_variable
|
|
43
|
-
map "family_name", to: :family_name
|
|
44
|
-
map "subfamily_name", to: :subfamily_name
|
|
45
|
-
map "full_name", to: :full_name
|
|
46
|
-
map "postscript_name", to: :postscript_name
|
|
47
|
-
map "postscript_cid_name", to: :postscript_cid_name
|
|
48
|
-
map "preferred_family", to: :preferred_family
|
|
49
|
-
map "preferred_subfamily", to: :preferred_subfamily
|
|
50
|
-
map "mac_font_menu_name", to: :mac_font_menu_name
|
|
51
|
-
map "version", to: :version
|
|
52
|
-
map "unique_id", to: :unique_id
|
|
53
|
-
map "description", to: :description
|
|
54
|
-
map "designer", to: :designer
|
|
55
|
-
map "designer_url", to: :designer_url
|
|
56
|
-
map "manufacturer", to: :manufacturer
|
|
57
|
-
map "vendor_url", to: :vendor_url
|
|
58
|
-
map "vendor_id", to: :vendor_id
|
|
59
|
-
map "trademark", to: :trademark
|
|
60
|
-
map "copyright", to: :copyright
|
|
61
|
-
map "license_description", to: :license_description
|
|
62
|
-
map "license_url", to: :license_url
|
|
63
|
-
map "sample_text", to: :sample_text
|
|
64
|
-
map "font_revision", to: :font_revision
|
|
65
|
-
map "permissions", to: :permissions
|
|
66
|
-
map "units_per_em", to: :units_per_em
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
yaml do
|
|
41
|
+
key_value do
|
|
70
42
|
map "font_format", to: :font_format
|
|
71
43
|
map "is_variable", to: :is_variable
|
|
72
44
|
map "family_name", to: :family_name
|
|
@@ -93,6 +65,7 @@ module Fontisan
|
|
|
93
65
|
map "font_revision", to: :font_revision
|
|
94
66
|
map "permissions", to: :permissions
|
|
95
67
|
map "units_per_em", to: :units_per_em
|
|
68
|
+
map "collection_offset", to: :collection_offset
|
|
96
69
|
end
|
|
97
70
|
end
|
|
98
71
|
end
|
data/lib/fontisan/models/hint.rb
CHANGED
|
@@ -1,7 +1,136 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module Fontisan
|
|
4
6
|
module Models
|
|
7
|
+
# Container for all font hint data
|
|
8
|
+
#
|
|
9
|
+
# This model holds complete hint information from a font including
|
|
10
|
+
# font-level programs, control values, and per-glyph hints. It provides
|
|
11
|
+
# a format-agnostic representation that can be converted between
|
|
12
|
+
# TrueType and PostScript formats.
|
|
13
|
+
#
|
|
14
|
+
# @example Creating a HintSet
|
|
15
|
+
# hint_set = HintSet.new(
|
|
16
|
+
# format: :truetype,
|
|
17
|
+
# font_program: fpgm_data,
|
|
18
|
+
# control_value_program: prep_data
|
|
19
|
+
# )
|
|
20
|
+
class HintSet
|
|
21
|
+
# @return [String] Hint format (:truetype or :postscript)
|
|
22
|
+
attr_accessor :format
|
|
23
|
+
|
|
24
|
+
# TrueType font-level hint data
|
|
25
|
+
# @return [String] Font program (fpgm table) - bytecode executed once
|
|
26
|
+
attr_accessor :font_program
|
|
27
|
+
|
|
28
|
+
# @return [String] Control value program (prep table) - initialization code
|
|
29
|
+
attr_accessor :control_value_program
|
|
30
|
+
|
|
31
|
+
# @return [Array<Integer>] Control values (cvt table) - metrics for hinting
|
|
32
|
+
attr_accessor :control_values
|
|
33
|
+
|
|
34
|
+
# PostScript font-level hint data
|
|
35
|
+
# @return [String] CFF Private dict hint data (BlueValues, StdHW, etc.) as JSON
|
|
36
|
+
attr_accessor :private_dict_hints
|
|
37
|
+
|
|
38
|
+
# @return [Integer] Number of glyphs with hints
|
|
39
|
+
attr_accessor :hinted_glyph_count
|
|
40
|
+
|
|
41
|
+
# @return [Boolean] Whether hints are present
|
|
42
|
+
attr_accessor :has_hints
|
|
43
|
+
|
|
44
|
+
# Initialize a new HintSet
|
|
45
|
+
#
|
|
46
|
+
# @param format [String, Symbol] Hint format (:truetype or :postscript)
|
|
47
|
+
# @param font_program [String] Font program bytecode
|
|
48
|
+
# @param control_value_program [String] Control value program bytecode
|
|
49
|
+
# @param control_values [Array<Integer>] Control values
|
|
50
|
+
# @param private_dict_hints [String] Private dict hints as JSON
|
|
51
|
+
# @param hinted_glyph_count [Integer] Number of hinted glyphs
|
|
52
|
+
# @param has_hints [Boolean] Whether hints are present
|
|
53
|
+
def initialize(format: nil, font_program: "", control_value_program: "",
|
|
54
|
+
control_values: [], private_dict_hints: "{}",
|
|
55
|
+
hinted_glyph_count: 0, has_hints: false)
|
|
56
|
+
@format = format.to_s if format
|
|
57
|
+
@font_program = font_program || ""
|
|
58
|
+
@control_value_program = control_value_program || ""
|
|
59
|
+
@control_values = control_values || []
|
|
60
|
+
@private_dict_hints = private_dict_hints || "{}"
|
|
61
|
+
@glyph_hints = "{}"
|
|
62
|
+
@hinted_glyph_count = hinted_glyph_count
|
|
63
|
+
@has_hints = has_hints
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Add hints for a specific glyph
|
|
67
|
+
#
|
|
68
|
+
# @param glyph_id [Integer, String] Glyph identifier
|
|
69
|
+
# @param hints [Array<Hint>] Hints for the glyph
|
|
70
|
+
def add_glyph_hints(glyph_id, hints)
|
|
71
|
+
return if hints.nil? || hints.empty?
|
|
72
|
+
|
|
73
|
+
glyph_hints_hash = parse_glyph_hints
|
|
74
|
+
# Convert Hint objects to hashes for storage
|
|
75
|
+
hints_data = hints.map do |h|
|
|
76
|
+
{
|
|
77
|
+
type: h.type,
|
|
78
|
+
data: h.data,
|
|
79
|
+
source_format: h.source_format,
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
glyph_hints_hash[glyph_id.to_s] = hints_data
|
|
83
|
+
@glyph_hints = glyph_hints_hash.to_json
|
|
84
|
+
@hinted_glyph_count = glyph_hints_hash.keys.length
|
|
85
|
+
@has_hints = true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Get hints for a specific glyph
|
|
89
|
+
#
|
|
90
|
+
# @param glyph_id [Integer, String] Glyph identifier
|
|
91
|
+
# @return [Array<Hint>] Hints for the glyph
|
|
92
|
+
def get_glyph_hints(glyph_id)
|
|
93
|
+
glyph_hints_hash = parse_glyph_hints
|
|
94
|
+
hints_data = glyph_hints_hash[glyph_id.to_s]
|
|
95
|
+
return [] unless hints_data
|
|
96
|
+
|
|
97
|
+
# Reconstruct Hint objects from serialized data
|
|
98
|
+
hints_data.map { |h| Hint.new(**h.transform_keys(&:to_sym)) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get all glyph IDs with hints
|
|
102
|
+
#
|
|
103
|
+
# @return [Array<String>] Glyph identifiers
|
|
104
|
+
def hinted_glyph_ids
|
|
105
|
+
parse_glyph_hints.keys
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if empty (no hints)
|
|
109
|
+
#
|
|
110
|
+
# @return [Boolean] True if no hints present
|
|
111
|
+
def empty?
|
|
112
|
+
!has_hints &&
|
|
113
|
+
(font_program.nil? || font_program.empty?) &&
|
|
114
|
+
(control_value_program.nil? || control_value_program.empty?) &&
|
|
115
|
+
(control_values.nil? || control_values.empty?) &&
|
|
116
|
+
(private_dict_hints.nil? || private_dict_hints == "{}")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# @return [String] Glyph hints as JSON
|
|
122
|
+
attr_accessor :glyph_hints
|
|
123
|
+
|
|
124
|
+
# Parse glyph hints JSON
|
|
125
|
+
def parse_glyph_hints
|
|
126
|
+
return {} if @glyph_hints.nil? || @glyph_hints.empty? || @glyph_hints == "{}"
|
|
127
|
+
|
|
128
|
+
JSON.parse(@glyph_hints)
|
|
129
|
+
rescue JSON::ParserError
|
|
130
|
+
{}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
5
134
|
# Universal hint representation supporting both TrueType and PostScript hints
|
|
6
135
|
#
|
|
7
136
|
# Hints are instructions that improve font rendering at small sizes by
|
|
@@ -60,16 +189,21 @@ module Fontisan
|
|
|
60
189
|
case type
|
|
61
190
|
when :stem
|
|
62
191
|
convert_stem_to_truetype
|
|
192
|
+
when :stem3
|
|
193
|
+
convert_stem3_to_truetype
|
|
63
194
|
when :flex
|
|
64
195
|
convert_flex_to_truetype
|
|
65
196
|
when :counter
|
|
66
197
|
convert_counter_to_truetype
|
|
198
|
+
when :hint_replacement
|
|
199
|
+
convert_hintmask_to_truetype
|
|
67
200
|
when :delta
|
|
68
201
|
# Already in TrueType format
|
|
69
202
|
data[:instructions] || []
|
|
70
203
|
when :interpolate
|
|
71
204
|
# IUP instruction
|
|
72
|
-
|
|
205
|
+
axis = data[:axis] || :y
|
|
206
|
+
axis == :x ? [0x31] : [0x30] # IUP[x] or IUP[y]
|
|
73
207
|
when :shift
|
|
74
208
|
# SHP instruction
|
|
75
209
|
data[:instructions] || []
|
|
@@ -80,6 +214,9 @@ module Fontisan
|
|
|
80
214
|
# Unknown hint type - return empty
|
|
81
215
|
[]
|
|
82
216
|
end
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
warn "Error converting hint type #{type} to TrueType: #{e.message}"
|
|
219
|
+
[]
|
|
83
220
|
end
|
|
84
221
|
|
|
85
222
|
# Convert hint to PostScript hint format
|
|
@@ -106,6 +243,9 @@ module Fontisan
|
|
|
106
243
|
# Unknown hint type
|
|
107
244
|
{}
|
|
108
245
|
end
|
|
246
|
+
rescue StandardError => e
|
|
247
|
+
warn "Error converting hint type #{type} to PostScript: #{e.message}"
|
|
248
|
+
{}
|
|
109
249
|
end
|
|
110
250
|
|
|
111
251
|
# Check if hint is compatible with target format
|
|
@@ -129,23 +269,22 @@ module Fontisan
|
|
|
129
269
|
|
|
130
270
|
# Convert stem hint to TrueType instructions
|
|
131
271
|
def convert_stem_to_truetype
|
|
132
|
-
|
|
133
|
-
|
|
272
|
+
data[:position] || 0
|
|
273
|
+
data[:width] || 0
|
|
134
274
|
orientation = data[:orientation] || :vertical
|
|
135
275
|
|
|
136
276
|
# TrueType uses MDAP (Move Direct Absolute Point) and MDRP (Move Direct Relative Point)
|
|
137
277
|
# to control stem positioning
|
|
138
278
|
instructions = []
|
|
139
279
|
|
|
140
|
-
if orientation == :vertical
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
end
|
|
280
|
+
instructions << if orientation == :vertical
|
|
281
|
+
# Vertical stem: use Y-axis instructions
|
|
282
|
+
0x2E # MDAP[rnd] - mark reference point
|
|
283
|
+
else
|
|
284
|
+
# Horizontal stem: use X-axis instructions
|
|
285
|
+
0x2F # MDAP[rnd]
|
|
286
|
+
end
|
|
287
|
+
instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
|
|
149
288
|
|
|
150
289
|
instructions
|
|
151
290
|
end
|
|
@@ -166,6 +305,38 @@ module Fontisan
|
|
|
166
305
|
[]
|
|
167
306
|
end
|
|
168
307
|
|
|
308
|
+
# Convert stem3 hint to TrueType instructions
|
|
309
|
+
def convert_stem3_to_truetype
|
|
310
|
+
stems = data[:stems] || []
|
|
311
|
+
orientation = data[:orientation] || :vertical
|
|
312
|
+
|
|
313
|
+
# Generate MDAP/MDRP pairs for each stem
|
|
314
|
+
instructions = []
|
|
315
|
+
|
|
316
|
+
stems.each do |_stem|
|
|
317
|
+
instructions << if orientation == :vertical
|
|
318
|
+
# Vertical stem: use Y-axis instructions
|
|
319
|
+
0x2E # MDAP[rnd] - mark reference point
|
|
320
|
+
else
|
|
321
|
+
# Horizontal stem: use X-axis instructions
|
|
322
|
+
0x2F # MDAP[rnd]
|
|
323
|
+
end
|
|
324
|
+
instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
instructions
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Convert hintmask hint to TrueType instructions
|
|
331
|
+
def convert_hintmask_to_truetype
|
|
332
|
+
# Hintmask controls which hints are active at runtime
|
|
333
|
+
# TrueType doesn't have a direct equivalent
|
|
334
|
+
# We can use conditional instructions, but it's complex
|
|
335
|
+
# For now, return empty and let the main stems handle hinting
|
|
336
|
+
# TODO: Implement conditional instruction generation if needed
|
|
337
|
+
[]
|
|
338
|
+
end
|
|
339
|
+
|
|
169
340
|
# Convert stem hint to PostScript operators
|
|
170
341
|
def convert_stem_to_postscript
|
|
171
342
|
position = data[:position] || 0
|
|
@@ -121,7 +121,10 @@ module Fontisan
|
|
|
121
121
|
|
|
122
122
|
# Get path from CharString
|
|
123
123
|
path = charstring.path
|
|
124
|
-
|
|
124
|
+
if path.nil? || path.empty?
|
|
125
|
+
raise ArgumentError,
|
|
126
|
+
"CharString has no path data"
|
|
127
|
+
end
|
|
125
128
|
|
|
126
129
|
commands = convert_cff_path_to_commands(path)
|
|
127
130
|
|