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,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Export
|
|
7
|
+
# TtxParser parses TTX XML format to font data
|
|
8
|
+
#
|
|
9
|
+
# Parses fonttools-compatible TTX XML files and reconstructs
|
|
10
|
+
# font data that can be written back to binary formats.
|
|
11
|
+
#
|
|
12
|
+
# @example Parsing TTX file
|
|
13
|
+
# parser = TtxParser.new
|
|
14
|
+
# font_data = parser.parse(File.read("font.ttx"))
|
|
15
|
+
# # Use font_data to rebuild binary font
|
|
16
|
+
class TtxParser
|
|
17
|
+
# Parse TTX XML content
|
|
18
|
+
#
|
|
19
|
+
# @param ttx_xml [String] TTX XML content
|
|
20
|
+
# @return [Hash] Parsed font data structure
|
|
21
|
+
def parse(ttx_xml)
|
|
22
|
+
doc = Nokogiri::XML(ttx_xml)
|
|
23
|
+
ttfont = doc.at_xpath("/ttFont")
|
|
24
|
+
raise "No ttFont root element found" unless ttfont
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
sfnt_version: parse_sfnt_version(ttfont["sfntVersion"]),
|
|
28
|
+
glyph_order: parse_glyph_order(ttfont),
|
|
29
|
+
tables: parse_tables(ttfont),
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Parse SFNT version
|
|
36
|
+
#
|
|
37
|
+
# @param version_str [String] Version string
|
|
38
|
+
# @return [Integer] Version integer
|
|
39
|
+
def parse_sfnt_version(version_str)
|
|
40
|
+
# Handle format like "\x00\x01\x00\x00" or "0x00010000"
|
|
41
|
+
if version_str.start_with?("\\x")
|
|
42
|
+
# Parse escaped hex bytes
|
|
43
|
+
bytes = version_str.scan(/\\x([0-9a-f]{2})/i).flatten
|
|
44
|
+
bytes.map { |b| b.to_i(16) }.pack("C*").unpack1("N")
|
|
45
|
+
elsif version_str.start_with?("0x")
|
|
46
|
+
version_str.to_i(16)
|
|
47
|
+
else
|
|
48
|
+
version_str.to_i
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Parse GlyphOrder section
|
|
53
|
+
#
|
|
54
|
+
# @param ttfont [Nokogiri::XML::Element] Root element
|
|
55
|
+
# @return [Array<Hash>] Array of glyph entries
|
|
56
|
+
def parse_glyph_order(ttfont)
|
|
57
|
+
glyph_order = ttfont.at_xpath("GlyphOrder")
|
|
58
|
+
return [] unless glyph_order
|
|
59
|
+
|
|
60
|
+
glyph_order.xpath("GlyphID").map do |glyph_id|
|
|
61
|
+
{
|
|
62
|
+
id: glyph_id["id"].to_i,
|
|
63
|
+
name: glyph_id["name"],
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Parse all tables
|
|
69
|
+
#
|
|
70
|
+
# @param ttfont [Nokogiri::XML::Element] Root element
|
|
71
|
+
# @return [Hash] Hash of table data by tag
|
|
72
|
+
def parse_tables(ttfont)
|
|
73
|
+
tables = {}
|
|
74
|
+
|
|
75
|
+
# Parse specific tables
|
|
76
|
+
parse_head_table(ttfont, tables)
|
|
77
|
+
parse_hhea_table(ttfont, tables)
|
|
78
|
+
parse_maxp_table(ttfont, tables)
|
|
79
|
+
parse_name_table(ttfont, tables)
|
|
80
|
+
parse_os2_table(ttfont, tables)
|
|
81
|
+
parse_post_table(ttfont, tables)
|
|
82
|
+
|
|
83
|
+
# Parse any remaining binary tables
|
|
84
|
+
parse_binary_tables(ttfont, tables)
|
|
85
|
+
|
|
86
|
+
tables
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Parse head table
|
|
90
|
+
#
|
|
91
|
+
# @param ttfont [Nokogiri::XML::Element] Root element
|
|
92
|
+
# @param tables [Hash] Tables hash
|
|
93
|
+
# @return [void]
|
|
94
|
+
def parse_head_table(ttfont, tables)
|
|
95
|
+
head = ttfont.at_xpath("head")
|
|
96
|
+
return unless head
|
|
97
|
+
|
|
98
|
+
tables["head"] = {
|
|
99
|
+
table_version: parse_fixed(head.at_xpath("tableVersion")&.[]("value")),
|
|
100
|
+
font_revision: parse_fixed(head.at_xpath("fontRevision")&.[]("value")),
|
|
101
|
+
checksum_adjustment: parse_hex(head.at_xpath("checkSumAdjustment")&.[]("value")),
|
|
102
|
+
magic_number: parse_hex(head.at_xpath("magicNumber")&.[]("value")),
|
|
103
|
+
flags: head.at_xpath("flags")&.[]("value").to_i,
|
|
104
|
+
units_per_em: head.at_xpath("unitsPerEm")&.[]("value").to_i,
|
|
105
|
+
created: parse_timestamp(head.at_xpath("created")&.[]("value")),
|
|
106
|
+
modified: parse_timestamp(head.at_xpath("modified")&.[]("value")),
|
|
107
|
+
x_min: head.at_xpath("xMin")&.[]("value").to_i,
|
|
108
|
+
y_min: head.at_xpath("yMin")&.[]("value").to_i,
|
|
109
|
+
x_max: head.at_xpath("xMax")&.[]("value").to_i,
|
|
110
|
+
y_max: head.at_xpath("yMax")&.[]("value").to_i,
|
|
111
|
+
mac_style: parse_binary_flags(head.at_xpath("macStyle")&.[]("value")),
|
|
112
|
+
lowest_rec_ppem: head.at_xpath("lowestRecPPEM")&.[]("value").to_i,
|
|
113
|
+
font_direction_hint: head.at_xpath("fontDirectionHint")&.[]("value").to_i,
|
|
114
|
+
index_to_loc_format: head.at_xpath("indexToLocFormat")&.[]("value").to_i,
|
|
115
|
+
glyph_data_format: head.at_xpath("glyphDataFormat")&.[]("value").to_i,
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Parse hhea table
|
|
120
|
+
#
|
|
121
|
+
# @param ttfont [Nokogiri::XML::Element] Root element
|
|
122
|
+
# @param tables [Hash] Tables hash
|
|
123
|
+
# @return [void]
|
|
124
|
+
def parse_hhea_table(ttfont, tables)
|
|
125
|
+
hhea = ttfont.at_xpath("hhea")
|
|
126
|
+
return unless hhea
|
|
127
|
+
|
|
128
|
+
tables["hhea"] = {
|
|
129
|
+
table_version: parse_hex(hhea.at_xpath("tableVersion")&.[]("value")),
|
|
130
|
+
ascent: hhea.at_xpath("ascent")&.[]("value").to_i,
|
|
131
|
+
descent: hhea.at_xpath("descent")&.[]("value").to_i,
|
|
132
|
+
line_gap: hhea.at_xpath("lineGap")&.[]("value").to_i,
|
|
133
|
+
advance_width_max: hhea.at_xpath("advanceWidthMax")&.[]("value").to_i,
|
|
134
|
+
min_left_side_bearing: hhea.at_xpath("minLeftSideBearing")&.[]("value").to_i,
|
|
135
|
+
min_right_side_bearing: hhea.at_xpath("minRightSideBearing")&.[]("value").to_i,
|
|
136
|
+
x_max_extent: hhea.at_xpath("xMaxExtent")&.[]("value").to_i,
|
|
137
|
+
caret_slope_rise: hhea.at_xpath("caretSlopeRise")&.[]("value").to_i,
|
|
138
|
+
caret_slope_run: hhea.at_xpath("caretSlopeRun")&.[]("value").to_i,
|
|
139
|
+
caret_offset: hhea.at_xpath("caretOffset")&.[]("value").to_i,
|
|
140
|
+
metric_data_format: hhea.at_xpath("metricDataFormat")&.[]("value").to_i,
|
|
141
|
+
number_of_h_metrics: hhea.at_xpath("numberOfHMetrics")&.[]("value").to_i,
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Parse maxp table
|
|
146
|
+
#
|
|
147
|
+
# @param ttfont [Nokogiri::XML::Element] Root element
|
|
148
|
+
# @param tables [Hash] Tables hash
|
|
149
|
+
# @return [void]
|
|
150
|
+
def parse_maxp_table(ttfont, tables)
|
|
151
|
+
maxp = ttfont.at_xpath("maxp")
|
|
152
|
+
return unless maxp
|
|
153
|
+
|
|
154
|
+
tables["maxp"] = {
|
|
155
|
+
table_version: parse_hex(maxp.at_xpath("tableVersion")&.[]("value")),
|
|
156
|
+
num_glyphs: maxp.at_xpath("numGlyphs")&.[]("value").to_i,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Version 1.0 fields
|
|
160
|
+
if tables["maxp"][:table_version] >= 0x00010000
|
|
161
|
+
tables["maxp"].merge!({
|
|
162
|
+
max_points: maxp.at_xpath("maxPoints")&.[]("value")&.to_i,
|
|
163
|
+
max_contours: maxp.at_xpath("maxContours")&.[]("value")&.to_i,
|
|
164
|
+
max_composite_points: maxp.at_xpath("maxCompositePoints")&.[]("value")&.to_i,
|
|
165
|
+
max_composite_contours: maxp.at_xpath("maxCompositeContours")&.[]("value")&.to_i,
|
|
166
|
+
})
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Parse name table
|
|
171
|
+
#
|
|
172
|
+
# @param ttfont [Nokogiri::XML::Element] Root element
|
|
173
|
+
# @param tables [Hash] Tables hash
|
|
174
|
+
# @return [void]
|
|
175
|
+
def parse_name_table(ttfont, tables)
|
|
176
|
+
name_elem = ttfont.at_xpath("name")
|
|
177
|
+
return unless name_elem
|
|
178
|
+
|
|
179
|
+
name_records = name_elem.xpath("namerecord").map do |record|
|
|
180
|
+
{
|
|
181
|
+
name_id: record["nameID"].to_i,
|
|
182
|
+
platform_id: record["platformID"].to_i,
|
|
183
|
+
encoding_id: record["platEncID"].to_i,
|
|
184
|
+
language_id: parse_hex(record["langID"]),
|
|
185
|
+
string: record.text,
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
tables["name"] = { name_records: name_records }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Parse OS/2 table (stub)
|
|
193
|
+
#
|
|
194
|
+
# @param ttfont [Nokogiri::XML::Element] Root element
|
|
195
|
+
# @param tables [Hash] Tables hash
|
|
196
|
+
# @return [void]
|
|
197
|
+
def parse_os2_table(ttfont, tables)
|
|
198
|
+
os2 = ttfont.at_xpath("OS/2")
|
|
199
|
+
return unless os2
|
|
200
|
+
|
|
201
|
+
# Basic OS/2 parsing - can be expanded
|
|
202
|
+
tables["OS/2"] = { parsed: false }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Parse post table (stub)
|
|
206
|
+
#
|
|
207
|
+
# @param ttfont [Nokogiri::XML::Element] Root element
|
|
208
|
+
# @param tables [Hash] Tables hash
|
|
209
|
+
# @return [void]
|
|
210
|
+
def parse_post_table(ttfont, tables)
|
|
211
|
+
post = ttfont.at_xpath("post")
|
|
212
|
+
return unless post
|
|
213
|
+
|
|
214
|
+
tables["post"] = {
|
|
215
|
+
format_type: parse_fixed(post.at_xpath("formatType")&.[]("value")),
|
|
216
|
+
italic_angle: parse_fixed(post.at_xpath("italicAngle")&.[]("value")),
|
|
217
|
+
underline_position: post.at_xpath("underlinePosition")&.[]("value").to_i,
|
|
218
|
+
underline_thickness: post.at_xpath("underlineThickness")&.[]("value").to_i,
|
|
219
|
+
is_fixed_pitch: post.at_xpath("isFixedPitch")&.[]("value").to_i,
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Parse binary tables (fallback)
|
|
224
|
+
#
|
|
225
|
+
# @param ttfont [Nokogiri::XML::Element] Root element
|
|
226
|
+
# @param tables [Hash] Tables hash
|
|
227
|
+
# @return [void]
|
|
228
|
+
def parse_binary_tables(ttfont, tables)
|
|
229
|
+
# Find all table elements not already parsed
|
|
230
|
+
ttfont.children.each do |elem|
|
|
231
|
+
next unless elem.element?
|
|
232
|
+
next if elem.name == "GlyphOrder"
|
|
233
|
+
next if tables.key?(elem.name)
|
|
234
|
+
|
|
235
|
+
hexdata = elem.at_xpath("hexdata")
|
|
236
|
+
if hexdata
|
|
237
|
+
tables[elem.name] = {
|
|
238
|
+
binary: parse_hex_data(hexdata.text),
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Parse fixed-point number
|
|
245
|
+
#
|
|
246
|
+
# @param value_str [String] Fixed-point string
|
|
247
|
+
# @return [Integer] Fixed-point integer (16.16)
|
|
248
|
+
def parse_fixed(value_str)
|
|
249
|
+
return 0 unless value_str
|
|
250
|
+
|
|
251
|
+
(value_str.to_f * 65536).to_i
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Parse hex value
|
|
255
|
+
#
|
|
256
|
+
# @param value_str [String] Hex string
|
|
257
|
+
# @return [Integer] Integer value
|
|
258
|
+
def parse_hex(value_str)
|
|
259
|
+
return 0 unless value_str
|
|
260
|
+
|
|
261
|
+
value_str.to_i(16)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Parse binary flags
|
|
265
|
+
#
|
|
266
|
+
# @param flags_str [String] Binary string with spaces
|
|
267
|
+
# @return [Integer] Integer value
|
|
268
|
+
def parse_binary_flags(flags_str)
|
|
269
|
+
return 0 unless flags_str
|
|
270
|
+
|
|
271
|
+
flags_str.gsub(/\s+/, "").to_i(2)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Parse timestamp
|
|
275
|
+
#
|
|
276
|
+
# @param timestamp_str [String] Timestamp string
|
|
277
|
+
# @return [Integer] Mac timestamp
|
|
278
|
+
def parse_timestamp(timestamp_str)
|
|
279
|
+
return 0 unless timestamp_str
|
|
280
|
+
|
|
281
|
+
begin
|
|
282
|
+
time = Time.strptime(timestamp_str, "%a %b %e %H:%M:%S %Y")
|
|
283
|
+
mac_epoch = Time.utc(1904, 1, 1)
|
|
284
|
+
(time - mac_epoch).to_i
|
|
285
|
+
rescue StandardError
|
|
286
|
+
0
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Parse hex data
|
|
291
|
+
#
|
|
292
|
+
# @param hex_str [String] Hex string with newlines
|
|
293
|
+
# @return [String] Binary data
|
|
294
|
+
def parse_hex_data(hex_str)
|
|
295
|
+
hex_clean = hex_str.gsub(/\s+/, "")
|
|
296
|
+
[hex_clean].pack("H*")
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
data/lib/fontisan/font_loader.rb
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "constants"
|
|
4
|
+
require_relative "loading_modes"
|
|
4
5
|
require_relative "true_type_font"
|
|
5
6
|
require_relative "open_type_font"
|
|
6
7
|
require_relative "true_type_collection"
|
|
7
8
|
require_relative "open_type_collection"
|
|
9
|
+
require_relative "woff_font"
|
|
10
|
+
require_relative "woff2_font"
|
|
8
11
|
require_relative "error"
|
|
9
12
|
|
|
10
13
|
module Fontisan
|
|
@@ -19,35 +22,52 @@ module Fontisan
|
|
|
19
22
|
# font = FontLoader.load("font.otf") # => OpenTypeFont
|
|
20
23
|
# font = FontLoader.load("fonts.ttc") # => TrueTypeFont (first in collection)
|
|
21
24
|
# font = FontLoader.load("fonts.ttc", font_index: 2) # => TrueTypeFont (third in collection)
|
|
25
|
+
#
|
|
26
|
+
# @example Loading modes
|
|
27
|
+
# font = FontLoader.load("font.ttf", mode: :metadata) # Load only metadata tables
|
|
28
|
+
# font = FontLoader.load("font.ttf", mode: :full) # Load all tables
|
|
29
|
+
#
|
|
30
|
+
# @example Lazy loading control
|
|
31
|
+
# font = FontLoader.load("font.ttf", lazy: true) # Tables loaded on-demand
|
|
32
|
+
# font = FontLoader.load("font.ttf", lazy: false) # All tables loaded upfront
|
|
22
33
|
class FontLoader
|
|
23
34
|
# Load a font from file with automatic format detection
|
|
24
35
|
#
|
|
25
36
|
# @param path [String] Path to the font file
|
|
26
37
|
# @param font_index [Integer] Index of font in collection (0-based, default: 0)
|
|
27
|
-
# @
|
|
38
|
+
# @param mode [Symbol] Loading mode (:metadata or :full, default: from ENV or :full)
|
|
39
|
+
# @param lazy [Boolean] If true, load tables on demand (default: false for eager loading)
|
|
40
|
+
# @return [TrueTypeFont, OpenTypeFont, WoffFont, Woff2Font] The loaded font object
|
|
28
41
|
# @raise [Errno::ENOENT] if file does not exist
|
|
29
|
-
# @raise [UnsupportedFormatError] for
|
|
42
|
+
# @raise [UnsupportedFormatError] for unsupported formats
|
|
30
43
|
# @raise [InvalidFontError] for corrupted or unknown formats
|
|
31
|
-
def self.load(path, font_index: 0)
|
|
44
|
+
def self.load(path, font_index: 0, mode: nil, lazy: nil)
|
|
32
45
|
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
33
46
|
|
|
47
|
+
# Resolve mode and lazy parameters with environment variables
|
|
48
|
+
resolved_mode = mode || env_mode || LoadingModes::FULL
|
|
49
|
+
resolved_lazy = lazy.nil? ? (env_lazy.nil? ? false : env_lazy) : lazy
|
|
50
|
+
|
|
51
|
+
# Validate mode
|
|
52
|
+
LoadingModes.validate_mode!(resolved_mode)
|
|
53
|
+
|
|
34
54
|
File.open(path, "rb") do |io|
|
|
35
55
|
signature = io.read(4)
|
|
36
56
|
io.rewind
|
|
37
57
|
|
|
38
58
|
case signature
|
|
39
59
|
when Constants::TTC_TAG
|
|
40
|
-
load_from_collection(io, path, font_index)
|
|
60
|
+
load_from_collection(io, path, font_index, mode: resolved_mode, lazy: resolved_lazy)
|
|
41
61
|
when pack_uint32(Constants::SFNT_VERSION_TRUETYPE)
|
|
42
|
-
TrueTypeFont.from_file(path)
|
|
62
|
+
TrueTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
43
63
|
when "OTTO"
|
|
44
|
-
OpenTypeFont.from_file(path)
|
|
64
|
+
OpenTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
45
65
|
when "wOFF"
|
|
46
66
|
raise UnsupportedFormatError,
|
|
47
|
-
"Unsupported font format: WOFF.
|
|
67
|
+
"Unsupported font format: WOFF. Please convert to TTF/OTF first."
|
|
48
68
|
when "wOF2"
|
|
49
69
|
raise UnsupportedFormatError,
|
|
50
|
-
"Unsupported font format: WOFF2.
|
|
70
|
+
"Unsupported font format: WOFF2. Please convert to TTF/OTF first."
|
|
51
71
|
else
|
|
52
72
|
raise InvalidFontError,
|
|
53
73
|
"Unknown font format. Expected TTF, OTF, TTC, or OTC file."
|
|
@@ -55,14 +75,103 @@ module Fontisan
|
|
|
55
75
|
end
|
|
56
76
|
end
|
|
57
77
|
|
|
78
|
+
# Check if a file is a collection (TTC or OTC)
|
|
79
|
+
#
|
|
80
|
+
# @param path [String] Path to the font file
|
|
81
|
+
# @return [Boolean] true if file is a TTC/OTC collection
|
|
82
|
+
# @raise [Errno::ENOENT] if file does not exist
|
|
83
|
+
#
|
|
84
|
+
# @example Check if file is collection
|
|
85
|
+
# FontLoader.collection?("fonts.ttc") # => true
|
|
86
|
+
# FontLoader.collection?("font.ttf") # => false
|
|
87
|
+
def self.collection?(path)
|
|
88
|
+
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
89
|
+
|
|
90
|
+
File.open(path, "rb") do |io|
|
|
91
|
+
signature = io.read(4)
|
|
92
|
+
signature == Constants::TTC_TAG
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Load a collection object without extracting fonts
|
|
97
|
+
#
|
|
98
|
+
# Returns the collection object (TrueTypeCollection or OpenTypeCollection)
|
|
99
|
+
# without extracting individual fonts. Useful for inspecting collection
|
|
100
|
+
# metadata and structure.
|
|
101
|
+
#
|
|
102
|
+
# @param path [String] Path to the collection file
|
|
103
|
+
# @return [TrueTypeCollection, OpenTypeCollection] The collection object
|
|
104
|
+
# @raise [Errno::ENOENT] if file does not exist
|
|
105
|
+
# @raise [InvalidFontError] if file is not a collection or type cannot be determined
|
|
106
|
+
#
|
|
107
|
+
# @example Load collection for inspection
|
|
108
|
+
# collection = FontLoader.load_collection("fonts.ttc")
|
|
109
|
+
# puts "Collection has #{collection.num_fonts} fonts"
|
|
110
|
+
def self.load_collection(path)
|
|
111
|
+
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
112
|
+
|
|
113
|
+
File.open(path, "rb") do |io|
|
|
114
|
+
signature = io.read(4)
|
|
115
|
+
|
|
116
|
+
unless signature == Constants::TTC_TAG
|
|
117
|
+
raise InvalidFontError,
|
|
118
|
+
"File is not a collection (TTC/OTC). Use FontLoader.load instead."
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Read first font offset to detect collection type
|
|
122
|
+
io.seek(12) # Skip tag (4) + versions (4) + num_fonts (4)
|
|
123
|
+
first_offset = io.read(4).unpack1("N")
|
|
124
|
+
|
|
125
|
+
# Peek at first font's sfnt_version
|
|
126
|
+
io.seek(first_offset)
|
|
127
|
+
sfnt_version = io.read(4).unpack1("N")
|
|
128
|
+
io.rewind
|
|
129
|
+
|
|
130
|
+
case sfnt_version
|
|
131
|
+
when Constants::SFNT_VERSION_TRUETYPE
|
|
132
|
+
TrueTypeCollection.from_file(path)
|
|
133
|
+
when Constants::SFNT_VERSION_OTTO
|
|
134
|
+
OpenTypeCollection.from_file(path)
|
|
135
|
+
else
|
|
136
|
+
raise InvalidFontError,
|
|
137
|
+
"Unknown font type in collection (sfnt version: 0x#{sfnt_version.to_s(16)})"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get mode from environment variable
|
|
143
|
+
#
|
|
144
|
+
# @return [Symbol, nil] Mode from FONTISAN_MODE or nil
|
|
145
|
+
# @api private
|
|
146
|
+
def self.env_mode
|
|
147
|
+
env_value = ENV["FONTISAN_MODE"]
|
|
148
|
+
return nil unless env_value
|
|
149
|
+
|
|
150
|
+
mode = env_value.to_sym
|
|
151
|
+
LoadingModes.valid_mode?(mode) ? mode : nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Get lazy setting from environment variable
|
|
155
|
+
#
|
|
156
|
+
# @return [Boolean, nil] Lazy setting from FONTISAN_LAZY or nil if not set
|
|
157
|
+
# @api private
|
|
158
|
+
def self.env_lazy
|
|
159
|
+
env_value = ENV["FONTISAN_LAZY"]
|
|
160
|
+
return nil unless env_value
|
|
161
|
+
|
|
162
|
+
env_value.downcase == "true"
|
|
163
|
+
end
|
|
164
|
+
|
|
58
165
|
# Load from a collection file (TTC or OTC)
|
|
59
166
|
#
|
|
60
167
|
# @param io [IO] Open file handle
|
|
61
168
|
# @param path [String] Path to the collection file
|
|
62
169
|
# @param font_index [Integer] Index of font to extract
|
|
170
|
+
# @param mode [Symbol] Loading mode (:metadata or :full)
|
|
171
|
+
# @param lazy [Boolean] If true, load tables on demand
|
|
63
172
|
# @return [TrueTypeFont, OpenTypeFont] The loaded font object
|
|
64
173
|
# @raise [InvalidFontError] if collection type cannot be determined
|
|
65
|
-
def self.load_from_collection(io, path, font_index)
|
|
174
|
+
def self.load_from_collection(io, path, font_index, mode: LoadingModes::FULL, lazy: true)
|
|
66
175
|
# Read collection header to get font offsets
|
|
67
176
|
io.seek(12) # Skip tag (4) + major_version (2) + minor_version (2) + num_fonts marker (4)
|
|
68
177
|
num_fonts = io.read(4).unpack1("N")
|
|
@@ -84,11 +193,11 @@ module Fontisan
|
|
|
84
193
|
when Constants::SFNT_VERSION_TRUETYPE
|
|
85
194
|
# TrueType Collection
|
|
86
195
|
ttc = TrueTypeCollection.from_file(path)
|
|
87
|
-
File.open(path, "rb") { |f| ttc.font(font_index, f) }
|
|
196
|
+
File.open(path, "rb") { |f| ttc.font(font_index, f, mode: mode) }
|
|
88
197
|
when Constants::SFNT_VERSION_OTTO
|
|
89
198
|
# OpenType Collection
|
|
90
199
|
otc = OpenTypeCollection.from_file(path)
|
|
91
|
-
File.open(path, "rb") { |f| otc.font(font_index, f) }
|
|
200
|
+
File.open(path, "rb") { |f| otc.font(font_index, f, mode: mode) }
|
|
92
201
|
else
|
|
93
202
|
raise InvalidFontError,
|
|
94
203
|
"Unknown font type in collection (sfnt version: 0x#{sfnt_version.to_s(16)})"
|
|
@@ -104,6 +213,6 @@ module Fontisan
|
|
|
104
213
|
[value].pack("N")
|
|
105
214
|
end
|
|
106
215
|
|
|
107
|
-
private_class_method :load_from_collection, :pack_uint32
|
|
216
|
+
private_class_method :load_from_collection, :pack_uint32, :env_mode, :env_lazy
|
|
108
217
|
end
|
|
109
218
|
end
|