//! Exports and the repaired copy (C11.8). use std::path::PathBuf; use ely_office::{Saving as _, Tabs as _}; use ely_pdf_engine::Optimizing; use gpui::TestAppContext; use super::{file, settle, shown, windowed}; use crate::output::{Kind, image_names}; #[test] fn image_names_pad_alike() { assert_eq!(image_names("doc", &[0, 9], 12), ["doc-01.png", "doc-10.png"]); } #[test] fn pages_export_as_images_and_text() { let dir = tempfile::tempdir().expect("a temp dir"); let mut cx = TestAppContext::single(); let path = file(dir.path(), "doc.pdf", &[(612.0, 792.0, &["First"]), (612.0, 792.0, &["Second"])]); let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true); shown(cx, &viewer); let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id); let targets: Vec = ["doc-1.png", "doc-2.png"].iter().map(|name| dir.path().join(name)).collect(); cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_images(id, (vec![0, 1], 0), 72.0, targets.clone(), cx))); settle(cx, |_| targets.iter().all(|target| std::fs::metadata(target).is_ok_and(|meta| meta.len() > 100))); let txt = dir.path().join("doc.txt"); let how = Optimizing { compress: true, from: 300, to: 0 }; cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_export(id, Kind::Text, how, txt.clone(), cx))); settle(cx, |_| txt.is_file()); let text = std::fs::read_to_string(&txt).expect("UTF-8"); assert!(text.contains("First") && text.contains("Second"), "{text}"); } #[test] fn a_damaged_file_opens_repaired_and_saves_as_a_new_file() { let dir = tempfile::tempdir().expect("a temp dir"); let mut cx = TestAppContext::single(); let path = file(dir.path(), "broken.pdf", &[(612.0, 792.0, &["Hello"])]); let mut bytes = std::fs::read(&path).expect("reads"); let at = bytes.windows(9).rposition(|w| w == b"startxref").expect("the trailer"); bytes.truncate(at); bytes.extend_from_slice(b"startxref\n9\n%%EOF\n"); std::fs::write(&path, &bytes).expect("writes"); let (viewer, cx) = windowed(&mut cx, dir.path(), &[path.clone()], true); let view = shown(cx, &viewer); assert_eq!(cx.update(|_, app| viewer.read(app).active_tab().and_then(|tab| tab.risk)), Some(ely_office::Risk::Repaired)); assert!(cx.update(|_, app| view.read(app).repair.as_ref().is_some_and(|report| !report.is_empty()))); cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.show_repair(true, cx))); super::frames(cx); let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id); cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.write_as(id, path.clone(), None, window, cx))); super::frames(cx); assert_eq!(std::fs::read(&path).expect("reads"), bytes, "Save As refuses the damaged original"); let fixed = dir.path().join("fixed.pdf"); cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.write_as(id, fixed.clone(), None, window, cx))); settle(cx, |app| fixed.is_file() && viewer.read(app).active_tab().is_some_and(|tab| tab.risk.is_none() && tab.view != view)); } #[test] fn an_edit_made_while_saving_stays_and_the_tab_reopens_after_the_next_save() { let dir = tempfile::tempdir().expect("a temp dir"); let mut cx = TestAppContext::single(); let path = file(dir.path(), "broken.pdf", &[(612.0, 792.0, &["Hello"])]); let mut bytes = std::fs::read(&path).expect("reads"); let at = bytes.windows(9).rposition(|w| w == b"startxref").expect("the trailer"); bytes.truncate(at); bytes.extend_from_slice(b"startxref\n9\n%%EOF\n"); std::fs::write(&path, &bytes).expect("writes"); let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true); let view = shown(cx, &viewer); let (id, fixed) = (cx.update(|_, app| viewer.read(app).core.tabs[0].id), dir.path().join("fixed.pdf")); cx.update(|window, app| viewer.update(app, |viewer, cx| { viewer.write_as(id, fixed.clone(), None, window, cx); // Lands after the save took its snapshot. view.update(cx, |view, cx| view.edit("rotating", |w, doc| w.rotate_pages(doc, &[0], 90), cx)); })); settle(cx, |app| fixed.is_file() && view.read(app).edited == 1 && viewer.read(app).active_tab().is_some_and(|tab| !tab.exporting)); for _ in 0..10 { super::frames(cx); } let tab = cx.update(|_, app| viewer.read(app).active_tab().map(|tab| (tab.view == view, tab.dirty(app)))); assert_eq!(tab, Some((true, true)), "the edit stays, unsaved"); assert_eq!(cx.update(|_, app| view.read(app).sizes[0]), (792.0, 612.0)); cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.save(id, None, window, cx))); settle(cx, |app| viewer.read(app).active_tab().is_some_and(|tab| tab.view != view && !tab.reopen)); let reopened = shown(cx, &viewer); assert_eq!(cx.update(|_, app| reopened.read(app).sizes[0]), (792.0, 612.0), "the reopened file holds the edit"); } #[test] fn picked_pages_from_before_an_edit_export_nothing() { let dir = tempfile::tempdir().expect("a temp dir"); let mut cx = TestAppContext::single(); let path = file(dir.path(), "doc.pdf", &[(100.0, 100.0, &["One"]), (200.0, 100.0, &["Two"]), (300.0, 100.0, &["Three"])]); let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true); let view = shown(cx, &viewer); let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id); let target = dir.path().join("doc-2.png"); cx.update(|_, app| viewer.update(app, |viewer, cx| { let version = view.read(cx).version; view.update(cx, |view, cx| view.edit("deleting", |w, doc| w.delete_pages(doc, &[0]), cx)); viewer.write_images(id, (vec![1], version), 72.0, vec![target.clone()], cx); })); settle(cx, |app| view.read(app).edited == 1 && view.read(app).settled()); for _ in 0..10 { super::frames(cx); } assert!(!target.exists(), "page 2 is another page now"); } /// A page drawing `/Im`, a picture that is no zlib. fn broken_picture(dir: &std::path::Path, height: f32, y: f32) -> (PathBuf, Vec) { let picture = "<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length 12 >>\nstream\nnot a zlib!!\nendstream".to_string(); let page = format!(" /MediaBox [0 0 612 {height}] /Resources << /XObject << /Im 5 0 R >> >>"); let bytes = ely_pdf_engine::sample::one_page("", &page, &format!("q 100 0 0 100 72 {y} cm /Im Do Q"), &[picture]); let path = dir.join("broken.pdf"); std::fs::write(&path, &bytes).expect("writes"); (path, bytes) } #[test] fn a_picture_that_breaks_as_it_draws_stops_an_overwrite() { let dir = tempfile::tempdir().expect("a temp dir"); let mut cx = TestAppContext::single(); let (path, bytes) = broken_picture(dir.path(), 792.0, 600.0); let (viewer, cx) = windowed(&mut cx, dir.path(), &[path.clone()], true); shown(cx, &viewer); settle(cx, |app| viewer.read(app).active_tab().and_then(|tab| tab.risk) == Some(ely_office::Risk::Repaired)); let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id); cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.save(id, None, window, cx))); for _ in 0..10 { super::frames(cx); } assert_eq!(std::fs::read(&path).expect("reads"), bytes); } #[test] fn a_picture_never_drawn_is_checked_before_an_overwrite() { let dir = tempfile::tempdir().expect("a temp dir"); let mut cx = TestAppContext::single(); let (one, _) = broken_picture(dir.path(), 792.0, 600.0); // Thirty blank pages first: the picture's page is never drawn. let mut worker = ely_pdf_engine::Worker::spawn().expect("the worker"); let mut doc = worker.open(&one).expect("opens").doc; for _ in 0..30 { let mark = worker.insert_blank(doc, 0, (612.0, 792.0)).expect("inserts"); doc = doc.at(&mark).expect("the new state"); } let path = dir.path().join("long.pdf"); worker.save(doc, &path).expect("saves"); let bytes = std::fs::read(&path).expect("reads"); let (viewer, cx) = windowed(&mut cx, dir.path(), &[path.clone()], true); shown(cx, &viewer); for _ in 0..10 { super::frames(cx); } let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id); assert_eq!(cx.update(|_, app| viewer.read(app).active_tab().and_then(|tab| tab.risk)), None, "nothing drawn yet"); cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.save(id, None, window, cx))); settle(cx, |app| viewer.read(app).active_tab().and_then(|tab| tab.risk) == Some(ely_office::Risk::Repaired)); assert_eq!(std::fs::read(&path).expect("reads"), bytes, "Save turned into Save As"); }