fontisan 0.4.23 → 0.4.25
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 -29
- data/Gemfile +6 -0
- data/Rakefile +9 -6
- data/TODO.improvements/01-cbdt-cblc-gid-stable-propagation.md +73 -0
- data/TODO.improvements/02-collection-outline-priority.md +48 -0
- data/TODO.improvements/03-fontisan-audit-command.md +189 -0
- data/TODO.improvements/04-ufo-composite-glyph-encoding.md +46 -0
- data/TODO.improvements/05-otf-compiler-real-cff.md +63 -0
- data/TODO.improvements/06-cff2-blend-vsindex-operators.md +51 -0
- data/TODO.improvements/07-cpal-v1-header-fields.md +71 -0
- data/TODO.improvements/08-cff-standard-string-table.md +52 -0
- data/TODO.improvements/09-type1-seac-expansion.md +57 -0
- data/TODO.improvements/10-ufo-image-set-feature-writers.md +71 -0
- data/TODO.improvements/11-kern-groups-plist.md +49 -0
- data/TODO.improvements/12-cbdt-fixture-bindata-conversion.md +68 -0
- data/TODO.improvements/13-split-octokit-fetcher.md +45 -0
- data/TODO.improvements/14-rubocop-baseline-chip.md +62 -0
- data/TODO.improvements/README.md +57 -0
- 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/converters/collection_converter.rb +6 -1
- data/lib/fontisan/converters/svg_generator.rb +3 -1
- data/lib/fontisan/stitcher/collection_result.rb +2 -1
- data/lib/fontisan/stitcher/glyph_copier.rb +2 -1
- 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.rb +2 -1
- data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +2 -1
- 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/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/cpal.rb +112 -11
- data/lib/fontisan/tables.rb +2 -1
- 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/info.rb +4 -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/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 +18 -4
- data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
- data/lib/fontisan/tasks.rb +0 -11
|
@@ -274,14 +274,18 @@ module Fontisan
|
|
|
274
274
|
private
|
|
275
275
|
|
|
276
276
|
def group_by_script(cp_map)
|
|
277
|
-
cp_map.each_with_object(Hash.new
|
|
277
|
+
cp_map.each_with_object(Hash.new do |h, k|
|
|
278
|
+
h[k] = []
|
|
279
|
+
end) do |(cp, label), h|
|
|
278
280
|
script = script_of_codepoint(cp)
|
|
279
281
|
h[script] << [cp, label]
|
|
280
282
|
end
|
|
281
283
|
end
|
|
282
284
|
|
|
283
285
|
def script_of_codepoint(cp)
|
|
284
|
-
block_label = ByBlock::BLOCKS.find
|
|
286
|
+
block_label = ByBlock::BLOCKS.find do |_label, range|
|
|
287
|
+
range.cover?(cp)
|
|
288
|
+
end&.first
|
|
285
289
|
return :other unless block_label
|
|
286
290
|
|
|
287
291
|
SCRIPT_OF_BLOCK[block_label] || :other
|
|
@@ -321,7 +321,8 @@ module Fontisan
|
|
|
321
321
|
compound.components.each do |component|
|
|
322
322
|
next unless component.args_are_xy?
|
|
323
323
|
|
|
324
|
-
raw = cache[:glyf].glyph_for(component.glyph_index, cache[:loca],
|
|
324
|
+
raw = cache[:glyf].glyph_for(component.glyph_index, cache[:loca],
|
|
325
|
+
cache[:head])
|
|
325
326
|
next unless raw
|
|
326
327
|
|
|
327
328
|
matrix = component.transformation_matrix
|
data/lib/fontisan/stitcher.rb
CHANGED
|
@@ -150,7 +150,8 @@ module Fontisan
|
|
|
150
150
|
Dir.mktmpdir do |dir|
|
|
151
151
|
sub_path = File.join(dir, "sub#{subfont_name}#{metadata.extension}")
|
|
152
152
|
metadata.compiler_class.new(target).compile(output_path: sub_path)
|
|
153
|
-
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source,
|
|
153
|
+
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source,
|
|
154
|
+
sub_path)
|
|
154
155
|
|
|
155
156
|
loaded = Fontisan::FontLoader.load(sub_path)
|
|
156
157
|
stats = SubfontStats.new(
|
|
@@ -61,7 +61,8 @@ module Fontisan
|
|
|
61
61
|
glyph_data = glyf.raw_data[offset, size]
|
|
62
62
|
|
|
63
63
|
if glyph_data.bytesize >= 10
|
|
64
|
-
_n, gx_min, gy_min, gx_max, gy_max = glyph_data[0,
|
|
64
|
+
_n, gx_min, gy_min, gx_max, gy_max = glyph_data[0,
|
|
65
|
+
10].unpack("n5")
|
|
65
66
|
gx_min = to_signed_16(gx_min)
|
|
66
67
|
gy_min = to_signed_16(gy_min)
|
|
67
68
|
gx_max = to_signed_16(gx_max)
|
|
@@ -118,7 +118,10 @@ module Fontisan
|
|
|
118
118
|
|
|
119
119
|
def bounding_box(glyph)
|
|
120
120
|
points = glyph.contours.flat_map(&:points)
|
|
121
|
-
|
|
121
|
+
if points.empty?
|
|
122
|
+
return { x_min: 0, y_min: 0, x_max: @units_per_em,
|
|
123
|
+
y_max: @units_per_em }
|
|
124
|
+
end
|
|
122
125
|
|
|
123
126
|
xs = points.map(&:x)
|
|
124
127
|
ys = points.map(&:y)
|
|
@@ -74,7 +74,8 @@ module Fontisan
|
|
|
74
74
|
private
|
|
75
75
|
|
|
76
76
|
def build_from_doc_path(data, group_transform, doc, codepoint)
|
|
77
|
-
final = normalizer_for(width: doc.viewbox_width,
|
|
77
|
+
final = normalizer_for(width: doc.viewbox_width,
|
|
78
|
+
height: doc.viewbox_height)
|
|
78
79
|
.final_transform(group_transform)
|
|
79
80
|
contours = build_contours(data, final)
|
|
80
81
|
assemble_glyph(glyph_name_for(codepoint), contours, codepoint)
|
|
@@ -89,13 +90,15 @@ module Fontisan
|
|
|
89
90
|
def transform_contour(contour, transform)
|
|
90
91
|
points = contour.points.map do |pt|
|
|
91
92
|
x, y = transform.apply(pt.x, pt.y)
|
|
92
|
-
Fontisan::Ufo::Point.new(x: x.round, y: y.round, type: pt.type,
|
|
93
|
+
Fontisan::Ufo::Point.new(x: x.round, y: y.round, type: pt.type,
|
|
94
|
+
smooth: pt.smooth)
|
|
93
95
|
end
|
|
94
96
|
Fontisan::Ufo::Contour.new(points)
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
def normalizer_for(width:, height:)
|
|
98
|
-
Geometry::Normalizer.new(viewbox_width: width, viewbox_height: height,
|
|
100
|
+
Geometry::Normalizer.new(viewbox_width: width, viewbox_height: height,
|
|
101
|
+
upm: @upm)
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
def assemble_glyph(name, contours, codepoint)
|
|
@@ -22,7 +22,9 @@ module Fontisan
|
|
|
22
22
|
transforms = transform_string.scan(FUNCTION_RE).map do |name, args|
|
|
23
23
|
build_transform(name, args)
|
|
24
24
|
end
|
|
25
|
-
transforms.reduce(AffineTransform.identity)
|
|
25
|
+
transforms.reduce(AffineTransform.identity) do |acc, t|
|
|
26
|
+
acc.compose(t)
|
|
27
|
+
end
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def self.build_transform(name, args_string)
|
|
@@ -35,7 +37,8 @@ module Fontisan
|
|
|
35
37
|
when "skewX" then AffineTransform.skew_x_radians(degrees_to_radians(args.fetch(0)))
|
|
36
38
|
when "skewY" then AffineTransform.skew_y_radians(degrees_to_radians(args.fetch(0)))
|
|
37
39
|
else
|
|
38
|
-
raise ArgumentError,
|
|
40
|
+
raise ArgumentError,
|
|
41
|
+
"unknown SVG transform function: #{name.inspect}"
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
|
|
@@ -59,9 +62,13 @@ module Fontisan
|
|
|
59
62
|
end
|
|
60
63
|
|
|
61
64
|
def self.build_matrix(args)
|
|
62
|
-
|
|
65
|
+
if args.size != 6
|
|
66
|
+
raise ArgumentError,
|
|
67
|
+
"matrix() requires 6 arguments, got #{args.size}"
|
|
68
|
+
end
|
|
63
69
|
|
|
64
|
-
AffineTransform.new(args[0], args[1], args[2], args[3], args[4],
|
|
70
|
+
AffineTransform.new(args[0], args[1], args[2], args[3], args[4],
|
|
71
|
+
args[5])
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
# Rotate around a specific point: translate to origin, rotate,
|
|
@@ -5,8 +5,10 @@ module Fontisan
|
|
|
5
5
|
# Geometry primitives for SvgToGlyf: affine transforms, SVG
|
|
6
6
|
# transform attribute parsing, and coordinate normalization.
|
|
7
7
|
module Geometry
|
|
8
|
-
autoload :AffineTransform,
|
|
9
|
-
|
|
8
|
+
autoload :AffineTransform,
|
|
9
|
+
"fontisan/svg_to_glyf/geometry/affine_transform"
|
|
10
|
+
autoload :TransformParser,
|
|
11
|
+
"fontisan/svg_to_glyf/geometry/transform_parser"
|
|
10
12
|
autoload :Normalizer, "fontisan/svg_to_glyf/geometry/normalizer"
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -40,8 +40,10 @@ module Fontisan
|
|
|
40
40
|
# @param num_regions [Integer] number of variation regions
|
|
41
41
|
# @param width [Integer, nil] advance width
|
|
42
42
|
# @return [String] CFF2 charstring bytes with blend sequences
|
|
43
|
-
def self.build_variable(outline, master_outlines:, num_regions:,
|
|
44
|
-
|
|
43
|
+
def self.build_variable(outline, master_outlines:, num_regions:,
|
|
44
|
+
width: nil)
|
|
45
|
+
new.build_variable_outline(outline, master_outlines, num_regions,
|
|
46
|
+
width)
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
# ---------- variable-font encoding ----------
|
|
@@ -65,7 +67,9 @@ module Fontisan
|
|
|
65
67
|
|
|
66
68
|
# Process an outline with blend operators for each varying coordinate.
|
|
67
69
|
def encode_variable_outline(outline, master_outlines, num_regions)
|
|
68
|
-
@master_states = master_outlines.map
|
|
70
|
+
@master_states = master_outlines.map do
|
|
71
|
+
MasterState.new(current_x: 0, current_y: 0)
|
|
72
|
+
end
|
|
69
73
|
@num_regions = num_regions
|
|
70
74
|
@has_blend = false
|
|
71
75
|
|
|
@@ -90,7 +94,8 @@ module Fontisan
|
|
|
90
94
|
# ---------- variable moveto ----------
|
|
91
95
|
|
|
92
96
|
def encode_variable_moveto(cmd, master_cmds)
|
|
93
|
-
dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd,
|
|
97
|
+
dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd,
|
|
98
|
+
master_cmds)
|
|
94
99
|
|
|
95
100
|
write_variable_number(dx, dx_deltas)
|
|
96
101
|
write_variable_number(dy, dy_deltas)
|
|
@@ -102,7 +107,8 @@ module Fontisan
|
|
|
102
107
|
# ---------- variable lineto ----------
|
|
103
108
|
|
|
104
109
|
def encode_variable_lineto(cmd, master_cmds)
|
|
105
|
-
dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd,
|
|
110
|
+
dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd,
|
|
111
|
+
master_cmds)
|
|
106
112
|
|
|
107
113
|
write_variable_number(dx, dx_deltas)
|
|
108
114
|
write_variable_number(dy, dy_deltas)
|
data/lib/fontisan/tables/cff.rb
CHANGED
|
@@ -47,7 +47,8 @@ module Fontisan
|
|
|
47
47
|
autoload :Charset, "fontisan/tables/cff/charset"
|
|
48
48
|
autoload :CharString, "fontisan/tables/cff/charstring"
|
|
49
49
|
autoload :CharStringBuilder, "fontisan/tables/cff/charstring_builder"
|
|
50
|
-
autoload :Cff2CharStringBuilder,
|
|
50
|
+
autoload :Cff2CharStringBuilder,
|
|
51
|
+
"fontisan/tables/cff/cff2_charstring_builder"
|
|
51
52
|
autoload :CharStringParser, "fontisan/tables/cff/charstring_parser"
|
|
52
53
|
autoload :CharStringRebuilder, "fontisan/tables/cff/charstring_rebuilder"
|
|
53
54
|
autoload :CharstringsIndex, "fontisan/tables/cff/charstrings_index"
|
|
@@ -66,7 +66,8 @@ module Fontisan
|
|
|
66
66
|
case off_size
|
|
67
67
|
when 1 then [value].pack("C")
|
|
68
68
|
when 2 then [value].pack("n")
|
|
69
|
-
when 3 then [(value >> 16) & 0xFF, (value >> 8) & 0xFF,
|
|
69
|
+
when 3 then [(value >> 16) & 0xFF, (value >> 8) & 0xFF,
|
|
70
|
+
value & 0xFF].pack("CCC")
|
|
70
71
|
when 4 then [value].pack("N")
|
|
71
72
|
else raise ArgumentError, "invalid off_size: #{off_size}"
|
|
72
73
|
end
|
data/lib/fontisan/tables/cpal.rb
CHANGED
|
@@ -27,7 +27,10 @@ module Fontisan
|
|
|
27
27
|
# - numColorRecords (uint16): Total number of color records
|
|
28
28
|
# - colorRecordsArrayOffset (uint32): Offset to color records array
|
|
29
29
|
#
|
|
30
|
-
# Version 1
|
|
30
|
+
# Version 1 Header (24 bytes) adds:
|
|
31
|
+
# - paletteTypesArrayOffset (uint32): Offset to palette types array
|
|
32
|
+
# - paletteLabelsArrayOffset (uint32): Offset to palette labels array
|
|
33
|
+
# - paletteEntryLabelsArrayOffset (uint32): Offset to palette entry labels
|
|
31
34
|
#
|
|
32
35
|
# Color Record Structure (4 bytes, BGRA format):
|
|
33
36
|
# - blue (uint8)
|
|
@@ -47,6 +50,13 @@ module Fontisan
|
|
|
47
50
|
# OpenType table tag for CPAL
|
|
48
51
|
TAG = "CPAL"
|
|
49
52
|
|
|
53
|
+
# Palette type bit flags (CPAL v1, paletteTypesArray entries).
|
|
54
|
+
PALETTE_TYPE_LIGHT_ON_DARK = 0x01
|
|
55
|
+
PALETTE_TYPE_DARK_ON_LIGHT = 0x02
|
|
56
|
+
|
|
57
|
+
# Offset that means "no table present" in CPAL v1.
|
|
58
|
+
NULL_OFFSET = 0
|
|
59
|
+
|
|
50
60
|
# @return [Integer] CPAL version (0 or 1)
|
|
51
61
|
attr_reader :version
|
|
52
62
|
|
|
@@ -71,6 +81,19 @@ module Fontisan
|
|
|
71
81
|
# @return [Array<Hash>] Parsed color records (RGBA hashes)
|
|
72
82
|
attr_reader :color_records
|
|
73
83
|
|
|
84
|
+
# @return [Array<Integer>, nil] Palette type bit flags, one per
|
|
85
|
+
# palette. nil for CPAL v0 or when v1 has no palette types array.
|
|
86
|
+
attr_reader :palette_types
|
|
87
|
+
|
|
88
|
+
# @return [Array<Integer>, nil] Name-record IDs (name table) for
|
|
89
|
+
# each palette. nil for CPAL v0 or when v1 has no labels array.
|
|
90
|
+
attr_reader :palette_labels
|
|
91
|
+
|
|
92
|
+
# @return [Array<Integer>, nil] Name-record IDs (name table) for
|
|
93
|
+
# each palette entry index (shared across palettes). nil for
|
|
94
|
+
# CPAL v0 or when v1 has no entry-labels array.
|
|
95
|
+
attr_reader :palette_entry_labels
|
|
96
|
+
|
|
74
97
|
# Override read to parse CPAL structure
|
|
75
98
|
#
|
|
76
99
|
# @param io [IO, String] Binary data to read
|
|
@@ -102,8 +125,8 @@ module Fontisan
|
|
|
102
125
|
# Parse color records
|
|
103
126
|
parse_color_records(io)
|
|
104
127
|
|
|
105
|
-
# Version 1
|
|
106
|
-
|
|
128
|
+
# Version 1: palette types + labels (lazily walked via offsets)
|
|
129
|
+
parse_v1_metadata if version == 1
|
|
107
130
|
rescue StandardError => e
|
|
108
131
|
raise CorruptedTableError, "Failed to parse CPAL table: #{e.message}"
|
|
109
132
|
end
|
|
@@ -152,6 +175,44 @@ module Fontisan
|
|
|
152
175
|
color_record ? color_to_hex(color_record) : nil
|
|
153
176
|
end
|
|
154
177
|
|
|
178
|
+
# Get the name-record ID for a palette's human-readable label.
|
|
179
|
+
#
|
|
180
|
+
# @param palette_index [Integer]
|
|
181
|
+
# @return [Integer, nil] nameID for the palette, or nil if v0 / no labels
|
|
182
|
+
def palette_label(palette_index)
|
|
183
|
+
return nil unless palette_labels
|
|
184
|
+
return nil if palette_index.negative? || palette_index >= palette_labels.length
|
|
185
|
+
|
|
186
|
+
palette_labels[palette_index]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Get the name-record ID for a palette entry's human-readable label.
|
|
190
|
+
#
|
|
191
|
+
# @param entry_index [Integer]
|
|
192
|
+
# @return [Integer, nil] nameID for the entry, or nil if v0 / no labels
|
|
193
|
+
def palette_entry_label(entry_index)
|
|
194
|
+
return nil unless palette_entry_labels
|
|
195
|
+
return nil if entry_index.negative? || entry_index >= palette_entry_labels.length
|
|
196
|
+
|
|
197
|
+
palette_entry_labels[entry_index]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Whether a palette is intended for light-on-dark use (CPAL v1).
|
|
201
|
+
#
|
|
202
|
+
# @param palette_index [Integer]
|
|
203
|
+
# @return [Boolean] true if the palette type bit is set; false otherwise
|
|
204
|
+
def light_on_dark?(palette_index)
|
|
205
|
+
palette_type_set?(palette_index, PALETTE_TYPE_LIGHT_ON_DARK)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Whether a palette is intended for dark-on-light use (CPAL v1).
|
|
209
|
+
#
|
|
210
|
+
# @param palette_index [Integer]
|
|
211
|
+
# @return [Boolean] true if the palette type bit is set; false otherwise
|
|
212
|
+
def dark_on_light?(palette_index)
|
|
213
|
+
palette_type_set?(palette_index, PALETTE_TYPE_DARK_ON_LIGHT)
|
|
214
|
+
end
|
|
215
|
+
|
|
155
216
|
# Validate the CPAL table structure
|
|
156
217
|
#
|
|
157
218
|
# @return [Boolean] True if valid
|
|
@@ -169,7 +230,7 @@ module Fontisan
|
|
|
169
230
|
|
|
170
231
|
private
|
|
171
232
|
|
|
172
|
-
# Parse CPAL header (12 bytes for
|
|
233
|
+
# Parse CPAL header (12 bytes for v0, 24 bytes for v1)
|
|
173
234
|
#
|
|
174
235
|
# @param io [StringIO] Input stream
|
|
175
236
|
def parse_header(io)
|
|
@@ -179,12 +240,14 @@ module Fontisan
|
|
|
179
240
|
@num_color_records = io.read(2).unpack1("n")
|
|
180
241
|
@color_records_array_offset = io.read(4).unpack1("N")
|
|
181
242
|
|
|
182
|
-
# Version 1 has additional
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
243
|
+
# Version 1 has three additional uint32 offset fields. Offsets
|
|
244
|
+
# are read here but the arrays themselves are walked lazily
|
|
245
|
+
# from parse_v1_metadata so the v0 path (most common) doesn't
|
|
246
|
+
# pay for the seeks.
|
|
247
|
+
if @version == 1
|
|
248
|
+
@palette_types_array_offset = io.read(4).unpack1("N")
|
|
249
|
+
@palette_labels_array_offset = io.read(4).unpack1("N")
|
|
250
|
+
@palette_entry_labels_array_offset = io.read(4).unpack1("N")
|
|
188
251
|
end
|
|
189
252
|
end
|
|
190
253
|
|
|
@@ -228,7 +291,7 @@ module Fontisan
|
|
|
228
291
|
@palette_indices = []
|
|
229
292
|
return if num_palettes.zero?
|
|
230
293
|
|
|
231
|
-
# Palette indices immediately follow header (at offset 12 for v0,
|
|
294
|
+
# Palette indices immediately follow header (at offset 12 for v0, 24 for v1)
|
|
232
295
|
# Each index is uint16 (2 bytes)
|
|
233
296
|
num_palettes.times do
|
|
234
297
|
index = io.read(2).unpack1("n")
|
|
@@ -262,6 +325,44 @@ module Fontisan
|
|
|
262
325
|
end
|
|
263
326
|
end
|
|
264
327
|
|
|
328
|
+
# CPAL v1 metadata: palette types + labels + entry labels.
|
|
329
|
+
# Walked lazily via the offsets parsed in parse_header. Any
|
|
330
|
+
# offset of 0 means "no table present" — leave the accessor nil
|
|
331
|
+
# rather than raise.
|
|
332
|
+
def parse_v1_metadata
|
|
333
|
+
@palette_types = walk_uint32_array(@palette_types_array_offset, num_palettes)
|
|
334
|
+
@palette_labels = walk_uint16_array(@palette_labels_array_offset, num_palettes)
|
|
335
|
+
@palette_entry_labels = walk_uint16_array(@palette_entry_labels_array_offset,
|
|
336
|
+
num_palette_entries)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def walk_uint32_array(offset, count)
|
|
340
|
+
return nil if offset.nil? || offset == NULL_OFFSET
|
|
341
|
+
return [] if count.zero?
|
|
342
|
+
|
|
343
|
+
bytes = raw_data[offset, count * 4]
|
|
344
|
+
return nil unless bytes && bytes.bytesize == count * 4
|
|
345
|
+
|
|
346
|
+
bytes.unpack("N*")
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def walk_uint16_array(offset, count)
|
|
350
|
+
return nil if offset.nil? || offset == NULL_OFFSET
|
|
351
|
+
return [] if count.zero?
|
|
352
|
+
|
|
353
|
+
bytes = raw_data[offset, count * 2]
|
|
354
|
+
return nil unless bytes && bytes.bytesize == count * 2
|
|
355
|
+
|
|
356
|
+
bytes.unpack("n*")
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def palette_type_set?(palette_index, bit)
|
|
360
|
+
return false unless palette_types
|
|
361
|
+
return false if palette_index.negative? || palette_index >= palette_types.length
|
|
362
|
+
|
|
363
|
+
(palette_types[palette_index] & bit).nonzero? ? true : false
|
|
364
|
+
end
|
|
365
|
+
|
|
265
366
|
# Convert color record to hex string
|
|
266
367
|
#
|
|
267
368
|
# @param color [Hash] Color hash with :red, :green, :blue, :alpha keys
|
data/lib/fontisan/tables.rb
CHANGED
|
@@ -6,7 +6,8 @@ module Fontisan
|
|
|
6
6
|
module Tables
|
|
7
7
|
autoload :CblcBigGlyphMetrics, "fontisan/tables/cblc_big_glyph_metrics"
|
|
8
8
|
autoload :CblcBitmapSize, "fontisan/tables/cblc_bitmap_size"
|
|
9
|
-
autoload :CblcGlyphBitmapLocation,
|
|
9
|
+
autoload :CblcGlyphBitmapLocation,
|
|
10
|
+
"fontisan/tables/cblc_glyph_bitmap_location"
|
|
10
11
|
autoload :CblcIndexSubTable, "fontisan/tables/cblc_index_subtable"
|
|
11
12
|
autoload :CblcIndexSubTableArrayEntry,
|
|
12
13
|
"fontisan/tables/cblc_index_subtable_array_entry"
|
data/lib/fontisan/ufo/cli.rb
CHANGED
|
@@ -74,7 +74,10 @@ module Fontisan
|
|
|
74
74
|
def extract(ufo, glyph_name, output)
|
|
75
75
|
font = Font.open(ufo)
|
|
76
76
|
glyph = font.glyph(glyph_name)
|
|
77
|
-
|
|
77
|
+
unless glyph
|
|
78
|
+
raise ArgumentError,
|
|
79
|
+
"glyph not found: #{glyph_name.inspect}"
|
|
80
|
+
end
|
|
78
81
|
|
|
79
82
|
head = font.info
|
|
80
83
|
renderer = Fontisan::Svg::StandaloneGlyph.new(
|
|
@@ -65,7 +65,8 @@ module Fontisan
|
|
|
65
65
|
# SbitLineMetrics (12 bytes): height, width, hori/vert bearings
|
|
66
66
|
def self.build_sbit_line_metrics(ppem, num_glyphs)
|
|
67
67
|
io = +""
|
|
68
|
-
io << [ppem & 0xFF, 0, ppem & 0xFF, 0, 0, 0, ppem & 0xFF, 0, 0, 0,
|
|
68
|
+
io << [ppem & 0xFF, 0, ppem & 0xFF, 0, 0, 0, ppem & 0xFF, 0, 0, 0,
|
|
69
|
+
0].pack("C11")
|
|
69
70
|
io << [12 + num_glyphs * 8].pack("n")
|
|
70
71
|
io
|
|
71
72
|
end
|
|
@@ -155,7 +155,10 @@ module Fontisan
|
|
|
155
155
|
return +"" if glyphs.size <= 1
|
|
156
156
|
|
|
157
157
|
bytes = +"\x00"
|
|
158
|
-
|
|
158
|
+
# SID 0 placeholder
|
|
159
|
+
(1...glyphs.size).each do |_|
|
|
160
|
+
bytes << [0].pack("n")
|
|
161
|
+
end
|
|
159
162
|
bytes
|
|
160
163
|
end
|
|
161
164
|
|
|
@@ -193,7 +196,8 @@ module Fontisan
|
|
|
193
196
|
when 1 then [value].pack("C")
|
|
194
197
|
when 2 then [value].pack("n")
|
|
195
198
|
when 3
|
|
196
|
-
[(value >> 16) & 0xFF, (value >> 8) & 0xFF,
|
|
199
|
+
[(value >> 16) & 0xFF, (value >> 8) & 0xFF,
|
|
200
|
+
value & 0xFF].pack("CCC")
|
|
197
201
|
when 4 then [value].pack("N")
|
|
198
202
|
end
|
|
199
203
|
end
|
|
@@ -36,7 +36,9 @@ module Fontisan
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
subtable_bmp = format4_subtable(mappings.reject
|
|
39
|
+
subtable_bmp = format4_subtable(mappings.reject do |cp, _|
|
|
40
|
+
cp > 0xFFFF
|
|
41
|
+
end)
|
|
40
42
|
subtable_full = format12_subtable(mappings)
|
|
41
43
|
|
|
42
44
|
header_size = 4 + (2 * 8) # version + numTables + 2 records
|
|
@@ -44,8 +46,10 @@ module Fontisan
|
|
|
44
46
|
offset_full = header_size + subtable_bmp.bytesize
|
|
45
47
|
|
|
46
48
|
header = [0, 2].pack("nn")
|
|
47
|
-
header << subtable_record(PLATFORM_WINDOWS, ENCODING_WINDOWS_BMP,
|
|
48
|
-
|
|
49
|
+
header << subtable_record(PLATFORM_WINDOWS, ENCODING_WINDOWS_BMP,
|
|
50
|
+
offset_bmp)
|
|
51
|
+
header << subtable_record(PLATFORM_WINDOWS, ENCODING_WINDOWS_FULL,
|
|
52
|
+
offset_full)
|
|
49
53
|
header + subtable_bmp + subtable_full
|
|
50
54
|
end
|
|
51
55
|
|
|
@@ -82,7 +86,8 @@ module Fontisan
|
|
|
82
86
|
segments = build_segments(mappings) + [0xFFFF..0xFFFF] # sentinel
|
|
83
87
|
seg_count = segments.size
|
|
84
88
|
search_range = largest_pow2_le(seg_count) * 2
|
|
85
|
-
entry_selector = (Math.log([1,
|
|
89
|
+
entry_selector = (Math.log([1,
|
|
90
|
+
search_range / 2].max) / Math.log(2)).to_i
|
|
86
91
|
range_shift = seg_count * 2 - search_range
|
|
87
92
|
|
|
88
93
|
end_codes = segments.map(&:end)
|
|
@@ -98,7 +103,8 @@ module Fontisan
|
|
|
98
103
|
end
|
|
99
104
|
|
|
100
105
|
body = +""
|
|
101
|
-
body << [seg_count * 2, search_range, entry_selector,
|
|
106
|
+
body << [seg_count * 2, search_range, entry_selector,
|
|
107
|
+
range_shift].pack("nnnn")
|
|
102
108
|
body << end_codes.pack("n*")
|
|
103
109
|
body << [0].pack("n") # reservedPad
|
|
104
110
|
body << start_codes.pack("n*")
|
|
@@ -38,7 +38,8 @@ module Fontisan
|
|
|
38
38
|
offset = HEADER_SIZE + (num_palettes * 2) # header + indices
|
|
39
39
|
|
|
40
40
|
io = +""
|
|
41
|
-
io << [VERSION, num_entries, num_palettes, num_records,
|
|
41
|
+
io << [VERSION, num_entries, num_palettes, num_records,
|
|
42
|
+
offset].pack("nnnnN")
|
|
42
43
|
indices.each { |idx| io << [idx].pack("n") }
|
|
43
44
|
records.each { |c| io << color_bytes(c) }
|
|
44
45
|
io
|
|
@@ -78,7 +78,9 @@ module Fontisan
|
|
|
78
78
|
def heuristic_class(glyph)
|
|
79
79
|
anchor_names = glyph.anchors.map(&:name)
|
|
80
80
|
mark_anchor_pattern = /\A_[a-zA-Z]+\z/
|
|
81
|
-
return 3 if anchor_names.any?
|
|
81
|
+
return 3 if anchor_names.any? do |n|
|
|
82
|
+
mark_anchor_pattern.match?(n.to_s)
|
|
83
|
+
end
|
|
82
84
|
return 2 if glyph.name.to_s.end_with?(".liga")
|
|
83
85
|
|
|
84
86
|
nil
|
|
@@ -28,7 +28,9 @@ module Fontisan
|
|
|
28
28
|
|
|
29
29
|
attachments[class_name] ||= new_class_entry
|
|
30
30
|
attachments[class_name][:marks][mark_name] = mark_anchor
|
|
31
|
-
bases.each
|
|
31
|
+
bases.each do |n, a|
|
|
32
|
+
attachments[class_name][base_bucket_key][n] = a
|
|
33
|
+
end
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
|
|
@@ -16,7 +16,8 @@ module Fontisan
|
|
|
16
16
|
# entry. No edits to the compiler.
|
|
17
17
|
module FeatureWriters
|
|
18
18
|
autoload :Base, "fontisan/ufo/compile/feature_writers/base"
|
|
19
|
-
autoload :MarkFamilyBase,
|
|
19
|
+
autoload :MarkFamilyBase,
|
|
20
|
+
"fontisan/ufo/compile/feature_writers/mark_family_base"
|
|
20
21
|
autoload :Kern, "fontisan/ufo/compile/feature_writers/kern"
|
|
21
22
|
autoload :Kern2, "fontisan/ufo/compile/feature_writers/kern2"
|
|
22
23
|
autoload :Gdef, "fontisan/ufo/compile/feature_writers/gdef"
|
|
@@ -134,8 +134,10 @@ module Fontisan
|
|
|
134
134
|
right_c2 = Point.new(x: r2x, y: r2y, type: "offcurve")
|
|
135
135
|
right_p3 = p3
|
|
136
136
|
|
|
137
|
-
left = subdivide_cubic(left_p0, left_c1, left_c2, left_p3,
|
|
138
|
-
|
|
137
|
+
left = subdivide_cubic(left_p0, left_c1, left_c2, left_p3,
|
|
138
|
+
tolerance)
|
|
139
|
+
right = subdivide_cubic(right_p0, right_c1, right_c2, right_p3,
|
|
140
|
+
tolerance)
|
|
139
141
|
|
|
140
142
|
# Drop the duplicated midpoint point between halves
|
|
141
143
|
left + right
|
|
@@ -56,7 +56,8 @@ module Fontisan
|
|
|
56
56
|
next if i == j || drop.include?(j)
|
|
57
57
|
|
|
58
58
|
# Drop inner when its bbox is fully inside outer's.
|
|
59
|
-
if contained_in?(bboxes[j],
|
|
59
|
+
if contained_in?(bboxes[j],
|
|
60
|
+
bboxes[i]) && (inner.points.size <= outer.points.size)
|
|
60
61
|
# Sanity: only drop if the point count is smaller —
|
|
61
62
|
# a fully-contained contour with MORE points is
|
|
62
63
|
# almost certainly the outer one in a malformed
|
|
@@ -28,7 +28,8 @@ module Fontisan
|
|
|
28
28
|
|
|
29
29
|
axes_bytes = build_axes(axes)
|
|
30
30
|
instances_bytes = build_instances(instances || [], axes.size)
|
|
31
|
-
header = build_header(axes.size, axes_bytes, instances_bytes,
|
|
31
|
+
header = build_header(axes.size, axes_bytes, instances_bytes,
|
|
32
|
+
instances)
|
|
32
33
|
header + axes_bytes + instances_bytes
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -38,7 +39,8 @@ module Fontisan
|
|
|
38
39
|
# majorVersion (2) + minorVersion (2) + axesArrayOffset (2)
|
|
39
40
|
# + reserved (2) + axisCount (2) + axisSize (2)
|
|
40
41
|
# + instanceCount (2) + instanceSize (2)
|
|
41
|
-
def self.build_header(axis_count, axes_bytes, _instances_bytes,
|
|
42
|
+
def self.build_header(axis_count, axes_bytes, _instances_bytes,
|
|
43
|
+
instances)
|
|
42
44
|
axes_offset = 16
|
|
43
45
|
axes_offset + axes_bytes.bytesize
|
|
44
46
|
|
|
@@ -86,7 +88,9 @@ module Fontisan
|
|
|
86
88
|
name_id = inst[:name_id] || inst["name_id"] || 0
|
|
87
89
|
flags = inst[:flags] || inst["flags"] || 0
|
|
88
90
|
coords = inst[:coords] || inst["coords"] || []
|
|
89
|
-
padded = coords.first(axis_count) + Array.new(
|
|
91
|
+
padded = coords.first(axis_count) + Array.new(
|
|
92
|
+
[axis_count - coords.size, 0].max, 0
|
|
93
|
+
)
|
|
90
94
|
padded = padded.first(axis_count).map { |c| fixed_value(c) }
|
|
91
95
|
|
|
92
96
|
io << [name_id, flags].pack("nn")
|
|
@@ -41,7 +41,8 @@ module Fontisan
|
|
|
41
41
|
offsets.map { |o| o / 2 }.pack("n*")
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
{ "glyf" => glyf_bytes, "loca" => loca_bytes,
|
|
44
|
+
{ "glyf" => glyf_bytes, "loca" => loca_bytes,
|
|
45
|
+
:loca_format => use_long ? 1 : 0 }
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# Encode a single glyph into glyf bytes. Empty glyphs (no
|
|
@@ -138,7 +139,8 @@ module Fontisan
|
|
|
138
139
|
# nothing).
|
|
139
140
|
""
|
|
140
141
|
end
|
|
141
|
-
private_class_method :encode_glyph, :encode_simple, :encode_points,
|
|
142
|
+
private_class_method :encode_glyph, :encode_simple, :encode_points,
|
|
143
|
+
:encode_composite
|
|
142
144
|
end
|
|
143
145
|
end
|
|
144
146
|
end
|
|
@@ -121,7 +121,9 @@ module Fontisan
|
|
|
121
121
|
deltas = tuple[:deltas]
|
|
122
122
|
|
|
123
123
|
# Encode delta data
|
|
124
|
-
all_fit_int8 = deltas.all?
|
|
124
|
+
all_fit_int8 = deltas.all? do |dx, dy|
|
|
125
|
+
dx.between?(-127, 127) && dy.between?(-127, 127)
|
|
126
|
+
end
|
|
125
127
|
|
|
126
128
|
point_count = deltas.size
|
|
127
129
|
flags = point_count & POINT_COUNT_MASK
|
|
@@ -195,7 +197,8 @@ module Fontisan
|
|
|
195
197
|
|
|
196
198
|
def self.build_empty(glyph_count)
|
|
197
199
|
# Minimal gvar with no variation data
|
|
198
|
-
header = [VERSION_MAJOR, VERSION_MINOR, 0, 0, 0, glyph_count, 1,
|
|
200
|
+
header = [VERSION_MAJOR, VERSION_MINOR, 0, 0, 0, glyph_count, 1,
|
|
201
|
+
glyph_count + 16].pack("nnnnnnNN")
|
|
199
202
|
offsets = Array.new(glyph_count + 1, 0).pack("C*")
|
|
200
203
|
header + offsets
|
|
201
204
|
end
|
|
@@ -19,7 +19,8 @@ module Fontisan
|
|
|
19
19
|
# @param glyphs [Array<Fontisan::Ufo::Glyph>] (unused here;
|
|
20
20
|
# bbox is computed by the caller for performance)
|
|
21
21
|
# @return [Fontisan::Tables::Head]
|
|
22
|
-
def self.build(font, glyphs:, units_per_em: nil,
|
|
22
|
+
def self.build(font, glyphs:, units_per_em: nil,
|
|
23
|
+
loca_format: LOCA_FORMAT_LONG)
|
|
23
24
|
bbox = font_bbox(font, glyphs)
|
|
24
25
|
Fontisan::Tables::Head.new(
|
|
25
26
|
version_raw: 0x00010000,
|