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,527 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Export
|
|
7
|
+
# TtxGenerator generates TTX XML format from font data
|
|
8
|
+
#
|
|
9
|
+
# Generates fonttools-compatible TTX XML format for font debugging
|
|
10
|
+
# and interoperability. Handles various table types with appropriate
|
|
11
|
+
# XML structures per the TTX specification.
|
|
12
|
+
#
|
|
13
|
+
# @example Generating TTX from a font
|
|
14
|
+
# generator = TtxGenerator.new(font, "font.ttf")
|
|
15
|
+
# ttx_xml = generator.generate
|
|
16
|
+
# File.write("font.ttx", ttx_xml)
|
|
17
|
+
#
|
|
18
|
+
# @example Selective table generation
|
|
19
|
+
# ttx_xml = generator.generate(tables: ["head", "name", "glyf"])
|
|
20
|
+
class TtxGenerator
|
|
21
|
+
# Initialize TTX generator
|
|
22
|
+
#
|
|
23
|
+
# @param font [TrueTypeFont, OpenTypeFont] The font to export
|
|
24
|
+
# @param source_path [String] Path to source font file
|
|
25
|
+
# @param options [Hash] Generation options
|
|
26
|
+
# @option options [Boolean] :pretty Pretty-print XML (default: true)
|
|
27
|
+
# @option options [Integer] :indent Indentation spaces (default: 2)
|
|
28
|
+
def initialize(font, source_path, options = {})
|
|
29
|
+
@font = font
|
|
30
|
+
@source_path = source_path
|
|
31
|
+
@pretty = options.fetch(:pretty, true)
|
|
32
|
+
@indent = options.fetch(:indent, 2)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Generate TTX XML
|
|
36
|
+
#
|
|
37
|
+
# @param options [Hash] Generation options
|
|
38
|
+
# @option options [Array<String>] :tables Specific tables to include
|
|
39
|
+
# @return [String] TTX XML content
|
|
40
|
+
def generate(options = {})
|
|
41
|
+
table_list = options[:tables] || :all
|
|
42
|
+
|
|
43
|
+
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
44
|
+
xml.ttFont(
|
|
45
|
+
"sfntVersion" => format_sfnt_version(to_int(@font.header.sfnt_version)),
|
|
46
|
+
"ttLibVersion" => "4.0",
|
|
47
|
+
) do
|
|
48
|
+
generate_glyph_order(xml)
|
|
49
|
+
|
|
50
|
+
tables_to_generate = select_tables(table_list)
|
|
51
|
+
tables_to_generate.each do |tag|
|
|
52
|
+
generate_table(xml, tag)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
format_output(builder.to_xml)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Convert BinData value to native Ruby integer
|
|
63
|
+
#
|
|
64
|
+
# @param value [Object] BinData value or integer
|
|
65
|
+
# @return [Integer] Native integer
|
|
66
|
+
def to_int(value)
|
|
67
|
+
value.respond_to?(:to_i) ? value.to_i : value
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Generate GlyphOrder section (required first)
|
|
71
|
+
#
|
|
72
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
73
|
+
# @return [void]
|
|
74
|
+
def generate_glyph_order(xml)
|
|
75
|
+
xml.GlyphOrder do
|
|
76
|
+
xml.comment(" The 'id' attribute is only for humans; it is ignored when parsed. ")
|
|
77
|
+
glyph_count.times do |glyph_id|
|
|
78
|
+
glyph_name = get_glyph_name(glyph_id)
|
|
79
|
+
xml.GlyphID("id" => glyph_id, "name" => glyph_name)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Generate individual table XML
|
|
85
|
+
#
|
|
86
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
87
|
+
# @param tag [String] Table tag
|
|
88
|
+
# @return [void]
|
|
89
|
+
def generate_table(xml, tag)
|
|
90
|
+
table = @font.table(tag)
|
|
91
|
+
|
|
92
|
+
# If table can't be parsed but data exists, use binary fallback
|
|
93
|
+
unless table
|
|
94
|
+
if @font.table_data && @font.table_data[tag]
|
|
95
|
+
generate_binary_table_from_data(xml, tag, @font.table_data[tag])
|
|
96
|
+
end
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
case tag
|
|
101
|
+
when "head"
|
|
102
|
+
generate_head_table(xml, table)
|
|
103
|
+
when "hhea"
|
|
104
|
+
generate_hhea_table(xml, table)
|
|
105
|
+
when "maxp"
|
|
106
|
+
generate_maxp_table(xml, table)
|
|
107
|
+
when "post"
|
|
108
|
+
generate_post_table(xml, table)
|
|
109
|
+
when "name"
|
|
110
|
+
generate_name_table(xml, table)
|
|
111
|
+
when "OS/2"
|
|
112
|
+
# Skip OS/2 for now - Nokogiri builder can't handle slashes in element names
|
|
113
|
+
# TODO: Implement OS/2 table generation with proper XML escaping
|
|
114
|
+
xml.comment(" OS/2 table skipped - requires special XML handling ")
|
|
115
|
+
when "cmap"
|
|
116
|
+
generate_cmap_table(xml, table)
|
|
117
|
+
when "loca"
|
|
118
|
+
generate_loca_table(xml, table)
|
|
119
|
+
when "glyf"
|
|
120
|
+
generate_glyf_table(xml, table)
|
|
121
|
+
when "CFF"
|
|
122
|
+
generate_cff_table(xml, table)
|
|
123
|
+
when "CFF "
|
|
124
|
+
generate_cff_table(xml, table)
|
|
125
|
+
when "hmtx"
|
|
126
|
+
generate_hmtx_table(xml, table)
|
|
127
|
+
when "fvar"
|
|
128
|
+
generate_fvar_table(xml, table)
|
|
129
|
+
when "gvar", "cvar", "HVAR", "VVAR", "MVAR"
|
|
130
|
+
generate_variation_table(xml, tag, table)
|
|
131
|
+
else
|
|
132
|
+
generate_binary_table(xml, tag, table)
|
|
133
|
+
end
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
# Fallback to binary on error
|
|
136
|
+
xml.comment(" Error generating #{tag}: #{e.message} ")
|
|
137
|
+
generate_binary_table(xml, tag, table)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Generate head table XML
|
|
141
|
+
#
|
|
142
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
143
|
+
# @param table [Tables::Head] Head table
|
|
144
|
+
# @return [void]
|
|
145
|
+
def generate_head_table(xml, table)
|
|
146
|
+
xml.head do
|
|
147
|
+
xml.comment(" Most of this table will be recalculated by the compiler ")
|
|
148
|
+
xml.tableVersion("value" => format_fixed(to_int(table.version)))
|
|
149
|
+
xml.fontRevision("value" => format_fixed(to_int(table.font_revision)))
|
|
150
|
+
xml.checkSumAdjustment("value" => format_hex(to_int(table.checksum_adjustment)))
|
|
151
|
+
xml.magicNumber("value" => format_hex(to_int(table.magic_number)))
|
|
152
|
+
xml.flags("value" => to_int(table.flags))
|
|
153
|
+
xml.unitsPerEm("value" => to_int(table.units_per_em))
|
|
154
|
+
xml.created("value" => format_timestamp(to_int(table.created)))
|
|
155
|
+
xml.modified("value" => format_timestamp(to_int(table.modified)))
|
|
156
|
+
xml.xMin("value" => to_int(table.x_min))
|
|
157
|
+
xml.yMin("value" => to_int(table.y_min))
|
|
158
|
+
xml.xMax("value" => to_int(table.x_max))
|
|
159
|
+
xml.yMax("value" => to_int(table.y_max))
|
|
160
|
+
xml.macStyle("value" => format_binary_flags(to_int(table.mac_style),
|
|
161
|
+
16))
|
|
162
|
+
xml.lowestRecPPEM("value" => to_int(table.lowest_rec_ppem))
|
|
163
|
+
xml.fontDirectionHint("value" => to_int(table.font_direction_hint))
|
|
164
|
+
xml.indexToLocFormat("value" => to_int(table.index_to_loc_format))
|
|
165
|
+
xml.glyphDataFormat("value" => to_int(table.glyph_data_format))
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Generate hhea table XML
|
|
170
|
+
#
|
|
171
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
172
|
+
# @param table [Tables::Hhea] Hhea table
|
|
173
|
+
# @return [void]
|
|
174
|
+
def generate_hhea_table(xml, table)
|
|
175
|
+
xml.hhea do
|
|
176
|
+
xml.tableVersion("value" => format_hex(to_int(table.version)))
|
|
177
|
+
xml.ascent("value" => to_int(table.ascent))
|
|
178
|
+
xml.descent("value" => to_int(table.descent))
|
|
179
|
+
xml.lineGap("value" => to_int(table.line_gap))
|
|
180
|
+
xml.advanceWidthMax("value" => to_int(table.advance_width_max))
|
|
181
|
+
xml.minLeftSideBearing("value" => to_int(table.min_left_side_bearing))
|
|
182
|
+
xml.minRightSideBearing("value" => to_int(table.min_right_side_bearing))
|
|
183
|
+
xml.xMaxExtent("value" => to_int(table.x_max_extent))
|
|
184
|
+
xml.caretSlopeRise("value" => to_int(table.caret_slope_rise))
|
|
185
|
+
xml.caretSlopeRun("value" => to_int(table.caret_slope_run))
|
|
186
|
+
xml.caretOffset("value" => to_int(table.caret_offset))
|
|
187
|
+
xml.reserved0("value" => 0)
|
|
188
|
+
xml.reserved1("value" => 0)
|
|
189
|
+
xml.reserved2("value" => 0)
|
|
190
|
+
xml.reserved3("value" => 0)
|
|
191
|
+
xml.metricDataFormat("value" => to_int(table.metric_data_format))
|
|
192
|
+
xml.numberOfHMetrics("value" => to_int(table.num_of_long_hor_metrics))
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Generate maxp table XML
|
|
197
|
+
#
|
|
198
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
199
|
+
# @param table [Tables::Maxp] Maxp table
|
|
200
|
+
# @return [void]
|
|
201
|
+
def generate_maxp_table(xml, table)
|
|
202
|
+
xml.maxp do
|
|
203
|
+
xml.comment(" Most of this table will be recalculated by the compiler ")
|
|
204
|
+
version = to_int(table.version)
|
|
205
|
+
xml.tableVersion("value" => format_hex(version))
|
|
206
|
+
xml.numGlyphs("value" => to_int(table.num_glyphs))
|
|
207
|
+
|
|
208
|
+
if version >= 0x00010000
|
|
209
|
+
xml.maxPoints("value" => to_int(table.max_points))
|
|
210
|
+
xml.maxContours("value" => to_int(table.max_contours))
|
|
211
|
+
xml.maxCompositePoints("value" => to_int(table.max_component_points))
|
|
212
|
+
xml.maxCompositeContours("value" => to_int(table.max_component_contours))
|
|
213
|
+
xml.maxZones("value" => to_int(table.max_zones))
|
|
214
|
+
xml.maxTwilightPoints("value" => to_int(table.max_twilight_points))
|
|
215
|
+
xml.maxStorage("value" => to_int(table.max_storage))
|
|
216
|
+
xml.maxFunctionDefs("value" => to_int(table.max_function_defs))
|
|
217
|
+
xml.maxInstructionDefs("value" => to_int(table.max_instruction_defs))
|
|
218
|
+
xml.maxStackElements("value" => to_int(table.max_stack_elements))
|
|
219
|
+
xml.maxSizeOfInstructions("value" => to_int(table.max_size_of_instructions))
|
|
220
|
+
xml.maxComponentElements("value" => to_int(table.max_component_elements))
|
|
221
|
+
xml.maxComponentDepth("value" => to_int(table.max_component_depth))
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Generate post table XML
|
|
227
|
+
#
|
|
228
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
229
|
+
# @param table [Tables::Post] Post table
|
|
230
|
+
# @return [void]
|
|
231
|
+
def generate_post_table(xml, table)
|
|
232
|
+
xml.post do
|
|
233
|
+
xml.formatType("value" => format_fixed(to_int(table.format)))
|
|
234
|
+
xml.italicAngle("value" => format_fixed(to_int(table.italic_angle)))
|
|
235
|
+
xml.underlinePosition("value" => to_int(table.underline_position))
|
|
236
|
+
xml.underlineThickness("value" => to_int(table.underline_thickness))
|
|
237
|
+
xml.isFixedPitch("value" => to_int(table.is_fixed_pitch))
|
|
238
|
+
xml.minMemType42("value" => to_int(table.min_mem_type42))
|
|
239
|
+
xml.maxMemType42("value" => to_int(table.max_mem_type42))
|
|
240
|
+
xml.minMemType1("value" => to_int(table.min_mem_type1))
|
|
241
|
+
xml.maxMemType1("value" => to_int(table.max_mem_type1))
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Generate name table XML
|
|
246
|
+
#
|
|
247
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
248
|
+
# @param table [Tables::Name] Name table
|
|
249
|
+
# @return [void]
|
|
250
|
+
def generate_name_table(xml, table)
|
|
251
|
+
xml.name do
|
|
252
|
+
table.name_records.each do |record|
|
|
253
|
+
xml.namerecord(
|
|
254
|
+
"nameID" => to_int(record.name_id),
|
|
255
|
+
"platformID" => to_int(record.platform_id),
|
|
256
|
+
"platEncID" => to_int(record.encoding_id),
|
|
257
|
+
"langID" => format_hex(to_int(record.language_id), width: 3),
|
|
258
|
+
) do
|
|
259
|
+
xml.text(record.string)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Generate OS/2 table XML
|
|
266
|
+
#
|
|
267
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
268
|
+
# @param table [Tables::Os2] OS/2 table
|
|
269
|
+
# @return [void]
|
|
270
|
+
def generate_os2_table(xml, table)
|
|
271
|
+
# OS/2 requires special handling due to slash in tag name
|
|
272
|
+
# Generate it as a string and insert into the parent
|
|
273
|
+
generate_binary_table(xml, "OS/2", table)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Helper to add element with value attribute
|
|
277
|
+
#
|
|
278
|
+
# @param parent [Nokogiri::XML::Element] Parent element
|
|
279
|
+
# @param name [String] Element name
|
|
280
|
+
# @param value [Object] Value
|
|
281
|
+
# @param doc [Nokogiri::XML::Document] Document
|
|
282
|
+
# @return [void]
|
|
283
|
+
def add_element_with_value(parent, name, value, doc)
|
|
284
|
+
elem = doc.create_element(name)
|
|
285
|
+
elem["value"] = value.to_s
|
|
286
|
+
parent.add_child(elem)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Generate binary table as hexdata
|
|
290
|
+
#
|
|
291
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
292
|
+
# @param tag [String] Table tag
|
|
293
|
+
# @param table [Object] Table object
|
|
294
|
+
# @return [void]
|
|
295
|
+
def generate_binary_table(xml, tag, table)
|
|
296
|
+
binary_data = table.respond_to?(:to_binary_s) ? table.to_binary_s : ""
|
|
297
|
+
xml.send(tag.to_sym) do
|
|
298
|
+
xml.hexdata do
|
|
299
|
+
xml.text("\n #{format_hex_data(binary_data)}\n ")
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Generate binary table from raw data
|
|
305
|
+
#
|
|
306
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
307
|
+
# @param tag [String] Table tag
|
|
308
|
+
# @param data [String] Raw binary data
|
|
309
|
+
# @return [void]
|
|
310
|
+
def generate_binary_table_from_data(xml, tag, data)
|
|
311
|
+
# Remove trailing space from tag for XML element name
|
|
312
|
+
clean_tag = tag.strip
|
|
313
|
+
xml.send(clean_tag.to_sym) do
|
|
314
|
+
xml.hexdata do
|
|
315
|
+
xml.text("\n #{format_hex_data(data)}\n ")
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Generate cmap table XML (simplified for now)
|
|
321
|
+
#
|
|
322
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
323
|
+
# @param table [Object] Cmap table
|
|
324
|
+
# @return [void]
|
|
325
|
+
def generate_cmap_table(xml, table)
|
|
326
|
+
generate_binary_table(xml, "cmap", table)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Generate loca table XML
|
|
330
|
+
#
|
|
331
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
332
|
+
# @param table [Object] Loca table
|
|
333
|
+
# @return [void]
|
|
334
|
+
def generate_loca_table(xml, _table)
|
|
335
|
+
xml.loca do
|
|
336
|
+
xml.comment(" The 'loca' table will be calculated by the compiler ")
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Generate glyf table XML (simplified for now)
|
|
341
|
+
#
|
|
342
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
343
|
+
# @param table [Object] Glyf table
|
|
344
|
+
# @return [void]
|
|
345
|
+
def generate_glyf_table(xml, table)
|
|
346
|
+
generate_binary_table(xml, "glyf", table)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Generate CFF table XML (simplified for now)
|
|
350
|
+
#
|
|
351
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
352
|
+
# @param table [Object] CFF table
|
|
353
|
+
# @return [void]
|
|
354
|
+
def generate_cff_table(xml, table)
|
|
355
|
+
generate_binary_table(xml, "CFF", table)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Generate hmtx table XML (simplified for now)
|
|
359
|
+
#
|
|
360
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
361
|
+
# @param table [Object] Hmtx table
|
|
362
|
+
# @return [void]
|
|
363
|
+
def generate_hmtx_table(xml, table)
|
|
364
|
+
generate_binary_table(xml, "hmtx", table)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Generate fvar table XML
|
|
368
|
+
#
|
|
369
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
370
|
+
# @param table [Tables::Fvar] Fvar table
|
|
371
|
+
# @return [void]
|
|
372
|
+
def generate_fvar_table(xml, table)
|
|
373
|
+
xml.fvar do
|
|
374
|
+
xml.Version("major" => to_int(table.major_version),
|
|
375
|
+
"minor" => to_int(table.minor_version))
|
|
376
|
+
|
|
377
|
+
table.axes.each do |axis|
|
|
378
|
+
xml.Axis do
|
|
379
|
+
xml.AxisTag axis.axis_tag
|
|
380
|
+
xml.MinValue to_int(axis.min_value) / 65536.0
|
|
381
|
+
xml.DefaultValue to_int(axis.default_value) / 65536.0
|
|
382
|
+
xml.MaxValue to_int(axis.max_value) / 65536.0
|
|
383
|
+
xml.AxisNameID to_int(axis.axis_name_id)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Generate variation table XML (gvar, cvar, HVAR, etc.)
|
|
390
|
+
#
|
|
391
|
+
# @param xml [Nokogiri::XML::Builder] XML builder
|
|
392
|
+
# @param tag [String] Table tag
|
|
393
|
+
# @param table [Object] Variation table
|
|
394
|
+
# @return [void]
|
|
395
|
+
def generate_variation_table(xml, tag, table)
|
|
396
|
+
generate_binary_table(xml, tag, table)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Select tables to generate
|
|
400
|
+
#
|
|
401
|
+
# @param table_list [Symbol, Array<String>] :all or list of tags
|
|
402
|
+
# @return [Array<String>] Table tags to generate
|
|
403
|
+
def select_tables(table_list)
|
|
404
|
+
if table_list == :all
|
|
405
|
+
@font.table_names
|
|
406
|
+
else
|
|
407
|
+
available = @font.table_names
|
|
408
|
+
requested = Array(table_list).map(&:to_s)
|
|
409
|
+
# Map CFF to "CFF " if needed
|
|
410
|
+
requested = requested.map do |tag|
|
|
411
|
+
if tag == "CFF" && !available.include?("CFF") && available.include?("CFF ")
|
|
412
|
+
"CFF "
|
|
413
|
+
else
|
|
414
|
+
tag
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
requested.select { |tag| available.include?(tag) }
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Get number of glyphs
|
|
422
|
+
#
|
|
423
|
+
# @return [Integer] Number of glyphs
|
|
424
|
+
def glyph_count
|
|
425
|
+
maxp = @font.table("maxp")
|
|
426
|
+
maxp ? to_int(maxp.num_glyphs) : 0
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Get glyph name by ID
|
|
430
|
+
#
|
|
431
|
+
# @param glyph_id [Integer] Glyph ID
|
|
432
|
+
# @return [String] Glyph name
|
|
433
|
+
def get_glyph_name(glyph_id)
|
|
434
|
+
post = @font.table("post")
|
|
435
|
+
if post.respond_to?(:glyph_names) && post.glyph_names
|
|
436
|
+
post.glyph_names[glyph_id] || ".notdef"
|
|
437
|
+
elsif glyph_id.zero?
|
|
438
|
+
".notdef"
|
|
439
|
+
else
|
|
440
|
+
"glyph#{glyph_id.to_s.rjust(5, '0')}"
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Format SFNT version
|
|
445
|
+
#
|
|
446
|
+
# @param version [Integer] SFNT version
|
|
447
|
+
# @return [String] Formatted version as escaped bytes
|
|
448
|
+
def format_sfnt_version(version)
|
|
449
|
+
# Format as 4 bytes for TTX compatibility
|
|
450
|
+
bytes = [version].pack("N").bytes
|
|
451
|
+
"\\x#{bytes.map { |b| b.to_s(16).rjust(2, '0') }.join('\\x')}"
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Format fixed-point number (16.16)
|
|
455
|
+
#
|
|
456
|
+
# @param value [Integer] Fixed-point value
|
|
457
|
+
# @return [String] Decimal string
|
|
458
|
+
def format_fixed(value)
|
|
459
|
+
result = value.to_f / 65536.0
|
|
460
|
+
# Format with minimal decimal places
|
|
461
|
+
if result == result.to_i
|
|
462
|
+
"#{result.to_i}.0"
|
|
463
|
+
else
|
|
464
|
+
result.to_s
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Format hex value
|
|
469
|
+
#
|
|
470
|
+
# @param value [Integer] Integer value
|
|
471
|
+
# @param width [Integer] Minimum hex width
|
|
472
|
+
# @return [String] Hex string (e.g., "0x1234")
|
|
473
|
+
def format_hex(value, width: 8)
|
|
474
|
+
int_value = value.respond_to?(:to_i) ? value.to_i : value
|
|
475
|
+
"0x#{int_value.to_s(16).rjust(width, '0')}"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Format binary flags
|
|
479
|
+
#
|
|
480
|
+
# @param value [Integer] Integer value
|
|
481
|
+
# @param bits [Integer] Number of bits
|
|
482
|
+
# @return [String] Binary string with spaces every 8 bits
|
|
483
|
+
def format_binary_flags(value, bits)
|
|
484
|
+
binary = value.to_s(2).rjust(bits, "0")
|
|
485
|
+
# Add spaces every 8 bits from left
|
|
486
|
+
binary.scan(/.{1,8}/).join(" ")
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# Format timestamp
|
|
490
|
+
#
|
|
491
|
+
# @param timestamp [Integer] Mac timestamp (seconds since 1904-01-01)
|
|
492
|
+
# @return [String] Human-readable date string
|
|
493
|
+
def format_timestamp(timestamp)
|
|
494
|
+
# Mac epoch: Jan 1, 1904 00:00:00 UTC
|
|
495
|
+
mac_epoch = Time.utc(1904, 1, 1)
|
|
496
|
+
time = mac_epoch + timestamp
|
|
497
|
+
time.strftime("%a %b %e %H:%M:%S %Y")
|
|
498
|
+
rescue StandardError
|
|
499
|
+
"Invalid Date"
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Format binary data as hex
|
|
503
|
+
#
|
|
504
|
+
# @param data [String] Binary data
|
|
505
|
+
# @return [String] Hex string with newlines every 32 bytes
|
|
506
|
+
def format_hex_data(data)
|
|
507
|
+
hex = data.unpack1("H*")
|
|
508
|
+
# Format in lines of 64 hex chars (32 bytes) for readability
|
|
509
|
+
hex.scan(/.{1,64}/).join("\n ")
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Format output XML
|
|
513
|
+
#
|
|
514
|
+
# @param xml [String] Raw XML
|
|
515
|
+
# @return [String] Formatted XML
|
|
516
|
+
def format_output(xml)
|
|
517
|
+
if @pretty
|
|
518
|
+
doc = Nokogiri::XML(xml)
|
|
519
|
+
doc.to_xml(indent: @indent)
|
|
520
|
+
else
|
|
521
|
+
# Remove extra whitespace for compact format
|
|
522
|
+
xml.gsub(/>\s+</, "><").gsub(/\n\s*/, "")
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|