fontisan 0.4.6 → 0.4.7

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 (51) 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 +106 -0
  6. data/README.adoc +121 -68
  7. data/benchmark/compile_benchmark.rb +70 -0
  8. data/docs/CFF2_SUPPORT.adoc +184 -0
  9. data/docs/STITCHER_GUIDE.adoc +151 -0
  10. data/docs/SVG_TO_GLYF.adoc +118 -0
  11. data/docs/UFO_COMPILATION.adoc +119 -0
  12. data/lib/fontisan/collection/writer.rb +5 -6
  13. data/lib/fontisan/error.rb +31 -0
  14. data/lib/fontisan/stitcher/deduplicator.rb +47 -0
  15. data/lib/fontisan/stitcher/glyph_limit.rb +53 -0
  16. data/lib/fontisan/stitcher/glyph_signature.rb +51 -0
  17. data/lib/fontisan/stitcher.rb +188 -167
  18. data/lib/fontisan/svg_to_glyf/assembler.rb +132 -0
  19. data/lib/fontisan/svg_to_glyf/document.rb +83 -0
  20. data/lib/fontisan/svg_to_glyf/geometry/affine_transform.rb +112 -0
  21. data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +45 -0
  22. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +91 -0
  23. data/lib/fontisan/svg_to_glyf/geometry.rb +13 -0
  24. data/lib/fontisan/svg_to_glyf/path/command.rb +18 -0
  25. data/lib/fontisan/svg_to_glyf/path/contour_builder.rb +140 -0
  26. data/lib/fontisan/svg_to_glyf/path/parser.rb +98 -0
  27. data/lib/fontisan/svg_to_glyf/path/state.rb +79 -0
  28. data/lib/fontisan/svg_to_glyf/path.rb +14 -0
  29. data/lib/fontisan/svg_to_glyf.rb +62 -0
  30. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +216 -0
  31. data/lib/fontisan/tables/cff.rb +1 -0
  32. data/lib/fontisan/tables/cff2/dict_encoder.rb +94 -0
  33. data/lib/fontisan/tables/cff2/fd_select.rb +69 -0
  34. data/lib/fontisan/tables/cff2/header.rb +34 -0
  35. data/lib/fontisan/tables/cff2/index_builder.rb +79 -0
  36. data/lib/fontisan/tables/cff2.rb +4 -0
  37. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +103 -0
  38. data/lib/fontisan/ufo/compile/cff2.rb +181 -0
  39. data/lib/fontisan/ufo/compile/cff2_subroutines.rb +39 -0
  40. data/lib/fontisan/ufo/compile/colr.rb +80 -0
  41. data/lib/fontisan/ufo/compile/cpal.rb +61 -0
  42. data/lib/fontisan/ufo/compile/math.rb +143 -0
  43. data/lib/fontisan/ufo/compile/meta.rb +51 -0
  44. data/lib/fontisan/ufo/compile/otf2_compiler.rb +46 -0
  45. data/lib/fontisan/ufo/compile/sbix.rb +99 -0
  46. data/lib/fontisan/ufo/compile/svg_table.rb +60 -0
  47. data/lib/fontisan/ufo/compile/variable_otf.rb +75 -0
  48. data/lib/fontisan/ufo/compile.rb +11 -0
  49. data/lib/fontisan/version.rb +1 -1
  50. data/lib/fontisan.rb +3 -0
  51. metadata +41 -2
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `sbix` (Apple Bitmap) table.
7
+ #
8
+ # sbix stores bitmap strikes (PNG/JPEG) at multiple sizes. Each
9
+ # strike is a set of glyph-to-bitmap mappings at a specific ppem
10
+ # and resolution.
11
+ #
12
+ # Layout:
13
+ # Header (8 bytes):
14
+ # uint16 version (= 1)
15
+ # uint16 flags
16
+ # uint32 numStrikes
17
+ # Offset32 strikeOffsets[numStrikes]
18
+ #
19
+ # Strike (per ppem):
20
+ # uint16 ppem
21
+ # uint16 resolution (ppi)
22
+ # Offset32 glyphDataOffsets[numGlyphs + 1]
23
+ #
24
+ # Glyph Data (per glyph):
25
+ # int16 originOffsetX
26
+ # int16 originOffsetY
27
+ # uint16 graphicType ("png " or "jpeg")
28
+ # uint8 bitmapData[]
29
+ #
30
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/sbix
31
+ module Sbix
32
+ VERSION = 1
33
+ PNG_TYPE = "png "
34
+ JPEG_TYPE = "jpeg"
35
+
36
+ # @param strikes [Array<Hash>] each with :ppem, :resolution,
37
+ # and :glyphs (Array<Hash> with :origin_x, :origin_y,
38
+ # :graphic_type, :data per glyph)
39
+ # @param num_glyphs [Integer] total glyph count
40
+ # @return [String, nil] sbix bytes, or nil if no strikes
41
+ def self.build(strikes:, num_glyphs:)
42
+ return nil if strikes.nil? || strikes.empty?
43
+
44
+ num_strikes = strikes.size
45
+ header_size = 4 + 4 + (num_strikes * 4) # version + flags + offsets
46
+
47
+ strike_bytes = strikes.map { |s| build_strike(s, num_glyphs) }
48
+
49
+ io = +""
50
+ io << [VERSION, 0, num_strikes].pack("nnN")
51
+
52
+ offset = header_size
53
+ strike_bytes.each do |s|
54
+ io << [offset].pack("N")
55
+ offset += s.bytesize
56
+ io << s
57
+ end
58
+ io
59
+ end
60
+
61
+ def self.build_strike(strike, num_glyphs)
62
+ glyphs = strike[:glyphs] || []
63
+ ppem = strike[:ppem] || 0
64
+ resolution = strike[:resolution] || 72
65
+
66
+ # Offset table: ppem(2) + resolution(2) + (numGlyphs+1) × offset(4)
67
+ offset_table_size = 4 + ((num_glyphs + 1) * 4)
68
+
69
+ offsets = []
70
+ glyph_data = +""
71
+ current_offset = offset_table_size
72
+
73
+ num_glyphs.times do |gid|
74
+ glyph = glyphs[gid]
75
+ if glyph && glyph[:data]
76
+ offsets << current_offset
77
+ data = glyph[:data].b
78
+ glyph_data << [glyph[:origin_x] || 0, glyph[:origin_y] || 0].pack("nn")
79
+ glyph_data << (glyph[:graphic_type] || PNG_TYPE).ljust(4)[0, 4]
80
+ glyph_data << data
81
+ current_offset += 8 + data.bytesize
82
+ else
83
+ offsets << current_offset
84
+ end
85
+ end
86
+ offsets << current_offset # sentinel
87
+
88
+ io = +""
89
+ io << [ppem, resolution].pack("nn")
90
+ offsets.each { |o| io << [o].pack("N") }
91
+ io << glyph_data
92
+ io
93
+ end
94
+
95
+ private_class_method :build_strike
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `SVG ` table for SVG-in-OpenType color glyphs.
7
+ #
8
+ # Layout:
9
+ # Header (10 bytes):
10
+ # uint16 version (= 0)
11
+ # Offset32 documentListOffset (= 10, immediately after header)
12
+ # uint32 reserved (= 0)
13
+ #
14
+ # Document List:
15
+ # uint16 numEntries
16
+ # DocumentRecord[numEntries]:
17
+ # uint16 startGlyphID
18
+ # uint16 endGlyphID
19
+ # Offset32 svgDocOffset (from start of SVG table)
20
+ # uint32 svgDocLength
21
+ #
22
+ # SVG Documents (raw XML, optionally gzipped)
23
+ #
24
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/svg
25
+ module SvgTable
26
+ VERSION = 0
27
+ HEADER_SIZE = 10
28
+ DOCUMENT_RECORD_SIZE = 12
29
+
30
+ # @param entries [Array<Hash>] each with :start_gid (Integer),
31
+ # :end_gid (Integer), :svg (String — raw SVG XML)
32
+ # @return [String, nil] SVG table bytes, or nil if no entries
33
+ def self.build(entries:)
34
+ return nil if entries.nil? || entries.empty?
35
+
36
+ num_entries = entries.size
37
+ doc_list_size = 2 + (num_entries * DOCUMENT_RECORD_SIZE)
38
+ doc_data_offset = HEADER_SIZE + doc_list_size
39
+
40
+ header = [VERSION, HEADER_SIZE, 0].pack("nNN")
41
+
42
+ records = +""
43
+ docs = +""
44
+ offset = doc_data_offset
45
+ entries.each do |entry|
46
+ svg_bytes = entry[:svg].b
47
+ records << [entry[:start_gid], entry[:end_gid],
48
+ offset, svg_bytes.bytesize].pack("nnNN")
49
+ docs << svg_bytes
50
+ offset += svg_bytes.bytesize
51
+ end
52
+
53
+ doc_list = [num_entries].pack("n") + records
54
+
55
+ header + doc_list + docs
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Fontisan
6
+ module Ufo
7
+ module Compile
8
+ # VariableOtf — produces a variable OTF with CFF2 outlines.
9
+ #
10
+ # The orchestrator assembles a default-master UFO plus variation
11
+ # masters into a variable OpenType font with:
12
+ # - CFF2 outlines (static — blend/vsindex operators are TODO 07/18)
13
+ # - fvar (axes + instances)
14
+ # - STAT (style attributes)
15
+ # - avar (axis remapping, when maps are provided)
16
+ #
17
+ # NOTE: the CFF2 table currently contains static outlines (no
18
+ # VariationStore, no blend operators). Full variable CFF2 requires
19
+ # TODO 07 (blend/vsindex) and TODO 18 (blend integration) to be
20
+ # wired into the charstring builder.
21
+ #
22
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/cff2
23
+ class VariableOtf
24
+ # @param default_font [Fontisan::Ufo::Font] the default master
25
+ # @param master_fonts [Array<Fontisan::Ufo::Font>] variation masters
26
+ # @param axes [Array<Hash>] fvar axes (tag, min, default, max, name_id)
27
+ # @param instances [Array<Hash>] named instances
28
+ def initialize(default_font, master_fonts: [], axes: [], instances: [])
29
+ @default = default_font
30
+ @masters = master_fonts
31
+ @axes = axes
32
+ @instances = instances
33
+ end
34
+
35
+ # Compile the variable OTF directly to the output path.
36
+ # @param output_path [String] output file path
37
+ # @return [String] the output path
38
+ def compile(output_path:)
39
+ tables = build_tables
40
+ Fontisan::FontWriter.write_to_file(
41
+ tables.transform_values { |t| t.is_a?(String) ? t : t.to_binary_s },
42
+ output_path,
43
+ sfnt_version: 0x4F54544F,
44
+ )
45
+ output_path
46
+ end
47
+
48
+ # Build all tables for the variable OTF.
49
+ # @return [Hash<String,String>] table tag → bytes
50
+ def build_tables
51
+ glyphs = @default.glyphs.values
52
+
53
+ tables = {
54
+ "head" => Head.build(@default, glyphs: glyphs, loca_format: Head::LOCA_FORMAT_LONG),
55
+ "hhea" => Hhea.build(@default, glyphs: glyphs),
56
+ "maxp" => Maxp.build(@default, glyphs: glyphs, version: Maxp::VERSION_OPEN_TYPE),
57
+ "OS/2" => Os2.build(@default, glyphs: glyphs),
58
+ "name" => Name.build(@default),
59
+ "post" => Post.build(@default),
60
+ "hmtx" => Hmtx.build(@default, glyphs: glyphs),
61
+ "cmap" => Cmap.build(@default, glyphs: glyphs),
62
+ "CFF2" => Cff2.build(@default, glyphs: glyphs),
63
+ "fvar" => Fvar.build(@default, axes: @axes, instances: @instances),
64
+ "STAT" => Stat.build(axes: @axes),
65
+ }
66
+
67
+ avar_bytes = Avar.build(axes: @axes)
68
+ tables["avar"] = avar_bytes if avar_bytes
69
+
70
+ tables
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -46,6 +46,17 @@ module Fontisan
46
46
  autoload :Cmap, "fontisan/ufo/compile/cmap"
47
47
  autoload :GlyfLoca, "fontisan/ufo/compile/glyf_loca"
48
48
  autoload :Cff, "fontisan/ufo/compile/cff"
49
+ autoload :Cff2, "fontisan/ufo/compile/cff2"
50
+ autoload :Otf2Compiler, "fontisan/ufo/compile/otf2_compiler"
51
+ autoload :Cpal, "fontisan/ufo/compile/cpal"
52
+ autoload :Meta, "fontisan/ufo/compile/meta"
53
+ autoload :SvgTable, "fontisan/ufo/compile/svg_table"
54
+ autoload :Sbix, "fontisan/ufo/compile/sbix"
55
+ autoload :MathTable, "fontisan/ufo/compile/math"
56
+ autoload :CbdtCblc, "fontisan/ufo/compile/cbdt_cblc"
57
+ autoload :Colr, "fontisan/ufo/compile/colr"
58
+ autoload :Cff2Subrs, "fontisan/ufo/compile/cff2_subroutines"
59
+ autoload :VariableOtf, "fontisan/ufo/compile/variable_otf"
49
60
  autoload :Avar, "fontisan/ufo/compile/avar"
50
61
  autoload :Hvar, "fontisan/ufo/compile/hvar"
51
62
  autoload :Mvar, "fontisan/ufo/compile/mvar"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.6"
4
+ VERSION = "0.4.7"
5
5
  end
data/lib/fontisan.rb CHANGED
@@ -70,6 +70,8 @@ module Fontisan
70
70
  autoload :CorruptedVariationDataError, "fontisan/error"
71
71
  autoload :InvalidVariationDataError, "fontisan/error"
72
72
  autoload :VariationDataCorruptedError, "fontisan/error"
73
+ autoload :MultipleCbdtSourcesError, "fontisan/error"
74
+ autoload :GlyphLimitExceededError, "fontisan/error"
73
75
 
74
76
  # Namespace hubs (each hub declares its own child autoloads)
75
77
  autoload :Binary, "fontisan/binary"
@@ -86,6 +88,7 @@ module Fontisan
86
88
  autoload :Pipeline, "fontisan/pipeline"
87
89
  autoload :Subset, "fontisan/subset"
88
90
  autoload :Svg, "fontisan/svg"
91
+ autoload :SvgToGlyf, "fontisan/svg_to_glyf"
89
92
  autoload :Tables, "fontisan/tables"
90
93
  autoload :Type1, "fontisan/type1"
91
94
  autoload :Ufo, "fontisan/ufo"
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.6
4
+ version: 0.4.7
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-01 00:00:00.000000000 Z
11
+ date: 2026-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -125,11 +125,15 @@ files:
125
125
  - ".rspec"
126
126
  - ".rubocop.yml"
127
127
  - ".rubocop_todo.yml"
128
+ - BUG-stitcher-drops-isolated-cps.md
129
+ - BUG-stitcher-drops-plane1-codepoints.md
130
+ - BUG-stitcher-gid-cap-65535.md
128
131
  - CHANGELOG.md
129
132
  - Gemfile
130
133
  - LICENSE
131
134
  - README.adoc
132
135
  - Rakefile
136
+ - benchmark/compile_benchmark.rb
133
137
  - benchmark/variation_quick_bench.rb
134
138
  - docs/.gitignore
135
139
  - docs/.vitepress/config.ts
@@ -141,12 +145,16 @@ files:
141
145
  - docs/.vitepress/theme/index.ts
142
146
  - docs/.vitepress/theme/style.css
143
147
  - docs/APPLE_LEGACY_FONTS.adoc
148
+ - docs/CFF2_SUPPORT.adoc
144
149
  - docs/COLLECTION_VALIDATION.adoc
145
150
  - docs/COLOR_FONTS.adoc
146
151
  - docs/CONVERSION_GUIDE.adoc
147
152
  - docs/EXTRACT_TTC_MIGRATION.md
148
153
  - docs/FONT_HINTING.adoc
154
+ - docs/STITCHER_GUIDE.adoc
155
+ - docs/SVG_TO_GLYF.adoc
149
156
  - docs/TYPE1_FONTS.adoc
157
+ - docs/UFO_COMPILATION.adoc
150
158
  - docs/VALIDATION.adoc
151
159
  - docs/VARIABLE_FONT_OPERATIONS.adoc
152
160
  - docs/WOFF_WOFF2_FORMATS.adoc
@@ -436,6 +444,9 @@ files:
436
444
  - lib/fontisan/sfnt_font.rb
437
445
  - lib/fontisan/sfnt_table.rb
438
446
  - lib/fontisan/stitcher.rb
447
+ - lib/fontisan/stitcher/deduplicator.rb
448
+ - lib/fontisan/stitcher/glyph_limit.rb
449
+ - lib/fontisan/stitcher/glyph_signature.rb
439
450
  - lib/fontisan/stitcher/selector.rb
440
451
  - lib/fontisan/stitcher/selector/codepoints.rb
441
452
  - lib/fontisan/stitcher/selector/gid.rb
@@ -453,10 +464,23 @@ files:
453
464
  - lib/fontisan/svg/font_generator.rb
454
465
  - lib/fontisan/svg/glyph_generator.rb
455
466
  - lib/fontisan/svg/view_box_calculator.rb
467
+ - lib/fontisan/svg_to_glyf.rb
468
+ - lib/fontisan/svg_to_glyf/assembler.rb
469
+ - lib/fontisan/svg_to_glyf/document.rb
470
+ - lib/fontisan/svg_to_glyf/geometry.rb
471
+ - lib/fontisan/svg_to_glyf/geometry/affine_transform.rb
472
+ - lib/fontisan/svg_to_glyf/geometry/normalizer.rb
473
+ - lib/fontisan/svg_to_glyf/geometry/transform_parser.rb
474
+ - lib/fontisan/svg_to_glyf/path.rb
475
+ - lib/fontisan/svg_to_glyf/path/command.rb
476
+ - lib/fontisan/svg_to_glyf/path/contour_builder.rb
477
+ - lib/fontisan/svg_to_glyf/path/parser.rb
478
+ - lib/fontisan/svg_to_glyf/path/state.rb
456
479
  - lib/fontisan/tables.rb
457
480
  - lib/fontisan/tables/cbdt.rb
458
481
  - lib/fontisan/tables/cblc.rb
459
482
  - lib/fontisan/tables/cff.rb
483
+ - lib/fontisan/tables/cff/cff2_charstring_builder.rb
460
484
  - lib/fontisan/tables/cff/cff_glyph.rb
461
485
  - lib/fontisan/tables/cff/charset.rb
462
486
  - lib/fontisan/tables/cff/charstring.rb
@@ -479,6 +503,10 @@ files:
479
503
  - lib/fontisan/tables/cff2.rb
480
504
  - lib/fontisan/tables/cff2/blend_operator.rb
481
505
  - lib/fontisan/tables/cff2/charstring_parser.rb
506
+ - lib/fontisan/tables/cff2/dict_encoder.rb
507
+ - lib/fontisan/tables/cff2/fd_select.rb
508
+ - lib/fontisan/tables/cff2/header.rb
509
+ - lib/fontisan/tables/cff2/index_builder.rb
482
510
  - lib/fontisan/tables/cff2/operand_stack.rb
483
511
  - lib/fontisan/tables/cff2/private_dict_blend_handler.rb
484
512
  - lib/fontisan/tables/cff2/region_matcher.rb
@@ -557,8 +585,13 @@ files:
557
585
  - lib/fontisan/ufo/compile.rb
558
586
  - lib/fontisan/ufo/compile/avar.rb
559
587
  - lib/fontisan/ufo/compile/base_compiler.rb
588
+ - lib/fontisan/ufo/compile/cbdt_cblc.rb
560
589
  - lib/fontisan/ufo/compile/cff.rb
590
+ - lib/fontisan/ufo/compile/cff2.rb
591
+ - lib/fontisan/ufo/compile/cff2_subroutines.rb
561
592
  - lib/fontisan/ufo/compile/cmap.rb
593
+ - lib/fontisan/ufo/compile/colr.rb
594
+ - lib/fontisan/ufo/compile/cpal.rb
562
595
  - lib/fontisan/ufo/compile/filters.rb
563
596
  - lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb
564
597
  - lib/fontisan/ufo/compile/filters/decompose_components.rb
@@ -573,14 +606,20 @@ files:
573
606
  - lib/fontisan/ufo/compile/hmtx.rb
574
607
  - lib/fontisan/ufo/compile/hvar.rb
575
608
  - lib/fontisan/ufo/compile/item_variation_store.rb
609
+ - lib/fontisan/ufo/compile/math.rb
576
610
  - lib/fontisan/ufo/compile/maxp.rb
611
+ - lib/fontisan/ufo/compile/meta.rb
577
612
  - lib/fontisan/ufo/compile/mvar.rb
578
613
  - lib/fontisan/ufo/compile/name.rb
579
614
  - lib/fontisan/ufo/compile/os2.rb
615
+ - lib/fontisan/ufo/compile/otf2_compiler.rb
580
616
  - lib/fontisan/ufo/compile/otf_compiler.rb
581
617
  - lib/fontisan/ufo/compile/post.rb
618
+ - lib/fontisan/ufo/compile/sbix.rb
582
619
  - lib/fontisan/ufo/compile/stat.rb
620
+ - lib/fontisan/ufo/compile/svg_table.rb
583
621
  - lib/fontisan/ufo/compile/ttf_compiler.rb
622
+ - lib/fontisan/ufo/compile/variable_otf.rb
584
623
  - lib/fontisan/ufo/compile/variable_ttf.rb
585
624
  - lib/fontisan/ufo/component.rb
586
625
  - lib/fontisan/ufo/contour.rb