fontisan 0.4.22 → 0.4.23

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +5 -0
  3. data/lib/fontisan/config/subset_profiles.yml +2 -0
  4. data/lib/fontisan/stitcher/cbdt_propagator.rb +44 -22
  5. data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
  6. data/lib/fontisan/stitcher/glyph_copier.rb +3 -33
  7. data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
  8. data/lib/fontisan/stitcher.rb +2 -0
  9. data/lib/fontisan/subset/shared_state.rb +42 -0
  10. data/lib/fontisan/subset/subset_context.rb +20 -0
  11. data/lib/fontisan/subset/table_strategy/cbdt.rb +30 -0
  12. data/lib/fontisan/subset/table_strategy/cblc.rb +27 -0
  13. data/lib/fontisan/subset/table_strategy/cmap.rb +146 -0
  14. data/lib/fontisan/subset/table_strategy/color_bitmap_placement.rb +20 -0
  15. data/lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb +45 -0
  16. data/lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb +62 -0
  17. data/lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb +182 -0
  18. data/lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb +59 -0
  19. data/lib/fontisan/subset/table_strategy/glyf.rb +29 -0
  20. data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +145 -0
  21. data/lib/fontisan/subset/table_strategy/head.rb +42 -0
  22. data/lib/fontisan/subset/table_strategy/hhea.rb +67 -0
  23. data/lib/fontisan/subset/table_strategy/hmtx.rb +52 -0
  24. data/lib/fontisan/subset/table_strategy/loca.rb +41 -0
  25. data/lib/fontisan/subset/table_strategy/maxp.rb +23 -0
  26. data/lib/fontisan/subset/table_strategy/name.rb +19 -0
  27. data/lib/fontisan/subset/table_strategy/os2.rb +21 -0
  28. data/lib/fontisan/subset/table_strategy/pass_through.rb +20 -0
  29. data/lib/fontisan/subset/table_strategy/post.rb +37 -0
  30. data/lib/fontisan/subset/table_strategy.rb +75 -0
  31. data/lib/fontisan/subset/table_subsetter.rb +49 -616
  32. data/lib/fontisan/subset.rb +3 -0
  33. data/lib/fontisan/tables/cbdt.rb +66 -121
  34. data/lib/fontisan/tables/cblc.rb +110 -231
  35. data/lib/fontisan/tables/cblc_big_glyph_metrics.rb +28 -0
  36. data/lib/fontisan/tables/cblc_bitmap_size.rb +67 -0
  37. data/lib/fontisan/tables/cblc_glyph_bitmap_location.rb +28 -0
  38. data/lib/fontisan/tables/cblc_index_subtable.rb +104 -0
  39. data/lib/fontisan/tables/cblc_index_subtable_array_entry.rb +20 -0
  40. data/lib/fontisan/tables/cblc_index_subtable_format_parser.rb +150 -0
  41. data/lib/fontisan/tables/cblc_index_subtable_header.rb +18 -0
  42. data/lib/fontisan/tables/cblc_sbit_line_metrics.rb +28 -0
  43. data/lib/fontisan/tables.rb +11 -0
  44. data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
  45. data/lib/fontisan/ufo/layer.rb +42 -2
  46. data/lib/fontisan/ufo.rb +2 -1
  47. data/lib/fontisan/version.rb +1 -1
  48. metadata +35 -2
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # 8-byte bigGlyphMetrics record embedded in CBLC IndexSubTable formats
6
+ # 2 and 5 (constant-metrics strikes). Layout is smallGlyphMetrics (5
7
+ # bytes) plus four additional signed advance fields.
8
+ #
9
+ # uint8 height
10
+ # uint8 width
11
+ # int8 bearing_x
12
+ # int8 bearing_y
13
+ # int8 advance
14
+ # int8 reserved
15
+ # int8 advance_lsb
16
+ # int8 advance_rsb
17
+ class CblcBigGlyphMetrics < Binary::BaseRecord
18
+ uint8 :height
19
+ uint8 :width
20
+ int8 :bearing_x
21
+ int8 :bearing_y
22
+ int8 :advance
23
+ int8 :reserved
24
+ int8 :advance_lsb
25
+ int8 :advance_rsb
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Fontisan
6
+ module Tables
7
+ # BitmapSize record embedded in a CBLC table (48 bytes).
8
+ #
9
+ # Each BitmapSize describes one bitmap strike: a glyph range rendered
10
+ # at a specific ppem with a specific bit depth. It points (via
11
+ # `index_subtable_array_offset`) at a sequence of IndexSubTableArray
12
+ # records that further describe how to find the bitmap data for each
13
+ # glyph in CBDT.
14
+ #
15
+ # Layout (48 bytes):
16
+ # uint32 index_subtable_array_offset
17
+ # uint32 index_tables_size
18
+ # uint32 number_of_index_subtables
19
+ # uint32 color_ref
20
+ # CblcSbitLineMetrics hori (12 bytes)
21
+ # CblcSbitLineMetrics vert (12 bytes)
22
+ # uint16 start_glyph_index
23
+ # uint16 end_glyph_index
24
+ # uint8 ppem_x
25
+ # uint8 ppem_y
26
+ # uint8 bit_depth
27
+ # int8 flags
28
+ #
29
+ # Reference: OpenType CBLC spec, "BitmapSize" record.
30
+ class CblcBitmapSize < Binary::BaseRecord
31
+ uint32 :index_subtable_array_offset
32
+ uint32 :index_tables_size
33
+ uint32 :number_of_index_subtables
34
+ uint32 :color_ref
35
+ CblcSbitLineMetrics :hori, type: Fontisan::Tables::CblcSbitLineMetrics
36
+ CblcSbitLineMetrics :vert, type: Fontisan::Tables::CblcSbitLineMetrics
37
+ uint16 :start_glyph_index
38
+ uint16 :end_glyph_index
39
+ uint8 :ppem_x
40
+ uint8 :ppem_y
41
+ uint8 :bit_depth
42
+ int8 :flags
43
+
44
+ # Pixels per em, assuming square pixels (the common case).
45
+ #
46
+ # @return [Integer] ppem value
47
+ def ppem
48
+ ppem_x
49
+ end
50
+
51
+ # Glyph ID range covered by this strike.
52
+ #
53
+ # @return [Range<Integer>] inclusive glyph ID range
54
+ def glyph_range
55
+ start_glyph_index..end_glyph_index
56
+ end
57
+
58
+ # Whether the strike covers a specific glyph.
59
+ #
60
+ # @param glyph_id [Integer] source glyph ID
61
+ # @return [Boolean]
62
+ def includes_glyph?(glyph_id)
63
+ glyph_range.include?(glyph_id)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # Immutable value object locating one glyph's bitmap inside a CBDT table.
6
+ #
7
+ # A CblcIndexSubTable produces one of these per glyph in its range.
8
+ # The subsetting pipeline reads them to know which CBDT byte range to
9
+ # retain, then computes fresh offsets for the subset output.
10
+ #
11
+ # @!attribute glyph_id
12
+ # @return [Integer] source glyph ID
13
+ # @!attribute image_format
14
+ # @return [Integer] CBDT image format (e.g. 17, 18, 19)
15
+ # @!attribute cbdt_offset
16
+ # @return [Integer] absolute byte offset within the source CBDT
17
+ # @!attribute byte_length
18
+ # @return [Integer] length of the bitmap block in CBDT
19
+ CblcGlyphBitmapLocation = Struct.new(:glyph_id, :image_format,
20
+ :cbdt_offset, :byte_length,
21
+ keyword_init: true) do
22
+ def initialize(*)
23
+ super
24
+ freeze
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # Parsed IndexSubTable from a CBLC table.
6
+ #
7
+ # The IndexSubTable is the format-specific directory describing how to
8
+ # locate each glyph's bitmap inside CBDT. Five formats are defined by
9
+ # the OpenType spec:
10
+ #
11
+ # 1 — variable metrics, variable size (uint32 offset array)
12
+ # 2 — constant metrics, constant size (single imageSize for the range)
13
+ # 3 — variable metrics, variable size, 4-byte-aligned (uint16 offsets × 4)
14
+ # 4 — variable metrics, variable size, bit-packed offsets (rare)
15
+ # 5 — constant metrics, constant size, with explicit uint32 offsetArray
16
+ #
17
+ # Each parsed IndexSubTable exposes one [CblcGlyphBitmapLocation] per
18
+ # glyph in its range, plus the raw bytes so a subsetter can preserve
19
+ # unchanged subtables verbatim if needed.
20
+ #
21
+ # Format-specific parsing is delegated to
22
+ # [CblcIndexSubTableFormatParser]; this class only owns the data model.
23
+ #
24
+ # Reference: OpenType CBLC spec, IndexSubTable formats 1–5.
25
+ class CblcIndexSubTable
26
+ # @return [Integer] indexFormat field (1, 2, 3, or 5)
27
+ attr_reader :index_format
28
+
29
+ # @return [Integer] imageFormat field (e.g. 17 = small metrics + PNG)
30
+ attr_reader :image_format
31
+
32
+ # @return [Integer] absolute imageDataOffset from start of CBDT
33
+ attr_reader :image_data_offset
34
+
35
+ # @return [Integer] first glyph ID covered by this subtable
36
+ attr_reader :first_glyph_index
37
+
38
+ # @return [Integer] last glyph ID covered by this subtable
39
+ attr_reader :last_glyph_index
40
+
41
+ # @return [Array<CblcGlyphBitmapLocation>] one per glyph in range
42
+ attr_reader :locations
43
+
44
+ # @return [String] raw bytes of this IndexSubTable in the source CBLC
45
+ attr_reader :raw_bytes
46
+
47
+ def initialize(index_format:, image_format:, image_data_offset:,
48
+ first_glyph_index:, last_glyph_index:, locations:,
49
+ raw_bytes:)
50
+ @index_format = index_format
51
+ @image_format = image_format
52
+ @image_data_offset = image_data_offset
53
+ @first_glyph_index = first_glyph_index
54
+ @last_glyph_index = last_glyph_index
55
+ @locations = locations
56
+ @raw_bytes = raw_bytes
57
+ end
58
+
59
+ # Parse an IndexSubTable from a CBLC binary blob.
60
+ #
61
+ # @param cblc_bytes [String] the full CBLC table bytes
62
+ # @param index_subtable_offset [Integer] absolute byte offset within
63
+ # `cblc_bytes` where this IndexSubTable begins
64
+ # @param first_glyph_index [Integer] first glyph ID covered
65
+ # @param last_glyph_index [Integer] last glyph ID covered
66
+ # @return [CblcIndexSubTable]
67
+ def self.parse(cblc_bytes, index_subtable_offset:, first_glyph_index:,
68
+ last_glyph_index:)
69
+ header = CblcIndexSubTableHeader.read(
70
+ cblc_bytes[index_subtable_offset, 8],
71
+ )
72
+
73
+ new(
74
+ index_format: header.index_format,
75
+ image_format: header.image_format,
76
+ image_data_offset: header.image_data_offset,
77
+ first_glyph_index: first_glyph_index,
78
+ last_glyph_index: last_glyph_index,
79
+ locations: CblcIndexSubTableFormatParser.locations(
80
+ header: header,
81
+ bytes: cblc_bytes,
82
+ base: index_subtable_offset,
83
+ first: first_glyph_index,
84
+ last: last_glyph_index,
85
+ ),
86
+ raw_bytes: CblcIndexSubTableFormatParser.raw_bytes(
87
+ header: header,
88
+ bytes: cblc_bytes,
89
+ base: index_subtable_offset,
90
+ first: first_glyph_index,
91
+ last: last_glyph_index,
92
+ ),
93
+ )
94
+ end
95
+
96
+ # Number of glyphs covered by this subtable.
97
+ #
98
+ # @return [Integer]
99
+ def glyph_count
100
+ last_glyph_index - first_glyph_index + 1
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # Entry in a CBLC IndexSubTableArray (8 bytes).
6
+ #
7
+ # The IndexSubTableArray sits at `BitmapSize#index_subtable_array_offset`
8
+ # (relative to the start of CBLC) and contains `number_of_index_subtables`
9
+ # of these entries. Each entry points (via `additional_offset_to_index_subtable`,
10
+ # relative to the IndexSubTableArray's own location) at an IndexSubTable
11
+ # record describing how to find the bitmaps for a glyph range.
12
+ #
13
+ # Reference: OpenType CBLC spec, IndexSubTableArray record.
14
+ class CblcIndexSubTableArrayEntry < Binary::BaseRecord
15
+ uint16 :first_glyph_index
16
+ uint16 :last_glyph_index
17
+ uint32 :additional_offset_to_index_subtable
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # Per-format CBLC IndexSubTable parser. Extracted as a sibling class so
6
+ # that CblcIndexSubTable remains a pure data model.
7
+ #
8
+ # The five OpenType formats differ in how the offset array encodes
9
+ # glyph lengths and CBDT offsets. This class hides that arithmetic
10
+ # behind a uniform [.locations] entry point that returns a flat list
11
+ # of CblcGlyphBitmapLocation objects (one per glyph in the range).
12
+ #
13
+ # Reference: OpenType CBLC spec, IndexSubTable formats 1–5.
14
+ class CblcIndexSubTableFormatParser
15
+ # Format 4 (bit-packed offsets) is rare and not currently parsed.
16
+ class UnsupportedFormat < Fontisan::Error; end
17
+
18
+ class << self
19
+ # Returns an Array of CblcGlyphBitmapLocation, one per glyph in
20
+ # the IndexSubTable's glyph range.
21
+ #
22
+ # @param header [CblcIndexSubTableHeader] the 8-byte IndexSubTable header
23
+ # @param bytes [String] the full CBLC table bytes
24
+ # @param base [Integer] absolute offset of this IndexSubTable within CBLC
25
+ # @param first [Integer] first glyph ID covered
26
+ # @param last [Integer] last glyph ID covered
27
+ # @return [Array<CblcGlyphBitmapLocation>]
28
+ def locations(header:, bytes:, base:, first:, last:)
29
+ case header.index_format
30
+ when 1 then format_1(header, bytes, base, first, last)
31
+ when 2 then format_2(header, bytes, base, first, last)
32
+ when 3 then format_3(header, bytes, base, first, last)
33
+ when 5 then format_5(header, bytes, base, first, last)
34
+ when 4
35
+ raise UnsupportedFormat,
36
+ "CBLC IndexSubTable format 4 (bit-packed) is unsupported"
37
+ else
38
+ raise UnsupportedFormat,
39
+ "Unknown CBLC IndexSubTable format: #{header.index_format}"
40
+ end
41
+ end
42
+
43
+ # Returns the raw bytes the IndexSubTable occupies in CBLC.
44
+ #
45
+ # @return [String]
46
+ def raw_bytes(header:, bytes:, base:, first:, last:)
47
+ count = last - first + 1
48
+ length = byte_length(header.index_format, count, bytes, base)
49
+ bytes[base, length]
50
+ end
51
+
52
+ private
53
+
54
+ # Format 1: variable metrics + variable size, uint32 offsetArray.
55
+ # offsets[0] is the first glyph's bitmap start (relative to
56
+ # imageDataOffset); offsets[i+1] - offsets[i] = glyph i's length.
57
+ def format_1(header, bytes, base, first, last)
58
+ count = last - first + 1
59
+ offsets = uint32_array(bytes, base + 8, count + 1)
60
+ build(first, count, offsets, header.image_format,
61
+ header.image_data_offset)
62
+ end
63
+
64
+ # Format 2: constant metrics + constant size. One imageSize field
65
+ # applies to every glyph; the i-th glyph's bitmap is at
66
+ # imageDataOffset + i * imageSize.
67
+ def format_2(header, bytes, base, first, last)
68
+ count = last - first + 1
69
+ image_size = bytes[base + 8, 4].unpack1("N")
70
+ Array.new(count) do |i|
71
+ CblcGlyphBitmapLocation.new(
72
+ glyph_id: first + i,
73
+ image_format: header.image_format,
74
+ cbdt_offset: header.image_data_offset + (i * image_size),
75
+ byte_length: image_size,
76
+ )
77
+ end
78
+ end
79
+
80
+ # Format 3: like format 1 but offsets are uint16 and stored as
81
+ # multiples of 4 (CBDT bitmap data is 4-byte aligned).
82
+ def format_3(header, bytes, base, first, last)
83
+ count = last - first + 1
84
+ raw = uint16_array(bytes, base + 8, count + 1)
85
+ offsets = raw.map { |v| v * 4 }
86
+ build(first, count, offsets, header.image_format,
87
+ header.image_data_offset)
88
+ end
89
+
90
+ # Format 5: constant metrics + constant size with an explicit
91
+ # uint32 offsetArray (count + 1 entries). Length per glyph is
92
+ # the constant imageSize; offset comes from the array.
93
+ def format_5(header, bytes, base, first, last)
94
+ count = last - first + 1
95
+ image_size = bytes[base + 8, 4].unpack1("N")
96
+ offset_array_start = base + 12 + 8 + 4
97
+ offsets = uint32_array(bytes, offset_array_start, count + 1)
98
+ Array.new(count) do |i|
99
+ start_off = offsets[i] || 0
100
+ CblcGlyphBitmapLocation.new(
101
+ glyph_id: first + i,
102
+ image_format: header.image_format,
103
+ cbdt_offset: header.image_data_offset + start_off,
104
+ byte_length: image_size,
105
+ )
106
+ end
107
+ end
108
+
109
+ def build(first, count, offsets, image_format, image_data_offset)
110
+ Array.new(count) do |i|
111
+ start_off = offsets[i] || 0
112
+ end_off = offsets[i + 1] || start_off
113
+ CblcGlyphBitmapLocation.new(
114
+ glyph_id: first + i,
115
+ image_format: image_format,
116
+ cbdt_offset: image_data_offset + start_off,
117
+ byte_length: end_off - start_off,
118
+ )
119
+ end
120
+ end
121
+
122
+ def byte_length(index_format, count, bytes, base)
123
+ case index_format
124
+ when 1, 3
125
+ entry_size = index_format == 1 ? 4 : 2
126
+ 8 + (count + 1) * entry_size
127
+ when 2
128
+ 8 + 4 + 8 # header + imageSize + bigGlyphMetrics
129
+ when 5
130
+ 8 + 4 + 8 + 4 + (count + 1) * 4
131
+ else
132
+ bytes.bytesize - base
133
+ end
134
+ end
135
+
136
+ def uint32_array(bytes, offset, count)
137
+ return [] if count.zero?
138
+
139
+ bytes[offset, count * 4].unpack("N*")
140
+ end
141
+
142
+ def uint16_array(bytes, offset, count)
143
+ return [] if count.zero?
144
+
145
+ bytes[offset, count * 2].unpack("n*")
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # 8-byte header common to all CBLC IndexSubTable formats.
6
+ #
7
+ # uint16 indexFormat
8
+ # uint16 imageFormat
9
+ # uint32 imageDataOffset (absolute, from start of CBDT)
10
+ #
11
+ # Format-specific data follows immediately after this header.
12
+ class CblcIndexSubTableHeader < Binary::BaseRecord
13
+ uint16 :index_format
14
+ uint16 :image_format
15
+ uint32 :image_data_offset
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # SbitLineMetrics record embedded in CBLC BitmapSize records.
6
+ #
7
+ # Two of these (horizontal + vertical) appear in every BitmapSize,
8
+ # totalling 24 bytes per strike. The fields are signed/unsigned 8-bit
9
+ # integers; OpenType specifies signed values for some, unsigned for
10
+ # others, mirroring the EBDT/BDF layout.
11
+ #
12
+ # Reference: OpenType CBLC spec, "SbitLineMetrics" sub-structure.
13
+ class CblcSbitLineMetrics < Binary::BaseRecord
14
+ int8 :ascender
15
+ int8 :descender
16
+ uint8 :width_max
17
+ int8 :caret_slope_numerator
18
+ int8 :caret_slope_denominator
19
+ int8 :caret_offset
20
+ int8 :min_origin_sb
21
+ int8 :min_advance_sb
22
+ int8 :max_before_bl
23
+ int8 :min_after_bl
24
+ int8 :pad1
25
+ int8 :pad2
26
+ end
27
+ end
28
+ end
@@ -4,6 +4,17 @@
4
4
 
5
5
  module Fontisan
6
6
  module Tables
7
+ autoload :CblcBigGlyphMetrics, "fontisan/tables/cblc_big_glyph_metrics"
8
+ autoload :CblcBitmapSize, "fontisan/tables/cblc_bitmap_size"
9
+ autoload :CblcGlyphBitmapLocation, "fontisan/tables/cblc_glyph_bitmap_location"
10
+ autoload :CblcIndexSubTable, "fontisan/tables/cblc_index_subtable"
11
+ autoload :CblcIndexSubTableArrayEntry,
12
+ "fontisan/tables/cblc_index_subtable_array_entry"
13
+ autoload :CblcIndexSubTableFormatParser,
14
+ "fontisan/tables/cblc_index_subtable_format_parser"
15
+ autoload :CblcIndexSubTableHeader,
16
+ "fontisan/tables/cblc_index_subtable_header"
17
+ autoload :CblcSbitLineMetrics, "fontisan/tables/cblc_sbit_line_metrics"
7
18
  autoload :Cbdt, "fontisan/tables/cbdt"
8
19
  autoload :Cblc, "fontisan/tables/cblc"
9
20
  autoload :Cff, "fontisan/tables/cff"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ # Raised by +Layer#add+ when a glyph with the same name is already
6
+ # present. Surfaces what would otherwise be a silent overwrite so
7
+ # the caller can decide between +Layer#put+ (intentional replace)
8
+ # and +Stitcher::UniqueGlyphName.in+ (auto-rename).
9
+ #
10
+ # Extracted as a sibling of +Layer+ (rather than nested inside it)
11
+ # so the namespace stays flat and the error can be referenced
12
+ # without pulling in the full Layer implementation.
13
+ class GlyphExistsError < StandardError
14
+ attr_reader :name
15
+
16
+ def initialize(name)
17
+ @name = name
18
+ super("a glyph named #{name.inspect} already exists; use #put " \
19
+ "to overwrite or UniqueGlyphName.in to deconflict")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -5,8 +5,27 @@ module Fontisan
5
5
  # A single layer in a UFO source. A Layer holds a set of glyphs
6
6
  # keyed by name. The default layer is `public.default` per UFO 3.
7
7
  #
8
- # glyphs is mutated in place by Reader and Writer; the Layer does
9
- # not own serialization concerns.
8
+ # == Naming contract
9
+ #
10
+ # Glyph names are the layer's primary key. Two distinct glyphs with
11
+ # the same name cannot coexist — adding the second would silently
12
+ # destroy the first, a class of bug that has historically cost real
13
+ # data (e.g. CJK Ext G cmap loss when CBDT placeholders collided
14
+ # with outline glyphs sharing "gid{N}" names).
15
+ #
16
+ # To make that contract unbreakable, +#add+ RAISES +GlyphExistsError+
17
+ # on conflict. Callers who want one of the other two semantics pick
18
+ # the method that names it:
19
+ #
20
+ # +#add+:: insert; raise if the name is taken (the safe default)
21
+ # +#put+:: insert; overwrite any existing glyph with the same name
22
+ #
23
+ # Callers that need auto-renaming (insertion without giving up on a
24
+ # collision) call +Stitcher::UniqueGlyphName.in(target, base)+ first,
25
+ # then +#add+ the glyph under the returned name.
26
+ #
27
+ # The error class itself lives at +Fontisan::Ufo::GlyphExistsError+
28
+ # (sibling, not nested) so the namespace stays flat.
10
29
  class Layer
11
30
  DEFAULT_NAME = "public.default"
12
31
 
@@ -21,7 +40,28 @@ module Fontisan
21
40
  @glyphs[glyph_name.to_s]
22
41
  end
23
42
 
43
+ # Insert +glyph+. Raises +GlyphExistsError+ if a glyph with the
44
+ # same name is already present — this is the contract that keeps
45
+ # the layer's key namespace trustworthy.
46
+ #
47
+ # @param glyph [Glyph]
48
+ # @return [Glyph] the same glyph
49
+ # @raise [GlyphExistsError] if +glyph.name+ is already taken
24
50
  def add(glyph)
51
+ name = glyph.name.to_s
52
+ raise GlyphExistsError, name if @glyphs.key?(name)
53
+
54
+ @glyphs[name] = glyph
55
+ glyph
56
+ end
57
+
58
+ # Insert +glyph+, replacing any existing glyph with the same
59
+ # name. Use this when the caller has positively decided that the
60
+ # previous glyph (if any) should be discarded.
61
+ #
62
+ # @param glyph [Glyph]
63
+ # @return [Glyph] the same glyph
64
+ def put(glyph)
25
65
  @glyphs[glyph.name.to_s] = glyph
26
66
  glyph
27
67
  end
data/lib/fontisan/ufo.rb CHANGED
@@ -17,7 +17,8 @@ module Fontisan
17
17
  #
18
18
  # Reference: https://unifiedfontobject.org
19
19
  module Ufo
20
- autoload :Font, "fontisan/ufo/font"
20
+ autoload :Font, "fontisan/ufo/font"
21
+ autoload :GlyphExistsError, "fontisan/ufo/glyph_exists_error"
21
22
  autoload :Info, "fontisan/ufo/info"
22
23
  autoload :Layer, "fontisan/ufo/layer"
23
24
  autoload :LayerSet, "fontisan/ufo/layer_set"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.22"
4
+ VERSION = "0.4.23"
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.22
4
+ version: 0.4.23
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-08 00:00:00.000000000 Z
11
+ date: 2026-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -440,6 +440,7 @@ files:
440
440
  - lib/fontisan/stitcher/collection_result.rb
441
441
  - lib/fontisan/stitcher/deduplicator.rb
442
442
  - lib/fontisan/stitcher/format_metadata.rb
443
+ - lib/fontisan/stitcher/glyph_cloner.rb
443
444
  - lib/fontisan/stitcher/glyph_copier.rb
444
445
  - lib/fontisan/stitcher/glyph_limit.rb
445
446
  - lib/fontisan/stitcher/glyph_signature.rb
@@ -455,12 +456,35 @@ files:
455
456
  - lib/fontisan/stitcher/selector/gid.rb
456
457
  - lib/fontisan/stitcher/selector/range.rb
457
458
  - lib/fontisan/stitcher/source.rb
459
+ - lib/fontisan/stitcher/unique_glyph_name.rb
458
460
  - lib/fontisan/stitcher_cli.rb
459
461
  - lib/fontisan/subset.rb
460
462
  - lib/fontisan/subset/builder.rb
461
463
  - lib/fontisan/subset/glyph_mapping.rb
462
464
  - lib/fontisan/subset/options.rb
463
465
  - lib/fontisan/subset/profile.rb
466
+ - lib/fontisan/subset/shared_state.rb
467
+ - lib/fontisan/subset/subset_context.rb
468
+ - lib/fontisan/subset/table_strategy.rb
469
+ - lib/fontisan/subset/table_strategy/cbdt.rb
470
+ - lib/fontisan/subset/table_strategy/cblc.rb
471
+ - lib/fontisan/subset/table_strategy/cmap.rb
472
+ - lib/fontisan/subset/table_strategy/color_bitmap_placement.rb
473
+ - lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb
474
+ - lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb
475
+ - lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb
476
+ - lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb
477
+ - lib/fontisan/subset/table_strategy/glyf.rb
478
+ - lib/fontisan/subset/table_strategy/glyf_loca_builder.rb
479
+ - lib/fontisan/subset/table_strategy/head.rb
480
+ - lib/fontisan/subset/table_strategy/hhea.rb
481
+ - lib/fontisan/subset/table_strategy/hmtx.rb
482
+ - lib/fontisan/subset/table_strategy/loca.rb
483
+ - lib/fontisan/subset/table_strategy/maxp.rb
484
+ - lib/fontisan/subset/table_strategy/name.rb
485
+ - lib/fontisan/subset/table_strategy/os2.rb
486
+ - lib/fontisan/subset/table_strategy/pass_through.rb
487
+ - lib/fontisan/subset/table_strategy/post.rb
464
488
  - lib/fontisan/subset/table_subsetter.rb
465
489
  - lib/fontisan/svg.rb
466
490
  - lib/fontisan/svg/font_face_generator.rb
@@ -483,6 +507,14 @@ files:
483
507
  - lib/fontisan/tables.rb
484
508
  - lib/fontisan/tables/cbdt.rb
485
509
  - lib/fontisan/tables/cblc.rb
510
+ - lib/fontisan/tables/cblc_big_glyph_metrics.rb
511
+ - lib/fontisan/tables/cblc_bitmap_size.rb
512
+ - lib/fontisan/tables/cblc_glyph_bitmap_location.rb
513
+ - lib/fontisan/tables/cblc_index_subtable.rb
514
+ - lib/fontisan/tables/cblc_index_subtable_array_entry.rb
515
+ - lib/fontisan/tables/cblc_index_subtable_format_parser.rb
516
+ - lib/fontisan/tables/cblc_index_subtable_header.rb
517
+ - lib/fontisan/tables/cblc_sbit_line_metrics.rb
486
518
  - lib/fontisan/tables/cff.rb
487
519
  - lib/fontisan/tables/cff/cff2_charstring_builder.rb
488
520
  - lib/fontisan/tables/cff/cff_glyph.rb
@@ -657,6 +689,7 @@ files:
657
689
  - lib/fontisan/ufo/features.rb
658
690
  - lib/fontisan/ufo/font.rb
659
691
  - lib/fontisan/ufo/glyph.rb
692
+ - lib/fontisan/ufo/glyph_exists_error.rb
660
693
  - lib/fontisan/ufo/guideline.rb
661
694
  - lib/fontisan/ufo/image.rb
662
695
  - lib/fontisan/ufo/image_set.rb