Search code examples
rustformattingpdf-generationrust-crates

Unsure how to format my pdf with the "Printpdf 0.5.3" crate in Rust


I am relatively new to programming in rust and need a bit of direction on how to implement my project.

Currently, I am trying to build a CLI tool that takes a persons name, position they are applying too, company they are applying too and the location of that company and auto-magically formats a PDF with the contents of their cover letter. eventually, i want to make this into a GUI tool that I can send to my friends so they can use it on their windows/mac machines.

the problem right now is that I am using the "Printpdf" crate to generate the document itself. as you can see with the picture below, the text doesn't automatically wrap/line-break/format when it reaches the end of the line. unfortunately, I can not simply break this up into different string variables as the program needs to be able to add arbitrary user input and return a perfectly formatted PDF file.

How to I go about formatting this document? should I use a crate that isn't "printpdf"? or can I use something else to get the text to behave the way I want?

here is the rust code and a picture of what the PDF outputs.

use std::io; 
use printpdf::*;
use std::fs::File;
use std::io::BufWriter;

fn main() {
    println!("Enter: Position");
    let mut position = String::new(); 
    io:: stdin()
        .read_line(&mut position)
        .expect("failed to read"); 

        
    println!("Enter: Company Name");
    let mut coname = String::new(); 
    io:: stdin()
        .read_line(&mut coname)
        .expect("failed to read"); 

    println!("Enter: Company Location");
    let mut location = String::new(); 
    io:: stdin()
        .read_line(&mut location)
        .expect("failed to read"); 

    let ntxt = " ";

    let sample = "This is some random Sample text, This text should eventaully be a user input. Currently, this text is not a user input. this text is supposed to be a text of long string data that will eventually be added to the document via user input";

    let (doc, page1, layer1) = PdfDocument::new("PDF_Document_title", Mm(216.0), Mm(279.0), "Layer 1");
    let current_layer = doc.get_page(page1).get_layer(layer1);


    let font = doc.add_external_font(File::open("./fonts/TNR-Regular.ttf").unwrap()).unwrap();

    current_layer.use_text(position.clone(), 14.0, Mm(25.0), Mm(250.0), &font);
    current_layer.use_text(coname.clone(), 14.0, Mm(25.0), Mm(240.0), &font);
    current_layer.use_text(location.clone(), 14.0, Mm(25.0), Mm(230.0), &font);
    current_layer.use_text(ntxt.clone(), 14.0, Mm(25.0), Mm(220.0), &font);

    current_layer.begin_text_section();

        current_layer.set_font(&font, 14.0);
        current_layer.set_text_cursor(Mm(25.0), Mm(210.0));

        // write one line, but write text2 in superscript
        current_layer.write_text(sample.clone(), &font);

    current_layer.end_text_section();

    doc.save(&mut BufWriter::new(File::create("test_working.pdf").unwrap())).unwrap();

}

the pdf output

I don't really know where to start with fixing this issue.

I have tried to look at the documentation for the crate "textwrap" but it looks like an utter enigma too me. I also looked at the documentation for "printpdf" but it says it doesn't support formatting and alignment.


Solution

  • There is probably a more sane way to do this, but this works:

    1. We load the font_data as a Vector of bytes Vec<u8>; both glyph_brush_layout and printpdf then independently work with that font as a slice of bytes &[u8].
    2. We use glyph_brush_layout to calculate the position of the glyphs* (I just adapted the example in the README), specifically getting the vertical y positions in a box 160mm wide.
    3. We group the glyphs on those y positions (they are the vertical positions of the baseline of each line of text), along with their index into the text (where text = sample). I used itertools to make this easier.
    4. We take just the first glyph in each group (group.next().unwrap().0), getting its index (and y position). These indexes are the positions at which we will split the text into individual lines. These are collected into a Vector.
    5. We loop over the Vector, writing the text to the PDF. To split the text we create a peekable iterator, so we can take the slice of the text sample for the current index and the next (peeked) index. Because of the different ways that printpdf and glyph_brush_layout deal with layout, we need to do some vertical offsets and conversions.

    *The assumption here is that one glyph equals one character in the text, i.e. assert_eq!(glyphs.len(), sample.chars().count());. If that's not the case, maybe you want to consider positioning one glyph at a time, directly.

    it works

    main.rs

    use printpdf::*;
    use std::fs::File;
    use std::io::BufWriter;
    use std::io::{self, Read};
    
    fn main() {
        println!("Enter: Position");
        let mut position = String::new();
        io::stdin()
            .read_line(&mut position)
            .expect("failed to read");
    
        println!("Enter: Company Name");
        let mut coname = String::new();
        io::stdin().read_line(&mut coname).expect("failed to read");
    
        println!("Enter: Company Location");
        let mut location = String::new();
        io::stdin()
            .read_line(&mut location)
            .expect("failed to read");
    
        let ntxt = " ";
    
        let sample = "This is some random Sample text, This text should eventaully be a user input. Currently, this text is not a user input. this text is supposed to be a text of long string data that will eventually be added to the document via user input";
    
        let (doc, page1, layer1) =
            PdfDocument::new("PDF_Document_title", Mm(216.0), Mm(279.0), "Layer 1");
        let current_layer = doc.get_page(page1).get_layer(layer1);
    
        // load the font data for the font "Times New Roman"
        let font_data = {
            let mut font_file = File::open("./times-new-roman.ttf").unwrap();
            let mut font_data = Vec::with_capacity(font_file.metadata().unwrap().len() as usize);
            font_file.read_to_end(&mut font_data).unwrap();
            font_data
        };
    
        // load the font reference for glyph_brush_layout
        let gbl_font = glyph_brush_layout::ab_glyph::FontRef::try_from_slice(&font_data).unwrap();
        // put it into a slice of glyph_brush_layout font references
        let gbl_fonts = &[gbl_font];
    
        // load the font reference for printpdf
        let font = doc.add_external_font(font_data.as_slice()).unwrap();
    
        current_layer.use_text(position.clone(), 14.0, Mm(25.0), Mm(250.0), &font);
        current_layer.use_text(coname.clone(), 14.0, Mm(25.0), Mm(240.0), &font);
        current_layer.use_text(location.clone(), 14.0, Mm(25.0), Mm(230.0), &font);
        current_layer.use_text(ntxt, 14.0, Mm(25.0), Mm(220.0), &font);
    
        // calculate the glyph positions using glyph_brush_layout
        use glyph_brush_layout::ab_glyph::Font;
        use glyph_brush_layout::GlyphPositioner;
        let glyphs = glyph_brush_layout::Layout::default().calculate_glyphs(
            gbl_fonts,
            &glyph_brush_layout::SectionGeometry {
                // width 160mm = 210mm - 2 * 25mm margins; height unbounded
                bounds: (mm_to_px(160.0), f32::INFINITY),
                ..Default::default()
            },
            &[glyph_brush_layout::SectionText {
                text: sample,
                scale: gbl_fonts[0].pt_to_px_scale(14.0).unwrap(),
                font_id: glyph_brush_layout::FontId(0),
            }],
        );
    
        // make sure the number of glyphs matches the number of chars in the sample text
        assert_eq!(glyphs.len(), sample.chars().count());
    
        // group the glyphs by y position
        use itertools::Itertools;
        let line_starts = glyphs
            .iter()
            .enumerate() // enumerate will give us the start index into the sample text of the start of the line
            .group_by(|(_, glyph)| glyph.glyph.position.y) // group by "y" which is effectively equivalent to the index of the line
            .into_iter()
            .map(|(y, mut group)| (y, group.next().unwrap().0))
            .collect::<Vec<_>>();
    
        // get the minimum y position
        // you could get the max a similar way, if you needed to calculate the vertical size of the text,
        // for example if you wanted to lay out text below it
        let min = glyphs
            .iter()
            .map(|glyph| glyph.glyph.position.y)
            .fold(f32::INFINITY, |a, b| a.min(b));
    
        // we need a peekable iterator so we can see where the next line starts
        let mut iter = line_starts.iter().peekable();
    
        // iterate over the line_starts and draw the text
        loop {
            // get the next line start, if there is none then we break out of the loop
            let Some((y, start)) = iter.next() else {
                break;
            };
            // peek into the line start after that to get the end index,
            // if there is none (we're at the last line in the loop), then we use the length of the sample text
            let end = if let Some((_, end)) = iter.peek() {
                *end
            } else {
                sample.chars().count()
            };
    
            // slice up the text
            // if you know you're only dealing with ASCII characters you can simplify this as
            // `let line = &sample[*start..end];`
            // which saves on an allocation to a String;
            // or you can use char_indices to get the byte indices and slice that way
            let line = sample
                .chars()
                .skip(*start)
                .take(end - start)
                .collect::<String>();
    
            // draw the text
            current_layer.use_text(
                line.trim(),
                14.0,
                Mm(25.0),
                // printpdf up = y positive, but glyph_brush_layout down = y positive
                Mm(210.0 + px_to_mm(min) - px_to_mm(*y)),
                &font,
            );
        }
    
        doc.save(&mut BufWriter::new(
            File::create("test_working.pdf").unwrap(),
        ))
        .unwrap();
    }
    
    /// glyph_brush_layout deals with f32 pixels, but printpdf deals with f64 mm.
    fn px_to_mm(px: f32) -> f64 {
        px as f64 * 3175.0 / 12000.0
    }
    
    /// printpdf deals with f64 mm, but glyph_brush_layout deals with f32 pixels.
    fn mm_to_px(mm: f64) -> f32 {
        (mm * 12000.0 / 3175.0) as f32
    }
    

    cargo.toml

    [package]
    name = "generate-pdf"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    glyph_brush_layout = "0.2.3"
    itertools = "0.10.5"
    printpdf = "0.5.3"