libreoffice_rs/lib.rs
1#![allow(
2 dead_code,
3 non_snake_case,
4 non_camel_case_types,
5 non_upper_case_globals
6)]
7#![allow(clippy::all)]
8include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
9
10mod error;
11pub mod urls;
12
13use error::Error;
14use urls::DocUrl;
15
16use std::ffi::{CStr, CString};
17
18/// A Wrapper for the `LibreOfficeKit` C API.
19#[derive(Clone)]
20pub struct Office {
21 lok: *mut LibreOfficeKit,
22 lok_clz: *mut LibreOfficeKitClass,
23}
24/// A Wrapper for the `LibreOfficeKitDocument` C API.
25pub struct Document {
26 doc: *mut LibreOfficeKitDocument,
27}
28
29/// Optional features of LibreOfficeKit, in particular callbacks that block
30/// LibreOfficeKit until the corresponding reply is received, which would
31/// deadlock if the client does not support the feature.
32///
33/// @see [Office::set_optional_features]
34#[derive(Copy, Clone)]
35pub enum LibreOfficeKitOptionalFeatures {
36 /// Handle `LOK_CALLBACK_DOCUMENT_PASSWORD` by prompting the user for a password.
37 ///
38 /// @see [Office::set_document_password]
39 LOK_FEATURE_DOCUMENT_PASSWORD = (1 << 0),
40
41 /// Handle `LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY` by prompting the user for a password.
42 ///
43 /// @see [Office::set_document_password]
44 LOK_FEATURE_DOCUMENT_PASSWORD_TO_MODIFY = (1 << 1),
45
46 /// Request to have the part number as an 5th value in the `LOK_CALLBACK_INVALIDATE_TILES` payload.
47 LOK_FEATURE_PART_IN_INVALIDATION_CALLBACK = (1 << 2),
48
49 /// Turn off tile rendering for annotations
50 LOK_FEATURE_NO_TILED_ANNOTATIONS = (1 << 3),
51
52 /// Enable range based header data
53 LOK_FEATURE_RANGE_HEADERS = (1 << 4),
54
55 /// Request to have the active view's Id as the 1st value in the `LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR` payload.
56 LOK_FEATURE_VIEWID_IN_VISCURSOR_INVALIDATION_CALLBACK = (1 << 5),
57}
58
59impl Office {
60 /// Create a new LibreOfficeKit instance.
61 ///
62 /// # Arguments
63 ///
64 /// * `install_path` - The path to the LibreOffice installation.
65 ///
66 /// # Example
67 ///
68 /// ```
69 /// use libreoffice_rs::Office;
70 ///
71 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
72 /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
73 ///
74 /// assert_eq!("", office.get_error());
75 /// # Ok(())
76 /// # }
77 /// ```
78 pub fn new(install_path: &str) -> Result<Office, Error> {
79 let c_install_path = CString::new(install_path).unwrap();
80 unsafe {
81 let lok = lok_init_wrapper(c_install_path.as_ptr());
82 let raw_error = (*(*lok).pClass).getError.unwrap()(lok);
83 match *raw_error {
84 0 => Ok(Office {
85 lok,
86 lok_clz: (*lok).pClass,
87 }),
88 _ => Err(Error::new(
89 CStr::from_ptr(raw_error).to_string_lossy().into_owned(),
90 )),
91 }
92 }
93 }
94
95 fn destroy(&mut self) {
96 unsafe {
97 (*self.lok_clz).destroy.unwrap()(self.lok);
98 }
99 }
100
101 /// Returns the last error as a string
102 pub fn get_error(&mut self) -> String {
103 unsafe {
104 let raw_error = (*self.lok_clz).getError.unwrap()(self.lok);
105 CStr::from_ptr(raw_error).to_string_lossy().into_owned()
106 }
107 }
108
109 /// Registers a callback. LOK will invoke this function when it wants to
110 /// inform the client about events.
111 ///
112 /// # Arguments
113 ///
114 /// * `cb` - the callback to invoke (type, payload)
115 ///
116 /// # Example
117 ///
118 /// ```
119 /// use libreoffice_rs::{Office, LibreOfficeKitOptionalFeatures};
120 ///
121 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
122 /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
123 /// office.set_optional_features(
124 /// [LibreOfficeKitOptionalFeatures::LOK_FEATURE_DOCUMENT_PASSWORD]
125 /// )?;
126 ///
127 /// office.register_callback(Box::new({
128 /// move |_type, _payload| {
129 /// println!("Call set_document_password and/or do something here!");
130 /// }
131 /// }))?;
132 ///
133 /// # Ok(())
134 /// # }
135 /// ```
136 pub fn register_callback<F: FnMut(std::os::raw::c_int, *const std::os::raw::c_char) + 'static>(
137 &mut self,
138 cb: F,
139 ) -> Result<(), Error> {
140 unsafe {
141 // LibreOfficeKitCallback typedef (int nType, const char* pPayload, void* pData);
142 unsafe extern "C" fn shim(
143 _type: std::os::raw::c_int,
144 _payload: *const std::os::raw::c_char,
145 data: *mut std::os::raw::c_void,
146 ) {
147 let a: *mut Box<dyn FnMut()> = data as *mut Box<dyn FnMut()>;
148 let f: &mut (dyn FnMut()) = &mut **a;
149 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
150 }
151 let a: *mut Box<dyn FnMut(std::os::raw::c_int, *const std::os::raw::c_char)> = Box::into_raw(Box::new(Box::new(cb)));
152 let data: *mut std::os::raw::c_void = a as *mut std::ffi::c_void;
153 let callback: LibreOfficeKitCallback = Some(shim);
154 (*self.lok_clz).registerCallback.unwrap()(self.lok, callback, data);
155
156 let error = self.get_error();
157 if error != "" {
158 return Err(Error::new(error));
159 }
160 }
161
162 Ok(())
163 }
164
165 /// Loads a document from a URL.
166 ///
167 /// # Arguments
168 /// * `url` - The URL to load.
169 ///
170 /// # Example
171 ///
172 /// ```
173 /// use libreoffice_rs::Office;
174 /// use libreoffice_rs::urls;
175 ///
176 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
177 /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
178 /// let doc_url = urls::local_into_abs("./test_data/test.odt")?;
179 /// office.document_load(doc_url)?;
180 ///
181 /// # Ok(())
182 /// # }
183 /// ```
184 pub fn document_load(&mut self, url: DocUrl) -> Result<Document, Error> {
185 let c_url = CString::new(url.to_string()).unwrap();
186 unsafe {
187 let doc = (*self.lok_clz).documentLoad.unwrap()(self.lok, c_url.as_ptr());
188 let error = self.get_error();
189 if error != "" {
190 return Err(Error::new(error));
191 }
192 Ok(Document { doc })
193 }
194 }
195
196 /// Set bitmask of optional features supported by the client and return the flags set.
197 ///
198 /// # Arguments
199 /// * `feature_flags` - The feature flags to set.
200 ///
201 /// @see [LibreOfficeKitOptionalFeatures]
202 ///
203 /// @since LibreOffice 6.0
204 ///
205 /// # Example
206 ///
207 /// ```
208 /// use libreoffice_rs::{Office, LibreOfficeKitOptionalFeatures};
209 ///
210 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
211 /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
212 /// let feature_flags = [
213 /// LibreOfficeKitOptionalFeatures::LOK_FEATURE_DOCUMENT_PASSWORD,
214 /// LibreOfficeKitOptionalFeatures::LOK_FEATURE_DOCUMENT_PASSWORD_TO_MODIFY,
215 /// ];
216 /// let flags_set = office.set_optional_features(feature_flags)?;
217 ///
218 /// // Integration tests assertions
219 /// for feature_flag in feature_flags {
220 /// assert!(flags_set & feature_flag as u64 > 0,
221 /// "Failed to set the flag with value: {}", feature_flag as u64
222 /// );
223 /// }
224 /// assert!(flags_set &
225 /// LibreOfficeKitOptionalFeatures::LOK_FEATURE_PART_IN_INVALIDATION_CALLBACK as u64 == 0,
226 /// "LOK_FEATURE_PART_IN_INVALIDATION_CALLBACK feature was wrongly set!"
227 /// );
228 ///
229 /// # Ok(())
230 /// # }
231 /// ```
232 pub fn set_optional_features<T>(&mut self, optional_features: T) -> Result<u64, Error>
233 where
234 T: IntoIterator<Item = LibreOfficeKitOptionalFeatures>,
235 {
236 let feature_flags: u64 = optional_features
237 .into_iter()
238 .map(|i| i as u64)
239 .fold(0, |acc, item| acc | item);
240
241 unsafe {
242 (*self.lok_clz).setOptionalFeatures.unwrap()(self.lok, feature_flags);
243 let error = self.get_error();
244 if error != "" {
245 return Err(Error::new(error));
246 }
247 }
248
249 Ok(feature_flags)
250 }
251
252 ///
253 /// Set password required for loading or editing a document.
254 ///
255 /// Loading the document is blocked until the password is provided.
256 /// This MUST be used in combination of features and within a callback
257 ///
258 /// # Arguments
259 /// * `url` - the URL of the document, as sent to the callback
260 /// * `password` - the password, nullptr indicates no password
261 ///
262 /// In response to `LOK_CALLBACK_DOCUMENT_PASSWORD`, a valid password
263 /// will continue loading the document, an invalid password will
264 /// result in another `LOK_CALLBACK_DOCUMENT_PASSWORD` request,
265 /// and a NULL password will abort loading the document.
266 ///
267 /// In response to `LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY`, a valid
268 /// password will continue loading the document, an invalid password will
269 /// result in another `LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY` request,
270 /// and a NULL password will continue loading the document in read-only
271 /// mode.
272 ///
273 /// @since LibreOffice 6.0
274 ///
275 /// # Example
276 ///
277 /// ```
278 /// use libreoffice_rs::{Office, LibreOfficeKitOptionalFeatures, urls};
279 /// use std::sync::atomic::{AtomicBool, Ordering};
280 ///
281 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
282 /// let doc_url = urls::local_into_abs("./test_data/test_password.odt")?;
283 /// let password = "test";
284 /// let password_was_set = AtomicBool::new(false);
285 /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
286 ///
287 /// office.set_optional_features([LibreOfficeKitOptionalFeatures::LOK_FEATURE_DOCUMENT_PASSWORD])?;
288 /// office.register_callback({
289 /// let mut office = office.clone();
290 /// let doc_url = doc_url.clone();
291 /// move |_, _| {
292 /// if !password_was_set.load(Ordering::Acquire) {
293 /// let ret = office.set_document_password(doc_url.clone(), &password);
294 /// password_was_set.store(true, Ordering::Release);
295 /// }
296 /// }
297 /// })?;
298 ///
299 /// let mut _doc = office.document_load(doc_url)?;
300 ///
301 /// # Ok(())
302 /// # }
303 /// ```
304 pub fn set_document_password(&mut self, url: DocUrl, password: &str) -> Result<(), Error> {
305 let c_url = CString::new(url.to_string()).unwrap();
306 let c_password = CString::new(password).unwrap();
307 unsafe {
308 (*self.lok_clz).setDocumentPassword.unwrap()(
309 self.lok,
310 c_url.as_ptr(),
311 c_password.as_ptr(),
312 );
313 let error = self.get_error();
314 if error != "" {
315 return Err(Error::new(error));
316 }
317 Ok(())
318 }
319 }
320
321 /// This method provides a defense mechanism against infinite loops, upon password entry failures:
322 /// * Loading the document is blocked until a valid password is set within callbacks
323 /// * A wrong password will result into infinite repeated callback loops
324 /// * This method advises `LibreOfficeKit` to stop requesting a password *"as soon as possible"*
325 ///
326 /// It is safe for this method to be invoked even if the originally provided password was correct:
327 /// - `LibreOfficeKit` appears to maintain thread-local values of the password. It will stick to the first password entry value.
328 /// That will translate into a a successfully loaded document.
329 /// - `LibreOfficeKit` seems to send an "excessive" number of callbacks (potential internal issues with locks/monitors)
330 ///
331 /// # Arguments
332 /// * `url` - the URL of the document, as sent to the callback
333 ///
334 /// # Example
335 ///
336 /// ```
337 /// use libreoffice_rs::{Office, LibreOfficeKitOptionalFeatures, urls};
338 /// use std::sync::atomic::{AtomicBool, Ordering};
339 ///
340 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
341 /// let doc_url = urls::local_into_abs("./test_data/test_password.odt")?;
342 /// let password = "forgotten_invalid_password_which_is_just_test";
343 /// let password_was_set = AtomicBool::new(false);
344 /// let failed_password_attempt = AtomicBool::new(false);
345 /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
346 ///
347 /// office.set_optional_features([LibreOfficeKitOptionalFeatures::LOK_FEATURE_DOCUMENT_PASSWORD])?;
348 /// office.register_callback({
349 /// let mut office = office.clone();
350 /// let doc_url = doc_url.clone();
351 /// move |_, _| {
352 /// if !password_was_set.load(Ordering::Acquire) {
353 /// let ret = office.set_document_password(doc_url.clone(), &password);
354 /// password_was_set.store(true, Ordering::Release);
355 /// } else {
356 /// if !failed_password_attempt.load(Ordering::Acquire) {
357 /// let ret = office.unset_document_password(doc_url.clone());
358 /// failed_password_attempt.store(true, Ordering::Release);
359 /// }
360 /// }
361 /// }
362 /// })?;
363 ///
364 /// assert!(office.document_load(doc_url).is_err(),
365 /// "Document loaded successfully with a wrong password!");
366 ///
367 /// # Ok(())
368 /// # }
369 /// ```
370 pub fn unset_document_password(&mut self, url: DocUrl) -> Result<(), Error> {
371 let c_url = CString::new(url.to_string()).unwrap();
372 unsafe {
373 (*self.lok_clz).setDocumentPassword.unwrap()(
374 self.lok,
375 c_url.as_ptr(),
376 std::ptr::null(),
377 );
378 let error = self.get_error();
379 if error != "" {
380 return Err(Error::new(error));
381 }
382 Ok(())
383 }
384 }
385
386 /// Loads a document from a URL with additional options.
387 ///
388 /// # Arguments
389 /// * `url` - The URL to load.
390 /// * `options` - options for the import filter, e.g. SkipImages.
391 /// Another useful FilterOption is "Language=...". It is consumed
392 /// by the documentLoad() itself, and when provided, LibreOfficeKit
393 /// switches the language accordingly first.
394 ///
395 /// # Example
396 ///
397 /// ```
398 /// use libreoffice_rs::{Office, urls};
399 ///
400 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
401 /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
402 /// let doc_url = urls::local_into_abs("./test_data/test.odt")?;
403 /// office.document_load_with(doc_url, "en-US")?;
404 ///
405 /// # Ok(())
406 /// # }
407 /// ```
408 pub fn document_load_with(&mut self, url: DocUrl, options: &str) -> Result<Document, Error> {
409 let c_url = CString::new(url.to_string()).unwrap();
410 let c_options = CString::new(options).unwrap();
411 unsafe {
412 let doc = (*self.lok_clz).documentLoadWithOptions.unwrap()(
413 self.lok,
414 c_url.as_ptr(),
415 c_options.as_ptr(),
416 );
417 let error = self.get_error();
418 if error != "" {
419 return Err(Error::new(error));
420 }
421 Ok(Document { doc })
422 }
423 }
424}
425
426impl Drop for Office {
427 fn drop(&mut self) {
428 self.destroy()
429 }
430}
431
432impl Document {
433 /// Stores the document's persistent data to a URL and
434 /// continues to be a representation of the old URL.
435 ///
436 /// If the result is not true, then there's an error (possibly unsupported format or other errors)
437 ///
438 /// # Arguments
439 /// * `url` - the location where to store the document
440 /// * `format` - the format to use while exporting, when omitted, then deducted from pURL's extension
441 /// * `filter` - options for the export filter, e.g. SkipImages.Another useful FilterOption is "TakeOwnership". It is consumed
442 /// by the saveAs() itself, and when provided, the document identity
443 /// changes to the provided pUrl - meaning that '.uno:ModifiedStatus'
444 /// is triggered as with the "Save As..." in the UI.
445 /// "TakeOwnership" mode must not be used when saving to PNG or PDF.
446 ///
447 /// # Example
448 ///
449 /// ```
450 /// use libreoffice_rs::Office;
451 /// use libreoffice_rs::urls;
452 ///
453 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
454 /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
455 /// let doc_url = urls::local_into_abs("./test_data/test.odt")?;
456 /// let mut doc = office.document_load(doc_url)?;
457 /// let output_path = std::env::temp_dir().join("libreoffice_rs_save_as.png");
458 /// let output_location = output_path.display().to_string();
459 /// let previously_saved = doc.save_as(&output_location, "png", None);
460 /// let _ = std::fs::remove_file(&output_path);
461 ///
462 /// assert!(previously_saved, "{}", office.get_error());
463 ///
464 /// # Ok(())
465 /// # }
466 /// ```
467 pub fn save_as(&mut self, url: &str, format: &str, filter: Option<&str>) -> bool {
468 let c_url = CString::new(url).unwrap();
469 let c_format: CString = CString::new(format).unwrap();
470 let c_filter: CString = CString::new(filter.unwrap_or_default()).unwrap();
471 let ret = unsafe {
472 (*(*self.doc).pClass).saveAs.unwrap()(
473 self.doc,
474 c_url.as_ptr(),
475 c_format.as_ptr(),
476 c_filter.as_ptr(),
477 )
478 };
479
480 ret != 0
481 }
482
483 fn destroy(&mut self) {
484 unsafe {
485 (*(*self.doc).pClass).destroy.unwrap()(self.doc);
486 }
487 }
488}
489
490impl Drop for Document {
491 fn drop(&mut self) {
492 self.destroy()
493 }
494}