fontisan 0.2.1 → 0.2.3
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 +58 -392
- data/README.adoc +1509 -1430
- data/Rakefile +3 -2
- data/benchmark/variation_quick_bench.rb +4 -4
- data/docs/FONT_HINTING.adoc +562 -0
- data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
- data/lib/fontisan/base_collection.rb +296 -0
- data/lib/fontisan/cli.rb +10 -3
- data/lib/fontisan/collection/builder.rb +2 -1
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/commands/base_command.rb +5 -2
- data/lib/fontisan/commands/convert_command.rb +6 -2
- data/lib/fontisan/commands/info_command.rb +129 -5
- data/lib/fontisan/commands/instance_command.rb +8 -7
- data/lib/fontisan/commands/validate_command.rb +4 -1
- data/lib/fontisan/constants.rb +24 -24
- data/lib/fontisan/converters/format_converter.rb +8 -4
- data/lib/fontisan/converters/outline_converter.rb +21 -16
- data/lib/fontisan/converters/woff_writer.rb +8 -3
- data/lib/fontisan/font_loader.rb +120 -30
- data/lib/fontisan/font_writer.rb +2 -0
- data/lib/fontisan/formatters/text_formatter.rb +116 -19
- data/lib/fontisan/hints/hint_converter.rb +43 -47
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +1 -3
- data/lib/fontisan/hints/postscript_hint_extractor.rb +78 -43
- data/lib/fontisan/hints/truetype_hint_extractor.rb +22 -26
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
- data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
- data/lib/fontisan/loading_modes.rb +4 -4
- data/lib/fontisan/models/collection_brief_info.rb +37 -0
- data/lib/fontisan/models/collection_info.rb +6 -1
- data/lib/fontisan/models/font_export.rb +2 -2
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +22 -23
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/models/validation_report.rb +1 -1
- data/lib/fontisan/open_type_collection.rb +17 -220
- data/lib/fontisan/open_type_font.rb +3 -1
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/output_writer.rb +8 -3
- data/lib/fontisan/pipeline/transformation_pipeline.rb +8 -3
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +38 -12
- data/lib/fontisan/tables/cff/charstring_parser.rb +23 -11
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +14 -14
- data/lib/fontisan/tables/cff/dict_builder.rb +4 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +6 -4
- data/lib/fontisan/tables/cff/offset_recalculator.rb +1 -1
- data/lib/fontisan/tables/cff/private_dict_writer.rb +10 -4
- data/lib/fontisan/tables/cff/table_builder.rb +1 -1
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +7 -6
- data/lib/fontisan/tables/cff2/region_matcher.rb +2 -2
- data/lib/fontisan/tables/cff2/table_builder.rb +26 -20
- data/lib/fontisan/tables/cff2/table_reader.rb +35 -33
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +2 -2
- data/lib/fontisan/tables/cff2.rb +1 -1
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
- data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
- data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_collection.rb +29 -113
- data/lib/fontisan/true_type_font.rb +3 -1
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +2 -1
- data/lib/fontisan/variation/delta_applier.rb +2 -1
- data/lib/fontisan/variation/inspector.rb +2 -1
- data/lib/fontisan/variation/instance_generator.rb +2 -1
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/variation_preserver.rb +4 -1
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/glyf_transformer.rb +57 -30
- data/lib/fontisan/woff2_font.rb +31 -15
- data/lib/fontisan.rb +42 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +9 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "lutaml/model"
|
|
4
4
|
require_relative "table_sharing_info"
|
|
5
|
+
require_relative "font_info"
|
|
5
6
|
|
|
6
7
|
module Fontisan
|
|
7
8
|
module Models
|
|
@@ -20,7 +21,8 @@ module Fontisan
|
|
|
20
21
|
# num_fonts: 6,
|
|
21
22
|
# font_offsets: [48, 380, 712, 1044, 1376, 1676],
|
|
22
23
|
# file_size_bytes: 2240000,
|
|
23
|
-
# table_sharing: table_sharing_obj
|
|
24
|
+
# table_sharing: table_sharing_obj,
|
|
25
|
+
# fonts: [font_info1, font_info2, ...]
|
|
24
26
|
# )
|
|
25
27
|
class CollectionInfo < Lutaml::Model::Serializable
|
|
26
28
|
attribute :collection_path, :string
|
|
@@ -32,6 +34,7 @@ module Fontisan
|
|
|
32
34
|
attribute :font_offsets, :integer, collection: true
|
|
33
35
|
attribute :file_size_bytes, :integer
|
|
34
36
|
attribute :table_sharing, TableSharingInfo
|
|
37
|
+
attribute :fonts, FontInfo, collection: true
|
|
35
38
|
|
|
36
39
|
yaml do
|
|
37
40
|
map "collection_path", to: :collection_path
|
|
@@ -43,6 +46,7 @@ module Fontisan
|
|
|
43
46
|
map "font_offsets", to: :font_offsets
|
|
44
47
|
map "file_size_bytes", to: :file_size_bytes
|
|
45
48
|
map "table_sharing", to: :table_sharing
|
|
49
|
+
map "fonts", to: :fonts
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
json do
|
|
@@ -55,6 +59,7 @@ module Fontisan
|
|
|
55
59
|
map "font_offsets", to: :font_offsets
|
|
56
60
|
map "file_size_bytes", to: :file_size_bytes
|
|
57
61
|
map "table_sharing", to: :table_sharing
|
|
62
|
+
map "fonts", to: :fonts
|
|
58
63
|
end
|
|
59
64
|
|
|
60
65
|
# Get version as a formatted string
|
|
@@ -71,8 +71,8 @@ module Fontisan
|
|
|
71
71
|
attribute :tag, :string
|
|
72
72
|
attribute :checksum, :string
|
|
73
73
|
attribute :parsed, :boolean, default: -> { false }
|
|
74
|
-
attribute :data, :string, default: -> {
|
|
75
|
-
attribute :fields, :string, default: -> {
|
|
74
|
+
attribute :data, :string, default: -> {}
|
|
75
|
+
attribute :fields, :string, default: -> {}
|
|
76
76
|
|
|
77
77
|
yaml do
|
|
78
78
|
map "tag", to: :tag
|
|
@@ -36,37 +36,9 @@ module Fontisan
|
|
|
36
36
|
attribute :font_revision, :float
|
|
37
37
|
attribute :permissions, :string
|
|
38
38
|
attribute :units_per_em, :integer
|
|
39
|
+
attribute :collection_offset, :integer
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
map "font_format", to: :font_format
|
|
42
|
-
map "is_variable", to: :is_variable
|
|
43
|
-
map "family_name", to: :family_name
|
|
44
|
-
map "subfamily_name", to: :subfamily_name
|
|
45
|
-
map "full_name", to: :full_name
|
|
46
|
-
map "postscript_name", to: :postscript_name
|
|
47
|
-
map "postscript_cid_name", to: :postscript_cid_name
|
|
48
|
-
map "preferred_family", to: :preferred_family
|
|
49
|
-
map "preferred_subfamily", to: :preferred_subfamily
|
|
50
|
-
map "mac_font_menu_name", to: :mac_font_menu_name
|
|
51
|
-
map "version", to: :version
|
|
52
|
-
map "unique_id", to: :unique_id
|
|
53
|
-
map "description", to: :description
|
|
54
|
-
map "designer", to: :designer
|
|
55
|
-
map "designer_url", to: :designer_url
|
|
56
|
-
map "manufacturer", to: :manufacturer
|
|
57
|
-
map "vendor_url", to: :vendor_url
|
|
58
|
-
map "vendor_id", to: :vendor_id
|
|
59
|
-
map "trademark", to: :trademark
|
|
60
|
-
map "copyright", to: :copyright
|
|
61
|
-
map "license_description", to: :license_description
|
|
62
|
-
map "license_url", to: :license_url
|
|
63
|
-
map "sample_text", to: :sample_text
|
|
64
|
-
map "font_revision", to: :font_revision
|
|
65
|
-
map "permissions", to: :permissions
|
|
66
|
-
map "units_per_em", to: :units_per_em
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
yaml do
|
|
41
|
+
key_value do
|
|
70
42
|
map "font_format", to: :font_format
|
|
71
43
|
map "is_variable", to: :is_variable
|
|
72
44
|
map "family_name", to: :family_name
|
|
@@ -93,6 +65,7 @@ module Fontisan
|
|
|
93
65
|
map "font_revision", to: :font_revision
|
|
94
66
|
map "permissions", to: :permissions
|
|
95
67
|
map "units_per_em", to: :units_per_em
|
|
68
|
+
map "collection_offset", to: :collection_offset
|
|
96
69
|
end
|
|
97
70
|
end
|
|
98
71
|
end
|
data/lib/fontisan/models/hint.rb
CHANGED
|
@@ -76,7 +76,7 @@ module Fontisan
|
|
|
76
76
|
{
|
|
77
77
|
type: h.type,
|
|
78
78
|
data: h.data,
|
|
79
|
-
source_format: h.source_format
|
|
79
|
+
source_format: h.source_format,
|
|
80
80
|
}
|
|
81
81
|
end
|
|
82
82
|
glyph_hints_hash[glyph_id.to_s] = hints_data
|
|
@@ -124,6 +124,7 @@ module Fontisan
|
|
|
124
124
|
# Parse glyph hints JSON
|
|
125
125
|
def parse_glyph_hints
|
|
126
126
|
return {} if @glyph_hints.nil? || @glyph_hints.empty? || @glyph_hints == "{}"
|
|
127
|
+
|
|
127
128
|
JSON.parse(@glyph_hints)
|
|
128
129
|
rescue JSON::ParserError
|
|
129
130
|
{}
|
|
@@ -202,7 +203,7 @@ module Fontisan
|
|
|
202
203
|
when :interpolate
|
|
203
204
|
# IUP instruction
|
|
204
205
|
axis = data[:axis] || :y
|
|
205
|
-
axis == :x ? [0x31] : [0x30]
|
|
206
|
+
axis == :x ? [0x31] : [0x30] # IUP[x] or IUP[y]
|
|
206
207
|
when :shift
|
|
207
208
|
# SHP instruction
|
|
208
209
|
data[:instructions] || []
|
|
@@ -268,23 +269,22 @@ module Fontisan
|
|
|
268
269
|
|
|
269
270
|
# Convert stem hint to TrueType instructions
|
|
270
271
|
def convert_stem_to_truetype
|
|
271
|
-
|
|
272
|
-
|
|
272
|
+
data[:position] || 0
|
|
273
|
+
data[:width] || 0
|
|
273
274
|
orientation = data[:orientation] || :vertical
|
|
274
275
|
|
|
275
276
|
# TrueType uses MDAP (Move Direct Absolute Point) and MDRP (Move Direct Relative Point)
|
|
276
277
|
# to control stem positioning
|
|
277
278
|
instructions = []
|
|
278
279
|
|
|
279
|
-
if orientation == :vertical
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
end
|
|
280
|
+
instructions << if orientation == :vertical
|
|
281
|
+
# Vertical stem: use Y-axis instructions
|
|
282
|
+
0x2E # MDAP[rnd] - mark reference point
|
|
283
|
+
else
|
|
284
|
+
# Horizontal stem: use X-axis instructions
|
|
285
|
+
0x2F # MDAP[rnd]
|
|
286
|
+
end
|
|
287
|
+
instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
|
|
288
288
|
|
|
289
289
|
instructions
|
|
290
290
|
end
|
|
@@ -313,16 +313,15 @@ module Fontisan
|
|
|
313
313
|
# Generate MDAP/MDRP pairs for each stem
|
|
314
314
|
instructions = []
|
|
315
315
|
|
|
316
|
-
stems.each do |
|
|
317
|
-
if orientation == :vertical
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
end
|
|
316
|
+
stems.each do |_stem|
|
|
317
|
+
instructions << if orientation == :vertical
|
|
318
|
+
# Vertical stem: use Y-axis instructions
|
|
319
|
+
0x2E # MDAP[rnd] - mark reference point
|
|
320
|
+
else
|
|
321
|
+
# Horizontal stem: use X-axis instructions
|
|
322
|
+
0x2F # MDAP[rnd]
|
|
323
|
+
end
|
|
324
|
+
instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
|
|
326
325
|
end
|
|
327
326
|
|
|
328
327
|
instructions
|
|
@@ -121,7 +121,10 @@ module Fontisan
|
|
|
121
121
|
|
|
122
122
|
# Get path from CharString
|
|
123
123
|
path = charstring.path
|
|
124
|
-
|
|
124
|
+
if path.nil? || path.empty?
|
|
125
|
+
raise ArgumentError,
|
|
126
|
+
"CharString has no path data"
|
|
127
|
+
end
|
|
125
128
|
|
|
126
129
|
commands = convert_cff_path_to_commands(path)
|
|
127
130
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative "constants"
|
|
3
|
+
require_relative "base_collection"
|
|
5
4
|
|
|
6
5
|
module Fontisan
|
|
7
|
-
# OpenType Collection domain object
|
|
6
|
+
# OpenType Collection domain object
|
|
8
7
|
#
|
|
9
|
-
# Represents a complete OpenType Collection file (OTC)
|
|
10
|
-
#
|
|
8
|
+
# Represents a complete OpenType Collection file (OTC). Inherits all shared
|
|
9
|
+
# functionality from BaseCollection and implements OTC-specific behavior.
|
|
11
10
|
#
|
|
12
11
|
# @example Reading and extracting fonts
|
|
13
12
|
# File.open("fonts.otc", "rb") do |io|
|
|
@@ -15,39 +14,26 @@ module Fontisan
|
|
|
15
14
|
# puts otc.num_fonts # => 4
|
|
16
15
|
# fonts = otc.extract_fonts(io) # => [OpenTypeFont, OpenTypeFont, ...]
|
|
17
16
|
# end
|
|
18
|
-
class OpenTypeCollection <
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
string :tag, length: 4, assert: "ttcf"
|
|
22
|
-
uint16 :major_version
|
|
23
|
-
uint16 :minor_version
|
|
24
|
-
uint32 :num_fonts
|
|
25
|
-
array :font_offsets, type: :uint32, initial_length: :num_fonts
|
|
26
|
-
|
|
27
|
-
# Read OpenType Collection from a file
|
|
17
|
+
class OpenTypeCollection < BaseCollection
|
|
18
|
+
# Get the font class for OpenType collections
|
|
28
19
|
#
|
|
29
|
-
# @
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def self.from_file(path)
|
|
35
|
-
if path.nil? || path.to_s.empty?
|
|
36
|
-
raise ArgumentError,
|
|
37
|
-
"path cannot be nil or empty"
|
|
38
|
-
end
|
|
39
|
-
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
20
|
+
# @return [Class] OpenTypeFont class
|
|
21
|
+
def self.font_class
|
|
22
|
+
require_relative "open_type_font"
|
|
23
|
+
OpenTypeFont
|
|
24
|
+
end
|
|
40
25
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
26
|
+
# Get the collection format identifier
|
|
27
|
+
#
|
|
28
|
+
# @return [String] "OTC" for OpenType Collection
|
|
29
|
+
def self.collection_format
|
|
30
|
+
"OTC"
|
|
46
31
|
end
|
|
47
32
|
|
|
48
33
|
# Extract fonts as OpenTypeFont objects
|
|
49
34
|
#
|
|
50
35
|
# Reads each font from the OTC file and returns them as OpenTypeFont objects.
|
|
36
|
+
# This method uses the from_collection method.
|
|
51
37
|
#
|
|
52
38
|
# @param io [IO] Open file handle to read fonts from
|
|
53
39
|
# @return [Array<OpenTypeFont>] Array of font objects
|
|
@@ -58,194 +44,5 @@ module Fontisan
|
|
|
58
44
|
OpenTypeFont.from_collection(io, offset)
|
|
59
45
|
end
|
|
60
46
|
end
|
|
61
|
-
|
|
62
|
-
# Get a single font from the collection
|
|
63
|
-
#
|
|
64
|
-
# @param index [Integer] Index of the font (0-based)
|
|
65
|
-
# @param io [IO] Open file handle
|
|
66
|
-
# @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
|
|
67
|
-
# @return [OpenTypeFont, nil] Font object or nil if index out of range
|
|
68
|
-
def font(index, io, mode: LoadingModes::FULL)
|
|
69
|
-
return nil if index >= num_fonts
|
|
70
|
-
|
|
71
|
-
require_relative "open_type_font"
|
|
72
|
-
OpenTypeFont.from_collection(io, font_offsets[index], mode: mode)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Get font count
|
|
76
|
-
#
|
|
77
|
-
# @return [Integer] Number of fonts in collection
|
|
78
|
-
def font_count
|
|
79
|
-
num_fonts
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Validate format correctness
|
|
83
|
-
#
|
|
84
|
-
# @return [Boolean] true if the format is valid, false otherwise
|
|
85
|
-
def valid?
|
|
86
|
-
tag == Constants::TTC_TAG && num_fonts.positive? && font_offsets.length == num_fonts
|
|
87
|
-
rescue StandardError
|
|
88
|
-
false
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Get the OTC version as a single integer
|
|
92
|
-
#
|
|
93
|
-
# @return [Integer] Version number (e.g., 0x00010000 for version 1.0)
|
|
94
|
-
def version
|
|
95
|
-
(major_version << 16) | minor_version
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# List all fonts in the collection with basic metadata
|
|
99
|
-
#
|
|
100
|
-
# Returns a CollectionListInfo model containing summaries of all fonts.
|
|
101
|
-
# This is the API method used by the `ls` command for collections.
|
|
102
|
-
#
|
|
103
|
-
# @param io [IO] Open file handle to read fonts from
|
|
104
|
-
# @return [CollectionListInfo] List of fonts with metadata
|
|
105
|
-
#
|
|
106
|
-
# @example List fonts in collection
|
|
107
|
-
# File.open("fonts.otc", "rb") do |io|
|
|
108
|
-
# otc = OpenTypeCollection.read(io)
|
|
109
|
-
# list = otc.list_fonts(io)
|
|
110
|
-
# list.fonts.each { |f| puts "#{f.index}: #{f.family_name}" }
|
|
111
|
-
# end
|
|
112
|
-
def list_fonts(io)
|
|
113
|
-
require_relative "models/collection_list_info"
|
|
114
|
-
require_relative "models/collection_font_summary"
|
|
115
|
-
require_relative "open_type_font"
|
|
116
|
-
require_relative "tables/name"
|
|
117
|
-
|
|
118
|
-
fonts = font_offsets.map.with_index do |offset, index|
|
|
119
|
-
font = OpenTypeFont.from_collection(io, offset)
|
|
120
|
-
|
|
121
|
-
# Extract basic font info
|
|
122
|
-
name_table = font.table("name")
|
|
123
|
-
post_table = font.table("post")
|
|
124
|
-
|
|
125
|
-
family_name = name_table&.english_name(Tables::Name::FAMILY) || "Unknown"
|
|
126
|
-
subfamily_name = name_table&.english_name(Tables::Name::SUBFAMILY) || "Regular"
|
|
127
|
-
postscript_name = name_table&.english_name(Tables::Name::POSTSCRIPT_NAME) || "Unknown"
|
|
128
|
-
|
|
129
|
-
# Determine font format
|
|
130
|
-
sfnt = font.header.sfnt_version
|
|
131
|
-
font_format = case sfnt
|
|
132
|
-
when 0x00010000, 0x74727565 # 0x74727565 = 'true'
|
|
133
|
-
"TrueType"
|
|
134
|
-
when 0x4F54544F # 'OTTO'
|
|
135
|
-
"OpenType"
|
|
136
|
-
else
|
|
137
|
-
"Unknown"
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
num_glyphs = post_table&.glyph_names&.length || 0
|
|
141
|
-
num_tables = font.table_names.length
|
|
142
|
-
|
|
143
|
-
Models::CollectionFontSummary.new(
|
|
144
|
-
index: index,
|
|
145
|
-
family_name: family_name,
|
|
146
|
-
subfamily_name: subfamily_name,
|
|
147
|
-
postscript_name: postscript_name,
|
|
148
|
-
font_format: font_format,
|
|
149
|
-
num_glyphs: num_glyphs,
|
|
150
|
-
num_tables: num_tables,
|
|
151
|
-
)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
Models::CollectionListInfo.new(
|
|
155
|
-
collection_path: nil, # Will be set by command
|
|
156
|
-
num_fonts: num_fonts,
|
|
157
|
-
fonts: fonts,
|
|
158
|
-
)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Get comprehensive collection metadata
|
|
162
|
-
#
|
|
163
|
-
# Returns a CollectionInfo model with header information, offsets,
|
|
164
|
-
# and table sharing statistics.
|
|
165
|
-
# This is the API method used by the `info` command for collections.
|
|
166
|
-
#
|
|
167
|
-
# @param io [IO] Open file handle to read fonts from
|
|
168
|
-
# @param path [String] Collection file path (for file size)
|
|
169
|
-
# @return [CollectionInfo] Collection metadata
|
|
170
|
-
#
|
|
171
|
-
# @example Get collection info
|
|
172
|
-
# File.open("fonts.otc", "rb") do |io|
|
|
173
|
-
# otc = OpenTypeCollection.read(io)
|
|
174
|
-
# info = otc.collection_info(io, "fonts.otc")
|
|
175
|
-
# puts "Version: #{info.version_string}"
|
|
176
|
-
# end
|
|
177
|
-
def collection_info(io, path)
|
|
178
|
-
require_relative "models/collection_info"
|
|
179
|
-
require_relative "models/table_sharing_info"
|
|
180
|
-
|
|
181
|
-
# Calculate table sharing statistics
|
|
182
|
-
table_sharing = calculate_table_sharing(io)
|
|
183
|
-
|
|
184
|
-
# Get file size
|
|
185
|
-
file_size = path ? File.size(path) : 0
|
|
186
|
-
|
|
187
|
-
Models::CollectionInfo.new(
|
|
188
|
-
collection_path: path,
|
|
189
|
-
collection_format: "OTC",
|
|
190
|
-
ttc_tag: tag,
|
|
191
|
-
major_version: major_version,
|
|
192
|
-
minor_version: minor_version,
|
|
193
|
-
num_fonts: num_fonts,
|
|
194
|
-
font_offsets: font_offsets.to_a,
|
|
195
|
-
file_size_bytes: file_size,
|
|
196
|
-
table_sharing: table_sharing,
|
|
197
|
-
)
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
private
|
|
201
|
-
|
|
202
|
-
# Calculate table sharing statistics
|
|
203
|
-
#
|
|
204
|
-
# Analyzes which tables are shared between fonts and calculates
|
|
205
|
-
# space savings from deduplication.
|
|
206
|
-
#
|
|
207
|
-
# @param io [IO] Open file handle
|
|
208
|
-
# @return [TableSharingInfo] Sharing statistics
|
|
209
|
-
def calculate_table_sharing(io)
|
|
210
|
-
require_relative "models/table_sharing_info"
|
|
211
|
-
require_relative "open_type_font"
|
|
212
|
-
|
|
213
|
-
# Extract all fonts
|
|
214
|
-
fonts = font_offsets.map do |offset|
|
|
215
|
-
OpenTypeFont.from_collection(io, offset)
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
# Build table hash map (checksum -> size)
|
|
219
|
-
table_map = {}
|
|
220
|
-
total_table_size = 0
|
|
221
|
-
|
|
222
|
-
fonts.each do |font|
|
|
223
|
-
font.tables.each do |entry|
|
|
224
|
-
key = entry.checksum
|
|
225
|
-
size = entry.table_length
|
|
226
|
-
table_map[key] ||= size
|
|
227
|
-
total_table_size += size
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Count unique vs shared
|
|
232
|
-
unique_tables = table_map.size
|
|
233
|
-
total_tables = fonts.sum { |f| f.tables.length }
|
|
234
|
-
shared_tables = total_tables - unique_tables
|
|
235
|
-
|
|
236
|
-
# Calculate space saved
|
|
237
|
-
unique_size = table_map.values.sum
|
|
238
|
-
space_saved = total_table_size - unique_size
|
|
239
|
-
|
|
240
|
-
# Calculate sharing percentage
|
|
241
|
-
sharing_pct = total_tables.positive? ? (shared_tables.to_f / total_tables * 100).round(2) : 0.0
|
|
242
|
-
|
|
243
|
-
Models::TableSharingInfo.new(
|
|
244
|
-
shared_tables: shared_tables,
|
|
245
|
-
unique_tables: unique_tables,
|
|
246
|
-
sharing_percentage: sharing_pct,
|
|
247
|
-
space_saved_bytes: space_saved,
|
|
248
|
-
)
|
|
249
|
-
end
|
|
250
47
|
end
|
|
251
48
|
end
|
|
@@ -211,7 +211,8 @@ module Fontisan
|
|
|
211
211
|
batch_entries.each do |entry|
|
|
212
212
|
relative_offset = entry.offset - batch_offset
|
|
213
213
|
tag_key = entry.tag.dup.force_encoding("UTF-8")
|
|
214
|
-
@table_data[tag_key] =
|
|
214
|
+
@table_data[tag_key] =
|
|
215
|
+
batch_data[relative_offset, entry.table_length]
|
|
215
216
|
end
|
|
216
217
|
end
|
|
217
218
|
|
|
@@ -286,6 +287,7 @@ module Fontisan
|
|
|
286
287
|
# @return [Boolean] true if table is available in current mode
|
|
287
288
|
def table_available?(tag)
|
|
288
289
|
return false unless has_table?(tag)
|
|
290
|
+
|
|
289
291
|
LoadingModes.table_allowed?(@loading_mode, tag)
|
|
290
292
|
end
|
|
291
293
|
|
|
@@ -222,7 +222,8 @@ module Fontisan
|
|
|
222
222
|
if @stack_aware
|
|
223
223
|
tracker = @stack_trackers[glyph_id]
|
|
224
224
|
next unless tracker
|
|
225
|
-
next unless tracker.stack_neutral?(start_pos,
|
|
225
|
+
next unless tracker.stack_neutral?(start_pos,
|
|
226
|
+
start_pos + length)
|
|
226
227
|
end
|
|
227
228
|
|
|
228
229
|
pattern_bytes = charstring[start_pos, length]
|
|
@@ -69,7 +69,10 @@ module Fontisan
|
|
|
69
69
|
# @return [Integer] Number of bytes written
|
|
70
70
|
def write_svg(result)
|
|
71
71
|
svg_xml = result[:svg_xml] || result["svg_xml"]
|
|
72
|
-
|
|
72
|
+
unless svg_xml
|
|
73
|
+
raise ArgumentError,
|
|
74
|
+
"SVG result must contain :svg_xml key"
|
|
75
|
+
end
|
|
73
76
|
|
|
74
77
|
File.write(@output_path, svg_xml)
|
|
75
78
|
end
|
|
@@ -80,7 +83,8 @@ module Fontisan
|
|
|
80
83
|
# @return [Integer] Number of bytes written
|
|
81
84
|
def write_sfnt(tables)
|
|
82
85
|
sfnt_version = determine_sfnt_version
|
|
83
|
-
FontWriter.write_to_file(tables, @output_path,
|
|
86
|
+
FontWriter.write_to_file(tables, @output_path,
|
|
87
|
+
sfnt_version: sfnt_version)
|
|
84
88
|
end
|
|
85
89
|
|
|
86
90
|
# Write WOFF format
|
|
@@ -145,7 +149,8 @@ module Fontisan
|
|
|
145
149
|
when :otf
|
|
146
150
|
OpenTypeFont.from_tables(tables)
|
|
147
151
|
else
|
|
148
|
-
raise ArgumentError,
|
|
152
|
+
raise ArgumentError,
|
|
153
|
+
"Cannot determine font type for format: #{@format}"
|
|
149
154
|
end
|
|
150
155
|
end
|
|
151
156
|
end
|
|
@@ -294,7 +294,8 @@ module Fontisan
|
|
|
294
294
|
when ".woff" then :woff
|
|
295
295
|
when ".woff2" then :woff2
|
|
296
296
|
else
|
|
297
|
-
raise ArgumentError,
|
|
297
|
+
raise ArgumentError,
|
|
298
|
+
"Cannot determine target format from extension: #{ext}"
|
|
298
299
|
end
|
|
299
300
|
end
|
|
300
301
|
|
|
@@ -304,7 +305,10 @@ module Fontisan
|
|
|
304
305
|
def variation_options
|
|
305
306
|
opts = {}
|
|
306
307
|
opts[:coordinates] = @options[:coordinates] if @options[:coordinates]
|
|
307
|
-
|
|
308
|
+
if @options[:instance_index]
|
|
309
|
+
opts[:instance_index] =
|
|
310
|
+
@options[:instance_index]
|
|
311
|
+
end
|
|
308
312
|
opts
|
|
309
313
|
end
|
|
310
314
|
|
|
@@ -344,7 +348,8 @@ module Fontisan
|
|
|
344
348
|
when :otf
|
|
345
349
|
OpenTypeFont.from_tables(tables)
|
|
346
350
|
else
|
|
347
|
-
raise ArgumentError,
|
|
351
|
+
raise ArgumentError,
|
|
352
|
+
"Cannot determine font type: format=#{format}, has_cff=#{has_cff}, has_glyf=#{has_glyf}"
|
|
348
353
|
end
|
|
349
354
|
end
|
|
350
355
|
end
|
|
@@ -122,11 +122,11 @@ module Fontisan
|
|
|
122
122
|
data = table.to_binary_s.dup
|
|
123
123
|
|
|
124
124
|
# Calculate new numberOfHMetrics
|
|
125
|
-
new_num_h_metrics = if hmtx
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
new_num_h_metrics = if hmtx&.h_metrics
|
|
126
|
+
hmtx.h_metrics.size
|
|
127
|
+
else
|
|
128
|
+
calculate_number_of_h_metrics
|
|
129
|
+
end
|
|
130
130
|
|
|
131
131
|
# Update numberOfHMetrics field (at offset 34, uint16)
|
|
132
132
|
data[34, 2] = [new_num_h_metrics].pack("n")
|