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.
- checksums.yaml +4 -4
- data/README.adoc +27 -1
- data/Rakefile +53 -39
- data/docs/AFDKO_MIGRATION.adoc +175 -0
- data/docs/FEATURE_PARITY.adoc +263 -0
- data/docs/STITCHER_GUIDE.adoc +100 -6
- data/docs/TTX.adoc +176 -0
- data/docs/UFO_COMPILATION.adoc +281 -3
- data/lib/fontisan/stitcher/partition_strategy/by_block.rb +429 -0
- data/lib/fontisan/stitcher/partition_strategy/by_script.rb +305 -0
- data/lib/fontisan/stitcher/partition_strategy.rb +2 -0
- data/lib/fontisan/stitcher/source.rb +11 -7
- data/lib/fontisan/subset/table_subsetter.rb +227 -11
- data/lib/fontisan/svg/standalone_glyph.rb +129 -0
- data/lib/fontisan/svg.rb +1 -0
- data/lib/fontisan/tasks/fixture_downloader.rb +162 -0
- data/lib/fontisan/tasks.rb +11 -0
- data/lib/fontisan/ufo/cli.rb +34 -21
- data/lib/fontisan/ufo/compile/feature_writers/base.rb +56 -0
- data/lib/fontisan/ufo/compile/feature_writers/curs.rb +72 -0
- data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +90 -0
- data/lib/fontisan/ufo/compile/feature_writers/kern.rb +51 -0
- data/lib/fontisan/ufo/compile/feature_writers/kern2.rb +57 -0
- data/lib/fontisan/ufo/compile/feature_writers/mark.rb +45 -0
- data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +122 -0
- data/lib/fontisan/ufo/compile/feature_writers/mkmk.rb +53 -0
- data/lib/fontisan/ufo/compile/feature_writers.rb +37 -0
- data/lib/fontisan/ufo/compile/filters/propagate_anchors.rb +102 -0
- data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +94 -0
- data/lib/fontisan/ufo/compile/filters/sort_contours.rb +69 -0
- data/lib/fontisan/ufo/compile/filters/transformations.rb +113 -0
- data/lib/fontisan/ufo/compile/filters.rb +12 -0
- data/lib/fontisan/ufo/compile.rb +1 -0
- data/lib/fontisan/ufo/convert/to_dfont.rb +41 -0
- data/lib/fontisan/ufo/convert/to_otc.rb +37 -0
- data/lib/fontisan/ufo/convert/to_otf.rb +18 -0
- data/lib/fontisan/ufo/convert/to_otf2.rb +20 -0
- data/lib/fontisan/ufo/convert/to_postscript.rb +59 -0
- data/lib/fontisan/ufo/convert/to_ttc.rb +35 -0
- data/lib/fontisan/ufo/convert/to_ttf.rb +21 -0
- data/lib/fontisan/ufo/convert/to_woff.rb +56 -0
- data/lib/fontisan/ufo/convert/to_woff2.rb +45 -0
- data/lib/fontisan/ufo/convert.rb +68 -5
- data/lib/fontisan/ufo/transformation.rb +11 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +1 -0
- 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
|
data/lib/fontisan/ufo/compile.rb
CHANGED
|
@@ -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
|
data/lib/fontisan/ufo/convert.rb
CHANGED
|
@@ -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(
|
|
9
|
-
# FromBinData.convert(loaded_font)
|
|
8
|
+
# ToBinData / To{Format} .convert(ufo, output_path:) → writes file
|
|
9
|
+
# FromBinData.convert(loaded_font) → Ufo::Font
|
|
10
10
|
#
|
|
11
|
-
# The
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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
|
data/lib/fontisan/version.rb
CHANGED
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"
|