2
\$\begingroup\$

I want to create thumbnails from PDF files – specifically, a thumbnail of the first page. Currently I use pdftocairo on the command line:

$ pdftocairo myfile.pdf -png -singlefile -scale-to-x 400 -scale-to-y -1

but I wanted to see if I could implement this without shelling out to an external tool. I've written a function in Rust that links to poppler, takes the path to a PDF file, and a path to write a thumbnail to. It reads the first page of the PDF, renders it as a PNG, and saves it to the thumbnail path.

The function seems to work in the handful of PDFs I've tested, and I have a larger corpus I can test on if I decide to use this code.

I'd be interested in knowing:

  • Is this idiomatic Rust, especially the error handling? I don't have much experience writing Rust and I've been doing everything on my own, so my knowledge of what counts as "good" Rust code is quite limited.

  • Are there any bugs? I think I've covered the happy path and all the errors (thanks Rust!), but I might have missed something less obvious.

  • Is there a better way of doing this in Rust? I'm not tied to Cairo or Poppler, and if there's a Rust-native PDF library or similar that can do this sort of thing, I'd have a look at that instead.

extern crate cairo;
extern crate glib;
extern crate poppler;

use std::convert::From;
use std::fs::File;
use std::result::Result;

use cairo::{Context, Format, ImageSurface};
use poppler::{PopplerDocument, PopplerPage};

#[derive(Debug)]
pub enum ThumbnailError {
    GlibError(glib::error::Error),
    NoPagesError,
    CairoError(cairo::Status),
    CairoIoError(cairo::IoError),
    IoError(std::io::Error),
}

impl From<glib::error::Error> for ThumbnailError {
    fn from(err: glib::error::Error) -> Self {
        ThumbnailError::GlibError(err)
    }
}

impl From<cairo::Status> for ThumbnailError {
    fn from(status: cairo::Status) -> Self {
        ThumbnailError::CairoError(status)
    }
}

impl From<cairo::IoError> for ThumbnailError {
    fn from(err: cairo::IoError) -> Self {
        ThumbnailError::CairoIoError(err)
    }
}

impl From<std::io::Error> for ThumbnailError {
    fn from(err: std::io::Error) -> Self {
        ThumbnailError::IoError(err)
    }
}

/// Create a JPEG thumbnail from the first page of a PDF.
fn create_thumbnail(pdf_path: &str, out_path: &str) -> Result<(), ThumbnailError> {

    // Assume the PDF is not password protected.
    let doc: PopplerDocument = PopplerDocument::new_from_file(pdf_path, "")?;

    // Note: PDF pages are 0-indexed
    let page: PopplerPage = match doc.get_page(0) {
        Some(p) => p,
        None => return Err(ThumbnailError::NoPagesError),
    };

    let (width, height) = page.get_size();

    let mut surface = ImageSurface::create(
        Format::Rgb24,
        width as i32,
        height as i32)?;

    // Draw a white background to start with.  If you don't, any transparent
    // regions in the PDF will be rendered as black in the final image.
    let ctxt = Context::new(&mut surface);
    ctxt.set_source_rgb(1.0 as f64, 1.0 as f64, 1.0 as f64);
    ctxt.paint();

    // Draw the contents of the PDF onto the page.
    page.render(&ctxt);

    let mut f: File = File::create(out_path)?;
    surface.write_to_png(&mut f)?;    

    Ok(())
}

fn main() {
    let pdf_path = "myfile.pdf";
    let out_path = "thumbnail.png";

    match create_thumbnail(pdf_path, out_path) {
        Ok(_) => println!("Created thumbnail of {} at {}", pdf_path, out_path),
        Err(err) => println!("Something went wrong: {:?}", err),
    };
}
```
\$\endgroup\$
2
\$\begingroup\$

This seems pretty good to me. I didn't notice any bugs, and I don't have knowledge of what pdf crates are available, so I just have a few points about idiomatic Rust.

To start, I just want to note that when I tried to create this project locally to work on, I had to fiddle around with the versions of the crates to get a working combination, which makes it an especially good idea to include what versions you're using somewhere in your question.

You can simply remove all uses of extern crate some_crate, since they aren't needed anymore in rust 2018 edition (the default by now).

I like the use of an error enum. You could consider using something like the crate thiserror to derive Display and From instances for the enum. (This just reduces boilerplate.)

I often like having type Result<T, E = MyErrorType> = std::result::Result<T, E>;, especially for a library, which lets me write Result<()> to use my error or Result<(), OtherError> to use another.

pub fn create_thumbnail(pdf_path: &Path, out_path: &Path) -> Result<()> {
    let doc = PopplerDocument::new_from_file(pdf_path, "")?;

    let page = doc.get_page(0).ok_or(Error::NoPagesError)?;

    let (width, height) = page.get_size();

    let surface = ImageSurface::create(Format::Rgb24, width as i32, height as i32)?;

    let ctxt = Context::new(&surface);
    ctxt.set_source_rgb(1.0, 1.0, 1.0);
    ctxt.paint();

    page.render(&ctxt);

    let mut f = File::create(out_path)?;
    surface.write_to_png(&mut f)?;

    Ok(())
}

You should use Paths when you're representing paths. You can use ok_or or ok_or_else to convert Option to Result, which works well with ?. Rust can infer the type of your floats in set_source_rgb, but if you want to be explicit then you should use 1.0f64 rather than 1.0 as f64. I removed your comments just for space in the answer, but they are meaningful since they give reason and don't just restate the code.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the comments! I didn't realise you could drop extern crate now, and the other stuff is useful too. I’ll add this to the code. \$\endgroup\$ – alexwlchan Jan 14 at 21:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.