Skip to content

Commit 7f3ff8a

Browse files
authored
feat: Allow import of encrypted PDF files (#1279)
1 parent a9bb13b commit 7f3ff8a

File tree

8 files changed

+197
-11
lines changed

8 files changed

+197
-11
lines changed

crates/rnote-engine/src/engine/import.rs

+3
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ impl Engine {
318318
bytes: Vec<u8>,
319319
insert_pos: na::Vector2<f64>,
320320
page_range: Option<Range<u32>>,
321+
password: Option<String>,
321322
) -> oneshot::Receiver<anyhow::Result<Vec<(Stroke, Option<StrokeLayer>)>>> {
322323
let (oneshot_sender, oneshot_receiver) =
323324
oneshot::channel::<anyhow::Result<Vec<(Stroke, Option<StrokeLayer>)>>>();
@@ -339,6 +340,7 @@ impl Engine {
339340
insert_pos,
340341
page_range,
341342
&format,
343+
password,
342344
)?
343345
.into_iter()
344346
.map(|s| (Stroke::BitmapImage(s), Some(StrokeLayer::Document)))
@@ -352,6 +354,7 @@ impl Engine {
352354
insert_pos,
353355
page_range,
354356
&format,
357+
password,
355358
)?
356359
.into_iter()
357360
.map(|s| (Stroke::VectorImage(s), Some(StrokeLayer::Document)))

crates/rnote-engine/src/strokes/bitmapimage.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,10 @@ impl BitmapImage {
132132
insert_pos: na::Vector2<f64>,
133133
page_range: Option<Range<u32>>,
134134
format: &Format,
135+
password: Option<String>,
135136
) -> Result<Vec<Self>, anyhow::Error> {
136-
let doc = poppler::Document::from_bytes(&glib::Bytes::from(to_be_read), None)?;
137+
let doc =
138+
poppler::Document::from_bytes(&glib::Bytes::from(to_be_read), password.as_deref())?;
137139
let page_range = page_range.unwrap_or(0..doc.n_pages() as u32);
138140
let page_width = if pdf_import_prefs.adjust_document {
139141
format.width()

crates/rnote-engine/src/strokes/vectorimage.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,9 @@ impl VectorImage {
210210
insert_pos: na::Vector2<f64>,
211211
page_range: Option<Range<u32>>,
212212
format: &Format,
213+
password: Option<String>,
213214
) -> Result<Vec<Self>, anyhow::Error> {
214-
let doc = poppler::Document::from_bytes(&glib::Bytes::from(bytes), None)?;
215+
let doc = poppler::Document::from_bytes(&glib::Bytes::from(bytes), password.as_deref())?;
215216
let page_range = page_range.unwrap_or(0..doc.n_pages() as u32);
216217

217218
let page_width = if pdf_import_prefs.adjust_document {

crates/rnote-ui/data/ui/dialogs/import.ui

+25
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,29 @@
269269
<property name="lower">1</property>
270270
<property name="value">96</property>
271271
</object>
272+
<object class="AdwAlertDialog" id="dialog_import_pdf_password">
273+
<property name="body" translatable="yes">is password protected</property>
274+
<property name="default-response">unlock</property>
275+
<property name="close-response">cancel</property>
276+
<property name="heading" translatable="yes">Encrypted PDF</property>
277+
<property name="follows-content-size">False</property>
278+
<responses>
279+
<response id="cancel" translatable="yes">_Cancel</response>
280+
<response id="unlock" translatable="yes" appearance="suggested">_Unlock</response>
281+
</responses>
282+
<property name="extra-child">
283+
<object class="GtkListBox" id="pdf_password_entry_box">
284+
<property name="selection-mode">none</property>
285+
<style>
286+
<class name="boxed-list"/>
287+
</style>
288+
<child>
289+
<object class="AdwPasswordEntryRow" id="pdf_password_entry">
290+
<property name="activates-default">True</property>
291+
<property name="title" translatable="yes">Enter the PDF password</property>
292+
</object>
293+
</child>
294+
</object>
295+
</property>
296+
</object>
272297
</interface>

crates/rnote-ui/po/de.po

+22-2
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ msgstr ""
88
"Project-Id-Version: rnote\n"
99
"Report-Msgid-Bugs-To: \n"
1010
"POT-Creation-Date: 2024-07-26 10:39+0200\n"
11-
"PO-Revision-Date: 2024-08-24 17:28+0000\n"
11+
"PO-Revision-Date: 2024-11-07 22:58+0100\n"
1212
"Last-Translator: Felix Zwettler <f.zwettler@posteo.de>\n"
1313
"Language-Team: German <https://hosted.weblate.org/projects/rnote/repo/de/>\n"
1414
"Language: de\n"
1515
"MIME-Version: 1.0\n"
1616
"Content-Type: text/plain; charset=UTF-8\n"
1717
"Content-Transfer-Encoding: 8bit\n"
1818
"Plural-Forms: nplurals=2; plural=n != 1;\n"
19-
"X-Generator: Weblate 5.7.1-dev\n"
19+
"X-Generator: Poedit 3.4.4\n"
2020

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

2539+
#: crates/rnote-ui/data/ui/dialogs/import.ui:273
2540+
msgid "is password protected"
2541+
msgstr "ist Passwort geschützt"
2542+
2543+
#: crates/rnote-ui/data/ui/dialogs/import.ui:276
2544+
msgid "Encrypted PDF"
2545+
msgstr "Verschlüsselte PDF"
2546+
2547+
#: crates/rnote-ui/data/ui/dialogs/import.ui:279
2548+
msgid "_Cancel"
2549+
msgstr "_Abbrechen"
2550+
2551+
#: crates/rnote-ui/data/ui/dialogs/import.ui:280
2552+
msgid "_Unlock"
2553+
msgstr "_Entsperren"
2554+
2555+
#: crates/rnote-ui/data/ui/dialogs/import.ui:287
2556+
msgid "Enter the PDF password"
2557+
msgstr "Gebe das PDF Passwort ein"
2558+
25392559
#~ msgid "Opened file was moved or deleted on disk"
25402560
#~ msgstr "Geöffnete Datei wurde auf dem Datenträger verschoben oder gelöscht"
25412561

crates/rnote-ui/po/rnote.pot

+20
Original file line numberDiff line numberDiff line change
@@ -2476,3 +2476,23 @@ msgstr ""
24762476
msgctxt "part of string representation of a color"
24772477
msgid "white"
24782478
msgstr ""
2479+
2480+
#: crates/rnote-ui/data/ui/dialogs/import.ui:273
2481+
msgid "is password protected"
2482+
msgstr ""
2483+
2484+
#: crates/rnote-ui/data/ui/dialogs/import.ui:276
2485+
msgid "Encrypted PDF"
2486+
msgstr ""
2487+
2488+
#: crates/rnote-ui/data/ui/dialogs/import.ui:279
2489+
msgid "_Cancel"
2490+
msgstr ""
2491+
2492+
#: crates/rnote-ui/data/ui/dialogs/import.ui:280
2493+
msgid "_Unlock"
2494+
msgstr ""
2495+
2496+
#: crates/rnote-ui/data/ui/dialogs/import.ui:287
2497+
msgid "Enter the PDF password"
2498+
msgstr ""

crates/rnote-ui/src/canvas/imexport.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ impl RnCanvas {
129129
bytes: Vec<u8>,
130130
target_pos: Option<na::Vector2<f64>>,
131131
page_range: Option<Range<u32>>,
132+
password: Option<String>,
132133
) -> anyhow::Result<()> {
133134
let pos = self.determine_stroke_import_pos(target_pos);
134135
let adjust_document = self
@@ -139,7 +140,7 @@ impl RnCanvas {
139140

140141
let strokes_receiver = self
141142
.engine_mut()
142-
.generate_pdf_pages_from_bytes(bytes, pos, page_range);
143+
.generate_pdf_pages_from_bytes(bytes, pos, page_range, password);
143144
let strokes = strokes_receiver.await??;
144145
let widget_flags = self
145146
.engine_mut()

crates/rnote-ui/src/dialogs/import.rs

+120-6
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use adw::prelude::*;
77
use futures::StreamExt;
88
use gettextrs::gettext;
99
use gtk4::{
10-
gio, glib, glib::clone, Builder, Button, CallbackAction, FileDialog, FileFilter, Label,
11-
Shortcut, ShortcutController, ShortcutTrigger, ToggleButton,
10+
gio, glib, glib::clone, graphene, gsk, Builder, Button, CallbackAction, FileDialog, FileFilter,
11+
Label, Shortcut, ShortcutController, ShortcutTrigger, ToggleButton,
1212
};
1313
use num_traits::ToPrimitive;
1414
use rnote_engine::engine::import::{PdfImportPageSpacing, PdfImportPagesType};
@@ -111,6 +111,115 @@ pub(crate) async fn filedialog_import_file(appwindow: &RnAppWindow) {
111111
}
112112
}
113113

114+
/// Check for a pdf encryption and request a password if needed from the user
115+
///
116+
/// Returns a password Option and a boolean weather the user canceled the file import or not
117+
pub(crate) async fn pdf_encryption_check_and_dialog(
118+
appwindow: &RnAppWindow,
119+
input_file: &gio::File,
120+
) -> (Option<String>, bool) {
121+
let builder = Builder::from_resource(
122+
(String::from(config::APP_IDPATH) + "ui/dialogs/import.ui").as_str(),
123+
);
124+
125+
let dialog_import_pdf_password: adw::AlertDialog =
126+
builder.object("dialog_import_pdf_password").unwrap();
127+
let pdf_password_entry: adw::PasswordEntryRow = builder.object("pdf_password_entry").unwrap();
128+
let pdf_password_entry_box: gtk4::ListBox = builder.object("pdf_password_entry_box").unwrap();
129+
130+
let target = adw::CallbackAnimationTarget::new(clone!(
131+
#[weak]
132+
pdf_password_entry_box,
133+
move |value| {
134+
let x = adw::lerp(0., 40.0, value);
135+
let p = graphene::Point::new(x as f32, 0.);
136+
let transform = gsk::Transform::new().translate(&p);
137+
pdf_password_entry_box.allocate(
138+
pdf_password_entry_box.width(),
139+
pdf_password_entry_box.height(),
140+
-1,
141+
Some(transform),
142+
);
143+
}
144+
));
145+
146+
let params = adw::SpringParams::new(0.2, 0.5, 500.0);
147+
148+
let animation = adw::SpringAnimation::builder()
149+
.widget(&pdf_password_entry_box)
150+
.value_from(0.0)
151+
.value_to(0.0)
152+
.spring_params(&params)
153+
.target(&target)
154+
.initial_velocity(10.0)
155+
.epsilon(0.001) // If amplitude of oscillation < epsilon, animation stops
156+
.clamp(false)
157+
.build();
158+
159+
let (tx, mut rx) = futures::channel::mpsc::unbounded::<(Option<String>, bool)>();
160+
let tx_cancel = tx.clone();
161+
let tx_unlock = tx.clone();
162+
163+
dialog_import_pdf_password.connect_response(
164+
Some("unlock"),
165+
clone!(
166+
#[weak]
167+
pdf_password_entry,
168+
move |_, _| {
169+
tx_unlock
170+
.unbounded_send((Some(pdf_password_entry.text().to_string()), false))
171+
.unwrap();
172+
}
173+
),
174+
);
175+
176+
dialog_import_pdf_password.connect_response(Some("cancel"), move |_, _| {
177+
tx_cancel.unbounded_send((None, true)).unwrap();
178+
});
179+
180+
let file_name = input_file.basename().map_or_else(
181+
|| gettext("- no file name -"),
182+
|s| s.to_string_lossy().to_string(),
183+
);
184+
let dialog_body = dialog_import_pdf_password.body();
185+
let dialog_body = file_name.clone() + " " + &dialog_body;
186+
dialog_import_pdf_password.set_body(&dialog_body);
187+
188+
let mut password: Option<String> = None;
189+
190+
loop {
191+
match poppler::Document::from_gfile(
192+
input_file,
193+
password.as_deref(),
194+
None::<&gio::Cancellable>,
195+
) {
196+
Ok(_) => return (password, false),
197+
Err(e) => {
198+
if e.matches(poppler::Error::Encrypted) {
199+
dialog_import_pdf_password.present(appwindow.root().as_ref());
200+
pdf_password_entry.grab_focus();
201+
202+
match rx.next().await {
203+
Some((new_password, cancel)) => {
204+
password = new_password;
205+
if cancel {
206+
return (None, true);
207+
}
208+
}
209+
None => {
210+
return (None, true);
211+
}
212+
}
213+
animation.play();
214+
pdf_password_entry.set_text("");
215+
} else {
216+
return (None, true);
217+
}
218+
}
219+
};
220+
}
221+
}
222+
114223
/// Imports the file as Pdf with an import dialog.
115224
///
116225
/// Returns true when the file was imported, else false.
@@ -120,6 +229,11 @@ pub(crate) async fn dialog_import_pdf_w_prefs(
120229
input_file: gio::File,
121230
target_pos: Option<na::Vector2<f64>>,
122231
) -> anyhow::Result<bool> {
232+
let (password, cancel) = pdf_encryption_check_and_dialog(appwindow, &input_file).await;
233+
if cancel {
234+
return Ok(false);
235+
}
236+
123237
let builder = Builder::from_resource(
124238
(String::from(config::APP_IDPATH) + "ui/dialogs/import.ui").as_str(),
125239
);
@@ -274,7 +388,7 @@ pub(crate) async fn dialog_import_pdf_w_prefs(
274388
));
275389

276390
if let Ok(poppler_doc) =
277-
poppler::Document::from_gfile(&input_file, None, None::<&gio::Cancellable>)
391+
poppler::Document::from_gfile(&input_file, password.as_deref(), None::<&gio::Cancellable>)
278392
{
279393
let file_name = input_file.basename().map_or_else(
280394
|| gettext("- no file name -"),
@@ -346,12 +460,12 @@ pub(crate) async fn dialog_import_pdf_w_prefs(
346460
}
347461
));
348462

349-
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 |_| {
463+
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 |_| {
350464
dialog.close();
351465

352466
let inner_tx_confirm = tx_confirm.clone();
353467

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

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

0 commit comments

Comments
 (0)