Files
zed/crates/pdf_viewer/tests/integration_tests.rs
T
yenru0 5ee6aa1e50 image_viewer: Fix swapped red and blue channels in PDF pages
GPUI's RenderImage contract is BGRA (assets.rs) and every platform atlas
uploads the bytes raw into BGRA8 textures. All other decode paths swap
R/B before building a RenderImage; the PDF path fed pdfium's RGBA buffer
straight through, so PDF pages rendered with swapped channels on every
platform (white/gray unaffected, colors wrong).

Swap R/B at both RenderImage construction sites (page view + thumbnails)
and pin pdf_viewer's RGBA raster contract with a native channel-order
regression test (skips when no libpdfium is present).
2026-10-03 20:09:25 +09:00

189 lines
6.6 KiB
Rust

use pdf_viewer::rasterizer::RasterizerOptions;
use pdf_viewer::{PageLayoutMode, PdfToolbarState, PdfView, PdfViewerSettings, PdfiumEngine};
use std::time::Duration;
const MINIMAL_PDF: &[u8] = b"%PDF-1.7\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\nxref\n0 4\n0000000000 65535 f \n0000000010 00000 n \n0000000060 00000 n \n0000000117 00000 n \ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n185\n%%EOF\n";
#[test]
fn test_end_to_end_pdf_view_lifecycle() {
let settings = PdfViewerSettings {
cache_budget_mb: 16,
..Default::default()
};
let mut view = PdfView::new(settings);
let sample_pdf = MINIMAL_PDF;
view.load_from_bytes(sample_pdf.to_vec(), None)
.expect("Load sample PDF bytes");
assert_eq!(view.total_pages(), 1);
assert_eq!(view.display_page_number(), 1);
// Initial container layout
view.set_container_size(1024.0, 768.0);
assert!(view.zoom_percentage() > 0);
// Zoom manipulations
view.set_zoom(1.5, Some((512.0, 384.0)));
assert_eq!(view.zoom_percentage(), 150);
view.zoom_in(None);
assert!(view.zoom_percentage() > 150);
view.reset_zoom();
assert_eq!(view.zoom_percentage(), 100);
// Rendering page
let rendered = view.get_or_render_page(0).expect("Render page 0");
assert!(rendered.width > 0);
assert!(rendered.height > 0);
assert!(rendered.dark_mode); // smart dark mode default
// Dark mode toggle
view.toggle_dark_mode();
let rendered_light = view.get_or_render_page(0).expect("Render light mode");
assert!(!rendered_light.dark_mode);
}
#[test]
fn test_reload_preserving_exact_viewport_and_zoom() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let pdf_path = temp_dir.path().join("document.pdf");
// Write initial PDF
{
std::fs::write(&pdf_path, MINIMAL_PDF).expect("write");
}
let mut view = PdfView::new(PdfViewerSettings::default());
view.open_file(&pdf_path).expect("open file");
// Simulate user zooming and panning
view.set_zoom(2.5, None);
view.handle_mouse_down((100.0, 100.0));
view.handle_mouse_move((150.0, 200.0));
view.handle_mouse_up();
let initial_zoom = view.zoom_percentage();
let snapshot = view.save_state_snapshot();
assert_eq!(initial_zoom, 250);
assert_eq!(snapshot.pan_x, 50.0);
assert_eq!(snapshot.pan_y, 100.0);
// Sleep to ensure timestamp difference
std::thread::sleep(Duration::from_millis(25));
// Compiler rebuilds the PDF
{
std::fs::write(&pdf_path, MINIMAL_PDF).expect("rebuild");
}
// Debounced hot reload trigger
let reloaded = view.reload_preserving_state().expect("hot reload");
assert!(reloaded, "Reload should succeed");
// Verify viewport state was strictly preserved
assert_eq!(view.zoom_percentage(), initial_zoom);
let new_snapshot = view.save_state_snapshot();
assert_eq!(new_snapshot.pan_x, 50.0);
assert_eq!(new_snapshot.pan_y, 100.0);
}
#[test]
fn test_toolbar_synchronization_with_view_state() {
let view = PdfView::new(PdfViewerSettings::default());
let toolbar_state = PdfToolbarState::new(
view.display_page_number(),
view.total_pages(),
view.zoom_percentage(),
view.dark_mode(),
view.layout_mode(),
);
assert_eq!(toolbar_state.current_page, 1);
assert_eq!(toolbar_state.zoom_percentage, 100);
assert!(toolbar_state.dark_mode);
assert_eq!(toolbar_state.layout_mode, PageLayoutMode::Continuous);
}
/// Builds a single-page PDF whose entire canvas is filled pure red, with a fully
/// computed xref table so no repair parsing is involved.
fn red_page_pdf() -> Vec<u8> {
let content = b"1 0 0 rg 0 0 612 792 re f";
let objects: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << >> /Contents 4 0 R >>"
.to_vec(),
format!(
"<< /Length {} >>\nstream\n{}\nendstream",
content.len(),
String::from_utf8_lossy(content)
)
.into_bytes(),
];
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::with_capacity(objects.len());
for (index, object) in objects.iter().enumerate() {
offsets.push(out.len() as u32);
out.extend_from_slice(format!("{} 0 obj\n", index + 1).as_bytes());
out.extend_from_slice(object);
out.extend_from_slice(b"\nendobj\n");
}
let xref_offset = out.len() as u32;
out.extend_from_slice(format!("xref\n0 {}\n", objects.len() + 1).as_bytes());
out.extend_from_slice(b"0000000000 65535 f \n");
for offset in &offsets {
out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(
format!(
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{}\n%%EOF",
objects.len() + 1,
xref_offset
)
.as_bytes(),
);
out
}
/// Guards against PDFium's native BGRA bitmap order leaking through: the rasterizer
/// must hand out RGBA buffers, so a red page must read as high-R/low-B. The mock
/// renderer also draws red, so this only proves something when a native engine
/// bound; without libpdfium the test cannot exercise the real byte order.
#[test]
fn test_native_render_outputs_rgba_channel_order() {
let engine = PdfiumEngine::new();
if !engine.is_native_available() {
eprintln!("skipping: native libpdfium not available on this machine");
return;
}
let rendered = engine
.render_page_from_bytes(&red_page_pdf(), 0, RasterizerOptions::default())
.expect("native render should succeed");
let stride = rendered.width as usize * 4;
assert_eq!(rendered.rgba_buffer.len(), stride * rendered.height as usize);
for (x, y) in [
(rendered.width as usize / 2, rendered.height as usize / 2),
(rendered.width as usize / 4, rendered.height as usize / 4),
(
rendered.width as usize * 3 / 4,
rendered.height as usize * 3 / 4,
),
] {
let pixel = &rendered.rgba_buffer[y * stride + x * 4..y * stride + x * 4 + 4];
let (r, g, b, a) = (pixel[0], pixel[1], pixel[2], pixel[3]);
assert!(
r > 200 && b < 60,
"buffer is not RGBA-ordered at ({x},{y}): R={r} G={g} B={b} A={a}"
);
assert_eq!(a, 255, "opaque page should carry alpha 255");
}
}