cairo/
svg.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3#[cfg(not(windows))]
4use std::os::unix::prelude::*;
5use std::{
6    ffi::{CStr, CString},
7    io, mem,
8    ops::Deref,
9    path::Path,
10    ptr,
11};
12
13#[cfg(feature = "use_glib")]
14use glib::translate::*;
15
16#[cfg(all(feature = "svg", feature = "v1_16"))]
17use crate::SvgUnit;
18use crate::{ffi, Error, Surface, SurfaceType, SvgVersion};
19
20impl SvgVersion {
21    pub fn as_str(self) -> Option<&'static str> {
22        unsafe {
23            let res = ffi::cairo_svg_version_to_string(self.into());
24            res.as_ref()
25                .and_then(|cstr| CStr::from_ptr(cstr as _).to_str().ok())
26        }
27    }
28}
29
30declare_surface!(SvgSurface, SurfaceType::Svg);
31
32impl SvgSurface {
33    #[doc(alias = "cairo_svg_surface_create")]
34    pub fn new<P: AsRef<Path>>(
35        width: f64,
36        height: f64,
37        path: Option<P>,
38    ) -> Result<SvgSurface, Error> {
39        #[cfg(not(windows))]
40        let path = path.map(|p| {
41            CString::new(p.as_ref().as_os_str().as_bytes()).expect("Invalid path with NULL bytes")
42        });
43        #[cfg(windows)]
44        let path = path.map(|p| {
45            let path_str = p
46                .as_ref()
47                .to_str()
48                .expect("Path can't be represented as UTF-8")
49                .to_owned();
50            if path_str.starts_with("\\\\?\\") {
51                CString::new(path_str[4..].as_bytes())
52            } else {
53                CString::new(path_str.as_bytes())
54            }
55            .expect("Invalid path with NUL bytes")
56        });
57
58        unsafe {
59            Ok(Self(Surface::from_raw_full(
60                ffi::cairo_svg_surface_create(
61                    path.as_ref().map(|p| p.as_ptr()).unwrap_or(ptr::null()),
62                    width,
63                    height,
64                ),
65            )?))
66        }
67    }
68
69    for_stream_constructors!(cairo_svg_surface_create_for_stream);
70
71    #[doc(alias = "cairo_svg_get_versions")]
72    #[doc(alias = "get_versions")]
73    pub fn versions() -> impl Iterator<Item = SvgVersion> {
74        let vers_slice = unsafe {
75            let mut vers_ptr = ptr::null_mut();
76            let mut num_vers = mem::MaybeUninit::uninit();
77            ffi::cairo_svg_get_versions(&mut vers_ptr, num_vers.as_mut_ptr());
78
79            let num_vers = num_vers.assume_init();
80            if num_vers == 0 {
81                &[]
82            } else {
83                std::slice::from_raw_parts(vers_ptr, num_vers as _)
84            }
85        };
86
87        vers_slice.iter().map(|v| SvgVersion::from(*v))
88    }
89
90    #[doc(alias = "cairo_svg_surface_restrict_to_version")]
91    pub fn restrict(&self, version: SvgVersion) {
92        unsafe {
93            ffi::cairo_svg_surface_restrict_to_version(self.0.to_raw_none(), version.into());
94        }
95    }
96
97    #[cfg(all(feature = "svg", feature = "v1_16"))]
98    #[cfg_attr(docsrs, doc(cfg(all(feature = "svg", feature = "v1_16"))))]
99    #[doc(alias = "cairo_svg_surface_set_document_unit")]
100    pub fn set_document_unit(&mut self, unit: SvgUnit) {
101        unsafe {
102            ffi::cairo_svg_surface_set_document_unit(self.0.to_raw_none(), unit.into());
103        }
104    }
105
106    #[cfg(all(feature = "svg", feature = "v1_16"))]
107    #[cfg_attr(docsrs, doc(cfg(all(feature = "svg", feature = "v1_16"))))]
108    #[doc(alias = "cairo_svg_surface_get_document_unit")]
109    #[doc(alias = "get_document_unit")]
110    pub fn document_unit(&self) -> SvgUnit {
111        unsafe {
112            SvgUnit::from(ffi::cairo_svg_surface_get_document_unit(
113                self.0.to_raw_none(),
114            ))
115        }
116    }
117}
118
119#[cfg(test)]
120mod test {
121    use tempfile::{tempfile, NamedTempFile};
122
123    use super::*;
124    use crate::context::*;
125
126    fn draw(surface: &Surface) {
127        let cr = Context::new(surface).expect("Can't create a Cairo context");
128
129        cr.set_line_width(25.0);
130
131        cr.set_source_rgba(1.0, 0.0, 0.0, 0.5);
132        cr.line_to(0., 0.);
133        cr.line_to(100., 100.);
134        cr.stroke().expect("Surface on an invalid state");
135
136        cr.set_source_rgba(0.0, 0.0, 1.0, 0.5);
137        cr.line_to(0., 100.);
138        cr.line_to(100., 0.);
139        cr.stroke().expect("Surface on an invalid state");
140    }
141
142    fn draw_in_buffer() -> Vec<u8> {
143        let buffer: Vec<u8> = vec![];
144
145        let surface = SvgSurface::for_stream(100., 100., buffer).unwrap();
146        draw(&surface);
147        *surface.finish_output_stream().unwrap().downcast().unwrap()
148    }
149
150    #[track_caller]
151    fn assert_len_close_enough(len_a: usize, len_b: usize) {
152        // It seems cairo randomizes some element IDs which might make one svg slightly
153        // larger than the other. Here we make sure the difference is within ~10%.
154        let len_diff = usize::abs_diff(len_a, len_b);
155        assert!(len_diff < len_b / 10);
156    }
157
158    #[test]
159    fn versions() {
160        assert!(SvgSurface::versions().any(|v| v == SvgVersion::_1_1));
161    }
162
163    #[test]
164    fn version_string() {
165        let ver_str = SvgVersion::_1_1.as_str().unwrap();
166        assert_eq!(ver_str, "SVG 1.1");
167    }
168
169    #[test]
170    fn without_file() {
171        let surface = SvgSurface::new(100., 100., None::<&Path>).unwrap();
172        draw(&surface);
173        surface.finish();
174    }
175
176    #[test]
177    fn file() {
178        let file = NamedTempFile::new().expect("tempfile failed");
179        let surface = SvgSurface::new(100., 100., Some(&file.path())).unwrap();
180        draw(&surface);
181        surface.finish();
182    }
183
184    #[test]
185    fn writer() {
186        let file = tempfile().expect("tempfile failed");
187        let surface = SvgSurface::for_stream(100., 100., file).unwrap();
188
189        draw(&surface);
190        let stream = surface.finish_output_stream().unwrap();
191        let file = stream.downcast::<std::fs::File>().unwrap();
192
193        let buffer = draw_in_buffer();
194        let file_size = file.metadata().unwrap().len();
195
196        assert_len_close_enough(file_size as usize, buffer.len());
197    }
198
199    #[test]
200    fn ref_writer() {
201        let mut file = tempfile().expect("tempfile failed");
202        let surface = unsafe { SvgSurface::for_raw_stream(100., 100., &mut file).unwrap() };
203
204        draw(&surface);
205        surface.finish_output_stream().unwrap();
206    }
207
208    #[test]
209    fn buffer() {
210        let buffer = draw_in_buffer();
211
212        let header = b"<?xml";
213        assert_eq!(&buffer[..header.len()], header);
214    }
215
216    #[test]
217    fn custom_writer() {
218        use std::fs;
219
220        struct CustomWriter(usize, fs::File);
221
222        impl io::Write for CustomWriter {
223            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
224                self.1.write_all(buf)?;
225
226                self.0 += buf.len();
227                Ok(buf.len())
228            }
229
230            fn flush(&mut self) -> io::Result<()> {
231                Ok(())
232            }
233        }
234
235        let file = tempfile().expect("tempfile failed");
236        let custom_writer = CustomWriter(0, file);
237
238        let surface = SvgSurface::for_stream(100., 100., custom_writer).unwrap();
239        draw(&surface);
240        let stream = surface.finish_output_stream().unwrap();
241        let custom_writer = stream.downcast::<CustomWriter>().unwrap();
242
243        let buffer = draw_in_buffer();
244
245        assert_len_close_enough(custom_writer.0, buffer.len());
246    }
247
248    fn with_panicky_stream() -> SvgSurface {
249        struct PanicWriter;
250
251        impl io::Write for PanicWriter {
252            fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
253                panic!("panic in writer");
254            }
255            fn flush(&mut self) -> io::Result<()> {
256                Ok(())
257            }
258        }
259
260        let surface = SvgSurface::for_stream(20., 20., PanicWriter).unwrap();
261        surface.finish();
262        surface
263    }
264
265    #[test]
266    #[should_panic]
267    fn finish_stream_propagates_panic() {
268        let _ = with_panicky_stream().finish_output_stream();
269    }
270}