├── test.pdf ├── .gitignore ├── .travis.yml ├── .gitlab-ci.yml ├── Cargo.toml ├── .github └── workflows │ └── rust.yml ├── README.md └── src ├── util.rs ├── ffi.rs └── lib.rs /test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DMSrs/poppler-rs/HEAD/test.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | .idea/ 4 | Cargo.lock 5 | output.pdf 6 | out*.png 7 | *.swp 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial # trusty's version of glib is too old 2 | language: rust 3 | rust: 4 | - stable 5 | - beta 6 | - nightly 7 | matrix: 8 | allow_failures: 9 | - rust: nightly 10 | cache: cargo 11 | script: 12 | - cargo build --verbose --all 13 | - cargo test --verbose --all 14 | addons: 15 | apt: 16 | packages: 17 | - libpoppler-glib-dev 18 | - libpoppler-dev 19 | - libcairo2-dev 20 | - libglib2.0-dev 21 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: rust:latest 2 | 3 | stages: 4 | - build 5 | - test 6 | 7 | before_script: 8 | - apt update 9 | - apt install --yes libpoppler-glib-dev libpoppler-dev libcairo2-dev libglib2.0-dev 10 | - rustup default nightly 11 | 12 | build: 13 | tags: 14 | - rust 15 | stage: build 16 | script: 17 | - cargo build --verbose --all 18 | artifacts: 19 | paths: 20 | - target 21 | 22 | test: 23 | tags: 24 | - rust 25 | stage: test 26 | script: 27 | - cargo test --verbose --all 28 | 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = [ 3 | "Marc Brinkmann ", 4 | "Denys Vitali ", 5 | ] 6 | name = "poppler" 7 | license = "GPL-2.0" 8 | version = "0.6.0" 9 | description = "Wrapper for the GPL-licensed Poppler PDF rendering library." 10 | repository = "https://github.com/DMSrs/poppler-rs" 11 | edition = "2018" 12 | 13 | [features] 14 | render = ["dep:cairo-rs"] 15 | 16 | [dependencies] 17 | cairo-rs = { version = "0.20.5", features = ["png", "pdf"], optional = true } 18 | glib = "0.20.6" 19 | gobject-sys = "0.20" 20 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Install libpoppler-glib-dev libcairo-dev 20 | run: sudo apt-get update && sudo apt-get install libpoppler-glib-dev libcairo-dev 21 | - name: Build 22 | run: cargo build --verbose --all-features 23 | - name: Run tests 24 | run: cargo test --verbose --all-features 25 | - name: Publish to Cargo Registry 26 | if: github.ref == 'refs/heads/master' 27 | env: 28 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 29 | run: cargo publish 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # poppler-rs 2 | 3 | [![poppler](https://img.shields.io/crates/v/poppler.svg)](https://crates.io/crates/poppler) 4 | [![Documentation](https://img.shields.io/static/v1.svg?label=documentation&message=docs.rs&color=blue)](https://docs.rs/poppler/*/poppler/) 5 | 6 | [libpoppler](https://poppler.freedesktop.org/) is a library for rendering PDF files, 7 | this crate is Rust bindings to it. 8 | 9 | It uses [cairo](https://crates.io/crates/cairo-rs) for rendering, 10 | as a result PDF content can be drawn onto a number of surfaces, including SVG, PDF or PNG. 11 | 12 | > [!WARNING] 13 | > libpoppler is based on the GPL-licensed [xpdf-3.0](http://www.foolabs.com/xpdf/) 14 | > and is unlikely to ever be released under a different license. 15 | > As a result, every program or library linking against this crate *must* be GPL licensed as well. 16 | 17 | The crate has only been tested on Linux; ensure that `libpoppler-glib` is installed to use it. 18 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{CString, OsString}; 2 | use std::{fs, path, ptr}; 3 | 4 | use glib::translate::FromGlibPtrFull; 5 | 6 | pub fn call_with_gerror(f: F) -> Result<*mut T, glib::error::Error> 7 | where 8 | F: FnOnce(*mut *mut glib::ffi::GError) -> *mut T, 9 | { 10 | // initialize error to a null-pointer 11 | let mut err = ptr::null_mut(); 12 | 13 | // call the c-library function 14 | let return_value = f(&mut err as *mut *mut glib::ffi::GError); 15 | 16 | if return_value.is_null() { 17 | unsafe { Err(glib::error::Error::from_glib_full(err)) } 18 | } else { 19 | Ok(return_value) 20 | } 21 | } 22 | 23 | pub fn path_to_glib_url>(p: P) -> Result { 24 | // canonicalize path, try to wrap failures into a glib error 25 | let canonical = fs::canonicalize(p).map_err(|_| { 26 | glib::error::Error::new( 27 | glib::FileError::Noent, 28 | "Could not turn path into canonical path. Maybe it does not exist?", 29 | ) 30 | })?; 31 | 32 | // construct path string 33 | let mut osstr_path: OsString = "file:///".into(); 34 | osstr_path.push(canonical); 35 | 36 | // we need to round-trip to string, as not all os strings are 8 bytes 37 | let pdf_string = osstr_path.into_string().map_err(|_| { 38 | glib::error::Error::new( 39 | glib::FileError::Inval, 40 | "Path invalid (contains non-utf8 characters)", 41 | ) 42 | })?; 43 | 44 | CString::new(pdf_string).map_err(|_| { 45 | glib::error::Error::new( 46 | glib::FileError::Inval, 47 | "Path invalid (contains NUL characters)", 48 | ) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::{c_char, c_double, c_int, c_uint}; 2 | 3 | // FIXME: is this the correct way to get opaque types? 4 | // FIXME: alternative: https://docs.rs/cairo-sys-rs/0.5.0/src/cairo_sys/lib.rs.html#64 5 | // NOTE: https://github.com/rust-lang/rust/issues/27303 6 | // NOTE: ask F/O about this 7 | pub enum PopplerDocument {} 8 | pub enum PopplerPage {} 9 | 10 | // FIXME: *const instead of mut pointers? 11 | 12 | #[link(name = "poppler-glib")] 13 | extern "C" { 14 | pub fn poppler_document_new_from_file( 15 | uri: *const c_char, 16 | password: *const c_char, 17 | error: *mut *mut glib::ffi::GError, 18 | ) -> *mut PopplerDocument; 19 | pub fn poppler_document_new_from_data( 20 | data: *mut c_char, 21 | length: c_int, 22 | password: *const c_char, 23 | error: *mut *mut glib::ffi::GError, 24 | ) -> *mut PopplerDocument; 25 | pub fn poppler_document_get_n_pages(document: *mut PopplerDocument) -> c_int; 26 | pub fn poppler_document_get_page( 27 | document: *mut PopplerDocument, 28 | index: c_int, 29 | ) -> *mut PopplerPage; 30 | 31 | pub fn poppler_document_get_title(document: *mut PopplerDocument) -> *mut c_char; 32 | pub fn poppler_document_get_metadata(document: *mut PopplerDocument) -> *mut c_char; 33 | pub fn poppler_document_get_pdf_version_string(document: *mut PopplerDocument) -> *mut c_char; 34 | pub fn poppler_document_get_permissions(document: *mut PopplerDocument) -> c_uint; 35 | 36 | pub fn poppler_page_get_size( 37 | page: *mut PopplerPage, 38 | width: *mut c_double, 39 | height: *mut c_double, 40 | ); 41 | 42 | #[cfg(feature = "render")] 43 | pub fn poppler_page_render(page: *mut PopplerPage, cairo: *mut cairo::ffi::cairo_t); 44 | 45 | #[cfg(feature = "render")] 46 | pub fn poppler_page_render_for_printing( 47 | page: *mut PopplerPage, 48 | cairo: *mut cairo::ffi::cairo_t, 49 | ); 50 | 51 | pub fn poppler_page_get_text(page: *mut PopplerPage) -> *mut c_char; 52 | } 53 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::ffi::CString; 3 | use std::os::raw::{c_char, c_double, c_int}; 4 | use std::path; 5 | 6 | /// Re-exports `cairo` to provide types required for rendering. 7 | #[cfg(feature = "render")] 8 | pub use cairo; 9 | 10 | mod ffi; 11 | mod util; 12 | 13 | /// A Poppler PDF document. 14 | #[derive(Debug)] 15 | pub struct PopplerDocument(*mut ffi::PopplerDocument); 16 | 17 | /// Represents a page in a [`PopplerDocument`]. 18 | #[derive(Debug)] 19 | pub struct PopplerPage(*mut ffi::PopplerPage); 20 | 21 | impl PopplerDocument { 22 | /// Creates a new Poppler document. 23 | pub fn new_from_file>( 24 | p: P, 25 | password: Option<&str>, 26 | ) -> Result { 27 | let pw = Self::get_password(password)?; 28 | 29 | let path_cstring = util::path_to_glib_url(p)?; 30 | let doc = util::call_with_gerror(|err_ptr| unsafe { 31 | ffi::poppler_document_new_from_file(path_cstring.as_ptr(), pw.as_ptr(), err_ptr) 32 | })?; 33 | 34 | Ok(PopplerDocument(doc)) 35 | } 36 | 37 | /// Creates a new Poppler document. 38 | pub fn new_from_data( 39 | data: &mut [u8], 40 | password: Option<&str>, 41 | ) -> Result { 42 | if data.is_empty() { 43 | return Err(glib::error::Error::new( 44 | glib::FileError::Inval, 45 | "data is empty", 46 | )); 47 | } 48 | let pw = Self::get_password(password)?; 49 | 50 | let doc = util::call_with_gerror(|err_ptr| unsafe { 51 | ffi::poppler_document_new_from_data( 52 | data.as_mut_ptr() as *mut c_char, 53 | data.len() as c_int, 54 | pw.as_ptr(), 55 | err_ptr, 56 | ) 57 | })?; 58 | 59 | Ok(PopplerDocument(doc)) 60 | } 61 | 62 | /// Returns the document's title. 63 | pub fn get_title(&self) -> Option { 64 | unsafe { 65 | let ptr: *mut c_char = ffi::poppler_document_get_title(self.0); 66 | if ptr.is_null() { 67 | None 68 | } else { 69 | CString::from_raw(ptr).into_string().ok() 70 | } 71 | } 72 | } 73 | 74 | /// Returns the XML metadata string of the document. 75 | pub fn get_metadata(&self) -> Option { 76 | unsafe { 77 | let ptr: *mut c_char = ffi::poppler_document_get_metadata(self.0); 78 | if ptr.is_null() { 79 | None 80 | } else { 81 | CString::from_raw(ptr).into_string().ok() 82 | } 83 | } 84 | } 85 | 86 | /// Returns the PDF version of document as a string (e.g. `PDF-1.6`). 87 | pub fn get_pdf_version_string(&self) -> Option { 88 | unsafe { 89 | let ptr: *mut c_char = ffi::poppler_document_get_pdf_version_string(self.0); 90 | if ptr.is_null() { 91 | None 92 | } else { 93 | CString::from_raw(ptr).into_string().ok() 94 | } 95 | } 96 | } 97 | 98 | /// Returns the flags specifying which operations are permitted when the document is opened. 99 | pub fn get_permissions(&self) -> u8 { 100 | unsafe { ffi::poppler_document_get_permissions(self.0) as u8 } 101 | } 102 | 103 | /// Returns the number of pages in a loaded document. 104 | pub fn get_n_pages(&self) -> usize { 105 | // FIXME: what's the correct type here? can we assume a document 106 | // has a positive number of pages? 107 | (unsafe { ffi::poppler_document_get_n_pages(self.0) }) as usize 108 | } 109 | 110 | /// Returns the page indexed at `index`. 111 | pub fn get_page(&self, index: usize) -> Option { 112 | match unsafe { ffi::poppler_document_get_page(self.0, index as c_int) } { 113 | ptr if ptr.is_null() => None, 114 | ptr => Some(PopplerPage(ptr)), 115 | } 116 | } 117 | 118 | pub fn pages(&self) -> PagesIter { 119 | PagesIter { 120 | total: self.get_n_pages(), 121 | index: 0, 122 | doc: self, 123 | } 124 | } 125 | 126 | 127 | /// Converts the provided password into a `CString`. 128 | fn get_password(password: Option<&str>) -> Result { 129 | Ok(CString::new(if password.is_none() { 130 | "" 131 | } else { 132 | password.expect("password.is_none() is false, but apparently it's lying.") 133 | }) 134 | .map_err(|_| { 135 | glib::error::Error::new( 136 | glib::FileError::Inval, 137 | "Password invalid (possibly contains NUL characters)", 138 | ) 139 | })?) 140 | } 141 | 142 | /// Returns the number of pages. 143 | /// 144 | /// This function is a convenience wrapper around [`get_n_pages`](Self::get_n_pages). 145 | pub fn len(&self) -> usize { 146 | self.get_n_pages() 147 | } 148 | 149 | /// Indicates whether this document has no pages. 150 | pub fn is_empty(&self) -> bool { 151 | self.len() == 0 152 | } 153 | } 154 | 155 | impl Drop for PopplerDocument { 156 | fn drop(&mut self) { 157 | unsafe { 158 | gobject_sys::g_object_unref(self.0 as *mut gobject_sys::GObject); 159 | } 160 | } 161 | } 162 | 163 | impl PopplerPage { 164 | /// Gets the size of page at the current scale and rotation. 165 | pub fn get_size(&self) -> (f64, f64) { 166 | let mut width: f64 = 0.0; 167 | let mut height: f64 = 0.0; 168 | 169 | unsafe { 170 | ffi::poppler_page_get_size( 171 | self.0, 172 | &mut width as *mut f64 as *mut c_double, 173 | &mut height as *mut f64 as *mut c_double, 174 | ) 175 | } 176 | 177 | (width, height) 178 | } 179 | 180 | /// Render the page to the given cairo context. 181 | /// 182 | /// This function is for rendering a page that will be displayed. If you want to render a 183 | /// page that will be printed use [`render_for_printing`](Self::render_for_printing) instead. Please see the 184 | /// Poppler documentation for that function for the differences between rendering to the screen 185 | /// and rendering to a printer. 186 | #[cfg(feature = "render")] 187 | pub fn render(&self, ctx: &cairo::Context) { 188 | let ctx_raw = ctx.to_raw_none(); 189 | unsafe { ffi::poppler_page_render(self.0, ctx_raw) } 190 | } 191 | 192 | /// Render the page to the given cairo context for printing with POPPLER_PRINT_ALL flags selected. 193 | /// 194 | /// The difference between [`render`](Self::render) and this function is that some things get 195 | /// rendered differently between screens and printers: 196 | /// 197 | /// * PDF annotations get rendered according to their PopplerAnnotFlag value. 198 | /// For example, POPPLER_ANNOT_FLAG_PRINT refers to whether an annotation is printed or not, 199 | /// whereas POPPLER_ANNOT_FLAG_NO_VIEW refers to whether an annotation is invisible when displaying to the screen. 200 | /// * PDF supports "hairlines" of width 0.0, which often get rendered as having a width of 1 201 | /// device pixel. When displaying on a screen, Cairo may render such lines wide so that 202 | /// they are hard to see, and Poppler makes use of PDF's Stroke Adjust graphics parameter 203 | /// to make the lines easier to see. However, when printing, Poppler is able to directly use a printer's pixel size instead. 204 | /// * Some advanced features in PDF may require an image to be rasterized before sending 205 | /// off to a printer. This may produce raster images which exceed Cairo's limits. 206 | /// The "printing" functions will detect this condition and try to down-scale the intermediate surfaces as appropriate. 207 | #[cfg(feature = "render")] 208 | pub fn render_for_printing(&self, ctx: &cairo::Context) { 209 | let ctx_raw = ctx.to_raw_none(); 210 | unsafe { ffi::poppler_page_render_for_printing(self.0, ctx_raw) } 211 | } 212 | 213 | /// Retrieves the text of the page. 214 | pub fn get_text(&self) -> Option<&str> { 215 | match unsafe { ffi::poppler_page_get_text(self.0) } { 216 | ptr if ptr.is_null() => None, 217 | ptr => unsafe { Some(CStr::from_ptr(ptr).to_str().unwrap_or_default()) }, 218 | } 219 | } 220 | } 221 | 222 | #[derive(Debug)] 223 | pub struct PagesIter<'a> { 224 | total: usize, 225 | index: usize, 226 | doc: &'a PopplerDocument, 227 | } 228 | 229 | impl<'a> Iterator for PagesIter<'a> { 230 | type Item = PopplerPage; 231 | 232 | fn next(&mut self) -> Option { 233 | if self.index < self.total { 234 | let page = self.doc.get_page(self.index); 235 | self.index += 1; 236 | page 237 | } else { 238 | None 239 | } 240 | } 241 | } 242 | 243 | impl Drop for PopplerPage { 244 | fn drop(&mut self) { 245 | unsafe { 246 | gobject_sys::g_object_unref(self.0 as *mut gobject_sys::GObject); 247 | } 248 | } 249 | } 250 | 251 | #[cfg(test)] 252 | mod tests { 253 | use crate::PopplerDocument; 254 | use crate::PopplerPage; 255 | #[cfg(feature = "render")] 256 | use cairo::Context; 257 | #[cfg(feature = "render")] 258 | use cairo::Format; 259 | #[cfg(feature = "render")] 260 | use cairo::ImageSurface; 261 | use std::{fs::File, io::Read}; 262 | 263 | #[test] 264 | fn test1() { 265 | let filename = "test.pdf"; 266 | let doc = PopplerDocument::new_from_file(filename, None).unwrap(); 267 | let num_pages = doc.get_n_pages(); 268 | 269 | println!("Document has {} page(s)", num_pages); 270 | 271 | #[cfg(feature = "render")] 272 | let surface = cairo::PdfSurface::new(420.0, 595.0, "output.pdf").unwrap(); 273 | #[cfg(feature = "render")] 274 | let ctx = Context::new(&surface).unwrap(); 275 | 276 | // FIXME: move iterator to poppler 277 | for page_num in 0..num_pages { 278 | let page = doc.get_page(page_num).unwrap(); 279 | let (w, h) = page.get_size(); 280 | println!("page {} has size {}, {}", page_num, w, h); 281 | #[cfg(feature = "render")] 282 | (|page: &PopplerPage, ctx: &Context| { 283 | surface.set_size(w, h).unwrap(); 284 | ctx.save().unwrap(); 285 | page.render(ctx); 286 | })(&page, &ctx); 287 | 288 | println!("Text: {:?}", page.get_text().unwrap_or("")); 289 | #[cfg(feature = "render")] 290 | (|ctx: &Context| { 291 | ctx.restore().unwrap(); 292 | ctx.show_page().unwrap(); 293 | })(&ctx); 294 | } 295 | //surface.write_to_png("file.png"); 296 | #[cfg(feature = "render")] 297 | surface.finish(); 298 | } 299 | 300 | #[test] 301 | fn test2_from_file() { 302 | let path = "test.pdf"; 303 | let doc: PopplerDocument = PopplerDocument::new_from_file(path, Some("upw")).unwrap(); 304 | let num_pages = doc.get_n_pages(); 305 | let title = doc.get_title().unwrap(); 306 | let metadata = doc.get_metadata(); 307 | let version_string = doc.get_pdf_version_string(); 308 | let permissions = doc.get_permissions(); 309 | let page: PopplerPage = doc.get_page(0).unwrap(); 310 | let (w, h) = page.get_size(); 311 | 312 | println!( 313 | "Document {} has {} page(s) and is {}x{}", 314 | title, num_pages, w, h 315 | ); 316 | println!( 317 | "Version: {:?}, Permissions: {:x?}", 318 | version_string, permissions 319 | ); 320 | 321 | assert!(metadata.is_some()); 322 | assert_eq!(version_string, Some("PDF-1.3".to_string())); 323 | assert_eq!(permissions, 0xff); 324 | 325 | assert_eq!(title, "This is a test PDF file"); 326 | 327 | #[cfg(feature = "render")] 328 | let surface = ImageSurface::create(Format::ARgb32, w as i32, h as i32).unwrap(); 329 | #[cfg(feature = "render")] 330 | let ctx = Context::new(&surface).unwrap(); 331 | 332 | #[cfg(feature = "render")] 333 | (|page: &PopplerPage, ctx: &Context| { 334 | ctx.save().unwrap(); 335 | page.render(ctx); 336 | ctx.restore().unwrap(); 337 | ctx.show_page().unwrap(); 338 | })(&page, &ctx); 339 | 340 | #[cfg(feature = "render")] 341 | let mut f: File = File::create("out.png").unwrap(); 342 | #[cfg(feature = "render")] 343 | surface.write_to_png(&mut f).expect("Unable to write PNG"); 344 | } 345 | #[test] 346 | fn test2_from_data() { 347 | let path = "test.pdf"; 348 | let mut file = File::open(path).unwrap(); 349 | let mut data: Vec = Vec::new(); 350 | file.read_to_end(&mut data).unwrap(); 351 | let doc: PopplerDocument = 352 | PopplerDocument::new_from_data(&mut data[..], Some("upw")).unwrap(); 353 | let num_pages = doc.get_n_pages(); 354 | let title = doc.get_title().unwrap(); 355 | let metadata = doc.get_metadata(); 356 | let version_string = doc.get_pdf_version_string(); 357 | let permissions = doc.get_permissions(); 358 | let page: PopplerPage = doc.get_page(0).unwrap(); 359 | let (w, h) = page.get_size(); 360 | 361 | println!( 362 | "Document {} has {} page(s) and is {}x{}", 363 | title, num_pages, w, h 364 | ); 365 | println!( 366 | "Version: {:?}, Permissions: {:x?}", 367 | version_string, permissions 368 | ); 369 | 370 | assert!(metadata.is_some()); 371 | assert_eq!(version_string, Some("PDF-1.3".to_string())); 372 | assert_eq!(permissions, 0xff); 373 | } 374 | 375 | #[test] 376 | fn test3() { 377 | let mut data = vec![]; 378 | 379 | assert!(PopplerDocument::new_from_data(&mut data[..], Some("upw")).is_err()); 380 | } 381 | 382 | #[test] 383 | fn test4() { 384 | let path = "test.pdf"; 385 | let doc: PopplerDocument = PopplerDocument::new_from_file(path, None).unwrap(); 386 | let total = doc.get_n_pages(); 387 | 388 | let mut count = 0; 389 | let src_w = 595f64; 390 | let src_h = 842f64; 391 | 392 | for (index, p) in doc.pages().enumerate() { 393 | let (w, h) = p.get_size(); 394 | assert_eq!(w, src_w); 395 | assert_eq!(h, src_h); 396 | 397 | println!("page {}/{} -- {}x{}", index + 1, total, w, h); 398 | count += 1; 399 | } 400 | 401 | assert_eq!(count, 1); 402 | 403 | #[cfg(feature = "render")] 404 | let mut count = 0; 405 | 406 | #[cfg(feature = "render")] 407 | for (index, p) in doc.pages().enumerate() { 408 | let (w, h) = p.get_size(); 409 | 410 | assert_eq!(w, src_w); 411 | assert_eq!(h, src_h); 412 | 413 | let surface = ImageSurface::create(Format::ARgb32, w as i32, h as i32).expect("failed to create image surface"); 414 | let context = Context::new(&surface).expect("failed to create cairo context"); 415 | (|page: &PopplerPage, ctx: &Context| { 416 | ctx.save().unwrap(); 417 | page.render(ctx); 418 | ctx.restore().unwrap(); 419 | ctx.show_page().unwrap(); 420 | })(&p, &context); 421 | let mut f: File = File::create(format!("out{}.png", index)).unwrap(); 422 | surface.write_to_png(&mut f).expect("Unable to write PNG"); 423 | 424 | count += 1; 425 | } 426 | 427 | #[cfg(feature = "render")] 428 | assert_eq!(count, 1) 429 | } 430 | } 431 | 432 | --------------------------------------------------------------------------------