feat: import Xournal++ and Xournal notebooks (Issue #47)

The import menu entry (formerly "Import PDF") and drag & drop now also
accept .xopp and .xoj files. Each Xournal page becomes a page of a new
notebook: strokes with pen pressure, highlighters, texts, embedded
images, lined/squared/dotted paper, image backgrounds and PDF
backgrounds (looked up at the stored path and next to the file). All
layers are flattened in order; LaTeX formulas are skipped and counted
in the status message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
haraldandClaude Opus 5.5 committed 2026-10-27 11:10:34 +01:00
1 parent b5d9d72e79
commit 2bd697ef82
11 files changed
+902 -44

No files matched your search

+25 -13
View File
@@ -2216,10 +2216,10 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
}
}
// Bilddatei ins Fenster gezogen: an der Ablagestelle einfügen
// Bilddatei ins Fenster gezogen: an der Ablagestelle einfügen; PDF/Xournal++: importieren
WindowEvent::DroppedFile(path) => {
if crate::file_picker::is_pdf_path(&path) {
ui_state.queued_actions.push(UiAction::ImportPdf(path));
if crate::file_picker::is_pdf_path(&path) || crate::xopp::is_xopp_path(&path) {
ui_state.queued_actions.push(UiAction::ImportFile(path));
} else if crate::assets::is_supported_image_path(&path) {
let (sx, sy) = cursor_screen_pos;
let center = canvas.screen_to_page_clamped(sx, sy);
@@ -3837,18 +3837,23 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
}
}
}
UiAction::ImportPdf(path) => {
UiAction::ImportFile(path) => {
// Neben das gerade geöffnete Notizbuch
let folder = ui_state.library.folder_of(&manifest.id).to_string();
let title = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "PDF".to_string());
let result = std::fs::read(&path)
.map_err(|e| e.to_string())
.and_then(|bytes| storage::create_notebook_from_pdf(&storage_base_dir, &title, bytes));
.unwrap_or_else(|| "Import".to_string());
let result = if crate::xopp::is_xopp_path(&path) {
crate::xopp::create_notebook_from_xopp(&storage_base_dir, &title, &path)
} else {
std::fs::read(&path)
.map_err(|e| e.to_string())
.and_then(|bytes| storage::create_notebook_from_pdf(&storage_base_dir, &title, bytes))
.map(|(nb, m, dir)| (nb, m, dir, crate::xopp::ImportReport::default()))
};
match result {
Ok((new_nb, new_manifest, new_dir)) => {
Ok((new_nb, new_manifest, new_dir, report)) => {
let id = new_manifest.id.clone();
change_library(&storage_base_dir, &mut ui_state, |library| {
library.move_notebook(&id, &folder)
@@ -3867,15 +3872,22 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
canvas.reset_view();
canvas.sync_page(notebook.current_page_mut());
update_window_title(&window, &notebook, &manifest, ui_state.active_tool, autosave.status());
ui_state.set_export_message(t!(
let mut message = t!(
"'{title}' importiert ({count} Seiten)",
title = manifest.title,
count = notebook.pages.len()
));
);
if let Some(pdf) = &report.missing_pdf {
message.push_str(&t!(", PDF-Hintergrund nicht gefunden: {file}", file = pdf));
}
if report.skipped > 0 {
message.push_str(&t!(", {count} Elemente übersprungen", count = report.skipped));
}
ui_state.set_export_message(message);
}
Err(err) => {
eprintln!("[PDF-Import] '{}': {err}", path.display());
ui_state.set_export_message(t!("PDF nicht importiert: {err}", err = err));
eprintln!("[Import] '{}': {err}", path.display());
ui_state.set_export_message(t!("Nicht importiert: {err}", err = err));
}
}
window.request_redraw();