fontisan 0.4.22 → 0.4.24

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +240 -24
  3. data/Gemfile +6 -0
  4. data/Rakefile +9 -6
  5. data/benchmark/compile_benchmark.rb +13 -5
  6. data/docs/.vitepress/config.ts +1 -0
  7. data/docs/STITCHER_GUIDE.adoc +47 -3
  8. data/docs/api/layer.md +89 -0
  9. data/docs/cli/subset.md +25 -6
  10. data/docs/guide/cli/subset.md +31 -0
  11. data/docs/guide/color-fonts/bitmaps.md +17 -0
  12. data/lib/fontisan/cli.rb +2 -1
  13. data/lib/fontisan/collection/writer.rb +2 -1
  14. data/lib/fontisan/commands/validate_collection_command.rb +7 -1
  15. data/lib/fontisan/commands.rb +2 -1
  16. data/lib/fontisan/config/subset_profiles.yml +2 -0
  17. data/lib/fontisan/converters/collection_converter.rb +6 -1
  18. data/lib/fontisan/converters/svg_generator.rb +3 -1
  19. data/lib/fontisan/stitcher/cbdt_propagator.rb +44 -22
  20. data/lib/fontisan/stitcher/collection_result.rb +2 -1
  21. data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
  22. data/lib/fontisan/stitcher/glyph_copier.rb +5 -34
  23. data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
  24. data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
  25. data/lib/fontisan/stitcher/source.rb +2 -1
  26. data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
  27. data/lib/fontisan/stitcher.rb +4 -1
  28. data/lib/fontisan/subset/shared_state.rb +42 -0
  29. data/lib/fontisan/subset/subset_context.rb +20 -0
  30. data/lib/fontisan/subset/table_strategy/cbdt.rb +30 -0
  31. data/lib/fontisan/subset/table_strategy/cblc.rb +27 -0
  32. data/lib/fontisan/subset/table_strategy/cmap.rb +146 -0
  33. data/lib/fontisan/subset/table_strategy/color_bitmap_placement.rb +20 -0
  34. data/lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb +45 -0
  35. data/lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb +62 -0
  36. data/lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb +182 -0
  37. data/lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb +59 -0
  38. data/lib/fontisan/subset/table_strategy/glyf.rb +29 -0
  39. data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +146 -0
  40. data/lib/fontisan/subset/table_strategy/head.rb +42 -0
  41. data/lib/fontisan/subset/table_strategy/hhea.rb +67 -0
  42. data/lib/fontisan/subset/table_strategy/hmtx.rb +52 -0
  43. data/lib/fontisan/subset/table_strategy/loca.rb +41 -0
  44. data/lib/fontisan/subset/table_strategy/maxp.rb +23 -0
  45. data/lib/fontisan/subset/table_strategy/name.rb +19 -0
  46. data/lib/fontisan/subset/table_strategy/os2.rb +21 -0
  47. data/lib/fontisan/subset/table_strategy/pass_through.rb +20 -0
  48. data/lib/fontisan/subset/table_strategy/post.rb +37 -0
  49. data/lib/fontisan/subset/table_strategy.rb +75 -0
  50. data/lib/fontisan/subset/table_subsetter.rb +49 -616
  51. data/lib/fontisan/subset.rb +3 -0
  52. data/lib/fontisan/svg/standalone_glyph.rb +4 -1
  53. data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
  54. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
  55. data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
  56. data/lib/fontisan/tables/cbdt.rb +66 -121
  57. data/lib/fontisan/tables/cblc.rb +110 -231
  58. data/lib/fontisan/tables/cblc_big_glyph_metrics.rb +28 -0
  59. data/lib/fontisan/tables/cblc_bitmap_size.rb +67 -0
  60. data/lib/fontisan/tables/cblc_glyph_bitmap_location.rb +28 -0
  61. data/lib/fontisan/tables/cblc_index_subtable.rb +104 -0
  62. data/lib/fontisan/tables/cblc_index_subtable_array_entry.rb +20 -0
  63. data/lib/fontisan/tables/cblc_index_subtable_format_parser.rb +150 -0
  64. data/lib/fontisan/tables/cblc_index_subtable_header.rb +18 -0
  65. data/lib/fontisan/tables/cblc_sbit_line_metrics.rb +28 -0
  66. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +11 -5
  67. data/lib/fontisan/tables/cff.rb +2 -1
  68. data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
  69. data/lib/fontisan/tables.rb +12 -0
  70. data/lib/fontisan/ufo/cli.rb +4 -1
  71. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
  72. data/lib/fontisan/ufo/compile/cff.rb +6 -2
  73. data/lib/fontisan/ufo/compile/cmap.rb +11 -5
  74. data/lib/fontisan/ufo/compile/cpal.rb +2 -1
  75. data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
  76. data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
  77. data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
  78. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
  79. data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
  80. data/lib/fontisan/ufo/compile/fvar.rb +7 -3
  81. data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
  82. data/lib/fontisan/ufo/compile/gvar.rb +5 -2
  83. data/lib/fontisan/ufo/compile/head.rb +2 -1
  84. data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
  85. data/lib/fontisan/ufo/compile/name.rb +3 -1
  86. data/lib/fontisan/ufo/compile/sbix.rb +2 -1
  87. data/lib/fontisan/ufo/compile/stat.rb +6 -3
  88. data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
  89. data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
  90. data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
  91. data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
  92. data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
  93. data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
  94. data/lib/fontisan/ufo/convert.rb +4 -1
  95. data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
  96. data/lib/fontisan/ufo/info.rb +4 -2
  97. data/lib/fontisan/ufo/layer.rb +42 -2
  98. data/lib/fontisan/ufo/plist.rb +7 -2
  99. data/lib/fontisan/ufo/reader.rb +3 -1
  100. data/lib/fontisan/ufo/writer.rb +13 -5
  101. data/lib/fontisan/ufo.rb +2 -1
  102. data/lib/fontisan/version.rb +1 -1
  103. data/lib/fontisan/woff2/collection_decoder.rb +4 -1
  104. data/lib/fontisan/woff2/collection_encoder.rb +4 -1
  105. data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
  106. data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
  107. data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
  108. data/lib/fontisan.rb +0 -1
  109. metadata +36 -4
  110. data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
  111. data/lib/fontisan/tasks.rb +0 -11
@@ -8,6 +8,9 @@ module Fontisan
8
8
  autoload :GlyphMapping, "fontisan/subset/glyph_mapping"
9
9
  autoload :Options, "fontisan/subset/options"
10
10
  autoload :Profile, "fontisan/subset/profile"
11
+ autoload :SharedState, "fontisan/subset/shared_state"
12
+ autoload :SubsetContext, "fontisan/subset/subset_context"
13
+ autoload :TableStrategy, "fontisan/subset/table_strategy"
11
14
  autoload :TableSubsetter, "fontisan/subset/table_subsetter"
12
15
  end
13
16
  end
@@ -118,7 +118,10 @@ module Fontisan
118
118
 
119
119
  def bounding_box(glyph)
120
120
  points = glyph.contours.flat_map(&:points)
121
- return { x_min: 0, y_min: 0, x_max: @units_per_em, y_max: @units_per_em } if points.empty?
121
+ if points.empty?
122
+ return { x_min: 0, y_min: 0, x_max: @units_per_em,
123
+ y_max: @units_per_em }
124
+ end
122
125
 
123
126
  xs = points.map(&:x)
124
127
  ys = points.map(&:y)
@@ -74,7 +74,8 @@ module Fontisan
74
74
  private
75
75
 
76
76
  def build_from_doc_path(data, group_transform, doc, codepoint)
77
- final = normalizer_for(width: doc.viewbox_width, height: doc.viewbox_height)
77
+ final = normalizer_for(width: doc.viewbox_width,
78
+ height: doc.viewbox_height)
78
79
  .final_transform(group_transform)
79
80
  contours = build_contours(data, final)
80
81
  assemble_glyph(glyph_name_for(codepoint), contours, codepoint)
@@ -89,13 +90,15 @@ module Fontisan
89
90
  def transform_contour(contour, transform)
90
91
  points = contour.points.map do |pt|
91
92
  x, y = transform.apply(pt.x, pt.y)
92
- Fontisan::Ufo::Point.new(x: x.round, y: y.round, type: pt.type, smooth: pt.smooth)
93
+ Fontisan::Ufo::Point.new(x: x.round, y: y.round, type: pt.type,
94
+ smooth: pt.smooth)
93
95
  end
94
96
  Fontisan::Ufo::Contour.new(points)
95
97
  end
96
98
 
97
99
  def normalizer_for(width:, height:)
98
- Geometry::Normalizer.new(viewbox_width: width, viewbox_height: height, upm: @upm)
100
+ Geometry::Normalizer.new(viewbox_width: width, viewbox_height: height,
101
+ upm: @upm)
99
102
  end
100
103
 
101
104
  def assemble_glyph(name, contours, codepoint)
@@ -22,7 +22,9 @@ module Fontisan
22
22
  transforms = transform_string.scan(FUNCTION_RE).map do |name, args|
23
23
  build_transform(name, args)
24
24
  end
25
- transforms.reduce(AffineTransform.identity) { |acc, t| acc.compose(t) }
25
+ transforms.reduce(AffineTransform.identity) do |acc, t|
26
+ acc.compose(t)
27
+ end
26
28
  end
27
29
 
28
30
  def self.build_transform(name, args_string)
@@ -35,7 +37,8 @@ module Fontisan
35
37
  when "skewX" then AffineTransform.skew_x_radians(degrees_to_radians(args.fetch(0)))
36
38
  when "skewY" then AffineTransform.skew_y_radians(degrees_to_radians(args.fetch(0)))
37
39
  else
38
- raise ArgumentError, "unknown SVG transform function: #{name.inspect}"
40
+ raise ArgumentError,
41
+ "unknown SVG transform function: #{name.inspect}"
39
42
  end
40
43
  end
41
44
 
@@ -59,9 +62,13 @@ module Fontisan
59
62
  end
60
63
 
61
64
  def self.build_matrix(args)
62
- raise ArgumentError, "matrix() requires 6 arguments, got #{args.size}" if args.size != 6
65
+ if args.size != 6
66
+ raise ArgumentError,
67
+ "matrix() requires 6 arguments, got #{args.size}"
68
+ end
63
69
 
64
- AffineTransform.new(args[0], args[1], args[2], args[3], args[4], args[5])
70
+ AffineTransform.new(args[0], args[1], args[2], args[3], args[4],
71
+ args[5])
65
72
  end
66
73
 
67
74
  # Rotate around a specific point: translate to origin, rotate,
@@ -5,8 +5,10 @@ module Fontisan
5
5
  # Geometry primitives for SvgToGlyf: affine transforms, SVG
6
6
  # transform attribute parsing, and coordinate normalization.
7
7
  module Geometry
8
- autoload :AffineTransform, "fontisan/svg_to_glyf/geometry/affine_transform"
9
- autoload :TransformParser, "fontisan/svg_to_glyf/geometry/transform_parser"
8
+ autoload :AffineTransform,
9
+ "fontisan/svg_to_glyf/geometry/affine_transform"
10
+ autoload :TransformParser,
11
+ "fontisan/svg_to_glyf/geometry/transform_parser"
10
12
  autoload :Normalizer, "fontisan/svg_to_glyf/geometry/normalizer"
11
13
  end
12
14
  end
@@ -1,167 +1,112 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "stringio"
4
-
5
3
  module Fontisan
6
4
  module Tables
7
- # CBDT (Color Bitmap Data) table parser
8
- #
9
- # The CBDT table contains the actual bitmap data for color glyphs. It works
10
- # together with the CBLC table which provides the location information for
11
- # finding bitmaps in this table.
12
- #
13
- # CBDT Table Structure:
14
- # ```
15
- # CBDT Table = Header (8 bytes)
16
- # + Bitmap Data (variable length)
17
- # ```
18
- #
19
- # Header (8 bytes):
20
- # - majorVersion (uint16): Major version (2 or 3)
21
- # - minorVersion (uint16): Minor version (0)
22
- # - reserved (uint32): Reserved, set to 0
5
+ # CBDT (Color Bitmap Data) table.
23
6
  #
24
- # The bitmap data format depends on the index subtable format in CBLC.
25
- # Common formats include:
26
- # - Format 17: Small metrics, PNG data
27
- # - Format 18: Big metrics, PNG data
28
- # - Format 19: Metrics in CBLC, PNG data
7
+ # CBDT stores the raw bitmap data blocks for color glyphs. CBLC indexes
8
+ # into CBDT via `imageDataOffset` and per-glyph offset arrays. CBDT
9
+ # itself is essentially a blob: the 4-byte header (majorVersion +
10
+ # minorVersion) followed by an opaque sequence of bitmap blocks.
29
11
  #
30
- # This parser provides low-level access to bitmap data. For proper bitmap
31
- # extraction, use together with CBLC table which contains the index.
12
+ # This model exposes the header fields via BinData and preserves the
13
+ # original bytes for subsetting and direct slicing. A Cbdt constructed
14
+ # via `Cbdt.new` (no `read`) has no captured bytes — `raw_data` is nil
15
+ # and `data_size` returns 0.
32
16
  #
33
- # Reference: OpenType CBDT specification
34
- # https://docs.microsoft.com/en-us/typography/opentype/spec/cbdt
17
+ # Reference: OpenType CBDT specification.
18
+ # https://learn.microsoft.com/en-us/typography/opentype/spec/cbdt
35
19
  #
36
- # @example Reading a CBDT table
37
- # data = font.table_data['CBDT']
38
- # cbdt = Fontisan::Tables::Cbdt.read(data)
39
- # bitmap_data = cbdt.bitmap_data_at(offset, length)
20
+ # @example Reading CBDT and slicing a bitmap block
21
+ # cbdt = Fontisan::Tables::Cbdt.read(font.table_data["CBDT"])
22
+ # bytes = cbdt.bitmap_data_at(120, 4096)
40
23
  class Cbdt < Binary::BaseRecord
41
- # OpenType table tag for CBDT
42
24
  TAG = "CBDT"
43
25
 
44
- # Supported CBDT versions
45
- VERSION_2_0 = 0x0002_0000
46
- VERSION_3_0 = 0x0003_0000
26
+ # CBDT v2.0 — original release
27
+ VERSION_2_0 = 0x00020000
28
+ # CBDT v3.0 — adds PNG image format support
29
+ VERSION_3_0 = 0x00030000
47
30
 
48
- # @return [Integer] Major version (2 or 3)
49
- attr_reader :major_version
31
+ uint16 :major_version
32
+ uint16 :minor_version
50
33
 
51
- # @return [Integer] Minor version (0)
52
- attr_reader :minor_version
53
-
54
- # @return [String] Raw binary data for the entire CBDT table
55
- attr_reader :raw_data
56
-
57
- # Override read to parse CBDT structure
34
+ # Whether this instance was populated from real table bytes.
58
35
  #
59
- # @param io [IO, String] Binary data to read
60
- # @return [Cbdt] Parsed CBDT table
61
- def self.read(io)
62
- cbdt = new
63
- return cbdt if io.nil?
64
-
65
- data = io.is_a?(String) ? io : io.read
66
- cbdt.parse!(data)
67
- cbdt
36
+ # @return [Boolean]
37
+ def has_raw_data?
38
+ defined?(@raw_data) && !@raw_data.nil? ? true : false
68
39
  end
69
40
 
70
- # Parse the CBDT table structure
41
+ # Raw bytes captured at read time, or nil for a fresh instance.
71
42
  #
72
- # @param data [String] Binary data for the CBDT table
73
- # @raise [CorruptedTableError] If CBDT structure is invalid
74
- def parse!(data)
75
- @raw_data = data
76
- io = StringIO.new(data)
77
-
78
- # Parse CBDT header (8 bytes)
79
- parse_header(io)
80
- validate_header!
81
- rescue StandardError => e
82
- raise CorruptedTableError, "Failed to parse CBDT table: #{e.message}"
83
- end
43
+ # @return [String, nil]
44
+ def raw_data
45
+ return @raw_data if defined?(@raw_data)
84
46
 
85
- # Get bitmap data at specific offset and length
86
- #
87
- # Used together with CBLC index to extract bitmap data.
88
- #
89
- # @param offset [Integer] Offset from start of table
90
- # @param length [Integer] Length of bitmap data
91
- # @return [String, nil] Binary bitmap data or nil
92
- def bitmap_data_at(offset, length)
93
- return nil if offset.nil? || length.nil?
94
- return nil if offset.negative? || length.negative?
95
- return nil if offset + length > raw_data.length
96
-
97
- raw_data[offset, length]
47
+ nil
98
48
  end
99
49
 
100
- # Get combined version number
50
+ # Combined version number (e.g. 0x00030000 for v3.0).
101
51
  #
102
- # @return [Integer] Combined version (e.g., 0x00020000 for v2.0)
52
+ # @return [Integer]
103
53
  def version
104
- return nil if major_version.nil? || minor_version.nil?
105
-
106
54
  (major_version << 16) | minor_version
107
55
  end
108
56
 
109
- # Get table data size
57
+ # Total byte size of the captured CBDT bytes, or 0 if none.
110
58
  #
111
- # @return [Integer] Size of CBDT table in bytes
59
+ # @return [Integer]
112
60
  def data_size
113
- raw_data&.length || 0
61
+ raw_data&.bytesize || 0
114
62
  end
115
63
 
116
- # Check if offset is valid for this table
64
+ # Whether `offset` falls within the captured CBDT byte range.
117
65
  #
118
- # @param offset [Integer] Offset to check
119
- # @return [Boolean] True if offset is within table bounds
66
+ # @param offset [Integer, nil]
67
+ # @return [Boolean]
120
68
  def valid_offset?(offset)
121
69
  return false if offset.nil? || offset.negative?
122
- return false if raw_data.nil?
123
70
 
124
- offset < raw_data.length
71
+ offset < data_size
125
72
  end
126
73
 
127
- # Validate the CBDT table structure
74
+ # Slice `length` bytes starting at `offset` from the CBDT blob.
75
+ # Returns nil for out-of-range offsets/lengths so callers can use a
76
+ # nil check instead of rescuing IndexError.
128
77
  #
129
- # @return [Boolean] True if valid
130
- def valid?
131
- return false if major_version.nil? || minor_version.nil?
132
- return false unless [2, 3].include?(major_version)
133
- return false unless minor_version.zero?
134
- return false unless raw_data
78
+ # @param offset [Integer, nil] start byte
79
+ # @param length [Integer, nil] number of bytes
80
+ # @return [String, nil]
81
+ def bitmap_data_at(offset, length)
82
+ return nil if offset.nil? || length.nil?
83
+ return nil if offset.negative? || length.negative?
84
+ return nil if offset + length > data_size
135
85
 
136
- true
86
+ raw_data[offset, length]
137
87
  end
138
88
 
139
- private
140
-
141
- # Parse CBDT header (8 bytes)
89
+ # Serialize the captured bytes back to binary. Falls back to BinData's
90
+ # default serialization (4-byte header) if no bytes were captured,
91
+ # so consumers can construct a CBDT from scratch.
142
92
  #
143
- # @param io [StringIO] Input stream
144
- def parse_header(io)
145
- @major_version = io.read(2).unpack1("n")
146
- @minor_version = io.read(2).unpack1("n")
147
- @reserved = io.read(4).unpack1("N")
93
+ # @return [String]
94
+ def to_binary_s
95
+ return raw_data if has_raw_data?
96
+
97
+ super
148
98
  end
149
99
 
150
- # Validate header values
100
+ # Whether the header declares a CBDT version fontisan understands
101
+ # (v2.0 or v3.0) and any raw bytes were captured.
151
102
  #
152
- # @raise [CorruptedTableError] If validation fails
153
- def validate_header!
154
- unless [2, 3].include?(major_version)
155
- raise CorruptedTableError,
156
- "Unsupported CBDT major version: #{major_version} " \
157
- "(only versions 2 and 3 supported)"
158
- end
159
-
160
- unless minor_version.zero?
161
- raise CorruptedTableError,
162
- "Unsupported CBDT minor version: #{minor_version} " \
163
- "(only version 0 supported)"
164
- end
103
+ # @return [Boolean]
104
+ def valid?
105
+ return false unless [2, 3].include?(major_version)
106
+ return false unless minor_version.zero?
107
+ return false unless has_raw_data?
108
+
109
+ true
165
110
  end
166
111
  end
167
112
  end