Better line spacing calculations ↕

- Only add line spacing between lines. Previously, line spacing was added below
  every line, making `#box[word]` higher than just `word`.
- Compute box height of text as `ascender - descender` so that the full word is
  contained in the box.
This commit is contained in:
Laurenz 2021-03-10 10:20:01 +01:00
parent b2b8d37ce0
commit bbb9ed07ff
38 changed files with 51 additions and 37 deletions

View File

@ -93,7 +93,7 @@ impl Default for ParState {
fn default() -> Self {
Self {
word_spacing: Relative::new(0.25).into(),
line_spacing: Relative::new(0.2).into(),
line_spacing: Linear::ZERO,
par_spacing: Relative::new(0.5).into(),
}
}

View File

@ -195,7 +195,7 @@ impl<'a> PdfExporter<'a> {
}
let x = pos.x.to_pt() as f32;
let y = (page.size.height - pos.y - size).to_pt() as f32;
let y = (page.size.height - pos.y).to_pt() as f32;
text.matrix(1.0, 0.0, 0.0, 1.0, x, y);
text.show(&shaped.encode_glyphs_be());
}

View File

@ -163,13 +163,17 @@ impl<'a> ParLayouter<'a> {
output.push_frame(pos, frame);
}
// Add line spacing, but only between lines.
if !self.lines.is_empty() {
self.lines_size.main += self.par.line_spacing;
*self.areas.current.get_mut(self.main) -= self.par.line_spacing;
}
// Update metrics of the whole paragraph.
self.lines.push((self.lines_size.main, output, self.line_ruler));
self.lines_size.main += full_size.main;
self.lines_size.main += self.par.line_spacing;
self.lines_size.cross = self.lines_size.cross.max(full_size.cross);
*self.areas.current.get_mut(self.main) -= full_size.main;
*self.areas.current.get_mut(self.main) -= self.par.line_spacing;
// Reset metrics for the single line.
self.line_size = Gen::ZERO;

View File

@ -70,6 +70,8 @@ pub fn shape(
let mut frame = Frame::new(Size::new(Length::ZERO, font_size));
let mut shaped = Shaped::new(FaceId::MAX, font_size);
let mut offset = Length::ZERO;
let mut ascender = Length::ZERO;
let mut descender = Length::ZERO;
// Create an iterator with conditional direction.
let mut forwards = text.chars();
@ -84,47 +86,56 @@ pub fn shape(
let query = FaceQuery { fallback: fallback.iter(), variant, c };
if let Some(id) = loader.query(query) {
let face = loader.face(id).get();
let (glyph, width) = match lookup_glyph(face, c, font_size) {
let (glyph, width) = match lookup_glyph(face, c) {
Some(v) => v,
None => continue,
};
// Flush the buffer if we change the font face.
if shaped.face != id && !shaped.text.is_empty() {
let pos = Point::new(frame.size.width, Length::ZERO);
frame.push(pos, Element::Text(shaped));
frame.size.width += offset;
shaped = Shaped::new(FaceId::MAX, font_size);
let units_per_em = f64::from(face.units_per_em().unwrap_or(1000));
let convert = |units| units / units_per_em * font_size;
// Flush the buffer and reset the metrics if we use a new font face.
if shaped.face != id {
place(&mut frame, shaped, offset, ascender, descender);
shaped = Shaped::new(id, font_size);
offset = Length::ZERO;
ascender = convert(f64::from(face.ascender()));
descender = convert(f64::from(face.descender()));
}
shaped.face = id;
shaped.text.push(c);
shaped.glyphs.push(glyph);
shaped.offsets.push(offset);
offset += width;
offset += convert(f64::from(width));
}
}
// Flush the last buffered parts of the word.
if !shaped.text.is_empty() {
let pos = Point::new(frame.size.width, Length::ZERO);
frame.push(pos, Element::Text(shaped));
frame.size.width += offset;
}
place(&mut frame, shaped, offset, ascender, descender);
frame
}
/// Looks up the glyph for `c` and returns its index alongside its width at the
/// given `size`.
fn lookup_glyph(face: &Face, c: char, size: Length) -> Option<(GlyphId, Length)> {
/// Look up the glyph for `c` and returns its index alongside its advance width.
fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> {
let glyph = face.glyph_index(c)?;
// Determine the width of the char.
let units_per_em = face.units_per_em().unwrap_or(1000) as f64;
let width_units = face.glyph_hor_advance(glyph)? as f64;
let width = width_units / units_per_em * size;
let width = face.glyph_hor_advance(glyph)?;
Some((glyph, width))
}
/// Place shaped text into a frame.
fn place(
frame: &mut Frame,
shaped: Shaped,
offset: Length,
ascender: Length,
descender: Length,
) {
if !shaped.text.is_empty() {
let pos = Point::new(frame.size.width, ascender);
frame.push(pos, Element::Text(shaped));
frame.size.width += offset;
frame.size.height = frame.size.height.max(ascender - descender);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 886 B

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 506 B

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,5 +1,5 @@
// Configuration with `page` and `font` functions.
#page(width: 450pt, height: 380pt, margins: 1cm)
#page(width: 450pt, margins: 1cm)
// There are variables and they can take normal values like strings, ...
#let city = "Berlin"

View File

@ -1,15 +1,15 @@
// Test the box function.
---
#page("a7", flip: true)
#page("a8", flip: true)
// Box with fixed width, should have text height.
#box(width: 2cm, color: #9650D6)[A]
#box(width: 2cm, color: #9650D6)[Legal]
Sometimes there is no box.
// Box with fixed height, should span line.
#box(height: 2cm, width: 100%, color: #734CED)[B]
#box(height: 1cm, width: 100%, color: #734CED)[B]
// Empty box with fixed width and height.
#box(width: 6cm, height: 12pt, color: #CB4CED)
@ -18,6 +18,6 @@ Sometimes there is no box.
#box(width: 2in, color: #ff0000)
// These are in a row!
#box(width: 1in, height: 10pt, color: #D6CD67)
#box(width: 1in, height: 10pt, color: #EDD466)
#box(width: 1in, height: 10pt, color: #E3BE62)
#box(width: 0.5in, height: 10pt, color: #D6CD67)
#box(width: 0.5in, height: 10pt, color: #EDD466)
#box(width: 0.5in, height: 10pt, color: #E3BE62)

View File

@ -32,9 +32,8 @@
{12e1pt} \
{2.5rad} \
{45deg} \
// Not in monospace via repr.
#repr(45deg)
#repr(45deg) \
---
// Colors.

View File

@ -393,7 +393,7 @@ fn draw_text(env: &Env, canvas: &mut Canvas, pos: Point, shaped: &Shaped) {
let units_per_em = face.units_per_em().unwrap_or(1000);
let x = (pos.x + offset).to_pt() as f32;
let y = (pos.y + shaped.font_size).to_pt() as f32;
let y = pos.y.to_pt() as f32;
let scale = (shaped.font_size / units_per_em as f64).to_pt() as f32;
let mut builder = WrappedPathBuilder(PathBuilder::new());