├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ ├── CI.yaml │ └── lint.yaml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .rustfmt.toml ├── .vscode └── settings.json ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── Readme.md ├── __test__ └── wasm.spec.ts ├── examples ├── AlimamaFangYuanTiVF.ttf ├── OpenSans-Italic.ttf ├── node.mjs └── out.svg ├── index.d.ts ├── index.js ├── package.json ├── src ├── bindings.rs ├── conv.rs ├── conv │ └── woff.rs ├── error.rs ├── font.rs ├── lib.rs ├── metrics.rs ├── metrics │ ├── arabic.rs │ └── compose.rs ├── ras.rs └── wit.rs ├── tests └── basic.rs ├── tsconfig.json └── wit └── world.wit /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: "@typescript-eslint/parser" 2 | 3 | parserOptions: 4 | ecmaFeatures: 5 | jsx: true 6 | ecmaVersion: latest 7 | sourceType: module 8 | 9 | env: 10 | browser: true 11 | es6: true 12 | node: true 13 | jest: true 14 | 15 | plugins: 16 | - import 17 | - sonarjs 18 | 19 | extends: 20 | - eslint:recommended 21 | - plugin:sonarjs/recommended 22 | - plugin:prettier/recommended 23 | 24 | rules: 25 | # 0 = off, 1 = warn, 2 = error 26 | "space-before-function-paren": 0 27 | "semi": [2, "always"] 28 | "no-useless-constructor": 0 29 | "no-undef": 2 30 | "no-console": [2, { allow: ["error", "warn", "info", "assert"] }] 31 | "comma-dangle": ["error", "only-multiline"] 32 | "no-unused-vars": 0 33 | "no-var": 2 34 | "one-var-declaration-per-line": 2 35 | "prefer-const": 2 36 | "no-const-assign": 2 37 | "no-duplicate-imports": 2 38 | "no-use-before-define": [2, { "functions": false, "classes": false }] 39 | "eqeqeq": [2, "always", { "null": "ignore" }] 40 | "no-case-declarations": 0 41 | "no-restricted-syntax": 42 | [ 43 | 2, 44 | { 45 | "selector": "BinaryExpression[operator=/(==|===|!=|!==)/][left.raw=true], BinaryExpression[operator=/(==|===|!=|!==)/][right.raw=true]", 46 | "message": Don't compare for equality against boolean literals, 47 | }, 48 | ] 49 | 50 | # https://github.com/benmosher/eslint-plugin-import/pull/334 51 | "import/no-duplicates": 2 52 | "import/first": 2 53 | "import/newline-after-import": 2 54 | "import/order": 55 | [ 56 | 2, 57 | { 58 | "newlines-between": "always", 59 | "alphabetize": { "order": "asc" }, 60 | "groups": 61 | ["builtin", "external", "internal", "parent", "sibling", "index"], 62 | }, 63 | ] 64 | 65 | "sonarjs/cognitive-complexity": 0 66 | "sonarjs/no-duplicate-string": 0 67 | "sonarjs/no-big-function": 0 68 | "sonarjs/no-identical-functions": 0 69 | "sonarjs/no-small-switch": 0 70 | "prettier/prettier": ["error", { "semi": true }] 71 | -------------------------------------------------------------------------------- /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | env: 4 | DEBUG: napi:* 5 | APP_NAME: font-toolkit 6 | MACOSX_DEPLOYMENT_TARGET: "10.13" 7 | 8 | "on": 9 | push: 10 | branches: 11 | - main 12 | tags-ignore: 13 | - "**" 14 | # 忽略以下文件 15 | paths-ignore: 16 | - "**/*.md" 17 | - "LICENSE" 18 | - "**/*.gitignore" 19 | - ".editorconfig" 20 | - "docs/**" 21 | - "example/*.png" 22 | 23 | pull_request: null 24 | 25 | jobs: 26 | build-wasm: 27 | name: Build and test wasm - node@${{ matrix.node }}(Linux-x64-gnu) 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | node: ["19"] 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | 37 | - name: Setup node 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: ${{ matrix.node }} 41 | check-latest: true 42 | 43 | - name: Install 44 | run: | 45 | rustup target add wasm32-wasi 46 | 47 | - name: Cache cargo 48 | uses: Swatinem/rust-cache@v1 49 | with: 50 | key: ${{ matrix.settings.target }}-node@${{ matrix.node }}-cargo-cache 51 | 52 | - name: Install cargo components 53 | run: | 54 | cargo install cargo-component && yarn global add @bytecodealliance/jco 55 | 56 | - name: Install dependencies 57 | run: yarn install --ignore-scripts --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 58 | 59 | - name: Build wasm 60 | run: yarn build 61 | 62 | - name: Test wasm 63 | run: yarn test 64 | 65 | - name: Upload artifacts 66 | uses: actions/upload-artifact@v2 67 | with: 68 | name: wasm32 69 | path: pkg/*.wasm 70 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - "**" 9 | pull_request: 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Setup node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 18 22 | 23 | - name: Install 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: nightly 28 | override: true 29 | components: rustfmt 30 | 31 | - name: Cache NPM dependencies 32 | uses: actions/cache@v2 33 | with: 34 | path: node_modules 35 | key: npm-cache-lint-node@16-${{ hashFiles('yarn.lock') }} 36 | 37 | - name: "Install dependencies" 38 | run: yarn install --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 39 | 40 | - name: ESLint 41 | run: yarn lint 42 | 43 | - name: Cargo fmt 44 | run: cargo fmt -- --check 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /target 3 | Cargo.lock 4 | /wasm-node 5 | /wasm-bundler 6 | pkg 7 | 8 | # Optional npm cache directory 9 | .npm 10 | 11 | # Optional eslint cache 12 | .eslintcache 13 | 14 | ### Node ### 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | lerna-debug.log* 22 | .pnpm-debug.log* 23 | 24 | *.node 25 | Cargo.lock 26 | 27 | ### macOS ### 28 | # General 29 | .DS_Store 30 | .AppleDouble 31 | .LSOverride 32 | 33 | ### Windows ### 34 | # Windows thumbnail cache files 35 | Thumbs.db 36 | Thumbs.db:encryptable 37 | ehthumbs.db 38 | ehthumbs_vista.db 39 | 40 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | target 2 | npm 3 | examples 4 | 5 | Cargo.lock 6 | .cargo 7 | .github 8 | .eslintrc 9 | .prettierignore 10 | rustfmt.toml 11 | *.node 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | wasm-node 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | unstable_features = true 3 | wrap_comments = true 4 | reorder_impl_items = true 5 | imports_granularity = "module" 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.target": "wasm32-wasip1" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | This changelog also contains important changes in dependencies. 9 | 10 | 11 | ## 0.5.0 12 | 13 | - Emoji support via `FontKit::set_emoji` api 14 | - WASI component support 15 | - npm `0.0.9` -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fontkit" 3 | version = "0.6.0" 4 | edition = "2021" 5 | authors = ["Zimon Dai "] 6 | description = "A simple library for font loading and indexing" 7 | repository = "https://github.com/alibaba/font-toolkit" 8 | license = "MIT OR Apache-2.0" 9 | readme = "Readme.md" 10 | 11 | [lib] 12 | crate-type = ["cdylib", "rlib"] 13 | 14 | [package.metadata.component] 15 | package = "alibaba:fontkit" 16 | 17 | [package.metadata.component.bindings] 18 | derives = ["Clone", "Hash", "PartialEq", "PartialOrd", "Eq", "Default"] 19 | with = { "alibaba:fontkit/commons" = "crate::font" } 20 | 21 | [dependencies] 22 | ab_glyph_rasterizer = { version = "0.1.5", optional = true } 23 | arc-swap = "1.6.0" 24 | brotli-decompressor = { version = "4.0.2", optional = true } 25 | byteorder = { version = "1.4.3", optional = true } 26 | dashmap = "6.1.0" 27 | flate2 = { version = "1.0.22", optional = true } 28 | log = "0.4.17" 29 | ordered-float = "4.6.0" 30 | ouroboros = "0.18.5" 31 | pathfinder_content = { version = "0.5.0", optional = true, default-features = false } 32 | pathfinder_geometry = { version = "0.5.1", optional = true } 33 | pathfinder_simd = { version = "0.5.2", optional = true, features = [ 34 | "pf-no-simd", 35 | ] } 36 | textwrap = { version = "0.16.1", optional = true, default-features = false, features = [ 37 | "smawk", 38 | "unicode-linebreak", 39 | ] } 40 | thiserror = "2.0.11" 41 | tiny-skia-path = "0.11.1" 42 | ttf-parser = "0.25.0" 43 | unicode-bidi = { version = "0.3.7", optional = true } 44 | unicode-normalization = { version = "0.1.19", optional = true } 45 | unicode-script = { version = "0.5.4", optional = true } 46 | woff2-patched = { version = "0.4.0", optional = true } 47 | png = { version = "0.17.13", optional = true } 48 | inflections = "1.1.1" 49 | indexmap = "2.7.1" 50 | serde = { version = "1.0.215", features = ["derive"] } 51 | serde_json = "1.0.133" 52 | talc = "4.4.2" 53 | 54 | [target.'cfg(target_arch = "wasm32")'.dependencies] 55 | wit-bindgen-rt = { version = "0.39.0", optional = true } 56 | resize = { version = "0.8.6", default-features = false, features = ["std"] } 57 | rgb = "0.8.48" 58 | 59 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 60 | fast_image_resize = { version = "5.1.1", optional = true, features = [ 61 | "only_u8x4", 62 | ] } 63 | 64 | [features] 65 | default = ["parse", "metrics", "ras", "wit"] 66 | parse = ["byteorder", "flate2", "woff2-patched"] 67 | metrics = [ 68 | "unicode-bidi", 69 | "unicode-normalization", 70 | "unicode-script", 71 | "textwrap", 72 | ] 73 | ras = [ 74 | "ab_glyph_rasterizer", 75 | "pathfinder_content", 76 | "pathfinder_geometry", 77 | "pathfinder_simd", 78 | "png", 79 | "fast_image_resize", 80 | ] 81 | optimize_stroke_broken = [] 82 | wit = ["wit-bindgen-rt"] 83 | 84 | [patch.crates-io] 85 | pathfinder_simd = { git = "https://github.com/pbdeuchler/pathfinder", branch = "patch-1" } 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alibaba 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 | # fontkit-rs 2 | 3 | GitHub CI Status 4 | fontkit-rs npm version 5 | fontkit-rs downloads 6 | 7 | Toolkit used to load, match, measure and render texts. 8 | 9 | **NOTE: This project is a work in progress. Text measuring and positioning is a complex topic. A more mature library is a sensible choice.** 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm i fontkit-rs 15 | ``` 16 | 17 | ## Compile the module: 18 | 19 | ```bash 20 | npm run build 21 | npm run build:wasi 22 | node examples/node.mjs 23 | ``` 24 | 25 | ### Build WASI 26 | 27 | ```bash 28 | rustup target add wasm32-wasi 29 | npm run build:wasi 30 | ``` 31 | 32 | ## Font querying 33 | 34 | This module uses a special font matching logic. A font's identity contains the font family (name), 35 | weight, italic, and stretch (font width). When querying, any subset of the memtioned information is allowed 36 | except that the font family is required. 37 | 38 | The query is then splitted into 4 filters, in the order of family -> weight -> italic -> stretch. Each filter 39 | could strim the result set, and: 40 | 41 | - If after any filter the result contains only one font, it is immediately returned. 42 | - If after all filters, 0 or more than 1 font are left, the query fails 43 | 44 | ## General API (WASM API) 45 | 46 | ### `new FontKit()` 47 | 48 | Create a new font registry 49 | 50 | ### `fontkit.add_font_from_buffer(buffer: Uint8Array) -> FontKey` 51 | 52 | Add a font from a buffer, return the standard key of this font 53 | 54 | ### `fontkit.query(key: FontKey) -> Font | undefined` 55 | 56 | Query a font 57 | 58 | ### `font.has_glyph(c: char) -> boolean` 59 | 60 | Check if the font contains glyph for a given char 61 | 62 | ### `font.ascender() -> number` 63 | 64 | The ascender metric of the font 65 | 66 | ### `font.descender() -> number` 67 | 68 | The descender metric of the font 69 | 70 | ### `font.units_per_em() -> number` 71 | 72 | The units of metrics of this font 73 | 74 | ### `font.glyph_path(c: char) -> GlyphPath | undefined` 75 | 76 | Get the glyph path of a char, if any 77 | 78 | ### `glyphPath.scale(scaleFactor: number)` 79 | 80 | The glyph of font is very large (by the unit of `units_per_em`), the method could scale the path for convenience 81 | 82 | ### `glyphPath.translate(x: number, y: number)` 83 | 84 | Translate the path 85 | 86 | ### `glyphPath.to_string() -> string` 87 | 88 | Export the glyph path as an SVG path string 89 | 90 | 91 | ## Node.js Support 92 | 93 | Currently this module supports Node.js via `WASI` [API](https://nodejs.org/docs/latest/api/wasi.html). This requires 94 | the `--experimental-wasi-unstable-preview1` flag and only limited APIs are provided. 95 | 96 | ## Example 97 | 98 | ```js 99 | import { dirname } from 'path'; 100 | import { fileURLToPath } from 'url'; 101 | 102 | // Import the Node.js specific entry module 103 | import { FontKitIndex } from '../node.js'; 104 | 105 | // Get current path, or you could use any path containing font files 106 | const __dirname = dirname(fileURLToPath(import.meta.url)); 107 | 108 | // Create an instance of `FontKitIndex`, which is generally a font registry. 109 | // By doing this, an object is created in Rust. So be sure to call `.free()` 110 | // later 111 | const fontkit = new FontKitIndex(); 112 | 113 | // Await here is needed as it initiate the WASM module 114 | await fontkit.initiate(); 115 | 116 | // Add a search path, fonts are recursively indexed. This method could 117 | // be called multiple times 118 | fontkit.addSearchPath(__dirname); 119 | 120 | // Query a font, additional params including weight, stretch, italic are supported 121 | const font = fontkit.font('Open Sans'); 122 | 123 | // Get the actual path of the font 124 | console.log(font.path()); 125 | 126 | // Free the memory of the registry 127 | fontkit.free(); 128 | ``` 129 | 130 | Being a minimal API, after getting the actual path of the font, you could load the file into 131 | a `Uint8Array` buffer, and use the normal `WASM` APIs to load / use the font. So basically the 132 | Node.js API here is only for indexing and querying fonts. 133 | 134 | ## API 135 | 136 | ### `new FontKitIndex()` 137 | 138 | Create a new font registry **only for indexing**. Holding the info (not the actual buffers) of the fonts it found. 139 | 140 | ### `fontkitIndex.addSearchPath(path: string)` 141 | 142 | Search recursively in a path for new fonts. Supports ttf/otf/woff/woff2 fonts. The fonts are 143 | loaded to grab their info, and then immediately released. 144 | 145 | ### `fontkitIndex.query(family: string, weight?=400, italic?=false, strech?=5) -> Font | undefined` 146 | 147 | Query a font. Check _querying_ for details 148 | 149 | ### `fontkitIndex.free()` 150 | 151 | Free the registry. Further calls will panic the program 152 | 153 | ### `font.path() -> string` 154 | 155 | Get the path of the font 156 | -------------------------------------------------------------------------------- /__test__/wasm.spec.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import { dirname, join } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | import test from 'ava'; 6 | 7 | import { FontKit } from '../index.js'; 8 | 9 | const __dirname = dirname(fileURLToPath(import.meta.url)); 10 | 11 | // init wasm 12 | let fontData: Uint8Array | null = null; 13 | 14 | test.before(async () => { 15 | fontData = await fs.readFile(join(__dirname, '../examples/OpenSans-Italic.ttf')); 16 | }); 17 | 18 | test('em box', (t) => { 19 | const fontkit = new FontKit(); 20 | const [key] = fontkit.addFontFromBuffer(fontData!); 21 | const font = fontkit.query(key); 22 | 23 | t.not(font, undefined); 24 | t.is(font!.unitsPerEm(), 2048); 25 | }); 26 | 27 | // test('glyph path to_string()', (t) => { 28 | // const fontkit = new FontKit(); 29 | // const [key] = fontkit.addFontFromBuffer(fontData!); 30 | // const font = fontkit.query(key); 31 | 32 | // t.is( 33 | // font!.('A')!.to_string(), 34 | // 'M 813 2324 L 317 2324 L 72 2789 L -117 2789 L 682 1327 L 856 1327 L 1040 2789 L 870 2789 L 813 2324 z M 795 2168 L 760 1869 Q 736 1690 731 1519 Q 694 1607 650.5 1694 Q 607 1781 401 2168 L 795 2168 z', 35 | // ); 36 | // }); 37 | -------------------------------------------------------------------------------- /examples/AlimamaFangYuanTiVF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/font-toolkit/8ffd2f7af93a24073e22a5d83e1345d5ccc8d4ca/examples/AlimamaFangYuanTiVF.ttf -------------------------------------------------------------------------------- /examples/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/font-toolkit/8ffd2f7af93a24073e22a5d83e1345d5ccc8d4ca/examples/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /examples/node.mjs: -------------------------------------------------------------------------------- 1 | import { homedir } from 'os'; 2 | import { dirname } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | import walk from 'walkdir'; 6 | 7 | import { fontkitInterface as fi } from '../pkg/fontkit.js'; 8 | 9 | const __dirname = dirname(fileURLToPath(import.meta.url)); 10 | 11 | const fontkit = new fi.FontKit(); 12 | // fontkit.addSearchPath(__dirname + '/OpenSans-Italic.ttf'); 13 | // fontkit.addSearchPath(homedir() + '/Library/Fonts'); 14 | walk.sync('./', (path) => { 15 | if (path.endsWith('.ttf') || path.endsWith('.otf') || path.endsWith('.ttc')) { 16 | // console.log(path); 17 | fontkit.addSearchPath(path); 18 | } 19 | }); 20 | 21 | // console.log(fontkit.writeData()); 22 | // const font = fontkit.query({ family: 'Open Sans' }); 23 | 24 | // // eslint-disable-next-line no-console 25 | // console.log(font.hasGlyph('c')); 26 | // // eslint-disable-next-line no-console 27 | // console.log(fontkit.fontsInfo()); 28 | -------------------------------------------------------------------------------- /examples/out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FontKit, 3 | Font, 4 | FontInfo, 5 | FontKey, 6 | AlibabaFontkitFontkitInterface, 7 | GlyphBitmap, 8 | } from './pkg/interfaces/alibaba-fontkit-fontkit-interface'; 9 | 10 | export import strWidthToNumber = AlibabaFontkitFontkitInterface.strWidthToNumber; 11 | export import numberWidthToStr = AlibabaFontkitFontkitInterface.numberWidthToStr; 12 | 13 | declare module './pkg/interfaces/alibaba-fontkit-fontkit-interface' { 14 | interface FontKit { 15 | has: (key: FontKey) => boolean; 16 | } 17 | } 18 | 19 | export { FontKit, Font, FontInfo, FontKey, GlyphBitmap }; 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { fontkitInterface } from './pkg/fontkit.js'; 2 | 3 | fontkitInterface.FontKit.prototype.has = function (key) { 4 | const font = this.exactMatch(key); 5 | const hasFont = !!font; 6 | if (font) font[Symbol.dispose](); 7 | return hasFont; 8 | }; 9 | 10 | export const Font = fontkitInterface.Font; 11 | export const FontKit = fontkitInterface.FontKit; 12 | export const TextMetrics = fontkitInterface.TextMetrics; 13 | export const numberWidthToStr = fontkitInterface.numberWidthToStr; 14 | export const strWidthToNumber = fontkitInterface.strWidthToNumber; 15 | export const GlyphBitmap = fontkitInterface.GlyphBitmap; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fontkit-rs", 3 | "version": "0.0.16-beta.1", 4 | "description": "Toolkit used to load, match, measure, and render texts", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "examples", 8 | "test": "tests" 9 | }, 10 | "files": [ 11 | "pkg/**/*", 12 | "index.js", 13 | "index.d.ts", 14 | "Readme.md" 15 | ], 16 | "author": "Zimon Dai ", 17 | "license": "MIT", 18 | "types": "./index.d.ts", 19 | "scripts": { 20 | "build": "cargo component build --release && jco transpile target/wasm32-wasip1/release/fontkit.wasm -o pkg --no-namespaced-exports -O -- --disable-simd", 21 | "test": "NODE_OPTIONS='--import=ts-node/esm --no-warnings' ava", 22 | "format:rs": "cargo fmt", 23 | "format:source": "prettier --config ./package.json --write './**/*.{js,ts,mjs}'", 24 | "format:yaml": "prettier --parser yaml --write './**/*.{yml,yaml}'", 25 | "lint": "eslint -c ./.eslintrc.yml './**/*.{ts,tsx,js}'", 26 | "lint:fix": "eslint -c ./.eslintrc.yml './**/*.{ts,tsx,js}' --fix" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git@github.com:alibaba/font-toolkit.git" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^18.11.7", 34 | "@typescript-eslint/eslint-plugin": "^5.15.0", 35 | "@typescript-eslint/parser": "^5.15.0", 36 | "ava": "^5.3.1", 37 | "eslint": "^8.11.0", 38 | "eslint-config-prettier": "^8.5.0", 39 | "eslint-plugin-import": "^2.25.4", 40 | "eslint-plugin-prettier": "^4.0.0", 41 | "eslint-plugin-sonarjs": "^0.12.0", 42 | "prettier": "^2.6.0", 43 | "ts-node": "^10.8.1", 44 | "tsx": "^4.2.0", 45 | "typescript": "^4.7.3", 46 | "walkdir": "^0.4.1" 47 | }, 48 | "ava": { 49 | "extensions": { 50 | "ts": "module" 51 | }, 52 | "nodeArguments": [ 53 | "--loader=ts-node/esm" 54 | ], 55 | "environmentVariables": { 56 | "TS_NODE_PROJECT": "./tsconfig.json" 57 | } 58 | }, 59 | "prettier": { 60 | "printWidth": 120, 61 | "semi": false, 62 | "trailingComma": "all", 63 | "singleQuote": true, 64 | "arrowParens": "always", 65 | "parser": "typescript" 66 | }, 67 | "type": "module", 68 | "dependencies": { 69 | "@bytecodealliance/preview2-shim": "^0.17.2" 70 | } 71 | } -------------------------------------------------------------------------------- /src/conv.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "parse")] 2 | pub mod woff; 3 | -------------------------------------------------------------------------------- /src/conv/woff.rs: -------------------------------------------------------------------------------- 1 | //! A pure-Rust converter from WOFF to OTF for display. 2 | //! 3 | //! The `woff2otf` script was used as a reference: `https://github.com/hanikesn/woff2otf` 4 | //! 5 | //! See the WOFF spec: `http://people.mozilla.org/~jkew/woff/woff-spec-latest.html` 6 | //! 7 | //! This code is adopted from: https://github.com/pcwalton/rust-woff 8 | 9 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 10 | use flate2::read::ZlibDecoder; 11 | use std::io::{self, Error, Read, Seek, SeekFrom, Write}; 12 | use std::mem; 13 | 14 | /// "WOFF Header", http://people.mozilla.org/~jkew/woff/woff-spec-latest.html 15 | #[allow(dead_code)] 16 | struct WoffHeader { 17 | signature: u32, 18 | flavor: u32, 19 | length: u32, 20 | num_tables: u16, 21 | reserved: u16, 22 | total_sfnt_size: u32, 23 | major_version: u16, 24 | minor_version: u16, 25 | meta_offset: u32, 26 | meta_length: u32, 27 | meta_orig_length: u32, 28 | priv_offset: u32, 29 | priv_length: u32, 30 | } 31 | 32 | struct OtfHeader { 33 | flavor: u32, 34 | num_tables: u16, 35 | search_range: u16, 36 | entry_selector: u16, 37 | range_shift: u16, 38 | } 39 | 40 | /// "WOFF TableDirectoryEntry", http://people.mozilla.org/~jkew/woff/woff-spec-latest.html 41 | struct WoffTableDirectoryEntry { 42 | tag: u32, 43 | offset: u32, 44 | comp_length: u32, 45 | orig_length: u32, 46 | orig_checksum: u32, 47 | } 48 | 49 | #[repr(C)] 50 | struct OtfTableDirectoryEntry { 51 | tag: u32, 52 | orig_checksum: u32, 53 | offset: u32, 54 | orig_length: u32, 55 | } 56 | 57 | pub fn convert_woff_to_otf(mut woff_reader: R, mut otf_writer: &mut W) -> Result<(), Error> 58 | where 59 | R: Read + Seek, 60 | W: Write + Seek, 61 | { 62 | // Hacker's Delight. 63 | fn previous_power_of_two(mut x: u16) -> u16 { 64 | x |= x >> 1; 65 | x |= x >> 2; 66 | x |= x >> 4; 67 | x |= x >> 8; 68 | x - (x >> 1) 69 | } 70 | 71 | fn tell(stream: &mut S) -> Result 72 | where 73 | S: Seek, 74 | { 75 | stream.seek(SeekFrom::Current(0)) 76 | } 77 | 78 | // Read in headers. 79 | let woff_header = WoffHeader { 80 | signature: woff_reader.read_u32::()?, 81 | flavor: woff_reader.read_u32::()?, 82 | length: woff_reader.read_u32::()?, 83 | num_tables: woff_reader.read_u16::()?, 84 | reserved: woff_reader.read_u16::()?, 85 | total_sfnt_size: woff_reader.read_u32::()?, 86 | major_version: woff_reader.read_u16::()?, 87 | minor_version: woff_reader.read_u16::()?, 88 | meta_offset: woff_reader.read_u32::()?, 89 | meta_length: woff_reader.read_u32::()?, 90 | meta_orig_length: woff_reader.read_u32::()?, 91 | priv_offset: woff_reader.read_u32::()?, 92 | priv_length: woff_reader.read_u32::()?, 93 | }; 94 | 95 | let mut woff_table_directory_entries = Vec::with_capacity(woff_header.num_tables as usize); 96 | for _ in 0..woff_header.num_tables { 97 | woff_table_directory_entries.push(WoffTableDirectoryEntry { 98 | tag: woff_reader.read_u32::()?, 99 | offset: woff_reader.read_u32::()?, 100 | comp_length: woff_reader.read_u32::()?, 101 | orig_length: woff_reader.read_u32::()?, 102 | orig_checksum: woff_reader.read_u32::()?, 103 | }) 104 | } 105 | 106 | // Write out headers. 107 | let num_tables_previous_power_of_two = previous_power_of_two(woff_header.num_tables); 108 | let otf_search_range = num_tables_previous_power_of_two * 16; 109 | let otf_entry_selector = num_tables_previous_power_of_two.trailing_zeros() as u16; 110 | let otf_header = OtfHeader { 111 | flavor: woff_header.flavor, 112 | num_tables: woff_header.num_tables, 113 | search_range: otf_search_range, 114 | entry_selector: otf_entry_selector, 115 | range_shift: woff_header.num_tables * 16 - otf_search_range, 116 | }; 117 | 118 | otf_writer 119 | .write_u32::(otf_header.flavor) 120 | .unwrap(); 121 | otf_writer 122 | .write_u16::(otf_header.num_tables) 123 | .unwrap(); 124 | otf_writer 125 | .write_u16::(otf_header.search_range) 126 | .unwrap(); 127 | otf_writer 128 | .write_u16::(otf_header.entry_selector) 129 | .unwrap(); 130 | otf_writer 131 | .write_u16::(otf_header.range_shift) 132 | .unwrap(); 133 | 134 | let mut otf_table_directory_entries = Vec::new(); 135 | let mut otf_offset = tell(&mut otf_writer)? as u32 136 | + (mem::size_of::() * woff_table_directory_entries.len()) as u32; 137 | for woff_table_directory_entry in &woff_table_directory_entries { 138 | let otf_table_directory_entry = OtfTableDirectoryEntry { 139 | tag: woff_table_directory_entry.tag, 140 | orig_checksum: woff_table_directory_entry.orig_checksum, 141 | offset: otf_offset, 142 | orig_length: woff_table_directory_entry.orig_length, 143 | }; 144 | otf_writer 145 | .write_u32::(otf_table_directory_entry.tag) 146 | .unwrap(); 147 | otf_writer 148 | .write_u32::(otf_table_directory_entry.orig_checksum) 149 | .unwrap(); 150 | otf_writer 151 | .write_u32::(otf_table_directory_entry.offset) 152 | .unwrap(); 153 | otf_writer 154 | .write_u32::(otf_table_directory_entry.orig_length) 155 | .unwrap(); 156 | 157 | otf_offset += otf_table_directory_entry.orig_length; 158 | if otf_offset % 4 != 0 { 159 | otf_offset += 4 - otf_offset % 4 160 | } 161 | 162 | otf_table_directory_entries.push(otf_table_directory_entry); 163 | } 164 | // Decompress data if necessary, and write it out. 165 | for (woff_table_directory_entry, otf_table_directory_entry) in woff_table_directory_entries 166 | .iter() 167 | .zip(otf_table_directory_entries.iter()) 168 | { 169 | debug_assert!(otf_table_directory_entry.offset as u64 == tell(&mut otf_writer)?); 170 | woff_reader.seek(SeekFrom::Start(woff_table_directory_entry.offset as u64))?; 171 | if woff_table_directory_entry.comp_length != woff_table_directory_entry.orig_length { 172 | let decoder = ZlibDecoder::new(woff_reader); 173 | let mut decoder = decoder.take(woff_table_directory_entry.orig_length as u64); 174 | io::copy(&mut decoder, &mut otf_writer)?; 175 | woff_reader = decoder.into_inner().into_inner(); 176 | } else { 177 | let mut limited_woff_reader = 178 | (&mut woff_reader).take(woff_table_directory_entry.orig_length as u64); 179 | io::copy(&mut limited_woff_reader, &mut otf_writer)?; 180 | }; 181 | woff_reader.seek(SeekFrom::Start( 182 | (woff_table_directory_entry.offset + woff_table_directory_entry.comp_length) as u64, 183 | ))?; 184 | 185 | let otf_end_offset = 186 | otf_table_directory_entry.offset + woff_table_directory_entry.orig_length; 187 | if otf_end_offset % 4 != 0 { 188 | let padding = 4 - otf_end_offset % 4; 189 | for _ in 0..padding { 190 | otf_writer.write_all(&[0])? 191 | } 192 | } 193 | } 194 | 195 | Ok(()) 196 | } 197 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::PositionedChar; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum Error { 7 | #[error("Unrecognized buffer")] 8 | UnrecognizedBuffer, 9 | #[error("MIME {0} not supported as a font")] 10 | UnsupportedMIME(&'static str), 11 | #[error("Font doesn't have a proper name")] 12 | EmptyName, 13 | #[error(transparent)] 14 | Parser(#[from] ttf_parser::FaceParsingError), 15 | #[error(transparent)] 16 | Io(#[from] std::io::Error), 17 | #[error("Glyph {c} not found in font")] 18 | GlyphNotFound { c: char }, 19 | #[cfg(feature = "woff2-patched")] 20 | #[error(transparent)] 21 | Woff2(#[from] woff2_patched::decode::DecodeError), 22 | #[error("Metrics mismatch: values {value:?} metrics {metrics:?}")] 23 | MetricsMismatch { 24 | value: Vec, 25 | metrics: Vec, 26 | }, 27 | #[cfg(feature = "png")] 28 | #[error("Color space not support when decoding rastered image, {0:?}")] 29 | PngNotSupported(png::ColorType), 30 | #[cfg(feature = "png")] 31 | #[error(transparent)] 32 | PngDocde(#[from] png::DecodingError), 33 | #[error(transparent)] 34 | Utf8(#[from] std::string::FromUtf8Error), 35 | } 36 | -------------------------------------------------------------------------------- /src/font.rs: -------------------------------------------------------------------------------- 1 | use arc_swap::ArcSwap; 2 | #[cfg(feature = "parse")] 3 | use byteorder::{BigEndian, ReadBytesExt}; 4 | #[cfg(feature = "parse")] 5 | use indexmap::IndexMap; 6 | use ordered_float::OrderedFloat; 7 | use ouroboros::self_referencing; 8 | use std::fmt; 9 | use std::hash::Hash; 10 | #[cfg(feature = "parse")] 11 | use std::io::Read; 12 | use std::path::PathBuf; 13 | use std::sync::atomic::{AtomicU32, Ordering}; 14 | use std::sync::Arc; 15 | pub use ttf_parser::LineMetrics; 16 | use ttf_parser::{Face, Tag, Width as ParserWidth}; 17 | 18 | use crate::{Error, Filter}; 19 | 20 | pub fn str_width_to_number(width: &str) -> u16 { 21 | match width { 22 | "ultra-condensed" => ParserWidth::UltraCondensed, 23 | "condensed" => ParserWidth::Condensed, 24 | "normal" => ParserWidth::Normal, 25 | "expanded" => ParserWidth::Expanded, 26 | "ultra-expanded" => ParserWidth::UltraExpanded, 27 | "extra-condensed" => ParserWidth::ExtraCondensed, 28 | "semi-condensed" => ParserWidth::SemiCondensed, 29 | "semi-expanded" => ParserWidth::SemiExpanded, 30 | "extra-expanded" => ParserWidth::ExtraExpanded, 31 | _ => ParserWidth::Normal, 32 | } 33 | .to_number() 34 | } 35 | 36 | pub fn number_width_to_str(width: u16) -> String { 37 | match width { 38 | 1 => "ultra-condensed", 39 | 2 => "extra-condensed", 40 | 3 => "condensed", 41 | 4 => "semi-condensed", 42 | 5 => "normal", 43 | 6 => "semi-expanded", 44 | 7 => "expanded", 45 | 8 => "extra-expanded", 46 | 9 => "ultra-expanded", 47 | _ => "normal", 48 | } 49 | .to_string() 50 | } 51 | 52 | #[derive(Clone, PartialEq, PartialOrd, Debug, Default, serde::Serialize, serde::Deserialize)] 53 | pub struct FontKey { 54 | /// Font weight, same as CSS [font-weight](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping) 55 | pub weight: Option, 56 | /// Italic or not, boolean 57 | pub italic: Option, 58 | /// Font stretch, same as css [font-stretch](https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch) 59 | pub stretch: Option, 60 | /// Font family string 61 | pub family: String, 62 | pub variations: Vec<(String, f32)>, 63 | } 64 | 65 | impl Eq for FontKey {} 66 | 67 | impl Hash for FontKey { 68 | fn hash(&self, state: &mut H) { 69 | self.weight.hash(state); 70 | self.italic.hash(state); 71 | self.stretch.hash(state); 72 | self.family.hash(state); 73 | self.variations 74 | .iter() 75 | .map(|(s, v)| (s, OrderedFloat(*v))) 76 | .collect::>() 77 | .hash(state); 78 | } 79 | } 80 | 81 | impl FontKey { 82 | pub fn new_with_family(family: String) -> Self { 83 | FontKey { 84 | weight: Some(400), 85 | italic: Some(false), 86 | stretch: Some(5), 87 | family, 88 | variations: vec![], 89 | } 90 | } 91 | } 92 | 93 | impl fmt::Display for FontKey { 94 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 95 | write!( 96 | f, 97 | "FontKey({}, {:?}, {:?}, {:?})", 98 | self.family, self.weight, self.italic, self.stretch 99 | ) 100 | } 101 | } 102 | 103 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] 104 | pub(super) struct Name { 105 | pub id: u16, 106 | pub name: String, 107 | #[allow(unused)] 108 | pub language_id: u16, 109 | } 110 | 111 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] 112 | pub(super) struct FvarInstance { 113 | #[allow(unused)] 114 | pub(super) sub_family: Name, 115 | pub(super) postscript: Name, 116 | } 117 | 118 | /// Returns whether a buffer is WOFF font data. 119 | pub fn is_woff(buf: &[u8]) -> bool { 120 | buf.len() > 4 && buf[0] == 0x77 && buf[1] == 0x4F && buf[2] == 0x46 && buf[3] == 0x46 121 | } 122 | 123 | /// Returns whether a buffer is WOFF2 font data. 124 | pub fn is_woff2(buf: &[u8]) -> bool { 125 | buf.len() > 4 && buf[0] == 0x77 && buf[1] == 0x4F && buf[2] == 0x46 && buf[3] == 0x32 126 | } 127 | 128 | /// Returns whether a buffer is TTF font data. 129 | pub fn is_ttf(buf: &[u8]) -> bool { 130 | buf.len() > 4 131 | && buf[0] == 0x00 132 | && buf[1] == 0x01 133 | && buf[2] == 0x00 134 | && buf[3] == 0x00 135 | && buf[4] == 0x00 136 | } 137 | 138 | /// Returns whether a buffer is OTF font data. 139 | pub fn is_otf(buf: &[u8]) -> bool { 140 | buf.len() > 4 141 | && buf[0] == 0x4F 142 | && buf[1] == 0x54 143 | && buf[2] == 0x54 144 | && buf[3] == 0x4F 145 | && buf[4] == 0x00 146 | } 147 | 148 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 149 | pub(crate) struct VariationData { 150 | pub key: FontKey, 151 | pub names: Vec, 152 | pub variation_names: Vec, 153 | pub style_names: Vec, 154 | pub index: u32, 155 | } 156 | 157 | impl VariationData { 158 | #[cfg(feature = "parse")] 159 | fn parse_buffer_with_index(buffer: &[u8], index: u32) -> Result, Error> { 160 | use ttf_parser::{name_id, Fixed, VariationAxis}; 161 | 162 | let face = Face::parse(buffer, index)?; 163 | let axes: Vec = face 164 | .tables() 165 | .fvar 166 | .map(|v| v.axes.into_iter()) 167 | .into_iter() 168 | .flatten() 169 | .collect::>(); 170 | 171 | // get fvar if any 172 | let mut instances: IndexMap>, Vec> = IndexMap::new(); 173 | if let (Some(_), Some(name_table)) = (face.tables().fvar, face.tables().name) { 174 | // currently ttf-parser is missing `fvar`'s instance records, we parse them 175 | // directly from `RawFace` 176 | let data: &[u8] = face 177 | .raw_face() 178 | .table(ttf_parser::Tag::from_bytes(b"fvar")) 179 | .unwrap(); 180 | let mut raw = &*data; 181 | let _version = raw.read_u32::()?; 182 | let axis_offset = raw.read_u16::()?; 183 | let _ = raw.read_u16::()?; 184 | let axis_count = raw.read_u16::()?; 185 | let axis_size = raw.read_u16::()?; 186 | let instance_count = raw.read_u16::()?; 187 | let instance_size = raw.read_u16::()?; 188 | 189 | let data = &data[(axis_offset as usize + (axis_count as usize * axis_size as usize))..]; 190 | for i in 0..instance_count { 191 | let mut raw = &data[(i as usize * instance_size as usize)..]; 192 | let sub_family_name_id = raw.read_u16::()?; 193 | let _ = raw.read_u16::()?; 194 | let coords = (0..axis_count) 195 | .map(|_| { 196 | use ttf_parser::FromData; 197 | let mut v = [0_u8; 4]; 198 | raw.read_exact(&mut v) 199 | .map(|_| OrderedFloat(Fixed::parse(&v).unwrap().0)) 200 | }) 201 | .collect::, _>>()?; 202 | let postscript_name_id = if raw.is_empty() { 203 | None 204 | } else { 205 | Some(raw.read_u16::()?) 206 | }; 207 | let sub_family = name_table 208 | .names 209 | .into_iter() 210 | .find(|name| name.name_id == sub_family_name_id) 211 | .and_then(|name| { 212 | Some(Name { 213 | id: name.name_id, 214 | name: name.to_string().or_else(|| { 215 | // try to force unicode encoding 216 | Some(std::str::from_utf8(name.name).ok()?.to_string()) 217 | })?, 218 | language_id: name.language_id, 219 | }) 220 | }); 221 | let postscript = name_table 222 | .names 223 | .into_iter() 224 | .find(|name| Some(name.name_id) == postscript_name_id) 225 | .and_then(|name| { 226 | Some(Name { 227 | id: name.name_id, 228 | name: name.to_string().or_else(|| { 229 | // try to force unicode encoding 230 | Some(std::str::from_utf8(name.name).ok()?.to_string()) 231 | })?, 232 | language_id: name.language_id, 233 | }) 234 | }); 235 | if let (Some(sub_family), Some(postscript)) = (sub_family, postscript) { 236 | instances.entry(coords).or_default().push(FvarInstance { 237 | sub_family, 238 | postscript, 239 | }) 240 | } 241 | } 242 | } 243 | let instances = instances 244 | .into_iter() 245 | .map(|(coords, names)| { 246 | return ( 247 | coords.into_iter().map(|v| Fixed(v.0)).collect::>(), 248 | names, 249 | ); 250 | }) 251 | .collect::>(); 252 | let mut style_names = vec![]; 253 | let names = face 254 | .names() 255 | .into_iter() 256 | .filter_map(|name| { 257 | let id = name.name_id; 258 | let mut name_str = name.to_string().or_else(|| { 259 | // try to force unicode encoding 260 | Some(std::str::from_utf8(name.name).ok()?.to_string()) 261 | })?; 262 | if id == name_id::TYPOGRAPHIC_SUBFAMILY { 263 | style_names.push(Name { 264 | id, 265 | name: name_str.clone(), 266 | language_id: name.language_id, 267 | }); 268 | } 269 | if id == name_id::FAMILY 270 | || id == name_id::FULL_NAME 271 | || id == name_id::POST_SCRIPT_NAME 272 | || id == name_id::TYPOGRAPHIC_FAMILY 273 | { 274 | if id == name_id::POST_SCRIPT_NAME { 275 | name_str = name_str.replace(" ", "-"); 276 | } 277 | Some(Name { 278 | id, 279 | name: name_str, 280 | language_id: name.language_id, 281 | }) 282 | } else { 283 | None 284 | } 285 | }) 286 | .collect::>(); 287 | 288 | if names.is_empty() { 289 | return Err(Error::EmptyName); 290 | } 291 | // Select a good name 292 | let ascii_name = names 293 | .iter() 294 | .map(|item| &item.name) 295 | .filter(|name| name.is_ascii() && name.len() > 3) 296 | .min_by(|n1, n2| match n1.len().cmp(&n2.len()) { 297 | std::cmp::Ordering::Equal => n1 298 | .chars() 299 | .filter(|c| *c == '-') 300 | .count() 301 | .cmp(&n2.chars().filter(|c| *c == '-').count()), 302 | ordering @ _ => ordering, 303 | }) 304 | .cloned() 305 | .map(|name| { 306 | if name.starts_with(".") { 307 | (&name[1..]).to_string() 308 | } else { 309 | name 310 | } 311 | }); 312 | let mut results = vec![]; 313 | let key = FontKey { 314 | weight: Some(face.weight().to_number()), 315 | italic: Some(face.is_italic()), 316 | stretch: Some(face.width().to_number()), 317 | family: ascii_name.clone().unwrap_or_else(|| names[0].name.clone()), 318 | variations: vec![], 319 | }; 320 | for (coords, variation_names) in instances { 321 | let mut key = key.clone(); 322 | let width_axis_index = axes 323 | .iter() 324 | .position(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"wdth")); 325 | let weight_axis_index = axes 326 | .iter() 327 | .position(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"wght")); 328 | if let Some(value) = width_axis_index.and_then(|i| coords.get(i)) { 329 | // mapping wdth to usWidthClass, ref: https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_wdth 330 | key.stretch = Some(((value.0 / 100.0) * 5.0).round().min(1.0).max(9.0) as u16); 331 | } 332 | if let Some(value) = weight_axis_index.and_then(|i| coords.get(i)) { 333 | key.weight = Some(value.0 as u16); 334 | } 335 | for (coord, axis) in coords.iter().zip(axes.iter()) { 336 | key.variations 337 | .push((String::from_utf8(axis.tag.to_bytes().to_vec())?, coord.0)); 338 | } 339 | results.push(VariationData { 340 | key, 341 | names: names.clone(), 342 | style_names: style_names.clone(), 343 | variation_names, 344 | index, 345 | }); 346 | } 347 | if results.is_empty() { 348 | // this is not a variable font, add normal font data 349 | results.push(VariationData { 350 | names, 351 | key, 352 | variation_names: vec![], 353 | style_names, 354 | index, 355 | }) 356 | } 357 | Ok(results) 358 | } 359 | 360 | fn is_variable(&self) -> bool { 361 | !self.key.variations.is_empty() 362 | } 363 | 364 | fn fulfils(&self, query: &Filter) -> bool { 365 | match *query { 366 | Filter::Family(name) => { 367 | if self.key.family == name { 368 | return true; 369 | } 370 | if self.names.iter().any(|n| n.name == name) { 371 | return true; 372 | } 373 | if self.is_variable() { 374 | use inflections::Inflect; 375 | return self.variation_names.iter().any(|n| { 376 | n.postscript.name == name 377 | || n.postscript 378 | .name 379 | .replace(&n.sub_family.name, &n.sub_family.name.to_pascal_case()) 380 | == name 381 | }); 382 | } 383 | 384 | false 385 | } 386 | Filter::Italic(i) => self.key.italic.unwrap_or_default() == i, 387 | Filter::Stretch(s) => self.key.stretch.unwrap_or(5) == s, 388 | Filter::Weight(w) => w == 0 || self.key.weight.unwrap_or(400) == w, 389 | Filter::Variations(v) => v.iter().all(|(s, v)| { 390 | self.key 391 | .variations 392 | .iter() 393 | .any(|(ss, sv)| ss == s && v == sv) 394 | }), 395 | } 396 | } 397 | } 398 | 399 | pub(crate) struct Font { 400 | path: Option, 401 | buffer: ArcSwap>, 402 | /// [Font variation](https://learn.microsoft.com/en-us/typography/opentype/spec/fvar) and font collection data 403 | variants: Vec, 404 | hit_counter: Arc, 405 | pub(crate) hit_index: AtomicU32, 406 | } 407 | 408 | impl Font { 409 | pub fn fulfils(&self, query: &Filter) -> bool { 410 | self.variants.iter().any(|v| v.fulfils(query)) 411 | } 412 | 413 | pub fn first_key(&self) -> FontKey { 414 | self.variants[0].key.clone() 415 | } 416 | 417 | pub fn set_path(&mut self, path: PathBuf) { 418 | self.path = Some(path); 419 | } 420 | 421 | #[cfg(feature = "parse")] 422 | pub(super) fn from_buffer( 423 | mut buffer: Vec, 424 | hit_counter: Arc, 425 | ) -> Result { 426 | let mut variants = vec![0]; 427 | if is_otf(&buffer) { 428 | variants = (0..ttf_parser::fonts_in_collection(&buffer).unwrap_or(1)).collect(); 429 | } 430 | #[cfg(feature = "woff2-patched")] 431 | if is_woff2(&buffer) { 432 | buffer = woff2_patched::convert_woff2_to_ttf(&mut buffer.as_slice())?; 433 | } 434 | #[cfg(feature = "parse")] 435 | if is_woff(&buffer) { 436 | use std::io::Cursor; 437 | 438 | let reader = Cursor::new(buffer); 439 | let mut otf_buf = Cursor::new(Vec::new()); 440 | crate::conv::woff::convert_woff_to_otf(reader, &mut otf_buf)?; 441 | buffer = otf_buf.into_inner(); 442 | } 443 | if buffer.is_empty() { 444 | return Err(Error::UnsupportedMIME("unknown")); 445 | } 446 | let variants = variants 447 | .into_iter() 448 | .map(|v| VariationData::parse_buffer_with_index(&buffer, v)) 449 | .collect::, _>>()? 450 | .into_iter() 451 | .flatten() 452 | .collect::>(); 453 | Ok(Font { 454 | path: None, 455 | buffer: ArcSwap::new(Arc::new(buffer)), 456 | variants, 457 | hit_index: AtomicU32::default(), 458 | hit_counter, 459 | }) 460 | } 461 | 462 | pub fn unload(&self) { 463 | if self.path.is_some() { 464 | self.buffer.swap(Arc::default()); 465 | } 466 | } 467 | 468 | pub fn load(&self) -> Result<(), Error> { 469 | if !self.buffer.load().is_empty() { 470 | return Ok(()); 471 | } 472 | let hit_index = self.hit_counter.fetch_add(1, Ordering::SeqCst); 473 | self.hit_index.store(hit_index, Ordering::SeqCst); 474 | #[cfg(feature = "parse")] 475 | if let Some(path) = self.path.as_ref() { 476 | let mut buffer = Vec::new(); 477 | let mut file = std::fs::File::open(path)?; 478 | file.read_to_end(&mut buffer).unwrap(); 479 | 480 | #[cfg(feature = "woff2-patched")] 481 | if is_woff2(&buffer) { 482 | buffer = woff2_patched::convert_woff2_to_ttf(&mut buffer.as_slice())?; 483 | } 484 | #[cfg(feature = "parse")] 485 | if is_woff(&buffer) { 486 | use std::io::Cursor; 487 | 488 | let reader = Cursor::new(buffer); 489 | let mut otf_buf = Cursor::new(Vec::new()); 490 | crate::conv::woff::convert_woff_to_otf(reader, &mut otf_buf)?; 491 | buffer = otf_buf.into_inner(); 492 | } 493 | self.buffer.swap(Arc::new(buffer)); 494 | } 495 | Ok(()) 496 | } 497 | 498 | pub fn path(&self) -> Option<&PathBuf> { 499 | self.path.as_ref() 500 | } 501 | 502 | pub fn face(&self, key: &FontKey) -> Result { 503 | self.load()?; 504 | let buffer = self.buffer.load_full(); 505 | let filters = Filter::from_key(key); 506 | let mut queue = self.variants.iter().collect::>(); 507 | for filter in filters { 508 | let mut q = queue.clone(); 509 | q.retain(|v| v.fulfils(&filter)); 510 | if q.len() == 1 { 511 | queue = q; 512 | break; 513 | } else if q.is_empty() { 514 | break; 515 | } else { 516 | queue = q; 517 | } 518 | } 519 | let variant = queue[0]; 520 | let mut face = StaticFaceTryBuilder { 521 | key: variant.key.clone(), 522 | // path: self.path.clone().unwrap_or_default(), 523 | buffer, 524 | face_builder: |buf| Face::parse(buf, variant.index), 525 | } 526 | .try_build() 527 | .unwrap(); 528 | face.with_face_mut(|face| { 529 | for (coord, axis) in &variant.key.variations { 530 | face.set_variation(Tag::from_bytes_lossy(coord.as_bytes()), *axis); 531 | } 532 | }); 533 | Ok(face) 534 | } 535 | 536 | pub fn variants(&self) -> &[VariationData] { 537 | &self.variants 538 | } 539 | 540 | pub(super) fn new( 541 | path: Option, 542 | variants: Vec, 543 | hit_counter: Arc, 544 | ) -> Self { 545 | Font { 546 | path, 547 | variants, 548 | buffer: ArcSwap::default(), 549 | hit_index: AtomicU32::default(), 550 | hit_counter, 551 | } 552 | } 553 | 554 | pub(super) fn buffer_size(&self) -> usize { 555 | self.buffer.load().len() 556 | } 557 | } 558 | 559 | #[self_referencing] 560 | pub struct StaticFace { 561 | key: FontKey, 562 | pub(crate) buffer: Arc>, 563 | #[borrows(buffer)] 564 | #[not_covariant] 565 | pub(crate) face: Face<'this>, 566 | } 567 | 568 | impl StaticFace { 569 | pub fn has_glyph(&self, c: char) -> bool { 570 | self.with_face(|f| f.glyph_index(c).is_some()) 571 | } 572 | 573 | pub fn ascender(&self) -> i16 { 574 | self.with_face(|f| f.ascender()) 575 | } 576 | 577 | pub fn descender(&self) -> i16 { 578 | self.with_face(|f| f.descender()) 579 | } 580 | 581 | pub fn units_per_em(&self) -> u16 { 582 | self.with_face(|f| f.units_per_em()) 583 | } 584 | 585 | pub fn strikeout_metrics(&self) -> Option { 586 | self.with_face(|f| f.strikeout_metrics()) 587 | } 588 | 589 | pub fn underline_metrics(&self) -> Option { 590 | self.with_face(|f| f.underline_metrics()) 591 | } 592 | 593 | pub fn key(&self) -> FontKey { 594 | self.borrow_key().clone() 595 | } 596 | } 597 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use arc_swap::ArcSwap; 2 | use std::collections::HashSet; 3 | #[cfg(feature = "parse")] 4 | use std::path::Path; 5 | use std::sync::atomic::{AtomicU32, Ordering}; 6 | use std::sync::Arc; 7 | pub use ttf_parser::LineMetrics; 8 | 9 | #[cfg(all(target_arch = "wasm32", feature = "wit"))] 10 | mod bindings; 11 | mod conv; 12 | mod error; 13 | mod font; 14 | #[cfg(feature = "metrics")] 15 | mod metrics; 16 | #[cfg(feature = "ras")] 17 | mod ras; 18 | #[cfg(all(target_arch = "wasm32", feature = "wit"))] 19 | mod wit; 20 | 21 | pub use error::Error; 22 | pub use font::*; 23 | #[cfg(feature = "metrics")] 24 | pub use metrics::*; 25 | #[cfg(feature = "ras")] 26 | pub use ras::*; 27 | pub use tiny_skia_path::{self, PathSegment}; 28 | 29 | #[cfg(all(target_arch = "wasm32", feature = "wit"))] 30 | pub use bindings::exports::alibaba::fontkit::fontkit_interface::TextMetrics; 31 | 32 | #[cfg(target_arch = "wasm32")] 33 | #[global_allocator] 34 | static ALLOCATOR: talc::TalckWasm = unsafe { talc::TalckWasm::new_global() }; 35 | 36 | struct Config { 37 | pub lru_limit: u32, 38 | pub cache_path: Option, 39 | } 40 | 41 | pub struct FontKit { 42 | fonts: dashmap::DashMap, 43 | fallback_font_key: Box Option + Send + Sync>, 44 | pub(crate) config: ArcSwap, 45 | hit_counter: Arc, 46 | } 47 | 48 | impl FontKit { 49 | /// Create a font registry 50 | pub fn new() -> Self { 51 | FontKit { 52 | fonts: dashmap::DashMap::new(), 53 | fallback_font_key: Box::new(|_| None), 54 | config: ArcSwap::new(Arc::new(Config { 55 | lru_limit: 0, 56 | cache_path: None, 57 | })), 58 | hit_counter: Arc::default(), 59 | } 60 | } 61 | 62 | pub fn len(&self) -> usize { 63 | self.fonts.len() 64 | } 65 | 66 | /// Setup a font as fallback. When measure fails, FontKit will use this 67 | /// fallback to measure, if possible 68 | pub fn set_fallback( 69 | &mut self, 70 | callback: impl Fn(font::FontKey) -> Option + Send + Sync + 'static, 71 | ) { 72 | self.fallback_font_key = Box::new(callback); 73 | } 74 | 75 | #[cfg(feature = "metrics")] 76 | pub fn measure(&self, font_key: &font::FontKey, text: &str) -> Option { 77 | let mut used_keys = HashSet::new(); 78 | let mut current_key = font_key.clone(); 79 | let mut current_metrics: Option = None; 80 | used_keys.insert(current_key.clone()); 81 | loop { 82 | match self 83 | .query(¤t_key) 84 | .and_then(|font| font.measure(text).ok()) 85 | { 86 | Some(metrics) => { 87 | if let Some(m) = current_metrics.as_ref() { 88 | m.replace(metrics, true); 89 | } else { 90 | current_metrics = Some(metrics); 91 | } 92 | } 93 | None => {} 94 | } 95 | if !current_metrics 96 | .as_ref() 97 | .map(|m| m.has_missing()) 98 | .unwrap_or(true) 99 | { 100 | break; 101 | } 102 | let callback = self.fallback_font_key.as_ref(); 103 | match (callback)(current_key) { 104 | Some(next_key) => { 105 | if used_keys.contains(&next_key) { 106 | break; 107 | } else { 108 | used_keys.insert(next_key.clone()); 109 | current_key = next_key; 110 | } 111 | } 112 | None => break, 113 | } 114 | } 115 | current_metrics 116 | } 117 | 118 | pub fn remove(&self, key: font::FontKey) { 119 | self.fonts.remove(&key); 120 | } 121 | 122 | pub fn buffer_size(&self) -> usize { 123 | self.fonts 124 | .iter() 125 | .map(|font| font.buffer_size()) 126 | .sum::() 127 | } 128 | 129 | pub fn check_lru(&self) { 130 | let limit = self.config.load().lru_limit as usize * 1024; 131 | if limit == 0 { 132 | return; 133 | } 134 | let mut current_size = self.buffer_size(); 135 | let mut loaded_fonts = self.fonts.iter().filter(|f| f.buffer_size() > 0).count(); 136 | while current_size > limit && loaded_fonts > 1 { 137 | let font = self 138 | .fonts 139 | .iter() 140 | .filter(|f| f.buffer_size() > 0) 141 | .min_by(|a, b| { 142 | b.hit_index 143 | .load(Ordering::SeqCst) 144 | .cmp(&a.hit_index.load(Ordering::SeqCst)) 145 | }); 146 | 147 | let hit_index = font 148 | .as_ref() 149 | .map(|f| f.hit_index.load(Ordering::SeqCst)) 150 | .unwrap_or(0); 151 | if let Some(f) = font { 152 | f.unload(); 153 | } 154 | if current_size == self.buffer_size() { 155 | break; 156 | } 157 | current_size = self.buffer_size(); 158 | self.hit_counter.fetch_sub(hit_index, Ordering::SeqCst); 159 | for f in self.fonts.iter() { 160 | f.hit_index.fetch_sub(hit_index, Ordering::SeqCst); 161 | } 162 | loaded_fonts = self.fonts.iter().filter(|f| f.buffer_size() > 0).count(); 163 | } 164 | } 165 | 166 | /// Add fonts from a buffer. This will load the fonts and store the buffer 167 | /// in FontKit. Type information is inferred from the magic number using 168 | /// `infer` crate. 169 | #[cfg(feature = "parse")] 170 | pub fn add_font_from_buffer(&self, buffer: Vec) -> Result<(), Error> { 171 | use std::fs::File; 172 | use std::io::Write; 173 | use std::path::PathBuf; 174 | use std::str::FromStr; 175 | 176 | let mut font = Font::from_buffer(buffer.clone(), self.hit_counter.clone())?; 177 | let key = font.first_key(); 178 | if let Some(v) = self.fonts.get(&key) { 179 | if let Some(path) = v.path().cloned() { 180 | font.set_path(path); 181 | } 182 | } 183 | let cache_path = self.config.load().cache_path.clone(); 184 | if let Some(mut path) = cache_path.and_then(|p| PathBuf::from_str(&p).ok()) { 185 | if let Some(original_path) = font.path() { 186 | let relative_path = if original_path.is_absolute() 187 | && !std::fs::exists(original_path.clone()).unwrap_or(false) 188 | { 189 | format!(".{}", original_path.display()) 190 | } else { 191 | format!("{}", original_path.display()) 192 | }; 193 | path.push(relative_path); 194 | let mut dir = path.clone(); 195 | dir.pop(); 196 | std::fs::create_dir_all(dir)?; 197 | } else { 198 | path.push(format!( 199 | "{}_{}_{}_{}.ttf", 200 | key.family.replace(['.', ' '], "_"), 201 | key.italic.unwrap_or_default(), 202 | key.stretch.unwrap_or(5), 203 | key.weight.unwrap_or(400) 204 | )); 205 | } 206 | let mut f = File::create(&path)?; 207 | f.write_all(&buffer)?; 208 | font.set_path(path); 209 | font.unload(); 210 | } 211 | self.fonts.insert(key, font); 212 | self.check_lru(); 213 | Ok(()) 214 | } 215 | 216 | /// Recursively scan a local path for fonts, this method will not store the 217 | /// font buffer to reduce memory consumption 218 | #[cfg(feature = "parse")] 219 | pub fn search_fonts_from_path(&self, path: impl AsRef) -> Result<(), Error> { 220 | use std::io::Read; 221 | // if path.as_ref().is_dir() { 222 | // let mut result = vec![]; 223 | // if let Ok(data) = fs::read_dir(path) { 224 | // for entry in data { 225 | // if let Ok(entry) = entry { 226 | // 227 | // result.extend(load_font_from_path(&entry.path()).into_iter().flatten()); 228 | // } 229 | // } 230 | // } 231 | // return Some(result); 232 | // } 233 | 234 | let mut buffer = Vec::new(); 235 | let path = path.as_ref(); 236 | let ext = path 237 | .extension() 238 | .and_then(|s| s.to_str()) 239 | .map(|s| s.to_lowercase()); 240 | let ext = ext.as_deref(); 241 | let ext = match ext { 242 | Some(e) => e, 243 | None => return Ok(()), 244 | }; 245 | match ext { 246 | "ttf" | "otf" | "ttc" | "woff2" | "woff" => { 247 | let mut file = std::fs::File::open(&path).unwrap(); 248 | buffer.clear(); 249 | file.read_to_end(&mut buffer).unwrap(); 250 | let mut font = match Font::from_buffer(buffer, self.hit_counter.clone()) { 251 | Ok(f) => f, 252 | Err(e) => { 253 | log::warn!("Failed loading font {:?}: {:?}", path, e); 254 | return Ok(()); 255 | } 256 | }; 257 | font.set_path(path.to_path_buf()); 258 | font.unload(); 259 | self.fonts.insert(font.first_key(), font); 260 | self.check_lru(); 261 | } 262 | _ => {} 263 | } 264 | Ok(()) 265 | } 266 | 267 | pub fn exact_match(&self, key: &font::FontKey) -> Option { 268 | let face = self.query(key)?; 269 | let mut patched_key = key.clone(); 270 | if patched_key.weight.is_none() { 271 | patched_key.weight = Some(400); 272 | } 273 | if patched_key.stretch.is_none() { 274 | patched_key.stretch = Some(5); 275 | } 276 | if patched_key.italic.is_none() { 277 | patched_key.italic = Some(false); 278 | } 279 | if face.key() == patched_key { 280 | return Some(face); 281 | } else { 282 | return None; 283 | } 284 | } 285 | 286 | pub fn query(&self, key: &font::FontKey) -> Option { 287 | let result = self.fonts.get(&self.query_font(key)?)?.face(key).ok(); 288 | self.check_lru(); 289 | result 290 | } 291 | 292 | pub(crate) fn query_font(&self, key: &font::FontKey) -> Option { 293 | let mut search_results = self 294 | .fonts 295 | .iter() 296 | .map(|item| item.key().clone()) 297 | .collect::>(); 298 | let filters = Filter::from_key(key); 299 | for filter in filters { 300 | let mut s = search_results.clone(); 301 | let is_family = if let Filter::Family(_) = filter { 302 | true 303 | } else { 304 | false 305 | }; 306 | s.retain(|key| { 307 | let font = self.fonts.get(key).unwrap(); 308 | font.fulfils(&filter) 309 | }); 310 | match s.len() { 311 | 1 => return s.iter().next().cloned(), 312 | 0 if is_family => return None, 313 | 0 => {} 314 | _ => search_results = s, 315 | } 316 | } 317 | None 318 | } 319 | 320 | pub fn keys(&self) -> Vec { 321 | self.fonts 322 | .iter() 323 | .flat_map(|i| { 324 | i.value() 325 | .variants() 326 | .iter() 327 | .map(|i| i.key.clone()) 328 | .collect::>() 329 | }) 330 | .collect() 331 | } 332 | } 333 | 334 | enum Filter<'a> { 335 | Family(&'a str), 336 | Italic(bool), 337 | Weight(u16), 338 | Stretch(u16), 339 | Variations(&'a Vec<(String, f32)>), 340 | } 341 | 342 | impl<'a> Filter<'a> { 343 | pub fn from_key(key: &'a FontKey) -> Vec> { 344 | let mut filters = vec![Filter::Family(&key.family)]; 345 | if let Some(italic) = key.italic { 346 | filters.push(Filter::Italic(italic)); 347 | } 348 | if let Some(weight) = key.weight { 349 | filters.push(Filter::Weight(weight)); 350 | } 351 | if let Some(stretch) = key.stretch { 352 | filters.push(Filter::Stretch(stretch)); 353 | } 354 | 355 | filters.push(Filter::Variations(&key.variations)); 356 | 357 | // Fallback weight logic 358 | filters.push(Filter::Weight(0)); 359 | filters 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /src/metrics.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, StaticFace}; 2 | pub use compose::*; 3 | use std::borrow::Cow; 4 | use std::sync::{Arc, RwLock}; 5 | use ttf_parser::{GlyphId, Rect}; 6 | use unicode_bidi::{BidiInfo, Level}; 7 | use unicode_normalization::UnicodeNormalization; 8 | use unicode_script::{Script, ScriptExtension}; 9 | 10 | mod arabic; 11 | mod compose; 12 | 13 | impl StaticFace { 14 | /// Measure a string slice. If certain character is missing, the related 15 | /// [`CharMetrics`] 's `missing` field will be `true` 16 | pub fn measure(&self, text: &str) -> Result { 17 | let mut positions = vec![]; 18 | let mut prev = 0 as char; 19 | let mut value = Cow::Borrowed(text); 20 | let scripts = ScriptExtension::for_str(&*value); 21 | if scripts.contains_script(Script::Arabic) { 22 | value = Cow::Owned(arabic::fix_arabic_ligatures_char(&*value)); 23 | } 24 | let bidi = BidiInfo::new(&value, None); 25 | let (value, levels) = if bidi.has_rtl() { 26 | let value = bidi 27 | .paragraphs 28 | .iter() 29 | .map(|para| { 30 | let line = para.range.clone(); 31 | bidi.reorder_line(para, line) 32 | }) 33 | .collect::>() 34 | .join(""); 35 | let levels = bidi 36 | .paragraphs 37 | .iter() 38 | .flat_map(|para| { 39 | let line = para.range.clone(); 40 | bidi.reordered_levels(para, line).into_iter() 41 | }) 42 | .collect::>(); 43 | (Cow::Owned(value), levels) 44 | } else { 45 | (value, vec![]) 46 | }; 47 | let (height, line_gap) = self.with_face(|f| (f.height(), f.line_gap())); 48 | for (char_code, level) in value.nfc().zip( 49 | levels 50 | .into_iter() 51 | .map(|l| Some(l)) 52 | .chain(std::iter::repeat(None)), 53 | ) { 54 | if char_code == '\n' { 55 | continue; 56 | } 57 | // let direction = if let Some(level) = levels.get(index) { 58 | // if level.is_ltr() { 59 | // TextDirection::LTR 60 | // } else { 61 | // TextDirection::RTL 62 | // } 63 | // } else { 64 | // text.direction 65 | // }; 66 | let m = self.measure_char(char_code).unwrap_or_else(|| CharMetrics { 67 | bbox: Rect { 68 | x_min: 0, 69 | y_min: 0, 70 | x_max: 1, 71 | y_max: 1, 72 | }, 73 | missing: true, 74 | c: char_code, 75 | glyph_id: GlyphId(0), 76 | advanced_x: 0, 77 | lsb: 0, 78 | units: 0.0, 79 | height, 80 | }); 81 | let kerning = self.kerning(prev, char_code).unwrap_or(0); 82 | prev = char_code; 83 | let metrics = PositionedChar { 84 | kerning: kerning as i32, 85 | metrics: m, 86 | level, 87 | }; 88 | positions.push(metrics); 89 | } 90 | 91 | Ok(TextMetrics { 92 | positions: Arc::new(RwLock::new(positions)), 93 | line_gap, 94 | content_height: height, 95 | ascender: self.ascender(), 96 | units: self.units_per_em(), 97 | }) 98 | } 99 | 100 | /// Measure the metrics of a single unicode charactor 101 | pub(crate) fn measure_char(&self, c: char) -> Option { 102 | self.with_face(|f| { 103 | let height = f.height(); 104 | let units = f.units_per_em() as f32; 105 | let glyph_id = f.glyph_index(c)?; 106 | let bbox = f.glyph_bounding_box(glyph_id).or_else(|| { 107 | Some(Rect { 108 | x_min: 0, 109 | y_min: 0, 110 | x_max: f.glyph_hor_advance(glyph_id)? as i16, 111 | y_max: units as i16, 112 | }) 113 | })?; 114 | let lsb = f.glyph_hor_side_bearing(glyph_id).unwrap_or(0); 115 | // ttf-parser added phantom points support, so the original fix is no 116 | // longer needed. Check history for details. 117 | let advanced_x = f.glyph_hor_advance(glyph_id)?; 118 | Some(CharMetrics { 119 | c, 120 | glyph_id, 121 | advanced_x, 122 | bbox, 123 | lsb, 124 | units, 125 | height, 126 | missing: false, 127 | }) 128 | }) 129 | } 130 | 131 | /// Check if there's any kerning data between two charactors, units are 132 | /// handled 133 | fn kerning(&self, prev: char, c: char) -> Option { 134 | self.with_face(|f| { 135 | let pid = f.glyph_index(prev)?; 136 | let cid = f.glyph_index(c)?; 137 | let mut kerning = 0; 138 | for table in f 139 | .tables() 140 | .kern 141 | .into_iter() 142 | .flat_map(|k| k.subtables.into_iter()) 143 | .filter(|st| st.horizontal && !st.variable) 144 | { 145 | if let Some(k) = table.glyphs_kerning(pid, cid) { 146 | kerning = k; 147 | } 148 | } 149 | Some(kerning) 150 | }) 151 | } 152 | } 153 | 154 | #[derive(Debug, Clone, Default)] 155 | pub struct TextMetrics { 156 | pub(crate) positions: Arc>>, 157 | content_height: i16, 158 | ascender: i16, 159 | line_gap: i16, 160 | units: u16, 161 | } 162 | 163 | impl TextMetrics { 164 | #[allow(unused)] 165 | pub fn new(value: String) -> Self { 166 | let mut m = TextMetrics::default(); 167 | let data = value 168 | .nfc() 169 | .map(|c| PositionedChar { 170 | metrics: CharMetrics { 171 | bbox: ttf_parser::Rect { 172 | x_min: 0, 173 | y_min: 0, 174 | x_max: 1, 175 | y_max: 1, 176 | }, 177 | glyph_id: GlyphId(0), 178 | c, 179 | advanced_x: 0, 180 | lsb: 0, 181 | units: 0.0, 182 | height: 0, 183 | missing: true, 184 | }, 185 | kerning: 0, 186 | level: None, 187 | }) 188 | .collect::>(); 189 | m.positions = Arc::new(RwLock::new(data)); 190 | m 191 | } 192 | 193 | pub(crate) fn append(&self, other: Self) { 194 | let mut p = self.positions.write().unwrap(); 195 | let mut other = other.positions.write().unwrap(); 196 | p.append(&mut other); 197 | } 198 | 199 | pub fn count(&self) -> usize { 200 | let p = self.positions.read().unwrap(); 201 | p.len() 202 | } 203 | 204 | pub(crate) fn is_rtl(&self) -> bool { 205 | self.positions 206 | .read() 207 | .map(|p| { 208 | p.iter() 209 | .all(|p| p.level.map(|l| l.is_rtl()).unwrap_or_default()) 210 | }) 211 | .unwrap_or(false) 212 | } 213 | 214 | pub(crate) fn slice(&self, start: u32, count: u32) -> Self { 215 | let start = start as usize; 216 | let count = count as usize; 217 | let positions = { 218 | let p = self.positions.read().unwrap(); 219 | if p.is_empty() { 220 | vec![] 221 | } else { 222 | let start = std::cmp::min(start, p.len() - 1); 223 | let count = std::cmp::min(p.len() - start, count); 224 | (&p[start..(start + count)]).to_vec() 225 | } 226 | }; 227 | TextMetrics { 228 | positions: Arc::new(RwLock::new(positions)), 229 | content_height: self.content_height, 230 | ascender: self.ascender, 231 | line_gap: self.line_gap, 232 | units: self.units, 233 | } 234 | } 235 | 236 | pub(crate) fn replace(&self, other: Self, fallback: bool) { 237 | let mut p = self.positions.write().unwrap(); 238 | let mut other_p = other.positions.write().unwrap(); 239 | let other_p = other_p.split_off(0); 240 | let content_height_factor = self.content_height() as f32 / other.content_height() as f32; 241 | if fallback { 242 | for (c, p) in p.iter_mut().zip(other_p.into_iter()) { 243 | if c.metrics.missing { 244 | *c = p; 245 | c.metrics.mul_factor(content_height_factor); 246 | } 247 | } 248 | } else { 249 | *p = other_p; 250 | for c in p.iter_mut() { 251 | c.metrics.mul_factor(content_height_factor); 252 | } 253 | } 254 | } 255 | 256 | pub fn has_missing(&self) -> bool { 257 | self.positions 258 | .read() 259 | .map(|p| p.iter().any(|c| c.metrics.missing)) 260 | .unwrap_or_default() 261 | } 262 | 263 | pub fn width(&self, font_size: f32, letter_spacing: f32) -> f32 { 264 | self.width_until( 265 | font_size, 266 | letter_spacing, 267 | self.positions.read().map(|p| p.len()).unwrap_or(0), 268 | ) 269 | } 270 | 271 | pub(crate) fn width_until(&self, font_size: f32, letter_spacing: f32, index: usize) -> f32 { 272 | if self.units == 0 { 273 | return 0.0; 274 | } 275 | let positions = self.positions.read().unwrap(); 276 | positions.iter().take(index).fold(0.0, |current, p| { 277 | current + p.kerning as f32 + p.metrics.advanced_x as f32 278 | }) * font_size 279 | / self.units as f32 280 | + letter_spacing * (index as f32) 281 | } 282 | 283 | pub fn width_trim_start(&self, font_size: f32, letter_spacing: f32) -> f32 { 284 | let positions = self.positions.read().unwrap(); 285 | if positions.is_empty() { 286 | return 0.0; 287 | } 288 | self.width(font_size, letter_spacing) 289 | - if positions[0].metrics.c == ' ' { 290 | positions[0].metrics.advanced_x as f32 / positions[0].metrics.units * font_size 291 | } else { 292 | 0.0 293 | } 294 | } 295 | 296 | pub fn height(&self, font_size: f32, line_height: Option) -> f32 { 297 | line_height.map(|h| h * font_size).unwrap_or_else(|| { 298 | let factor = font_size / self.units as f32; 299 | (self.content_height as f32 + self.line_gap as f32) * factor 300 | }) 301 | } 302 | 303 | pub(crate) fn content_height(&self) -> i16 { 304 | self.content_height 305 | } 306 | 307 | pub(crate) fn ascender(&self) -> i16 { 308 | self.ascender 309 | } 310 | 311 | pub(crate) fn line_gap(&self) -> i16 { 312 | self.line_gap 313 | } 314 | 315 | pub fn units(&self) -> u16 { 316 | self.units 317 | } 318 | 319 | pub fn value(&self) -> String { 320 | let rtl = self.is_rtl(); 321 | let positions = self.positions.read().unwrap(); 322 | let iter = positions.iter().map(|p| p.metrics.c); 323 | if rtl { 324 | iter.rev().collect::() 325 | } else { 326 | iter.collect::() 327 | } 328 | } 329 | } 330 | 331 | #[derive(Debug, Clone)] 332 | pub struct PositionedChar { 333 | /// Various metrics data of current character 334 | pub metrics: CharMetrics, 335 | /// Kerning between previous and current character 336 | pub kerning: i32, 337 | pub(crate) level: Option, 338 | } 339 | 340 | /// Metrics for a single unicode charactor in a certain font 341 | #[derive(Debug, Clone, Copy)] 342 | pub struct CharMetrics { 343 | pub(crate) bbox: Rect, 344 | pub(crate) glyph_id: GlyphId, 345 | pub c: char, 346 | pub advanced_x: u16, 347 | pub lsb: i16, 348 | pub units: f32, 349 | pub height: i16, 350 | pub missing: bool, 351 | } 352 | 353 | impl CharMetrics { 354 | pub(crate) fn mul_factor(&mut self, factor: f32) { 355 | self.advanced_x = (self.advanced_x as f32 * factor) as u16; 356 | self.units = self.units * factor; 357 | self.height = (self.height as f32 * factor) as i16; 358 | self.lsb = (self.lsb as f32 * factor) as i16; 359 | self.bbox.x_min = (self.bbox.x_min as f32 * factor) as i16; 360 | self.bbox.x_max = (self.bbox.x_max as f32 * factor) as i16; 361 | self.bbox.y_min = (self.bbox.y_min as f32 * factor) as i16; 362 | self.bbox.y_max = (self.bbox.y_max as f32 * factor) as i16; 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/metrics/arabic.rs: -------------------------------------------------------------------------------- 1 | use unicode_normalization::UnicodeNormalization; 2 | 3 | const FINA: usize = 0; 4 | const INIT: usize = 1; 5 | const MEDI: usize = 2; 6 | const ISO: usize = 3; 7 | 8 | // fina, init, medi, iso 9 | const ARABIC_POSITION: [[u32; 4]; 42] = [ 10 | [0xfe80, 0xfe80, 0xfe80, 0xfe80], // 0x621 11 | [0xfe82, 0xfe81, 0xfe82, 0xfe81], 12 | [0xfe84, 0xfe83, 0xfe84, 0xfe83], 13 | [0xfe86, 0xfe85, 0xfe86, 0xfe85], 14 | [0xfe88, 0xfe87, 0xfe88, 0xfe87], 15 | [0xfe8a, 0xfe8b, 0xfe8c, 0xfe89], 16 | [0xfe8e, 0xfe8d, 0xfe8e, 0xfe8d], 17 | [0xfe90, 0xfe91, 0xfe92, 0xfe8f], // 0x628 18 | [0xfe94, 0xfe93, 0xfe93, 0xfe93], 19 | [0xfe96, 0xfe97, 0xfe98, 0xfe95], // 0x62A 20 | [0xfe9a, 0xfe9b, 0xfe9c, 0xfe99], 21 | [0xfe9e, 0xfe9f, 0xfea0, 0xfe9d], 22 | [0xfea2, 0xfea3, 0xfea4, 0xfea1], 23 | [0xfea6, 0xfea7, 0xfea8, 0xfea5], 24 | [0xfeaa, 0xfea9, 0xfeaa, 0xfea9], 25 | [0xfeac, 0xfeab, 0xfeac, 0xfeab], // 0x630 26 | [0xfeae, 0xfead, 0xfeae, 0xfead], 27 | [0xfeb0, 0xfeaf, 0xfeb0, 0xfeaf], 28 | [0xfeb2, 0xfeb3, 0xfeb4, 0xfeb1], 29 | [0xfeb6, 0xfeb7, 0xfeb8, 0xfeb5], 30 | [0xfeba, 0xfebb, 0xfebc, 0xfeb9], 31 | [0xfebe, 0xfebf, 0xfec0, 0xfebd], 32 | [0xfec2, 0xfec3, 0xfec4, 0xfec1], 33 | [0xfec6, 0xfec7, 0xfec8, 0xfec5], // 0x638 34 | [0xfeca, 0xfecb, 0xfecc, 0xfec9], 35 | [0xfece, 0xfecf, 0xfed0, 0xfecd], //0x63A 36 | [0x63b, 0x63b, 0x63b, 0x63b], 37 | [0x63c, 0x63c, 0x63c, 0x63c], 38 | [0x63d, 0x63d, 0x63d, 0x63d], 39 | [0x63e, 0x63e, 0x63e, 0x63e], 40 | [0x63f, 0x63f, 0x63f, 0x63f], 41 | [0x640, 0x640, 0x640, 0x640], // 0x640 42 | [0xfed2, 0xfed3, 0xfed4, 0xfed1], 43 | [0xfed6, 0xfed7, 0xfed8, 0xfed5], 44 | [0xfeda, 0xfedb, 0xfedc, 0xfed9], 45 | [0xfede, 0xfedf, 0xfee0, 0xfedd], 46 | [0xfee2, 0xfee3, 0xfee4, 0xfee1], 47 | [0xfee6, 0xfee7, 0xfee8, 0xfee5], 48 | [0xfeea, 0xfeeb, 0xfeec, 0xfee9], 49 | [0xfeee, 0xfeed, 0xfeee, 0xfeed], // 0x648 50 | [0xfef0, 0xfeef, 0xfef0, 0xfeef], 51 | [0xfef2, 0xfef3, 0xfef4, 0xfef1], // 0x64A 52 | ]; 53 | 54 | const SPECIAL_BEHIND: [u32; 4] = [0x622, 0x623, 0x625, 0x627]; 55 | 56 | // 0x622,0x623,0x625,0x627 57 | const ARABIC_SPECIAL: [[u32; 2]; 4] = [ 58 | [0xFEF5, 0xFEF6], 59 | [0xFEF7, 0xFEF8], 60 | [0xFEF9, 0xFEFA], 61 | [0xFEFB, 0xFEFC], 62 | ]; 63 | 64 | const ARABIC_FRONT_SET: [u32; 23] = [ 65 | 0x62c, 0x62d, 0x62e, 0x647, 0x639, 0x63a, 0x641, 0x642, 0x62b, 0x635, 0x636, 0x637, 0x643, 66 | 0x645, 0x646, 0x62a, 0x644, 0x628, 0x64a, 0x633, 0x634, 0x638, 0x626, 67 | ]; 68 | 69 | static ARABIC_BEHIND_SET: [u32; 35] = [ 70 | 0x62c, 0x62d, 0x62e, 0x647, 0x639, 0x63a, 0x641, 0x642, 0x62b, 0x635, 0x636, 0x637, 0x643, 71 | 0x645, 0x646, 0x62a, 0x644, 0x628, 0x64a, 0x633, 0x634, 0x638, 0x626, 0x627, 0x623, 0x625, 72 | 0x622, 0x62f, 0x630, 0x631, 0x632, 0x648, 0x624, 0x629, 0x649, 73 | ]; 74 | 75 | #[inline] 76 | fn is_in_front_set(target: &u32) -> bool { 77 | ARABIC_FRONT_SET.contains(target) 78 | } 79 | 80 | #[inline] 81 | fn is_in_behind_set(target: &u32) -> bool { 82 | ARABIC_BEHIND_SET.contains(target) 83 | } 84 | 85 | #[inline] 86 | fn need_ligatures(target: u32) -> bool { 87 | if target >= 0x621 && target <= 0x64A { 88 | true 89 | } else { 90 | false 91 | } 92 | } 93 | 94 | /// we think zero width char will not influence ligatures 95 | #[inline] 96 | fn zero_width_char(target: u32) -> bool { 97 | if target >= 0x610 && target <= 0x61A 98 | || target >= 0x64B && target <= 0x65F 99 | || target >= 0x670 100 | || target >= 0x6D6 && target <= 0x6ED 101 | { 102 | true 103 | } else { 104 | false 105 | } 106 | } 107 | 108 | fn do_ligatures(curr: u32, front: Option, behind: Option) -> u32 { 109 | let front = front.map(|x| x as u32).unwrap_or(0); 110 | let behind = behind.map(|x| x as u32).unwrap_or(0); 111 | 112 | let curr_index = (curr - 0x621) as usize; 113 | if is_in_front_set(&front) && is_in_behind_set(&behind) { 114 | // medi 115 | ARABIC_POSITION[curr_index][MEDI] 116 | } else if is_in_front_set(&front) && !is_in_behind_set(&behind) { 117 | ARABIC_POSITION[curr_index][FINA] 118 | } else if !is_in_front_set(&front) && is_in_behind_set(&behind) { 119 | ARABIC_POSITION[curr_index][INIT] 120 | } else { 121 | ARABIC_POSITION[curr_index][ISO] 122 | } 123 | } 124 | 125 | #[inline] 126 | fn is_special_char(target: u32, behind: Option) -> bool { 127 | let behind = behind.map(|x| x as u32).unwrap_or(0); 128 | target == 0x644 && SPECIAL_BEHIND.contains(&behind) 129 | } 130 | 131 | // 0x622,0x623,0x625,0x627 132 | fn handle_special_char(front: Option, behind: Option) -> u32 { 133 | let front = front.map(|x| x as u32).unwrap_or(0); 134 | let behind = behind.map(|x| x as u32).unwrap_or(0); 135 | 136 | match behind { 137 | 0x622 => { 138 | if is_in_front_set(&front) { 139 | ARABIC_SPECIAL[0][1] 140 | } else { 141 | ARABIC_SPECIAL[0][0] 142 | } 143 | } 144 | 0x623 => { 145 | if is_in_front_set(&front) { 146 | ARABIC_SPECIAL[1][1] 147 | } else { 148 | ARABIC_SPECIAL[1][0] 149 | } 150 | } 151 | 0x625 => { 152 | if is_in_front_set(&front) { 153 | ARABIC_SPECIAL[2][1] 154 | } else { 155 | ARABIC_SPECIAL[2][0] 156 | } 157 | } 158 | 0x627 => { 159 | if is_in_front_set(&front) { 160 | ARABIC_SPECIAL[3][1] 161 | } else { 162 | ARABIC_SPECIAL[3][0] 163 | } 164 | } 165 | _ => 0x644, 166 | } 167 | } 168 | 169 | pub fn fix_arabic_ligatures_char(text: &str) -> String { 170 | let mut res = String::new(); 171 | let mut vowel = String::new(); 172 | let mut iter = text.nfc(); 173 | 174 | let mut front = None; 175 | let mut behind = iter.next(); 176 | let mut curr = behind; 177 | 178 | while curr.is_some() { 179 | if !vowel.is_empty() { 180 | res.push_str(&vowel); 181 | vowel = String::new(); 182 | } 183 | let cha = curr.unwrap(); 184 | let cha_usize = cha as u32; 185 | behind = iter.next(); 186 | // if next is vowel, jump to next and remark this char 187 | if let Some(x) = behind { 188 | if zero_width_char(x as u32) { 189 | vowel.push(x); 190 | behind = iter.next(); 191 | } 192 | } 193 | 194 | if need_ligatures(cha_usize) { 195 | // special ligatures 0x644 196 | if is_special_char(cha_usize, behind) { 197 | res.push( 198 | std::char::from_u32(handle_special_char(front, behind)) 199 | .or_else(|| { 200 | println!("ERROR: arabic char is not exist"); 201 | Some(' ') 202 | }) 203 | .unwrap_or(' '), 204 | ); 205 | curr = behind; 206 | behind = iter.next(); 207 | } else { 208 | res.push( 209 | std::char::from_u32(do_ligatures(cha_usize, front, behind)) 210 | .or_else(|| { 211 | println!("ERROR: arabic char is not exist"); 212 | Some(' ') 213 | }) 214 | .unwrap_or(' '), 215 | ); 216 | } 217 | } else { 218 | res.push(cha); 219 | } 220 | front = curr; 221 | curr = behind; 222 | } 223 | 224 | if !vowel.is_empty() { 225 | res.push_str(&vowel); 226 | } 227 | res 228 | } 229 | -------------------------------------------------------------------------------- /src/metrics/compose.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::sync::{Arc, RwLock}; 3 | 4 | use textwrap::Options; 5 | use textwrap::WordSplitter::NoHyphenation; 6 | use unicode_normalization::UnicodeNormalization; 7 | 8 | use crate::metrics::TextMetrics; 9 | use crate::{Error, FontKey}; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct Line { 13 | pub spans: Vec>, 14 | pub hard_break: bool, 15 | } 16 | 17 | impl Line { 18 | pub fn width(&self) -> f32 { 19 | self.spans 20 | .iter() 21 | .fold(0.0, |current, span| current + span.width()) 22 | } 23 | 24 | pub fn height(&self) -> f32 { 25 | self.spans 26 | .iter() 27 | .fold(0.0, |current, span| current.max(span.height())) 28 | } 29 | 30 | pub fn spans(&self) -> &[Span] { 31 | &self.spans 32 | } 33 | 34 | pub fn new(span: Span) -> Self { 35 | Line { 36 | spans: vec![span], 37 | hard_break: true, 38 | } 39 | } 40 | 41 | fn is_rtl(&self) -> bool { 42 | self.spans.iter().all(|span| span.metrics.is_rtl()) 43 | } 44 | } 45 | 46 | #[derive(Debug, Clone, Default)] 47 | pub struct Span { 48 | pub font_key: FontKey, 49 | pub letter_spacing: f32, 50 | pub line_height: Option, 51 | pub size: f32, 52 | pub broke_from_prev: bool, 53 | pub metrics: M, 54 | pub swallow_leading_space: bool, 55 | pub additional: T, 56 | } 57 | 58 | impl Span { 59 | fn width(&self) -> f32 { 60 | if self.metrics.count() == 0 { 61 | return 0.0; 62 | } 63 | let mut width = self.metrics.width(self.size, self.letter_spacing); 64 | if self.swallow_leading_space { 65 | let metrics = self.metrics.slice(0, 1); 66 | if metrics.value() == "" { 67 | width -= metrics.width(self.size, self.letter_spacing); 68 | } 69 | } 70 | width 71 | } 72 | 73 | fn height(&self) -> f32 { 74 | self.metrics.height(self.size, self.line_height) 75 | } 76 | } 77 | 78 | /// Metrics of an area of rich-content text 79 | #[derive(Debug, Clone, Default)] 80 | pub struct Area { 81 | pub lines: Vec>, 82 | } 83 | 84 | impl Area 85 | where 86 | T: Clone, 87 | { 88 | pub fn new() -> Area { 89 | Area { lines: vec![] } 90 | } 91 | 92 | // The height of a text area 93 | pub fn height(&self) -> f32 { 94 | self.lines 95 | .iter() 96 | .fold(0.0, |current, line| current + line.height()) 97 | } 98 | 99 | /// The width of a text area 100 | pub fn width(&self) -> f32 { 101 | self.lines 102 | .iter() 103 | .fold(0.0_f32, |current, line| current.max(line.width())) 104 | } 105 | 106 | pub fn unwrap_text(&mut self) { 107 | let has_soft_break = self.lines.iter().any(|line| !line.hard_break); 108 | if !has_soft_break { 109 | return; 110 | } 111 | let lines = std::mem::replace(&mut self.lines, Vec::new()); 112 | for line in lines { 113 | if line.hard_break { 114 | self.lines.push(line); 115 | } else { 116 | let last_line = self.lines.last_mut().unwrap(); 117 | let rtl = last_line.is_rtl() && line.is_rtl(); 118 | let last_line = &mut last_line.spans; 119 | for mut span in line.spans { 120 | span.swallow_leading_space = false; 121 | if rtl { 122 | if span.broke_from_prev { 123 | if let Some(first_span) = last_line.first_mut() { 124 | span.metrics.append(first_span.metrics.duplicate()); 125 | std::mem::swap(first_span, &mut span); 126 | } else { 127 | last_line.insert(0, span); 128 | } 129 | } else { 130 | last_line.insert(0, span); 131 | } 132 | } else { 133 | if span.broke_from_prev { 134 | if let Some(last_span) = last_line.last_mut() { 135 | last_span.metrics.append(span.metrics.duplicate()); 136 | } else { 137 | last_line.push(span); 138 | } 139 | } else { 140 | last_line.push(span); 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | pub fn wrap_text(&mut self, width: f32) -> Result<(), Error> { 149 | let rtl = self 150 | .lines 151 | .iter() 152 | .all(|line| line.spans.iter().all(|span| span.metrics.is_rtl())); 153 | let mut lines = self.lines.clone().into_iter().collect::>(); 154 | if rtl { 155 | lines.make_contiguous().reverse(); 156 | } 157 | let mut result = vec![]; 158 | let mut current_line = Line { 159 | hard_break: true, 160 | spans: Vec::new(), 161 | }; 162 | let mut current_line_width = 0.0; 163 | let mut is_first_line = true; 164 | let mut failed_with_no_acception = false; 165 | while let Some(mut line) = lines.pop_front() { 166 | log::trace!( 167 | "current line {}", 168 | line.spans 169 | .iter() 170 | .map(|span| span.metrics.value()) 171 | .collect::>() 172 | .join("") 173 | ); 174 | if line.hard_break && !is_first_line { 175 | // Start a new line 176 | result.push(std::mem::replace( 177 | &mut current_line, 178 | Line { 179 | hard_break: true, 180 | spans: Vec::new(), 181 | }, 182 | )); 183 | current_line_width = 0.0; 184 | } 185 | is_first_line = false; 186 | let line_width = line.width(); 187 | if width - (line_width + current_line_width) >= -0.1 { 188 | // Current line fits, push all of its spans into current line 189 | current_line_width += line_width; 190 | current_line.spans.append(&mut line.spans); 191 | } else { 192 | if rtl { 193 | line.spans.reverse(); 194 | } 195 | // Go through spans to get the first not-fitting span 196 | let index = line.spans.iter().position(|span| { 197 | let span_width = span.width(); 198 | if span_width + current_line_width - width <= 0.1 { 199 | current_line_width += span_width; 200 | false 201 | } else { 202 | true 203 | } 204 | }); 205 | let index = match index { 206 | Some(index) => index, 207 | None => { 208 | // after shrinking letter-spacing, the line fits 209 | current_line_width += line.width(); 210 | current_line.spans.append(&mut line.spans); 211 | continue; 212 | } 213 | }; 214 | // put all spans before this into the line 215 | let mut approved_spans = line.spans.split_off(index); 216 | std::mem::swap(&mut approved_spans, &mut line.spans); 217 | if approved_spans.is_empty() { 218 | if failed_with_no_acception { 219 | // Failed to fit a span twice, fail 220 | return Ok(()); 221 | } else { 222 | failed_with_no_acception = true; 223 | } 224 | } else { 225 | failed_with_no_acception = false; 226 | } 227 | current_line.spans.append(&mut approved_spans); 228 | let span = &mut line.spans[0]; 229 | let new_metrics = span.metrics.split_by_width( 230 | span.size, 231 | span.letter_spacing, 232 | width - current_line_width, 233 | ); 234 | if new_metrics.count() != 0 { 235 | failed_with_no_acception = false; 236 | } 237 | let mut new_span = span.clone(); 238 | new_span.metrics = new_metrics; 239 | new_span.broke_from_prev = true; 240 | if rtl { 241 | std::mem::swap(span, &mut new_span); 242 | } 243 | if span.metrics.count() != 0 { 244 | current_line.spans.push(span.clone()); 245 | } 246 | // Create a new line 247 | result.push(std::mem::replace( 248 | &mut current_line, 249 | Line { 250 | hard_break: false, 251 | spans: Vec::new(), 252 | }, 253 | )); 254 | // Add new_span to next line 255 | let mut new_line = Line { 256 | hard_break: false, 257 | spans: vec![], 258 | }; 259 | if new_span.metrics.count() != 0 { 260 | new_line.spans.push(new_span); 261 | } 262 | for span in line.spans.into_iter().skip(1) { 263 | new_line.spans.push(span); 264 | } 265 | current_line_width = 0.0; 266 | if new_line.spans.is_empty() { 267 | continue; 268 | } 269 | // Check for swallowed leading space 270 | if new_line.spans[0].metrics.value().starts_with(" ") { 271 | new_line.spans[0].swallow_leading_space = true; 272 | } 273 | lines.push_front(new_line); 274 | } 275 | } 276 | if !current_line.spans.is_empty() { 277 | result.push(current_line); 278 | } 279 | if result.is_empty() || result[0].spans.is_empty() { 280 | return Ok(()); 281 | } 282 | self.lines = result; 283 | log::trace!("adjust result: {}", self.value_string()); 284 | Ok(()) 285 | } 286 | 287 | pub fn valid(&self) -> bool { 288 | !self 289 | .lines 290 | .iter() 291 | .any(|line| line.spans.iter().any(|span| span.metrics.units() == 0.0)) 292 | } 293 | 294 | pub fn value_string(&self) -> String { 295 | self.lines 296 | .iter() 297 | .map(|line| { 298 | line.spans 299 | .iter() 300 | .map(|span| span.metrics.value()) 301 | .collect::>() 302 | .join("") 303 | }) 304 | .collect::>() 305 | .join("\n") 306 | } 307 | 308 | pub fn span_count(&self) -> usize { 309 | self.lines 310 | .iter() 311 | .fold(0, |current, line| line.spans.len() + current) 312 | } 313 | 314 | pub fn ellipsis(&mut self, width: f32, height: f32, postfix: M) { 315 | // No need to do ellipsis 316 | if height - self.height() >= -0.01 && width - self.width() >= -0.01 { 317 | return; 318 | } 319 | let mut ellipsis_span = self.lines[0].spans[0].clone(); 320 | let mut lines_height = 0.0; 321 | let index = std::cmp::max( 322 | 1, 323 | self.lines 324 | .iter() 325 | .position(|line| { 326 | lines_height += line.height(); 327 | height - lines_height < -0.01 328 | }) 329 | .unwrap_or(1), 330 | ); 331 | let _ = self.lines.split_off(index); 332 | 333 | for line in &mut self.lines { 334 | line.hard_break = true; 335 | if let Some(ref mut first) = line.spans.first_mut() { 336 | first.metrics.trim_start(); 337 | } 338 | } 339 | 340 | if let Some(ref mut line) = self.lines.last_mut() { 341 | while line.width() + postfix.width(ellipsis_span.size, ellipsis_span.letter_spacing) 342 | - width 343 | >= 0.01 344 | && line.width() > 0.0 345 | { 346 | let span = line.spans.last_mut().unwrap(); 347 | span.metrics.pop(); 348 | if span.metrics.count() == 0 { 349 | line.spans.pop(); 350 | } 351 | } 352 | ellipsis_span.metrics = postfix; 353 | line.spans.push(ellipsis_span); 354 | } 355 | } 356 | } 357 | 358 | pub trait Metrics: Clone { 359 | fn new(value: String) -> Self; 360 | fn duplicate(&self) -> Self; 361 | fn width(&self, font_size: f32, letter_spacing: f32) -> f32; 362 | fn height(&self, font_size: f32, line_height: Option) -> f32; 363 | fn ascender(&self, font_size: f32) -> f32; 364 | fn line_gap(&self) -> f32; 365 | fn slice(&self, start: u32, count: u32) -> Self; 366 | fn value(&self) -> String; 367 | fn units(&self) -> f32; 368 | fn is_rtl(&self) -> bool; 369 | fn append(&self, other: Self); 370 | fn count(&self) -> u32; 371 | /// replace this metrics with another, allowing fallback 372 | /// logic 373 | fn replace(&self, other: Self, fallback: bool); 374 | fn split_by_width(&self, font_size: f32, letter_spacing: f32, width: f32) -> Self; 375 | fn chars(&self) -> Vec; 376 | fn trim_start(&self) { 377 | loop { 378 | let m = self.slice(0, 1); 379 | if m.value() == " " { 380 | self.replace(self.slice(1, self.count() as u32 - 1), false); 381 | } else { 382 | break; 383 | } 384 | } 385 | } 386 | 387 | fn pop(&self) { 388 | self.replace(self.slice(0, self.count() as u32 - 1), false); 389 | } 390 | } 391 | 392 | impl Metrics for TextMetrics { 393 | fn new(value: String) -> Self { 394 | TextMetrics::new(value) 395 | } 396 | 397 | fn duplicate(&self) -> TextMetrics { 398 | self.clone() 399 | } 400 | 401 | fn width(&self, font_size: f32, letter_spacing: f32) -> f32 { 402 | TextMetrics::width(&self, font_size, letter_spacing) 403 | } 404 | 405 | fn height(&self, font_size: f32, line_height: Option) -> f32 { 406 | TextMetrics::height(&self, font_size, line_height) 407 | } 408 | 409 | fn ascender(&self, font_size: f32) -> f32 { 410 | let factor = font_size / self.units() as f32; 411 | (self.ascender() as f32 + self.line_gap() as f32 / 2.0) * factor 412 | } 413 | 414 | fn line_gap(&self) -> f32 { 415 | self.line_gap() as f32 / self.units() as f32 416 | } 417 | 418 | fn slice(&self, start: u32, count: u32) -> TextMetrics { 419 | TextMetrics::slice(&self, start, count) 420 | } 421 | 422 | fn value(&self) -> String { 423 | TextMetrics::value(&self) 424 | } 425 | 426 | fn is_rtl(&self) -> bool { 427 | TextMetrics::is_rtl(&self) 428 | } 429 | 430 | fn append(&self, other: TextMetrics) { 431 | TextMetrics::append(&self, other) 432 | } 433 | 434 | fn count(&self) -> u32 { 435 | TextMetrics::count(&self) as u32 436 | } 437 | 438 | fn replace(&self, other: TextMetrics, fallback: bool) { 439 | TextMetrics::replace(&self, other, fallback) 440 | } 441 | 442 | fn split_by_width(&self, font_size: f32, letter_spacing: f32, width: f32) -> TextMetrics { 443 | TextMetrics::split_by_width(&self, font_size, letter_spacing, width) 444 | } 445 | 446 | fn chars(&self) -> Vec { 447 | let p = self.positions.read().unwrap(); 448 | p.iter().map(|c| c.metrics.c).collect() 449 | } 450 | 451 | fn units(&self) -> f32 { 452 | self.units() as f32 453 | } 454 | } 455 | 456 | impl TextMetrics { 457 | pub(crate) fn split_by_width(&self, font_size: f32, letter_spacing: f32, width: f32) -> Self { 458 | // Try to find a naive break point 459 | let total_count = self.count(); 460 | let mut naive_break_index = total_count; 461 | let rtl = self.is_rtl(); 462 | if rtl { 463 | self.positions.write().unwrap().reverse(); 464 | } 465 | // Textwrap cannot find a good break point, we directly drop chars 466 | loop { 467 | let span_width = self.width_until(font_size, letter_spacing, naive_break_index); 468 | if span_width - width <= 0.1 || naive_break_index == 0 { 469 | break; 470 | } 471 | naive_break_index -= 1; 472 | } 473 | 474 | // NOTE: str.nfc() & textwrap all handles RTL text well, so we do 475 | // not take extra effort here 476 | let positions = self.positions.read().unwrap(); 477 | let positions_rev = positions 478 | .iter() 479 | .take(naive_break_index) 480 | .map(|c| c.metrics.c); 481 | let display_str = if rtl { 482 | positions_rev.rev().collect::() 483 | } else { 484 | positions_rev.collect::() 485 | }; 486 | let display_width = textwrap::core::display_width(&display_str); 487 | let options = Options::new(display_width) 488 | .wrap_algorithm(textwrap::WrapAlgorithm::FirstFit) 489 | .word_splitter(NoHyphenation); 490 | let value = self.value(); 491 | let wrapped = textwrap::wrap(&value, options); 492 | log::trace!("{:?}", wrapped); 493 | let mut real_index = 0; 494 | if rtl { 495 | real_index = total_count - 1; 496 | } 497 | let mut current_line_width = 0.0; 498 | for seg in wrapped { 499 | let count = seg.nfc().count(); 500 | if count == 0 { 501 | continue; 502 | } 503 | let span_values = seg.nfc().collect::>(); 504 | let mut current_real_index = real_index; 505 | while positions[current_real_index].metrics.c == ' ' 506 | && span_values 507 | .get(current_real_index - real_index) 508 | .map(|c| *c != ' ') 509 | .unwrap_or(true) 510 | { 511 | if rtl { 512 | current_real_index -= 1; 513 | } else { 514 | current_real_index += 1; 515 | } 516 | } 517 | let factor = font_size / self.units() as f32; 518 | let range = if rtl { 519 | (current_real_index + 1 - count)..current_real_index 520 | } else { 521 | current_real_index..(current_real_index + count) 522 | }; 523 | let acc_seg_width = 524 | range 525 | .map(|index| positions.get(index).unwrap()) 526 | .fold(0.0, |current, p| { 527 | current 528 | + p.kerning as f32 * factor 529 | + p.metrics.advanced_x as f32 * factor 530 | + letter_spacing 531 | }); 532 | let acc_seg_width_with_space = if current_real_index == real_index { 533 | acc_seg_width 534 | } else { 535 | let range = if rtl { 536 | (current_real_index + 1 - count)..real_index 537 | } else { 538 | real_index..(current_real_index + count) 539 | }; 540 | range 541 | .map(|index| positions.get(index).unwrap()) 542 | .fold(0.0, |current, p| { 543 | current 544 | + p.kerning as f32 * factor 545 | + p.metrics.advanced_x as f32 * factor 546 | + letter_spacing 547 | }) 548 | }; 549 | if current_line_width + acc_seg_width_with_space <= width { 550 | if rtl { 551 | if current_real_index < count { 552 | real_index = 0; 553 | break; 554 | } 555 | real_index = current_real_index - count; 556 | } else { 557 | real_index = current_real_index + count; 558 | } 559 | current_line_width += acc_seg_width; 560 | } else { 561 | break; 562 | } 563 | } 564 | if (real_index == 0 && !rtl) || (rtl && real_index == positions.len() - 1) { 565 | real_index = naive_break_index; 566 | } 567 | 568 | drop(positions); 569 | // Split here, create a new span 570 | let mut new_metrics = self.clone(); 571 | new_metrics.positions = { 572 | let mut p = self.positions.write().unwrap(); 573 | Arc::new(RwLock::new(p.split_off(real_index))) 574 | }; 575 | new_metrics 576 | } 577 | } 578 | -------------------------------------------------------------------------------- /src/ras.rs: -------------------------------------------------------------------------------- 1 | use ab_glyph_rasterizer::{Point as AbPoint, Rasterizer}; 2 | use pathfinder_content::outline::{Contour, ContourIterFlags, Outline}; 3 | #[cfg(feature = "optimize_stroke_broken")] 4 | use pathfinder_content::segment::{Segment, SegmentFlags, SegmentKind}; 5 | use pathfinder_content::stroke::{LineCap, LineJoin, OutlineStrokeToFill, StrokeStyle}; 6 | #[cfg(feature = "optimize_stroke_broken")] 7 | use pathfinder_geometry::line_segment::LineSegment2F; 8 | use pathfinder_geometry::vector::Vector2F; 9 | use tiny_skia_path::PathBuilder as PathData; 10 | use ttf_parser::{OutlineBuilder, Rect}; 11 | 12 | use crate::metrics::CharMetrics; 13 | use crate::*; 14 | 15 | impl StaticFace { 16 | /// Output the outline instructions of a glyph 17 | pub fn outline(&self, c: char) -> Option<(Glyph, Outline)> { 18 | let CharMetrics { 19 | glyph_id, 20 | bbox, 21 | advanced_x, 22 | units, 23 | .. 24 | } = self.measure_char(c)?; 25 | let (builder, outline) = self.with_face(|f| { 26 | let mut builder = PathBuilder::new(); 27 | let outline = f.outline_glyph(glyph_id, &mut builder).unwrap_or(bbox); 28 | builder.finish(); 29 | (builder, outline) 30 | }); 31 | let glyph = Glyph { 32 | units: units as u16, 33 | path: builder.path, 34 | bbox: outline, 35 | advanced_x, 36 | }; 37 | Some((glyph, builder.outline)) 38 | } 39 | 40 | /// Rasterize the outline of a glyph for a certain font_size, and a possible 41 | /// stroke. This method is costy 42 | pub fn bitmap(&self, c: char, font_size: f32, stroke_width: f32) -> Option { 43 | if !self.has_glyph(c) { 44 | return None; 45 | } 46 | self.with_face(|f| { 47 | let a = f.ascender(); 48 | let d = f.descender(); 49 | let units = f.units_per_em() as f32; 50 | let factor = font_size / units; 51 | let glyph_id = f.glyph_index(c)?; 52 | if let Some(bb) = f.glyph_raster_image(glyph_id, 1) { 53 | let advanced_x = f.glyph_hor_advance(glyph_id)? as f32 * factor; 54 | let width = advanced_x; 55 | let height = width * (bb.height as f32 / bb.width as f32); 56 | let width_factor = width as f32 / bb.width as f32; 57 | let height_factor = height as f32 / bb.height as f32; 58 | 59 | let x = bb.x as f32 * width_factor; 60 | let y = bb.y as f32 * height_factor; 61 | 62 | let bbox = ttf_parser::Rect { 63 | x_min: (x / factor) as i16, 64 | y_min: (y / factor) as i16, 65 | x_max: ((x as f32 + width as f32) / factor) as i16, 66 | y_max: ((y as f32 + height as f32) / factor) as i16, 67 | }; 68 | let mut decoder = png::Decoder::new(bb.data); 69 | decoder.set_transformations(png::Transformations::normalize_to_color8()); 70 | decoder.set_ignore_text_chunk(true); 71 | let (bitmap, bitmap_width, bitmap_height) = decoder 72 | .read_info() 73 | .map_err(|e| e.into()) 74 | .and_then(|mut reader| { 75 | let mut buf = vec![0; reader.output_buffer_size()]; 76 | reader.next_frame(&mut buf)?; 77 | let buf: Vec = match reader.output_color_type().0 { 78 | png::ColorType::Rgb => buf 79 | .chunks(3) 80 | .map(|c| [c[0], c[1], c[2], 255].into_iter()) 81 | .flatten() 82 | .collect(), 83 | png::ColorType::Rgba => buf, 84 | png::ColorType::GrayscaleAlpha => buf 85 | .chunks_mut(2) 86 | .map(|c| { 87 | c[0] = ((c[0] as u16 * c[1] as u16) >> 8) as u8; 88 | [c[0], c[0], c[0], c[1]].into_iter() 89 | }) 90 | .flatten() 91 | .collect(), 92 | png::ColorType::Grayscale => buf 93 | .chunks_mut(1) 94 | .map(|c| [c[0], c[0], c[0], 255].into_iter()) 95 | .flatten() 96 | .collect(), 97 | _ => return Err(Error::PngNotSupported(reader.info().color_type)), 98 | }; 99 | Ok((buf, reader.info().width, reader.info().height)) 100 | }) 101 | .ok()?; 102 | // https://docs.rs/fast_image_resize/latest/fast_image_resize/ 103 | let result; 104 | #[cfg(target_arch = "wasm32")] 105 | { 106 | use rgb::FromSlice; 107 | let mut dst = vec![0; width as usize * height as usize * 4]; 108 | let mut resizer = resize::new( 109 | bitmap_width as usize, 110 | bitmap_height as usize, 111 | width as usize, 112 | height as usize, 113 | resize::Pixel::RGBA8, 114 | resize::Type::Lanczos3, 115 | ) 116 | .unwrap(); 117 | let _ = resizer.resize(bitmap.as_rgba(), dst.as_rgba_mut()); 118 | result = dst; 119 | } 120 | #[cfg(not(target_arch = "wasm32"))] 121 | { 122 | use fast_image_resize::images::Image; 123 | use fast_image_resize::{ 124 | FilterType, MulDiv, PixelType, ResizeAlg, ResizeOptions, Resizer, 125 | }; 126 | 127 | let mut image = 128 | Image::from_vec_u8(bitmap_width, bitmap_height, bitmap, PixelType::U8x4) 129 | .unwrap(); 130 | let alpha_mul_div = MulDiv::default(); 131 | alpha_mul_div.multiply_alpha_inplace(&mut image).unwrap(); 132 | let mut resizer = Resizer::new(); 133 | let opts = ResizeOptions::new() 134 | .resize_alg(ResizeAlg::Convolution(FilterType::Bilinear)); 135 | let mut dst = Image::new(width as u32, height as u32, PixelType::U8x4); 136 | resizer.resize(&image, &mut dst, &opts).unwrap(); 137 | alpha_mul_div.divide_alpha_inplace(&mut dst).unwrap(); 138 | result = dst.into_vec(); 139 | } 140 | 141 | Some(GlyphBitmap(GlyphBitmapVariants::Raster(GlyphRasterImage { 142 | width: width as u16, 143 | height: height as u16, 144 | bbox, 145 | factor, 146 | ascender: a as f32 * factor, 147 | descender: d as f32 * factor, 148 | advanced_x, 149 | bitmap: result, 150 | }))) 151 | } else { 152 | let (glyph, outline) = self.outline(c)?; 153 | let advanced_x = glyph.advanced_x as f32 * factor; 154 | let mut width = (glyph.bbox.x_max as f32 * factor).ceil() 155 | - (glyph.bbox.x_min as f32 * factor).floor(); 156 | if width == 0.0 { 157 | width = advanced_x; 158 | } 159 | if width == 0.0 { 160 | width = font_size; 161 | } 162 | let mut height = (glyph.bbox.y_max as f32 * factor).ceil() 163 | - (glyph.bbox.y_min as f32 * factor).floor(); 164 | 165 | let mut stroke_x_min = (glyph.bbox.x_min as f32 * factor).floor(); 166 | let mut stroke_y_max = (glyph.bbox.y_max as f32 * factor).ceil(); 167 | 168 | // try to render stroke 169 | let stroke_bitmap = if stroke_width > 0.0 { 170 | #[cfg(feature = "optimize_stroke_broken")] 171 | let outline = remove_obtuse_angle(&outline); 172 | let mut filler = OutlineStrokeToFill::new( 173 | &outline, 174 | StrokeStyle { 175 | line_width: stroke_width / factor, 176 | line_cap: LineCap::default(), 177 | line_join: LineJoin::Miter(4.0), 178 | }, 179 | ); 180 | filler.offset(); 181 | let outline = filler.into_outline(); 182 | let bounds = outline.bounds(); 183 | let width = 184 | (bounds.max_x() * factor).ceil() - (bounds.min_x() * factor).floor(); 185 | let height = 186 | (bounds.max_y() * factor).ceil() - (bounds.min_y() * factor).floor(); 187 | stroke_x_min = (bounds.origin_x() * factor).floor(); 188 | stroke_y_max = ((bounds.size().y() + bounds.origin_y()) * factor).ceil(); 189 | let mut ras = FontkitRas { 190 | ras: Rasterizer::new(width as usize, height as usize), 191 | factor, 192 | x_min: stroke_x_min, 193 | y_max: stroke_y_max, 194 | prev: None, 195 | start: None, 196 | }; 197 | ras.load_outline(outline); 198 | let mut bitmap = vec![0_u8; width as usize * height as usize]; 199 | ras.ras.for_each_pixel_2d(|x, y, alpha| { 200 | if x < width as u32 && y < height as u32 { 201 | bitmap[((height as u32 - y - 1) * width as u32 + x) as usize] = 202 | (alpha * 255.0) as u8; 203 | } 204 | }); 205 | Some((bitmap, width as u32)) 206 | } else { 207 | None 208 | }; 209 | width = width.ceil(); 210 | height = height.ceil(); 211 | 212 | let mut ras = FontkitRas { 213 | ras: Rasterizer::new(width as usize, height as usize), 214 | factor, 215 | x_min: (glyph.bbox.x_min as f32 * factor).floor(), 216 | y_max: (glyph.bbox.y_max as f32 * factor).ceil(), 217 | prev: None, 218 | start: None, 219 | }; 220 | ras.load_outline(outline); 221 | let mut bitmap = vec![0_u8; width as usize * height as usize]; 222 | ras.ras.for_each_pixel_2d(|x, y, alpha| { 223 | if x < width as u32 && y < height as u32 { 224 | bitmap[((height as u32 - y - 1) * width as u32 + x) as usize] = 225 | (alpha * 255.0) as u8; 226 | } 227 | }); 228 | 229 | Some(GlyphBitmap(GlyphBitmapVariants::Outline( 230 | GlyphRasterOutline { 231 | width: width as u16, 232 | bbox: glyph.bbox, 233 | factor, 234 | ascender: a as f32 * factor, 235 | descender: d as f32 * factor, 236 | advanced_x, 237 | bitmap, 238 | stroke_bitmap, 239 | stroke_x_correction: (glyph.bbox.x_min as f32 * factor).floor() 240 | - stroke_x_min, 241 | stroke_y_correction: stroke_y_max 242 | - (glyph.bbox.y_max as f32 * factor).ceil(), 243 | }, 244 | ))) 245 | } 246 | }) 247 | } 248 | } 249 | 250 | struct PathBuilder { 251 | path: PathData, 252 | outline: Outline, 253 | contour: Contour, 254 | } 255 | 256 | impl PathBuilder { 257 | pub fn new() -> Self { 258 | PathBuilder { 259 | path: PathData::default(), 260 | outline: Outline::new(), 261 | contour: Contour::new(), 262 | } 263 | } 264 | 265 | pub fn finish(&mut self) { 266 | if !self.contour.is_empty() { 267 | self.outline 268 | .push_contour(std::mem::replace(&mut self.contour, Contour::new())); 269 | } 270 | } 271 | } 272 | 273 | impl OutlineBuilder for PathBuilder { 274 | fn move_to(&mut self, x: f32, y: f32) { 275 | self.path.move_to(x, y); 276 | let mut c = Contour::new(); 277 | c.push_endpoint(Vector2F::new(x, y)); 278 | self.contour = c; 279 | } 280 | 281 | fn line_to(&mut self, x: f32, y: f32) { 282 | self.contour.push_endpoint(Vector2F::new(x, y)); 283 | self.path.line_to(x, y); 284 | } 285 | 286 | fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { 287 | self.contour 288 | .push_quadratic(Vector2F::new(x1, y1), Vector2F::new(x, y)); 289 | self.path.quad_to(x1, y1, x, y); 290 | } 291 | 292 | fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { 293 | self.contour.push_cubic( 294 | Vector2F::new(x1, y1), 295 | Vector2F::new(x2, y2), 296 | Vector2F::new(x, y), 297 | ); 298 | self.path.cubic_to(x1, y1, x2, y2, x, y); 299 | } 300 | 301 | fn close(&mut self) { 302 | self.contour.close(); 303 | let c = std::mem::replace(&mut self.contour, Contour::new()); 304 | self.outline.push_contour(c); 305 | self.path.close(); 306 | } 307 | } 308 | 309 | struct FontkitRas { 310 | ras: Rasterizer, 311 | factor: f32, 312 | x_min: f32, 313 | y_max: f32, 314 | prev: Option, 315 | start: Option, 316 | } 317 | 318 | impl FontkitRas { 319 | fn load_outline(&mut self, outline: Outline) { 320 | for contour in outline.into_contours() { 321 | let mut started = false; 322 | for segment in contour.iter(ContourIterFlags::IGNORE_CLOSE_SEGMENT) { 323 | if !started { 324 | let start = segment.baseline.from(); 325 | self.move_to(start.x(), start.y()); 326 | started = true; 327 | } 328 | let to = segment.baseline.to(); 329 | if segment.is_line() { 330 | self.line_to(to.x(), to.y()); 331 | } else if segment.is_quadratic() { 332 | let ctrl = segment.ctrl.from(); 333 | self.quad_to(ctrl.x(), ctrl.y(), to.x(), to.y()); 334 | } else if segment.is_cubic() { 335 | let ctrl1 = segment.ctrl.from(); 336 | let ctrl2 = segment.ctrl.to(); 337 | self.curve_to(ctrl1.x(), ctrl1.y(), ctrl2.x(), ctrl2.y(), to.x(), to.y()); 338 | } 339 | } 340 | if contour.is_closed() { 341 | self.close(); 342 | } 343 | } 344 | } 345 | } 346 | 347 | impl OutlineBuilder for FontkitRas { 348 | fn move_to(&mut self, x: f32, y: f32) { 349 | let p = AbPoint { 350 | x: x * self.factor - self.x_min, 351 | y: self.y_max - y * self.factor, 352 | }; 353 | self.prev = Some(p); 354 | self.start = Some(p); 355 | } 356 | 357 | fn line_to(&mut self, x: f32, y: f32) { 358 | let to = AbPoint { 359 | x: x * self.factor - self.x_min, 360 | y: self.y_max - y * self.factor, 361 | }; 362 | if let Some(prev) = self.prev.take() { 363 | self.ras.draw_line(prev, to); 364 | } 365 | self.prev = Some(to); 366 | } 367 | 368 | fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { 369 | let to = AbPoint { 370 | x: x * self.factor - self.x_min, 371 | y: self.y_max - y * self.factor, 372 | }; 373 | let c = AbPoint { 374 | x: x1 * self.factor - self.x_min, 375 | y: self.y_max - y1 * self.factor, 376 | }; 377 | if let Some(prev) = self.prev.take() { 378 | self.ras.draw_quad(prev, c, to); 379 | } 380 | self.prev = Some(to); 381 | } 382 | 383 | fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { 384 | let to = AbPoint { 385 | x: x * self.factor - self.x_min, 386 | y: self.y_max - y * self.factor, 387 | }; 388 | 389 | let c1 = AbPoint { 390 | x: x1 * self.factor - self.x_min, 391 | y: self.y_max - y1 * self.factor, 392 | }; 393 | let c2 = AbPoint { 394 | x: x2 * self.factor - self.x_min, 395 | y: self.y_max - y2 * self.factor, 396 | }; 397 | if let Some(prev) = self.prev.take() { 398 | self.ras.draw_cubic(prev, c1, c2, to); 399 | } 400 | self.prev = Some(to); 401 | } 402 | 403 | fn close(&mut self) { 404 | if let (Some(a), Some(b)) = (self.start.take(), self.prev.take()) { 405 | self.ras.draw_line(b, a); 406 | } 407 | } 408 | } 409 | 410 | /// The outline of a glyph, with some metrics data 411 | pub struct Glyph { 412 | pub units: u16, 413 | pub path: PathData, 414 | pub bbox: Rect, 415 | pub advanced_x: u16, 416 | } 417 | 418 | #[derive(Clone, Debug)] 419 | enum GlyphBitmapVariants { 420 | Outline(GlyphRasterOutline), 421 | Raster(GlyphRasterImage), 422 | } 423 | 424 | #[derive(Clone, Debug)] 425 | pub struct GlyphBitmap(GlyphBitmapVariants); 426 | 427 | /// Rasterized data of a [Glyph](Glyph) 428 | #[derive(Clone, Debug)] 429 | pub struct GlyphRasterOutline { 430 | width: u16, 431 | bbox: ttf_parser::Rect, 432 | factor: f32, 433 | ascender: f32, 434 | descender: f32, 435 | advanced_x: f32, 436 | bitmap: Vec, 437 | stroke_bitmap: Option<(Vec, u32)>, 438 | stroke_x_correction: f32, 439 | stroke_y_correction: f32, 440 | } 441 | 442 | #[derive(Clone, Debug)] 443 | pub struct GlyphRasterImage { 444 | width: u16, 445 | height: u16, 446 | bbox: ttf_parser::Rect, 447 | factor: f32, 448 | ascender: f32, 449 | descender: f32, 450 | advanced_x: f32, 451 | bitmap: Vec, 452 | } 453 | 454 | impl GlyphBitmap { 455 | pub fn width(&self) -> u32 { 456 | match &self.0 { 457 | GlyphBitmapVariants::Outline(g) => g.width as u32, 458 | GlyphBitmapVariants::Raster(g) => g.width as u32, 459 | } 460 | } 461 | 462 | pub fn height(&self) -> u32 { 463 | match &self.0 { 464 | GlyphBitmapVariants::Outline(g) => g.bitmap.len() as u32 / g.width as u32, 465 | GlyphBitmapVariants::Raster(g) => g.height as u32, 466 | } 467 | } 468 | 469 | pub fn x_min(&self) -> f32 { 470 | match &self.0 { 471 | GlyphBitmapVariants::Outline(g) => g.bbox.x_min as f32 * g.factor, 472 | GlyphBitmapVariants::Raster(g) => g.bbox.x_min as f32 * g.factor, 473 | } 474 | } 475 | 476 | pub fn y_min(&self) -> f32 { 477 | match &self.0 { 478 | GlyphBitmapVariants::Outline(g) => g.bbox.y_min as f32 * g.factor, 479 | GlyphBitmapVariants::Raster(g) => g.bbox.y_min as f32 * g.factor, 480 | } 481 | } 482 | 483 | pub fn x_max(&self) -> f32 { 484 | match &self.0 { 485 | GlyphBitmapVariants::Outline(g) => g.bbox.x_max as f32 * g.factor, 486 | GlyphBitmapVariants::Raster(g) => g.bbox.x_max as f32 * g.factor, 487 | } 488 | } 489 | 490 | pub fn y_max(&self) -> f32 { 491 | match &self.0 { 492 | GlyphBitmapVariants::Outline(g) => g.bbox.y_max as f32 * g.factor, 493 | GlyphBitmapVariants::Raster(g) => g.bbox.y_max as f32 * g.factor, 494 | } 495 | } 496 | 497 | pub fn advanced_x(&self) -> f32 { 498 | match &self.0 { 499 | GlyphBitmapVariants::Outline(g) => g.advanced_x, 500 | GlyphBitmapVariants::Raster(g) => g.advanced_x, 501 | } 502 | } 503 | 504 | pub fn ascender(&self) -> f32 { 505 | match &self.0 { 506 | GlyphBitmapVariants::Outline(g) => g.ascender, 507 | GlyphBitmapVariants::Raster(g) => g.ascender, 508 | } 509 | } 510 | 511 | pub fn descender(&self) -> f32 { 512 | match &self.0 { 513 | GlyphBitmapVariants::Outline(g) => g.descender, 514 | GlyphBitmapVariants::Raster(g) => g.descender, 515 | } 516 | } 517 | 518 | pub fn stroke_x(&self) -> f32 { 519 | match &self.0 { 520 | GlyphBitmapVariants::Outline(g) => g.stroke_x_correction, 521 | GlyphBitmapVariants::Raster(_) => 0.0, 522 | } 523 | } 524 | 525 | pub fn stroke_y(&self) -> f32 { 526 | match &self.0 { 527 | GlyphBitmapVariants::Outline(g) => g.stroke_y_correction, 528 | GlyphBitmapVariants::Raster(_) => 0.0, 529 | } 530 | } 531 | 532 | pub fn bitmap(&self) -> &Vec { 533 | match &self.0 { 534 | GlyphBitmapVariants::Outline(g) => &g.bitmap, 535 | GlyphBitmapVariants::Raster(g) => &g.bitmap, 536 | } 537 | } 538 | 539 | pub fn stroke_bitmap(&self) -> Option<(&Vec, u32)> { 540 | match &self.0 { 541 | GlyphBitmapVariants::Outline(g) => { 542 | let (bitmap, width) = g.stroke_bitmap.as_ref()?; 543 | Some((bitmap, *width)) 544 | } 545 | GlyphBitmapVariants::Raster(_) => None, 546 | } 547 | } 548 | } 549 | 550 | #[cfg(feature = "optimize_stroke_broken")] 551 | fn calc_distance(p1: Vector2F, p2: Vector2F) -> f32 { 552 | ((p1.x() - p2.x()).powi(2) + (p1.y() - p2.y()).powi(2)).sqrt() 553 | } 554 | 555 | #[cfg(feature = "optimize_stroke_broken")] 556 | fn remove_obtuse_angle(outline: &Outline) -> Outline { 557 | let mut segments: Vec = vec![]; 558 | let mut head_index: usize = 0; 559 | for contour in outline.contours() { 560 | for (index, segment) in contour 561 | .iter(ContourIterFlags::IGNORE_CLOSE_SEGMENT) 562 | .enumerate() 563 | { 564 | if index == 0 { 565 | head_index = segments.len(); 566 | segments.push(Segment { 567 | baseline: segment.baseline, 568 | ctrl: segment.ctrl, 569 | kind: SegmentKind::None, 570 | flags: SegmentFlags::FIRST_IN_SUBPATH, 571 | }); 572 | } 573 | let from = segment.baseline.from(); 574 | let to = segment.baseline.to(); 575 | if segment.is_quadratic() { 576 | let ctrl = segment.ctrl.from(); 577 | let d = segment.baseline.square_length().sqrt(); 578 | let d1 = calc_distance(ctrl, from); 579 | let d2 = calc_distance(ctrl, to); 580 | if d1 <= 10.0 || d2 <= 10.0 { 581 | let mut cos = (d1 * d1 + d * d - d2 * d2) / 2.0 * d1 * d; 582 | if cos > 0.0 { 583 | cos = (d2 * d2 + d * d - d1 * d1) / 2.0 * d2 * d; 584 | } 585 | if cos <= 0.0 { 586 | segments.push(Segment::line(LineSegment2F::new(from, to))); 587 | continue; 588 | } 589 | } 590 | } 591 | if segment.is_cubic() { 592 | // TODO 593 | } 594 | segments.push(segment) 595 | } 596 | let mut last_seg = segments.last().unwrap().clone(); 597 | let first_seg_pos = segments[head_index].baseline.from(); 598 | if last_seg.kind == SegmentKind::Line && first_seg_pos == last_seg.baseline.to() { 599 | segments.pop(); 600 | } 601 | last_seg.flags = SegmentFlags::CLOSES_SUBPATH; 602 | segments.push(last_seg); 603 | } 604 | Outline::from_segments(segments.into_iter()) 605 | } 606 | -------------------------------------------------------------------------------- /src/wit.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::Arc; 3 | 4 | use crate::bindings::exports::alibaba::fontkit::fontkit_interface as fi; 5 | use crate::font::FontKey; 6 | use crate::metrics::TextMetrics; 7 | use crate::{Config, Font, FontKit, GlyphBitmap, StaticFace, VariationData}; 8 | 9 | use crate::bindings::exports::alibaba::fontkit::fontkit_interface::GuestTextMetrics; 10 | 11 | impl fi::GuestFont for StaticFace { 12 | fn has_glyph(&self, c: char) -> bool { 13 | self.has_glyph(c) 14 | } 15 | 16 | fn buffer(&self) -> Vec { 17 | self.borrow_buffer().to_vec() 18 | } 19 | 20 | fn path(&self) -> String { 21 | String::new() 22 | } 23 | 24 | fn key(&self) -> fi::FontKey { 25 | self.key().into() 26 | } 27 | 28 | fn measure(&self, text: String) -> Result { 29 | Ok(fi::TextMetrics::new( 30 | self.measure(&text).map_err(|e| e.to_string())?, 31 | )) 32 | } 33 | 34 | fn ascender(&self) -> i16 { 35 | self.ascender() 36 | } 37 | 38 | fn descender(&self) -> i16 { 39 | self.descender() 40 | } 41 | 42 | fn units_per_em(&self) -> u16 { 43 | self.units_per_em() 44 | } 45 | 46 | fn bitmap(&self, c: char, font_size: f32, stroke_width: f32) -> Option { 47 | Some(fi::GlyphBitmap::new(self.bitmap( 48 | c, 49 | font_size, 50 | stroke_width, 51 | )?)) 52 | } 53 | 54 | fn underline_metrics(&self) -> Option { 55 | let m = self.underline_metrics()?; 56 | Some(fi::LineMetrics { 57 | position: m.position, 58 | thickness: m.thickness, 59 | }) 60 | } 61 | 62 | fn glyph_path_string(&self, c: char) -> Option { 63 | use pathfinder_geometry::transform2d::Transform2F; 64 | use pathfinder_geometry::vector::Vector2F; 65 | 66 | let (_, mut outline) = self.outline(c)?; 67 | let height = self.ascender() as f32 - self.descender() as f32; 68 | outline.transform(&Transform2F::from_scale(Vector2F::new(1.0, -1.0))); 69 | outline.transform(&Transform2F::from_translation(Vector2F::new(0.0, height))); 70 | Some(format!("{:?}", outline)) 71 | } 72 | } 73 | 74 | impl fi::GuestGlyphBitmap for GlyphBitmap { 75 | fn width(&self) -> u32 { 76 | self.width() 77 | } 78 | 79 | fn height(&self) -> u32 { 80 | self.height() 81 | } 82 | 83 | fn bitmap(&self) -> Vec { 84 | self.bitmap().clone() 85 | } 86 | 87 | fn ascender(&self) -> f32 { 88 | self.ascender() 89 | } 90 | 91 | fn descender(&self) -> f32 { 92 | self.descender() 93 | } 94 | 95 | fn advanced_x(&self) -> f32 { 96 | self.advanced_x() 97 | } 98 | 99 | fn x_min(&self) -> f32 { 100 | self.x_min() 101 | } 102 | 103 | fn y_max(&self) -> f32 { 104 | self.y_max() 105 | } 106 | 107 | fn stroke_x(&self) -> f32 { 108 | self.stroke_x() 109 | } 110 | 111 | fn stroke_y(&self) -> f32 { 112 | self.stroke_y() 113 | } 114 | 115 | fn stroke_bitmap(&self) -> Option<(Vec, u32)> { 116 | let (bitmap, w) = self.stroke_bitmap()?; 117 | Some((bitmap.clone(), w)) 118 | } 119 | } 120 | 121 | impl fi::GuestFontKit for FontKit { 122 | fn new() -> Self { 123 | FontKit::new() 124 | } 125 | 126 | fn add_font_from_buffer(&self, buffer: Vec) { 127 | #[cfg(feature = "parse")] 128 | let _ = self.add_font_from_buffer(buffer); 129 | } 130 | 131 | fn query(&self, key: fi::FontKey) -> Option { 132 | self.query(&FontKey::from(key)).map(fi::Font::new) 133 | } 134 | 135 | fn exact_match(&self, key: fi::FontKey) -> Option { 136 | self.exact_match(&FontKey::from(key)).map(fi::Font::new) 137 | } 138 | 139 | fn len(&self) -> u32 { 140 | self.len() as u32 141 | } 142 | 143 | fn remove(&self, key: fi::FontKey) { 144 | self.remove(FontKey::from(key)) 145 | } 146 | 147 | fn add_search_path(&self, path: String) { 148 | #[cfg(feature = "parse")] 149 | self.search_fonts_from_path(path).unwrap() 150 | } 151 | 152 | fn fonts_info(&self) -> Vec { 153 | self.fonts.iter().flat_map(|i| font_info(&*i)).collect() 154 | } 155 | 156 | fn measure(&self, key: fi::FontKey, text: String) -> Option { 157 | Some(fi::TextMetrics::new(self.measure(&key.into(), &text)?)) 158 | } 159 | 160 | fn read_data(&self, data: String) { 161 | let result: Result>, _> = 162 | serde_json::from_str(&data); 163 | if let Ok(data) = result { 164 | for item in data { 165 | let path = item 166 | .get("path") 167 | .and_then(|v| serde_json::from_value::>(v.clone()).ok()); 168 | let variants = item 169 | .get("variants") 170 | .and_then(|v| serde_json::from_value::>(v.clone()).ok()); 171 | if let (Some(path), Some(variants)) = (path, variants) { 172 | let font = Font::new(path, variants, self.hit_counter.clone()); 173 | let key = font.first_key(); 174 | self.fonts.insert(key, font); 175 | } 176 | } 177 | } 178 | } 179 | 180 | fn write_data(&self) -> String { 181 | let mut result = vec![]; 182 | for value in self.fonts.iter() { 183 | let font = value.value(); 184 | let mut value = serde_json::Map::new(); 185 | value.insert( 186 | "path".to_string(), 187 | serde_json::to_value(font.path()).unwrap(), 188 | ); 189 | value.insert( 190 | "variants".to_string(), 191 | serde_json::to_value(font.variants()).unwrap(), 192 | ); 193 | result.push(value); 194 | } 195 | serde_json::to_string(&result).unwrap() 196 | } 197 | 198 | fn query_font_info(&self, key: fi::FontKey) -> Option> { 199 | let font = self.fonts.get(&self.query_font(&key)?)?; 200 | Some(font_info(&*font)) 201 | } 202 | 203 | fn set_config(&self, limit: u32, cache_path: Option) { 204 | if let Some(p) = cache_path.as_ref() { 205 | std::fs::create_dir_all(p).unwrap(); 206 | } 207 | self.config.store(Arc::new(Config { 208 | lru_limit: limit, 209 | cache_path, 210 | })); 211 | } 212 | 213 | fn buffer_size(&self) -> u32 { 214 | self.buffer_size() as u32 215 | } 216 | } 217 | 218 | impl GuestTextMetrics for TextMetrics { 219 | fn new(value: String) -> Self { 220 | TextMetrics::new(value) 221 | } 222 | 223 | fn duplicate(&self) -> fi::TextMetrics { 224 | fi::TextMetrics::new(Clone::clone(self)) 225 | } 226 | 227 | fn width(&self, font_size: f32, letter_spacing: f32) -> f32 { 228 | self.width(font_size, letter_spacing) 229 | } 230 | 231 | fn height(&self, font_size: f32, line_height: Option) -> f32 { 232 | self.height(font_size, line_height) 233 | } 234 | 235 | fn ascender(&self, font_size: f32) -> f32 { 236 | ::ascender(self, font_size) 237 | } 238 | 239 | fn line_gap(&self) -> f32 { 240 | self.line_gap() as f32 / self.units() as f32 241 | } 242 | 243 | fn slice(&self, start: u32, count: u32) -> fi::TextMetrics { 244 | fi::TextMetrics::new(self.slice(start, count)) 245 | } 246 | 247 | fn value(&self) -> String { 248 | TextMetrics::value(&self) 249 | } 250 | 251 | fn is_rtl(&self) -> bool { 252 | self.is_rtl() 253 | } 254 | 255 | fn append(&self, other: fi::TextMetrics) { 256 | TextMetrics::append(self, other.get::().clone()) 257 | } 258 | 259 | fn count(&self) -> u32 { 260 | self.count() as u32 261 | } 262 | 263 | fn replace(&self, other: fi::TextMetrics, fallback: bool) { 264 | TextMetrics::replace(self, other.get::().clone(), fallback); 265 | } 266 | 267 | fn split_by_width(&self, font_size: f32, letter_spacing: f32, width: f32) -> fi::TextMetrics { 268 | fi::TextMetrics::new(self.split_by_width(font_size, letter_spacing, width)) 269 | } 270 | 271 | fn chars(&self) -> Vec { 272 | let p = self.positions.read().unwrap(); 273 | p.iter().map(|c| c.metrics.c).collect() 274 | } 275 | 276 | fn units(&self) -> f32 { 277 | self.units() as f32 278 | } 279 | 280 | fn has_missing(&self) -> bool { 281 | self.has_missing() 282 | } 283 | } 284 | 285 | struct Component; 286 | 287 | impl fi::Guest for Component { 288 | type Font = StaticFace; 289 | type FontKit = FontKit; 290 | type GlyphBitmap = GlyphBitmap; 291 | type TextMetrics = TextMetrics; 292 | 293 | fn str_width_to_number(width: String) -> u16 { 294 | crate::str_width_to_number(&width) 295 | } 296 | 297 | fn number_width_to_str(width: u16) -> String { 298 | crate::number_width_to_str(width).to_string() 299 | } 300 | } 301 | 302 | crate::bindings::export!(Component with_types_in crate::bindings); 303 | 304 | fn font_info(font: &Font) -> Vec { 305 | font.variants() 306 | .iter() 307 | .map(|v| fi::FontInfo { 308 | style_names: v 309 | .style_names 310 | .iter() 311 | .map(|n| fi::Name { 312 | id: n.id, 313 | name: n.name.clone(), 314 | language_id: n.language_id, 315 | }) 316 | .collect(), 317 | key: fi::FontKey::from(v.key.clone()), 318 | names: v 319 | .names 320 | .iter() 321 | .map(|n| fi::Name { 322 | id: n.id, 323 | name: n.name.clone(), 324 | language_id: n.language_id, 325 | }) 326 | .collect(), 327 | path: font.path().and_then(|p| Some(p.to_str()?.to_string())), 328 | }) 329 | .collect::>() 330 | } 331 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | use fontkit::{Area, Error, FontKey, FontKit, Line, Span, TextMetrics}; 2 | use std::fs; 3 | use std::io::Read; 4 | 5 | #[test] 6 | pub fn test_font_loading() -> Result<(), Error> { 7 | let mut buf = vec![]; 8 | let mut f = fs::File::open("examples/OpenSans-Italic.ttf")?; 9 | f.read_to_end(&mut buf)?; 10 | let fontkit = FontKit::new(); 11 | let _ = fontkit.add_font_from_buffer(buf)?; 12 | Ok(()) 13 | } 14 | 15 | #[test] 16 | pub fn test_variable_font_loading() -> Result<(), Error> { 17 | let mut buf = vec![]; 18 | let mut f = fs::File::open("examples/AlimamaFangYuanTiVF.ttf")?; 19 | f.read_to_end(&mut buf)?; 20 | let fontkit = FontKit::new(); 21 | let _ = fontkit.add_font_from_buffer(buf)?; 22 | let mut key = FontKey::default(); 23 | key.family = "AlimamaFangYuanTiVF-Medium-Round".into(); 24 | let bitmap_1 = fontkit 25 | .query(&key) 26 | .and_then(|font| font.bitmap('G', 10.0, 0.0)) 27 | .map(|g| g.bitmap().iter().filter(|p| **p > 0).count()); 28 | key.family = "AlimamaFangYuanTiVF-BoldRound".into(); 29 | assert!(fontkit.query(&key).is_some()); 30 | key.family = "AlimamaFangYuanTiVF-Thin".into(); 31 | assert!(fontkit.query(&key).is_none()); 32 | key.weight = Some(200); 33 | key.italic = Some(false); 34 | key.stretch = Some(5); 35 | assert!(fontkit.query(&key).is_some()); 36 | let bitmap_2 = fontkit 37 | .query(&key) 38 | .and_then(|font| font.bitmap('G', 10.0, 0.0)) 39 | .map(|g| g.bitmap().iter().filter(|p| **p > 0).count()); 40 | assert!(bitmap_2.is_some()); 41 | assert!(bitmap_1 > bitmap_2); 42 | Ok(()) 43 | } 44 | 45 | #[test] 46 | pub fn test_search_font() -> Result<(), Error> { 47 | let fontkit = FontKit::new(); 48 | fontkit.search_fonts_from_path("examples/AlimamaFangYuanTiVF.ttf")?; 49 | assert_eq!(fontkit.len(), 18); 50 | Ok(()) 51 | } 52 | 53 | #[test] 54 | pub fn test_text_wrap() -> Result<(), Error> { 55 | let fontkit = FontKit::new(); 56 | fontkit.search_fonts_from_path("examples/AlimamaFangYuanTiVF.ttf")?; 57 | let mut key = FontKey::default(); 58 | key.family = "AlimamaFangYuanTiVF-Medium-Round".into(); 59 | let mut area = Area::<(), TextMetrics>::new(); 60 | let metrics = fontkit 61 | .measure(&key, " 傲冬黑色真皮皮衣 穿着舒适显瘦") 62 | .unwrap(); 63 | let mut span = Span::default(); 64 | span.font_key = key.clone(); 65 | span.size = 66.0; 66 | span.metrics = metrics; 67 | area.lines.push(Line { 68 | spans: vec![span], 69 | hard_break: true, 70 | }); 71 | area.unwrap_text(); 72 | area.wrap_text(576.0)?; 73 | assert_eq!(area.width(), 553.608); 74 | Ok(()) 75 | } 76 | 77 | #[test] 78 | pub fn test_complex_text_wrap() -> Result<(), Error> { 79 | let fontkit = FontKit::new(); 80 | fontkit.search_fonts_from_path("examples/AlimamaFangYuanTiVF.ttf")?; 81 | let mut key = FontKey::default(); 82 | key.family = "AlimamaFangYuanTiVF-Medium-Round".into(); 83 | let mut area = Area::<(), TextMetrics>::new(); 84 | let metrics = fontkit.measure(&key, "商家").unwrap(); 85 | let mut span = Span::default(); 86 | span.font_key = key.clone(); 87 | span.size = 32.0; 88 | span.metrics = metrics; 89 | let metrics = fontkit.measure(&key, "热卖12345678").unwrap(); 90 | let mut span_1 = Span::default(); 91 | span_1.font_key = key.clone(); 92 | span_1.size = 32.0; 93 | span_1.metrics = metrics; 94 | area.lines.push(Line { 95 | spans: vec![span], 96 | hard_break: true, 97 | }); 98 | area.lines.push(Line { 99 | spans: vec![span_1], 100 | hard_break: true, 101 | }); 102 | area.unwrap_text(); 103 | area.wrap_text(64.4)?; 104 | assert_eq!(area.value_string(), "商家\n热卖\n123\n456\n78"); 105 | Ok(()) 106 | } 107 | 108 | #[test] 109 | pub fn test_lru_cache() -> Result<(), Error> { 110 | let fontkit = FontKit::new(); 111 | fontkit.set_lru_limit(1); 112 | fontkit.search_fonts_from_path("examples/AlimamaFangYuanTiVF.ttf")?; 113 | assert_eq!(fontkit.buffer_size(), 0); 114 | let key = fontkit.keys().pop().unwrap(); 115 | assert!(fontkit.query(&key).is_some()); 116 | assert_eq!(fontkit.buffer_size(), 7412388); 117 | fontkit.search_fonts_from_path("examples/OpenSans-Italic.ttf")?; 118 | let key2 = FontKey::new_with_family("Open Sans".to_string()); 119 | assert!(fontkit.query(&key2).is_some()); 120 | assert_eq!(fontkit.buffer_size(), 212896); 121 | fontkit.query(&key); 122 | assert_eq!(fontkit.buffer_size(), 7412388); 123 | Ok(()) 124 | } 125 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "strict": true, 5 | "moduleResolution": "node", 6 | "module": "ES2020", 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | }, 12 | "include": [ 13 | "." 14 | ], 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /wit/world.wit: -------------------------------------------------------------------------------- 1 | package alibaba:fontkit; 2 | 3 | interface commons { 4 | record font-key { 5 | weight: option, 6 | italic: option, 7 | stretch: option, 8 | family: string, 9 | variations: list>, 10 | } 11 | } 12 | 13 | interface fontkit-interface { 14 | use commons.{font-key}; 15 | 16 | record name { 17 | id: u16, 18 | name: string, 19 | language-id: u16 20 | } 21 | record font-info { 22 | style-names: list, 23 | names: list, 24 | path: option, 25 | key: font-key, 26 | } 27 | record line-metrics { 28 | position: s16, 29 | thickness: s16, 30 | } 31 | resource text-metrics { 32 | constructor(value: string); 33 | duplicate: func() -> text-metrics; 34 | width: func(font-size: f32, letter-spacing: f32) -> f32; 35 | height: func(font-size: f32, line-height: option) -> f32; 36 | ascender: func(font-size: f32) -> f32; 37 | line-gap: func() -> f32; 38 | units: func() -> f32; 39 | slice: func(start: u32, count: u32) -> text-metrics; 40 | value: func() -> string; 41 | is-rtl: func() -> bool; 42 | append: func(other: text-metrics); 43 | count: func() -> u32; 44 | has-missing: func() -> bool; 45 | /// replace this metrics with another, allowing fallback logic 46 | replace: func(other: text-metrics, fallback: bool); 47 | split-by-width: func(font-size: f32, letter-spacing: f32, width: f32) -> text-metrics; 48 | chars: func() -> list; 49 | } 50 | resource glyph-bitmap { 51 | width: func() -> u32; 52 | height: func() -> u32; 53 | bitmap: func() -> list; 54 | x-min: func() -> f32; 55 | y-max: func() -> f32; 56 | stroke-x: func() -> f32; 57 | stroke-y: func() -> f32; 58 | stroke-bitmap: func() -> option, u32>>; 59 | advanced-x: func() -> f32; 60 | ascender: func() -> f32; 61 | descender: func() -> f32; 62 | } 63 | /// Instance of a single font 64 | resource font { 65 | /// Check if the font has valid data for a character 66 | has-glyph: func(c: char) -> bool; 67 | /// Output the svg path string of a glyph 68 | glyph-path-string: func(c: char) -> option; 69 | /// Return the font buffer 70 | buffer: func() -> list; 71 | /// Return the path if this font is added from searching a path 72 | path: func() -> string; 73 | /// Return the key of this font 74 | key: func() -> font-key; 75 | /// Measure text using this font 76 | measure: func(text: string) -> result; 77 | ascender: func() -> s16; 78 | descender: func() -> s16; 79 | units-per-em: func() -> u16; 80 | bitmap: func(c: char, font-size: f32, stroke-width: f32) -> option; 81 | underline-metrics: func() -> option; 82 | } 83 | /// Stores font buffer and provides font-querying APIs 84 | resource font-kit { 85 | constructor(); 86 | /// add an LRU limit for font buffer registry, `limit`'s unit is KB, 0 means caching is disabled. 87 | /// If a cache path is provided, `addFontFromBuffer` will dump the buffer into the path to save memory 88 | set-config: func(limit: u32, cache-path: option); 89 | /// Register a font (or several fonts in case of ttc), return the keys of added fonts. 90 | /// The file type is extracted from the buffer by checking magic numbers 91 | add-font-from-buffer: func(buffer: list) ; 92 | /// Search and add fonts from a path 93 | add-search-path: func(path: string); 94 | /// Query font using a key, this API returns valid result only if a single result is found 95 | query: func(key: font-key) -> option; 96 | /// Query the info of font, even if the font is unloaded and `query` returns `None` 97 | query-font-info: func(key: font-key) -> option>; 98 | /// Using exact-match method to directly obtain a font, skipping the querying logic 99 | exact-match: func(key: font-key) -> option; 100 | /// Get detailed info of all fonts registered 101 | fonts-info: func() -> list; 102 | /// Get number of registered fonts 103 | len: func() -> u32; 104 | /// Remove a font matching the given key 105 | remove: func(key: font-key); 106 | /// Measure a text with some fallback logic 107 | measure: func(key: font-key, text: string) -> option; 108 | /// Export all font data into a JSON string 109 | write-data: func() -> string; 110 | /// Load font data from JSON 111 | read-data: func(data: string); 112 | buffer-size: func() -> u32; 113 | } 114 | str-width-to-number: func(width: string) -> u16; 115 | number-width-to-str: func(width: u16) -> string; 116 | } 117 | 118 | world fontkit { 119 | export fontkit-interface; 120 | } 121 | --------------------------------------------------------------------------------