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
data/lib/fontisan/font_writer.rb
CHANGED
|
@@ -263,17 +263,18 @@ module Fontisan
|
|
|
263
263
|
head_entry = table_entries.find { |e| e[:tag] == "head" }
|
|
264
264
|
return unless head_entry
|
|
265
265
|
|
|
266
|
-
#
|
|
267
|
-
#
|
|
266
|
+
# Zero out checksumAdjustment field (offset 8 in head table) before calculating
|
|
267
|
+
# This ensures we calculate the correct checksum regardless of source font's value
|
|
268
|
+
head_offset = head_entry[:offset]
|
|
269
|
+
checksum_offset = head_offset + 8
|
|
270
|
+
font_data[checksum_offset, 4] = "\x00\x00\x00\x00"
|
|
271
|
+
|
|
272
|
+
# Calculate font checksum (with head checksumAdjustment zeroed)
|
|
268
273
|
font_checksum = calculate_font_checksum(font_data)
|
|
269
274
|
|
|
270
275
|
# Calculate adjustment
|
|
271
276
|
adjustment = (Constants::CHECKSUM_ADJUSTMENT_MAGIC - font_checksum) & 0xFFFFFFFF
|
|
272
277
|
|
|
273
|
-
# Update head table checksumAdjustment field (offset 8 in head table)
|
|
274
|
-
head_offset = head_entry[:offset]
|
|
275
|
-
checksum_offset = head_offset + 8
|
|
276
|
-
|
|
277
278
|
# Write adjustment as uint32 big-endian
|
|
278
279
|
font_data[checksum_offset, 4] = [adjustment].pack("N")
|
|
279
280
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
3
4
|
require_relative "../models/hint"
|
|
4
5
|
|
|
5
6
|
module Fontisan
|
|
@@ -67,6 +68,59 @@ module Fontisan
|
|
|
67
68
|
remove_conflicts(unique_hints)
|
|
68
69
|
end
|
|
69
70
|
|
|
71
|
+
# Convert entire HintSet between formats
|
|
72
|
+
#
|
|
73
|
+
# @param hint_set [Models::HintSet] Source hint set
|
|
74
|
+
# @param target_format [Symbol] Target format (:truetype or :postscript)
|
|
75
|
+
# @return [Models::HintSet] Converted hint set
|
|
76
|
+
def convert_hint_set(hint_set, target_format)
|
|
77
|
+
return hint_set if hint_set.format == target_format.to_s
|
|
78
|
+
|
|
79
|
+
result = Models::HintSet.new(format: target_format.to_s)
|
|
80
|
+
|
|
81
|
+
case target_format
|
|
82
|
+
when :postscript
|
|
83
|
+
# Convert font-level TT → PS
|
|
84
|
+
if hint_set.font_program || hint_set.control_value_program ||
|
|
85
|
+
hint_set.control_values&.any?
|
|
86
|
+
ps_dict = convert_tt_programs_to_ps_dict(
|
|
87
|
+
hint_set.font_program,
|
|
88
|
+
hint_set.control_value_program,
|
|
89
|
+
hint_set.control_values
|
|
90
|
+
)
|
|
91
|
+
result.private_dict_hints = ps_dict.to_json
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Convert per-glyph hints
|
|
95
|
+
hint_set.hinted_glyph_ids.each do |glyph_id|
|
|
96
|
+
glyph_hints = hint_set.get_glyph_hints(glyph_id)
|
|
97
|
+
ps_hints = to_postscript(glyph_hints)
|
|
98
|
+
result.add_glyph_hints(glyph_id, ps_hints) unless ps_hints.empty?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
when :truetype
|
|
102
|
+
# Convert font-level PS → TT
|
|
103
|
+
if hint_set.private_dict_hints && hint_set.private_dict_hints != "{}"
|
|
104
|
+
tt_programs = convert_ps_dict_to_tt_programs(
|
|
105
|
+
JSON.parse(hint_set.private_dict_hints)
|
|
106
|
+
)
|
|
107
|
+
result.font_program = tt_programs[:fpgm]
|
|
108
|
+
result.control_value_program = tt_programs[:prep]
|
|
109
|
+
result.control_values = tt_programs[:cvt]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Convert per-glyph hints
|
|
113
|
+
hint_set.hinted_glyph_ids.each do |glyph_id|
|
|
114
|
+
glyph_hints = hint_set.get_glyph_hints(glyph_id)
|
|
115
|
+
tt_hints = to_truetype(glyph_hints)
|
|
116
|
+
result.add_glyph_hints(glyph_id, tt_hints) unless tt_hints.empty?
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
result.has_hints = !result.empty?
|
|
121
|
+
result
|
|
122
|
+
end
|
|
123
|
+
|
|
70
124
|
private
|
|
71
125
|
|
|
72
126
|
# Convert a single hint to PostScript format
|
|
@@ -172,6 +226,85 @@ module Fontisan
|
|
|
172
226
|
|
|
173
227
|
pos1 < end2 && pos2 < end1
|
|
174
228
|
end
|
|
229
|
+
|
|
230
|
+
# Convert TrueType font programs to PostScript Private dict
|
|
231
|
+
#
|
|
232
|
+
# Analyzes TrueType fpgm, prep, and cvt to extract semantic intent
|
|
233
|
+
# and generate corresponding PostScript hint parameters.
|
|
234
|
+
#
|
|
235
|
+
# @param fpgm [String] Font program bytecode
|
|
236
|
+
# @param prep [String] Control value program bytecode
|
|
237
|
+
# @param cvt [Array<Integer>] Control values
|
|
238
|
+
# @return [Hash] PostScript Private dict hint parameters
|
|
239
|
+
def convert_tt_programs_to_ps_dict(fpgm, prep, cvt)
|
|
240
|
+
hints = {}
|
|
241
|
+
|
|
242
|
+
# Extract stem widths from cvt if present
|
|
243
|
+
# CVT values typically contain standard widths
|
|
244
|
+
if cvt && !cvt.empty?
|
|
245
|
+
# First CVT value often represents standard horizontal stem
|
|
246
|
+
hints[:std_hw] = cvt[0].abs if cvt.length > 0
|
|
247
|
+
# Second CVT value often represents standard vertical stem
|
|
248
|
+
hints[:std_vw] = cvt[1].abs if cvt.length > 1
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Analyze control value program for alignment zones
|
|
252
|
+
# TrueType doesn't have exact Blue zones, so we use defaults
|
|
253
|
+
# These are standard values that work for most Latin fonts
|
|
254
|
+
hints[:blue_values] = [-20, 0, 706, 726]
|
|
255
|
+
|
|
256
|
+
# Optional: Add other_blues for descenders if we detect them
|
|
257
|
+
# This would require analyzing prep program, which is complex
|
|
258
|
+
# For now, use conservative defaults
|
|
259
|
+
|
|
260
|
+
hints
|
|
261
|
+
rescue StandardError => e
|
|
262
|
+
warn "Error converting TT programs to PS dict: #{e.message}"
|
|
263
|
+
{}
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Convert PostScript Private dict to TrueType font programs
|
|
267
|
+
#
|
|
268
|
+
# Generates TrueType control values and programs from PostScript
|
|
269
|
+
# hint parameters.
|
|
270
|
+
#
|
|
271
|
+
# @param ps_dict [Hash] PostScript Private dict parameters
|
|
272
|
+
# @return [Hash] TrueType programs ({ fpgm:, prep:, cvt: })
|
|
273
|
+
def convert_ps_dict_to_tt_programs(ps_dict)
|
|
274
|
+
# Handle both string and symbol keys from JSON
|
|
275
|
+
ps_dict = ps_dict.transform_keys(&:to_sym) if ps_dict.keys.first.is_a?(String)
|
|
276
|
+
|
|
277
|
+
# Generate control values from PS parameters
|
|
278
|
+
cvt = []
|
|
279
|
+
|
|
280
|
+
# Add standard stem widths as CVT values
|
|
281
|
+
cvt << ps_dict[:std_hw] if ps_dict[:std_hw]
|
|
282
|
+
cvt << ps_dict[:std_vw] if ps_dict[:std_vw]
|
|
283
|
+
|
|
284
|
+
# Add stem snap values if present
|
|
285
|
+
if ps_dict[:stem_snap_h]&.is_a?(Array)
|
|
286
|
+
cvt.concat(ps_dict[:stem_snap_h])
|
|
287
|
+
end
|
|
288
|
+
if ps_dict[:stem_snap_v]&.is_a?(Array)
|
|
289
|
+
cvt.concat(ps_dict[:stem_snap_v])
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Remove duplicates and sort
|
|
293
|
+
cvt = cvt.uniq.sort
|
|
294
|
+
|
|
295
|
+
# Generate basic prep program (empty for converted fonts)
|
|
296
|
+
# A real implementation would generate instructions to set up CVT
|
|
297
|
+
prep = ""
|
|
298
|
+
|
|
299
|
+
# fpgm typically empty for converted fonts
|
|
300
|
+
# Functions would need to be synthesized from scratch
|
|
301
|
+
fpgm = ""
|
|
302
|
+
|
|
303
|
+
{ fpgm: fpgm, prep: prep, cvt: cvt }
|
|
304
|
+
rescue StandardError => e
|
|
305
|
+
warn "Error converting PS dict to TT programs: #{e.message}"
|
|
306
|
+
{ fpgm: "", prep: "", cvt: [] }
|
|
307
|
+
end
|
|
175
308
|
end
|
|
176
309
|
end
|
|
177
310
|
end
|
|
@@ -1,184 +1,265 @@
|
|
|
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
|
+
else
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
new_cff_data = Tables::Cff::TableBuilder.rebuild(
|
|
155
|
+
cff_table,
|
|
156
|
+
private_dict_hints: hint_params,
|
|
157
|
+
per_glyph_hints: per_glyph_hints
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
tables["CFF "] = new_cff_data
|
|
161
|
+
tables
|
|
162
|
+
rescue StandardError => e
|
|
163
|
+
warn "Failed to apply PostScript hints: #{e.message}"
|
|
164
|
+
tables
|
|
165
|
+
end
|
|
102
166
|
end
|
|
103
167
|
|
|
104
|
-
#
|
|
168
|
+
# Parse hint parameters from HintSet
|
|
105
169
|
#
|
|
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
|
|
170
|
+
# @param hint_set [HintSet] Hint set with Private dict hints
|
|
171
|
+
# @return [Hash, nil] Parsed hint parameters, or nil if invalid
|
|
172
|
+
def parse_hint_parameters(hint_set)
|
|
173
|
+
return nil unless hint_set.private_dict_hints
|
|
174
|
+
return nil if hint_set.private_dict_hints == "{}"
|
|
175
|
+
|
|
176
|
+
JSON.parse(hint_set.private_dict_hints)
|
|
177
|
+
rescue JSON::ParserError => e
|
|
178
|
+
warn "Failed to parse Private dict hints: #{e.message}"
|
|
179
|
+
nil
|
|
126
180
|
end
|
|
127
181
|
|
|
128
|
-
#
|
|
182
|
+
# Validate hint parameters against CFF specification limits
|
|
129
183
|
#
|
|
130
|
-
# @param
|
|
131
|
-
# @return [
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
-
|
|
184
|
+
# @param params [Hash] Hint parameters
|
|
185
|
+
# @return [Boolean] True if all parameters are valid
|
|
186
|
+
def valid_hint_parameters?(params)
|
|
187
|
+
# Validate blue values (must be pairs, max 7 pairs = 14 values)
|
|
188
|
+
if params["blue_values"] || params[:blue_values]
|
|
189
|
+
values = params["blue_values"] || params[:blue_values]
|
|
190
|
+
return false unless values.is_a?(Array)
|
|
191
|
+
return false if values.length > 14 # Max 7 pairs
|
|
192
|
+
return false if values.length.odd? # Must be pairs
|
|
193
|
+
end
|
|
135
194
|
|
|
136
|
-
#
|
|
137
|
-
|
|
195
|
+
# Validate other_blues (max 5 pairs = 10 values)
|
|
196
|
+
if params["other_blues"] || params[:other_blues]
|
|
197
|
+
values = params["other_blues"] || params[:other_blues]
|
|
198
|
+
return false unless values.is_a?(Array)
|
|
199
|
+
return false if values.length > 10
|
|
200
|
+
return false if values.length.odd?
|
|
201
|
+
end
|
|
138
202
|
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
203
|
+
# Validate stem widths (single values)
|
|
204
|
+
if params["std_hw"] || params[:std_hw]
|
|
205
|
+
value = params["std_hw"] || params[:std_hw]
|
|
206
|
+
return false unless value.is_a?(Numeric)
|
|
207
|
+
return false if value.negative?
|
|
208
|
+
end
|
|
142
209
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
210
|
+
if params["std_vw"] || params[:std_vw]
|
|
211
|
+
value = params["std_vw"] || params[:std_vw]
|
|
212
|
+
return false unless value.is_a?(Numeric)
|
|
213
|
+
return false if value.negative?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Validate stem snaps (arrays, max 12 values each)
|
|
217
|
+
%w[stem_snap_h stem_snap_v].each do |key|
|
|
218
|
+
next unless params[key] || params[key.to_sym]
|
|
219
|
+
|
|
220
|
+
values = params[key] || params[key.to_sym]
|
|
221
|
+
return false unless values.is_a?(Array)
|
|
222
|
+
return false if values.length > 12
|
|
151
223
|
end
|
|
152
224
|
|
|
153
|
-
#
|
|
154
|
-
if
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return
|
|
225
|
+
# Validate blue_scale (should be positive)
|
|
226
|
+
if params["blue_scale"] || params[:blue_scale]
|
|
227
|
+
value = params["blue_scale"] || params[:blue_scale]
|
|
228
|
+
return false unless value.is_a?(Numeric)
|
|
229
|
+
return false if value <= 0
|
|
158
230
|
end
|
|
159
231
|
|
|
160
|
-
#
|
|
161
|
-
if
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return [b0, b1].pack("C*")
|
|
232
|
+
# Validate language_group (0 or 1 only)
|
|
233
|
+
if params["language_group"] || params[:language_group]
|
|
234
|
+
value = params["language_group"] || params[:language_group]
|
|
235
|
+
return false unless [0, 1].include?(value)
|
|
165
236
|
end
|
|
166
237
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
238
|
+
true
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Extract specific hint parameter with symbol/string key support
|
|
242
|
+
#
|
|
243
|
+
# @param params [Hash] Hint parameters
|
|
244
|
+
# @param key [String] Parameter name
|
|
245
|
+
# @return [Object, nil] Parameter value
|
|
246
|
+
def extract_param(params, key)
|
|
247
|
+
params[key] || params[key.to_sym]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Extract per-glyph hint data from HintSet
|
|
251
|
+
#
|
|
252
|
+
# @param hint_set [HintSet] Hint set with per-glyph hints
|
|
253
|
+
# @return [Hash] Hash mapping glyph_id => Array<Hint>
|
|
254
|
+
def extract_per_glyph_hints(hint_set)
|
|
255
|
+
per_glyph = {}
|
|
256
|
+
|
|
257
|
+
hint_set.hinted_glyph_ids.each do |glyph_id|
|
|
258
|
+
hints = hint_set.get_glyph_hints(glyph_id)
|
|
259
|
+
per_glyph[glyph_id.to_i] = hints unless hints.empty?
|
|
171
260
|
end
|
|
172
261
|
|
|
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*")
|
|
262
|
+
per_glyph
|
|
182
263
|
end
|
|
183
264
|
end
|
|
184
265
|
end
|