ajrcarey/pdfium-render

Impossible to load font, then use it, into mutable `Document`

stephenjudkins opened this issue · 12 comments

Please forgive me if this question reflects Rust inexperience. But here's my impression:

If we want to load a font and then use it in a document, the borrowing rules make this impossible.

    let mut doc = pdfium.create_new_pdf()?;

    let font = PdfFont::new_true_type_from_bytes(&doc, include_bytes!("../font.ttf"), true)?;   

    doc.pages_mut().copy_page_from_document(&template, 0, 0)?; // copy first page #

gives us:

error[E0502]: cannot borrow `doc` as mutable because it is also borrowed as immutable
  --> src/main.rs:40:5
   |
38 |     let font = PdfFont::new_true_type_from_bytes(&doc, include_bytes!("../font.ttf"), true)?;
   |                                                  ---- immutable borrow occurs here
39 |
40 |     doc.pages_mut().copy_page_from_document(&template, 0, 0)?; // copy first page #
   |     ^^^^^^^^^^^^^^^ mutable borrow occurs here

Likewise, if we pass it &mut doc:

    let mut doc = pdfium.create_new_pdf()?;

    let font = PdfFont::new_true_type_from_bytes(&mut doc, include_bytes!("../font.ttf"), true)?;   

    doc.pages_mut().copy_page_from_document(&template, 0, 0)?; // copy first page #

we see

error[E0499]: cannot borrow `doc` as mutable more than once at a time
  --> src/main.rs:40:5
   |
38 |     let font = PdfFont::new_true_type_from_bytes(&mut doc, include_bytes!("../font.ttf"), true)?;
   |                                                  -------- first mutable borrow occurs here
39 |
40 |     doc.pages_mut().copy_page_from_document(&template, 0, 0)?; // copy first page #
   |     ^^^^^^^^^^^^^^^ second mutable borrow occurs here

It is possible to load a new font after we call pages_mut(). But the problem is, we have to do this after every page, and we're loading the font once per page. That is not ideal. I cannot see a way around that.

PEBCAK. I was able to solve this through loading the font at the right place and manual use drop(font). Apologies!

Ideally it would be a bit simpler and more intuitive than that, though. I think you've identified a genuine usability problem, if nothing else. Let me come back to you after the Easter break once I've had a chance to examine the PdfFont constructor code more closely. Perhaps there's a way of avoiding taking that &PdfDocument reference, or making it cleaner to avoid lifetime hassles.

Yeah, I am actually still struggling with this! It is not ergonomic at all; ideally the font should be usable across the entire lifetime of the document but I've found I've have to allocate a new one much more frequently, which is very slow and presumably inefficient

The Pdfium FPDFText_LoadFont() and FPDFText_LoadStandardFont() functions both take FPDF_DOCUMENT handles. This is why the lifetime of a PdfFont object is bound to that of a PdfDocument in pdfium-render's API.

The need to pass a PdfDocument reference into the PdfFont constructor functions could be avoided if fonts were created by PdfDocument itself (i.e. inversion of control). This could result in a more ergonomic API. Let me think about this over the next day or two.

PS in the meantime, if you are able to share a code sample that demonstrates the difficulty of retaining a PdfFont across the lifetime of a document, that would be great.

Sure! Here's a simplification of my code:

use pdfium_render::prelude::*;

fn go() -> Result<(), PdfiumError> {
    let pdfium = Pdfium::new(
        Pdfium::bind_to_library(Pdfium::pdfium_platform_library_name_at_path("./"))
            .or_else(|_| Pdfium::bind_to_system_library())?,
    );
    
   
    let mut doc = pdfium.create_new_pdf()?;

    let font = PdfFont::new_true_type_from_bytes(&doc, include_bytes!("../font.ttf"), true)?;
    
    let margin = PdfPoints::new(12.0);
    let mut page_num = 1;

    let mut page = doc.pages_mut().create_page_at_end(PdfPagePaperSize::a4())?;

    let mut top = page.height() - margin;

    for i in 1..5000 {
        if top < margin {
            page_num += 1;
            page = doc.pages_mut().create_page_at_end(PdfPagePaperSize::a4())?;
            top = page.height() - margin;
        }

       // note that I could load the font here, it'd just be far, far slower and less efficient

        let mut object = PdfPageTextObject::new(
            &doc,
            format!("Item #{}", i),
            &font // &PdfFont::helvetica(&doc),
            PdfPoints::new(8.0)
        )?;
        object.translate(margin, top)?;
        top = top - margin;
        page.objects_mut().add_text_object(object)?;
    }

    doc.save_to_file("result.pdf")?;

    Ok(())
}

fn main() {
    go().unwrap();
}

The fundamental problem here is the need to mix mutable and immutable references to PdfDocument. I believe the only solution is for PdfDocument to take direct ownership of the PdfFont object(s) you want to use. I propose adding a new PdfDocument::fonts() collection to facilitate this. Adding a new PdfFont to that collection will give you back a token that you can use to refer to the font from that point forward. The token will just be a numeric value that is not bound to a reference of PdfDocument itself.

Added PdfDocument::fonts() and PdfDocument::fonts_mut() accessor functions, providing access to the PdfFonts collection for a document. Creating a PdfFont using the PdfFonts collection returns a PdfFontToken that can be used to refer to the font later, bypassing issues of lifetime and mutability.

With these changes in place, I can now run your sample like so:

use pdfium_render::prelude::*;

fn main() -> Result<(), PdfiumError> {
    let pdfium = Pdfium::new(
        Pdfium::bind_to_library(Pdfium::pdfium_platform_library_name_at_path("../pdfium/"))
            .or_else(|_| Pdfium::bind_to_system_library())?,
    );

    let mut doc = pdfium.create_new_pdf()?;

    let font = doc
        .fonts_mut()
        .load_true_type_from_bytes(include_bytes!("../font.ttf"), true)?;

    let margin = PdfPoints::new(12.0);
    let mut page_num = 1;

    doc.pages_mut().create_page_at_end(PdfPagePaperSize::a4())?;

    let mut page = doc.pages().last()?;

    let mut top = page.height() - margin;

    for i in 1..5000 {
        if top < margin {
            page_num += 1;
            page = doc.pages_mut().create_page_at_end(PdfPagePaperSize::a4())?;
            top = page.height() - margin;
        }

        // note that I could load the font here, it'd just be far, far slower and less efficient

        let mut object = PdfPageTextObject::new(
            &doc,
            format!("Item #{} on page #{}", i, page_num),
            font, // Note: now passing in a PdfFontToken instead of a &PdfFont
            PdfPoints::new(8.0),
        )?;
        object.translate(margin, top)?;
        top = top - margin;
        page.objects_mut().add_text_object(object)?;
    }

    doc.save_to_file("./result.pdf")?;

    Ok(())
}

Still to do:

  • Pass test suite
  • README updates

Updated examples/ to pass full test suite.

Bumped crate version to 0.8.3. Updated README.md. Made tweaks to internal PdfFont documentation. Scheduled for release as crate version 0.8.3. Waiting for confirmation from @stephenjudkins that the original problem is resolved.

Original problem is solved! Greatly appreciate the work here.