fontisan 0.4.23 → 0.4.25

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +240 -29
  3. data/Gemfile +6 -0
  4. data/Rakefile +9 -6
  5. data/TODO.improvements/01-cbdt-cblc-gid-stable-propagation.md +73 -0
  6. data/TODO.improvements/02-collection-outline-priority.md +48 -0
  7. data/TODO.improvements/03-fontisan-audit-command.md +189 -0
  8. data/TODO.improvements/04-ufo-composite-glyph-encoding.md +46 -0
  9. data/TODO.improvements/05-otf-compiler-real-cff.md +63 -0
  10. data/TODO.improvements/06-cff2-blend-vsindex-operators.md +51 -0
  11. data/TODO.improvements/07-cpal-v1-header-fields.md +71 -0
  12. data/TODO.improvements/08-cff-standard-string-table.md +52 -0
  13. data/TODO.improvements/09-type1-seac-expansion.md +57 -0
  14. data/TODO.improvements/10-ufo-image-set-feature-writers.md +71 -0
  15. data/TODO.improvements/11-kern-groups-plist.md +49 -0
  16. data/TODO.improvements/12-cbdt-fixture-bindata-conversion.md +68 -0
  17. data/TODO.improvements/13-split-octokit-fetcher.md +45 -0
  18. data/TODO.improvements/14-rubocop-baseline-chip.md +62 -0
  19. data/TODO.improvements/README.md +57 -0
  20. data/benchmark/compile_benchmark.rb +13 -5
  21. data/docs/.vitepress/config.ts +1 -0
  22. data/docs/STITCHER_GUIDE.adoc +47 -3
  23. data/docs/api/layer.md +89 -0
  24. data/docs/cli/subset.md +25 -6
  25. data/docs/guide/cli/subset.md +31 -0
  26. data/docs/guide/color-fonts/bitmaps.md +17 -0
  27. data/lib/fontisan/cli.rb +2 -1
  28. data/lib/fontisan/collection/writer.rb +2 -1
  29. data/lib/fontisan/commands/validate_collection_command.rb +7 -1
  30. data/lib/fontisan/commands.rb +2 -1
  31. data/lib/fontisan/converters/collection_converter.rb +6 -1
  32. data/lib/fontisan/converters/svg_generator.rb +3 -1
  33. data/lib/fontisan/stitcher/collection_result.rb +2 -1
  34. data/lib/fontisan/stitcher/glyph_copier.rb +2 -1
  35. data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
  36. data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
  37. data/lib/fontisan/stitcher/source.rb +2 -1
  38. data/lib/fontisan/stitcher.rb +2 -1
  39. data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +2 -1
  40. data/lib/fontisan/svg/standalone_glyph.rb +4 -1
  41. data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
  42. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
  43. data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
  44. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +11 -5
  45. data/lib/fontisan/tables/cff.rb +2 -1
  46. data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
  47. data/lib/fontisan/tables/cpal.rb +112 -11
  48. data/lib/fontisan/tables.rb +2 -1
  49. data/lib/fontisan/ufo/cli.rb +4 -1
  50. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
  51. data/lib/fontisan/ufo/compile/cff.rb +6 -2
  52. data/lib/fontisan/ufo/compile/cmap.rb +11 -5
  53. data/lib/fontisan/ufo/compile/cpal.rb +2 -1
  54. data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
  55. data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
  56. data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
  57. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
  58. data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
  59. data/lib/fontisan/ufo/compile/fvar.rb +7 -3
  60. data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
  61. data/lib/fontisan/ufo/compile/gvar.rb +5 -2
  62. data/lib/fontisan/ufo/compile/head.rb +2 -1
  63. data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
  64. data/lib/fontisan/ufo/compile/name.rb +3 -1
  65. data/lib/fontisan/ufo/compile/sbix.rb +2 -1
  66. data/lib/fontisan/ufo/compile/stat.rb +6 -3
  67. data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
  68. data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
  69. data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
  70. data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
  71. data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
  72. data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
  73. data/lib/fontisan/ufo/convert.rb +4 -1
  74. data/lib/fontisan/ufo/info.rb +4 -2
  75. data/lib/fontisan/ufo/plist.rb +7 -2
  76. data/lib/fontisan/ufo/reader.rb +3 -1
  77. data/lib/fontisan/ufo/writer.rb +13 -5
  78. data/lib/fontisan/version.rb +1 -1
  79. data/lib/fontisan/woff2/collection_decoder.rb +4 -1
  80. data/lib/fontisan/woff2/collection_encoder.rb +4 -1
  81. data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
  82. data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
  83. data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
  84. data/lib/fontisan.rb +0 -1
  85. metadata +18 -4
  86. data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
  87. data/lib/fontisan/tasks.rb +0 -11
@@ -274,14 +274,18 @@ module Fontisan
274
274
  private
275
275
 
276
276
  def group_by_script(cp_map)
277
- cp_map.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(cp, label), h|
277
+ cp_map.each_with_object(Hash.new do |h, k|
278
+ h[k] = []
279
+ end) do |(cp, label), h|
278
280
  script = script_of_codepoint(cp)
279
281
  h[script] << [cp, label]
280
282
  end
281
283
  end
282
284
 
283
285
  def script_of_codepoint(cp)
284
- block_label = ByBlock::BLOCKS.find { |_label, range| range.cover?(cp) }&.first
286
+ block_label = ByBlock::BLOCKS.find do |_label, range|
287
+ range.cover?(cp)
288
+ end&.first
285
289
  return :other unless block_label
286
290
 
287
291
  SCRIPT_OF_BLOCK[block_label] || :other
@@ -321,7 +321,8 @@ module Fontisan
321
321
  compound.components.each do |component|
322
322
  next unless component.args_are_xy?
323
323
 
324
- raw = cache[:glyf].glyph_for(component.glyph_index, cache[:loca], cache[:head])
324
+ raw = cache[:glyf].glyph_for(component.glyph_index, cache[:loca],
325
+ cache[:head])
325
326
  next unless raw
326
327
 
327
328
  matrix = component.transformation_matrix
@@ -150,7 +150,8 @@ module Fontisan
150
150
  Dir.mktmpdir do |dir|
151
151
  sub_path = File.join(dir, "sub#{subfont_name}#{metadata.extension}")
152
152
  metadata.compiler_class.new(target).compile(output_path: sub_path)
153
- cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source, sub_path)
153
+ cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source,
154
+ sub_path)
154
155
 
155
156
  loaded = Fontisan::FontLoader.load(sub_path)
156
157
  stats = SubfontStats.new(
@@ -61,7 +61,8 @@ module Fontisan
61
61
  glyph_data = glyf.raw_data[offset, size]
62
62
 
63
63
  if glyph_data.bytesize >= 10
64
- _n, gx_min, gy_min, gx_max, gy_max = glyph_data[0, 10].unpack("n5")
64
+ _n, gx_min, gy_min, gx_max, gy_max = glyph_data[0,
65
+ 10].unpack("n5")
65
66
  gx_min = to_signed_16(gx_min)
66
67
  gy_min = to_signed_16(gy_min)
67
68
  gx_max = to_signed_16(gx_max)
@@ -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
@@ -40,8 +40,10 @@ module Fontisan
40
40
  # @param num_regions [Integer] number of variation regions
41
41
  # @param width [Integer, nil] advance width
42
42
  # @return [String] CFF2 charstring bytes with blend sequences
43
- def self.build_variable(outline, master_outlines:, num_regions:, width: nil)
44
- new.build_variable_outline(outline, master_outlines, num_regions, width)
43
+ def self.build_variable(outline, master_outlines:, num_regions:,
44
+ width: nil)
45
+ new.build_variable_outline(outline, master_outlines, num_regions,
46
+ width)
45
47
  end
46
48
 
47
49
  # ---------- variable-font encoding ----------
@@ -65,7 +67,9 @@ module Fontisan
65
67
 
66
68
  # Process an outline with blend operators for each varying coordinate.
67
69
  def encode_variable_outline(outline, master_outlines, num_regions)
68
- @master_states = master_outlines.map { MasterState.new(current_x: 0, current_y: 0) }
70
+ @master_states = master_outlines.map do
71
+ MasterState.new(current_x: 0, current_y: 0)
72
+ end
69
73
  @num_regions = num_regions
70
74
  @has_blend = false
71
75
 
@@ -90,7 +94,8 @@ module Fontisan
90
94
  # ---------- variable moveto ----------
91
95
 
92
96
  def encode_variable_moveto(cmd, master_cmds)
93
- dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd, master_cmds)
97
+ dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd,
98
+ master_cmds)
94
99
 
95
100
  write_variable_number(dx, dx_deltas)
96
101
  write_variable_number(dy, dy_deltas)
@@ -102,7 +107,8 @@ module Fontisan
102
107
  # ---------- variable lineto ----------
103
108
 
104
109
  def encode_variable_lineto(cmd, master_cmds)
105
- dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd, master_cmds)
110
+ dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd,
111
+ master_cmds)
106
112
 
107
113
  write_variable_number(dx, dx_deltas)
108
114
  write_variable_number(dy, dy_deltas)
@@ -47,7 +47,8 @@ module Fontisan
47
47
  autoload :Charset, "fontisan/tables/cff/charset"
48
48
  autoload :CharString, "fontisan/tables/cff/charstring"
49
49
  autoload :CharStringBuilder, "fontisan/tables/cff/charstring_builder"
50
- autoload :Cff2CharStringBuilder, "fontisan/tables/cff/cff2_charstring_builder"
50
+ autoload :Cff2CharStringBuilder,
51
+ "fontisan/tables/cff/cff2_charstring_builder"
51
52
  autoload :CharStringParser, "fontisan/tables/cff/charstring_parser"
52
53
  autoload :CharStringRebuilder, "fontisan/tables/cff/charstring_rebuilder"
53
54
  autoload :CharstringsIndex, "fontisan/tables/cff/charstrings_index"
@@ -66,7 +66,8 @@ 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 >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF].pack("CCC")
69
+ when 3 then [(value >> 16) & 0xFF, (value >> 8) & 0xFF,
70
+ value & 0xFF].pack("CCC")
70
71
  when 4 then [value].pack("N")
71
72
  else raise ArgumentError, "invalid off_size: #{off_size}"
72
73
  end
@@ -27,7 +27,10 @@ module Fontisan
27
27
  # - numColorRecords (uint16): Total number of color records
28
28
  # - colorRecordsArrayOffset (uint32): Offset to color records array
29
29
  #
30
- # Version 1 adds optional metadata for palette types and labels.
30
+ # Version 1 Header (24 bytes) adds:
31
+ # - paletteTypesArrayOffset (uint32): Offset to palette types array
32
+ # - paletteLabelsArrayOffset (uint32): Offset to palette labels array
33
+ # - paletteEntryLabelsArrayOffset (uint32): Offset to palette entry labels
31
34
  #
32
35
  # Color Record Structure (4 bytes, BGRA format):
33
36
  # - blue (uint8)
@@ -47,6 +50,13 @@ module Fontisan
47
50
  # OpenType table tag for CPAL
48
51
  TAG = "CPAL"
49
52
 
53
+ # Palette type bit flags (CPAL v1, paletteTypesArray entries).
54
+ PALETTE_TYPE_LIGHT_ON_DARK = 0x01
55
+ PALETTE_TYPE_DARK_ON_LIGHT = 0x02
56
+
57
+ # Offset that means "no table present" in CPAL v1.
58
+ NULL_OFFSET = 0
59
+
50
60
  # @return [Integer] CPAL version (0 or 1)
51
61
  attr_reader :version
52
62
 
@@ -71,6 +81,19 @@ module Fontisan
71
81
  # @return [Array<Hash>] Parsed color records (RGBA hashes)
72
82
  attr_reader :color_records
73
83
 
84
+ # @return [Array<Integer>, nil] Palette type bit flags, one per
85
+ # palette. nil for CPAL v0 or when v1 has no palette types array.
86
+ attr_reader :palette_types
87
+
88
+ # @return [Array<Integer>, nil] Name-record IDs (name table) for
89
+ # each palette. nil for CPAL v0 or when v1 has no labels array.
90
+ attr_reader :palette_labels
91
+
92
+ # @return [Array<Integer>, nil] Name-record IDs (name table) for
93
+ # each palette entry index (shared across palettes). nil for
94
+ # CPAL v0 or when v1 has no entry-labels array.
95
+ attr_reader :palette_entry_labels
96
+
74
97
  # Override read to parse CPAL structure
75
98
  #
76
99
  # @param io [IO, String] Binary data to read
@@ -102,8 +125,8 @@ module Fontisan
102
125
  # Parse color records
103
126
  parse_color_records(io)
104
127
 
105
- # Version 1 features (palette types, labels) not implemented yet
106
- # TODO: Add version 1 features in follow-up task
128
+ # Version 1: palette types + labels (lazily walked via offsets)
129
+ parse_v1_metadata if version == 1
107
130
  rescue StandardError => e
108
131
  raise CorruptedTableError, "Failed to parse CPAL table: #{e.message}"
109
132
  end
@@ -152,6 +175,44 @@ module Fontisan
152
175
  color_record ? color_to_hex(color_record) : nil
153
176
  end
154
177
 
178
+ # Get the name-record ID for a palette's human-readable label.
179
+ #
180
+ # @param palette_index [Integer]
181
+ # @return [Integer, nil] nameID for the palette, or nil if v0 / no labels
182
+ def palette_label(palette_index)
183
+ return nil unless palette_labels
184
+ return nil if palette_index.negative? || palette_index >= palette_labels.length
185
+
186
+ palette_labels[palette_index]
187
+ end
188
+
189
+ # Get the name-record ID for a palette entry's human-readable label.
190
+ #
191
+ # @param entry_index [Integer]
192
+ # @return [Integer, nil] nameID for the entry, or nil if v0 / no labels
193
+ def palette_entry_label(entry_index)
194
+ return nil unless palette_entry_labels
195
+ return nil if entry_index.negative? || entry_index >= palette_entry_labels.length
196
+
197
+ palette_entry_labels[entry_index]
198
+ end
199
+
200
+ # Whether a palette is intended for light-on-dark use (CPAL v1).
201
+ #
202
+ # @param palette_index [Integer]
203
+ # @return [Boolean] true if the palette type bit is set; false otherwise
204
+ def light_on_dark?(palette_index)
205
+ palette_type_set?(palette_index, PALETTE_TYPE_LIGHT_ON_DARK)
206
+ end
207
+
208
+ # Whether a palette is intended for dark-on-light use (CPAL v1).
209
+ #
210
+ # @param palette_index [Integer]
211
+ # @return [Boolean] true if the palette type bit is set; false otherwise
212
+ def dark_on_light?(palette_index)
213
+ palette_type_set?(palette_index, PALETTE_TYPE_DARK_ON_LIGHT)
214
+ end
215
+
155
216
  # Validate the CPAL table structure
156
217
  #
157
218
  # @return [Boolean] True if valid
@@ -169,7 +230,7 @@ module Fontisan
169
230
 
170
231
  private
171
232
 
172
- # Parse CPAL header (12 bytes for version 0, 16 bytes for version 1)
233
+ # Parse CPAL header (12 bytes for v0, 24 bytes for v1)
173
234
  #
174
235
  # @param io [StringIO] Input stream
175
236
  def parse_header(io)
@@ -179,12 +240,14 @@ module Fontisan
179
240
  @num_color_records = io.read(2).unpack1("n")
180
241
  @color_records_array_offset = io.read(4).unpack1("N")
181
242
 
182
- # Version 1 has additional header fields
183
- if version == 1
184
- # TODO: Parse version 1 header fields
185
- # - paletteTypesArrayOffset (uint32)
186
- # - paletteLabelsArrayOffset (uint32)
187
- # - paletteEntryLabelsArrayOffset (uint32)
243
+ # Version 1 has three additional uint32 offset fields. Offsets
244
+ # are read here but the arrays themselves are walked lazily
245
+ # from parse_v1_metadata so the v0 path (most common) doesn't
246
+ # pay for the seeks.
247
+ if @version == 1
248
+ @palette_types_array_offset = io.read(4).unpack1("N")
249
+ @palette_labels_array_offset = io.read(4).unpack1("N")
250
+ @palette_entry_labels_array_offset = io.read(4).unpack1("N")
188
251
  end
189
252
  end
190
253
 
@@ -228,7 +291,7 @@ module Fontisan
228
291
  @palette_indices = []
229
292
  return if num_palettes.zero?
230
293
 
231
- # Palette indices immediately follow header (at offset 12 for v0, 16 for v1)
294
+ # Palette indices immediately follow header (at offset 12 for v0, 24 for v1)
232
295
  # Each index is uint16 (2 bytes)
233
296
  num_palettes.times do
234
297
  index = io.read(2).unpack1("n")
@@ -262,6 +325,44 @@ module Fontisan
262
325
  end
263
326
  end
264
327
 
328
+ # CPAL v1 metadata: palette types + labels + entry labels.
329
+ # Walked lazily via the offsets parsed in parse_header. Any
330
+ # offset of 0 means "no table present" — leave the accessor nil
331
+ # rather than raise.
332
+ def parse_v1_metadata
333
+ @palette_types = walk_uint32_array(@palette_types_array_offset, num_palettes)
334
+ @palette_labels = walk_uint16_array(@palette_labels_array_offset, num_palettes)
335
+ @palette_entry_labels = walk_uint16_array(@palette_entry_labels_array_offset,
336
+ num_palette_entries)
337
+ end
338
+
339
+ def walk_uint32_array(offset, count)
340
+ return nil if offset.nil? || offset == NULL_OFFSET
341
+ return [] if count.zero?
342
+
343
+ bytes = raw_data[offset, count * 4]
344
+ return nil unless bytes && bytes.bytesize == count * 4
345
+
346
+ bytes.unpack("N*")
347
+ end
348
+
349
+ def walk_uint16_array(offset, count)
350
+ return nil if offset.nil? || offset == NULL_OFFSET
351
+ return [] if count.zero?
352
+
353
+ bytes = raw_data[offset, count * 2]
354
+ return nil unless bytes && bytes.bytesize == count * 2
355
+
356
+ bytes.unpack("n*")
357
+ end
358
+
359
+ def palette_type_set?(palette_index, bit)
360
+ return false unless palette_types
361
+ return false if palette_index.negative? || palette_index >= palette_types.length
362
+
363
+ (palette_types[palette_index] & bit).nonzero? ? true : false
364
+ end
365
+
265
366
  # Convert color record to hex string
266
367
  #
267
368
  # @param color [Hash] Color hash with :red, :green, :blue, :alpha keys
@@ -6,7 +6,8 @@ module Fontisan
6
6
  module Tables
7
7
  autoload :CblcBigGlyphMetrics, "fontisan/tables/cblc_big_glyph_metrics"
8
8
  autoload :CblcBitmapSize, "fontisan/tables/cblc_bitmap_size"
9
- autoload :CblcGlyphBitmapLocation, "fontisan/tables/cblc_glyph_bitmap_location"
9
+ autoload :CblcGlyphBitmapLocation,
10
+ "fontisan/tables/cblc_glyph_bitmap_location"
10
11
  autoload :CblcIndexSubTable, "fontisan/tables/cblc_index_subtable"
11
12
  autoload :CblcIndexSubTableArrayEntry,
12
13
  "fontisan/tables/cblc_index_subtable_array_entry"
@@ -74,7 +74,10 @@ module Fontisan
74
74
  def extract(ufo, glyph_name, output)
75
75
  font = Font.open(ufo)
76
76
  glyph = font.glyph(glyph_name)
77
- raise ArgumentError, "glyph not found: #{glyph_name.inspect}" unless glyph
77
+ unless glyph
78
+ raise ArgumentError,
79
+ "glyph not found: #{glyph_name.inspect}"
80
+ end
78
81
 
79
82
  head = font.info
80
83
  renderer = Fontisan::Svg::StandaloneGlyph.new(
@@ -65,7 +65,8 @@ module Fontisan
65
65
  # SbitLineMetrics (12 bytes): height, width, hori/vert bearings
66
66
  def self.build_sbit_line_metrics(ppem, num_glyphs)
67
67
  io = +""
68
- io << [ppem & 0xFF, 0, ppem & 0xFF, 0, 0, 0, ppem & 0xFF, 0, 0, 0, 0].pack("C11")
68
+ io << [ppem & 0xFF, 0, ppem & 0xFF, 0, 0, 0, ppem & 0xFF, 0, 0, 0,
69
+ 0].pack("C11")
69
70
  io << [12 + num_glyphs * 8].pack("n")
70
71
  io
71
72
  end
@@ -155,7 +155,10 @@ module Fontisan
155
155
  return +"" if glyphs.size <= 1
156
156
 
157
157
  bytes = +"\x00"
158
- (1...glyphs.size).each { |_| bytes << [0].pack("n") } # SID 0 placeholder
158
+ # SID 0 placeholder
159
+ (1...glyphs.size).each do |_|
160
+ bytes << [0].pack("n")
161
+ end
159
162
  bytes
160
163
  end
161
164
 
@@ -193,7 +196,8 @@ module Fontisan
193
196
  when 1 then [value].pack("C")
194
197
  when 2 then [value].pack("n")
195
198
  when 3
196
- [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF].pack("CCC")
199
+ [(value >> 16) & 0xFF, (value >> 8) & 0xFF,
200
+ value & 0xFF].pack("CCC")
197
201
  when 4 then [value].pack("N")
198
202
  end
199
203
  end
@@ -36,7 +36,9 @@ module Fontisan
36
36
  end
37
37
  end
38
38
 
39
- subtable_bmp = format4_subtable(mappings.reject { |cp, _| cp > 0xFFFF })
39
+ subtable_bmp = format4_subtable(mappings.reject do |cp, _|
40
+ cp > 0xFFFF
41
+ end)
40
42
  subtable_full = format12_subtable(mappings)
41
43
 
42
44
  header_size = 4 + (2 * 8) # version + numTables + 2 records
@@ -44,8 +46,10 @@ module Fontisan
44
46
  offset_full = header_size + subtable_bmp.bytesize
45
47
 
46
48
  header = [0, 2].pack("nn")
47
- header << subtable_record(PLATFORM_WINDOWS, ENCODING_WINDOWS_BMP, offset_bmp)
48
- header << subtable_record(PLATFORM_WINDOWS, ENCODING_WINDOWS_FULL, offset_full)
49
+ header << subtable_record(PLATFORM_WINDOWS, ENCODING_WINDOWS_BMP,
50
+ offset_bmp)
51
+ header << subtable_record(PLATFORM_WINDOWS, ENCODING_WINDOWS_FULL,
52
+ offset_full)
49
53
  header + subtable_bmp + subtable_full
50
54
  end
51
55
 
@@ -82,7 +86,8 @@ module Fontisan
82
86
  segments = build_segments(mappings) + [0xFFFF..0xFFFF] # sentinel
83
87
  seg_count = segments.size
84
88
  search_range = largest_pow2_le(seg_count) * 2
85
- entry_selector = (Math.log([1, search_range / 2].max) / Math.log(2)).to_i
89
+ entry_selector = (Math.log([1,
90
+ search_range / 2].max) / Math.log(2)).to_i
86
91
  range_shift = seg_count * 2 - search_range
87
92
 
88
93
  end_codes = segments.map(&:end)
@@ -98,7 +103,8 @@ module Fontisan
98
103
  end
99
104
 
100
105
  body = +""
101
- body << [seg_count * 2, search_range, entry_selector, range_shift].pack("nnnn")
106
+ body << [seg_count * 2, search_range, entry_selector,
107
+ range_shift].pack("nnnn")
102
108
  body << end_codes.pack("n*")
103
109
  body << [0].pack("n") # reservedPad
104
110
  body << start_codes.pack("n*")
@@ -38,7 +38,8 @@ module Fontisan
38
38
  offset = HEADER_SIZE + (num_palettes * 2) # header + indices
39
39
 
40
40
  io = +""
41
- io << [VERSION, num_entries, num_palettes, num_records, offset].pack("nnnnN")
41
+ io << [VERSION, num_entries, num_palettes, num_records,
42
+ offset].pack("nnnnN")
42
43
  indices.each { |idx| io << [idx].pack("n") }
43
44
  records.each { |c| io << color_bytes(c) }
44
45
  io
@@ -78,7 +78,9 @@ module Fontisan
78
78
  def heuristic_class(glyph)
79
79
  anchor_names = glyph.anchors.map(&:name)
80
80
  mark_anchor_pattern = /\A_[a-zA-Z]+\z/
81
- return 3 if anchor_names.any? { |n| mark_anchor_pattern.match?(n.to_s) }
81
+ return 3 if anchor_names.any? do |n|
82
+ mark_anchor_pattern.match?(n.to_s)
83
+ end
82
84
  return 2 if glyph.name.to_s.end_with?(".liga")
83
85
 
84
86
  nil
@@ -28,7 +28,9 @@ module Fontisan
28
28
 
29
29
  attachments[class_name] ||= new_class_entry
30
30
  attachments[class_name][:marks][mark_name] = mark_anchor
31
- bases.each { |n, a| attachments[class_name][base_bucket_key][n] = a }
31
+ bases.each do |n, a|
32
+ attachments[class_name][base_bucket_key][n] = a
33
+ end
32
34
  end
33
35
  end
34
36
 
@@ -16,7 +16,8 @@ module Fontisan
16
16
  # entry. No edits to the compiler.
17
17
  module FeatureWriters
18
18
  autoload :Base, "fontisan/ufo/compile/feature_writers/base"
19
- autoload :MarkFamilyBase, "fontisan/ufo/compile/feature_writers/mark_family_base"
19
+ autoload :MarkFamilyBase,
20
+ "fontisan/ufo/compile/feature_writers/mark_family_base"
20
21
  autoload :Kern, "fontisan/ufo/compile/feature_writers/kern"
21
22
  autoload :Kern2, "fontisan/ufo/compile/feature_writers/kern2"
22
23
  autoload :Gdef, "fontisan/ufo/compile/feature_writers/gdef"
@@ -134,8 +134,10 @@ module Fontisan
134
134
  right_c2 = Point.new(x: r2x, y: r2y, type: "offcurve")
135
135
  right_p3 = p3
136
136
 
137
- left = subdivide_cubic(left_p0, left_c1, left_c2, left_p3, tolerance)
138
- right = subdivide_cubic(right_p0, right_c1, right_c2, right_p3, tolerance)
137
+ left = subdivide_cubic(left_p0, left_c1, left_c2, left_p3,
138
+ tolerance)
139
+ right = subdivide_cubic(right_p0, right_c1, right_c2, right_p3,
140
+ tolerance)
139
141
 
140
142
  # Drop the duplicated midpoint point between halves
141
143
  left + right
@@ -56,7 +56,8 @@ module Fontisan
56
56
  next if i == j || drop.include?(j)
57
57
 
58
58
  # Drop inner when its bbox is fully inside outer's.
59
- if contained_in?(bboxes[j], bboxes[i]) && (inner.points.size <= outer.points.size)
59
+ if contained_in?(bboxes[j],
60
+ bboxes[i]) && (inner.points.size <= outer.points.size)
60
61
  # Sanity: only drop if the point count is smaller —
61
62
  # a fully-contained contour with MORE points is
62
63
  # almost certainly the outer one in a malformed
@@ -28,7 +28,8 @@ module Fontisan
28
28
 
29
29
  axes_bytes = build_axes(axes)
30
30
  instances_bytes = build_instances(instances || [], axes.size)
31
- header = build_header(axes.size, axes_bytes, instances_bytes, instances)
31
+ header = build_header(axes.size, axes_bytes, instances_bytes,
32
+ instances)
32
33
  header + axes_bytes + instances_bytes
33
34
  end
34
35
 
@@ -38,7 +39,8 @@ module Fontisan
38
39
  # majorVersion (2) + minorVersion (2) + axesArrayOffset (2)
39
40
  # + reserved (2) + axisCount (2) + axisSize (2)
40
41
  # + instanceCount (2) + instanceSize (2)
41
- def self.build_header(axis_count, axes_bytes, _instances_bytes, instances)
42
+ def self.build_header(axis_count, axes_bytes, _instances_bytes,
43
+ instances)
42
44
  axes_offset = 16
43
45
  axes_offset + axes_bytes.bytesize
44
46
 
@@ -86,7 +88,9 @@ module Fontisan
86
88
  name_id = inst[:name_id] || inst["name_id"] || 0
87
89
  flags = inst[:flags] || inst["flags"] || 0
88
90
  coords = inst[:coords] || inst["coords"] || []
89
- padded = coords.first(axis_count) + Array.new([axis_count - coords.size, 0].max, 0)
91
+ padded = coords.first(axis_count) + Array.new(
92
+ [axis_count - coords.size, 0].max, 0
93
+ )
90
94
  padded = padded.first(axis_count).map { |c| fixed_value(c) }
91
95
 
92
96
  io << [name_id, flags].pack("nn")
@@ -41,7 +41,8 @@ module Fontisan
41
41
  offsets.map { |o| o / 2 }.pack("n*")
42
42
  end
43
43
 
44
- { "glyf" => glyf_bytes, "loca" => loca_bytes, :loca_format => use_long ? 1 : 0 }
44
+ { "glyf" => glyf_bytes, "loca" => loca_bytes,
45
+ :loca_format => use_long ? 1 : 0 }
45
46
  end
46
47
 
47
48
  # Encode a single glyph into glyf bytes. Empty glyphs (no
@@ -138,7 +139,8 @@ module Fontisan
138
139
  # nothing).
139
140
  ""
140
141
  end
141
- private_class_method :encode_glyph, :encode_simple, :encode_points, :encode_composite
142
+ private_class_method :encode_glyph, :encode_simple, :encode_points,
143
+ :encode_composite
142
144
  end
143
145
  end
144
146
  end
@@ -121,7 +121,9 @@ module Fontisan
121
121
  deltas = tuple[:deltas]
122
122
 
123
123
  # Encode delta data
124
- all_fit_int8 = deltas.all? { |dx, dy| dx.between?(-127, 127) && dy.between?(-127, 127) }
124
+ all_fit_int8 = deltas.all? do |dx, dy|
125
+ dx.between?(-127, 127) && dy.between?(-127, 127)
126
+ end
125
127
 
126
128
  point_count = deltas.size
127
129
  flags = point_count & POINT_COUNT_MASK
@@ -195,7 +197,8 @@ module Fontisan
195
197
 
196
198
  def self.build_empty(glyph_count)
197
199
  # Minimal gvar with no variation data
198
- header = [VERSION_MAJOR, VERSION_MINOR, 0, 0, 0, glyph_count, 1, glyph_count + 16].pack("nnnnnnNN")
200
+ header = [VERSION_MAJOR, VERSION_MINOR, 0, 0, 0, glyph_count, 1,
201
+ glyph_count + 16].pack("nnnnnnNN")
199
202
  offsets = Array.new(glyph_count + 1, 0).pack("C*")
200
203
  header + offsets
201
204
  end
@@ -19,7 +19,8 @@ module Fontisan
19
19
  # @param glyphs [Array<Fontisan::Ufo::Glyph>] (unused here;
20
20
  # bbox is computed by the caller for performance)
21
21
  # @return [Fontisan::Tables::Head]
22
- def self.build(font, glyphs:, units_per_em: nil, loca_format: LOCA_FORMAT_LONG)
22
+ def self.build(font, glyphs:, units_per_em: nil,
23
+ loca_format: LOCA_FORMAT_LONG)
23
24
  bbox = font_bbox(font, glyphs)
24
25
  Fontisan::Tables::Head.new(
25
26
  version_raw: 0x00010000,