├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── NOTICE ├── README.md ├── fonts ├── ClickerScript-Regular.ttf ├── DejaVuSansMono.ttf ├── LatinModernRoman-Regular.otf ├── MPLUS1p-Regular.ttf ├── NewCMMath-Regular.otf ├── NotoSans-Regular.ttf ├── NotoSansCJKsc-Bold-subset1.otf ├── NotoSansCJKsc-Regular.otf ├── NotoSansCJKsc-Regular_custom_font_matrix.otf ├── Roboto-Regular.ttf ├── Syne-Regular_subset.otf └── TestTTC.ttc ├── rustfmt.toml ├── src ├── cff │ ├── argstack.rs │ ├── charset.rs │ ├── charstring.rs │ ├── cid_font.rs │ ├── dict │ │ ├── font_dict.rs │ │ ├── mod.rs │ │ ├── private_dict.rs │ │ └── top_dict.rs │ ├── index.rs │ ├── mod.rs │ ├── number.rs │ ├── operator.rs │ ├── remapper.rs │ ├── sid_font.rs │ └── subroutines.rs ├── glyf.rs ├── head.rs ├── hmtx.rs ├── lib.rs ├── maxp.rs ├── name.rs ├── post.rs ├── read.rs ├── remapper.rs └── write.rs └── tests ├── README.md ├── cff ├── LatinModernRoman-Regular_1.txt ├── LatinModernRoman-Regular_2.txt ├── NewCMMath-Regular_1.txt ├── NotoSansCJKsc-Regular_1.txt └── NotoSansCJKsc-Regular_custom_font_matrix_1.txt ├── cli ├── Cargo.toml └── src │ └── main.rs ├── data ├── cff.tests ├── fonttools.tests └── subsets.tests ├── fuzz ├── Cargo.toml └── src │ └── main.rs ├── scripts └── gen-tests.py ├── src ├── cff.rs ├── font_tools.rs ├── main.rs └── subsets.rs └── ttx ├── ClickerScript-Regular_1.ttx ├── DejaVuSansMono_1.ttx ├── LatinModernRoman-Regular_1.ttx ├── MPLUS1p-Regular_1.ttx ├── NewCMMath-Regular_1.ttx ├── NotoSans-Regular_1.ttx ├── NotoSansCJKsc-Bold-subset1_1.ttx ├── NotoSansCJKsc-Regular_1.ttx ├── Roboto-Regular_1.ttx └── clear.sh /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | tests: 6 | name: Tests 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Install Java 11 | uses: actions/setup-java@v3 12 | with: 13 | distribution: 'temurin' 14 | java-version: '17' 15 | - name: Download the CFF dump utility 16 | run: wget https://github.com/janpe2/CFFDump/releases/download/v1.3.0/CFFDump_bin_cli_1.3.0.jar -O CFFDump_bin_cli_1.3.0.jar 17 | - name: Set CFF_DUMP_BIN environment variable 18 | run: echo "CFF_DUMP_BIN=$PWD/CFFDump_bin_cli_1.3.0.jar" >> $GITHUB_ENV 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.11" 23 | - name: Install fonttools 24 | run: pip install fonttools==4.50 25 | - uses: dtolnay/rust-toolchain@stable 26 | - run: cargo build 27 | name: Build 28 | - run: cargo test 29 | name: Run tests 30 | 31 | checks: 32 | name: Check clippy, formatting, and documentation 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: dtolnay/rust-toolchain@1.83.0 37 | with: 38 | components: clippy, rustfmt 39 | - uses: Swatinem/rust-cache@v2 40 | - uses: taiki-e/install-action@cargo-hack 41 | - run: cargo clippy 42 | - run: cargo fmt --check --all 43 | - run: cargo doc --workspace --no-deps 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .vscode 3 | _things 4 | desktop.ini 5 | .DS_Store 6 | 7 | # Rust 8 | /target 9 | bench/target 10 | debug 11 | .idea 12 | 13 | # Tests 14 | tests/subsets 15 | tests/ttx/*.otf 16 | tests/cff/*.otf 17 | tests/ttx/*_ref.ttx 18 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.4.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "2.9.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 16 | 17 | [[package]] 18 | name = "bytemuck" 19 | version = "1.22.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" 22 | dependencies = [ 23 | "bytemuck_derive", 24 | ] 25 | 26 | [[package]] 27 | name = "bytemuck_derive" 28 | version = "1.9.3" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" 31 | dependencies = [ 32 | "proc-macro2", 33 | "quote", 34 | "syn", 35 | ] 36 | 37 | [[package]] 38 | name = "byteorder" 39 | version = "1.5.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 42 | 43 | [[package]] 44 | name = "cfg-if" 45 | version = "1.0.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 48 | 49 | [[package]] 50 | name = "cli" 51 | version = "0.2.1" 52 | dependencies = [ 53 | "subsetter", 54 | ] 55 | 56 | [[package]] 57 | name = "crossbeam-deque" 58 | version = "0.8.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 61 | dependencies = [ 62 | "crossbeam-epoch", 63 | "crossbeam-utils", 64 | ] 65 | 66 | [[package]] 67 | name = "crossbeam-epoch" 68 | version = "0.9.18" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 71 | dependencies = [ 72 | "crossbeam-utils", 73 | ] 74 | 75 | [[package]] 76 | name = "crossbeam-utils" 77 | version = "0.8.21" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 80 | 81 | [[package]] 82 | name = "either" 83 | version = "1.15.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 86 | 87 | [[package]] 88 | name = "font-types" 89 | version = "0.8.3" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "d868ec188a98bb014c606072edd47e52e7ab7297db943b0b28503121e1d037bd" 92 | dependencies = [ 93 | "bytemuck", 94 | ] 95 | 96 | [[package]] 97 | name = "fuzz" 98 | version = "0.2.1" 99 | dependencies = [ 100 | "rand", 101 | "rand_distr", 102 | "rayon", 103 | "skrifa", 104 | "subsetter", 105 | "ttf-parser", 106 | "walkdir", 107 | ] 108 | 109 | [[package]] 110 | name = "fxhash" 111 | version = "0.2.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 114 | dependencies = [ 115 | "byteorder", 116 | ] 117 | 118 | [[package]] 119 | name = "getrandom" 120 | version = "0.3.2" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 123 | dependencies = [ 124 | "cfg-if", 125 | "libc", 126 | "r-efi", 127 | "wasi", 128 | ] 129 | 130 | [[package]] 131 | name = "libc" 132 | version = "0.2.171" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 135 | 136 | [[package]] 137 | name = "libm" 138 | version = "0.2.11" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" 141 | 142 | [[package]] 143 | name = "num-traits" 144 | version = "0.2.19" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 147 | dependencies = [ 148 | "autocfg", 149 | "libm", 150 | ] 151 | 152 | [[package]] 153 | name = "ppv-lite86" 154 | version = "0.2.21" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 157 | dependencies = [ 158 | "zerocopy", 159 | ] 160 | 161 | [[package]] 162 | name = "proc-macro2" 163 | version = "1.0.94" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 166 | dependencies = [ 167 | "unicode-ident", 168 | ] 169 | 170 | [[package]] 171 | name = "quote" 172 | version = "1.0.40" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 175 | dependencies = [ 176 | "proc-macro2", 177 | ] 178 | 179 | [[package]] 180 | name = "r-efi" 181 | version = "5.2.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 184 | 185 | [[package]] 186 | name = "rand" 187 | version = "0.9.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 190 | dependencies = [ 191 | "rand_chacha", 192 | "rand_core", 193 | "zerocopy", 194 | ] 195 | 196 | [[package]] 197 | name = "rand_chacha" 198 | version = "0.9.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 201 | dependencies = [ 202 | "ppv-lite86", 203 | "rand_core", 204 | ] 205 | 206 | [[package]] 207 | name = "rand_core" 208 | version = "0.9.3" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 211 | dependencies = [ 212 | "getrandom", 213 | ] 214 | 215 | [[package]] 216 | name = "rand_distr" 217 | version = "0.5.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" 220 | dependencies = [ 221 | "num-traits", 222 | "rand", 223 | ] 224 | 225 | [[package]] 226 | name = "rayon" 227 | version = "1.10.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 230 | dependencies = [ 231 | "either", 232 | "rayon-core", 233 | ] 234 | 235 | [[package]] 236 | name = "rayon-core" 237 | version = "1.12.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 240 | dependencies = [ 241 | "crossbeam-deque", 242 | "crossbeam-utils", 243 | ] 244 | 245 | [[package]] 246 | name = "read-fonts" 247 | version = "0.27.5" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "f14974c88fb4fd0a7203719f98020209248c9dbebaf9d10d860337797a905097" 250 | dependencies = [ 251 | "bytemuck", 252 | "font-types", 253 | ] 254 | 255 | [[package]] 256 | name = "same-file" 257 | version = "1.0.6" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 260 | dependencies = [ 261 | "winapi-util", 262 | ] 263 | 264 | [[package]] 265 | name = "skrifa" 266 | version = "0.29.2" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "7c0ca53de9bb9bee1720c727606275148463cd938eb6bde249dcedeec4967747" 269 | dependencies = [ 270 | "bytemuck", 271 | "read-fonts", 272 | ] 273 | 274 | [[package]] 275 | name = "subsetter" 276 | version = "0.2.1" 277 | dependencies = [ 278 | "fxhash", 279 | "skrifa", 280 | "ttf-parser", 281 | ] 282 | 283 | [[package]] 284 | name = "syn" 285 | version = "2.0.100" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 288 | dependencies = [ 289 | "proc-macro2", 290 | "quote", 291 | "unicode-ident", 292 | ] 293 | 294 | [[package]] 295 | name = "ttf-parser" 296 | version = "0.25.1" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" 299 | 300 | [[package]] 301 | name = "unicode-ident" 302 | version = "1.0.18" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 305 | 306 | [[package]] 307 | name = "walkdir" 308 | version = "2.5.0" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 311 | dependencies = [ 312 | "same-file", 313 | "winapi-util", 314 | ] 315 | 316 | [[package]] 317 | name = "wasi" 318 | version = "0.14.2+wasi-0.2.4" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 321 | dependencies = [ 322 | "wit-bindgen-rt", 323 | ] 324 | 325 | [[package]] 326 | name = "winapi-util" 327 | version = "0.1.9" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 330 | dependencies = [ 331 | "windows-sys", 332 | ] 333 | 334 | [[package]] 335 | name = "windows-sys" 336 | version = "0.59.0" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 339 | dependencies = [ 340 | "windows-targets", 341 | ] 342 | 343 | [[package]] 344 | name = "windows-targets" 345 | version = "0.52.6" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 348 | dependencies = [ 349 | "windows_aarch64_gnullvm", 350 | "windows_aarch64_msvc", 351 | "windows_i686_gnu", 352 | "windows_i686_gnullvm", 353 | "windows_i686_msvc", 354 | "windows_x86_64_gnu", 355 | "windows_x86_64_gnullvm", 356 | "windows_x86_64_msvc", 357 | ] 358 | 359 | [[package]] 360 | name = "windows_aarch64_gnullvm" 361 | version = "0.52.6" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 364 | 365 | [[package]] 366 | name = "windows_aarch64_msvc" 367 | version = "0.52.6" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 370 | 371 | [[package]] 372 | name = "windows_i686_gnu" 373 | version = "0.52.6" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 376 | 377 | [[package]] 378 | name = "windows_i686_gnullvm" 379 | version = "0.52.6" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 382 | 383 | [[package]] 384 | name = "windows_i686_msvc" 385 | version = "0.52.6" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 388 | 389 | [[package]] 390 | name = "windows_x86_64_gnu" 391 | version = "0.52.6" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 394 | 395 | [[package]] 396 | name = "windows_x86_64_gnullvm" 397 | version = "0.52.6" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 400 | 401 | [[package]] 402 | name = "windows_x86_64_msvc" 403 | version = "0.52.6" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 406 | 407 | [[package]] 408 | name = "wit-bindgen-rt" 409 | version = "0.39.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 412 | dependencies = [ 413 | "bitflags", 414 | ] 415 | 416 | [[package]] 417 | name = "zerocopy" 418 | version = "0.8.24" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 421 | dependencies = [ 422 | "zerocopy-derive", 423 | ] 424 | 425 | [[package]] 426 | name = "zerocopy-derive" 427 | version = "0.8.24" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 430 | dependencies = [ 431 | "proc-macro2", 432 | "quote", 433 | "syn", 434 | ] 435 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["tests/cli", "tests/fuzz"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | version = "0.2.1" 7 | authors = ["Laurenz Mädje ", "Laurenz Stampfl "] 8 | edition = "2021" 9 | repository = "https://github.com/typst/subsetter" 10 | readme = "README.md" 11 | license = "MIT OR Apache-2.0" 12 | 13 | [package] 14 | name = "subsetter" 15 | description = "Reduces the size and coverage of OpenType fonts." 16 | categories = ["compression", "encoding"] 17 | keywords = ["subsetting", "OpenType", "PDF"] 18 | exclude = ["fonts/*", "tests/*"] 19 | version = { workspace = true } 20 | authors = { workspace = true } 21 | edition = { workspace = true } 22 | repository = { workspace = true } 23 | readme = { workspace = true } 24 | license = { workspace = true } 25 | 26 | [dependencies] 27 | fxhash = "0.2.1" 28 | 29 | [dev-dependencies] 30 | skrifa = "0.29.0" 31 | ttf-parser = "0.25.1" 32 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # subsetter 2 | [![Crates.io](https://img.shields.io/crates/v/subsetter.svg)](https://crates.io/crates/subsetter) 3 | [![Documentation](https://docs.rs/subsetter/badge.svg)](https://docs.rs/subsetter) 4 | 5 | Reduces the size and coverage of OpenType fonts with TrueType or CFF outlines for embedding 6 | in PDFs. You can in general expect very good results in terms of font size, as most of the things 7 | that can be subsetted are also subsetted. 8 | 9 | # Scope 10 | **Note that the resulting font subsets will most likely be unusable in any other contexts than PDF writing, 11 | since a lot of information will be removed from the font which is not necessary in PDFs, but is 12 | necessary in other contexts.** This is on purpose, and for now, there are no plans to expand the 13 | scope of this crate to become a general purpose subsetter, as this is a massive undertaking and 14 | will make the already complex codebase even more complex. 15 | 16 | In the future, 17 | [klippa](https://github.com/googlefonts/fontations/tree/main/klippa) will hopefully fill this gap. 18 | 19 | For an example on how to use this crate, have a look at the 20 | [documentation](https://docs.rs/subsetter/latest/subsetter/). 21 | 22 | ## Limitations 23 | As mentioned above, this crate is specifically aimed at subsetting a font with the purpose of 24 | including it in a PDF file. For any other purposes, this crate will most likely not be very useful. 25 | 26 | Potential future work could include allowing to define variation coordinates for which to generate 27 | the subset for. However, apart from that there are no plans to increase the scope of this crate, apart from 28 | fixing bugs and adding new APIs to the existing interface. 29 | 30 | ## Safety and Dependencies 31 | This crate forbids unsafe code and has only a dependency on the `fxhash` crate. 32 | 33 | ## License 34 | This crate is dual-licensed under the MIT and Apache 2.0 licenses. 35 | -------------------------------------------------------------------------------- /fonts/ClickerScript-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/ClickerScript-Regular.ttf -------------------------------------------------------------------------------- /fonts/DejaVuSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/DejaVuSansMono.ttf -------------------------------------------------------------------------------- /fonts/LatinModernRoman-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/LatinModernRoman-Regular.otf -------------------------------------------------------------------------------- /fonts/MPLUS1p-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/MPLUS1p-Regular.ttf -------------------------------------------------------------------------------- /fonts/NewCMMath-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/NewCMMath-Regular.otf -------------------------------------------------------------------------------- /fonts/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /fonts/NotoSansCJKsc-Bold-subset1.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/NotoSansCJKsc-Bold-subset1.otf -------------------------------------------------------------------------------- /fonts/NotoSansCJKsc-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/NotoSansCJKsc-Regular.otf -------------------------------------------------------------------------------- /fonts/NotoSansCJKsc-Regular_custom_font_matrix.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/NotoSansCJKsc-Regular_custom_font_matrix.otf -------------------------------------------------------------------------------- /fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /fonts/Syne-Regular_subset.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/Syne-Regular_subset.otf -------------------------------------------------------------------------------- /fonts/TestTTC.ttc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/subsetter/a50893d011e3ba7670b1bc8f26956652346e4e45/fonts/TestTTC.ttc -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" 2 | max_width = 90 3 | chain_width = 70 4 | struct_lit_width = 50 5 | use_field_init_shorthand = true 6 | merge_derives = false 7 | -------------------------------------------------------------------------------- /src/cff/argstack.rs: -------------------------------------------------------------------------------- 1 | use crate::cff::number::Number; 2 | use crate::Error::CFFError; 3 | use crate::Result; 4 | 5 | const MAX_OPERANDS_LEN: usize = 48; 6 | 7 | // Taken from ttf-parser. 8 | /// TODO: Use array instead? 9 | pub struct ArgumentsStack { 10 | pub data: Vec, 11 | } 12 | 13 | impl ArgumentsStack { 14 | pub fn new() -> Self { 15 | Self { data: vec![] } 16 | } 17 | 18 | #[inline] 19 | pub fn len(&self) -> usize { 20 | self.data.len() 21 | } 22 | 23 | #[inline] 24 | pub fn push(&mut self, n: Number) -> Result<()> { 25 | if self.len() == MAX_OPERANDS_LEN { 26 | Err(CFFError) 27 | } else { 28 | self.data.push(n); 29 | Ok(()) 30 | } 31 | } 32 | 33 | #[inline] 34 | pub fn pop(&mut self) -> Option { 35 | self.data.pop() 36 | } 37 | 38 | #[inline] 39 | pub fn pop_all(&mut self) -> Vec { 40 | let mut ret_vec = vec![]; 41 | std::mem::swap(&mut self.data, &mut ret_vec); 42 | ret_vec 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/cff/charset.rs: -------------------------------------------------------------------------------- 1 | use crate::write::Writer; 2 | use crate::GlyphRemapper; 3 | use crate::Result; 4 | 5 | /// Rewrite the charset of the font. We do not perserve the CID's from the original font. Instead, 6 | /// we assign each glyph it's original glyph as the CID. This makes it easier to reference them 7 | /// from the PDF, since we know the CID a glyph will have before it's subsetted. 8 | pub fn rewrite_charset(gid_mapper: &GlyphRemapper, w: &mut Writer) -> Result<()> { 9 | if gid_mapper.num_gids() == 1 { 10 | // We only have .notdef, so use format 0. 11 | w.write::(0); 12 | } else { 13 | // Use format 2. 14 | w.write::(2); 15 | 16 | w.write::(1); 17 | // -2 because -1 for not including .notdef and -1 since the first glyph 18 | // in the range is not counted. 19 | w.write::(gid_mapper.num_gids() - 2); 20 | } 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /src/cff/charstring.rs: -------------------------------------------------------------------------------- 1 | use crate::cff::argstack::ArgumentsStack; 2 | use crate::cff::number::Number; 3 | use crate::cff::operator::Operator; 4 | use crate::cff::subroutines::SubroutineHandler; 5 | use crate::read::Reader; 6 | use crate::write::Writer; 7 | use crate::Error::{CFFError, Unimplemented}; 8 | use crate::{Error, Result}; 9 | use operators::*; 10 | use std::fmt::{Debug, Formatter}; 11 | 12 | pub type CharString<'a> = &'a [u8]; 13 | 14 | // Adapted from fonttools. 15 | /// A charstring decompiler. 16 | pub struct Decompiler<'a> { 17 | gsubr_handler: SubroutineHandler<'a>, 18 | lsubr_handler: SubroutineHandler<'a>, 19 | stack: ArgumentsStack, 20 | hint_count: u16, 21 | hint_mask_bytes: u16, 22 | } 23 | 24 | impl<'a> Decompiler<'a> { 25 | /// Create a new charstring decompiler. 26 | pub fn new( 27 | gsubr_handler: SubroutineHandler<'a>, 28 | lsubr_handler: SubroutineHandler<'a>, 29 | ) -> Self { 30 | Self { 31 | gsubr_handler, 32 | lsubr_handler, 33 | stack: ArgumentsStack::new(), 34 | hint_count: 0, 35 | hint_mask_bytes: 0, 36 | } 37 | } 38 | 39 | /// Decompile a charstring with the given decompiler. 40 | pub fn decompile(mut self, charstring: CharString<'a>) -> Result> { 41 | let mut program = Program::default(); 42 | self.decompile_inner(charstring, &mut program, 1)?; 43 | Ok(program) 44 | } 45 | 46 | fn decompile_inner( 47 | &mut self, 48 | charstring: CharString<'a>, 49 | program: &mut Program<'a>, 50 | depth: u8, 51 | ) -> Result<()> { 52 | if depth > 64 { 53 | return Err(CFFError); 54 | } 55 | 56 | let mut r = Reader::new(charstring); 57 | 58 | while !r.at_end() { 59 | // We need to peak instead of read because parsing a number requires 60 | // access to the whole buffer. 61 | let op = r.peak::().ok_or(Error::CFFError)?; 62 | 63 | // Numbers 64 | if matches!(op, 28 | 32..=255) { 65 | let number = Number::parse_char_string_number(&mut r).ok_or(CFFError)?; 66 | self.stack.push(number)?; 67 | program.push(Instruction::Operand(number)); 68 | continue; 69 | } 70 | 71 | // No numbers can appear now, so now we can actually read it. 72 | let op = r.read::().ok_or(CFFError)?; 73 | let operator = if op == 12 { 74 | Operator::from_two_byte(r.read::().ok_or(CFFError)?) 75 | } else { 76 | Operator::from_one_byte(op) 77 | }; 78 | 79 | match operator { 80 | HFLEX | FLEX | HFLEX1 | FLEX1 => { 81 | self.stack.pop_all(); 82 | program.push(Instruction::Operator(operator)); 83 | } 84 | HORIZONTAL_STEM 85 | | VERTICAL_STEM 86 | | HORIZONTAL_STEM_HINT_MASK 87 | | VERTICAL_STEM_HINT_MASK => { 88 | self.count_hints(); 89 | program.push(Instruction::Operator(operator)); 90 | } 91 | VERTICAL_MOVE_TO | HORIZONTAL_MOVE_TO | LINE_TO | VERTICAL_LINE_TO 92 | | HORIZONTAL_LINE_TO | MOVE_TO | CURVE_LINE | LINE_CURVE 93 | | VV_CURVE_TO | VH_CURVE_TO | HH_CURVE_TO | HV_CURVE_TO | CURVE_TO => { 94 | self.stack.pop_all(); 95 | program.push(Instruction::Operator(operator)); 96 | } 97 | RETURN => { 98 | // Don't do anything for return, since we desubroutinize. 99 | } 100 | CALL_GLOBAL_SUBROUTINE => { 101 | // Pop the subroutine index from the program. 102 | program.0.pop(); 103 | 104 | let biased_index = 105 | self.stack.pop().and_then(|n| n.as_i32()).ok_or(CFFError)?; 106 | let gsubr = self 107 | .gsubr_handler 108 | .get_with_biased(biased_index) 109 | .ok_or(CFFError)?; 110 | self.decompile_inner(gsubr, program, depth + 1)?; 111 | } 112 | CALL_LOCAL_SUBROUTINE => { 113 | // Pop the subroutine index from the program. 114 | program.0.pop(); 115 | 116 | let biased_index = 117 | self.stack.pop().and_then(|n| n.as_i32()).ok_or(CFFError)?; 118 | let lsubr = self 119 | .lsubr_handler 120 | .get_with_biased(biased_index) 121 | .ok_or(CFFError)?; 122 | self.decompile_inner(lsubr, program, depth + 1)?; 123 | } 124 | HINT_MASK | COUNTER_MASK => { 125 | program.push(Instruction::Operator(operator)); 126 | if self.hint_mask_bytes == 0 { 127 | // Hintmask can contain implicit stems. 128 | self.count_hints(); 129 | self.hint_mask_bytes = self.hint_count.div_ceil(8); 130 | } 131 | 132 | let hint_bytes = 133 | r.read_bytes(self.hint_mask_bytes as usize).ok_or(CFFError)?; 134 | program.push(Instruction::HintMask(hint_bytes)); 135 | } 136 | ENDCHAR => { 137 | // We don't support seac for now. It's a deprecated feature and Typst for some 138 | // reason does not support it anyway. 139 | if self.stack.len() >= 4 { 140 | return Err(Unimplemented); 141 | } 142 | 143 | program.push(Instruction::Operator(operator)); 144 | } 145 | _ => { 146 | return Err(CFFError); 147 | } 148 | } 149 | } 150 | 151 | Ok(()) 152 | } 153 | 154 | fn count_hints(&mut self) { 155 | let elements = self.stack.pop_all(); 156 | self.hint_count += elements.len() as u16 / 2; 157 | } 158 | } 159 | 160 | /// A type of instruction in a charstring program. 161 | #[derive(Debug)] 162 | pub enum Instruction<'a> { 163 | Operand(Number), 164 | Operator(Operator), 165 | HintMask(&'a [u8]), 166 | } 167 | 168 | /// A charstring program, decompiled into its constituent instructions. 169 | #[derive(Default)] 170 | pub struct Program<'a>(Vec>); 171 | 172 | impl Debug for Program<'_> { 173 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 174 | let mut formatted_strings = vec![]; 175 | let mut str_buffer = vec![]; 176 | 177 | for instr in &self.0 { 178 | match instr { 179 | Instruction::Operand(op) => str_buffer.push(format!("{}", op.as_f64())), 180 | Instruction::Operator(op) => { 181 | str_buffer.push(format!("op({})", op)); 182 | 183 | if *op != HINT_MASK && *op != COUNTER_MASK { 184 | formatted_strings.push(str_buffer.join(" ")); 185 | str_buffer.clear(); 186 | } 187 | } 188 | Instruction::HintMask(bytes) => { 189 | let mut byte_string = String::new(); 190 | 191 | for byte in *bytes { 192 | byte_string.push_str(&format!("{:08b}", *byte)); 193 | } 194 | 195 | str_buffer.push(byte_string); 196 | formatted_strings.push(str_buffer.join(" ")); 197 | str_buffer.clear(); 198 | } 199 | } 200 | } 201 | 202 | write!(f, "{}", formatted_strings.join("\n")) 203 | } 204 | } 205 | 206 | impl<'a> Program<'a> { 207 | /// Push a new instruction to the program. 208 | pub fn push(&mut self, instruction: Instruction<'a>) { 209 | self.0.push(instruction); 210 | } 211 | 212 | /// Compile the program. 213 | pub fn compile(&self) -> Vec { 214 | let mut w = Writer::new(); 215 | 216 | for instr in &self.0 { 217 | match instr { 218 | Instruction::Operand(num) => { 219 | w.write(num); 220 | } 221 | Instruction::Operator(op) => { 222 | w.write(op); 223 | } 224 | Instruction::HintMask(hm) => { 225 | w.write(hm); 226 | } 227 | } 228 | } 229 | 230 | w.finish() 231 | } 232 | } 233 | 234 | #[allow(dead_code)] 235 | pub mod operators { 236 | use crate::cff::operator::Operator; 237 | 238 | pub const HORIZONTAL_STEM: Operator = Operator::from_one_byte(1); 239 | pub const VERTICAL_STEM: Operator = Operator::from_one_byte(3); 240 | pub const VERTICAL_MOVE_TO: Operator = Operator::from_one_byte(4); 241 | pub const LINE_TO: Operator = Operator::from_one_byte(5); 242 | pub const HORIZONTAL_LINE_TO: Operator = Operator::from_one_byte(6); 243 | pub const VERTICAL_LINE_TO: Operator = Operator::from_one_byte(7); 244 | pub const CURVE_TO: Operator = Operator::from_one_byte(8); 245 | pub const CALL_LOCAL_SUBROUTINE: Operator = Operator::from_one_byte(10); 246 | pub const RETURN: Operator = Operator::from_one_byte(11); 247 | pub const ENDCHAR: Operator = Operator::from_one_byte(14); 248 | pub const HORIZONTAL_STEM_HINT_MASK: Operator = Operator::from_one_byte(18); 249 | pub const HINT_MASK: Operator = Operator::from_one_byte(19); 250 | pub const COUNTER_MASK: Operator = Operator::from_one_byte(20); 251 | pub const MOVE_TO: Operator = Operator::from_one_byte(21); 252 | pub const HORIZONTAL_MOVE_TO: Operator = Operator::from_one_byte(22); 253 | pub const VERTICAL_STEM_HINT_MASK: Operator = Operator::from_one_byte(23); 254 | pub const CURVE_LINE: Operator = Operator::from_one_byte(24); 255 | pub const LINE_CURVE: Operator = Operator::from_one_byte(25); 256 | pub const VV_CURVE_TO: Operator = Operator::from_one_byte(26); 257 | pub const HH_CURVE_TO: Operator = Operator::from_one_byte(27); 258 | pub const SHORT_INT: Operator = Operator::from_one_byte(28); 259 | pub const CALL_GLOBAL_SUBROUTINE: Operator = Operator::from_one_byte(29); 260 | pub const VH_CURVE_TO: Operator = Operator::from_one_byte(30); 261 | pub const HV_CURVE_TO: Operator = Operator::from_one_byte(31); 262 | pub const HFLEX: Operator = Operator::from_two_byte(34); 263 | pub const FLEX: Operator = Operator::from_two_byte(35); 264 | pub const HFLEX1: Operator = Operator::from_two_byte(36); 265 | pub const FLEX1: Operator = Operator::from_two_byte(37); 266 | pub const FIXED_16_16: Operator = Operator::from_one_byte(255); 267 | } 268 | -------------------------------------------------------------------------------- /src/cff/cid_font.rs: -------------------------------------------------------------------------------- 1 | use crate::cff::dict::font_dict; 2 | use crate::cff::dict::font_dict::FontDict; 3 | use crate::cff::dict::top_dict::TopDictData; 4 | use crate::cff::index::{parse_index, Index}; 5 | use crate::cff::remapper::FontDictRemapper; 6 | use crate::read::{LazyArray16, Reader}; 7 | use crate::write::Writer; 8 | use crate::Error::{MalformedFont, SubsetError}; 9 | use crate::GlyphRemapper; 10 | use crate::Result; 11 | 12 | // The parsing logic was taken from ttf-parser. 13 | 14 | /// Parse CID metadata from a font. 15 | pub fn parse_cid_metadata<'a>( 16 | data: &'a [u8], 17 | top_dict: &TopDictData, 18 | number_of_glyphs: u16, 19 | ) -> Option> { 20 | let (fd_array_offset, fd_select_offset) = 21 | match (top_dict.fd_array, top_dict.fd_select) { 22 | (Some(a), Some(b)) => (a, b), 23 | _ => return None, 24 | }; 25 | 26 | let mut metadata = CIDMetadata { 27 | fd_array: { 28 | let mut r = Reader::new_at(data, fd_array_offset); 29 | parse_index::(&mut r)? 30 | }, 31 | fd_select: { 32 | let mut s = Reader::new_at(data, fd_select_offset); 33 | parse_fd_select(number_of_glyphs, &mut s)? 34 | }, 35 | ..CIDMetadata::default() 36 | }; 37 | 38 | for font_dict_data in metadata.fd_array { 39 | metadata 40 | .font_dicts 41 | .push(font_dict::parse_font_dict(data, font_dict_data)?); 42 | } 43 | 44 | Some(metadata) 45 | } 46 | 47 | fn parse_fd_select<'a>( 48 | number_of_glyphs: u16, 49 | r: &mut Reader<'a>, 50 | ) -> Option> { 51 | let format = r.read::()?; 52 | match format { 53 | 0 => Some(FDSelect::Format0(r.read_array16::(number_of_glyphs)?)), 54 | 3 => Some(FDSelect::Format3(r.tail()?)), 55 | _ => None, 56 | } 57 | } 58 | 59 | /// Metadata necessary for processing CID-keyed fonts. 60 | #[derive(Clone, Default, Debug)] 61 | pub struct CIDMetadata<'a> { 62 | pub font_dicts: Vec>, 63 | pub fd_array: Index<'a>, 64 | pub fd_select: FDSelect<'a>, 65 | } 66 | 67 | #[derive(Clone, Copy, Debug)] 68 | pub enum FDSelect<'a> { 69 | Format0(LazyArray16<'a, u8>), 70 | Format3(&'a [u8]), 71 | } 72 | 73 | impl Default for FDSelect<'_> { 74 | fn default() -> Self { 75 | FDSelect::Format0(LazyArray16::default()) 76 | } 77 | } 78 | 79 | impl FDSelect<'_> { 80 | /// Get the font dict index for a glyph. 81 | pub fn font_dict_index(&self, glyph_id: u16) -> Option { 82 | match self { 83 | FDSelect::Format0(ref array) => array.get(glyph_id), 84 | FDSelect::Format3(data) => { 85 | let mut r = Reader::new(data); 86 | let number_of_ranges = r.read::()?; 87 | if number_of_ranges == 0 { 88 | return None; 89 | } 90 | 91 | let number_of_ranges = number_of_ranges.checked_add(1)?; 92 | 93 | let mut prev_first_glyph = r.read::()?; 94 | let mut prev_index = r.read::()?; 95 | for _ in 1..number_of_ranges { 96 | let curr_first_glyph = r.read::()?; 97 | if (prev_first_glyph..curr_first_glyph).contains(&glyph_id) { 98 | return Some(prev_index); 99 | } else { 100 | prev_index = r.read::()?; 101 | } 102 | 103 | prev_first_glyph = curr_first_glyph; 104 | } 105 | 106 | None 107 | } 108 | } 109 | } 110 | } 111 | 112 | /// Rewrite the FD INDEX for CID-keyed font. 113 | pub fn rewrite_fd_index( 114 | gid_remapper: &GlyphRemapper, 115 | fd_select: FDSelect, 116 | fd_remapper: &FontDictRemapper, 117 | w: &mut Writer, 118 | ) -> Result<()> { 119 | // We always use format 0, since it's the simplest. 120 | w.write::(0); 121 | 122 | for gid in gid_remapper.remapped_gids() { 123 | let old_fd = fd_select.font_dict_index(gid).ok_or(MalformedFont)?; 124 | let new_fd = fd_remapper.get(old_fd).ok_or(SubsetError)?; 125 | w.write(new_fd); 126 | } 127 | 128 | Ok(()) 129 | } 130 | -------------------------------------------------------------------------------- /src/cff/dict/font_dict.rs: -------------------------------------------------------------------------------- 1 | use crate::cff::cid_font::CIDMetadata; 2 | use crate::cff::dict::operators::*; 3 | use crate::cff::dict::private_dict::parse_subr_offset; 4 | use crate::cff::dict::DictionaryParser; 5 | use crate::cff::index::{create_index, parse_index, Index}; 6 | use crate::cff::number::{Number, StringId}; 7 | use crate::cff::remapper::{FontDictRemapper, SidRemapper}; 8 | use crate::cff::Offsets; 9 | use crate::read::Reader; 10 | use crate::write::Writer; 11 | use crate::Error::SubsetError; 12 | use crate::Result; 13 | use std::array; 14 | 15 | // The parsing logic was adapted from ttf-parser. 16 | 17 | /// A font DICT. 18 | #[derive(Default, Clone, Debug)] 19 | pub struct FontDict<'a> { 20 | /// The local subroutines that are linked in the font DICT. 21 | pub local_subrs: Index<'a>, 22 | /// The underlying data of the private dict. 23 | pub private_dict: &'a [u8], 24 | /// The StringID of the font name in this font DICT. 25 | pub font_name: Option, 26 | /// The font matrix. 27 | pub font_matrix: Option<[Number; 6]>, 28 | } 29 | 30 | pub fn parse_font_dict<'a>( 31 | font_data: &'a [u8], 32 | font_dict_data: &[u8], 33 | ) -> Option> { 34 | let mut font_dict = FontDict::default(); 35 | 36 | let mut operands_buffer: [Number; 48] = array::from_fn(|_| Number::zero()); 37 | let mut dict_parser = DictionaryParser::new(font_dict_data, &mut operands_buffer); 38 | while let Some(operator) = dict_parser.parse_next() { 39 | match operator { 40 | PRIVATE => { 41 | let private_dict_range = dict_parser.parse_range()?; 42 | let private_dict_data = font_data.get(private_dict_range.clone())?; 43 | font_dict.private_dict = private_dict_data; 44 | font_dict.local_subrs = { 45 | if let Some(subrs_offset) = parse_subr_offset(private_dict_data) { 46 | let start = private_dict_range.start.checked_add(subrs_offset)?; 47 | let subrs_data = font_data.get(start..)?; 48 | let mut r = Reader::new(subrs_data); 49 | parse_index::(&mut r)? 50 | } else { 51 | Index::default() 52 | } 53 | }; 54 | } 55 | FONT_NAME => font_dict.font_name = Some(dict_parser.parse_sid()?), 56 | FONT_MATRIX => font_dict.font_matrix = Some(dict_parser.parse_font_matrix()?), 57 | _ => {} 58 | } 59 | } 60 | 61 | Some(font_dict) 62 | } 63 | 64 | /// Rewrite the font DICT INDEX for CID-keyed fonts. 65 | pub fn rewrite_font_dict_index( 66 | fd_remapper: &FontDictRemapper, 67 | sid_remapper: &SidRemapper, 68 | offsets: &mut Offsets, 69 | metadata: &CIDMetadata, 70 | w: &mut Writer, 71 | top_dict_is_missing_font_matrix: bool, 72 | ) -> Result<()> { 73 | let mut dicts = vec![]; 74 | 75 | for (new_df, old_df) in fd_remapper.sorted_iter().enumerate() { 76 | let new_df = new_df as u8; 77 | 78 | let dict = metadata.font_dicts.get(old_df as usize).ok_or(SubsetError)?; 79 | let mut w = Writer::new(); 80 | 81 | // See comment in `rewrite_top_dict_index`. 82 | w.write( 83 | dict.font_matrix 84 | .map(|m| { 85 | if top_dict_is_missing_font_matrix { 86 | let scale = [ 87 | Number::from_f32(1000.0), 88 | Number::zero(), 89 | Number::zero(), 90 | Number::from_f32(1000.0), 91 | Number::zero(), 92 | Number::zero(), 93 | ]; 94 | Number::combine(m, scale) 95 | } else { 96 | m 97 | } 98 | }) 99 | .unwrap_or([ 100 | Number::one(), 101 | Number::zero(), 102 | Number::zero(), 103 | Number::one(), 104 | Number::zero(), 105 | Number::zero(), 106 | ]), 107 | ); 108 | w.write(FONT_MATRIX); 109 | 110 | // Write the length and offset of the private dict. 111 | // Private dicts have already been written, so the offsets are already correct. 112 | // This means that these two offsets are a bit special compared to the others, since 113 | // we never use the `location` field of the offset and we don't overwrite it like we do 114 | // for the others. 115 | offsets 116 | .private_dicts_lens 117 | .get(new_df as usize) 118 | .ok_or(SubsetError)? 119 | .value 120 | .write_as_5_bytes(&mut w); 121 | 122 | offsets 123 | .private_dicts_offsets 124 | .get_mut(new_df as usize) 125 | .ok_or(SubsetError)? 126 | .value 127 | .write_as_5_bytes(&mut w); 128 | w.write(PRIVATE); 129 | 130 | if let Some(font_name) = dict.font_name.and_then(|s| sid_remapper.get_new_sid(s)) 131 | { 132 | w.write(Number::from_i32(font_name.0 as i32)); 133 | w.write(FONT_NAME); 134 | } 135 | 136 | dicts.push(w.finish()); 137 | } 138 | 139 | w.write(create_index(dicts)?); 140 | 141 | Ok(()) 142 | } 143 | 144 | /// Generate a new font DICT INDEX for SID-keyed fonts. 145 | pub fn generate_font_dict_index(offsets: &mut Offsets, w: &mut Writer) -> Result<()> { 146 | let mut sub_w = Writer::new(); 147 | 148 | // See comment in `rewrite_top_dict_index` 149 | sub_w.write([ 150 | Number::one(), 151 | Number::zero(), 152 | Number::zero(), 153 | Number::one(), 154 | Number::zero(), 155 | Number::zero(), 156 | ]); 157 | sub_w.write(FONT_MATRIX); 158 | 159 | // Write the length and offset of the private dict. 160 | // Private dicts have already been written, so the offsets are already correct. 161 | // This means that these two offsets are a bit special compared to the others, since 162 | // we never use the `location` field of the offset and we don't overwrite it like we do 163 | // for the others. 164 | offsets 165 | .private_dicts_lens 166 | .first() 167 | .ok_or(SubsetError)? 168 | .value 169 | .write_as_5_bytes(&mut sub_w); 170 | 171 | offsets 172 | .private_dicts_offsets 173 | .first_mut() 174 | .ok_or(SubsetError)? 175 | .value 176 | .write_as_5_bytes(&mut sub_w); 177 | 178 | sub_w.write(PRIVATE); 179 | w.write(create_index(vec![sub_w.finish()])?); 180 | 181 | // TODO: Maybe write a font name as well? But shouldn't matter. 182 | Ok(()) 183 | } 184 | -------------------------------------------------------------------------------- /src/cff/dict/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod font_dict; 2 | pub(crate) mod private_dict; 3 | pub(crate) mod top_dict; 4 | 5 | // The `DictionaryParser` was taken from ttf-parser. 6 | 7 | use crate::cff::number::{Number, StringId}; 8 | use crate::cff::operator::{Operator, TWO_BYTE_OPERATOR_MARK}; 9 | use crate::read::Reader; 10 | use std::ops::Range; 11 | 12 | pub struct DictionaryParser<'a> { 13 | data: &'a [u8], 14 | offset: usize, 15 | operands_offset: usize, 16 | operands: &'a mut [Number], 17 | operands_len: u16, 18 | } 19 | 20 | impl<'a> DictionaryParser<'a> { 21 | pub fn new(data: &'a [u8], operands_buffer: &'a mut [Number]) -> Self { 22 | DictionaryParser { 23 | data, 24 | offset: 0, 25 | operands_offset: 0, 26 | operands: operands_buffer, 27 | operands_len: 0, 28 | } 29 | } 30 | 31 | pub fn parse_next(&mut self) -> Option { 32 | let mut r = Reader::new_at(self.data, self.offset); 33 | self.operands_offset = self.offset; 34 | while !r.at_end() { 35 | // 0..=21 bytes are operators. 36 | if is_dict_one_byte_op(r.peak::()?) { 37 | let b = r.read::()?; 38 | let mut operator = Operator::from_one_byte(b); 39 | 40 | if b == TWO_BYTE_OPERATOR_MARK { 41 | operator = Operator::from_two_byte(r.read::()?); 42 | } 43 | 44 | self.offset = r.offset(); 45 | return Some(operator); 46 | } else { 47 | let _ = Number::parse_cff_number(&mut r)?; 48 | } 49 | } 50 | 51 | None 52 | } 53 | 54 | pub fn parse_operands(&mut self) -> Option<()> { 55 | let mut r = Reader::new_at(self.data, self.operands_offset); 56 | self.operands_len = 0; 57 | while !r.at_end() { 58 | let b = r.peak::()?; 59 | // 0..=21 bytes are operators. 60 | if is_dict_one_byte_op(b) { 61 | r.read::()?; 62 | break; 63 | } else { 64 | let op = Number::parse_cff_number(&mut r)?; 65 | self.operands[usize::from(self.operands_len)] = op; 66 | self.operands_len += 1; 67 | 68 | if usize::from(self.operands_len) >= self.operands.len() { 69 | break; 70 | } 71 | } 72 | } 73 | 74 | Some(()) 75 | } 76 | 77 | pub fn operands(&self) -> &[Number] { 78 | &self.operands[..usize::from(self.operands_len)] 79 | } 80 | 81 | pub fn parse_sid(&mut self) -> Option { 82 | self.parse_operands()?; 83 | let operands = self.operands(); 84 | if operands.len() == 1 { 85 | Some(StringId(u16::try_from(operands[0].as_i32()?).ok()?)) 86 | } else { 87 | None 88 | } 89 | } 90 | 91 | pub fn parse_offset(&mut self) -> Option { 92 | self.parse_operands()?; 93 | let operands = self.operands(); 94 | if operands.len() == 1 { 95 | usize::try_from(operands[0].as_u32()?).ok() 96 | } else { 97 | None 98 | } 99 | } 100 | 101 | pub fn parse_font_bbox(&mut self) -> Option<[Number; 4]> { 102 | self.parse_operands()?; 103 | let operands = self.operands(); 104 | if operands.len() == 4 { 105 | Some([operands[0], operands[1], operands[2], operands[3]]) 106 | } else { 107 | None 108 | } 109 | } 110 | 111 | pub fn parse_font_matrix(&mut self) -> Option<[Number; 6]> { 112 | self.parse_operands()?; 113 | let operands = self.operands(); 114 | if operands.len() == 6 { 115 | Some([ 116 | operands[0], 117 | operands[1], 118 | operands[2], 119 | operands[3], 120 | operands[4], 121 | operands[5], 122 | ]) 123 | } else { 124 | None 125 | } 126 | } 127 | 128 | pub fn parse_range(&mut self) -> Option> { 129 | self.parse_operands()?; 130 | let operands = self.operands(); 131 | if operands.len() == 2 { 132 | let len = usize::try_from(operands[0].as_u32()?).ok()?; 133 | let start = usize::try_from(operands[1].as_u32()?).ok()?; 134 | let end = start.checked_add(len)?; 135 | Some(start..end) 136 | } else { 137 | None 138 | } 139 | } 140 | } 141 | 142 | fn is_dict_one_byte_op(b: u8) -> bool { 143 | match b { 144 | 0..=27 => true, 145 | 28..=30 => false, // numbers 146 | 31 => true, // Reserved 147 | 32..=254 => false, // numbers 148 | 255 => true, // Reserved 149 | } 150 | } 151 | 152 | /// A subset of the operators for DICT's we care about. 153 | pub(crate) mod operators { 154 | use crate::cff::operator::{Operator, OperatorType, TWO_BYTE_OPERATOR_MARK}; 155 | 156 | // TOP DICT OPERATORS 157 | pub const NOTICE: Operator = Operator(OperatorType::OneByteOperator([1])); 158 | pub const FONT_BBOX: Operator = Operator(OperatorType::OneByteOperator([5])); 159 | pub const COPYRIGHT: Operator = 160 | Operator(OperatorType::TwoByteOperator([TWO_BYTE_OPERATOR_MARK, 0])); 161 | pub const FONT_MATRIX: Operator = 162 | Operator(OperatorType::TwoByteOperator([TWO_BYTE_OPERATOR_MARK, 7])); 163 | pub const CHARSET: Operator = Operator(OperatorType::OneByteOperator([15])); 164 | pub const ENCODING: Operator = Operator(OperatorType::OneByteOperator([16])); 165 | pub const CHAR_STRINGS: Operator = Operator(OperatorType::OneByteOperator([17])); 166 | pub const PRIVATE: Operator = Operator(OperatorType::OneByteOperator([18])); 167 | 168 | // TOP DICT OPERATORS (CID FONTS) 169 | pub const ROS: Operator = 170 | Operator(OperatorType::TwoByteOperator([TWO_BYTE_OPERATOR_MARK, 30])); 171 | pub const CID_COUNT: Operator = 172 | Operator(OperatorType::TwoByteOperator([TWO_BYTE_OPERATOR_MARK, 34])); 173 | pub const FD_ARRAY: Operator = 174 | Operator(OperatorType::TwoByteOperator([TWO_BYTE_OPERATOR_MARK, 36])); 175 | pub const FD_SELECT: Operator = 176 | Operator(OperatorType::TwoByteOperator([TWO_BYTE_OPERATOR_MARK, 37])); 177 | pub const FONT_NAME: Operator = 178 | Operator(OperatorType::TwoByteOperator([TWO_BYTE_OPERATOR_MARK, 38])); 179 | 180 | // PRIVATE DICT OPERATORS 181 | pub const SUBRS: Operator = Operator(OperatorType::OneByteOperator([19])); 182 | } 183 | -------------------------------------------------------------------------------- /src/cff/dict/private_dict.rs: -------------------------------------------------------------------------------- 1 | use crate::cff::cid_font::CIDMetadata; 2 | use crate::cff::dict::operators::*; 3 | use crate::cff::dict::DictionaryParser; 4 | use crate::cff::number::Number; 5 | use crate::cff::remapper::FontDictRemapper; 6 | use crate::cff::Offsets; 7 | use crate::write::Writer; 8 | use crate::Error::{MalformedFont, SubsetError}; 9 | use crate::Result; 10 | use std::array; 11 | 12 | // The parsing logic was adapted from ttf-parser. 13 | 14 | /// Parse the subroutine offset from a private dict. 15 | pub fn parse_subr_offset(data: &[u8]) -> Option { 16 | let mut operands_buffer: [Number; 48] = array::from_fn(|_| Number::zero()); 17 | let mut dict_parser = DictionaryParser::new(data, &mut operands_buffer); 18 | 19 | while let Some(operator) = dict_parser.parse_next() { 20 | if operator == SUBRS { 21 | return dict_parser.parse_offset(); 22 | } 23 | } 24 | 25 | None 26 | } 27 | 28 | /// Write the private dicts of a CID font for each font dict. 29 | pub fn rewrite_cid_private_dicts( 30 | fd_remapper: &FontDictRemapper, 31 | offsets: &mut Offsets, 32 | metadata: &CIDMetadata, 33 | w: &mut Writer, 34 | ) -> Result<()> { 35 | for (new_df, old_df) in fd_remapper.sorted_iter().enumerate() { 36 | let font_dict = metadata.font_dicts.get(old_df as usize).ok_or(SubsetError)?; 37 | rewrite_private_dict(offsets, font_dict.private_dict, w, new_df)?; 38 | } 39 | 40 | Ok(()) 41 | } 42 | 43 | pub(crate) fn rewrite_private_dict( 44 | offsets: &mut Offsets, 45 | private_dict_data: &[u8], 46 | w: &mut Writer, 47 | dict_index: usize, 48 | ) -> Result<()> { 49 | let private_dict_offset = w.len(); 50 | 51 | let private_dict_data = { 52 | let mut operands_buffer: [Number; 48] = array::from_fn(|_| Number::zero()); 53 | let mut dict_parser = 54 | DictionaryParser::new(private_dict_data, &mut operands_buffer); 55 | 56 | let mut sub_w = Writer::new(); 57 | 58 | // We just make sure that no subroutine offset gets written, all other operators stay the 59 | // same. 60 | while let Some(operator) = dict_parser.parse_next() { 61 | match operator { 62 | SUBRS => { 63 | // We don't have any subroutines, so don't rewrite this DICT entry. 64 | } 65 | _ => { 66 | dict_parser.parse_operands().ok_or(MalformedFont)?; 67 | let operands = dict_parser.operands(); 68 | 69 | sub_w.write(operands); 70 | sub_w.write(operator); 71 | } 72 | } 73 | } 74 | 75 | sub_w.finish() 76 | }; 77 | 78 | let private_dict_len = private_dict_data.len(); 79 | 80 | offsets 81 | .private_dicts_lens 82 | .get_mut(dict_index) 83 | .ok_or(SubsetError)? 84 | .update_value(private_dict_len)?; 85 | 86 | offsets 87 | .private_dicts_offsets 88 | .get_mut(dict_index) 89 | .ok_or(SubsetError)? 90 | .update_value(private_dict_offset)?; 91 | 92 | w.extend(&private_dict_data); 93 | 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /src/cff/dict/top_dict.rs: -------------------------------------------------------------------------------- 1 | use crate::cff::dict::DictionaryParser; 2 | use crate::cff::index::{create_index, parse_index}; 3 | use crate::cff::number::{Number, StringId}; 4 | use crate::cff::remapper::SidRemapper; 5 | use crate::cff::{Offsets, DUMMY_VALUE}; 6 | use crate::read::Reader; 7 | use crate::write::Writer; 8 | use crate::Error::SubsetError; 9 | use crate::Result; 10 | use std::array; 11 | use std::ops::Range; 12 | 13 | // The parsing logic was adapted from ttf-parser. 14 | 15 | #[derive(Default, Debug, Clone)] 16 | pub struct TopDictData { 17 | pub charset: Option, 18 | pub char_strings: Option, 19 | pub private: Option>, 20 | pub fd_array: Option, 21 | pub fd_select: Option, 22 | pub notice: Option, 23 | pub copyright: Option, 24 | pub font_name: Option, 25 | pub has_ros: bool, 26 | pub font_matrix: Option<[Number; 6]>, 27 | pub font_bbox: Option<[Number; 4]>, 28 | } 29 | 30 | pub fn parse_top_dict_index(r: &mut Reader) -> Option { 31 | use super::operators::*; 32 | let mut top_dict = TopDictData::default(); 33 | 34 | let index = parse_index::(r)?; 35 | 36 | // The Top DICT INDEX should have only one dictionary in CFF fonts. 37 | let data = index.get(0)?; 38 | 39 | let mut operands_buffer: [Number; 48] = array::from_fn(|_| Number::zero()); 40 | let mut dict_parser = DictionaryParser::new(data, &mut operands_buffer); 41 | 42 | while let Some(operator) = dict_parser.parse_next() { 43 | match operator { 44 | // We only need to preserve the copyrights and font name. 45 | NOTICE => top_dict.notice = Some(dict_parser.parse_sid()?), 46 | COPYRIGHT => top_dict.copyright = Some(dict_parser.parse_sid()?), 47 | FONT_NAME => top_dict.font_name = Some(dict_parser.parse_sid()?), 48 | CHARSET => top_dict.charset = Some(dict_parser.parse_offset()?), 49 | // We don't care about encoding since we convert to CID-keyed font anyway. 50 | ENCODING => {} 51 | CHAR_STRINGS => top_dict.char_strings = Some(dict_parser.parse_offset()?), 52 | PRIVATE => top_dict.private = Some(dict_parser.parse_range()?), 53 | // We will rewrite the ROS, so no need to grab it from here. But we need to 54 | // register it, so we know we are dealing with a CID-keyed font. 55 | ROS => top_dict.has_ros = true, 56 | FD_ARRAY => top_dict.fd_array = Some(dict_parser.parse_offset()?), 57 | FD_SELECT => top_dict.fd_select = Some(dict_parser.parse_offset()?), 58 | FONT_MATRIX => top_dict.font_matrix = Some(dict_parser.parse_font_matrix()?), 59 | FONT_BBOX => top_dict.font_bbox = Some(dict_parser.parse_font_bbox()?), 60 | _ => {} 61 | } 62 | } 63 | 64 | Some(top_dict) 65 | } 66 | 67 | /// Rewrite the top dict. Implementation is based on what ghostscript seems to keep when 68 | /// rewriting a font subset. 69 | pub fn rewrite_top_dict_index( 70 | top_dict_data: &TopDictData, 71 | offsets: &mut Offsets, 72 | sid_remapper: &SidRemapper, 73 | w: &mut Writer, 74 | ) -> Result<()> { 75 | use super::operators::*; 76 | 77 | let mut sub_w = Writer::new(); 78 | 79 | // ROS. 80 | sub_w 81 | .write(Number::from_i32(sid_remapper.get(b"Adobe").ok_or(SubsetError)?.0 as i32)); 82 | sub_w.write(Number::from_i32( 83 | sid_remapper.get(b"Identity").ok_or(SubsetError)?.0 as i32, 84 | )); 85 | sub_w.write(Number::zero()); 86 | sub_w.write(ROS); 87 | 88 | // Copyright notices. 89 | if let Some(copyright) = 90 | top_dict_data.copyright.and_then(|s| sid_remapper.get_new_sid(s)) 91 | { 92 | sub_w.write(Number::from_i32(copyright.0 as i32)); 93 | sub_w.write(COPYRIGHT); 94 | } 95 | 96 | if let Some(notice) = top_dict_data.notice.and_then(|s| sid_remapper.get_new_sid(s)) { 97 | sub_w.write(Number::from_i32(notice.0 as i32)); 98 | sub_w.write(NOTICE); 99 | } 100 | 101 | // Font name. 102 | if let Some(font_name) = 103 | top_dict_data.font_name.and_then(|s| sid_remapper.get_new_sid(s)) 104 | { 105 | sub_w.write(Number::from_i32(font_name.0 as i32)); 106 | sub_w.write(FONT_NAME); 107 | } 108 | 109 | // See https://bugs.ghostscript.com/show_bug.cgi?id=690724#c12 and https://leahneukirchen.org/blog/archive/2022/10/50-blank-pages-or-black-box-debugging-of-pdf-rendering-in-printers.html 110 | // We assume that if at least one font dict has a matrix, all of them do. 111 | // Case 1: Top DICT MATRIX is some, FONT DICT MATRIX is some -> supplied, supplied 112 | // Case 2: Top DICT MATRIX is none, FONT DICT MATRIX is some -> (0.001, 0, 0, 0.001, 0, 0), supplied * 1000 113 | // Case 3: Top DICT MATRIX is some, FONT DICT MATRIX is none -> supplied, (1, 0, 0, 1, 0, 0) 114 | // Case 4: Top DICT MATRIX is none, FONT DICT MATRIX is none -> (0.001, 0, 0, 0.001, 0 0), (1, 0, 0, 1, 0, 0) 115 | sub_w.write(top_dict_data.font_matrix.as_ref().unwrap_or(&[ 116 | Number::from_f32(0.001), 117 | Number::zero(), 118 | Number::zero(), 119 | Number::from_f32(0.001), 120 | Number::zero(), 121 | Number::zero(), 122 | ])); 123 | sub_w.write(FONT_MATRIX); 124 | 125 | // Write a default font bbox, if it does not exist. 126 | sub_w.write(top_dict_data.font_bbox.as_ref().unwrap_or(&[ 127 | Number::zero(), 128 | Number::zero(), 129 | Number::zero(), 130 | Number::zero(), 131 | ])); 132 | sub_w.write(FONT_BBOX); 133 | 134 | // Note: When writing the offsets, we need to add the current length of w AND sub_w. 135 | // Charset 136 | offsets.charset_offset.update_location(sub_w.len() + w.len()); 137 | DUMMY_VALUE.write_as_5_bytes(&mut sub_w); 138 | sub_w.write(CHARSET); 139 | 140 | // Charstrings 141 | offsets.char_strings_offset.update_location(sub_w.len() + w.len()); 142 | DUMMY_VALUE.write_as_5_bytes(&mut sub_w); 143 | sub_w.write(CHAR_STRINGS); 144 | 145 | sub_w.write(Number::from_i32(u16::MAX as i32)); 146 | sub_w.write(CID_COUNT); 147 | 148 | // Note: Previously, we wrote those two entries directly after ROS. 149 | // However, for some reason not known to me, Apple Preview does not like show the CFF font 150 | // at all if that's the case. This is why we now write the offsets in the very end. 151 | 152 | // FD array. 153 | offsets.fd_array_offset.update_location(sub_w.len() + w.len()); 154 | DUMMY_VALUE.write_as_5_bytes(&mut sub_w); 155 | sub_w.write(FD_ARRAY); 156 | 157 | // FD select. 158 | offsets.fd_select_offset.update_location(sub_w.len() + w.len()); 159 | DUMMY_VALUE.write_as_5_bytes(&mut sub_w); 160 | sub_w.write(FD_SELECT); 161 | 162 | let finished = sub_w.finish(); 163 | 164 | // TOP DICT INDEX always has size 1 in CFF. 165 | let index = create_index(vec![finished])?; 166 | 167 | // The offsets we calculated before were calculated under the assumption 168 | // that the contents of sub_w will be appended directly to w. However, when we create an index, 169 | // the INDEX header data will be appended in the beginning, meaning that we need to adjust the offsets 170 | // to account for that. 171 | offsets.charset_offset.adjust_location(index.header_size); 172 | offsets.char_strings_offset.adjust_location(index.header_size); 173 | offsets.fd_array_offset.adjust_location(index.header_size); 174 | offsets.fd_select_offset.adjust_location(index.header_size); 175 | 176 | w.write(index); 177 | 178 | Ok(()) 179 | } 180 | -------------------------------------------------------------------------------- /src/cff/index.rs: -------------------------------------------------------------------------------- 1 | use crate::cff::number::U24; 2 | use crate::read::{Readable, Reader}; 3 | use crate::write::{Writeable, Writer}; 4 | use crate::Error::OverflowError; 5 | use crate::Result; 6 | 7 | // Taken from ttf-parser. 8 | 9 | pub trait IndexSize: for<'a> Readable<'a> { 10 | fn to_u32(self) -> u32; 11 | } 12 | 13 | impl IndexSize for u16 { 14 | fn to_u32(self) -> u32 { 15 | u32::from(self) 16 | } 17 | } 18 | 19 | impl IndexSize for u32 { 20 | fn to_u32(self) -> u32 { 21 | self 22 | } 23 | } 24 | 25 | pub fn parse_index<'a, T: IndexSize>(r: &mut Reader<'a>) -> Option> { 26 | let count = r.read::()?; 27 | parse_index_impl(count.to_u32(), r) 28 | } 29 | 30 | fn parse_index_impl<'a>(count: u32, r: &mut Reader<'a>) -> Option> { 31 | if count == 0 || count == u32::MAX { 32 | return Some(Index::default()); 33 | } 34 | 35 | let offset_size = r.read::()?; 36 | let offsets_len = (count + 1).checked_mul(offset_size.to_u32())?; 37 | let offsets = VarOffsets { 38 | data: r.read_bytes(offsets_len as usize)?, 39 | offset_size, 40 | }; 41 | 42 | match offsets.last() { 43 | Some(last_offset) => { 44 | let data = r.read_bytes(last_offset as usize)?; 45 | Some(Index { data, offsets }) 46 | } 47 | None => Some(Index::default()), 48 | } 49 | } 50 | 51 | pub fn skip_index(r: &mut Reader) -> Option<()> { 52 | let count = r.read::()?; 53 | skip_index_impl(count.to_u32(), r) 54 | } 55 | 56 | fn skip_index_impl(count: u32, r: &mut Reader) -> Option<()> { 57 | if count == 0 || count == u32::MAX { 58 | return Some(()); 59 | } 60 | 61 | let offset_size = r.read::()?; 62 | let offsets_len = (count + 1).checked_mul(offset_size.to_u32())?; 63 | let offsets = VarOffsets { 64 | data: r.read_bytes(offsets_len as usize)?, 65 | offset_size, 66 | }; 67 | 68 | if let Some(last_offset) = offsets.last() { 69 | r.skip_bytes(last_offset as usize); 70 | } 71 | 72 | Some(()) 73 | } 74 | 75 | #[derive(Clone, Copy, Debug)] 76 | pub struct VarOffsets<'a> { 77 | pub data: &'a [u8], 78 | pub offset_size: OffsetSize, 79 | } 80 | 81 | impl VarOffsets<'_> { 82 | pub fn get(&self, index: u32) -> Option { 83 | if index >= self.len() { 84 | return None; 85 | } 86 | 87 | let start = index as usize * self.offset_size.to_usize(); 88 | let mut r = Reader::new_at(self.data, start); 89 | let n: u32 = match self.offset_size { 90 | OffsetSize::Size1 => u32::from(r.read::()?), 91 | OffsetSize::Size2 => u32::from(r.read::()?), 92 | OffsetSize::Size3 => r.read::()?.0, 93 | OffsetSize::Size4 => r.read::()?, 94 | }; 95 | 96 | // Offsets are offset by one byte in the font, 97 | // so we have to shift them back. 98 | n.checked_sub(1) 99 | } 100 | 101 | #[inline] 102 | pub fn last(&self) -> Option { 103 | if !self.is_empty() { 104 | self.get(self.len() - 1) 105 | } else { 106 | None 107 | } 108 | } 109 | 110 | #[inline] 111 | pub fn len(&self) -> u32 { 112 | self.data.len() as u32 / self.offset_size as u32 113 | } 114 | 115 | #[inline] 116 | pub fn is_empty(&self) -> bool { 117 | self.len() == 0 118 | } 119 | } 120 | 121 | #[derive(Clone, Copy, Debug)] 122 | pub struct Index<'a> { 123 | pub data: &'a [u8], 124 | pub offsets: VarOffsets<'a>, 125 | } 126 | 127 | impl Default for Index<'_> { 128 | #[inline] 129 | fn default() -> Self { 130 | Index { 131 | data: b"", 132 | offsets: VarOffsets { data: b"", offset_size: OffsetSize::Size1 }, 133 | } 134 | } 135 | } 136 | 137 | impl<'a> IntoIterator for Index<'a> { 138 | type Item = &'a [u8]; 139 | type IntoIter = IndexIter<'a>; 140 | 141 | #[inline] 142 | fn into_iter(self) -> Self::IntoIter { 143 | IndexIter { data: self, offset_index: 0 } 144 | } 145 | } 146 | 147 | impl<'a> Index<'a> { 148 | #[inline] 149 | pub fn len(&self) -> u32 { 150 | self.offsets.len().saturating_sub(1) 151 | } 152 | 153 | pub fn get(&self, index: u32) -> Option<&'a [u8]> { 154 | let next_index = index.checked_add(1)?; 155 | let start = self.offsets.get(index)? as usize; 156 | let end = self.offsets.get(next_index)? as usize; 157 | self.data.get(start..end) 158 | } 159 | } 160 | 161 | pub struct IndexIter<'a> { 162 | data: Index<'a>, 163 | offset_index: u32, 164 | } 165 | 166 | impl<'a> Iterator for IndexIter<'a> { 167 | type Item = &'a [u8]; 168 | 169 | #[inline] 170 | fn next(&mut self) -> Option { 171 | if self.offset_index == self.data.len() { 172 | return None; 173 | } 174 | 175 | let index = self.offset_index; 176 | self.offset_index += 1; 177 | self.data.get(index) 178 | } 179 | } 180 | 181 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 182 | pub enum OffsetSize { 183 | Size1 = 1, 184 | Size2 = 2, 185 | Size3 = 3, 186 | Size4 = 4, 187 | } 188 | 189 | impl OffsetSize { 190 | #[inline] 191 | pub fn to_u32(self) -> u32 { 192 | self as u32 193 | } 194 | #[inline] 195 | pub fn to_usize(self) -> usize { 196 | self as usize 197 | } 198 | } 199 | 200 | impl Readable<'_> for OffsetSize { 201 | const SIZE: usize = 1; 202 | 203 | fn read(r: &mut Reader<'_>) -> Option { 204 | match r.read::()? { 205 | 1 => Some(OffsetSize::Size1), 206 | 2 => Some(OffsetSize::Size2), 207 | 3 => Some(OffsetSize::Size3), 208 | 4 => Some(OffsetSize::Size4), 209 | _ => None, 210 | } 211 | } 212 | } 213 | 214 | /// An index that owns its data. 215 | pub struct OwnedIndex { 216 | pub data: Vec, 217 | pub header_size: usize, 218 | } 219 | 220 | impl Writeable for OwnedIndex { 221 | fn write(&self, w: &mut Writer) { 222 | w.extend(&self.data); 223 | } 224 | } 225 | 226 | impl Default for OwnedIndex { 227 | fn default() -> Self { 228 | Self { data: vec![0, 0], header_size: 2 } 229 | } 230 | } 231 | 232 | /// Create an index from a vector of data. 233 | pub fn create_index(data: Vec>) -> Result { 234 | let count = u16::try_from(data.len()).map_err(|_| OverflowError)?; 235 | // + 1 Since we start counting from the preceding byte. 236 | let offsize = data.iter().map(|v| v.len() as u32).sum::() + 1; 237 | 238 | // Empty Index only contains the count field 239 | if count == 0 { 240 | return Ok(OwnedIndex::default()); 241 | } 242 | 243 | let offset_size = if offsize <= u8::MAX as u32 { 244 | OffsetSize::Size1 245 | } else if offsize <= u16::MAX as u32 { 246 | OffsetSize::Size2 247 | } else if offsize <= U24::MAX { 248 | OffsetSize::Size3 249 | } else { 250 | OffsetSize::Size4 251 | }; 252 | 253 | let mut w = Writer::new(); 254 | w.write(count); 255 | w.write(offset_size as u8); 256 | 257 | let mut cur_offset: u32 = 0; 258 | 259 | let mut write_offset = |len| { 260 | cur_offset += len; 261 | 262 | match offset_size { 263 | OffsetSize::Size1 => { 264 | let num = u8::try_from(cur_offset).map_err(|_| OverflowError)?; 265 | w.write(num); 266 | } 267 | OffsetSize::Size2 => { 268 | let num = u16::try_from(cur_offset).map_err(|_| OverflowError)?; 269 | w.write(num); 270 | } 271 | OffsetSize::Size3 => { 272 | let num = U24(cur_offset); 273 | w.write(num); 274 | } 275 | OffsetSize::Size4 => w.write(cur_offset), 276 | } 277 | 278 | Ok(()) 279 | }; 280 | 281 | write_offset(1)?; 282 | for el in &data { 283 | write_offset(el.len() as u32)?; 284 | } 285 | 286 | let header_size = w.len(); 287 | 288 | for el in &data { 289 | w.extend(el); 290 | } 291 | 292 | Ok(OwnedIndex { header_size, data: w.finish() }) 293 | } 294 | -------------------------------------------------------------------------------- /src/cff/operator.rs: -------------------------------------------------------------------------------- 1 | use crate::write::{Writeable, Writer}; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | pub const TWO_BYTE_OPERATOR_MARK: u8 = 12; 5 | 6 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 7 | pub enum OperatorType { 8 | OneByteOperator([u8; 1]), 9 | TwoByteOperator([u8; 2]), 10 | } 11 | 12 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 13 | pub struct Operator(pub OperatorType); 14 | 15 | impl Display for Operator { 16 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 17 | match self.0 { 18 | OperatorType::OneByteOperator(b) => write!(f, "{}", b[0]), 19 | OperatorType::TwoByteOperator(b) => write!(f, "{}{}", b[0], b[1]), 20 | } 21 | } 22 | } 23 | 24 | impl Writeable for Operator { 25 | fn write(&self, w: &mut Writer) { 26 | match &self.0 { 27 | OperatorType::OneByteOperator(b) => w.write(b), 28 | OperatorType::TwoByteOperator(b) => w.write(b), 29 | } 30 | } 31 | } 32 | 33 | impl Operator { 34 | pub const fn from_one_byte(b: u8) -> Self { 35 | Self(OperatorType::OneByteOperator([b])) 36 | } 37 | 38 | pub const fn from_two_byte(b: u8) -> Self { 39 | Self(OperatorType::TwoByteOperator([TWO_BYTE_OPERATOR_MARK, b])) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/cff/remapper.rs: -------------------------------------------------------------------------------- 1 | use crate::cff::number::StringId; 2 | use crate::remapper::Remapper; 3 | use std::borrow::Cow; 4 | use std::collections::{BTreeMap, HashMap}; 5 | 6 | pub type FontDictRemapper = Remapper; 7 | 8 | /// Remap old SIDs to new SIDs, and also allow the insertion of 9 | /// new strings. 10 | pub struct SidRemapper<'a> { 11 | /// Next SID to be assigned. 12 | counter: StringId, 13 | /// A map from SIDs to their corresponding string. 14 | sid_to_string: BTreeMap>, 15 | /// A map from strings to their corresponding SID (so the reverse of `sid_to_string`). 16 | string_to_sid: HashMap, StringId>, 17 | /// A map from old SIDs to new SIDs. 18 | old_sid_to_new_sid: HashMap, 19 | } 20 | 21 | impl<'a> SidRemapper<'a> { 22 | pub fn new() -> Self { 23 | Self { 24 | counter: StringId(StringId::STANDARD_STRING_LEN), 25 | sid_to_string: BTreeMap::new(), 26 | string_to_sid: HashMap::new(), 27 | old_sid_to_new_sid: HashMap::new(), 28 | } 29 | } 30 | 31 | pub fn get(&self, string: &[u8]) -> Option { 32 | self.string_to_sid.get(string).copied() 33 | } 34 | 35 | /// Get the new SID to a correpsonding old SID. 36 | pub fn get_new_sid(&self, sid: StringId) -> Option { 37 | self.old_sid_to_new_sid.get(&sid).copied() 38 | } 39 | 40 | /// Remap a string. 41 | pub fn remap(&mut self, string: impl Into> + Clone) -> StringId { 42 | *self.string_to_sid.entry(string.clone().into()).or_insert_with(|| { 43 | let value = self.counter; 44 | self.sid_to_string.insert(value, string.into()); 45 | self.counter = 46 | StringId(self.counter.0.checked_add(1).expect("sid remapper overflowed")); 47 | 48 | value 49 | }) 50 | } 51 | 52 | /// Remap an old SID and its corresponding string. 53 | pub fn remap_with_old_sid( 54 | &mut self, 55 | sid: StringId, 56 | string: impl Into> + Clone, 57 | ) -> StringId { 58 | if let Some(new_sid) = self.old_sid_to_new_sid.get(&sid) { 59 | *new_sid 60 | } else { 61 | let new_sid = self.remap(string); 62 | self.old_sid_to_new_sid.insert(sid, new_sid); 63 | new_sid 64 | } 65 | } 66 | 67 | /// Returns an iterator over the strings, ordered by their new SID. 68 | pub fn sorted_strings(&self) -> impl Iterator> + '_ { 69 | self.sid_to_string.values() 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use crate::cff::number::StringId; 76 | use crate::cff::remapper::SidRemapper; 77 | use std::borrow::Cow; 78 | 79 | #[test] 80 | fn test_remap_1() { 81 | let mut sid_remapper = SidRemapper::new(); 82 | assert_eq!(sid_remapper.remap(b"hi".to_vec()), StringId(391)); 83 | assert_eq!(sid_remapper.remap(b"there".to_vec()), StringId(392)); 84 | assert_eq!(sid_remapper.remap(b"hi".to_vec()), StringId(391)); 85 | assert_eq!(sid_remapper.remap(b"test".to_vec()), StringId(393)); 86 | 87 | assert_eq!( 88 | sid_remapper.sorted_strings().cloned().collect::>(), 89 | vec![ 90 | Cow::<[u8]>::Owned(b"hi".to_vec()), 91 | Cow::Owned(b"there".to_vec()), 92 | Cow::Owned(b"test".to_vec()) 93 | ] 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/cff/sid_font.rs: -------------------------------------------------------------------------------- 1 | use crate::cff::dict::private_dict::parse_subr_offset; 2 | use crate::cff::dict::top_dict::TopDictData; 3 | use crate::cff::index::{parse_index, Index}; 4 | use crate::read::Reader; 5 | use crate::write::Writer; 6 | use crate::GlyphRemapper; 7 | 8 | /// Metadata required for handling SID-keyed fonts. 9 | #[derive(Clone, Copy, Default, Debug)] 10 | pub struct SIDMetadata<'a> { 11 | pub local_subrs: Index<'a>, 12 | pub private_dict_data: &'a [u8], 13 | } 14 | 15 | // The parsing logic was taken from ttf-parser. 16 | pub fn parse_sid_metadata<'a>(data: &'a [u8], top_dict: &TopDictData) -> SIDMetadata<'a> { 17 | top_dict 18 | .private 19 | .clone() 20 | .and_then(|private_dict_range| { 21 | let mut metadata = SIDMetadata::default(); 22 | let private_dict_data = data.get(private_dict_range.clone())?; 23 | 24 | metadata.local_subrs = 25 | if let Some(subrs_offset) = parse_subr_offset(private_dict_data) { 26 | let start = private_dict_range.start.checked_add(subrs_offset)?; 27 | let subrs_data = data.get(start..)?; 28 | let mut r = Reader::new(subrs_data); 29 | parse_index::(&mut r)? 30 | } else { 31 | Index::default() 32 | }; 33 | 34 | metadata.private_dict_data = private_dict_data; 35 | Some(metadata) 36 | }) 37 | .unwrap_or_default() 38 | } 39 | 40 | /// Write the FD INDEX for SID-keyed fonts. 41 | /// They all get mapped to the font DICT 0. 42 | pub fn generate_fd_index( 43 | gid_remapper: &GlyphRemapper, 44 | w: &mut Writer, 45 | ) -> crate::Result<()> { 46 | // Format 47 | w.write::(3); 48 | // nRanges 49 | w.write::(1); 50 | // first 51 | w.write::(0); 52 | // fd index 53 | w.write::(0); 54 | // sentinel 55 | w.write::(gid_remapper.num_gids()); 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /src/cff/subroutines.rs: -------------------------------------------------------------------------------- 1 | use crate::cff::charstring::CharString; 2 | 3 | /// A wrapper over a vector of subroutine containers (for local subroutines, where 4 | /// we have a list of subroutines for each font dict). 5 | pub struct SubroutineCollection<'a> { 6 | subroutines: Vec>, 7 | } 8 | 9 | impl<'a> SubroutineCollection<'a> { 10 | pub fn new(subroutines: Vec>>) -> Self { 11 | debug_assert!(subroutines.len() <= 255); 12 | Self { 13 | subroutines: subroutines.into_iter().map(SubroutineContainer::new).collect(), 14 | } 15 | } 16 | 17 | pub fn get_handler(&self, fd_index: u8) -> Option { 18 | self.subroutines.get(fd_index as usize).map(|s| s.get_handler()) 19 | } 20 | } 21 | 22 | /// A wrapper over a vector of charstrings (for global subroutines). 23 | pub struct SubroutineContainer<'a> { 24 | subroutines: Vec>, 25 | } 26 | 27 | impl<'a> SubroutineContainer<'a> { 28 | pub fn new(subroutines: Vec>) -> Self { 29 | Self { subroutines } 30 | } 31 | 32 | pub fn get_handler(&self) -> SubroutineHandler { 33 | SubroutineHandler::new(self.subroutines.as_ref()) 34 | } 35 | } 36 | 37 | /// Wrapper over a list of subroutines to allow for convenient access to subroutines 38 | /// given a biased or unbiased index. 39 | #[derive(Clone)] 40 | pub struct SubroutineHandler<'a> { 41 | subroutines: &'a [CharString<'a>], 42 | bias: u16, 43 | } 44 | 45 | impl<'a> SubroutineHandler<'a> { 46 | pub fn new(char_strings: &'a [CharString<'a>]) -> Self { 47 | Self { 48 | subroutines: char_strings, 49 | bias: calc_subroutine_bias(char_strings.len() as u32), 50 | } 51 | } 52 | 53 | pub fn get_with_biased(&self, index: i32) -> Option> { 54 | self.get_with_unbiased(unapply_bias(index, self.bias)?) 55 | } 56 | 57 | pub fn get_with_unbiased(&self, index: u32) -> Option> { 58 | self.subroutines.get(index as usize).copied() 59 | } 60 | } 61 | 62 | fn calc_subroutine_bias(len: u32) -> u16 { 63 | if len < 1240 { 64 | 107 65 | } else if len < 33900 { 66 | 1131 67 | } else { 68 | 32768 69 | } 70 | } 71 | 72 | /// Unapply the bias from a biased subroutine offset. 73 | pub fn unapply_bias(index: i32, bias: u16) -> Option { 74 | let bias = i32::from(bias); 75 | 76 | u32::try_from(index.checked_add(bias)?).ok() 77 | } 78 | -------------------------------------------------------------------------------- /src/glyf.rs: -------------------------------------------------------------------------------- 1 | //! The `glyf` table contains the main description of the glyphs. In order to 2 | //! subset it, there are 5 things we need to do: 3 | //! 1. We need to form the glyph closure. Glyphs can reference other glyphs, meaning that 4 | //! if a user for example requests the glyph 1, and this glyph references the glyph 2, then 5 | //! we need to include both of them in our subset. 6 | //! 2. We need to remove glyph descriptions that are not needed for the subset, and reorder 7 | //! the existing glyph descriptions to match the order defined by the remapper. 8 | //! 3. For component glyphs, we need to rewrite their description so that they reference 9 | //! the new glyph ID of the glyphs they reference. 10 | //! 4. We need to calculate which format to use in the `loca` table. 11 | //! 5. We need to update the `loca` table itself with the new offsets. 12 | use super::*; 13 | 14 | /// Form the glyph closure of all glyphs in `gid_set`. 15 | pub fn closure(face: &Face, glyph_remapper: &mut GlyphRemapper) -> Result<()> { 16 | let table = Table::new(face).ok_or(MalformedFont)?; 17 | 18 | let mut process_glyphs = glyph_remapper.remapped_gids().collect::>(); 19 | 20 | while let Some(glyph) = process_glyphs.pop() { 21 | glyph_remapper.remap(glyph); 22 | 23 | let glyph_data = match table.glyph_data(glyph) { 24 | Some(glph_data) => glph_data, 25 | None => continue, 26 | }; 27 | 28 | if glyph_data.is_empty() { 29 | continue; 30 | } 31 | 32 | let mut r = Reader::new(glyph_data); 33 | let num_contours = r.read::().ok_or(MalformedFont)?; 34 | 35 | // If we have a composite glyph, add its components to the closure. 36 | if num_contours < 0 { 37 | for component in component_glyphs(glyph_data).ok_or(MalformedFont)? { 38 | if glyph_remapper.get(component).is_none() { 39 | process_glyphs.push(component); 40 | } 41 | } 42 | } 43 | } 44 | 45 | Ok(()) 46 | } 47 | 48 | pub fn subset(ctx: &mut Context) -> Result<()> { 49 | let subsetted_entries = subset_glyf_entries(ctx)?; 50 | 51 | let mut sub_glyf = Writer::new(); 52 | let mut sub_loca = Writer::new(); 53 | 54 | let mut write_offset = |offset: usize| { 55 | if ctx.long_loca { 56 | sub_loca.write::(offset as u32); 57 | } else { 58 | sub_loca.write::((offset / 2) as u16); 59 | } 60 | }; 61 | 62 | for entry in &subsetted_entries { 63 | write_offset(sub_glyf.len()); 64 | sub_glyf.extend(entry); 65 | 66 | if !ctx.long_loca { 67 | sub_glyf.align(2); 68 | } 69 | } 70 | 71 | // Write the final offset. 72 | write_offset(sub_glyf.len()); 73 | 74 | ctx.push(Tag::LOCA, sub_loca.finish()); 75 | ctx.push(Tag::GLYF, sub_glyf.finish()); 76 | 77 | Ok(()) 78 | } 79 | 80 | /// A glyf + loca table. 81 | struct Table<'a> { 82 | loca: &'a [u8], 83 | glyf: &'a [u8], 84 | long: bool, 85 | } 86 | 87 | impl<'a> Table<'a> { 88 | fn new(face: &Face<'a>) -> Option { 89 | let loca = face.table(Tag::LOCA)?; 90 | let glyf = face.table(Tag::GLYF)?; 91 | let head = face.table(Tag::HEAD)?; 92 | 93 | let mut r = Reader::new_at(head, 50); 94 | let long = r.read::()? != 0; 95 | Some(Self { loca, glyf, long }) 96 | } 97 | 98 | fn glyph_data(&self, id: u16) -> Option<&'a [u8]> { 99 | let read_offset = |n| { 100 | Some(if self.long { 101 | let mut r = Reader::new_at(self.loca, 4 * n); 102 | r.read::()? as usize 103 | } else { 104 | let mut r = Reader::new_at(self.loca, 2 * n); 105 | 2 * r.read::()? as usize 106 | }) 107 | }; 108 | 109 | let from = read_offset(id as usize)?; 110 | let to = read_offset(id as usize + 1)?; 111 | self.glyf.get(from..to) 112 | } 113 | } 114 | 115 | fn subset_glyf_entries<'a>(ctx: &mut Context<'a>) -> Result>> { 116 | let table = Table::new(&ctx.face).ok_or(MalformedFont)?; 117 | 118 | let mut size = 0; 119 | let mut glyf_entries = vec![]; 120 | 121 | for old_gid in ctx.mapper.remapped_gids() { 122 | let glyph_data = table.glyph_data(old_gid).ok_or(MalformedFont)?; 123 | 124 | // Empty glyph. 125 | if glyph_data.is_empty() { 126 | glyf_entries.push(Cow::Borrowed(glyph_data)); 127 | continue; 128 | } 129 | 130 | let mut r = Reader::new(glyph_data); 131 | let num_contours = r.read::().ok_or(MalformedFont)?; 132 | 133 | let glyph_data = if num_contours < 0 { 134 | Cow::Owned(remap_component_glyph(&ctx.mapper, glyph_data)?) 135 | } else { 136 | // Simple glyphs don't need any subsetting. 137 | Cow::Borrowed(glyph_data) 138 | }; 139 | 140 | let mut len = glyph_data.len(); 141 | len += (len % 2 != 0) as usize; 142 | size += len; 143 | 144 | glyf_entries.push(glyph_data); 145 | } 146 | 147 | // Decide on the loca format. 148 | ctx.long_loca = size > 2 * (u16::MAX as usize); 149 | 150 | Ok(glyf_entries) 151 | } 152 | 153 | fn remap_component_glyph(mapper: &GlyphRemapper, data: &[u8]) -> Result> { 154 | let mut r = Reader::new(data); 155 | let mut w = Writer::with_capacity(data.len()); 156 | 157 | // number of contours 158 | w.write(r.read::().ok_or(MalformedFont)?); 159 | 160 | // xMin, yMin, xMax, yMax 161 | w.write(r.read::().ok_or(MalformedFont)?); 162 | w.write(r.read::().ok_or(MalformedFont)?); 163 | w.write(r.read::().ok_or(MalformedFont)?); 164 | w.write(r.read::().ok_or(MalformedFont)?); 165 | 166 | const ARG_1_AND_2_ARE_WORDS: u16 = 0x0001; 167 | const WE_HAVE_A_SCALE: u16 = 0x0008; 168 | const MORE_COMPONENTS: u16 = 0x0020; 169 | const WE_HAVE_AN_X_AND_Y_SCALE: u16 = 0x0040; 170 | const WE_HAVE_A_TWO_BY_TWO: u16 = 0x0080; 171 | const WE_HAVE_INSTRUCTIONS: u16 = 0x0100; 172 | 173 | let mut done; 174 | 175 | loop { 176 | let flags = r.read::().ok_or(MalformedFont)?; 177 | w.write(flags); 178 | let old_component = r.read::().ok_or(MalformedFont)?; 179 | let new_component = mapper.get(old_component).ok_or(MalformedFont)?; 180 | w.write(new_component); 181 | 182 | if flags & ARG_1_AND_2_ARE_WORDS != 0 { 183 | w.write(r.read::().ok_or(MalformedFont)?); 184 | w.write(r.read::().ok_or(MalformedFont)?); 185 | } else { 186 | w.write(r.read::().ok_or(MalformedFont)?); 187 | } 188 | 189 | if flags & WE_HAVE_A_SCALE != 0 { 190 | w.write(r.read::().ok_or(MalformedFont)?); 191 | } else if flags & WE_HAVE_AN_X_AND_Y_SCALE != 0 { 192 | w.write(r.read::().ok_or(MalformedFont)?); 193 | w.write(r.read::().ok_or(MalformedFont)?); 194 | } else if flags & WE_HAVE_A_TWO_BY_TWO != 0 { 195 | w.write(r.read::().ok_or(MalformedFont)?); 196 | w.write(r.read::().ok_or(MalformedFont)?); 197 | w.write(r.read::().ok_or(MalformedFont)?); 198 | w.write(r.read::().ok_or(MalformedFont)?); 199 | } 200 | 201 | done = flags & MORE_COMPONENTS == 0; 202 | 203 | if done { 204 | if flags & WE_HAVE_INSTRUCTIONS != 0 { 205 | w.write(r.tail().ok_or(MalformedFont)?); 206 | } 207 | 208 | break; 209 | } 210 | } 211 | 212 | Ok(w.finish()) 213 | } 214 | 215 | /// Returns an iterator over the component glyphs of a glyph. 216 | fn component_glyphs(glyph_data: &[u8]) -> Option + '_> { 217 | let mut r = Reader::new(glyph_data); 218 | 219 | // Number of contours 220 | r.read::()?; 221 | 222 | // xMin, yMin, xMax, yMax 223 | r.read::()?; 224 | r.read::()?; 225 | r.read::()?; 226 | r.read::()?; 227 | 228 | const ARG_1_AND_2_ARE_WORDS: u16 = 0x0001; 229 | const WE_HAVE_A_SCALE: u16 = 0x0008; 230 | const MORE_COMPONENTS: u16 = 0x0020; 231 | const WE_HAVE_AN_X_AND_Y_SCALE: u16 = 0x0040; 232 | const WE_HAVE_A_TWO_BY_TWO: u16 = 0x0080; 233 | 234 | let mut done = false; 235 | Some(std::iter::from_fn(move || { 236 | if done { 237 | return None; 238 | } 239 | 240 | let flags = r.read::()?; 241 | let component = r.read::()?; 242 | 243 | if flags & ARG_1_AND_2_ARE_WORDS != 0 { 244 | r.read::(); 245 | r.read::(); 246 | } else { 247 | r.read::(); 248 | } 249 | 250 | if flags & WE_HAVE_A_SCALE != 0 { 251 | r.read::(); 252 | } else if flags & WE_HAVE_AN_X_AND_Y_SCALE != 0 { 253 | r.read::(); 254 | r.read::(); 255 | } else if flags & WE_HAVE_A_TWO_BY_TWO != 0 { 256 | r.read::(); 257 | r.read::(); 258 | r.read::(); 259 | r.read::(); 260 | } 261 | 262 | done = flags & MORE_COMPONENTS == 0; 263 | Some(component) 264 | })) 265 | } 266 | -------------------------------------------------------------------------------- /src/head.rs: -------------------------------------------------------------------------------- 1 | //! The `head` table mostly contains information that can be reused from the 2 | //! old table, except for the `loca` format, which depends on the size of the 3 | //! glyph data. The checksum will be recalculated in the very end. 4 | 5 | use super::*; 6 | 7 | pub fn subset(ctx: &mut Context) -> Result<()> { 8 | let mut head = ctx.expect_table(Tag::HEAD).ok_or(MalformedFont)?.to_vec(); 9 | let index_to_loc = head.get_mut(50..52).ok_or(MalformedFont)?; 10 | index_to_loc[0] = 0; 11 | index_to_loc[1] = ctx.long_loca as u8; 12 | ctx.push(Tag::HEAD, head); 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /src/hmtx.rs: -------------------------------------------------------------------------------- 1 | //! The `hmtx` table contains the horizontal metrics for each glyph. 2 | //! All we need to do is to rewrite the table so that it matches the 3 | //! sequence of the new glyphs. A minor pain point is that the table 4 | //! allows omitting the advance width for the last few glyphs if it is 5 | //! the same. In order to keep the code simple, we do not keep this optimization 6 | //! when rewriting the table. 7 | //! While doing so, we also rewrite the `hhea` table, which contains 8 | //! the number of glyphs that contain both, advance width and 9 | //! left side bearing metrics. 10 | 11 | // The parsing logic was taken from ttf-parser. 12 | 13 | use super::*; 14 | use crate::Error::OverflowError; 15 | 16 | pub fn subset(ctx: &mut Context) -> Result<()> { 17 | let hmtx = ctx.expect_table(Tag::HMTX).ok_or(MalformedFont)?; 18 | 19 | let new_metrics = { 20 | let mut new_metrics = vec![]; 21 | 22 | // Extract the number of horizontal metrics from the `hhea` table. 23 | let num_h_metrics = { 24 | let hhea = ctx.expect_table(Tag::HHEA).ok_or(MalformedFont)?; 25 | let mut r = Reader::new(hhea); 26 | r.skip_bytes(34); 27 | r.read::().ok_or(MalformedFont)? 28 | }; 29 | 30 | let last_advance_width = { 31 | let index = 4 * num_h_metrics.checked_sub(1).ok_or(OverflowError)? as usize; 32 | let mut r = Reader::new(hmtx.get(index..).ok_or(MalformedFont)?); 33 | r.read::().ok_or(MalformedFont)? 34 | }; 35 | 36 | for old_gid in ctx.mapper.remapped_gids() { 37 | let has_advance_width = old_gid < num_h_metrics; 38 | 39 | let offset = if has_advance_width { 40 | old_gid as usize * 4 41 | } else { 42 | let num_h_metrics = num_h_metrics as usize; 43 | num_h_metrics * 4 + (old_gid as usize - num_h_metrics) * 2 44 | }; 45 | 46 | let mut r = Reader::new(hmtx.get(offset..).ok_or(MalformedFont)?); 47 | 48 | if has_advance_width { 49 | let adv = r.read::().ok_or(MalformedFont)?; 50 | let lsb = r.read::().ok_or(MalformedFont)?; 51 | new_metrics.push((adv, lsb)); 52 | } else { 53 | new_metrics 54 | .push((last_advance_width, r.read::().ok_or(MalformedFont)?)); 55 | } 56 | } 57 | 58 | new_metrics 59 | }; 60 | 61 | // Find out the last index we need to include the advance width for. 62 | let mut last_advance_width_index = 63 | u16::try_from(new_metrics.len()).map_err(|_| OverflowError)? - 1; 64 | let last_advance_width = new_metrics[last_advance_width_index as usize].0; 65 | 66 | for gid in new_metrics.iter().rev().skip(1) { 67 | if gid.0 == last_advance_width { 68 | last_advance_width_index -= 1; 69 | } else { 70 | break; 71 | } 72 | } 73 | 74 | let mut sub_hmtx = Writer::new(); 75 | 76 | for (index, metric) in new_metrics.iter().enumerate() { 77 | let index = u16::try_from(index).map_err(|_| OverflowError)?; 78 | if index <= last_advance_width_index { 79 | sub_hmtx.write::(metric.0); 80 | } 81 | 82 | sub_hmtx.write::(metric.1); 83 | } 84 | 85 | ctx.push(Tag::HMTX, sub_hmtx.finish()); 86 | 87 | let hhea = ctx.expect_table(Tag::HHEA).ok_or(MalformedFont)?; 88 | let mut sub_hhea = Writer::new(); 89 | sub_hhea.extend(&hhea[0..hhea.len() - 2]); 90 | sub_hhea.write::(last_advance_width_index + 1); 91 | 92 | ctx.push(Tag::HHEA, sub_hhea.finish()); 93 | 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /src/maxp.rs: -------------------------------------------------------------------------------- 1 | //! The `maxp` table contains the number of glyphs (and some additional information 2 | //! depending on the version). All we need to do is rewrite the number of glyphs, the rest 3 | //! can be copied from the old table. 4 | 5 | use super::*; 6 | 7 | pub fn subset(ctx: &mut Context) -> Result<()> { 8 | let maxp = ctx.expect_table(Tag::MAXP).ok_or(MalformedFont)?; 9 | let mut r = Reader::new(maxp); 10 | let version = r.read::().ok_or(MalformedFont)?; 11 | // number of glyphs 12 | r.read::().ok_or(MalformedFont)?; 13 | 14 | let mut sub_maxp = Writer::new(); 15 | sub_maxp.write::(version); 16 | sub_maxp.write::(ctx.mapper.num_gids()); 17 | 18 | if version == 0x00010000 { 19 | sub_maxp.extend(r.tail().ok_or(MalformedFont)?); 20 | } 21 | 22 | ctx.push(Tag::MAXP, sub_maxp.finish()); 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /src/name.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::Error::{MalformedFont, SubsetError}; 3 | use std::collections::HashMap; 4 | 5 | pub fn subset(ctx: &mut Context) -> Result<()> { 6 | let name = ctx.expect_table(Tag::NAME).ok_or(MalformedFont)?; 7 | let mut r = Reader::new(name); 8 | 9 | let version = r.read::().ok_or(MalformedFont)?; 10 | 11 | // From my personal experiments, version 1 isn't used at all, so we 12 | // don't bother subsetting it. 13 | if version != 0 { 14 | ctx.push(Tag::NAME, name); 15 | return Ok(()); 16 | } 17 | 18 | let table = Table::parse(name).ok_or(MalformedFont)?; 19 | let subsetted_table = subset_table(&table).ok_or(SubsetError)?; 20 | 21 | let mut w = Writer::new(); 22 | w.write(subsetted_table); 23 | 24 | ctx.push(Tag::NAME, w.finish()); 25 | Ok(()) 26 | } 27 | 28 | pub fn subset_table<'a>(table: &Table<'a>) -> Option> { 29 | let mut names = table 30 | .names 31 | .iter() 32 | .copied() 33 | .filter(|record| { 34 | record.is_unicode() && [0, 1, 2, 3, 4, 5, 6].contains(&record.name_id) 35 | }) 36 | .collect::>(); 37 | 38 | let mut storage = Vec::new(); 39 | let mut cur_storage_offset = 0; 40 | 41 | let mut name_deduplicator: HashMap<&[u8], u16> = HashMap::new(); 42 | 43 | for record in &mut names { 44 | let name = table.storage.get( 45 | (record.string_offset as usize) 46 | ..((record.string_offset + record.length) as usize), 47 | )?; 48 | let offset = *name_deduplicator.entry(name).or_insert_with(|| { 49 | storage.extend(name); 50 | let offset = cur_storage_offset; 51 | cur_storage_offset += record.length; 52 | offset 53 | }); 54 | 55 | record.string_offset = offset; 56 | } 57 | 58 | Some(Table { names, storage: Cow::Owned(storage) }) 59 | } 60 | 61 | impl Writeable for Table<'_> { 62 | fn write(&self, w: &mut Writer) { 63 | let count = u16::try_from(self.names.len()).unwrap(); 64 | 65 | // version 66 | w.write::(0); 67 | // count 68 | w.write::(count); 69 | // storage offset 70 | w.write::(u16::SIZE as u16 * 3 + count * NameRecord::SIZE as u16); 71 | for name in &self.names { 72 | w.write(name); 73 | } 74 | w.extend(&self.storage); 75 | } 76 | } 77 | 78 | impl Writeable for &NameRecord { 79 | fn write(&self, w: &mut Writer) { 80 | w.write::(self.platform_id); 81 | w.write::(self.encoding_id); 82 | w.write::(self.language_id); 83 | w.write::(self.name_id); 84 | w.write::(self.length); 85 | w.write::(self.string_offset); 86 | } 87 | } 88 | 89 | #[derive(Clone, Debug)] 90 | pub struct Table<'a> { 91 | pub names: Vec, 92 | pub storage: Cow<'a, [u8]>, 93 | } 94 | 95 | impl<'a> Table<'a> { 96 | // The parsing logic was adapted from ttf-parser. 97 | pub fn parse(data: &'a [u8]) -> Option { 98 | let mut r = Reader::new(data); 99 | 100 | let version = r.read::()?; 101 | 102 | if version != 0 { 103 | return None; 104 | } 105 | 106 | let count = r.read::()?; 107 | r.read::()?; // storage offset 108 | 109 | let mut names = Vec::with_capacity(count as usize); 110 | 111 | for _ in 0..count { 112 | names.push(r.read::()?); 113 | } 114 | 115 | let storage = Cow::Borrowed(r.tail()?); 116 | 117 | Some(Self { names, storage }) 118 | } 119 | } 120 | 121 | impl Readable<'_> for NameRecord { 122 | const SIZE: usize = u16::SIZE * 6; 123 | 124 | fn read(r: &mut Reader<'_>) -> Option { 125 | let platform_id = r.read::()?; 126 | let encoding_id = r.read::()?; 127 | let language_id = r.read::()?; 128 | let name_id = r.read::()?; 129 | let length = r.read::()?; 130 | let string_offset = r.read::()?; 131 | 132 | Some(Self { 133 | platform_id, 134 | encoding_id, 135 | language_id, 136 | name_id, 137 | length, 138 | string_offset, 139 | }) 140 | } 141 | } 142 | 143 | #[derive(Clone, Copy, Debug)] 144 | pub struct NameRecord { 145 | pub platform_id: u16, 146 | pub encoding_id: u16, 147 | pub language_id: u16, 148 | pub name_id: u16, 149 | pub length: u16, 150 | pub string_offset: u16, 151 | } 152 | 153 | impl NameRecord { 154 | pub fn is_unicode(&self) -> bool { 155 | self.platform_id == 0 156 | || (self.platform_id == 3 && [0, 1, 10].contains(&self.encoding_id)) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/post.rs: -------------------------------------------------------------------------------- 1 | //! Subset the `post` table. The `post` table contains name information for glyphs 2 | //! needed for some PostScript printers. Only version 2 table contains actual custom names, 3 | //! so this is the only version that we need to subset. All we need to do is to extract 4 | //! the strings for all requested glyphs and write them into a new `post` table in the 5 | //! given order. 6 | 7 | use super::*; 8 | use crate::read::LazyArray16; 9 | use crate::Error::OverflowError; 10 | 11 | pub fn subset(ctx: &mut Context) -> Result<()> { 12 | let post = ctx.expect_table(Tag::POST).ok_or(MalformedFont)?; 13 | let mut r = Reader::new(post); 14 | 15 | let version = r.read::().ok_or(MalformedFont)?; 16 | if version != 0x00020000 { 17 | ctx.push(Tag::POST, post); 18 | return Ok(()); 19 | } 20 | 21 | let table = Version2Table::parse(post).ok_or(MalformedFont)?; 22 | let names = table.names().collect::>(); 23 | 24 | let mut sub_post = Writer::new(); 25 | sub_post.extend(table.header); 26 | sub_post.write(ctx.mapper.num_gids()); 27 | 28 | let mut string_storage = Writer::new(); 29 | let mut string_index = 0; 30 | 31 | for old_gid in ctx.mapper.remapped_gids() { 32 | let index = table.glyph_indexes.get(old_gid).ok_or(MalformedFont)?; 33 | 34 | // IDs smaller than 258 refer to the names in the Macintosh TrueType file. 35 | if index <= 257 { 36 | sub_post.write(index); 37 | } else { 38 | let index = index - 258; 39 | // Phetsarath-Regular.ttf from Google Fonts seems to have a wrong name table. 40 | // If name cannot be fetched, use empty name instead. 41 | let name = names.get(index as usize).copied().unwrap_or(&[][..]); 42 | let name_len = u8::try_from(name.len()).map_err(|_| OverflowError)?; 43 | let index = u16::try_from(string_index + 258).map_err(|_| OverflowError)?; 44 | sub_post.write(index); 45 | 46 | string_storage.write(name_len); 47 | string_storage.write(name); 48 | string_index += 1; 49 | } 50 | } 51 | 52 | sub_post.extend(&string_storage.finish()); 53 | 54 | ctx.push(Tag::POST, sub_post.finish()); 55 | Ok(()) 56 | } 57 | 58 | /// An iterator over glyph names. 59 | /// 60 | /// The `post` table doesn't provide the glyph names count, 61 | /// so we have to simply iterate over all of them to find it out. 62 | #[derive(Clone, Copy, Default)] 63 | pub struct Names<'a> { 64 | data: &'a [u8], 65 | offset: usize, 66 | } 67 | 68 | impl<'a> Iterator for Names<'a> { 69 | type Item = &'a [u8]; 70 | 71 | fn next(&mut self) -> Option { 72 | if self.offset >= self.data.len() { 73 | return None; 74 | } 75 | 76 | let len = self.data[self.offset]; 77 | self.offset += 1; 78 | 79 | // An empty name is an error. 80 | if len == 0 { 81 | return None; 82 | } 83 | 84 | let name = self.data.get(self.offset..self.offset + usize::from(len))?; 85 | self.offset += usize::from(len); 86 | Some(name) 87 | } 88 | } 89 | 90 | /// A version 2 `name` table. 91 | #[derive(Clone, Debug)] 92 | pub struct Version2Table<'a> { 93 | pub header: &'a [u8], 94 | pub glyph_indexes: LazyArray16<'a, u16>, 95 | pub names_data: &'a [u8], 96 | } 97 | 98 | impl<'a> Version2Table<'a> { 99 | /// Parse a version 2 table. 100 | pub fn parse(data: &'a [u8]) -> Option { 101 | // Do not check the exact length, because some fonts include 102 | // padding in table's length in table records, which is incorrect. 103 | if data.len() < 32 || Reader::new(data).read::()? != 0x00020000 { 104 | return None; 105 | } 106 | 107 | let mut r = Reader::new(data); 108 | let header = r.read_bytes(32)?; 109 | 110 | let indexes_count = r.read::()?; 111 | let glyph_indexes = r.read_array16::(indexes_count)?; 112 | let names_data = r.tail()?; 113 | 114 | Some(Version2Table { header, glyph_indexes, names_data }) 115 | } 116 | 117 | /// Returns an iterator over glyph names. 118 | /// 119 | /// Default/predefined names are not included. Just the one in the font file. 120 | pub fn names(&self) -> Names<'a> { 121 | Names { data: self.names_data, offset: 0 } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/read.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | #[derive(Clone, Debug)] 4 | /// A readable stream of binary data. 5 | pub struct Reader<'a> { 6 | /// The underlying data of the reader. 7 | data: &'a [u8], 8 | /// The current offset in bytes. Is not guaranteed to be in range. 9 | offset: usize, 10 | } 11 | 12 | impl<'a> Reader<'a> { 13 | /// Create a new readable stream of binary data. 14 | #[inline] 15 | pub fn new(data: &'a [u8]) -> Self { 16 | Self { data, offset: 0 } 17 | } 18 | 19 | /// Create a new readable stream of binary data at a specific position. 20 | #[inline] 21 | pub fn new_at(data: &'a [u8], offset: usize) -> Self { 22 | Self { data, offset } 23 | } 24 | 25 | /// The remaining data from the current offset. 26 | #[inline] 27 | pub fn tail(&self) -> Option<&'a [u8]> { 28 | self.data.get(self.offset..) 29 | } 30 | 31 | /// Returns the current offset. 32 | #[inline] 33 | pub fn offset(&self) -> usize { 34 | self.offset 35 | } 36 | 37 | /// Try to read `T` from the data. 38 | #[inline] 39 | pub fn read>(&mut self) -> Option { 40 | T::read(self) 41 | } 42 | 43 | /// Try to read `T` from the data. 44 | #[inline] 45 | pub fn peak>(&mut self) -> Option { 46 | let mut r = self.clone(); 47 | T::read(&mut r) 48 | } 49 | 50 | /// Read a certain number of bytes. 51 | #[inline] 52 | pub fn read_bytes(&mut self, len: usize) -> Option<&'a [u8]> { 53 | let v = self.data.get(self.offset..self.offset + len)?; 54 | self.offset += len; 55 | Some(v) 56 | } 57 | 58 | /// Reads the next `count` types as a slice. 59 | #[inline] 60 | pub fn read_array16>( 61 | &mut self, 62 | count: u16, 63 | ) -> Option> { 64 | let len = usize::from(count) * T::SIZE; 65 | self.read_bytes(len).map(LazyArray16::new) 66 | } 67 | 68 | /// Advances by `Readable::SIZE`. 69 | #[inline] 70 | pub fn skip>(&mut self) { 71 | self.skip_bytes(T::SIZE); 72 | } 73 | 74 | /// Check whether the reader is at the end of the buffer. 75 | #[inline] 76 | pub fn at_end(&self) -> bool { 77 | self.offset >= self.data.len() 78 | } 79 | 80 | /// Jump to a specific location. 81 | #[inline] 82 | pub fn jump(&mut self, offset: usize) { 83 | self.offset = offset; 84 | } 85 | 86 | /// Skip the next `n` bytes from the stream. 87 | #[inline] 88 | pub fn skip_bytes(&mut self, n: usize) { 89 | self.read_bytes(n); 90 | } 91 | } 92 | 93 | /// Trait for an object that can be read from a byte stream with a fixed size. 94 | pub trait Readable<'a>: Sized { 95 | const SIZE: usize; 96 | 97 | fn read(r: &mut Reader<'a>) -> Option; 98 | } 99 | 100 | impl Readable<'_> for [u8; N] { 101 | const SIZE: usize = u8::SIZE * N; 102 | 103 | fn read(r: &mut Reader) -> Option { 104 | Some(r.read_bytes(N)?.try_into().unwrap_or([0; N])) 105 | } 106 | } 107 | 108 | impl Readable<'_> for u8 { 109 | const SIZE: usize = 1; 110 | 111 | fn read(r: &mut Reader) -> Option { 112 | r.read::<[u8; 1]>().map(Self::from_be_bytes) 113 | } 114 | } 115 | 116 | impl Readable<'_> for u16 { 117 | const SIZE: usize = 2; 118 | 119 | fn read(r: &mut Reader) -> Option { 120 | r.read::<[u8; 2]>().map(Self::from_be_bytes) 121 | } 122 | } 123 | 124 | impl Readable<'_> for i16 { 125 | const SIZE: usize = 2; 126 | 127 | fn read(r: &mut Reader) -> Option { 128 | r.read::<[u8; 2]>().map(Self::from_be_bytes) 129 | } 130 | } 131 | 132 | impl Readable<'_> for u32 { 133 | const SIZE: usize = 4; 134 | 135 | fn read(r: &mut Reader) -> Option { 136 | r.read::<[u8; 4]>().map(Self::from_be_bytes) 137 | } 138 | } 139 | 140 | impl Readable<'_> for i32 { 141 | const SIZE: usize = 4; 142 | 143 | fn read(r: &mut Reader) -> Option { 144 | r.read::<[u8; 4]>().map(Self::from_be_bytes) 145 | } 146 | } 147 | 148 | /// A slice-like container that converts internal binary data only on access. 149 | /// 150 | /// Array values are stored in a continuous data chunk. 151 | #[derive(Clone, Copy)] 152 | pub struct LazyArray16<'a, T> { 153 | data: &'a [u8], 154 | data_type: core::marker::PhantomData, 155 | } 156 | 157 | impl Default for LazyArray16<'_, T> { 158 | #[inline] 159 | fn default() -> Self { 160 | LazyArray16 { data: &[], data_type: core::marker::PhantomData } 161 | } 162 | } 163 | 164 | impl<'a, T: Readable<'a>> LazyArray16<'a, T> { 165 | /// Creates a new `LazyArray`. 166 | #[inline] 167 | pub fn new(data: &'a [u8]) -> Self { 168 | LazyArray16 { data, data_type: core::marker::PhantomData } 169 | } 170 | 171 | /// Returns a value at `index`. 172 | #[inline] 173 | pub fn get(&self, index: u16) -> Option { 174 | if index < self.len() { 175 | let start = usize::from(index) * T::SIZE; 176 | let end = start + T::SIZE; 177 | self.data 178 | .get(start..end) 179 | .map(Reader::new) 180 | .and_then(|mut r| T::read(&mut r)) 181 | } else { 182 | None 183 | } 184 | } 185 | 186 | /// Returns array's length. 187 | #[inline] 188 | pub fn len(&self) -> u16 { 189 | (self.data.len() / T::SIZE) as u16 190 | } 191 | } 192 | 193 | impl<'a, T: Readable<'a> + core::fmt::Debug + Copy> core::fmt::Debug 194 | for LazyArray16<'a, T> 195 | { 196 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 197 | f.debug_list().entries(*self).finish() 198 | } 199 | } 200 | 201 | impl<'a, T: Readable<'a>> IntoIterator for LazyArray16<'a, T> { 202 | type Item = T; 203 | type IntoIter = LazyArrayIter16<'a, T>; 204 | 205 | #[inline] 206 | fn into_iter(self) -> Self::IntoIter { 207 | LazyArrayIter16 { data: self, index: 0 } 208 | } 209 | } 210 | 211 | /// An iterator over `LazyArray16`. 212 | #[derive(Clone, Copy)] 213 | #[allow(missing_debug_implementations)] 214 | pub struct LazyArrayIter16<'a, T> { 215 | data: LazyArray16<'a, T>, 216 | index: u16, 217 | } 218 | 219 | impl<'a, T: Readable<'a>> Default for LazyArrayIter16<'a, T> { 220 | #[inline] 221 | fn default() -> Self { 222 | LazyArrayIter16 { data: LazyArray16::new(&[]), index: 0 } 223 | } 224 | } 225 | 226 | impl<'a, T: Readable<'a>> Iterator for LazyArrayIter16<'a, T> { 227 | type Item = T; 228 | 229 | #[inline] 230 | fn next(&mut self) -> Option { 231 | self.index += 1; 232 | self.data.get(self.index - 1) 233 | } 234 | 235 | #[inline] 236 | fn count(self) -> usize { 237 | usize::from(self.data.len().saturating_sub(self.index)) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/remapper.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use std::hash::{Hash, Hasher}; 3 | use std::ops::Add; 4 | 5 | use fxhash::FxHashMap; 6 | 7 | /// A structure that allows to remap numeric types to new 8 | /// numbers so that they form a contiguous sequence of numbers. 9 | #[derive(Debug, Clone)] 10 | pub struct Remapper { 11 | /// The counter that keeps track of the next number to be assigned. 12 | /// Should always start with 0. 13 | counter: C, 14 | /// The map that maps numbers from their old value to their new value. 15 | forward: FxHashMap, 16 | /// The vector that stores the "reverse" mapping, i.e. given a new number, 17 | /// it allows to map back to the old one. 18 | backward: Vec, 19 | } 20 | 21 | impl Hash for Remapper { 22 | fn hash(&self, state: &mut H) { 23 | self.backward.hash(state); 24 | } 25 | } 26 | 27 | impl PartialEq for Remapper { 28 | fn eq(&self, other: &Self) -> bool { 29 | self.backward == other.backward 30 | } 31 | } 32 | 33 | impl Eq for Remapper {} 34 | 35 | /// A wrapper trait around `checked_add` so we can require it for the remapper. 36 | pub trait CheckedAdd: Sized + Add { 37 | fn checked_add(&self, v: &Self) -> Option; 38 | } 39 | 40 | impl CheckedAdd for u8 { 41 | fn checked_add(&self, v: &Self) -> Option { 42 | u8::checked_add(*self, *v) 43 | } 44 | } 45 | 46 | impl CheckedAdd for u16 { 47 | fn checked_add(&self, v: &Self) -> Option { 48 | u16::checked_add(*self, *v) 49 | } 50 | } 51 | 52 | impl CheckedAdd for u32 { 53 | fn checked_add(&self, v: &Self) -> Option { 54 | u32::checked_add(*self, *v) 55 | } 56 | } 57 | 58 | impl Remapper 59 | where 60 | C: CheckedAdd + Copy + From, 61 | T: From + Copy + From + Eq + Hash, 62 | { 63 | /// Create a new instance of a remapper. 64 | pub fn new() -> Self 65 | where 66 | C: Default, 67 | { 68 | Self { 69 | counter: C::default(), 70 | forward: FxHashMap::default(), 71 | backward: Vec::new(), 72 | } 73 | } 74 | 75 | /// Get the new mapping of a value that has been remapped before. 76 | /// Returns `None` if it has not been remapped. 77 | pub fn get(&self, old: T) -> Option { 78 | self.forward.get(&old).copied() 79 | } 80 | 81 | /// Remap a new value, either returning the previously assigned number 82 | /// if it already has been remapped, and assigning a new number if it 83 | /// has not been remapped. 84 | pub fn remap(&mut self, old: T) -> T { 85 | *self.forward.entry(old).or_insert_with(|| { 86 | let value = self.counter; 87 | self.backward.push(old); 88 | self.counter = self 89 | .counter 90 | .checked_add(&C::from(1)) 91 | .expect("remapper was overflowed"); 92 | value.into() 93 | }) 94 | } 95 | 96 | /// Get the number of elements that have been remapped. Assumes that 97 | /// the remapper was constructed with a type where `C::default` yields 0. 98 | pub fn len(&self) -> C { 99 | self.counter 100 | } 101 | 102 | /// Returns an iterator over the old values, in ascending order that is defined 103 | /// by the remapping. 104 | pub fn sorted_iter(&self) -> impl Iterator + '_ { 105 | self.backward.iter().copied() 106 | } 107 | } 108 | 109 | /// A remapper that allows to assign a new ordering to a subset of glyphs. 110 | /// 111 | /// For example, let's say that we want to subset a font that only contains the 112 | /// glyphs 4, 9 and 16. In this case, the remapper could yield a remapping 113 | /// that assigns the following glyph IDs: 114 | /// - 0 -> 0 (The .notdef glyph will always be included) 115 | /// - 4 -> 1 116 | /// - 9 -> 2 117 | /// - 16 -> 3 118 | /// 119 | /// This is necessary because a font needs to have a contiguous sequence of 120 | /// glyph IDs that start from 0, so we cannot just reuse the old ones, but we 121 | /// need to define a mapping. 122 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 123 | pub struct GlyphRemapper(Remapper); 124 | 125 | impl Default for GlyphRemapper { 126 | fn default() -> Self { 127 | let mut remapper = Remapper::new(); 128 | // .notdef is always a part of a subset. 129 | remapper.remap(0); 130 | Self(remapper) 131 | } 132 | } 133 | 134 | impl GlyphRemapper { 135 | /// Create a new instance of a glyph remapper. `.notdef` will always be a member 136 | /// of the subset. 137 | pub fn new() -> Self { 138 | Self::default() 139 | } 140 | 141 | /// Create a remapper from an existing set of glyphs 142 | pub fn new_from_glyphs(glyphs: &[u16]) -> Self { 143 | let mut map = Self::new(); 144 | 145 | for glyph in glyphs { 146 | map.remap(*glyph); 147 | } 148 | 149 | map 150 | } 151 | 152 | /// Create a remapper from an existing set of glyphs. The method 153 | /// will ensure that the mapping is monotonically increasing. 154 | pub fn new_from_glyphs_sorted(glyphs: &[u16]) -> Self { 155 | let mut sorted = 156 | BTreeSet::from_iter(glyphs).iter().map(|g| **g).collect::>(); 157 | sorted.sort(); 158 | GlyphRemapper::new_from_glyphs(&sorted) 159 | } 160 | 161 | /// Get the number of gids that have been remapped. 162 | pub fn num_gids(&self) -> u16 { 163 | self.0.len() 164 | } 165 | 166 | /// Remap a glyph ID, or return the existing mapping if the 167 | /// glyph ID has already been remapped before. 168 | #[inline] 169 | pub fn remap(&mut self, old: u16) -> u16 { 170 | self.0.remap(old) 171 | } 172 | 173 | /// Get the mapping of a glyph ID, if it has been remapped before. 174 | #[inline] 175 | pub fn get(&self, old: u16) -> Option { 176 | self.0.get(old) 177 | } 178 | 179 | /// Return an iterator that yields the old glyphs, in ascending order that 180 | /// is defined by the remapping. For example, if we perform the following remappings: 181 | /// 3, 39, 8, 3, 10, 2 182 | /// 183 | /// Then the iterator will yield the following items in the order below. The order 184 | /// also implicitly defines the glyph IDs in the new mapping: 185 | /// 186 | /// 0 (0), 3 (1), 39 (2), 8 (3), 10 (4), 2 (5) 187 | pub fn remapped_gids(&self) -> impl Iterator + '_ { 188 | self.0.backward.iter().copied() 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/write.rs: -------------------------------------------------------------------------------- 1 | /// A writable stream of binary data. 2 | pub struct Writer(Vec); 3 | 4 | impl Writer { 5 | /// Create a new writable stream of binary data. 6 | #[inline] 7 | pub fn new() -> Self { 8 | Self(Vec::with_capacity(1024)) 9 | } 10 | 11 | /// Create a new writable stream of binary data with a capacity. 12 | #[inline] 13 | pub fn with_capacity(capacity: usize) -> Self { 14 | Self(Vec::with_capacity(capacity)) 15 | } 16 | 17 | /// Write `T` into the data. 18 | #[inline] 19 | pub fn write(&mut self, data: T) { 20 | data.write(self); 21 | } 22 | 23 | /// Give bytes into the writer. 24 | #[inline] 25 | pub fn extend(&mut self, bytes: &[u8]) { 26 | self.0.extend(bytes); 27 | } 28 | 29 | /// Align the contents to a byte boundary. 30 | #[inline] 31 | pub fn align(&mut self, to: usize) { 32 | while self.0.len() % to != 0 { 33 | self.0.push(0); 34 | } 35 | } 36 | 37 | /// The number of written bytes. 38 | #[inline] 39 | pub fn len(&self) -> usize { 40 | self.0.len() 41 | } 42 | 43 | /// Return the written bytes. 44 | #[inline] 45 | pub fn finish(self) -> Vec { 46 | self.0 47 | } 48 | } 49 | 50 | /// Trait for an object that can be written into a byte stream. 51 | pub trait Writeable: Sized { 52 | fn write(&self, w: &mut Writer); 53 | } 54 | 55 | impl Writeable for [T; N] { 56 | fn write(&self, w: &mut Writer) { 57 | for i in self { 58 | w.write(i); 59 | } 60 | } 61 | } 62 | 63 | impl Writeable for u8 { 64 | fn write(&self, w: &mut Writer) { 65 | w.extend(&self.to_be_bytes()); 66 | } 67 | } 68 | 69 | impl Writeable for &[T] 70 | where 71 | T: Writeable, 72 | { 73 | fn write(&self, w: &mut Writer) { 74 | for el in *self { 75 | w.write(el); 76 | } 77 | } 78 | } 79 | 80 | impl Writeable for &T 81 | where 82 | T: Writeable, 83 | { 84 | fn write(&self, w: &mut Writer) { 85 | T::write(self, w) 86 | } 87 | } 88 | 89 | impl Writeable for u16 { 90 | fn write(&self, w: &mut Writer) { 91 | w.write::<[u8; 2]>(self.to_be_bytes()); 92 | } 93 | } 94 | 95 | impl Writeable for i16 { 96 | fn write(&self, w: &mut Writer) { 97 | w.write::<[u8; 2]>(self.to_be_bytes()); 98 | } 99 | } 100 | 101 | impl Writeable for u32 { 102 | fn write(&self, w: &mut Writer) { 103 | w.write::<[u8; 4]>(self.to_be_bytes()); 104 | } 105 | } 106 | 107 | impl Writeable for i32 { 108 | fn write(&self, w: &mut Writer) { 109 | w.write::<[u8; 4]>(self.to_be_bytes()); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | ## Requirements 4 | You need to have `fonttools 4.50` installed on your system and in your PATH. Note that you need to have that 5 | exact version, otherwise the tests will fail. 6 | 7 | In addition to that, you need Java installed on your system, install the [CFF dump utility](https://github.com/janpe2/CFFDump/releases/tag/v1.3.0) and point the `CFF_DUMP_BIN` environment variable to it. 8 | 9 | ## Generating tests 10 | In order to create new fonttools tests, you can edit `data/fonttools.tests`. 11 | For CFF tests, you can edit `data/cff.tests`. For subset tests, you can edit `data/subsets.tests` 12 | 13 | In order to generate the tests, run `scripts/gen-tests.py`. 14 | 15 | ## Description 16 | Testing is very important, as having errors in the subsetting logic could have fatal consequences. 17 | Because of this, we have four different testing approaches that cover 4 different 18 | font readers and 7 different PDF readers in total. 19 | 20 | ### Subset tests 21 | We use `fontations` and `ttf-parser` to ensure that the outlines in the new font are the same as in the 22 | old font. By checking 3 different implementations, we can assert with relatively high confidence that there are 23 | no issues in this regard. For each font, we test a selected subset of glyphs (see `data/subsets.tests`), but 24 | for each font we also make one subset where all glyphs are included, to make sure that "rewriting the whole font" 25 | also works as expected. 26 | 27 | Using `ttf-parser`, we also check that the metrics for a glyph still match. This is especially useful for testing 28 | `hmtx` subsetting. 29 | 30 | ### fonttools tests 31 | `fonttools` has a feature that allows us to dump a font's internal structure as an XML file. This is 32 | _incredibly_ useful, because it allows us to easily inspect the structure of a font file. We use this to 33 | dump small subsets of fonts and compare the output to how fonttools would subset the font. This allows us 34 | to identify other kinds of potential issues in the implementation. And it conveniently also allows us to 35 | have a fourth implementation to test against. 36 | 37 | ### CFF tests 38 | A problem with CFF tests is that fonttools abstracts away the exact structure of the CFF table, 39 | and stuff like the order of operators in DICTs as well as missing entries are not preserved. Because of 40 | this, we use the above-mentioned CFF dump utility, which provides a much more detailed insight into the 41 | structure of the CFF table, and allows us to detect regressions in CFF subsetting more easily. 42 | 43 | ### Fuzzing tests 44 | In `examples`, we have a binary that takes an environment variable `FONT_DIR` and recursively iterates over all fonts 45 | in that directory and basically performs the same test as in #1, but on a randomly selected sets of glyphs. We currently 46 | try to run the fuzzer every once in a while on a set of 1000+ fonts to make sure it that the subsetter also works with 47 | other fonts than the one included in this repository. 48 | 49 | ### PDF tests 50 | Occasionally, we will also use a subset of fonts and use `typst` to create a PDF file with it and check 51 | that the output looks correct in Adobe Acrobat, mupdf, xpdf, Firefox, Chrome, Apple Preview and pdfbox. These tests 52 | only happen manually though. -------------------------------------------------------------------------------- /tests/cff/LatinModernRoman-Regular_1.txt: -------------------------------------------------------------------------------- 1 | % CFF Dump Output 2 | % File: LatinModernRoman-Regular_1.otf 3 | % Dumping an OpenType font file. 4 | % CFF data starts at 0x7C and its length is 674 bytes. 5 | % All dumped offsets are relative to 0x7C. 6 | 7 | 8 | -------------------------------------------------------------------------------- 9 | 10 | Header (0x00000000): 11 | major: 1 12 | minor: 0 13 | hdrSize: 4 14 | offSize: 4 15 | 16 | -------------------------------------------------------------------------------- 17 | 18 | Name INDEX (0x00000004): 19 | count: 1, offSize: 1 20 | Offsets of INDEX (relative to 8): 21 | [0] = 1 22 | [1] = 18 23 | Data: 24 | [0]: (LMRoman10-Regular) 25 | 26 | -------------------------------------------------------------------------------- 27 | 28 | Top DICT INDEX (0x0000001a): 29 | count: 1, offSize: 1 30 | Offsets of INDEX (relative to 30): 31 | [0] = 1 32 | [1] = 68 33 | Data: 34 | [0] (0x0000001f): 35 | << 36 | /ROS << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> 37 | /Notice (Copyright 2003, 2009 B. Jackowski and J. M. Nowacki (on behalf of TeX users groups). This work is released under the GUST Font License -- see http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt for details.) % SID 393 38 | /FontMatrix [0.001 0 0 0.001 0 0] 39 | /FontBBox [-430 -290 1417 1127] 40 | /charset 327 % offset 41 | /CharStrings 417 % offset 42 | /CIDCount 65535 43 | /FDArray 393 % offset 44 | /FDSelect 385 % offset 45 | % ----- Following entries are missing, so they get default values: ----- 46 | /isFixedPitch false % default 47 | /ItalicAngle 0 % default 48 | /UnderlinePosition -100 % default 49 | /UnderlineThickness 50 % default 50 | /PaintType 0 % default 51 | /CharstringType 2 % default 52 | /StrokeWidth 0 % default 53 | /CIDFontVersion 0 % default 54 | /CIDFontRevision 0 % default 55 | /CIDFontType 0 % default 56 | >> 57 | 58 | -------------------------------------------------------------------------------- 59 | 60 | String INDEX (0x00000062): 61 | count: 3, offSize: 1 62 | Offsets of INDEX (relative to 104): 63 | [0] = 1 64 | [1] = 6 65 | [2] = 14 66 | [3] = 221 67 | Data: 68 | [0](SID = 391): (Adobe) 69 | [1](SID = 392): (Identity) 70 | [2](SID = 393): (Copyright 2003, 2009 B. Jackowski and J. M. Nowacki (on behalf of TeX users groups). This work is released under the GUST Font License -- see http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt for details.) 71 | 72 | -------------------------------------------------------------------------------- 73 | 74 | Global Subr INDEX (0x00000145): 75 | count: 0 76 | 77 | 78 | -------------------------------------------------------------------------------- 79 | 80 | Charset (0x00000147): 81 | (format 2; [GID] = ): 82 | ([0] = CID 0), 83 | [1] = CID 1, 84 | [2] = CID 2, 85 | 86 | -------------------------------------------------------------------------------- 87 | 88 | FDSelect (0x00000181): 89 | (format 3) 90 | FD #0 for GIDs 0...2 91 | 92 | 93 | -------------------------------------------------------------------------------- 94 | 95 | Font DICT INDEX (a.k.a. FDArray) (0x00000189): 96 | count: 1, offSize: 1 97 | Offsets of INDEX (relative to 397): 98 | [0] = 1 99 | [1] = 20 100 | Data: 101 | [0] (0x0000018e): 102 | << 103 | /FontMatrix [1 0 0 1 0 0] 104 | /Private [53 332] % [size offset] 105 | % ----- Following entries are missing, so they get default values: ----- 106 | /isFixedPitch false % default 107 | /ItalicAngle 0 % default 108 | /UnderlinePosition -100 % default 109 | /UnderlineThickness 50 % default 110 | /PaintType 0 % default 111 | /CharstringType 2 % default 112 | /FontBBox [0 0 0 0] % default 113 | /StrokeWidth 0 % default 114 | >> 115 | 116 | -------------------------------------------------------------------------------- 117 | 118 | CharStrings INDEX (0x000001a1): 119 | count: 3, offSize: 1 120 | Offsets of INDEX (relative to 423): 121 | [0] = 1 122 | [1] = 4 123 | [2] = 120 124 | [3] = 251 125 | Data: 126 | ([GID] (offset): ) 127 | [0] CID 0 (0x000001a8): 128 | 280 endchar % glyph width = 280 + nominalWidthX = 280 129 | [1] CID 1 (0x000001ab): 130 | 681 0 31 307 31 86 31 163 31 hstem % glyph width = 681 + nominalWidthX = 681 131 | 33 25 3 25 151 25 198 89 vstem 132 | 652 hmoveto 133 | 31 -24 vlineto 134 | -77 -2 11 36 hvcurveto 135 | 524 vlineto 136 | 36 2 11 77 vhcurveto 137 | 24 31 -563 hlineto 138 | -28 -225 rlineto 139 | 25 hlineto 140 | 139 16 27 55 153 hhcurveto 141 | 129 hlineto 142 | 47 2 -7 -33 hvcurveto 143 | -240 -90 vlineto 144 | -97 -11 31 86 hvcurveto 145 | -25 -265 25 hlineto 146 | 85 11 32 97 vhcurveto 147 | 90 -267 hlineto 148 | -33 -2 -7 -47 vhcurveto 149 | -133 hlineto 150 | -172 -23 73 154 -25 hvcurveto 151 | -25 hlineto 152 | 42 -258 rlineto 153 | endchar 154 | [2] CID 2 (0x0000021f): 155 | 676 -10 25 330 21 312 23 hstem % glyph width = 676 + nominalWidthX = 676 156 | 28 103 421 95 vstem 157 | 647 121 rmoveto 158 | 6 -7 5 -4 -15 -14 -36 -15 -15 vhcurveto 159 | -55 -57 -90 -11 -73 hhcurveto 160 | -90 -73 58 77 -40 hvcurveto 161 | -30 60 -8 69 66 vvcurveto 162 | 500 hlineto 163 | 14 2 7 12 92 -24 106 -69 65 hvcurveto 164 | 50 -53 -71 24 -72 hhcurveto 165 | -188 -142 -164 -191 -171 118 -170 204 -15 hvcurveto 166 | 14 hlineto 167 | 97 125 17 96 53 hvcurveto 168 | 3 5 5 7 6 vvcurveto 169 | -95 245 rmoveto 170 | -421 hlineto 171 | 133 3 57 179 161 hhcurveto 172 | 72 58 -43 -69 33 hvcurveto 173 | 29 -62 8 -70 -68 vvcurveto 174 | endchar 175 | 176 | -------------------------------------------------------------------------------- 177 | 178 | CIDFont's Private DICTs: 179 | Private DICT for Font DICT #0 (0x0000014c): 180 | << 181 | /BlueValues [-22 0 431 448 666 677 683 705] % Original delta values: [-22 22 431 17 218 11 6 22] 182 | /BlueScale 0.04546 183 | /BlueFuzz 0 184 | /StdHW 31 185 | /StdVW 69 186 | /StemSnapH [22 23 25 26 28 30 31 38 40 42 45 106] % Original delta values: [22 1 2 1 2 2 1 7 2 2 3 61] 187 | /StemSnapV [25 66 69 75 77 83 86 89 92 97 103 107] % Original delta values: [25 41 3 6 2 6 3 3 3 5 6 4] 188 | % ----- Following entries are missing, so they get default values: ----- 189 | /BlueShift 7 % default 190 | /ForceBold false % default 191 | /LanguageGroup 0 % default 192 | /ExpansionFactor 0.06 % default 193 | /initialRandomSeed 0 % default 194 | /defaultWidthX 0 % default 195 | /nominalWidthX 0 % default 196 | >> 197 | 198 | -------------------------------------------------------------------------------- 199 | 200 | Local Subr INDEX for Font DICT #0: 201 | 202 | 203 | -------------------------------------------------------------------------------- 204 | 205 | Info messages: 206 | Info: This is a CIDFont. 207 | 208 | % End of dump 209 | 210 | -------------------------------------------------------------------------------- /tests/cff/LatinModernRoman-Regular_2.txt: -------------------------------------------------------------------------------- 1 | % CFF Dump Output 2 | % File: LatinModernRoman-Regular_2.otf 3 | % Dumping an OpenType font file. 4 | % CFF data starts at 0x7C and its length is 635 bytes. 5 | % All dumped offsets are relative to 0x7C. 6 | 7 | 8 | -------------------------------------------------------------------------------- 9 | 10 | Header (0x00000000): 11 | major: 1 12 | minor: 0 13 | hdrSize: 4 14 | offSize: 4 15 | 16 | -------------------------------------------------------------------------------- 17 | 18 | Name INDEX (0x00000004): 19 | count: 1, offSize: 1 20 | Offsets of INDEX (relative to 8): 21 | [0] = 1 22 | [1] = 18 23 | Data: 24 | [0]: (LMRoman10-Regular) 25 | 26 | -------------------------------------------------------------------------------- 27 | 28 | Top DICT INDEX (0x0000001a): 29 | count: 1, offSize: 1 30 | Offsets of INDEX (relative to 30): 31 | [0] = 1 32 | [1] = 68 33 | Data: 34 | [0] (0x0000001f): 35 | << 36 | /ROS << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> 37 | /Notice (Copyright 2003, 2009 B. Jackowski and J. M. Nowacki (on behalf of TeX users groups). This work is released under the GUST Font License -- see http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt for details.) % SID 393 38 | /FontMatrix [0.001 0 0 0.001 0 0] 39 | /FontBBox [-430 -290 1417 1127] 40 | /charset 327 % offset 41 | /CharStrings 417 % offset 42 | /CIDCount 65535 43 | /FDArray 393 % offset 44 | /FDSelect 385 % offset 45 | % ----- Following entries are missing, so they get default values: ----- 46 | /isFixedPitch false % default 47 | /ItalicAngle 0 % default 48 | /UnderlinePosition -100 % default 49 | /UnderlineThickness 50 % default 50 | /PaintType 0 % default 51 | /CharstringType 2 % default 52 | /StrokeWidth 0 % default 53 | /CIDFontVersion 0 % default 54 | /CIDFontRevision 0 % default 55 | /CIDFontType 0 % default 56 | >> 57 | 58 | -------------------------------------------------------------------------------- 59 | 60 | String INDEX (0x00000062): 61 | count: 3, offSize: 1 62 | Offsets of INDEX (relative to 104): 63 | [0] = 1 64 | [1] = 6 65 | [2] = 14 66 | [3] = 221 67 | Data: 68 | [0](SID = 391): (Adobe) 69 | [1](SID = 392): (Identity) 70 | [2](SID = 393): (Copyright 2003, 2009 B. Jackowski and J. M. Nowacki (on behalf of TeX users groups). This work is released under the GUST Font License -- see http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt for details.) 71 | 72 | -------------------------------------------------------------------------------- 73 | 74 | Global Subr INDEX (0x00000145): 75 | count: 0 76 | 77 | 78 | -------------------------------------------------------------------------------- 79 | 80 | Charset (0x00000147): 81 | (format 2; [GID] = ): 82 | ([0] = CID 0), 83 | [1] = CID 1, 84 | 85 | -------------------------------------------------------------------------------- 86 | 87 | FDSelect (0x00000181): 88 | (format 3) 89 | FD #0 for GIDs 0...1 90 | 91 | 92 | -------------------------------------------------------------------------------- 93 | 94 | Font DICT INDEX (a.k.a. FDArray) (0x00000189): 95 | count: 1, offSize: 1 96 | Offsets of INDEX (relative to 397): 97 | [0] = 1 98 | [1] = 20 99 | Data: 100 | [0] (0x0000018e): 101 | << 102 | /FontMatrix [1 0 0 1 0 0] 103 | /Private [53 332] % [size offset] 104 | % ----- Following entries are missing, so they get default values: ----- 105 | /isFixedPitch false % default 106 | /ItalicAngle 0 % default 107 | /UnderlinePosition -100 % default 108 | /UnderlineThickness 50 % default 109 | /PaintType 0 % default 110 | /CharstringType 2 % default 111 | /FontBBox [0 0 0 0] % default 112 | /StrokeWidth 0 % default 113 | >> 114 | 115 | -------------------------------------------------------------------------------- 116 | 117 | CharStrings INDEX (0x000001a1): 118 | count: 2, offSize: 1 119 | Offsets of INDEX (relative to 422): 120 | [0] = 1 121 | [1] = 4 122 | [2] = 213 123 | Data: 124 | ([GID] (offset): ) 125 | [0] CID 0 (0x000001a7): 126 | 280 endchar % glyph width = 280 + nominalWidthX = 280 127 | [1] CID 1 (0x000001aa): 128 | 750 -22 31 565 31 47 31 53 95 83 -21 163 -20 hstemhm % glyph width = 750 + nominalWidthX = 750 129 | 136 89 20 97 48 89 -24 97 30 31 hintmask 0xE2 0xA0 130 | 716 652 rmoveto 131 | 31 vlineto 132 | -118 -3 -119 3 rlineto 133 | -31 vlineto 134 | 103 0 -47 -27 hvcurveto 135 | -347 vlineto 136 | -142 -97 -80 -95 -47 -118 25 190 vhcurveto 137 | 381 vlineto 138 | 36 2 11 77 vhcurveto 139 | 24 31 hlineto 140 | -3 -35 -74 0 -38 hhcurveto 141 | -38 -75 0 3 -35 hvcurveto 142 | -31 24 vlineto 143 | 77 2 -11 -36 hvcurveto 144 | -377 vlineto 145 | -141 116 -109 136 115 90 93 114 17 vhcurveto 146 | 3 20 0 9 40 vvcurveto 147 | 320 vlineto 148 | 33 0 45 103 vhcurveto 149 | hintmask 0x1D 0x40 150 | -374 132 rmoveto 151 | 24 -21 23 -27 -31 -18 -25 -22 -25 21 -23 27 31 18 25 23 vhcurveto 152 | 210 hmoveto 153 | 24 -21 23 -27 -31 -18 -25 -22 -25 21 -23 27 31 18 25 23 vhcurveto 154 | -17 230 rmoveto 155 | 22 4 -17 20 -22 hhcurveto 156 | -12 -10 -8 -4 -4 hvcurveto 157 | -145 -130 14 -21 170 94 11 5 9 10 2 12 rlinecurve 158 | endchar 159 | 160 | -------------------------------------------------------------------------------- 161 | 162 | CIDFont's Private DICTs: 163 | Private DICT for Font DICT #0 (0x0000014c): 164 | << 165 | /BlueValues [-22 0 431 448 666 677 683 705] % Original delta values: [-22 22 431 17 218 11 6 22] 166 | /BlueScale 0.04546 167 | /BlueFuzz 0 168 | /StdHW 31 169 | /StdVW 69 170 | /StemSnapH [22 23 25 26 28 30 31 38 40 42 45 106] % Original delta values: [22 1 2 1 2 2 1 7 2 2 3 61] 171 | /StemSnapV [25 66 69 75 77 83 86 89 92 97 103 107] % Original delta values: [25 41 3 6 2 6 3 3 3 5 6 4] 172 | % ----- Following entries are missing, so they get default values: ----- 173 | /BlueShift 7 % default 174 | /ForceBold false % default 175 | /LanguageGroup 0 % default 176 | /ExpansionFactor 0.06 % default 177 | /initialRandomSeed 0 % default 178 | /defaultWidthX 0 % default 179 | /nominalWidthX 0 % default 180 | >> 181 | 182 | -------------------------------------------------------------------------------- 183 | 184 | Local Subr INDEX for Font DICT #0: 185 | 186 | 187 | -------------------------------------------------------------------------------- 188 | 189 | Info messages: 190 | Info: This is a CIDFont. 191 | 192 | % End of dump 193 | 194 | -------------------------------------------------------------------------------- /tests/cff/NewCMMath-Regular_1.txt: -------------------------------------------------------------------------------- 1 | % CFF Dump Output 2 | % File: NewCMMath-Regular_1.otf 3 | % Dumping an OpenType font file. 4 | % CFF data starts at 0x7C and its length is 785 bytes. 5 | % All dumped offsets are relative to 0x7C. 6 | 7 | 8 | -------------------------------------------------------------------------------- 9 | 10 | Header (0x00000000): 11 | major: 1 12 | minor: 0 13 | hdrSize: 4 14 | offSize: 4 15 | 16 | -------------------------------------------------------------------------------- 17 | 18 | Name INDEX (0x00000004): 19 | count: 1, offSize: 1 20 | Offsets of INDEX (relative to 8): 21 | [0] = 1 22 | [1] = 18 23 | Data: 24 | [0]: (NewCMMath-Regular) 25 | 26 | -------------------------------------------------------------------------------- 27 | 28 | Top DICT INDEX (0x0000001a): 29 | count: 1, offSize: 1 30 | Offsets of INDEX (relative to 30): 31 | [0] = 1 32 | [1] = 70 33 | Data: 34 | [0] (0x0000001f): 35 | << 36 | /ROS << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> 37 | /Notice ((C) 2019-2021 Antonis Tsolomitis. 38 | This work is released under the GUST Font License -- see http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt for details.) % SID 393 39 | /FontMatrix [0.001 0 0 0.001 0 0] 40 | /FontBBox [-1042 -3060 4082 3560] 41 | /charset 278 % offset 42 | /CharStrings 375 % offset 43 | /CIDCount 65535 44 | /FDArray 351 % offset 45 | /FDSelect 343 % offset 46 | % ----- Following entries are missing, so they get default values: ----- 47 | /isFixedPitch false % default 48 | /ItalicAngle 0 % default 49 | /UnderlinePosition -100 % default 50 | /UnderlineThickness 50 % default 51 | /PaintType 0 % default 52 | /CharstringType 2 % default 53 | /StrokeWidth 0 % default 54 | /CIDFontVersion 0 % default 55 | /CIDFontRevision 0 % default 56 | /CIDFontType 0 % default 57 | >> 58 | 59 | -------------------------------------------------------------------------------- 60 | 61 | String INDEX (0x00000064): 62 | count: 3, offSize: 1 63 | Offsets of INDEX (relative to 106): 64 | [0] = 1 65 | [1] = 6 66 | [2] = 14 67 | [3] = 170 68 | Data: 69 | [0](SID = 391): (Adobe) 70 | [1](SID = 392): (Identity) 71 | [2](SID = 393): ((C) 2019-2021 Antonis Tsolomitis. 72 | This work is released under the GUST Font License -- see http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt for details.) 73 | 74 | -------------------------------------------------------------------------------- 75 | 76 | Global Subr INDEX (0x00000114): 77 | count: 0 78 | 79 | 80 | -------------------------------------------------------------------------------- 81 | 82 | Charset (0x00000116): 83 | (format 2; [GID] = ): 84 | ([0] = CID 0), 85 | [1] = CID 1, 86 | [2] = CID 2, 87 | 88 | -------------------------------------------------------------------------------- 89 | 90 | FDSelect (0x00000157): 91 | (format 3) 92 | FD #0 for GIDs 0...2 93 | 94 | 95 | -------------------------------------------------------------------------------- 96 | 97 | Font DICT INDEX (a.k.a. FDArray) (0x0000015f): 98 | count: 1, offSize: 1 99 | Offsets of INDEX (relative to 355): 100 | [0] = 1 101 | [1] = 20 102 | Data: 103 | [0] (0x00000164): 104 | << 105 | /FontMatrix [1 0 0 1 0 0] 106 | /Private [60 283] % [size offset] 107 | % ----- Following entries are missing, so they get default values: ----- 108 | /isFixedPitch false % default 109 | /ItalicAngle 0 % default 110 | /UnderlinePosition -100 % default 111 | /UnderlineThickness 50 % default 112 | /PaintType 0 % default 113 | /CharstringType 2 % default 114 | /FontBBox [0 0 0 0] % default 115 | /StrokeWidth 0 % default 116 | >> 117 | 118 | -------------------------------------------------------------------------------- 119 | 120 | CharStrings INDEX (0x00000177): 121 | count: 3, offSize: 2 122 | Offsets of INDEX (relative to 385): 123 | [0] = 1 124 | [1] = 4 125 | [2] = 188 126 | [3] = 400 127 | Data: 128 | ([GID] (offset): ) 129 | [0] CID 0 (0x00000182): 130 | -326 endchar % glyph width = -326 + nominalWidthX = 280 131 | [1] CID 1 (0x00000185): 132 | 196 -60 106 184 40 184 106 hstem % glyph width = 196 + nominalWidthX = 802 133 | 356 106 vstem 134 | cntrmask 0xE0 135 | 740 -81 rmoveto 136 | 7 8 0 13 -7 8 rrcurveto 137 | -283 282 245 0 rlineto 138 | 11 9 9 11 11 -9 9 -11 hvcurveto 139 | -245 0 283 282 rlineto 140 | 7 8 0 13 -7 8 -8 7 -13 0 -8 -7 rrcurveto 141 | -302 -303 -302 303 rlineto 142 | -8 7 -13 0 -8 -7 -7 -8 0 -13 7 -8 rrcurveto 143 | 283 -282 -285 0 rlineto 144 | -11 -9 -9 -11 -11 9 -9 11 hvcurveto 145 | 285 0 -283 -282 rlineto 146 | -7 -8 0 -13 7 -8 8 -7 13 0 8 7 rrcurveto 147 | 302 303 302 -303 rlineto 148 | 8 -7 13 0 8 7 rrcurveto 149 | -278 74 rmoveto 150 | 29 -24 24 -29 -29 -24 -24 -29 -30 24 -23 29 29 24 23 30 vhcurveto 151 | 514 vmoveto 152 | 30 -24 23 -29 -29 -24 -23 -30 -29 24 -24 29 29 24 24 29 vhcurveto 153 | endchar 154 | [2] CID 2 (0x0000023d): 155 | -153 -10 28 404 24 252 -20 hstemhm % glyph width = -153 + nominalWidthX = 453 156 | 45 55 -55 35 265 34 -27 55 hintmask 0xF2 157 | 407 131 rmoveto 158 | 0 38 -17 30 -26 24 -38 35 -46 8 -35 6 -80 14 -65 12 0 53 0 32 27 39 95 0 rrcurveto 159 | hintmask 0xF4 160 | 116 0 5 -81 2 -29 1 -11 12 0 4 0 rrcurveto 161 | 17 0 7 19 hvcurveto 162 | 93 vlineto 163 | 17 0 9 -14 vhcurveto 164 | -5 0 -2 0 -13 -12 -2 -1 -10 -9 -6 -5 -30 21 -38 6 -37 0 -143 0 -34 -75 0 -50 0 -32 14 -26 24 -20 38 -33 38 -7 62 -10 rrcurveto 165 | hintmask 0xEA 166 | 50 -9 81 -14 0 -67 0 -39 -27 -46 -96 0 -96 0 -35 63 -18 68 -3 13 -1 4 -14 0 rrcurveto 167 | -17 0 -7 -20 hvcurveto 168 | -123 vlineto 169 | -17 0 -9 14 vhcurveto 170 | 9 0 19 21 20 22 44 -41 54 -2 24 0 rrcurveto 171 | 130 48 70 71 hvcurveto 172 | -43 526 rmoveto 173 | 19 -17 22 -24 vhcurveto 174 | -16 0 -8 -9 -10 -11 rrcurveto 175 | -131 -146 23 -26 158 114 rlineto 176 | 16 11 9 7 0 19 rrcurveto 177 | endchar 178 | 179 | -------------------------------------------------------------------------------- 180 | 181 | CIDFont's Private DICTs: 182 | Private DICT for Font DICT #0 (0x0000011b): 183 | << 184 | /defaultWidthX 778 185 | /nominalWidthX 606 186 | /BlueValues [-22 0 431 448 666 666 677 705] % Original delta values: [-22 22 431 17 218 0 11 28] 187 | /OtherBlues [-206 -205] % Original delta values: [-206 1] 188 | /BlueScale 0.04546 189 | /BlueShift 6 190 | /BlueFuzz 0 191 | /StdHW 40 192 | /StemSnapH [22 31 36 40 44 56 61 67] % Original delta values: [22 9 5 4 4 12 5 6] 193 | /StdVW 40 194 | /StemSnapV [40 46 60 64 69 75 79 83 89 95] % Original delta values: [40 6 14 4 5 6 4 4 6 6] 195 | % ----- Following entries are missing, so they get default values: ----- 196 | /ForceBold false % default 197 | /LanguageGroup 0 % default 198 | /ExpansionFactor 0.06 % default 199 | /initialRandomSeed 0 % default 200 | >> 201 | 202 | -------------------------------------------------------------------------------- 203 | 204 | Local Subr INDEX for Font DICT #0: 205 | 206 | 207 | -------------------------------------------------------------------------------- 208 | 209 | Info messages: 210 | Info: This is a CIDFont. 211 | 212 | % End of dump 213 | 214 | -------------------------------------------------------------------------------- /tests/cff/NotoSansCJKsc-Regular_1.txt: -------------------------------------------------------------------------------- 1 | % CFF Dump Output 2 | % File: NotoSansCJKsc-Regular_1.otf 3 | % Dumping an OpenType font file. 4 | % CFF data starts at 0x7C and its length is 786 bytes. 5 | % All dumped offsets are relative to 0x7C. 6 | 7 | 8 | -------------------------------------------------------------------------------- 9 | 10 | Header (0x00000000): 11 | major: 1 12 | minor: 0 13 | hdrSize: 4 14 | offSize: 4 15 | 16 | -------------------------------------------------------------------------------- 17 | 18 | Name INDEX (0x00000004): 19 | count: 1, offSize: 1 20 | Offsets of INDEX (relative to 8): 21 | [0] = 1 22 | [1] = 22 23 | Data: 24 | [0]: (NotoSansCJKsc-Regular) 25 | 26 | -------------------------------------------------------------------------------- 27 | 28 | Top DICT INDEX (0x0000001e): 29 | count: 1, offSize: 1 30 | Offsets of INDEX (relative to 34): 31 | [0] = 1 32 | [1] = 69 33 | Data: 34 | [0] (0x00000023): 35 | << 36 | /ROS << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> 37 | /Notice (Copyright 2014-2021 Adobe (http://www.adobe.com/). Noto is a trademark of Google Inc.) % SID 393 38 | /FontMatrix [0.001 0 0 0.001 0 0] 39 | /FontBBox [-1002 -1048 2928 1808] 40 | /charset 275 % offset 41 | /CharStrings 398 % offset 42 | /CIDCount 65535 43 | /FDArray 346 % offset 44 | /FDSelect 341 % offset 45 | % ----- Following entries are missing, so they get default values: ----- 46 | /isFixedPitch false % default 47 | /ItalicAngle 0 % default 48 | /UnderlinePosition -100 % default 49 | /UnderlineThickness 50 % default 50 | /PaintType 0 % default 51 | /CharstringType 2 % default 52 | /StrokeWidth 0 % default 53 | /CIDFontVersion 0 % default 54 | /CIDFontRevision 0 % default 55 | /CIDFontType 0 % default 56 | >> 57 | 58 | -------------------------------------------------------------------------------- 59 | 60 | String INDEX (0x00000067): 61 | count: 5, offSize: 1 62 | Offsets of INDEX (relative to 111): 63 | [0] = 1 64 | [1] = 6 65 | [2] = 14 66 | [3] = 99 67 | [4] = 128 68 | [5] = 162 69 | Data: 70 | [0](SID = 391): (Adobe) 71 | [1](SID = 392): (Identity) 72 | [2](SID = 393): (Copyright 2014-2021 Adobe (http://www.adobe.com/). Noto is a trademark of Google Inc.) 73 | [3](SID = 394): (NotoSansCJKsc-Regular-Generic) 74 | [4](SID = 395): (NotoSansCJKsc-Regular-Proportional) 75 | 76 | -------------------------------------------------------------------------------- 77 | 78 | Global Subr INDEX (0x00000111): 79 | count: 0 80 | 81 | 82 | -------------------------------------------------------------------------------- 83 | 84 | Charset (0x00000113): 85 | (format 2; [GID] = ): 86 | ([0] = CID 0), 87 | [1] = CID 1, 88 | [2] = CID 2, 89 | [3] = CID 3, 90 | 91 | -------------------------------------------------------------------------------- 92 | 93 | FDSelect (0x00000155): 94 | (format 0; [GID] = ): 95 | [0] = 0, 96 | [1] = 1, 97 | [2] = 1, 98 | [3] = 1, 99 | 100 | -------------------------------------------------------------------------------- 101 | 102 | Font DICT INDEX (a.k.a. FDArray) (0x0000015a): 103 | count: 2, offSize: 1 104 | Offsets of INDEX (relative to 351): 105 | [0] = 1 106 | [1] = 24 107 | [2] = 47 108 | Data: 109 | [0] (0x00000160): 110 | << 111 | /FontMatrix [1 0 0 1 0 0] 112 | /Private [28 280] % [size offset] 113 | /FontName (NotoSansCJKsc-Regular-Generic) % SID 394 114 | % ----- Following entries are missing, so they get default values: ----- 115 | /isFixedPitch false % default 116 | /ItalicAngle 0 % default 117 | /UnderlinePosition -100 % default 118 | /UnderlineThickness 50 % default 119 | /PaintType 0 % default 120 | /CharstringType 2 % default 121 | /FontBBox [0 0 0 0] % default 122 | /StrokeWidth 0 % default 123 | >> 124 | [1] (0x00000177): 125 | << 126 | /FontMatrix [1 0 0 1 0 0] 127 | /Private [33 308] % [size offset] 128 | /FontName (NotoSansCJKsc-Regular-Proportional) % SID 395 129 | % ----- Following entries are missing, so they get default values: ----- 130 | /isFixedPitch false % default 131 | /ItalicAngle 0 % default 132 | /UnderlinePosition -100 % default 133 | /UnderlineThickness 50 % default 134 | /PaintType 0 % default 135 | /CharstringType 2 % default 136 | /FontBBox [0 0 0 0] % default 137 | /StrokeWidth 0 % default 138 | >> 139 | 140 | -------------------------------------------------------------------------------- 141 | 142 | CharStrings INDEX (0x0000018e): 143 | count: 4, offSize: 2 144 | Offsets of INDEX (relative to 410): 145 | [0] = 1 146 | [1] = 84 147 | [2] = 182 148 | [3] = 265 149 | [4] = 376 150 | Data: 151 | ([GID] (offset): ) 152 | [0] CID 0 (0x0000019b): 153 | -120 50 859 91 -50 50 hstemhm % glyph width = defaultWidthX = 1000 154 | 100 50 700 50 hintmask 0xB8 155 | 100 -120 rmoveto 156 | 800 1000 -800 hlineto 157 | 400 -459 rmoveto 158 | -318 409 rlineto 159 | 636 hlineto 160 | -286 -450 rmoveto 161 | hintmask 0xD8 162 | 318 409 rlineto 163 | -818 vlineto 164 | -668 -41 rmoveto 165 | 318 409 318 -409 rlineto 166 | -668 859 rmoveto 167 | 318 -409 -318 -409 rlineto 168 | endchar 169 | [1] CID 1 (0x000001ee): 170 | -11 -13 76 417 77 85 65 hstem % glyph width = -11 + nominalWidthX = 606 171 | 52 94 315 93 vstem 172 | 303 -13 rmoveto 173 | 133 118 104 180 181 -118 105 -133 -133 -118 -105 -181 -180 118 -104 133 hvcurveto 174 | 76 vmoveto 175 | -94 -63 83 125 125 63 84 94 94 64 -84 -125 -125 -64 -83 -94 hvcurveto 176 | -46 579 rmoveto 177 | 92 hlineto 178 | 127 155 -39 36 -132 -126 rlineto 179 | -5 hlineto 180 | -131 126 -39 -36 rlineto 181 | endchar 182 | [2] CID 2 (0x00000250): 183 | 104 -13 81 665 -20 76 52 hstem % glyph width = 104 + nominalWidthX = 721 184 | 98 92 345 89 vstem 185 | 361 -13 rmoveto 186 | 149 114 80 235 hvcurveto 187 | 431 -89 -433 vlineto 188 | -176 -77 -56 -97 -96 -75 56 176 vhcurveto 189 | 433 -92 -431 vlineto 190 | -235 113 -80 150 vhcurveto 191 | -50 802 rmoveto 192 | 97 hlineto 193 | 117 126 -41 29 -121 -103 rlineto 194 | -5 hlineto 195 | -123 103 -40 -29 rlineto 196 | endchar 197 | [3] CID 3 (0x000002a3): 198 | -10 -13 79 -45 -21 543 -20 119 65 hstemhm % glyph width = -10 + nominalWidthX = 607 199 | 84 92 249 91 -79.5 79.5 hintmask 0xBC 200 | 251 -13 rmoveto 201 | hintmask 0xBA 202 | 74 54 39 59 51 hvcurveto 203 | 3 hlineto 204 | hintmask 0x7A 205 | 7 -85 rlineto 206 | hintmask 0x7C 207 | 76 543 -91 hlineto 208 | hintmask 0xBC 209 | -385 vlineto 210 | -64 -52 -39 -28 -56 hhcurveto 211 | -72 -30 43 101 hvcurveto 212 | 333 -92 -344 vlineto 213 | -139 52 -73 115 vhcurveto 214 | 6 655 rmoveto 215 | 91 hlineto 216 | 128 155 -40 36 -131 -126 rlineto 217 | -4 hlineto 218 | -132 126 -39 -36 rlineto 219 | endchar 220 | 221 | -------------------------------------------------------------------------------- 222 | 223 | CIDFont's Private DICTs: 224 | Private DICT for Font DICT #0 (0x00000118): 225 | << 226 | /BlueValues [-250 -250 1100 1100] % Original delta values: [-250 0 1350 0] 227 | /StdHW 40 228 | /StdVW 40 229 | /StemSnapH [40 120] % Original delta values: [40 80] 230 | /StemSnapV [40 120] % Original delta values: [40 80] 231 | /LanguageGroup 1 232 | /defaultWidthX 1000 233 | /nominalWidthX 107 234 | % ----- Following entries are missing, so they get default values: ----- 235 | /BlueScale 0.039625 % default 236 | /BlueShift 7 % default 237 | /BlueFuzz 1 % default 238 | /ForceBold false % default 239 | /ExpansionFactor 0.06 % default 240 | /initialRandomSeed 0 % default 241 | >> 242 | Private DICT for Font DICT #1 (0x00000134): 243 | << 244 | /BlueValues [-13 0 543 557 733 747] % Original delta values: [-13 13 543 14 176 14] 245 | /OtherBlues [-250 -229] % Original delta values: [-250 21] 246 | /StdHW 69 247 | /StdVW 85 248 | /StemSnapH [69 79 88] % Original delta values: [69 10 9] 249 | /StemSnapV [85 95 111] % Original delta values: [85 10 16] 250 | /defaultWidthX 742 251 | /nominalWidthX 617 252 | % ----- Following entries are missing, so they get default values: ----- 253 | /BlueScale 0.039625 % default 254 | /BlueShift 7 % default 255 | /BlueFuzz 1 % default 256 | /ForceBold false % default 257 | /LanguageGroup 0 % default 258 | /ExpansionFactor 0.06 % default 259 | /initialRandomSeed 0 % default 260 | >> 261 | 262 | -------------------------------------------------------------------------------- 263 | 264 | Local Subr INDEX for Font DICT #0: 265 | 266 | 267 | -------------------------------------------------------------------------------- 268 | 269 | Local Subr INDEX for Font DICT #1: 270 | 271 | 272 | -------------------------------------------------------------------------------- 273 | 274 | Info messages: 275 | Info: This is a CIDFont. 276 | 277 | % End of dump 278 | 279 | -------------------------------------------------------------------------------- /tests/cff/NotoSansCJKsc-Regular_custom_font_matrix_1.txt: -------------------------------------------------------------------------------- 1 | % CFF Dump Output 2 | % File: NotoSansCJKsc-Regular_custom_font_matrix_1.otf 3 | % Dumping an OpenType font file. 4 | % CFF data starts at 0x7C and its length is 845 bytes. 5 | % All dumped offsets are relative to 0x7C. 6 | 7 | 8 | -------------------------------------------------------------------------------- 9 | 10 | Header (0x00000000): 11 | major: 1 12 | minor: 0 13 | hdrSize: 4 14 | offSize: 4 15 | 16 | -------------------------------------------------------------------------------- 17 | 18 | Name INDEX (0x00000004): 19 | count: 1, offSize: 1 20 | Offsets of INDEX (relative to 8): 21 | [0] = 1 22 | [1] = 22 23 | Data: 24 | [0]: (NotoSansCJKsc-Regular) 25 | 26 | -------------------------------------------------------------------------------- 27 | 28 | Top DICT INDEX (0x0000001e): 29 | count: 1, offSize: 1 30 | Offsets of INDEX (relative to 34): 31 | [0] = 1 32 | [1] = 65 33 | Data: 34 | [0] (0x00000023): 35 | << 36 | /ROS << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> 37 | /Notice (Copyright 2014-2021 Adobe (http://www.adobe.com/). Noto is a trademark of Google Inc.) % SID 393 38 | /FontMatrix [0.001 0 0 0.001 0 0] 39 | /FontBBox [16 -82 963 840] 40 | /charset 269 % offset 41 | /CharStrings 413 % offset 42 | /CIDCount 65535 43 | /FDArray 334 % offset 44 | /FDSelect 330 % offset 45 | % ----- Following entries are missing, so they get default values: ----- 46 | /isFixedPitch false % default 47 | /ItalicAngle 0 % default 48 | /UnderlinePosition -100 % default 49 | /UnderlineThickness 50 % default 50 | /PaintType 0 % default 51 | /CharstringType 2 % default 52 | /StrokeWidth 0 % default 53 | /CIDFontVersion 0 % default 54 | /CIDFontRevision 0 % default 55 | /CIDFontType 0 % default 56 | >> 57 | 58 | -------------------------------------------------------------------------------- 59 | 60 | String INDEX (0x00000063): 61 | count: 5, offSize: 1 62 | Offsets of INDEX (relative to 107): 63 | [0] = 1 64 | [1] = 6 65 | [2] = 14 66 | [3] = 99 67 | [4] = 128 68 | [5] = 160 69 | Data: 70 | [0](SID = 391): (Adobe) 71 | [1](SID = 392): (Identity) 72 | [2](SID = 393): (Copyright 2014-2021 Adobe (http://www.adobe.com/). Noto is a trademark of Google Inc.) 73 | [3](SID = 394): (NotoSansCJKsc-Regular-Generic) 74 | [4](SID = 395): (NotoSansCJKsc-Regular-Ideographs) 75 | 76 | -------------------------------------------------------------------------------- 77 | 78 | Global Subr INDEX (0x0000010b): 79 | count: 0 80 | 81 | 82 | -------------------------------------------------------------------------------- 83 | 84 | Charset (0x0000010d): 85 | (format 2; [GID] = ): 86 | ([0] = CID 0), 87 | [1] = CID 1, 88 | [2] = CID 2, 89 | 90 | -------------------------------------------------------------------------------- 91 | 92 | FDSelect (0x0000014a): 93 | (format 0; [GID] = ): 94 | [0] = 0, 95 | [1] = 1, 96 | [2] = 1, 97 | 98 | -------------------------------------------------------------------------------- 99 | 100 | Font DICT INDEX (a.k.a. FDArray) (0x0000014e): 101 | count: 2, offSize: 1 102 | Offsets of INDEX (relative to 339): 103 | [0] = 1 104 | [1] = 35 105 | [2] = 74 106 | Data: 107 | [0] (0x00000154): 108 | << 109 | /FontMatrix [1 11 5 2 0.005 0.008] 110 | /Private [28 274] % [size offset] 111 | /FontName (NotoSansCJKsc-Regular-Generic) % SID 394 112 | % ----- Following entries are missing, so they get default values: ----- 113 | /isFixedPitch false % default 114 | /ItalicAngle 0 % default 115 | /UnderlinePosition -100 % default 116 | /UnderlineThickness 50 % default 117 | /PaintType 0 % default 118 | /CharstringType 2 % default 119 | /FontBBox [0 0 0 0] % default 120 | /StrokeWidth 0 % default 121 | >> 122 | [1] (0x00000176): 123 | << 124 | /FontMatrix [0.79999995 5 7 0.7 0.003 0.002] 125 | /Private [28 302] % [size offset] 126 | /FontName (NotoSansCJKsc-Regular-Ideographs) % SID 395 127 | % ----- Following entries are missing, so they get default values: ----- 128 | /isFixedPitch false % default 129 | /ItalicAngle 0 % default 130 | /UnderlinePosition -100 % default 131 | /UnderlineThickness 50 % default 132 | /PaintType 0 % default 133 | /CharstringType 2 % default 134 | /FontBBox [0 0 0 0] % default 135 | /StrokeWidth 0 % default 136 | >> 137 | 138 | -------------------------------------------------------------------------------- 139 | 140 | CharStrings INDEX (0x0000019d): 141 | count: 3, offSize: 2 142 | Offsets of INDEX (relative to 423): 143 | [0] = 1 144 | [1] = 2 145 | [2] = 200 146 | [3] = 422 147 | Data: 148 | ([GID] (offset): ) 149 | [0] CID 0 (0x000001a8): 150 | endchar % glyph width = defaultWidthX = 1000 151 | [1] CID 1 (0x000001a9): 152 | -81 76 582 70 hstem % glyph width = defaultWidthX = 1000 153 | 160 72 380 74 vstem 154 | 449 412 rmoveto 155 | -28 -120 -48 -119 -62 -77 18 -10 32 -20 14 -11 61 83 54 127 32 132 rrcurveto 156 | 236 hmoveto 157 | 55 -106 50 -141 16 -92 72 25 rcurveline 158 | -17 92 -51 138 -57 106 rrcurveto 159 | -360 417 rmoveto 160 | -34 -147 -57 -144 -75 -93 18 -11 30 -25 13 -12 36 47 33 60 29 66 rrcurveto 161 | 153 -566 hlineto 162 | -13 -5 -3 -12 vhcurveto 163 | -14 -1 -43 -1 -48 2 11 -21 12 -33 4 -22 rrcurveto 164 | 62 44 3 12 27 hvcurveto 165 | 27 13 9 22 42 vvcurveto 166 | 566 189 vlineto 167 | -8 -51 -9 -53 -7 -37 64 -12 rcurveline 168 | 13 54 18 87 13 73 -51 12 rcurveline 169 | -13 -3 rlineto 170 | -408 hlineto 171 | 21 55 18 58 14 59 rrcurveto 172 | -276 17 rmoveto 173 | -56 -152 -93 -150 -99 -97 14 -17 21 -39 7 -18 35 36 34 42 33 46 rrcurveto 174 | -565 72 678 vlineto 175 | 39 69 36 73 28 73 rrcurveto 176 | endchar 177 | [2] CID 2 (0x0000026f): 178 | -78 74 370 71 126 71 85 70 hstem % glyph width = defaultWidthX = 1000 179 | 343 75 232 74 vstem 180 | 650 570 rmoveto 181 | -133 -237 -71 237 -351 vlineto 182 | -15 -6 -4 -17 -1 vhcurveto 183 | -18 -1 -55 0 -64 2 11 -21 13 -32 4 -21 rrcurveto 184 | 79 53 2 11 32 hvcurveto 185 | 32 13 10 21 46 vvcurveto 186 | 351 239 71 -239 88 vlineto 187 | 77 61 86 90 56 81 -50 36 rcurveline 188 | -15 -4 rlineto 189 | -431 -70 376 hlineto 190 | -41 -52 -53 -57 -50 -40 rrcurveto 191 | -485 270 rmoveto 192 | -11 -62 -14 -71 -15 -73 rrcurveto 193 | -111 -71 95 hlineto 194 | -27 -125 -30 -123 -24 -86 62 -34 rcurveline 195 | 11 42 35 -21 35 -25 34 -25 rlinecurve 196 | -48 -87 -61 -62 -73 -39 16 -15 20 -27 10 -18 78 47 65 66 51 89 42 -35 36 -35 24 -31 46 61 rcurveline 197 | -27 33 -41 37 -47 37 49 113 31 144 13 183 -45 9 rcurveline 198 | -13 -2 rlineto 199 | -135 hlineto 200 | 15 69 14 68 12 61 rrcurveto 201 | -57 -269 rmoveto 202 | 134 hlineto 203 | -14 -131 -26 -111 -38 -90 -39 27 -40 26 -38 22 20 78 21 90 20 89 rrcurveto 204 | endchar 205 | 206 | -------------------------------------------------------------------------------- 207 | 208 | CIDFont's Private DICTs: 209 | Private DICT for Font DICT #0 (0x00000112): 210 | << 211 | /BlueValues [-250 -250 1100 1100] % Original delta values: [-250 0 1350 0] 212 | /StdHW 40 213 | /StdVW 40 214 | /StemSnapH [40 120] % Original delta values: [40 80] 215 | /StemSnapV [40 120] % Original delta values: [40 80] 216 | /LanguageGroup 1 217 | /defaultWidthX 1000 218 | /nominalWidthX 107 219 | % ----- Following entries are missing, so they get default values: ----- 220 | /BlueScale 0.039625 % default 221 | /BlueShift 7 % default 222 | /BlueFuzz 1 % default 223 | /ForceBold false % default 224 | /ExpansionFactor 0.06 % default 225 | /initialRandomSeed 0 % default 226 | >> 227 | Private DICT for Font DICT #1 (0x0000012e): 228 | << 229 | /BlueValues [-250 -250 1100 1100] % Original delta values: [-250 0 1350 0] 230 | /StdHW 58 231 | /StdVW 63 232 | /StemSnapH [58 65 84] % Original delta values: [58 7 19] 233 | /StemSnapV [63 73 89] % Original delta values: [63 10 16] 234 | /LanguageGroup 1 235 | /defaultWidthX 1000 236 | % ----- Following entries are missing, so they get default values: ----- 237 | /BlueScale 0.039625 % default 238 | /BlueShift 7 % default 239 | /BlueFuzz 1 % default 240 | /ForceBold false % default 241 | /ExpansionFactor 0.06 % default 242 | /initialRandomSeed 0 % default 243 | /nominalWidthX 0 % default 244 | >> 245 | 246 | -------------------------------------------------------------------------------- 247 | 248 | Local Subr INDEX for Font DICT #0: 249 | 250 | 251 | -------------------------------------------------------------------------------- 252 | 253 | Local Subr INDEX for Font DICT #1: 254 | 255 | 256 | -------------------------------------------------------------------------------- 257 | 258 | Info messages: 259 | Info: This is a CIDFont. 260 | 261 | % End of dump 262 | 263 | -------------------------------------------------------------------------------- /tests/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli" 3 | version = { workspace = true } 4 | edition = { workspace = true } 5 | publish = false 6 | 7 | [dependencies] 8 | subsetter = { path = "../.." } 9 | -------------------------------------------------------------------------------- /tests/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use subsetter::{subset, GlyphRemapper}; 3 | 4 | fn parse_gids(gids: &str) -> Vec { 5 | if gids == "*" { 6 | return (0..u16::MAX).collect(); 7 | } 8 | 9 | let split = gids.split(',').filter(|s| !s.is_empty()).collect::>(); 10 | let mut gids = vec![]; 11 | 12 | for el in &split { 13 | if el.contains('-') { 14 | let range = el.split('-').collect::>(); 15 | let first = range[0].parse::().unwrap(); 16 | let second = range[1].parse::().unwrap(); 17 | 18 | gids.extend(first..=second); 19 | } else { 20 | gids.push(el.parse::().unwrap()); 21 | } 22 | } 23 | 24 | gids 25 | } 26 | 27 | // Note that this is more of an experimental CLI used for testing. 28 | fn main() { 29 | let args: Vec = env::args().collect(); 30 | let data = std::fs::read(&args[1]).unwrap(); 31 | let gids = parse_gids(args.get(3).to_owned().unwrap_or(&"0-5".to_owned())); 32 | let remapper = GlyphRemapper::new_from_glyphs(gids.as_slice()); 33 | 34 | let sub = subset(&data, 0, &remapper).unwrap(); 35 | 36 | std::fs::write(args.get(2).unwrap_or(&"res.otf".to_owned()), sub).unwrap(); 37 | } 38 | -------------------------------------------------------------------------------- /tests/data/cff.tests: -------------------------------------------------------------------------------- 1 | // These tests act as regression tests to prevent regressions in CFF subsetting 2 | 3 | LatinModernRoman-Regular.otf;290,292 4 | LatinModernRoman-Regular.otf;580 5 | NewCMMath-Regular.otf;1034,4789 6 | NotoSansCJKsc-Regular.otf;230-232 7 | NotoSansCJKsc-Regular_custom_font_matrix.otf;1-2 8 | 9 | -------------------------------------------------------------------------------- /tests/data/fonttools.tests: -------------------------------------------------------------------------------- 1 | // These tests will check whether the fonttools output of a subsetted fonts 2 | // matches the expected output. 3 | 4 | ClickerScript-Regular.ttf;5,8,10,100-104 5 | DejaVuSansMono.ttf;140-155,100-105 6 | LatinModernRoman-Regular.otf;307,309,314,221 7 | MPLUS1p-Regular.ttf;3,45-50 8 | NotoSansCJKsc-Regular.otf;6543-6550,371-375 9 | NotoSans-Regular.ttf;567-570,2345-2350 10 | Roboto-Regular.ttf;456,460-463 11 | NewCMMath-Regular.otf;803-806,950-952,5600-5602 12 | NotoSansCJKsc-Bold-subset1.otf;1 13 | 14 | -------------------------------------------------------------------------------- /tests/data/subsets.tests: -------------------------------------------------------------------------------- 1 | // These tests will check whether the face metrics of the subsetted fonts 2 | // stay the same, as well as whether the outlines created by ttf-parser and skrifa 3 | // stay the same. 4 | 5 | // 372 glyphs in total 6 | ClickerScript-Regular.ttf;0 7 | ClickerScript-Regular.ttf;2 8 | ClickerScript-Regular.ttf;5,8,10 9 | ClickerScript-Regular.ttf;0-20,33,35,36,41-47 10 | ClickerScript-Regular.ttf;100-200,202,301,304 11 | ClickerScript-Regular.ttf;10,100-105,108,130-145,234,247,253,267,278-283,340 12 | ClickerScript-Regular.ttf;* 13 | 14 | // 3377 glyphs in total 15 | DejaVuSansMono.ttf;100-105,140-155 16 | DejaVuSansMono.ttf;10-30,40-80 17 | DejaVuSansMono.ttf;100-245,2456-2609,800-900 18 | DejaVuSansMono.ttf;600-620,3000-3146 19 | DejaVuSansMono.ttf;* 20 | 21 | // 821 glyphs in total 22 | LatinModernRoman-Regular.otf;5-15,60-57 23 | LatinModernRoman-Regular.otf;100-130,200-280 24 | LatinModernRoman-Regular.otf;370-400,526-612,701,710,800 25 | LatinModernRoman-Regular.otf;* 26 | 27 | // 8662 glyphs in total 28 | MPLUS1p-Regular.ttf;0,1 29 | MPLUS1p-Regular.ttf;346 30 | MPLUS1p-Regular.ttf;3,45,98,120,245,389,1043,1055-1063 31 | MPLUS1p-Regular.ttf;2030-2310,4590-5120 32 | MPLUS1p-Regular.ttf;* 33 | 34 | // 65535 glyphs in total 35 | NotoSansCJKsc-Regular.otf;1-2 36 | NotoSansCJKsc-Regular.otf;4500-5746 37 | NotoSansCJKsc-Regular.otf;12000-14000,34-60,80-90 38 | NotoSansCJKsc-Regular.otf;45-90,16000-16897,23569-25697 39 | NotoSansCJKsc-Regular.otf;65532 40 | NotoSansCJKsc-Regular.otf;* 41 | 42 | // 4688 glyphs in total 43 | NotoSans-Regular.ttf;567-570 44 | NotoSans-Regular.ttf;1034-1037,3020-3045 45 | NotoSans-Regular.ttf;3,6,8,9,11 46 | NotoSans-Regular.ttf;10-30 47 | NotoSans-Regular.ttf;30-50,132,137 48 | NotoSans-Regular.ttf;20-25,30,40,45,47,48,52-70,300-350,500-522,3001 49 | NotoSans-Regular.ttf;* 50 | 51 | // 1294 glyphs in total 52 | Roboto-Regular.ttf;3,4 53 | Roboto-Regular.ttf;40-60,90-130,156,180 54 | Roboto-Regular.ttf;45-60,120-145,750-780,810,812,830,850,900 55 | Roboto-Regular.ttf;757 56 | Roboto-Regular.ttf;* 57 | 58 | // 7601 glyphs in total 59 | NewCMMath-Regular.otf;400-402 60 | NewCMMath-Regular.otf;256-300,113-117 61 | NewCMMath-Regular.otf;137-400,890-900,2345-2400 62 | NewCMMath-Regular.otf;8 63 | NewCMMath-Regular.otf;4622 64 | NewCMMath-Regular.otf;* 65 | 66 | Syne-Regular_subset.otf;5 67 | TestTTC.ttc;* 68 | -------------------------------------------------------------------------------- /tests/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuzz" 3 | version = { workspace = true } 4 | edition = { workspace = true } 5 | publish = false 6 | 7 | [dependencies] 8 | subsetter = { path = "../.." } 9 | rand = "0.9.0" 10 | rand_distr = "0.5.1" 11 | rayon = "1.10.0" 12 | skrifa = "0.29.0" 13 | ttf-parser = "0.25.1" 14 | walkdir = "2.5.0" 15 | -------------------------------------------------------------------------------- /tests/fuzz/src/main.rs: -------------------------------------------------------------------------------- 1 | use rand::distr::weighted::WeightedIndex; 2 | use rand::prelude::{IteratorRandom, ThreadRng}; 3 | use rand::rng; 4 | use rand_distr::Distribution; 5 | use rayon::iter::IntoParallelRefIterator; 6 | use rayon::iter::ParallelIterator; 7 | use skrifa::instance::{LocationRef, Size}; 8 | use skrifa::outline::{DrawSettings, OutlinePen}; 9 | use skrifa::MetadataProvider; 10 | use std::ffi::OsStr; 11 | use std::fs; 12 | use std::path::Path; 13 | use subsetter::{subset, GlyphRemapper}; 14 | use ttf_parser::GlyphId; 15 | 16 | // Note that this is not really meant as an example for how to use this crate, but 17 | // rather just so that we can conveniently run the fuzzer. 18 | 19 | const NUM_ITERATIONS: usize = 200; 20 | 21 | fn main() { 22 | let exclude_fonts = [ 23 | // Seems to be an invalid font for some reason, fonttools can't read it either. 24 | // Glyph 822 doesn't seem to draw properly with ttf-parser... But most likely a ttf-parser 25 | // bug because it does work with skrifa and freetype. fonttools ttx subset matches 26 | // the output you get when subsetting with fonttools. 27 | "Souliyo-Regular.ttf", 28 | // Has `seac` operator. 29 | "waltograph42.otf", 30 | // Color font. 31 | "NotoColorEmojiCompatTest-Regular.ttf", 32 | ]; 33 | 34 | let paths = walkdir::WalkDir::new(std::env::var("FONTS_DIR").unwrap()) 35 | .into_iter() 36 | .map(|p| p.unwrap().path().to_path_buf()) 37 | .filter(|p| { 38 | let extension = p.extension().and_then(OsStr::to_str); 39 | (extension == Some("ttf") || extension == Some("otf")) 40 | && !exclude_fonts.contains(&p.file_name().unwrap().to_str().unwrap()) 41 | }) 42 | .collect::>(); 43 | 44 | loop { 45 | println!("Starting an iteration..."); 46 | 47 | paths.par_iter().for_each(|path| { 48 | let mut rng = rng(); 49 | let extension = path.extension().and_then(OsStr::to_str); 50 | let is_font_file = extension == Some("ttf") || extension == Some("otf"); 51 | 52 | if is_font_file { 53 | match run_test(path, &mut rng) { 54 | Ok(_) => {} 55 | Err(msg) => { 56 | println!("Error while fuzzing {:?}: {:}", path.clone(), msg) 57 | } 58 | } 59 | } 60 | }); 61 | } 62 | } 63 | 64 | fn run_test(path: &Path, rng: &mut ThreadRng) -> Result<(), String> { 65 | let data = fs::read(path).map_err(|_| "failed to read file".to_string())?; 66 | let old_ttf_face = ttf_parser::Face::parse(&data, 0) 67 | .map_err(|_| "failed to parse old face".to_string())?; 68 | 69 | let num_glyphs = old_ttf_face.number_of_glyphs(); 70 | let possible_gids = (0..num_glyphs).collect::>(); 71 | let dist = get_distribution(num_glyphs); 72 | 73 | let old_skrifa_face = skrifa::FontRef::new(&data).unwrap(); 74 | 75 | for _ in 0..NUM_ITERATIONS { 76 | let num = dist.sample(rng); 77 | let sample = possible_gids.clone().into_iter().choose_multiple(rng, num); 78 | let remapper = GlyphRemapper::new_from_glyphs(sample.as_slice()); 79 | let sample_strings = sample.iter().map(|g| g.to_string()).collect::>(); 80 | let subset = subset(&data, 0, &remapper).map_err(|_| { 81 | format!("subset failed for gids {:?}", sample_strings.join(",")) 82 | })?; 83 | let new_ttf_face = ttf_parser::Face::parse(&subset, 0).map_err(|_| { 84 | format!( 85 | "failed to parse new ttf face with gids {:?}", 86 | sample_strings.join(",") 87 | ) 88 | })?; 89 | let new_skrifa_face = skrifa::FontRef::new(&subset).map_err(|_| { 90 | format!( 91 | "failed to parse new skrifa face with gids {:?}", 92 | sample_strings.join(",") 93 | ) 94 | })?; 95 | 96 | glyph_outlines_ttf_parser(&old_ttf_face, &new_ttf_face, &remapper, &sample) 97 | .map_err(|g| { 98 | format!( 99 | "outlines didn't match for gid {:?} with ttf-parser, with sample {:?}", 100 | g, 101 | sample_strings.join(",") 102 | ) 103 | })?; 104 | 105 | glyph_outlines_skrifa(&old_skrifa_face, &new_skrifa_face, &remapper, &sample) 106 | .map_err(|g| { 107 | format!( 108 | "outlines didn't match for gid {:?} with skrifa, with sample {:?}", 109 | g, 110 | sample_strings.join(",") 111 | ) 112 | })?; 113 | 114 | ttf_parser_glyph_metrics(&old_ttf_face, &new_ttf_face, &remapper, &sample) 115 | .map_err(|e| { 116 | format!( 117 | "glyph metrics for sample {:?} didn't match: {:?}", 118 | sample_strings.join(","), 119 | e 120 | ) 121 | })?; 122 | } 123 | 124 | Ok(()) 125 | } 126 | 127 | fn get_distribution(num_glyphs: u16) -> WeightedIndex { 128 | let mut weights = vec![0]; 129 | 130 | for i in 1..num_glyphs { 131 | if i <= 10 { 132 | weights.push(8000); 133 | } else if i <= 50 { 134 | weights.push(16000); 135 | } else if i <= 200 { 136 | weights.push(6000); 137 | } else if i <= 2000 { 138 | weights.push(100); 139 | } else if i <= 5000 { 140 | weights.push(2); 141 | } 142 | } 143 | 144 | WeightedIndex::new(&weights).unwrap() 145 | } 146 | 147 | fn ttf_parser_glyph_metrics( 148 | old_face: &ttf_parser::Face, 149 | new_face: &ttf_parser::Face, 150 | mapper: &GlyphRemapper, 151 | gids: &[u16], 152 | ) -> Result<(), String> { 153 | for glyph in gids.iter().copied() { 154 | let mapped = mapper.get(glyph).unwrap(); 155 | 156 | // For some reason the glyph bounding box differs sometimes, so we don't check 157 | // that anymore. I verified via fonttools that our subset matches theirs. So it is 158 | // probably a ttf-parser issue... 159 | // if old_face.glyph_bounding_box(GlyphId(glyph)) 160 | // != new_face.glyph_bounding_box(GlyphId(mapped)) 161 | // { 162 | // return Err(format!("glyph bounding box for glyph {:?} didn't match.", glyph)); 163 | // } 164 | 165 | if old_face.glyph_hor_side_bearing(GlyphId(glyph)) 166 | != new_face.glyph_hor_side_bearing(GlyphId(mapped)) 167 | { 168 | return Err(format!( 169 | "glyph hor side bearing for glyph {:?} didn't match.", 170 | glyph 171 | )); 172 | } 173 | 174 | if old_face.glyph_hor_advance(GlyphId(glyph)) 175 | != new_face.glyph_hor_advance(GlyphId(mapped)) 176 | { 177 | return Err(format!("glyph hor advance for glyph {:?} didn't match.", glyph)); 178 | } 179 | } 180 | 181 | Ok(()) 182 | } 183 | 184 | fn glyph_outlines_skrifa( 185 | old_face: &skrifa::FontRef, 186 | new_face: &skrifa::FontRef, 187 | mapper: &GlyphRemapper, 188 | gids: &[u16], 189 | ) -> Result<(), String> { 190 | // let hinting_instance_old = HintingInstance::new( 191 | // &old_face.outline_glyphs(), 192 | // Size::new(150.0), 193 | // LocationRef::default(), 194 | // HintingMode::Smooth { lcd_subpixel: None, preserve_linear_metrics: false }, 195 | // ).map_err(|_| "failed to create old hinting instance".to_string())?; 196 | // 197 | // let hinting_instance_new = HintingInstance::new( 198 | // &new_face.outline_glyphs(), 199 | // Size::new(150.0), 200 | // LocationRef::default(), 201 | // HintingMode::Smooth { lcd_subpixel: None, preserve_linear_metrics: false }, 202 | // ).map_err(|_| "failed to create new hinting instance".to_string())?; 203 | 204 | let mut sink1 = Sink(vec![]); 205 | let mut sink2 = Sink(vec![]); 206 | 207 | for glyph in gids.iter().copied() { 208 | let new_glyph = mapper.get(glyph).ok_or("failed to remap glyph".to_string())?; 209 | // We don't to hinted because for some reason skrifa fails to do so even on the old face in many 210 | // cases. So it's not a subsetting issue. 211 | let settings = DrawSettings::unhinted(Size::new(150.0), LocationRef::default()); 212 | 213 | if let Some(glyph1) = 214 | old_face.outline_glyphs().get(skrifa::GlyphId::new(glyph as u32)) 215 | { 216 | glyph1 217 | .draw(settings, &mut sink1) 218 | .map_err(|e| format!("failed to draw old glyph {}: {}", glyph, e))?; 219 | 220 | let settings = 221 | DrawSettings::unhinted(Size::new(150.0), LocationRef::default()); 222 | let glyph2 = new_face 223 | .outline_glyphs() 224 | .get(skrifa::GlyphId::new(new_glyph as u32)) 225 | .unwrap_or_else(|| panic!("failed to find glyph {} in new face", glyph)); 226 | glyph2 227 | .draw(settings, &mut sink2) 228 | .map_err(|e| format!("failed to draw new glyph {}: {}", glyph, e))?; 229 | 230 | if sink1 != sink2 { 231 | return Err(format!("{}", glyph)); 232 | } 233 | } 234 | } 235 | 236 | Ok(()) 237 | } 238 | 239 | fn glyph_outlines_ttf_parser( 240 | old_face: &ttf_parser::Face, 241 | new_face: &ttf_parser::Face, 242 | mapper: &GlyphRemapper, 243 | gids: &[u16], 244 | ) -> Result<(), u16> { 245 | for glyph in gids { 246 | let new_glyph = mapper.get(*glyph).unwrap(); 247 | let mut sink1 = Sink::default(); 248 | let mut sink2 = Sink::default(); 249 | 250 | if old_face.outline_glyph(GlyphId(*glyph), &mut sink1).is_some() { 251 | new_face.outline_glyph(GlyphId(new_glyph), &mut sink2); 252 | if sink1 != sink2 { 253 | return Err(*glyph); 254 | } 255 | } 256 | } 257 | 258 | Ok(()) 259 | } 260 | 261 | #[derive(Debug, Default, PartialEq)] 262 | struct Sink(Vec); 263 | 264 | #[derive(Debug, PartialEq)] 265 | enum Inst { 266 | MoveTo(f32, f32), 267 | LineTo(f32, f32), 268 | QuadTo(f32, f32, f32, f32), 269 | CurveTo(f32, f32, f32, f32, f32, f32), 270 | Close, 271 | } 272 | 273 | impl OutlinePen for Sink { 274 | fn move_to(&mut self, x: f32, y: f32) { 275 | self.0.push(Inst::MoveTo(x, y)); 276 | } 277 | 278 | fn line_to(&mut self, x: f32, y: f32) { 279 | self.0.push(Inst::LineTo(x, y)); 280 | } 281 | 282 | fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { 283 | self.0.push(Inst::QuadTo(x1, y1, x, y)); 284 | } 285 | 286 | fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { 287 | self.0.push(Inst::CurveTo(x1, y1, x2, y2, x, y)); 288 | } 289 | 290 | fn close(&mut self) { 291 | self.0.push(Inst::Close); 292 | } 293 | } 294 | 295 | impl ttf_parser::OutlineBuilder for Sink { 296 | fn move_to(&mut self, x: f32, y: f32) { 297 | self.0.push(Inst::MoveTo(x, y)); 298 | } 299 | 300 | fn line_to(&mut self, x: f32, y: f32) { 301 | self.0.push(Inst::LineTo(x, y)); 302 | } 303 | 304 | fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { 305 | self.0.push(Inst::QuadTo(x1, y1, x, y)); 306 | } 307 | 308 | fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { 309 | self.0.push(Inst::CurveTo(x1, y1, x2, y2, x, y)); 310 | } 311 | 312 | fn close(&mut self) { 313 | self.0.push(Inst::Close); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /tests/scripts/gen-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | 4 | from pathlib import Path 5 | 6 | 7 | ROOT = Path(__file__).parent.parent 8 | DATA_DIR = ROOT / "data" 9 | FONT_DIR = ROOT / ".." / "fonts" 10 | SUBSETS_PATH = ROOT / "src" / "subsets.rs" 11 | FONT_TOOLS_PATH = ROOT / "src" / "font_tools.rs" 12 | CFF_PATH = ROOT / "src" / "cff.rs" 13 | 14 | 15 | def main(): 16 | gen_font_tools_tests() 17 | gen_cff_tests() 18 | gen_subset_tests() 19 | 20 | 21 | def gen_font_tools_tests(): 22 | cff_fonttools_impl("fonttools.tests", Path(FONT_TOOLS_PATH), "test_font_tools") 23 | 24 | def gen_cff_tests(): 25 | cff_fonttools_impl("cff.tests", Path(CFF_PATH), "test_cff_dump") 26 | 27 | def cff_fonttools_impl(test_src, out_path, fn_name): 28 | test_string = f"// This file was auto-generated by `{Path(__file__).name}`, do not edit manually.\n\n" 29 | test_string += "#![allow(non_snake_case)]\n\n" 30 | test_string += f"use crate::*;\n\n" 31 | 32 | counters = {} 33 | with open(DATA_DIR / test_src) as file: 34 | content = file.read().splitlines() 35 | for line in content: 36 | if line.startswith("//") or len(line.strip()) == 0: 37 | continue 38 | 39 | parts = line.split(";") 40 | 41 | font_file = parts[0] 42 | gids = parts[1] 43 | 44 | if font_file not in counters: 45 | counters[font_file] = 1 46 | 47 | counter = counters[font_file] 48 | counters[font_file] += 1 49 | 50 | function_name = f"{font_name_to_function(font_file)}_{counter}" 51 | 52 | test_string += "#[test] " 53 | test_string += f'fn {function_name}() {{{fn_name}("{font_file}", "{gids}", {counter})}}\n' 54 | 55 | with open(out_path, "w+") as file: 56 | file.write(test_string) 57 | 58 | 59 | def gen_subset_tests(): 60 | test_string = f"// This file was auto-generated by `{Path(__file__).name}`, do not edit manually.\n\n" 61 | test_string += "#![allow(non_snake_case)]\n\n" 62 | test_string += f"use crate::*;\n\n" 63 | 64 | counters = {} 65 | with open(DATA_DIR / "subsets.tests") as file: 66 | content = file.read().splitlines() 67 | for line in content: 68 | if line.startswith("//") or len(line.strip()) == 0: 69 | continue 70 | 71 | parts = line.split(";") 72 | 73 | font_file = parts[0] 74 | gids = parts[1] 75 | 76 | if font_file not in counters: 77 | counters[font_file] = 1 78 | 79 | counter = counters[font_file] 80 | counters[font_file] += 1 81 | 82 | functions = ["glyph_metrics", "glyph_outlines_ttf_parser", "glyph_outlines_skrifa"] 83 | 84 | for function in functions: 85 | function_name = f"{font_name_to_function(font_file)}_{counter}_{function}" 86 | 87 | test_string += "#[test] " 88 | test_string += f'fn {function_name}() {{{function}("{font_file}", "{gids}")}}\n' 89 | 90 | with open(Path(SUBSETS_PATH), "w+") as file: 91 | file.write(test_string) 92 | 93 | 94 | def font_name_to_function(font_name: str): 95 | camel_case_pattern = re.compile(r'(? 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Copyright 2003, 2009 B. Jackowski and J. M. Nowacki (on behalf of TeX users groups). This work is released under the GUST Font License -- see http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt for details. 62 | 63 | 64 | LM Roman 10 65 | 66 | 67 | Regular 68 | 69 | 70 | 2.004;UKWN;LMRoman10-Regular 71 | 72 | 73 | LMRoman10-Regular 74 | 75 | 76 | Version 2.004;PS 2.004;hotconv 1.0.49;makeotf.lib2.0.14853 77 | 78 | 79 | LMRoman10-Regular 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 280 endchar 140 | 141 | 142 | 785 -22 31 233 31 401 31 41 38 134 -20 hstemhm 143 | 56 103 418 89 hintmask 11100110 144 | 735 242 rmoveto 145 | 31 vlineto 146 | -122 -3 rlineto 147 | -40 -85 0 3 -36 hvcurveto 148 | -31 32 vlineto 149 | 90 3 -11 -37 hvcurveto 150 | -64 vlineto 151 | -112 -127 -9 -28 -65 -198 35 298 299 197 33 60 107 91 -90 -147 20 vhcurveto 152 | -14 2 0 -3 14 hhcurveto 153 | 16 0 3 21 hvcurveto 154 | 237 vlineto 155 | 17 0 7 -11 -4 -4 0 -12 -8 vhcurveto 156 | -50 -74 rlineto 157 | 32 -32 -54 54 -99 hhcurveto 158 | -186 -162 -158 -205 -205 160 -159 190 73 80 26 59 34 hvcurveto 159 | -22 13 40 -40 11 hhcurveto 160 | 9 0 8 15 hvcurveto 161 | 174 vlineto 162 | 39 4 5 65 vhcurveto 163 | hintmask 00011000 164 | -193 676 rmoveto 165 | -22 hlineto 166 | -87 -2 -66 -47 -59 hhcurveto 167 | -63 -63 49 85 -2 hvcurveto 168 | -22 hlineto 169 | -108 2 73 -64 74 hhcurveto 170 | 78 70 67 105 2 hvcurveto 171 | endchar 172 | 173 | 174 | 785 -22 31 233 31 401 31 52 -21 133 -20 hstemhm 175 | 56 103 418 89 hintmask 11100110 176 | 735 242 rmoveto 177 | 31 vlineto 178 | -122 -3 rlineto 179 | -40 -85 0 3 -36 hvcurveto 180 | -31 32 vlineto 181 | 90 3 -11 -37 hvcurveto 182 | -64 vlineto 183 | -112 -127 -9 -28 -65 -198 35 298 299 197 33 60 107 91 -90 -147 20 vhcurveto 184 | -14 2 0 -3 14 hhcurveto 185 | 16 0 3 21 hvcurveto 186 | 237 vlineto 187 | 17 0 7 -11 -4 -4 0 -12 -8 vhcurveto 188 | -50 -74 rlineto 189 | 32 -32 -54 54 -99 hhcurveto 190 | -186 -162 -158 -205 -205 160 -159 190 73 80 26 59 34 hvcurveto 191 | -22 13 40 -40 11 hhcurveto 192 | 9 0 8 15 hvcurveto 193 | 174 vlineto 194 | 39 4 5 65 vhcurveto 195 | hintmask 00011000 196 | -192 611 rmoveto 197 | -10 16 -140 -66 -140 66 -12 -16 151 -117 rlineto 198 | endchar 199 | 200 | 201 | 500 -206 23 190 58 45 23 16 23 247 23 -12 23 61 79 145 -20 hstemhm 202 | 28 52 -20 75 -59 75 -44 52 0 75 75 75 35 52 -38 52 hintmask 0000011100000000 203 | 259 554 rmoveto 204 | 19 -13 20 -27 -11 -11 -4 -8 -8 vhcurveto 205 | -1 60 21 45 33 36 rrcurveto 206 | 4 4 1 1 3 vvcurveto 207 | 5 -4 3 -4 -9 -59 -59 -86 -48 18 -31 30 26 14 20 20 vhcurveto 208 | 226 -150 rmoveto 209 | 17 -12 32 -39 -20 -44 -6 -41 -42 vhcurveto 210 | hintmask 0000100001000000 211 | 33 -42 -42 3 -22 hhcurveto 212 | -93 -69 -69 -77 hvcurveto 213 | hintmask 0000000000010000 214 | -44 22 -38 25 -21 vhcurveto 215 | hintmask 0010000000100000 216 | -13 -15 -18 -33 -35 vvcurveto 217 | -31 13 -38 31 -20 vhcurveto 218 | hintmask 1111000010001110 219 | -60 -17 -32 -43 -40 vvcurveto 220 | -72 99 -55 122 118 104 51 78 35 -14 51 -51 28 vhcurveto 221 | 28 -53 -58 0 -61 hhcurveto 222 | -25 -43 0 1 -7 hvcurveto 223 | -32 4 -21 31 32 vvcurveto 224 | 4 0 23 17 20 vhcurveto 225 | -28 39 41 -3 19 hhcurveto 226 | 93 69 69 77 37 -16 37 -25 23 hvcurveto 227 | hintmask 0001010001000001 228 | 34 36 36 5 18 hhcurveto 229 | 0 7 0 3 -1 vhcurveto 230 | -11 -4 -5 -11 -12 vvcurveto 231 | -17 13 -12 16 10 19 7 23 vhcurveto 232 | -176 -108 rmoveto 233 | -27 -1 -32 -15 -25 vhcurveto 234 | -12 -8 -23 -28 -40 hhcurveto 235 | -87 0 100 23 hvcurveto 236 | hintmask 1000100000100100 237 | 27 1 32 15 25 vhcurveto 238 | 12 8 23 28 40 hhcurveto 239 | 87 0 -100 -23 hvcurveto 240 | 110 -375 rmoveto 241 | -54 -71 -50 -98 vhcurveto 242 | hintmask 0100000010000010 243 | -101 -69 51 53 46 38 37 44 3 hvcurveto 244 | 59 hlineto 245 | 86 112 0 -86 hvcurveto 246 | endchar 247 | 248 | 249 | 778 54 40 312 40 0 40 hstemhm 250 | 153 40 0 40 311 40 0 40 hintmask 10110010 251 | 624 15 rmoveto 252 | 14 14 -13 13 -10 10 -74 75 rcurveline 253 | 27 33 16 43 47 vvcurveto 254 | 47 -16 43 -28 34 vhcurveto 255 | 74 74 10 10 14 13 -14 14 rlinecurve 256 | -15 14 -13 -13 -10 -10 -74 -74 rcurveline 257 | 27 -34 -42 17 -47 hhcurveto 258 | -47 -43 -17 -27 -33 hvcurveto 259 | -75 75 -10 10 -13 13 -15 -14 rlinecurve 260 | -14 -14 14 -14 10 -10 74 -74 rcurveline 261 | -27 -34 -17 -43 -47 vvcurveto 262 | -47 17 -43 27 -33 vhcurveto 263 | -74 -75 -10 -10 -14 -13 14 -14 rlinecurve 264 | 15 -14 13 13 10 10 74 74 rcurveline 265 | -27 34 43 -17 47 hhcurveto 266 | 47 43 17 27 33 hvcurveto 267 | 75 -74 10 -10 13 -13 14 14 rlinecurve 268 | -80 235 rmoveto 269 | -86 -69 -70 -86 vhcurveto 270 | hintmask 01001100 271 | -86 -70 70 86 86 70 70 86 86 69 -70 -86 hvcurveto 272 | endchar 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /tests/ttx/NotoSansCJKsc-Bold-subset1_1.ttx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | © 2014-2021 Adobe (http://www.adobe.com/). 59 | 60 | 61 | Noto Sans CJK SC 62 | 63 | 64 | Bold 65 | 66 | 67 | 2.004;GOOG;NotoSansCJKsc-Bold;ADOBE 68 | 69 | 70 | Noto Sans CJK SC Bold 71 | 72 | 73 | Version 2.004;hotconv 1.0.118;makeotfexe 2.5.65603 74 | 75 | 76 | NotoSansCJKsc-Bold 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -120 50 859 91 -50 50 hstemhm 158 | 100 50 700 50 hintmask 10111000 159 | 100 -120 rmoveto 160 | 800 1000 -800 hlineto 161 | 400 -459 rmoveto 162 | -318 409 rlineto 163 | 636 hlineto 164 | -286 -450 rmoveto 165 | hintmask 11011000 166 | 318 409 rlineto 167 | -818 vlineto 168 | -668 -41 rmoveto 169 | 318 409 318 -409 rlineto 170 | -668 859 rmoveto 171 | 318 -409 -318 -409 rlineto 172 | endchar 173 | 174 | 175 | 21 -21 180 113 324 107 hstem 176 | 276 135 vstem 177 | 276 hmoveto 178 | 135 180 65 113 -65 431 -190 hlineto 179 | -196 -443 rlineto 180 | -101 251 vlineto 181 | 113 vmoveto 182 | -127 hlineto 183 | 80 181 14 40 20 56 14 47 rlinecurve 184 | 4 hlineto 185 | -3 -51 -2 -62 -45 vvcurveto 186 | endchar 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /tests/ttx/clear.sh: -------------------------------------------------------------------------------- 1 | rm ./*.otf 2 | rm ./*_ref.ttx --------------------------------------------------------------------------------