cairo/
pdf.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use std::{
4    ffi::{CStr, CString},
5    io, mem,
6    ops::Deref,
7    path::Path,
8    ptr,
9};
10
11#[cfg(feature = "use_glib")]
12use glib::translate::*;
13
14use crate::{ffi, Error, PdfVersion, Surface, SurfaceType};
15#[cfg(all(feature = "pdf", feature = "v1_16"))]
16use crate::{PdfMetadata, PdfOutline};
17
18impl PdfVersion {
19    pub fn as_str(self) -> Option<&'static str> {
20        unsafe {
21            let res = ffi::cairo_pdf_version_to_string(self.into());
22            res.as_ref()
23                .and_then(|cstr| CStr::from_ptr(cstr as _).to_str().ok())
24        }
25    }
26}
27
28declare_surface!(PdfSurface, SurfaceType::Pdf);
29
30impl PdfSurface {
31    #[doc(alias = "cairo_pdf_surface_create")]
32    pub fn new<P: AsRef<Path>>(width: f64, height: f64, path: P) -> Result<Self, Error> {
33        let path = path.as_ref().to_string_lossy().into_owned();
34        let path = CString::new(path).unwrap();
35
36        unsafe { Self::from_raw_full(ffi::cairo_pdf_surface_create(path.as_ptr(), width, height)) }
37    }
38
39    for_stream_constructors!(cairo_pdf_surface_create_for_stream);
40
41    #[doc(alias = "cairo_pdf_get_versions")]
42    #[doc(alias = "get_versions")]
43    pub fn versions() -> impl Iterator<Item = PdfVersion> {
44        let vers_slice = unsafe {
45            let mut vers_ptr = ptr::null_mut();
46            let mut num_vers = mem::MaybeUninit::uninit();
47            ffi::cairo_pdf_get_versions(&mut vers_ptr, num_vers.as_mut_ptr());
48
49            let num_vers = num_vers.assume_init();
50            if num_vers == 0 {
51                &[]
52            } else {
53                std::slice::from_raw_parts(vers_ptr, num_vers as _)
54            }
55        };
56        vers_slice.iter().map(|v| PdfVersion::from(*v))
57    }
58
59    #[doc(alias = "cairo_pdf_surface_restrict_to_version")]
60    pub fn restrict(&self, version: PdfVersion) -> Result<(), Error> {
61        unsafe {
62            ffi::cairo_pdf_surface_restrict_to_version(self.0.to_raw_none(), version.into());
63        }
64        self.status()
65    }
66
67    #[doc(alias = "cairo_pdf_surface_set_size")]
68    pub fn set_size(&self, width: f64, height: f64) -> Result<(), Error> {
69        unsafe {
70            ffi::cairo_pdf_surface_set_size(self.0.to_raw_none(), width, height);
71        }
72        self.status()
73    }
74
75    #[cfg(all(feature = "pdf", feature = "v1_16"))]
76    #[cfg_attr(docsrs, doc(cfg(all(feature = "pdf", feature = "v1_16"))))]
77    #[doc(alias = "cairo_pdf_surface_set_metadata")]
78    pub fn set_metadata(&self, metadata: PdfMetadata, value: &str) -> Result<(), Error> {
79        let value = CString::new(value).unwrap();
80        unsafe {
81            ffi::cairo_pdf_surface_set_metadata(
82                self.0.to_raw_none(),
83                metadata.into(),
84                value.as_ptr(),
85            );
86        }
87        self.status()
88    }
89
90    #[cfg(all(feature = "pdf", feature = "v1_18"))]
91    #[cfg_attr(docsrs, doc(cfg(all(feature = "pdf", feature = "v1_18"))))]
92    #[doc(alias = "cairo_pdf_surface_set_custom_metadata")]
93    pub fn set_custom_metadata(&self, name: &str, value: &str) -> Result<(), Error> {
94        let name = CString::new(name).unwrap();
95        let value = CString::new(value).unwrap();
96        unsafe {
97            ffi::cairo_pdf_surface_set_custom_metadata(
98                self.0.to_raw_none(),
99                name.as_ptr(),
100                value.as_ptr(),
101            );
102        }
103        self.status()
104    }
105
106    #[cfg(all(feature = "pdf", feature = "v1_16"))]
107    #[cfg_attr(docsrs, doc(cfg(all(feature = "pdf", feature = "v1_16"))))]
108    #[doc(alias = "cairo_pdf_surface_set_page_label")]
109    pub fn set_page_label(&self, label: &str) -> Result<(), Error> {
110        let label = CString::new(label).unwrap();
111        unsafe {
112            ffi::cairo_pdf_surface_set_page_label(self.0.to_raw_none(), label.as_ptr());
113        }
114        self.status()
115    }
116
117    #[cfg(all(feature = "pdf", feature = "v1_16"))]
118    #[cfg_attr(docsrs, doc(cfg(all(feature = "pdf", feature = "v1_16"))))]
119    #[doc(alias = "cairo_pdf_surface_set_thumbnail_size")]
120    pub fn set_thumbnail_size(&self, width: i32, height: i32) -> Result<(), Error> {
121        unsafe {
122            ffi::cairo_pdf_surface_set_thumbnail_size(
123                self.0.to_raw_none(),
124                width as _,
125                height as _,
126            );
127        }
128        self.status()
129    }
130
131    #[cfg(all(feature = "pdf", feature = "v1_16"))]
132    #[cfg_attr(docsrs, doc(cfg(all(feature = "pdf", feature = "v1_16"))))]
133    #[doc(alias = "cairo_pdf_surface_add_outline")]
134    pub fn add_outline(
135        &self,
136        parent_id: i32,
137        name: &str,
138        link_attribs: &str,
139        flags: PdfOutline,
140    ) -> Result<i32, Error> {
141        let name = CString::new(name).unwrap();
142        let link_attribs = CString::new(link_attribs).unwrap();
143
144        let res = unsafe {
145            ffi::cairo_pdf_surface_add_outline(
146                self.0.to_raw_none(),
147                parent_id,
148                name.as_ptr(),
149                link_attribs.as_ptr(),
150                flags.bits() as _,
151            ) as _
152        };
153
154        self.status()?;
155        Ok(res)
156    }
157}
158
159#[cfg(test)]
160mod test {
161    use tempfile::tempfile;
162
163    use super::*;
164    use crate::context::*;
165
166    fn draw(surface: &Surface) {
167        let cr = Context::new(surface).expect("Can't create a Cairo context");
168
169        cr.set_line_width(25.0);
170
171        cr.set_source_rgba(1.0, 0.0, 0.0, 0.5);
172        cr.line_to(0., 0.);
173        cr.line_to(100., 100.);
174        cr.stroke().expect("Surface on an invalid state");
175
176        cr.set_source_rgba(0.0, 0.0, 1.0, 0.5);
177        cr.line_to(0., 100.);
178        cr.line_to(100., 0.);
179        cr.stroke().expect("Surface on an invalid state");
180    }
181
182    fn draw_in_buffer() -> Vec<u8> {
183        let buffer: Vec<u8> = vec![];
184
185        let surface = PdfSurface::for_stream(100., 100., buffer).unwrap();
186        surface.restrict(PdfVersion::_1_5).unwrap();
187        draw(&surface);
188        *surface.finish_output_stream().unwrap().downcast().unwrap()
189    }
190
191    #[test]
192    fn versions() {
193        assert!(PdfSurface::versions().any(|v| v == PdfVersion::_1_4));
194    }
195
196    #[test]
197    fn version_string() {
198        let ver_str = PdfVersion::_1_4.as_str().unwrap();
199        assert_eq!(ver_str, "PDF 1.4");
200    }
201
202    #[test]
203    #[cfg(unix)]
204    fn file() {
205        let surface = PdfSurface::new(100., 100., "/dev/null").unwrap();
206        draw(&surface);
207        surface.finish();
208    }
209
210    #[test]
211    fn writer() {
212        let file = tempfile().expect("tempfile failed");
213        let surface = PdfSurface::for_stream(100., 100., file).unwrap();
214
215        draw(&surface);
216        let stream = surface.finish_output_stream().unwrap();
217        let file = stream.downcast::<std::fs::File>().unwrap();
218
219        let buffer = draw_in_buffer();
220        let file_size = file.metadata().unwrap().len();
221        assert_eq!(file_size, buffer.len() as u64);
222    }
223
224    #[test]
225    fn ref_writer() {
226        let mut file = tempfile().expect("tempfile failed");
227        let surface = unsafe { PdfSurface::for_raw_stream(100., 100., &mut file).unwrap() };
228
229        draw(&surface);
230        surface.finish_output_stream().unwrap();
231        drop(file);
232    }
233
234    #[test]
235    fn buffer() {
236        let buffer = draw_in_buffer();
237
238        let header = b"%PDF-1.5";
239        assert_eq!(&buffer[..header.len()], header);
240    }
241
242    #[test]
243    fn custom_writer() {
244        struct CustomWriter(usize);
245
246        impl io::Write for CustomWriter {
247            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
248                self.0 += buf.len();
249                Ok(buf.len())
250            }
251
252            fn flush(&mut self) -> io::Result<()> {
253                Ok(())
254            }
255        }
256
257        let custom_writer = CustomWriter(0);
258
259        let surface = PdfSurface::for_stream(20., 20., custom_writer).unwrap();
260        surface.set_size(100., 100.).unwrap();
261        draw(&surface);
262        let stream = surface.finish_output_stream().unwrap();
263        let custom_writer = stream.downcast::<CustomWriter>().unwrap();
264
265        let buffer = draw_in_buffer();
266
267        assert_eq!(custom_writer.0, buffer.len());
268    }
269
270    fn with_panicky_stream() -> PdfSurface {
271        struct PanicWriter;
272
273        impl io::Write for PanicWriter {
274            fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
275                panic!("panic in writer");
276            }
277            fn flush(&mut self) -> io::Result<()> {
278                Ok(())
279            }
280        }
281
282        let surface = PdfSurface::for_stream(20., 20., PanicWriter).unwrap();
283        surface.finish();
284        surface
285    }
286
287    #[test]
288    #[should_panic]
289    fn finish_stream_propagates_panic() {
290        let _ = with_panicky_stream().finish_output_stream();
291    }
292}