Skip to content

Commit

Permalink
Add script runs and layout session
Browse files Browse the repository at this point in the history
This commit analyzes script runs based on Unicode data. It also starts
using a "LayoutSession" type, which will eventually support queries of
substrings.

Access to the layout is done through iterators, which will improve
flexibility. These iterators don't allocate. In addition, the iterator
now breaks out a run of glyphs of the same font, which will very likely
improve performance downstream.

It's still work in progress, but I wanted to checkpoint.

Progress towards #4
  • Loading branch information
raphlinus authored and SimonSapin committed Jul 9, 2019
1 parent 12f4cb6 commit 30090ca
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 10 deletions.
73 changes: 68 additions & 5 deletions examples/render.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Example program for testing rendering with skribo.

use std::env;
use std::fs::File;
use std::io::Write;

Expand All @@ -11,7 +12,7 @@ use font_kit::properties::Properties;
use font_kit::source::SystemSource;

use skribo::{
layout, layout_run, make_layout, FontCollection, FontFamily, FontRef, Layout, TextStyle,
layout, layout_run, make_layout, FontCollection, FontFamily, FontRef, Layout, LayoutSession, TextStyle,
};

#[cfg(target_family = "windows")]
Expand Down Expand Up @@ -119,6 +120,61 @@ impl SimpleSurface {
}
}
}

fn paint_layout_session(&mut self, layout: &LayoutSession, x: i32, y: i32) {
for run in layout.iter_all() {
let font = run.font();
let size = 32.0; // TODO: probably should get this from run
println!("run, font = {:?}", font);
for glyph in run.glyphs() {
let glyph_id = glyph.glyph_id;
let glyph_x = (glyph.offset.x as i32) + x;
let glyph_y = (glyph.offset.y as i32) + y;
let bounds = font
.font
.raster_bounds(
glyph_id,
size,
&Point2D::zero(),
HintingOptions::None,
RasterizationOptions::GrayscaleAa,
)
.unwrap();
println!(
"glyph {}, bounds {:?}, {},{}",
glyph_id, bounds, glyph_x, glyph_y
);
if !bounds.is_empty() {
let origin_adj = bounds.origin.to_f32();
let neg_origin = Point2D::new(-origin_adj.x, -origin_adj.y);
let mut canvas = Canvas::new(
// Not sure why we need to add the extra pixel of height, probably a rounding isssue.
// In any case, seems to get the job done (with CoreText rendering, anyway).
&Size2D::new(bounds.size.width as u32, 1 + bounds.size.height as u32),
Format::A8,
);
font
.font
.rasterize_glyph(
&mut canvas,
glyph_id,
// TODO(font-kit): this is missing anamorphic and skew features
size,
&neg_origin,
HintingOptions::None,
RasterizationOptions::GrayscaleAa,
)
.unwrap();
self.paint_from_canvas(
&canvas,
glyph_x + bounds.origin.x,
glyph_y - bounds.origin.y,
);
}
println!("glyph {} @ {:?}", glyph.glyph_id, glyph.offset);
}
}
}
}

fn make_collection() -> FontCollection {
Expand Down Expand Up @@ -190,12 +246,19 @@ fn main() {
)
.unwrap();

let text = "Hello हिन्दी";
//let layout = make_layout(&style, &font, text);
let mut args = std::env::args();
args.next();
let text = args
.next()
.unwrap_or("Hello हिन्दी".to_string());
//let layout = make_layout(&style, &font, &text);
let collection = make_collection();
let layout = layout(&style, &collection, text);
/*
let layout = layout(&style, &collection, &text);
println!("{:?}", layout);
*/
let layout = LayoutSession::create(&text, &style, &collection);
let mut surface = SimpleSurface::new(200, 50);
surface.paint_layout(&layout, 0, 35);
surface.paint_layout_session(&layout, 0, 35);
surface.write_pgm("out.pgm").unwrap();
}
63 changes: 60 additions & 3 deletions src/harfbuzz.rs → src/hb_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ use harfbuzz::sys::{
hb_buffer_get_glyph_positions, hb_face_create, hb_face_destroy, hb_face_reference, hb_face_t,
hb_font_create, hb_font_destroy, hb_position_t, hb_shape,
};
use harfbuzz::sys::{HB_SCRIPT_DEVANAGARI};
use harfbuzz::{Blob, Buffer, Direction, Language};
use harfbuzz::sys::{
hb_script_t, HB_MEMORY_MODE_READONLY, HB_SCRIPT_COMMON, HB_SCRIPT_DEVANAGARI,
HB_SCRIPT_INHERITED, HB_SCRIPT_UNKNOWN,
};

use crate::unicode_funcs::install_unicode_funcs;
use crate::session::LayoutFragment;
use crate::{FontRef};
use crate::unicode_funcs::{install_unicode_funcs, lookup_script};
use crate::{Glyph, Layout, TextStyle};

struct HbFace {
pub(crate) struct HbFace {
hb_face: *mut hb_face_t,
}

Expand Down Expand Up @@ -92,6 +96,59 @@ pub fn layout_run(style: &TextStyle, font: &FontRef, text: &str) -> Layout {
}
}

pub(crate) fn layout_fragment(
style: &TextStyle,
font: &FontRef,
script: hb_script_t,
text: &str,
) -> LayoutFragment {
let mut b = Buffer::new();
install_unicode_funcs(&mut b);
b.add_str(text);
b.set_direction(Direction::LTR);
b.set_script(script);
b.set_language(Language::from_string("en_US"));
let hb_face = HbFace::new(font);
unsafe {
let hb_font = hb_font_create(hb_face.hb_face);
hb_shape(hb_font, b.as_ptr(), std::ptr::null(), 0);
hb_font_destroy(hb_font);
let mut n_glyph = 0;
let glyph_infos = hb_buffer_get_glyph_infos(b.as_ptr(), &mut n_glyph);
println!("number of glyphs: {}", n_glyph);
let glyph_infos = std::slice::from_raw_parts(glyph_infos, n_glyph as usize);
let mut n_glyph_pos = 0;
let glyph_positions = hb_buffer_get_glyph_positions(b.as_ptr(), &mut n_glyph_pos);
let glyph_positions = std::slice::from_raw_parts(glyph_positions, n_glyph_pos as usize);
let mut total_adv = Vector2D::zero();
let mut glyphs = Vec::new();
let scale = style.size / (font.font.metrics().units_per_em as f32);
for (glyph, pos) in glyph_infos.iter().zip(glyph_positions.iter()) {
//println!("{:?} {:?}", glyph, pos);
let adv = Vector2D::new(pos.x_advance, pos.y_advance);
let adv_f = adv.to_f32() * scale;
let offset = Vector2D::new(pos.x_offset, pos.y_offset).to_f32() * scale;
let g = Glyph {
font: font.clone(),
glyph_id: glyph.codepoint,
offset: total_adv + offset,
};
total_adv += adv_f;
glyphs.push(g);
}

LayoutFragment {
//size: style.size,
substr_len: text.len(),
script,
glyphs: glyphs,
advance: total_adv,
hb_face: hb_face.clone(),
font: font.clone(),
}
}
}

#[allow(unused)]
fn float_to_fixed(f: f32) -> i32 {
(f * 65536.0 + 0.5).floor() as i32
Expand Down
8 changes: 6 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,29 @@ use euclid::Vector2D;
use font_kit::loaders::default::Font;

mod collection;
mod harfbuzz;
mod hb_layout;
mod session;
mod tables;
mod unicode_funcs;

pub use crate::collection::{FontCollection, FontFamily, FontRef};
pub use crate::harfbuzz::layout_run;
pub use crate::hb_layout::layout_run;
pub use crate::session::LayoutSession;

pub struct TextStyle {
// This should be either horiz and vert, or a 2x2 matrix
pub size: f32,
}

// TODO: remove this (in favor of LayoutSession, which might take over this name)
#[derive(Debug)]
pub struct Layout {
pub size: f32,
pub glyphs: Vec<Glyph>,
pub advance: Vector2D<f32>,
}

// TODO: remove this (in favor of GlyphInfo as a public API)
#[derive(Debug)]
pub struct Glyph {
pub font: FontRef,
Expand Down
163 changes: 163 additions & 0 deletions src/session.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! Retained layout that supports substring queries.

use harfbuzz::sys::{hb_script_t, HB_SCRIPT_COMMON, HB_SCRIPT_INHERITED, HB_SCRIPT_UNKNOWN};

use euclid::Vector2D;

use crate::hb_layout::{layout_fragment, HbFace};
use crate::unicode_funcs::lookup_script;
use crate::{FontCollection, FontRef, Glyph, TextStyle};

pub struct LayoutSession<'a> {
text: &'a str,
fragments: Vec<LayoutFragment>,
}

pub(crate) struct LayoutFragment {
// Length of substring covered by this fragment.
pub(crate) substr_len: usize,
pub(crate) script: hb_script_t,
pub(crate) advance: Vector2D<f32>,
pub(crate) glyphs: Vec<Glyph>,
pub(crate) hb_face: HbFace,
pub(crate) font: FontRef,
}

pub struct LayoutRangeIter<'a> {
// This probably wants to be a mut ref so we can stash resources in the session.
session: &'a LayoutSession<'a>,
offset: Vector2D<f32>,
fragment_ix: usize,
}

pub struct LayoutRun<'a> {
// This should potentially be in fragment (would make it easier to binary search)
offset: Vector2D<f32>,
fragment: &'a LayoutFragment,
}

pub struct RunIter<'a> {
offset: Vector2D<f32>,
fragment: &'a LayoutFragment,
glyph_ix: usize,
}

pub struct GlyphInfo {
pub glyph_id: u32,
pub offset: Vector2D<f32>,
}

impl<'a> LayoutSession<'a> {
pub fn create(
text: &'a str,
style: &TextStyle,
collection: &FontCollection,
) -> LayoutSession<'a> {
let mut i = 0;
let mut fragments = Vec::new();
while i < text.len() {
let (script, script_len) = get_script_run(&text[i..]);
let script_substr = &text[i..i + script_len];
for (range, font) in collection.itemize(script_substr) {
let fragment = layout_fragment(style, font, script, &script_substr[range]);
fragments.push(fragment);
}
i += script_len;
}
LayoutSession { text, fragments }
}

pub fn iter_all(&self) -> LayoutRangeIter {
LayoutRangeIter {
offset: Vector2D::zero(),
session: &self,
fragment_ix: 0,
}
}

// TODO: similar function as iter_all but takes a range (maybe subsumes iter_all, as
// it has the same behavior with [0..text.len()]).
}

impl<'a> Iterator for LayoutRangeIter<'a> {
type Item = LayoutRun<'a>;

fn next(&mut self) -> Option<LayoutRun<'a>> {
if self.fragment_ix == self.session.fragments.len() {
None
} else {
let fragment = &self.session.fragments[self.fragment_ix];
self.fragment_ix += 1;
let offset = self.offset;
self.offset += fragment.advance;
Some(LayoutRun { offset, fragment })
}
}
}

impl<'a> LayoutRun<'a> {
pub fn font(&self) -> &FontRef {
&self.fragment.font
}

pub fn glyphs(&self) -> RunIter<'a> {
RunIter {
offset: self.offset,
fragment: self.fragment,
glyph_ix: 0,
}
}
}

impl<'a> Iterator for RunIter<'a> {
type Item = GlyphInfo;

fn next(&mut self) -> Option<GlyphInfo> {
if self.glyph_ix == self.fragment.glyphs.len() {
None
} else {
let glyph = &self.fragment.glyphs[self.glyph_ix];
self.glyph_ix += 1;
Some(GlyphInfo {
glyph_id: glyph.glyph_id,
offset: self.offset + glyph.offset,
})
}
}
}

/// Figure out the script for the initial part of the buffer, and also
/// return the length of the run where that script is valid.
pub(crate) fn get_script_run(text: &str) -> (hb_script_t, usize) {
let mut char_iter = text.chars();
if let Some(cp) = char_iter.next() {
let mut current_script = lookup_script(cp.into());
let mut len = cp.len_utf8();
while let Some(cp) = char_iter.next() {
let script = lookup_script(cp.into());
if script != current_script {
if current_script == HB_SCRIPT_INHERITED || current_script == HB_SCRIPT_COMMON {
current_script = script;
} else if script != HB_SCRIPT_INHERITED && script != HB_SCRIPT_COMMON {
break;
}
}
len += cp.len_utf8();
}
if current_script == HB_SCRIPT_INHERITED {
current_script = HB_SCRIPT_COMMON;
}
(current_script, len)
} else {
(HB_SCRIPT_UNKNOWN, 0)
}
}

fn debug_script_runs(text: &str) {
let mut text_substr = text;
while !text_substr.is_empty() {
let (script, len) = get_script_run(text_substr);
println!("text {:?} script {:x}", &text_substr[..len], script);
text_substr = &text_substr[len..];
}
}

0 comments on commit 30090ca

Please sign in to comment.