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 { fn default() -> Self {
Self { Self {
word_spacing: Relative::new(0.25).into(), 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(), 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 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.matrix(1.0, 0.0, 0.0, 1.0, x, y);
text.show(&shaped.encode_glyphs_be()); text.show(&shaped.encode_glyphs_be());
} }

View File

@ -163,13 +163,17 @@ impl<'a> ParLayouter<'a> {
output.push_frame(pos, frame); 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. // Update metrics of the whole paragraph.
self.lines.push((self.lines_size.main, output, self.line_ruler)); self.lines.push((self.lines_size.main, output, self.line_ruler));
self.lines_size.main += full_size.main; 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.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) -= full_size.main;
*self.areas.current.get_mut(self.main) -= self.par.line_spacing;
// Reset metrics for the single line. // Reset metrics for the single line.
self.line_size = Gen::ZERO; 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 frame = Frame::new(Size::new(Length::ZERO, font_size));
let mut shaped = Shaped::new(FaceId::MAX, font_size); let mut shaped = Shaped::new(FaceId::MAX, font_size);
let mut offset = Length::ZERO; let mut offset = Length::ZERO;
let mut ascender = Length::ZERO;
let mut descender = Length::ZERO;
// Create an iterator with conditional direction. // Create an iterator with conditional direction.
let mut forwards = text.chars(); let mut forwards = text.chars();
@ -84,47 +86,56 @@ pub fn shape(
let query = FaceQuery { fallback: fallback.iter(), variant, c }; let query = FaceQuery { fallback: fallback.iter(), variant, c };
if let Some(id) = loader.query(query) { if let Some(id) = loader.query(query) {
let face = loader.face(id).get(); 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, Some(v) => v,
None => continue, None => continue,
}; };
// Flush the buffer if we change the font face. let units_per_em = f64::from(face.units_per_em().unwrap_or(1000));
if shaped.face != id && !shaped.text.is_empty() { let convert = |units| units / units_per_em * font_size;
let pos = Point::new(frame.size.width, Length::ZERO);
frame.push(pos, Element::Text(shaped)); // Flush the buffer and reset the metrics if we use a new font face.
frame.size.width += offset; if shaped.face != id {
shaped = Shaped::new(FaceId::MAX, font_size); place(&mut frame, shaped, offset, ascender, descender);
shaped = Shaped::new(id, font_size);
offset = Length::ZERO; offset = Length::ZERO;
ascender = convert(f64::from(face.ascender()));
descender = convert(f64::from(face.descender()));
} }
shaped.face = id;
shaped.text.push(c); shaped.text.push(c);
shaped.glyphs.push(glyph); shaped.glyphs.push(glyph);
shaped.offsets.push(offset); shaped.offsets.push(offset);
offset += width; offset += convert(f64::from(width));
} }
} }
// Flush the last buffered parts of the word. // Flush the last buffered parts of the word.
if !shaped.text.is_empty() { place(&mut frame, shaped, offset, ascender, descender);
let pos = Point::new(frame.size.width, Length::ZERO);
frame.push(pos, Element::Text(shaped));
frame.size.width += offset;
}
frame frame
} }
/// Looks up the glyph for `c` and returns its index alongside its width at the /// Look up the glyph for `c` and returns its index alongside its advance width.
/// given `size`. fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> {
fn lookup_glyph(face: &Face, c: char, size: Length) -> Option<(GlyphId, Length)> {
let glyph = face.glyph_index(c)?; let glyph = face.glyph_index(c)?;
let width = face.glyph_hor_advance(glyph)?;
// 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;
Some((glyph, width)) 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. // 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, ... // There are variables and they can take normal values like strings, ...
#let city = "Berlin" #let city = "Berlin"

View File

@ -1,15 +1,15 @@
// Test the box function. // Test the box function.
--- ---
#page("a7", flip: true) #page("a8", flip: true)
// Box with fixed width, should have text height. // 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. Sometimes there is no box.
// Box with fixed height, should span line. // 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. // Empty box with fixed width and height.
#box(width: 6cm, height: 12pt, color: #CB4CED) #box(width: 6cm, height: 12pt, color: #CB4CED)
@ -18,6 +18,6 @@ Sometimes there is no box.
#box(width: 2in, color: #ff0000) #box(width: 2in, color: #ff0000)
// These are in a row! // These are in a row!
#box(width: 1in, height: 10pt, color: #D6CD67) #box(width: 0.5in, height: 10pt, color: #D6CD67)
#box(width: 1in, height: 10pt, color: #EDD466) #box(width: 0.5in, height: 10pt, color: #EDD466)
#box(width: 1in, height: 10pt, color: #E3BE62) #box(width: 0.5in, height: 10pt, color: #E3BE62)

View File

@ -32,9 +32,8 @@
{12e1pt} \ {12e1pt} \
{2.5rad} \ {2.5rad} \
{45deg} \ {45deg} \
// Not in monospace via repr. // Not in monospace via repr.
#repr(45deg) #repr(45deg) \
--- ---
// Colors. // 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 units_per_em = face.units_per_em().unwrap_or(1000);
let x = (pos.x + offset).to_pt() as f32; 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 scale = (shaped.font_size / units_per_em as f64).to_pt() as f32;
let mut builder = WrappedPathBuilder(PathBuilder::new()); let mut builder = WrappedPathBuilder(PathBuilder::new());