1use std::{error, f64, marker, path};
2#[cfg(any(feature = "svg", feature = "png"))]
3use std::{fs, io};
4#[cfg(feature = "svg")]
5use std::env;
6
7fn convert_err<E: error::Error + marker::Sync + marker::Send + 'static>(
9 e: E,
10) -> draw::DrawError {
11 draw::DrawError::BackendError(e.into())
12}
13
14#[derive(Debug)]
16pub struct CairoCanvas {
17 size: draw::Size,
18 context: cairo::Context,
19 image_format: draw::ImageFormat,
20 #[allow(dead_code)]
21 temp_file: Option<path::PathBuf>,
22}
23impl CairoCanvas {
24 pub fn from_context(
26 context: &cairo::Context,
27 size: draw::Size,
28 image_format: draw::ImageFormat,
29 ) -> Self {
30 Self {
31 size,
32 context: context.clone(),
33 image_format,
34 temp_file: None,
35 }
36 }
37}
38impl draw::Canvas for CairoCanvas {
39 fn new(desc: draw::CanvasDescriptor) -> Result<Self, draw::DrawError> {
40 let (context, temp_file) = match desc.image_format {
41 draw::ImageFormat::Bitmap => {
42 let surface = cairo::ImageSurface::create(
43 cairo::Format::ARgb32,
44 desc.size.width as i32,
45 desc.size.height as i32,
46 )
47 .map_err(convert_err)?;
48
49 (cairo::Context::new(&surface).map_err(convert_err)?, None)
50 },
51 draw::ImageFormat::Svg => {
52 #[cfg(feature = "svg")]
53 {
54 let mut temp_filename = env::temp_dir();
55 temp_filename.push("plt_temp.svg");
56 let temp_file = Some(temp_filename);
57
58 let surface = cairo::SvgSurface::new(
59 desc.size.width.into(),
60 desc.size.height.into(),
61 temp_file.as_ref(),
62 )
63 .map_err(|e| draw::DrawError::BackendError(e.into()))?;
64
65 (cairo::Context::new(&surface).map_err(convert_err)?, temp_file)
66 }
67
68 #[cfg(not(feature = "svg"))]
69 return Err(draw::DrawError::UnsupportedImageFormat(
70 "svg feature is not enabled".to_string()
71 ))
72 },
73 image_format => {
74 return Err(draw::DrawError::UnsupportedImageFormat(
75 format!("{:?} is not supported by the Cairo backend", image_format)
76 ))
77 }
78 };
79
80 context.set_source_rgba(
81 desc.face_color.r,
82 desc.face_color.g,
83 desc.face_color.b,
84 desc.face_color.a,
85 );
86
87 context.paint().unwrap();
88
89 Ok(Self {
90 size: desc.size,
91 context,
92 image_format: desc.image_format,
93 temp_file,
94 })
95 }
96
97 fn draw_shape(&mut self, desc: draw::ShapeDescriptor) -> Result<(), draw::DrawError> {
98 let origin = CairoPoint::from_point(desc.point, self.size);
99
100 self.context.save().map_err(convert_err)?;
101
102 if let Some(area) = desc.clip_area {
103 self.clip_area(area);
104 }
105
106 match desc.shape {
107 draw::Shape::Rectangle { h, w } => {
108 self.context.rectangle(
109 origin.x - (w as f64) / 2.0,
110 origin.y - (h as f64) / 2.0,
111 w as f64,
112 h as f64,
113 );
114 self.context.close_path();
115 },
116 draw::Shape::Square { l } => {
117 self.context.rectangle(
118 origin.x - (l as f64) / 2.0,
119 origin.y - (l as f64) / 2.0,
120 l as f64,
121 l as f64,
122 );
123 self.context.close_path();
124 },
125 draw::Shape::Circle { r } => {
126 self.context.arc(
127 origin.x,
128 origin.y,
129 r as f64,
130 0.0,
131 2.0 * f64::consts::PI,
132 );
133 self.context.close_path();
134 },
135 shape => {
136 return Err(draw::DrawError::UnsupportedShape(
137 format!("{:?} is not supported by the Cairo backend", shape)
138 ))
139 }
140 };
141
142 self.context.set_source_rgba(
144 desc.fill_color.r,
145 desc.fill_color.g,
146 desc.fill_color.b,
147 desc.fill_color.a,
148 );
149 self.context.fill_preserve().map_err(convert_err)?;
150
151 self.context.set_dash(desc.line_dashes, 0.0);
153 self.context.set_line_width(desc.line_width as f64);
154 self.context.set_source_rgba(
155 desc.line_color.r,
156 desc.line_color.g,
157 desc.line_color.b,
158 desc.line_color.a,
159 );
160 self.context.stroke().map_err(convert_err)?;
161
162 self.reset_clip();
163
164 self.context.restore().map_err(convert_err)?;
165
166 Ok(())
167 }
168
169 fn draw_line(&mut self, desc: draw::LineDescriptor) -> Result<(), draw::DrawError> {
170 let p1 = CairoPoint::from_point(desc.line.p1, self.size);
171 let p2 = CairoPoint::from_point(desc.line.p2, self.size);
172
173 self.context.save().map_err(convert_err)?;
174
175 if let Some(area) = desc.clip_area {
176 self.clip_area(area);
177 }
178
179 self.context.set_source_rgba(
180 desc.line_color.r,
181 desc.line_color.g,
182 desc.line_color.b,
183 desc.line_color.a,
184 );
185 self.context.set_line_width(desc.line_width as f64);
186
187 self.context.set_dash(desc.dashes, 0.0);
188
189 let offset = if desc.line_width % 2 == 0 { 0.0 } else { 0.5 };
190
191 self.context.line_to(p1.x + offset, p1.y - offset);
192 self.context.line_to(p2.x + offset, p2.y - offset);
193
194 self.context.stroke().map_err(convert_err)?;
195
196 self.reset_clip();
197
198 self.context.restore().map_err(convert_err)?;
199
200 Ok(())
201 }
202
203 fn draw_curve(&mut self, desc: draw::CurveDescriptor) -> Result<(), draw::DrawError> {
204 self.context.save().map_err(convert_err)?;
205
206 if let Some(area) = desc.clip_area {
207 self.clip_area(area);
208 }
209
210 self.context.set_source_rgba(
211 desc.line_color.r,
212 desc.line_color.g,
213 desc.line_color.b,
214 desc.line_color.a,
215 );
216 self.context.set_line_width(desc.line_width as f64);
217 self.context.set_line_join(cairo::LineJoin::Round);
218
219 self.context.set_dash(desc.dashes, 0.0);
220
221 let offset = if desc.line_width % 2 == 0 { 0.0 } else { 0.5 };
222
223 for point in desc.points {
224 let point = CairoPoint::from_point(point, self.size);
225
226 self.context.line_to(point.x + offset, point.y - offset);
227 }
228
229 self.context.stroke().map_err(convert_err)?;
230
231 self.reset_clip();
232
233 self.context.restore().map_err(convert_err)?;
234
235 Ok(())
236 }
237
238 fn fill_region(&mut self, desc: draw::FillDescriptor) -> Result<(), draw::DrawError> {
239 self.context.save().map_err(convert_err)?;
240
241 if let Some(area) = desc.clip_area {
242 self.clip_area(area);
243 }
244
245 self.context.set_source_rgba(
246 desc.fill_color.r,
247 desc.fill_color.g,
248 desc.fill_color.b,
249 desc.fill_color.a,
250 );
251
252 for point in desc.points {
253 let point = CairoPoint::from_point(point, self.size);
254
255 self.context.line_to(point.x, point.y);
256 }
257
258 self.context.close_path();
259
260 self.context.fill().map_err(convert_err)?;
261
262 self.reset_clip();
263
264 self.context.restore().map_err(convert_err)?;
265
266 Ok(())
267 }
268
269 fn draw_text(&mut self, desc: draw::TextDescriptor) -> Result<(), draw::DrawError> {
270 let position = CairoPoint::from_point(desc.position, self.size);
271
272 self.context.save().map_err(convert_err)?;
273
274 if let Some(area) = desc.clip_area {
275 self.clip_area(area);
276 }
277
278 self.context.set_source_rgba(
279 desc.color.r,
280 desc.color.g,
281 desc.color.b,
282 desc.color.a,
283 );
284
285 self.context.select_font_face(
286 font_to_cairo(desc.font.name),
287 font_slant_to_cairo(desc.font.slant),
288 font_weight_to_cairo(desc.font.weight),
289 );
290 self.context.set_font_size(desc.font.size as f64);
291
292 let extents = self.context.text_extents(&desc.text).map_err(convert_err)?;
293
294 let position = align_text(position, desc.rotation, extents, desc.alignment);
295 self.context.move_to(position.x, position.y);
296
297 self.context.save().map_err(convert_err)?;
298 self.context.rotate(desc.rotation);
299 self.context.show_text(&desc.text).map_err(convert_err)?;
300 self.context.restore().map_err(convert_err)?;
301
302 self.context.stroke().map_err(convert_err)?;
303
304 self.reset_clip();
305
306 self.context.restore().map_err(convert_err)?;
307
308 Ok(())
309 }
310
311 fn text_size(&mut self, desc: draw::TextDescriptor) -> Result<draw::Size, draw::DrawError> {
312 self.context.save().map_err(convert_err)?;
313
314 self.context.set_source_rgba(
315 desc.color.r,
316 desc.color.g,
317 desc.color.b,
318 desc.color.a,
319 );
320
321 self.context.select_font_face(
322 font_to_cairo(desc.font.name),
323 font_slant_to_cairo(desc.font.slant),
324 font_weight_to_cairo(desc.font.weight),
325 );
326 self.context.set_font_size(desc.font.size as f64);
327
328 let extents = self.context.text_extents(&desc.text).map_err(convert_err)?;
329
330 self.context.stroke().map_err(convert_err)?;
331
332 self.context.restore().map_err(convert_err)?;
333
334 Ok(draw::Size {
335 width: extents.width().ceil() as u32,
336 height: extents.height().ceil() as u32,
337 })
338 }
339
340 fn save_file<P: AsRef<path::Path>>(
341 &mut self,
342 desc: draw::SaveFileDescriptor<P>,
343 ) -> Result<(), draw::DrawError> {
344 match self.image_format {
345 draw::ImageFormat::Bitmap => {
346 match desc.format {
347 #[cfg(feature = "png")]
348 draw::FileFormat::Png => {
349 let mut surface = cairo::ImageSurface::try_from(
351 self.context.target()
352 )
353 .unwrap();
354 let blank_surface = cairo::ImageSurface::create(
355 cairo::Format::ARgb32,
356 0,
357 0,
358 )
359 .map_err(convert_err)?;
360 self.context = cairo::Context::new(&blank_surface).map_err(convert_err)?;
361
362 let file = fs::File::create(desc.filename)?;
363 let w = &mut io::BufWriter::new(file);
364
365 let mut encoder = png::Encoder::new(
367 w,
368 self.size.width as u32,
369 self.size.height as u32,
370 );
371 encoder.set_color(png::ColorType::Rgba);
372 encoder.set_depth(png::BitDepth::Eight);
373 let mut writer = encoder.write_header().map_err(convert_err)?;
374
375 let buffer_raw = surface.data().map_err(convert_err)?;
377 let buffer = buffer_raw.chunks(4)
379 .flat_map(|rgba| [rgba[2], rgba[1], rgba[0], rgba[3]])
380 .collect::<Vec<_>>();
381
382 let ppu = (desc.dpi as f64 * (1000.0 / 25.4)) as u32;
384 let xppu = ppu.to_be_bytes();
385 let yppu = ppu.to_be_bytes();
386 let unit = png::Unit::Meter;
387 writer.write_chunk(
388 png::chunk::pHYs,
389 &[
390 xppu[0], xppu[1], xppu[2], xppu[3],
391 yppu[0], yppu[1], yppu[2], yppu[3],
392 unit as u8,
393 ],
394 )
395 .map_err(convert_err)?;
396
397 writer.write_image_data(&buffer[..]).map_err(convert_err)?;
398
399 drop(buffer_raw);
400 drop(buffer);
401
402 self.context = cairo::Context::new(&surface).map_err(convert_err)?;
404 },
405 #[cfg(not(feature = "png"))]
406 draw::FileFormat::Png => {
407 return Err(draw::DrawError::UnsupportedFileFormat(
408 "png feature is not enabled".to_string()
409 ))
410 },
411 file_format => {
412 return Err(draw::DrawError::UnsupportedFileFormat(format!(
413 "{:?} is not supported by the Cairo backend for bitmap images",
414 file_format,
415 )))
416 },
417 }
418 },
419 draw::ImageFormat::Svg => {
420 #[cfg(feature = "svg")]
421 match desc.format {
422 draw::FileFormat::Svg => {
423 let old_surface = cairo::SvgSurface::try_from(
425 self.context.target()
426 )
427 .unwrap();
428 old_surface.finish();
429
430 if let Some(temp_file) = &self.temp_file {
431 fs::copy(temp_file, desc.filename.as_ref())?;
433
434 fs::remove_file(temp_file)?;
436 }
437 },
438 file_format => {
439 return Err(draw::DrawError::UnsupportedFileFormat(
440 format!("{:?} is not supported for svg images", file_format)
441 ))
442 },
443 }
444
445 #[cfg(not(feature = "svg"))]
446 return Err(draw::DrawError::UnsupportedFileFormat(
447 "svg feature is not enabled".to_string()
448 ))
449 },
450 image_format => {
451 return Err(draw::DrawError::UnsupportedImageFormat(
452 format!("{:?} is not supported by the Cairo backend", image_format)
453 ))
454 }
455 };
456
457 #[allow(unreachable_code)]
458 Ok(())
459 }
460 fn size(&self) -> Result<draw::Size, draw::DrawError> {
461 Ok(self.size)
462 }
463}
464impl CairoCanvas {
465 fn reset_clip(&mut self) {
466 self.context.reset_clip();
467 }
468 fn clip_area(&mut self, area: draw::Area) {
469 self.context.reset_clip();
470 self.context.new_path();
471
472 let points = [
473 draw::Point { x: area.xmin as f64, y: area.ymin as f64 },
474 draw::Point { x: area.xmin as f64, y: area.ymax as f64 },
475 draw::Point { x: area.xmax as f64, y: area.ymax as f64 },
476 draw::Point { x: area.xmax as f64, y: area.ymin as f64 },
477 ];
478
479 for point in points {
480 let point = CairoPoint::from_point(point, self.size);
481 self.context.line_to(point.x, point.y);
482 }
483
484 self.context.clip();
485 }
486}
487
488#[derive(Copy, Clone, Debug)]
491struct CairoPoint {
492 pub x: f64,
493 pub y: f64,
494}
495impl CairoPoint {
496 fn from_point(point: draw::Point, size: draw::Size) -> Self {
497 Self { x: point.x, y: (size.height as f64 - point.y) }
498 }
499}
500
501fn font_to_cairo(name: draw::FontName) -> &'static str {
502 match name {
503 draw::FontName::Arial => "Arial",
504 draw::FontName::Georgia => "Georgia",
505 _ => "Arial",
506 }
507}
508fn font_slant_to_cairo(slant: draw::FontSlant) -> cairo::FontSlant {
509 match slant {
510 draw::FontSlant::Normal => cairo::FontSlant::Normal,
511 draw::FontSlant::Italic => cairo::FontSlant::Italic,
512 draw::FontSlant::Oblique => cairo::FontSlant::Oblique,
513 }
514}
515fn font_weight_to_cairo(weight: draw::FontWeight) -> cairo::FontWeight {
516 match weight {
517 draw::FontWeight::Normal => cairo::FontWeight::Normal,
518 draw::FontWeight::Bold => cairo::FontWeight::Bold,
519 }
520}
521
522fn align_text(
523 position: CairoPoint,
524 rotation: f64,
525 extents: cairo::TextExtents,
526 alignment: draw::Alignment,
527) -> CairoPoint {
528 let (x, y) = match alignment {
529 draw::Alignment::Center => (
530 position.x - (extents.x_bearing() + extents.width() / 2.0)*rotation.cos()
531 + (extents.y_bearing() + extents.height() / 2.0)*rotation.sin(),
532 position.y - (extents.y_bearing() + extents.height() / 2.0)*rotation.cos()
533 - (extents.x_bearing() + extents.width() / 2.0)*rotation.sin(),
534 ),
535 draw::Alignment::Right => (
536 position.x - extents.x_bearing()*rotation.cos()
537 - extents.width()*rotation.cos().clamp(0.0, 1.0)
538 + extents.y_bearing()*rotation.sin().clamp(0.0, 1.0),
539 position.y - (extents.y_bearing() + (extents.height() / 2.0))*rotation.cos()
540 - (extents.x_bearing() + extents.width() / 2.0)*rotation.sin(),
541 ),
542 draw::Alignment::Left => (
543 position.x - extents.x_bearing()*rotation.cos()
544 - extents.width()*rotation.cos().clamp(-1.0, 0.0)
545 + extents.y_bearing()*rotation.sin()
546 + extents.height()*rotation.sin().clamp(0.0, 1.0),
547 position.y - (extents.y_bearing() + extents.height() / 2.0)*rotation.cos()
548 - (extents.x_bearing() + extents.width() / 2.0)*rotation.sin(),
549 ),
550 draw::Alignment::Top => (
551 position.x - (extents.x_bearing() + extents.width() / 2.0)*rotation.cos()
552 + (extents.y_bearing() + extents.height() / 2.0)*rotation.sin(),
553 position.y - extents.y_bearing()*rotation.cos()
554 - extents.x_bearing()*rotation.sin()
555 - extents.width()*rotation.sin().clamp(-1.0, 0.0)
556 - extents.height()*rotation.cos().clamp(-1.0, 0.0),
557 ),
558 draw::Alignment::Bottom => (
559 position.x - (extents.x_bearing() + extents.width() / 2.0)*rotation.cos()
560 + (extents.y_bearing() + extents.height() / 2.0)*rotation.sin(),
561 position.y - extents.y_bearing()*rotation.cos()
562 - extents.height()*rotation.cos().clamp(0.0, 1.0)
563 - extents.x_bearing()*rotation.sin()
564 - extents.width()*rotation.sin().clamp(0.0, 1.0),
565 ),
566 draw::Alignment::TopRight => (
567 position.x - extents.x_bearing()*rotation.cos()
568 - extents.width()*rotation.cos().clamp(0.0, 1.0)
569 + extents.y_bearing()*rotation.sin()
570 + extents.height()*rotation.sin().clamp(-1.0, 0.0),
571 position.y - extents.y_bearing()*rotation.cos()
572 - extents.height()*rotation.cos().clamp(-1.0, 0.0)
573 - extents.x_bearing()*rotation.sin()
574 - extents.width()*rotation.sin().clamp(-1.0, 0.0),
575 ),
576 draw::Alignment::TopLeft => (
577 position.x - extents.x_bearing()*rotation.cos()
578 - extents.width()*rotation.cos().clamp(-1.0, 0.0)
579 + extents.y_bearing()*rotation.sin()
580 + extents.height()*rotation.sin().clamp(0.0, 1.0),
581 position.y - extents.y_bearing()*rotation.cos()
582 - extents.height()*rotation.cos().clamp(-1.0, 0.0)
583 + extents.x_bearing()*rotation.sin()
584 - extents.width()*rotation.sin().clamp(-1.0, 0.0),
585 ),
586 draw::Alignment::BottomRight => (
587 position.x - extents.x_bearing()*rotation.cos()
588 - extents.width()*rotation.cos().clamp(0.0, 1.0)
589 + extents.y_bearing()*rotation.sin()
590 + extents.height()*rotation.sin().clamp(-1.0, 0.0),
591 position.y - extents.y_bearing()*rotation.cos()
592 - extents.height()*rotation.cos().clamp(0.0, 1.0)
593 + extents.x_bearing()*rotation.sin()
594 - extents.width()*rotation.sin().clamp(0.0, 1.0),
595 ),
596 draw::Alignment::BottomLeft => (
597 position.x - extents.x_bearing()*rotation.cos()
598 - extents.width()*rotation.cos().clamp(-1.0, 0.0)
599 + extents.y_bearing()*rotation.sin()
600 + extents.height()*rotation.sin().clamp(0.0, 1.0),
601 position.y - extents.y_bearing()*rotation.cos()
602 - extents.height()*rotation.cos().clamp(0.0, 1.0)
603 + extents.x_bearing()*rotation.sin()
604 - extents.width()*rotation.sin().clamp(0.0, 1.0),
605 ),
606 };
607
608 CairoPoint { x, y }
609}