fontisan 0.4.8 → 0.4.10

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.
@@ -20,7 +20,7 @@ module Fontisan
20
20
  #
21
21
  # @example Generate SVG glyph element
22
22
  # generator = GlyphGenerator.new(calculator)
23
- # xml = generator.generate_glyph_xml(outline, unicode: "A", advance_width: 600)
23
+ # xml = generator.generate_glyph_xml(outline, codepoints: [0x41], advance_width: 600)
24
24
  class GlyphGenerator
25
25
  # @return [ViewBoxCalculator] Coordinate calculator
26
26
  attr_reader :calculator
@@ -38,23 +38,30 @@ module Fontisan
38
38
  # Generate SVG glyph element
39
39
  #
40
40
  # @param outline [Models::GlyphOutline] Glyph outline
41
- # @param unicode [String, nil] Unicode character
42
- # @param glyph_name [String, nil] Glyph name
41
+ # @param codepoints [Array<Integer>] Unicode codepoints that cmap
42
+ # maps to this glyph. Empty for unmapped glyphs (.notdef, raw
43
+ # ligatures, etc.). Each codepoint is rendered per SVG Fonts
44
+ # spec: printable ASCII as the character (XML-escaped), others
45
+ # as &#xHEX; entities. Multiple codepoints are concatenated —
46
+ # rare but valid (e.g. a glyph mapped from both space and
47
+ # non-breaking space).
48
+ # @param glyph_name [String, nil] Glyph name from the post table
43
49
  # @param advance_width [Integer] Horizontal advance width
44
50
  # @param indent [String] Indentation string
45
51
  # @return [String] XML glyph element
46
- def generate_glyph_xml(outline, unicode: nil, glyph_name: nil,
52
+ def generate_glyph_xml(outline, codepoints: [], glyph_name: nil,
47
53
  advance_width: 0, indent: " ")
48
- # Build attribute parts
49
54
  attr_parts = []
50
55
 
51
- attr_parts << "unicode=\"#{escape_xml(unicode)}\"" if unicode
52
- attr_parts << "glyph-name=\"#{escape_xml(glyph_name)}\"" if glyph_name
53
- attr_parts << "horiz-adv-x=\"#{advance_width}\"" if advance_width&.positive?
56
+ unless codepoints.empty?
57
+ attr_parts << %(unicode="#{format_codepoints(codepoints)}")
58
+ end
59
+ attr_parts << %(glyph-name="#{escape_xml(glyph_name)}") if glyph_name
60
+ attr_parts << %(horiz-adv-x="#{advance_width}") if advance_width&.positive?
54
61
 
55
62
  # Generate SVG path with Y-axis transformation
56
63
  path_data = generate_svg_path(outline)
57
- attr_parts << "d=\"#{path_data}\"" if path_data && !path_data.empty?
64
+ attr_parts << %(d="#{path_data}") if path_data && !path_data.empty?
58
65
 
59
66
  "#{indent}<glyph #{attr_parts.join(' ')}/>"
60
67
  end
@@ -88,6 +95,34 @@ advance_width: 0, indent: " ")
88
95
 
89
96
  private
90
97
 
98
+ # Format an array of codepoints as the SVG `unicode=` attribute
99
+ # value. Returns the FINAL string — already escaped / entity-fied.
100
+ # The caller must not re-escape it (would double-escape the
101
+ # entities this method emits).
102
+ #
103
+ # Per SVG Fonts spec, each codepoint renders as either:
104
+ # - printable ASCII (0x20..0x7E): the character itself, with
105
+ # XML-special chars (&, <, >, ", ') escaped
106
+ # - any other codepoint: a numeric entity &#xHEX;
107
+ #
108
+ # Multiple codepoints are concatenated in codepoint order. The
109
+ # SVG spec accepts this for ligature-style mappings.
110
+ def format_codepoints(codepoints)
111
+ codepoints.map { |cp| format_codepoint(cp) }.join
112
+ end
113
+
114
+ def format_codepoint(cp)
115
+ if printable_ascii?(cp)
116
+ escape_xml(cp.chr(Encoding::UTF_8))
117
+ else
118
+ "&#x#{cp.to_s(16).upcase};"
119
+ end
120
+ end
121
+
122
+ def printable_ascii?(cp)
123
+ (0x20..0x7E).cover?(cp)
124
+ end
125
+
91
126
  # Build SVG path for a contour with Y-axis transformation
92
127
  #
93
128
  # @param contour [Array<Hash>] Array of point hashes
@@ -15,18 +15,6 @@ module Fontisan
15
15
  TIP = 3
16
16
  SSP = 14
17
17
 
18
- # The handful of mega-blocks large enough to overflow a single
19
- # plane's 65,535-glyph cap when partitioning by plane. Range +
20
- # label only — full Unicode Blocks.txt data lives in +Block+
21
- # (follow-up).
22
- LARGE_CJK_BLOCKS = {
23
- "CJK_Ext_B" => 0x2A700..0x2B73F,
24
- "CJK_Ext_C" => 0x2B740..0x2B81F,
25
- "CJK_Ext_D" => 0x2B820..0x2CEAF,
26
- "CJK_Ext_E" => 0x2CEB0..0x2EBEF,
27
- "CJK_Ext_F" => 0x2EBF0..0x2EE5F,
28
- }.freeze
29
-
30
18
  # @param codepoint [Integer]
31
19
  # @return [Integer] plane number (0..16)
32
20
  def self.of(codepoint)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.8"
4
+ VERSION = "0.4.10"
5
5
  end
data/lib/fontisan.rb CHANGED
@@ -32,6 +32,7 @@ require "logger"
32
32
  require "bindata"
33
33
  require "zlib"
34
34
  require "stringio"
35
+ require "tmpdir"
35
36
  require "lutaml/model"
36
37
 
37
38
  # Configure lutaml-model to use Nokogiri adapter for XML serialization
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.8
4
+ version: 0.4.10
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-03 00:00:00.000000000 Z
11
+ date: 2026-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -188,6 +188,7 @@ files:
188
188
  - docs/cli/subset.md
189
189
  - docs/cli/tables.md
190
190
  - docs/cli/unicode.md
191
+ - docs/cli/validate-collection.md
191
192
  - docs/cli/validate.md
192
193
  - docs/cli/variable.md
193
194
  - docs/cli/version.md
@@ -433,6 +434,7 @@ files:
433
434
  - lib/fontisan/stitcher.rb
434
435
  - lib/fontisan/stitcher/collection_result.rb
435
436
  - lib/fontisan/stitcher/deduplicator.rb
437
+ - lib/fontisan/stitcher/format_metadata.rb
436
438
  - lib/fontisan/stitcher/glyph_limit.rb
437
439
  - lib/fontisan/stitcher/glyph_signature.rb
438
440
  - lib/fontisan/stitcher/partition_strategy.rb