#![cfg(feature = "async")]
use core::ffi::c_void;
use core::fmt;
use std::ops::BitOr;
use std::ptr;
use std::thread::{self, JoinHandle};
use doom_fish_utils::stream::{AsyncStreamSender, BoundedAsyncStream, NextItem};
use serde::Deserialize;
use crate::error::{PdfKitError, Result};
use crate::ffi;
use crate::handle::ObjectHandle;
use crate::util;
use crate::{PdfDocument, PdfDocumentNotification, PdfTextRange};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PdfDocumentFindOptions(u64);
impl PdfDocumentFindOptions {
pub const NONE: Self = Self(0);
pub const CASE_INSENSITIVE: Self = Self(1);
pub const LITERAL: Self = Self(1 << 1);
pub const BACKWARDS: Self = Self(1 << 2);
#[must_use]
pub const fn bits(self) -> u64 {
self.0
}
}
impl BitOr for PdfDocumentFindOptions {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct PdfDocumentFindPageMatch {
pub page_index: usize,
pub ranges: Vec<PdfTextRange>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct PdfDocumentFindMatch {
pub text: Option<String>,
pub pages: Vec<PdfDocumentFindPageMatch>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PdfDocumentFindEvent {
Notification(PdfDocumentNotification),
Match(PdfDocumentFindMatch),
Failed(PdfKitError),
}
struct SearchThreadHandle {
join: Option<JoinHandle<()>>,
}
impl Drop for SearchThreadHandle {
fn drop(&mut self) {
if let Some(join) = self.join.take() {
let _ = join.join();
}
}
}
impl fmt::Debug for SearchThreadHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SearchThreadHandle")
.field("thread_running", &self.join.is_some())
.finish_non_exhaustive()
}
}
fn push_error(sender: &AsyncStreamSender<PdfDocumentFindEvent>, error: PdfKitError) {
sender.push(PdfDocumentFindEvent::Failed(error));
}
#[derive(Debug)]
pub struct PdfDocumentFindStream {
inner: BoundedAsyncStream<PdfDocumentFindEvent>,
_handle: SearchThreadHandle,
}
impl PdfDocumentFindStream {
pub fn find_string(
document: &PdfDocument,
needle: &str,
options: PdfDocumentFindOptions,
capacity: usize,
) -> Result<Self> {
if capacity == 0 {
return Err(PdfKitError::new(
ffi::status::INVALID_ARGUMENT,
"async stream capacity must be > 0",
));
}
let needle = util::c_string(needle)?;
let document_ptr = unsafe { ffi::pdf_object_retain(document.as_handle_ptr()) };
if document_ptr.is_null() {
return Err(PdfKitError::new(
ffi::status::NULL_RESULT,
"PDFDocument retain returned null",
));
}
let document_addr = document_ptr as usize;
let (stream, sender) = BoundedAsyncStream::new(capacity);
let join = thread::spawn(move || {
let Some(handle) =
(unsafe { ObjectHandle::from_retained_ptr(document_addr as *mut c_void) })
else {
push_error(
&sender,
PdfKitError::new(ffi::status::NULL_RESULT, "PDFDocument retain returned null"),
);
return;
};
let document = PdfDocument::from_handle(handle);
sender.push(PdfDocumentFindEvent::Notification(
PdfDocumentNotification::DidBeginFind,
));
let mut out_error = ptr::null_mut();
let json_ptr = unsafe {
ffi::pdf_document_find_string_json(
document.as_handle_ptr(),
needle.as_ptr(),
options.bits(),
&mut out_error,
)
};
let Some(json) = util::take_string(json_ptr) else {
let message = util::take_string(out_error)
.unwrap_or_else(|| "PDFDocument.findString returned null".to_string());
push_error(&sender, PdfKitError::new(ffi::status::FRAMEWORK, message));
return;
};
match serde_json::from_str::<Vec<PdfDocumentFindMatch>>(&json) {
Ok(matches) => {
for found in matches {
sender.push(PdfDocumentFindEvent::Match(found));
}
sender.push(PdfDocumentFindEvent::Notification(
PdfDocumentNotification::DidEndFind,
));
}
Err(error) => push_error(
&sender,
PdfKitError::new(
ffi::status::FRAMEWORK,
format!("failed to parse PDFDocument find results: {error}"),
),
),
}
});
Ok(Self {
inner: stream,
_handle: SearchThreadHandle { join: Some(join) },
})
}
#[must_use]
pub const fn next(&self) -> NextItem<'_, PdfDocumentFindEvent> {
self.inner.next()
}
#[must_use]
pub fn try_next(&self) -> Option<PdfDocumentFindEvent> {
self.inner.try_next()
}
#[must_use]
pub fn buffered_count(&self) -> usize {
self.inner.buffered_count()
}
#[must_use]
pub fn is_closed(&self) -> bool {
self.inner.is_closed()
}
}