Skip to content

Commit

Permalink
feat: Allow import of encrypted PDF files (#1279)
Browse files Browse the repository at this point in the history
  • Loading branch information
kq98 authored Nov 9, 2024
1 parent a9bb13b commit 7f3ff8a
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 11 deletions.
3 changes: 3 additions & 0 deletions crates/rnote-engine/src/engine/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ impl Engine {
bytes: Vec<u8>,
insert_pos: na::Vector2<f64>,
page_range: Option<Range<u32>>,
password: Option<String>,
) -> oneshot::Receiver<anyhow::Result<Vec<(Stroke, Option<StrokeLayer>)>>> {
let (oneshot_sender, oneshot_receiver) =
oneshot::channel::<anyhow::Result<Vec<(Stroke, Option<StrokeLayer>)>>>();
Expand All @@ -339,6 +340,7 @@ impl Engine {
insert_pos,
page_range,
&format,
password,
)?
.into_iter()
.map(|s| (Stroke::BitmapImage(s), Some(StrokeLayer::Document)))
Expand All @@ -352,6 +354,7 @@ impl Engine {
insert_pos,
page_range,
&format,
password,
)?
.into_iter()
.map(|s| (Stroke::VectorImage(s), Some(StrokeLayer::Document)))
Expand Down
4 changes: 3 additions & 1 deletion crates/rnote-engine/src/strokes/bitmapimage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,10 @@ impl BitmapImage {
insert_pos: na::Vector2<f64>,
page_range: Option<Range<u32>>,
format: &Format,
password: Option<String>,
) -> Result<Vec<Self>, anyhow::Error> {
let doc = poppler::Document::from_bytes(&glib::Bytes::from(to_be_read), None)?;
let doc =
poppler::Document::from_bytes(&glib::Bytes::from(to_be_read), password.as_deref())?;
let page_range = page_range.unwrap_or(0..doc.n_pages() as u32);
let page_width = if pdf_import_prefs.adjust_document {
format.width()
Expand Down
3 changes: 2 additions & 1 deletion crates/rnote-engine/src/strokes/vectorimage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,9 @@ impl VectorImage {
insert_pos: na::Vector2<f64>,
page_range: Option<Range<u32>>,
format: &Format,
password: Option<String>,
) -> Result<Vec<Self>, anyhow::Error> {
let doc = poppler::Document::from_bytes(&glib::Bytes::from(bytes), None)?;
let doc = poppler::Document::from_bytes(&glib::Bytes::from(bytes), password.as_deref())?;
let page_range = page_range.unwrap_or(0..doc.n_pages() as u32);

let page_width = if pdf_import_prefs.adjust_document {
Expand Down
25 changes: 25 additions & 0 deletions crates/rnote-ui/data/ui/dialogs/import.ui
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,29 @@
<property name="lower">1</property>
<property name="value">96</property>
</object>
<object class="AdwAlertDialog" id="dialog_import_pdf_password">
<property name="body" translatable="yes">is password protected</property>
<property name="default-response">unlock</property>
<property name="close-response">cancel</property>
<property name="heading" translatable="yes">Encrypted PDF</property>
<property name="follows-content-size">False</property>
<responses>
<response id="cancel" translatable="yes">_Cancel</response>
<response id="unlock" translatable="yes" appearance="suggested">_Unlock</response>
</responses>
<property name="extra-child">
<object class="GtkListBox" id="pdf_password_entry_box">
<property name="selection-mode">none</property>
<style>
<class name="boxed-list"/>
</style>
<child>
<object class="AdwPasswordEntryRow" id="pdf_password_entry">
<property name="activates-default">True</property>
<property name="title" translatable="yes">Enter the PDF password</property>
</object>
</child>
</object>
</property>
</object>
</interface>
24 changes: 22 additions & 2 deletions crates/rnote-ui/po/de.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ msgstr ""
"Project-Id-Version: rnote\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-07-26 10:39+0200\n"
"PO-Revision-Date: 2024-08-24 17:28+0000\n"
"PO-Revision-Date: 2024-11-07 22:58+0100\n"
"Last-Translator: Felix Zwettler <f.zwettler@posteo.de>\n"
"Language-Team: German <https://hosted.weblate.org/projects/rnote/repo/de/>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.7.1-dev\n"
"X-Generator: Poedit 3.4.4\n"

#: crates/rnote-ui/data/app.desktop.in.in:5
#: crates/rnote-ui/data/app.metainfo.xml.in.in:9
Expand Down Expand Up @@ -2536,6 +2536,26 @@ msgctxt "part of string representation of a color"
msgid "white"
msgstr "weiss"

#: crates/rnote-ui/data/ui/dialogs/import.ui:273
msgid "is password protected"
msgstr "ist Passwort geschützt"

#: crates/rnote-ui/data/ui/dialogs/import.ui:276
msgid "Encrypted PDF"
msgstr "Verschlüsselte PDF"

#: crates/rnote-ui/data/ui/dialogs/import.ui:279
msgid "_Cancel"
msgstr "_Abbrechen"

#: crates/rnote-ui/data/ui/dialogs/import.ui:280
msgid "_Unlock"
msgstr "_Entsperren"

#: crates/rnote-ui/data/ui/dialogs/import.ui:287
msgid "Enter the PDF password"
msgstr "Gebe das PDF Passwort ein"

#~ msgid "Opened file was moved or deleted on disk"
#~ msgstr "Geöffnete Datei wurde auf dem Datenträger verschoben oder gelöscht"

Expand Down
20 changes: 20 additions & 0 deletions crates/rnote-ui/po/rnote.pot
Original file line number Diff line number Diff line change
Expand Up @@ -2476,3 +2476,23 @@ msgstr ""
msgctxt "part of string representation of a color"
msgid "white"
msgstr ""

#: crates/rnote-ui/data/ui/dialogs/import.ui:273
msgid "is password protected"
msgstr ""

#: crates/rnote-ui/data/ui/dialogs/import.ui:276
msgid "Encrypted PDF"
msgstr ""

#: crates/rnote-ui/data/ui/dialogs/import.ui:279
msgid "_Cancel"
msgstr ""

#: crates/rnote-ui/data/ui/dialogs/import.ui:280
msgid "_Unlock"
msgstr ""

#: crates/rnote-ui/data/ui/dialogs/import.ui:287
msgid "Enter the PDF password"
msgstr ""
3 changes: 2 additions & 1 deletion crates/rnote-ui/src/canvas/imexport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ impl RnCanvas {
bytes: Vec<u8>,
target_pos: Option<na::Vector2<f64>>,
page_range: Option<Range<u32>>,
password: Option<String>,
) -> anyhow::Result<()> {
let pos = self.determine_stroke_import_pos(target_pos);
let adjust_document = self
Expand All @@ -139,7 +140,7 @@ impl RnCanvas {

let strokes_receiver = self
.engine_mut()
.generate_pdf_pages_from_bytes(bytes, pos, page_range);
.generate_pdf_pages_from_bytes(bytes, pos, page_range, password);
let strokes = strokes_receiver.await??;
let widget_flags = self
.engine_mut()
Expand Down
126 changes: 120 additions & 6 deletions crates/rnote-ui/src/dialogs/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use adw::prelude::*;
use futures::StreamExt;
use gettextrs::gettext;
use gtk4::{
gio, glib, glib::clone, Builder, Button, CallbackAction, FileDialog, FileFilter, Label,
Shortcut, ShortcutController, ShortcutTrigger, ToggleButton,
gio, glib, glib::clone, graphene, gsk, Builder, Button, CallbackAction, FileDialog, FileFilter,
Label, Shortcut, ShortcutController, ShortcutTrigger, ToggleButton,
};
use num_traits::ToPrimitive;
use rnote_engine::engine::import::{PdfImportPageSpacing, PdfImportPagesType};
Expand Down Expand Up @@ -111,6 +111,115 @@ pub(crate) async fn filedialog_import_file(appwindow: &RnAppWindow) {
}
}

/// Check for a pdf encryption and request a password if needed from the user
///
/// Returns a password Option and a boolean weather the user canceled the file import or not
pub(crate) async fn pdf_encryption_check_and_dialog(
appwindow: &RnAppWindow,
input_file: &gio::File,
) -> (Option<String>, bool) {
let builder = Builder::from_resource(
(String::from(config::APP_IDPATH) + "ui/dialogs/import.ui").as_str(),
);

let dialog_import_pdf_password: adw::AlertDialog =
builder.object("dialog_import_pdf_password").unwrap();
let pdf_password_entry: adw::PasswordEntryRow = builder.object("pdf_password_entry").unwrap();
let pdf_password_entry_box: gtk4::ListBox = builder.object("pdf_password_entry_box").unwrap();

let target = adw::CallbackAnimationTarget::new(clone!(
#[weak]
pdf_password_entry_box,
move |value| {
let x = adw::lerp(0., 40.0, value);
let p = graphene::Point::new(x as f32, 0.);
let transform = gsk::Transform::new().translate(&p);
pdf_password_entry_box.allocate(
pdf_password_entry_box.width(),
pdf_password_entry_box.height(),
-1,
Some(transform),
);
}
));

let params = adw::SpringParams::new(0.2, 0.5, 500.0);

let animation = adw::SpringAnimation::builder()
.widget(&pdf_password_entry_box)
.value_from(0.0)
.value_to(0.0)
.spring_params(&params)
.target(&target)
.initial_velocity(10.0)
.epsilon(0.001) // If amplitude of oscillation < epsilon, animation stops
.clamp(false)
.build();

let (tx, mut rx) = futures::channel::mpsc::unbounded::<(Option<String>, bool)>();
let tx_cancel = tx.clone();
let tx_unlock = tx.clone();

dialog_import_pdf_password.connect_response(
Some("unlock"),
clone!(
#[weak]
pdf_password_entry,
move |_, _| {
tx_unlock
.unbounded_send((Some(pdf_password_entry.text().to_string()), false))
.unwrap();
}
),
);

dialog_import_pdf_password.connect_response(Some("cancel"), move |_, _| {
tx_cancel.unbounded_send((None, true)).unwrap();
});

let file_name = input_file.basename().map_or_else(
|| gettext("- no file name -"),
|s| s.to_string_lossy().to_string(),
);
let dialog_body = dialog_import_pdf_password.body();
let dialog_body = file_name.clone() + " " + &dialog_body;
dialog_import_pdf_password.set_body(&dialog_body);

let mut password: Option<String> = None;

loop {
match poppler::Document::from_gfile(
input_file,
password.as_deref(),
None::<&gio::Cancellable>,
) {
Ok(_) => return (password, false),
Err(e) => {
if e.matches(poppler::Error::Encrypted) {
dialog_import_pdf_password.present(appwindow.root().as_ref());
pdf_password_entry.grab_focus();

match rx.next().await {
Some((new_password, cancel)) => {
password = new_password;
if cancel {
return (None, true);
}
}
None => {
return (None, true);
}
}
animation.play();
pdf_password_entry.set_text("");
} else {
return (None, true);
}
}
};
}
}

/// Imports the file as Pdf with an import dialog.
///
/// Returns true when the file was imported, else false.
Expand All @@ -120,6 +229,11 @@ pub(crate) async fn dialog_import_pdf_w_prefs(
input_file: gio::File,
target_pos: Option<na::Vector2<f64>>,
) -> anyhow::Result<bool> {
let (password, cancel) = pdf_encryption_check_and_dialog(appwindow, &input_file).await;
if cancel {
return Ok(false);
}

let builder = Builder::from_resource(
(String::from(config::APP_IDPATH) + "ui/dialogs/import.ui").as_str(),
);
Expand Down Expand Up @@ -274,7 +388,7 @@ pub(crate) async fn dialog_import_pdf_w_prefs(
));

if let Ok(poppler_doc) =
poppler::Document::from_gfile(&input_file, None, None::<&gio::Cancellable>)
poppler::Document::from_gfile(&input_file, password.as_deref(), None::<&gio::Cancellable>)
{
let file_name = input_file.basename().map_or_else(
|| gettext("- no file name -"),
Expand Down Expand Up @@ -346,12 +460,12 @@ pub(crate) async fn dialog_import_pdf_w_prefs(
}
));

import_pdf_button_confirm.connect_clicked(clone!(#[weak] pdf_page_start_row, #[weak] pdf_page_end_row, #[weak] input_file, #[weak] dialog, #[weak] canvas , move |_| {
import_pdf_button_confirm.connect_clicked(clone!(#[weak] pdf_page_start_row, #[weak] pdf_page_end_row, #[weak] input_file, #[weak] dialog, #[weak] canvas, #[strong] password, move |_| {
dialog.close();

let inner_tx_confirm = tx_confirm.clone();

glib::spawn_future_local(clone!(#[weak] pdf_page_start_row, #[weak] pdf_page_end_row, #[weak] input_file, #[weak] canvas , async move {
glib::spawn_future_local(clone!(#[weak] pdf_page_start_row, #[weak] pdf_page_end_row, #[weak] input_file, #[weak] canvas, #[strong] password , async move {
let page_range =
(pdf_page_start_row.value() as u32 - 1)..pdf_page_end_row.value() as u32;

Expand All @@ -364,7 +478,7 @@ pub(crate) async fn dialog_import_pdf_w_prefs(
return;
}
};
if let Err(e) = canvas.load_in_pdf_bytes(bytes.to_vec(), target_pos, Some(page_range)).await {
if let Err(e) = canvas.load_in_pdf_bytes(bytes.to_vec(), target_pos, Some(page_range), password).await {
if let Err(e) = inner_tx_confirm.unbounded_send(Err(e)) {
error!("Failed to load PDF, but failed to send signal through channel. Err: {e:?}");
}
Expand Down

0 comments on commit 7f3ff8a

Please sign in to comment.