Commit 683c55e8 authored by Alex Butler's avatar Alex Butler
Browse files

Merge branch 'otf' into 'master'

Support .otf fonts by switching to ttf-parser & ab_glyph_rasterizer

Closes #137

See merge request redox-os/rusttype!151
parents 767a7275 069cec55
......@@ -43,7 +43,7 @@ test:stable:gpu_cache:
script:
- cargo +stable test --features 'gpu_cache'
# Heavier testing using rusttype-dev
# Heavier testing using "dev"
test:dev:
stage: test
variables:
......
## Unreleased
* Major rework to use crates **ttf-parser** & **ab_glyph_rasterizer** to respectively read and render OpenType .oft format fonts.
* Remove dependencies **approx**, **stb_truetype** & **ordered-float** along with in-crate rasterization code.
* Strip back some non-vital API functionality.
- Remove support for `.standalone()` variants which are sparsely used.
- Remove some functions that didn't immediately translate to ttf-parser. Please raise issues to re-add any you relied on via the new stack.
## 0.8.3
* Remove arrayvec dependency.
* Add `Default` implementations for geometry structs.
......
......@@ -26,9 +26,8 @@ exclude = ["/dev/**"]
features = ["gpu_cache"]
[dependencies]
stb_truetype = { version = "0.3.1", default-features = false }
ordered-float = { version = "1", default-features = false }
approx = { version = "0.3", default-features = false }
ttf-parser = { version = "0.5", default-features = false }
ab_glyph_rasterizer = { version = "0.1", default-features = false }
libm = { version = "0.2.1", default-features = false, optional = true }
......@@ -41,14 +40,15 @@ crossbeam-utils = { version = "0.7", optional = true }
num_cpus = { version = "1.0", optional = true }
[dev-dependencies]
# don't add any, instead use ./dev
# don't add any more, instead use ./dev
approx = { version = "0.3", default-features = false }
[features]
default = ["std", "has-atomics"]
default = ["std"]
# Activates usage of std.
std = ["has-atomics", "stb_truetype/std"]
std = ["has-atomics", "ttf-parser/default", "ab_glyph_rasterizer/default"]
# Uses libm when not using std. This needs to be active in that case.
libm-math = ["libm", "stb_truetype/libm"]
libm-math = ["libm", "ab_glyph_rasterizer/libm"]
# Some targets don't have atomics, this activates usage of Arc<T> instead of Rc<T>.
has-atomics = []
# Adds `gpu_cache` module
......
......@@ -6,26 +6,22 @@ RustType is a pure Rust alternative to libraries like FreeType.
The current capabilities of RustType:
* Reading TrueType formatted fonts and font collections. This includes `*.ttf`
as well as a subset of `*.otf` font files.
* Reading OpenType formatted fonts and font collections. This includes `*.ttf`
as well as `*.otf` font files.
* Retrieving glyph shapes and commonly used properties for a font and its glyphs.
* Laying out glyphs horizontally using horizontal and vertical metrics, and
glyph-pair-specific kerning.
* Rasterising glyphs with sub-pixel positioning using an accurate analytical
algorithm (not based on sampling).
* Managing a font cache on the GPU with the `gpu_cache` module. This keeps
recently used glyph renderings
in a dynamic cache in GPU memory to minimise texture uploads per-frame. It
also allows you keep the draw call count for text very low, as all glyphs are
kept in one GPU texture.
recently used glyph renderings in a dynamic cache in GPU memory to minimise
texture uploads per-frame. It also allows you keep the draw call count for
text very low, as all glyphs are kept in one GPU texture.
Notable things that RustType does not support *yet*:
* OpenType formatted fonts that are not just TrueType fonts (OpenType is a
superset of TrueType). Notably there is no support yet for cubic Bezier curves
used in glyphs.
* Font hinting.
* Ligatures of any kind
* Ligatures of any kind.
* Some less common TrueType sub-formats.
* Right-to-left and vertical text layout.
......@@ -34,28 +30,21 @@ Heavier examples, tests & benchmarks are in the `./dev` directory. This avoids d
Run all tests with `cargo test --all --all-features`.
Run examples with `cargo run --example <NAME> -p rusttype-dev`
Run examples with `cargo run --example <NAME> -p dev`
## Getting Started
To hit the ground running with RustType, look at `dev/examples/simple.rs`
To hit the ground running with RustType, look at `dev/examples/ascii.rs`
supplied with the crate. It demonstrates loading a font file, rasterising an
arbitrary string, and displaying the result as ASCII art. If you prefer to just
look at the documentation, the entry point for loading fonts is
`FontCollection`, from which you can access individual fonts, then their glyphs.
look at the documentation, the entry point for loading fonts is `Font`,
from which you can access individual fonts, then their glyphs.
## Future Plans
The initial motivation for the project was to provide easy-to-use font rendering for games.
There are numerous avenues for improving RustType. Ideas:
* Some form of hinting for improved legibility at small font sizes.
* Replacing the dependency on
[stb_truetype-rs](https://gitlab.redox-os.org/redox-os/stb_truetype-rs)
(a translation of [stb_truetype.h](https://github.com/nothings/stb/blob/master/stb_truetype.h)),
with OpenType font loading written in idiomatic Rust.
* Add support for cubic curves in OpenType fonts.
* Extract the rasterisation code into a separate vector graphics rendering crate.
* Support for some common forms of ligatures.
* And, eventually, support for embedded right-to-left Unicode text.
......
[package]
name = "rusttype-dev"
name = "dev"
version = "0.1.0"
description = "Tests, examples & benchmarks avoiding dependency feature bleed"
edition = "2018"
......@@ -7,11 +7,12 @@ publish = false
[dev-dependencies]
rusttype = { path = "../", features = ["gpu_cache"] }
glium = "0.26"
glium = "0.27"
image = { version = "0.23", default-features = false, features = ["png"] }
once_cell = "1"
blake2 = "0.8"
criterion = "0.3"
ttf-parser = "0.5"
[[bench]]
name = "cache"
......
......@@ -67,7 +67,7 @@ static FONTS: Lazy<Vec<Font<'static>>> = Lazy::new(|| {
include_bytes!("../fonts/opensans/OpenSans-Italic.ttf") as &[u8],
]
.into_iter()
.map(|bytes| Font::from_bytes(bytes).unwrap())
.map(|bytes| Font::try_from_bytes(bytes).unwrap())
.collect()
});
......
use blake2::{Blake2s, Digest};
use criterion::{criterion_group, criterion_main, Criterion};
use once_cell::sync::Lazy;
use rusttype::*;
static DEJA_VU_MONO: Lazy<Font<'static>> = Lazy::new(|| {
Font::from_bytes(include_bytes!("../fonts/dejavu/DejaVuSansMono.ttf") as &[u8]).unwrap()
Font::try_from_bytes(include_bytes!("../fonts/dejavu/DejaVuSansMono.ttf") as &[u8]).unwrap()
});
static OPEN_SANS_ITALIC: Lazy<Font<'static>> = Lazy::new(|| {
Font::from_bytes(include_bytes!("../fonts/opensans/OpenSans-Italic.ttf") as &[u8]).unwrap()
Font::try_from_bytes(include_bytes!("../fonts/opensans/OpenSans-Italic.ttf") as &[u8]).unwrap()
});
static EXO2_OFT: Lazy<Font<'static>> = Lazy::new(|| {
Font::try_from_bytes(include_bytes!("../fonts/Exo2-Light.otf") as &[u8]).unwrap()
});
fn bench_draw_big_biohazard(c: &mut Criterion) {
fn draw_big_biohazard(c: &mut Criterion) {
let glyph = DEJA_VU_MONO
.glyph('☣')
.scaled(Scale::uniform(600.0))
......@@ -33,15 +35,10 @@ fn bench_draw_big_biohazard(c: &mut Criterion) {
target[WIDTH * y + x] = (alpha * 255.0) as u8;
})
});
// verify the draw result against static reference hash
assert_eq!(
format!("{:x}", Blake2s::digest(&target)),
"8e3927a33c6d563d45f82fb9620dea8036274b403523a2e98cd5f93eafdb2125"
);
});
}
fn bench_draw_w(c: &mut Criterion) {
fn draw_w(c: &mut Criterion) {
let glyph = DEJA_VU_MONO
.glyph('w')
.scaled(Scale::uniform(16.0))
......@@ -64,15 +61,10 @@ fn bench_draw_w(c: &mut Criterion) {
target[WIDTH * y + x] = (alpha * 255.0) as u8;
})
});
// verify the draw result against static reference hash
assert_eq!(
format!("{:x}", Blake2s::digest(&target)),
"c0e795601e3412144d1bfdc0cd94d9507aa9775a0f0f4f9862fe7ec7e83d7684"
);
});
}
fn bench_draw_iota(c: &mut Criterion) {
fn draw_iota(c: &mut Criterion) {
let glyph = OPEN_SANS_ITALIC
.glyph('ΐ')
.scaled(Scale::uniform(60.0))
......@@ -95,19 +87,41 @@ fn bench_draw_iota(c: &mut Criterion) {
target[WIDTH * y + x] = (alpha * 255.0) as u8;
})
});
// verify the draw result against static reference hash
assert_eq!(
format!("{:x}", Blake2s::digest(&target)),
"cdad348e38263a13f68ae41a95ce3b900d2881375a745232309ebd568a27cd4c"
);
});
}
fn draw_oft_tailed_e(c: &mut Criterion) {
let glyph = EXO2_OFT
.glyph('ę')
.scaled(Scale::uniform(300.0))
.positioned(point(0.0, 0.0));
const WIDTH: usize = 106;
const HEIGHT: usize = 183;
let bounds = glyph.pixel_bounding_box().unwrap();
assert_eq!(
(bounds.width() as usize, bounds.height() as usize),
(WIDTH, HEIGHT)
);
let mut target = [0u8; WIDTH * HEIGHT];
c.bench_function("draw_oft_tailed_e", |b| {
b.iter(|| {
glyph.draw(|x, y, alpha| {
let (x, y) = (x as usize, y as usize);
target[WIDTH * y + x] = (alpha * 255.0) as u8;
})
});
});
}
criterion_group!(
benches,
bench_draw_big_biohazard,
bench_draw_w,
bench_draw_iota,
draw_benches,
draw_big_biohazard,
draw_w,
draw_iota,
draw_oft_tailed_e,
);
criterion_main!(benches);
criterion_main!(draw_benches);
......@@ -10,7 +10,8 @@ fn bench_layout_a_sentence(c: &mut Criterion) {
clause and sometimes one or more subordinate clauses.";
let font =
Font::from_bytes(include_bytes!("../fonts/opensans/OpenSans-Italic.ttf") as &[u8]).unwrap();
Font::try_from_bytes(include_bytes!("../fonts/opensans/OpenSans-Italic.ttf") as &[u8])
.unwrap();
c.bench_function("layout_a_sentence", |b| {
let mut glyphs = vec![];
......@@ -39,8 +40,41 @@ fn bench_layout_a_sentence(c: &mut Criterion) {
"c2a3483ddf5598ec869440c62d17efa5a4fe72f9893bcc05dd17be2adcaa7629"
);
});
let font = Font::try_from_vec(include_bytes!("../fonts/opensans/OpenSans-Italic.ttf").to_vec())
.unwrap();
c.bench_function("layout_a_sentence (vec)", |b| {
let mut glyphs = vec![];
b.iter(|| {
glyphs.clear();
glyphs.extend(font.layout(SENTENCE, Scale::uniform(25.0), point(100.0, 25.0)))
});
// verify the layout result against static reference hash
let mut hash = Blake2s::default();
for g in glyphs {
write!(
hash,
"{id}:{scale_x}:{scale_y}:{pos_x}:{pos_y}",
id = g.id().0,
scale_x = g.scale().x,
scale_y = g.scale().y,
pos_x = g.position().x,
pos_y = g.position().y,
)
.unwrap();
}
assert_eq!(
format!("{:x}", hash.result()),
"c2a3483ddf5598ec869440c62d17efa5a4fe72f9893bcc05dd17be2adcaa7629"
);
});
}
criterion_group!(benches, bench_layout_a_sentence);
criterion_group!(
name = benches;
config = Criterion::default().sample_size(400);
targets = bench_layout_a_sentence);
criterion_main!(benches);
use rusttype::{point, FontCollection, PositionedGlyph, Scale};
//! Render example where each glyph pixel is output as an ascii character.
use rusttype::{point, Font, Scale};
use std::io::Write;
fn main() {
let font_data = include_bytes!("../fonts/wqy-microhei/WenQuanYiMicroHei.ttf");
let collection = FontCollection::from_bytes(font_data as &[u8]).unwrap_or_else(|e| {
panic!("error constructing a FontCollection from bytes: {}", e);
});
let font = collection
.into_font() // only succeeds if collection consists of one font
.unwrap_or_else(|e| {
panic!("error turning FontCollection into a Font: {}", e);
});
let font = if let Some(font_path) = std::env::args().nth(1) {
let font_path = std::env::current_dir().unwrap().join(font_path);
let data = std::fs::read(&font_path).unwrap();
Font::try_from_vec(data).unwrap_or_else(|| {
panic!(format!(
"error constructing a Font from data at {:?}",
font_path
));
})
} else {
eprintln!("No font specified ... using WenQuanYiMicroHei.ttf");
let font_data = include_bytes!("../fonts/wqy-microhei/WenQuanYiMicroHei.ttf");
Font::try_from_bytes(font_data as &[u8]).expect("error constructing a Font from bytes")
};
// Desired font pixel height
let height: f32 = 12.4; // to get 80 chars across (fits most terminals); adjust as desired
......@@ -31,7 +37,7 @@ fn main() {
let offset = point(0.0, v_metrics.ascent);
// Glyphs to draw for "RustType". Feel free to try other strings.
let glyphs: Vec<PositionedGlyph<'_>> = font.layout("RustType", scale, offset).collect();
let glyphs: Vec<_> = font.layout("RustType", scale, offset).collect();
// Find the most visually pleasing width to display
let width = glyphs
......
......@@ -56,7 +56,7 @@ fn main() -> Result<(), Box<dyn Error>> {
}
let font_data = include_bytes!("../fonts/wqy-microhei/WenQuanYiMicroHei.ttf");
let font: Font<'static> = Font::from_bytes(font_data as &[u8])?;
let font = Font::try_from_bytes(font_data as &[u8]).unwrap();
let window = glium::glutin::window::WindowBuilder::new()
.with_inner_size(glium::glutin::dpi::PhysicalSize::new(512, 512))
......
......@@ -5,7 +5,7 @@ fn main() {
// Load the font
let font_data = include_bytes!("../fonts/wqy-microhei/WenQuanYiMicroHei.ttf");
// This only succeeds if collection consists of one font
let font = Font::from_bytes(font_data as &[u8]).expect("Error constructing Font");
let font = Font::try_from_bytes(font_data as &[u8]).expect("Error constructing Font");
// The font size to use
let scale = Scale::uniform(32.0);
......
use rusttype::*;
#[test]
fn static_lazy_shared_bytes() {
use once_cell::sync::Lazy;
static FONT_BYTES: Lazy<Vec<u8>> = Lazy::new(|| vec![0, 1, 2, 3]);
let shared_bytes: SharedBytes<'static> = (&*FONT_BYTES).into();
assert_eq!(&*shared_bytes, &[0, 1, 2, 3]);
}
......@@ -4,7 +4,7 @@ static ROBOTO_REGULAR: &[u8] = include_bytes!("../fonts/Roboto-Regular.ttf");
#[test]
fn consistent_bounding_box_subpixel_size_proxy() {
let font = Font::from_bytes(ROBOTO_REGULAR).unwrap();
let font = Font::try_from_bytes(ROBOTO_REGULAR).unwrap();
let height_at_y = |y| {
font.glyph('s')
.scaled(rusttype::Scale::uniform(20.0))
......@@ -15,18 +15,3 @@ fn consistent_bounding_box_subpixel_size_proxy() {
};
assert_eq!(height_at_y(50.833_336), height_at_y(110.833_336));
}
#[test]
fn consistent_bounding_box_subpixel_size_standalone() {
let font = Font::from_bytes(ROBOTO_REGULAR).unwrap();
let height_at_y = |y| {
font.glyph('s')
.standalone()
.scaled(rusttype::Scale::uniform(20.0))
.positioned(rusttype::Point { x: 0.0, y })
.pixel_bounding_box()
.unwrap()
.height()
};
assert_eq!(height_at_y(50.833_336), height_at_y(110.833_336));
}
use rusttype::*;
#[test]
fn move_and_use() {
let owned_data = include_bytes!("../fonts/opensans/OpenSans-Italic.ttf").to_vec();
let pin_font = Font::try_from_vec(owned_data).unwrap();
let ascent = pin_font.v_metrics_unscaled().ascent;
// force a move
let moved = Box::new(pin_font);
assert_eq!(moved.v_metrics_unscaled().ascent, ascent);
}
dev/tests/reference_big_biohazard.png

9.85 KB | W: | H:

dev/tests/reference_big_biohazard.png

7.27 KB | W: | H:

dev/tests/reference_big_biohazard.png
dev/tests/reference_big_biohazard.png
dev/tests/reference_big_biohazard.png
dev/tests/reference_big_biohazard.png
  • 2-up
  • Swipe
  • Onion skin
dev/tests/reference_iota.png

540 Bytes | W: | H:

dev/tests/reference_iota.png

403 Bytes | W: | H:

dev/tests/reference_iota.png
dev/tests/reference_iota.png
dev/tests/reference_iota.png
dev/tests/reference_iota.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -4,10 +4,13 @@ use rusttype::{point, Font, Scale, ScaledGlyph};
use std::io::Cursor;
static DEJA_VU_MONO: Lazy<Font<'static>> = Lazy::new(|| {
Font::from_bytes(include_bytes!("../fonts/dejavu/DejaVuSansMono.ttf") as &[u8]).unwrap()
Font::try_from_bytes(include_bytes!("../fonts/dejavu/DejaVuSansMono.ttf") as &[u8]).unwrap()
});
static OPEN_SANS_ITALIC: Lazy<Font<'static>> = Lazy::new(|| {
Font::from_bytes(include_bytes!("../fonts/opensans/OpenSans-Italic.ttf") as &[u8]).unwrap()
Font::try_from_bytes(include_bytes!("../fonts/opensans/OpenSans-Italic.ttf") as &[u8]).unwrap()
});
static EXO2_OFT: Lazy<Font<'static>> = Lazy::new(|| {
Font::try_from_bytes(include_bytes!("../fonts/Exo2-Light.otf") as &[u8]).unwrap()
});
fn draw_luma_alpha(glyph: ScaledGlyph<'_>) -> image::GrayAlphaImage {
......@@ -113,3 +116,33 @@ fn render_to_reference_iota() {
}
}
}
/// Render a 300px 'ę' character that uses cubic beziers & require it to match the reference.
#[test]
fn render_to_reference_oft_tailed_e() {
let new_image = draw_luma_alpha(EXO2_OFT.glyph('ę').scaled(Scale::uniform(300.0)));
// save the new render for manual inspection
new_image.save("../target/otf_tailed_e.png").unwrap();
let reference = image::load(
Cursor::new(include_bytes!("reference_otf_tailed_e.png") as &[u8]),
image::ImageFormat::Png,
)
.expect("!image::load")
.to_luma_alpha();
assert_eq!(reference.dimensions(), new_image.dimensions());
for y in 0..reference.height() {
for x in 0..reference.width() {
assert_eq!(
reference.get_pixel(x, y),
new_image.get_pixel(x, y),
"unexpected alpha difference at ({}, {})",
x,
y
);
}
}
}
use crate::{Glyph, GlyphIter, IntoGlyphId, LayoutIter, Point, Scale, VMetrics};
use core::fmt;
#[cfg(not(feature = "has-atomics"))]
use alloc::rc::Rc as Arc;
#[cfg(feature = "has-atomics")]
use alloc::sync::Arc;
/// A single font. This may or may not own the font data.
///
/// # Lifetime
/// The lifetime reflects the font data lifetime. `Font<'static>` covers most
/// cases ie both dynamically loaded owned data and for referenced compile time
/// font data.
///
/// # Example
///
/// ```
/// # use rusttype::Font;
/// # fn example() -> Option<()> {
/// let font_data: &[u8] = include_bytes!("../dev/fonts/dejavu/DejaVuSansMono.ttf");
/// let font: Font<'static> = Font::try_from_bytes(font_data)?;
///
/// let owned_font_data: Vec<u8> = font_data.to_vec();
/// let from_owned_font: Font<'static> = Font::try_from_vec(owned_font_data)?;
/// # Some(())
/// # }
/// ```
#[derive(Clone)]
pub enum Font<'a> {
Ref(Arc<ttf_parser::Font<'a>>),
Owned(Arc<owned_ttf_parser::OwnedFont>),
}
impl fmt::Debug for Font<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Font")
}
}
impl Font<'_> {
/// Creates a Font from byte-slice data.
///
/// Returns `None` for invalid data.
pub fn try_from_bytes(bytes: &[u8]) -> Option<Font<'_>> {
Self::try_from_bytes_and_index(bytes, 0)
}
/// Creates a Font from byte-slice data & a font collection `index`.
///
/// Returns `None` for invalid data.
pub fn try_from_bytes_and_index(bytes: &[u8], index: u32) -> Option<Font<'_>> {
let inner = Arc::new(ttf_parser::Font::from_data(bytes, index)?);
Some(Font::Ref(inner))
}
}
impl<'font> Font<'font> {
#[inline]
pub(crate) fn inner(&self) -> &ttf_parser::Font<'_> {
match self {
Self::Ref(f) => f,
Self::Owned(f) => f.inner_ref(),
}
}
/// The "vertical metrics" for this font at a given scale. These metrics are
/// shared by all of the glyphs in the font. See `VMetrics` for more detail.
pub fn v_metrics(&self, scale: Scale) -> VMetrics {
self.v_metrics_unscaled() * self.scale_for_pixel_height(scale.y)
}
/// Get the unscaled VMetrics for this font, shared by all glyphs.
/// See `VMetrics` for more detail.
pub fn v_metrics_unscaled(&self) -> VMetrics {
let font = self.inner();
VMetrics {
ascent: font.ascender() as f32,
descent: font.descender() as f32,
line_gap: font.line_gap() as f32,
}
}
/// Returns the units per EM square of this font
pub fn units_per_em(&self) -> u16 {
self.inner()
.units_per_em()
.expect("Invalid font units_per_em")
}
/// The number of glyphs present in this font. Glyph identifiers for this
/// font will always be in the range `0..self.glyph_count()`
pub fn glyph_count(&self) -> usize {
self.inner().number_of_glyphs() as _
}
/// Returns the corresponding glyph for a Unicode code point or a glyph id
/// for this font.
///
/// If `id` is a `GlyphId`, it must be valid for this font; otherwise, this
/// function panics. `GlyphId`s should always be produced by looking up some
/// other sort of designator (like a Unicode code point) in a font, and
/// should only be used to index the font they were produced for.
///
/// Note that code points without corresponding glyphs in this font map to
/// the ".notdef" glyph, glyph 0.
pub fn glyph<C: IntoGlyphId>(&self, id: C) -> Glyph<'font> {
let gid = id.into_glyph_id(self);
assert!((gid.0 as usize) < self.glyph_count());
// font clone either a reference clone, or arc clone
Glyph {
font: self.clone(),
id: gid,
}
}
/// A convenience function.
///
/// Returns an iterator that produces the glyphs corresponding to the code
/// points or glyph ids produced by the given iterator `itr`.
///
/// This is equivalent in behaviour to `itr.map(|c| font.glyph(c))`.
pub fn glyphs_for<I: Iterator>(&self, itr: I) -> GlyphIter<'_, I>
where
I::Item: IntoGlyphId,
{