fontisan 0.4.6 → 0.4.8

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/BUG-stitcher-drops-isolated-cps.md +58 -0
  3. data/BUG-stitcher-drops-plane1-codepoints.md +310 -0
  4. data/BUG-stitcher-gid-cap-65535.md +110 -0
  5. data/CHANGELOG.md +119 -0
  6. data/README.adoc +121 -68
  7. data/benchmark/compile_benchmark.rb +70 -0
  8. data/docs/.vitepress/config.ts +0 -7
  9. data/docs/CFF2_SUPPORT.adoc +184 -0
  10. data/docs/STITCHER_GUIDE.adoc +151 -0
  11. data/docs/SVG_TO_GLYF.adoc +118 -0
  12. data/docs/UFO_COMPILATION.adoc +119 -0
  13. data/docs/cli/index.md +5 -28
  14. data/docs/index.md +0 -2
  15. data/lib/fontisan/cli.rb +29 -8
  16. data/lib/fontisan/collection/reader/stats.rb +23 -0
  17. data/lib/fontisan/collection/reader.rb +90 -0
  18. data/lib/fontisan/collection/writer.rb +5 -6
  19. data/lib/fontisan/collection.rb +1 -0
  20. data/lib/fontisan/commands/convert_command.rb +96 -18
  21. data/lib/fontisan/commands/multi_format_output.rb +59 -0
  22. data/lib/fontisan/commands/validate_collection_command.rb +121 -0
  23. data/lib/fontisan/commands.rb +2 -0
  24. data/lib/fontisan/error.rb +56 -0
  25. data/lib/fontisan/models.rb +0 -1
  26. data/lib/fontisan/stitcher/collection_result.rb +18 -0
  27. data/lib/fontisan/stitcher/deduplicator.rb +47 -0
  28. data/lib/fontisan/stitcher/glyph_limit.rb +53 -0
  29. data/lib/fontisan/stitcher/glyph_signature.rb +51 -0
  30. data/lib/fontisan/stitcher/partition_strategy/base.rb +23 -0
  31. data/lib/fontisan/stitcher/partition_strategy/blueprint.rb +24 -0
  32. data/lib/fontisan/stitcher/partition_strategy/by_plane.rb +131 -0
  33. data/lib/fontisan/stitcher/partition_strategy/partition.rb +24 -0
  34. data/lib/fontisan/stitcher/partition_strategy.rb +22 -0
  35. data/lib/fontisan/stitcher.rb +223 -168
  36. data/lib/fontisan/svg_to_glyf/assembler.rb +132 -0
  37. data/lib/fontisan/svg_to_glyf/document.rb +83 -0
  38. data/lib/fontisan/svg_to_glyf/geometry/affine_transform.rb +112 -0
  39. data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +45 -0
  40. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +91 -0
  41. data/lib/fontisan/svg_to_glyf/geometry.rb +13 -0
  42. data/lib/fontisan/svg_to_glyf/path/command.rb +18 -0
  43. data/lib/fontisan/svg_to_glyf/path/contour_builder.rb +140 -0
  44. data/lib/fontisan/svg_to_glyf/path/parser.rb +98 -0
  45. data/lib/fontisan/svg_to_glyf/path/state.rb +79 -0
  46. data/lib/fontisan/svg_to_glyf/path.rb +14 -0
  47. data/lib/fontisan/svg_to_glyf.rb +62 -0
  48. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +216 -0
  49. data/lib/fontisan/tables/cff.rb +1 -0
  50. data/lib/fontisan/tables/cff2/dict_encoder.rb +94 -0
  51. data/lib/fontisan/tables/cff2/fd_select.rb +69 -0
  52. data/lib/fontisan/tables/cff2/header.rb +34 -0
  53. data/lib/fontisan/tables/cff2/index_builder.rb +79 -0
  54. data/lib/fontisan/tables/cff2.rb +4 -0
  55. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +103 -0
  56. data/lib/fontisan/ufo/compile/cff2.rb +181 -0
  57. data/lib/fontisan/ufo/compile/cff2_subroutines.rb +39 -0
  58. data/lib/fontisan/ufo/compile/colr.rb +80 -0
  59. data/lib/fontisan/ufo/compile/cpal.rb +61 -0
  60. data/lib/fontisan/ufo/compile/math.rb +143 -0
  61. data/lib/fontisan/ufo/compile/meta.rb +51 -0
  62. data/lib/fontisan/ufo/compile/name.rb +2 -2
  63. data/lib/fontisan/ufo/compile/otf2_compiler.rb +46 -0
  64. data/lib/fontisan/ufo/compile/sbix.rb +99 -0
  65. data/lib/fontisan/ufo/compile/svg_table.rb +60 -0
  66. data/lib/fontisan/ufo/compile/variable_otf.rb +75 -0
  67. data/lib/fontisan/ufo/compile.rb +11 -0
  68. data/lib/fontisan/ufo/info.rb +48 -0
  69. data/lib/fontisan/unicode/plane.rb +56 -0
  70. data/lib/fontisan/unicode.rb +17 -0
  71. data/lib/fontisan/version.rb +1 -1
  72. data/lib/fontisan.rb +5 -2
  73. metadata +53 -19
  74. data/docs/cli/audit.md +0 -337
  75. data/lib/fontisan/cldr/aggregator.rb +0 -33
  76. data/lib/fontisan/cldr/cache_manager.rb +0 -110
  77. data/lib/fontisan/cldr/config.rb +0 -59
  78. data/lib/fontisan/cldr/download_error.rb +0 -9
  79. data/lib/fontisan/cldr/downloader.rb +0 -79
  80. data/lib/fontisan/cldr/error.rb +0 -8
  81. data/lib/fontisan/cldr/index.rb +0 -64
  82. data/lib/fontisan/cldr/index_builder.rb +0 -72
  83. data/lib/fontisan/cldr/unicode_set_parser.rb +0 -189
  84. data/lib/fontisan/cldr/unknown_version_error.rb +0 -9
  85. data/lib/fontisan/cldr/version_resolver.rb +0 -91
  86. data/lib/fontisan/cldr.rb +0 -23
  87. data/lib/fontisan/cli/cldr_cli.rb +0 -85
  88. data/lib/fontisan/config/cldr.yml +0 -22
  89. data/lib/fontisan/models/cldr/language_coverage.rb +0 -31
  90. data/lib/fontisan/models/cldr.rb +0 -12
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ class Stitcher
5
+ module PartitionStrategy
6
+ # The result of partitioning a codepoint set: an ordered list of
7
+ # {Partition}s. Applying a blueprint to a Stitcher declares one
8
+ # subfont per partition.
9
+ Blueprint = Struct.new(:partitions, keyword_init: true) do
10
+ # @param stitcher [Fontisan::Stitcher]
11
+ # @return [Array<Symbol>] names of subfonts declared
12
+ def apply_to(stitcher)
13
+ partitions.each { |p| p.apply_to(stitcher) }
14
+ partitions.map(&:name)
15
+ end
16
+
17
+ # @return [Array<Symbol>]
18
+ def names
19
+ partitions.map(&:name)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ class Stitcher
5
+ module PartitionStrategy
6
+ # Partition codepoints by Unicode plane (BMP, SMP, SIP, …).
7
+ #
8
+ # For each plane with codepoints:
9
+ # - if the count fits under +cap+, emit one partition named
10
+ # +:plane_<n>+ (e.g. +:plane_0+, +:plane_2+);
11
+ # - otherwise sub-split using the large CJK extension block
12
+ # boundaries, naming partitions +:plane_<n>_a+, +:plane_<n>_b+,
13
+ # and so on.
14
+ #
15
+ # If a single CJK extension block alone exceeds +cap+, raises
16
+ # {Fontisan::GlyphLimitExceededError} — the partitioner cannot
17
+ # satisfy the cap and the caller must use a smaller cap, split
18
+ # manually, or switch to a format with a higher glyph limit.
19
+ class ByPlane < Base
20
+ # @param cp_map [Hash{Integer=>Object}] codepoint → donor label
21
+ # @param cap [Integer] max codepoints per partition
22
+ # @return [Blueprint]
23
+ def call(cp_map, cap: DEFAULT_CAP)
24
+ grouped = group_by_plane(cp_map)
25
+ partitions = grouped.flat_map do |plane_num, entries|
26
+ build_partitions_for_plane(plane_num, entries, cap)
27
+ end
28
+ Blueprint.new(partitions: partitions)
29
+ end
30
+
31
+ private
32
+
33
+ def group_by_plane(cp_map)
34
+ cp_map.group_by { |cp, _label| Fontisan::Unicode::Plane.of(cp) }
35
+ end
36
+
37
+ def build_partitions_for_plane(plane_num, entries, cap)
38
+ if entries.size <= cap
39
+ return [single_partition(plane_num, entries)]
40
+ end
41
+
42
+ sub_split_by_block(plane_num, entries, cap)
43
+ end
44
+
45
+ def single_partition(plane_num, entries)
46
+ Partition.new(
47
+ name: :"plane_#{plane_num}",
48
+ cps: entries.map(&:first),
49
+ donor_map: entries.to_h,
50
+ )
51
+ end
52
+
53
+ # When a plane overflows +cap+, carve it along the large CJK
54
+ # extension block boundaries. The +:other+ bucket (everything
55
+ # outside the known mega-blocks) is split into chunks of +cap+,
56
+ # while each known large block becomes one partition atomically —
57
+ # if a single block alone exceeds +cap+, we cannot sub-split it
58
+ # (its codepoints are contiguous and we don't have finer-grained
59
+ # boundaries to use), so raise.
60
+ def sub_split_by_block(plane_num, entries, cap)
61
+ buckets = bucket_by_large_block(entries)
62
+ partitions = []
63
+ suffix = "a"
64
+
65
+ buckets.each do |label, bucket_entries|
66
+ if large_block?(label) && bucket_entries.size > cap
67
+ raise PartitionCapExceededError.new(
68
+ block_label: label,
69
+ actual: bucket_entries.size,
70
+ cap: cap,
71
+ )
72
+ end
73
+
74
+ chunks(bucket_entries, cap).each do |chunk|
75
+ partitions << Partition.new(
76
+ name: :"plane_#{plane_num}_#{suffix}",
77
+ cps: chunk.map(&:first),
78
+ donor_map: chunk.to_h,
79
+ )
80
+ suffix = suffix.succ
81
+ end
82
+ end
83
+ partitions
84
+ end
85
+
86
+ def large_block?(label)
87
+ Fontisan::Unicode::Plane::LARGE_CJK_BLOCKS.key?(label)
88
+ end
89
+
90
+ # Split +entries+ into sub-arrays of at most +cap+ each.
91
+ def chunks(entries, cap)
92
+ entries.each_slice(cap).to_a
93
+ end
94
+
95
+ # Bucket entries by which large CJK block (or :other) they fall
96
+ # into. Entries outside any known large block go into :other,
97
+ # which is then packed into its own partition(s).
98
+ def bucket_by_large_block(entries)
99
+ buckets = { other: [] }
100
+ Fontisan::Unicode::Plane::LARGE_CJK_BLOCKS.each_key do |label|
101
+ buckets[label] = []
102
+ end
103
+
104
+ entries.each do |cp, label|
105
+ block = find_large_block(cp)
106
+ if block
107
+ buckets[block] << [cp, label]
108
+ else
109
+ buckets[:other] << [cp, label]
110
+ end
111
+ end
112
+
113
+ # Drop empty buckets; preserve order (other first if non-empty,
114
+ # then CJK blocks in declaration order).
115
+ ordered = []
116
+ ordered << [:other, buckets[:other]] unless buckets[:other].empty?
117
+ Fontisan::Unicode::Plane::LARGE_CJK_BLOCKS.each_key do |label|
118
+ ordered << [label, buckets[label]] unless buckets[label].empty?
119
+ end
120
+ ordered
121
+ end
122
+
123
+ def find_large_block(codepoint)
124
+ Fontisan::Unicode::Plane::LARGE_CJK_BLOCKS.find do |_label, range|
125
+ range.include?(codepoint)
126
+ end&.first
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ class Stitcher
5
+ module PartitionStrategy
6
+ # One slice of a partitioned codepoint set. Each partition names
7
+ # a target subfont and the codepoints + donor mapping that should
8
+ # land in it.
9
+ #
10
+ # {#apply_to} pushes the partition's bindings into a Stitcher via
11
+ # +include_codepoints_map+ (TODO 68). If that API is unavailable
12
+ # (older Stitcher), it falls back to one +include_codepoints+
13
+ # call per donor — but the preferred path is the map API.
14
+ Partition = Struct.new(:name, :cps, :donor_map, keyword_init: true) do
15
+ # @param stitcher [Fontisan::Stitcher]
16
+ # @return [void]
17
+ def apply_to(stitcher)
18
+ slice = cps.to_h { |cp| [cp, donor_map[cp]] }
19
+ stitcher.include_codepoints_map(slice, into: name)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ class Stitcher
5
+ # Namespace for codepoint partitioners that split a codepoint set
6
+ # across named subfonts while respecting the format's glyph cap.
7
+ #
8
+ # A partitioner is a strategy object with a single entry point
9
+ # (`.partition`) that takes a +{codepoint => donor}+ map and returns
10
+ # a {Blueprint}. The blueprint is then applied to a Stitcher via
11
+ # {Blueprint#apply_to}.
12
+ #
13
+ # Adding a new partitioner = adding a new file + a new entry here.
14
+ # No edits to existing partitioners required (open/closed).
15
+ module PartitionStrategy
16
+ autoload :Base, "fontisan/stitcher/partition_strategy/base"
17
+ autoload :Blueprint, "fontisan/stitcher/partition_strategy/blueprint"
18
+ autoload :Partition, "fontisan/stitcher/partition_strategy/partition"
19
+ autoload :ByPlane, "fontisan/stitcher/partition_strategy/by_plane"
20
+ end
21
+ end
22
+ end