fontisan 0.4.19 → 0.4.20

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec589024241926918e56eb58fbe63cd2b14cc0d82fd92908337f7f2f7ef5c076
4
- data.tar.gz: f3ef0a193655de4790717559c7bce5722f5d9e7b5548bc5b313cf400a3b2121d
3
+ metadata.gz: 7eea2bdc6a2142dc7aec297e4753ea530f9b66d1e89f7cd4a83d8582bd511a8b
4
+ data.tar.gz: 2d6c594a71b01982f226e2c1db4f952280f167aaf2bc404b8bbfc2ef95bfea79
5
5
  SHA512:
6
- metadata.gz: b83304cf80e5f7a55ee98717b0f3bec987ad81cef329d0f1d2ab9ab91ecd66db58f6e2ed5eada8c3b4049665e3a37240a26477dbd25405a586d1f9ca7fc7dd61
7
- data.tar.gz: ebc0f3e2c752e7752452280d83e4220aabb12c23a4766e431dbc3e13b480b85b0259a929656919da42e9993a8bd2354a9283fbef21a95ba49b666c54c8d0812b
6
+ metadata.gz: 41e75040b3020c6ddcd22ca1567c8a7d44c6511d6fbdd886e6712de23b4ae7146a85fb64ef135c0e3aa73af3054c9c622315663c1bbd60ca744092cc2c6c6b0b
7
+ data.tar.gz: 7ffb7e4f4a4546d3c42f18ef92566693ca8250e287b307011afa8cd782769c328917eb7929c1bb664581b9c82fafa1355801893e08b8cddda60f222c68e467cc
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-07 11:03:20 UTC using RuboCop version 1.86.1.
3
+ # on 2026-07-07 23:48:55 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: 2090
15
+ # Offense count: 2102
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
@@ -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: 1660
224
+ # Offense count: 1664
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: 2097
301
+ # Offense count: 2102
302
302
  RSpec/MultipleExpectations:
303
303
  Max: 19
304
304
 
@@ -273,9 +273,14 @@ module Fontisan
273
273
 
274
274
  compiled = Fontisan::FontLoader.load(path)
275
275
 
276
+ # Read every table as raw bytes straight from the file's table
277
+ # directory. We deliberately bypass #table (which parses via
278
+ # BinData) because some tables — notably CFF2 — don't yet have
279
+ # round-trippable BinData models; calling #table on them returns
280
+ # nil and would silently drop them from the rewritten font.
276
281
  tables = {}
277
282
  compiled.table_names.each do |tag|
278
- raw = extract_raw_table(compiled, tag)
283
+ raw = compiled.table_data[tag]
279
284
  tables[tag] = raw if raw
280
285
  end
281
286
 
@@ -288,15 +293,6 @@ module Fontisan
288
293
  Fontisan::FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
289
294
  end
290
295
 
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
296
  def add_all_cbdt_glyphs(source, target)
301
297
  ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
302
298
  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("C3")
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
@@ -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
- issues << "missing .notdef glyph" unless font.glyph(".notdef")
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 = font.glyphs.values
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 == "."
@@ -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 || 1000,
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 = font.glyphs.values
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 = font.glyphs.values
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 = font.glyphs.values
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.19"
4
+ VERSION = "0.4.20"
5
5
  end
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.19
4
+ version: 0.4.20
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-07 00:00:00.000000000 Z
11
+ date: 2026-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64