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
|
@@ -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
|
|
@@ -249,6 +271,84 @@ module Fontisan
|
|
|
249
271
|
source_format: :postscript
|
|
250
272
|
)
|
|
251
273
|
end
|
|
274
|
+
|
|
275
|
+
# Extract Private dict hints from CFF table
|
|
276
|
+
#
|
|
277
|
+
# Private dict contains font-level hint parameters like BlueValues,
|
|
278
|
+
# StdHW, StdVW, etc.
|
|
279
|
+
#
|
|
280
|
+
# @param font [OpenTypeFont] OpenType font
|
|
281
|
+
# @return [Hash] Private dict hint parameters
|
|
282
|
+
def extract_private_dict_hints(font)
|
|
283
|
+
hints = {}
|
|
284
|
+
|
|
285
|
+
return hints unless font.has_table?("CFF ")
|
|
286
|
+
|
|
287
|
+
cff_table = font.table("CFF ")
|
|
288
|
+
return hints unless cff_table
|
|
289
|
+
|
|
290
|
+
# Get Private DICT for first font (index 0)
|
|
291
|
+
private_dict = cff_table.private_dict(0)
|
|
292
|
+
return hints unless private_dict
|
|
293
|
+
|
|
294
|
+
# Extract hint-related parameters from Private DICT
|
|
295
|
+
# These are the key hinting parameters in CFF
|
|
296
|
+
hints[:blue_values] = private_dict.blue_values if private_dict.respond_to?(:blue_values)
|
|
297
|
+
hints[:other_blues] = private_dict.other_blues if private_dict.respond_to?(:other_blues)
|
|
298
|
+
hints[:family_blues] = private_dict.family_blues if private_dict.respond_to?(:family_blues)
|
|
299
|
+
hints[:family_other_blues] = private_dict.family_other_blues if private_dict.respond_to?(:family_other_blues)
|
|
300
|
+
hints[:blue_scale] = private_dict.blue_scale if private_dict.respond_to?(:blue_scale)
|
|
301
|
+
hints[:blue_shift] = private_dict.blue_shift if private_dict.respond_to?(:blue_shift)
|
|
302
|
+
hints[:blue_fuzz] = private_dict.blue_fuzz if private_dict.respond_to?(:blue_fuzz)
|
|
303
|
+
hints[:std_hw] = private_dict.std_hw if private_dict.respond_to?(:std_hw)
|
|
304
|
+
hints[:std_vw] = private_dict.std_vw if private_dict.respond_to?(:std_vw)
|
|
305
|
+
hints[:stem_snap_h] = private_dict.stem_snap_h if private_dict.respond_to?(:stem_snap_h)
|
|
306
|
+
hints[:stem_snap_v] = private_dict.stem_snap_v if private_dict.respond_to?(:stem_snap_v)
|
|
307
|
+
hints[:force_bold] = private_dict.force_bold if private_dict.respond_to?(:force_bold)
|
|
308
|
+
hints[:language_group] = private_dict.language_group if private_dict.respond_to?(:language_group)
|
|
309
|
+
|
|
310
|
+
hints.compact
|
|
311
|
+
rescue StandardError => e
|
|
312
|
+
warn "Failed to extract Private dict hints: #{e.message}"
|
|
313
|
+
{}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Extract per-glyph CharString hints from CFF table
|
|
317
|
+
#
|
|
318
|
+
# @param font [OpenTypeFont] OpenType font
|
|
319
|
+
# @param hint_set [Models::HintSet] Hint set to populate
|
|
320
|
+
# @return [void]
|
|
321
|
+
def extract_charstring_hints(font, hint_set)
|
|
322
|
+
return unless font.has_table?("CFF ")
|
|
323
|
+
|
|
324
|
+
cff_table = font.table("CFF ")
|
|
325
|
+
return unless cff_table
|
|
326
|
+
|
|
327
|
+
# Get CharStrings INDEX
|
|
328
|
+
charstrings_index = cff_table.charstrings_index(0)
|
|
329
|
+
return unless charstrings_index
|
|
330
|
+
|
|
331
|
+
# Iterate through all glyphs
|
|
332
|
+
glyph_count = cff_table.glyph_count(0)
|
|
333
|
+
(0...glyph_count).each do |glyph_id|
|
|
334
|
+
begin
|
|
335
|
+
# Get CharString for this glyph
|
|
336
|
+
charstring = cff_table.charstring_for_glyph(glyph_id, 0)
|
|
337
|
+
next unless charstring
|
|
338
|
+
|
|
339
|
+
# Extract hints from CharString
|
|
340
|
+
hints = extract(charstring)
|
|
341
|
+
next if hints.empty?
|
|
342
|
+
|
|
343
|
+
# Store glyph hints
|
|
344
|
+
hint_set.add_glyph_hints(glyph_id, hints)
|
|
345
|
+
rescue StandardError => e
|
|
346
|
+
warn "Failed to extract hints for glyph #{glyph_id}: #{e.message}"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
rescue StandardError => e
|
|
350
|
+
warn "Failed to extract CharString hints: #{e.message}"
|
|
351
|
+
end
|
|
252
352
|
end
|
|
253
353
|
end
|
|
254
354
|
end
|
|
@@ -1,70 +1,116 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../models/hint"
|
|
4
|
-
|
|
5
3
|
module Fontisan
|
|
6
4
|
module Hints
|
|
7
|
-
# Applies rendering hints to TrueType
|
|
5
|
+
# Applies rendering hints to TrueType font tables
|
|
6
|
+
#
|
|
7
|
+
# This applier writes TrueType hint data into font-level tables:
|
|
8
|
+
# - fpgm (Font Program) - bytecode executed once at font initialization
|
|
9
|
+
# - prep (Control Value Program) - bytecode for glyph preparation
|
|
10
|
+
# - cvt (Control Values) - array of 16-bit values for hinting metrics
|
|
8
11
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# instruction sequencing and maintains compatibility with TrueType
|
|
12
|
-
# instruction execution model.
|
|
12
|
+
# The applier ensures proper table structure with correct checksums
|
|
13
|
+
# and does not corrupt the font if hint application fails.
|
|
13
14
|
#
|
|
14
|
-
# @example Apply hints
|
|
15
|
+
# @example Apply hints from a HintSet
|
|
15
16
|
# applier = TrueTypeHintApplier.new
|
|
16
|
-
#
|
|
17
|
+
# tables = {}
|
|
18
|
+
# updated_tables = applier.apply(hint_set, tables)
|
|
17
19
|
class TrueTypeHintApplier
|
|
18
|
-
# Apply hints to
|
|
20
|
+
# Apply TrueType hints to font tables
|
|
19
21
|
#
|
|
20
|
-
# @param
|
|
21
|
-
# @param
|
|
22
|
-
# @return [
|
|
23
|
-
def apply(
|
|
24
|
-
return
|
|
25
|
-
return
|
|
22
|
+
# @param hint_set [HintSet] Hint data to apply
|
|
23
|
+
# @param tables [Hash] Font tables to update
|
|
24
|
+
# @return [Hash] Updated font tables
|
|
25
|
+
def apply(hint_set, tables)
|
|
26
|
+
return tables if hint_set.nil? || hint_set.empty?
|
|
27
|
+
return tables unless hint_set.format == "truetype"
|
|
26
28
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
+
# Write fpgm table if present
|
|
30
|
+
if hint_set.font_program && !hint_set.font_program.empty?
|
|
31
|
+
tables["fpgm"] = build_fpgm_table(hint_set.font_program)
|
|
32
|
+
end
|
|
29
33
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# 3. Update glyph instruction data
|
|
34
|
+
# Write prep table if present
|
|
35
|
+
if hint_set.control_value_program && !hint_set.control_value_program.empty?
|
|
36
|
+
tables["prep"] = build_prep_table(hint_set.control_value_program)
|
|
37
|
+
end
|
|
35
38
|
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
# Write cvt table if present
|
|
40
|
+
if hint_set.control_values && !hint_set.control_values.empty?
|
|
41
|
+
tables["cvt "] = build_cvt_table(hint_set.control_values)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Future enhancement: Apply per-glyph hints to glyf table
|
|
45
|
+
# For now, font-level tables only
|
|
46
|
+
|
|
47
|
+
tables
|
|
40
48
|
end
|
|
41
49
|
|
|
42
50
|
private
|
|
43
51
|
|
|
44
|
-
# Build
|
|
52
|
+
# Build fpgm (Font Program) table
|
|
45
53
|
#
|
|
46
|
-
# @param
|
|
47
|
-
# @return [
|
|
48
|
-
def
|
|
49
|
-
|
|
54
|
+
# @param program_data [String] Raw bytecode
|
|
55
|
+
# @return [Hash] Table structure with tag, data, and checksum
|
|
56
|
+
def build_fpgm_table(program_data)
|
|
57
|
+
{
|
|
58
|
+
tag: "fpgm",
|
|
59
|
+
data: program_data,
|
|
60
|
+
checksum: calculate_checksum(program_data),
|
|
61
|
+
}
|
|
62
|
+
end
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
# Build prep (Control Value Program) table
|
|
65
|
+
#
|
|
66
|
+
# @param program_data [String] Raw bytecode
|
|
67
|
+
# @return [Hash] Table structure with tag, data, and checksum
|
|
68
|
+
def build_prep_table(program_data)
|
|
69
|
+
{
|
|
70
|
+
tag: "prep",
|
|
71
|
+
data: program_data,
|
|
72
|
+
checksum: calculate_checksum(program_data),
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Build cvt (Control Values) table
|
|
77
|
+
#
|
|
78
|
+
# CVT values are 16-bit signed integers (FWORD) in big-endian format.
|
|
79
|
+
# Each value represents a design-space coordinate used for hinting.
|
|
80
|
+
#
|
|
81
|
+
# @param control_values [Array<Integer>] Array of 16-bit signed values
|
|
82
|
+
# @return [Hash] Table structure with tag, data, and checksum
|
|
83
|
+
def build_cvt_table(control_values)
|
|
84
|
+
# Pack as 16-bit big-endian signed integers (s> = signed big-endian)
|
|
85
|
+
data = control_values.pack("s>*")
|
|
55
86
|
|
|
56
|
-
|
|
87
|
+
{
|
|
88
|
+
tag: "cvt ",
|
|
89
|
+
data: data,
|
|
90
|
+
checksum: calculate_checksum(data),
|
|
91
|
+
}
|
|
57
92
|
end
|
|
58
93
|
|
|
59
|
-
#
|
|
94
|
+
# Calculate OpenType table checksum
|
|
95
|
+
#
|
|
96
|
+
# OpenType spec requires tables to be checksummed as 32-bit unsigned
|
|
97
|
+
# integers in big-endian format. The table is padded to a multiple of
|
|
98
|
+
# 4 bytes with zeros before checksum calculation.
|
|
60
99
|
#
|
|
61
|
-
# @param
|
|
62
|
-
# @return [
|
|
63
|
-
def
|
|
64
|
-
|
|
100
|
+
# @param data [String] Table data
|
|
101
|
+
# @return [Integer] 32-bit checksum
|
|
102
|
+
def calculate_checksum(data)
|
|
103
|
+
# Pad to 4-byte boundary with zeros
|
|
104
|
+
padding_needed = (4 - data.length % 4) % 4
|
|
105
|
+
padded = data + ("\x00" * padding_needed)
|
|
106
|
+
|
|
107
|
+
# Sum as 32-bit unsigned integers in big-endian
|
|
108
|
+
checksum = 0
|
|
109
|
+
(0...padded.length).step(4) do |i|
|
|
110
|
+
checksum = (checksum + padded[i, 4].unpack1("N")) & 0xFFFFFFFF
|
|
111
|
+
end
|
|
65
112
|
|
|
66
|
-
|
|
67
|
-
instructions.all? { |byte| byte >= 0 && byte <= 255 }
|
|
113
|
+
checksum
|
|
68
114
|
end
|
|
69
115
|
end
|
|
70
116
|
end
|
|
@@ -35,6 +35,30 @@ module Fontisan
|
|
|
35
35
|
DELTAP2 = 0x71
|
|
36
36
|
DELTAP3 = 0x72
|
|
37
37
|
|
|
38
|
+
# Extract complete hint data from TrueType font
|
|
39
|
+
#
|
|
40
|
+
# This extracts both font-level hints (fpgm, prep, cvt tables) and
|
|
41
|
+
# per-glyph hints from glyph instructions.
|
|
42
|
+
#
|
|
43
|
+
# @param font [TrueTypeFont] TrueType font object
|
|
44
|
+
# @return [Models::HintSet] Complete hint set
|
|
45
|
+
def extract_from_font(font)
|
|
46
|
+
hint_set = Models::HintSet.new(format: "truetype")
|
|
47
|
+
|
|
48
|
+
# Extract font-level programs
|
|
49
|
+
hint_set.font_program = extract_font_program(font)
|
|
50
|
+
hint_set.control_value_program = extract_control_value_program(font)
|
|
51
|
+
hint_set.control_values = extract_control_values(font)
|
|
52
|
+
|
|
53
|
+
# Extract per-glyph hints
|
|
54
|
+
extract_glyph_hints(font, hint_set)
|
|
55
|
+
|
|
56
|
+
# Update metadata
|
|
57
|
+
hint_set.has_hints = !hint_set.empty?
|
|
58
|
+
|
|
59
|
+
hint_set
|
|
60
|
+
end
|
|
61
|
+
|
|
38
62
|
# Extract hints from TrueType glyph
|
|
39
63
|
#
|
|
40
64
|
# @param glyph [Glyph] TrueType glyph with instructions
|
|
@@ -157,6 +181,109 @@ module Fontisan
|
|
|
157
181
|
nil
|
|
158
182
|
end
|
|
159
183
|
end
|
|
184
|
+
|
|
185
|
+
# Extract font program (fpgm table)
|
|
186
|
+
#
|
|
187
|
+
# @param font [TrueTypeFont] TrueType font
|
|
188
|
+
# @return [String] Font program bytecode (binary string)
|
|
189
|
+
def extract_font_program(font)
|
|
190
|
+
return "" unless font.has_table?("fpgm")
|
|
191
|
+
|
|
192
|
+
font_program_data = font.instance_variable_get(:@table_data)["fpgm"]
|
|
193
|
+
return "" unless font_program_data
|
|
194
|
+
|
|
195
|
+
# Return as binary string
|
|
196
|
+
font_program_data.force_encoding("ASCII-8BIT")
|
|
197
|
+
rescue StandardError => e
|
|
198
|
+
warn "Failed to extract font program: #{e.message}"
|
|
199
|
+
""
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Extract control value program (prep table)
|
|
203
|
+
#
|
|
204
|
+
# @param font [TrueTypeFont] TrueType font
|
|
205
|
+
# @return [String] Control value program bytecode (binary string)
|
|
206
|
+
def extract_control_value_program(font)
|
|
207
|
+
return "" unless font.has_table?("prep")
|
|
208
|
+
|
|
209
|
+
prep_data = font.instance_variable_get(:@table_data)["prep"]
|
|
210
|
+
return "" unless prep_data
|
|
211
|
+
|
|
212
|
+
# Return as binary string
|
|
213
|
+
prep_data.force_encoding("ASCII-8BIT")
|
|
214
|
+
rescue StandardError => e
|
|
215
|
+
warn "Failed to extract control value program: #{e.message}"
|
|
216
|
+
""
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Extract control values (cvt table)
|
|
220
|
+
#
|
|
221
|
+
# @param font [TrueTypeFont] TrueType font
|
|
222
|
+
# @return [Array<Integer>] Control values
|
|
223
|
+
def extract_control_values(font)
|
|
224
|
+
return [] unless font.has_table?("cvt ")
|
|
225
|
+
|
|
226
|
+
cvt_data = font.instance_variable_get(:@table_data)["cvt "]
|
|
227
|
+
return [] unless cvt_data
|
|
228
|
+
|
|
229
|
+
# CVT table is an array of 16-bit signed integers (FWord values)
|
|
230
|
+
values = []
|
|
231
|
+
io = StringIO.new(cvt_data)
|
|
232
|
+
while !io.eof?
|
|
233
|
+
# Read 16-bit big-endian signed integer
|
|
234
|
+
bytes = io.read(2)
|
|
235
|
+
break unless bytes&.length == 2
|
|
236
|
+
|
|
237
|
+
value = bytes.unpack1("n") # Unsigned short
|
|
238
|
+
# Convert to signed
|
|
239
|
+
value = value - 65536 if value > 32767
|
|
240
|
+
values << value
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
values
|
|
244
|
+
rescue StandardError => e
|
|
245
|
+
warn "Failed to extract control values: #{e.message}"
|
|
246
|
+
[]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Extract per-glyph hints from glyf table
|
|
250
|
+
#
|
|
251
|
+
# @param font [TrueTypeFont] TrueType font
|
|
252
|
+
# @param hint_set [Models::HintSet] Hint set to populate
|
|
253
|
+
# @return [void]
|
|
254
|
+
def extract_glyph_hints(font, hint_set)
|
|
255
|
+
return unless font.has_table?("glyf")
|
|
256
|
+
|
|
257
|
+
glyf_table = font.table("glyf")
|
|
258
|
+
return unless glyf_table
|
|
259
|
+
|
|
260
|
+
# Get number of glyphs from maxp table
|
|
261
|
+
maxp_table = font.table("maxp")
|
|
262
|
+
return unless maxp_table
|
|
263
|
+
|
|
264
|
+
num_glyphs = maxp_table.num_glyphs
|
|
265
|
+
|
|
266
|
+
# Iterate through all glyphs
|
|
267
|
+
(0...num_glyphs).each do |glyph_id|
|
|
268
|
+
begin
|
|
269
|
+
glyph = glyf_table.glyph_for(glyph_id)
|
|
270
|
+
next unless glyph
|
|
271
|
+
next if glyph.number_of_contours <= 0 # Skip compound glyphs and empty glyphs
|
|
272
|
+
|
|
273
|
+
# Extract hints from simple glyph instructions
|
|
274
|
+
hints = extract(glyph)
|
|
275
|
+
next if hints.empty?
|
|
276
|
+
|
|
277
|
+
# Store glyph hints
|
|
278
|
+
hint_set.add_glyph_hints(glyph_id, hints)
|
|
279
|
+
rescue StandardError => e
|
|
280
|
+
# Skip glyphs that fail to parse
|
|
281
|
+
next
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
rescue StandardError => e
|
|
285
|
+
warn "Failed to extract glyph hints: #{e.message}"
|
|
286
|
+
end
|
|
160
287
|
end
|
|
161
288
|
end
|
|
162
289
|
end
|
|
@@ -71,8 +71,8 @@ module Fontisan
|
|
|
71
71
|
attribute :tag, :string
|
|
72
72
|
attribute :checksum, :string
|
|
73
73
|
attribute :parsed, :boolean, default: -> { false }
|
|
74
|
-
attribute :data, :string, default: -> {}
|
|
75
|
-
attribute :fields, :string, default: -> {}
|
|
74
|
+
attribute :data, :string, default: -> { nil }
|
|
75
|
+
attribute :fields, :string, default: -> { nil }
|
|
76
76
|
|
|
77
77
|
yaml do
|
|
78
78
|
map "tag", to: :tag
|
data/lib/fontisan/models/hint.rb
CHANGED
|
@@ -1,7 +1,135 @@
|
|
|
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
|
+
JSON.parse(@glyph_hints)
|
|
128
|
+
rescue JSON::ParserError
|
|
129
|
+
{}
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
5
133
|
# Universal hint representation supporting both TrueType and PostScript hints
|
|
6
134
|
#
|
|
7
135
|
# Hints are instructions that improve font rendering at small sizes by
|
|
@@ -60,16 +188,21 @@ module Fontisan
|
|
|
60
188
|
case type
|
|
61
189
|
when :stem
|
|
62
190
|
convert_stem_to_truetype
|
|
191
|
+
when :stem3
|
|
192
|
+
convert_stem3_to_truetype
|
|
63
193
|
when :flex
|
|
64
194
|
convert_flex_to_truetype
|
|
65
195
|
when :counter
|
|
66
196
|
convert_counter_to_truetype
|
|
197
|
+
when :hint_replacement
|
|
198
|
+
convert_hintmask_to_truetype
|
|
67
199
|
when :delta
|
|
68
200
|
# Already in TrueType format
|
|
69
201
|
data[:instructions] || []
|
|
70
202
|
when :interpolate
|
|
71
203
|
# IUP instruction
|
|
72
|
-
|
|
204
|
+
axis = data[:axis] || :y
|
|
205
|
+
axis == :x ? [0x31] : [0x30] # IUP[x] or IUP[y]
|
|
73
206
|
when :shift
|
|
74
207
|
# SHP instruction
|
|
75
208
|
data[:instructions] || []
|
|
@@ -80,6 +213,9 @@ module Fontisan
|
|
|
80
213
|
# Unknown hint type - return empty
|
|
81
214
|
[]
|
|
82
215
|
end
|
|
216
|
+
rescue StandardError => e
|
|
217
|
+
warn "Error converting hint type #{type} to TrueType: #{e.message}"
|
|
218
|
+
[]
|
|
83
219
|
end
|
|
84
220
|
|
|
85
221
|
# Convert hint to PostScript hint format
|
|
@@ -106,6 +242,9 @@ module Fontisan
|
|
|
106
242
|
# Unknown hint type
|
|
107
243
|
{}
|
|
108
244
|
end
|
|
245
|
+
rescue StandardError => e
|
|
246
|
+
warn "Error converting hint type #{type} to PostScript: #{e.message}"
|
|
247
|
+
{}
|
|
109
248
|
end
|
|
110
249
|
|
|
111
250
|
# Check if hint is compatible with target format
|
|
@@ -166,6 +305,39 @@ 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
|
+
if orientation == :vertical
|
|
318
|
+
# Vertical stem: use Y-axis instructions
|
|
319
|
+
instructions << 0x2E # MDAP[rnd] - mark reference point
|
|
320
|
+
instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
|
|
321
|
+
else
|
|
322
|
+
# Horizontal stem: use X-axis instructions
|
|
323
|
+
instructions << 0x2F # MDAP[rnd]
|
|
324
|
+
instructions << 0xC0 # MDRP[min,rnd,black]
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
instructions
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Convert hintmask hint to TrueType instructions
|
|
332
|
+
def convert_hintmask_to_truetype
|
|
333
|
+
# Hintmask controls which hints are active at runtime
|
|
334
|
+
# TrueType doesn't have a direct equivalent
|
|
335
|
+
# We can use conditional instructions, but it's complex
|
|
336
|
+
# For now, return empty and let the main stems handle hinting
|
|
337
|
+
# TODO: Implement conditional instruction generation if needed
|
|
338
|
+
[]
|
|
339
|
+
end
|
|
340
|
+
|
|
169
341
|
# Convert stem hint to PostScript operators
|
|
170
342
|
def convert_stem_to_postscript
|
|
171
343
|
position = data[:position] || 0
|