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,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
# Loading modes module that defines which tables are loaded in each mode.
|
|
5
|
+
#
|
|
6
|
+
# This module provides a MECE (Mutually Exclusive, Collectively Exhaustive)
|
|
7
|
+
# architecture for font loading modes. Each mode defines a specific set of
|
|
8
|
+
# tables to load, enabling efficient parsing for different use cases.
|
|
9
|
+
#
|
|
10
|
+
# @example Using metadata mode
|
|
11
|
+
# mode = LoadingModes::METADATA
|
|
12
|
+
# tables = LoadingModes.tables_for(mode) # => ["name", "head", "hhea", "maxp", "OS/2", "post"]
|
|
13
|
+
#
|
|
14
|
+
# @example Checking table availability
|
|
15
|
+
# LoadingModes.table_allowed?(:metadata, "GSUB") # => false
|
|
16
|
+
# LoadingModes.table_allowed?(:full, "GSUB") # => true
|
|
17
|
+
module LoadingModes
|
|
18
|
+
# Metadata mode: loads only tables needed for font identification and metrics
|
|
19
|
+
# Equivalent to otfinfo functionality
|
|
20
|
+
METADATA = :metadata
|
|
21
|
+
|
|
22
|
+
# Full mode: loads all tables in the font
|
|
23
|
+
FULL = :full
|
|
24
|
+
|
|
25
|
+
# Mode definitions with their respective table lists
|
|
26
|
+
MODES = {
|
|
27
|
+
METADATA => {
|
|
28
|
+
tables: %w[name head hhea maxp OS/2 post].freeze,
|
|
29
|
+
description: "Metadata mode - loads only identification and metrics tables (otfinfo-equivalent)"
|
|
30
|
+
}.freeze,
|
|
31
|
+
FULL => {
|
|
32
|
+
tables: :all,
|
|
33
|
+
description: "Full mode - loads all tables in the font"
|
|
34
|
+
}.freeze
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# Pre-computed Set for O(1) lookup of metadata tables
|
|
38
|
+
# This constant avoids recreating the Set on every font load
|
|
39
|
+
METADATA_TABLES_SET = MODES[METADATA][:tables].to_set.freeze
|
|
40
|
+
|
|
41
|
+
# Get the list of tables allowed for a given mode
|
|
42
|
+
#
|
|
43
|
+
# @param mode [Symbol] The loading mode (:metadata or :full)
|
|
44
|
+
# @return [Array<String>, Symbol] Array of table tags or :all for full mode
|
|
45
|
+
# @raise [ArgumentError] if mode is invalid
|
|
46
|
+
def self.tables_for(mode)
|
|
47
|
+
validate_mode!(mode)
|
|
48
|
+
MODES[mode][:tables]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check if a table is allowed in a given mode
|
|
52
|
+
#
|
|
53
|
+
# @param mode [Symbol] The loading mode (:metadata or :full)
|
|
54
|
+
# @param tag [String] The table tag to check
|
|
55
|
+
# @return [Boolean] true if table is allowed in the mode
|
|
56
|
+
# @raise [ArgumentError] if mode is invalid
|
|
57
|
+
def self.table_allowed?(mode, tag)
|
|
58
|
+
validate_mode!(mode)
|
|
59
|
+
|
|
60
|
+
tables = MODES[mode][:tables]
|
|
61
|
+
return true if tables == :all
|
|
62
|
+
|
|
63
|
+
tables.include?(tag)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Validate that a mode is valid
|
|
67
|
+
#
|
|
68
|
+
# @param mode [Symbol] The mode to validate
|
|
69
|
+
# @return [Boolean] true if mode is valid
|
|
70
|
+
def self.valid_mode?(mode)
|
|
71
|
+
MODES.key?(mode)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get the default lazy loading setting for a mode
|
|
75
|
+
#
|
|
76
|
+
# @param mode [Symbol] The loading mode
|
|
77
|
+
# @return [Boolean] true if lazy loading is recommended for this mode
|
|
78
|
+
# @raise [ArgumentError] if mode is invalid
|
|
79
|
+
def self.default_lazy?(mode)
|
|
80
|
+
validate_mode!(mode)
|
|
81
|
+
true # Lazy loading is recommended for all modes
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get mode description
|
|
85
|
+
#
|
|
86
|
+
# @param mode [Symbol] The loading mode
|
|
87
|
+
# @return [String] Description of the mode
|
|
88
|
+
# @raise [ArgumentError] if mode is invalid
|
|
89
|
+
def self.description(mode)
|
|
90
|
+
validate_mode!(mode)
|
|
91
|
+
MODES[mode][:description]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get all available modes
|
|
95
|
+
#
|
|
96
|
+
# @return [Array<Symbol>] List of all mode symbols
|
|
97
|
+
def self.all_modes
|
|
98
|
+
MODES.keys
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Validate mode and raise error if invalid
|
|
102
|
+
#
|
|
103
|
+
# @param mode [Symbol] The mode to validate
|
|
104
|
+
# @return [void]
|
|
105
|
+
# @raise [ArgumentError] if mode is invalid
|
|
106
|
+
def self.validate_mode!(mode)
|
|
107
|
+
return if valid_mode?(mode)
|
|
108
|
+
|
|
109
|
+
raise ArgumentError,
|
|
110
|
+
"Invalid mode: #{mode.inspect}. Valid modes are: #{all_modes.map(&:inspect).join(', ')}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
# High-level utility class for accessing font metrics
|
|
7
|
+
#
|
|
8
|
+
# MetricsCalculator provides a convenient API for querying font metrics from
|
|
9
|
+
# multiple OpenType tables without needing to work with the low-level table
|
|
10
|
+
# structures directly. It wraps access to hhea, hmtx, head, maxp, and cmap
|
|
11
|
+
# tables.
|
|
12
|
+
#
|
|
13
|
+
# The calculator handles missing tables gracefully and provides both
|
|
14
|
+
# individual glyph metrics and string-level calculations.
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# font = FontLoader.from_file("path/to/font.ttf")
|
|
18
|
+
# calc = MetricsCalculator.new(font)
|
|
19
|
+
#
|
|
20
|
+
# puts calc.ascent # => 2048
|
|
21
|
+
# puts calc.descent # => -512
|
|
22
|
+
# puts calc.line_height # => 2650
|
|
23
|
+
# puts calc.units_per_em # => 2048
|
|
24
|
+
#
|
|
25
|
+
# @example Glyph metrics
|
|
26
|
+
# width = calc.glyph_width(42)
|
|
27
|
+
# lsb = calc.glyph_left_side_bearing(42)
|
|
28
|
+
#
|
|
29
|
+
# @example String width calculation
|
|
30
|
+
# width = calc.string_width("Hello")
|
|
31
|
+
#
|
|
32
|
+
# @example Checking for metrics support
|
|
33
|
+
# if calc.has_metrics?
|
|
34
|
+
# puts "Font has complete horizontal metrics"
|
|
35
|
+
# end
|
|
36
|
+
class MetricsCalculator
|
|
37
|
+
# The font object this calculator operates on
|
|
38
|
+
#
|
|
39
|
+
# @return [OpenTypeFont, TrueTypeFont] The font instance
|
|
40
|
+
attr_reader :font
|
|
41
|
+
|
|
42
|
+
# Initialize a new MetricsCalculator
|
|
43
|
+
#
|
|
44
|
+
# @param font [OpenTypeFont, TrueTypeFont] Font instance to calculate metrics for
|
|
45
|
+
# @raise [ArgumentError] if font is nil
|
|
46
|
+
def initialize(font)
|
|
47
|
+
raise ArgumentError, "Font cannot be nil" if font.nil?
|
|
48
|
+
|
|
49
|
+
@font = font
|
|
50
|
+
@hhea_table = nil
|
|
51
|
+
@hmtx_table = nil
|
|
52
|
+
@head_table = nil
|
|
53
|
+
@maxp_table = nil
|
|
54
|
+
@cmap_table = nil
|
|
55
|
+
@hmtx_parsed = false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get typographic ascent from hhea table
|
|
59
|
+
#
|
|
60
|
+
# The ascent is the distance from the baseline to the highest ascender.
|
|
61
|
+
# It is a positive value in font units (FUnits).
|
|
62
|
+
#
|
|
63
|
+
# @return [Integer, nil] Ascent value in FUnits, or nil if hhea table is missing
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# calc.ascent # => 2048
|
|
67
|
+
def ascent
|
|
68
|
+
hhea&.ascent
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get typographic descent from hhea table
|
|
72
|
+
#
|
|
73
|
+
# The descent is the distance from the baseline to the lowest descender.
|
|
74
|
+
# It is typically a negative value in font units (FUnits).
|
|
75
|
+
#
|
|
76
|
+
# @return [Integer, nil] Descent value in FUnits, or nil if hhea table is missing
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# calc.descent # => -512
|
|
80
|
+
def descent
|
|
81
|
+
hhea&.descent
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get line gap from hhea table
|
|
85
|
+
#
|
|
86
|
+
# The line gap is additional vertical space between lines of text.
|
|
87
|
+
# It is a non-negative value in font units (FUnits).
|
|
88
|
+
#
|
|
89
|
+
# @return [Integer, nil] Line gap value in FUnits, or nil if hhea table is missing
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# calc.line_gap # => 90
|
|
93
|
+
def line_gap
|
|
94
|
+
hhea&.line_gap
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get units per em from head table
|
|
98
|
+
#
|
|
99
|
+
# This value defines the font's coordinate system scale. Common values
|
|
100
|
+
# are 1000 (PostScript fonts) or 2048 (TrueType fonts).
|
|
101
|
+
#
|
|
102
|
+
# @return [Integer, nil] Units per em value, or nil if head table is missing
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# calc.units_per_em # => 2048
|
|
106
|
+
def units_per_em
|
|
107
|
+
head&.units_per_em
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get advance width for a specific glyph
|
|
111
|
+
#
|
|
112
|
+
# The advance width is the horizontal distance to advance the pen position
|
|
113
|
+
# after rendering this glyph. It is in font units (FUnits).
|
|
114
|
+
#
|
|
115
|
+
# @param glyph_id [Integer] The glyph ID (0-based)
|
|
116
|
+
# @return [Integer, nil] Advance width in FUnits, or nil if not available
|
|
117
|
+
#
|
|
118
|
+
# @example
|
|
119
|
+
# calc.glyph_width(42) # => 1234
|
|
120
|
+
def glyph_width(glyph_id)
|
|
121
|
+
ensure_hmtx_parsed
|
|
122
|
+
return nil unless hmtx
|
|
123
|
+
|
|
124
|
+
metric = hmtx.metric_for(glyph_id)
|
|
125
|
+
metric&.dig(:advance_width)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Alias for {#glyph_width}
|
|
129
|
+
#
|
|
130
|
+
# @param glyph_id [Integer] The glyph ID (0-based)
|
|
131
|
+
# @return [Integer, nil] Advance width in FUnits, or nil if not available
|
|
132
|
+
alias glyph_advance_width glyph_width
|
|
133
|
+
|
|
134
|
+
# Get left side bearing for a specific glyph
|
|
135
|
+
#
|
|
136
|
+
# The left side bearing (LSB) is the horizontal distance from the pen
|
|
137
|
+
# position to the leftmost point of the glyph. It can be negative if
|
|
138
|
+
# the glyph extends to the left of the pen position.
|
|
139
|
+
#
|
|
140
|
+
# @param glyph_id [Integer] The glyph ID (0-based)
|
|
141
|
+
# @return [Integer, nil] Left side bearing in FUnits, or nil if not available
|
|
142
|
+
#
|
|
143
|
+
# @example
|
|
144
|
+
# calc.glyph_left_side_bearing(42) # => 50
|
|
145
|
+
def glyph_left_side_bearing(glyph_id)
|
|
146
|
+
ensure_hmtx_parsed
|
|
147
|
+
return nil unless hmtx
|
|
148
|
+
|
|
149
|
+
metric = hmtx.metric_for(glyph_id)
|
|
150
|
+
metric&.dig(:lsb)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Calculate total width for a string
|
|
154
|
+
#
|
|
155
|
+
# Calculates the sum of advance widths for all characters in the string.
|
|
156
|
+
# This is a simplified calculation that does not account for kerning,
|
|
157
|
+
# ligatures, or other advanced typography features.
|
|
158
|
+
#
|
|
159
|
+
# Characters not mapped in the font are skipped.
|
|
160
|
+
#
|
|
161
|
+
# @param string [String] The string to measure
|
|
162
|
+
# @return [Integer, nil] Total width in FUnits, or nil if metrics unavailable
|
|
163
|
+
#
|
|
164
|
+
# @example
|
|
165
|
+
# calc.string_width("Hello") # => 5420
|
|
166
|
+
def string_width(string)
|
|
167
|
+
return nil unless has_metrics?
|
|
168
|
+
return 0 if string.nil? || string.empty?
|
|
169
|
+
|
|
170
|
+
total_width = 0
|
|
171
|
+
string.each_codepoint do |codepoint|
|
|
172
|
+
glyph_id = codepoint_to_glyph_id(codepoint)
|
|
173
|
+
next unless glyph_id
|
|
174
|
+
|
|
175
|
+
width = glyph_width(glyph_id)
|
|
176
|
+
total_width += width if width
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
total_width
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Calculate line height
|
|
183
|
+
#
|
|
184
|
+
# Line height is calculated as: ascent - descent + line_gap
|
|
185
|
+
# This represents the recommended spacing between consecutive baselines.
|
|
186
|
+
#
|
|
187
|
+
# @return [Integer, nil] Line height in FUnits, or nil if hhea table is missing
|
|
188
|
+
#
|
|
189
|
+
# @example
|
|
190
|
+
# calc.line_height # => 2650 (when ascent=2048, descent=-512, line_gap=90)
|
|
191
|
+
def line_height
|
|
192
|
+
return nil unless hhea
|
|
193
|
+
|
|
194
|
+
ascent - descent + line_gap
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Alias for {#units_per_em}
|
|
198
|
+
#
|
|
199
|
+
# @return [Integer, nil] Units per em value, or nil if head table is missing
|
|
200
|
+
alias em_height units_per_em
|
|
201
|
+
|
|
202
|
+
# Check if font has complete horizontal metrics
|
|
203
|
+
#
|
|
204
|
+
# Returns true if the font has all required tables for horizontal metrics:
|
|
205
|
+
# hhea, hmtx, head, and maxp tables.
|
|
206
|
+
#
|
|
207
|
+
# @return [Boolean] True if all metrics tables are present
|
|
208
|
+
#
|
|
209
|
+
# @example
|
|
210
|
+
# calc.has_metrics? # => true
|
|
211
|
+
def has_metrics?
|
|
212
|
+
!hhea.nil? && !hmtx.nil? && !head.nil? && !maxp.nil?
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
# Get hhea table, caching the result
|
|
218
|
+
#
|
|
219
|
+
# @return [Tables::Hhea, nil] The hhea table or nil
|
|
220
|
+
def hhea
|
|
221
|
+
@hhea ||= font.table(Constants::HHEA_TAG)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Get hmtx table, caching the result
|
|
225
|
+
#
|
|
226
|
+
# @return [Tables::Hmtx, nil] The hmtx table or nil
|
|
227
|
+
def hmtx
|
|
228
|
+
@hmtx ||= font.table(Constants::HMTX_TAG)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Get head table, caching the result
|
|
232
|
+
#
|
|
233
|
+
# @return [Tables::Head, nil] The head table or nil
|
|
234
|
+
def head
|
|
235
|
+
@head ||= font.table(Constants::HEAD_TAG)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Get maxp table, caching the result
|
|
239
|
+
#
|
|
240
|
+
# @return [Tables::Maxp, nil] The maxp table or nil
|
|
241
|
+
def maxp
|
|
242
|
+
@maxp ||= font.table(Constants::MAXP_TAG)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Get cmap table, caching the result
|
|
246
|
+
#
|
|
247
|
+
# @return [Tables::Cmap, nil] The cmap table or nil
|
|
248
|
+
def cmap
|
|
249
|
+
@cmap ||= font.table(Constants::CMAP_TAG)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Ensure hmtx table is parsed with context
|
|
253
|
+
#
|
|
254
|
+
# The hmtx table requires numberOfHMetrics from hhea and numGlyphs from maxp
|
|
255
|
+
# to be parsed correctly. This method ensures parsing happens lazily on first use.
|
|
256
|
+
#
|
|
257
|
+
# @return [void]
|
|
258
|
+
def ensure_hmtx_parsed
|
|
259
|
+
return if @hmtx_parsed
|
|
260
|
+
return unless hmtx && hhea && maxp
|
|
261
|
+
|
|
262
|
+
hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
|
|
263
|
+
@hmtx_parsed = true
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Map Unicode codepoint to glyph ID using cmap table
|
|
267
|
+
#
|
|
268
|
+
# @param codepoint [Integer] Unicode codepoint
|
|
269
|
+
# @return [Integer, nil] Glyph ID or nil if not mapped
|
|
270
|
+
def codepoint_to_glyph_id(codepoint)
|
|
271
|
+
return nil unless cmap
|
|
272
|
+
|
|
273
|
+
mappings = cmap.unicode_mappings
|
|
274
|
+
mappings[codepoint]
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Models
|
|
7
|
+
# Model for individual font summary within a collection
|
|
8
|
+
#
|
|
9
|
+
# Represents basic metadata for a single font in a TTC/OTC collection.
|
|
10
|
+
# Used by CollectionListInfo to provide per-font summaries.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating a font summary
|
|
13
|
+
# summary = CollectionFontSummary.new(
|
|
14
|
+
# index: 0,
|
|
15
|
+
# family_name: "Helvetica",
|
|
16
|
+
# subfamily_name: "Regular",
|
|
17
|
+
# postscript_name: "Helvetica-Regular",
|
|
18
|
+
# font_format: "TrueType",
|
|
19
|
+
# num_glyphs: 268,
|
|
20
|
+
# num_tables: 14
|
|
21
|
+
# )
|
|
22
|
+
class CollectionFontSummary < Lutaml::Model::Serializable
|
|
23
|
+
attribute :index, :integer
|
|
24
|
+
attribute :family_name, :string
|
|
25
|
+
attribute :subfamily_name, :string
|
|
26
|
+
attribute :postscript_name, :string
|
|
27
|
+
attribute :font_format, :string
|
|
28
|
+
attribute :num_glyphs, :integer
|
|
29
|
+
attribute :num_tables, :integer
|
|
30
|
+
|
|
31
|
+
yaml do
|
|
32
|
+
map "index", to: :index
|
|
33
|
+
map "family_name", to: :family_name
|
|
34
|
+
map "subfamily_name", to: :subfamily_name
|
|
35
|
+
map "postscript_name", to: :postscript_name
|
|
36
|
+
map "font_format", to: :font_format
|
|
37
|
+
map "num_glyphs", to: :num_glyphs
|
|
38
|
+
map "num_tables", to: :num_tables
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
json do
|
|
42
|
+
map "index", to: :index
|
|
43
|
+
map "family_name", to: :family_name
|
|
44
|
+
map "subfamily_name", to: :subfamily_name
|
|
45
|
+
map "postscript_name", to: :postscript_name
|
|
46
|
+
map "font_format", to: :font_format
|
|
47
|
+
map "num_glyphs", to: :num_glyphs
|
|
48
|
+
map "num_tables", to: :num_tables
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
require_relative "table_sharing_info"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Models
|
|
8
|
+
# Model for collection metadata
|
|
9
|
+
#
|
|
10
|
+
# Represents comprehensive information about a TTC/OTC collection.
|
|
11
|
+
# Used by InfoCommand when operating on collection files.
|
|
12
|
+
#
|
|
13
|
+
# @example Creating collection info
|
|
14
|
+
# info = CollectionInfo.new(
|
|
15
|
+
# collection_path: "fonts.ttc",
|
|
16
|
+
# collection_format: "TTC",
|
|
17
|
+
# ttc_tag: "ttcf",
|
|
18
|
+
# major_version: 2,
|
|
19
|
+
# minor_version: 0,
|
|
20
|
+
# num_fonts: 6,
|
|
21
|
+
# font_offsets: [48, 380, 712, 1044, 1376, 1676],
|
|
22
|
+
# file_size_bytes: 2240000,
|
|
23
|
+
# table_sharing: table_sharing_obj
|
|
24
|
+
# )
|
|
25
|
+
class CollectionInfo < Lutaml::Model::Serializable
|
|
26
|
+
attribute :collection_path, :string
|
|
27
|
+
attribute :collection_format, :string
|
|
28
|
+
attribute :ttc_tag, :string
|
|
29
|
+
attribute :major_version, :integer
|
|
30
|
+
attribute :minor_version, :integer
|
|
31
|
+
attribute :num_fonts, :integer
|
|
32
|
+
attribute :font_offsets, :integer, collection: true
|
|
33
|
+
attribute :file_size_bytes, :integer
|
|
34
|
+
attribute :table_sharing, TableSharingInfo
|
|
35
|
+
|
|
36
|
+
yaml do
|
|
37
|
+
map "collection_path", to: :collection_path
|
|
38
|
+
map "collection_format", to: :collection_format
|
|
39
|
+
map "ttc_tag", to: :ttc_tag
|
|
40
|
+
map "major_version", to: :major_version
|
|
41
|
+
map "minor_version", to: :minor_version
|
|
42
|
+
map "num_fonts", to: :num_fonts
|
|
43
|
+
map "font_offsets", to: :font_offsets
|
|
44
|
+
map "file_size_bytes", to: :file_size_bytes
|
|
45
|
+
map "table_sharing", to: :table_sharing
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
json do
|
|
49
|
+
map "collection_path", to: :collection_path
|
|
50
|
+
map "collection_format", to: :collection_format
|
|
51
|
+
map "ttc_tag", to: :ttc_tag
|
|
52
|
+
map "major_version", to: :major_version
|
|
53
|
+
map "minor_version", to: :minor_version
|
|
54
|
+
map "num_fonts", to: :num_fonts
|
|
55
|
+
map "font_offsets", to: :font_offsets
|
|
56
|
+
map "file_size_bytes", to: :file_size_bytes
|
|
57
|
+
map "table_sharing", to: :table_sharing
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get version as a formatted string
|
|
61
|
+
#
|
|
62
|
+
# @return [String] Version string (e.g., "2.0")
|
|
63
|
+
def version_string
|
|
64
|
+
"#{major_version}.#{minor_version}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get version as a hexadecimal string
|
|
68
|
+
#
|
|
69
|
+
# @return [String] Hex version (e.g., "0x00020000")
|
|
70
|
+
def version_hex
|
|
71
|
+
version_int = (major_version << 16) | minor_version
|
|
72
|
+
format("0x%08X", version_int)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
require_relative "collection_font_summary"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Models
|
|
8
|
+
# Model for collection font listing
|
|
9
|
+
#
|
|
10
|
+
# Represents a list of fonts within a TTC/OTC collection.
|
|
11
|
+
# Used by LsCommand when operating on collection files.
|
|
12
|
+
#
|
|
13
|
+
# @example Creating a collection list
|
|
14
|
+
# list = CollectionListInfo.new(
|
|
15
|
+
# collection_path: "fonts.ttc",
|
|
16
|
+
# num_fonts: 6,
|
|
17
|
+
# fonts: [summary1, summary2, ...]
|
|
18
|
+
# )
|
|
19
|
+
class CollectionListInfo < Lutaml::Model::Serializable
|
|
20
|
+
attribute :collection_path, :string
|
|
21
|
+
attribute :num_fonts, :integer
|
|
22
|
+
attribute :fonts, CollectionFontSummary, collection: true
|
|
23
|
+
|
|
24
|
+
yaml do
|
|
25
|
+
map "collection_path", to: :collection_path
|
|
26
|
+
map "num_fonts", to: :num_fonts
|
|
27
|
+
map "fonts", to: :fonts
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
json do
|
|
31
|
+
map "collection_path", to: :collection_path
|
|
32
|
+
map "num_fonts", to: :num_fonts
|
|
33
|
+
map "fonts", to: :fonts
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|