├── .gitignore ├── tests ├── (.ppm ├── A.ppm ├── g.ppm ├── l.ppm └── raster.rs ├── fonts ├── Roboto-Regular.ttf ├── IBMPlexSans-Bold.ttf ├── SourceSans3-Regular.otf └── LibertinusSerif-Regular.otf ├── rustfmt.toml ├── .github └── workflows │ └── ci.yml ├── Cargo.toml ├── README.md ├── benches └── oneshot.rs ├── NOTICE ├── LICENSE └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /tests/(.ppm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/pixglyph/HEAD/tests/(.ppm -------------------------------------------------------------------------------- /tests/A.ppm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/pixglyph/HEAD/tests/A.ppm -------------------------------------------------------------------------------- /tests/g.ppm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/pixglyph/HEAD/tests/g.ppm -------------------------------------------------------------------------------- /tests/l.ppm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/pixglyph/HEAD/tests/l.ppm -------------------------------------------------------------------------------- /fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/pixglyph/HEAD/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /fonts/IBMPlexSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/pixglyph/HEAD/fonts/IBMPlexSans-Bold.ttf -------------------------------------------------------------------------------- /fonts/SourceSans3-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/pixglyph/HEAD/fonts/SourceSans3-Regular.otf -------------------------------------------------------------------------------- /fonts/LibertinusSerif-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typst/pixglyph/HEAD/fonts/LibertinusSerif-Regular.otf -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" 2 | max_width = 90 3 | chain_width = 70 4 | struct_lit_width = 50 5 | use_field_init_shorthand = true 6 | merge_derives = false 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | ci: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: dtolnay/rust-toolchain@stable 10 | - run: cargo build 11 | - run: cargo test 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pixglyph" 3 | version = "0.6.0" 4 | authors = ["Laurenz "] 5 | edition = "2021" 6 | description = "Font-rendering with subpixel positioning." 7 | repository = "https://github.com/typst/pixglyph" 8 | readme = "README.md" 9 | license = "Apache-2.0" 10 | categories = ["rendering"] 11 | keywords = ["rendering", "font"] 12 | exclude = ["fonts/*"] 13 | 14 | [dependencies] 15 | ttf-parser = "0.25" 16 | 17 | [dev-dependencies] 18 | iai = { git = "https://github.com/reknih/iai" } 19 | 20 | [[bench]] 21 | name = "oneshot" 22 | path = "benches/oneshot.rs" 23 | harness = false 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pixglyph 2 | [![Crates.io](https://img.shields.io/crates/v/pixglyph.svg)](https://crates.io/crates/pixglyph) 3 | [![Documentation](https://docs.rs/pixglyph/badge.svg)](https://docs.rs/pixglyph) 4 | 5 | OpenType glyph rendering. 6 | 7 | ```toml 8 | [dependencies] 9 | pixglyph = "0.6" 10 | ``` 11 | 12 | ## Features 13 | - Render glyph outlines into coverage bitmaps. 14 | - Place glyphs at subpixel offsets and scale them to subpixel sizes. This is 15 | important if you plan to render more than a single glyph since inter-glyph 16 | spacing will look off if every glyph origin must be pixel-aligned. 17 | - No font data structure you have to store somewhere. Just owned glyphs 18 | which you can load individually from a font, cache if you care about 19 | performance, and then render at any size. 20 | - No unsafe code. 21 | 22 | ## License 23 | This crate is licensed under the terms of the Apache 2.0 license. 24 | -------------------------------------------------------------------------------- /benches/oneshot.rs: -------------------------------------------------------------------------------- 1 | use iai::{main, Iai}; 2 | 3 | use pixglyph::Glyph; 4 | use ttf_parser::Face; 5 | 6 | const ROBOTO: &[u8] = include_bytes!("../fonts/Roboto-Regular.ttf"); 7 | const SOURCE_SANS: &[u8] = include_bytes!("../fonts/SourceSans3-Regular.otf"); 8 | const SIZE: f32 = 14.0; 9 | 10 | main!( 11 | bench_load_simple, 12 | bench_load_complex, 13 | bench_rasterize_simple, 14 | bench_rasterize_complex, 15 | bench_rasterize_cubic, 16 | ); 17 | 18 | fn bench_load_simple(iai: &mut Iai) { 19 | let face = Face::parse(ROBOTO, 0).unwrap(); 20 | let id = face.glyph_index('A').unwrap(); 21 | iai.run(|| Glyph::load(&face, id)); 22 | } 23 | 24 | fn bench_load_complex(iai: &mut Iai) { 25 | let face = Face::parse(ROBOTO, 0).unwrap(); 26 | let id = face.glyph_index('g').unwrap(); 27 | iai.run(|| Glyph::load(&face, id)); 28 | } 29 | 30 | fn bench_rasterize_simple(iai: &mut Iai) { 31 | let face = Face::parse(ROBOTO, 0).unwrap(); 32 | let id = face.glyph_index('A').unwrap(); 33 | let glyph = Glyph::load(&face, id).unwrap(); 34 | iai.run(|| glyph.rasterize(0.0, 0.0, SIZE)); 35 | } 36 | 37 | fn bench_rasterize_complex(iai: &mut Iai) { 38 | let face = Face::parse(ROBOTO, 0).unwrap(); 39 | let id = face.glyph_index('g').unwrap(); 40 | let glyph = Glyph::load(&face, id).unwrap(); 41 | iai.run(|| glyph.rasterize(0.0, 0.0, SIZE)); 42 | } 43 | 44 | fn bench_rasterize_cubic(iai: &mut Iai) { 45 | let face = Face::parse(SOURCE_SANS, 0).unwrap(); 46 | let id = face.glyph_index('g').unwrap(); 47 | let glyph = Glyph::load(&face, id).unwrap(); 48 | iai.run(|| glyph.rasterize(0.0, 0.0, SIZE)); 49 | } 50 | -------------------------------------------------------------------------------- /tests/raster.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use pixglyph::Glyph; 4 | use ttf_parser::{Face, GlyphId}; 5 | 6 | const ROBOTO: &[u8] = include_bytes!("../fonts/Roboto-Regular.ttf"); 7 | const SOURCE_SANS: &[u8] = include_bytes!("../fonts/SourceSans3-Regular.otf"); 8 | const IBM_PLEX: &[u8] = include_bytes!("../fonts/IBMPlexSans-Bold.ttf"); 9 | const LIBERTINUS: &[u8] = include_bytes!("../fonts/LibertinusSerif-Regular.otf"); 10 | 11 | #[test] 12 | fn test_load_all() { 13 | let face = Face::parse(SOURCE_SANS, 0).unwrap(); 14 | for i in 0..face.number_of_glyphs() { 15 | Glyph::load(&face, GlyphId(i)); 16 | } 17 | } 18 | 19 | #[test] 20 | fn test_rasterize() { 21 | let mut ok = true; 22 | ok &= raster_letter(ROBOTO, 'A', 0.0, 0.0, 100.0); 23 | ok &= raster_letter(SOURCE_SANS, 'g', 0.0, 0.0, 400.0); 24 | ok &= raster_letter(IBM_PLEX, 'l', 138.48, 95.84, 80.0); 25 | ok &= raster_letter(LIBERTINUS, '(', 114.09056, 34.47, 22.0); 26 | if !ok { 27 | panic!(); 28 | } 29 | } 30 | 31 | fn raster_letter(font: &[u8], letter: char, x: f32, y: f32, s: f32) -> bool { 32 | let out_path = format!("target/{}.ppm", letter); 33 | let ref_path = format!("tests/{}.ppm", letter); 34 | 35 | let face = Face::parse(font, 0).unwrap(); 36 | let id = face.glyph_index(letter).unwrap(); 37 | let glyph = Glyph::load(&face, id).unwrap(); 38 | let bitmap = glyph.rasterize(x, y, s); 39 | 40 | let mut ppm = vec![]; 41 | write!(ppm, "P6\n{} {}\n255\n", bitmap.width, bitmap.height).unwrap(); 42 | 43 | for &c in &bitmap.coverage { 44 | ppm.extend([255 - c; 3]); 45 | } 46 | 47 | std::fs::write(out_path, &ppm).unwrap(); 48 | 49 | let reference = std::fs::read(ref_path).ok(); 50 | 51 | let ok = Some(ppm) == reference; 52 | if !ok { 53 | eprintln!("Letter {letter:?} differs ❌"); 54 | } 55 | 56 | ok 57 | } 58 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Licenses for third party components used by this project can be found below. 2 | 3 | ================================================================================ 4 | The Apache License Version 2.0 applies to: 5 | 6 | * Rasterization code used in the `Canvas` taken from the font-rs project 7 | Copyright 2015 Google Inc. All rights reserved. 8 | (https://github.com/raphlinus/font-rs) 9 | 10 | * Cubic to quadratic bezier conversion in `Canvas::cubic` adapted from the 11 | kurbo 2D curves library 12 | Copyright 2018 The kurbo Authors. 13 | (https://github.com/linebender/kurbo) 14 | 15 | * Roboto-Regular.ttf 16 | (https://github.com/googlefonts/roboto) 17 | 18 | You may find a copy of the Apache 2.0 License in the `LICENSE` file. 19 | ================================================================================ 20 | 21 | ================================================================================ 22 | The SIL Open Font License Version 1.1 applies to: 23 | 24 | * IBMPlexSans-Bold.ttf 25 | Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" 26 | (https://github.com/IBM/plex) 27 | 28 | * SourceSans3-Regular.otf 29 | Copyright 2010-2020 Adobe (http://www.adobe.com/), with Reserved Font Name 30 | 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United 31 | States and/or other countries. 32 | (https://github.com/adobe-fonts/source-sans) 33 | 34 | * LibertinusSerif-Regular.otf 35 | Copyright © 2012-2024 The Libertinus Project Authors, 36 | with Reserved Font Name "Linux Libertine", "Biolinum", "STIX Fonts". 37 | 38 | ----------------------------------------------------------- 39 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 40 | ----------------------------------------------------------- 41 | 42 | PREAMBLE 43 | The goals of the Open Font License (OFL) are to stimulate worldwide 44 | development of collaborative font projects, to support the font creation 45 | efforts of academic and linguistic communities, and to provide a free and 46 | open framework in which fonts may be shared and improved in partnership 47 | with others. 48 | 49 | The OFL allows the licensed fonts to be used, studied, modified and 50 | redistributed freely as long as they are not sold by themselves. The 51 | fonts, including any derivative works, can be bundled, embedded, 52 | redistributed and/or sold with any software provided that any reserved 53 | names are not used by derivative works. The fonts and derivatives, 54 | however, cannot be released under any other type of license. The 55 | requirement for fonts to remain under this license does not apply 56 | to any document created using the fonts or their derivatives. 57 | 58 | DEFINITIONS 59 | "Font Software" refers to the set of files released by the Copyright 60 | Holder(s) under this license and clearly marked as such. This may 61 | include source files, build scripts and documentation. 62 | 63 | "Reserved Font Name" refers to any names specified as such after the 64 | copyright statement(s). 65 | 66 | "Original Version" refers to the collection of Font Software components as 67 | distributed by the Copyright Holder(s). 68 | 69 | "Modified Version" refers to any derivative made by adding to, deleting, 70 | or substituting -- in part or in whole -- any of the components of the 71 | Original Version, by changing formats or by porting the Font Software to a 72 | new environment. 73 | 74 | "Author" refers to any designer, engineer, programmer, technical 75 | writer or other person who contributed to the Font Software. 76 | 77 | PERMISSION & CONDITIONS 78 | Permission is hereby granted, free of charge, to any person obtaining 79 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 80 | redistribute, and sell modified and unmodified copies of the Font 81 | Software, subject to the following conditions: 82 | 83 | 1) Neither the Font Software nor any of its individual components, 84 | in Original or Modified Versions, may be sold by itself. 85 | 86 | 2) Original or Modified Versions of the Font Software may be bundled, 87 | redistributed and/or sold with any software, provided that each copy 88 | contains the above copyright notice and this license. These can be 89 | included either as stand-alone text files, human-readable headers or 90 | in the appropriate machine-readable metadata fields within text or 91 | binary files as long as those fields can be easily viewed by the user. 92 | 93 | 3) No Modified Version of the Font Software may use the Reserved Font 94 | Name(s) unless explicit written permission is granted by the corresponding 95 | Copyright Holder. This restriction only applies to the primary font name as 96 | presented to the users. 97 | 98 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 99 | Software shall not be used to promote, endorse or advertise any 100 | Modified Version, except to acknowledge the contribution(s) of the 101 | Copyright Holder(s) and the Author(s) or with their explicit written 102 | permission. 103 | 104 | 5) The Font Software, modified or unmodified, in part or in whole, 105 | must be distributed entirely under this license, and must not be 106 | distributed under any other license. The requirement for fonts to 107 | remain under this license does not apply to any document created 108 | using the Font Software. 109 | 110 | TERMINATION 111 | This license becomes null and void if any of the above conditions are 112 | not met. 113 | 114 | DISCLAIMER 115 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 116 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 117 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 118 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 119 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 120 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 121 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 122 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 123 | OTHER DEALINGS IN THE FONT SOFTWARE. 124 | ================================================================================ 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! OpenType glyph rendering. 2 | //! 3 | //! - Render glyph outlines into coverage bitmaps. 4 | //! - Place glyphs at subpixel offsets and scale them to subpixel sizes. This is 5 | //! important if you plan to render more than a single glyph since inter-glyph 6 | //! spacing will look off if every glyph origin must be pixel-aligned. 7 | //! - No font data structure you have to store somewhere. Just owned glyphs 8 | //! which you can load individually from a font, cache if you care about 9 | //! performance, and then render at any size. 10 | //! - No unsafe code. 11 | //! 12 | //! _Note on text:_ This library does not provide any capabilities to map 13 | //! text/characters to glyph ids. Instead, you should use a proper shaping 14 | //! library (like [`rustybuzz`]) to do this step. This will take care of proper 15 | //! glyph positioning, ligatures and more. 16 | //! 17 | //! _Note on emojis:_ This library only supports normal outlines. How to best 18 | //! render bitmap, SVG and colored glyphs depends very much on your rendering 19 | //! environment. 20 | //! 21 | //! [`rustybuzz`]: https://github.com/RazrFalcon/rustybuzz 22 | 23 | #![forbid(unsafe_code)] 24 | #![deny(missing_docs)] 25 | 26 | use std::fmt::{self, Debug, Formatter}; 27 | use std::ops::{Add, Div, Mul, Sub}; 28 | 29 | use ttf_parser::{Face, GlyphId, OutlineBuilder, Rect}; 30 | 31 | /// A loaded glyph that is ready for rendering. 32 | #[derive(Debug, Clone)] 33 | pub struct Glyph { 34 | /// The number of font design units per em unit. 35 | units_per_em: u16, 36 | /// The glyph bounding box. 37 | bbox: Rect, 38 | /// The path segments. 39 | segments: Vec, 40 | } 41 | 42 | /// A path segment. 43 | #[derive(Debug, Copy, Clone)] 44 | enum Segment { 45 | /// A straight line. 46 | Line(Point, Point), 47 | /// A quadratic bezier curve. 48 | Quad(Point, Point, Point), 49 | /// A cubic bezier curve. 50 | Cubic(Point, Point, Point, Point), 51 | } 52 | 53 | impl Glyph { 54 | /// Load the glyph with the given `glyph_id` from the face. 55 | /// 56 | /// This method takes a `ttf-parser` font face. If you don't already use 57 | /// `ttf-parser`, you can [create a face](ttf_parser::Face::from_slice) from 58 | /// raw OpenType font bytes with very little overhead. 59 | /// 60 | /// Returns `None` if the glyph does not exist or the outline is malformed. 61 | pub fn load(face: &Face, glyph_id: GlyphId) -> Option { 62 | let mut builder = Builder::default(); 63 | Some(Self { 64 | units_per_em: face.units_per_em(), 65 | bbox: face.outline_glyph(glyph_id, &mut builder)?, 66 | segments: builder.segments, 67 | }) 68 | } 69 | 70 | /// Rasterize the glyph. 71 | /// 72 | /// # Placing & scaling 73 | /// The values of `x` and `y` determine the subpixel positions at which the 74 | /// glyph origin should reside in some larger pixel raster (i.e. a canvas 75 | /// which you render text into). This is important when you're rendering the 76 | /// resulting bitmap into a larger pixel buffer and the glyph origin is not 77 | /// pixel-aligned in that raster. 78 | /// 79 | /// For example, if you want to render a glyph into your own canvas with its 80 | /// origin at `(3.5, 4.6)` (in pixels) you would use these exact values for 81 | /// `x` and `y`. 82 | /// 83 | /// The `size` defines how many pixels should correspond to `1em` 84 | /// horizontally and vertically. So, if you wanted to want to render your 85 | /// text at a size of `12px`, then `size` should be `12.0`. 86 | /// 87 | /// # Rendering into a larger canvas 88 | /// The result of rasterization is a coverage bitmap along with position and 89 | /// sizing data for it. Each individual coverage value defines how much one 90 | /// pixel is covered by the text. So if you have an RGB text color, you can 91 | /// directly use the coverage values as alpha values to form RGBA pixels. 92 | /// The returned `left` and `top` values define on top of which pixels in 93 | /// your canvas you should blend each of these new pixels. 94 | /// 95 | /// In our example, we have `glyph.rasterize(3.5, 4.6, 12.0, 12.0)`. Now, 96 | /// let's say the returned values are `left: 3`, `top: 1`, `width: 6` and 97 | /// `height: 9`. Then you need to apply the coverage values to your canvas 98 | /// starting at `(3, 1)` and going to `(9, 10)` row-by-row. 99 | pub fn rasterize(&self, x: f32, y: f32, size: f32) -> Bitmap { 100 | // Scale is in pixel per em, but curve data is in font design units, so 101 | // we have to divide by units per em. 102 | let s = size / self.units_per_em as f32; 103 | 104 | // Determine the pixel-aligned bounding box of the glyph in the larger 105 | // pixel raster. For y, we flip and sign and min/max because Y-up. We 106 | // add a bit of horizontal slack to prevent floating problems when the 107 | // curve is directly at the border (only needed horizontally due to 108 | // row-by-row data layout). 109 | let slack = 0.01; 110 | let left = (x + s * self.bbox.x_min as f32 - slack).floor() as i32; 111 | let right = (x + s * self.bbox.x_max as f32 + slack).ceil() as i32; 112 | let top = (y - s * self.bbox.y_max as f32).floor() as i32; 113 | let bottom = (y - s * self.bbox.y_min as f32).ceil() as i32; 114 | let width = (right - left) as u32; 115 | let height = (bottom - top) as u32; 116 | 117 | // Create function to transform individual points. 118 | let dx = x - left as f32; 119 | let dy = y - top as f32; 120 | let t = |p: Point| point(dx + p.x * s, dy - p.y * s); 121 | 122 | // Draw! 123 | let mut canvas = Canvas::new(width, height); 124 | for &segment in &self.segments { 125 | match segment { 126 | Segment::Line(p0, p1) => canvas.line(t(p0), t(p1)), 127 | Segment::Quad(p0, p1, p2) => canvas.quad(t(p0), t(p1), t(p2)), 128 | Segment::Cubic(p0, p1, p2, p3) => { 129 | canvas.cubic(t(p0), t(p1), t(p2), t(p3)) 130 | } 131 | } 132 | } 133 | 134 | Bitmap { 135 | left, 136 | top, 137 | width, 138 | height, 139 | coverage: canvas.accumulate(), 140 | } 141 | } 142 | } 143 | 144 | /// The result of rasterizing a glyph. 145 | pub struct Bitmap { 146 | /// Horizontal pixel position (from the left) at which the bitmap should be 147 | /// placed in the larger raster. 148 | pub left: i32, 149 | /// Vertical pixel position (from the top) at which the bitmap should be 150 | /// placed in the larger raster. 151 | pub top: i32, 152 | /// The width of the coverage bitmap in pixels. 153 | pub width: u32, 154 | /// The height of the coverage bitmap in pixels. 155 | pub height: u32, 156 | /// How much each pixel should be covered, `0` means 0% coverage and `255` 157 | /// means 100% coverage. 158 | /// 159 | /// The length of this vector is `width * height`, with the values being 160 | /// stored row-by-row. 161 | pub coverage: Vec, 162 | } 163 | 164 | impl Debug for Bitmap { 165 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 166 | f.debug_struct("Bitmap") 167 | .field("left", &self.left) 168 | .field("top", &self.top) 169 | .field("width", &self.width) 170 | .field("height", &self.height) 171 | .finish() 172 | } 173 | } 174 | 175 | /// Builds the glyph outline. 176 | #[derive(Default)] 177 | struct Builder { 178 | segments: Vec, 179 | start: Option, 180 | last: Point, 181 | } 182 | 183 | impl OutlineBuilder for Builder { 184 | fn move_to(&mut self, x: f32, y: f32) { 185 | self.start = Some(point(x, y)); 186 | self.last = point(x, y); 187 | } 188 | 189 | fn line_to(&mut self, x: f32, y: f32) { 190 | self.segments.push(Segment::Line(self.last, point(x, y))); 191 | self.last = point(x, y); 192 | } 193 | 194 | fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) { 195 | self.segments 196 | .push(Segment::Quad(self.last, point(x1, y1), point(x2, y2))); 197 | self.last = point(x2, y2); 198 | } 199 | 200 | fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) { 201 | self.segments.push(Segment::Cubic( 202 | self.last, 203 | point(x1, y1), 204 | point(x2, y2), 205 | point(x3, y3), 206 | )); 207 | self.last = point(x3, y3); 208 | } 209 | 210 | fn close(&mut self) { 211 | if let Some(start) = self.start.take() { 212 | self.segments.push(Segment::Line(self.last, start)); 213 | self.last = start; 214 | } 215 | } 216 | } 217 | 218 | // Accumulation, line and quad drawing taken from here: 219 | // https://github.com/raphlinus/font-rs 220 | // 221 | // Copyright 2015 Google Inc. All rights reserved. 222 | // 223 | // Licensed under the Apache License, Version 2.0 (the "License"); 224 | // you may not use this file except in compliance with the License. 225 | // You may obtain a copy of the License at 226 | // 227 | // http://www.apache.org/licenses/LICENSE-2.0 228 | // 229 | // Unless required by applicable law or agreed to in writing, software 230 | // distributed under the License is distributed on an "AS IS" BASIS, 231 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 232 | // See the License for the specific language governing permissions and 233 | // limitations under the License. 234 | 235 | /// The internal rendering buffer. 236 | struct Canvas { 237 | w: usize, 238 | h: usize, 239 | a: Vec, 240 | } 241 | 242 | impl Canvas { 243 | /// Create a completely uncovered canvas. 244 | fn new(w: u32, h: u32) -> Self { 245 | Self { 246 | w: w as usize, 247 | h: h as usize, 248 | a: vec![0.0; (w * h + 4) as usize], 249 | } 250 | } 251 | 252 | /// Return the accumulated coverage values. 253 | fn accumulate(self) -> Vec { 254 | let mut acc = 0.0; 255 | self.a[..self.w * self.h] 256 | .iter() 257 | .map(|c| { 258 | acc += c; 259 | (255.0 * acc.abs().min(1.0)) as u8 260 | }) 261 | .collect() 262 | } 263 | 264 | /// Add to a value in the accumulation buffer. 265 | fn add(&mut self, linestart: usize, x: i32, delta: f32) { 266 | if let Ok(x) = usize::try_from(x) { 267 | if let Some(a) = self.a.get_mut(linestart + x) { 268 | *a += delta; 269 | } 270 | } 271 | } 272 | 273 | /// Draw a straight line. 274 | fn line(&mut self, p0: Point, p1: Point) { 275 | if (p0.y - p1.y).abs() <= core::f32::EPSILON { 276 | return; 277 | } 278 | let (dir, p0, p1) = if p0.y < p1.y { (1.0, p0, p1) } else { (-1.0, p1, p0) }; 279 | let dxdy = (p1.x - p0.x) / (p1.y - p0.y); 280 | let mut x = p0.x; 281 | let y0 = p0.y as usize; 282 | if p0.y < 0.0 { 283 | x -= p0.y * dxdy; 284 | } 285 | for y in y0..self.h.min(p1.y.ceil() as usize) { 286 | let linestart = y * self.w; 287 | let dy = ((y + 1) as f32).min(p1.y) - (y as f32).max(p0.y); 288 | let xnext = x + dxdy * dy; 289 | let d = dy * dir; 290 | let (x0, x1) = if x < xnext { (x, xnext) } else { (xnext, x) }; 291 | let x0floor = x0.floor(); 292 | let x0i = x0floor as i32; 293 | let x1ceil = x1.ceil(); 294 | let x1i = x1ceil as i32; 295 | if x1i <= x0i + 1 { 296 | let xmf = 0.5 * (x + xnext) - x0floor; 297 | self.add(linestart, x0i, d - d * xmf); 298 | self.add(linestart, x0i + 1, d * xmf); 299 | } else { 300 | let s = (x1 - x0).recip(); 301 | let x0f = x0 - x0floor; 302 | let a0 = 0.5 * s * (1.0 - x0f) * (1.0 - x0f); 303 | let x1f = x1 - x1ceil + 1.0; 304 | let am = 0.5 * s * x1f * x1f; 305 | self.add(linestart, x0i, d * a0); 306 | if x1i == x0i + 2 { 307 | self.add(linestart, x0i + 1, d * (1.0 - a0 - am)); 308 | } else { 309 | let a1 = s * (1.5 - x0f); 310 | self.add(linestart, x0i + 1, d * (a1 - a0)); 311 | for xi in x0i + 2..x1i - 1 { 312 | self.add(linestart, xi, d * s); 313 | } 314 | let a2 = a1 + (x1i - x0i - 3) as f32 * s; 315 | self.add(linestart, x1i - 1, d * (1.0 - a2 - am)); 316 | } 317 | self.add(linestart, x1i, d * am); 318 | } 319 | x = xnext; 320 | } 321 | } 322 | 323 | /// Draw a quadratic bezier curve. 324 | fn quad(&mut self, p0: Point, p1: Point, p2: Point) { 325 | // How much does the curve deviate from a straight line? 326 | let devsq = hypot2(p0 - 2.0 * p1 + p2); 327 | 328 | // Check if the curve is already flat enough. 329 | if devsq < 0.333 { 330 | self.line(p0, p2); 331 | return; 332 | } 333 | 334 | // Estimate the required number of subdivisions for flattening. 335 | let tol = 3.0; 336 | let n = 1.0 + (tol * devsq).sqrt().sqrt().floor().min(30.0); 337 | let nu = n as usize; 338 | let step = n.recip(); 339 | 340 | // Flatten the curve. 341 | let mut t = 0.0; 342 | let mut p = p0; 343 | for _ in 0..nu.saturating_sub(1) { 344 | t += step; 345 | 346 | // Evaluate the curve at `t` using De Casteljau and draw a line from 347 | // the last point to the new evaluated point. 348 | let p01 = lerp(t, p0, p1); 349 | let p12 = lerp(t, p1, p2); 350 | let pt = lerp(t, p01, p12); 351 | self.line(p, pt); 352 | 353 | // Then set the evaluated point as the start point of the new line. 354 | p = pt; 355 | } 356 | 357 | // Draw a final line. 358 | self.line(p, p2); 359 | } 360 | } 361 | 362 | // Cubic to quad conversion adapted from here: 363 | // https://github.com/linebender/kurbo/blob/master/src/cubicbez.rs 364 | // 365 | // Copyright 2018 The kurbo Authors. 366 | // 367 | // Licensed under the Apache License, Version 2.0 (the "License"); 368 | // you may not use this file except in compliance with the License. 369 | // You may obtain a copy of the License at 370 | // 371 | // https://www.apache.org/licenses/LICENSE-2.0 372 | // 373 | // Unless required by applicable law or agreed to in writing, software 374 | // distributed under the License is distributed on an "AS IS" BASIS, 375 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 376 | // See the License for the specific language governing permissions and 377 | // limitations under the License. 378 | 379 | impl Canvas { 380 | /// Draw a cubic bezier curve. 381 | fn cubic(&mut self, p0: Point, p1: Point, p2: Point, p3: Point) { 382 | // How much does the curve deviate? 383 | let p1x2 = 3.0 * p1 - p0; 384 | let p2x2 = 3.0 * p2 - p3; 385 | let err = hypot2(p2x2 - p1x2); 386 | 387 | // Estimate the required number of subdivisions for conversion. 388 | let tol = 0.333; 389 | let max = 432.0 * tol * tol; 390 | let n = (err / max).powf(1.0 / 6.0).ceil().clamp(1.0, 20.0); 391 | let nu = n as usize; 392 | let step = n.recip(); 393 | let step4 = step / 4.0; 394 | 395 | // Compute the derivative of the cubic. 396 | let dp0 = 3.0 * (p1 - p0); 397 | let dp1 = 3.0 * (p2 - p1); 398 | let dp2 = 3.0 * (p3 - p2); 399 | 400 | // Convert the cubics to quadratics. 401 | let mut t = 0.0; 402 | let mut p = p0; 403 | let mut pd = dp0; 404 | for _ in 0..nu { 405 | t += step; 406 | 407 | // Evaluate the curve at `t` using De Casteljau. 408 | let p01 = lerp(t, p0, p1); 409 | let p12 = lerp(t, p1, p2); 410 | let p23 = lerp(t, p2, p3); 411 | let p012 = lerp(t, p01, p12); 412 | let p123 = lerp(t, p12, p23); 413 | let pt = lerp(t, p012, p123); 414 | 415 | // Evaluate the derivative of the curve at `t` using De Casteljau. 416 | let dp01 = lerp(t, dp0, dp1); 417 | let dp12 = lerp(t, dp1, dp2); 418 | let pdt = lerp(t, dp01, dp12); 419 | 420 | // Determine the control point of the quadratic. 421 | let pc = (p + pt) / 2.0 + step4 * (pd - pdt); 422 | 423 | // Draw the quadratic. 424 | self.quad(p, pc, pt); 425 | 426 | p = pt; 427 | pd = pdt; 428 | } 429 | } 430 | } 431 | 432 | /// Create a point. 433 | fn point(x: f32, y: f32) -> Point { 434 | Point { x, y } 435 | } 436 | 437 | /// Linearly interpolate between two points. 438 | fn lerp(t: f32, p1: Point, p2: Point) -> Point { 439 | Point { 440 | x: p1.x + t * (p2.x - p1.x), 441 | y: p1.y + t * (p2.y - p1.y), 442 | } 443 | } 444 | 445 | /// The squared distance of the point from the origin. 446 | fn hypot2(p: Point) -> f32 { 447 | p.x * p.x + p.y * p.y 448 | } 449 | 450 | /// A point in 2D. 451 | #[derive(Debug, Default, Copy, Clone)] 452 | struct Point { 453 | x: f32, 454 | y: f32, 455 | } 456 | 457 | impl Add for Point { 458 | type Output = Self; 459 | 460 | fn add(self, rhs: Self) -> Self::Output { 461 | Self { x: self.x + rhs.x, y: self.y + rhs.y } 462 | } 463 | } 464 | 465 | impl Sub for Point { 466 | type Output = Self; 467 | 468 | fn sub(self, rhs: Self) -> Self::Output { 469 | Self { x: self.x - rhs.x, y: self.y - rhs.y } 470 | } 471 | } 472 | 473 | impl Mul for f32 { 474 | type Output = Point; 475 | 476 | fn mul(self, rhs: Point) -> Self::Output { 477 | Point { x: self * rhs.x, y: self * rhs.y } 478 | } 479 | } 480 | 481 | impl Div for Point { 482 | type Output = Point; 483 | 484 | fn div(self, rhs: f32) -> Self::Output { 485 | Point { x: self.x / rhs, y: self.y / rhs } 486 | } 487 | } 488 | --------------------------------------------------------------------------------