fontisan 0.4.19 → 0.4.21
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 +7 -7
- data/lib/fontisan/stitcher.rb +28 -23
- data/lib/fontisan/tables/cff2/index_builder.rb +1 -1
- data/lib/fontisan/ufo/cli.rb +5 -1
- data/lib/fontisan/ufo/compile/base_compiler.rb +26 -1
- data/lib/fontisan/ufo/compile/cmap.rb +8 -0
- data/lib/fontisan/ufo/compile/gvar.rb +1 -1
- data/lib/fontisan/ufo/compile/head.rb +1 -1
- data/lib/fontisan/ufo/compile/otf2_compiler.rb +1 -7
- data/lib/fontisan/ufo/compile/otf_compiler.rb +1 -7
- data/lib/fontisan/ufo/compile/ttf_compiler.rb +1 -1
- data/lib/fontisan/ufo/glyph.rb +24 -0
- data/lib/fontisan/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55632b625474e82c1cf9573ad6fce2de32f5636823701023abee87fc8817d8fc
|
|
4
|
+
data.tar.gz: 5a26b3428874b792c101484dbed0468c25eb9f8eb7644a2c0ffff61c8f2baf6a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e7334f4be856d9a86712a0215c3671c7148d623649d4b81d232aa5f70b8aee71e1e55047a2780f48a7018721026e13dc99f0fff6dcbeda49801f88a832cbf99e
|
|
7
|
+
data.tar.gz: a6432a0a4db97bdfedd597f5f79f40be36b0fd5d450f21e43319023a85b3e331377b2ee7e002fceb9f90c2e9e9a9ee4c1489ebd25e295058e5605d5be0482f57
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-07-
|
|
3
|
+
# on 2026-07-08 06:50:36 UTC using RuboCop version 1.86.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -12,7 +12,7 @@ Gemspec/RequiredRubyVersion:
|
|
|
12
12
|
- 'docs/node_modules/speakingurl/speakingurl-rails.gemspec'
|
|
13
13
|
- 'fontisan.gemspec'
|
|
14
14
|
|
|
15
|
-
# Offense count:
|
|
15
|
+
# Offense count: 2110
|
|
16
16
|
# This cop supports safe autocorrection (--autocorrect).
|
|
17
17
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
18
18
|
# URISchemes: http, https
|
|
@@ -95,7 +95,7 @@ Lint/UselessConstantScoping:
|
|
|
95
95
|
- 'lib/fontisan/conversion_options.rb'
|
|
96
96
|
- 'lib/fontisan/type1/charstrings.rb'
|
|
97
97
|
|
|
98
|
-
# Offense count:
|
|
98
|
+
# Offense count: 704
|
|
99
99
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
100
100
|
Metrics/AbcSize:
|
|
101
101
|
Enabled: false
|
|
@@ -123,7 +123,7 @@ Metrics/CollectionLiteralLength:
|
|
|
123
123
|
Metrics/CyclomaticComplexity:
|
|
124
124
|
Enabled: false
|
|
125
125
|
|
|
126
|
-
# Offense count:
|
|
126
|
+
# Offense count: 978
|
|
127
127
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
128
128
|
Metrics/MethodLength:
|
|
129
129
|
Max: 135
|
|
@@ -221,7 +221,7 @@ RSpec/DescribeMethod:
|
|
|
221
221
|
- 'spec/fontisan/type1/seac_expander_spec.rb'
|
|
222
222
|
- 'spec/fontisan/type1_real_fonts_spec.rb'
|
|
223
223
|
|
|
224
|
-
# Offense count:
|
|
224
|
+
# Offense count: 1666
|
|
225
225
|
# Configuration parameters: CountAsOne.
|
|
226
226
|
RSpec/ExampleLength:
|
|
227
227
|
Max: 66
|
|
@@ -298,7 +298,7 @@ RSpec/MultipleDescribes:
|
|
|
298
298
|
- 'spec/fontisan/utils/thread_pool_spec.rb'
|
|
299
299
|
- 'spec/fontisan/variation/cache_spec.rb'
|
|
300
300
|
|
|
301
|
-
# Offense count:
|
|
301
|
+
# Offense count: 2103
|
|
302
302
|
RSpec/MultipleExpectations:
|
|
303
303
|
Max: 19
|
|
304
304
|
|
|
@@ -330,7 +330,7 @@ RSpec/RepeatedExample:
|
|
|
330
330
|
- 'spec/fontisan/metrics_calculator_spec.rb'
|
|
331
331
|
- 'spec/fontisan/tables/os2_spec.rb'
|
|
332
332
|
|
|
333
|
-
# Offense count:
|
|
333
|
+
# Offense count: 48
|
|
334
334
|
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
|
|
335
335
|
# SupportedInflectors: default, active_support
|
|
336
336
|
RSpec/SpecFilePathFormat:
|
data/lib/fontisan/stitcher.rb
CHANGED
|
@@ -139,6 +139,15 @@ module Fontisan
|
|
|
139
139
|
cbdts.first
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
+
# Safe variant of #cbdt_source: returns nil instead of raising when
|
|
143
|
+
# the project has multiple CBDT sources. Used inside the per-subfont
|
|
144
|
+
# glyph-copy pass where raising mid-compile would leave partial state.
|
|
145
|
+
def safe_cbdt_source
|
|
146
|
+
cbdt_source
|
|
147
|
+
rescue MultipleCbdtSourcesError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
142
151
|
def build_target_for(subfont_name)
|
|
143
152
|
bindings = @subfonts[subfont_name] || []
|
|
144
153
|
target = Ufo::Font.new
|
|
@@ -171,15 +180,22 @@ module Fontisan
|
|
|
171
180
|
def assign_gids_and_copy_glyphs(bindings, target, deduplicator)
|
|
172
181
|
cbdt = safe_cbdt_source
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
# Glyph ordering matters: Cmap.build uses first-wins semantics
|
|
184
|
+
# (Cmap.build docstring), so outline donors must be stitched
|
|
185
|
+
# before CBDT placeholders. Otherwise the empty CBDT placeholder
|
|
186
|
+
# would land at a lower GID and win the cp→gid mapping for any
|
|
187
|
+
# codepoint covered by both donors, hiding the real outline glyph.
|
|
188
|
+
# CBDT bitmap data still propagates via propagate_cbdt_tables.
|
|
189
|
+
add_notdef_from(bindings, target, deduplicator)
|
|
190
|
+
add_outline_glyphs(bindings, target, deduplicator,
|
|
191
|
+
skip_sources: cbdt ? [cbdt] : [])
|
|
192
|
+
add_all_cbdt_glyphs(cbdt, target) if cbdt
|
|
193
|
+
end
|
|
179
194
|
|
|
195
|
+
def add_outline_glyphs(bindings, target, deduplicator, skip_sources:)
|
|
180
196
|
sorted_bindings(bindings).each do |binding|
|
|
181
197
|
next if binding[:donor_gid].zero?
|
|
182
|
-
next if
|
|
198
|
+
next if skip_sources.any? { |s| s.equal?(binding[:source]) }
|
|
183
199
|
|
|
184
200
|
glyph = binding[:source].glyph_for_gid(binding[:donor_gid])
|
|
185
201
|
next unless glyph
|
|
@@ -197,13 +213,6 @@ module Fontisan
|
|
|
197
213
|
end
|
|
198
214
|
end
|
|
199
215
|
|
|
200
|
-
def safe_cbdt_source
|
|
201
|
-
cbdts = @sources.values.select { |s| s.bitmap_mode == :cbdt }
|
|
202
|
-
cbdts.size == 1 ? cbdts.first : nil
|
|
203
|
-
rescue MultipleCbdtSourcesError
|
|
204
|
-
nil
|
|
205
|
-
end
|
|
206
|
-
|
|
207
216
|
def sorted_bindings(bindings)
|
|
208
217
|
bindings.sort_by { |b| [b[:codepoint] || Float::INFINITY, b[:donor_gid]] }
|
|
209
218
|
end
|
|
@@ -273,9 +282,14 @@ module Fontisan
|
|
|
273
282
|
|
|
274
283
|
compiled = Fontisan::FontLoader.load(path)
|
|
275
284
|
|
|
285
|
+
# Read every table as raw bytes straight from the file's table
|
|
286
|
+
# directory. We deliberately bypass #table (which parses via
|
|
287
|
+
# BinData) because some tables — notably CFF2 — don't yet have
|
|
288
|
+
# round-trippable BinData models; calling #table on them returns
|
|
289
|
+
# nil and would silently drop them from the rewritten font.
|
|
276
290
|
tables = {}
|
|
277
291
|
compiled.table_names.each do |tag|
|
|
278
|
-
raw =
|
|
292
|
+
raw = compiled.table_data[tag]
|
|
279
293
|
tables[tag] = raw if raw
|
|
280
294
|
end
|
|
281
295
|
|
|
@@ -288,15 +302,6 @@ module Fontisan
|
|
|
288
302
|
Fontisan::FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
289
303
|
end
|
|
290
304
|
|
|
291
|
-
def extract_raw_table(font, tag)
|
|
292
|
-
sfnt_table = font.table(tag)
|
|
293
|
-
return nil unless sfnt_table
|
|
294
|
-
|
|
295
|
-
sfnt_table.raw_data
|
|
296
|
-
rescue StandardError
|
|
297
|
-
nil
|
|
298
|
-
end
|
|
299
|
-
|
|
300
305
|
def add_all_cbdt_glyphs(source, target)
|
|
301
306
|
ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
|
|
302
307
|
if ufo
|
|
@@ -66,7 +66,7 @@ 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].pack("
|
|
69
|
+
when 3 then [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF].pack("CCC")
|
|
70
70
|
when 4 then [value].pack("N")
|
|
71
71
|
else raise ArgumentError, "invalid off_size: #{off_size}"
|
|
72
72
|
end
|
data/lib/fontisan/ufo/cli.rb
CHANGED
|
@@ -55,7 +55,11 @@ module Fontisan
|
|
|
55
55
|
issues << "no glyphs in default layer" if font.glyphs.empty?
|
|
56
56
|
issues << "no family name" unless font.info.family_name
|
|
57
57
|
issues << "unitsPerEm not set" unless font.info.units_per_em
|
|
58
|
-
|
|
58
|
+
# The compiler auto-injects an empty .notdef at GID 0 when the
|
|
59
|
+
# UFO source omits it (OpenType requires GID 0 to be .notdef).
|
|
60
|
+
# Surface this as a warning, not a failure — compilation will
|
|
61
|
+
# still succeed.
|
|
62
|
+
warn "NOTE missing .notdef glyph — compiler will inject one at GID 0" unless font.glyph(".notdef")
|
|
59
63
|
|
|
60
64
|
if issues.empty?
|
|
61
65
|
puts "OK #{ufo}"
|
|
@@ -43,7 +43,7 @@ module Fontisan
|
|
|
43
43
|
# All tables every OTF/TTF must have, plus optional feature
|
|
44
44
|
# tables (GPOS for kerning) when the UFO source has kerning data.
|
|
45
45
|
def build_tables
|
|
46
|
-
glyphs =
|
|
46
|
+
glyphs = glyphs_with_notdef
|
|
47
47
|
tables = {
|
|
48
48
|
"head" => Head.build(font, glyphs: glyphs),
|
|
49
49
|
"hhea" => Hhea.build(font, glyphs: glyphs),
|
|
@@ -62,6 +62,31 @@ module Fontisan
|
|
|
62
62
|
tables.merge(build_outline_tables)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# OpenType requires GID 0 to be `.notdef`. Normalize the source
|
|
66
|
+
# glyph list so that:
|
|
67
|
+
# - if `.notdef` is already at GID 0, the list is unchanged;
|
|
68
|
+
# - if `.notdef` exists elsewhere, it's moved to GID 0;
|
|
69
|
+
# - if no `.notdef` exists, an empty one is prepended.
|
|
70
|
+
#
|
|
71
|
+
# Without this, the alphabetically-first source glyph lands at
|
|
72
|
+
# GID 0 and the cmap parser (which treats GID 0 as `.notdef` per
|
|
73
|
+
# spec) silently drops it from the cmap.
|
|
74
|
+
def glyphs_with_notdef
|
|
75
|
+
source = font.glyphs.values
|
|
76
|
+
return source if source.empty?
|
|
77
|
+
|
|
78
|
+
existing = source.find { |g| g.name == ".notdef" }
|
|
79
|
+
return source if existing == source.first
|
|
80
|
+
|
|
81
|
+
rest = source - [existing]
|
|
82
|
+
notdef = existing || Ufo::Glyph.notdef(units_per_em: default_units_per_em)
|
|
83
|
+
[notdef, *rest]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def default_units_per_em
|
|
87
|
+
font.info&.units_per_em&.to_i || Ufo::Glyph::DEFAULT_UNITS_PER_EM
|
|
88
|
+
end
|
|
89
|
+
|
|
65
90
|
def write(tables_hash, output_path)
|
|
66
91
|
dir = File.dirname(output_path)
|
|
67
92
|
FileUtils.mkpath(dir) unless dir == "."
|
|
@@ -11,6 +11,14 @@ module Fontisan
|
|
|
11
11
|
# Both subtables share the same segment list (capped to BMP for
|
|
12
12
|
# format 4); format 4 is required by Windows even though format
|
|
13
13
|
# 12 is more capable.
|
|
14
|
+
#
|
|
15
|
+
# Contract — first-wins: when multiple glyphs declare the same
|
|
16
|
+
# codepoint, the glyph at the LOWER GID wins the cp→gid mapping.
|
|
17
|
+
# Callers that want one donor to outrank another for a shared
|
|
18
|
+
# codepoint (e.g. outline donor vs. CBDT placeholder) MUST order
|
|
19
|
+
# the higher-priority glyphs earlier in the `glyphs:` array.
|
|
20
|
+
# Stitcher relies on this for outline-first stitching.
|
|
21
|
+
#
|
|
14
22
|
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/cmap
|
|
15
23
|
module Cmap
|
|
16
24
|
PLATFORM_WINDOWS = 3
|
|
@@ -195,7 +195,7 @@ module Fontisan
|
|
|
195
195
|
|
|
196
196
|
def self.build_empty(glyph_count)
|
|
197
197
|
# Minimal gvar with no variation data
|
|
198
|
-
header = [VERSION_MAJOR, VERSION_MINOR, 0, 0, 0, glyph_count, 1, glyph_count
|
|
198
|
+
header = [VERSION_MAJOR, VERSION_MINOR, 0, 0, 0, glyph_count, 1, glyph_count + 16].pack("nnnnnnNN")
|
|
199
199
|
offsets = Array.new(glyph_count + 1, 0).pack("C*")
|
|
200
200
|
header + offsets
|
|
201
201
|
end
|
|
@@ -27,7 +27,7 @@ module Fontisan
|
|
|
27
27
|
checksum_adjustment: 0, # patched by FontWriter
|
|
28
28
|
magic_number: MAGIC_NUMBER,
|
|
29
29
|
flags: DEFAULT_FLAGS,
|
|
30
|
-
units_per_em: units_per_em || font.info.units_per_em ||
|
|
30
|
+
units_per_em: units_per_em || font.info.units_per_em || Glyph::DEFAULT_UNITS_PER_EM,
|
|
31
31
|
created_raw: time_to_longdatetime(font.info.created),
|
|
32
32
|
modified_raw: time_to_longdatetime(font.info.modified),
|
|
33
33
|
x_min: bbox[:x_min].to_i,
|
|
@@ -16,14 +16,8 @@ module Fontisan
|
|
|
16
16
|
class Otf2Compiler < BaseCompiler
|
|
17
17
|
SFNT_VERSION = SFNT_VERSION_OPEN_TYPE
|
|
18
18
|
|
|
19
|
-
def build_outline_tables
|
|
20
|
-
{
|
|
21
|
-
"CFF2" => Cff2.build(font, glyphs: font.glyphs.values),
|
|
22
|
-
}
|
|
23
|
-
end
|
|
24
|
-
|
|
25
19
|
def compile(output_path:)
|
|
26
|
-
glyphs =
|
|
20
|
+
glyphs = glyphs_with_notdef
|
|
27
21
|
|
|
28
22
|
tables = {
|
|
29
23
|
"head" => Head.build(font, glyphs: glyphs, loca_format: Head::LOCA_FORMAT_LONG),
|
|
@@ -13,14 +13,8 @@ module Fontisan
|
|
|
13
13
|
class OtfCompiler < BaseCompiler
|
|
14
14
|
SFNT_VERSION = SFNT_VERSION_OPEN_TYPE
|
|
15
15
|
|
|
16
|
-
def build_outline_tables
|
|
17
|
-
{
|
|
18
|
-
"CFF " => Cff.build(font, glyphs: font.glyphs.values),
|
|
19
|
-
}
|
|
20
|
-
end
|
|
21
|
-
|
|
22
16
|
def compile(output_path:)
|
|
23
|
-
glyphs =
|
|
17
|
+
glyphs = glyphs_with_notdef
|
|
24
18
|
|
|
25
19
|
tables = {
|
|
26
20
|
"head" => Head.build(font, glyphs: glyphs, loca_format: Head::LOCA_FORMAT_LONG),
|
|
@@ -14,7 +14,7 @@ module Fontisan
|
|
|
14
14
|
|
|
15
15
|
# @return [Hash<String, #to_binary_s>] all TTF tables, not yet written
|
|
16
16
|
def build_tables
|
|
17
|
-
glyphs =
|
|
17
|
+
glyphs = glyphs_with_notdef
|
|
18
18
|
|
|
19
19
|
# Deep-clone glyphs so filters don't mutate the source UFO.
|
|
20
20
|
filtered = clone_glyphs(glyphs)
|
data/lib/fontisan/ufo/glyph.rb
CHANGED
|
@@ -11,6 +11,30 @@ module Fontisan
|
|
|
11
11
|
# The `.glif` XML format has multiple revisions (1, 2, 3); the
|
|
12
12
|
# parser accepts all of them.
|
|
13
13
|
class Glyph
|
|
14
|
+
# OpenType requires GID 0 to be a glyph named `.notdef`. UFO
|
|
15
|
+
# sources that lack an explicit `.notdef` (synthetic SVG donors,
|
|
16
|
+
# programmatic sources) need one injected before compile. This
|
|
17
|
+
# constant is the default width used for that injected glyph, and
|
|
18
|
+
# doubles as the fallback unitsPerEm in Head.build when UFO info
|
|
19
|
+
# omits the field. Single source of truth for both call sites.
|
|
20
|
+
DEFAULT_UNITS_PER_EM = 1000
|
|
21
|
+
|
|
22
|
+
# Synthesize an empty `.notdef` glyph suitable for GID 0.
|
|
23
|
+
#
|
|
24
|
+
# OpenType mandates GID 0 be `.notdef`. UFO sources that lack an
|
|
25
|
+
# explicit `.notdef` (synthetic SVG donors, programmatic sources)
|
|
26
|
+
# need one injected before compile. The width defaults to the
|
|
27
|
+
# font's units_per_em so the .notdef consumes one em of advance.
|
|
28
|
+
#
|
|
29
|
+
# @param units_per_em [Integer, nil] Advance width for the
|
|
30
|
+
# synthesized glyph. Defaults to DEFAULT_UNITS_PER_EM when nil.
|
|
31
|
+
# @return [Glyph]
|
|
32
|
+
def self.notdef(units_per_em: nil)
|
|
33
|
+
new(name: ".notdef").tap do |g|
|
|
34
|
+
g.width = units_per_em&.to_i || DEFAULT_UNITS_PER_EM
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
14
38
|
attr_accessor :width, :height, :note, :lib
|
|
15
39
|
attr_reader :name, :unicodes, :contours, :components, :anchors,
|
|
16
40
|
:guidelines, :images
|
data/lib/fontisan/version.rb
CHANGED
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.21
|
|
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-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|