fontisan 0.4.10 → 0.4.12

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +27 -1
  3. data/Rakefile +53 -39
  4. data/docs/AFDKO_MIGRATION.adoc +175 -0
  5. data/docs/FEATURE_PARITY.adoc +263 -0
  6. data/docs/STITCHER_GUIDE.adoc +100 -6
  7. data/docs/TTX.adoc +176 -0
  8. data/docs/UFO_COMPILATION.adoc +281 -3
  9. data/lib/fontisan/stitcher/partition_strategy/by_block.rb +429 -0
  10. data/lib/fontisan/stitcher/partition_strategy/by_script.rb +305 -0
  11. data/lib/fontisan/stitcher/partition_strategy.rb +2 -0
  12. data/lib/fontisan/stitcher/source.rb +11 -7
  13. data/lib/fontisan/subset/table_subsetter.rb +227 -11
  14. data/lib/fontisan/svg/standalone_glyph.rb +129 -0
  15. data/lib/fontisan/svg.rb +1 -0
  16. data/lib/fontisan/tasks/fixture_downloader.rb +162 -0
  17. data/lib/fontisan/tasks.rb +11 -0
  18. data/lib/fontisan/ufo/cli.rb +34 -21
  19. data/lib/fontisan/ufo/compile/feature_writers/base.rb +56 -0
  20. data/lib/fontisan/ufo/compile/feature_writers/curs.rb +72 -0
  21. data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +90 -0
  22. data/lib/fontisan/ufo/compile/feature_writers/kern.rb +51 -0
  23. data/lib/fontisan/ufo/compile/feature_writers/kern2.rb +57 -0
  24. data/lib/fontisan/ufo/compile/feature_writers/mark.rb +45 -0
  25. data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +122 -0
  26. data/lib/fontisan/ufo/compile/feature_writers/mkmk.rb +53 -0
  27. data/lib/fontisan/ufo/compile/feature_writers.rb +37 -0
  28. data/lib/fontisan/ufo/compile/filters/propagate_anchors.rb +102 -0
  29. data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +94 -0
  30. data/lib/fontisan/ufo/compile/filters/sort_contours.rb +69 -0
  31. data/lib/fontisan/ufo/compile/filters/transformations.rb +113 -0
  32. data/lib/fontisan/ufo/compile/filters.rb +12 -0
  33. data/lib/fontisan/ufo/compile.rb +1 -0
  34. data/lib/fontisan/ufo/convert/to_dfont.rb +41 -0
  35. data/lib/fontisan/ufo/convert/to_otc.rb +37 -0
  36. data/lib/fontisan/ufo/convert/to_otf.rb +18 -0
  37. data/lib/fontisan/ufo/convert/to_otf2.rb +20 -0
  38. data/lib/fontisan/ufo/convert/to_postscript.rb +59 -0
  39. data/lib/fontisan/ufo/convert/to_ttc.rb +35 -0
  40. data/lib/fontisan/ufo/convert/to_ttf.rb +21 -0
  41. data/lib/fontisan/ufo/convert/to_woff.rb +56 -0
  42. data/lib/fontisan/ufo/convert/to_woff2.rb +45 -0
  43. data/lib/fontisan/ufo/convert.rb +68 -5
  44. data/lib/fontisan/ufo/transformation.rb +11 -0
  45. data/lib/fontisan/version.rb +1 -1
  46. data/lib/fontisan.rb +1 -0
  47. metadata +32 -2
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ module Filters
7
+ # Applies a 2×3 affine transformation to every glyph's
8
+ # contours + components. The general-purpose filter — other
9
+ # filters (decompose_components, propagate_anchors) are
10
+ # special cases of geometric transforms applied to specific
11
+ # glyph subsets.
12
+ #
13
+ # Used for:
14
+ # - Symmetric mirroring (e.g. italic→upright flip).
15
+ # - Slant adjustment.
16
+ # - Custom per-font offsets that callers want baked in at
17
+ # compile time rather than carried as variation axis deltas.
18
+ #
19
+ # Identity matrix is a no-op fast path.
20
+ module Transformations
21
+ DEFAULT_OFFSET_X = 0.0
22
+ DEFAULT_OFFSET_Y = 0.0
23
+
24
+ # @param glyphs [Array<Fontisan::Ufo::Glyph>]
25
+ # @param matrix [Fontisan::Ufo::Transformation, Array<Numeric>, nil]
26
+ # The affine matrix to apply. Accepts either a
27
+ # +Transformation+ value object or the raw +[a, b, c, d, e, f]+
28
+ # array. +nil+ (the default) is a no-op — leaves glyphs
29
+ # unchanged.
30
+ # @return [Array<Fontisan::Ufo::Glyph>] the same array (mutated)
31
+ def self.run(glyphs, matrix: nil, **_opts)
32
+ return glyphs if matrix.nil?
33
+
34
+ transform = coerce_transformation(matrix)
35
+ return glyphs if transform.identity?
36
+
37
+ glyphs.each { |g| apply_to_glyph(g, transform) }
38
+ glyphs
39
+ end
40
+
41
+ class << self
42
+ private
43
+
44
+ # Coerce the matrix argument into a Transformation.
45
+ # Centralizes the "array, object, or nil?" decision so
46
+ # callers can pass any form.
47
+ def coerce_transformation(matrix)
48
+ return Fontisan::Ufo::Transformation.new if matrix.nil?
49
+ return matrix if matrix.is_a?(Fontisan::Ufo::Transformation)
50
+
51
+ Fontisan::Ufo::Transformation.new(
52
+ a: matrix[0], b: matrix[1],
53
+ c: matrix[2], d: matrix[3],
54
+ e: matrix[4], f: matrix[5]
55
+ )
56
+ end
57
+
58
+ # Apply the transform in place: each contour's points are
59
+ # rewritten with transformed coordinates; each component's
60
+ # +transformation+ is composed with the new one (so nested
61
+ # component refs chain correctly).
62
+ def apply_to_glyph(glyph, transform)
63
+ glyph.contours.each do |contour|
64
+ contour.points = contour.points.map do |pt|
65
+ tx, ty = transform.apply(pt.x, pt.y)
66
+ Fontisan::Ufo::Point.new(
67
+ x: tx, y: ty,
68
+ type: pt.type, smooth: pt.smooth
69
+ )
70
+ end
71
+ end
72
+
73
+ glyph.components.map! do |component|
74
+ Fontisan::Ufo::Component.new(
75
+ base_glyph: component.base_glyph,
76
+ # Composition order: outer transform ∘ existing.
77
+ # For a base-glyph point P, the component places it
78
+ # at T_existing(P) in this glyph; the outer transform
79
+ # then maps that to T_outer(T_existing(P)). So the
80
+ # new component transformation is T_outer ∘ T_existing.
81
+ transformation: compose(transform, component.transformation),
82
+ identifier: component.identifier,
83
+ )
84
+ end
85
+ end
86
+
87
+ # Compose two affine transformations: result = outer ∘ inner
88
+ # | outer.a outer.c outer.e | | inner.a inner.c inner.e |
89
+ # | outer.b outer.d outer.f | × | inner.b inner.d inner.f |
90
+ # | 0 0 1 | | 0 0 1 |
91
+ #
92
+ # Each input can be a Transformation, a 6-array, or nil
93
+ # (treated as identity — the default Component state).
94
+ # The result is always a Transformation.
95
+ def compose(outer, inner)
96
+ outer_t = coerce_transformation(outer)
97
+ inner_t = coerce_transformation(inner)
98
+
99
+ Fontisan::Ufo::Transformation.new(
100
+ a: outer_t.a * inner_t.a + outer_t.c * inner_t.b,
101
+ b: outer_t.b * inner_t.a + outer_t.d * inner_t.b,
102
+ c: outer_t.a * inner_t.c + outer_t.c * inner_t.d,
103
+ d: outer_t.b * inner_t.c + outer_t.d * inner_t.d,
104
+ e: outer_t.a * inner_t.e + outer_t.c * inner_t.f + outer_t.e,
105
+ f: outer_t.b * inner_t.e + outer_t.d * inner_t.f + outer_t.f,
106
+ )
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -20,6 +20,14 @@ module Fontisan
20
20
  "fontisan/ufo/compile/filters/decompose_components"
21
21
  autoload :FlattenComponents,
22
22
  "fontisan/ufo/compile/filters/flatten_components"
23
+ autoload :Transformations,
24
+ "fontisan/ufo/compile/filters/transformations"
25
+ autoload :SortContours,
26
+ "fontisan/ufo/compile/filters/sort_contours"
27
+ autoload :PropagateAnchors,
28
+ "fontisan/ufo/compile/filters/propagate_anchors"
29
+ autoload :RemoveOverlaps,
30
+ "fontisan/ufo/compile/filters/remove_overlaps"
23
31
 
24
32
  # Filters that MUST run for TTF output (TrueType only
25
33
  # supports quadratic curves + clockwise outer winding).
@@ -38,6 +46,10 @@ module Fontisan
38
46
  cubic_to_quadratic: CubicToQuadratic,
39
47
  decompose_components: DecomposeComponents,
40
48
  flatten_components: FlattenComponents,
49
+ transformations: Transformations,
50
+ sort_contours: SortContours,
51
+ propagate_anchors: PropagateAnchors,
52
+ remove_overlaps: RemoveOverlaps,
41
53
  }.freeze
42
54
 
43
55
  # @param names [Array<Symbol>] filter names from REGISTRY
@@ -33,6 +33,7 @@ module Fontisan
33
33
  autoload :TtfCompiler, "fontisan/ufo/compile/ttf_compiler"
34
34
  autoload :OtfCompiler, "fontisan/ufo/compile/otf_compiler"
35
35
  autoload :Filters, "fontisan/ufo/compile/filters"
36
+ autoload :FeatureWriters, "fontisan/ufo/compile/feature_writers"
36
37
  autoload :Fvar, "fontisan/ufo/compile/fvar"
37
38
  autoload :Gpos, "fontisan/ufo/compile/gpos"
38
39
  autoload :Gvar, "fontisan/ufo/compile/gvar"
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Fontisan
6
+ module Ufo
7
+ module Convert
8
+ # UFO → dfont (Mac OS resource-fork font container). Two-step:
9
+ #
10
+ # 1. Compile UFO → TTF/OTF in tmpdir.
11
+ # 2. Load + feed to Collection::DfontBuilder, which wraps the
12
+ # SFNT binary in a Mac resource fork.
13
+ module ToDfont
14
+ # @param ufo [Fontisan::Ufo::Font]
15
+ # @param output_path [String]
16
+ # @param compiler [Symbol] :ttf (default) or :otf
17
+ # @return [String] the output_path
18
+ def self.convert(ufo, output_path:, compiler: :ttf, **_opts)
19
+ compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
20
+ raise ArgumentError, "unknown intermediate compiler: #{compiler.inspect}" unless compiler_class
21
+
22
+ Dir.mktmpdir do |dir|
23
+ intermediate_path = File.join(dir, "intermediate#{ext_for(compiler)}")
24
+ compiler_class.new(ufo).compile(output_path: intermediate_path)
25
+
26
+ loaded = Fontisan::FontLoader.load(intermediate_path)
27
+ result = Fontisan::Collection::DfontBuilder.new([loaded]).build
28
+ File.binwrite(output_path, result[:binary])
29
+ end
30
+
31
+ output_path
32
+ end
33
+
34
+ def self.ext_for(compiler)
35
+ %i[otf otf2].include?(compiler) ? ".otf" : ".ttf"
36
+ end
37
+ private_class_method :ext_for
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Fontisan
6
+ module Ufo
7
+ module Convert
8
+ # UFO → OpenType Collection (.otc). Two-step:
9
+ #
10
+ # 1. Compile UFO → OTF (CFF) in tmpdir.
11
+ # 2. Load + feed to Collection::Builder with format: :otc.
12
+ #
13
+ # A single UFO produces a single-face collection.
14
+ module ToOtc
15
+ # @param ufo [Fontisan::Ufo::Font]
16
+ # @param output_path [String]
17
+ # @param compiler [Symbol] :otf (default, CFF1) or :otf2 (CFF2)
18
+ # @return [String] the output_path
19
+ def self.convert(ufo, output_path:, compiler: :otf, **_opts)
20
+ compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
21
+ raise ArgumentError, "unknown intermediate compiler: #{compiler.inspect}" unless compiler_class
22
+
23
+ Dir.mktmpdir do |dir|
24
+ intermediate_path = File.join(dir, "intermediate.otf")
25
+ compiler_class.new(ufo).compile(output_path: intermediate_path)
26
+
27
+ loaded = Fontisan::FontLoader.load(intermediate_path)
28
+ Fontisan::Collection::Builder.new([loaded], format: :otc,
29
+ optimize: true).build_to_file(output_path)
30
+ end
31
+
32
+ output_path
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Convert
6
+ # Thin facade over Compile::OtfCompiler (CFF1 outlines).
7
+ module ToOtf
8
+ # @param ufo [Fontisan::Ufo::Font]
9
+ # @param output_path [String]
10
+ # @return [String] the output_path
11
+ def self.convert(ufo, output_path:)
12
+ Compile::OtfCompiler.new(ufo).compile(output_path: output_path)
13
+ output_path
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Convert
6
+ # Thin facade over Compile::Otf2Compiler (CFF2 outlines, used
7
+ # for variable OTF). CFF2 still inherits the 65,535-glyph cap
8
+ # because maxp.numGlyphs is uint16 — see Stitcher::GlyphLimit.
9
+ module ToOtf2
10
+ # @param ufo [Fontisan::Ufo::Font]
11
+ # @param output_path [String]
12
+ # @return [String] the output_path
13
+ def self.convert(ufo, output_path:)
14
+ Compile::Otf2Compiler.new(ufo).compile(output_path: output_path)
15
+ output_path
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Fontisan
6
+ module Ufo
7
+ module Convert
8
+ # Shared logic for UFO → PostScript Type 1 (PFB or PFA).
9
+ # Used by ToPfb and ToPfa below; not registered in
10
+ # WRAPPER_FOR_FORMAT directly. Callers should use the
11
+ # format-specific modules.
12
+ module ToPostscript
13
+ # @param ufo [Fontisan::Ufo::Font]
14
+ # @param output_path [String]
15
+ # @param compiler [Symbol] :ttf (default) or :otf
16
+ # @param generator_class [Class] Type1::PFBGenerator or Type1::PFAGenerator
17
+ # @param ps_options [Hash] forwarded to the generator
18
+ # (encoding, upm_scale, convert_curves)
19
+ # @return [String] the output_path
20
+ def self.convert(ufo, output_path:, generator_class:, compiler: :ttf, **ps_options)
21
+ compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
22
+ raise ArgumentError, "unknown intermediate compiler: #{compiler.inspect}" unless compiler_class
23
+
24
+ Dir.mktmpdir do |dir|
25
+ intermediate_path = File.join(dir, "intermediate#{ext_for(compiler)}")
26
+ compiler_class.new(ufo).compile(output_path: intermediate_path)
27
+
28
+ loaded = Fontisan::FontLoader.load(intermediate_path)
29
+ bytes = generator_class.generate(loaded, ps_options)
30
+ File.binwrite(output_path, bytes)
31
+ end
32
+
33
+ output_path
34
+ end
35
+
36
+ def self.ext_for(compiler)
37
+ %i[otf otf2].include?(compiler) ? ".otf" : ".ttf"
38
+ end
39
+ private_class_method :ext_for
40
+ end
41
+
42
+ # UFO → PostScript Type 1 Binary (.pfb).
43
+ module ToPfb
44
+ def self.convert(ufo, output_path:, **)
45
+ ToPostscript.convert(ufo, output_path: output_path,
46
+ generator_class: Fontisan::Type1::PFBGenerator, **)
47
+ end
48
+ end
49
+
50
+ # UFO → PostScript Type 1 ASCII (.pfa).
51
+ module ToPfa
52
+ def self.convert(ufo, output_path:, **)
53
+ ToPostscript.convert(ufo, output_path: output_path,
54
+ generator_class: Fontisan::Type1::PFAGenerator, **)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Fontisan
6
+ module Ufo
7
+ module Convert
8
+ # UFO → TrueType Collection (.ttc). Two-step:
9
+ #
10
+ # 1. Compile UFO → TTF in tmpdir.
11
+ # 2. Load + feed to Collection::Builder with format: :ttc.
12
+ #
13
+ # A single UFO produces a single-face collection. Multi-face
14
+ # collections from multiple UFOs are a separate concern (the
15
+ # Stitcher pipeline handles that case).
16
+ module ToTtc
17
+ # @param ufo [Fontisan::Ufo::Font]
18
+ # @param output_path [String]
19
+ # @return [String] the output_path
20
+ def self.convert(ufo, output_path:, **_opts)
21
+ Dir.mktmpdir do |dir|
22
+ intermediate_path = File.join(dir, "intermediate.ttf")
23
+ Compile::TtfCompiler.new(ufo).compile(output_path: intermediate_path)
24
+
25
+ loaded = Fontisan::FontLoader.load(intermediate_path)
26
+ Fontisan::Collection::Builder.new([loaded], format: :ttc,
27
+ optimize: true).build_to_file(output_path)
28
+ end
29
+
30
+ output_path
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Convert
6
+ # Thin facade over Compile::TtfCompiler. Exists so callers
7
+ # have a uniform +Ufo::Convert::To{Format}.convert+ API across
8
+ # all output formats, and so the CLI's format dispatch can be
9
+ # a hash lookup instead of a case statement.
10
+ module ToTtf
11
+ # @param ufo [Fontisan::Ufo::Font]
12
+ # @param output_path [String]
13
+ # @return [String] the output_path
14
+ def self.convert(ufo, output_path:)
15
+ Compile::TtfCompiler.new(ufo).compile(output_path: output_path)
16
+ output_path
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Fontisan
6
+ module Ufo
7
+ module Convert
8
+ # UFO → WOFF. Two-step pipeline:
9
+ #
10
+ # 1. Compile the UFO to a TTF (or OTF) in a tmpdir via the
11
+ # corresponding Compile::*Compiler.
12
+ # 2. Load the compiled binary back via FontLoader, then run
13
+ # Converters::WoffWriter to wrap it in the WOFF container.
14
+ #
15
+ # The tmpfile is cleaned up automatically (Dir.mktmpdir block).
16
+ # The original UFO is never mutated.
17
+ #
18
+ # Why two steps instead of one in-memory pass: the WOFF
19
+ # encoders operate on a loaded font's table directory (they
20
+ # need checksums, offsets, alignment), not on a UFO model.
21
+ # Round-tripping through the compiled binary is the simplest
22
+ # path; an in-memory variant would duplicate the table-bytes
23
+ # extraction that FontLoader already does.
24
+ module ToWoff
25
+ # @param ufo [Fontisan::Ufo::Font]
26
+ # @param output_path [String]
27
+ # @param compiler [Symbol] which intermediate format to use:
28
+ # :ttf (default) or :otf
29
+ # @param woff_options [Hash] forwarded to WoffWriter.convert
30
+ # (zlib_level, uncompressed, compression_threshold,
31
+ # metadata_xml, etc.)
32
+ # @return [String] the output_path
33
+ def self.convert(ufo, output_path:, compiler: :ttf, **woff_options)
34
+ compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
35
+ raise ArgumentError, "unknown intermediate compiler: #{compiler.inspect}" unless compiler_class
36
+
37
+ Dir.mktmpdir do |dir|
38
+ intermediate_path = File.join(dir, "intermediate#{format_ext(compiler)}")
39
+ compiler_class.new(ufo).compile(output_path: intermediate_path)
40
+
41
+ loaded = Fontisan::FontLoader.load(intermediate_path)
42
+ woff_bytes = Fontisan::Converters::WoffWriter.new.convert(loaded, woff_options)
43
+ File.binwrite(output_path, woff_bytes)
44
+ end
45
+
46
+ output_path
47
+ end
48
+
49
+ def self.format_ext(compiler)
50
+ %i[otf otf2].include?(compiler) ? ".otf" : ".ttf"
51
+ end
52
+ private_class_method :format_ext
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Fontisan
6
+ module Ufo
7
+ module Convert
8
+ # UFO → WOFF2. Same two-step pipeline as ToWoff, but with the
9
+ # WOFF2 encoder (Brotli compression, optional glyf/loca
10
+ # transforms) instead of WOFF (zlib).
11
+ #
12
+ # Returns the output_path; the WOFF2 binary itself lands at
13
+ # +output_path+.
14
+ module ToWoff2
15
+ # @param ufo [Fontisan::Ufo::Font]
16
+ # @param output_path [String]
17
+ # @param compiler [Symbol] which intermediate format to use:
18
+ # :ttf (default) or :otf
19
+ # @param woff2_options [Hash] forwarded to Woff2Encoder.convert
20
+ # (brotli_quality, transform_tables, quality legacy alias)
21
+ # @return [String] the output_path
22
+ def self.convert(ufo, output_path:, compiler: :ttf, **woff2_options)
23
+ compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
24
+ raise ArgumentError, "unknown intermediate compiler: #{compiler.inspect}" unless compiler_class
25
+
26
+ Dir.mktmpdir do |dir|
27
+ intermediate_path = File.join(dir, "intermediate#{format_ext(compiler)}")
28
+ compiler_class.new(ufo).compile(output_path: intermediate_path)
29
+
30
+ loaded = Fontisan::FontLoader.load(intermediate_path)
31
+ result = Fontisan::Converters::Woff2Encoder.new.convert(loaded, woff2_options)
32
+ File.binwrite(output_path, result[:woff2_binary])
33
+ end
34
+
35
+ output_path
36
+ end
37
+
38
+ def self.format_ext(compiler)
39
+ %i[otf otf2].include?(compiler) ? ".otf" : ".ttf"
40
+ end
41
+ private_class_method :format_ext
42
+ end
43
+ end
44
+ end
45
+ end
@@ -5,14 +5,77 @@ module Fontisan
5
5
  # Conversion layer between the UFO model and fontisan's BinData
6
6
  # table layer. Two directions:
7
7
  #
8
- # ToBinData.convert(ufo_font) → Hash<tag, BinData record or bytes>
9
- # FromBinData.convert(loaded_font) Fontisan::Ufo::Font
8
+ # ToBinData / To{Format} .convert(ufo, output_path:) writes file
9
+ # FromBinData.convert(loaded_font) → Ufo::Font
10
10
  #
11
- # The ToBinData path is already implemented as Compile::* modules
12
- # (each table builder IS a UFO→BinData converter). This namespace
13
- # owns the reverse direction.
11
+ # The To* paths are thin facades over the corresponding
12
+ # Compile::*Compiler classes. They exist to give callers a single
13
+ # uniform convert API and to keep the CLI's format dispatch in
14
+ # one data-driven place (COMPILER_FOR_FORMAT) instead of a switch
15
+ # statement per CLI command.
14
16
  module Convert
15
17
  autoload :FromBinData, "fontisan/ufo/convert/from_bin_data"
18
+ autoload :ToTtf, "fontisan/ufo/convert/to_ttf"
19
+ autoload :ToOtf, "fontisan/ufo/convert/to_otf"
20
+ autoload :ToOtf2, "fontisan/ufo/convert/to_otf2"
21
+ autoload :ToWoff, "fontisan/ufo/convert/to_woff"
22
+ autoload :ToWoff2, "fontisan/ufo/convert/to_woff2"
23
+ autoload :ToDfont, "fontisan/ufo/convert/to_dfont"
24
+ autoload :ToTtc, "fontisan/ufo/convert/to_ttc"
25
+ autoload :ToOtc, "fontisan/ufo/convert/to_otc"
26
+ autoload :ToPostscript, "fontisan/ufo/convert/to_postscript"
27
+ autoload :ToPfb, "fontisan/ufo/convert/to_postscript"
28
+ autoload :ToPfa, "fontisan/ufo/convert/to_postscript"
29
+
30
+ # Single source of truth for "which compiler handles this
31
+ # output format?". OCP: adding a format = adding one entry
32
+ # here + one wrapper file. The CLI reads from this hash
33
+ # instead of a case statement.
34
+ COMPILER_FOR_FORMAT = {
35
+ ttf: Compile::TtfCompiler,
36
+ otf: Compile::OtfCompiler,
37
+ otf2: Compile::Otf2Compiler,
38
+ }.freeze
39
+
40
+ # Wrapper modules for 2-step pipelines (UFO → compiler →
41
+ # tmpfile → encoder → output). These don't fit the 1-step
42
+ # COMPILER_FOR_FORMAT registry because they need a tmpdir +
43
+ # a second encoder pass. OCP: adding a 2-step format =
44
+ # adding one entry here + one wrapper file.
45
+ WRAPPER_FOR_FORMAT = {
46
+ woff: ToWoff,
47
+ woff2: ToWoff2,
48
+ dfont: ToDfont,
49
+ ttc: ToTtc,
50
+ otc: ToOtc,
51
+ pfb: ToPfb,
52
+ pfa: ToPfa,
53
+ }.freeze
54
+
55
+ # Convert a UFO to a binary format, writing to +output_path+.
56
+ # Routes through either COMPILER_FOR_FORMAT (1-step: ttf/otf/
57
+ # otf2) or WRAPPER_FOR_FORMAT (2-step: woff/woff2). Raises
58
+ # +ArgumentError+ for unknown formats.
59
+ #
60
+ # @param ufo [Fontisan::Ufo::Font]
61
+ # @param to [Symbol] format key
62
+ # @param output_path [String]
63
+ # @param opts [Hash] forwarded to the compiler or wrapper
64
+ # (woff/woff2 wrappers take +compiler:+ + woff-specific opts)
65
+ # @return [String] the output_path
66
+ def self.convert(ufo, to:, output_path:, **)
67
+ format_key = to.to_sym
68
+
69
+ if (wrapper = WRAPPER_FOR_FORMAT[format_key])
70
+ return wrapper.convert(ufo, output_path: output_path, **)
71
+ end
72
+
73
+ compiler = COMPILER_FOR_FORMAT[format_key]
74
+ raise ArgumentError, "unknown UFO output format: #{to.inspect}" unless compiler
75
+
76
+ compiler.new(ufo).compile(output_path: output_path)
77
+ output_path
78
+ end
16
79
  end
17
80
  end
18
81
  end
@@ -34,6 +34,17 @@ module Fontisan
34
34
  def to_a
35
35
  [a, b, c, d, e, f]
36
36
  end
37
+
38
+ # Apply this transformation to a single (x, y) coordinate.
39
+ #
40
+ # @param x [Numeric]
41
+ # @param y [Numeric]
42
+ # @return [Array(Float, Float)] the transformed [tx, ty]
43
+ def apply(x, y)
44
+ xf = x.to_f
45
+ yf = y.to_f
46
+ [a * xf + c * yf + e, b * xf + d * yf + f]
47
+ end
37
48
  end
38
49
  end
39
50
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.10"
4
+ VERSION = "0.4.12"
5
5
  end
data/lib/fontisan.rb CHANGED
@@ -118,6 +118,7 @@ module Fontisan
118
118
  autoload :SfntTable, "fontisan/sfnt_table"
119
119
  autoload :Stitcher, "fontisan/stitcher"
120
120
  autoload :StitcherCli, "fontisan/stitcher_cli"
121
+ autoload :Tasks, "fontisan/tasks"
121
122
  autoload :TrueTypeCollection, "fontisan/true_type_collection"
122
123
  autoload :TrueTypeFont, "fontisan/true_type_font"
123
124
  autoload :TrueTypeFontExtensions, "fontisan/true_type_font_extensions"