Skip to main content

pdf_thumb/
lib.rs

1//! This library is a thin wrapper of WinRT [PdfDocument Class](https://learn.microsoft.com/en-us/uwp/api/windows.data.pdf.pdfdocument?view=winrt-26100) to generate a thumbnail image for PDF.
2//!
3//! # Example
4//!
5//! ```rust
6//! use anyhow::Result;
7//! use pdf_thumb::PdfDoc;
8//!
9//! fn main() -> Result<()> {
10//!     let pdf = PdfDoc::open("test.pdf")?;
11//!     let thumb = pdf.thumb()?;
12//!     std::fs::write("thumb.png", &thumb)?; // PNG is default.
13//!     Ok(())
14//! }
15//! ```
16//!
17//! Some options and async operation are also available.
18//!
19//! ```rust
20//! use anyhow::Result;
21//! use pdf_thumb::{ImageFormat, Options, PdfDoc};
22//!
23//! #[tokio::main]
24//! fn main() -> Result<()> {
25//!     let pdf = PdfDoc::open_async("test.pdf").await?;
26//!     let options = Options {
27//!         width: 320,                // Set thumbnail image width.
28//!         format: ImageFormat::Jpeg, // Set thumbnail image format.
29//!         ..Default::default()
30//!     };
31//!     let thumb = pdf.thumb_with_options_async(options).await?;
32//!     tokio::fs::write("thumb.jpg", &thumb).await?;
33//!     Ok(())
34//! }
35//! ```
36//!
37//! - [crates.io](https://crates.io/crates/pdf-thumb)
38//! - [Repository](https://github.com/zxrs/pdf-thumb)
39
40#![cfg(target_os = "windows")]
41
42use dunce::canonicalize;
43use std::path::Path;
44use thiserror::Error;
45use windows::{
46    core::{GUID, HSTRING},
47    Data::Pdf::{PdfDocument, PdfPage, PdfPageRenderOptions},
48    Foundation::{self, IAsyncAction, IAsyncOperation},
49    Storage::{
50        StorageFile,
51        Streams::{DataReader, DataWriter, InMemoryRandomAccessStream},
52    },
53};
54
55mod guid;
56use guid::*;
57
58#[derive(Debug, Error)]
59pub enum PdfThumbError {
60    #[error("io error")]
61    Io(#[from] std::io::Error),
62    #[error("windows error")]
63    Windows(#[from] windows::core::Error),
64}
65
66#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
67pub struct Rect {
68    pub x: u32,
69    pub y: u32,
70    pub width: u32,
71    pub height: u32,
72}
73
74impl From<Rect> for Foundation::Rect {
75    fn from(r: Rect) -> Self {
76        Self {
77            X: r.x as _,
78            Y: r.y as _,
79            Width: r.width as _,
80            Height: r.height as _,
81        }
82    }
83}
84
85#[derive(Debug, Default, Clone, Copy)]
86pub struct Options {
87    /// The destination width of the rendered page. If `width` is not specified, the page's aspect ratio is maintained relative to the destination height.
88    pub width: u32,
89    /// The destination height of the rendered page. If `height` is not specified, the page's aspect ratio is maintained relative to the destination width.
90    pub height: u32,
91    /// The portion of the PDF page to be rendered. If `rect` is not specified, the whole page is rendered.
92    pub rect: Rect,
93    /// The page index to be rendered. If `page` is not specified, the first page is rendered.
94    pub page: u32,
95    /// The image format of thumbnail. If `format` is not specified, PNG format is used.
96    pub format: ImageFormat,
97}
98
99impl TryFrom<Options> for PdfPageRenderOptions {
100    type Error = PdfThumbError;
101    fn try_from(options: Options) -> Result<Self, Self::Error> {
102        let op = PdfPageRenderOptions::new()?;
103        if options.width > 0 {
104            op.SetDestinationWidth(options.width)?;
105        }
106        if options.height > 0 {
107            op.SetDestinationHeight(options.height)?;
108        }
109        if options.rect.ne(&Rect::default()) {
110            op.SetSourceRect(options.rect.into())?;
111        }
112        op.SetBitmapEncoderId(options.format.guid())?;
113        Ok(op)
114    }
115}
116
117#[derive(Debug, Clone, Copy)]
118pub enum ImageFormat {
119    Png,
120    Bmp,
121    Jpeg,
122    Tiff,
123    Gif,
124}
125
126impl Default for ImageFormat {
127    fn default() -> Self {
128        Self::Png
129    }
130}
131
132impl ImageFormat {
133    const fn guid(&self) -> GUID {
134        use ImageFormat::*;
135        match self {
136            Png => PNG_ENCORDER_ID,
137            Bmp => BITMAP_ENCODER_ID,
138            Jpeg => JPEG_ENCORDER_ID,
139            Tiff => TIFF_ENCODER_ID,
140            Gif => GIF_ENCODER_ID,
141        }
142    }
143}
144
145#[derive(Debug)]
146pub struct PdfDoc {
147    doc: PdfDocument,
148}
149
150impl PdfDoc {
151    /// Load a PDF document from memory.
152    pub fn load(pdf: &[u8]) -> Result<Self, PdfThumbError> {
153        let stream = InMemoryRandomAccessStream::new()?;
154        let writer = DataWriter::CreateDataWriter(&stream)?;
155        writer.WriteBytes(pdf)?;
156        writer.StoreAsync()?.get()?;
157        writer.FlushAsync()?.get()?;
158        writer.DetachStream()?;
159        let doc = PdfDocument::LoadFromStreamAsync(&stream)?.get()?;
160        Ok(Self { doc })
161    }
162
163    /// Open a PDF document from a path.
164    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, PdfThumbError> {
165        let file = get_file(path)?.get()?;
166        let doc = open(&file)?.get()?;
167        Ok(Self { doc })
168    }
169
170    /// Open a PDF document from a path asynchronously.
171    pub async fn open_async<P: AsRef<Path>>(path: P) -> Result<Self, PdfThumbError> {
172        let file = get_file(path)?.await?;
173        let doc = open(&file)?.await?;
174        Ok(Self { doc })
175    }
176
177    /// Get the number of PDF document.
178    pub fn page_count(&self) -> Result<u32, PdfThumbError> {
179        Ok(self.doc.PageCount()?)
180    }
181
182    /// Generate a thumbnail image with default options.
183    pub fn thumb(&self) -> Result<Vec<u8>, PdfThumbError> {
184        let options = Options::default();
185        self.thumb_with_options(options)
186    }
187
188    /// Generate a thumbnail image with default options asynchronously.
189    pub async fn thumb_async(&self) -> Result<Vec<u8>, PdfThumbError> {
190        let options = Options::default();
191        self.thumb_with_options_async(options).await
192    }
193
194    /// Generate a thumbnail image with the specified options.
195    pub fn thumb_with_options(&self, options: Options) -> Result<Vec<u8>, PdfThumbError> {
196        let page = self.doc.GetPage(options.page)?;
197        let output = InMemoryRandomAccessStream::new()?;
198        render(page, &output, options)?.get()?;
199        read_bytes(output)
200    }
201
202    /// Generate a thumbnail image with the specified options asynchronously.
203    pub async fn thumb_with_options_async(
204        &self,
205        options: Options,
206    ) -> Result<Vec<u8>, PdfThumbError> {
207        let page = self.doc.GetPage(options.page)?;
208        let output = InMemoryRandomAccessStream::new()?;
209        render(page, &output, options)?.await?;
210        read_bytes(output)
211    }
212}
213
214fn get_file<P: AsRef<Path>>(path: P) -> Result<IAsyncOperation<StorageFile>, PdfThumbError> {
215    let path = HSTRING::from(canonicalize(path)?.as_path());
216    StorageFile::GetFileFromPathAsync(&path).map_err(Into::into)
217}
218
219fn open(file: &StorageFile) -> Result<IAsyncOperation<PdfDocument>, PdfThumbError> {
220    PdfDocument::LoadFromFileAsync(file).map_err(Into::into)
221}
222
223fn render(
224    page: PdfPage,
225    output: &InMemoryRandomAccessStream,
226    options: Options,
227) -> Result<IAsyncAction, PdfThumbError> {
228    page.RenderWithOptionsToStreamAsync(output, options.try_into().as_ref().ok())
229        .map_err(Into::into)
230}
231
232fn read_bytes(output: InMemoryRandomAccessStream) -> Result<Vec<u8>, PdfThumbError> {
233    let input = output.GetInputStreamAt(0)?;
234    let reader = DataReader::CreateDataReader(&input)?;
235    let size = output.Size()?;
236    reader.LoadAsync(size as u32)?.get()?;
237    let mut buf = vec![0; size as usize];
238    reader.ReadBytes(&mut buf)?;
239    Ok(buf)
240}