ea 0.6.18 → 0.6.19

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.
@@ -4,12 +4,14 @@ require "json"
4
4
 
5
5
  module Ea
6
6
  module Fonts
7
- # Advance-width metrics for Carlito, metric-compatible with
8
- # Calibri. EA's exported textLength values encode Carlito
9
- # advances scaled by per-style factors, so text widths computed
10
- # here match EA's published SVGs to sub-pixel precision — far
11
- # tighter than the legacy fitted-codepoint table, and style-aware
12
- # (bold/italic variants), which the legacy table lacked.
7
+ # Advance-width metrics for the fonts EA's published SVGs use:
8
+ # Carlito (metric-compatible with Calibri), Arial, and Arial
9
+ # Narrow. EA's exported textLength values encode per-family,
10
+ # per-style, per-POINT-SIZE scale factors applied to the font's
11
+ # design advances (fitted by exact-count maximization against
12
+ # EA-published SVGs), so text widths computed here match EA's
13
+ # output to sub-pixel precision — far tighter than any legacy
14
+ # fitted-codepoint table, and style/size aware.
13
15
  module Metrics
14
16
  DATA_PATH = File.expand_path("carlito_metrics.json", __dir__)
15
17
 
@@ -21,16 +23,23 @@ module Ea
21
23
  [true, true] => :bold_italic
22
24
  }.freeze
23
25
 
24
- # Families whose glyph metrics match the Carlito tables.
25
- SUPPORTED_FAMILIES = %w[Carlito Calibri].freeze
26
+ # Families metric-compatible with a data-file family (the key
27
+ # is rendered even when the QEA named the other one).
28
+ FAMILY_ALIASES = {
29
+ "calibri" => "Carlito",
30
+ "carlito" => "Carlito",
31
+ "arial" => "Arial",
32
+ "arial narrow" => "Arial Narrow"
33
+ }.freeze
26
34
 
27
35
  # Advance used for codepoints missing from the tables.
28
36
  DEFAULT_ADVANCE = 0.25
29
37
 
30
- # Carlito has no CJK glyphs; EA's renderer falls back to a
31
- # proportional CJK font whose advances measure ~0.709em at the
32
- # NOMINAL size (not scaled by the per-style factors) — fitted
33
- # from EA-published textLengths (52 mixed-script samples).
38
+ # Carlito/Arial have no CJK glyphs; EA's renderer falls back to
39
+ # a proportional CJK font whose advances measure ~0.709em at
40
+ # the NOMINAL size (not scaled by the per-style factors) —
41
+ # fitted from EA-published textLengths (52 mixed-script
42
+ # samples).
34
43
  CJK_ADVANCE = 0.709
35
44
  CJK_RANGES = [
36
45
  (0x3000..0x303F), # CJK punctuation
@@ -48,18 +57,23 @@ module Ea
48
57
  return nil unless supported_family?(family)
49
58
  return nil if text.nil? || text.empty?
50
59
 
51
- latin, cjk_count = split_advance(text, weight: weight, style: style)
52
- (latin * size_pt.to_f * factor_for(weight, style) +
60
+ latin, cjk_count = split_advance(text, family: family,
61
+ weight: weight, style: style)
62
+ (latin * size_pt.to_f * factor_for(family, size_pt, weight, style) +
53
63
  cjk_count * CJK_ADVANCE * size_pt.to_f).round(3)
54
64
  end
55
65
 
56
66
  def supported_family?(family)
57
- SUPPORTED_FAMILIES.include?(family.to_s)
67
+ !resolve_family(family).nil?
68
+ end
69
+
70
+ def resolve_family(family)
71
+ FAMILY_ALIASES[family.to_s.downcase]
58
72
  end
59
73
 
60
74
  # Returns [latin_em (factor-scaled later), cjk_glyph_count].
61
- def split_advance(text, weight: nil, style: nil)
62
- table = table_for(weight, style)
75
+ def split_advance(text, family: nil, weight: nil, style: nil)
76
+ table = table_for(family, weight, style)
63
77
  latin = 0.0
64
78
  cjk = 0
65
79
  text.each_char.each do |ch|
@@ -76,12 +90,20 @@ module Ea
76
90
  CJK_RANGES.any? { |range| range.cover?(cp) }
77
91
  end
78
92
 
79
- def factor_for(weight, style)
80
- data["factors"][STYLE_KEYS.fetch([bold?(weight), italic?(style)], :regular).to_s]
93
+ # EA's scale factor varies by point size (GDI quantization):
94
+ # Carlito regular fits 1.390 at 7pt but 1.432 at 9pt. Sizes
95
+ # without a fitted entry use the style's default (7pt, the
96
+ # corpus's dominant size).
97
+ def factor_for(family, size_pt, weight, style)
98
+ style = STYLE_KEYS.fetch([bold?(weight), italic?(style)], :regular).to_s
99
+ table = data["factors"].fetch(resolve_family(family), {})
100
+ .fetch(style, {})
101
+ table[size_pt.to_i.to_s] || table["default"] || 1.0
81
102
  end
82
103
 
83
- def table_for(weight, style)
84
- data["advance"][STYLE_KEYS.fetch([bold?(weight), italic?(style)], :regular).to_s]
104
+ def table_for(family, weight, style)
105
+ style = STYLE_KEYS.fetch([bold?(weight), italic?(style)], :regular).to_s
106
+ data["advance"].fetch(resolve_family(family), {}).fetch(style, {})
85
107
  end
86
108
 
87
109
  def data
@@ -100,28 +122,46 @@ module Ea
100
122
  format("U+%04X", ch.ord)
101
123
  end
102
124
 
103
- # Resolves the four Carlito style TTFs through the fontist gem
104
- # (system index + downloaded formulas). Empty when fontist is
105
- # unavailable or Carlito is not installed.
106
- def carlito_font_files
125
+ # Resolves a family's four style TTFs through the fontist gem
126
+ # (system index + downloaded formulas). Style variants are
127
+ # identified from each file's head.macStyle bits — filenames
128
+ # vary across fontist formulas and system copies ("ArialBd",
129
+ # "Arial Narrow Bold"), so name-matching is unreliable. The
130
+ # first file matching each style wins (fontist orders the
131
+ # downloaded formulas ahead of system copies). Empty when
132
+ # fontist is unavailable or the family is not installed.
133
+ def font_files_for(family)
107
134
  require "fontist"
108
- styles = %w[Regular Bold Italic BoldItalic]
109
- Fontist::SystemFont.find("Carlito").filter_map do |path|
135
+ Fontist::SystemFont.find(family).each_with_object({}) do |path, out|
110
136
  next unless path.downcase.end_with?(".ttf")
111
137
 
112
- style = File.basename(path, ".ttf").delete_prefix("Carlito-")
113
- match = styles.find { |s| s.casecmp?(style) }
114
- next unless match
138
+ style = style_from_mac_style(path)
139
+ next unless style
115
140
 
116
- [match, path]
117
- end.to_h
141
+ out[style] ||= path
142
+ end
118
143
  rescue LoadError, StandardError => e
119
- raise if ENV['EA_FONTS_DEBUG']
144
+ raise if ENV["EA_FONTS_DEBUG"]
120
145
 
121
146
  {}
122
147
  end
123
148
 
124
- # Reads advance widths from a TTF via the fontian gem (used by
149
+ # head.macStyle: bit 0 = BOLD, bit 1 = ITALIC.
150
+ def style_from_mac_style(path)
151
+ require "fontisan"
152
+ font = Fontisan::FontLoader.load(path)
153
+ mac_style = font.table("head").mac_style
154
+ key = [mac_style & 1 == 1, mac_style & 2 == 2]
155
+ STYLE_KEYS[key]
156
+ end
157
+
158
+ # Carlito's data-file name (historical) differs from the
159
+ # fontist family name used to resolve its TTFs.
160
+ def carlito_font_files
161
+ font_files_for("Carlito")
162
+ end
163
+
164
+ # Reads advance widths from a TTF via the fontisan gem (used by
125
165
  # `rake fonts:validate` to re-derive the tables).
126
166
  def advances_from_font(path)
127
167
  require "fontisan"
data/lib/ea/version.rb CHANGED
@@ -4,5 +4,5 @@ module Ea
4
4
  # Gem release version. Extracted to its own file so the
5
5
  # `gem-release` tool's `gem bump` can find and update it via the
6
6
  # standard `lib/<gem>/version.rb` convention.
7
- VERSION = "0.6.18"
7
+ VERSION = "0.6.19"
8
8
  end
data/lib/tasks/fonts.rake CHANGED
@@ -3,30 +3,35 @@
3
3
  require "ea/fonts/metrics"
4
4
 
5
5
  namespace :fonts do
6
- desc "Validate the shipped Carlito metrics against Fontist-resolved TTFs"
6
+ desc "Validate the shipped font metrics against Fontist-resolved TTFs"
7
7
  task :validate do
8
- files = Ea::Fonts::Metrics.carlito_font_files
9
- abort "Carlito not resolvable via Fontist — install it first" if files.empty?
10
-
11
8
  shipped = JSON.parse(File.read(Ea::Fonts::Metrics::DATA_PATH))["advance"]
12
9
  style_key = { "regular" => "regular", "bold" => "bold",
13
10
  "italic" => "italic", "bold_italic" => "bold_italic",
14
11
  "bolditalic" => "bold_italic" }
15
12
  drift = []
16
13
 
17
- files.each do |style, path|
18
- table = shipped[style_key[style.downcase]]
19
- actual = Ea::Fonts::Metrics.advances_from_font(path)
20
- missing = actual.keys.count { |cp| table[format("U+%04X", cp)].nil? }
21
- puts " (shipped table lacks #{missing} of #{actual.size} codepoints — skipped)"
22
- actual.each do |cp, adv|
23
- key = format("U+%04X", cp)
24
- ours = table[key]
25
- next if ours.nil? || (ours - adv).abs <= 0.0005
14
+ shipped.keys.each do |family|
15
+ files = Ea::Fonts::Metrics.font_files_for(family)
16
+ if files.empty?
17
+ puts "#{family}: NOT RESOLVABLE VIA FONTIST — skipped"
18
+ next
19
+ end
20
+
21
+ files.each do |style, path|
22
+ table = shipped[family][style_key[style.downcase]] || {}
23
+ actual = Ea::Fonts::Metrics.advances_from_font(path)
24
+ missing = actual.keys.count { |cp| table[format("U+%04X", cp)].nil? }
25
+ puts " (shipped #{family}/#{style} table lacks #{missing} of #{actual.size} codepoints — skipped)"
26
+ actual.each do |cp, adv|
27
+ key = format("U+%04X", cp)
28
+ ours = table[key]
29
+ next if ours.nil? || (ours - adv).abs <= 0.0005
26
30
 
27
- drift << format("%s %s: json=%s font=%s", style, key, ours, adv)
31
+ drift << format("%s/%s %s: json=%s font=%s", family, style, key, ours, adv)
32
+ end
33
+ puts "#{family}/#{style}: #{path} — #{actual.size} codepoints checked"
28
34
  end
29
- puts "#{style}: #{path} — #{actual.size} codepoints checked"
30
35
  end
31
36
 
32
37
  if drift.empty?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.18
4
+ version: 0.6.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.