├── .editorconfig ├── .github ├── renovate.json └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── files ├── figma-agent.service ├── figma-agent.socket └── install.sh ├── rust-toolchain.toml ├── rustfmt.toml └── src ├── config.rs ├── font.rs ├── lib.rs ├── main.rs ├── path.rs ├── payload.rs ├── renderer.rs ├── routes.rs └── scanner.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.rs] 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>neetly/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v5 14 | - uses: moonrepo/setup-rust@v1 15 | with: 16 | inherit-toolchain: true 17 | - run: rustup component add rustfmt --toolchain nightly 18 | 19 | - run: cargo fetch --locked 20 | - run: cargo +nightly fmt --check 21 | - run: cargo clippy -- --deny warnings 22 | - run: cargo test 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: ["**"] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | include: 15 | - target: x86_64-unknown-linux-gnu 16 | runner: ubuntu-22.04 17 | - target: aarch64-unknown-linux-gnu 18 | runner: ubuntu-22.04-arm 19 | 20 | runs-on: ${{ matrix.runner }} 21 | 22 | steps: 23 | - uses: actions/checkout@v5 24 | - uses: moonrepo/setup-rust@v1 25 | with: 26 | inherit-toolchain: true 27 | 28 | - run: cargo fetch --locked --target "${{ matrix.target }}" 29 | - run: cargo build --release --target "${{ matrix.target }}" 30 | 31 | - run: | 32 | mkdir ./release 33 | cp "./target/${{ matrix.target }}/release/figma-agent" \ 34 | "./release/figma-agent-${{ matrix.target }}" 35 | 36 | - uses: actions/upload-artifact@v5 37 | with: 38 | name: ${{ matrix.target }} 39 | path: ./release/figma-agent-${{ matrix.target }} 40 | 41 | release: 42 | needs: build 43 | 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - uses: actions/download-artifact@v6 48 | with: 49 | path: ./release 50 | merge-multiple: true 51 | - uses: softprops/action-gh-release@v2 52 | with: 53 | name: Figma Agent for Linux ${{ github.ref_name }} 54 | prerelease: ${{ contains(github.ref_name, '-') }} 55 | files: ./release/figma-agent-* 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Artifacts 2 | /target/ 3 | -------------------------------------------------------------------------------- /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 = "adler2" 7 | version = "2.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 | 11 | [[package]] 12 | name = "alloc-no-stdlib" 13 | version = "2.0.4" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 16 | 17 | [[package]] 18 | name = "alloc-stdlib" 19 | version = "0.2.2" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 22 | dependencies = [ 23 | "alloc-no-stdlib", 24 | ] 25 | 26 | [[package]] 27 | name = "anyhow" 28 | version = "1.0.100" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 31 | 32 | [[package]] 33 | name = "async-compression" 34 | version = "0.4.32" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" 37 | dependencies = [ 38 | "compression-codecs", 39 | "compression-core", 40 | "futures-core", 41 | "pin-project-lite", 42 | "tokio", 43 | ] 44 | 45 | [[package]] 46 | name = "atomic-waker" 47 | version = "1.1.2" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 50 | 51 | [[package]] 52 | name = "autocfg" 53 | version = "1.5.0" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 56 | 57 | [[package]] 58 | name = "axum" 59 | version = "0.8.6" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" 62 | dependencies = [ 63 | "axum-core", 64 | "bytes", 65 | "form_urlencoded", 66 | "futures-util", 67 | "http", 68 | "http-body", 69 | "http-body-util", 70 | "hyper", 71 | "hyper-util", 72 | "itoa", 73 | "matchit", 74 | "memchr", 75 | "mime", 76 | "percent-encoding", 77 | "pin-project-lite", 78 | "serde_core", 79 | "serde_json", 80 | "serde_path_to_error", 81 | "serde_urlencoded", 82 | "sync_wrapper", 83 | "tokio", 84 | "tower", 85 | "tower-layer", 86 | "tower-service", 87 | "tracing", 88 | ] 89 | 90 | [[package]] 91 | name = "axum-core" 92 | version = "0.5.5" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" 95 | dependencies = [ 96 | "bytes", 97 | "futures-core", 98 | "http", 99 | "http-body", 100 | "http-body-util", 101 | "mime", 102 | "pin-project-lite", 103 | "sync_wrapper", 104 | "tower-layer", 105 | "tower-service", 106 | "tracing", 107 | ] 108 | 109 | [[package]] 110 | name = "base64" 111 | version = "0.22.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 114 | 115 | [[package]] 116 | name = "bitflags" 117 | version = "2.10.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 120 | 121 | [[package]] 122 | name = "brotli" 123 | version = "8.0.2" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" 126 | dependencies = [ 127 | "alloc-no-stdlib", 128 | "alloc-stdlib", 129 | "brotli-decompressor", 130 | ] 131 | 132 | [[package]] 133 | name = "brotli-decompressor" 134 | version = "5.0.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" 137 | dependencies = [ 138 | "alloc-no-stdlib", 139 | "alloc-stdlib", 140 | ] 141 | 142 | [[package]] 143 | name = "bumpalo" 144 | version = "3.19.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 147 | 148 | [[package]] 149 | name = "bytemuck" 150 | version = "1.24.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" 153 | dependencies = [ 154 | "bytemuck_derive", 155 | ] 156 | 157 | [[package]] 158 | name = "bytemuck_derive" 159 | version = "1.10.2" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" 162 | dependencies = [ 163 | "proc-macro2", 164 | "quote", 165 | "syn", 166 | ] 167 | 168 | [[package]] 169 | name = "byteorder" 170 | version = "1.5.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 173 | 174 | [[package]] 175 | name = "bytes" 176 | version = "1.10.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 179 | 180 | [[package]] 181 | name = "cc" 182 | version = "1.2.44" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" 185 | dependencies = [ 186 | "find-msvc-tools", 187 | "jobserver", 188 | "libc", 189 | "shlex", 190 | ] 191 | 192 | [[package]] 193 | name = "cfg-if" 194 | version = "1.0.4" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 197 | 198 | [[package]] 199 | name = "compression-codecs" 200 | version = "0.4.31" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" 203 | dependencies = [ 204 | "brotli", 205 | "compression-core", 206 | "flate2", 207 | "memchr", 208 | "zstd", 209 | "zstd-safe", 210 | ] 211 | 212 | [[package]] 213 | name = "compression-core" 214 | version = "0.4.29" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" 217 | 218 | [[package]] 219 | name = "core_maths" 220 | version = "0.1.1" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" 223 | dependencies = [ 224 | "libm", 225 | ] 226 | 227 | [[package]] 228 | name = "crc32fast" 229 | version = "1.5.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 232 | dependencies = [ 233 | "cfg-if", 234 | ] 235 | 236 | [[package]] 237 | name = "either" 238 | version = "1.15.0" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 241 | 242 | [[package]] 243 | name = "equivalent" 244 | version = "1.0.2" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 247 | 248 | [[package]] 249 | name = "figma-agent" 250 | version = "0.4.2" 251 | dependencies = [ 252 | "anyhow", 253 | "axum", 254 | "fontconfig-parser", 255 | "harfrust", 256 | "interp", 257 | "itertools", 258 | "jsonc-parser", 259 | "listenfd", 260 | "read-fonts", 261 | "serde", 262 | "serde_json", 263 | "skrifa", 264 | "svg", 265 | "thiserror", 266 | "tokio", 267 | "tower", 268 | "tower-http", 269 | "tracing", 270 | "tracing-subscriber", 271 | "walkdir", 272 | "xdg", 273 | ] 274 | 275 | [[package]] 276 | name = "find-msvc-tools" 277 | version = "0.1.4" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" 280 | 281 | [[package]] 282 | name = "flate2" 283 | version = "1.1.5" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" 286 | dependencies = [ 287 | "crc32fast", 288 | "miniz_oxide", 289 | ] 290 | 291 | [[package]] 292 | name = "fnv" 293 | version = "1.0.7" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 296 | 297 | [[package]] 298 | name = "font-types" 299 | version = "0.10.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "511e2c18a516c666d27867d2f9821f76e7d591f762e9fc41dd6cc5c90fe54b0b" 302 | dependencies = [ 303 | "bytemuck", 304 | ] 305 | 306 | [[package]] 307 | name = "fontconfig-parser" 308 | version = "0.5.8" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" 311 | dependencies = [ 312 | "roxmltree", 313 | ] 314 | 315 | [[package]] 316 | name = "form_urlencoded" 317 | version = "1.2.2" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 320 | dependencies = [ 321 | "percent-encoding", 322 | ] 323 | 324 | [[package]] 325 | name = "futures-channel" 326 | version = "0.3.31" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 329 | dependencies = [ 330 | "futures-core", 331 | ] 332 | 333 | [[package]] 334 | name = "futures-core" 335 | version = "0.3.31" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 338 | 339 | [[package]] 340 | name = "futures-sink" 341 | version = "0.3.31" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 344 | 345 | [[package]] 346 | name = "futures-task" 347 | version = "0.3.31" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 350 | 351 | [[package]] 352 | name = "futures-util" 353 | version = "0.3.31" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 356 | dependencies = [ 357 | "futures-core", 358 | "futures-task", 359 | "pin-project-lite", 360 | "pin-utils", 361 | "slab", 362 | ] 363 | 364 | [[package]] 365 | name = "getrandom" 366 | version = "0.3.4" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 369 | dependencies = [ 370 | "cfg-if", 371 | "libc", 372 | "r-efi", 373 | "wasip2", 374 | ] 375 | 376 | [[package]] 377 | name = "harfrust" 378 | version = "0.3.2" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8" 381 | dependencies = [ 382 | "bitflags", 383 | "bytemuck", 384 | "core_maths", 385 | "read-fonts", 386 | "smallvec", 387 | ] 388 | 389 | [[package]] 390 | name = "hashbrown" 391 | version = "0.16.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" 394 | 395 | [[package]] 396 | name = "hdrhistogram" 397 | version = "7.5.4" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" 400 | dependencies = [ 401 | "byteorder", 402 | "num-traits", 403 | ] 404 | 405 | [[package]] 406 | name = "http" 407 | version = "1.3.1" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 410 | dependencies = [ 411 | "bytes", 412 | "fnv", 413 | "itoa", 414 | ] 415 | 416 | [[package]] 417 | name = "http-body" 418 | version = "1.0.1" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 421 | dependencies = [ 422 | "bytes", 423 | "http", 424 | ] 425 | 426 | [[package]] 427 | name = "http-body-util" 428 | version = "0.1.3" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 431 | dependencies = [ 432 | "bytes", 433 | "futures-core", 434 | "http", 435 | "http-body", 436 | "pin-project-lite", 437 | ] 438 | 439 | [[package]] 440 | name = "http-range-header" 441 | version = "0.4.2" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" 444 | 445 | [[package]] 446 | name = "httparse" 447 | version = "1.10.1" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 450 | 451 | [[package]] 452 | name = "httpdate" 453 | version = "1.0.3" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 456 | 457 | [[package]] 458 | name = "hyper" 459 | version = "1.7.0" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" 462 | dependencies = [ 463 | "atomic-waker", 464 | "bytes", 465 | "futures-channel", 466 | "futures-core", 467 | "http", 468 | "http-body", 469 | "httparse", 470 | "httpdate", 471 | "itoa", 472 | "pin-project-lite", 473 | "pin-utils", 474 | "smallvec", 475 | "tokio", 476 | ] 477 | 478 | [[package]] 479 | name = "hyper-util" 480 | version = "0.1.17" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" 483 | dependencies = [ 484 | "bytes", 485 | "futures-core", 486 | "http", 487 | "http-body", 488 | "hyper", 489 | "pin-project-lite", 490 | "tokio", 491 | "tower-service", 492 | ] 493 | 494 | [[package]] 495 | name = "indexmap" 496 | version = "2.12.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" 499 | dependencies = [ 500 | "equivalent", 501 | "hashbrown", 502 | ] 503 | 504 | [[package]] 505 | name = "interp" 506 | version = "2.1.1" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "433698c934a80497f6a2c37d6ce8398e70e0a5b8a50335e75d45f79d22259c26" 509 | dependencies = [ 510 | "itertools", 511 | "num-traits", 512 | ] 513 | 514 | [[package]] 515 | name = "iri-string" 516 | version = "0.7.8" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 519 | dependencies = [ 520 | "memchr", 521 | "serde", 522 | ] 523 | 524 | [[package]] 525 | name = "itertools" 526 | version = "0.14.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 529 | dependencies = [ 530 | "either", 531 | ] 532 | 533 | [[package]] 534 | name = "itoa" 535 | version = "1.0.15" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 538 | 539 | [[package]] 540 | name = "jobserver" 541 | version = "0.1.34" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 544 | dependencies = [ 545 | "getrandom", 546 | "libc", 547 | ] 548 | 549 | [[package]] 550 | name = "js-sys" 551 | version = "0.3.82" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" 554 | dependencies = [ 555 | "once_cell", 556 | "wasm-bindgen", 557 | ] 558 | 559 | [[package]] 560 | name = "jsonc-parser" 561 | version = "0.27.0" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "7ec4ac49f13c7b00f435f8a5bb55d725705e2cf620df35a5859321595102eb7e" 564 | dependencies = [ 565 | "serde_json", 566 | ] 567 | 568 | [[package]] 569 | name = "lazy_static" 570 | version = "1.5.0" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 573 | 574 | [[package]] 575 | name = "libc" 576 | version = "0.2.177" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 579 | 580 | [[package]] 581 | name = "libm" 582 | version = "0.2.15" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 585 | 586 | [[package]] 587 | name = "listenfd" 588 | version = "1.0.2" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "b87bc54a4629b4294d0b3ef041b64c40c611097a677d9dc07b2c67739fe39dba" 591 | dependencies = [ 592 | "libc", 593 | "uuid", 594 | "winapi", 595 | ] 596 | 597 | [[package]] 598 | name = "lock_api" 599 | version = "0.4.14" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 602 | dependencies = [ 603 | "scopeguard", 604 | ] 605 | 606 | [[package]] 607 | name = "log" 608 | version = "0.4.28" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 611 | 612 | [[package]] 613 | name = "matchit" 614 | version = "0.8.4" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 617 | 618 | [[package]] 619 | name = "memchr" 620 | version = "2.7.6" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 623 | 624 | [[package]] 625 | name = "mime" 626 | version = "0.3.17" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 629 | 630 | [[package]] 631 | name = "mime_guess" 632 | version = "2.0.5" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 635 | dependencies = [ 636 | "mime", 637 | "unicase", 638 | ] 639 | 640 | [[package]] 641 | name = "miniz_oxide" 642 | version = "0.8.9" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 645 | dependencies = [ 646 | "adler2", 647 | "simd-adler32", 648 | ] 649 | 650 | [[package]] 651 | name = "mio" 652 | version = "1.1.0" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" 655 | dependencies = [ 656 | "libc", 657 | "wasi", 658 | "windows-sys 0.61.2", 659 | ] 660 | 661 | [[package]] 662 | name = "nu-ansi-term" 663 | version = "0.50.3" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 666 | dependencies = [ 667 | "windows-sys 0.61.2", 668 | ] 669 | 670 | [[package]] 671 | name = "num-traits" 672 | version = "0.2.19" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 675 | dependencies = [ 676 | "autocfg", 677 | ] 678 | 679 | [[package]] 680 | name = "once_cell" 681 | version = "1.21.3" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 684 | 685 | [[package]] 686 | name = "parking_lot" 687 | version = "0.12.5" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 690 | dependencies = [ 691 | "lock_api", 692 | "parking_lot_core", 693 | ] 694 | 695 | [[package]] 696 | name = "parking_lot_core" 697 | version = "0.9.12" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 700 | dependencies = [ 701 | "cfg-if", 702 | "libc", 703 | "redox_syscall", 704 | "smallvec", 705 | "windows-link", 706 | ] 707 | 708 | [[package]] 709 | name = "percent-encoding" 710 | version = "2.3.2" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 713 | 714 | [[package]] 715 | name = "pin-project-lite" 716 | version = "0.2.16" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 719 | 720 | [[package]] 721 | name = "pin-utils" 722 | version = "0.1.0" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 725 | 726 | [[package]] 727 | name = "pkg-config" 728 | version = "0.3.32" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 731 | 732 | [[package]] 733 | name = "proc-macro2" 734 | version = "1.0.103" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 737 | dependencies = [ 738 | "unicode-ident", 739 | ] 740 | 741 | [[package]] 742 | name = "quote" 743 | version = "1.0.41" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 746 | dependencies = [ 747 | "proc-macro2", 748 | ] 749 | 750 | [[package]] 751 | name = "r-efi" 752 | version = "5.3.0" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 755 | 756 | [[package]] 757 | name = "read-fonts" 758 | version = "0.35.0" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" 761 | dependencies = [ 762 | "bytemuck", 763 | "core_maths", 764 | "font-types", 765 | ] 766 | 767 | [[package]] 768 | name = "redox_syscall" 769 | version = "0.5.18" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 772 | dependencies = [ 773 | "bitflags", 774 | ] 775 | 776 | [[package]] 777 | name = "roxmltree" 778 | version = "0.20.0" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" 781 | 782 | [[package]] 783 | name = "rustversion" 784 | version = "1.0.22" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 787 | 788 | [[package]] 789 | name = "ryu" 790 | version = "1.0.20" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 793 | 794 | [[package]] 795 | name = "same-file" 796 | version = "1.0.6" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 799 | dependencies = [ 800 | "winapi-util", 801 | ] 802 | 803 | [[package]] 804 | name = "scopeguard" 805 | version = "1.2.0" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 808 | 809 | [[package]] 810 | name = "serde" 811 | version = "1.0.228" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 814 | dependencies = [ 815 | "serde_core", 816 | "serde_derive", 817 | ] 818 | 819 | [[package]] 820 | name = "serde_core" 821 | version = "1.0.228" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 824 | dependencies = [ 825 | "serde_derive", 826 | ] 827 | 828 | [[package]] 829 | name = "serde_derive" 830 | version = "1.0.228" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 833 | dependencies = [ 834 | "proc-macro2", 835 | "quote", 836 | "syn", 837 | ] 838 | 839 | [[package]] 840 | name = "serde_json" 841 | version = "1.0.145" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 844 | dependencies = [ 845 | "itoa", 846 | "memchr", 847 | "ryu", 848 | "serde", 849 | "serde_core", 850 | ] 851 | 852 | [[package]] 853 | name = "serde_path_to_error" 854 | version = "0.1.20" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" 857 | dependencies = [ 858 | "itoa", 859 | "serde", 860 | "serde_core", 861 | ] 862 | 863 | [[package]] 864 | name = "serde_urlencoded" 865 | version = "0.7.1" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 868 | dependencies = [ 869 | "form_urlencoded", 870 | "itoa", 871 | "ryu", 872 | "serde", 873 | ] 874 | 875 | [[package]] 876 | name = "sharded-slab" 877 | version = "0.1.7" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 880 | dependencies = [ 881 | "lazy_static", 882 | ] 883 | 884 | [[package]] 885 | name = "shlex" 886 | version = "1.3.0" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 889 | 890 | [[package]] 891 | name = "signal-hook-registry" 892 | version = "1.4.6" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 895 | dependencies = [ 896 | "libc", 897 | ] 898 | 899 | [[package]] 900 | name = "simd-adler32" 901 | version = "0.3.7" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 904 | 905 | [[package]] 906 | name = "skrifa" 907 | version = "0.37.0" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" 910 | dependencies = [ 911 | "bytemuck", 912 | "read-fonts", 913 | ] 914 | 915 | [[package]] 916 | name = "slab" 917 | version = "0.4.11" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 920 | 921 | [[package]] 922 | name = "smallvec" 923 | version = "1.15.1" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 926 | 927 | [[package]] 928 | name = "socket2" 929 | version = "0.6.1" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" 932 | dependencies = [ 933 | "libc", 934 | "windows-sys 0.60.2", 935 | ] 936 | 937 | [[package]] 938 | name = "svg" 939 | version = "0.18.0" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "94afda9cd163c04f6bee8b4bf2501c91548deae308373c436f36aeff3cf3c4a3" 942 | 943 | [[package]] 944 | name = "syn" 945 | version = "2.0.108" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" 948 | dependencies = [ 949 | "proc-macro2", 950 | "quote", 951 | "unicode-ident", 952 | ] 953 | 954 | [[package]] 955 | name = "sync_wrapper" 956 | version = "1.0.2" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 959 | 960 | [[package]] 961 | name = "thiserror" 962 | version = "2.0.17" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 965 | dependencies = [ 966 | "thiserror-impl", 967 | ] 968 | 969 | [[package]] 970 | name = "thiserror-impl" 971 | version = "2.0.17" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 974 | dependencies = [ 975 | "proc-macro2", 976 | "quote", 977 | "syn", 978 | ] 979 | 980 | [[package]] 981 | name = "thread_local" 982 | version = "1.1.9" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 985 | dependencies = [ 986 | "cfg-if", 987 | ] 988 | 989 | [[package]] 990 | name = "tokio" 991 | version = "1.48.0" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 994 | dependencies = [ 995 | "bytes", 996 | "libc", 997 | "mio", 998 | "parking_lot", 999 | "pin-project-lite", 1000 | "signal-hook-registry", 1001 | "socket2", 1002 | "tokio-macros", 1003 | "windows-sys 0.61.2", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "tokio-macros" 1008 | version = "2.6.0" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 1011 | dependencies = [ 1012 | "proc-macro2", 1013 | "quote", 1014 | "syn", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "tokio-util" 1019 | version = "0.7.16" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" 1022 | dependencies = [ 1023 | "bytes", 1024 | "futures-core", 1025 | "futures-sink", 1026 | "pin-project-lite", 1027 | "tokio", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "tower" 1032 | version = "0.5.2" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1035 | dependencies = [ 1036 | "futures-core", 1037 | "futures-util", 1038 | "hdrhistogram", 1039 | "indexmap", 1040 | "pin-project-lite", 1041 | "slab", 1042 | "sync_wrapper", 1043 | "tokio", 1044 | "tokio-util", 1045 | "tower-layer", 1046 | "tower-service", 1047 | "tracing", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "tower-http" 1052 | version = "0.6.6" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 1055 | dependencies = [ 1056 | "async-compression", 1057 | "base64", 1058 | "bitflags", 1059 | "bytes", 1060 | "futures-core", 1061 | "futures-util", 1062 | "http", 1063 | "http-body", 1064 | "http-body-util", 1065 | "http-range-header", 1066 | "httpdate", 1067 | "iri-string", 1068 | "mime", 1069 | "mime_guess", 1070 | "percent-encoding", 1071 | "pin-project-lite", 1072 | "tokio", 1073 | "tokio-util", 1074 | "tower", 1075 | "tower-layer", 1076 | "tower-service", 1077 | "tracing", 1078 | "uuid", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "tower-layer" 1083 | version = "0.3.3" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1086 | 1087 | [[package]] 1088 | name = "tower-service" 1089 | version = "0.3.3" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1092 | 1093 | [[package]] 1094 | name = "tracing" 1095 | version = "0.1.41" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1098 | dependencies = [ 1099 | "log", 1100 | "pin-project-lite", 1101 | "tracing-attributes", 1102 | "tracing-core", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "tracing-attributes" 1107 | version = "0.1.30" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 1110 | dependencies = [ 1111 | "proc-macro2", 1112 | "quote", 1113 | "syn", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "tracing-core" 1118 | version = "0.1.34" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 1121 | dependencies = [ 1122 | "once_cell", 1123 | "valuable", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "tracing-log" 1128 | version = "0.2.0" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1131 | dependencies = [ 1132 | "log", 1133 | "once_cell", 1134 | "tracing-core", 1135 | ] 1136 | 1137 | [[package]] 1138 | name = "tracing-subscriber" 1139 | version = "0.3.20" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" 1142 | dependencies = [ 1143 | "nu-ansi-term", 1144 | "sharded-slab", 1145 | "smallvec", 1146 | "thread_local", 1147 | "tracing-core", 1148 | "tracing-log", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "unicase" 1153 | version = "2.8.1" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 1156 | 1157 | [[package]] 1158 | name = "unicode-ident" 1159 | version = "1.0.22" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 1162 | 1163 | [[package]] 1164 | name = "uuid" 1165 | version = "1.18.1" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 1168 | dependencies = [ 1169 | "getrandom", 1170 | "js-sys", 1171 | "wasm-bindgen", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "valuable" 1176 | version = "0.1.1" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1179 | 1180 | [[package]] 1181 | name = "walkdir" 1182 | version = "2.5.0" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1185 | dependencies = [ 1186 | "same-file", 1187 | "winapi-util", 1188 | ] 1189 | 1190 | [[package]] 1191 | name = "wasi" 1192 | version = "0.11.1+wasi-snapshot-preview1" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1195 | 1196 | [[package]] 1197 | name = "wasip2" 1198 | version = "1.0.1+wasi-0.2.4" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 1201 | dependencies = [ 1202 | "wit-bindgen", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "wasm-bindgen" 1207 | version = "0.2.105" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" 1210 | dependencies = [ 1211 | "cfg-if", 1212 | "once_cell", 1213 | "rustversion", 1214 | "wasm-bindgen-macro", 1215 | "wasm-bindgen-shared", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "wasm-bindgen-macro" 1220 | version = "0.2.105" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" 1223 | dependencies = [ 1224 | "quote", 1225 | "wasm-bindgen-macro-support", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "wasm-bindgen-macro-support" 1230 | version = "0.2.105" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" 1233 | dependencies = [ 1234 | "bumpalo", 1235 | "proc-macro2", 1236 | "quote", 1237 | "syn", 1238 | "wasm-bindgen-shared", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "wasm-bindgen-shared" 1243 | version = "0.2.105" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" 1246 | dependencies = [ 1247 | "unicode-ident", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "winapi" 1252 | version = "0.3.9" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1255 | dependencies = [ 1256 | "winapi-i686-pc-windows-gnu", 1257 | "winapi-x86_64-pc-windows-gnu", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "winapi-i686-pc-windows-gnu" 1262 | version = "0.4.0" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1265 | 1266 | [[package]] 1267 | name = "winapi-util" 1268 | version = "0.1.11" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 1271 | dependencies = [ 1272 | "windows-sys 0.61.2", 1273 | ] 1274 | 1275 | [[package]] 1276 | name = "winapi-x86_64-pc-windows-gnu" 1277 | version = "0.4.0" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1280 | 1281 | [[package]] 1282 | name = "windows-link" 1283 | version = "0.2.1" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1286 | 1287 | [[package]] 1288 | name = "windows-sys" 1289 | version = "0.60.2" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1292 | dependencies = [ 1293 | "windows-targets", 1294 | ] 1295 | 1296 | [[package]] 1297 | name = "windows-sys" 1298 | version = "0.61.2" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1301 | dependencies = [ 1302 | "windows-link", 1303 | ] 1304 | 1305 | [[package]] 1306 | name = "windows-targets" 1307 | version = "0.53.5" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 1310 | dependencies = [ 1311 | "windows-link", 1312 | "windows_aarch64_gnullvm", 1313 | "windows_aarch64_msvc", 1314 | "windows_i686_gnu", 1315 | "windows_i686_gnullvm", 1316 | "windows_i686_msvc", 1317 | "windows_x86_64_gnu", 1318 | "windows_x86_64_gnullvm", 1319 | "windows_x86_64_msvc", 1320 | ] 1321 | 1322 | [[package]] 1323 | name = "windows_aarch64_gnullvm" 1324 | version = "0.53.1" 1325 | source = "registry+https://github.com/rust-lang/crates.io-index" 1326 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 1327 | 1328 | [[package]] 1329 | name = "windows_aarch64_msvc" 1330 | version = "0.53.1" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 1333 | 1334 | [[package]] 1335 | name = "windows_i686_gnu" 1336 | version = "0.53.1" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 1339 | 1340 | [[package]] 1341 | name = "windows_i686_gnullvm" 1342 | version = "0.53.1" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 1345 | 1346 | [[package]] 1347 | name = "windows_i686_msvc" 1348 | version = "0.53.1" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 1351 | 1352 | [[package]] 1353 | name = "windows_x86_64_gnu" 1354 | version = "0.53.1" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 1357 | 1358 | [[package]] 1359 | name = "windows_x86_64_gnullvm" 1360 | version = "0.53.1" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 1363 | 1364 | [[package]] 1365 | name = "windows_x86_64_msvc" 1366 | version = "0.53.1" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 1369 | 1370 | [[package]] 1371 | name = "wit-bindgen" 1372 | version = "0.46.0" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 1375 | 1376 | [[package]] 1377 | name = "xdg" 1378 | version = "3.0.0" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" 1381 | 1382 | [[package]] 1383 | name = "zstd" 1384 | version = "0.13.3" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 1387 | dependencies = [ 1388 | "zstd-safe", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "zstd-safe" 1393 | version = "7.2.4" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 1396 | dependencies = [ 1397 | "zstd-sys", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "zstd-sys" 1402 | version = "2.0.16+zstd.1.5.7" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" 1405 | dependencies = [ 1406 | "cc", 1407 | "pkg-config", 1408 | ] 1409 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "figma-agent" 3 | version = "0.4.2" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | anyhow = "=1.0.100" 8 | axum = "=0.8.6" 9 | fontconfig-parser = "=0.5.8" 10 | harfrust = "=0.3.2" 11 | interp = "=2.1.1" 12 | itertools = "=0.14.0" 13 | jsonc-parser = { version = "=0.27.0", features = ["serde"] } 14 | listenfd = "=1.0.2" 15 | read-fonts = "=0.35.0" 16 | serde = { version = "=1.0.228", features = ["derive"] } 17 | serde_json = "=1.0.145" 18 | skrifa = "=0.37.0" 19 | svg = "=0.18.0" 20 | thiserror = "=2.0.17" 21 | tokio = { version = "=1.48.0", features = ["full"] } 22 | tower = { version = "=0.5.2", features = ["full"] } 23 | tower-http = { version = "=0.6.6", features = ["full"] } 24 | tracing = "=0.1.41" 25 | tracing-subscriber = "=0.3.20" 26 | walkdir = "=2.5.0" 27 | xdg = "=3.0.0" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Hikari Hayashi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Figma Agent for Linux 2 | 3 | [![CI](https://github.com/neetly/figma-agent-linux/actions/workflows/ci.yml/badge.svg)](https://github.com/neetly/figma-agent-linux/actions/workflows/ci.yml) 4 | 5 | This service allows you to use your locally installed fonts on [figma.com](https://www.figma.com/). 6 | 7 | ## Features 8 | 9 | - Variable fonts support 10 | - Preview fonts in the font picker 11 | 12 | ## Installation 13 | 14 | > [!IMPORTANT] 15 | > To make this service work, you need to override the browser's user agent to a Windows one. See [this thread](https://forum.figma.com/report-a-problem-6/requests-to-font-helper-on-linux-stopped-working-16569) for more information. 16 | 17 | ```sh 18 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/neetly/figma-agent-linux/main/files/install.sh)" 19 | ``` 20 | 21 | > [!TIP] 22 | > You can run the command again to update this service to the latest version. 23 | 24 | ### Package Managers 25 | 26 | | Package Manager | Package | 27 | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | 28 | | Arch Linux | [figma-agent-linux](https://aur.archlinux.org/packages/figma-agent-linux) / [figma-agent-linux-bin](https://aur.archlinux.org/packages/figma-agent-linux-bin) | 29 | | Nix | [figma-agent](https://search.nixos.org/packages?show=figma-agent) | 30 | 31 | ### Uninstallation 32 | 33 |
34 | 35 | ```sh 36 | systemctl --user disable --now figma-agent.{service,socket} 37 | rm -rf ~/.local/share/figma-agent ~/.local/share/systemd/user/figma-agent.{service,socket} 38 | ``` 39 | 40 |
41 | 42 | ## Configuration 43 | 44 | ```jsonc 45 | // ~/.config/figma-agent/config.json 46 | { 47 | // Default: "127.0.0.1:44950" 48 | "bind": "127.0.0.1:44950", 49 | // Default: true 50 | "use_system_fonts": true, 51 | // Default: [] 52 | "font_directories": ["~/Fonts"], 53 | // Default: true 54 | "enable_font_rescan": true, 55 | // Default: true 56 | "enable_font_preview": true, 57 | } 58 | ``` 59 | 60 | > [!NOTE] 61 | > You need to restart this service to apply the configuration changes. 62 | > 63 | > ```sh 64 | > systemctl --user restart figma-agent.service 65 | > ``` 66 | 67 | ## Caveats 68 | 69 | ### Ad Blockers 70 | 71 | Ad blockers may prevent websites from connecting to localhost for privacy concerns. Please disable the relevant rules or create an exception rule for [figma.com](https://www.figma.com/). 72 | 73 | ### Brave Browser 74 | 75 | In Brave browser, websites require special permissions to access localhost. Please follow the instructions in [the documentation](https://brave.com/privacy-updates/27-localhost-permission/) to grant the permission to [figma.com](https://www.figma.com/). 76 | 77 | ## Credits 78 | 79 | This project is inspired by [Figma Linux Font Helper](https://github.com/Figma-Linux/figma-linux-font-helper). 80 | -------------------------------------------------------------------------------- /files/figma-agent.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Figma Agent for Linux Service 3 | Requires=figma-agent.socket 4 | 5 | [Service] 6 | Type=exec 7 | ExecStart=/usr/bin/figma-agent 8 | 9 | [Install] 10 | WantedBy=default.target 11 | -------------------------------------------------------------------------------- /files/figma-agent.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Figma Agent for Linux Socket 3 | 4 | [Socket] 5 | ListenStream=127.0.0.1:44950 6 | 7 | [Install] 8 | WantedBy=sockets.target 9 | -------------------------------------------------------------------------------- /files/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" 5 | 6 | echo ":: Figma Agent for Linux" 7 | echo 8 | 9 | echo ":: Downloading figma-agent..." 10 | TMPFILE="$(mktemp)" 11 | curl --fail --location "https://github.com/neetly/figma-agent-linux/releases/latest/download/figma-agent-$(uname -m)-unknown-linux-gnu" \ 12 | --output "$TMPFILE" 13 | chmod +x "$TMPFILE" 14 | mkdir -p "$XDG_DATA_HOME/figma-agent" 15 | mv "$TMPFILE" "$XDG_DATA_HOME/figma-agent/figma-agent" 16 | echo 17 | 18 | echo ":: Creating figma-agent.service and figma-agent.socket..." 19 | mkdir -p "$XDG_DATA_HOME/systemd/user" 20 | cat > "$XDG_DATA_HOME/systemd/user/figma-agent.service" << EOF 21 | [Unit] 22 | Description=Figma Agent for Linux Service 23 | Requires=figma-agent.socket 24 | 25 | [Service] 26 | Type=exec 27 | ExecStart="$XDG_DATA_HOME/figma-agent/figma-agent" 28 | 29 | [Install] 30 | WantedBy=default.target 31 | EOF 32 | cat > "$XDG_DATA_HOME/systemd/user/figma-agent.socket" << EOF 33 | [Unit] 34 | Description=Figma Agent for Linux Socket 35 | 36 | [Socket] 37 | ListenStream=127.0.0.1:44950 38 | 39 | [Install] 40 | WantedBy=sockets.target 41 | EOF 42 | echo 43 | 44 | echo ":: Enabling figma-agent.socket..." 45 | systemctl --user daemon-reload 46 | systemctl --user enable --now figma-agent.socket 47 | echo 48 | 49 | if systemctl --user is-active figma-agent.service > /dev/null; then 50 | echo ":: Restarting figma-agent.service..." 51 | systemctl --user restart figma-agent.service 52 | echo 53 | fi 54 | 55 | echo ":: Done" 56 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.90.0" 3 | targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] 4 | profile = "default" 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_field_init_shorthand = true 2 | unstable_features = true 3 | group_imports = "StdExternalCrate" 4 | imports_granularity = "Crate" 5 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, io, iter, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use fontconfig_parser::FontConfig; 7 | use itertools::{Either, Itertools}; 8 | 9 | use crate::path::expand_home; 10 | 11 | #[derive(Debug, thiserror::Error)] 12 | pub enum ConfigError { 13 | #[error("Failed to read config file")] 14 | Read(#[from] io::Error), 15 | #[error("Failed to parse config file")] 16 | Parse(#[from] jsonc_parser::errors::ParseError), 17 | #[error("Failed to parse config file")] 18 | Deserialize(#[from] serde_json::Error), 19 | #[error("Failed to parse config file")] 20 | Invalid, 21 | } 22 | 23 | #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize)] 24 | pub struct Config { 25 | #[serde(default = "default_bind")] 26 | pub bind: String, 27 | #[serde(default = "default_bool::")] 28 | pub use_system_fonts: bool, 29 | #[serde(default)] 30 | pub font_directories: Vec, 31 | #[serde(default = "default_bool::")] 32 | pub enable_font_rescan: bool, 33 | #[serde(default = "default_bool::")] 34 | pub enable_font_preview: bool, 35 | } 36 | 37 | fn default_bind() -> String { 38 | "127.0.0.1:44950".into() 39 | } 40 | 41 | fn default_bool() -> bool { 42 | V 43 | } 44 | 45 | impl Default for Config { 46 | fn default() -> Self { 47 | serde_json::from_value(serde_json::json!({})).unwrap() 48 | } 49 | } 50 | 51 | impl Config { 52 | pub fn parse(text: impl AsRef) -> Result { 53 | let value = jsonc_parser::parse_to_serde_value( 54 | text.as_ref(), 55 | &jsonc_parser::ParseOptions::default(), 56 | )?; 57 | let value = value.ok_or(ConfigError::Invalid)?; 58 | let config = serde_json::from_value(value)?; 59 | Ok(config) 60 | } 61 | 62 | pub fn from_path(path: impl AsRef) -> Result { 63 | let text = fs::read_to_string(path)?; 64 | Config::parse(text) 65 | } 66 | } 67 | 68 | #[test] 69 | fn test_default() { 70 | assert_eq!( 71 | Config::default(), 72 | Config { 73 | bind: "127.0.0.1:44950".into(), 74 | use_system_fonts: true, 75 | font_directories: vec![], 76 | enable_font_rescan: true, 77 | enable_font_preview: true, 78 | }, 79 | ); 80 | } 81 | 82 | #[test] 83 | fn test_parse() { 84 | assert_eq!(Config::parse("{}").unwrap(), Config::default()); 85 | assert_eq!(Config::parse("{} // comment").unwrap(), Config::default()); 86 | assert_eq!( 87 | Config::parse( 88 | r#"{ "bind": "0.0.0.0:44950", "use_system_fonts": false, "font_directories": ["/usr/share/fonts"], "enable_font_rescan": false, "enable_font_preview": false }"#, 89 | ) 90 | .unwrap(), 91 | Config { 92 | bind: "0.0.0.0:44950".into(), 93 | use_system_fonts: false, 94 | font_directories: vec![PathBuf::from("/usr/share/fonts")], 95 | enable_font_rescan: false, 96 | enable_font_preview: false, 97 | }, 98 | ); 99 | } 100 | 101 | impl Config { 102 | pub fn effective_font_directories( 103 | &self, 104 | fontconfig: &FontConfig, 105 | ) -> impl Iterator { 106 | self.font_directories 107 | .iter() 108 | .filter_map(|directory| match expand_home(directory) { 109 | Ok(directory) => Some(directory), 110 | Err(error) => { 111 | tracing::debug!("Skipped font directory: {directory:?}, error: {error:?}"); 112 | None 113 | } 114 | }) 115 | .chain(if self.use_system_fonts { 116 | Either::Left(fontconfig.dirs.iter().map(|dir| dir.path.clone())) 117 | } else { 118 | Either::Right(iter::empty()) 119 | }) 120 | .filter_map(|directory| match directory.canonicalize() { 121 | Ok(directory) => Some(directory), 122 | Err(error) => { 123 | tracing::debug!("Skipped font directory: {directory:?}, error: {error:?}"); 124 | None 125 | } 126 | }) 127 | .unique() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/font.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | time::SystemTime, 5 | }; 6 | 7 | use interp::{InterpMode, interp}; 8 | use skrifa::{MetadataProvider, string::StringId}; 9 | 10 | #[derive(Debug, thiserror::Error)] 11 | pub enum FontError { 12 | #[error("Failed to read font file")] 13 | Read(#[from] std::io::Error), 14 | #[error("Failed to parse font file")] 15 | Parse(Vec<(usize, read_fonts::ReadError)>, Option), 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct FontFile { 20 | pub path: PathBuf, 21 | pub fonts: Vec, 22 | pub modified_at: Option, 23 | } 24 | 25 | impl FontFile { 26 | pub fn from_path(path: impl AsRef) -> Result { 27 | let path = path.as_ref(); 28 | 29 | let data = fs::read(path)?; 30 | let metadata = fs::metadata(path)?; 31 | 32 | let mut errors = Vec::new(); 33 | let fonts = skrifa::FontRef::fonts(&data) 34 | .enumerate() 35 | .filter_map(|(index, font)| match font { 36 | Ok(font) => Some(Font::from(&font, index)), 37 | Err(error) => { 38 | errors.push((index, error)); 39 | None 40 | } 41 | }) 42 | .collect(); 43 | 44 | let font_file = FontFile { 45 | path: path.into(), 46 | fonts, 47 | modified_at: metadata.modified().ok(), 48 | }; 49 | 50 | if errors.is_empty() { 51 | Ok(font_file) 52 | } else { 53 | Err(FontError::Parse(errors, Some(font_file))) 54 | } 55 | } 56 | } 57 | 58 | #[derive(Debug, Clone)] 59 | pub struct FontQuery<'a> { 60 | pub family_name: Option<&'a str>, 61 | pub subfamily_name: Option<&'a str>, 62 | pub postscript_name: Option<&'a str>, 63 | } 64 | 65 | #[derive(Debug, Clone)] 66 | pub struct FontQueryResult<'a> { 67 | pub font: &'a Font, 68 | pub named_instance: Option<&'a NamedInstance>, 69 | } 70 | 71 | impl<'a> FontFile { 72 | pub fn query(&'a self, query: FontQuery<'_>) -> Option> { 73 | fn matches(value: &Option>, query: &Option>) -> bool { 74 | match (value, query) { 75 | (Some(value), Some(query)) => value.as_ref() == query.as_ref(), 76 | (None, Some(_)) => false, 77 | (_, None) => true, 78 | } 79 | } 80 | 81 | self.fonts 82 | .iter() 83 | .filter_map(|font| { 84 | if matches(&font.family_name, &query.family_name) { 85 | if matches(&font.subfamily_name, &query.subfamily_name) 86 | && matches(&font.postscript_name, &query.postscript_name) 87 | { 88 | Some(FontQueryResult { 89 | font, 90 | named_instance: None, 91 | }) 92 | } else { 93 | font.named_instances 94 | .iter() 95 | .filter_map(|named_instance| { 96 | if matches(&named_instance.subfamily_name, &query.subfamily_name) 97 | && matches( 98 | &named_instance.postscript_name, 99 | &query.postscript_name, 100 | ) 101 | { 102 | Some(FontQueryResult { 103 | font, 104 | named_instance: Some(named_instance), 105 | }) 106 | } else { 107 | None 108 | } 109 | }) 110 | .next() 111 | } 112 | } else { 113 | None 114 | } 115 | }) 116 | .next() 117 | } 118 | } 119 | 120 | #[derive(Debug, Clone)] 121 | pub struct Font { 122 | pub index: usize, 123 | pub family_name: Option, 124 | pub subfamily_name: Option, 125 | pub postscript_name: Option, 126 | pub weight: f32, 127 | pub width: f32, 128 | pub is_italic: bool, 129 | pub is_oblique: bool, 130 | pub axes: Vec, 131 | pub named_instances: Vec, 132 | } 133 | 134 | impl Font { 135 | pub fn from(font: &skrifa::FontRef, index: usize) -> Self { 136 | let attributes = font.attributes(); 137 | 138 | Font { 139 | index, 140 | family_name: font 141 | .string(StringId::TYPOGRAPHIC_FAMILY_NAME) 142 | .or_else(|| font.string(StringId::FAMILY_NAME)), 143 | subfamily_name: font 144 | .string(StringId::TYPOGRAPHIC_SUBFAMILY_NAME) 145 | .or_else(|| font.string(StringId::SUBFAMILY_NAME)), 146 | postscript_name: font.string(StringId::POSTSCRIPT_NAME), 147 | weight: attributes.weight.value(), 148 | width: attributes.stretch.percentage(), 149 | is_italic: matches!(attributes.style, skrifa::attribute::Style::Italic), 150 | is_oblique: matches!(attributes.style, skrifa::attribute::Style::Oblique(_)), 151 | axes: font 152 | .axes() 153 | .iter() 154 | .enumerate() 155 | .map(|(index, axis)| Axis::from(font, &axis, index)) 156 | .collect(), 157 | named_instances: font 158 | .named_instances() 159 | .iter() 160 | .enumerate() 161 | .map(|(index, named_instance)| NamedInstance::from(font, &named_instance, index)) 162 | .collect(), 163 | } 164 | } 165 | } 166 | 167 | #[derive(Debug, Clone)] 168 | pub struct Axis { 169 | pub index: usize, 170 | pub tag: String, 171 | pub name: Option, 172 | pub min_value: f32, 173 | pub max_value: f32, 174 | pub default_value: f32, 175 | pub is_hidden: bool, 176 | } 177 | 178 | impl Axis { 179 | pub fn from(font: &skrifa::FontRef, axis: &skrifa::Axis, index: usize) -> Self { 180 | Axis { 181 | index, 182 | tag: axis.tag().to_string(), 183 | name: font.string(axis.name_id()), 184 | min_value: axis.min_value(), 185 | max_value: axis.max_value(), 186 | default_value: axis.default_value(), 187 | is_hidden: axis.is_hidden(), 188 | } 189 | } 190 | } 191 | 192 | #[derive(Debug, Clone)] 193 | pub struct NamedInstance { 194 | pub index: usize, 195 | pub subfamily_name: Option, 196 | pub postscript_name: Option, 197 | pub coordinates: Vec, 198 | } 199 | 200 | impl NamedInstance { 201 | pub fn from( 202 | font: &skrifa::FontRef, 203 | named_instance: &skrifa::NamedInstance, 204 | index: usize, 205 | ) -> Self { 206 | NamedInstance { 207 | index, 208 | subfamily_name: font.string(named_instance.subfamily_name_id()), 209 | postscript_name: named_instance 210 | .postscript_name_id() 211 | .and_then(|id| font.string(id)) 212 | .or_else(|| { 213 | // https://adobe-type-tools.github.io/font-tech-notes/pdfs/5902.AdobePSNameGeneration.pdf 214 | font.string(StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX) 215 | .or_else(|| { 216 | font.string(StringId::TYPOGRAPHIC_FAMILY_NAME) 217 | .map(|family_name| family_name.postscript()) 218 | }) 219 | .zip(font.string(named_instance.subfamily_name_id())) 220 | .map(|(postscript_family_prefix, subfamily)| { 221 | format!("{}-{}", postscript_family_prefix, subfamily.postscript()) 222 | }) 223 | }), 224 | coordinates: named_instance.user_coords().collect(), 225 | } 226 | } 227 | } 228 | 229 | pub trait StringExt { 230 | fn postscript(&self) -> String; 231 | } 232 | 233 | impl StringExt for String { 234 | fn postscript(&self) -> String { 235 | self.chars() 236 | .filter(|char| char.is_ascii_alphanumeric()) 237 | .collect() 238 | } 239 | } 240 | 241 | pub trait SkrifaFontRefExt { 242 | fn string(&self, id: StringId) -> Option; 243 | } 244 | 245 | impl SkrifaFontRefExt for skrifa::FontRef<'_> { 246 | fn string(&self, id: StringId) -> Option { 247 | self.localized_strings(id) 248 | .english_or_first() 249 | .map(|localized_string| localized_string.to_string()) 250 | } 251 | } 252 | 253 | /// Convert weight axis (wght) to OS/2 usWeightClass. 254 | /// 255 | /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass 256 | pub fn to_us_weight_class(weight: f32) -> u16 { 257 | weight.round() as u16 258 | } 259 | 260 | /// Convert width axis (wdth) to OS/2 usWidthClass. 261 | /// 262 | /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass 263 | pub fn to_us_width_class(width: f32) -> u16 { 264 | static WIDTH_VALUES: [f32; 9] = [50.0, 62.5, 75.0, 87.5, 100.0, 112.5, 125.0, 150.0, 200.0]; 265 | static US_WIDTH_CLASS_VALUES: [f32; 9] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; 266 | 267 | interp( 268 | &WIDTH_VALUES, 269 | &US_WIDTH_CLASS_VALUES, 270 | width, 271 | &InterpMode::FirstLast, 272 | ) 273 | .round() as u16 274 | } 275 | 276 | #[test] 277 | fn test_to_us_width_class() { 278 | assert_eq!(to_us_width_class(50.0), 1); 279 | assert_eq!(to_us_width_class(62.5), 2); 280 | assert_eq!(to_us_width_class(75.0), 3); 281 | assert_eq!(to_us_width_class(87.5), 4); 282 | assert_eq!(to_us_width_class(100.0), 5); 283 | assert_eq!(to_us_width_class(112.5), 6); 284 | assert_eq!(to_us_width_class(125.0), 7); 285 | assert_eq!(to_us_width_class(150.0), 8); 286 | assert_eq!(to_us_width_class(200.0), 9); 287 | } 288 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | fs, 4 | path::{Path, PathBuf}, 5 | sync::LazyLock, 6 | }; 7 | 8 | use fontconfig_parser::FontConfig; 9 | use tokio::sync::RwLock; 10 | 11 | use crate::{ 12 | config::Config, 13 | font::{FontError, FontFile}, 14 | scanner::scan_font_paths, 15 | }; 16 | 17 | pub mod config; 18 | pub mod font; 19 | pub mod path; 20 | pub mod payload; 21 | pub mod renderer; 22 | pub mod routes; 23 | pub mod scanner; 24 | 25 | pub static XDG_DIRECTORIES: LazyLock = 26 | LazyLock::new(|| xdg::BaseDirectories::with_prefix("figma-agent")); 27 | 28 | pub static FONTCONFIG: LazyLock = LazyLock::new(|| { 29 | let mut font_config = FontConfig::default(); 30 | if let Err(error) = font_config.merge_config("/etc/fonts/fonts.conf") { 31 | tracing::warn!( 32 | "Failed to load Fontconfig config file: /etc/fonts/fonts.conf, error: {error:?}" 33 | ); 34 | } 35 | font_config 36 | }); 37 | 38 | pub static CONFIG: LazyLock = LazyLock::new(|| { 39 | XDG_DIRECTORIES 40 | .find_config_file("config.json") 41 | .and_then(|path| { 42 | tracing::info!("Use config file: {path:?}"); 43 | match Config::from_path(&path) { 44 | Ok(config) => { 45 | tracing::info!("Use config: {config:?}"); 46 | Some(config) 47 | } 48 | Err(error) => { 49 | tracing::error!("Failed to load config file: {path:?}, error: {error:?}"); 50 | None 51 | } 52 | } 53 | }) 54 | .unwrap_or_else(|| { 55 | let config = Config::default(); 56 | tracing::info!("Use default config: {config:?}"); 57 | config 58 | }) 59 | }); 60 | 61 | pub static EFFECTIVE_FONT_DIRECTORIES: LazyLock> = LazyLock::new(|| { 62 | let directories = CONFIG.effective_font_directories(&FONTCONFIG).collect(); 63 | tracing::info!("Use effective font directories: {directories:?}"); 64 | directories 65 | }); 66 | 67 | pub static FONT_FILES: LazyLock>> = 68 | LazyLock::new(|| RwLock::new(HashMap::new())); 69 | 70 | #[tracing::instrument] 71 | pub async fn scan_font_files() { 72 | tracing::debug!("Scanning font files..."); 73 | 74 | let mut font_files = FONT_FILES.write().await; 75 | 76 | let (mut added_count, mut updated_count, mut removed_count) = (0, 0, 0); 77 | let mut font_paths = scan_font_paths(&*EFFECTIVE_FONT_DIRECTORIES).collect::>(); 78 | 79 | font_files.retain(|path, _| { 80 | let contains = font_paths.contains(path); 81 | if !contains { 82 | removed_count += 1; 83 | } 84 | contains 85 | }); 86 | 87 | font_paths.retain(|path| { 88 | if let Some(font_file) = font_files.get(path) { 89 | let modified_at = fs::metadata(path) 90 | .and_then(|metadata| metadata.modified()) 91 | .ok(); 92 | modified_at > font_file.modified_at 93 | } else { 94 | true 95 | } 96 | }); 97 | 98 | for path in font_paths { 99 | if let Some(font_file) = load_font_file(&path) { 100 | if font_files.insert(path, font_file).is_none() { 101 | added_count += 1; 102 | } else { 103 | updated_count += 1; 104 | } 105 | } 106 | } 107 | 108 | tracing::debug!( 109 | "{count} font files loaded ({added_count} added, {updated_count} updated, {removed_count} removed)", 110 | count = font_files.len(), 111 | ); 112 | } 113 | 114 | pub fn load_font_file(path: impl AsRef) -> Option { 115 | let path = path.as_ref(); 116 | 117 | match FontFile::from_path(path) { 118 | Ok(font_file) => Some(font_file), 119 | Err(FontError::Read(error)) => { 120 | tracing::debug!("Failed to load font file: {path:?}, error: {error:?}"); 121 | None 122 | } 123 | Err(FontError::Parse(errors, font_file)) => { 124 | for (index, error) in errors { 125 | tracing::debug!("Failed to load font file: {path:?} ({index}), error: {error:?}",); 126 | } 127 | font_file 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use axum::{Router, http::HeaderValue, routing::get}; 4 | use figma_agent::{CONFIG, EFFECTIVE_FONT_DIRECTORIES, routes, scan_font_files}; 5 | use listenfd::ListenFd; 6 | use tokio::net::TcpListener; 7 | use tower::ServiceBuilder; 8 | use tower_http::{cors::CorsLayer, trace::TraceLayer}; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<(), anyhow::Error> { 12 | tracing_subscriber::fmt::init(); 13 | 14 | LazyLock::force(&CONFIG); 15 | LazyLock::force(&EFFECTIVE_FONT_DIRECTORIES); 16 | 17 | scan_font_files().await; 18 | 19 | let app = Router::new() 20 | .route("/figma/font-files", get(routes::font_files)) 21 | .route("/figma/font-file", get(routes::font_file)) 22 | .route("/figma/font-preview", get(routes::font_preview)) 23 | .layer( 24 | ServiceBuilder::new() 25 | .layer(TraceLayer::new_for_http()) 26 | .layer( 27 | CorsLayer::new() 28 | .allow_origin(HeaderValue::from_static("https://www.figma.com")) 29 | .allow_private_network(true), 30 | ), 31 | ); 32 | 33 | let listener = match ListenFd::from_env().take_tcp_listener(0)? { 34 | Some(listener) => { 35 | listener.set_nonblocking(true)?; 36 | TcpListener::from_std(listener)? 37 | } 38 | None => TcpListener::bind(&CONFIG.bind).await?, 39 | }; 40 | tracing::info!("Listening on {}", listener.local_addr()?); 41 | 42 | axum::serve(listener, app).await?; 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | #[derive(Debug, thiserror::Error)] 7 | pub enum PathError { 8 | #[error("Failed to get home directory")] 9 | HomeNotDefined, 10 | } 11 | 12 | pub fn expand_home(path: impl AsRef) -> Result { 13 | let path = path.as_ref(); 14 | 15 | if let Ok(path_relative_to_home) = path.strip_prefix("~") { 16 | env::home_dir() 17 | .map(|home_directory| home_directory.join(path_relative_to_home)) 18 | .ok_or(PathError::HomeNotDefined) 19 | } else { 20 | Ok(path.into()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/payload.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, path::PathBuf}; 2 | 3 | #[derive(Debug, Clone, serde::Serialize)] 4 | pub struct FontFilesEndpointPayload { 5 | #[serde(rename = "fontFiles")] 6 | pub font_files: HashMap>, 7 | pub modified_at: Option, 8 | pub modified_fonts: Option>>, 9 | pub package: String, 10 | pub version: u32, 11 | } 12 | 13 | #[derive(Debug, Clone, serde::Serialize)] 14 | pub struct FontPayload { 15 | pub family: String, // Family name 16 | pub style: String, // Subfamily name 17 | pub postscript: String, // PostScript name 18 | pub weight: u16, // Weight (OS/2 usWeightClass) 19 | pub stretch: u16, // Width (OS/2 usWidthClass) 20 | pub italic: bool, 21 | #[serde(rename = "variationAxes", skip_serializing_if = "Option::is_none")] 22 | pub variation_axes: Option>, 23 | pub modified_at: u64, 24 | pub user_installed: bool, 25 | } 26 | 27 | #[derive(Debug, Clone, serde::Serialize)] 28 | pub struct VariationAxisPayload { 29 | pub tag: String, 30 | pub name: String, 31 | pub value: f32, 32 | pub min: f32, 33 | pub max: f32, 34 | pub default: f32, 35 | pub hidden: bool, 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, iter, path::Path}; 2 | 3 | use harfrust::{ShaperData, ShaperInstance, UnicodeBuffer}; 4 | use skrifa::{ 5 | FontRef, GlyphId, MetadataProvider, 6 | instance::Size, 7 | outline::{DrawError, DrawSettings, OutlinePen}, 8 | }; 9 | use svg::{ 10 | Document, 11 | node::element::{ 12 | self, 13 | path::{Command, Position}, 14 | }, 15 | }; 16 | 17 | #[derive(Debug, thiserror::Error)] 18 | pub enum RenderError { 19 | #[error("Failed to read font file")] 20 | Read(#[from] std::io::Error), 21 | #[error("Failed to parse font file")] 22 | Parse(#[from] read_fonts::ReadError), 23 | #[error("Failed to draw glyph")] 24 | Draw(#[from] DrawError), 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub struct RenderOptions<'a> { 29 | pub font: (&'a Path, usize), 30 | pub size: f32, 31 | pub named_instance_index: Option, 32 | } 33 | 34 | pub fn render_text( 35 | text: impl AsRef, 36 | RenderOptions { 37 | font: (font_path, font_index), 38 | size, 39 | named_instance_index, 40 | }: RenderOptions, 41 | ) -> Result, RenderError> { 42 | let data = fs::read(font_path)?; 43 | let font = FontRef::from_index(&data, font_index as u32)?; 44 | 45 | let size = Size::new(size); 46 | let location = named_instance_index 47 | .and_then(|index| font.named_instances().get(index)) 48 | .map(|named_instance| named_instance.location()) 49 | .unwrap_or_default(); 50 | 51 | let metrics = font.metrics(size, &location); 52 | let charmap = font.charmap(); 53 | let outlines = font.outline_glyphs(); 54 | 55 | let text = text.as_ref(); 56 | if text.chars().any(|char| charmap.map(char).is_none()) { 57 | return Ok(None); 58 | } 59 | 60 | let shaper_data = ShaperData::new(&font); 61 | let shaper_instance = named_instance_index 62 | .map(|index| ShaperInstance::from_named_instance(&font, index)) 63 | .unwrap_or_default(); 64 | let shaper = shaper_data 65 | .shaper(&font) 66 | .point_size(size.ppem()) 67 | .instance(Some(&shaper_instance)) 68 | .build(); 69 | 70 | let mut buffer = UnicodeBuffer::new(); 71 | buffer.push_str(text); 72 | buffer.guess_segment_properties(); 73 | 74 | let glyph_buffer = shaper.shape(buffer, &[]); 75 | 76 | let (mut cursor_x, mut cursor_y) = (0.0, metrics.ascent); 77 | let mut text_path = TextPath::new(); 78 | 79 | let scale = size.linear_scale(metrics.units_per_em); 80 | let scale_unit = |unit: i32| unit as f32 * scale; 81 | 82 | for (info, position) in iter::zip(glyph_buffer.glyph_infos(), glyph_buffer.glyph_positions()) { 83 | let glyph = outlines 84 | .get(GlyphId::new(info.glyph_id)) 85 | .ok_or_else(|| DrawError::GlyphNotFound(GlyphId::new(info.glyph_id)))?; 86 | 87 | text_path.origin_x = cursor_x + scale_unit(position.x_offset); 88 | text_path.origin_y = cursor_y + scale_unit(position.y_offset); 89 | 90 | glyph.draw(DrawSettings::unhinted(size, &location), &mut text_path)?; 91 | 92 | cursor_x += scale_unit(position.x_advance); 93 | cursor_y += scale_unit(position.y_advance); 94 | } 95 | 96 | let width = cursor_x; 97 | let height = metrics.ascent - metrics.descent; 98 | 99 | let document = Document::new() 100 | .set("width", width) 101 | .set("height", height) 102 | .set("viewBox", (0.0, 0.0, width, height)) 103 | .add(element::Path::new().set("d", text_path.data)); 104 | 105 | Ok(Some(document.to_string())) 106 | } 107 | 108 | #[derive(Debug, Clone, Default)] 109 | pub struct TextPath { 110 | origin_x: f32, 111 | origin_y: f32, 112 | data: element::path::Data, 113 | } 114 | 115 | impl TextPath { 116 | pub fn new() -> Self { 117 | TextPath::default() 118 | } 119 | } 120 | 121 | impl OutlinePen for TextPath { 122 | fn move_to(&mut self, x: f32, y: f32) { 123 | self.data.append(Command::Move( 124 | Position::Absolute, 125 | (self.origin_x + x, self.origin_y - y).into(), 126 | )); 127 | } 128 | 129 | fn line_to(&mut self, x: f32, y: f32) { 130 | self.data.append(Command::Line( 131 | Position::Absolute, 132 | (self.origin_x + x, self.origin_y - y).into(), 133 | )); 134 | } 135 | 136 | fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { 137 | self.data.append(Command::QuadraticCurve( 138 | Position::Absolute, 139 | ( 140 | self.origin_x + x1, 141 | self.origin_y - y1, 142 | self.origin_x + x, 143 | self.origin_y - y, 144 | ) 145 | .into(), 146 | )); 147 | } 148 | 149 | fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { 150 | self.data.append(Command::CubicCurve( 151 | Position::Absolute, 152 | ( 153 | self.origin_x + x1, 154 | self.origin_y - y1, 155 | self.origin_x + x2, 156 | self.origin_y - y2, 157 | self.origin_x + x, 158 | self.origin_y - y, 159 | ) 160 | .into(), 161 | )); 162 | } 163 | 164 | fn close(&mut self) { 165 | self.data.append(Command::Close); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/routes.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, time::SystemTime}; 2 | 3 | use axum::{ 4 | Json, 5 | extract::{Query, Request}, 6 | http::{StatusCode, header}, 7 | response::IntoResponse, 8 | }; 9 | use tower::ServiceExt; 10 | use tower_http::services::ServeFile; 11 | 12 | use crate::{ 13 | CONFIG, FONT_FILES, 14 | font::{Font, FontFile, FontQuery, FontQueryResult, to_us_weight_class, to_us_width_class}, 15 | payload::{FontFilesEndpointPayload, FontPayload, VariationAxisPayload}, 16 | renderer::{RenderOptions, render_text}, 17 | scan_font_files, 18 | }; 19 | 20 | #[tracing::instrument] 21 | pub async fn font_files() -> impl IntoResponse { 22 | if CONFIG.enable_font_rescan { 23 | scan_font_files().await; 24 | } 25 | 26 | let font_files = FONT_FILES.read().await; 27 | 28 | fn map_font(font: &Font, font_file: &FontFile) -> Vec { 29 | let font_payload = FontPayload { 30 | family: font.family_name.clone().unwrap_or_default(), 31 | style: font.subfamily_name.clone().unwrap_or_default(), 32 | postscript: font.postscript_name.clone().unwrap_or_default(), 33 | weight: to_us_weight_class(font.weight), 34 | stretch: to_us_width_class(font.width), 35 | italic: font.is_italic || font.is_oblique, 36 | variation_axes: if font.axes.is_empty() { 37 | None 38 | } else { 39 | Some( 40 | font.axes 41 | .iter() 42 | .map(|axis| VariationAxisPayload { 43 | tag: axis.tag.clone(), 44 | name: axis.name.clone().unwrap_or_default(), 45 | value: axis.default_value, 46 | min: axis.min_value, 47 | max: axis.max_value, 48 | default: axis.default_value, 49 | hidden: axis.is_hidden, 50 | }) 51 | .collect(), 52 | ) 53 | }, 54 | modified_at: font_file 55 | .modified_at 56 | .and_then(|modified_at| modified_at.duration_since(SystemTime::UNIX_EPOCH).ok()) 57 | .map(|duration| duration.as_secs()) 58 | .unwrap_or_default(), 59 | user_installed: true, 60 | }; 61 | 62 | if font.named_instances.is_empty() { 63 | vec![font_payload] 64 | } else { 65 | font.named_instances 66 | .iter() 67 | .map(|named_instance| { 68 | let mut font_payload = font_payload.clone(); 69 | font_payload.style = named_instance.subfamily_name.clone().unwrap_or_default(); 70 | font_payload.postscript = 71 | named_instance.postscript_name.clone().unwrap_or_default(); 72 | if let Some(variation_axes) = &mut font_payload.variation_axes { 73 | let mut is_italic = font.is_italic; 74 | let mut is_oblique = font.is_oblique; 75 | variation_axes 76 | .iter_mut() 77 | .zip(&named_instance.coordinates) 78 | .for_each(|(variation_axis, coordinate)| { 79 | variation_axis.value = *coordinate; 80 | if variation_axis.tag == "wght" { 81 | font_payload.weight = to_us_weight_class(*coordinate); 82 | } 83 | if variation_axis.tag == "wdth" { 84 | font_payload.stretch = to_us_width_class(*coordinate); 85 | } 86 | if variation_axis.tag == "ital" { 87 | is_italic = *coordinate != 0.0; 88 | } 89 | if variation_axis.tag == "slnt" { 90 | is_oblique = *coordinate != 0.0; 91 | } 92 | }); 93 | font_payload.italic = is_italic || is_oblique; 94 | } 95 | font_payload 96 | }) 97 | .collect() 98 | } 99 | } 100 | 101 | Json(FontFilesEndpointPayload { 102 | font_files: font_files 103 | .iter() 104 | .map(|(path, font_file)| { 105 | ( 106 | path.clone(), 107 | font_file 108 | .fonts 109 | .iter() 110 | .flat_map(|font| map_font(font, font_file)) 111 | .collect(), 112 | ) 113 | }) 114 | .collect(), 115 | modified_at: None, 116 | modified_fonts: None, 117 | package: "125.8.8".into(), 118 | version: 23, 119 | }) 120 | } 121 | 122 | #[derive(Debug, Clone, serde::Deserialize)] 123 | pub struct FontFileQuery { 124 | pub file: PathBuf, 125 | } 126 | 127 | #[tracing::instrument] 128 | pub async fn font_file( 129 | Query(query): Query, 130 | request: Request, 131 | ) -> Result { 132 | let font_files = FONT_FILES.read().await; 133 | 134 | let font_file = font_files.get(&query.file).ok_or_else(|| { 135 | tracing::error!("Font file not found: {path:?}", path = query.file); 136 | StatusCode::NOT_FOUND 137 | })?; 138 | 139 | Ok(ServeFile::new(&font_file.path).oneshot(request).await) 140 | } 141 | 142 | #[derive(Debug, Clone, serde::Deserialize)] 143 | pub struct FontPreviewQuery { 144 | pub file: PathBuf, 145 | pub family: String, 146 | pub style: String, 147 | pub postscript: String, 148 | pub font_size: f32, // Point (pt) 149 | } 150 | 151 | #[tracing::instrument] 152 | pub async fn font_preview( 153 | Query(query): Query, 154 | ) -> Result { 155 | if !CONFIG.enable_font_preview { 156 | return Err(StatusCode::NOT_FOUND); 157 | } 158 | 159 | let font_files = FONT_FILES.read().await; 160 | 161 | let font_file = font_files.get(&query.file).ok_or_else(|| { 162 | tracing::error!("Font file not found: {path:?}", path = query.file); 163 | StatusCode::NOT_FOUND 164 | })?; 165 | 166 | let FontQueryResult { 167 | font, 168 | named_instance, 169 | } = font_file 170 | .query(FontQuery { 171 | family_name: Some(query.family.as_str()).filter(|family| !family.is_empty()), 172 | subfamily_name: Some(query.style.as_str()).filter(|style| !style.is_empty()), 173 | postscript_name: Some(query.postscript.as_str()).filter(|postscript| !postscript.is_empty()), 174 | }) 175 | .ok_or_else(|| { 176 | tracing::error!( 177 | "Font not found: {family_name:?}, subfamily: {subfamily_name:?}, postscript: {postscript_name:?}", 178 | family_name = query.family, 179 | subfamily_name = query.style, 180 | postscript_name = query.postscript, 181 | ); 182 | StatusCode::NOT_FOUND 183 | })?; 184 | 185 | let content = render_text( 186 | &query.family, 187 | RenderOptions { 188 | font: (&font_file.path, font.index), 189 | size: query.font_size / 72.0 * 96.0, 190 | named_instance_index: named_instance.map(|named_instance| named_instance.index), 191 | }, 192 | ) 193 | .map_err(|error| { 194 | tracing::error!("Failed to render font preview, error: {error:?}"); 195 | StatusCode::INTERNAL_SERVER_ERROR 196 | })?; 197 | 198 | if let Some(content) = content { 199 | Ok(([(header::CONTENT_TYPE, "image/svg+xml")], content)) 200 | } else { 201 | Err(StatusCode::NOT_FOUND) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/scanner.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use itertools::Itertools; 4 | use walkdir::WalkDir; 5 | 6 | pub fn scan_font_paths( 7 | directories: impl IntoIterator>, 8 | ) -> impl Iterator { 9 | static FONT_EXTENSIONS: [&str; 4] = ["ttf", "ttc", "otf", "otc"]; 10 | 11 | directories 12 | .into_iter() 13 | .flat_map(|directory| WalkDir::new(directory)) 14 | .filter_map(|entry| match entry { 15 | Ok(entry) => Some(entry), 16 | Err(error) => { 17 | tracing::debug!( 18 | "Skipped font file/directory: {path:?}, error: {error:?}", 19 | path = error.path().unwrap_or_else(|| Path::new("")), 20 | ); 21 | None 22 | } 23 | }) 24 | .filter(|entry| { 25 | entry.file_type().is_file() 26 | && match entry.path().extension() { 27 | Some(extension) => FONT_EXTENSIONS 28 | .iter() 29 | .any(|item| extension.eq_ignore_ascii_case(item)), 30 | None => false, 31 | } 32 | }) 33 | .filter_map(|entry| match entry.path().canonicalize() { 34 | Ok(path) => Some(path), 35 | Err(error) => { 36 | tracing::debug!( 37 | "Skipped font file: {path:?}, error: {error:?}", 38 | path = entry.path(), 39 | ); 40 | None 41 | } 42 | }) 43 | .unique() 44 | } 45 | --------------------------------------------------------------------------------