~ireas/public-inbox

penpdf-rs: Implemented line styling effects (underline, strikethrough, overline) v1 PROPOSED

Thomas Taylor: 1
 Implemented line styling effects (underline, strikethrough, overline)

 3 files changed, 176 insertions(+), 8 deletions(-)
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/36422/mbox | git am -3
Learn more about email & git

[PATCH penpdf-rs] Implemented line styling effects (underline, strikethrough, overline) Export this patch

---
 src/elements.rs |   8 +++-
 src/render.rs   | 105 +++++++++++++++++++++++++++++++++++++++++++++---
 src/style.rs    |  71 +++++++++++++++++++++++++++++++-
 3 files changed, 176 insertions(+), 8 deletions(-)

diff --git a/src/elements.rs b/src/elements.rs
index 242fc1c..7f49445 100644
--- a/src/elements.rs
+++ b/src/elements.rs
@@ -352,9 +352,14 @@ impl Element for Paragraph {
                .fold(fonts::Metrics::default(), |max, m| max.max(&m));
            let position = Position::new(self.get_offset(width, area.size().width), 0);

            let mut posx = Mm(0.0);
            if let Some(mut section) = area.text_section(&context.font_cache, position, metrics) {
                for s in line {
                    section.print_str(&s.s, s.style)?;
                    // section.print_str(&s.s, s.style, posx)?;
                    match section.print_str(&s.s, s.style, posx) {
                        Err(e) => return Err(e),
                        Ok(x) => posx=x,
                    }
                    rendered_len += s.s.len();
                }
                rendered_len -= delta;
@@ -362,6 +367,7 @@ impl Element for Paragraph {
                result.has_more = true;
                break;
            }

            result.size = result
                .size
                .stack_vertical(Size::new(width, metrics.line_height));
diff --git a/src/render.rs b/src/render.rs
index 8e79910..397dff2 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -19,6 +19,7 @@
//! [`TextSection`]: struct.TextSection.html

use std::cell;
use std::fmt;
use std::io;
use std::ops;
use std::rc;
@@ -31,6 +32,7 @@ use crate::{Margins, Mm, Position, Size};
#[cfg(feature = "images")]
use crate::{Rotation, Scale};

#[derive(Debug, Clone)]
/// A position relative to the top left corner of a layer.
struct LayerPosition(Position);

@@ -40,6 +42,7 @@ impl LayerPosition {
    }
}

#[derive(Debug, Clone)]
/// A position relative to the bottom left corner of a layer (“user space” in PDF terms).
struct UserSpacePosition(Position);

@@ -200,6 +203,12 @@ pub struct Page {
    layers: Layers,
}

impl fmt::Debug for Page {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Page").field("size", &self.size).field("layers", &self.layers).finish()
    }
}

impl Page {
    fn new(
        page: printpdf::PdfPageReference,
@@ -295,7 +304,7 @@ impl Layers {
/// This is a wrapper around a [`printpdf::PdfLayerReference`][].
///
/// [`printpdf::PdfLayerReference`]: https://docs.rs/printpdf/0.3.2/printpdf/types/pdf_layer/struct.PdfLayerReference.html
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Layer<'p> {
    page: &'p Page,
    data: rc::Rc<LayerData>,
@@ -470,6 +479,13 @@ pub struct Area<'p> {
    layer: Layer<'p>,
    origin: Position,
    size: Size,
    lines: Vec<Line>,
}

impl fmt::Debug for Area<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Area").field("origin", &self.origin).field("size", &self.size).field("lines",&self.lines).finish()
    }
}

impl<'p> Area<'p> {
@@ -478,6 +494,7 @@ impl<'p> Area<'p> {
            layer,
            origin,
            size,
            lines: Vec::new(),
        }
    }

@@ -491,6 +508,7 @@ impl<'p> Area<'p> {
            layer,
            origin: self.origin,
            size: self.size,
            lines: Vec::new(),
        }
    }

@@ -576,9 +594,9 @@ impl<'p> Area<'p> {
            .add_image(image, self.position(position), scale, rotation, dpi);
    }

    /// Draws a line with the given points and the given line style.
    /// Draws a line with the given points and the given line style immediately.
    ///
    /// The points are relative to the upper left corner of the area.
    /// The points are relative to the upper left corner of the area. Drawing the line immediately will mess with the layout of following text, use add_line instead.
    pub fn draw_line<I>(&self, points: I, line_style: LineStyle)
    where
        I: IntoIterator<Item = Position>,
@@ -589,6 +607,16 @@ impl<'p> Area<'p> {
            .add_line_shape(points.into_iter().map(|pos| self.position(pos)));
    }

    /// Saves a line to be drawn later with the given points and the given line style.
    ///
    /// The points are relative to the upper left corner of the area. Drawing the line later is necessary to prevent side effects on text layout, the drawing will occur on dropping the area object.
    pub fn add_line<I>(&mut self, points: I, line_style: LineStyle)
    where
        I: IntoIterator<Item = Position>,
    {
        self.lines.push(Line::save(points.into_iter().map(|pos| self.position(pos)).collect(), &line_style));
    }

    /// Tries to draw the given string at the given position and returns `true` if the area was
    /// large enough to draw the string.
    ///
@@ -604,7 +632,7 @@ impl<'p> Area<'p> {
        if let Some(mut section) =
            self.text_section(font_cache, position, style.metrics(font_cache))
        {
            section.print_str(s, style)?;
            section.print_str(s, style, 0)?;
            Ok(true)
        } else {
            Ok(false)
@@ -633,6 +661,21 @@ impl<'p> Area<'p> {
    }
}

impl Drop for Area<'_> {
    /// Draw all the saved lines before drop.
    ///
    /// When the area goes out of scope all the saved lines will be drawn.
    fn drop(&mut self) {
        {
            for line in self.lines.clone() {
                self.layer.set_outline_thickness(line.style.thickness());
                self.layer.set_outline_color(line.style.color());
                self.layer.add_line_shape(line.points);
            }
        }
    }
}

/// A text section that is drawn on an area of a PDF layer.
pub struct TextSection<'f, 'p> {
    font_cache: &'f fonts::FontCache,
@@ -700,7 +743,7 @@ impl<'f, 'p> TextSection<'f, 'p> {
    /// Prints the given string with the given style.
    ///
    /// 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> {
    pub fn print_str(&mut self, s: impl AsRef<str>, style: Style, posx: impl Into<Mm>) -> Result<Mm, Error> {
        let font = style.font(self.font_cache);
        let s = s.as_ref();

@@ -738,7 +781,42 @@ impl<'f, 'p> TextSection<'f, 'p> {
        self.area
            .layer
            .write_positioned_codepoints(positions, codepoints);
        Ok(())

        let x1 = posx.into();
        let x2 = style.str_width(&self.font_cache,s)+x1;

        // Line effects (underline, strikethrough, overline) cannot be drawn immediately because of side effects on the layout
        if style.is_underline() || style.is_strikethrough() || style.is_overline() {
            let yo = self.metrics.ascent*0.15;
            let ys = self.metrics.ascent*0.7;
            let yu = self.metrics.ascent*1.1;
            let ls = LineStyle::new().with_color(Color::Rgb(0,0,0)).with_thickness(Mm(0.1));
            if style.is_overline() {
                // self.area.draw_line(
                self.area.add_line(
                    vec![Position::new(x1, yo),Position::new(x2,yo)], ls);
                // println!("over line: ({:?},{:?})-({:?},{:?}) of {:?}", x1,yo,x2,yo,self.metrics);
            }
            if style.is_strikethrough() {
                // self.area.draw_line(
                self.area.add_line(
                    vec![Position::new(x1, ys),Position::new(x2,ys)],
                    ls,
                );
                // println!("strike line: ({:?},{:?})-({:?},{:?})", x1,ys,x2,ys);
            }
            if style.is_underline() {
                // self.area.draw_line(
                self.area.add_line(
                    vec![Position::new(x1, yu),Position::new(x2,yu)],
                    ls,
                );
                // println!("under line: ({:?},{:?})-({:?},{:?})", x1,yu,x2,yu);
            }
            // println!("string: {} at {:?} line {:?}-{:?}",s,self.area,x1,x2);
        }

        Ok(x2)
    }
}

@@ -770,3 +848,18 @@ fn encode_win1252(s: &str) -> Result<Vec<u16>, Error> {
        Ok(bytes)
    }
}

/// A line element.
///
/// This struct provides storage for a line, so it can be drawn later.
#[derive(Debug, Clone)]
struct Line {
    points: Vec<LayerPosition>,
    style: LineStyle,
}

impl Line {
    fn save(points: Vec<LayerPosition>, style: &LineStyle) -> Line {
        Line {points, style: style.clone()}
    }
}
diff --git a/src/style.rs b/src/style.rs
index b3c12f4..94617c6 100644
--- a/src/style.rs
+++ b/src/style.rs
@@ -80,13 +80,19 @@ impl From<Color> for printpdf::Color {
    }
}

/// A text effect (bold or italic).
/// A text effect (bold, italic, underline, overline, strikethrough).
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Effect {
    /// Bold text.
    Bold,
    /// Italic text.
    Italic,
    /// Underlined text.
    Underline,
    /// Overlined text.
    Overline,
    /// Stricken through text.
    Strikethrough,
}

/// A style annotation for a string.
@@ -113,6 +119,9 @@ pub struct Style {
    color: Option<Color>,
    is_bold: bool,
    is_italic: bool,
    is_underline: bool,
    is_overline: bool,
    is_strikethrough: bool,
}

impl Style {
@@ -139,6 +148,15 @@ impl Style {
        if style.is_italic {
            self.is_italic = true;
        }
        if style.is_underline {
            self.is_underline = true;
        }
        if style.is_overline {
            self.is_overline = true;
        }
        if style.is_strikethrough {
            self.is_strikethrough = true;
        }
    }

    /// Combines this style and the given style and returns the result.
@@ -167,6 +185,21 @@ impl Style {
        self.is_italic
    }

    /// Returns whether the underline text effect is set.
    pub fn is_underline(&self) -> bool {
        self.is_underline
    }

    /// Returns whether the overline text effect is set.
    pub fn is_overline(&self) -> bool {
        self.is_overline
    }

    /// Returns whether the strikethrough text effect is set.
    pub fn is_strikethrough(&self) -> bool {
        self.is_strikethrough
    }

    /// Returns the font size for this style in points, or 12 if no font size is set.
    pub fn font_size(&self) -> u8 {
        self.font_size.unwrap_or(12)
@@ -199,6 +232,39 @@ impl Style {
        self
    }

    /// Sets the underline effect for this style.
    pub fn set_underline(&mut self) {
        self.is_underline = true;
    }

    /// Sets the underline effect for this style and returns it.
    pub fn underline(mut self) -> Style {
        self.set_underline();
        self
    }

    /// Sets the overline effect for this style.
    pub fn set_overline(&mut self) {
        self.is_overline = true;
    }

    /// Sets the overline effect for this style and returns it.
    pub fn overline(mut self) -> Style {
        self.set_overline();
        self
    }

    /// Sets the strikethrough effect for this style.
    pub fn set_strikethrough(&mut self) {
        self.is_strikethrough = true;
    }

    /// Sets the strikethrough effect for this style and returns it.
    pub fn strikethrough(mut self) -> Style {
        self.set_strikethrough();
        self
    }

    /// Sets the font family for this style.
    pub fn set_font_family(&mut self, font_family: fonts::FontFamily<fonts::Font>) {
        self.font_family = Some(font_family);
@@ -330,6 +396,9 @@ impl From<Effect> for Style {
        match effect {
            Effect::Bold => style.bold(),
            Effect::Italic => style.italic(),
            Effect::Underline => style.underline(),
            Effect::Overline => style.overline(),
            Effect::Strikethrough => style.strikethrough(),
        }
    }
}
--
2.38.1