qpdf 0.3.5

Rust bindings to QPDF C++ library
Documentation
use std::collections::HashSet;

use qpdf::*;

fn load_pdf() -> QPdf {
    QPdf::read("tests/data/test.pdf").unwrap()
}

fn load_pdf_from_memory() -> QPdf {
    let data = std::fs::read("tests/data/test.pdf").unwrap();
    QPdf::read_from_memory(data).unwrap()
}

#[test]
fn test_qpdf_version() {
    let version = dbg!(QPdf::library_version());
    assert!(!version.is_empty());
}

#[test]
fn test_writer() {
    let qpdf = load_pdf();
    let mut writer = qpdf.writer();
    writer
        .force_pdf_version("1.7")
        .normalize_content(true)
        .preserve_unreferenced_objects(true)
        .object_stream_mode(ObjectStreamMode::Disable)
        .linearize(true)
        .compress_streams(true)
        .stream_data_mode(StreamDataMode::Compress);

    let mem = writer.write_to_memory().unwrap();

    let mem_pdf = QPdf::read_from_memory(mem).unwrap();
    assert_eq!(mem_pdf.get_pdf_version(), "1.7");
    assert!(mem_pdf.is_linearized());
}

#[test]
fn test_pdf_from_scratch() {
    let qpdf = QPdf::empty();

    let font = qpdf
        .parse_object(
            r#"<<
                        /Type /Font
                        /Subtype /Type1
                        /Name /F1
                        /BaseFont /Helvetica
                        /Encoding /WinAnsiEncoding
                      >>"#,
        )
        .unwrap();

    let procset = qpdf.parse_object("[/PDF /Text]").unwrap();
    let contents = qpdf.new_stream(b"BT /F1 15 Tf 72 720 Td (First Page) Tj ET\n");
    let mediabox = qpdf.parse_object("[0 0 612 792]").unwrap();
    let rfont = qpdf.new_dictionary_from([("/F1", font.into_indirect())]);
    let resources = qpdf.new_dictionary_from([("/ProcSet", procset.into_indirect()), ("/Font", rfont.into())]);
    let page = qpdf.new_dictionary_from([
        ("/Type", qpdf.new_name("/Page")),
        ("/MediaBox", mediabox),
        ("/Contents", contents.into()),
        ("/Resources", resources.into()),
    ]);

    qpdf.add_page(page.into_indirect(), true).unwrap();

    let mem = qpdf
        .writer()
        .static_id(true)
        .force_pdf_version("1.7")
        .normalize_content(true)
        .preserve_unreferenced_objects(true)
        .object_stream_mode(ObjectStreamMode::Preserve)
        .linearize(true)
        .compress_streams(true)
        .stream_data_mode(StreamDataMode::Preserve)
        .write_to_memory()
        .unwrap();

    let mem_pdf = QPdf::read_from_memory(mem).unwrap();
    assert_eq!(mem_pdf.get_pdf_version(), "1.7");
    assert!(mem_pdf.is_linearized());
}

#[test]
fn test_qpdf_basic_objects() {
    let qpdf = QPdf::empty();
    let obj = qpdf.new_bool(true);
    assert!(obj.get_type() == QPdfObjectType::Boolean && obj.as_bool());
    assert_eq!(obj.to_string(), "true");

    let obj = qpdf.new_name("foo");
    assert!(obj.get_type() == QPdfObjectType::Name && obj.as_name() == "foo");
    assert_eq!(obj.to_string(), "foo");

    let obj = qpdf.new_integer(12_3456_7890);
    assert!(obj.is_scalar() && obj.as_i64() == 12_3456_7890);
    assert_eq!(obj.to_string(), "1234567890");

    let obj = qpdf.new_null();
    assert_eq!(obj.get_type(), QPdfObjectType::Null);
    assert_eq!(obj.to_string(), "null");

    let obj = qpdf.new_real(1.2345, 3);
    assert_eq!(obj.as_real(), "1.234");
    assert_eq!(obj.to_string(), "1.234");

    let obj = qpdf.new_stream([]);
    assert_eq!(obj.get_type(), QPdfObjectType::Stream);
    assert_eq!(obj.to_string(), "3 0 R");

    obj.get_dictionary().set("/Type", qpdf.new_name("/Stream"));

    let obj_id = obj.get_id();
    assert_ne!(obj.into_indirect().get_id(), obj_id);
}

#[test]
fn test_qpdf_streams() {
    let qpdf = QPdf::empty();

    let obj = qpdf.get_object_by_id(1234, 1);
    assert!(obj.is_none());

    let obj = qpdf.new_stream_with_dictionary([("/Type", qpdf.new_name("/Test"))], [1, 2, 3, 4]);
    assert_eq!(obj.get_type(), QPdfObjectType::Stream);

    let by_id: QPdfStream = qpdf
        .get_object_by_id(obj.get_id(), obj.get_generation())
        .unwrap()
        .into();
    println!("{by_id}");

    let data = by_id.get_data(StreamDecodeLevel::None).unwrap();
    assert_eq!(data.as_ref(), &[1, 2, 3, 4]);

    assert_eq!(obj.get_dictionary().get("/Type").unwrap().as_name(), "/Test");

    let indirect = obj.into_indirect();
    assert!(indirect.is_indirect());
    assert_ne!(indirect.get_id(), 0);
    assert_eq!(indirect.get_generation(), 0);
}

#[test]
fn test_parse_object() {
    let text = "<< /Type /Page /Resources << /XObject null >> /MediaBox null /Contents null >>";
    let qpdf = QPdf::empty();
    let obj = qpdf.parse_object(text).unwrap();
    assert_eq!(obj.get_type(), QPdfObjectType::Dictionary);
    println!("{obj}");
    println!("version: {}", qpdf.get_pdf_version());
}

#[test]
fn test_error() {
    let qpdf = QPdf::empty();
    assert!(qpdf.get_page(0).is_none());
    let result = qpdf.parse_object("<<--< /Type -- null >>");
    assert!(result.is_err());
    println!("{result:?}");
}

#[test]
fn test_array() {
    let qpdf = QPdf::empty();
    let mut arr = qpdf.new_array();
    arr.push(qpdf.new_integer(1));
    arr.push(qpdf.new_integer(2));
    arr.push(qpdf.new_integer(3));
    assert_eq!(arr.to_string(), "[ 1 2 3 ]");

    assert!(arr.get(10).is_none());

    assert_eq!(
        arr.iter().map(|v| QPdfScalar::from(v).as_i32()).collect::<Vec<_>>(),
        vec![1, 2, 3]
    );

    arr.set(1, qpdf.new_integer(5));
    assert_eq!(arr.to_string(), "[ 1 5 3 ]");
}

#[test]
fn test_dictionary() {
    let qpdf = QPdf::empty();
    let dict: QPdfDictionary = qpdf
        .parse_object("<< /Type /Page /Resources << /XObject null >> /MediaBox [1 2 3 4] /Contents (hello) >>")
        .unwrap()
        .into();

    let keys = dict.keys().into_iter().collect::<HashSet<_>>();
    assert_eq!(
        keys,
        ["/Type", "/Resources", "/MediaBox", "/Contents"]
            .into_iter()
            .map(|s| s.to_owned())
            .collect::<HashSet<_>>()
    );

    assert_eq!(dict.get("/Type").unwrap().get_type(), QPdfObjectType::Name);
    assert_eq!(dict.get("/Contents").unwrap().as_string(), "hello");

    let bval = qpdf.new_bool(true);
    dict.set("/MyKey", &bval);

    let setval = dict.get("/MyKey").unwrap();
    assert!(setval.as_bool());
    assert_ne!(bval, setval);

    dict.remove("/MyKey");
    assert!(dict.get("/MyKey").is_none());
}

#[test]
fn test_strings() {
    let qpdf = QPdf::empty();
    let bin_str = qpdf.new_binary_string([1, 2, 3, 4]);
    assert_eq!(bin_str.to_string(), "<01020304>");

    let utf8_str = qpdf.new_utf8_string("привет");
    assert_eq!(utf8_str.to_string(), "<feff043f04400438043204350442>");

    let plain_str = qpdf.new_string("hello");
    assert_eq!(plain_str.to_string(), "(hello)");
    assert_eq!(plain_str.to_binary(), "<68656c6c6f>");
}

#[test]
fn test_pdf_ops() {
    let qpdf = load_pdf_from_memory();
    println!("{:?}", qpdf.get_pdf_version());

    let trailer = qpdf.get_trailer().unwrap();
    println!("trailer: {trailer}");

    let root = qpdf.get_root().unwrap();
    println!("root: {root}");
    assert_eq!(root.get("/Type").unwrap().as_name(), "/Catalog");
    assert!(root.has("/Pages"));

    let pages = qpdf.get_pages().unwrap();
    assert_eq!(pages.len(), 2);

    for page in pages {
        let keys = page.keys();
        assert!(!keys.is_empty());
        println!("{keys:?}");

        let data = page.get_page_content_data().unwrap();
        println!("{}", String::from_utf8_lossy(data.as_ref()));

        qpdf.add_page(&page, false).unwrap();
    }

    let buffer = qpdf.writer().write_to_memory().unwrap();
    let saved_pdf = QPdf::read_from_memory(&buffer).unwrap();
    assert_eq!(saved_pdf.get_num_pages().unwrap(), 4);

    let pages = saved_pdf.get_pages().unwrap();
    for page in pages {
        saved_pdf.remove_page(&page).unwrap();
    }
    assert_eq!(saved_pdf.get_num_pages().unwrap(), 0);
}

#[test]
fn test_pdf_encrypted_read() {
    let qpdf = QPdf::read("tests/data/encrypted.pdf");
    assert!(qpdf.is_err());
    println!("{qpdf:?}");

    let qpdf = QPdf::read_encrypted("tests/data/encrypted.pdf", "test");
    assert!(qpdf.is_ok());

    let data = std::fs::read("tests/data/encrypted.pdf").unwrap();
    let qpdf = QPdf::read_from_memory_encrypted(data, "test");
    assert!(qpdf.is_ok());
}

fn test_pdf_encrypted_write(params: EncryptionParams) {
    let qpdf = QPdf::read("tests/data/test.pdf").unwrap();

    let data = qpdf.writer().encryption_params(params).write_to_memory().unwrap();

    let qpdf = QPdf::read_from_memory_encrypted(&data, "foo");
    assert!(qpdf.is_ok());

    let qpdf = QPdf::read_from_memory_encrypted(&data, "foo2");
    assert!(qpdf.is_err());
}

#[cfg(feature = "legacy")]
#[test]
fn test_pdf_encrypted_r2_write() {
    test_pdf_encrypted_write(EncryptionParams::R2(EncryptionParamsR2 {
        user_password: "foo".to_string(),
        owner_password: "foo".to_string(),
        allow_print: true,
        allow_modify: true,
        allow_extract: true,
        allow_annotate: true,
    }))
}

#[cfg(feature = "legacy")]
#[test]
fn test_pdf_encrypted_r3_write() {
    test_pdf_encrypted_write(EncryptionParams::R3(EncryptionParamsR3 {
        user_password: "foo".to_string(),
        owner_password: "foo".to_string(),
        allow_accessibility: true,
        allow_extract: true,
        allow_assemble: true,
        allow_annotate_and_form: true,
        allow_form_filling: true,
        allow_modify_other: true,
        allow_print: Default::default(),
    }))
}

#[cfg(feature = "legacy")]
#[test]
fn test_pdf_encrypted_r4_write() {
    test_pdf_encrypted_write(EncryptionParams::R4(EncryptionParamsR4 {
        user_password: "foo".to_string(),
        owner_password: "foo".to_string(),
        allow_accessibility: true,
        allow_extract: true,
        allow_assemble: true,
        allow_annotate_and_form: true,
        allow_form_filling: true,
        allow_modify_other: true,
        allow_print: Default::default(),
        encrypt_metadata: true,
        use_aes: true,
    }))
}

#[test]
fn test_pdf_encrypted_r6_write() {
    test_pdf_encrypted_write(EncryptionParams::R6(EncryptionParamsR6 {
        user_password: "foo".to_string(),
        owner_password: "foo".to_string(),
        allow_accessibility: true,
        allow_extract: true,
        allow_assemble: true,
        allow_annotate_and_form: true,
        allow_form_filling: true,
        allow_modify_other: true,
        allow_print: Default::default(),
        encrypt_metadata: true,
    }))
}

#[test]
fn test_foreign_objects() {
    let source = load_pdf_from_memory();
    let sink = QPdf::empty();

    let page = sink.copy_from_foreign(&source.get_pages().unwrap()[0]);
    drop(source);

    sink.add_page(page, false).unwrap();

    // shouldn't crash
    sink.writer()
        .static_id(true)
        .force_pdf_version("1.7")
        .normalize_content(true)
        .preserve_unreferenced_objects(false)
        .object_stream_mode(ObjectStreamMode::Preserve)
        .compress_streams(false)
        .stream_data_mode(StreamDataMode::Preserve)
        .write_to_memory()
        .unwrap();
}

#[test]
fn test_foreign_pages() {
    let sink = QPdf::empty();

    for source in [load_pdf_from_memory(), load_pdf_from_memory(), load_pdf_from_memory()] {
        source.get_pages().unwrap().iter().for_each(|p| {
            sink.add_page(p, false).unwrap();
        });
    }

    // shouldn't crash
    sink.writer().write_to_memory().unwrap();
}