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.
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 716 B After Width: | Height: | Size: 746 B |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 886 B After Width: | Height: | Size: 929 B |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 506 B After Width: | Height: | Size: 522 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 807 B After Width: | Height: | Size: 827 B |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 814 B After Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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());
|
||||||
|