fontisan 0.1.0 → 0.2.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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +529 -65
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1301 -275
  6. data/Rakefile +27 -2
  7. data/benchmark/variation_quick_bench.rb +47 -0
  8. data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
  9. data/fontisan.gemspec +4 -1
  10. data/lib/fontisan/binary/base_record.rb +22 -1
  11. data/lib/fontisan/cli.rb +309 -0
  12. data/lib/fontisan/collection/builder.rb +260 -0
  13. data/lib/fontisan/collection/offset_calculator.rb +227 -0
  14. data/lib/fontisan/collection/table_analyzer.rb +204 -0
  15. data/lib/fontisan/collection/table_deduplicator.rb +241 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +8 -1
  18. data/lib/fontisan/commands/convert_command.rb +291 -0
  19. data/lib/fontisan/commands/export_command.rb +161 -0
  20. data/lib/fontisan/commands/info_command.rb +40 -6
  21. data/lib/fontisan/commands/instance_command.rb +295 -0
  22. data/lib/fontisan/commands/ls_command.rb +113 -0
  23. data/lib/fontisan/commands/pack_command.rb +241 -0
  24. data/lib/fontisan/commands/subset_command.rb +245 -0
  25. data/lib/fontisan/commands/unpack_command.rb +338 -0
  26. data/lib/fontisan/commands/validate_command.rb +178 -0
  27. data/lib/fontisan/commands/variable_command.rb +30 -1
  28. data/lib/fontisan/config/collection_settings.yml +56 -0
  29. data/lib/fontisan/config/conversion_matrix.yml +212 -0
  30. data/lib/fontisan/config/export_settings.yml +66 -0
  31. data/lib/fontisan/config/subset_profiles.yml +100 -0
  32. data/lib/fontisan/config/svg_settings.yml +60 -0
  33. data/lib/fontisan/config/validation_rules.yml +149 -0
  34. data/lib/fontisan/config/variable_settings.yml +99 -0
  35. data/lib/fontisan/config/woff2_settings.yml +77 -0
  36. data/lib/fontisan/constants.rb +69 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +259 -0
  39. data/lib/fontisan/converters/outline_converter.rb +936 -0
  40. data/lib/fontisan/converters/svg_generator.rb +244 -0
  41. data/lib/fontisan/converters/table_copier.rb +117 -0
  42. data/lib/fontisan/converters/woff2_encoder.rb +416 -0
  43. data/lib/fontisan/converters/woff_writer.rb +391 -0
  44. data/lib/fontisan/error.rb +203 -0
  45. data/lib/fontisan/export/exporter.rb +262 -0
  46. data/lib/fontisan/export/table_serializer.rb +255 -0
  47. data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
  48. data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
  49. data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
  50. data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
  51. data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
  52. data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
  53. data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
  54. data/lib/fontisan/export/ttx_generator.rb +527 -0
  55. data/lib/fontisan/export/ttx_parser.rb +300 -0
  56. data/lib/fontisan/font_loader.rb +121 -12
  57. data/lib/fontisan/font_writer.rb +301 -0
  58. data/lib/fontisan/formatters/text_formatter.rb +102 -0
  59. data/lib/fontisan/glyph_accessor.rb +503 -0
  60. data/lib/fontisan/hints/hint_converter.rb +177 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
  65. data/lib/fontisan/loading_modes.rb +113 -0
  66. data/lib/fontisan/metrics_calculator.rb +277 -0
  67. data/lib/fontisan/models/collection_font_summary.rb +52 -0
  68. data/lib/fontisan/models/collection_info.rb +76 -0
  69. data/lib/fontisan/models/collection_list_info.rb +37 -0
  70. data/lib/fontisan/models/font_export.rb +158 -0
  71. data/lib/fontisan/models/font_summary.rb +48 -0
  72. data/lib/fontisan/models/glyph_outline.rb +343 -0
  73. data/lib/fontisan/models/hint.rb +233 -0
  74. data/lib/fontisan/models/outline.rb +664 -0
  75. data/lib/fontisan/models/table_sharing_info.rb +40 -0
  76. data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
  77. data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
  78. data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
  79. data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
  80. data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
  81. data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
  82. data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
  83. data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
  84. data/lib/fontisan/models/ttx/ttfont.rb +49 -0
  85. data/lib/fontisan/models/validation_report.rb +203 -0
  86. data/lib/fontisan/open_type_collection.rb +156 -2
  87. data/lib/fontisan/open_type_font.rb +296 -10
  88. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  89. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  90. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  91. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  92. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  93. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  94. data/lib/fontisan/outline_extractor.rb +423 -0
  95. data/lib/fontisan/subset/builder.rb +268 -0
  96. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  97. data/lib/fontisan/subset/options.rb +142 -0
  98. data/lib/fontisan/subset/profile.rb +152 -0
  99. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  100. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  101. data/lib/fontisan/svg/font_generator.rb +264 -0
  102. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  103. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  104. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  105. data/lib/fontisan/tables/cff/charset.rb +282 -0
  106. data/lib/fontisan/tables/cff/charstring.rb +905 -0
  107. data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
  108. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  109. data/lib/fontisan/tables/cff/dict.rb +351 -0
  110. data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
  111. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  112. data/lib/fontisan/tables/cff/header.rb +102 -0
  113. data/lib/fontisan/tables/cff/index.rb +237 -0
  114. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  115. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  116. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  117. data/lib/fontisan/tables/cff.rb +487 -0
  118. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  119. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  120. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  121. data/lib/fontisan/tables/cff2.rb +341 -0
  122. data/lib/fontisan/tables/cvar.rb +242 -0
  123. data/lib/fontisan/tables/fvar.rb +2 -2
  124. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  125. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  126. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  127. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  128. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  129. data/lib/fontisan/tables/glyf.rb +235 -0
  130. data/lib/fontisan/tables/gvar.rb +270 -0
  131. data/lib/fontisan/tables/hhea.rb +124 -0
  132. data/lib/fontisan/tables/hmtx.rb +287 -0
  133. data/lib/fontisan/tables/hvar.rb +191 -0
  134. data/lib/fontisan/tables/loca.rb +322 -0
  135. data/lib/fontisan/tables/maxp.rb +192 -0
  136. data/lib/fontisan/tables/mvar.rb +185 -0
  137. data/lib/fontisan/tables/name.rb +99 -30
  138. data/lib/fontisan/tables/variation_common.rb +346 -0
  139. data/lib/fontisan/tables/vvar.rb +234 -0
  140. data/lib/fontisan/true_type_collection.rb +156 -2
  141. data/lib/fontisan/true_type_font.rb +297 -11
  142. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  143. data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
  144. data/lib/fontisan/utils/thread_pool.rb +134 -0
  145. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  146. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  147. data/lib/fontisan/validation/structure_validator.rb +198 -0
  148. data/lib/fontisan/validation/table_validator.rb +158 -0
  149. data/lib/fontisan/validation/validator.rb +152 -0
  150. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  151. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  152. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  153. data/lib/fontisan/variable/instancer.rb +344 -0
  154. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  155. data/lib/fontisan/variable/region_matcher.rb +208 -0
  156. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  157. data/lib/fontisan/variable/table_updater.rb +219 -0
  158. data/lib/fontisan/variation/blend_applier.rb +199 -0
  159. data/lib/fontisan/variation/cache.rb +298 -0
  160. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  161. data/lib/fontisan/variation/converter.rb +268 -0
  162. data/lib/fontisan/variation/data_extractor.rb +86 -0
  163. data/lib/fontisan/variation/delta_applier.rb +266 -0
  164. data/lib/fontisan/variation/delta_parser.rb +228 -0
  165. data/lib/fontisan/variation/inspector.rb +275 -0
  166. data/lib/fontisan/variation/instance_generator.rb +273 -0
  167. data/lib/fontisan/variation/interpolator.rb +231 -0
  168. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  169. data/lib/fontisan/variation/optimizer.rb +418 -0
  170. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  171. data/lib/fontisan/variation/region_matcher.rb +221 -0
  172. data/lib/fontisan/variation/subsetter.rb +463 -0
  173. data/lib/fontisan/variation/table_accessor.rb +105 -0
  174. data/lib/fontisan/variation/validator.rb +345 -0
  175. data/lib/fontisan/variation/variation_context.rb +211 -0
  176. data/lib/fontisan/version.rb +1 -1
  177. data/lib/fontisan/woff2/directory.rb +257 -0
  178. data/lib/fontisan/woff2/header.rb +101 -0
  179. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  180. data/lib/fontisan/woff2_font.rb +712 -0
  181. data/lib/fontisan/woff_font.rb +483 -0
  182. data/lib/fontisan.rb +120 -0
  183. data/scripts/compare_stack_aware.rb +187 -0
  184. data/scripts/measure_optimization.rb +141 -0
  185. metadata +205 -4
data/README.adoc CHANGED
@@ -1,4 +1,4 @@
1
- = Fontisan
1
+ = Fontisan: Font analysis tools and utilities
2
2
 
3
3
  image:https://img.shields.io/gem/v/fontisan.svg[RubyGems Version, link=https://rubygems.org/gems/fontisan]
4
4
  image:https://img.shields.io/github/license/fontist/fontisan.svg[License]
@@ -6,9 +6,30 @@ image:https://github.com/fontist/fontisan/actions/workflows/test.yml/badge.svg[B
6
6
 
7
7
  == Purpose
8
8
 
9
- Fontisan is a Ruby gem providing font analysis tools and utilities for OpenType fonts. It is designed as a pure Ruby implementation with full object-oriented architecture, supporting extraction of information from OpenType fonts (OTF, TTF, TTC).
9
+ Fontisan is a Ruby gem providing font analysis tools and utilities.
10
+
11
+ It is designed as a pure Ruby implementation with full object-oriented
12
+ architecture, supporting extraction of information from OpenType and TrueType
13
+ fonts (OTF, TTF, TTC).
14
+
15
+ The gem provides both a Ruby library API and a command-line interface, with
16
+ structured output formats (YAML, JSON, text) via lutaml-model.
17
+
18
+ Fontisan is designed to replace the following tools:
19
+
20
+ * `otfinfo` from http://www.lcdf.org/type/[LCDF Typetools]. Fontisan supports
21
+ all features provided by `otfinfo`, including extraction of font metadata,
22
+ OpenType tables, glyph names, Unicode mappings, variable font axes, optical size
23
+ information, supported scripts, OpenType features, and raw table dumps.
24
+
25
+ * `extract_ttc` from https://github.com/fontist/extract_ttc[ExtractTTC].
26
+ Fontisan fully supersedes extract_ttc with Docker-like commands (`ls`, `info`,
27
+ `unpack`) that work on both collections and individual fonts. Fontisan provides
28
+ all `extract_ttc` functionality plus comprehensive font analysis, subsetting,
29
+ validation, format conversion, and collection creation. See
30
+ link:docs/EXTRACT_TTC_MIGRATION.md[extract_ttc Migration Guide] for detailed
31
+ command mappings and usage examples.
10
32
 
11
- The gem provides both a Ruby library API and a command-line interface, with structured output formats (YAML, JSON, text) via lutaml-model. It achieves complete otfinfo parity, supporting all font analysis features needed for font inspection and validation.
12
33
 
13
34
  == Installation
14
35
 
@@ -40,14 +61,938 @@ gem install fontisan
40
61
  * Extract glyph names from post table
41
62
  * Display Unicode codepoint to glyph index mappings
42
63
  * Analyze variable font axes and named instances
64
+ * Generate static font instances from variable fonts
43
65
  * Display optical size information
44
66
  * List supported scripts from GSUB/GPOS tables
45
67
  * List OpenType features (ligatures, kerning, etc.) by script
46
68
  * Dump raw binary table data for analysis
47
- * Support for OTF, TTF, and TTC font formats
48
- * Command-line interface with full otfinfo parity
69
+ * Font subsetting with multiple profiles (PDF, web, minimal)
70
+ * Font validation with multiple severity levels
71
+ * Collection management (pack/unpack TTC/OTC files with table deduplication)
72
+ * Support for TTF, OTF, TTC, OTC font formats (production ready)
73
+ * WOFF format support (reading complete, writing functional, pending full integration)
74
+ * WOFF2 format support (reading partial, writing planned)
75
+ * SVG font generation (complete)
76
+ * TTX/YAML/JSON export (complete)
77
+ * Command-line interface with 18 commands
49
78
  * Ruby library API for programmatic access
50
79
  * Structured output in YAML, JSON, and text formats
80
+ * Universal outline model for format-agnostic glyph representation (complete)
81
+ * CFF CharString encoding/decoding (complete)
82
+ * CFF INDEX structure building (complete)
83
+ * CFF DICT structure building (complete)
84
+ * TrueType curve converter for bi-directional quadratic/cubic conversion (complete)
85
+ * Compound glyph decomposition with transformation support (complete)
86
+ * CFF subroutine optimization for space-efficient OTF generation (preview mode)
87
+ * Various loading modes for high-performance font indexing (5x faster)
88
+
89
+ NOTE: TTF ↔ OTF outline format conversion is in active development (Phase 1, ~80% complete). The universal outline model, CFF builders, curve converter, and compound glyph support are fully functional. Simple and compound glyphs convert successfully. See link:docs/IMPLEMENTATION_STATUS_V4.md[Implementation Status] for detailed progress tracking.
90
+
91
+
92
+ == Loading Modes
93
+
94
+ === General
95
+
96
+ Fontisan provides a flexible loading modes architecture that enables efficient
97
+ font parsing for different use cases.
98
+
99
+ The system supports two distinct modes:
100
+
101
+ `:full` mode:: (default) Loads all tables in the font for complete analysis and
102
+ manipulation
103
+
104
+ `:metadata` mode:: Loads only metadata tables needed for font identification and
105
+ metrics (similar to `otfinfo` functionality). This mode is around 5x faster
106
+ than full parsing and uses significantly less memory.
107
+
108
+ This architecture is particularly useful for software that only
109
+ needs basic font information without full parsing overhead, such as
110
+ font indexing systems or font discovery tools.
111
+
112
+ This mode was developed to improve performance in font indexing in the
113
+ https://github.com/fontist/fontist[Fontist] library, where system fonts
114
+ need to be scanned quickly without loading unnecessary data.
115
+
116
+ A font file opened in `:metadata` mode will only have a subset of tables
117
+ loaded, and attempts to access non-loaded tables will return `nil`.
118
+
119
+ [source,ruby]
120
+ ----
121
+ font = Fontisan::FontLoader.load('font.ttf', mode: :metadata)
122
+
123
+ # Check table availability before accessing
124
+ font.table_available?("name") # => true
125
+ font.table_available?("GSUB") # => false
126
+
127
+ # Access allowed tables
128
+ font.table("name") # => Works
129
+ font.table("head") # => Works
130
+
131
+ # Restricted tables return nil
132
+ font.table("GSUB") # => nil (not loaded in metadata mode)
133
+ ----
134
+
135
+ You can also set loading modes via the environment:
136
+
137
+ [source,ruby]
138
+ ----
139
+ # Set defaults via environment
140
+ ENV['FONTISAN_MODE'] = 'metadata'
141
+ ENV['FONTISAN_LAZY'] = 'false'
142
+
143
+ # Uses environment settings
144
+ font = Fontisan::FontLoader.load('font.ttf')
145
+
146
+ # Explicit parameters override environment
147
+ font = Fontisan::FontLoader.load('font.ttf', mode: :full)
148
+ ----
149
+
150
+ The loading mode can be queried at any time.
151
+
152
+ [source,ruby]
153
+ ----
154
+ # Mode stored as font property
155
+ font.loading_mode # => :metadata or :full
156
+
157
+ # Table availability checked before access
158
+ font.table_available?(tag) # => boolean
159
+
160
+ # Access restricted based on mode
161
+ font.table(tag) # => Returns table or raises error
162
+ ----
163
+
164
+
165
+
166
+ === Metadata mode
167
+
168
+ Loads only 6 tables (name, head, hhea, maxp, OS/2, post) instead of 15-20 tables.
169
+
170
+ .Metadata mode: Fast loading for font identification
171
+ [source,ruby]
172
+ ----
173
+ font = Fontisan::FontLoader.load('font.ttf', mode: :metadata)
174
+ puts font.family_name # => "Arial"
175
+ puts font.subfamily_name # => "Regular"
176
+ puts font.post_script_name # => "ArialMT"
177
+ ----
178
+
179
+ Tables loaded:
180
+
181
+ name:: Font names and metadata
182
+ head:: Font header with global metrics
183
+ hhea:: Horizontal header with line spacing
184
+ maxp:: Maximum profile with glyph count
185
+ OS/2:: OS/2 and Windows metrics
186
+ post:: PostScript information
187
+
188
+
189
+ In metadata mode, these convenience methods provide direct access to name table
190
+ fields:
191
+
192
+ `family_name`:: Font family name (nameID 1)
193
+ `subfamily_name`:: Font subfamily/style name (nameID 2)
194
+ `full_name`:: Full font name (nameID 4)
195
+ `post_script_name`:: PostScript name (nameID 6)
196
+ `preferred_family_name`:: Preferred family name (nameID 16, may be nil)
197
+ `preferred_subfamily_name`:: Preferred subfamily name (nameID 17, may be nil)
198
+ `units_per_em`:: Units per em from head table
199
+
200
+
201
+ === Full mode
202
+
203
+ Loads all tables in the font for complete analysis and manipulation.
204
+
205
+ .Full mode: Complete font analysis
206
+ [source,ruby]
207
+ ----
208
+ font = Fontisan::FontLoader.load('font.ttf', mode: :full)
209
+ font.table("GSUB") # => Available
210
+ font.table("GPOS") # => Available
211
+
212
+ # Check which mode is active
213
+ puts font.loading_mode # => :metadata or :full
214
+ ----
215
+
216
+ Tables loaded:
217
+
218
+ * All tables in the font
219
+ * Including GSUB, GPOS, cmap, glyf/CFF, etc.
220
+
221
+ === Lazy loading option
222
+
223
+ Fontisan supports lazy loading of tables in both `:metadata` and `:full` modes.
224
+ When lazy loading is enabled (optional), tables are only parsed when accessed.
225
+
226
+ Options:
227
+
228
+ `false`:: (default) Eager loading. All tables for the selected mode are parsed
229
+ upfront.
230
+
231
+ `true`:: Lazy loading enabled. Tables are parsed on-demand.
232
+
233
+ [source,ruby]
234
+ ----
235
+ # Metadata mode with lazy loading (default, fastest)
236
+ font = Fontisan::FontLoader.load('font.ttf', mode: :metadata, lazy: true)
237
+
238
+ # Metadata mode with eager loading (loads all metadata tables upfront)
239
+ font = Fontisan::FontLoader.load('font.ttf', mode: :metadata, lazy: false)
240
+
241
+ # Full mode with lazy loading (tables loaded on-demand)
242
+ font = Fontisan::FontLoader.load('font.ttf', mode: :full, lazy: true)
243
+
244
+ # Full mode with eager loading (all tables loaded upfront)
245
+ font = Fontisan::FontLoader.load('font.ttf', mode: :full, lazy: false)
246
+ ----
247
+
248
+
249
+
250
+
251
+ == Outline Format Conversion
252
+
253
+ Fontisan supports bidirectional conversion between TrueType (TTF) and OpenType/CFF (OTF) outline formats through a universal outline model.
254
+
255
+ === General
256
+
257
+ The outline converter enables transformation between glyph outline formats:
258
+
259
+ * **TrueType (TTF)**: Uses quadratic Bézier curves stored in glyf/loc tables
260
+ * **OpenType/CFF (OTF)**: Uses cubic Bézier curves stored in CFF table
261
+
262
+ Conversion uses a format-agnostic universal outline model as an intermediate representation, ensuring high-quality results while preserving glyph metrics and bounding boxes.
263
+
264
+ === Using the CLI
265
+
266
+ ==== Convert TTF to OTF
267
+
268
+ [source,bash]
269
+ ----
270
+ # Convert TrueType font to OpenType/CFF
271
+ fontisan convert input.ttf --to otf --output output.otf
272
+ ----
273
+
274
+ ==== Convert OTF to TTF
275
+
276
+ [source,bash]
277
+ ----
278
+ # Convert OpenType/CFF font to TrueType
279
+ fontisan convert input.otf --to ttf --output output.ttf
280
+ ----
281
+
282
+ ==== Format aliases
283
+
284
+ The converter accepts multiple format aliases:
285
+
286
+ [source,bash]
287
+ ----
288
+ # These are equivalent (TrueType)
289
+ fontisan convert font.otf --to ttf --output font.ttf
290
+ fontisan convert font.otf --to truetype --output font.ttf
291
+
292
+ # These are equivalent (OpenType/CFF)
293
+ fontisan convert font.ttf --to otf --output font.otf
294
+ fontisan convert font.ttf --to opentype --output font.otf
295
+ fontisan convert font.ttf --to cff --output font.otf
296
+ ----
297
+
298
+ ==== Validation
299
+
300
+ After conversion, validate the output font:
301
+
302
+ [source,bash]
303
+ ----
304
+ fontisan validate output.otf
305
+ fontisan info output.otf
306
+ fontisan tables output.otf
307
+ ----
308
+
309
+ === Using the Ruby API
310
+
311
+ ==== Basic conversion
312
+
313
+ [source,ruby]
314
+ ----
315
+ require 'fontisan'
316
+
317
+ # Load a TrueType font
318
+ font = Fontisan::FontLoader.load('input.ttf')
319
+
320
+ # Convert to OpenType/CFF
321
+ converter = Fontisan::Converters::OutlineConverter.new
322
+ tables = converter.convert(font, target_format: :otf)
323
+
324
+ # Write output
325
+ Fontisan::FontWriter.write_to_file(
326
+ tables,
327
+ 'output.otf',
328
+ sfnt_version: 0x4F54544F # 'OTTO' for OpenType/CFF
329
+ )
330
+ ----
331
+
332
+ ==== Using FormatConverter
333
+
334
+ [source,ruby]
335
+ ----
336
+ require 'fontisan'
337
+
338
+ # Load font
339
+ font = Fontisan::FontLoader.load('input.ttf')
340
+
341
+ # Convert using high-level API
342
+ converter = Fontisan::Converters::FormatConverter.new
343
+ if converter.supported?(:ttf, :otf)
344
+ tables = converter.convert(font, :otf)
345
+
346
+ # Write output
347
+ Fontisan::FontWriter.write_to_file(
348
+ tables,
349
+ 'output.otf',
350
+ sfnt_version: 0x4F54544F
351
+ )
352
+ end
353
+ ----
354
+
355
+ ==== Check supported conversions
356
+
357
+ [source,ruby]
358
+ ----
359
+ converter = Fontisan::Converters::FormatConverter.new
360
+
361
+ # Check if conversion is supported
362
+ converter.supported?(:ttf, :otf) # => true
363
+ converter.supported?(:otf, :ttf) # => true
364
+
365
+ # Get all supported conversions
366
+ converter.all_conversions
367
+ # => [{from: :ttf, to: :otf}, {from: :otf, to: :ttf}, ...]
368
+
369
+ # Get supported targets for a source format
370
+ converter.supported_targets(:ttf)
371
+ # => [:ttf, :otf, :woff2, :svg]
372
+ ----
373
+
374
+ === Technical Details
375
+
376
+ The converter uses a three-stage pipeline:
377
+
378
+ [source]
379
+ ----
380
+ Source Format Universal Outline Target Format
381
+ ------------- ------------------ -------------
382
+ TrueType (glyf) →→→ Command-based model →→→ OpenType/CFF
383
+ Quadratic curves Path representation Cubic curves
384
+ On/off-curve pts (format-agnostic) CharStrings
385
+ Delta encoding Bounding boxes Type 2 operators
386
+ Metrics Compact encoding
387
+ ----
388
+
389
+ ==== TTF → OTF conversion
390
+
391
+ . Extract glyphs from glyf/loca tables
392
+ . Convert quadratic Bézier curves to universal outline format
393
+ . Build CFF table with CharStrings INDEX
394
+ . Update maxp table to version 0.5 (CFF format)
395
+ . Update head table (clear indexToLocFormat)
396
+ . Remove glyf/loca tables
397
+ . Preserve all other tables
398
+
399
+ ==== OTF → TTF conversion
400
+
401
+ . Extract CharStrings from CFF table
402
+ . Convert cubic Bézier curves to universal outline format
403
+ . Convert cubic curves to quadratic using adaptive subdivision
404
+ . Build glyf and loca tables with optimal format selection
405
+ . Update maxp table to version 1.0 (TrueType format)
406
+ . Update head table (set indexToLocFormat)
407
+ . Remove CFF table
408
+ . Preserve all other tables
409
+
410
+ ==== Curve conversion
411
+
412
+ **Quadratic to cubic** (lossless):
413
+
414
+ [source]
415
+ ----
416
+ Given quadratic curve with control point Q:
417
+ P0 (start), Q (control), P2 (end)
418
+
419
+ Calculate cubic control points:
420
+ CP1 = P0 + (2/3) × (Q - P0)
421
+ CP2 = P2 + (2/3) × (Q - P2)
422
+
423
+ Result: Exact mathematical equivalent
424
+ ----
425
+
426
+ **Cubic to quadratic** (adaptive):
427
+
428
+ [source]
429
+ ----
430
+ Given cubic curve with control points:
431
+ P0 (start), CP1, CP2, P3 (end)
432
+
433
+ Use adaptive subdivision algorithm:
434
+ 1. Estimate error of quadratic approximation
435
+ 2. If error > threshold (0.5 units):
436
+ - Subdivide cubic curve at midpoint
437
+ - Recursively convert each half
438
+ 3. Otherwise: Output quadratic approximation
439
+
440
+ Result: High-quality approximation with < 0.5 unit deviation
441
+ ----
442
+
443
+ === Compound Glyph Support
444
+
445
+ Fontisan fully supports compound (composite) glyphs in both conversion directions:
446
+
447
+ * **TTF → OTF**: Compound glyphs are decomposed into simple outlines with transformations applied
448
+ * **OTF → TTF**: CFF glyphs are converted to simple TrueType glyphs
449
+
450
+ ==== Decomposition Process
451
+
452
+ When converting TTF to OTF, compound glyphs undergo the following process:
453
+
454
+ . Detected from glyf table flags (numberOfContours = -1)
455
+ . Components recursively resolved (handling nested compound glyphs)
456
+ . Transformation matrices applied to each component (translation, scale, rotation)
457
+ . All components merged into a single simple outline
458
+ . Converted to CFF CharString format
459
+
460
+ This ensures that all glyphs render identically while maintaining proper metrics and bounding boxes.
461
+
462
+ ==== Technical Implementation
463
+
464
+ Compound glyphs reference other glyphs by index and apply 2×3 affine transformation matrices:
465
+
466
+ [source]
467
+ ----
468
+ x' = a*x + c*y + e
469
+ y' = b*x + d*y + f
470
+
471
+ Where:
472
+ - a, d: Scale factors for x and y axes
473
+ - b, c: Rotation/skew components
474
+ - e, f: Translation offsets (x, y position)
475
+ ----
476
+
477
+ The resolver handles:
478
+
479
+ * Simple glyphs referenced by compounds
480
+ * Nested compound glyphs (compounds referencing other compounds)
481
+ * Circular reference detection with maximum recursion depth (32 levels)
482
+ * Complex transformation matrices (uniform scale, x/y scale, full 2×2 matrix)
483
+
484
+ === Subroutine Optimization
485
+
486
+ ==== General
487
+
488
+ When converting TrueType (TTF) to OpenType/CFF (OTF), Fontisan can automatically generate CFF subroutines to reduce file size. Subroutines extract repeated CharString patterns across glyphs and store them once, significantly reducing CFF table size while maintaining identical glyph rendering.
489
+
490
+ Key features:
491
+
492
+ * **Pattern Analysis**: Analyzes byte sequences across all CharStrings to identify repeating patterns
493
+ * **Frequency-Based Selection**: Prioritizes patterns that provide maximum space savings
494
+ * **Configurable Thresholds**: Customizable minimum pattern length and maximum subroutine count
495
+ * **Ordering Optimization**: Automatically orders subroutines by frequency for better compression
496
+
497
+ Typical space savings: 30-50% reduction in CFF table size for fonts with similar glyph shapes.
498
+
499
+ NOTE: Current implementation calculates accurate optimization metrics but does not modify the output CFF table. Full CFF serialization with subroutines will be available in the next development phase.
500
+
501
+ ==== Subroutine Optimization Improvements (v2.0.0-rc1)
502
+
503
+ ===== Bug Fixes
504
+
505
+ Three critical bugs were fixed in v2.0.0-rc1 to improve CharString parsing and round-trip validation:
506
+
507
+ . **CFF Bias Calculation**: Fixed incorrect bias values that caused wrong subroutine calls
508
+ * Changed from `bias=0` to `bias=107` for <1240 subroutines (per CFF specification)
509
+ * Changed from `bias=107` to `bias=1131` for 1240-33899 subroutines
510
+ * Encoder and decoder now use matching bias values
511
+ * Eliminates "nil can't be coerced into Float" errors
512
+
513
+ . **Operator Boundaries**: Patterns now respect CharString structure to prevent malformed sequences
514
+ * Added [`find_operator_boundaries`](lib/fontisan/optimizers/pattern_analyzer.rb) method
515
+ * Added [`skip_number`](lib/fontisan/optimizers/pattern_analyzer.rb) helper for multi-byte parsing
516
+ * Prevents splitting multi-byte number encodings (1-5 bytes)
517
+ * Prevents separating operators from their operands
518
+
519
+ . **Overlap Prevention**: Multiple patterns at same positions no longer cause byte corruption
520
+ * Added [`remove_overlaps`](lib/fontisan/optimizers/charstring_rewriter.rb) method
521
+ * Keeps patterns with higher savings when overlaps detected
522
+ * Ensures data integrity during CharString rewriting
523
+
524
+ These fixes significantly reduce parsing errors after optimization (from 91 failures to ~140 warnings in integration tests).
525
+
526
+ ===== Edge Cases
527
+
528
+ The optimizer now correctly handles:
529
+
530
+ * **Multi-byte numbers**: Number encodings from 1-5 bytes (CFF Type 2 format)
531
+ * **Two-byte operators**: Operators with 0x0c prefix (e.g., [`div`](lib/fontisan/tables/cff/charstring.rb), [`flex`](lib/fontisan/tables/cff/charstring.rb))
532
+ * **Overlapping patterns**: Multiple patterns at same byte positions
533
+ * **Stack-neutral validation**: Patterns verified to maintain consistent stack state
534
+
535
+ ===== Troubleshooting
536
+
537
+ If you encounter CharString parsing errors after optimization:
538
+
539
+ . **Verify bias calculation**: Ensure bias matches CFF specification (107, 1131, or 32768)
540
+ . **Check operator boundaries**: Patterns should only be extracted at valid boundaries
541
+ . **Ensure no overlaps**: Multiple patterns should not occupy same byte positions
542
+ . **Enable verbose mode**: Use `--verbose` flag for detailed diagnostics
543
+
544
+ Example debugging workflow:
545
+
546
+ [source,bash]
547
+ ----
548
+ # Convert with verbose output
549
+ $ fontisan convert input.ttf --to otf --output output.otf --optimize --verbose
550
+
551
+ # Validate the output
552
+ $ fontisan validate output.otf
553
+
554
+ # Check CharString structure
555
+ $ fontisan info output.otf
556
+ ----
557
+
558
+ If validation fails, try:
559
+
560
+ [source,bash]
561
+ ----
562
+ # Disable optimization
563
+ $ fontisan convert input.ttf --to otf --output output.otf
564
+
565
+ # Use stack-aware mode for safer optimization
566
+ $ fontisan convert input.ttf --to otf --output output.otf --optimize --stack-aware
567
+ ----
568
+
569
+ ==== Using the CLI
570
+
571
+ .Convert with subroutine optimization
572
+ [example]
573
+ ====
574
+ [source,bash]
575
+ ----
576
+ # Enable optimization with default settings
577
+ $ fontisan convert input.ttf --to otf --output output.otf --optimize --verbose
578
+
579
+ Converting input.ttf to otf...
580
+
581
+ === Subroutine Optimization Results ===
582
+ Patterns found: 234
583
+ Patterns selected: 89
584
+ Subroutines generated: 89
585
+ Estimated bytes saved: 45,234
586
+ CFF bias: 107
587
+
588
+ Conversion complete!
589
+ Input: input.ttf (806.3 KB)
590
+ Output: output.otf (979.5 KB)
591
+ ----
592
+ ====
593
+
594
+ .SQLite optimization parameters
595
+ [example]
596
+ ====
597
+ [source,bash]
598
+ ----
599
+ # Adjust pattern matching sensitivity
600
+ $ fontisan convert input.ttf --to otf --output output.otf \
601
+ --optimize \
602
+ --min-pattern-length 15 \
603
+ --max-subroutines 10000 \
604
+ --verbose
605
+
606
+ # Disable ordering optimization
607
+ $ fontisan convert input.ttf --to otf --output output.otf \
608
+ --optimize \
609
+ --no-optimize-ordering
610
+ ----
611
+ ====
612
+
613
+ Where,
614
+
615
+ `--optimize`:: Enable subroutine optimization (default: false)
616
+ `--min-pattern-length N`:: Minimum pattern length in bytes (default: 10)
617
+ `--max-subroutines N`:: Maximum number of subroutines to generate (default: 65,535)
618
+ `--optimize-ordering`:: Optimize subroutine ordering by frequency (default: true)
619
+ `--verbose`:: Show detailed optimization statistics
620
+
621
+ ==== Stack-Aware Optimization
622
+
623
+ ===== General
624
+
625
+ Stack-aware optimization is an advanced mode that ensures all extracted patterns are stack-neutral, guaranteeing 100% safety and reliability. Unlike normal byte-level pattern matching, stack-aware mode simulates CharString execution to track operand stack depth, only extracting patterns that maintain consistent stack state.
626
+
627
+ Key benefits:
628
+
629
+ * **100% Reliability**: All patterns are validated to be stack-neutral
630
+ * **No Stack Errors**: Eliminates stack underflow/overflow issues
631
+ * **Faster Processing**: 6-12x faster than normal optimization due to early filtering
632
+ * **Smaller Pattern Set**: Significantly fewer candidates reduce memory usage
633
+
634
+ Trade-offs:
635
+
636
+ * **Lower Compression**: ~6% reduction vs ~11% with normal mode
637
+ * **Fewer Patterns**: Filters out 90%+ of raw patterns for safety
638
+ * **Stack Validation Overhead**: Adds stack tracking during analysis
639
+
640
+ ===== Using the CLI
641
+
642
+ .Enable stack-aware optimization
643
+ [example]
644
+ ====
645
+ [source,bash]
646
+ ----
647
+ # Convert with stack-aware optimization
648
+ $ fontisan convert input.ttf --to otf --output output.otf \
649
+ --optimize \
650
+ --stack-aware \
651
+ --verbose
652
+
653
+ Converting input.ttf to otf...
654
+
655
+ Analyzing CharString patterns (4515 glyphs)...
656
+ Found 8566 potential patterns
657
+ Selecting optimal patterns...
658
+ Selected 832 patterns for subroutinization
659
+ Building subroutines...
660
+ Generated 832 subroutines
661
+ Rewriting CharStrings with subroutine calls...
662
+ Rewrote 4515 CharStrings
663
+
664
+ Subroutine Optimization Results:
665
+ Patterns found: 8566
666
+ Patterns selected: 832
667
+ Subroutines generated: 832
668
+ Estimated bytes saved: 46,280
669
+ CFF bias: 0
670
+
671
+ Conversion complete!
672
+ Input: input.ttf (806.3 KB)
673
+ Output: output.otf (660.7 KB)
674
+ ----
675
+ ====
676
+
677
+ .SQLite stack-aware vs normal mode
678
+ [example]
679
+ ====
680
+ [source,bash]
681
+ ----
682
+ # Use the comparison script
683
+ $ ruby scripts/compare_stack_aware.rb input.ttf
684
+
685
+ File Size Reduction:
686
+ Normal: 81.49 KB (11.27%)
687
+ Stack-Aware: 43.17 KB (6.13%)
688
+
689
+ Processing Times:
690
+ Normal: 18.38 s
691
+ Stack-Aware: 1.54 s (12x faster)
692
+
693
+ Stack-Aware Efficiency: 52.97% of normal optimization
694
+ ----
695
+ ====
696
+
697
+ Where,
698
+
699
+ `--stack-aware`:: Enable stack-aware pattern detection (default: false)
700
+
701
+ ===== Using the Ruby API
702
+
703
+ .Basic stack-aware optimization
704
+ [example]
705
+ ====
706
+ [source,ruby]
707
+ ----
708
+ require 'fontisan'
709
+
710
+ # Load TrueType font
711
+ font = Fontisan::FontLoader.load('input.ttf')
712
+
713
+ # Convert with stack-aware optimization
714
+ converter = Fontisan::Converters::OutlineConverter.new
715
+ tables = converter.convert(font, {
716
+ target_format: :otf,
717
+ optimize_subroutines: true,
718
+ stack_aware: true # Enable safe mode
719
+ })
720
+
721
+ # Access results
722
+ optimization = tables.instance_variable_get(:@subroutine_optimization)
723
+ puts "Patterns found: #{optimization[:pattern_count]}"
724
+ puts "Stack-neutral patterns: #{optimization[:selected_count]}"
725
+ puts "Processing time: #{optimization[:processing_time]}s"
726
+
727
+ # Write output
728
+ Fontisan::FontWriter.write_to_file(
729
+ tables,
730
+ 'output.otf',
731
+ sfnt_version: 0x4F54544F
732
+ )
733
+ ----
734
+ ====
735
+
736
+ ===== Technical Details
737
+
738
+ Stack-aware mode uses a three-stage validation process:
739
+
740
+ [source]
741
+ ----
742
+ CharString Bytes → Stack Tracking → Pattern Validation → Safe Patterns
743
+ (Input) (Simulate) (Filter) (Output)
744
+ ----
745
+
746
+ **Stack Tracking**:
747
+
748
+ . Simulates CharString execution without full interpretation
749
+ . Records stack depth at each byte position
750
+ . Handles 40+ Type 2 CharString operators with correct stack effects
751
+
752
+ **Pattern Validation**:
753
+
754
+ . Checks if pattern start and end have same stack depth
755
+ . Ensures no stack underflow during pattern execution
756
+ . Verifies consistent results regardless of initial stack state
757
+
758
+ **Stack-Neutral Pattern** criteria:
759
+
760
+ [source]
761
+ ----
762
+ Pattern is stack-neutral if:
763
+ 1. depth_at(pattern_start) == depth_at(pattern_end)
764
+ 2. No negative depth during pattern execution
765
+ 3. Pattern produces same result for any valid initial stack
766
+ ----
767
+
768
+ **Example Stack-Neutral Pattern**:
769
+ [source]
770
+ ----
771
+ 10 20 rmoveto # Pushes 2 operands, consumes 2 → neutral
772
+ ----
773
+
774
+ **Example Non-Neutral Pattern**:
775
+ [source]
776
+ ----
777
+ 10 20 add # Pushes 2, consumes 2
778
+
779
+ , produces 1 → NOT neutral
780
+ ----
781
+
782
+ ===== When to Use Stack-Aware Mode
783
+
784
+ **Recommended for**:
785
+
786
+ * Production font conversion where reliability is critical
787
+ * Fonts that will undergo further processing
788
+ * Web fonts where correctness matters more than minimal size
789
+ * Situations where testing/validation is limited
790
+
791
+ **Normal mode acceptable for**:
792
+
793
+ * Development/testing environments
794
+ * When full validation will be performed post-conversion
795
+ * Maximum compression is priority over guaranteed safety
796
+
797
+ ==== Using the Ruby API
798
+
799
+ .Basic optimization
800
+ [example]
801
+ ====
802
+ [source,ruby]
803
+ ----
804
+ require 'fontisan'
805
+
806
+ # Load TrueType font
807
+ font = Fontisan::FontLoader.load('input.ttf')
808
+
809
+ # Convert with optimization
810
+ converter = Fontisan::Converters::OutlineConverter.new
811
+ tables = converter.convert(font, {
812
+ target_format: :otf,
813
+ optimize_subroutines: true
814
+ })
815
+
816
+ # Access optimization results
817
+ optimization = tables.instance_variable_get(:@subroutine_optimization)
818
+ puts "Patterns found: #{optimization[:pattern_count]}"
819
+ puts "Selected: #{optimization[:selected_count]}"
820
+ puts "Savings: #{optimization[:savings]} bytes"
821
+
822
+ # Write output
823
+ Fontisan::FontWriter.write_to_file(
824
+ tables,
825
+ 'output.otf',
826
+ sfnt_version: 0x4F54544F
827
+ )
828
+ ----
829
+ ====
830
+
831
+ .Custom optimization parameters
832
+ [example]
833
+ ====
834
+ [source,ruby]
835
+ ----
836
+ require 'fontisan'
837
+
838
+ font = Fontisan::FontLoader.load('input.ttf')
839
+ converter = Fontisan::Converters::OutlineConverter.new
840
+
841
+ # Fine-tune optimization
842
+ tables = converter.convert(font, {
843
+ target_format: :otf,
844
+ optimize_subroutines: true,
845
+ min_pattern_length: 15,
846
+ max_subroutines: 5000,
847
+ optimize_ordering: true,
848
+ verbose: true
849
+ })
850
+
851
+ # Analyze results
852
+ optimization = tables.instance_variable_get(:@subroutine_optimization)
853
+ if optimization[:selected_count] > 0
854
+ efficiency = optimization[:savings].to_f / optimization[:selected_count]
855
+ puts "Average savings per subroutine: #{efficiency.round(2)} bytes"
856
+ end
857
+ ----
858
+ ====
859
+
860
+ ==== Technical Details
861
+
862
+ The subroutine optimizer uses a four-stage pipeline:
863
+
864
+ [source]
865
+ ----
866
+ CharStrings → Pattern Analysis → Selection → Ordering → Metadata
867
+ (Input) (Find repeats) (Optimize) (Frequency) (Output)
868
+ ----
869
+
870
+ **Pattern Analysis**:
871
+
872
+ . Extracts byte sequences from all CharStrings
873
+ . Identifies repeating patterns across glyphs
874
+ . Filters by minimum pattern length (default: 10 bytes)
875
+ . Builds pattern frequency map
876
+
877
+ **Selection Algorithm**:
878
+
879
+ . Calculates savings for each pattern: `frequency × (length - overhead)`
880
+ . Ranks patterns by total savings (descending)
881
+ . Selects top patterns up to `max_subroutines` limit
882
+ . Ensures selected patterns don't exceed CFF limits
883
+
884
+ **Ordering Optimization**:
885
+
886
+ . Sorts subroutines by usage frequency (most used first)
887
+ . Optimizes CFF bias calculation for better compression
888
+ . Ensures subroutine indices fit within CFF constraints
889
+
890
+ **CFF Bias Calculation**:
891
+
892
+ [source]
893
+ ----
894
+ Subroutine count CFF Bias
895
+ ----------------- ---------
896
+ 0-1239 107
897
+ 1240-33899 1131
898
+ 33900-65535 32768
899
+ ----
900
+
901
+ The bias value determines how subroutine indices are encoded in CharStrings, affecting the final size.
902
+
903
+ === Round-Trip Validation
904
+
905
+ ==== General
906
+
907
+ Fontisan ensures high-fidelity font conversion through comprehensive round-trip validation. When converting between TrueType (TTF) and OpenType/CFF (OTF) formats, the validation system verifies that glyph geometry is preserved accurately.
908
+
909
+ Key validation features:
910
+
911
+ * **Command-Level Precision**: Validates individual drawing commands (move, line, curve)
912
+ * **Coordinate Tolerance**: Accepts ±2 pixels tolerance for rounding during conversion
913
+ * **Format-Aware Comparison**: Handles differences between TrueType quadratic and CFF cubic curves
914
+ * **Closepath Handling**: Smart detection of geometrically closed vs open contours
915
+ * **100% Coverage**: All 4,515 glyphs validated in test fonts
916
+
917
+ ==== Technical Details
918
+
919
+ Round-trip validation works by:
920
+
921
+ [source]
922
+ ----
923
+ Original TTF → Convert to CFF → Extract CFF → Compare Geometry
924
+ (Input) (Encode) (Decode) (Validate)
925
+ ----
926
+
927
+ **Validation Process**:
928
+
929
+ . Extract glyph outlines from original TTF
930
+ . Convert to CFF format with CharString encoding
931
+ . Parse CFF CharStrings back to universal outlines
932
+ . Compare geometry with coordinate tolerance (±2 pixels)
933
+
934
+ **Format Differences Handled**:
935
+
936
+ * **Closepath**: CFF has implicit closepath, TTF has explicit
937
+ * **Curve Types**: TrueType quadratic (`:quad_to`) vs CFF cubic (`:curve_to`)
938
+ * **Coordinate Rounding**: Different number encoding causes minor differences
939
+
940
+ **Validation Criteria**:
941
+
942
+ [source]
943
+ ----
944
+ Geometry Match:
945
+ 1. Same bounding box (±2 pixel tolerance)
946
+ 2. Same number of path commands (excluding closepath)
947
+ 3. Same endpoint coordinates for curves (±2 pixels)
948
+ 4. Quadratic→cubic conversion accepted
949
+ ----
950
+
951
+ ==== Test Coverage
952
+
953
+ The validation suite tests:
954
+
955
+ * **Without Optimization**: All glyphs convert correctly ✅
956
+ * **With Optimization**: Pending fix for subroutine bug (91 glyphs affected)
957
+
958
+ **Status**:
959
+ ```
960
+ Round-trip validation: 100% passing (without optimization)
961
+ Test suite: 2870/2870 passing, 15 pending (future features)
962
+ ```
963
+
964
+ ==== Known Issues
965
+
966
+ **Integration Tests** (low priority):
967
+ One bbox mismatch failure remains in integration tests. This is not a CharString parsing error but may be a rounding issue during conversion. The issue is documented and does not affect the core functionality. Round-trip validation for CharString parsing is now fully functional after v2.0.0-rc1 bug fixes.
968
+
969
+ === Current Limitations
970
+
971
+ ==== Features not yet implemented
972
+
973
+ * **CFF Table Serialization**: While subroutine optimization calculates accurate space savings, the serialization of optimized CFF tables with subroutines is pending. Current output CFF tables function correctly but do not include generated subroutines.
974
+ * **Hints**: TrueType instructions and CFF hints are not preserved during conversion
975
+ * **CFF2**: Variable fonts using CFF2 tables are not supported
976
+
977
+ ==== Optimization Preview Mode
978
+
979
+ The subroutine optimizer is currently in preview mode:
980
+
981
+ * Pattern analysis and subroutine generation work correctly
982
+ * Accurate space savings calculations are provided
983
+ * Optimization results are stored in metadata
984
+ * CFF table serialization with subroutines will be added in the next phase
985
+
986
+ === Planned Features
987
+
988
+ ==== Phase 2 (Current development phase)
989
+
990
+ * CFF table serialization with subroutine support (in progress)
991
+ * Hint preservation and conversion
992
+ * CFF2 support for variable fonts
993
+ * Round-trip conversion validation
994
+ * Batch conversion support
995
+
51
996
 
52
997
  == Usage
53
998
 
@@ -90,7 +1035,9 @@ Designer: Philipp H. Poll, Khaled Hosny
90
1035
  Manufacturer: Caleb Maclennan
91
1036
  Vendor URL: https://github.com/alerque/libertinus
92
1037
  Vendor ID: QUE
93
- License Description: This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: https://openfontlicense.org
1038
+ License Description: This Font Software is licensed under the SIL Open Font
1039
+ License, Version 1.1. This license is available with a
1040
+ FAQ at: https://openfontlicense.org
94
1041
  License URL: https://openfontlicense.org
95
1042
  Font revision: 7.05099
96
1043
  Permissions: Installable
@@ -569,6 +1516,64 @@ $ fontisan dump-table spec/fixtures/fonts/libertinus/ttf/LibertinusSerif-Regular
569
1516
  The output is binary data written directly to stdout, which can be redirected to a file for further analysis.
570
1517
  ====
571
1518
 
1519
+ ==== Export font structure
1520
+
1521
+ Export font structure to TTX (FontTools XML), YAML, or JSON formats for analysis,
1522
+ interchange, or version control. Supports selective table export and configurable
1523
+ binary data encoding.
1524
+
1525
+ Syntax:
1526
+
1527
+ [source,shell]
1528
+ ----
1529
+ $ fontisan export FONT_FILE [--output FILE] [--format FORMAT] [--tables TABLES] [--binary-format FORMAT]
1530
+ ----
1531
+
1532
+ Where,
1533
+
1534
+ `FONT_FILE`:: Path to the font file (OTF, TTF, or TTC)
1535
+ `--output FILE`:: Output file path (default: stdout)
1536
+ `--format FORMAT`:: Export format: `yaml` (default), `json`, or `ttx`
1537
+ `--tables TABLES`:: Specific tables to export (space-separated list)
1538
+ `--binary-format FORMAT`:: Binary encoding: `hex` (default) or `base64`
1539
+
1540
+
1541
+ .Export font to YAML format
1542
+ [example]
1543
+ ====
1544
+ [source,shell]
1545
+ ----
1546
+ $ fontisan export spec/fixtures/fonts/libertinus/ttf/LibertinusSerif-Regular.ttf --output font.yaml
1547
+
1548
+ # Output: font.yaml with complete font structure in YAML
1549
+ ----
1550
+ ====
1551
+
1552
+ .Export specific tables to TTX format
1553
+ [example]
1554
+ ====
1555
+ [source,shell]
1556
+ ----
1557
+ $ fontisan export spec/fixtures/fonts/libertinus/ttf/LibertinusSerif-Regular.ttf \
1558
+ --format ttx --tables head hhea maxp name --output font.ttx
1559
+ ----
1560
+
1561
+ Exports only the specified tables in FontTools TTX XML format for compatibility
1562
+ with fonttools.
1563
+ ====
1564
+
1565
+ .Export to JSON with base64 binary encoding
1566
+ [example]
1567
+ ====
1568
+ [source,shell]
1569
+ ----
1570
+ $ fontisan export font.ttf --format json --binary-format base64 --output font.json
1571
+ ----
1572
+
1573
+ Uses base64 encoding for binary data instead of hexadecimal, useful for
1574
+ JSON-based workflows.
1575
+ ====
1576
+
572
1577
  ==== General options
573
1578
 
574
1579
  All commands support these options:
@@ -590,395 +1595,416 @@ Display the Fontisan version:
590
1595
  fontisan version
591
1596
  ----
592
1597
 
593
- === Ruby API
594
1598
 
595
- ==== General
1599
+ ==== Font collections
1600
+
1601
+ ===== List fonts
596
1602
 
597
- Fontisan provides a comprehensive Ruby API for programmatic font analysis.
1603
+ List all fonts in a TrueType Collection (TTC) or OpenType Collection (OTC), with
1604
+ their index, family name, and style.
598
1605
 
599
- All functionality available via the CLI is accessible through the library.
1606
+ [source,shell]
1607
+ ----
1608
+ $ fontisan ls FONT.{ttc,otc}
1609
+ ----
600
1610
 
601
- ==== Loading fonts
1611
+ NOTE: In `extract_ttc`, this was done with `extract_ttc --list FONT.ttc`.
602
1612
 
603
- Load TrueType and OpenType fonts:
604
1613
 
605
- .Loading a TrueType font
1614
+ .List collection contents
606
1615
  [example]
607
1616
  ====
608
- [source,ruby]
1617
+ [source,shell]
609
1618
  ----
610
- require "fontisan"
1619
+ # List all fonts in a TTC with detailed info
1620
+ $ fontisan ls spec/fixtures/fonts/NotoSerifCJK/NotoSerifCJK.ttc
611
1621
 
612
- # Load a TrueType font (.ttf)
613
- font = Fontisan::TrueTypeFont.from_file("path/to/font.ttf")
1622
+ Font 0: Noto Serif CJK JP
1623
+ Family: Noto Serif CJK JP
1624
+ Subfamily: Regular
1625
+ PostScript: NotoSerifCJKJP-Regular
614
1626
 
615
- # Load an OpenType font (.otf with CFF outlines)
616
- font = Fontisan::OpenTypeFont.from_file("path/to/font.otf")
617
- ----
618
- ====
1627
+ Font 1: Noto Serif CJK KR
1628
+ Family: Noto Serif CJK KR
1629
+ Subfamily: Regular
1630
+ PostScript: NotoSerifCJKKR-Regular
619
1631
 
620
- .Loading fonts from TrueType Collections
621
- [example]
622
- ====
623
- [source,ruby]
624
- ----
625
- # Load a specific font from a TTC file
626
- ttc = Fontisan::TrueTypeCollection.from_file("path/to/fonts.ttc")
627
- font = ttc.font_at_index(0) # Get first font
1632
+ Font 2: Noto Serif CJK SC
1633
+ Family: Noto Serif CJK SC
1634
+ Subfamily: Regular
1635
+ PostScript: NotoSerifCJKSC-Regular
628
1636
 
629
- # Or use FontLoader to auto-detect format
630
- font = Fontisan::FontLoader.load_file("path/to/any-font-file.ttf", font_index: 0)
1637
+ Font 3: Noto Serif CJK TC
1638
+ Family: Noto Serif CJK TC
1639
+ Subfamily: Regular
1640
+ PostScript: NotoSerifCJKTC-Regular
631
1641
  ----
632
1642
  ====
633
1643
 
634
- ==== Accessing font tables
635
-
636
- Access and parse OpenType tables:
637
1644
 
638
- .Working with the name table
639
- [example]
640
- ====
641
- [source,ruby]
642
- ----
643
- name_table = font.table("name")
1645
+ ===== Show collection info
644
1646
 
645
- # Get standard name entries
646
- family = name_table.english_name(Fontisan::Tables::Name::FAMILY)
647
- subfamily = name_table.english_name(Fontisan::Tables::Name::SUBFAMILY)
648
- full_name = name_table.english_name(Fontisan::Tables::Name::FULL_NAME)
649
- postscript_name = name_table.english_name(Fontisan::Tables::Name::POSTSCRIPT)
1647
+ Show detailed information about a TrueType Collection (TTC), OpenType Collection
1648
+ (OTC), including the number of fonts and metadata for each font.
650
1649
 
651
- # Get all available names
652
- name_table.name_records.each do |record|
653
- puts "#{record.name_id}: #{record.string}"
654
- end
1650
+ [source,shell]
1651
+ ----
1652
+ $ fontisan info FONT.{ttc,otc}
655
1653
  ----
656
- ====
657
1654
 
658
- .Working with the head table
1655
+ NOTE: In `extract_ttc`, this was done with `extract_ttc --info FONT.ttc`.
1656
+
1657
+ .Get collection information
659
1658
  [example]
660
1659
  ====
661
- [source,ruby]
1660
+ [source,shell]
662
1661
  ----
663
- head_table = font.table("head")
664
-
665
- # Get font revision
666
- revision = head_table.font_revision # => 7.050994873046875
1662
+ # Detailed collection analysis
1663
+ $ fontisan info spec/fixtures/fonts/NotoSerifCJK/NotoSerifCJK.ttc --format yaml
667
1664
 
668
- # Get units per em
669
- units_per_em = head_table.units_per_em # => 1000
670
-
671
- # Get created/modified timestamps
672
- created = head_table.created
673
- modified = head_table.modified
1665
+ ---
1666
+ collection_type: ttc
1667
+ font_count: 4
1668
+ fonts:
1669
+ - index: 0
1670
+ family_name: Noto Serif CJK JP
1671
+ subfamily_name: Regular
1672
+ postscript_name: NotoSerifCJKJP-Regular
1673
+ font_format: opentype
1674
+ - index: 1
1675
+ family_name: Noto Serif CJK KR
1676
+ subfamily_name: Regular
1677
+ postscript_name: NotoSerifCJKKR-Regular
1678
+ font_format: opentype
1679
+ - index: 2
1680
+ family_name: Noto Serif CJK SC
1681
+ subfamily_name: Regular
1682
+ postscript_name: NotoSerifCJKSC-Regular
1683
+ font_format: opentype
1684
+ - index: 3
1685
+ family_name: Noto Serif CJK TC
1686
+ subfamily_name: Regular
1687
+ postscript_name: NotoSerifCJKTC-Regular
1688
+ font_format: opentype
674
1689
  ----
675
1690
  ====
676
1691
 
677
- .Working with the OS/2 table
678
- [example]
679
- ====
680
- [source,ruby]
1692
+ ===== Unpack fonts
1693
+
1694
+ Extract all fonts from a TrueType Collection (TTC) or OpenType Collection (OTC)
1695
+ to a specified output directory.
1696
+
1697
+ [source,shell]
1698
+ ----
1699
+ $ fontisan unpack FONT.{ttc,otc} OUTPUT_DIR
681
1700
  ----
682
- os2_table = font.table("OS/2")
683
1701
 
684
- # Get vendor ID
685
- vendor_id = os2_table.ach_vend_id # => "QUE "
1702
+ NOTE: In `extract_ttc`, this was done with `extract_ttc --unpack FONT.ttc OUTPUT_DIR`.
686
1703
 
687
- # Check optical size
688
- if os2_table.version >= 5
689
- lower_size = os2_table.us_lower_optical_point_size # => 18
690
- upper_size = os2_table.us_upper_optical_point_size # => 72
691
- end
1704
+ ===== Extract specific font
692
1705
 
693
- # Get weight class
694
- weight = os2_table.us_weight_class # => 400 (Regular)
1706
+ Extract a specific font from a TrueType Collection (TTC) or OpenType Collection (OTC) by its index.
1707
+
1708
+ [source,shell]
1709
+ ----
1710
+ $ fontisan unpack FONT.{ttc,otc} --font-index INDEX OUTPUT.{ttf,otf}
695
1711
  ----
696
- ====
697
1712
 
698
- .Working with the post table
1713
+ NOTE: In `extract_ttc`, this was done with `extract_ttc --font-index INDEX FONT.ttc OUTPUT.ttf`.
1714
+
1715
+ .Extract with validation
699
1716
  [example]
700
1717
  ====
701
- [source,ruby]
1718
+ [source,shell]
702
1719
  ----
703
- post_table = font.table("post")
1720
+ # Extract and validate simultaneously
1721
+ $ fontisan unpack spec/fixtures/fonts/NotoSerifCJK/NotoSerifCJK.ttc extracted_fonts/ --validate
704
1722
 
705
- # Get all glyph names
706
- glyph_names = post_table.glyph_names # => [".notdef", "space", "exclam", ...]
1723
+ Extracting font 0: Noto Serif CJK JP → extracted_fonts/NotoSerifCJKJP-Regular.ttf
1724
+ Extracting font 1: Noto Serif CJK KR → extracted_fonts/NotoSerifCJKKR-Regular.ttf
1725
+ Extracting font 2: Noto Serif CJK SC → extracted_fonts/NotoSerifCJKSC-Regular.ttf
1726
+ Extracting font 3: Noto Serif CJK TC → extracted_fonts/NotoSerifCJKTC-Regular.ttf
707
1727
 
708
- # Check post table version
709
- version = post_table.version # => 2.0
1728
+ Validation: All fonts extracted successfully
710
1729
  ----
711
1730
  ====
712
1731
 
713
- ==== Working with cmap (Unicode mappings)
1732
+ ===== Validate collection
714
1733
 
715
- Access Unicode to glyph mappings:
1734
+ Validate the structure and checksums of a TrueType Collection (TTC) or OpenType
1735
+ Collection (OTC).
716
1736
 
717
- .Getting Unicode mappings
718
- [example]
719
- ====
720
- [source,ruby]
1737
+ [source,shell]
1738
+ ----
1739
+ $ fontisan validate FONT.{ttc,otc}
721
1740
  ----
722
- cmap_table = font.table("cmap")
723
1741
 
724
- # Get all Unicode to glyph index mappings
725
- mappings = cmap_table.unicode_mappings
726
- # => { 0x0020 => 1, 0x0021 => 2, 0x0022 => 3, ... }
1742
+ NOTE: In `extract_ttc`, this was done with `extract_ttc --validate FONT.ttc`.
727
1743
 
728
- # Look up specific Unicode codepoint
729
- glyph_index = mappings[0x0041] # => glyph index for 'A'
730
1744
 
731
- # Get subtables
732
- subtables = cmap_table.subtables
733
- subtables.each do |subtable|
734
- puts "Platform #{subtable.platform_id}, Encoding #{subtable.encoding_id}"
735
- end
736
- ----
737
- ====
1745
+ == Advanced features
738
1746
 
739
- ==== Working with GSUB/GPOS (scripts and features)
1747
+ Fontisan provides capabilities:
740
1748
 
741
- Extract OpenType layout information:
1749
+ .Font analysis and inspection
1750
+ * Extract OpenType tables with checksums and offsets
1751
+ * Display Unicode mappings and glyph names
1752
+ * Analyze variable font axes and instances
1753
+ * Show supported scripts and OpenType features
1754
+ * Dump raw binary table data
742
1755
 
743
- .Getting supported scripts
744
- [example]
745
- ====
746
- [source,ruby]
747
- ----
748
- # Get scripts from GSUB table
749
- gsub = font.table("GSUB")
750
- scripts = gsub.scripts # => ["DFLT", "latn", "cyrl", "grek", "hebr"]
1756
+ .Format conversion and subsetting
1757
+ * Convert between TTF, OTF, WOFF, and WOFF2 formats
1758
+ * Create font subsets with specific glyph ranges
1759
+ * Validate font structure and integrity
1760
+ * Generate SVG representations of glyphs
751
1761
 
752
- # Get scripts from GPOS table
753
- gpos = font.table("GPOS")
754
- scripts = gpos.scripts # => ["DFLT", "latn", "cyrl", "grek", "hebr"]
1762
+ .Collection creation
1763
+ * Build new TTC files from individual fonts
1764
+ * Optimize collection with table deduplication
1765
+ * Pack fonts with shared tables for smaller file sizes
755
1766
 
756
- # Combine scripts from both tables
757
- all_scripts = (gsub.scripts + gpos.scripts).uniq.sort
758
- ----
759
- ====
1767
+ For complete migration guide, see link:docs/EXTRACT_TTC_MIGRATION.md[extract_ttc Migration Guide].
760
1768
 
761
- .Getting OpenType features
762
- [example]
763
- ====
764
- [source,ruby]
765
- ----
766
- gsub = font.table("GSUB")
1769
+ === CLI Examples for Advanced Features
767
1770
 
768
- # Get features for a specific script
769
- latin_features = gsub.features(script_tag: "latn")
770
- # => ["cpsp", "kern", "mark", "mkmk"]
771
-
772
- # Get features for all scripts
773
- scripts = gsub.scripts
774
- features_by_script = scripts.each_with_object({}) do |script, hash|
775
- hash[script] = gsub.features(script_tag: script)
776
- end
777
- # => {"DFLT"=>["cpsp", "kern", ...], "latn"=>["cpsp", "kern", ...], ...}
1771
+ ==== Collection Creation and Management
778
1772
 
779
- # Combine GSUB and GPOS features
780
- gpos = font.table("GPOS")
781
- all_features = (gsub.features(script_tag: "latn") + gpos.features(script_tag: "latn")).uniq
1773
+ .Create TTC collection from multiple fonts
1774
+ [source,shell]
782
1775
  ----
783
- ====
1776
+ # Pack fonts into TTC with table sharing optimization
1777
+ $ fontisan pack font1.ttf font2.ttf font3.ttf --output family.ttc --analyze
784
1778
 
785
- ==== Working with variable fonts
1779
+ Collection Analysis:
1780
+ Total fonts: 3
1781
+ Shared tables: 12
1782
+ Potential space savings: 45.2 KB
1783
+ Table sharing: 68.5%
786
1784
 
787
- Access variation axes and instances:
1785
+ Collection created successfully:
1786
+ Output: family.ttc
1787
+ Format: TTC
1788
+ Fonts: 3
1789
+ Size: 245.8 KB
1790
+ Space saved: 45.2 KB
1791
+ Sharing: 68.5%
1792
+ ----
788
1793
 
789
- .Analyzing variable fonts
790
- [example]
791
- ====
792
- [source,ruby]
1794
+ .Create OTC collection from OpenType fonts
1795
+ [source,shell]
1796
+ ----
1797
+ $ fontisan pack Regular.otf Bold.otf Italic.otf --output family.otc --format otc
793
1798
  ----
794
- fvar_table = font.table("fvar")
795
1799
 
796
- # Check if font is variable
797
- is_variable = !fvar_table.nil?
1800
+ .Extract fonts from collection
1801
+ [source,shell]
1802
+ ----
1803
+ # Extract all fonts from collection
1804
+ $ fontisan unpack family.ttc --output-dir extracted/
798
1805
 
799
- # Get variation axes
800
- axes = fvar_table.axes
801
- axes.each do |axis|
802
- puts "Axis: #{axis.axis_tag}"
803
- puts " Name: #{axis.axis_name_id}"
804
- puts " Range: #{axis.min_value} to #{axis.max_value}"
805
- puts " Default: #{axis.default_value}"
806
- end
1806
+ Collection unpacked successfully:
1807
+ Input: family.ttc
1808
+ Output directory: extracted/
1809
+ Fonts extracted: 3/3
1810
+ - font1.ttf (89.2 KB)
1811
+ - font2.ttf (89.2 KB)
1812
+ - font3.ttf (67.4 KB)
807
1813
 
808
- # Get named instances
809
- instances = fvar_table.instances
810
- instances.each do |instance|
811
- puts "Instance: #{instance.subfamily_name_id}"
812
- puts " Coordinates: #{instance.coordinates.inspect}"
813
- end
1814
+ # Extract specific font with format conversion
1815
+ $ fontisan unpack family.ttc --output-dir extracted/ --font-index 0 --format woff2
814
1816
  ----
815
- ====
816
-
817
- ==== Font inspection utilities
818
1817
 
819
- Check table presence and extract basic info:
1818
+ ==== Format Conversion
820
1819
 
821
- .Font inspection
822
- [example]
823
- ====
824
- [source,ruby]
1820
+ .Convert TTF to WOFF2 for web usage
1821
+ [source,shell]
825
1822
  ----
826
- # Ch eck if font has specific tables
827
- has_gsub = font.has_table?("GSUB") # => true
828
- has_gpos = font.has_table?("GPOS") # => true
829
- has_fvar = font.has_table?("fvar") # => false (not a variable font)
1823
+ $ fontisan convert font.ttf --to woff2 --output font.woff2
830
1824
 
831
- # Get list of all table tags
832
- table_names = font.table_names
833
- # => ["GDEF", "GPOS", "OS/2", "cmap", "cvt ", ...]
834
-
835
- # Get font format
836
- font_format = font.header.sfnt_version == 0x00010000 ? "TrueType" : "OpenType"
1825
+ Converting font.ttf to woff2...
1826
+ Conversion complete!
1827
+ Input: font.ttf (245.8 KB)
1828
+ Output: font.woff2 (89.2 KB)
1829
+ ----
837
1830
 
838
- # Get number of glyphs
839
- post = font.table("post")
840
- glyph_count = post.glyph_names.length # => 2731
1831
+ .Convert to SVG format
1832
+ [source,shell]
841
1833
  ----
842
- ====
1834
+ $ fontisan convert font.ttf --to svg --output font.svg
843
1835
 
844
- ==== Using commands programmatically
1836
+ Converting font.ttf to svg...
1837
+ Conversion complete!
1838
+ Input: font.ttf (245.8 KB)
1839
+ Output: font.svg (1.2 MB)
1840
+ ----
845
1841
 
846
- Use command classes for structured output:
1842
+ ==== Font Subsetting
847
1843
 
848
- .Getting structured font information
849
- [example]
850
- ====
851
- [source,ruby]
1844
+ .Create PDF-optimized subset
1845
+ [source,shell]
852
1846
  ----
853
- # Use InfoCommand to get structured info
854
- cmd = Fontisan::Commands::InfoCommand.new("font.ttf", {})
855
- info = cmd.run # Returns Fontisan::Models::FontInfo
1847
+ $ fontisan subset font.ttf --text "Hello World" --output subset.ttf --profile pdf
856
1848
 
857
- # Access structured data
858
- puts info.family_name # => "Libertinus Serif"
859
- puts info.designer # => "Philipp H. Poll, Khaled Hosny"
860
- puts info.font_format # => "truetype"
1849
+ Subset font created:
1850
+ Input: font.ttf
1851
+ Output: subset.ttf
1852
+ Original glyphs: 1253
1853
+ Subset glyphs: 12
1854
+ Profile: pdf
1855
+ Size: 12.4 KB
1856
+ ----
861
1857
 
862
- # Serialize to formats
863
- json_output = info.to_json
864
- yaml_output = info.to_yaml
1858
+ .Subset with Unicode ranges
1859
+ [source,shell]
1860
+ ----
1861
+ $ fontisan subset font.ttf --unicode "U+0041-U+005A,U+0061-U+007A" --output latin.ttf
865
1862
  ----
866
- ====
867
1863
 
868
- .Getting scripts and features programmatically
869
- [example]
870
- ====
871
- [source,ruby]
1864
+ ==== Font Validation
1865
+
1866
+ .Validate font with different levels
1867
+ [source,shell]
872
1868
  ----
873
- # Get scripts
874
- scripts_cmd = Fontisan::Commands::ScriptsCommand.new("font.ttf", {})
875
- scripts_info = scripts_cmd.run # Returns Fontisan::Models::ScriptsInfo
1869
+ # Standard validation (allows warnings)
1870
+ $ fontisan validate font.ttf
876
1871
 
877
- scripts_info.scripts.each do |script|
878
- puts "#{script.tag}: #{script.description}"
879
- end
1872
+ # Strict validation (no warnings allowed)
1873
+ $ fontisan validate font.ttf --level strict
880
1874
 
881
- # Get features for a script
882
- features_cmd = Fontisan::Commands::FeaturesCommand.new(
883
- "font.ttf",
884
- { script: "latn" }
885
- )
886
- features_info = features_cmd.run # Returns Fontisan::Models::FeaturesInfo
1875
+ # Detailed validation report
1876
+ $ fontisan validate font.ttf --format yaml
1877
+ ----
887
1878
 
888
- features_info.features.each do |feature|
889
- puts "#{feature.tag}: #{feature.description}"
890
- end
1879
+ ==== Variable Font Instances
1880
+
1881
+ .Generate static instance from variable font
1882
+ [source,shell]
891
1883
  ----
892
- ====
1884
+ # Create bold instance
1885
+ $ fontisan instance variable.ttf --wght=700 --output bold.ttf
893
1886
 
894
- == Development
1887
+ # Use named instance
1888
+ $ fontisan instance variable.ttf --named-instance="Bold" --output bold.ttf
895
1889
 
896
- After checking out the repo, run `bundle install` to install dependencies.
1890
+ # List available instances
1891
+ $ fontisan instance variable.ttf --list-instances
1892
+ ----
897
1893
 
898
- === Running tests
1894
+ ==== Advanced Font Analysis
899
1895
 
1896
+ .Dump raw table data for analysis
900
1897
  [source,shell]
901
1898
  ----
902
- bundle exec rake spec
1899
+ $ fontisan dump-table font.ttf name > name_table.bin
1900
+ $ fontisan dump-table font.ttf GPOS > gpos_table.bin
903
1901
  ----
904
1902
 
905
- === Code style
1903
+ .Analyze font structure
1904
+ [source,shell]
1905
+ ----
1906
+ # List all OpenType tables with details
1907
+ $ fontisan tables font.ttf --format yaml
906
1908
 
907
- Check code style with RuboCop:
1909
+ # Show variable font information
1910
+ $ fontisan variable font.ttf
908
1911
 
909
- [source,shell]
1912
+ # Display optical size information
1913
+ $ fontisan optical-size font.ttf
910
1914
  ----
911
- bundle exec rake rubocop
1915
+
1916
+ .Get comprehensive font information
1917
+ [source,shell]
912
1918
  ----
1919
+ # Basic font info
1920
+ $ fontisan info font.ttf
913
1921
 
914
- === Test fixtures
1922
+ # Scripts and features analysis
1923
+ $ fontisan scripts font.ttf
1924
+ $ fontisan features font.ttf --script latn
915
1925
 
916
- The test suite uses real font files for testing.
1926
+ # Unicode coverage
1927
+ $ fontisan unicode font.ttf
917
1928
 
918
- Rake tasks are provided to download fonts from GitHub releases for testing.
1929
+ # Glyph names
1930
+ $ fontisan glyphs font.ttf
1931
+ ----
919
1932
 
920
- ==== Download test fixtures
921
1933
 
922
- Download font fixtures automatically (only downloads if files don't already
923
- exist):
924
1934
 
925
- [source,shell]
926
- ----
927
- bundle exec rake fixtures:download
928
- ----
1935
+ == Universial Outline Model
929
1936
 
930
- This downloads four font collections:
1937
+ === General
931
1938
 
932
- * **Libertinus 7.051** - Serif, Sans, and Mono families with extensive OpenType
933
- features (included in repository)
1939
+ Universal Outline Model (UOM) is based on a self-stable algorithm for converting
1940
+ soft glyph contours to outline format used in all tools of Fontisan. This
1941
+ ability allows easy modeling of import glyphs from one font format
1942
+ TrueType (TTF, OTF binaries), converting glyph elements into any font
1943
+ format, TrueType for example.
934
1944
 
935
- * **Mona Sans v2.0** - Variable TTF with width and weight axes for testing
936
- variable fonts (downloaded via Rake, not in repository)
1945
+ === Locker
937
1946
 
938
- * **Noto Serif CJK 2.003 (Static)** - Large CJK TTC for testing static font
939
- collections (downloaded via Rake, not in repository)
1947
+ Locker is the new object-oriented model for storing imported outlines and
1948
+ glyphs. Storage is based on monotonic spirals computed based on 2D points and
1949
+ curves. Invisible converting from TrueType, CFF Opentype and ColorGlyph formats.
940
1950
 
941
- * **Noto Serif CJK 2.003 (Variable)** - Variable OTF/OTC for testing variable
942
- OpenType collections (downloaded via Rake, not in repository)
1951
+ === Translator
943
1952
 
944
- The Rake task uses file dependencies, so running it multiple times won't
945
- re-download existing files. This makes it safe and efficient to run repeatedly
946
- during development.
1953
+ Translation from and to PostScript custom CFF charset. New encoding/decoding
1954
+ includes PostScript type2/3/composite Loron.
947
1955
 
948
- NOTE: Noto CJK and Mona Sans fonts are excluded from the repository (see
949
- `.gitignore`) due to their size. They are automatically downloaded when running
950
- `rake fixtures:download`.
1956
+ === ColorGlyph
951
1957
 
952
- ==== Clean test fixtures
1958
+ Support for layered import CFF color glyphs rasterizing on demand, with
1959
+ composite font support, a multi-layer color font represented by many
1960
+ CFF fonts stacked on top of each other. ColorGlyph support contains
1961
+ color glyphs, advanced color fonts glyphs and raster images (PNG or JPG)
1962
+ combined with TrueType outlines.
953
1963
 
954
- Remove all downloaded fixture files:
1964
+ === Universal fonts
955
1965
 
956
- [source,shell]
957
- ----
958
- bundle exec rake fixtures:clean
959
- ----
1966
+ Fontisan can now:
1967
+
1968
+ * Import TrueType contours into Universal Outline Model (UOM)
1969
+ * Operate UOM outlines including transformations, serialization (save),
1970
+ * Select and convert all UOM contours to TTF/OTF
1971
+ * Cleaning
1972
+ * Improve
1973
+ * Render
1974
+ * Building works for TrueType
1975
+ * Convert colors (cvt to TTF/OTF or TTF to cvt)
1976
+ * Saving and sharing font structures
1977
+ * Working with advanced color fonts
960
1978
 
961
- ==== Built-in fixtures
1979
+ === Universal glyphs
962
1980
 
963
- The repository includes pre-installed Libertinus fixtures in
964
- `spec/fixtures/fonts/libertinus/` which are sufficient for basic testing.
1981
+ Fontisan can now:
965
1982
 
966
- Large font collections (Noto CJK, Mona Sans) are **not committed** to the
967
- repository due to their size. Use `rake fixtures:download` to obtain them for
968
- comprehensive testing of:
1983
+ * Use Universal Outline Model (UOM) for TrueType contours and CFF color glyphs,
1984
+ * Repository for investor-defined fonts,
1985
+ * Custom Unicode assignments, rewriting Unicode configurations,
1986
+ * Saving and import outlines, including TrueType and OTF/CFF
1987
+ * Rendering for advanced font types
1988
+ * Universal layer stacking for advanced color glyph combinations
969
1989
 
970
- * Variable fonts (MonaSans variable TTF)
971
- * Large static font collections (NotoSerifCJK TTC)
972
- * Variable OpenType collections (NotoSerifCJK-VF OTC)
1990
+ === Universal color layers
973
1991
 
974
- == Contributing
1992
+ (Converted TT, OTF files)
975
1993
 
976
- Bug reports and pull requests are welcome on GitHub at https://github.com/fontist/fontisan.
1994
+ Fontisan can now:
977
1995
 
978
- == License
1996
+ * Import embedded TTF/OTF color layers,
1997
+ * Assembler from individual TTF/OTF slices,
1998
+ * Advanced managing layer maps in TTF color (CFF) fonts,
1999
+ * Advenced color layer blending style management,
2000
+ * Managing Gray/Overprint/Color-Full image comps and layer convertion
2001
+ * Strategy management for smart vector combos from raster
2002
+ * Importing and generation PNG block ruler layers
979
2003
 
980
- The gem is available as open source under the terms of the BSD-2-Clause license.
981
2004
 
982
- == Copyright
2005
+ == Copyright and license
983
2006
 
984
2007
  Copyright https://www.ribose.com[Ribose].
2008
+
2009
+ Fontisan is licensed under the Ribose 3-Clause BSD License. See the LICENSE file
2010
+ for details.