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,487 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../binary/base_record"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# CFF (Compact Font Format) table parser
|
|
9
|
+
#
|
|
10
|
+
# The CFF table contains PostScript-based glyph outline data for OpenType
|
|
11
|
+
# fonts with CFF outlines (as opposed to TrueType glyf/loca outlines).
|
|
12
|
+
# CFF is identified by the 'OTTO' signature in the font's sfnt version.
|
|
13
|
+
#
|
|
14
|
+
# CFF Table Structure:
|
|
15
|
+
# ```
|
|
16
|
+
# CFF Table = Header
|
|
17
|
+
# + Name INDEX
|
|
18
|
+
# + Top DICT INDEX
|
|
19
|
+
# + String INDEX
|
|
20
|
+
# + Global Subr INDEX
|
|
21
|
+
# + [Encodings]
|
|
22
|
+
# + [Charsets]
|
|
23
|
+
# + [FDSelect]
|
|
24
|
+
# + [CharStrings INDEX]
|
|
25
|
+
# + [Font DICT INDEX]
|
|
26
|
+
# + [Private DICT]
|
|
27
|
+
# + [Local Subr INDEX]
|
|
28
|
+
# ```
|
|
29
|
+
#
|
|
30
|
+
# This implementation focuses on the foundational structures (Header and
|
|
31
|
+
# INDEX) which are used throughout CFF. Additional structures like DICT,
|
|
32
|
+
# CharStrings, Charset, and Encoding require separate implementations.
|
|
33
|
+
#
|
|
34
|
+
# Reference: Adobe CFF specification
|
|
35
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
36
|
+
#
|
|
37
|
+
# Reference: docs/ttfunk-feature-analysis.md lines 2607-2648
|
|
38
|
+
#
|
|
39
|
+
# @example Reading a CFF table
|
|
40
|
+
# data = font.table_data['CFF ']
|
|
41
|
+
# cff = Fontisan::Tables::Cff.read(data)
|
|
42
|
+
# puts cff.font_count # => 1
|
|
43
|
+
# puts cff.header.version # => "1.0"
|
|
44
|
+
class Cff < Binary::BaseRecord
|
|
45
|
+
# OpenType table tag for CFF
|
|
46
|
+
TAG = "CFF "
|
|
47
|
+
|
|
48
|
+
# @return [Cff::Header] CFF header structure
|
|
49
|
+
attr_reader :header
|
|
50
|
+
|
|
51
|
+
# @return [Cff::Index] Name INDEX containing font names
|
|
52
|
+
attr_reader :name_index
|
|
53
|
+
|
|
54
|
+
# @return [Cff::Index] Top DICT INDEX containing font-level data
|
|
55
|
+
attr_reader :top_dict_index
|
|
56
|
+
|
|
57
|
+
# @return [Array<TopDict>] Parsed Top DICT objects
|
|
58
|
+
attr_reader :top_dicts
|
|
59
|
+
|
|
60
|
+
# @return [Cff::Index] String INDEX containing string data
|
|
61
|
+
attr_reader :string_index
|
|
62
|
+
|
|
63
|
+
# @return [Cff::Index] Global Subr INDEX containing global subroutines
|
|
64
|
+
attr_reader :global_subr_index
|
|
65
|
+
|
|
66
|
+
# @return [String] Raw binary data for the entire CFF table
|
|
67
|
+
attr_reader :raw_data
|
|
68
|
+
|
|
69
|
+
# Override read to parse CFF structure
|
|
70
|
+
#
|
|
71
|
+
# @param io [IO, String] Binary data to read
|
|
72
|
+
# @return [Cff] Parsed CFF table
|
|
73
|
+
def self.read(io)
|
|
74
|
+
cff = new
|
|
75
|
+
return cff if io.nil?
|
|
76
|
+
|
|
77
|
+
data = io.is_a?(String) ? io : io.read
|
|
78
|
+
cff.parse!(data)
|
|
79
|
+
cff
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Parse the CFF table structure
|
|
83
|
+
#
|
|
84
|
+
# This parses the foundational CFF structures: Header, Name INDEX,
|
|
85
|
+
# Top DICT INDEX, String INDEX, and Global Subr INDEX.
|
|
86
|
+
#
|
|
87
|
+
# Additional structures (CharStrings, Charset, Encoding, Private DICT)
|
|
88
|
+
# will be implemented in follow-up tasks.
|
|
89
|
+
#
|
|
90
|
+
# @param data [String] Binary data for the CFF table
|
|
91
|
+
# @raise [CorruptedTableError] If CFF structure is invalid
|
|
92
|
+
def parse!(data)
|
|
93
|
+
@raw_data = data
|
|
94
|
+
io = StringIO.new(data)
|
|
95
|
+
|
|
96
|
+
# Parse CFF Header (4 bytes minimum)
|
|
97
|
+
@header = Cff::Header.read(io)
|
|
98
|
+
@header.validate!
|
|
99
|
+
|
|
100
|
+
# Skip any additional header bytes beyond the standard 4
|
|
101
|
+
# (hdr_size can be larger for extensions)
|
|
102
|
+
if @header.hdr_size > 4
|
|
103
|
+
io.seek(@header.hdr_size)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Parse Name INDEX
|
|
107
|
+
# Contains PostScript names of fonts in this CFF
|
|
108
|
+
# Typically just one name for single-font CFF
|
|
109
|
+
name_start = io.pos
|
|
110
|
+
@name_index = Cff::Index.new(io, start_offset: name_start)
|
|
111
|
+
|
|
112
|
+
# Validate that we have at least one font
|
|
113
|
+
if @name_index.count.zero?
|
|
114
|
+
raise CorruptedTableError, "CFF table must contain at least one font"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Parse Top DICT INDEX
|
|
118
|
+
# Contains font-level DICTs with metadata and pointers
|
|
119
|
+
# Count should match name_index count (one DICT per font)
|
|
120
|
+
top_dict_start = io.pos
|
|
121
|
+
@top_dict_index = Cff::Index.new(io, start_offset: top_dict_start)
|
|
122
|
+
|
|
123
|
+
# Validate Top DICT count matches Name count
|
|
124
|
+
unless @top_dict_index.count == @name_index.count
|
|
125
|
+
raise CorruptedTableError,
|
|
126
|
+
"Top DICT count (#{@top_dict_index.count}) " \
|
|
127
|
+
"must match Name count (#{@name_index.count})"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Parse String INDEX
|
|
131
|
+
# Contains additional string data beyond standard strings
|
|
132
|
+
# Standard strings (SIDs 0-390) are built-in
|
|
133
|
+
string_start = io.pos
|
|
134
|
+
@string_index = Cff::Index.new(io, start_offset: string_start)
|
|
135
|
+
|
|
136
|
+
# Parse Global Subr INDEX
|
|
137
|
+
# Contains subroutines used across all fonts in CFF
|
|
138
|
+
# Can be empty (count = 0)
|
|
139
|
+
global_subr_start = io.pos
|
|
140
|
+
@global_subr_index = Cff::Index.new(io, start_offset: global_subr_start)
|
|
141
|
+
|
|
142
|
+
# Parse Top DICTs
|
|
143
|
+
@top_dicts = []
|
|
144
|
+
@top_dict_index.each do |dict_data|
|
|
145
|
+
@top_dicts << TopDict.new(dict_data)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Additional parsing will be added in follow-up tasks:
|
|
149
|
+
# - Charset parsing
|
|
150
|
+
# - Encoding parsing
|
|
151
|
+
# - CharStrings parsing
|
|
152
|
+
# - FDSelect parsing (for CIDFonts)
|
|
153
|
+
# - Private DICT parsing (requires Top DICT offsets)
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
raise CorruptedTableError, "Failed to parse CFF table: #{e.message}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Get the number of fonts in this CFF table
|
|
159
|
+
#
|
|
160
|
+
# Typically 1 for most OpenType fonts, but CFF supports multiple fonts
|
|
161
|
+
#
|
|
162
|
+
# @return [Integer] Number of fonts
|
|
163
|
+
def font_count
|
|
164
|
+
@name_index&.count || 0
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Get the PostScript name of a font by index
|
|
168
|
+
#
|
|
169
|
+
# @param index [Integer] Font index (0-based)
|
|
170
|
+
# @return [String, nil] PostScript font name, or nil if invalid index
|
|
171
|
+
def font_name(index = 0)
|
|
172
|
+
name_data = @name_index[index]
|
|
173
|
+
return nil unless name_data
|
|
174
|
+
|
|
175
|
+
# Font names in Name INDEX are ASCII strings
|
|
176
|
+
name_data.force_encoding("ASCII-8BIT")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Get all font names in this CFF
|
|
180
|
+
#
|
|
181
|
+
# @return [Array<String>] Array of PostScript font names
|
|
182
|
+
def font_names
|
|
183
|
+
@name_index.to_a.map { |name| name.force_encoding("ASCII-8BIT") }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Check if this is a CFF2 table (variable CFF)
|
|
187
|
+
#
|
|
188
|
+
# @return [Boolean] True if CFF version 2
|
|
189
|
+
def cff2?
|
|
190
|
+
@header&.cff2? || false
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Check if this is a standard CFF table (non-variable)
|
|
194
|
+
#
|
|
195
|
+
# @return [Boolean] True if CFF version 1
|
|
196
|
+
def cff?
|
|
197
|
+
@header&.cff? || false
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Get the CFF version string
|
|
201
|
+
#
|
|
202
|
+
# @return [String] Version in "major.minor" format
|
|
203
|
+
def version
|
|
204
|
+
@header&.version || "unknown"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Get a string by String ID (SID)
|
|
208
|
+
#
|
|
209
|
+
# CFF has 391 predefined standard strings (SIDs 0-390).
|
|
210
|
+
# Additional strings are stored in the String INDEX.
|
|
211
|
+
#
|
|
212
|
+
# @param sid [Integer] String ID
|
|
213
|
+
# @return [String, nil] String data, or nil if invalid SID
|
|
214
|
+
def string_for_sid(sid)
|
|
215
|
+
# Standard strings (SIDs 0-390) are predefined
|
|
216
|
+
# See CFF spec Appendix A for the complete list
|
|
217
|
+
if sid <= 390
|
|
218
|
+
standard_string(sid)
|
|
219
|
+
else
|
|
220
|
+
# Custom strings start at SID 391
|
|
221
|
+
string_index_offset = sid - 391
|
|
222
|
+
string_data = @string_index[string_index_offset]
|
|
223
|
+
string_data&.force_encoding("ASCII-8BIT")
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Get count of custom strings (beyond standard strings)
|
|
228
|
+
#
|
|
229
|
+
# @return [Integer] Number of custom strings
|
|
230
|
+
def custom_string_count
|
|
231
|
+
@string_index&.count || 0
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Get count of global subroutines
|
|
235
|
+
#
|
|
236
|
+
# @return [Integer] Number of global subroutines
|
|
237
|
+
def global_subr_count
|
|
238
|
+
@global_subr_index&.count || 0
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Get the Top DICT for a specific font
|
|
242
|
+
#
|
|
243
|
+
# @param index [Integer] Font index (0-based)
|
|
244
|
+
# @return [TopDict, nil] Top DICT object, or nil if invalid index
|
|
245
|
+
def top_dict(index = 0)
|
|
246
|
+
@top_dicts&.[](index)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Parse the Private DICT for a specific font
|
|
250
|
+
#
|
|
251
|
+
# The Private DICT location is specified in the Top DICT
|
|
252
|
+
#
|
|
253
|
+
# @param index [Integer] Font index (0-based)
|
|
254
|
+
# @return [PrivateDict, nil] Private DICT object, or nil if not present
|
|
255
|
+
def private_dict(index = 0)
|
|
256
|
+
top = top_dict(index)
|
|
257
|
+
return nil unless top
|
|
258
|
+
|
|
259
|
+
private_info = top.private
|
|
260
|
+
return nil unless private_info
|
|
261
|
+
|
|
262
|
+
size, offset = private_info
|
|
263
|
+
return nil if size <= 0 || offset.negative?
|
|
264
|
+
|
|
265
|
+
# Extract Private DICT data from raw CFF data
|
|
266
|
+
private_data = @raw_data[offset, size]
|
|
267
|
+
return nil unless private_data
|
|
268
|
+
|
|
269
|
+
PrivateDict.new(private_data)
|
|
270
|
+
rescue StandardError => e
|
|
271
|
+
warn "Failed to parse Private DICT: #{e.message}"
|
|
272
|
+
nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Get the Local Subr INDEX for a specific font
|
|
276
|
+
#
|
|
277
|
+
# Local subroutines are stored in the Private DICT area
|
|
278
|
+
#
|
|
279
|
+
# @param index [Integer] Font index (0-based)
|
|
280
|
+
# @return [Index, nil] Local Subr INDEX, or nil if not present
|
|
281
|
+
def local_subrs(index = 0)
|
|
282
|
+
priv_dict = private_dict(index)
|
|
283
|
+
return nil unless priv_dict
|
|
284
|
+
|
|
285
|
+
subrs_offset = priv_dict.subrs
|
|
286
|
+
return nil unless subrs_offset
|
|
287
|
+
|
|
288
|
+
top = top_dict(index)
|
|
289
|
+
return nil unless top
|
|
290
|
+
|
|
291
|
+
private_info = top.private
|
|
292
|
+
return nil unless private_info
|
|
293
|
+
|
|
294
|
+
_size, private_offset = private_info
|
|
295
|
+
|
|
296
|
+
# Local Subr offset is relative to Private DICT start
|
|
297
|
+
absolute_offset = private_offset + subrs_offset
|
|
298
|
+
|
|
299
|
+
io = StringIO.new(@raw_data)
|
|
300
|
+
io.seek(absolute_offset)
|
|
301
|
+
Index.new(io, start_offset: absolute_offset)
|
|
302
|
+
rescue StandardError => e
|
|
303
|
+
warn "Failed to parse Local Subr INDEX: #{e.message}"
|
|
304
|
+
nil
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Get the CharStrings INDEX for a specific font
|
|
308
|
+
#
|
|
309
|
+
# The CharStrings INDEX contains glyph outline programs
|
|
310
|
+
#
|
|
311
|
+
# @param index [Integer] Font index (0-based)
|
|
312
|
+
# @return [CharstringsIndex, nil] CharStrings INDEX, or nil if not
|
|
313
|
+
# present
|
|
314
|
+
def charstrings_index(index = 0)
|
|
315
|
+
top = top_dict(index)
|
|
316
|
+
return nil unless top
|
|
317
|
+
|
|
318
|
+
charstrings_offset = top.charstrings
|
|
319
|
+
return nil unless charstrings_offset
|
|
320
|
+
|
|
321
|
+
io = StringIO.new(@raw_data)
|
|
322
|
+
io.seek(charstrings_offset)
|
|
323
|
+
CharstringsIndex.new(io, start_offset: charstrings_offset)
|
|
324
|
+
rescue StandardError => e
|
|
325
|
+
warn "Failed to parse CharStrings INDEX: #{e.message}"
|
|
326
|
+
nil
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Get a CharString for a specific glyph
|
|
330
|
+
#
|
|
331
|
+
# This returns an interpreted CharString object with the glyph's
|
|
332
|
+
# outline data
|
|
333
|
+
#
|
|
334
|
+
# @param glyph_index [Integer] Glyph index (0-based, 0 is typically
|
|
335
|
+
# .notdef)
|
|
336
|
+
# @param font_index [Integer] Font index in CFF (default 0)
|
|
337
|
+
# @return [CharString, nil] Interpreted CharString, or nil if not found
|
|
338
|
+
#
|
|
339
|
+
# @example Getting a glyph's CharString
|
|
340
|
+
# cff = Fontisan::Tables::Cff.read(data)
|
|
341
|
+
# charstring = cff.charstring_for_glyph(42)
|
|
342
|
+
# puts charstring.width
|
|
343
|
+
# puts charstring.bounding_box
|
|
344
|
+
# charstring.to_commands.each { |cmd| puts cmd.inspect }
|
|
345
|
+
def charstring_for_glyph(glyph_index, font_index = 0)
|
|
346
|
+
charstrings = charstrings_index(font_index)
|
|
347
|
+
return nil unless charstrings
|
|
348
|
+
|
|
349
|
+
priv_dict = private_dict(font_index)
|
|
350
|
+
return nil unless priv_dict
|
|
351
|
+
|
|
352
|
+
local_subr_index = local_subrs(font_index)
|
|
353
|
+
|
|
354
|
+
charstrings.charstring_at(
|
|
355
|
+
glyph_index,
|
|
356
|
+
priv_dict,
|
|
357
|
+
@global_subr_index,
|
|
358
|
+
local_subr_index,
|
|
359
|
+
)
|
|
360
|
+
rescue StandardError => e
|
|
361
|
+
warn "Failed to get CharString for glyph #{glyph_index}: #{e.message}"
|
|
362
|
+
nil
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Get the number of glyphs in a font
|
|
366
|
+
#
|
|
367
|
+
# @param index [Integer] Font index (0-based)
|
|
368
|
+
# @return [Integer] Number of glyphs, or 0 if CharStrings not available
|
|
369
|
+
def glyph_count(index = 0)
|
|
370
|
+
charstrings = charstrings_index(index)
|
|
371
|
+
charstrings&.glyph_count || 0
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Validate the CFF table structure
|
|
375
|
+
#
|
|
376
|
+
# @return [Boolean] True if valid
|
|
377
|
+
def valid?
|
|
378
|
+
return false unless @header&.valid?
|
|
379
|
+
return false unless @name_index&.count&.positive?
|
|
380
|
+
return false unless @top_dict_index
|
|
381
|
+
return false unless @top_dict_index.count == @name_index.count
|
|
382
|
+
return false unless @string_index
|
|
383
|
+
return false unless @global_subr_index
|
|
384
|
+
|
|
385
|
+
true
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
private
|
|
389
|
+
|
|
390
|
+
# Get a standard CFF string by SID
|
|
391
|
+
#
|
|
392
|
+
# This is a placeholder that returns a generic string.
|
|
393
|
+
# A complete implementation would include all 391 standard strings
|
|
394
|
+
# from CFF spec Appendix A.
|
|
395
|
+
#
|
|
396
|
+
# TODO: Implement complete standard string table in follow-up task
|
|
397
|
+
#
|
|
398
|
+
# @param sid [Integer] String ID (0-390)
|
|
399
|
+
# @return [String] Standard string
|
|
400
|
+
def standard_string(sid)
|
|
401
|
+
# Placeholder implementation
|
|
402
|
+
# Full implementation should include all standard strings
|
|
403
|
+
# from CFF specification Appendix A
|
|
404
|
+
case sid
|
|
405
|
+
when 0 then ".notdef"
|
|
406
|
+
when 1 then "space"
|
|
407
|
+
when 2 then "exclam"
|
|
408
|
+
# ... (388 more standard strings)
|
|
409
|
+
else
|
|
410
|
+
".notdef" # Fallback
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Get the Charset for a specific font
|
|
415
|
+
#
|
|
416
|
+
# Charset maps glyph IDs to glyph names via String IDs
|
|
417
|
+
#
|
|
418
|
+
# @param index [Integer] Font index (0-based)
|
|
419
|
+
# @return [Charset, nil] Charset object, or nil if not present
|
|
420
|
+
def charset(index = 0)
|
|
421
|
+
top = top_dict(index)
|
|
422
|
+
return nil unless top
|
|
423
|
+
|
|
424
|
+
charset_offset = top.charset
|
|
425
|
+
return nil unless charset_offset
|
|
426
|
+
|
|
427
|
+
# Handle predefined charsets
|
|
428
|
+
if charset_offset <= 2
|
|
429
|
+
num_glyphs = glyph_count(index)
|
|
430
|
+
return Charset.new(charset_offset, num_glyphs, self)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Parse custom charset from offset
|
|
434
|
+
charset_data = @raw_data[charset_offset..]
|
|
435
|
+
return nil unless charset_data
|
|
436
|
+
|
|
437
|
+
num_glyphs = glyph_count(index)
|
|
438
|
+
Charset.new(charset_data, num_glyphs, self)
|
|
439
|
+
rescue StandardError => e
|
|
440
|
+
warn "Failed to parse Charset: #{e.message}"
|
|
441
|
+
nil
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Get the Encoding for a specific font
|
|
445
|
+
#
|
|
446
|
+
# Encoding maps character codes to glyph IDs
|
|
447
|
+
#
|
|
448
|
+
# @param index [Integer] Font index (0-based)
|
|
449
|
+
# @return [Encoding, nil] Encoding object, or nil if not present
|
|
450
|
+
def encoding(index = 0)
|
|
451
|
+
top = top_dict(index)
|
|
452
|
+
return nil unless top
|
|
453
|
+
|
|
454
|
+
encoding_offset = top.encoding
|
|
455
|
+
return nil unless encoding_offset
|
|
456
|
+
|
|
457
|
+
# Handle predefined encodings
|
|
458
|
+
if encoding_offset <= 1
|
|
459
|
+
num_glyphs = glyph_count(index)
|
|
460
|
+
return Encoding.new(encoding_offset, num_glyphs)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Parse custom encoding from offset
|
|
464
|
+
encoding_data = @raw_data[encoding_offset..]
|
|
465
|
+
return nil unless encoding_data
|
|
466
|
+
|
|
467
|
+
num_glyphs = glyph_count(index)
|
|
468
|
+
Encoding.new(encoding_data, num_glyphs)
|
|
469
|
+
rescue StandardError => e
|
|
470
|
+
warn "Failed to parse Encoding: #{e.message}"
|
|
471
|
+
nil
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Load nested class definitions after the main class is defined
|
|
476
|
+
require_relative "cff/header"
|
|
477
|
+
require_relative "cff/index"
|
|
478
|
+
require_relative "cff/dict"
|
|
479
|
+
require_relative "cff/top_dict"
|
|
480
|
+
require_relative "cff/private_dict"
|
|
481
|
+
require_relative "cff/charstring"
|
|
482
|
+
require_relative "cff/charstrings_index"
|
|
483
|
+
require_relative "cff/charset"
|
|
484
|
+
require_relative "cff/encoding"
|
|
485
|
+
require_relative "cff/cff_glyph"
|
|
486
|
+
end
|
|
487
|
+
end
|