fontisan 0.2.23 → 0.3.0

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fontisan/cli.rb +6 -0
  3. data/lib/fontisan/stitcher/selector/codepoints.rb +29 -0
  4. data/lib/fontisan/stitcher/selector/gid.rb +25 -0
  5. data/lib/fontisan/stitcher/selector/range.rb +30 -0
  6. data/lib/fontisan/stitcher/selector.rb +26 -0
  7. data/lib/fontisan/stitcher/source.rb +97 -0
  8. data/lib/fontisan/stitcher.rb +182 -0
  9. data/lib/fontisan/stitcher_cli.rb +69 -0
  10. data/lib/fontisan/ufo/anchor.rb +17 -0
  11. data/lib/fontisan/ufo/cli.rb +85 -0
  12. data/lib/fontisan/ufo/compile/base_compiler.rb +81 -0
  13. data/lib/fontisan/ufo/compile/cff.rb +224 -0
  14. data/lib/fontisan/ufo/compile/cmap.rb +129 -0
  15. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +174 -0
  16. data/lib/fontisan/ufo/compile/filters/decompose_components.rb +33 -0
  17. data/lib/fontisan/ufo/compile/filters/flatten_components.rb +22 -0
  18. data/lib/fontisan/ufo/compile/filters/reverse_contour_direction.rb +27 -0
  19. data/lib/fontisan/ufo/compile/filters.rb +57 -0
  20. data/lib/fontisan/ufo/compile/glyf_loca.rb +145 -0
  21. data/lib/fontisan/ufo/compile/head.rb +98 -0
  22. data/lib/fontisan/ufo/compile/hhea.rb +36 -0
  23. data/lib/fontisan/ufo/compile/hmtx.rb +27 -0
  24. data/lib/fontisan/ufo/compile/maxp.rb +57 -0
  25. data/lib/fontisan/ufo/compile/name.rb +79 -0
  26. data/lib/fontisan/ufo/compile/os2.rb +81 -0
  27. data/lib/fontisan/ufo/compile/otf_compiler.rb +43 -0
  28. data/lib/fontisan/ufo/compile/post.rb +32 -0
  29. data/lib/fontisan/ufo/compile/ttf_compiler.rb +69 -0
  30. data/lib/fontisan/ufo/compile.rb +48 -0
  31. data/lib/fontisan/ufo/component.rb +18 -0
  32. data/lib/fontisan/ufo/contour.rb +29 -0
  33. data/lib/fontisan/ufo/convert/from_bin_data.rb +246 -0
  34. data/lib/fontisan/ufo/convert.rb +18 -0
  35. data/lib/fontisan/ufo/data_set.rb +21 -0
  36. data/lib/fontisan/ufo/features.rb +17 -0
  37. data/lib/fontisan/ufo/font.rb +61 -0
  38. data/lib/fontisan/ufo/glyph.rb +421 -0
  39. data/lib/fontisan/ufo/guideline.rb +19 -0
  40. data/lib/fontisan/ufo/image.rb +16 -0
  41. data/lib/fontisan/ufo/image_set.rb +19 -0
  42. data/lib/fontisan/ufo/info.rb +79 -0
  43. data/lib/fontisan/ufo/kerning.rb +32 -0
  44. data/lib/fontisan/ufo/layer.rb +38 -0
  45. data/lib/fontisan/ufo/layer_set.rb +37 -0
  46. data/lib/fontisan/ufo/lib.rb +24 -0
  47. data/lib/fontisan/ufo/plist.rb +118 -0
  48. data/lib/fontisan/ufo/point.rb +38 -0
  49. data/lib/fontisan/ufo/reader.rb +144 -0
  50. data/lib/fontisan/ufo/transformation.rb +39 -0
  51. data/lib/fontisan/ufo/writer.rb +115 -0
  52. data/lib/fontisan/ufo.rb +44 -0
  53. data/lib/fontisan/version.rb +1 -1
  54. data/lib/fontisan.rb +3 -0
  55. metadata +51 -1
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ # Compiler layer: turns a typed Fontisan::Ufo::Font into OpenType
6
+ # binary tables, then into a TTF or OTF file via Fontisan::FontWriter.
7
+ #
8
+ # Pipeline:
9
+ #
10
+ # Fontisan::Ufo::Font (typed)
11
+ # │
12
+ # ▼
13
+ # BaseCompiler#compile
14
+ # │
15
+ # ├─ Head.build(font) → Tables::Head BinData record
16
+ # ├─ Hhea.build(font) → Tables::Hhea
17
+ # ├─ Maxp.build(font) → Tables::Maxp
18
+ # ├─ Os2.build(font) → Tables::Os2
19
+ # ├─ Name.build(font) → Tables::Name
20
+ # ├─ Post.build(font) → Tables::Post
21
+ # ├─ Hmtx.build(font) → Tables::Hmtx
22
+ # ├─ Cmap.build(font) → Tables::Cmap
23
+ # ├─ (TTF) GlyfLoca.build → Tables::Glyf + Tables::Loca
24
+ # └─ (OTF) Cff.build → Tables::Cff
25
+ # │
26
+ # ▼
27
+ # tables_hash.transform_values(&:to_binary_s)
28
+ # │
29
+ # ▼
30
+ # Fontisan::FontWriter.write_to_file(...)
31
+ module Compile
32
+ autoload :BaseCompiler, "fontisan/ufo/compile/base_compiler"
33
+ autoload :TtfCompiler, "fontisan/ufo/compile/ttf_compiler"
34
+ autoload :OtfCompiler, "fontisan/ufo/compile/otf_compiler"
35
+ autoload :Filters, "fontisan/ufo/compile/filters"
36
+ autoload :Head, "fontisan/ufo/compile/head"
37
+ autoload :Hhea, "fontisan/ufo/compile/hhea"
38
+ autoload :Maxp, "fontisan/ufo/compile/maxp"
39
+ autoload :Os2, "fontisan/ufo/compile/os2"
40
+ autoload :Name, "fontisan/ufo/compile/name"
41
+ autoload :Post, "fontisan/ufo/compile/post"
42
+ autoload :Hmtx, "fontisan/ufo/compile/hmtx"
43
+ autoload :Cmap, "fontisan/ufo/compile/cmap"
44
+ autoload :GlyfLoca, "fontisan/ufo/compile/glyf_loca"
45
+ autoload :Cff, "fontisan/ufo/compile/cff"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ # A composite-glyph reference: this glyph draws by transforming
6
+ # another glyph. Used in UFO 3 composites; often the same shape as
7
+ # the OpenType composite-glyph flag set.
8
+ class Component
9
+ attr_reader :base_glyph, :transformation, :identifier
10
+
11
+ def initialize(base_glyph:, transformation: nil, identifier: nil)
12
+ @base_glyph = base_glyph.to_s
13
+ @transformation = transformation # Fontisan::Ufo::Transformation or nil
14
+ @identifier = identifier
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ # An ordered list of points forming one closed (or open) contour
6
+ # in a glyph's outline.
7
+ class Contour
8
+ attr_accessor :points
9
+ attr_reader :closed
10
+
11
+ def initialize(points = [], closed: true)
12
+ @points = points
13
+ @closed = closed
14
+ end
15
+
16
+ def closed?
17
+ @closed
18
+ end
19
+
20
+ def open?
21
+ !@closed
22
+ end
23
+
24
+ def point_count
25
+ @points.size
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Convert
6
+ # Converts a loaded TTF/OTF font (from Fontisan::FontLoader.load)
7
+ # into a typed Fontisan::Ufo::Font. This is the reverse of
8
+ # Compile::TtfCompiler / Compile::OtfCompiler.
9
+ #
10
+ # Reads each BinData table, extracts per-glyph data, and builds
11
+ # Ufo::Glyph objects in the default layer.
12
+ #
13
+ # Composite glyphs are preserved as UFO Components (not decomposed).
14
+ # This keeps the round-trip faithful to the source.
15
+ module FromBinData
16
+ # @param font [Fontisan::SfntFont] loaded TTF or OTF
17
+ # @return [Fontisan::Ufo::Font] typed UFO model
18
+ def self.convert(font)
19
+ ufo = Ufo::Font.new
20
+ ufo.ufo_version = 3
21
+
22
+ extract_info(font, ufo)
23
+ extract_glyphs(font, ufo)
24
+ ufo
25
+ end
26
+
27
+ # Map head/hhea/OS2/name/post fields → Ufo::Info.
28
+ def self.extract_info(font, ufo)
29
+ info = ufo.info
30
+
31
+ head = font.table("head")
32
+ if head
33
+ info.units_per_em = head.units_per_em
34
+ info.version_major = 1
35
+ info.version_minor = 0
36
+ end
37
+
38
+ hhea = font.table("hhea")
39
+ if hhea
40
+ info.ascender = hhea.ascent
41
+ info.descender = hhea.descent
42
+ info.open_type_hhea_line_gap = hhea.line_gap
43
+ end
44
+
45
+ os2 = font.table("OS/2")
46
+ if os2
47
+ info.open_type_os2_weight_class = os2.us_weight_class
48
+ info.open_type_os2_width_class = os2.us_width_class
49
+ end
50
+
51
+ extract_name_records(font, info)
52
+ extract_post(font, info)
53
+ rescue NoMethodError
54
+ # Some tables may not be present in all fonts; skip silently
55
+ end
56
+
57
+ def self.extract_name_records(font, info)
58
+ name_table = font.table("name")
59
+ return unless name_table
60
+
61
+ records = name_table.respond_to?(:name_records) ? name_table.name_records : []
62
+ records.each do |record|
63
+ next unless record.platform_id == 3 && record.encoding_id == 1 # Windows Unicode BMP
64
+
65
+ value = decode_name_value(record, name_table)
66
+ next unless value
67
+
68
+ case record.name_id
69
+ when 0 then info.copyright = value
70
+ when 1 then info.family_name = value
71
+ when 2 then info.style_name = value
72
+ when 4 then info.postscript_full_name = value
73
+ when 6 then info.postscript_font_name = value
74
+ end
75
+ end
76
+ end
77
+
78
+ def self.decode_name_value(record, name_table)
79
+ raw = if name_table.respond_to?(:string_for_record)
80
+ name_table.string_for_record(record)
81
+ end
82
+
83
+ if raw && raw.encoding == Encoding::UTF_16BE
84
+ raw.encode("UTF-8")
85
+ elsif raw && raw.bytesize >= 2 && raw.getbyte(0).between?(0, 127) && raw.getbyte(1).zero?
86
+ # Looks like UTF-16BE
87
+ raw.force_encoding("UTF-16BE").encode("UTF-8")
88
+ else
89
+ raw&.force_encoding("UTF-8")
90
+ end
91
+ rescue Encoding::InvalidByteSequenceError, Encoding::ConverterNotFoundError
92
+ raw&.force_encoding("UTF-8")
93
+ end
94
+
95
+ def self.extract_post(font, info)
96
+ post = font.table("post")
97
+ return unless post
98
+
99
+ if post.respond_to?(:italic_angle)
100
+ info.italic_angle = post.italic_angle
101
+ elsif post.respond_to?(:italic_angle_raw)
102
+ raw = post.italic_angle_raw
103
+ info.italic_angle = raw.to_i / 65536.0
104
+ end
105
+ rescue NoMethodError
106
+ # post table may not have italic_angle
107
+ end
108
+
109
+ # Extract every glyph from glyf (TTF) or CFF (OTF).
110
+ def self.extract_glyphs(font, ufo)
111
+ cmap = build_cmap_lookup(font)
112
+ widths = build_width_lookup(font)
113
+ num_glyphs = font.table("maxp")&.num_glyphs || 0
114
+
115
+ if font.has_table?("glyf")
116
+ extract_truetype_glyphs(font, ufo, cmap, widths, num_glyphs)
117
+ elsif font.has_table?("CFF ")
118
+ extract_cff_glyphs(font, ufo, cmap, widths, num_glyphs)
119
+ end
120
+ end
121
+
122
+ # Build {codepoint → gid} from the cmap table.
123
+ def self.build_cmap_lookup(font)
124
+ cmap_table = font.table("cmap")
125
+ return {} unless cmap_table
126
+
127
+ mappings = cmap_table.respond_to?(:unicode_mappings) ? cmap_table.unicode_mappings : {}
128
+ # Invert: gid → [codepoints]
129
+ inverted = Hash.new { |h, k| h[k] = [] }
130
+ mappings.each { |cp, gid| inverted[gid] << cp }
131
+ inverted
132
+ end
133
+
134
+ # Build {gid → advance_width} from hmtx.
135
+ def self.build_width_lookup(font)
136
+ hmtx = font.table("hmtx")
137
+ return {} unless hmtx
138
+
139
+ hhea = font.table("hhea")
140
+ maxp = font.table("maxp")
141
+ num_h_metrics = hhea&.number_of_h_metrics || 1
142
+ num_glyphs = maxp&.num_glyphs || 0
143
+
144
+ # Hmtx requires context-aware parsing before metric_for works.
145
+ if hmtx.respond_to?(:parse_with_context)
146
+ hmtx.parse_with_context(num_h_metrics, num_glyphs)
147
+ end
148
+
149
+ widths = {}
150
+ num_glyphs.times do |gid|
151
+ metric = hmtx.respond_to?(:metric_for) ? hmtx.metric_for(gid) : nil
152
+ widths[gid] = metric ? metric[:advance_width] : 0
153
+ end
154
+ widths
155
+ rescue RuntimeError
156
+ # If hmtx parsing fails, return empty widths
157
+ {}
158
+ end
159
+
160
+ # TTF: extract contours from glyf table via SimpleGlyph.
161
+ def self.extract_truetype_glyphs(font, ufo, cmap, widths, num_glyphs)
162
+ glyf = font.table("glyf")
163
+ loca = font.table("loca")
164
+ head = font.table("head")
165
+ return unless glyf && loca && head
166
+
167
+ # Tables need context-aware initialization before per-glyph access.
168
+ loca.parse_with_context(head.index_to_loc_format, num_glyphs) if loca.respond_to?(:parse_with_context)
169
+
170
+ num_glyphs.times do |gid|
171
+ glyph_name = glyph_name_for(font, gid) || "glyph#{gid}"
172
+ ufo_glyph = Ufo::Glyph.new(name: glyph_name)
173
+ ufo_glyph.width = widths.fetch(gid, 0).to_f
174
+
175
+ cmap.fetch(gid, []).each { |cp| ufo_glyph.add_unicode(cp) }
176
+
177
+ simple = begin
178
+ glyf.glyph_for(gid, loca, head)
179
+ rescue StandardError
180
+ nil
181
+ end
182
+ next unless simple
183
+
184
+ if simple.is_a?(Fontisan::Tables::SimpleGlyph)
185
+ extract_simple_contours(simple, ufo_glyph)
186
+ end
187
+
188
+ ufo.layers.default_layer.add(ufo_glyph)
189
+ end
190
+ end
191
+
192
+ # Convert a SimpleGlyph's contours + points into UFO contours.
193
+ def self.extract_simple_contours(simple, ufo_glyph)
194
+ num_contours = simple.end_pts_of_contours&.size || 0
195
+
196
+ num_contours.times do |ci|
197
+ points = simple.points_for_contour(ci)
198
+ next unless points && !points.empty?
199
+
200
+ ufo_points = points.map do |pt|
201
+ x = pt[:x] || pt["x"]
202
+ y = pt[:y] || pt["y"]
203
+ on_curve = pt[:on_curve].nil? || pt[:on_curve]
204
+ type = on_curve ? "line" : "offcurve"
205
+ Ufo::Point.new(x: x.to_f, y: y.to_f, type: type)
206
+ end
207
+ ufo_glyph.add_contour(Ufo::Contour.new(ufo_points))
208
+ end
209
+ end
210
+
211
+ # OTF: extract outlines from CFF charstrings. TODO.full/10b —
212
+ # for now, stub with advance widths only (no contours).
213
+ def self.extract_cff_glyphs(font, ufo, cmap, widths, num_glyphs)
214
+ num_glyphs.times do |gid|
215
+ glyph_name = glyph_name_for(font, gid) || "glyph#{gid}"
216
+ ufo_glyph = Ufo::Glyph.new(name: glyph_name)
217
+ ufo_glyph.width = widths.fetch(gid, 0).to_f
218
+ cmap.fetch(gid, []).each { |cp| ufo_glyph.add_unicode(cp) }
219
+ ufo.layers.default_layer.add(ufo_glyph)
220
+ end
221
+ end
222
+
223
+ # Look up a glyph name from the post table (v2.0) or synthesize.
224
+ def self.glyph_name_for(font, gid)
225
+ post = font.table("post")
226
+ return nil unless post
227
+
228
+ if post.respond_to?(:glyph_name)
229
+ name = post.glyph_name(gid)
230
+ return name unless name.nil? || name.empty?
231
+ end
232
+
233
+ nil
234
+ rescue NoMethodError
235
+ nil
236
+ end
237
+
238
+ private_class_method :extract_info, :extract_name_records, :decode_name_value,
239
+ :extract_post, :extract_glyphs, :build_cmap_lookup,
240
+ :build_width_lookup, :extract_truetype_glyphs,
241
+ :extract_simple_contours, :extract_cff_glyphs,
242
+ :glyph_name_for
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ # Conversion layer between the UFO model and fontisan's BinData
6
+ # table layer. Two directions:
7
+ #
8
+ # ToBinData.convert(ufo_font) → Hash<tag, BinData record or bytes>
9
+ # FromBinData.convert(loaded_font) → Fontisan::Ufo::Font
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.
14
+ module Convert
15
+ autoload :FromBinData, "fontisan/ufo/convert/from_bin_data"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ # Per-glyph custom data from `glyphs/<glyph>.plist`. Each glyph
6
+ # has its own plist file with arbitrary key/value data.
7
+ class DataSet
8
+ def initialize
9
+ @data = {} # glyph_name => Hash<String, Object>
10
+ end
11
+
12
+ def [](glyph_name)
13
+ @data[glyph_name.to_s] ||= {}
14
+ end
15
+
16
+ def keys
17
+ @data.keys
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ # Wrapper around the FEA feature source in `features.fea`.
6
+ #
7
+ # The MVP just stores the raw text. A FEA parser/compiler lands in
8
+ # TODO 08 (feature writers).
9
+ class Features
10
+ attr_accessor :text
11
+
12
+ def initialize(text: "")
13
+ @text = text
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ # Top-level container for a UFO source directory.
6
+ #
7
+ # A Fontisan::Ufo::Font corresponds to one `.ufo` directory. It
8
+ # exposes typed access to the font's metadata, layers, kerning,
9
+ # features, and any custom data.
10
+ #
11
+ # The class is the read/write API; serialization is handled by
12
+ # Fontisan::Ufo::Reader and Fontisan::Ufo::Writer.
13
+ class Font
14
+ attr_accessor :path, :info, :features, :kerning, :lib, :ufo_version
15
+ attr_reader :layers, :data, :images
16
+
17
+ def initialize
18
+ @path = nil
19
+ @ufo_version = nil
20
+ @info = Info.new
21
+ @layers = LayerSet.new
22
+ @features = Features.new
23
+ @kerning = Kerning.new
24
+ @lib = Lib.new
25
+ @data = nil # DataSet needs the Font ref, set by Reader
26
+ @images = ImageSet.new
27
+ end
28
+
29
+ # Convenience accessor for the default layer's glyphs.
30
+ def glyphs
31
+ @layers.default_layer.glyphs
32
+ end
33
+
34
+ # Convenience: lookup a glyph in the default layer by name.
35
+ def glyph(name)
36
+ @layers.default_layer[name]
37
+ end
38
+
39
+ # Convenience: read family name through the Info model.
40
+ def family_name
41
+ @info.family_name
42
+ end
43
+
44
+ # Convenience: iterate every glyph in every layer.
45
+ def each_glyph(&)
46
+ @layers.each do |layer|
47
+ layer.each(&)
48
+ end
49
+ end
50
+
51
+ # @param path [String, Pathname] directory containing the UFO
52
+ # @return [Fontisan::Ufo::Font] the parsed font
53
+ def self.open(path)
54
+ font = new
55
+ font.path = path.to_s
56
+ Reader.new(font).read
57
+ font
58
+ end
59
+ end
60
+ end
61
+ end