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,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Svg
|
|
5
|
+
# Calculates SVG viewBox and handles coordinate transformations
|
|
6
|
+
#
|
|
7
|
+
# [`ViewBoxCalculator`](lib/fontisan/svg/view_box_calculator.rb) manages
|
|
8
|
+
# the coordinate system transformation between font space and SVG space.
|
|
9
|
+
# Font coordinates use a Y-up system (ascender is positive), while SVG
|
|
10
|
+
# uses Y-down (origin at top-left).
|
|
11
|
+
#
|
|
12
|
+
# Responsibilities:
|
|
13
|
+
# - Calculate appropriate viewBox for glyphs
|
|
14
|
+
# - Transform Y-coordinates (flip Y-axis)
|
|
15
|
+
# - Scale coordinates based on units-per-em
|
|
16
|
+
# - Provide consistent coordinate mapping
|
|
17
|
+
#
|
|
18
|
+
# This is a pure utility class with no state or side effects.
|
|
19
|
+
#
|
|
20
|
+
# @example Transform a Y coordinate
|
|
21
|
+
# calculator = ViewBoxCalculator.new(units_per_em: 1000, ascent: 800, descent: -200)
|
|
22
|
+
# svg_y = calculator.transform_y(700) # Font Y to SVG Y
|
|
23
|
+
#
|
|
24
|
+
# @example Calculate viewBox for a glyph
|
|
25
|
+
# viewbox = calculator.calculate_viewbox(x_min: 100, y_min: 0, x_max: 600, y_max: 700)
|
|
26
|
+
# # => "100 100 500 700"
|
|
27
|
+
class ViewBoxCalculator
|
|
28
|
+
# @return [Integer] Units per em from font
|
|
29
|
+
attr_reader :units_per_em
|
|
30
|
+
|
|
31
|
+
# @return [Integer] Font ascent
|
|
32
|
+
attr_reader :ascent
|
|
33
|
+
|
|
34
|
+
# @return [Integer] Font descent (typically negative)
|
|
35
|
+
attr_reader :descent
|
|
36
|
+
|
|
37
|
+
# Initialize calculator with font metrics
|
|
38
|
+
#
|
|
39
|
+
# @param units_per_em [Integer] Units per em from font head table
|
|
40
|
+
# @param ascent [Integer] Font ascent from hhea table
|
|
41
|
+
# @param descent [Integer] Font descent from hhea table (typically negative)
|
|
42
|
+
# @raise [ArgumentError] If parameters are invalid
|
|
43
|
+
def initialize(units_per_em:, ascent:, descent:)
|
|
44
|
+
validate_parameters!(units_per_em, ascent, descent)
|
|
45
|
+
|
|
46
|
+
@units_per_em = units_per_em
|
|
47
|
+
@ascent = ascent
|
|
48
|
+
@descent = descent
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Transform Y coordinate from font space to SVG space
|
|
52
|
+
#
|
|
53
|
+
# Font space: Y-up (ascender positive, descender negative)
|
|
54
|
+
# SVG space: Y-down (origin at top)
|
|
55
|
+
#
|
|
56
|
+
# Transformation: svg_y = ascent - font_y
|
|
57
|
+
#
|
|
58
|
+
# @param font_y [Numeric] Y coordinate in font space
|
|
59
|
+
# @return [Numeric] Y coordinate in SVG space
|
|
60
|
+
def transform_y(font_y)
|
|
61
|
+
ascent - font_y
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Transform point from font space to SVG space
|
|
65
|
+
#
|
|
66
|
+
# @param font_x [Numeric] X coordinate in font space
|
|
67
|
+
# @param font_y [Numeric] Y coordinate in font space
|
|
68
|
+
# @return [Array<Numeric>] [svg_x, svg_y]
|
|
69
|
+
def transform_point(font_x, font_y)
|
|
70
|
+
[font_x, transform_y(font_y)]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Calculate viewBox string for SVG
|
|
74
|
+
#
|
|
75
|
+
# @param x_min [Numeric] Minimum X coordinate
|
|
76
|
+
# @param y_min [Numeric] Minimum Y coordinate
|
|
77
|
+
# @param x_max [Numeric] Maximum X coordinate
|
|
78
|
+
# @param y_max [Numeric] Maximum Y coordinate
|
|
79
|
+
# @return [String] ViewBox string "x y width height"
|
|
80
|
+
def calculate_viewbox(x_min:, y_min:, x_max:, y_max:)
|
|
81
|
+
# Transform bounding box to SVG space
|
|
82
|
+
svg_y_min = transform_y(y_max) # Y is flipped
|
|
83
|
+
svg_y_max = transform_y(y_min)
|
|
84
|
+
|
|
85
|
+
width = x_max - x_min
|
|
86
|
+
height = svg_y_max - svg_y_min
|
|
87
|
+
|
|
88
|
+
"#{x_min} #{svg_y_min} #{width} #{height}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Calculate font-level viewBox
|
|
92
|
+
#
|
|
93
|
+
# Uses font metrics to create a viewBox covering the entire font space
|
|
94
|
+
#
|
|
95
|
+
# @return [String] ViewBox string for entire font
|
|
96
|
+
def font_viewbox
|
|
97
|
+
# Typical font viewBox covers descent to ascent
|
|
98
|
+
# Width is units_per_em
|
|
99
|
+
height = ascent - descent
|
|
100
|
+
"0 0 #{units_per_em} #{height}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get scale factor for coordinate precision
|
|
104
|
+
#
|
|
105
|
+
# @param target_units [Integer] Target units per em (default 1000)
|
|
106
|
+
# @return [Float] Scale factor
|
|
107
|
+
def scale_factor(target_units: 1000)
|
|
108
|
+
target_units.to_f / units_per_em
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
# Validate initialization parameters
|
|
114
|
+
#
|
|
115
|
+
# @param units_per_em [Integer] Units per em
|
|
116
|
+
# @param ascent [Integer] Font ascent
|
|
117
|
+
# @param descent [Integer] Font descent
|
|
118
|
+
# @raise [ArgumentError] If validation fails
|
|
119
|
+
def validate_parameters!(units_per_em, ascent, descent)
|
|
120
|
+
unless units_per_em.is_a?(Integer) && units_per_em.positive?
|
|
121
|
+
raise ArgumentError,
|
|
122
|
+
"units_per_em must be a positive Integer, got: #{units_per_em.inspect}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
unless ascent.is_a?(Integer)
|
|
126
|
+
raise ArgumentError,
|
|
127
|
+
"ascent must be an Integer, got: #{ascent.inspect}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
unless descent.is_a?(Integer)
|
|
131
|
+
raise ArgumentError,
|
|
132
|
+
"descent must be an Integer, got: #{descent.inspect}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
class Cff
|
|
6
|
+
# Wrapper class for CFF glyph data
|
|
7
|
+
#
|
|
8
|
+
# [`CFFGlyph`](lib/fontisan/tables/cff/cff_glyph.rb) provides a unified
|
|
9
|
+
# interface for CFF glyphs that matches the API of TrueType glyphs
|
|
10
|
+
# ([`SimpleGlyph`](lib/fontisan/tables/glyf/simple_glyph.rb) and
|
|
11
|
+
# [`CompoundGlyph`](lib/fontisan/tables/glyf/compound_glyph.rb)).
|
|
12
|
+
#
|
|
13
|
+
# This allows [`GlyphAccessor`](lib/fontisan/glyph_accessor.rb) to work
|
|
14
|
+
# transparently with both TrueType (glyf) and OpenType/CFF fonts.
|
|
15
|
+
#
|
|
16
|
+
# CFF Glyph Characteristics:
|
|
17
|
+
# - Always "simple" (no composite structure like TrueType compound glyphs)
|
|
18
|
+
# - Outline data stored as Type 2 CharString programs
|
|
19
|
+
# - Width information embedded in CharString
|
|
20
|
+
# - Glyph names from Charset
|
|
21
|
+
#
|
|
22
|
+
# @example Accessing a CFF glyph
|
|
23
|
+
# cff = font.table("CFF ")
|
|
24
|
+
# charstring = cff.charstring_for_glyph(42)
|
|
25
|
+
# glyph = CFFGlyph.new(42, charstring, cff.charset, cff.encoding)
|
|
26
|
+
#
|
|
27
|
+
# puts glyph.name # => "A"
|
|
28
|
+
# puts glyph.width # => 500
|
|
29
|
+
# puts glyph.bounding_box # => [10, 0, 490, 700]
|
|
30
|
+
# puts glyph.simple? # => true
|
|
31
|
+
# puts glyph.compound? # => false
|
|
32
|
+
#
|
|
33
|
+
# Reference: [`docs/ttfunk-feature-analysis.md:541-575`](docs/ttfunk-feature-analysis.md:541)
|
|
34
|
+
class CFFGlyph
|
|
35
|
+
# @return [Integer] Glyph ID (GID)
|
|
36
|
+
attr_reader :glyph_id
|
|
37
|
+
|
|
38
|
+
# @return [CharString] Interpreted CharString with path data
|
|
39
|
+
attr_reader :charstring
|
|
40
|
+
|
|
41
|
+
# Initialize a CFF glyph wrapper
|
|
42
|
+
#
|
|
43
|
+
# @param glyph_id [Integer] Glyph ID (0-based, 0 is .notdef)
|
|
44
|
+
# @param charstring [CharString] Interpreted CharString object
|
|
45
|
+
# @param charset [Charset] Charset for name lookup
|
|
46
|
+
# @param encoding [Encoding, nil] Encoding (optional, for character code
|
|
47
|
+
# mapping)
|
|
48
|
+
def initialize(glyph_id, charstring, charset, encoding = nil)
|
|
49
|
+
@glyph_id = glyph_id
|
|
50
|
+
@charstring = charstring
|
|
51
|
+
@charset = charset
|
|
52
|
+
@encoding = encoding
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if this is a simple glyph
|
|
56
|
+
#
|
|
57
|
+
# CFF glyphs are conceptually "simple" - they don't have the composite
|
|
58
|
+
# structure that TrueType compound glyphs have. While CFF CharStrings
|
|
59
|
+
# can call subroutines, these are code reuse mechanisms, not glyph
|
|
60
|
+
# composition.
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean] Always true for CFF glyphs
|
|
63
|
+
def simple?
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Check if this is a compound glyph
|
|
68
|
+
#
|
|
69
|
+
# CFF glyphs don't have components like TrueType compound glyphs.
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean] Always false for CFF glyphs
|
|
72
|
+
def compound?
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if this glyph has no outline data
|
|
77
|
+
#
|
|
78
|
+
# A glyph is empty if its CharString path is empty (e.g., space
|
|
79
|
+
# character)
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean] True if glyph has no path data
|
|
82
|
+
def empty?
|
|
83
|
+
return true unless @charstring
|
|
84
|
+
|
|
85
|
+
@charstring.path.empty?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Get the bounding box for this glyph
|
|
89
|
+
#
|
|
90
|
+
# Returns the glyph's bounding box in font units as calculated from
|
|
91
|
+
# the CharString path.
|
|
92
|
+
#
|
|
93
|
+
# @return [Array<Float>, nil] [xMin, yMin, xMax, yMax] or nil if empty
|
|
94
|
+
def bounding_box
|
|
95
|
+
return nil unless @charstring
|
|
96
|
+
|
|
97
|
+
@charstring.bounding_box
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get the advance width for this glyph
|
|
101
|
+
#
|
|
102
|
+
# Returns the glyph's advance width from the CharString.
|
|
103
|
+
#
|
|
104
|
+
# @return [Integer, nil] Advance width in font units, or nil if not
|
|
105
|
+
# available
|
|
106
|
+
def width
|
|
107
|
+
return nil unless @charstring
|
|
108
|
+
|
|
109
|
+
@charstring.width
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get the PostScript glyph name
|
|
113
|
+
#
|
|
114
|
+
# Looks up the glyph name from the Charset using the glyph ID.
|
|
115
|
+
#
|
|
116
|
+
# @return [String] Glyph name (e.g., "A", "Aacute", ".notdef")
|
|
117
|
+
def name
|
|
118
|
+
return ".notdef" unless @charset
|
|
119
|
+
|
|
120
|
+
@charset.glyph_name(@glyph_id) || ".notdef"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Convert the glyph outline to drawing commands
|
|
124
|
+
#
|
|
125
|
+
# Returns an array of drawing commands that can be used to render
|
|
126
|
+
# the glyph outline.
|
|
127
|
+
#
|
|
128
|
+
# @return [Array<Array>] Array of command arrays:
|
|
129
|
+
# - [:move_to, x, y]
|
|
130
|
+
# - [:line_to, x, y]
|
|
131
|
+
# - [:curve_to, x1, y1, x2, y2, x, y]
|
|
132
|
+
#
|
|
133
|
+
# @example Rendering a glyph
|
|
134
|
+
# glyph.to_commands.each do |cmd|
|
|
135
|
+
# case cmd[0]
|
|
136
|
+
# when :move_to
|
|
137
|
+
# canvas.move_to(cmd[1], cmd[2])
|
|
138
|
+
# when :line_to
|
|
139
|
+
# canvas.line_to(cmd[1], cmd[2])
|
|
140
|
+
# when :curve_to
|
|
141
|
+
# canvas.curve_to(cmd[1], cmd[2], cmd[3], cmd[4], cmd[5], cmd[6])
|
|
142
|
+
# end
|
|
143
|
+
# end
|
|
144
|
+
def to_commands
|
|
145
|
+
return [] unless @charstring
|
|
146
|
+
|
|
147
|
+
@charstring.to_commands
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Get the raw path data
|
|
151
|
+
#
|
|
152
|
+
# Returns the raw path array from the CharString for advanced use cases.
|
|
153
|
+
#
|
|
154
|
+
# @return [Array<Hash>] Array of path command hashes with keys:
|
|
155
|
+
# - :type (:move_to, :line_to, :curve_to)
|
|
156
|
+
# - :x, :y (coordinates)
|
|
157
|
+
# - :x1, :y1, :x2, :y2 (control points for curves)
|
|
158
|
+
def path
|
|
159
|
+
return [] unless @charstring
|
|
160
|
+
|
|
161
|
+
@charstring.path
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# String representation for debugging
|
|
165
|
+
#
|
|
166
|
+
# @return [String] Human-readable representation
|
|
167
|
+
def to_s
|
|
168
|
+
"#<#{self.class.name} gid=#{@glyph_id} name=#{name.inspect} " \
|
|
169
|
+
"width=#{width} bbox=#{bounding_box.inspect}>"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
alias inspect to_s
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../../binary/base_record"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
class Cff
|
|
9
|
+
# CFF Charset structure
|
|
10
|
+
#
|
|
11
|
+
# Charset maps glyph IDs (GIDs) to glyph names via String IDs (SIDs).
|
|
12
|
+
# GID 0 is always `.notdef` and is not included in the Charset data.
|
|
13
|
+
#
|
|
14
|
+
# Three formats:
|
|
15
|
+
# - Format 0: Array of SIDs, one per glyph (except .notdef)
|
|
16
|
+
# - Format 1: Ranges with 8-bit nLeft counts
|
|
17
|
+
# - Format 2: Ranges with 16-bit nLeft counts
|
|
18
|
+
#
|
|
19
|
+
# Predefined charsets:
|
|
20
|
+
# - 0: ISOAdobe charset (SIDs 0-228)
|
|
21
|
+
# - 1: Expert charset
|
|
22
|
+
# - 2: Expert Subset charset
|
|
23
|
+
#
|
|
24
|
+
# Reference: CFF specification section 13 "Charsets"
|
|
25
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
26
|
+
#
|
|
27
|
+
# @example Reading a Charset
|
|
28
|
+
# charset = Fontisan::Tables::Cff::Charset.new(
|
|
29
|
+
# data, num_glyphs, cff_table
|
|
30
|
+
# )
|
|
31
|
+
# puts charset.glyph_name(5) # => "A"
|
|
32
|
+
# puts charset.glyph_id("A") # => 5
|
|
33
|
+
class Charset
|
|
34
|
+
# Format identifiers
|
|
35
|
+
FORMATS = {
|
|
36
|
+
0 => :array,
|
|
37
|
+
1 => :range_8,
|
|
38
|
+
2 => :range_16,
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# Predefined charset identifiers
|
|
42
|
+
PREDEFINED = {
|
|
43
|
+
0 => :iso_adobe,
|
|
44
|
+
1 => :expert,
|
|
45
|
+
2 => :expert_subset,
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# @return [Integer] Charset format (0, 1, or 2)
|
|
49
|
+
attr_reader :format_type
|
|
50
|
+
|
|
51
|
+
# @return [Array<String>] Glyph names indexed by GID
|
|
52
|
+
attr_reader :glyph_names
|
|
53
|
+
|
|
54
|
+
# Initialize a Charset
|
|
55
|
+
#
|
|
56
|
+
# @param data [String, Integer] Binary data or predefined charset ID
|
|
57
|
+
# @param num_glyphs [Integer] Number of glyphs in the font
|
|
58
|
+
# @param cff_table [Cff] Parent CFF table for string lookup
|
|
59
|
+
def initialize(data, num_glyphs, cff_table)
|
|
60
|
+
@num_glyphs = num_glyphs
|
|
61
|
+
@cff_table = cff_table
|
|
62
|
+
@glyph_names = [".notdef"] # GID 0 is always .notdef
|
|
63
|
+
@glyph_name_to_id = { ".notdef" => 0 }
|
|
64
|
+
|
|
65
|
+
if data.is_a?(Integer) && PREDEFINED.key?(data)
|
|
66
|
+
load_predefined_charset(data)
|
|
67
|
+
else
|
|
68
|
+
@data = data
|
|
69
|
+
parse!
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get glyph name for a GID
|
|
74
|
+
#
|
|
75
|
+
# @param gid [Integer] Glyph ID
|
|
76
|
+
# @return [String, nil] Glyph name or nil if invalid GID
|
|
77
|
+
def glyph_name(gid)
|
|
78
|
+
return nil if gid.negative? || gid >= @glyph_names.size
|
|
79
|
+
|
|
80
|
+
@glyph_names[gid]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get GID for a glyph name
|
|
84
|
+
#
|
|
85
|
+
# @param name [String] Glyph name
|
|
86
|
+
# @return [Integer, nil] Glyph ID or nil if not found
|
|
87
|
+
def glyph_id(name)
|
|
88
|
+
@glyph_name_to_id[name]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get the format symbol
|
|
92
|
+
#
|
|
93
|
+
# @return [Symbol] Format identifier (:array, :range_8, :range_16, or
|
|
94
|
+
# :predefined)
|
|
95
|
+
def format
|
|
96
|
+
@format_type ? FORMATS[@format_type] : :predefined
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# Parse the Charset from binary data
|
|
102
|
+
def parse!
|
|
103
|
+
io = StringIO.new(@data)
|
|
104
|
+
@format_type = read_uint8(io)
|
|
105
|
+
|
|
106
|
+
case @format_type
|
|
107
|
+
when 0
|
|
108
|
+
parse_format_0(io)
|
|
109
|
+
when 1
|
|
110
|
+
parse_format_1(io)
|
|
111
|
+
when 2
|
|
112
|
+
parse_format_2(io)
|
|
113
|
+
else
|
|
114
|
+
raise CorruptedTableError,
|
|
115
|
+
"Invalid Charset format: #{@format_type}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
build_name_to_id_map
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
raise CorruptedTableError,
|
|
121
|
+
"Failed to parse Charset: #{e.message}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Parse Format 0: Array of SIDs
|
|
125
|
+
#
|
|
126
|
+
# Format 0 directly lists SIDs for each glyph (except .notdef at GID 0)
|
|
127
|
+
#
|
|
128
|
+
# @param io [StringIO] Input stream positioned after format byte
|
|
129
|
+
def parse_format_0(io)
|
|
130
|
+
# Read one SID per glyph (num_glyphs - 1, excluding .notdef)
|
|
131
|
+
(@num_glyphs - 1).times do
|
|
132
|
+
sid = read_uint16(io)
|
|
133
|
+
glyph_name = sid_to_glyph_name(sid)
|
|
134
|
+
@glyph_names << glyph_name
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Parse Format 1: Ranges with 8-bit counts
|
|
139
|
+
#
|
|
140
|
+
# Format 1 uses ranges: first SID, nLeft (number of consecutive SIDs)
|
|
141
|
+
#
|
|
142
|
+
# @param io [StringIO] Input stream positioned after format byte
|
|
143
|
+
def parse_format_1(io)
|
|
144
|
+
glyph_count = 1 # Start at 1 (we already have .notdef at 0)
|
|
145
|
+
|
|
146
|
+
while glyph_count < @num_glyphs
|
|
147
|
+
first_sid = read_uint16(io)
|
|
148
|
+
n_left = read_uint8(io)
|
|
149
|
+
|
|
150
|
+
# Add glyphs for this range
|
|
151
|
+
(n_left + 1).times do |i|
|
|
152
|
+
sid = first_sid + i
|
|
153
|
+
glyph_name = sid_to_glyph_name(sid)
|
|
154
|
+
@glyph_names << glyph_name
|
|
155
|
+
glyph_count += 1
|
|
156
|
+
break if glyph_count >= @num_glyphs
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Parse Format 2: Ranges with 16-bit counts
|
|
162
|
+
#
|
|
163
|
+
# Format 2 is like Format 1 but with 16-bit nLeft values
|
|
164
|
+
#
|
|
165
|
+
# @param io [StringIO] Input stream positioned after format byte
|
|
166
|
+
def parse_format_2(io)
|
|
167
|
+
glyph_count = 1 # Start at 1 (we already have .notdef at 0)
|
|
168
|
+
|
|
169
|
+
while glyph_count < @num_glyphs
|
|
170
|
+
first_sid = read_uint16(io)
|
|
171
|
+
n_left = read_uint16(io)
|
|
172
|
+
|
|
173
|
+
# Add glyphs for this range
|
|
174
|
+
(n_left + 1).times do |i|
|
|
175
|
+
sid = first_sid + i
|
|
176
|
+
glyph_name = sid_to_glyph_name(sid)
|
|
177
|
+
@glyph_names << glyph_name
|
|
178
|
+
glyph_count += 1
|
|
179
|
+
break if glyph_count >= @num_glyphs
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Load a predefined charset
|
|
185
|
+
#
|
|
186
|
+
# @param charset_id [Integer] Predefined charset ID (0, 1, or 2)
|
|
187
|
+
def load_predefined_charset(charset_id)
|
|
188
|
+
@format_type = nil # Predefined charsets don't have a format
|
|
189
|
+
|
|
190
|
+
case charset_id
|
|
191
|
+
when 0
|
|
192
|
+
load_iso_adobe_charset
|
|
193
|
+
when 1
|
|
194
|
+
load_expert_charset
|
|
195
|
+
when 2
|
|
196
|
+
load_expert_subset_charset
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
build_name_to_id_map
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Load ISOAdobe charset (SIDs 0-228)
|
|
203
|
+
#
|
|
204
|
+
# This is the standard charset containing common Latin glyphs
|
|
205
|
+
def load_iso_adobe_charset
|
|
206
|
+
# ISOAdobe charset contains SIDs 0-228
|
|
207
|
+
# For a full implementation, we would need all 229 glyphs
|
|
208
|
+
# Here we generate them from SIDs
|
|
209
|
+
(@num_glyphs - 1).times do |i|
|
|
210
|
+
sid = i + 1 # Skip 0 (.notdef)
|
|
211
|
+
break if sid > 228
|
|
212
|
+
|
|
213
|
+
@glyph_names << sid_to_glyph_name(sid)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Load Expert charset
|
|
218
|
+
#
|
|
219
|
+
# This is a special charset for expert fonts with additional glyphs
|
|
220
|
+
def load_expert_charset
|
|
221
|
+
# Expert charset contains specific SIDs for expert glyphs
|
|
222
|
+
# This is a placeholder - a full implementation would include the
|
|
223
|
+
# complete expert charset SID list from the CFF specification
|
|
224
|
+
(@num_glyphs - 1).times do |i|
|
|
225
|
+
@glyph_names << sid_to_glyph_name(i + 1)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Load Expert Subset charset
|
|
230
|
+
#
|
|
231
|
+
# This is a subset of the Expert charset
|
|
232
|
+
def load_expert_subset_charset
|
|
233
|
+
# Expert Subset contains a subset of expert glyphs
|
|
234
|
+
# This is a placeholder - a full implementation would include the
|
|
235
|
+
# complete expert subset charset SID list from the CFF specification
|
|
236
|
+
(@num_glyphs - 1).times do |i|
|
|
237
|
+
@glyph_names << sid_to_glyph_name(i + 1)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Convert SID to glyph name
|
|
242
|
+
#
|
|
243
|
+
# @param sid [Integer] String ID
|
|
244
|
+
# @return [String] Glyph name
|
|
245
|
+
def sid_to_glyph_name(sid)
|
|
246
|
+
@cff_table.string_for_sid(sid) || ".notdef"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Build the name-to-ID lookup map
|
|
250
|
+
def build_name_to_id_map
|
|
251
|
+
@glyph_names.each_with_index do |name, gid|
|
|
252
|
+
@glyph_name_to_id[name] = gid
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Read an unsigned 8-bit integer
|
|
257
|
+
#
|
|
258
|
+
# @param io [StringIO] Input stream
|
|
259
|
+
# @return [Integer] The value
|
|
260
|
+
def read_uint8(io)
|
|
261
|
+
byte = io.read(1)
|
|
262
|
+
raise CorruptedTableError, "Unexpected end of Charset data" if
|
|
263
|
+
byte.nil?
|
|
264
|
+
|
|
265
|
+
byte.unpack1("C")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Read an unsigned 16-bit integer (big-endian)
|
|
269
|
+
#
|
|
270
|
+
# @param io [StringIO] Input stream
|
|
271
|
+
# @return [Integer] The value
|
|
272
|
+
def read_uint16(io)
|
|
273
|
+
bytes = io.read(2)
|
|
274
|
+
raise CorruptedTableError, "Unexpected end of Charset data" if
|
|
275
|
+
bytes.nil? || bytes.bytesize < 2
|
|
276
|
+
|
|
277
|
+
bytes.unpack1("n") # Big-endian unsigned 16-bit
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|