fontisan 0.1.0 → 0.2.0
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 +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- 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 +309 -0
- data/lib/fontisan/collection/builder.rb +260 -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 +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -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 +295 -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 +178 -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 +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -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 +121 -12
- data/lib/fontisan/font_writer.rb +301 -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 +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -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 +233 -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 +296 -10
- 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/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 +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -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 +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -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/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -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.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -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 +270 -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 +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -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/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 +268 -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/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/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
# High-level utility class for unified glyph access across font formats
|
|
5
|
+
#
|
|
6
|
+
# [`GlyphAccessor`](lib/fontisan/glyph_accessor.rb) provides a clean, unified
|
|
7
|
+
# interface for accessing glyphs regardless of the underlying font format
|
|
8
|
+
# (TrueType with glyf table or OpenType with CFF table).
|
|
9
|
+
#
|
|
10
|
+
# This class automatically detects the font format and delegates to the
|
|
11
|
+
# appropriate table parser, abstracting away the complexity of different
|
|
12
|
+
# glyph storage mechanisms.
|
|
13
|
+
#
|
|
14
|
+
# Key features:
|
|
15
|
+
# - Unified glyph access by ID, Unicode character, or PostScript name
|
|
16
|
+
# - Automatic format detection (TrueType glyf vs CFF)
|
|
17
|
+
# - Metrics retrieval (advance width, left sidebearing)
|
|
18
|
+
# - Glyph closure calculation for subsetting (tracks composite dependencies)
|
|
19
|
+
# - Validation of glyph IDs and character mappings
|
|
20
|
+
#
|
|
21
|
+
# @example Basic usage
|
|
22
|
+
# font = Fontisan::TrueTypeFont.from_file('font.ttf')
|
|
23
|
+
# accessor = Fontisan::GlyphAccessor.new(font)
|
|
24
|
+
#
|
|
25
|
+
# # Access glyph by ID
|
|
26
|
+
# glyph = accessor.glyph_for_id(42)
|
|
27
|
+
# puts glyph.class # => SimpleGlyph or CompoundGlyph
|
|
28
|
+
#
|
|
29
|
+
# # Access glyph by Unicode character
|
|
30
|
+
# glyph_a = accessor.glyph_for_char(0x0041) # 'A'
|
|
31
|
+
#
|
|
32
|
+
# # Get metrics
|
|
33
|
+
# metrics = accessor.metrics_for_id(42)
|
|
34
|
+
# puts "Width: #{metrics[:advance_width]}, LSB: #{metrics[:lsb]}"
|
|
35
|
+
#
|
|
36
|
+
# @example Subsetting workflow with closure
|
|
37
|
+
# # Calculate all glyphs needed (including composite dependencies)
|
|
38
|
+
# base_glyphs = [0, 1, 65, 66, 67] # .notdef, A, B, C
|
|
39
|
+
# all_glyphs = accessor.closure_for(base_glyphs)
|
|
40
|
+
# puts "Total glyphs needed: #{all_glyphs.size}"
|
|
41
|
+
#
|
|
42
|
+
# Reference: [`docs/ttfunk-feature-analysis.md:541-575`](docs/ttfunk-feature-analysis.md:541)
|
|
43
|
+
class GlyphAccessor
|
|
44
|
+
# Font instance this accessor operates on
|
|
45
|
+
# @return [TrueTypeFont, OpenTypeFont]
|
|
46
|
+
attr_reader :font
|
|
47
|
+
|
|
48
|
+
# Initialize a new glyph accessor
|
|
49
|
+
#
|
|
50
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font instance to access
|
|
51
|
+
# @raise [ArgumentError] If font is nil or doesn't respond to table method
|
|
52
|
+
def initialize(font)
|
|
53
|
+
raise ArgumentError, "Font cannot be nil" if font.nil?
|
|
54
|
+
|
|
55
|
+
unless font.respond_to?(:table)
|
|
56
|
+
raise ArgumentError, "Font must respond to :table method"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@font = font
|
|
60
|
+
@glyph_cache = {}
|
|
61
|
+
@closure_cache = {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get glyph object for a glyph ID
|
|
65
|
+
#
|
|
66
|
+
# Returns the appropriate glyph object based on the font format:
|
|
67
|
+
# - TrueType fonts: [`SimpleGlyph`](lib/fontisan/tables/glyf/simple_glyph.rb)
|
|
68
|
+
# or [`CompoundGlyph`](lib/fontisan/tables/glyf/compound_glyph.rb)
|
|
69
|
+
# - CFF fonts: [`CFFGlyph`](lib/fontisan/tables/cff/cff_glyph.rb)
|
|
70
|
+
#
|
|
71
|
+
# @param glyph_id [Integer] Glyph ID (0-based, 0 is .notdef)
|
|
72
|
+
# @return [SimpleGlyph, CompoundGlyph, CFFGlyph, nil] Glyph object or nil
|
|
73
|
+
# if glyph is empty or invalid
|
|
74
|
+
# @raise [ArgumentError] If glyph_id is invalid
|
|
75
|
+
# @raise [Fontisan::MissingTableError] If required tables are missing
|
|
76
|
+
#
|
|
77
|
+
# @example Get a glyph
|
|
78
|
+
# glyph = accessor.glyph_for_id(65)
|
|
79
|
+
# if glyph
|
|
80
|
+
# puts "Bounding box: #{glyph.bounding_box}"
|
|
81
|
+
# puts "Type: #{glyph.simple? ? 'simple' : 'compound'}"
|
|
82
|
+
# end
|
|
83
|
+
def glyph_for_id(glyph_id)
|
|
84
|
+
validate_glyph_id!(glyph_id)
|
|
85
|
+
|
|
86
|
+
return @glyph_cache[glyph_id] if @glyph_cache.key?(glyph_id)
|
|
87
|
+
|
|
88
|
+
glyph = if truetype?
|
|
89
|
+
truetype_glyph(glyph_id)
|
|
90
|
+
elsif cff?
|
|
91
|
+
cff_glyph(glyph_id)
|
|
92
|
+
else
|
|
93
|
+
raise Fontisan::MissingTableError,
|
|
94
|
+
"Font has neither glyf nor CFF table"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@glyph_cache[glyph_id] = glyph
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get glyph object for a Unicode character code
|
|
101
|
+
#
|
|
102
|
+
# Uses the cmap table to map the character code to a glyph ID,
|
|
103
|
+
# then retrieves the corresponding glyph.
|
|
104
|
+
#
|
|
105
|
+
# @param char_code [Integer] Unicode character code (e.g., 0x0041 for 'A')
|
|
106
|
+
# @return [SimpleGlyph, CompoundGlyph, nil] Glyph object or nil if
|
|
107
|
+
# character is not mapped
|
|
108
|
+
# @raise [Fontisan::MissingTableError] If cmap table is missing
|
|
109
|
+
#
|
|
110
|
+
# @example Get glyph for 'A'
|
|
111
|
+
# glyph_a = accessor.glyph_for_char(0x0041)
|
|
112
|
+
# glyph_a = accessor.glyph_for_char('A'.ord) # Equivalent
|
|
113
|
+
def glyph_for_char(char_code)
|
|
114
|
+
glyph_id = char_to_glyph_id(char_code)
|
|
115
|
+
return nil unless glyph_id
|
|
116
|
+
|
|
117
|
+
glyph_for_id(glyph_id)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get glyph object for a PostScript glyph name
|
|
121
|
+
#
|
|
122
|
+
# Uses the post table (if available) to map the glyph name to a glyph ID.
|
|
123
|
+
# This method is primarily useful for fonts with post table version 2.0.
|
|
124
|
+
#
|
|
125
|
+
# @param glyph_name [String] PostScript glyph name (e.g., "A", "Aacute")
|
|
126
|
+
# @return [SimpleGlyph, CompoundGlyph, nil] Glyph object or nil if
|
|
127
|
+
# name is not found
|
|
128
|
+
# @raise [Fontisan::MissingTableError] If post table is missing or
|
|
129
|
+
# unsupported version
|
|
130
|
+
#
|
|
131
|
+
# @example Get glyph by name
|
|
132
|
+
# glyph = accessor.glyph_for_name("A")
|
|
133
|
+
# glyph = accessor.glyph_for_name("Aacute")
|
|
134
|
+
def glyph_for_name(glyph_name)
|
|
135
|
+
glyph_id = name_to_glyph_id(glyph_name)
|
|
136
|
+
return nil unless glyph_id
|
|
137
|
+
|
|
138
|
+
glyph_for_id(glyph_id)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Get horizontal metrics for a glyph ID
|
|
142
|
+
#
|
|
143
|
+
# Returns a hash with advance width and left sidebearing in font units.
|
|
144
|
+
#
|
|
145
|
+
# @param glyph_id [Integer] Glyph ID
|
|
146
|
+
# @return [Hash{Symbol => Integer}, nil] Hash with :advance_width and
|
|
147
|
+
# :lsb keys, or nil if glyph is invalid
|
|
148
|
+
# @raise [Fontisan::MissingTableError] If hmtx table is missing or not parsed
|
|
149
|
+
#
|
|
150
|
+
# @example Get metrics
|
|
151
|
+
# metrics = accessor.metrics_for_id(65) # 'A'
|
|
152
|
+
# puts "Advance width: #{metrics[:advance_width]} FUnits"
|
|
153
|
+
# puts "Left sidebearing: #{metrics[:lsb]} FUnits"
|
|
154
|
+
def metrics_for_id(glyph_id)
|
|
155
|
+
validate_glyph_id!(glyph_id)
|
|
156
|
+
|
|
157
|
+
hmtx = font.table("hmtx")
|
|
158
|
+
raise_missing_table!("hmtx") unless hmtx
|
|
159
|
+
|
|
160
|
+
unless hmtx.parsed?
|
|
161
|
+
# Auto-parse if not already parsed
|
|
162
|
+
parse_hmtx_with_context!(hmtx)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
hmtx.metric_for(glyph_id)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Get horizontal metrics for a Unicode character
|
|
169
|
+
#
|
|
170
|
+
# @param char_code [Integer] Unicode character code
|
|
171
|
+
# @return [Hash{Symbol => Integer}, nil] Metrics hash or nil if not mapped
|
|
172
|
+
# @raise [Fontisan::MissingTableError] If required tables are missing
|
|
173
|
+
#
|
|
174
|
+
# @example Get metrics for 'A'
|
|
175
|
+
# metrics = accessor.metrics_for_char(0x0041)
|
|
176
|
+
def metrics_for_char(char_code)
|
|
177
|
+
glyph_id = char_to_glyph_id(char_code)
|
|
178
|
+
return nil unless glyph_id
|
|
179
|
+
|
|
180
|
+
metrics_for_id(glyph_id)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Get outline for glyph by ID
|
|
184
|
+
#
|
|
185
|
+
# Extracts the complete outline data for a glyph, including all contours,
|
|
186
|
+
# points, and bounding box information. The outline can be converted to
|
|
187
|
+
# SVG paths or drawing commands for rendering.
|
|
188
|
+
#
|
|
189
|
+
# This method uses [`OutlineExtractor`](lib/fontisan/outline_extractor.rb)
|
|
190
|
+
# to handle both TrueType (glyf) and CFF outline formats transparently.
|
|
191
|
+
# For compound glyphs, it recursively resolves component dependencies.
|
|
192
|
+
#
|
|
193
|
+
# @param glyph_id [Integer] Glyph ID (0-based, 0 is .notdef)
|
|
194
|
+
# @return [Models::GlyphOutline, nil] Outline object or nil if glyph is
|
|
195
|
+
# empty or invalid
|
|
196
|
+
# @raise [ArgumentError] If glyph_id is invalid
|
|
197
|
+
# @raise [Fontisan::MissingTableError] If required tables are missing
|
|
198
|
+
#
|
|
199
|
+
# @example Get outline for a glyph
|
|
200
|
+
# outline = accessor.outline_for_id(65) # 'A'
|
|
201
|
+
# if outline
|
|
202
|
+
# puts "Contours: #{outline.contour_count}"
|
|
203
|
+
# puts "Points: #{outline.point_count}"
|
|
204
|
+
# puts "SVG: #{outline.to_svg_path}"
|
|
205
|
+
# end
|
|
206
|
+
def outline_for_id(glyph_id)
|
|
207
|
+
extractor = OutlineExtractor.new(@font)
|
|
208
|
+
extractor.extract(glyph_id)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Get outline for Unicode codepoint
|
|
212
|
+
#
|
|
213
|
+
# Maps a Unicode codepoint to a glyph ID via the cmap table, then
|
|
214
|
+
# extracts the outline for that glyph.
|
|
215
|
+
#
|
|
216
|
+
# @param codepoint [Integer] Unicode codepoint (e.g., 0x0041 for 'A')
|
|
217
|
+
# @return [Models::GlyphOutline, nil] Outline object or nil if character
|
|
218
|
+
# is not mapped or glyph is empty
|
|
219
|
+
# @raise [Fontisan::MissingTableError] If required tables are missing
|
|
220
|
+
#
|
|
221
|
+
# @example Get outline for 'A'
|
|
222
|
+
# outline = accessor.outline_for_codepoint(0x0041)
|
|
223
|
+
# svg_path = outline.to_svg_path if outline
|
|
224
|
+
def outline_for_codepoint(codepoint)
|
|
225
|
+
glyph_id = char_to_glyph_id(codepoint)
|
|
226
|
+
return nil unless glyph_id
|
|
227
|
+
|
|
228
|
+
outline_for_id(glyph_id)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Get outline for character
|
|
232
|
+
#
|
|
233
|
+
# Convenience method that takes a character string and extracts its
|
|
234
|
+
# outline. The character is converted to its Unicode codepoint first.
|
|
235
|
+
#
|
|
236
|
+
# @param char [String] Single character (e.g., 'A', '中', '😀')
|
|
237
|
+
# @return [Models::GlyphOutline, nil] Outline object or nil if character
|
|
238
|
+
# is not mapped or glyph is empty
|
|
239
|
+
# @raise [ArgumentError] If char is not a single character
|
|
240
|
+
# @raise [Fontisan::MissingTableError] If required tables are missing
|
|
241
|
+
#
|
|
242
|
+
# @example Get outline for 'A'
|
|
243
|
+
# outline = accessor.outline_for_char('A')
|
|
244
|
+
# commands = outline.to_commands if outline
|
|
245
|
+
#
|
|
246
|
+
# @example Handle multi-codepoint characters
|
|
247
|
+
# outline = accessor.outline_for_char('A') # Works
|
|
248
|
+
# outline = accessor.outline_for_char('AB') # ArgumentError
|
|
249
|
+
def outline_for_char(char)
|
|
250
|
+
unless char.is_a?(String) && char.length == 1
|
|
251
|
+
raise ArgumentError,
|
|
252
|
+
"char must be a single character String, got: #{char.inspect}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
outline_for_codepoint(char.ord)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Check if a glyph ID exists and is valid
|
|
259
|
+
#
|
|
260
|
+
# @param glyph_id [Integer] Glyph ID to check
|
|
261
|
+
# @return [Boolean] True if glyph ID is valid
|
|
262
|
+
def glyph_exists?(glyph_id)
|
|
263
|
+
return false if glyph_id.nil? || glyph_id.negative?
|
|
264
|
+
|
|
265
|
+
maxp = font.table("maxp")
|
|
266
|
+
return false unless maxp
|
|
267
|
+
|
|
268
|
+
glyph_id < maxp.num_glyphs
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Check if a Unicode character is mapped in the font
|
|
272
|
+
#
|
|
273
|
+
# @param char_code [Integer] Unicode character code
|
|
274
|
+
# @return [Boolean] True if character has a glyph mapping
|
|
275
|
+
def has_glyph_for_char?(char_code)
|
|
276
|
+
!char_to_glyph_id(char_code).nil?
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Check if font uses TrueType outlines (glyf table)
|
|
280
|
+
#
|
|
281
|
+
# @return [Boolean] True if font has glyf table
|
|
282
|
+
def truetype?
|
|
283
|
+
font.table("glyf") != nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Check if font uses CFF outlines (CFF table)
|
|
287
|
+
#
|
|
288
|
+
# @return [Boolean] True if font has CFF table
|
|
289
|
+
def cff?
|
|
290
|
+
font.table("CFF ") != nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Calculate glyph closure for subsetting
|
|
294
|
+
#
|
|
295
|
+
# This method recursively tracks all glyphs needed for a given set of
|
|
296
|
+
# glyph IDs, including component glyphs referenced by compound glyphs.
|
|
297
|
+
# This is essential for font subsetting to ensure all required glyphs
|
|
298
|
+
# are included.
|
|
299
|
+
#
|
|
300
|
+
# The closure always includes glyph 0 (.notdef) as required by the
|
|
301
|
+
# OpenType specification.
|
|
302
|
+
#
|
|
303
|
+
# @param glyph_ids [Array<Integer>] Base set of glyph IDs
|
|
304
|
+
# @return [Set<Integer>] Complete set of glyph IDs needed (including
|
|
305
|
+
# composite dependencies)
|
|
306
|
+
# @raise [ArgumentError] If glyph_ids is not an array
|
|
307
|
+
#
|
|
308
|
+
# @example Calculate closure for subsetting
|
|
309
|
+
# # Want to subset to just "ABC"
|
|
310
|
+
# base_glyphs = [65, 66, 67] # Assuming these are glyph IDs for A, B, C
|
|
311
|
+
# all_needed = accessor.closure_for(base_glyphs)
|
|
312
|
+
# # all_needed includes base glyphs + any composite dependencies + .notdef
|
|
313
|
+
#
|
|
314
|
+
# @example Closure with composite glyphs
|
|
315
|
+
# # If 'Ä' (glyph 100) is composite referencing 'A' (glyph 65) and
|
|
316
|
+
# # dieresis (glyph 200)
|
|
317
|
+
# closure = accessor.closure_for([100])
|
|
318
|
+
# # Returns: [0, 100, 65, 200] (includes .notdef, Ä, A, dieresis)
|
|
319
|
+
def closure_for(glyph_ids)
|
|
320
|
+
unless glyph_ids.is_a?(Array)
|
|
321
|
+
raise ArgumentError, "glyph_ids must be an Array"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Start with provided glyphs plus .notdef
|
|
325
|
+
result = Set.new([0])
|
|
326
|
+
glyph_ids.each { |id| result.add(id) if glyph_exists?(id) }
|
|
327
|
+
|
|
328
|
+
# CFF fonts have no composite glyphs, so return early
|
|
329
|
+
return result if cff?
|
|
330
|
+
|
|
331
|
+
# Recursively collect composite dependencies (TrueType only)
|
|
332
|
+
to_process = result.to_a.dup
|
|
333
|
+
processed = Set.new
|
|
334
|
+
|
|
335
|
+
while (glyph_id = to_process.shift)
|
|
336
|
+
next if processed.include?(glyph_id)
|
|
337
|
+
|
|
338
|
+
processed.add(glyph_id)
|
|
339
|
+
|
|
340
|
+
# Get glyph and check if it's compound
|
|
341
|
+
glyph = glyph_for_id(glyph_id)
|
|
342
|
+
next unless glyph
|
|
343
|
+
next unless glyph.respond_to?(:compound?) && glyph.compound?
|
|
344
|
+
|
|
345
|
+
# Add component glyph IDs
|
|
346
|
+
if glyph.respond_to?(:components)
|
|
347
|
+
glyph.components.each do |component|
|
|
348
|
+
component_id = component[:glyph_index]
|
|
349
|
+
next unless glyph_exists?(component_id)
|
|
350
|
+
|
|
351
|
+
unless result.include?(component_id)
|
|
352
|
+
result.add(component_id)
|
|
353
|
+
to_process << component_id
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
result
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Clear internal caches to free memory
|
|
363
|
+
#
|
|
364
|
+
# Useful for long-running processes that access many glyphs.
|
|
365
|
+
#
|
|
366
|
+
# @return [void]
|
|
367
|
+
def clear_cache
|
|
368
|
+
@glyph_cache.clear
|
|
369
|
+
@closure_cache.clear
|
|
370
|
+
|
|
371
|
+
# Also clear glyf table cache if present
|
|
372
|
+
glyf = font.table("glyf")
|
|
373
|
+
glyf&.clear_cache if glyf.respond_to?(:clear_cache)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
private
|
|
377
|
+
|
|
378
|
+
# Validate a glyph ID
|
|
379
|
+
#
|
|
380
|
+
# @param glyph_id [Integer] Glyph ID to validate
|
|
381
|
+
# @raise [ArgumentError] If glyph ID is invalid
|
|
382
|
+
def validate_glyph_id!(glyph_id)
|
|
383
|
+
if glyph_id.nil?
|
|
384
|
+
raise ArgumentError, "glyph_id cannot be nil"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
if glyph_id.negative?
|
|
388
|
+
raise ArgumentError, "glyph_id must be >= 0, got: #{glyph_id}"
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
unless glyph_exists?(glyph_id)
|
|
392
|
+
maxp = font.table("maxp")
|
|
393
|
+
num_glyphs = maxp ? maxp.num_glyphs : "unknown"
|
|
394
|
+
raise ArgumentError,
|
|
395
|
+
"glyph_id #{glyph_id} exceeds number of glyphs (#{num_glyphs})"
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Get TrueType glyph from glyf table
|
|
400
|
+
#
|
|
401
|
+
# @param glyph_id [Integer] Glyph ID
|
|
402
|
+
# @return [SimpleGlyph, CompoundGlyph, nil] Glyph object
|
|
403
|
+
def truetype_glyph(glyph_id)
|
|
404
|
+
glyf = font.table("glyf")
|
|
405
|
+
raise_missing_table!("glyf") unless glyf
|
|
406
|
+
|
|
407
|
+
loca = font.table("loca")
|
|
408
|
+
raise_missing_table!("loca") unless loca
|
|
409
|
+
|
|
410
|
+
head = font.table("head")
|
|
411
|
+
raise_missing_table!("head") unless head
|
|
412
|
+
|
|
413
|
+
# Ensure loca is parsed
|
|
414
|
+
unless loca.parsed?
|
|
415
|
+
parse_loca_with_context!(loca, head)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
glyf.glyph_for(glyph_id, loca, head)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Get CFF glyph from CFF table
|
|
422
|
+
#
|
|
423
|
+
# @param glyph_id [Integer] Glyph ID
|
|
424
|
+
# @return [CFFGlyph, nil] CFF glyph object or nil if empty
|
|
425
|
+
def cff_glyph(glyph_id)
|
|
426
|
+
cff = font.table(Constants::CFF_TAG)
|
|
427
|
+
raise_missing_table!(Constants::CFF_TAG) unless cff
|
|
428
|
+
|
|
429
|
+
# Get CharString for glyph
|
|
430
|
+
charstring = cff.charstring_for_glyph(glyph_id)
|
|
431
|
+
return nil unless charstring
|
|
432
|
+
|
|
433
|
+
# Get Charset and Encoding
|
|
434
|
+
charset = cff.charset
|
|
435
|
+
encoding = cff.encoding
|
|
436
|
+
|
|
437
|
+
# Wrap in CFFGlyph class
|
|
438
|
+
Tables::Cff::CFFGlyph.new(glyph_id, charstring, charset, encoding)
|
|
439
|
+
rescue StandardError => e
|
|
440
|
+
warn "Failed to get CFF glyph #{glyph_id}: #{e.message}"
|
|
441
|
+
nil
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Map character code to glyph ID
|
|
445
|
+
#
|
|
446
|
+
# @param char_code [Integer] Unicode character code
|
|
447
|
+
# @return [Integer, nil] Glyph ID or nil if not mapped
|
|
448
|
+
def char_to_glyph_id(char_code)
|
|
449
|
+
cmap = font.table("cmap")
|
|
450
|
+
raise_missing_table!("cmap") unless cmap
|
|
451
|
+
|
|
452
|
+
cmap.unicode_mappings[char_code]
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Map glyph name to glyph ID
|
|
456
|
+
#
|
|
457
|
+
# @param glyph_name [String] PostScript glyph name
|
|
458
|
+
# @return [Integer, nil] Glyph ID or nil if not found
|
|
459
|
+
def name_to_glyph_id(glyph_name)
|
|
460
|
+
post = font.table("post")
|
|
461
|
+
raise_missing_table!("post") unless post
|
|
462
|
+
|
|
463
|
+
# post.glyph_names returns array of names indexed by glyph ID
|
|
464
|
+
names = post.glyph_names
|
|
465
|
+
return nil if names.empty?
|
|
466
|
+
|
|
467
|
+
names.index(glyph_name)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Parse loca table with context
|
|
471
|
+
#
|
|
472
|
+
# @param loca [Loca] Loca table instance
|
|
473
|
+
# @param head [Head] Head table instance
|
|
474
|
+
def parse_loca_with_context!(loca, head)
|
|
475
|
+
maxp = font.table("maxp")
|
|
476
|
+
raise_missing_table!("maxp") unless maxp
|
|
477
|
+
|
|
478
|
+
loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Parse hmtx table with context
|
|
482
|
+
#
|
|
483
|
+
# @param hmtx [Hmtx] Hmtx table instance
|
|
484
|
+
def parse_hmtx_with_context!(hmtx)
|
|
485
|
+
hhea = font.table("hhea")
|
|
486
|
+
raise_missing_table!("hhea") unless hhea
|
|
487
|
+
|
|
488
|
+
maxp = font.table("maxp")
|
|
489
|
+
raise_missing_table!("maxp") unless maxp
|
|
490
|
+
|
|
491
|
+
hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Raise MissingTableError
|
|
495
|
+
#
|
|
496
|
+
# @param table_tag [String] Table tag
|
|
497
|
+
# @raise [Fontisan::MissingTableError]
|
|
498
|
+
def raise_missing_table!(table_tag)
|
|
499
|
+
raise Fontisan::MissingTableError,
|
|
500
|
+
"Required table '#{table_tag}' not found in font"
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../models/hint"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Hints
|
|
7
|
+
# Converts hints between TrueType and PostScript formats
|
|
8
|
+
#
|
|
9
|
+
# This converter handles bidirectional conversion of rendering hints,
|
|
10
|
+
# translating between TrueType instruction-based hinting and PostScript
|
|
11
|
+
# operator-based hinting while preserving intent where possible.
|
|
12
|
+
#
|
|
13
|
+
# **Conversion Strategy:**
|
|
14
|
+
#
|
|
15
|
+
# - TrueType → PostScript: Extract semantic meaning from instructions
|
|
16
|
+
# and convert to corresponding PostScript operators
|
|
17
|
+
# - PostScript → TrueType: Analyze hint operators and generate
|
|
18
|
+
# equivalent TrueType instructions
|
|
19
|
+
#
|
|
20
|
+
# @example Convert TrueType hints to PostScript
|
|
21
|
+
# converter = HintConverter.new
|
|
22
|
+
# ps_hints = converter.to_postscript(tt_hints)
|
|
23
|
+
#
|
|
24
|
+
# @example Convert PostScript hints to TrueType
|
|
25
|
+
# converter = HintConverter.new
|
|
26
|
+
# tt_hints = converter.to_truetype(ps_hints)
|
|
27
|
+
class HintConverter
|
|
28
|
+
# Convert hints to PostScript format
|
|
29
|
+
#
|
|
30
|
+
# @param hints [Array<Hint>] Source hints (any format)
|
|
31
|
+
# @return [Array<Hint>] Hints in PostScript format
|
|
32
|
+
def to_postscript(hints)
|
|
33
|
+
return [] if hints.nil? || hints.empty?
|
|
34
|
+
|
|
35
|
+
hints.map do |hint|
|
|
36
|
+
next hint if hint.source_format == :postscript
|
|
37
|
+
|
|
38
|
+
convert_hint_to_postscript(hint)
|
|
39
|
+
end.compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Convert hints to TrueType format
|
|
43
|
+
#
|
|
44
|
+
# @param hints [Array<Hint>] Source hints (any format)
|
|
45
|
+
# @return [Array<Hint>] Hints in TrueType format
|
|
46
|
+
def to_truetype(hints)
|
|
47
|
+
return [] if hints.nil? || hints.empty?
|
|
48
|
+
|
|
49
|
+
hints.map do |hint|
|
|
50
|
+
next hint if hint.source_format == :truetype
|
|
51
|
+
|
|
52
|
+
convert_hint_to_truetype(hint)
|
|
53
|
+
end.compact
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Optimize hint set by removing redundant hints
|
|
57
|
+
#
|
|
58
|
+
# @param hints [Array<Hint>] Hints to optimize
|
|
59
|
+
# @return [Array<Hint>] Optimized hints
|
|
60
|
+
def optimize(hints)
|
|
61
|
+
return [] if hints.nil? || hints.empty?
|
|
62
|
+
|
|
63
|
+
# Remove duplicate hints
|
|
64
|
+
unique_hints = hints.uniq { |h| [h.type, h.data] }
|
|
65
|
+
|
|
66
|
+
# Remove conflicting hints (keep first)
|
|
67
|
+
remove_conflicts(unique_hints)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Convert a single hint to PostScript format
|
|
73
|
+
#
|
|
74
|
+
# @param hint [Hint] Source hint
|
|
75
|
+
# @return [Hint, nil] Converted hint or nil if incompatible
|
|
76
|
+
def convert_hint_to_postscript(hint)
|
|
77
|
+
return nil unless hint.compatible_with?(:postscript)
|
|
78
|
+
|
|
79
|
+
# Get PostScript representation from hint
|
|
80
|
+
ps_data = hint.to_postscript
|
|
81
|
+
|
|
82
|
+
# Create new hint with PostScript format
|
|
83
|
+
Models::Hint.new(
|
|
84
|
+
type: hint.type,
|
|
85
|
+
data: ps_data,
|
|
86
|
+
source_format: :postscript
|
|
87
|
+
)
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
warn "Failed to convert hint to PostScript: #{e.message}"
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Convert a single hint to TrueType format
|
|
94
|
+
#
|
|
95
|
+
# @param hint [Hint] Source hint
|
|
96
|
+
# @return [Hint, nil] Converted hint or nil if incompatible
|
|
97
|
+
def convert_hint_to_truetype(hint)
|
|
98
|
+
return nil unless hint.compatible_with?(:truetype)
|
|
99
|
+
|
|
100
|
+
# Get TrueType representation from hint
|
|
101
|
+
tt_instructions = hint.to_truetype
|
|
102
|
+
|
|
103
|
+
# Create new hint with TrueType format
|
|
104
|
+
Models::Hint.new(
|
|
105
|
+
type: hint.type,
|
|
106
|
+
data: { instructions: tt_instructions },
|
|
107
|
+
source_format: :truetype
|
|
108
|
+
)
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
warn "Failed to convert hint to TrueType: #{e.message}"
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Remove conflicting hints from set
|
|
115
|
+
#
|
|
116
|
+
# @param hints [Array<Hint>] Hints to check
|
|
117
|
+
# @return [Array<Hint>] Non-conflicting hints
|
|
118
|
+
def remove_conflicts(hints)
|
|
119
|
+
non_conflicting = []
|
|
120
|
+
|
|
121
|
+
hints.each do |hint|
|
|
122
|
+
# Check if this hint conflicts with any already selected
|
|
123
|
+
conflicts = non_conflicting.any? do |existing|
|
|
124
|
+
hints_conflict?(hint, existing)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
non_conflicting << hint unless conflicts
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
non_conflicting
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if two hints conflict
|
|
134
|
+
#
|
|
135
|
+
# @param hint1 [Hint] First hint
|
|
136
|
+
# @param hint2 [Hint] Second hint
|
|
137
|
+
# @return [Boolean] True if hints conflict
|
|
138
|
+
def hints_conflict?(hint1, hint2)
|
|
139
|
+
# Hints of different types don't conflict
|
|
140
|
+
return false if hint1.type != hint2.type
|
|
141
|
+
|
|
142
|
+
case hint1.type
|
|
143
|
+
when :stem
|
|
144
|
+
# Stem hints conflict if they overlap
|
|
145
|
+
stems_overlap?(hint1.data, hint2.data)
|
|
146
|
+
when :interpolate
|
|
147
|
+
# Multiple interpolation hints on same axis conflict
|
|
148
|
+
hint1.data[:axis] == hint2.data[:axis]
|
|
149
|
+
else
|
|
150
|
+
# Other hint types don't conflict
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check if two stem hints overlap
|
|
156
|
+
#
|
|
157
|
+
# @param stem1 [Hash] First stem data
|
|
158
|
+
# @param stem2 [Hash] Second stem data
|
|
159
|
+
# @return [Boolean] True if stems overlap
|
|
160
|
+
def stems_overlap?(stem1, stem2)
|
|
161
|
+
# Must be same orientation to conflict
|
|
162
|
+
return false if stem1[:orientation] != stem2[:orientation]
|
|
163
|
+
|
|
164
|
+
pos1 = stem1[:position] || 0
|
|
165
|
+
width1 = stem1[:width] || 0
|
|
166
|
+
pos2 = stem2[:position] || 0
|
|
167
|
+
width2 = stem2[:width] || 0
|
|
168
|
+
|
|
169
|
+
# Check if ranges overlap
|
|
170
|
+
end1 = pos1 + width1
|
|
171
|
+
end2 = pos2 + width2
|
|
172
|
+
|
|
173
|
+
pos1 < end2 && pos2 < end1
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|