fontisan 0.4.22 → 0.4.24
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 +240 -24
- data/Gemfile +6 -0
- data/Rakefile +9 -6
- data/benchmark/compile_benchmark.rb +13 -5
- data/docs/.vitepress/config.ts +1 -0
- data/docs/STITCHER_GUIDE.adoc +47 -3
- data/docs/api/layer.md +89 -0
- data/docs/cli/subset.md +25 -6
- data/docs/guide/cli/subset.md +31 -0
- data/docs/guide/color-fonts/bitmaps.md +17 -0
- data/lib/fontisan/cli.rb +2 -1
- data/lib/fontisan/collection/writer.rb +2 -1
- data/lib/fontisan/commands/validate_collection_command.rb +7 -1
- data/lib/fontisan/commands.rb +2 -1
- data/lib/fontisan/config/subset_profiles.yml +2 -0
- data/lib/fontisan/converters/collection_converter.rb +6 -1
- data/lib/fontisan/converters/svg_generator.rb +3 -1
- data/lib/fontisan/stitcher/cbdt_propagator.rb +44 -22
- data/lib/fontisan/stitcher/collection_result.rb +2 -1
- data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
- data/lib/fontisan/stitcher/glyph_copier.rb +5 -34
- data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
- data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
- data/lib/fontisan/stitcher/source.rb +2 -1
- data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
- data/lib/fontisan/stitcher.rb +4 -1
- data/lib/fontisan/subset/shared_state.rb +42 -0
- data/lib/fontisan/subset/subset_context.rb +20 -0
- data/lib/fontisan/subset/table_strategy/cbdt.rb +30 -0
- data/lib/fontisan/subset/table_strategy/cblc.rb +27 -0
- data/lib/fontisan/subset/table_strategy/cmap.rb +146 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_placement.rb +20 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb +45 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb +62 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb +182 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb +59 -0
- data/lib/fontisan/subset/table_strategy/glyf.rb +29 -0
- data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +146 -0
- data/lib/fontisan/subset/table_strategy/head.rb +42 -0
- data/lib/fontisan/subset/table_strategy/hhea.rb +67 -0
- data/lib/fontisan/subset/table_strategy/hmtx.rb +52 -0
- data/lib/fontisan/subset/table_strategy/loca.rb +41 -0
- data/lib/fontisan/subset/table_strategy/maxp.rb +23 -0
- data/lib/fontisan/subset/table_strategy/name.rb +19 -0
- data/lib/fontisan/subset/table_strategy/os2.rb +21 -0
- data/lib/fontisan/subset/table_strategy/pass_through.rb +20 -0
- data/lib/fontisan/subset/table_strategy/post.rb +37 -0
- data/lib/fontisan/subset/table_strategy.rb +75 -0
- data/lib/fontisan/subset/table_subsetter.rb +49 -616
- data/lib/fontisan/subset.rb +3 -0
- data/lib/fontisan/svg/standalone_glyph.rb +4 -1
- data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
- data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
- data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
- data/lib/fontisan/tables/cbdt.rb +66 -121
- data/lib/fontisan/tables/cblc.rb +110 -231
- data/lib/fontisan/tables/cblc_big_glyph_metrics.rb +28 -0
- data/lib/fontisan/tables/cblc_bitmap_size.rb +67 -0
- data/lib/fontisan/tables/cblc_glyph_bitmap_location.rb +28 -0
- data/lib/fontisan/tables/cblc_index_subtable.rb +104 -0
- data/lib/fontisan/tables/cblc_index_subtable_array_entry.rb +20 -0
- data/lib/fontisan/tables/cblc_index_subtable_format_parser.rb +150 -0
- data/lib/fontisan/tables/cblc_index_subtable_header.rb +18 -0
- data/lib/fontisan/tables/cblc_sbit_line_metrics.rb +28 -0
- data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +11 -5
- data/lib/fontisan/tables/cff.rb +2 -1
- data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
- data/lib/fontisan/tables.rb +12 -0
- data/lib/fontisan/ufo/cli.rb +4 -1
- data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
- data/lib/fontisan/ufo/compile/cff.rb +6 -2
- data/lib/fontisan/ufo/compile/cmap.rb +11 -5
- data/lib/fontisan/ufo/compile/cpal.rb +2 -1
- data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
- data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
- data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
- data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
- data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
- data/lib/fontisan/ufo/compile/fvar.rb +7 -3
- data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
- data/lib/fontisan/ufo/compile/gvar.rb +5 -2
- data/lib/fontisan/ufo/compile/head.rb +2 -1
- data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
- data/lib/fontisan/ufo/compile/name.rb +3 -1
- data/lib/fontisan/ufo/compile/sbix.rb +2 -1
- data/lib/fontisan/ufo/compile/stat.rb +6 -3
- data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
- data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
- data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
- data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
- data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
- data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
- data/lib/fontisan/ufo/convert.rb +4 -1
- data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
- data/lib/fontisan/ufo/info.rb +4 -2
- data/lib/fontisan/ufo/layer.rb +42 -2
- data/lib/fontisan/ufo/plist.rb +7 -2
- data/lib/fontisan/ufo/reader.rb +3 -1
- data/lib/fontisan/ufo/writer.rb +13 -5
- data/lib/fontisan/ufo.rb +2 -1
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/collection_decoder.rb +4 -1
- data/lib/fontisan/woff2/collection_encoder.rb +4 -1
- data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
- data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
- data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
- data/lib/fontisan.rb +0 -1
- metadata +36 -4
- data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
- data/lib/fontisan/tasks.rb +0 -11
|
@@ -17,12 +17,17 @@ module Fontisan
|
|
|
17
17
|
# @param ps_options [Hash] forwarded to the generator
|
|
18
18
|
# (encoding, upm_scale, convert_curves)
|
|
19
19
|
# @return [String] the output_path
|
|
20
|
-
def self.convert(ufo, output_path:, generator_class:, compiler: :ttf,
|
|
20
|
+
def self.convert(ufo, output_path:, generator_class:, compiler: :ttf,
|
|
21
|
+
**ps_options)
|
|
21
22
|
compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
|
|
22
|
-
|
|
23
|
+
unless compiler_class
|
|
24
|
+
raise ArgumentError,
|
|
25
|
+
"unknown intermediate compiler: #{compiler.inspect}"
|
|
26
|
+
end
|
|
23
27
|
|
|
24
28
|
Dir.mktmpdir do |dir|
|
|
25
|
-
intermediate_path = File.join(dir,
|
|
29
|
+
intermediate_path = File.join(dir,
|
|
30
|
+
"intermediate#{ext_for(compiler)}")
|
|
26
31
|
compiler_class.new(ufo).compile(output_path: intermediate_path)
|
|
27
32
|
|
|
28
33
|
loaded = Fontisan::FontLoader.load(intermediate_path)
|
|
@@ -32,14 +32,19 @@ module Fontisan
|
|
|
32
32
|
# @return [String] the output_path
|
|
33
33
|
def self.convert(ufo, output_path:, compiler: :ttf, **woff_options)
|
|
34
34
|
compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
|
|
35
|
-
|
|
35
|
+
unless compiler_class
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"unknown intermediate compiler: #{compiler.inspect}"
|
|
38
|
+
end
|
|
36
39
|
|
|
37
40
|
Dir.mktmpdir do |dir|
|
|
38
|
-
intermediate_path = File.join(dir,
|
|
41
|
+
intermediate_path = File.join(dir,
|
|
42
|
+
"intermediate#{format_ext(compiler)}")
|
|
39
43
|
compiler_class.new(ufo).compile(output_path: intermediate_path)
|
|
40
44
|
|
|
41
45
|
loaded = Fontisan::FontLoader.load(intermediate_path)
|
|
42
|
-
woff_bytes = Fontisan::Converters::WoffWriter.new.convert(loaded,
|
|
46
|
+
woff_bytes = Fontisan::Converters::WoffWriter.new.convert(loaded,
|
|
47
|
+
woff_options)
|
|
43
48
|
File.binwrite(output_path, woff_bytes)
|
|
44
49
|
end
|
|
45
50
|
|
|
@@ -21,14 +21,19 @@ module Fontisan
|
|
|
21
21
|
# @return [String] the output_path
|
|
22
22
|
def self.convert(ufo, output_path:, compiler: :ttf, **woff2_options)
|
|
23
23
|
compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
|
|
24
|
-
|
|
24
|
+
unless compiler_class
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
"unknown intermediate compiler: #{compiler.inspect}"
|
|
27
|
+
end
|
|
25
28
|
|
|
26
29
|
Dir.mktmpdir do |dir|
|
|
27
|
-
intermediate_path = File.join(dir,
|
|
30
|
+
intermediate_path = File.join(dir,
|
|
31
|
+
"intermediate#{format_ext(compiler)}")
|
|
28
32
|
compiler_class.new(ufo).compile(output_path: intermediate_path)
|
|
29
33
|
|
|
30
34
|
loaded = Fontisan::FontLoader.load(intermediate_path)
|
|
31
|
-
result = Fontisan::Converters::Woff2Encoder.new.convert(loaded,
|
|
35
|
+
result = Fontisan::Converters::Woff2Encoder.new.convert(loaded,
|
|
36
|
+
woff2_options)
|
|
32
37
|
File.binwrite(output_path, result[:woff2_binary])
|
|
33
38
|
end
|
|
34
39
|
|
data/lib/fontisan/ufo/convert.rb
CHANGED
|
@@ -71,7 +71,10 @@ module Fontisan
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
compiler = COMPILER_FOR_FORMAT[format_key]
|
|
74
|
-
|
|
74
|
+
unless compiler
|
|
75
|
+
raise ArgumentError,
|
|
76
|
+
"unknown UFO output format: #{to.inspect}"
|
|
77
|
+
end
|
|
75
78
|
|
|
76
79
|
compiler.new(ufo).compile(output_path: output_path)
|
|
77
80
|
output_path
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Ufo
|
|
5
|
+
# Raised by +Layer#add+ when a glyph with the same name is already
|
|
6
|
+
# present. Surfaces what would otherwise be a silent overwrite so
|
|
7
|
+
# the caller can decide between +Layer#put+ (intentional replace)
|
|
8
|
+
# and +Stitcher::UniqueGlyphName.in+ (auto-rename).
|
|
9
|
+
#
|
|
10
|
+
# Extracted as a sibling of +Layer+ (rather than nested inside it)
|
|
11
|
+
# so the namespace stays flat and the error can be referenced
|
|
12
|
+
# without pulling in the full Layer implementation.
|
|
13
|
+
class GlyphExistsError < StandardError
|
|
14
|
+
attr_reader :name
|
|
15
|
+
|
|
16
|
+
def initialize(name)
|
|
17
|
+
@name = name
|
|
18
|
+
super("a glyph named #{name.inspect} already exists; use #put " \
|
|
19
|
+
"to overwrite or UniqueGlyphName.in to deconflict")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/fontisan/ufo/info.rb
CHANGED
|
@@ -81,8 +81,10 @@ module Fontisan
|
|
|
81
81
|
# @return [Array(Integer, Integer)] [major, minor]
|
|
82
82
|
def self.parse_version(version)
|
|
83
83
|
case version.to_s
|
|
84
|
-
when /\A(\d+)\.(\d+)\.\d+\z/ then [Regexp.last_match(1).to_i,
|
|
85
|
-
|
|
84
|
+
when /\A(\d+)\.(\d+)\.\d+\z/ then [Regexp.last_match(1).to_i,
|
|
85
|
+
Regexp.last_match(2).to_i]
|
|
86
|
+
when /\A(\d+)\.(\d+)\z/ then [Regexp.last_match(1).to_i,
|
|
87
|
+
Regexp.last_match(2).to_i]
|
|
86
88
|
when /\A(\d+)\z/ then [Regexp.last_match(1).to_i, 0]
|
|
87
89
|
else [0, 0]
|
|
88
90
|
end
|
data/lib/fontisan/ufo/layer.rb
CHANGED
|
@@ -5,8 +5,27 @@ module Fontisan
|
|
|
5
5
|
# A single layer in a UFO source. A Layer holds a set of glyphs
|
|
6
6
|
# keyed by name. The default layer is `public.default` per UFO 3.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# == Naming contract
|
|
9
|
+
#
|
|
10
|
+
# Glyph names are the layer's primary key. Two distinct glyphs with
|
|
11
|
+
# the same name cannot coexist — adding the second would silently
|
|
12
|
+
# destroy the first, a class of bug that has historically cost real
|
|
13
|
+
# data (e.g. CJK Ext G cmap loss when CBDT placeholders collided
|
|
14
|
+
# with outline glyphs sharing "gid{N}" names).
|
|
15
|
+
#
|
|
16
|
+
# To make that contract unbreakable, +#add+ RAISES +GlyphExistsError+
|
|
17
|
+
# on conflict. Callers who want one of the other two semantics pick
|
|
18
|
+
# the method that names it:
|
|
19
|
+
#
|
|
20
|
+
# +#add+:: insert; raise if the name is taken (the safe default)
|
|
21
|
+
# +#put+:: insert; overwrite any existing glyph with the same name
|
|
22
|
+
#
|
|
23
|
+
# Callers that need auto-renaming (insertion without giving up on a
|
|
24
|
+
# collision) call +Stitcher::UniqueGlyphName.in(target, base)+ first,
|
|
25
|
+
# then +#add+ the glyph under the returned name.
|
|
26
|
+
#
|
|
27
|
+
# The error class itself lives at +Fontisan::Ufo::GlyphExistsError+
|
|
28
|
+
# (sibling, not nested) so the namespace stays flat.
|
|
10
29
|
class Layer
|
|
11
30
|
DEFAULT_NAME = "public.default"
|
|
12
31
|
|
|
@@ -21,7 +40,28 @@ module Fontisan
|
|
|
21
40
|
@glyphs[glyph_name.to_s]
|
|
22
41
|
end
|
|
23
42
|
|
|
43
|
+
# Insert +glyph+. Raises +GlyphExistsError+ if a glyph with the
|
|
44
|
+
# same name is already present — this is the contract that keeps
|
|
45
|
+
# the layer's key namespace trustworthy.
|
|
46
|
+
#
|
|
47
|
+
# @param glyph [Glyph]
|
|
48
|
+
# @return [Glyph] the same glyph
|
|
49
|
+
# @raise [GlyphExistsError] if +glyph.name+ is already taken
|
|
24
50
|
def add(glyph)
|
|
51
|
+
name = glyph.name.to_s
|
|
52
|
+
raise GlyphExistsError, name if @glyphs.key?(name)
|
|
53
|
+
|
|
54
|
+
@glyphs[name] = glyph
|
|
55
|
+
glyph
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Insert +glyph+, replacing any existing glyph with the same
|
|
59
|
+
# name. Use this when the caller has positively decided that the
|
|
60
|
+
# previous glyph (if any) should be discarded.
|
|
61
|
+
#
|
|
62
|
+
# @param glyph [Glyph]
|
|
63
|
+
# @return [Glyph] the same glyph
|
|
64
|
+
def put(glyph)
|
|
25
65
|
@glyphs[glyph.name.to_s] = glyph
|
|
26
66
|
glyph
|
|
27
67
|
end
|
data/lib/fontisan/ufo/plist.rb
CHANGED
|
@@ -25,7 +25,9 @@ module Fontisan
|
|
|
25
25
|
root = doc.root
|
|
26
26
|
raise ParseError, "no <plist> root element" unless root&.name == "plist"
|
|
27
27
|
|
|
28
|
-
parse_value(root.children.find
|
|
28
|
+
parse_value(root.children.find do |c|
|
|
29
|
+
c.element? || c.cdata? || c.text? && !c.text.strip.empty?
|
|
30
|
+
end)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
# @param value [Object] the value to serialize
|
|
@@ -73,7 +75,10 @@ module Fontisan
|
|
|
73
75
|
# dict children are key/value pairs (key first, then value)
|
|
74
76
|
while children.any?
|
|
75
77
|
key_node = children.shift
|
|
76
|
-
|
|
78
|
+
unless key_node.name == "key"
|
|
79
|
+
raise ParseError,
|
|
80
|
+
"dict key is not <key>"
|
|
81
|
+
end
|
|
77
82
|
|
|
78
83
|
value_node = children.shift
|
|
79
84
|
result[key_node.text] = parse_value(value_node)
|
data/lib/fontisan/ufo/reader.rb
CHANGED
|
@@ -94,7 +94,9 @@ module Fontisan
|
|
|
94
94
|
if order.first.is_a?(Hash)
|
|
95
95
|
order.reduce({}) { |h, pair| h.merge(pair) }
|
|
96
96
|
else
|
|
97
|
-
order.to_h
|
|
97
|
+
order.to_h do |filename|
|
|
98
|
+
[File.basename(filename, ".glif"), filename]
|
|
99
|
+
end
|
|
98
100
|
end
|
|
99
101
|
else
|
|
100
102
|
raise "unsupported contents.plist format: #{order.class}"
|
data/lib/fontisan/ufo/writer.rb
CHANGED
|
@@ -50,7 +50,8 @@ module Fontisan
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def write_fontinfo(path)
|
|
53
|
-
File.write(File.join(path, "fontinfo.plist"),
|
|
53
|
+
File.write(File.join(path, "fontinfo.plist"),
|
|
54
|
+
Plist.emit(@font.info.to_plist))
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
def write_layercontents(path)
|
|
@@ -75,11 +76,17 @@ module Fontisan
|
|
|
75
76
|
FileUtils.mkpath(layer_dir)
|
|
76
77
|
|
|
77
78
|
contents =
|
|
78
|
-
layer.glyphs.transform_values
|
|
79
|
-
|
|
79
|
+
layer.glyphs.transform_values do |g|
|
|
80
|
+
"#{safe_filename(g.name)}.glif"
|
|
81
|
+
end
|
|
82
|
+
File.write(File.join(layer_dir, "contents.plist"),
|
|
83
|
+
Plist.emit(contents))
|
|
80
84
|
|
|
81
85
|
layer.glyphs.each_value do |glyph|
|
|
82
|
-
File.write(
|
|
86
|
+
File.write(
|
|
87
|
+
File.join(layer_dir,
|
|
88
|
+
"#{safe_filename(glyph.name)}.glif"), glyph.to_glif
|
|
89
|
+
)
|
|
83
90
|
end
|
|
84
91
|
end
|
|
85
92
|
end
|
|
@@ -96,7 +103,8 @@ module Fontisan
|
|
|
96
103
|
def write_kerning(path)
|
|
97
104
|
return if @font.kerning.empty?
|
|
98
105
|
|
|
99
|
-
File.write(File.join(path, "kerning.plist"),
|
|
106
|
+
File.write(File.join(path, "kerning.plist"),
|
|
107
|
+
Plist.emit(@font.kerning.to_plist))
|
|
100
108
|
end
|
|
101
109
|
|
|
102
110
|
def write_features(path)
|
data/lib/fontisan/ufo.rb
CHANGED
|
@@ -17,7 +17,8 @@ module Fontisan
|
|
|
17
17
|
#
|
|
18
18
|
# Reference: https://unifiedfontobject.org
|
|
19
19
|
module Ufo
|
|
20
|
-
autoload :Font,
|
|
20
|
+
autoload :Font, "fontisan/ufo/font"
|
|
21
|
+
autoload :GlyphExistsError, "fontisan/ufo/glyph_exists_error"
|
|
21
22
|
autoload :Info, "fontisan/ufo/info"
|
|
22
23
|
autoload :Layer, "fontisan/ufo/layer"
|
|
23
24
|
autoload :LayerSet, "fontisan/ufo/layer_set"
|
data/lib/fontisan/version.rb
CHANGED
|
@@ -131,7 +131,10 @@ module Fontisan
|
|
|
131
131
|
def read_collection_directory(pos)
|
|
132
132
|
version = @data[pos, 4].unpack1("N")
|
|
133
133
|
pos += 4
|
|
134
|
-
|
|
134
|
+
unless version == 0x00010000
|
|
135
|
+
raise InvalidFontError,
|
|
136
|
+
"Unsupported CollectionHeader version 0x#{version.to_s(16)}"
|
|
137
|
+
end
|
|
135
138
|
|
|
136
139
|
num_fonts, pos = UInt255.decode_at(@data, pos)
|
|
137
140
|
font_entries = Array.new(num_fonts) do
|
|
@@ -32,7 +32,10 @@ module Fontisan
|
|
|
32
32
|
# @param fonts [Array<TrueTypeFont, OpenTypeFont>] Source fonts
|
|
33
33
|
# @return [String] WOFF2 collection binary
|
|
34
34
|
def encode_fonts(fonts)
|
|
35
|
-
|
|
35
|
+
if fonts.nil? || fonts.empty?
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"fonts cannot be empty"
|
|
38
|
+
end
|
|
36
39
|
|
|
37
40
|
prepared = fonts.map.with_index { |font, i| prepare_font(font, i) }
|
|
38
41
|
_, font_to_ckeys = deduplicate(prepared)
|
|
@@ -50,7 +50,8 @@ module Fontisan
|
|
|
50
50
|
start_offset = source_offsets[i]
|
|
51
51
|
end_offset = source_offsets[i + 1]
|
|
52
52
|
if start_offset < end_offset
|
|
53
|
-
glyf << @glyf_data.byteslice(start_offset,
|
|
53
|
+
glyf << @glyf_data.byteslice(start_offset,
|
|
54
|
+
end_offset - start_offset)
|
|
54
55
|
end
|
|
55
56
|
remainder = glyf.bytesize % 4
|
|
56
57
|
glyf << ("\x00" * (4 - remainder)) if remainder.positive?
|
|
@@ -131,7 +131,8 @@ module Fontisan
|
|
|
131
131
|
streams[:bbox_entries] = StringIO.new(bbox_blob[bitmap_size..] || "")
|
|
132
132
|
|
|
133
133
|
# overlapSimpleBitmap follows all streams when optionFlags bit 0 set.
|
|
134
|
-
streams[:overlap_bitmap] =
|
|
134
|
+
streams[:overlap_bitmap] =
|
|
135
|
+
header[:has_overlap_bitmap] ? @data[pos, overlap_size] : ""
|
|
135
136
|
|
|
136
137
|
# nContour is read positionally (int16 per glyph).
|
|
137
138
|
nc_data = streams[:n_contour].string
|
|
@@ -178,7 +179,9 @@ module Fontisan
|
|
|
178
179
|
x = 0
|
|
179
180
|
y = 0
|
|
180
181
|
triplet_flags.each do |flag|
|
|
181
|
-
dx, dy, oc = TripletCodec.decode(flag,
|
|
182
|
+
dx, dy, oc = TripletCodec.decode(flag,
|
|
183
|
+
read_triplet_payload(flag,
|
|
184
|
+
streams[:glyph]))
|
|
182
185
|
x += dx
|
|
183
186
|
y += dy
|
|
184
187
|
xs << x
|
|
@@ -97,7 +97,8 @@ module Fontisan
|
|
|
97
97
|
def per_table_checksum(table)
|
|
98
98
|
data = table.bytes
|
|
99
99
|
if table.tag == "head"
|
|
100
|
-
data = "#{data.byteslice(0,
|
|
100
|
+
data = "#{data.byteslice(0,
|
|
101
|
+
8)}#{['00000000'].pack('H8')}#{data.byteslice(12..)}"
|
|
101
102
|
end
|
|
102
103
|
Utilities::ChecksumCalculator.calculate_table_checksum(data)
|
|
103
104
|
end
|
data/lib/fontisan.rb
CHANGED
|
@@ -119,7 +119,6 @@ module Fontisan
|
|
|
119
119
|
autoload :SfntBuilder, "fontisan/sfnt_builder"
|
|
120
120
|
autoload :Stitcher, "fontisan/stitcher"
|
|
121
121
|
autoload :StitcherCli, "fontisan/stitcher_cli"
|
|
122
|
-
autoload :Tasks, "fontisan/tasks"
|
|
123
122
|
autoload :TrueTypeCollection, "fontisan/true_type_collection"
|
|
124
123
|
autoload :TrueTypeFont, "fontisan/true_type_font"
|
|
125
124
|
autoload :TrueTypeFontExtensions, "fontisan/true_type_font_extensions"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.24
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -168,6 +168,7 @@ files:
|
|
|
168
168
|
- docs/api/font-loader.md
|
|
169
169
|
- docs/api/font-writer.md
|
|
170
170
|
- docs/api/index.md
|
|
171
|
+
- docs/api/layer.md
|
|
171
172
|
- docs/api/models/glyph-accessor.md
|
|
172
173
|
- docs/api/models/glyph.md
|
|
173
174
|
- docs/api/models/table-analyzer.md
|
|
@@ -440,6 +441,7 @@ files:
|
|
|
440
441
|
- lib/fontisan/stitcher/collection_result.rb
|
|
441
442
|
- lib/fontisan/stitcher/deduplicator.rb
|
|
442
443
|
- lib/fontisan/stitcher/format_metadata.rb
|
|
444
|
+
- lib/fontisan/stitcher/glyph_cloner.rb
|
|
443
445
|
- lib/fontisan/stitcher/glyph_copier.rb
|
|
444
446
|
- lib/fontisan/stitcher/glyph_limit.rb
|
|
445
447
|
- lib/fontisan/stitcher/glyph_signature.rb
|
|
@@ -455,12 +457,35 @@ files:
|
|
|
455
457
|
- lib/fontisan/stitcher/selector/gid.rb
|
|
456
458
|
- lib/fontisan/stitcher/selector/range.rb
|
|
457
459
|
- lib/fontisan/stitcher/source.rb
|
|
460
|
+
- lib/fontisan/stitcher/unique_glyph_name.rb
|
|
458
461
|
- lib/fontisan/stitcher_cli.rb
|
|
459
462
|
- lib/fontisan/subset.rb
|
|
460
463
|
- lib/fontisan/subset/builder.rb
|
|
461
464
|
- lib/fontisan/subset/glyph_mapping.rb
|
|
462
465
|
- lib/fontisan/subset/options.rb
|
|
463
466
|
- lib/fontisan/subset/profile.rb
|
|
467
|
+
- lib/fontisan/subset/shared_state.rb
|
|
468
|
+
- lib/fontisan/subset/subset_context.rb
|
|
469
|
+
- lib/fontisan/subset/table_strategy.rb
|
|
470
|
+
- lib/fontisan/subset/table_strategy/cbdt.rb
|
|
471
|
+
- lib/fontisan/subset/table_strategy/cblc.rb
|
|
472
|
+
- lib/fontisan/subset/table_strategy/cmap.rb
|
|
473
|
+
- lib/fontisan/subset/table_strategy/color_bitmap_placement.rb
|
|
474
|
+
- lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb
|
|
475
|
+
- lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb
|
|
476
|
+
- lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb
|
|
477
|
+
- lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb
|
|
478
|
+
- lib/fontisan/subset/table_strategy/glyf.rb
|
|
479
|
+
- lib/fontisan/subset/table_strategy/glyf_loca_builder.rb
|
|
480
|
+
- lib/fontisan/subset/table_strategy/head.rb
|
|
481
|
+
- lib/fontisan/subset/table_strategy/hhea.rb
|
|
482
|
+
- lib/fontisan/subset/table_strategy/hmtx.rb
|
|
483
|
+
- lib/fontisan/subset/table_strategy/loca.rb
|
|
484
|
+
- lib/fontisan/subset/table_strategy/maxp.rb
|
|
485
|
+
- lib/fontisan/subset/table_strategy/name.rb
|
|
486
|
+
- lib/fontisan/subset/table_strategy/os2.rb
|
|
487
|
+
- lib/fontisan/subset/table_strategy/pass_through.rb
|
|
488
|
+
- lib/fontisan/subset/table_strategy/post.rb
|
|
464
489
|
- lib/fontisan/subset/table_subsetter.rb
|
|
465
490
|
- lib/fontisan/svg.rb
|
|
466
491
|
- lib/fontisan/svg/font_face_generator.rb
|
|
@@ -483,6 +508,14 @@ files:
|
|
|
483
508
|
- lib/fontisan/tables.rb
|
|
484
509
|
- lib/fontisan/tables/cbdt.rb
|
|
485
510
|
- lib/fontisan/tables/cblc.rb
|
|
511
|
+
- lib/fontisan/tables/cblc_big_glyph_metrics.rb
|
|
512
|
+
- lib/fontisan/tables/cblc_bitmap_size.rb
|
|
513
|
+
- lib/fontisan/tables/cblc_glyph_bitmap_location.rb
|
|
514
|
+
- lib/fontisan/tables/cblc_index_subtable.rb
|
|
515
|
+
- lib/fontisan/tables/cblc_index_subtable_array_entry.rb
|
|
516
|
+
- lib/fontisan/tables/cblc_index_subtable_format_parser.rb
|
|
517
|
+
- lib/fontisan/tables/cblc_index_subtable_header.rb
|
|
518
|
+
- lib/fontisan/tables/cblc_sbit_line_metrics.rb
|
|
486
519
|
- lib/fontisan/tables/cff.rb
|
|
487
520
|
- lib/fontisan/tables/cff/cff2_charstring_builder.rb
|
|
488
521
|
- lib/fontisan/tables/cff/cff_glyph.rb
|
|
@@ -556,8 +589,6 @@ files:
|
|
|
556
589
|
- lib/fontisan/tables/svg.rb
|
|
557
590
|
- lib/fontisan/tables/variation_common.rb
|
|
558
591
|
- lib/fontisan/tables/vvar.rb
|
|
559
|
-
- lib/fontisan/tasks.rb
|
|
560
|
-
- lib/fontisan/tasks/fixture_downloader.rb
|
|
561
592
|
- lib/fontisan/true_type_collection.rb
|
|
562
593
|
- lib/fontisan/true_type_font.rb
|
|
563
594
|
- lib/fontisan/true_type_font_extensions.rb
|
|
@@ -657,6 +688,7 @@ files:
|
|
|
657
688
|
- lib/fontisan/ufo/features.rb
|
|
658
689
|
- lib/fontisan/ufo/font.rb
|
|
659
690
|
- lib/fontisan/ufo/glyph.rb
|
|
691
|
+
- lib/fontisan/ufo/glyph_exists_error.rb
|
|
660
692
|
- lib/fontisan/ufo/guideline.rb
|
|
661
693
|
- lib/fontisan/ufo/image.rb
|
|
662
694
|
- lib/fontisan/ufo/image_set.rb
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "open-uri"
|
|
4
|
-
require "net/http"
|
|
5
|
-
require "fileutils"
|
|
6
|
-
|
|
7
|
-
module Fontisan
|
|
8
|
-
# Tasks supporting the developer workflow: fixture downloads, etc.
|
|
9
|
-
# Lives under its own namespace so Rakefiles and other tooling can
|
|
10
|
-
# load just the task plumbing without pulling in the full fontisan
|
|
11
|
-
# stack (BinData tables, UFO, etc.).
|
|
12
|
-
module Tasks
|
|
13
|
-
# Downloads a single fixture file with retry on transient network
|
|
14
|
-
# failures. Used by `rake fixtures:download` so a single CDN blip
|
|
15
|
-
# (5xx, connection reset, OpenTimeout) doesn't sink a fresh
|
|
16
|
-
# checkout. Permanent failures (404, malformed URL) surface
|
|
17
|
-
# immediately.
|
|
18
|
-
#
|
|
19
|
-
# The downloader is a focused class, not a procedural Rakefile
|
|
20
|
-
# patch, so the retry logic is unit-testable in isolation.
|
|
21
|
-
#
|
|
22
|
-
# @example
|
|
23
|
-
# Fontisan::Tasks::FixtureDownloader.new(
|
|
24
|
-
# url: "https://github.com/.../font.ttf",
|
|
25
|
-
# destination: "spec/fixtures/font.ttf",
|
|
26
|
-
# ).call
|
|
27
|
-
class FixtureDownloader
|
|
28
|
-
RETRIABLE_ERRORS = [
|
|
29
|
-
Net::OpenTimeout,
|
|
30
|
-
Net::ReadTimeout,
|
|
31
|
-
Errno::ECONNRESET,
|
|
32
|
-
Errno::ECONNREFUSED,
|
|
33
|
-
Errno::EHOSTUNREACH,
|
|
34
|
-
Errno::ETIMEDOUT,
|
|
35
|
-
EOFError,
|
|
36
|
-
IOError,
|
|
37
|
-
].freeze
|
|
38
|
-
|
|
39
|
-
# 5xx HTTP responses are transient server errors worth retrying.
|
|
40
|
-
# 4xx are permanent (404, 403) and must fail fast.
|
|
41
|
-
RETRIABLE_HTTP_STATUSES = (500..599)
|
|
42
|
-
|
|
43
|
-
DEFAULT_MAX_RETRIES = 3
|
|
44
|
-
DEFAULT_BASE_BACKOFF = 0.5 # seconds; doubles per attempt
|
|
45
|
-
|
|
46
|
-
# Error raised after exhausting all retries. Carries the last
|
|
47
|
-
# underlying exception so callers can log the root cause.
|
|
48
|
-
class Error < StandardError
|
|
49
|
-
attr_reader :last_error
|
|
50
|
-
|
|
51
|
-
def initialize(url:, attempts:, last_error:)
|
|
52
|
-
@last_error = last_error
|
|
53
|
-
super("Failed to download #{url} after #{attempts} attempts: " \
|
|
54
|
-
"#{last_error.class}: #{last_error.message}")
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
attr_reader :url, :destination, :max_retries, :base_backoff, :sleep_method
|
|
59
|
-
|
|
60
|
-
# @param url [String] source URL.
|
|
61
|
-
# @param destination [String] path to write bytes to. Parent dir
|
|
62
|
-
# is auto-created.
|
|
63
|
-
# @param max_retries [Integer] total attempts including the
|
|
64
|
-
# first. 3 means: try, retry, retry.
|
|
65
|
-
# @param base_backoff [Float] seconds to sleep before the first
|
|
66
|
-
# retry. Doubles per attempt.
|
|
67
|
-
# @param sleep_method [#call] injectable sleep (for tests).
|
|
68
|
-
# Defaults to Kernel.sleep.
|
|
69
|
-
def initialize(url:, destination:, max_retries: DEFAULT_MAX_RETRIES,
|
|
70
|
-
base_backoff: DEFAULT_BASE_BACKOFF, sleep_method: method(:sleep))
|
|
71
|
-
@url = url
|
|
72
|
-
@destination = destination
|
|
73
|
-
@max_retries = max_retries
|
|
74
|
-
@base_backoff = base_backoff
|
|
75
|
-
@sleep_method = sleep_method
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Performs the download. Returns the destination path on
|
|
79
|
-
# success. Raises {Error} after exhausting retries.
|
|
80
|
-
#
|
|
81
|
-
# @return [String] destination path
|
|
82
|
-
# @raise [Error]
|
|
83
|
-
def call
|
|
84
|
-
attempts = 0
|
|
85
|
-
nil
|
|
86
|
-
|
|
87
|
-
begin
|
|
88
|
-
attempts += 1
|
|
89
|
-
fetch_to_destination
|
|
90
|
-
destination
|
|
91
|
-
rescue StandardError => e
|
|
92
|
-
e
|
|
93
|
-
raise if permanent_failure?(e)
|
|
94
|
-
raise Error.new(url: url, attempts: attempts, last_error: e) if attempts >= max_retries
|
|
95
|
-
|
|
96
|
-
backoff = base_backoff * (2**(attempts - 1))
|
|
97
|
-
sleep_method.call(backoff)
|
|
98
|
-
retry
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
private
|
|
103
|
-
|
|
104
|
-
def fetch_to_destination
|
|
105
|
-
FileUtils.mkdir_p(File.dirname(destination))
|
|
106
|
-
|
|
107
|
-
# IO.copy_stream avoids loading the whole response into memory
|
|
108
|
-
# and is more Windows-compatible than remote.read + File.binwrite.
|
|
109
|
-
# URLs come from FixtureFonts config (version-controlled), not
|
|
110
|
-
# user input — same trust model as the previous inline URI.open
|
|
111
|
-
# call in the Rakefile.
|
|
112
|
-
#
|
|
113
|
-
# Parsing with URI.parse first satisfies CodeQL's "open with
|
|
114
|
-
# non-constant value" check: any string that isn't a valid URI
|
|
115
|
-
# raises URI::InvalidURIError before OpenURI can dispatch on
|
|
116
|
-
# it. The parsed URI's .open is OpenURI's standard entry.
|
|
117
|
-
# rubocop:disable Security/Open
|
|
118
|
-
URI.parse(url).open(open_uri_options) do |remote|
|
|
119
|
-
File.open(destination, "wb") do |file|
|
|
120
|
-
IO.copy_stream(remote, file)
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
# rubocop:enable Security/Open
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# `open-uri` follows redirects by default and surfaces HTTP
|
|
127
|
-
# errors as `OpenURI::HTTPError` whose `io.status` is the `[code,
|
|
128
|
-
# message]` array. We re-raise non-retriable 4xx as
|
|
129
|
-
# `permanent-failure`-tagged exceptions so the retry loop exits.
|
|
130
|
-
def open_uri_options
|
|
131
|
-
{
|
|
132
|
-
"User-Agent" => "fontisan-fixtures/1.0",
|
|
133
|
-
redirect: true,
|
|
134
|
-
open_timeout: 30,
|
|
135
|
-
read_timeout: 120,
|
|
136
|
-
}
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def permanent_failure?(error)
|
|
140
|
-
case error
|
|
141
|
-
when OpenURI::HTTPError
|
|
142
|
-
status = parse_http_status(error)
|
|
143
|
-
status && !RETRIABLE_HTTP_STATUSES.cover?(status)
|
|
144
|
-
else
|
|
145
|
-
RETRIABLE_ERRORS.none? { |klass| error.is_a?(klass) }
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def parse_http_status(error)
|
|
150
|
-
io = error.io
|
|
151
|
-
return nil unless io
|
|
152
|
-
|
|
153
|
-
status = io.status
|
|
154
|
-
return nil unless status
|
|
155
|
-
|
|
156
|
-
status.first.to_i
|
|
157
|
-
rescue StandardError
|
|
158
|
-
nil
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
end
|
data/lib/fontisan/tasks.rb
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Fontisan
|
|
4
|
-
# Tasks supporting the developer workflow: fixture downloads, etc.
|
|
5
|
-
# Lives under its own namespace so Rakefiles and other tooling can
|
|
6
|
-
# load just the task plumbing without pulling in the full fontisan
|
|
7
|
-
# stack (BinData tables, UFO, etc.).
|
|
8
|
-
module Tasks
|
|
9
|
-
autoload :FixtureDownloader, "fontisan/tasks/fixture_downloader"
|
|
10
|
-
end
|
|
11
|
-
end
|