mupdf 0.8.0

Safe Rust wrapper to MuPDF
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use mupdf::pdf::{PdfDocument, PdfObject, PdfPage};
use mupdf::shape::{
    FinishOptions, PdfColor, RectRadius, Shape, TextAlign, TextOptions, TextboxOptions,
};
use mupdf::{Colorspace, ImageFormat, Matrix, Point, Quad, Rect, Size};

const CUSTOM_FONT_BYTES: &[u8] = include_bytes!("../tests/files/custom.ttf");

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let output_dir = output_dir();
    fs::create_dir_all(&output_dir)?;

    let doc = build_shape_demo_document()?;
    let pdf_path = output_dir.join("shape_demo.pdf");
    doc.save(&pdf_path.to_string_lossy())?;

    render_saved_pages(&pdf_path, &output_dir)?;
    println!("wrote {}", pdf_path.display());
    Ok(())
}

fn output_dir() -> PathBuf {
    std::env::args_os()
        .nth(1)
        .map(PathBuf::from)
        .or_else(|| std::env::var_os("SHAPE_DEMO_OUTPUT_DIR").map(PathBuf::from))
        .unwrap_or_else(|| PathBuf::from("target/shape_demo"))
}

fn build_shape_demo_document() -> Result<PdfDocument, mupdf::Error> {
    let mut doc = PdfDocument::new();
    let oc_xref = add_ocg(&mut doc, "Shape demo optional content")?;

    let mut drawing_page = doc.new_page(Size::A4)?;
    draw_primitives_page(&mut doc, &mut drawing_page)?;

    let mut text_page = doc.new_page(Size::A4)?;
    draw_text_page(&mut doc, &mut text_page)?;

    let mut polish_page = doc.new_page(Size::A4)?;
    draw_polish_page(&mut doc, &mut polish_page, oc_xref)?;

    Ok(doc)
}

fn draw_primitives_page(doc: &mut PdfDocument, page: &mut PdfPage) -> Result<(), mupdf::Error> {
    let mut shape = Shape::new(page)?;
    shape
        .draw_line(Point::new(40.0, 48.0), Point::new(555.0, 48.0))?
        .draw_polyline(&[
            Point::new(60.0, 110.0),
            Point::new(120.0, 70.0),
            Point::new(180.0, 110.0),
            Point::new(60.0, 110.0),
        ])?
        .draw_rect(&Rect::new(220.0, 70.0, 330.0, 125.0))?
        .draw_rect_with_radius(
            &Rect::new(370.0, 70.0, 515.0, 130.0),
            RectRadius::absolute(12.0),
        )?
        .finish(&FinishOptions {
            color: Some(PdfColor::rgb(0.0, 0.0, 0.0)),
            fill: Some(PdfColor::rgb(0.92, 0.96, 1.0)),
            width: 1.5,
            close_path: false,
            ..Default::default()
        })?
        .draw_bezier(
            Point::new(60.0, 230.0),
            Point::new(110.0, 155.0),
            Point::new(180.0, 305.0),
            Point::new(230.0, 230.0),
        )?
        .draw_curve(
            Point::new(280.0, 230.0),
            Point::new(340.0, 150.0),
            Point::new(400.0, 230.0),
        )?
        .draw_sector(
            Point::new(495.0, 230.0),
            Point::new(540.0, 230.0),
            130.0,
            true,
        )?
        .finish(&FinishOptions {
            color: Some(PdfColor::rgb(0.75, 0.0, 0.0)),
            fill: Some(PdfColor::rgb(1.0, 0.88, 0.82)),
            width: 2.0,
            close_path: false,
            ..Default::default()
        })?
        .draw_circle(Point::new(105.0, 380.0), 38.0)?
        .draw_oval(Rect::new(190.0, 342.0, 320.0, 418.0))?
        .draw_quad(Quad::new(
            Point::new(385.0, 340.0),
            Point::new(520.0, 370.0),
            Point::new(360.0, 435.0),
            Point::new(495.0, 465.0),
        ))?
        .finish(&FinishOptions {
            color: Some(PdfColor::rgb(0.0, 0.35, 0.0)),
            fill: Some(PdfColor::rgb(0.86, 1.0, 0.86)),
            width: 2.0,
            close_path: false,
            ..Default::default()
        })?
        .draw_zigzag(Point::new(60.0, 570.0), Point::new(250.0, 570.0), 7.0)?
        .draw_squiggle(Point::new(315.0, 560.0), Point::new(535.0, 610.0), 7.0)?
        .finish(&FinishOptions {
            color: Some(PdfColor::rgb(0.1, 0.1, 0.7)),
            width: 1.5,
            close_path: false,
            ..Default::default()
        })?
        .commit(doc, true)?;
    Ok(())
}

fn draw_text_page(doc: &mut PdfDocument, page: &mut PdfPage) -> Result<(), mupdf::Error> {
    let mut shape = Shape::new(page)?;
    shape.insert_text(
        Point::new(72.0, 96.0),
        "Shape text at a point\nwith multiple lines",
        &TextOptions {
            fontsize: 24.0,
            lineheight: 1.15,
            fill: Some(PdfColor::rgb(0.0, 0.0, 0.0)),
            ..Default::default()
        },
    )?;
    shape.insert_textbox(
        Rect::new(72.0, 185.0, 300.0, 320.0),
        "Left aligned textbox wraps text into a rectangle using built-in font metrics.",
        &TextboxOptions {
            fontsize: 15.0,
            lineheight: 1.1,
            fill: Some(PdfColor::rgb(0.0, 0.2, 0.65)),
            ..Default::default()
        },
    )?;
    shape.insert_textbox(
        Rect::new(330.0, 185.0, 535.0, 320.0),
        "Centered textbox text demonstrates alternate alignment and word wrapping.",
        &TextboxOptions {
            fontsize: 15.0,
            lineheight: 1.1,
            align: TextAlign::Center,
            fill: Some(PdfColor::rgb(0.55, 0.0, 0.0)),
            ..Default::default()
        },
    )?;
    shape
        .insert_text(
            Point::new(92.0, 500.0),
            "Rotated text",
            &TextOptions {
                fontsize: 20.0,
                rotate: 90,
                fill: Some(PdfColor::rgb(0.0, 0.45, 0.0)),
                ..Default::default()
            },
        )?
        .commit(doc, true)?;
    Ok(())
}

fn draw_polish_page(
    doc: &mut PdfDocument,
    page: &mut PdfPage,
    oc_xref: i32,
) -> Result<(), mupdf::Error> {
    let mut shape = Shape::new(page)?;
    shape
        .draw_circle(Point::new(145.0, 150.0), 70.0)?
        .finish(&FinishOptions {
            color: Some(PdfColor::rgb(0.0, 0.0, 0.0)),
            fill: Some(PdfColor::rgb(1.0, 0.0, 0.0)),
            width: 2.0,
            fill_opacity: Some(0.45),
            oc: Some(oc_xref),
            ..Default::default()
        })?
        .draw_rect_with_radius(
            &Rect::new(250.0, 85.0, 510.0, 215.0),
            RectRadius::fractional(0.12, 0.25),
        )?
        .finish(&FinishOptions {
            color: Some(PdfColor::rgb(0.0, 0.0, 0.0)),
            fill: Some(PdfColor::rgb(0.0, 0.25, 1.0)),
            width: 2.0,
            fill_opacity: Some(0.45),
            oc: Some(oc_xref),
            ..Default::default()
        })?
        .insert_text(
            Point::new(72.0, 310.0),
            "Custom font + opacity + optional content",
            &TextOptions {
                fontsize: 26.0,
                fontname: "ShapeDemoCustom".to_owned(),
                fontfile: Some(CUSTOM_FONT_BYTES),
                fill: Some(PdfColor::rgb(0.1, 0.1, 0.1)),
                fill_opacity: Some(0.8),
                oc: Some(oc_xref),
                ..Default::default()
            },
        )?;
    shape.insert_textbox(
        Rect::new(72.0, 390.0, 525.0, 515.0),
        "Justified text spreads words across each non-final line while the final line stays left aligned.",
        &TextboxOptions {
            fontsize: 16.0,
            lineheight: 1.15,
            align: TextAlign::Justify,
            fill: Some(PdfColor::rgb(0.0, 0.0, 0.0)),
            ..Default::default()
        },
    )?;
    shape.commit(doc, true)?;
    Ok(())
}

fn add_ocg(doc: &mut PdfDocument, name: &str) -> Result<i32, mupdf::Error> {
    let mut ocg = doc.new_dict_with_capacity(2)?;
    ocg.dict_put("Type", PdfObject::new_name("OCG")?)?;
    ocg.dict_put("Name", PdfObject::new_string(name)?)?;
    doc.add_object(&ocg)?.as_indirect()
}

fn render_saved_pages(
    pdf_path: &Path,
    output_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let bytes = fs::read(pdf_path)?;
    let doc = PdfDocument::from_bytes(&bytes)?;
    for page_index in 0..doc.page_count()? {
        let page = PdfPage::try_from(doc.load_page(page_index)?)?;
        let pixmap = page.to_pixmap(
            &Matrix::new_scale(1.0, 1.0),
            &Colorspace::device_rgb(),
            false,
            true,
        )?;
        let png_path = output_dir.join(format!("shape_demo_page_{}.png", page_index + 1));
        pixmap.save_as(&png_path.to_string_lossy(), ImageFormat::PNG)?;
    }
    Ok(())
}