fontisan 0.4.23 → 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 (71) 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/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/converters/collection_converter.rb +6 -1
  17. data/lib/fontisan/converters/svg_generator.rb +3 -1
  18. data/lib/fontisan/stitcher/collection_result.rb +2 -1
  19. data/lib/fontisan/stitcher/glyph_copier.rb +2 -1
  20. data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
  21. data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
  22. data/lib/fontisan/stitcher/source.rb +2 -1
  23. data/lib/fontisan/stitcher.rb +2 -1
  24. data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +2 -1
  25. data/lib/fontisan/svg/standalone_glyph.rb +4 -1
  26. data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
  27. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
  28. data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
  29. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +11 -5
  30. data/lib/fontisan/tables/cff.rb +2 -1
  31. data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
  32. data/lib/fontisan/tables.rb +2 -1
  33. data/lib/fontisan/ufo/cli.rb +4 -1
  34. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
  35. data/lib/fontisan/ufo/compile/cff.rb +6 -2
  36. data/lib/fontisan/ufo/compile/cmap.rb +11 -5
  37. data/lib/fontisan/ufo/compile/cpal.rb +2 -1
  38. data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
  39. data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
  40. data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
  41. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
  42. data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
  43. data/lib/fontisan/ufo/compile/fvar.rb +7 -3
  44. data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
  45. data/lib/fontisan/ufo/compile/gvar.rb +5 -2
  46. data/lib/fontisan/ufo/compile/head.rb +2 -1
  47. data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
  48. data/lib/fontisan/ufo/compile/name.rb +3 -1
  49. data/lib/fontisan/ufo/compile/sbix.rb +2 -1
  50. data/lib/fontisan/ufo/compile/stat.rb +6 -3
  51. data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
  52. data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
  53. data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
  54. data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
  55. data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
  56. data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
  57. data/lib/fontisan/ufo/convert.rb +4 -1
  58. data/lib/fontisan/ufo/info.rb +4 -2
  59. data/lib/fontisan/ufo/plist.rb +7 -2
  60. data/lib/fontisan/ufo/reader.rb +3 -1
  61. data/lib/fontisan/ufo/writer.rb +13 -5
  62. data/lib/fontisan/version.rb +1 -1
  63. data/lib/fontisan/woff2/collection_decoder.rb +4 -1
  64. data/lib/fontisan/woff2/collection_encoder.rb +4 -1
  65. data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
  66. data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
  67. data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
  68. data/lib/fontisan.rb +0 -1
  69. metadata +2 -3
  70. data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
  71. data/lib/fontisan/tasks.rb +0 -11
data/docs/api/layer.md ADDED
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: Ufo::Layer
3
+ ---
4
+
5
+ # `Ufo::Layer`
6
+
7
+ A single layer in a UFO source. A `Layer` holds a set of glyphs keyed by name. The default layer is `public.default` per UFO 3.
8
+
9
+ ```ruby
10
+ layer = Fontisan::Ufo::Layer.new # => "public.default"
11
+ layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
12
+ layer["A"] # => the glyph
13
+ layer.size # => 1
14
+ ```
15
+
16
+ ## Naming contract
17
+
18
+ Glyph names are the layer's primary key. Two distinct glyphs with the same name cannot coexist — adding the second would silently destroy the first, a class of bug that has historically cost real data (e.g. the CJK Ext G cmap loss when CBDT placeholders collided with outline glyphs sharing `"gid{N}"` names).
19
+
20
+ To make that contract unbreakable, the API exposes two methods with named semantics:
21
+
22
+ | Method | Behavior on conflict |
23
+ |--------|----------------------|
24
+ | `Layer#add(glyph)` | **Raises** `Ufo::GlyphExistsError` (the safe default) |
25
+ | `Layer#put(glyph)` | Overwrites the existing glyph (explicit replace) |
26
+
27
+ Callers that need auto-renaming — insert without giving up on a collision — call `Stitcher::UniqueGlyphName.in(target, base)` first to allocate a free name, then `Layer#add` the glyph under that name.
28
+
29
+ ## API
30
+
31
+ ### `Layer.new(name = "public.default")`
32
+
33
+ Initialize an empty layer.
34
+
35
+ ### `#add(glyph) -> Glyph`
36
+
37
+ Insert `glyph`. Raises `Ufo::GlyphExistsError` if a glyph with the same name is already present.
38
+
39
+ ```ruby
40
+ layer = Fontisan::Ufo::Layer.new
41
+ layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
42
+
43
+ layer.add(Fontisan::Ufo::Glyph.new(name: "A"))
44
+ # => raises Fontisan::Ufo::GlyphExistsError: a glyph named "A" already exists
45
+ ```
46
+
47
+ The original glyph is preserved when the second `add` raises — the layer is never partially mutated.
48
+
49
+ ### `#put(glyph) -> Glyph`
50
+
51
+ Insert `glyph`, replacing any existing glyph with the same name. Use this when the caller has positively decided the previous glyph (if any) should be discarded.
52
+
53
+ ```ruby
54
+ layer.put(Fontisan::Ufo::Glyph.new(name: "A")) # overwrites the prior "A"
55
+ ```
56
+
57
+ ### `#[](glyph_name) -> Glyph, nil`
58
+
59
+ Look up a glyph by name.
60
+
61
+ ### `#each { |glyph| ... }`
62
+
63
+ Iterate over glyphs (no ordering guarantee).
64
+
65
+ ### `#size -> Integer`
66
+
67
+ Number of glyphs in the layer.
68
+
69
+ ## `Ufo::GlyphExistsError`
70
+
71
+ Raised by `Layer#add` on a name conflict. Carries the colliding name so callers can branch programmatically:
72
+
73
+ ```ruby
74
+ begin
75
+ layer.add(glyph)
76
+ rescue Fontisan::Ufo::GlyphExistsError => e
77
+ puts "name #{e.name.inspect} already taken"
78
+ # Decide: layer.put(glyph) to overwrite, or
79
+ # unique = Fontisan::Stitcher::UniqueGlyphName.in(target, e.name)
80
+ # layer.add(glyph.dup_as(name: unique))
81
+ end
82
+ ```
83
+
84
+ The error class is a sibling of `Layer` (not nested inside it) so the namespace stays flat — both `Ufo::Layer` and `Ufo::GlyphExistsError` live directly under `Fontisan::Ufo`.
85
+
86
+ ## See also
87
+
88
+ - `docs/STITCHER_GUIDE.adoc` — `UniqueGlyphName`, `GlyphCopier`, and `CbdtPropagator` all build on the Layer naming contract.
89
+ - `docs/UFO_COMPILATION.adoc` — how layers feed the TTF/OTF/CFF2 compilers.
data/docs/cli/subset.md CHANGED
@@ -18,8 +18,23 @@ fontisan subset <font> [options]
18
18
  |--------|-------------|
19
19
  | `--chars TEXT` | Characters to include |
20
20
  | `--file FILE` | File containing characters |
21
+ | `--unicodes RANGE` | Unicode ranges (`"U+0000-007F"`) |
22
+ | `--glyphs LIST` | Glyph names/IDs |
21
23
  | `--output FILE` | Output file path |
22
- | `--format FORMAT` | Output format |
24
+ | `--format FORMAT` | Output format (`ttf`, `woff`, `woff2`, …) |
25
+ | `--profile PROFILE` | Subsetting profile (default: `pdf`) |
26
+ | `--retain-gids` | Preserve original glyph IDs |
27
+
28
+ ## Profiles
29
+
30
+ | Profile | Tables included | Use case |
31
+ |---------|-----------------|----------|
32
+ | `pdf` | core + glyf/loca | PDF embedding (smallest) |
33
+ | `web` | core + glyf/loca + GSUB/GPOS + **CBDT/CBLC** | Web fonts that retain color-emoji bitmaps |
34
+ | `minimal` | core only | Smallest valid font |
35
+ | `full` | every standard table | Lossless re-export |
36
+
37
+ The `web` profile is the one to reach for when the subset must still render color emoji in a browser: it preserves the CBDT (bitmap data) and CBLC (bitmap location) tables, and the paired subsetter rewrites them to keep only the bitmaps for the retained glyphs.
23
38
 
24
39
  ## Examples
25
40
 
@@ -27,13 +42,17 @@ fontisan subset <font> [options]
27
42
  # Subset to specific characters
28
43
  fontisan subset font.ttf --chars "ABCDEF" --output subset.ttf
29
44
 
30
- # Subset from file
31
- fontisan subset font.ttf --file chars.txt --output subset.ttf
45
+ # Web subset that retains color-emoji bitmaps
46
+ fontisan subset NotoColorEmoji.ttf \
47
+ --chars "😀😁😂🤣😊" \
48
+ --profile web \
49
+ --format woff2 \
50
+ --output emoji.woff2
32
51
 
33
- # Subset and convert format
34
- fontisan subset font.ttf --chars "Hello" --format woff2
52
+ # PDF embedding profile (drops CBDT/CBLC, smaller output)
53
+ fontisan subset font.ttf --chars "ABC" --profile pdf --output pdf-subset.ttf
35
54
  ```
36
55
 
37
56
  ## Detailed Documentation
38
57
 
39
- For comprehensive documentation including unicode ranges and advanced subsetting, see the [subset command guide](/guide/cli/subset).
58
+ For comprehensive documentation including unicode ranges, profile selection, and CBDT/CBLC behavior, see the [subset command guide](/guide/cli/subset).
@@ -28,6 +28,7 @@ fontisan subset FONT [options]
28
28
  | `--glyphs LIST` | Glyph names/IDs |
29
29
  | `--output PATH` | Output file |
30
30
  | `--output-format FORMAT` | Output format |
31
+ | `--profile PROFILE` | Subsetting profile (`pdf`, `web`, `minimal`, `full`; default `pdf`) |
31
32
  | `--retain-gids` | Retain glyph IDs |
32
33
 
33
34
  ## Examples
@@ -86,6 +87,36 @@ fontisan subset font.ttf --chars "ABC" --output-format woff2 --output subset.wof
86
87
 
87
88
  ## Subsetting Strategies
88
89
 
90
+ ### Profile selection
91
+
92
+ The `--profile` option controls which font tables the subset retains. The right choice depends on what the subset is for:
93
+
94
+ | Profile | When to use | Notable tables dropped |
95
+ |---------|-------------|------------------------|
96
+ | `pdf` (default) | PDF embedding, smallest valid output | GSUB, GPOS, CBDT, CBLC |
97
+ | `web` | Browser/web font, **retains color-emoji bitmaps** | nothing that browsers need |
98
+ | `minimal` | Absolute smallest core that still renders | glyf/loca, CBDT/CBLC, GSUB/GPOS |
99
+ | `full` | Lossless re-export | (none) |
100
+
101
+ ```bash
102
+ # Web subset that keeps color-emoji bitmaps so 😀 renders in color
103
+ fontisan subset NotoColorEmoji.ttf \
104
+ --chars "😀😁😂🤣😊" \
105
+ --profile web \
106
+ --output-format woff2 \
107
+ --output emoji.woff2
108
+
109
+ # PDF embedding — drops color bitmaps for size
110
+ fontisan subset NotoColorEmoji.ttf \
111
+ --chars "😀" \
112
+ --profile pdf \
113
+ --output emoji-pdf.ttf
114
+ ```
115
+
116
+ #### Web profile and CBDT/CBLC
117
+
118
+ The `web` profile is the only built-in profile that retains CBDT (Color Bitmap Data) and CBLC (Color Bitmap Location) tables. The subsetter walks CBLC to find each retained glyph's bitmap block, rewrites CBDT to keep only those blocks, and rebuilds CBLC's IndexSubTableArray to point at the new offsets. Glyph IDs are remapped through the subset's `GlyphMapping`, so the output CBLC references subset GIDs, not source GIDs.
119
+
89
120
  ### Web Font Optimization
90
121
 
91
122
  ```bash
@@ -144,6 +144,23 @@ fontisan convert bitmap-font.ttf --to ttf --output bitmap-font.ttf
144
144
  fontisan convert bitmap-font.ttf --to ttf --no-bitmaps --output outline-only.ttf
145
145
  ```
146
146
 
147
+ ### Subsetting color-emoji fonts
148
+
149
+ The `web` subsetting profile retains CBDT and CBLC tables and rewrites them to keep only the bitmaps for retained glyphs. Use it when subsetting a color-emoji source for browser delivery:
150
+
151
+ ```bash
152
+ # Keep only 😀 + 😁 bitmaps; output WOFF2 for web
153
+ fontisan subset NotoColorEmoji.ttf \
154
+ --chars "😀😁" \
155
+ --profile web \
156
+ --output-format woff2 \
157
+ --output emoji-subset.woff2
158
+ ```
159
+
160
+ Other profiles (`pdf`, `minimal`, `full`) drop CBDT/CBLC — pick them when the subset doesn't need color-emoji rendering (e.g. PDF embedding where the subsetter is just selecting glyphs for outlines).
161
+
162
+ See the [subset CLI docs](/cli/subset) and `docs/STITCHER_GUIDE.adoc` (CBDT/CBLC passthrough section) for the full story, including the GID-stability caveat when CBDT and outline donors cover overlapping codepoint ranges in stitch mode.
163
+
147
164
  ## Comparison
148
165
 
149
166
  | Feature | sbix | CBDT/CBLC |
data/lib/fontisan/cli.rb CHANGED
@@ -360,7 +360,8 @@ module Fontisan
360
360
 
361
361
  # Handle --show-options
362
362
  if options[:show_options]
363
- show_recommended_options(source_format, options[:to].is_a?(Array) ? options[:to].first : options[:to])
363
+ show_recommended_options(source_format,
364
+ options[:to].is_a?(Array) ? options[:to].first : options[:to])
364
365
  return
365
366
  end
366
367
 
@@ -95,7 +95,8 @@ module Fontisan
95
95
  #
96
96
  # @return [String] TTC header binary
97
97
  def write_ttc_header
98
- [TTC_TAG, VERSION_1_0_MAJOR, VERSION_1_0_MINOR, @fonts.size].pack("a4nnN")
98
+ [TTC_TAG, VERSION_1_0_MAJOR, VERSION_1_0_MINOR,
99
+ @fonts.size].pack("a4nnN")
99
100
  end
100
101
 
101
102
  # Write offset table
@@ -81,7 +81,13 @@ module Fontisan
81
81
  Check.new(
82
82
  name: :glyph_cap,
83
83
  passed: over.empty?,
84
- message: over.empty? ? nil : "faces over cap: #{over.map { |s| "##{s.index}=#{s.glyph_count}" }.join(', ')}",
84
+ message: if over.empty?
85
+ nil
86
+ else
87
+ "faces over cap: #{over.map do |s|
88
+ "##{s.index}=#{s.glyph_count}"
89
+ end.join(', ')}"
90
+ end,
85
91
  )
86
92
  end
87
93
 
@@ -21,7 +21,8 @@ module Fontisan
21
21
  autoload :TablesCommand, "fontisan/commands/tables_command"
22
22
  autoload :UnicodeCommand, "fontisan/commands/unicode_command"
23
23
  autoload :UnpackCommand, "fontisan/commands/unpack_command"
24
- autoload :ValidateCollectionCommand, "fontisan/commands/validate_collection_command"
24
+ autoload :ValidateCollectionCommand,
25
+ "fontisan/commands/validate_collection_command"
25
26
  autoload :ValidateCommand, "fontisan/commands/validate_command"
26
27
  autoload :VariableCommand, "fontisan/commands/variable_command"
27
28
  end
@@ -173,7 +173,12 @@ module Fontisan
173
173
  io.read(8) # signature + flavor
174
174
  io.rewind
175
175
  flavor = File.binread(path, 4, 4)&.unpack1("N")
176
- flavor == Woff2::CollectionDecoder::TTC_FLAVOR ? :woff2_collection : (raise Error, "Not a WOFF2 collection: #{path}")
176
+ if flavor == Woff2::CollectionDecoder::TTC_FLAVOR
177
+ :woff2_collection
178
+ else
179
+ (raise Error,
180
+ "Not a WOFF2 collection: #{path}")
181
+ end
177
182
  elsif Parsers::DfontParser.dfont?(io)
178
183
  :dfont
179
184
  else
@@ -192,7 +192,9 @@ palette_index: 0)
192
192
  def build_glyph_to_codepoints(cmap)
193
193
  return {} unless cmap
194
194
 
195
- cmap.unicode_mappings.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(cp, gid), h|
195
+ cmap.unicode_mappings.each_with_object(Hash.new do |h, k|
196
+ h[k] = []
197
+ end) do |(cp, gid), h|
196
198
  h[gid] << cp
197
199
  end
198
200
  rescue StandardError => e
@@ -9,7 +9,8 @@ module Fontisan
9
9
 
10
10
  # Return value of {Stitcher#write_collection}. Carries the output path,
11
11
  # total bytes, and one {SubfontStats} per declared subfont.
12
- CollectionResult = Struct.new(:path, :bytes, :subfonts, keyword_init: true) do
12
+ CollectionResult = Struct.new(:path, :bytes, :subfonts,
13
+ keyword_init: true) do
13
14
  def face_count
14
15
  subfonts.size
15
16
  end
@@ -89,7 +89,8 @@ module Fontisan
89
89
  bindings.sort_by { |b| [b[:codepoint] || Float::INFINITY, b[:donor_gid]] }
90
90
  end
91
91
 
92
- def copy_glyph_into(target_font, name:, source:, donor_gid:, codepoint: nil)
92
+ def copy_glyph_into(target_font, name:, source:, donor_gid:,
93
+ codepoint: nil)
93
94
  original = source.glyph_for_gid(donor_gid)
94
95
  return unless original
95
96
 
@@ -403,7 +403,9 @@ module Fontisan
403
403
  private
404
404
 
405
405
  def group_by_block(cp_map)
406
- cp_map.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(cp, label), h|
406
+ cp_map.each_with_object(Hash.new do |h, k|
407
+ h[k] = []
408
+ end) do |(cp, label), h|
407
409
  block_label = find_block_label(cp) || :other
408
410
  h[block_label] << [cp, label]
409
411
  end
@@ -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
@@ -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