Extra styles for FrameElements

For FrameElements it would be nice to have a way to add more styles other than the color, like border thickness per side, rounded corners, a background color, etc.

Just as an possible idea on how this might be done second style struct could be added that contains non-inherited styles, say ElementStyle. It'd track all the border and background options and could also include margins. This could possibly apply directly to other elements and replace the FrameElement and PaddingElement wrappers. Everything text-related would remain in the other Style struct and would still be inherited

Assigned to
1 year, 9 months ago
1 year, 9 months ago
No labels applied.

~ap4sc 1 year, 9 months ago*

I tried my hand at implementing part of this for my own project. It doesn't yet handle an element spanning over multiple pages

In my fork's genpdf::render::Area

pub fn draw_rounded_rect(&self, p: Position, w: Mm, h: Mm, r_bl: Mm, r_tl: Mm, r_tr: Mm, t_br: Mm, style: Style, has_fill: bool) {
  // The same code as draw_line except with 16 bezier points

Where r_* are the radii of the four rounded corners

In my project's crate::elements

impl<E: Element> Element for RoundedFrameElement<E> {
    fn render(
        &mut self,
        context: &Context,
        area: render::Area<'_>,
        style: Style,
    ) -> Result<RenderResult, Error> {
        let result = self.element.render(context, area.clone(), style)?;
        let r = genpdf::Mm::from(5);
        area.draw_rounded_rect(Position::default(), area.size().width, result.size.height, r, r, r, r, style, true);

Drawing code in my main program

let mut pdf_para = genpdf::elements::Paragraph::default();
pdf_para.push_styled("Rounded Frame Element");
        genpdf::Margins::trbl(3, 3, 5, 5),
    genpdf::style::Color::Rgb(1, 0, 0)

Which works nicely (although I have to change the text color back)


But setting the fill covers the inner elements, which I can't figure out how to fix since I need to render the text first to get the height


~ireas 1 year, 9 months ago

Looks good! I think we can split this up into three aspects:

  1. Improve the style configuration for a FramedElement, namely setting a frame color that is independent of the text color and changing the line thickness. I like the idea of a separate style struct for this use case. I think I’d call it LineStyle so that we can also use it to draw shapes (once that is implemented).
  2. Add support for rounded borders. We could maybe have something like a BorderStyle struct, but I’d like to start by just adding that functionality to the FramedElement struct.
  3. Add support for a fill color. I think this also belongs into FramedElement for the time being. Regarding the covering of the inner elements, I think we have to start using multiple layers. I’ll look into that.

Do you want to help with the implementation?

~ap4sc 1 year, 9 months ago*

I can do 1 & 2. I'll put LineStyle and BorderStyle in genpdf::style.


  line_style: Option<LineStyle>,
  //fill_color: Option<Color>

(with convenience functions for a new BorderStyle from a uniform radius or no radius)

Or it could be called FrameStyle. I think it makes sense to have it since there are so many fields and it'd be easier to extend to add the fill options, which I'll leave out for now. This would be another field for FramedElement, set by a second constructor with_frame_style

~ireas 1 year, 9 months ago

Great, thank you! Please start with just implementing LineStyle. I’ll have to give the BorderLayout some more thought.

~ap4sc 1 year, 9 months ago*

I've implemented it for FramedElements here https://git.sr.ht/~ap4sc/genpdf-rs/commit/4b77bb05205f122ba92d2a6e46c90928404b731d The comments are the beizer and fill code I can remove before the final patch. I had to change the way lines are drawn across multiple pages because at large line thicknesses the corners of the lines wouldn't be connected when they were drawn separately. I assume this will be the same for Tables, which I haven't got to implementing style for

Are there any other methods I should impl for LineStyle and Framed Element, and am I deriving the right traits for LineStyle. ? I just followed the example of Style for these.

Edit: also in the docs for printpdf set_outline_thickness it says

NOTE: 0.0 is a special value, it does not make the line disappear, but rather makes it appear 1px wide across all devices

I made 0 the default line thickness in draw_line but it looks strange because of this, should I set it to 1?

~ireas 1 year, 9 months ago

Are there any other methods I should impl for LineStyle and Framed Element, and am I deriving the right traits for LineStyle. ?

Looks good! Maybe implement From<Color> for LineStyle and then change FramedElement::wih_line_style to accept impl Into<LineStyle>?

Two things I noticed:

  • In genpdf, we always use Mm except for font sizes. I think we should also use Mm here (i. e. change thickness to Mm in LineStyle). You can convert that to pt using printpdf::Pt::from(thickness).0.
  • In Style, color is optional because we inherit the style, so None is different from Some(black). This does not apply for draw_line as we default to black. So we should either remove the Option and directly store the color in LineStyle (defaulting to black), or use the text color for the line if no line color is set explicitly.

I made 0 the default line thickness in draw_line but it looks strange because of this, should I set it to 1?

If we always set the thickness before drawing a shape, we don’t have to reset it in draw_line. Still, the question is what should be the default in LineStyle. Why do you think that 0.0 looks strange? I think it looks okay.

~ap4sc 1 year, 9 months ago*

I used f64 instead of Mm because printpdf uses it in the function for setting the thickness rather than a wrapper value like Pt or printpdf::Mm

pub fn set_outline_thickness(&self, outline_thickness: f64)

However I can still change this

defaulting to black

Will do this one

Why do you think that 0.0 looks strange?

It's always 1px no matter how far you zoom in or out. I don't know if users would expect this special behavior as a default. Mainly because I think the general impression of a PDF is a fixed document that appears the same everywhere you view it, I've never seen this feature used before.

~ap4sc 1 year, 9 months ago*

Ok I've added all the above changes, as well as implemented the style for Tables using FrameCellDecorator


Except I don't know how to do this:

change FramedElement::with_line_style to accept impl Into<LineStyle>

and the Table has the same issue as FramedElement where the borders don't connect. Here's it at line thickness 10:


This could probably be a separate ticket since it requires changing the whole way the tables are drawn to draw the outer border separately on each page


Here's my working proposal for rounded corners on a separate branch, which supports continuity over multiple pages and different radii for each corner. I've gone ahead and implemented it since I need it for my own project so if you don't want to include it or want it done another way I can do that:


~ireas 1 year, 9 months ago

I had another look at the PDF specification. It says that “[s]ince the results of rendering such ‘zero-width’ lines are device-dependent, their use is not recommended.” So you are right about not using 0.0 as the default width.

It also says that the line width “is a nonnegative number expressed in user space units”, which is 1/72 inch, which is approximately 1 pt. So we should definitely use Mm here.

Except I don't know how to do this:

change FramedElement::with_line_style to accept impl Into

You can do something like this:

impl<E: Element> FramedElement<E> {
    pub fn with_line_style(element: E, line_style: impl Into<LineStyle>) -> FramedElement<E> {
        Self {
            line_style: Some(line_style.into()),

Your LineStyle implementation looks good! Though this might not work as intended:


line_style.thickness() is Mm, set_outline_thickness accepts a pt measure as f64. But this line would pass the mm measure as f64. So I think you have to use printpdf::Pt::from(line_style.thickness()).0 here.

Once you have changed these two things and think that everything is ready, can you please squash the commits into one and submit it? Thanks!

I’ll take a closer look at the rounded corners once the LineStyle patch is merged. I definitely want to include it, I just have to make up my mind about the best API.

~ireas REPORTED INVALID 1 year, 9 months ago

I’ve created #36, #37, #38 so that we can discuss these topics separately.

Register here or Log in to comment, or comment via email.