~ireas/public-inbox

merge-rs: Calculate line spacing in paragraphs and remove left bearing on first character of lines v1 APPLIED

ap4sc: 1
 Calculate line spacing in paragraphs and remove left bearing on first character of lines

 4 files changed, 145 insertions(+), 34 deletions(-)
#529227 archlinux-msrv.yml failed
#529228 archlinux.yml failed
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~ireas/public-inbox/patches/23401/mbox | git am -3
Learn more about email & git

[PATCH merge-rs] Calculate line spacing in paragraphs and remove left bearing on first character of lines Export this patch

---
 src/elements.rs | 33 ++++++++++++++++-------
 src/fonts.rs    | 71 +++++++++++++++++++++++++++++++++++++++++++++----
 src/render.rs   | 59 ++++++++++++++++++++++++++--------------
 src/style.rs    | 16 +++++++++++
 4 files changed, 145 insertions(+), 34 deletions(-)

diff --git a/src/elements.rs b/src/elements.rs
index 1ff2dea..b7205eb 100644
--- a/src/elements.rs
+++ b/src/elements.rs
@@ -48,6 +48,7 @@ use std::iter;
use std::mem;

use crate::error::{Error, ErrorKind};
use crate::fonts;
use crate::render;
use crate::style::{Style, StyledString};
use crate::wrap;
@@ -197,9 +198,7 @@ impl Element for Text {
/// strings to this paragraph.  Besides the styling of the text (see [`Style`][]), you can also set
/// an [`Alignment`][] for the paragraph.
///
/// Note that the line height and spacing is currently calculated based on the style of the entire
/// paragraph.  If the font family or font size is changed in the [`Style`][] settings for a
/// string, the line height and spacing might be incorrect.
/// The line height and spacing are calculated based on the style of each string.
///
/// # Examples
///
@@ -315,14 +314,28 @@ impl Element for Paragraph {
            self.words = wrap::Words::new(mem::take(&mut self.text)).collect();
        }

        let height = style.line_height(&context.font_cache);
        let p_metrics = style.metrics(&context.font_cache);
        let words = self.words.iter().map(Into::into);
        let mut rendered_len = 0;
        for (line, delta) in wrap::Wrapper::new(words, context, area.size().width) {
            let width = line.iter().map(|s| s.width(&context.font_cache)).sum();
            let position = Position::new(self.get_offset(width, area.size().width), 0);
            // TODO: calculate the maximum line height
            if let Some(mut section) = area.text_section(&context.font_cache, position, style) {
            // Calculate the maximum line height
            let mut max_line_height = Mm::from(0.0);
            for s in &line {
                let line_height = s.style.metrics(&context.font_cache).line_height();
                if line_height > max_line_height {
                    max_line_height = line_height;
                }
            }
            let line_metrics = fonts::Metrics::new(max_line_height, p_metrics.glyph_height());
            let position = Position::new(
                self.get_offset(width, area.size().width),
                line_metrics.line_height(),
            );

            if let Some(mut section) =
                area.text_section(&context.font_cache, position, line_metrics)
            {
                for s in line {
                    section.print_str(&s.s, s.style)?;
                    rendered_len += s.s.len();
@@ -332,8 +345,10 @@ impl Element for Paragraph {
                result.has_more = true;
                break;
            }
            result.size = result.size.stack_vertical(Size::new(width, height));
            area.add_offset(Position::new(0, height));
            result.size = result
                .size
                .stack_vertical(Size::new(width, line_metrics.line_height()));
            area.add_offset(Position::new(0, line_metrics.line_height()));
        }

        // Remove the rendered data from self.words so that we don’t render it again on the next
diff --git a/src/fonts.rs b/src/fonts.rs
index efcbb6d..7378c7d 100644
--- a/src/fonts.rs
+++ b/src/fonts.rs
@@ -377,15 +377,31 @@ impl Font {
    ///
    /// [`FontCache`]: struct.FontCache.html
    pub fn char_width(&self, font_cache: &FontCache, c: char, font_size: u8) -> Mm {
        let advance_width = font_cache
        let advance_width = self.char_h_metrics(font_cache, c).advance_width;
        Mm::from(printpdf::Pt(f64::from(
            advance_width * f32::from(font_size),
        )))
    }

    /// Returns the width of the empty space between the origin of the glyph bounding
    /// box and the leftmost edge of the character, for a given font and font size.
    ///
    /// The given [`FontCache`][] must be the font cache that loaded this font.
    ///
    /// [`FontCache`]: struct.FontCache.html
    pub fn char_left_side_bearing(&self, font_cache: &FontCache, c: char, font_size: u8) -> Mm {
        let left_side_bearing = self.char_h_metrics(font_cache, c).left_side_bearing;
        Mm::from(printpdf::Pt(f64::from(
            left_side_bearing * f32::from(font_size),
        )))
    }

    fn char_h_metrics(&self, font_cache: &FontCache, c: char) -> rusttype::HMetrics {
        font_cache
            .get_rt_font(*self)
            .glyph(c)
            .scaled(self.scale)
            .h_metrics()
            .advance_width;
        Mm::from(printpdf::Pt(f64::from(
            advance_width * f32::from(font_size),
        )))
    }

    /// Returns the width of a string with this font and the given font size.
@@ -449,6 +465,14 @@ impl Font {
            .map(|g| g.id().0 as u16)
            .collect()
    }

    /// Calculate the metrics of a given font size for this font.
    pub fn metrics(&self, font_size: u8) -> Metrics {
        Metrics::new(
            self.line_height * f64::from(font_size),
            self.glyph_height * f64::from(font_size),
        )
    }
}

fn from_file(
@@ -488,3 +512,40 @@ pub fn from_files(
        bold_italic: from_file(dir, name, FontStyle::BoldItalic, builtin)?,
    })
}

/// The metrics of a font: glyph height and line height.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Metrics {
    line_height: Mm,
    glyph_height: Mm,
}

impl Metrics {
    /// Create a new metrics instance with the given heights.
    pub fn new(line_height: Mm, glyph_height: Mm) -> Metrics {
        Metrics {
            line_height,
            glyph_height,
        }
    }

    /// Line height setter.
    pub fn set_line_height(&mut self, line_height: Mm) {
        self.line_height = line_height;
    }

    /// Glyph height setter.
    pub fn set_glyph_height(&mut self, glyph_height: Mm) {
        self.glyph_height = glyph_height;
    }

    /// Line height getter.
    pub fn line_height(&self) -> Mm {
        self.line_height
    }

    /// Glyph height getter.
    pub fn glyph_height(&self) -> Mm {
        self.glyph_height
    }
}
diff --git a/src/render.rs b/src/render.rs
index 5ecce1e..19f717c 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -370,7 +370,9 @@ impl<'a> Area<'a> {
        style: Style,
        s: S,
    ) -> Result<bool, Error> {
        if let Some(mut section) = self.text_section(font_cache, position, style) {
        if let Some(mut section) =
            self.text_section(font_cache, position, style.metrics(font_cache))
        {
            section.print_str(s, style)?;
            Ok(true)
        } else {
@@ -387,9 +389,9 @@ impl<'a> Area<'a> {
        &self,
        font_cache: &'f fonts::FontCache,
        position: Position,
        style: Style,
        metrics: fonts::Metrics,
    ) -> Option<TextSection<'_, 'f, 'a>> {
        TextSection::new(font_cache, self, position, style)
        TextSection::new(font_cache, self, position, metrics)
    }

    /// Transforms the given position that is relative to the upper left corner of the area to a
@@ -408,9 +410,10 @@ impl<'a> Area<'a> {
pub struct TextSection<'a, 'f, 'l> {
    font_cache: &'f fonts::FontCache,
    area: &'a Area<'l>,
    line_height: Mm,
    cursor: Position,
    position: Position,
    fill_color: Option<Color>,
    is_first: bool,
    metrics: fonts::Metrics,
}

impl<'a, 'f, 'l> TextSection<'a, 'f, 'l> {
@@ -418,40 +421,45 @@ impl<'a, 'f, 'l> TextSection<'a, 'f, 'l> {
        font_cache: &'f fonts::FontCache,
        area: &'a Area<'l>,
        position: Position,
        style: Style,
        metrics: fonts::Metrics,
    ) -> Option<TextSection<'a, 'f, 'l>> {
        let height = style.font(font_cache).glyph_height(style.font_size());

        if position.y + height > area.size.height {
        if position.y + metrics.glyph_height() > area.size.height {
            return None;
        }

        let line_height = style.line_height(font_cache);
        let section = TextSection {
            font_cache,
            area,
            line_height,
            cursor: position,
            position,
            fill_color: None,
            is_first: true,
            metrics,
        };

        section.layer().begin_text_section();
        section.layer().set_line_height(line_height.into());
        let cursor = area.transform_position(position);
        section
            .layer()
            .set_text_cursor(cursor.x.into(), (cursor.y - height).into());
            .set_line_height(metrics.line_height().into());
        Some(section)
    }

    fn set_text_cursor(&mut self) {
        let cursor_t = self.area.transform_position(self.position);
        self.layer().set_text_cursor(
            cursor_t.x.into(),
            (cursor_t.y - self.metrics.glyph_height()).into(),
        );
    }

    /// Tries to add a new line and returns `true` if the area was large enough to fit the new
    /// line.
    #[must_use]
    pub fn add_newline(&mut self) -> bool {
        if self.cursor.y + self.line_height > self.area.size.height {
        if self.position.y + self.metrics.line_height() > self.area.size.height {
            false
        } else {
            self.layer().add_line_break();
            self.cursor.y += self.line_height;
            self.position.y += self.metrics.line_height();
            true
        }
    }
@@ -461,18 +469,29 @@ impl<'a, 'f, 'l> TextSection<'a, 'f, 'l> {
    /// The font cache for this text section must contain the PDF font for the given style.
    pub fn print_str(&mut self, s: impl AsRef<str>, style: Style) -> Result<(), Error> {
        let font = style.font(self.font_cache);
        let s_ref = s.as_ref();

        // Adjust cursor to remove left bearing of the first character of the first string
        if self.is_first {
            if let Some(first_c) = s_ref.chars().nth(0) {
                let l_bearing = style.char_left_side_bearing(self.font_cache, first_c);
                self.position.x -= l_bearing;
            }
            self.set_text_cursor();
        }
        self.is_first = false;

        let positions = font
            .kerning(self.font_cache, s.as_ref().chars())
            .kerning(self.font_cache, s_ref.chars())
            .into_iter()
            // Kerning is measured in 1/1000 em
            .map(|pos| pos * -1000.0)
            .map(|pos| pos as i64);
        let codepoints = if font.is_builtin() {
            // Built-in fonts always use the Windows-1252 encoding
            encode_win1252(s.as_ref())?
            encode_win1252(s_ref)?
        } else {
            font.glyph_ids(&self.font_cache, s.as_ref().chars())
            font.glyph_ids(&self.font_cache, s_ref.chars())
        };

        let font = self
diff --git a/src/style.rs b/src/style.rs
index e77258a..adb0973 100644
--- a/src/style.rs
+++ b/src/style.rs
@@ -254,6 +254,17 @@ impl Style {
            .char_width(font_cache, c, self.font_size())
    }

    /// Returns the width of the empty space between the origin of the glyph bounding
    /// box and the leftmost edge of the character, for this style using the given font cache.
    ///
    /// If the font family is set, it must have been created by the given [`FontCache`][].
    ///
    /// [`FontCache`]: ../fonts/struct.FontCache.html
    pub fn char_left_side_bearing(&self, font_cache: &fonts::FontCache, c: char) -> Mm {
        self.font(font_cache)
            .char_left_side_bearing(font_cache, c, self.font_size())
    }

    /// Calculates the width of the given string with this style using the data in the given font
    /// cache.
    ///
@@ -294,6 +305,11 @@ impl Style {
    pub fn line_height(&self, font_cache: &fonts::FontCache) -> Mm {
        self.font(font_cache).get_line_height(self.font_size()) * self.line_spacing()
    }

    /// Calculate the metrics of the font for this style.
    pub fn metrics(&self, font_cache: &fonts::FontCache) -> fonts::Metrics {
        self.font(font_cache).metrics(self.font_size())
    }
}

impl From<Color> for Style {
-- 
2.28.0
merge-rs/patches: FAILED in 59s

[Calculate line spacing in paragraphs and remove left bearing on first character of lines][0] from [ap4sc][1]

[0]: https://lists.sr.ht/~ireas/public-inbox/patches/23401
[1]: mailto:ap4sc4@gmail.com

✗ #529228 FAILED merge-rs/patches/archlinux.yml      https://builds.sr.ht/~ireas/job/529228
✗ #529227 FAILED merge-rs/patches/archlinux-msrv.yml https://builds.sr.ht/~ireas/job/529227
I’m not sure why this failed. Do I need to merge into the latest state of your master branch first before patching?
Thank you very much for this patch!  I’ve applied it to master with some
small changes: