fontisan 0.2.13 → 0.2.16

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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +56 -196
  3. data/Gemfile +1 -1
  4. data/docs/.gitignore +17 -0
  5. data/docs/.vitepress/config.ts +317 -0
  6. data/docs/.vitepress/theme/components/ApiMethod.vue +127 -0
  7. data/docs/.vitepress/theme/components/Badge.vue +51 -0
  8. data/docs/.vitepress/theme/components/FeatureComparison.vue +87 -0
  9. data/docs/.vitepress/theme/components/HeroCodeBlock.vue +98 -0
  10. data/docs/.vitepress/theme/components/WithinHero.vue +30 -0
  11. data/docs/.vitepress/theme/index.ts +22 -0
  12. data/docs/.vitepress/theme/style.css +330 -0
  13. data/docs/api/conversion-options.md +141 -0
  14. data/docs/api/converters/curve-converter.md +34 -0
  15. data/docs/api/converters/hint-converter.md +34 -0
  16. data/docs/api/converters/outline-converter.md +27 -0
  17. data/docs/api/font-loader.md +111 -0
  18. data/docs/api/font-writer.md +103 -0
  19. data/docs/api/index.md +79 -0
  20. data/docs/api/models/glyph-accessor.md +43 -0
  21. data/docs/api/models/glyph.md +40 -0
  22. data/docs/api/models/table-analyzer.md +35 -0
  23. data/docs/api/sfnt-font.md +53 -0
  24. data/docs/api/type1-font.md +43 -0
  25. data/docs/api/validators/font-validator.md +31 -0
  26. data/docs/api/validators/helper.md +36 -0
  27. data/docs/api/validators/profile.md +39 -0
  28. data/docs/cli/convert.md +87 -0
  29. data/docs/cli/dump-table.md +110 -0
  30. data/docs/cli/export.md +176 -0
  31. data/docs/cli/features.md +124 -0
  32. data/docs/cli/glyphs.md +90 -0
  33. data/docs/cli/index.md +208 -0
  34. data/docs/cli/info.md +254 -0
  35. data/docs/cli/instance.md +122 -0
  36. data/docs/cli/ls.md +95 -0
  37. data/docs/cli/optical-size.md +94 -0
  38. data/docs/cli/pack.md +125 -0
  39. data/docs/cli/scripts.md +105 -0
  40. data/docs/cli/subset.md +39 -0
  41. data/docs/cli/tables.md +84 -0
  42. data/docs/cli/unicode.md +101 -0
  43. data/docs/cli/validate.md +48 -0
  44. data/docs/cli/variable.md +126 -0
  45. data/docs/cli/version.md +46 -0
  46. data/docs/guide/cli/convert.md +108 -0
  47. data/docs/guide/cli/export.md +138 -0
  48. data/docs/guide/cli/index.md +99 -0
  49. data/docs/guide/cli/info.md +144 -0
  50. data/docs/guide/cli/pack.md +155 -0
  51. data/docs/guide/cli/subset.md +118 -0
  52. data/docs/guide/cli/validate.md +139 -0
  53. data/docs/guide/color-fonts/bitmaps.md +177 -0
  54. data/docs/guide/color-fonts/colr-cpal.md +175 -0
  55. data/docs/guide/color-fonts/index.md +140 -0
  56. data/docs/guide/color-fonts/svg.md +154 -0
  57. data/docs/guide/color.md +51 -0
  58. data/docs/guide/comparisons/font-validator.md +222 -0
  59. data/docs/guide/comparisons/fonttools.md +200 -0
  60. data/docs/guide/comparisons/index.md +83 -0
  61. data/docs/guide/comparisons/lcdf-typetools.md +205 -0
  62. data/docs/guide/contributing.md +279 -0
  63. data/docs/guide/conversion/collections.md +251 -0
  64. data/docs/guide/conversion/curves.md +246 -0
  65. data/docs/guide/conversion/index.md +157 -0
  66. data/docs/guide/conversion/options.md +251 -0
  67. data/docs/guide/conversion/ttf-otf.md +184 -0
  68. data/docs/guide/conversion/type1.md +208 -0
  69. data/docs/guide/conversion/web.md +240 -0
  70. data/docs/guide/conversion.md +39 -0
  71. data/docs/guide/formats/collections.md +147 -0
  72. data/docs/guide/formats/dfont.md +99 -0
  73. data/docs/guide/formats/index.md +65 -0
  74. data/docs/guide/formats/otf.md +103 -0
  75. data/docs/guide/formats/svg.md +97 -0
  76. data/docs/guide/formats/ttf.md +105 -0
  77. data/docs/guide/formats/type1.md +118 -0
  78. data/docs/guide/formats/woff.md +115 -0
  79. data/docs/guide/hinting/autohint.md +141 -0
  80. data/docs/guide/hinting/conversion.md +161 -0
  81. data/docs/guide/hinting/index.md +86 -0
  82. data/docs/guide/hinting/postscript.md +149 -0
  83. data/docs/guide/hinting/truetype.md +135 -0
  84. data/docs/guide/hinting.md +44 -0
  85. data/docs/guide/index.md +152 -0
  86. data/docs/guide/installation.md +116 -0
  87. data/docs/guide/migrations/extract-ttc.md +549 -0
  88. data/docs/guide/migrations/font-validator.md +260 -0
  89. data/docs/guide/migrations/fonttools.md +208 -0
  90. data/docs/guide/migrations/index.md +64 -0
  91. data/docs/guide/migrations/otfinfo.md +197 -0
  92. data/docs/guide/quick-start.md +204 -0
  93. data/docs/guide/type1.md +58 -0
  94. data/docs/guide/universal-outline.md +151 -0
  95. data/docs/guide/validation/custom.md +195 -0
  96. data/docs/guide/validation/helpers.md +188 -0
  97. data/docs/guide/validation/index.md +132 -0
  98. data/docs/guide/validation/profiles.md +156 -0
  99. data/docs/guide/validation.md +47 -0
  100. data/docs/guide/variable-fonts/advanced.md +231 -0
  101. data/docs/guide/variable-fonts/axes.md +209 -0
  102. data/docs/guide/variable-fonts/conversion.md +197 -0
  103. data/docs/guide/variable-fonts/index.md +84 -0
  104. data/docs/guide/variable-fonts/instances.md +187 -0
  105. data/docs/guide/variable-fonts/named-instances.md +194 -0
  106. data/docs/guide/variable-fonts/static.md +168 -0
  107. data/docs/guide/variable.md +58 -0
  108. data/docs/guide/woff.md +59 -0
  109. data/docs/index.md +136 -0
  110. data/docs/lychee.toml +37 -0
  111. data/docs/package-lock.json +2560 -0
  112. data/docs/package.json +15 -0
  113. data/docs/public/apple-touch-icon.png +0 -0
  114. data/docs/public/favicon-96x96.png +0 -0
  115. data/docs/public/favicon.ico +0 -0
  116. data/docs/public/favicon.svg +1 -0
  117. data/docs/public/logo-full.svg +1 -0
  118. data/docs/public/logo.svg +1 -0
  119. data/docs/public/site.webmanifest +21 -0
  120. data/docs/public/web-app-manifest-192x192.png +0 -0
  121. data/docs/public/web-app-manifest-512x512.png +0 -0
  122. data/fontisan.gemspec +1 -1
  123. data/lib/fontisan/commands/features_command.rb +0 -1
  124. data/lib/fontisan/commands/info_command.rb +5 -5
  125. data/lib/fontisan/commands/scripts_command.rb +0 -1
  126. data/lib/fontisan/converters/format_converter.rb +2 -1
  127. data/lib/fontisan/converters/type1_converter.rb +65 -60
  128. data/lib/fontisan/hints/hint_converter.rb +2 -1
  129. data/lib/fontisan/loading_modes.rb +0 -2
  130. data/lib/fontisan/open_type_font.rb +0 -40
  131. data/lib/fontisan/sfnt_font.rb +41 -22
  132. data/lib/fontisan/tables/glyf/compound_glyph.rb +0 -1
  133. data/lib/fontisan/true_type_collection.rb +8 -8
  134. data/lib/fontisan/true_type_font.rb +1 -59
  135. data/lib/fontisan/type1/afm_parser.rb +2 -1
  136. data/lib/fontisan/type1/cff_to_type1_converter.rb +24 -19
  137. data/lib/fontisan/type1/private_dict.rb +28 -7
  138. data/lib/fontisan/type1/seac_expander.rb +22 -17
  139. data/lib/fontisan/variable/delta_applicator.rb +3 -3
  140. data/lib/fontisan/variation/optimizer.rb +0 -1
  141. data/lib/fontisan/version.rb +1 -1
  142. data/lib/fontisan/woff2_font.rb +2 -2
  143. data/lib/fontisan/woff_font.rb +3 -3
  144. data/lib/fontisan.rb +3 -2
  145. metadata +122 -4
data/docs/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "scripts": {
3
+ "dev": "vitepress dev",
4
+ "build": "vitepress build",
5
+ "preview": "vitepress preview",
6
+ "format": "prettier -w ."
7
+ },
8
+ "type": "module",
9
+ "devDependencies": {
10
+ "@types/node": "^25.4.0",
11
+ "prettier": "^3.8.1",
12
+ "typescript": "^5.9.3",
13
+ "vitepress": "^1.6.4"
14
+ }
15
+ }
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" id="uuid-c6d7bb0b-ce20-4c4f-b0df-e25e79a561a7" viewBox="0 0 500 500" width="500" height="500"><path d="M64.43,140.29h67.44v13.2h-48.96v180h48.96v13.2h-67.44v-206.4Z" style="fill:#4d4b54;"></path><path d="M352.67,333.5h48.96v-180h-48.96v-13.2h67.44v206.4h-67.44v-13.2Z" style="fill:#4d4b54;"></path><path d="M237.77,169.8c4.32-20.16,8.64-39.6,16.92-54.36,9.36-16.92,24.12-33.48,55.08-33.48,21.96,0,34.2,10.8,34.2,26.28,0,10.44-6.84,15.84-14.4,15.84-9.72,0-13.32-6.48-14.4-14.04-1.44-9-2.16-17.64-10.8-17.64-18.36,0-27.36,21.24-32.76,51.84l-4.32,25.56h34.2l-2.16,14.76h-33.84l-20.52,122.76c-5.04,30.96-21.6,111.6-70.92,111.6-23.04,0-32.04-13.32-32.04-25.92,0-10.08,5.04-17.64,14.4-17.64,21.6,0,9.72,32.04,23.4,32.04,19.08,0,28.44-59.04,32.4-82.8l23.4-140.04h-30.24l1.44-14.76h30.96Z" style="fill:#bf4e6a;"></path></svg>
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><svg id="uuid-5b0c2658-028c-4702-accc-5ed4e2e91676" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2100.48 500"><path d="M257.61,451.33c-14.44,6.96-31.78,5.55-41.96-7.84L67.48,248.75c-5.62-7.38-6.35-18.76-.56-25.73l75.41-90.8c3.17-3.82,7.58-5.34,12.06-7.47l160.97-76.62c14.33-6.82,30.68-3.53,42.04,7.07l105.12,142.11c2.69,5.58,5.13,10.27,8.53,15.28l55.82,82.24c5.37,7.91,6.05,23.69-4.06,28.56l-265.21,127.93ZM287.03,310.96l161.6-76.96c11.16-5.32,21.54-22.67,13.21-34.01l-105.49-143.66c-11.5-10.31-27.05-13.14-41.14-6.4l-166.89,79.87c-13.23,6.33-14.59,25.94-6.48,36.81l97.77,131.09c11.25,15.09,29.74,21.66,47.42,13.24ZM232.38,452.15c6.57,2.92,14.18,2.5,21.22-.89l267.88-128.95c8.75-4.21,9.43-18.42,4.56-25.6l-59.66-88.07c-1.3,13.05-7.4,22.31-18.52,27.6l-157.65,75.1c-9.63,4.59-18.53,5.83-29.47,4.16-6.26-3.25-13.52-5.43-18.2-11.67l-100.41-133.93c-6.12-8.16-7.99-15.97-6.35-27.06l-67.77,81.52c-5.01,6.03-4.38,16.57.54,23.04l148.59,195.3c3.88,5.09,9.36,7.96,15.25,9.45Z" style="fill:#676565;"/><path d="M287.03,310.96c-17.68,8.42-36.17,1.85-47.42-13.24l-97.77-131.09c-8.11-10.88-6.75-30.48,6.48-36.81l166.89-79.87c14.09-6.74,29.64-3.91,41.14,6.4l105.49,143.66c8.33,11.34-2.05,28.69-13.21,34.01l-161.6,76.96Z" style="fill:#e1dfd2;"/><path d="M232.38,452.15c1.96-4.82,3.23-9.57,4.66-15.14l27.96-108.44c1.32-5.12-3.24-8.92-4.26-13.08,10.94,1.67,19.84.42,29.47-4.16l157.65-75.1c11.12-5.3,17.22-14.55,18.52-27.6l59.66,88.07c4.87,7.19,4.2,21.39-4.56,25.6l-267.88,128.95c-7.05,3.39-14.65,3.81-21.22.89Z" style="fill:#bebbac;"/><path d="M260.74,315.5c1.02,4.16,5.58,7.95,4.26,13.08l-27.96,108.44c-1.44,5.57-2.71,10.31-4.66,15.14-5.89-1.49-11.37-4.36-15.25-9.45L68.54,247.4c-4.92-6.47-5.55-17.01-.54-23.04l67.77-81.52c-1.64,11.09.23,18.9,6.35,27.06l100.41,133.93c4.68,6.24,11.94,8.42,18.2,11.67Z" style="fill:#dddac8;"/><polygon points="282.54 238.11 289.49 249.09 263.7 260.65 194.99 159.94 222.35 148.43 228.68 158.31 214.22 164.38 269.97 243.75 282.54 238.11" style="fill:#4d4b54;"/><polygon points="369.09 198.8 383.44 192.08 329.33 114.13 315.75 120.26 309.88 111.47 336 98.94 403.17 194.97 375.96 207.94 369.09 198.8" style="fill:#4d4b54;"/><path d="M292.11,160.41c9.03,15.98,17.59,31.55,25.28,48.2,7.43,16.09,16.44,39,4.36,54.53-5.6,7.2-13.93,10.74-22.32,9.15-3.42-.65-5.8-2.78-5.93-5.92-.12-2.78,1.59-5.75,4.93-6.61,2.7-.69,6.2-.29,8.92.64,5.17,1.76,9.75-.71,11.18-6.18,1.14-4.4.26-9.46-1.57-13.97-4.76-11.73-10.73-22.33-17-33.4l-21.8-38.5c-.25-.44-6.36-11.42-6.36-11.42-2.87-6.21-16.16-25.52-5.57-44.8,5.23-9.53,15.89-15.13,26.35-12.59,3.15.76,5.82,3.09,6.27,5.63.57,3.22-1.12,6.43-4.3,7.81-8,3.48-12.19-4.65-17.66-2.08-7.12,3.35-3.91,13.49-1.03,19.02l10.48,20.15c2.29,4.41,1.97,3.58,5.79,10.33Z" style="fill:#bf4e6a;"/><polygon points="266.05 179.81 260.64 171.75 310.91 148.47 315.92 155.96 266.05 179.81" style="fill:#bf4e6a;"/><path d="M703.28,220.77v144.67h-27.95v-144.67h-31.82v-24.08h31.99v-20.92c0-38.32,24.43-61.35,63.11-61.35,15.12,0,24.79,4.22,27.77,5.45l-3.34,22.68c-4.75-2.11-13.54-4.22-23.2-4.22-21.45,0-36.91,10.55-36.91,39.38v18.98h52.38v24.08h-52.03Z" style="fill:#bf4e6a;"/><path d="M863.6,193.17c51.33,0,89.65,38.5,89.65,87.89s-38.32,87.89-89.65,87.89-89.65-38.5-89.65-87.89,38.32-87.89,89.65-87.89ZM863.6,342.94c35.51,0,61.7-27.07,61.7-61.88s-26.19-61.88-61.7-61.88-61.7,27.07-61.7,61.88,26.19,61.88,61.7,61.88Z" style="fill:#bf4e6a;"/><path d="M1154.16,262.78v102.66h-27.77v-95.8c0-32.87-17.23-50.1-47.81-50.1-28.83,0-52.21,19.86-52.21,50.62v95.27h-27.95v-168.75h28.12v25.31c9.49-15.64,29.53-28.83,56.43-28.83,44.47,0,71.19,26.19,71.19,69.61Z" style="fill:#bf4e6a;"/><path d="M1246.27,220.77v91.23c0,22.15,11.95,31.82,29.18,31.82,9.14,0,20.74-1.76,28.83-4.75l3.69,24.08c-7.21,3.16-21.8,5.8-35.51,5.8-32.52,0-54.14-17.93-54.14-52.38v-95.8h-31.11v-24.08h31.29v-54.14h27.6v54.14h53.96v24.08h-53.79Z" style="fill:#bf4e6a;"/><path d="M1339.26,140.09c0-10.2,7.03-17.23,17.23-17.23s17.58,7.03,17.58,17.23-7.38,17.4-17.58,17.4-17.23-7.21-17.23-17.4ZM1342.6,196.69h27.95v168.75h-27.95v-168.75Z" style="fill:#4d4b54;"/><path d="M1496.76,270.34c33.57,6.68,51.33,21.8,51.33,48.16,0,32.87-26.19,50.45-67.15,50.45-23.91,0-47.46-4.75-63.28-12.66l3.87-24.96c15.47,7.91,37.09,12.66,57.48,12.66,27.07,0,41.66-8.44,41.66-24.08s-13.01-21.45-36.74-26.02l-21.97-4.39c-27.95-5.45-45.88-20.21-45.88-46.23,0-30.06,26.54-50.1,68.55-50.1,20.39,0,40.96,3.87,54.14,9.32l-3.52,24.43c-13.71-5.45-32.34-8.96-48.87-8.96-27.42,0-42.54,9.14-42.54,24.61,0,14.59,11.6,19.69,35.33,24.26l17.58,3.52Z" style="fill:#4d4b54;"/><path d="M1757.09,365.44h-27.95v-27.77c-12.66,18.11-34.8,31.29-65.04,31.29-48.34,0-85.08-37.62-85.08-88.42s36.74-87.36,85.08-87.36c30.23,0,52.38,13.01,65.04,31.11v-27.6h27.95v168.75ZM1606.8,280.54c0,34.98,24.96,63.11,60.12,63.11s62.58-28.12,62.58-63.11-26.54-62.05-62.58-62.05-60.12,27.25-60.12,62.05Z" style="fill:#4d4b54;"/><path d="M1971.19,262.78v102.66h-27.77v-95.8c0-32.87-17.23-50.1-47.81-50.1-28.83,0-52.21,19.86-52.21,50.62v95.27h-27.95v-168.75h28.12v25.31c9.49-15.64,29.53-28.83,56.43-28.83,44.47,0,71.19,26.19,71.19,69.61Z" style="fill:#4d4b54;"/></svg>
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><svg id="uuid-bbbd5b2f-b286-4c12-becd-414fca1ebde9" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path d="M210.8,451.33c-14.44,6.96-31.78,5.55-41.96-7.84L20.67,248.75c-5.62-7.38-6.35-18.76-.56-25.73l75.41-90.8c3.17-3.82,7.58-5.34,12.06-7.47l160.97-76.62c14.33-6.82,30.68-3.53,42.04,7.07l105.12,142.11c2.69,5.58,5.13,10.27,8.53,15.28l55.82,82.24c5.37,7.91,6.05,23.69-4.06,28.56l-265.21,127.93ZM240.21,310.96l161.6-76.96c11.16-5.32,21.54-22.67,13.21-34.01l-105.49-143.66c-11.5-10.31-27.05-13.14-41.14-6.4l-166.89,79.87c-13.23,6.33-14.59,25.94-6.48,36.81l97.77,131.09c11.25,15.09,29.74,21.66,47.42,13.24ZM185.57,452.15c6.57,2.92,14.18,2.5,21.22-.89l267.88-128.95c8.75-4.21,9.43-18.42,4.56-25.6l-59.66-88.07c-1.3,13.05-7.4,22.31-18.52,27.6l-157.65,75.1c-9.63,4.59-18.53,5.83-29.47,4.16-6.26-3.25-13.52-5.43-18.2-11.67l-100.41-133.93c-6.12-8.16-7.99-15.97-6.35-27.06L21.19,224.36c-5.01,6.03-4.38,16.57.54,23.04l148.59,195.3c3.88,5.09,9.36,7.96,15.25,9.45Z" style="fill:#676565;"/><path d="M240.21,310.96c-17.68,8.42-36.17,1.85-47.42-13.24l-97.77-131.09c-8.11-10.88-6.75-30.48,6.48-36.81l166.89-79.87c14.09-6.74,29.64-3.91,41.14,6.4l105.49,143.66c8.33,11.34-2.05,28.69-13.21,34.01l-161.6,76.96Z" style="fill:#e1dfd2;"/><path d="M185.57,452.15c1.96-4.82,3.23-9.57,4.66-15.14l27.96-108.44c1.32-5.12-3.24-8.92-4.26-13.08,10.94,1.67,19.84.42,29.47-4.16l157.65-75.1c11.12-5.3,17.22-14.55,18.52-27.6l59.66,88.07c4.87,7.19,4.2,21.39-4.56,25.6l-267.88,128.95c-7.05,3.39-14.65,3.81-21.22.89Z" style="fill:#bebbac;"/><path d="M213.93,315.5c1.02,4.16,5.58,7.95,4.26,13.08l-27.96,108.44c-1.44,5.57-2.71,10.31-4.66,15.14-5.89-1.49-11.37-4.36-15.25-9.45L21.73,247.4c-4.92-6.47-5.55-17.01-.54-23.04l67.77-81.52c-1.64,11.09.23,18.9,6.35,27.06l100.41,133.93c4.68,6.24,11.94,8.42,18.2,11.67Z" style="fill:#dddac8;"/><polygon points="235.73 238.11 242.68 249.09 216.89 260.65 148.18 159.94 175.53 148.43 181.86 158.31 167.41 164.38 223.16 243.75 235.73 238.11" style="fill:#4d4b54;"/><polygon points="322.28 198.8 336.63 192.08 282.52 114.13 268.93 120.26 263.07 111.47 289.19 98.94 356.36 194.97 329.15 207.94 322.28 198.8" style="fill:#4d4b54;"/><path d="M245.3,160.41c9.03,15.98,17.59,31.55,25.28,48.2,7.43,16.09,16.44,39,4.36,54.53-5.6,7.2-13.93,10.74-22.32,9.15-3.42-.65-5.8-2.78-5.93-5.92-.12-2.78,1.59-5.75,4.93-6.61,2.7-.69,6.2-.29,8.92.64,5.17,1.76,9.75-.71,11.18-6.18,1.14-4.4.26-9.46-1.57-13.97-4.76-11.73-10.73-22.33-17-33.4l-21.8-38.5c-.25-.44-6.36-11.42-6.36-11.42-2.87-6.21-16.16-25.52-5.57-44.8,5.23-9.53,15.89-15.13,26.35-12.59,3.15.76,5.82,3.09,6.27,5.63.57,3.22-1.12,6.43-4.3,7.81-8,3.48-12.19-4.65-17.66-2.08-7.12,3.35-3.91,13.49-1.03,19.02l10.48,20.15c2.29,4.41,1.97,3.58,5.79,10.33Z" style="fill:#bf4e6a;"/><polygon points="219.23 179.81 213.82 171.75 264.09 148.47 269.11 155.96 219.23 179.81" style="fill:#bf4e6a;"/></svg>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "MyWebSite",
3
+ "short_name": "MySite",
4
+ "icons": [
5
+ {
6
+ "src": "/web-app-manifest-192x192.png",
7
+ "sizes": "192x192",
8
+ "type": "image/png",
9
+ "purpose": "maskable"
10
+ },
11
+ {
12
+ "src": "/web-app-manifest-512x512.png",
13
+ "sizes": "512x512",
14
+ "type": "image/png",
15
+ "purpose": "maskable"
16
+ }
17
+ ],
18
+ "theme_color": "#ffffff",
19
+ "background_color": "#ffffff",
20
+ "display": "standalone"
21
+ }
data/fontisan.gemspec CHANGED
@@ -41,7 +41,7 @@ Gem::Specification.new do |spec|
41
41
  spec.add_dependency "base64"
42
42
  spec.add_dependency "bindata", "~> 2.5"
43
43
  spec.add_dependency "brotli", "~> 0.5"
44
- spec.add_dependency "lutaml-model", "~> 0.7"
44
+ spec.add_dependency "lutaml-model", "~> 0.8"
45
45
  spec.add_dependency "nokogiri", "~> 1.16"
46
46
  spec.add_dependency "thor", "~> 1.3"
47
47
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require "yaml"
5
4
  require_relative "base_command"
6
5
  require_relative "../models/features_info"
@@ -161,10 +161,10 @@ module Fontisan
161
161
 
162
162
  # Check if variable font (Type1 fonts are never variable)
163
163
  info.is_variable = if font.is_a?(Type1Font)
164
- false
165
- else
166
- font.has_table?(Constants::FVAR_TAG)
167
- end
164
+ false
165
+ else
166
+ font.has_table?(Constants::FVAR_TAG)
167
+ end
168
168
  end
169
169
 
170
170
  # Populate essential fields for brief mode (metadata tables only).
@@ -273,7 +273,7 @@ module Fontisan
273
273
  info.version = font_info.version
274
274
  info.copyright = font_info.copyright
275
275
  info.description = font_info.notice
276
- info.designer = nil # Type 1 fonts may not have designer info
276
+ info.designer = nil # Type 1 fonts may not have designer info
277
277
 
278
278
  # Metrics
279
279
  if font_dict&.font_b_box
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require "yaml"
5
4
  require_relative "base_command"
6
5
  require_relative "../models/scripts_info"
@@ -351,7 +351,8 @@ _options)
351
351
  is_type1 = font.is_a?(Type1Font)
352
352
 
353
353
  unless is_type1 || font.respond_to?(:table)
354
- raise ArgumentError, "Font must respond to :table method or be a Type1Font"
354
+ raise ArgumentError,
355
+ "Font must respond to :table method or be a Type1Font"
355
356
  end
356
357
 
357
358
  unless target_format.is_a?(Symbol)
@@ -327,7 +327,10 @@ module Fontisan
327
327
 
328
328
  # Get CharStrings INDEX from CFF
329
329
  charstrings_index = cff_table.charstrings_index(0)
330
- raise Fontisan::Error, "CharStrings INDEX not found" unless charstrings_index
330
+ unless charstrings_index
331
+ raise Fontisan::Error,
332
+ "CharStrings INDEX not found"
333
+ end
331
334
 
332
335
  # Get Private DICT for context
333
336
  private_dict = cff_table.private_dict(0)
@@ -335,7 +338,7 @@ module Fontisan
335
338
  # Create CFF to Type 1 converter
336
339
  converter = Type1::CffToType1Converter.new(
337
340
  nominal_width: private_dict&.nominal_width || 0,
338
- default_width: private_dict&.default_width || 0
341
+ default_width: private_dict&.default_width || 0,
339
342
  )
340
343
 
341
344
  # Convert each CFF CharString to Type 1 format
@@ -354,7 +357,7 @@ module Fontisan
354
357
  private_dict_hash = build_private_dict_hash(private_dict)
355
358
  type1_charstrings[glyph_name] = converter.convert(
356
359
  cff_charstring,
357
- private_dict: private_dict_hash
360
+ private_dict: private_dict_hash,
358
361
  )
359
362
  end
360
363
 
@@ -551,7 +554,7 @@ module Fontisan
551
554
  # Parse version (e.g., "001.000" => 1.0)
552
555
  version_parts = version_str.split(".")
553
556
  major = version_parts[0].to_i
554
- minor = version_parts[1]&.to_i || 0
557
+ minor = version_parts[1].to_i
555
558
  version = major + (minor / 1000.0)
556
559
 
557
560
  # Version (Fixed 16.16) - stored as int32
@@ -626,24 +629,24 @@ module Fontisan
626
629
 
627
630
  # Ascent (int16) - Distance from baseline to highest ascender
628
631
  # Use BlueValues[2] or [3] if available, otherwise font_bbox[3]
629
- if blue_values.length >= 4
630
- ascent = blue_values[3] # Top zone top
631
- elsif blue_values.length >= 3
632
- ascent = blue_values[2] # Top zone bottom
633
- else
634
- ascent = font_bbox[3] # y_max
635
- end
632
+ ascent = if blue_values.length >= 4
633
+ blue_values[3] # Top zone top
634
+ elsif blue_values.length >= 3
635
+ blue_values[2] # Top zone bottom
636
+ else
637
+ font_bbox[3] # y_max
638
+ end
636
639
  data << [ascent].pack("s>")
637
640
 
638
641
  # Descent (int16) - Distance from baseline to lowest descender (negative)
639
642
  # Use BlueValues[0] or [1] if available, otherwise font_bbox[1]
640
- if blue_values.length >= 2
641
- descent = blue_values[0] # Bottom zone bottom (negative)
642
- elsif blue_values.length >= 1
643
- descent = blue_values[0]
644
- else
645
- descent = font_bbox[1] # y_min (should be negative)
646
- end
643
+ descent = if blue_values.length >= 2
644
+ blue_values[0] # Bottom zone bottom (negative)
645
+ elsif blue_values.length >= 1
646
+ blue_values[0]
647
+ else
648
+ font_bbox[1] # y_min (should be negative)
649
+ end
647
650
  data << [descent].pack("s>")
648
651
 
649
652
  # Line Gap (int16) - Additional space between lines
@@ -728,33 +731,33 @@ module Fontisan
728
731
 
729
732
  # Extract font names with fallbacks
730
733
  font_name = font.font_name || font_dict&.font_name || "Unnamed"
731
- family_name = if font_info&.respond_to?(:family_name)
734
+ family_name = if font_info.respond_to?(:family_name)
732
735
  font_info.family_name || font_dict&.family_name || font_name
733
736
  else
734
737
  font_dict&.family_name || font_name
735
738
  end
736
- full_name = if font_info&.respond_to?(:full_name)
739
+ full_name = if font_info.respond_to?(:full_name)
737
740
  font_info.full_name || font_dict&.full_name || family_name
738
741
  else
739
742
  font_dict&.full_name || family_name
740
743
  end
741
- version = if font_info&.respond_to?(:version)
744
+ version = if font_info.respond_to?(:version)
742
745
  font_info.version || font.version || "001.000"
743
746
  else
744
747
  font.version || "001.000"
745
748
  end
746
- copyright = if font_info&.respond_to?(:copyright)
747
- font_info.copyright || font_dict&.raw_data&.dig(:copyright) || ""
748
- else
749
- font_dict&.raw_data&.dig(:copyright) || ""
750
- end
749
+ copyright = if font_info.respond_to?(:copyright)
750
+ font_info.copyright || font_dict&.raw_data&.dig(:copyright) || ""
751
+ else
752
+ font_dict&.raw_data&.dig(:copyright) || ""
753
+ end
751
754
  postscript_name = font_name
752
- weight = if font_info&.respond_to?(:weight)
755
+ weight = if font_info.respond_to?(:weight)
753
756
  font_info.weight
754
757
  else
755
758
  "Regular"
756
759
  end
757
- notice = if font_info&.respond_to?(:notice)
760
+ notice = if font_info.respond_to?(:notice)
758
761
  font_info.notice
759
762
  else
760
763
  ""
@@ -782,7 +785,9 @@ module Fontisan
782
785
  ]
783
786
 
784
787
  # Filter out empty strings and build string storage
785
- name_records = name_records.select { |r| !r[:string].nil? && !r[:string].empty? }
788
+ name_records = name_records.select do |r|
789
+ !r[:string].nil? && !r[:string].empty?
790
+ end
786
791
 
787
792
  # Build string storage (UTF-16BE encoded for Windows platform)
788
793
  string_storage = (+"").b
@@ -810,15 +815,15 @@ module Fontisan
810
815
  # Write name records
811
816
  platform_id = 3 # Windows
812
817
  encoding_id = 1 # Unicode BMP
813
- language_id = 0x0409 # US English
818
+ language_id = 0x0409 # US English
814
819
 
815
820
  name_records.each do |record|
816
821
  data << [platform_id].pack("n") # platform ID
817
822
  data << [encoding_id].pack("n") # encoding ID
818
823
  data << [language_id].pack("n") # language ID
819
824
  data << [record[:name_id]].pack("n") # name ID
820
- data << [record[:encoded].bytesize].pack("n") # string length
821
- data << [record[:offset]].pack("n") # string offset
825
+ data << [record[:encoded].bytesize].pack("n") # string length
826
+ data << [record[:offset]].pack("n") # string offset
822
827
  end
823
828
 
824
829
  # Write string storage
@@ -926,13 +931,13 @@ module Fontisan
926
931
 
927
932
  # Unicode ranges (4 x uint32) - Basic Latin + Latin-1
928
933
  # Bits 0-31: Basic Latin, Latin-1, Latin Extended-A/B, etc.
929
- data << [0x00000001].pack("N") # Basic Latin (0-7F)
934
+ data << [0x00000001].pack("N") # Basic Latin (0-7F)
930
935
  data << [0x00000000].pack("N")
931
936
  data << [0x00000000].pack("N")
932
937
  data << [0x00000000].pack("N")
933
938
 
934
939
  # achVendID (4 bytes) - Vendor ID
935
- data << "UKWN" # Unknown
940
+ data << "UKWN" # Unknown
936
941
 
937
942
  # fsSelection (uint16) - Font selection flags
938
943
  # Bit 6 (0x40) = Regular weight if 400-500
@@ -946,25 +951,25 @@ module Fontisan
946
951
  data << [fs_selection].pack("n")
947
952
 
948
953
  # usFirstCharIndex (uint16) - First Unicode character
949
- data << [32].pack("n") # Space
954
+ data << [32].pack("n") # Space
950
955
 
951
956
  # usLastCharIndex (uint16) - Last Unicode character
952
- data << [0xFFFD].pack("n") # Replacement character
957
+ data << [0xFFFD].pack("n") # Replacement character
953
958
 
954
959
  # sTypoAscender (int16) - Use BlueValues or font bbox
955
- if blue_values.length >= 4
956
- typo_ascender = blue_values[3]
957
- else
958
- typo_ascender = font_bbox[3]
959
- end
960
+ typo_ascender = if blue_values.length >= 4
961
+ blue_values[3]
962
+ else
963
+ font_bbox[3]
964
+ end
960
965
  data << [typo_ascender].pack("s>")
961
966
 
962
967
  # sTypoDescender (int16) - Use BlueValues or font bbox (negative)
963
- if blue_values.length >= 2
964
- typo_descender = blue_values[0]
965
- else
966
- typo_descender = font_bbox[1]
967
- end
968
+ typo_descender = if blue_values.length >= 2
969
+ blue_values[0]
970
+ else
971
+ font_bbox[1]
972
+ end
968
973
  data << [typo_descender].pack("s>")
969
974
 
970
975
  # sTypoLineGap (int16)
@@ -1013,7 +1018,7 @@ module Fontisan
1013
1018
  # Version (Fixed 16.16) - Use version 3.0 for CFF fonts (no glyph names)
1014
1019
  # Version 2.0 would include glyph names, but for OTF output version 3.0 is fine
1015
1020
  # since CFF table contains the glyph names
1016
- data << [0x00030000].pack("N") # Version 3.0
1021
+ data << [0x00030000].pack("N") # Version 3.0
1017
1022
 
1018
1023
  # Italic Angle (Fixed 16.16)
1019
1024
  # Get from FontInfo if available, otherwise default to 0
@@ -1030,7 +1035,7 @@ module Fontisan
1030
1035
  data << [underline_thickness].pack("s>")
1031
1036
 
1032
1037
  # Fixed Pitch (uint32) - Boolean for monospace
1033
- is_fixed_pitch = (font_info.is_fixed_pitch || false) ? 1 : 0
1038
+ is_fixed_pitch = font_info.is_fixed_pitch || false ? 1 : 0
1034
1039
  data << [is_fixed_pitch].pack("N")
1035
1040
 
1036
1041
  # Min/Max Memory for Type 42 (uint32 each) - Not used for CFF, set to 0
@@ -1066,10 +1071,10 @@ module Fontisan
1066
1071
  unicode = Type1::AGL.unicode_for_glyph_name(glyph_name)
1067
1072
 
1068
1073
  # If no Unicode mapping, try to derive from encoding position
1069
- if unicode.nil?
1074
+ if unicode.nil? && (glyph_index < 128)
1070
1075
  # For standard encoding, try to map from position
1071
1076
  # This is a simplified approach - real implementation would be more robust
1072
- unicode = glyph_index if glyph_index < 128
1077
+ unicode = glyph_index
1073
1078
  end
1074
1079
 
1075
1080
  # Map Unicode to glyph index
@@ -1088,21 +1093,21 @@ module Fontisan
1088
1093
  subtable_data = build_cmap_format_4(unicode_to_glyph)
1089
1094
 
1090
1095
  # Calculate offsets
1091
- encoding_records_offset = 4 # After version (2) + num_tables (2)
1092
- subtable_offset = encoding_records_offset + 8 # After one encoding record (8 bytes)
1096
+ encoding_records_offset = 4 # After version (2) + num_tables (2)
1097
+ subtable_offset = encoding_records_offset + 8 # After one encoding record (8 bytes)
1093
1098
 
1094
1099
  # Build cmap table header
1095
1100
  # Version (uint16)
1096
1101
  data << [0].pack("n")
1097
1102
 
1098
1103
  # Number of encoding records (uint16)
1099
- data << [1].pack("n") # One encoding record
1104
+ data << [1].pack("n") # One encoding record
1100
1105
 
1101
1106
  # Encoding record: Platform ID (uint16), Encoding ID (uint16), Subtable offset (uint32)
1102
1107
  # Platform 3 (Windows), Encoding 1 (Unicode BMP)
1103
1108
  data << [3].pack("n") # Platform ID: Windows
1104
1109
  data << [1].pack("n") # Encoding ID: Unicode BMP
1105
- data << [subtable_offset].pack("N") # Subtable offset
1110
+ data << [subtable_offset].pack("N") # Subtable offset
1106
1111
 
1107
1112
  # Append subtable data
1108
1113
  data << subtable_data
@@ -1160,13 +1165,13 @@ module Fontisan
1160
1165
  # Calculate segment count and related values
1161
1166
  seg_count = segments.length
1162
1167
  seg_count_x2 = seg_count * 2
1163
- search_range = 2 ** (Math.log2(seg_count).to_i) * 2
1168
+ search_range = 2**Math.log2(seg_count).to_i * 2
1164
1169
  entry_selector = Math.log2(search_range / 2).to_i
1165
1170
  range_shift = (seg_count - search_range / 2) * 2
1166
1171
 
1167
1172
  # Build format 4 subtable header (14 bytes)
1168
- data << [4].pack("n") # Format
1169
- data << [calculate_cmap4_length(segments)].pack("n") # Length (placeholder)
1173
+ data << [4].pack("n") # Format
1174
+ data << [calculate_cmap4_length(segments)].pack("n") # Length (placeholder)
1170
1175
  data << [0].pack("n") # Language (0 = independent)
1171
1176
  data << [seg_count_x2].pack("n") # segCountX2
1172
1177
  data << [search_range].pack("n") # searchRange
@@ -1203,9 +1208,9 @@ module Fontisan
1203
1208
 
1204
1209
  # Write arrays (padded to even length)
1205
1210
  end_codes.each { |code| data << [code].pack("n") }
1206
- data << [0].pack("n") # Reserved padding
1211
+ data << [0].pack("n") # Reserved padding
1207
1212
  start_codes.each { |code| data << [code].pack("n") }
1208
- id_deltas.each { |delta| data << [delta].pack("s>") } # Signed
1213
+ id_deltas.each { |delta| data << [delta].pack("s>") } # Signed
1209
1214
  id_range_offsets.each { |offset| data << [offset].pack("n") }
1210
1215
  glyph_id_array.each { |gid| data << [gid].pack("n") }
1211
1216
 
@@ -1227,7 +1232,7 @@ module Fontisan
1227
1232
  seg_count = segments.length
1228
1233
 
1229
1234
  # Rough estimate (actual calculation done during construction)
1230
- 14 + (seg_count * 8) + (seg_count * 2) + 100 # 100 for glyph ID array estimate
1235
+ 14 + (seg_count * 8) + (seg_count * 2) + 100 # 100 for glyph ID array estimate
1231
1236
  end
1232
1237
  end
1233
1238
  end
@@ -276,7 +276,8 @@ module Fontisan
276
276
  # Merge all extracted hints (prep_hints and fpgm_hints override stem widths if present)
277
277
  # Note: fpgm_hints contains metadata (fpgm_size, has_functions, complexity)
278
278
  # which we must filter out before merging into PostScript dict hints
279
- fpgm_dict_hints = fpgm_hints.reject { |k, _| %i[fpgm_size has_functions complexity].include?(k) }
279
+ fpgm_dict_hints = fpgm_hints.except(:fpgm_size, :has_functions,
280
+ :complexity)
280
281
  hints.merge!(prep_hints).merge!(fpgm_dict_hints).merge!(blue_zones)
281
282
 
282
283
  # Provide default blue_values if none were detected
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module Fontisan
6
4
  # Loading modes module that defines which tables are loaded in each mode.
7
5
  #
@@ -32,46 +32,6 @@ module Fontisan
32
32
  # Page size for lazy loading alignment (typical filesystem page size)
33
33
  PAGE_SIZE = 4096
34
34
 
35
- # Read OpenType Font from a file
36
- #
37
- # @param path [String] Path to the OTF file
38
- # @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
39
- # @param lazy [Boolean] If true, load tables on demand (default: false)
40
- # @return [OpenTypeFont] A new instance
41
- # @raise [ArgumentError] if path is nil or empty, or if mode is invalid
42
- # @raise [Errno::ENOENT] if file does not exist
43
- # @raise [RuntimeError] if file format is invalid
44
- def self.from_file(path, mode: LoadingModes::FULL, lazy: false)
45
- if path.nil? || path.to_s.empty?
46
- raise ArgumentError,
47
- "path cannot be nil or empty"
48
- end
49
- raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
50
-
51
- # Validate mode
52
- LoadingModes.validate_mode!(mode)
53
-
54
- File.open(path, "rb") do |io|
55
- font = read(io)
56
- font.initialize_storage
57
- font.loading_mode = mode
58
- font.lazy_load_enabled = lazy
59
-
60
- if lazy
61
- # Keep file handle open for lazy loading
62
- font.io_source = File.open(path, "rb")
63
- font.setup_finalizer
64
- else
65
- # Read tables upfront
66
- font.read_table_data(io)
67
- end
68
-
69
- font
70
- end
71
- rescue BinData::ValidityError, EOFError => e
72
- raise "Invalid OTF file: #{e.message}"
73
- end
74
-
75
35
  # Initialize storage hashes
76
36
  #
77
37
  # Extends base class to add page_cache for lazy loading.
@@ -149,34 +149,55 @@ module Fontisan
149
149
  # @raise [RuntimeError] if file format is invalid
150
150
  def self.from_file(path, mode: LoadingModes::FULL, lazy: false)
151
151
  if path.nil? || path.to_s.empty?
152
- raise ArgumentError,
153
- "path cannot be nil or empty"
152
+ raise ArgumentError, "path cannot be nil or empty"
154
153
  end
155
154
  raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
156
155
 
157
- # Validate mode
158
156
  LoadingModes.validate_mode!(mode)
159
157
 
160
- File.open(path, "rb") do |io|
161
- font = read(io)
162
- font.initialize_storage
163
- font.loading_mode = mode
164
- font.lazy_load_enabled = lazy
165
-
166
- if lazy
167
- # Keep file handle open for lazy loading
168
- font.io_source = File.open(path, "rb")
169
- font.setup_finalizer
170
- else
171
- # Read tables upfront
172
- font.read_table_data(io)
173
- end
158
+ font = new
159
+ font.initialize_storage
160
+ font.loading_mode = mode
161
+ font.lazy_load_enabled = lazy
162
+
163
+ lazy ? load_lazy(path, font) : load_eager(path, font)
164
+ end
165
+
166
+ # Load font with lazy loading (keeps file handle open)
167
+ #
168
+ # @param path [String] Path to the font file
169
+ # @param font [SfntFont] Font instance to populate
170
+ # @return [SfntFont] The populated font instance
171
+ def self.load_lazy(path, font)
172
+ # rubocop:disable Security/Open -- Intentionally kept open for lazy loading
173
+ # Finalizer handles cleanup when object is GC'd
174
+ font.io_source = File.open(path, "rb")
175
+ # rubocop:enable Security/Open
176
+ font.setup_finalizer
177
+ font.io_source.rewind
178
+ font.read(font.io_source)
179
+ font
180
+ rescue BinData::ValidityError, EOFError => e
181
+ font_type = name.split("::").last
182
+ raise "Invalid #{font_type} file: #{e.message}"
183
+ end
174
184
 
175
- font
185
+ # Load font with eager loading (reads all data, closes file)
186
+ #
187
+ # @param path [String] Path to the font file
188
+ # @param font [SfntFont] Font instance to populate
189
+ # @return [SfntFont] The populated font instance
190
+ def self.load_eager(path, font)
191
+ File.open(path, "rb") do |io|
192
+ font.read(io)
193
+ font.read_table_data(io)
176
194
  end
195
+ font
177
196
  rescue BinData::ValidityError, EOFError => e
178
- raise "Invalid font file: #{e.message}"
197
+ font_type = name.split("::").last
198
+ raise "Invalid #{font_type} file: #{e.message}"
179
199
  end
200
+ private_class_method :load_lazy, :load_eager
180
201
 
181
202
  # Read SFNT Font from collection at specific offset
182
203
  #
@@ -422,9 +443,7 @@ module Fontisan
422
443
  #
423
444
  # @return [Hash<String, SfntTable>] Hash mapping tag => SfntTable instance
424
445
  def all_sfnt_tables
425
- table_names.each_with_object({}) do |tag, hash|
426
- hash[tag] = sfnt_table(tag)
427
- end
446
+ table_names.to_h { |tag| [tag, sfnt_table(tag)] }
428
447
  end
429
448
 
430
449
  # Get parsed table instance
@@ -61,7 +61,6 @@ module Fontisan
61
61
  :scale_y,
62
62
  :scale_01,
63
63
  :scale_10,
64
- keyword_init: true,
65
64
  ) do
66
65
  # Check if arguments are x,y offsets (vs point numbers)
67
66
  def args_are_xy?