fontisan 0.1.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 +672 -69
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1477 -297
- data/Rakefile +63 -41
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +364 -4
- data/lib/fontisan/collection/builder.rb +341 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +317 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +24 -1
- data/lib/fontisan/commands/convert_command.rb +218 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +286 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +203 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +79 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +408 -0
- data/lib/fontisan/converters/outline_converter.rb +998 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +122 -15
- data/lib/fontisan/font_writer.rb +302 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +310 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
- data/lib/fontisan/loading_modes.rb +115 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +405 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +321 -19
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -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/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +934 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +356 -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/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +257 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -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/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +489 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -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 +346 -0
- data/lib/fontisan/tables/cvar.rb +203 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +231 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +321 -20
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +60 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +375 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_context.rb +211 -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/directory.rb +257 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +717 -0
- data/lib/fontisan/woff_font.rb +488 -0
- data/lib/fontisan.rb +132 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +234 -4
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dict"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# CFF Private DICT structure
|
|
9
|
+
#
|
|
10
|
+
# The Private DICT contains glyph-specific hinting and width data.
|
|
11
|
+
# Each font has its own Private DICT (or multiple for CIDFonts).
|
|
12
|
+
#
|
|
13
|
+
# Private DICT Operators:
|
|
14
|
+
# - blue_values: Alignment zones for overshoot suppression
|
|
15
|
+
# - other_blues: Additional alignment zones
|
|
16
|
+
# - family_blues: Family-wide alignment zones
|
|
17
|
+
# - family_other_blues: Family-wide additional alignment zones
|
|
18
|
+
# - blue_scale: Point size for overshoot suppression
|
|
19
|
+
# - blue_shift: Pixels to shift alignment zones
|
|
20
|
+
# - blue_fuzz: Tolerance for alignment zones
|
|
21
|
+
# - std_hw: Standard horizontal stem width
|
|
22
|
+
# - std_vw: Standard vertical stem width
|
|
23
|
+
# - stem_snap_h: Horizontal stem snap widths
|
|
24
|
+
# - stem_snap_v: Vertical stem snap widths
|
|
25
|
+
# - force_bold: Force bold flag
|
|
26
|
+
# - language_group: Language group (0=Latin, 1=CJK)
|
|
27
|
+
# - expansion_factor: Expansion factor for counters
|
|
28
|
+
# - initial_random_seed: Random seed for Type 1 hinting
|
|
29
|
+
# - subrs: Offset to Local Subr INDEX (relative to Private DICT)
|
|
30
|
+
# - default_width_x: Default glyph width
|
|
31
|
+
# - nominal_width_x: Nominal glyph width
|
|
32
|
+
#
|
|
33
|
+
# Reference: CFF specification section 10 "Private DICT"
|
|
34
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
35
|
+
#
|
|
36
|
+
# @example Parsing a Private DICT
|
|
37
|
+
# private_size, private_offset = top_dict.private
|
|
38
|
+
# private_data = cff.raw_data[private_offset, private_size]
|
|
39
|
+
# private_dict = Fontisan::Tables::Cff::PrivateDict.new(private_data)
|
|
40
|
+
# puts private_dict[:blue_values] # => [array of blue values]
|
|
41
|
+
# puts private_dict.default_width_x # => default glyph width
|
|
42
|
+
class PrivateDict < Dict
|
|
43
|
+
# Private DICT specific operators
|
|
44
|
+
#
|
|
45
|
+
# These extend the common operators defined in the base Dict class
|
|
46
|
+
PRIVATE_DICT_OPERATORS = {
|
|
47
|
+
6 => :blue_values,
|
|
48
|
+
7 => :other_blues,
|
|
49
|
+
8 => :family_blues,
|
|
50
|
+
9 => :family_other_blues,
|
|
51
|
+
[12, 9] => :blue_scale,
|
|
52
|
+
[12, 10] => :blue_shift,
|
|
53
|
+
[12, 11] => :blue_fuzz,
|
|
54
|
+
10 => :std_hw,
|
|
55
|
+
11 => :std_vw,
|
|
56
|
+
[12, 12] => :stem_snap_h,
|
|
57
|
+
[12, 13] => :stem_snap_v,
|
|
58
|
+
[12, 14] => :force_bold,
|
|
59
|
+
[12, 17] => :language_group,
|
|
60
|
+
[12, 18] => :expansion_factor,
|
|
61
|
+
[12, 19] => :initial_random_seed,
|
|
62
|
+
19 => :subrs,
|
|
63
|
+
20 => :default_width_x,
|
|
64
|
+
21 => :nominal_width_x,
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
# Default values for Private DICT operators
|
|
68
|
+
#
|
|
69
|
+
# These are used when an operator is not present in the DICT
|
|
70
|
+
DEFAULTS = {
|
|
71
|
+
blue_scale: 0.039625,
|
|
72
|
+
blue_shift: 7,
|
|
73
|
+
blue_fuzz: 1,
|
|
74
|
+
force_bold: false,
|
|
75
|
+
language_group: 0,
|
|
76
|
+
expansion_factor: 0.06,
|
|
77
|
+
initial_random_seed: 0,
|
|
78
|
+
default_width_x: 0,
|
|
79
|
+
nominal_width_x: 0,
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
82
|
+
# Get a value with default fallback
|
|
83
|
+
#
|
|
84
|
+
# @param key [Symbol] Operator name
|
|
85
|
+
# @return [Object] Value or default value
|
|
86
|
+
def fetch(key, default = nil)
|
|
87
|
+
@dict.fetch(key, DEFAULTS.fetch(key, default))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get the blue values (alignment zones)
|
|
91
|
+
#
|
|
92
|
+
# Blue values define vertical zones for overshoot suppression
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<Integer>, nil] Array of blue values (pairs of bottom/top)
|
|
95
|
+
def blue_values
|
|
96
|
+
@dict[:blue_values]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get the other blue values
|
|
100
|
+
#
|
|
101
|
+
# Additional alignment zones beyond the baseline and cap height
|
|
102
|
+
#
|
|
103
|
+
# @return [Array<Integer>, nil] Array of other blue values
|
|
104
|
+
def other_blues
|
|
105
|
+
@dict[:other_blues]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get the family blue values
|
|
109
|
+
#
|
|
110
|
+
# Family-wide alignment zones shared across fonts in a family
|
|
111
|
+
#
|
|
112
|
+
# @return [Array<Integer>, nil] Array of family blue values
|
|
113
|
+
def family_blues
|
|
114
|
+
@dict[:family_blues]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get the family other blue values
|
|
118
|
+
#
|
|
119
|
+
# @return [Array<Integer>, nil] Array of family other blue values
|
|
120
|
+
def family_other_blues
|
|
121
|
+
@dict[:family_other_blues]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Get the blue scale
|
|
125
|
+
#
|
|
126
|
+
# Point size at which overshoot suppression is maximum
|
|
127
|
+
#
|
|
128
|
+
# @return [Float] Blue scale value
|
|
129
|
+
def blue_scale
|
|
130
|
+
fetch(:blue_scale)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get the blue shift
|
|
134
|
+
#
|
|
135
|
+
# Number of device pixels to shift alignment zones
|
|
136
|
+
#
|
|
137
|
+
# @return [Integer] Blue shift in pixels
|
|
138
|
+
def blue_shift
|
|
139
|
+
fetch(:blue_shift)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get the blue fuzz
|
|
143
|
+
#
|
|
144
|
+
# Tolerance for alignment zone matching
|
|
145
|
+
#
|
|
146
|
+
# @return [Integer] Blue fuzz in font units
|
|
147
|
+
def blue_fuzz
|
|
148
|
+
fetch(:blue_fuzz)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Get the standard horizontal width
|
|
152
|
+
#
|
|
153
|
+
# Dominant horizontal stem width
|
|
154
|
+
#
|
|
155
|
+
# @return [Integer, nil] Standard horizontal width
|
|
156
|
+
def std_hw
|
|
157
|
+
value = @dict[:std_hw]
|
|
158
|
+
# std_hw is stored as an array with one element
|
|
159
|
+
value.is_a?(Array) ? value.first : value
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Get the standard vertical width
|
|
163
|
+
#
|
|
164
|
+
# Dominant vertical stem width
|
|
165
|
+
#
|
|
166
|
+
# @return [Integer, nil] Standard vertical width
|
|
167
|
+
def std_vw
|
|
168
|
+
value = @dict[:std_vw]
|
|
169
|
+
# std_vw is stored as an array with one element
|
|
170
|
+
value.is_a?(Array) ? value.first : value
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Get the horizontal stem snap widths
|
|
174
|
+
#
|
|
175
|
+
# Array of horizontal stem widths for stem snapping
|
|
176
|
+
#
|
|
177
|
+
# @return [Array<Integer>, nil] Horizontal stem snap widths
|
|
178
|
+
def stem_snap_h
|
|
179
|
+
@dict[:stem_snap_h]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Get the vertical stem snap widths
|
|
183
|
+
#
|
|
184
|
+
# Array of vertical stem widths for stem snapping
|
|
185
|
+
#
|
|
186
|
+
# @return [Array<Integer>, nil] Vertical stem snap widths
|
|
187
|
+
def stem_snap_v
|
|
188
|
+
@dict[:stem_snap_v]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Check if force bold is enabled
|
|
192
|
+
#
|
|
193
|
+
# @return [Boolean] True if force bold is enabled
|
|
194
|
+
def force_bold?
|
|
195
|
+
fetch(:force_bold)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Get the language group
|
|
199
|
+
#
|
|
200
|
+
# 0 = Latin/Greek/Cyrillic, 1 = CJK
|
|
201
|
+
#
|
|
202
|
+
# @return [Integer] Language group (0 or 1)
|
|
203
|
+
def language_group
|
|
204
|
+
fetch(:language_group)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Get the expansion factor
|
|
208
|
+
#
|
|
209
|
+
# Controls horizontal counter expansion
|
|
210
|
+
#
|
|
211
|
+
# @return [Float] Expansion factor
|
|
212
|
+
def expansion_factor
|
|
213
|
+
fetch(:expansion_factor)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Get the initial random seed
|
|
217
|
+
#
|
|
218
|
+
# Seed for pseudo-random number generation in Type 1 hinting
|
|
219
|
+
#
|
|
220
|
+
# @return [Integer] Initial random seed
|
|
221
|
+
def initial_random_seed
|
|
222
|
+
fetch(:initial_random_seed)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Get the Local Subr INDEX offset
|
|
226
|
+
#
|
|
227
|
+
# Offset is relative to the beginning of the Private DICT
|
|
228
|
+
#
|
|
229
|
+
# @return [Integer, nil] Offset to Local Subr INDEX
|
|
230
|
+
def subrs
|
|
231
|
+
@dict[:subrs]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Get the default glyph width
|
|
235
|
+
#
|
|
236
|
+
# Used when width is not explicitly specified in CharString
|
|
237
|
+
#
|
|
238
|
+
# @return [Integer] Default width in font units
|
|
239
|
+
def default_width_x
|
|
240
|
+
fetch(:default_width_x)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Get the nominal glyph width
|
|
244
|
+
#
|
|
245
|
+
# Base value for width calculations in CharStrings
|
|
246
|
+
#
|
|
247
|
+
# @return [Integer] Nominal width in font units
|
|
248
|
+
def nominal_width_x
|
|
249
|
+
fetch(:nominal_width_x)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Check if this Private DICT has local subroutines
|
|
253
|
+
#
|
|
254
|
+
# @return [Boolean] True if subrs offset is present
|
|
255
|
+
def has_local_subrs?
|
|
256
|
+
!subrs.nil?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Check if this Private DICT has blue values defined
|
|
260
|
+
#
|
|
261
|
+
# @return [Boolean] True if blue values are present
|
|
262
|
+
def has_blue_values?
|
|
263
|
+
!blue_values.nil? && !blue_values.empty?
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Check if this is for CJK language group
|
|
267
|
+
#
|
|
268
|
+
# @return [Boolean] True if language group is 1 (CJK)
|
|
269
|
+
def cjk?
|
|
270
|
+
language_group == 1
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
private
|
|
274
|
+
|
|
275
|
+
# Get Private DICT specific operators
|
|
276
|
+
#
|
|
277
|
+
# @return [Hash] Private DICT operators merged with base operators
|
|
278
|
+
def derived_operators
|
|
279
|
+
PRIVATE_DICT_OPERATORS
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dict_builder"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# Builds CFF Private DICT with hint parameters
|
|
9
|
+
#
|
|
10
|
+
# Private DICT contains font-level hint information used for rendering quality.
|
|
11
|
+
# This writer validates hint parameters against CFF spec limits and serializes
|
|
12
|
+
# them into binary DICT format.
|
|
13
|
+
#
|
|
14
|
+
# Supported hint parameters:
|
|
15
|
+
# - blue_values: Alignment zones (max 14 values, pairs)
|
|
16
|
+
# - other_blues: Additional zones (max 10 values, pairs)
|
|
17
|
+
# - family_blues: Family alignment zones (max 14 values, pairs)
|
|
18
|
+
# - family_other_blues: Family zones (max 10 values, pairs)
|
|
19
|
+
# - std_hw: Standard horizontal stem width
|
|
20
|
+
# - std_vw: Standard vertical stem width
|
|
21
|
+
# - stem_snap_h: Horizontal stem snap widths (max 12)
|
|
22
|
+
# - stem_snap_v: Vertical stem snap widths (max 12)
|
|
23
|
+
# - blue_scale, blue_shift, blue_fuzz: Overshoot parameters
|
|
24
|
+
# - force_bold: Bold flag
|
|
25
|
+
# - language_group: 0=Latin, 1=CJK
|
|
26
|
+
class PrivateDictWriter
|
|
27
|
+
# CFF specification limits for hint parameters
|
|
28
|
+
HINT_LIMITS = {
|
|
29
|
+
blue_values: { max: 14, pairs: true },
|
|
30
|
+
other_blues: { max: 10, pairs: true },
|
|
31
|
+
family_blues: { max: 14, pairs: true },
|
|
32
|
+
family_other_blues: { max: 10, pairs: true },
|
|
33
|
+
stem_snap_h: { max: 12 },
|
|
34
|
+
stem_snap_v: { max: 12 },
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# Initialize writer with optional source Private DICT
|
|
38
|
+
#
|
|
39
|
+
# @param source_dict [PrivateDict, nil] Source to copy non-hint params from
|
|
40
|
+
def initialize(source_dict = nil)
|
|
41
|
+
@params = {}
|
|
42
|
+
parse_source(source_dict) if source_dict
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Update hint parameters
|
|
46
|
+
#
|
|
47
|
+
# @param hint_params [Hash] Hint parameters to add/update
|
|
48
|
+
# @raise [ArgumentError] If parameters are invalid
|
|
49
|
+
def update_hints(hint_params)
|
|
50
|
+
validate!(hint_params)
|
|
51
|
+
@params.merge!(hint_params.transform_keys(&:to_sym))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Serialize to binary DICT format
|
|
55
|
+
#
|
|
56
|
+
# @return [String] Binary DICT data
|
|
57
|
+
def serialize
|
|
58
|
+
DictBuilder.build(@params)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get serialized size in bytes
|
|
62
|
+
#
|
|
63
|
+
# @return [Integer] Size in bytes
|
|
64
|
+
def size
|
|
65
|
+
serialize.bytesize
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Parse non-hint parameters from source Private DICT
|
|
71
|
+
#
|
|
72
|
+
# @param source_dict [PrivateDict] Source dictionary
|
|
73
|
+
def parse_source(source_dict)
|
|
74
|
+
return unless source_dict.respond_to?(:to_h)
|
|
75
|
+
|
|
76
|
+
# Extract only non-hint params (subrs, widths)
|
|
77
|
+
@params = source_dict.to_h.select do |k, _|
|
|
78
|
+
%i[subrs default_width_x nominal_width_x].include?(k)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Validate hint parameters against CFF spec
|
|
83
|
+
#
|
|
84
|
+
# @param params [Hash] Hint parameters
|
|
85
|
+
# @raise [ArgumentError] If validation fails
|
|
86
|
+
def validate!(params)
|
|
87
|
+
params.each do |key, value|
|
|
88
|
+
k = key.to_sym
|
|
89
|
+
validate_hint_param(k, value)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Validate individual hint parameter
|
|
94
|
+
#
|
|
95
|
+
# @param key [Symbol] Parameter name
|
|
96
|
+
# @param value [Object] Parameter value
|
|
97
|
+
# @raise [ArgumentError] If validation fails
|
|
98
|
+
def validate_hint_param(key, value)
|
|
99
|
+
# Check array limits
|
|
100
|
+
if HINT_LIMITS[key]
|
|
101
|
+
raise ArgumentError, "#{key} invalid" unless value.is_a?(Array)
|
|
102
|
+
raise ArgumentError, "#{key} too long" if value.length > HINT_LIMITS[key][:max]
|
|
103
|
+
if HINT_LIMITS[key][:pairs] && value.length.odd?
|
|
104
|
+
raise ArgumentError, "#{key} must be pairs"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check value-specific constraints
|
|
109
|
+
case key
|
|
110
|
+
when :std_hw, :std_vw
|
|
111
|
+
raise ArgumentError, "#{key} negative" if value.negative?
|
|
112
|
+
when :blue_scale
|
|
113
|
+
raise ArgumentError, "#{key} not positive" if value <= 0
|
|
114
|
+
when :blue_shift, :blue_fuzz
|
|
115
|
+
raise ArgumentError, "#{key} invalid" unless value.is_a?(Numeric)
|
|
116
|
+
when :force_bold
|
|
117
|
+
raise ArgumentError, "#{key} must be 0 or 1" unless [0, 1].include?(value)
|
|
118
|
+
when :language_group
|
|
119
|
+
raise ArgumentError, "#{key} must be 0 or 1" unless [0, 1].include?(value)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "private_dict_writer"
|
|
4
|
+
require_relative "offset_recalculator"
|
|
5
|
+
require_relative "index_builder"
|
|
6
|
+
require_relative "dict_builder"
|
|
7
|
+
require_relative "charstring_rebuilder"
|
|
8
|
+
require_relative "hint_operation_injector"
|
|
9
|
+
require "stringio"
|
|
10
|
+
|
|
11
|
+
module Fontisan
|
|
12
|
+
module Tables
|
|
13
|
+
class Cff
|
|
14
|
+
# Rebuilds CFF table with modifications
|
|
15
|
+
#
|
|
16
|
+
# This builder extracts sections from a source CFF table, applies
|
|
17
|
+
# modifications (e.g., hint parameters to Private DICT), recalculates
|
|
18
|
+
# offsets, and assembles a new CFF table.
|
|
19
|
+
#
|
|
20
|
+
# Process:
|
|
21
|
+
# 1. Extract all CFF sections (header, indexes, dicts)
|
|
22
|
+
# 2. Apply modifications to Private DICT
|
|
23
|
+
# 3. Recalculate offsets (charstrings, private)
|
|
24
|
+
# 4. Rebuild Top DICT INDEX with new offsets
|
|
25
|
+
# 5. Reassemble all sections into new CFF table
|
|
26
|
+
#
|
|
27
|
+
# @example Rebuild with hints
|
|
28
|
+
# new_cff = TableBuilder.rebuild(source_cff, {
|
|
29
|
+
# private_dict_hints: { blue_values: [-15, 0], std_hw: 70 }
|
|
30
|
+
# })
|
|
31
|
+
class TableBuilder
|
|
32
|
+
# Rebuild CFF table with modifications
|
|
33
|
+
#
|
|
34
|
+
# @param source_cff [Cff] Source CFF table
|
|
35
|
+
# @param modifications [Hash] Modifications to apply
|
|
36
|
+
# @return [String] Binary CFF table data
|
|
37
|
+
def self.rebuild(source_cff, modifications = {})
|
|
38
|
+
new(source_cff).tap do |builder|
|
|
39
|
+
builder.apply_modifications(modifications)
|
|
40
|
+
end.serialize
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Initialize with source CFF
|
|
44
|
+
#
|
|
45
|
+
# @param source_cff [Cff] Source CFF table
|
|
46
|
+
def initialize(source_cff)
|
|
47
|
+
@source = source_cff
|
|
48
|
+
@sections = extract_sections
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Apply modifications to CFF structure
|
|
52
|
+
#
|
|
53
|
+
# @param mods [Hash] Modifications hash
|
|
54
|
+
def apply_modifications(mods)
|
|
55
|
+
update_private_dict(mods[:private_dict_hints]) if mods[:private_dict_hints]
|
|
56
|
+
update_charstrings(mods[:per_glyph_hints]) if mods[:per_glyph_hints]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Serialize to binary CFF table
|
|
60
|
+
#
|
|
61
|
+
# @return [String] Binary CFF data
|
|
62
|
+
def serialize
|
|
63
|
+
# Calculate initial offsets
|
|
64
|
+
offsets = OffsetRecalculator.calculate_offsets(@sections)
|
|
65
|
+
top_dict = extract_top_dict_data
|
|
66
|
+
updated = OffsetRecalculator.update_top_dict(top_dict, offsets)
|
|
67
|
+
rebuild_top_dict_index(updated)
|
|
68
|
+
|
|
69
|
+
# Recalculate after Top DICT rebuild (size may change)
|
|
70
|
+
offsets = OffsetRecalculator.calculate_offsets(@sections)
|
|
71
|
+
updated = OffsetRecalculator.update_top_dict(top_dict, offsets)
|
|
72
|
+
rebuild_top_dict_index(updated)
|
|
73
|
+
|
|
74
|
+
assemble
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Extract all CFF sections from source
|
|
80
|
+
#
|
|
81
|
+
# @return [Hash] Hash of section_name => binary_data
|
|
82
|
+
def extract_sections
|
|
83
|
+
{
|
|
84
|
+
header: extract_header,
|
|
85
|
+
name_index: extract_index(@source.name_index),
|
|
86
|
+
top_dict_index: extract_index(@source.top_dict_index),
|
|
87
|
+
string_index: extract_index(@source.string_index),
|
|
88
|
+
global_subr_index: extract_index(@source.global_subr_index),
|
|
89
|
+
charstrings_index: extract_index(@source.charstrings_index(0)),
|
|
90
|
+
private_dict: extract_private_dict,
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Extract header bytes
|
|
95
|
+
#
|
|
96
|
+
# @return [String] Binary header data
|
|
97
|
+
def extract_header
|
|
98
|
+
@source.raw_data[0, @source.header.hdr_size]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Extract INDEX as binary data
|
|
102
|
+
#
|
|
103
|
+
# @param index [Index] INDEX object
|
|
104
|
+
# @return [String] Binary INDEX data
|
|
105
|
+
def extract_index(index)
|
|
106
|
+
return [0].pack("n") if index.nil? || index.count.zero?
|
|
107
|
+
|
|
108
|
+
start = index.instance_variable_get(:@start_offset)
|
|
109
|
+
io = StringIO.new(@source.raw_data)
|
|
110
|
+
io.seek(start)
|
|
111
|
+
|
|
112
|
+
count = io.read(2).unpack1("n")
|
|
113
|
+
return [0].pack("n") if count.zero?
|
|
114
|
+
|
|
115
|
+
off_size = io.read(1).unpack1("C")
|
|
116
|
+
offset_array_size = (count + 1) * off_size
|
|
117
|
+
|
|
118
|
+
# Read last offset to determine data size
|
|
119
|
+
io.seek(start + 3 + count * off_size)
|
|
120
|
+
last_offset = read_offset(io, off_size)
|
|
121
|
+
data_size = last_offset - 1
|
|
122
|
+
|
|
123
|
+
# Read entire INDEX
|
|
124
|
+
io.seek(start)
|
|
125
|
+
io.read(3 + offset_array_size + data_size)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Extract Private DICT bytes
|
|
129
|
+
#
|
|
130
|
+
# @return [String] Binary Private DICT data
|
|
131
|
+
def extract_private_dict
|
|
132
|
+
priv_info = @source.top_dict(0).private
|
|
133
|
+
return "".b unless priv_info
|
|
134
|
+
|
|
135
|
+
size, offset = priv_info
|
|
136
|
+
@source.raw_data[offset, size]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Update Private DICT with hints
|
|
140
|
+
#
|
|
141
|
+
# @param hints [Hash] Hint parameters
|
|
142
|
+
def update_private_dict(hints)
|
|
143
|
+
source_priv = @source.private_dict(0)
|
|
144
|
+
writer = PrivateDictWriter.new(source_priv)
|
|
145
|
+
writer.update_hints(hints)
|
|
146
|
+
@sections[:private_dict] = writer.serialize
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Update CharStrings with per-glyph hints
|
|
150
|
+
#
|
|
151
|
+
# @param per_glyph_hints [Hash] Hash of glyph_id => Array<Hint>
|
|
152
|
+
def update_charstrings(per_glyph_hints)
|
|
153
|
+
return if per_glyph_hints.nil? || per_glyph_hints.empty?
|
|
154
|
+
|
|
155
|
+
# Create CharStringRebuilder
|
|
156
|
+
charstrings_index = @source.charstrings_index(0)
|
|
157
|
+
rebuilder = CharStringRebuilder.new(charstrings_index)
|
|
158
|
+
|
|
159
|
+
# Inject hints for each glyph
|
|
160
|
+
per_glyph_hints.each do |glyph_id, hints|
|
|
161
|
+
injector = HintOperationInjector.new
|
|
162
|
+
|
|
163
|
+
rebuilder.modify_charstring(glyph_id) do |operations|
|
|
164
|
+
# Inject hint operations
|
|
165
|
+
injector.inject(hints, operations)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Rebuild CharStrings INDEX
|
|
170
|
+
@sections[:charstrings_index] = rebuilder.rebuild
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Extract Top DICT data as hash
|
|
174
|
+
#
|
|
175
|
+
# @return [Hash] Top DICT parameters
|
|
176
|
+
def extract_top_dict_data
|
|
177
|
+
@source.top_dict(0).to_h
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Rebuild Top DICT INDEX with updated data
|
|
181
|
+
#
|
|
182
|
+
# @param data [Hash] Top DICT parameters
|
|
183
|
+
def rebuild_top_dict_index(data)
|
|
184
|
+
dict_bytes = DictBuilder.build(data)
|
|
185
|
+
@sections[:top_dict_index] = IndexBuilder.build([dict_bytes])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Assemble all sections into CFF table
|
|
189
|
+
#
|
|
190
|
+
# @return [String] Binary CFF table
|
|
191
|
+
def assemble
|
|
192
|
+
output = StringIO.new("".b)
|
|
193
|
+
output.write(@sections[:header])
|
|
194
|
+
output.write(@sections[:name_index])
|
|
195
|
+
output.write(@sections[:top_dict_index])
|
|
196
|
+
output.write(@sections[:string_index])
|
|
197
|
+
output.write(@sections[:global_subr_index])
|
|
198
|
+
output.write(@sections[:charstrings_index])
|
|
199
|
+
output.write(@sections[:private_dict])
|
|
200
|
+
output.string
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Read offset of specified size
|
|
204
|
+
#
|
|
205
|
+
# @param io [IO] IO object
|
|
206
|
+
# @param size [Integer] Offset size (1-4 bytes)
|
|
207
|
+
# @return [Integer] Offset value
|
|
208
|
+
def read_offset(io, size)
|
|
209
|
+
case size
|
|
210
|
+
when 1 then io.read(1).unpack1("C")
|
|
211
|
+
when 2 then io.read(2).unpack1("n")
|
|
212
|
+
when 3
|
|
213
|
+
bytes = io.read(3).unpack("C*")
|
|
214
|
+
(bytes[0] << 16) | (bytes[1] << 8) | bytes[2]
|
|
215
|
+
when 4 then io.read(4).unpack1("N")
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|