fontisan 0.2.22 → 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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/.rubocop_todo.yml +93 -17
  4. data/CHANGELOG.md +12 -2
  5. data/README.adoc +6 -210
  6. data/fontisan.gemspec +48 -0
  7. data/lib/fontisan/cldr/unicode_set_parser.rb +23 -6
  8. data/lib/fontisan/cldr/version_resolver.rb +1 -1
  9. data/lib/fontisan/cli.rb +4 -168
  10. data/lib/fontisan/commands.rb +0 -3
  11. data/lib/fontisan/formatters/text_formatter.rb +0 -6
  12. data/lib/fontisan/formatters.rb +0 -3
  13. data/lib/fontisan/hints.rb +6 -3
  14. data/lib/fontisan/models.rb +4 -4
  15. data/lib/fontisan/pipeline/strategies.rb +4 -2
  16. data/lib/fontisan/pipeline.rb +2 -1
  17. data/lib/fontisan/stitcher/selector/codepoints.rb +29 -0
  18. data/lib/fontisan/stitcher/selector/gid.rb +25 -0
  19. data/lib/fontisan/stitcher/selector/range.rb +30 -0
  20. data/lib/fontisan/stitcher/selector.rb +26 -0
  21. data/lib/fontisan/stitcher/source.rb +97 -0
  22. data/lib/fontisan/stitcher.rb +182 -0
  23. data/lib/fontisan/stitcher_cli.rb +69 -0
  24. data/lib/fontisan/tables/cff.rb +2 -1
  25. data/lib/fontisan/tables.rb +2 -1
  26. data/lib/fontisan/ufo/anchor.rb +17 -0
  27. data/lib/fontisan/ufo/cli.rb +85 -0
  28. data/lib/fontisan/ufo/compile/base_compiler.rb +81 -0
  29. data/lib/fontisan/ufo/compile/cff.rb +224 -0
  30. data/lib/fontisan/ufo/compile/cmap.rb +129 -0
  31. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +174 -0
  32. data/lib/fontisan/ufo/compile/filters/decompose_components.rb +33 -0
  33. data/lib/fontisan/ufo/compile/filters/flatten_components.rb +22 -0
  34. data/lib/fontisan/ufo/compile/filters/reverse_contour_direction.rb +27 -0
  35. data/lib/fontisan/ufo/compile/filters.rb +57 -0
  36. data/lib/fontisan/ufo/compile/glyf_loca.rb +145 -0
  37. data/lib/fontisan/ufo/compile/head.rb +98 -0
  38. data/lib/fontisan/ufo/compile/hhea.rb +36 -0
  39. data/lib/fontisan/ufo/compile/hmtx.rb +27 -0
  40. data/lib/fontisan/ufo/compile/maxp.rb +57 -0
  41. data/lib/fontisan/ufo/compile/name.rb +79 -0
  42. data/lib/fontisan/ufo/compile/os2.rb +81 -0
  43. data/lib/fontisan/ufo/compile/otf_compiler.rb +43 -0
  44. data/lib/fontisan/ufo/compile/post.rb +32 -0
  45. data/lib/fontisan/ufo/compile/ttf_compiler.rb +69 -0
  46. data/lib/fontisan/ufo/compile.rb +48 -0
  47. data/lib/fontisan/ufo/component.rb +18 -0
  48. data/lib/fontisan/ufo/contour.rb +29 -0
  49. data/lib/fontisan/ufo/convert/from_bin_data.rb +246 -0
  50. data/lib/fontisan/ufo/convert.rb +18 -0
  51. data/lib/fontisan/ufo/data_set.rb +21 -0
  52. data/lib/fontisan/ufo/features.rb +17 -0
  53. data/lib/fontisan/ufo/font.rb +61 -0
  54. data/lib/fontisan/ufo/glyph.rb +421 -0
  55. data/lib/fontisan/ufo/guideline.rb +19 -0
  56. data/lib/fontisan/ufo/image.rb +16 -0
  57. data/lib/fontisan/ufo/image_set.rb +19 -0
  58. data/lib/fontisan/ufo/info.rb +79 -0
  59. data/lib/fontisan/ufo/kerning.rb +32 -0
  60. data/lib/fontisan/ufo/layer.rb +38 -0
  61. data/lib/fontisan/ufo/layer_set.rb +37 -0
  62. data/lib/fontisan/ufo/lib.rb +24 -0
  63. data/lib/fontisan/ufo/plist.rb +118 -0
  64. data/lib/fontisan/ufo/point.rb +38 -0
  65. data/lib/fontisan/ufo/reader.rb +144 -0
  66. data/lib/fontisan/ufo/transformation.rb +39 -0
  67. data/lib/fontisan/ufo/writer.rb +115 -0
  68. data/lib/fontisan/ufo.rb +44 -0
  69. data/lib/fontisan/version.rb +1 -1
  70. data/lib/fontisan.rb +3 -3
  71. metadata +57 -70
  72. data/lib/fontisan/audit/codepoint_range_coalescer.rb +0 -41
  73. data/lib/fontisan/audit/context.rb +0 -122
  74. data/lib/fontisan/audit/differ.rb +0 -124
  75. data/lib/fontisan/audit/extractors/aggregations.rb +0 -54
  76. data/lib/fontisan/audit/extractors/base.rb +0 -26
  77. data/lib/fontisan/audit/extractors/color_capabilities.rb +0 -141
  78. data/lib/fontisan/audit/extractors/coverage.rb +0 -48
  79. data/lib/fontisan/audit/extractors/hinting.rb +0 -197
  80. data/lib/fontisan/audit/extractors/identity.rb +0 -52
  81. data/lib/fontisan/audit/extractors/language_coverage.rb +0 -37
  82. data/lib/fontisan/audit/extractors/licensing.rb +0 -79
  83. data/lib/fontisan/audit/extractors/metrics.rb +0 -103
  84. data/lib/fontisan/audit/extractors/opentype_layout.rb +0 -69
  85. data/lib/fontisan/audit/extractors/provenance.rb +0 -29
  86. data/lib/fontisan/audit/extractors/style.rb +0 -32
  87. data/lib/fontisan/audit/extractors/variation_detail.rb +0 -99
  88. data/lib/fontisan/audit/extractors.rb +0 -27
  89. data/lib/fontisan/audit/library_aggregator.rb +0 -83
  90. data/lib/fontisan/audit/library_auditor.rb +0 -90
  91. data/lib/fontisan/audit/registry.rb +0 -60
  92. data/lib/fontisan/audit/style_extractor.rb +0 -80
  93. data/lib/fontisan/audit.rb +0 -20
  94. data/lib/fontisan/cli/ucd_cli.rb +0 -97
  95. data/lib/fontisan/commands/audit_command.rb +0 -123
  96. data/lib/fontisan/commands/audit_compare_command.rb +0 -66
  97. data/lib/fontisan/commands/audit_library_command.rb +0 -46
  98. data/lib/fontisan/config/ucd.yml +0 -23
  99. data/lib/fontisan/formatters/audit_diff_text_renderer.rb +0 -122
  100. data/lib/fontisan/formatters/audit_text_renderer.rb +0 -324
  101. data/lib/fontisan/formatters/library_summary_text_renderer.rb +0 -99
  102. data/lib/fontisan/models/audit/audit_axis.rb +0 -30
  103. data/lib/fontisan/models/audit/audit_block.rb +0 -32
  104. data/lib/fontisan/models/audit/audit_diff.rb +0 -77
  105. data/lib/fontisan/models/audit/audit_report.rb +0 -153
  106. data/lib/fontisan/models/audit/codepoint_range.rb +0 -40
  107. data/lib/fontisan/models/audit/codepoint_set_diff.rb +0 -34
  108. data/lib/fontisan/models/audit/color_capabilities.rb +0 -93
  109. data/lib/fontisan/models/audit/duplicate_group.rb +0 -23
  110. data/lib/fontisan/models/audit/embedding_type.rb +0 -76
  111. data/lib/fontisan/models/audit/field_change.rb +0 -28
  112. data/lib/fontisan/models/audit/fs_selection_flags.rb +0 -61
  113. data/lib/fontisan/models/audit/gasp_range.rb +0 -63
  114. data/lib/fontisan/models/audit/hinting.rb +0 -93
  115. data/lib/fontisan/models/audit/library_summary.rb +0 -40
  116. data/lib/fontisan/models/audit/licensing.rb +0 -48
  117. data/lib/fontisan/models/audit/metrics.rb +0 -111
  118. data/lib/fontisan/models/audit/named_instance.rb +0 -41
  119. data/lib/fontisan/models/audit/opentype_layout.rb +0 -40
  120. data/lib/fontisan/models/audit/script_coverage_row.rb +0 -26
  121. data/lib/fontisan/models/audit/script_features.rb +0 -28
  122. data/lib/fontisan/models/audit/variation_detail.rb +0 -44
  123. data/lib/fontisan/models/audit.rb +0 -33
  124. data/lib/fontisan/models/ucd/ucd.rb +0 -38
  125. data/lib/fontisan/models/ucd/ucd_char.rb +0 -67
  126. data/lib/fontisan/models/ucd.rb +0 -19
  127. data/lib/fontisan/ucd/aggregator.rb +0 -73
  128. data/lib/fontisan/ucd/cache_manager.rb +0 -111
  129. data/lib/fontisan/ucd/config.rb +0 -59
  130. data/lib/fontisan/ucd/download_error.rb +0 -9
  131. data/lib/fontisan/ucd/downloader.rb +0 -88
  132. data/lib/fontisan/ucd/error.rb +0 -8
  133. data/lib/fontisan/ucd/index.rb +0 -103
  134. data/lib/fontisan/ucd/index_builder.rb +0 -107
  135. data/lib/fontisan/ucd/range_entry.rb +0 -56
  136. data/lib/fontisan/ucd/unknown_version_error.rb +0 -9
  137. data/lib/fontisan/ucd/version_resolver.rb +0 -79
  138. data/lib/fontisan/ucd.rb +0 -23
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `hhea` (horizontal header) table.
7
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/hhea
8
+ module Hhea
9
+ VERSION_1_0 = 0x00010000
10
+
11
+ # @param font [Fontisan::Ufo::Font]
12
+ # @param glyphs [Array<Fontisan::Ufo::Glyph>]
13
+ # @return [Fontisan::Tables::Hhea]
14
+ def self.build(font, glyphs:)
15
+ info = font.info
16
+ widths = glyphs.map { |g| g.width.to_i }
17
+ Fontisan::Tables::Hhea.new(
18
+ version_raw: VERSION_1_0,
19
+ ascent: info.ascender || 800,
20
+ descent: info.descender || -200,
21
+ line_gap: info.open_type_hhea_line_gap || 0,
22
+ advance_width_max: widths.max || 0,
23
+ min_left_side_bearing: 0,
24
+ min_right_side_bearing: 0,
25
+ x_max_extent: widths.max || 0,
26
+ caret_slope_rise: 1,
27
+ caret_slope_run: 0,
28
+ caret_offset: 0,
29
+ metric_data_format: 0,
30
+ number_of_h_metrics: glyphs.size,
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `hmtx` (horizontal metrics) table.
7
+ # One LongHorMetric per glyph (4 bytes each):
8
+ # uint16 advanceWidth, int16 lsb
9
+ # No trailing "leftSideBearing" array (use numberOfHMetrics = numGlyphs).
10
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/hmtx
11
+ module Hmtx
12
+ # @param _font [Fontisan::Ufo::Font]
13
+ # @param glyphs [Array<Fontisan::Ufo::Glyph>] in gid order
14
+ # @return [String] hmtx table bytes
15
+ def self.build(_font, glyphs:)
16
+ data = +""
17
+ glyphs.each do |glyph|
18
+ bbox = glyph.bbox
19
+ lsb = bbox ? bbox.x_min.to_i : 0
20
+ data << [glyph.width.to_i, lsb].pack("nn")
21
+ end
22
+ data
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `maxp` (maximum profile) table.
7
+ # TrueType (0x00010000) carries 13 metrics; CFF (0x00005000)
8
+ # carries just num_glyphs. We pick the version based on which
9
+ # outline compiler is in use — caller passes `version:`.
10
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/maxp
11
+ module Maxp
12
+ VERSION_TRUE_TYPE = 0x00010000
13
+ VERSION_OPEN_TYPE = 0x00005000
14
+
15
+ # @param _font [Fontisan::Ufo::Font]
16
+ # @param glyphs [Array<Fontisan::Ufo::Glyph>]
17
+ # @param version [Integer] one of VERSION_TRUE_TYPE / VERSION_OPEN_TYPE
18
+ # @return [Fontisan::Tables::Maxp]
19
+ def self.build(_font, glyphs:, version: VERSION_OPEN_TYPE)
20
+ if version == VERSION_TRUE_TYPE
21
+ build_truetype(glyphs)
22
+ else
23
+ Fontisan::Tables::Maxp.new(
24
+ version_raw: VERSION_OPEN_TYPE,
25
+ num_glyphs: glyphs.size,
26
+ )
27
+ end
28
+ end
29
+
30
+ def self.build_truetype(glyphs)
31
+ max_points = glyphs.map(&:point_count).max || 0
32
+ max_contours = glyphs.map { |g| g.contours.size }.max || 0
33
+ max_components = glyphs.map { |g| g.components.size }.max || 0
34
+
35
+ Fontisan::Tables::Maxp.new(
36
+ version_raw: VERSION_TRUE_TYPE,
37
+ num_glyphs: glyphs.size,
38
+ max_points: max_points,
39
+ max_contours: max_contours,
40
+ max_composite_points: max_points,
41
+ max_composite_contours: max_contours,
42
+ max_zones: 2,
43
+ max_twilight_points: 0,
44
+ max_storage: 0,
45
+ max_function_defs: 0,
46
+ max_instruction_defs: 0,
47
+ max_stack_elements: 0,
48
+ max_size_of_instructions: 0,
49
+ max_component_elements: max_components,
50
+ max_component_depth: max_components.positive? ? 1 : 0,
51
+ )
52
+ end
53
+ private_class_method :build_truetype
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `name` table from UFO fontinfo data.
7
+ # Writes Windows-Unicode (platform 3, encoding 1) name records
8
+ # for the standard 6 name IDs (copyright, family, subfamily,
9
+ # unique ID, full name, version, PostScript name).
10
+ #
11
+ # BinData's Tables::Name structure doesn't make construction
12
+ # from a record list easy (it has a custom after_read_hook and
13
+ # a `rest :string_storage`); this builder produces the bytes
14
+ # directly so we don't fight the BinData shape.
15
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/name
16
+ module Name
17
+ PLATFORM_WINDOWS_UNICODE = 3
18
+ ENCODING_WINDOWS_UNICODE_BMP = 1
19
+ LANGUAGE_WINDOWS_ENGLISH_US = 0x0409
20
+
21
+ # @param font [Fontisan::Ufo::Font]
22
+ # @return [String] the name table bytes
23
+ def self.build(font, **_opts)
24
+ records = default_records(font)
25
+ format0_bytes(records)
26
+ end
27
+
28
+ # @param font [Fontisan::Ufo::Font] (used without glyph info)
29
+ # @return [Array<Hash{name_id: Integer, value: String}>]
30
+ def self.default_records(font)
31
+ family = font.info.family_name || "Untitled"
32
+ subfamily = font.info.style_name || "Regular"
33
+ ps_name = font.info.postscript_font_name || "#{family}-#{subfamily}"
34
+ full_name = "#{family} #{subfamily}".strip
35
+ major = font.info.version_major || 0
36
+ minor = font.info.version_minor || 0
37
+ version_str = "Version #{major}.#{minor}"
38
+ unique_id = "#{family}-#{subfamily};#{version_str}"
39
+ copyright = font.info.copyright || ""
40
+
41
+ [
42
+ { name_id: 0, value: copyright }, # copyright
43
+ { name_id: 1, value: family }, # family
44
+ { name_id: 2, value: subfamily }, # subfamily
45
+ { name_id: 3, value: unique_id }, # unique ID
46
+ { name_id: 4, value: full_name }, # full name
47
+ { name_id: 5, value: version_str }, # version
48
+ { name_id: 6, value: ps_name }, # PostScript name
49
+ ]
50
+ end
51
+
52
+ def self.format0_bytes(records)
53
+ # Strings are stored UTF-16BE on disk (Windows Unicode).
54
+ encoded = records.map { |r| r[:value].encode("UTF-16BE").force_encoding("BINARY") }
55
+ storage = encoded.join
56
+ storage_offset = 6 + (records.size * 12)
57
+
58
+ header = [0, records.size, storage_offset].pack("nnn")
59
+ body = +""
60
+ offset = 0
61
+ records.zip(encoded).each do |r, bytes|
62
+ body << [
63
+ PLATFORM_WINDOWS_UNICODE,
64
+ ENCODING_WINDOWS_UNICODE_BMP,
65
+ LANGUAGE_WINDOWS_ENGLISH_US,
66
+ r[:name_id],
67
+ bytes.bytesize,
68
+ offset,
69
+ ].pack("nnnnnn")
70
+ offset += bytes.bytesize
71
+ end
72
+
73
+ header + body + storage
74
+ end
75
+ private_class_method :format0_bytes, :default_records
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `OS/2` table from UFO fontinfo data.
7
+ # Default version is 4 (modern). Most fields are 0 unless the
8
+ # UFO has explicit values.
9
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/os2
10
+ module Os2
11
+ VERSION_DEFAULT = 4
12
+ WEIGHT_REGULAR = 400
13
+ WIDTH_NORMAL = 5
14
+ FS_SELECTION_REGULAR = 0x0040
15
+
16
+ # @param font [Fontisan::Ufo::Font]
17
+ # @param glyphs [Array<Fontisan::Ufo::Glyph>] used to compute
18
+ # usFirstCharIndex/usLastCharIndex + xAvgCharWidth
19
+ # @return [Fontisan::Tables::Os2]
20
+ def self.build(font, glyphs:)
21
+ info = font.info
22
+ cp_min, cp_max = unicode_range(glyphs)
23
+ Fontisan::Tables::Os2.new(
24
+ version: VERSION_DEFAULT,
25
+ x_avg_char_width: avg_advance(glyphs),
26
+ us_weight_class: info.open_type_os2_weight_class || WEIGHT_REGULAR,
27
+ us_width_class: info.open_type_os2_width_class || WIDTH_NORMAL,
28
+ fs_type: 0,
29
+ y_subscript_x_size: 650,
30
+ y_subscript_y_size: 600,
31
+ y_subscript_x_offset: 0,
32
+ y_subscript_y_offset: 75,
33
+ y_superscript_x_size: 650,
34
+ y_superscript_y_size: 600,
35
+ y_superscript_x_offset: 0,
36
+ y_superscript_y_offset: 350,
37
+ y_strikeout_size: 50,
38
+ y_strikeout_position: 300,
39
+ s_family_class: 0,
40
+ panose: Array.new(10, 0),
41
+ ul_unicode_range1: 1, # Basic Latin
42
+ ul_unicode_range2: 0,
43
+ ul_unicode_range3: 0,
44
+ ul_unicode_range4: 0,
45
+ ach_vend_id: "NONE",
46
+ fs_selection: FS_SELECTION_REGULAR,
47
+ us_first_char_index: cp_min,
48
+ us_last_char_index: cp_max,
49
+ s_typo_ascender: info.ascender || 800,
50
+ s_typo_descender: info.descender || -200,
51
+ s_typo_line_gap: info.open_type_hhea_line_gap || 0,
52
+ us_win_ascent: info.ascender || 1000,
53
+ us_win_descent: -(info.descender || -200),
54
+ ul_code_page_range1: 1, # Latin 1
55
+ ul_code_page_range2: 0,
56
+ sx_height: info.x_height || 500,
57
+ s_cap_height: info.cap_height || 700,
58
+ us_default_char: 0,
59
+ us_break_char: 0x20,
60
+ us_max_context: 0,
61
+ )
62
+ end
63
+
64
+ def self.unicode_range(glyphs)
65
+ cps = glyphs.flat_map(&:unicodes).sort
66
+ return [0xFFFF, 0] if cps.empty?
67
+
68
+ [cps.first, cps.last]
69
+ end
70
+ private_class_method :unicode_range
71
+
72
+ def self.avg_advance(glyphs)
73
+ return 0 if glyphs.empty?
74
+
75
+ (glyphs.sum { |g| g.width.to_i } / glyphs.size.to_f).round
76
+ end
77
+ private_class_method :avg_advance
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # UFO → OTF. Uses CFF outlines (instead of TrueType glyf/loca).
7
+ # Maxp version 0.5 (no TrueType metrics); sfnt version OTTO.
8
+ #
9
+ # TODO.full/10: this currently emits a placeholder CFF table
10
+ # that satisfies the OTTO signature but does NOT yet encode
11
+ # real charstrings. Full CFF construction lands when TODO 10
12
+ # ships.
13
+ class OtfCompiler < BaseCompiler
14
+ SFNT_VERSION = SFNT_VERSION_OPEN_TYPE
15
+
16
+ def build_outline_tables
17
+ {
18
+ "CFF " => Cff.build(font, glyphs: font.glyphs.values),
19
+ }
20
+ end
21
+
22
+ def compile(output_path:)
23
+ glyphs = font.glyphs.values
24
+
25
+ tables = {
26
+ "head" => Head.build(font, glyphs: glyphs, loca_format: Head::LOCA_FORMAT_LONG),
27
+ "hhea" => Hhea.build(font, glyphs: glyphs),
28
+ "maxp" => Maxp.build(font, glyphs: glyphs, version: Maxp::VERSION_OPEN_TYPE),
29
+ "OS/2" => Os2.build(font, glyphs: glyphs),
30
+ "name" => Name.build(font),
31
+ "post" => Post.build(font),
32
+ "hmtx" => Hmtx.build(font, glyphs: glyphs),
33
+ "cmap" => Cmap.build(font, glyphs: glyphs),
34
+ "CFF " => Cff.build(font, glyphs: glyphs),
35
+ }
36
+
37
+ write(tables, output_path)
38
+ output_path
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `post` (PostScript name) table.
7
+ # Default version is 3.0 (no per-glyph names; smallest).
8
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/post
9
+ module Post
10
+ VERSION_3_0_RAW = 0x00030000
11
+
12
+ # @param _font [Fontisan::Ufo::Font] italic angle + underline
13
+ # read from info, but everything else is zero (no hinting)
14
+ # @return [String] the post table bytes (32 bytes for v3.0)
15
+ def self.build(font, **_opts)
16
+ italic_angle = font.info.italic_angle || 0.0
17
+ [
18
+ VERSION_3_0_RAW, # version 3.0
19
+ (italic_angle * 0x10000).to_i, # italicAngle (Fixed 16.16)
20
+ -100, # underlinePosition (FUnits)
21
+ 50, # underlineThickness
22
+ 0, # isFixedPitch
23
+ 0, # minMemType42
24
+ 0, # maxMemType42
25
+ 0, # minMemType1
26
+ 0, # maxMemType1
27
+ ].pack("NNnnNNNNN")
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # UFO → TTF. Same tables as the OTF compiler plus glyf + loca
7
+ # (no CFF). Maxp version 1.0 carries the TrueType metrics.
8
+ #
9
+ # Before glyf encoding, the TTF-required filters run:
10
+ # - cubic_to_quadratic (TTF only supports quadratic Beziers)
11
+ # - reverse_contour_direction (TTF winding convention)
12
+ class TtfCompiler < BaseCompiler
13
+ SFNT_VERSION = SFNT_VERSION_TRUE_TYPE
14
+
15
+ def compile(output_path:)
16
+ glyphs = font.glyphs.values
17
+
18
+ # Deep-clone glyphs so filters don't mutate the source UFO.
19
+ filtered = clone_glyphs(glyphs)
20
+ Filters.apply(Filters::TTF_REQUIRED, filtered)
21
+
22
+ glyf_loca = GlyfLoca.build(font, glyphs: filtered)
23
+ loca_format = glyf_loca.delete(:loca_format)
24
+
25
+ tables = {
26
+ "head" => Head.build(font, glyphs: filtered,
27
+ loca_format: loca_format || Head::LOCA_FORMAT_LONG),
28
+ "hhea" => Hhea.build(font, glyphs: filtered),
29
+ "maxp" => Maxp.build(font, glyphs: filtered,
30
+ version: Maxp::VERSION_TRUE_TYPE),
31
+ "OS/2" => Os2.build(font, glyphs: filtered),
32
+ "name" => Name.build(font),
33
+ "post" => Post.build(font),
34
+ "hmtx" => Hmtx.build(font, glyphs: filtered),
35
+ "cmap" => Cmap.build(font, glyphs: filtered),
36
+ "glyf" => glyf_loca["glyf"],
37
+ "loca" => glyf_loca["loca"],
38
+ }
39
+
40
+ write(tables, output_path)
41
+ output_path
42
+ end
43
+
44
+ private
45
+
46
+ def clone_glyphs(glyphs)
47
+ glyphs.map { |g| clone_glyph(g) }
48
+ end
49
+
50
+ def clone_glyph(original)
51
+ copy = Ufo::Glyph.new(name: original.name)
52
+ copy.width = original.width
53
+ copy.height = original.height
54
+ original.unicodes.each { |cp| copy.add_unicode(cp) }
55
+ original.contours.each { |c| copy.add_contour(clone_contour(c)) }
56
+ original.components.each { |c| copy.add_component(c) }
57
+ copy
58
+ end
59
+
60
+ def clone_contour(original)
61
+ points = original.points.map do |p|
62
+ Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
63
+ end
64
+ Ufo::Contour.new(points)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -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