rpdfium 7676.6.4

A faithful Rust port of Google's PDFium PDF rendering engine
Documentation
//! Integration tests for the rendering pipeline.
//!
//! Each test builds a programmatic PDF, parses it, interprets the content stream,
//! and renders the display tree to verify pixel output.

use rpdfium_core::ParsingMode;
use rpdfium_page::{DefaultFontCache, InterpreterContext, ResourceDict, interpret};
use rpdfium_parser::ObjectStore;
use rpdfium_render::{RenderConfig, RgbaColor, render};

/// Build a minimal PDF with a content stream on a single page.
fn build_pdf_with_content(content: &[u8]) -> Vec<u8> {
    let mut pdf = Vec::new();
    pdf.extend_from_slice(b"%PDF-1.4\n");

    let obj1_offset = pdf.len();
    pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");

    let obj2_offset = pdf.len();
    pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");

    let obj3_offset = pdf.len();
    pdf.extend_from_slice(
        b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >>\nendobj\n",
    );

    let obj4_offset = pdf.len();
    pdf.extend_from_slice(format!("4 0 obj\n<< /Length {} >>\nstream\n", content.len()).as_bytes());
    pdf.extend_from_slice(content);
    pdf.extend_from_slice(b"\nendstream\nendobj\n");

    let xref_offset = pdf.len();
    pdf.extend_from_slice(b"xref\n0 5\n");
    pdf.extend_from_slice(b"0000000000 65535 f \r\n");
    pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
    pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
    pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj3_offset).as_bytes());
    pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj4_offset).as_bytes());
    pdf.extend_from_slice(b"trailer\n<< /Size 5 /Root 1 0 R >>\n");
    pdf.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF").as_bytes());

    pdf
}

fn parse_and_render(content: &[u8], width: u32, height: u32) -> rpdfium_graphics::Bitmap {
    let pdf = build_pdf_with_content(content);
    let store = ObjectStore::open(pdf, ParsingMode::Lenient).unwrap();
    let font_cache = DefaultFontCache;
    let ctx = InterpreterContext {
        store: &store,
        font_cache: &font_cache,
        mode: ParsingMode::Lenient,
        oc_context: None,
    };
    let resources = ResourceDict::default();

    // Tokenize and interpret
    let operators = rpdfium_parser::tokenize_content_stream(content).unwrap();
    let tree = interpret(
        &operators,
        &ctx,
        &resources,
        rpdfium_core::fx_system::DEFAULT_MAX_OPERATORS_PER_PAGE,
    )
    .unwrap();

    // Render
    let config = RenderConfig {
        width,
        height,
        background: RgbaColor::WHITE,
        ..RenderConfig::default()
    };
    render(&tree, &config).unwrap()
}

#[test]
fn test_render_empty_page() {
    let bitmap = parse_and_render(b"", 100, 100);
    assert_eq!(bitmap.width, 100);
    assert_eq!(bitmap.height, 100);
    // All pixels should be white (background)
    assert!(
        bitmap
            .data
            .chunks(4)
            .all(|px| px[0] == 255 && px[1] == 255 && px[2] == 255)
    );
}

#[test]
fn test_render_red_rectangle() {
    let content = b"1 0 0 rg 50 50 100 100 re f";
    let bitmap = parse_and_render(content, 200, 200);
    assert_eq!(bitmap.width, 200);
    // With identity transform, PDF coords map directly.
    // The rectangle is at (50,50)-(150,150).
    // Check that there are red pixels somewhere in the bitmap.
    let has_red = bitmap
        .data
        .chunks(4)
        .any(|px| px[0] == 255 && px[1] == 0 && px[2] == 0);
    assert!(has_red, "expected red pixels in the bitmap");
}

#[test]
fn test_render_gray_fill() {
    let content = b"0.5 g 0 0 200 200 re f";
    let bitmap = parse_and_render(content, 200, 200);
    // Should have gray pixels (value ~127)
    let has_gray = bitmap.data.chunks(4).any(|px| {
        let r = px[0] as i16;
        // Allow a small tolerance for rounding
        (r - 127).unsigned_abs() <= 1
    });
    assert!(has_gray, "expected gray pixels in the bitmap");
}

#[test]
fn test_render_blue_fill() {
    let content = b"0 0 1 rg 0 0 100 100 re f";
    let bitmap = parse_and_render(content, 100, 100);
    let has_blue = bitmap
        .data
        .chunks(4)
        .any(|px| px[0] == 0 && px[1] == 0 && px[2] == 255);
    assert!(has_blue, "expected blue pixels in the bitmap");
}

#[test]
fn test_render_stroked_line() {
    // Draw a diagonal line with 2pt stroke in black
    let content = b"2 w 0 0 m 100 100 l S";
    let bitmap = parse_and_render(content, 100, 100);
    // Should have some non-white pixels (the stroke)
    let has_non_white = bitmap
        .data
        .chunks(4)
        .any(|px| px[0] < 255 || px[1] < 255 || px[2] < 255);
    assert!(has_non_white, "expected stroked pixels in the bitmap");
}

// ---------------------------------------------------------------------------
// High-level facade PDF-to-bitmap render integration tests
//
// These tests use the `rpdfium` facade API (Document + Page::render) rather
// than the low-level rpdfium_page / rpdfium_render crates directly.
// ---------------------------------------------------------------------------

/// Build a minimal PDF whose single page has the given content stream and
/// a 200×200 MediaBox.  Returns raw PDF bytes.
fn build_facade_pdf(content: &[u8]) -> Vec<u8> {
    build_pdf_with_content(content)
}

#[test]
fn test_facade_render_blank_page_produces_correct_dimensions() {
    // Build a minimal PDF with an empty content stream.
    let pdf_bytes = build_facade_pdf(b"");

    let lib = rpdfium::Library::new();
    let opts = rpdfium::OpenOptions::default();
    let doc =
        rpdfium::Document::open(&lib, pdf_bytes, &opts).expect("document should open successfully");

    assert_eq!(doc.page_count(), 1, "expected exactly one page");

    let page = doc.page(0).expect("page 0 should be accessible");
    let media_box = page.media_box();

    // Render to 200×200 with white background using the page media box so that
    // the correct page-to-device coordinate transform is computed.
    let config = rpdfium::RenderConfig {
        width: 200,
        height: 200,
        background: rpdfium::RgbaColor::WHITE,
        media_box: Some(media_box),
        ..rpdfium::RenderConfig::default()
    };
    let bitmap = page.render(&config).expect("render should succeed");

    assert_eq!(bitmap.width, 200, "bitmap width should be 200");
    assert_eq!(bitmap.height, 200, "bitmap height should be 200");
    // 200×200 RGBA = 160 000 bytes
    assert!(
        !bitmap.data.is_empty(),
        "bitmap pixel data must not be empty"
    );
    assert_eq!(
        bitmap.data.len(),
        200 * 200 * 4,
        "bitmap should have 200×200×4 bytes for RGBA"
    );
}

#[test]
fn test_facade_render_blank_page_is_all_white() {
    let pdf_bytes = build_facade_pdf(b"");

    let lib = rpdfium::Library::new();
    let opts = rpdfium::OpenOptions::default();
    let doc = rpdfium::Document::open(&lib, pdf_bytes, &opts).unwrap();
    let page = doc.page(0).unwrap();
    let media_box = page.media_box();

    let config = rpdfium::RenderConfig {
        width: 100,
        height: 100,
        background: rpdfium::RgbaColor::WHITE,
        media_box: Some(media_box),
        ..rpdfium::RenderConfig::default()
    };
    let bitmap = page.render(&config).unwrap();

    // An empty content stream on a white background must produce all-white pixels.
    let all_white = bitmap
        .data
        .chunks(4)
        .all(|px| px[0] == 255 && px[1] == 255 && px[2] == 255);
    assert!(all_white, "blank page should render as all-white pixels");
}

#[test]
fn test_facade_render_page_with_fill_produces_non_empty_bitmap() {
    // A page with a blue fill rectangle — verifies that the full pipeline
    // (parse → interpret → render via facade) produces non-trivial output.
    let content = b"0 0 1 rg 0 0 200 200 re f";
    let pdf_bytes = build_facade_pdf(content);

    let lib = rpdfium::Library::new();
    let opts = rpdfium::OpenOptions::default();
    let doc = rpdfium::Document::open(&lib, pdf_bytes, &opts).unwrap();
    let page = doc.page(0).unwrap();
    let media_box = page.media_box();

    let config = rpdfium::RenderConfig {
        width: 200,
        height: 200,
        background: rpdfium::RgbaColor::WHITE,
        media_box: Some(media_box),
        ..rpdfium::RenderConfig::default()
    };
    let bitmap = page.render(&config).unwrap();

    assert_eq!(bitmap.width, 200);
    assert_eq!(bitmap.height, 200);
    assert!(!bitmap.data.is_empty());
    // The rendered output must contain some non-white pixels from the blue fill.
    let has_non_white = bitmap
        .data
        .chunks(4)
        .any(|px| px[0] < 255 || px[1] < 255 || px[2] < 255);
    assert!(has_non_white, "filled page must produce non-white pixels");
}