Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src-tauri/src/commands/pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,17 @@ pub async fn edit_pdf_overlays(
groups: Vec<PageGroup>,
document: crate::pdf_engine::edit_overlay::EditDocumentIn,
incomplete_source_paths: Option<Vec<String>>,
form_values: Option<Vec<crate::pdf_engine::edit_forms::FormValue>>,
flatten_form: Option<bool>,
flatten_annotations: Option<bool>,
) -> Result<JobResult, AppError> {
let handle = registry.register(&job_id);
let app2 = app.clone();
let jid = job_id.clone();
let incomplete = incomplete_source_paths.unwrap_or_default();
let flatten = flatten_annotations.unwrap_or(false);
let form_values = form_values.unwrap_or_default();
let flatten_form = flatten_form.unwrap_or(false);
let flatten_annotations = flatten_annotations.unwrap_or(false);
let res = tauri::async_runtime::spawn_blocking(move || {
crate::pdf_engine::edit_overlay::edit_pdf_overlays(
&app2,
Expand All @@ -235,7 +239,9 @@ pub async fn edit_pdf_overlays(
&output_path,
&document,
&incomplete,
flatten,
&form_values,
flatten_form,
flatten_annotations,
)
})
.await
Expand Down
113 changes: 91 additions & 22 deletions src-tauri/src/commands/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ pub async fn pdf_to_images(
registry.remove(&job_id);
let output_paths = res?;
let _ = app.emit("job:update", JobUpdate::new(&job_id, "completed", "Done"));
Ok(JobResult { job_id, output_paths, status: "completed".to_string() })
Ok(JobResult {
job_id,
output_paths,
status: "completed".to_string(),
})
}

/// Per-page text of a PDF, for in-app search. Only text crosses IPC.
Expand All @@ -107,12 +111,28 @@ pub async fn pdf_text(app: tauri::AppHandle, input_path: String) -> Result<Vec<S
/// One page as a small standalone PDF (base64), for true-vector zoom with pdf.js.
/// Returns null when the page is too large to load into the webview.
#[tauri::command]
pub async fn page_pdf(app: tauri::AppHandle, input_path: String, page: u32) -> Result<Option<String>, AppError> {
pub async fn page_pdf(
app: tauri::AppHandle,
input_path: String,
page: u32,
) -> Result<Option<String>, AppError> {
tauri::async_runtime::spawn_blocking(move || render::page_pdf_b64(&app, &input_path, page))
.await
.map_err(|e| AppError::io("Could not read the page.", e))?
}

/// AcroForm fields on `input_path` (paths in, JSON out). Never PDF bytes.
#[tauri::command]
pub async fn list_pdf_form_fields(
input_path: String,
) -> Result<Vec<crate::pdf_engine::edit_forms::FormField>, AppError> {
tauri::async_runtime::spawn_blocking(move || {
crate::pdf_engine::edit_forms::list_form_fields(&input_path)
})
.await
.map_err(|e| AppError::io("Could not read the form fields.", e))?
}

/// List leftover and session markup annots (paths in, JSON out). Never PDF bytes.
#[tauri::command]
pub async fn list_pdf_annots(
Expand Down Expand Up @@ -158,9 +178,11 @@ pub async fn diff_pages(
b_page: u32,
size: u32,
) -> Result<crate::models::DiffResult, AppError> {
tauri::async_runtime::spawn_blocking(move || render::diff_pages(&app, &a_path, a_page, &b_path, b_page, size))
.await
.map_err(|e| AppError::io("Could not compare the pages.", e))?
tauri::async_runtime::spawn_blocking(move || {
render::diff_pages(&app, &a_path, a_page, &b_path, b_page, size)
})
.await
.map_err(|e| AppError::io("Could not compare the pages.", e))?
}

/// Whether Tesseract (OCR) is available.
Expand Down Expand Up @@ -192,7 +214,11 @@ pub async fn ocr_pdf(
registry.remove(&job_id);
let output_paths = res?;
let _ = app.emit("job:update", JobUpdate::new(&job_id, "completed", "Done"));
Ok(JobResult { job_id, output_paths, status: "completed".to_string() })
Ok(JobResult {
job_id,
output_paths,
status: "completed".to_string(),
})
}

/// Whether LibreOffice is available (controls Office conversion features).
Expand Down Expand Up @@ -264,8 +290,7 @@ pub async fn office_to_pdf_batch(
office::to_pdf(&app2, Some(&handle), inp, &scratch.to_string_lossy())?;
let mut k = 2;
let target = loop {
let cand =
std::path::Path::new(&output_dir).join(format!("{stem} ({k}).pdf"));
let cand = std::path::Path::new(&output_dir).join(format!("{stem} ({k}).pdf"));
let cand_str = cand.to_string_lossy().to_string();
if !taken.contains(&cand_str) && !cand.exists() {
break cand_str;
Expand All @@ -286,7 +311,11 @@ pub async fn office_to_pdf_batch(
registry.remove(&job_id);
let output_paths = res?;
let _ = app.emit("job:update", JobUpdate::new(&job_id, "completed", "Done"));
Ok(JobResult { job_id, output_paths, status: "completed".to_string() })
Ok(JobResult {
job_id,
output_paths,
status: "completed".to_string(),
})
}

/// Convert the combined document to PDF/A-2b (via LibreOffice).
Expand All @@ -312,11 +341,18 @@ pub async fn pdfa_pdf(
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "output".into());
let work = temp::root(&app2)?.join("work").join(&jid);
std::fs::create_dir_all(&work).map_err(|e| AppError::io("Could not create a temp directory.", e))?;
let merged = work.join(format!("{stem}.pdf")).to_string_lossy().to_string();
std::fs::create_dir_all(&work)
.map_err(|e| AppError::io("Could not create a temp directory.", e))?;
let merged = work
.join(format!("{stem}.pdf"))
.to_string_lossy()
.to_string();
let result = (|| -> Result<Vec<String>, AppError> {
crate::pdf_engine::assemble(&app2, &handle, &jid, &groups, &merged)?;
let _ = app2.emit("job:update", JobUpdate::new(&jid, "running", "Converting to PDF/A"));
let _ = app2.emit(
"job:update",
JobUpdate::new(&jid, "running", "Converting to PDF/A"),
);
let produced = office::to_pdfa(&app2, Some(&handle), &merged, &out_dir)?;
Ok(vec![produced])
})();
Expand All @@ -328,7 +364,11 @@ pub async fn pdfa_pdf(
registry.remove(&job_id);
let output_paths = res?;
let _ = app.emit("job:update", JobUpdate::new(&job_id, "completed", "Done"));
Ok(JobResult { job_id, output_paths, status: "completed".to_string() })
Ok(JobResult {
job_id,
output_paths,
status: "completed".to_string(),
})
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -363,9 +403,11 @@ pub async fn detect_blank_pages(
pub async fn read_pdf_meta(
input_path: String,
) -> Result<crate::pdf_engine::metadata::PdfMeta, AppError> {
tauri::async_runtime::spawn_blocking(move || crate::pdf_engine::metadata::read_meta(&input_path))
.await
.map_err(|e| AppError::io("Could not read the metadata.", e))?
tauri::async_runtime::spawn_blocking(move || {
crate::pdf_engine::metadata::read_meta(&input_path)
})
.await
.map_err(|e| AppError::io("Could not read the metadata.", e))?
}

/// Write /Info metadata to a copy of the PDF. Empty/null fields are removed;
Expand All @@ -386,15 +428,25 @@ pub async fn write_pdf_meta(
let jid = job_id.clone();
let res = tauri::async_runtime::spawn_blocking(move || {
crate::pdf_engine::metadata::write_meta(
&app2, &handle, &jid, &input_path, &output_path, &fields, clear_all,
&app2,
&handle,
&jid,
&input_path,
&output_path,
&fields,
clear_all,
)
})
.await
.map_err(|e| AppError::engine_failed(format!("worker join error: {e}")))?;
registry.remove(&job_id);
let output_paths = res?;
let _ = app.emit("job:update", JobUpdate::new(&job_id, "completed", "Done"));
Ok(JobResult { job_id, output_paths, status: "completed".to_string() })
Ok(JobResult {
job_id,
output_paths,
status: "completed".to_string(),
})
}

/// Export a PDF's text (whole document or a page range) to a UTF-8 .txt file.
Expand All @@ -413,15 +465,25 @@ pub async fn export_pdf_text(
let jid = job_id.clone();
let res = tauri::async_runtime::spawn_blocking(move || {
crate::pdf_engine::textexport::export_text(
&app2, &handle, &jid, &input_path, &output_path, first_page, last_page,
&app2,
&handle,
&jid,
&input_path,
&output_path,
first_page,
last_page,
)
})
.await
.map_err(|e| AppError::engine_failed(format!("worker join error: {e}")))?;
registry.remove(&job_id);
let output_paths = res?;
let _ = app.emit("job:update", JobUpdate::new(&job_id, "completed", "Done"));
Ok(JobResult { job_id, output_paths, status: "completed".to_string() })
Ok(JobResult {
job_id,
output_paths,
status: "completed".to_string(),
})
}

/// "PDF → Office": assemble the combined document, then convert to docx/pptx/xlsx.
Expand All @@ -444,7 +506,10 @@ pub async fn pdf_to_office(
// LibreOffice names its output after the input stem, so name the
// intermediate merged PDF after the user's first source file — the
// converted document then gets a meaningful name (not "merged.docx").
let base_stem = groups.first().map(|g| stem_of(&g.path)).unwrap_or_else(|| "document".into());
let base_stem = groups
.first()
.map(|g| stem_of(&g.path))
.unwrap_or_else(|| "document".into());
// Never silently overwrite an existing file in the output folder.
let mut stem = base_stem.clone();
let mut k = 2;
Expand All @@ -471,5 +536,9 @@ pub async fn pdf_to_office(
registry.remove(&job_id);
let output_paths = res?;
let _ = app.emit("job:update", JobUpdate::new(&job_id, "completed", "Done"));
Ok(JobResult { job_id, output_paths, status: "completed".to_string() })
Ok(JobResult {
job_id,
output_paths,
status: "completed".to_string(),
})
}
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ pub fn run() {
commands::render::page_pdf,
commands::render::pdf_outline,
commands::render::list_pdf_links,
commands::render::list_pdf_form_fields,
commands::render::list_pdf_annots,
commands::render::diff_pages,
commands::render::office_available,
Expand Down
41 changes: 21 additions & 20 deletions src-tauri/src/pdf_engine/edit_annots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,7 @@ fn file_too_large(path: &Path) -> AppError {
AppError::new(
"PDF_TOO_LARGE",
"This PDF is too large to read annotations",
format!(
"\"{}\" is larger than 400 MB.",
path.display()
),
format!("\"{}\" is larger than 400 MB.", path.display()),
)
.with_suggestion("Use a smaller file.")
}
Expand Down Expand Up @@ -146,12 +143,7 @@ fn rect_xywh(nums: &[f64]) -> [f64; 4] {
let y1 = nums[1];
let x2 = nums[2];
let y2 = nums[3];
[
x1.min(x2),
y1.min(y2),
(x2 - x1).abs(),
(y2 - y1).abs(),
]
[x1.min(x2), y1.min(y2), (x2 - x1).abs(), (y2 - y1).abs()]
}

fn real_array(vals: impl IntoIterator<Item = f64>) -> Object {
Expand Down Expand Up @@ -334,7 +326,12 @@ fn build_session_annot(doc: &mut Document, item: &SessionMarkup) -> ObjectId {
MarkupKind::Note => {
annot.set("Name", Object::Name(b"Comment".to_vec()));
annot.set("Open", Object::Boolean(false));
attach_appearance(doc, &mut annot, appearance_stream(rect_pdf, note_ap_content(rect_pdf, item.color)), false);
attach_appearance(
doc,
&mut annot,
appearance_stream(rect_pdf, note_ap_content(rect_pdf, item.color)),
false,
);
}
MarkupKind::Highlight => {
let quads = item
Expand Down Expand Up @@ -384,9 +381,7 @@ fn build_session_annot(doc: &mut Document, item: &SessionMarkup) -> ObjectId {
let strokes = item.ink_list.clone().unwrap_or_default();
let ink_list: Vec<Object> = strokes
.iter()
.map(|stroke| {
real_array(stroke.iter().flat_map(|p| [p[0], p[1]]))
})
.map(|stroke| real_array(stroke.iter().flat_map(|p| [p[0], p[1]])))
.collect();
annot.set("InkList", Object::Array(ink_list));
attach_appearance(
Expand All @@ -401,11 +396,7 @@ fn build_session_annot(doc: &mut Document, item: &SessionMarkup) -> ObjectId {
doc.add_object(Object::Dictionary(annot))
}

fn keep_existing_annot(
doc: &Document,
entry: &Object,
session_ids: &HashSet<String>,
) -> bool {
fn keep_existing_annot(doc: &Document, entry: &Object, session_ids: &HashSet<String>) -> bool {
let Some(dict) = resolve_dict(doc, entry) else {
return true;
};
Expand Down Expand Up @@ -531,6 +522,14 @@ pub fn apply_markup_annots(
flatten: bool,
) -> Result<(), AppError> {
let mut doc = load_doc(staged)?;
if flatten && crate::pdf_engine::validate_output::catalog_flags_from_doc(&doc).acro_form {
return Err(AppError::new(
"FORM_FLATTEN_REQUIRED",
"Flatten form fields first",
"Flattening all annotations would also remove interactive form fields.",
)
.with_suggestion("Enable Flatten form fields, then save again."));
}
let session_ids: HashSet<String> = session
.iter()
.filter(|s| !s.id.is_empty())
Expand All @@ -546,7 +545,9 @@ pub fn apply_markup_annots(
let page_index = page_1based.saturating_sub(1);
let empty: Vec<&SessionMarkup> = Vec::new();
let items = by_page.get(&page_index).unwrap_or(&empty);
if items.is_empty() && session_ids.is_empty() && page_annot_objects(&doc, *page_id).is_empty()
if items.is_empty()
&& session_ids.is_empty()
&& page_annot_objects(&doc, *page_id).is_empty()
{
continue;
}
Expand Down
Loading
Loading