1
10
use clap::crate_version;
2
use clap_complete::{Generator, Shell};
3

            
4
use gio::prelude::*;
5
use gio::{Cancellable, FileCreateFlags, InputStream, OutputStream};
6

            
7
#[cfg(unix)]
8
use gio::{UnixInputStream, UnixOutputStream};
9

            
10
#[cfg(windows)]
11
mod windows_imports {
12
    pub use gio::{Win32InputStream, WriteOutputStream};
13
    pub use glib::translate::*;
14
}
15
#[cfg(windows)]
16
use self::windows_imports::*;
17

            
18
use cssparser::{match_ignore_ascii_case, Color};
19

            
20
use librsvg_c::{handle::PathOrUrl, sizing::LegacySize};
21
use rsvg::rsvg_convert_only::{
22
    set_source_color_on_cairo, AspectRatio, CssLength, Dpi, Horizontal, Length, Normalize,
23
    NormalizeParams, Parse, Rect, Signed, ULength, Unsigned, Validate, Vertical, ViewBox,
24
};
25
use rsvg::{AcceptLanguage, CairoRenderer, Language, LengthUnit, Loader, RenderingError};
26

            
27
use std::ffi::OsString;
28
use std::io;
29
use std::io::IsTerminal;
30
use std::ops::Deref;
31
use std::path::PathBuf;
32

            
33
#[derive(Debug)]
34
pub struct Error(String);
35

            
36
impl std::fmt::Display for Error {
37
20
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38
20
        write!(f, "{}", self.0)
39
20
    }
40
}
41

            
42
impl From<cairo::Error> for Error {
43
2
    fn from(s: cairo::Error) -> Self {
44
2
        match s {
45
2
            cairo::Error::InvalidSize => Self(String::from(
46
                "The resulting image would be larger than 32767 pixels on either dimension.\n\
47
                 Librsvg currently cannot render to images bigger than that.\n\
48
                 Please specify a smaller size.",
49
2
            )),
50
            e => Self(format!("{e}")),
51
        }
52
2
    }
53
}
54

            
55
macro_rules! impl_error_from {
56
    ($err:ty) => {
57
        impl From<$err> for Error {
58
7
            fn from(e: $err) -> Self {
59
7
                Self(format!("{e}"))
60
7
            }
61
        }
62
    };
63
}
64

            
65
impl_error_from!(RenderingError);
66
impl_error_from!(cairo::IoError);
67
impl_error_from!(cairo::StreamWithError);
68
impl_error_from!(clap::Error);
69

            
70
macro_rules! error {
71
    ($($arg:tt)*) => (Error(std::format!($($arg)*)));
72
}
73

            
74
#[derive(Clone, Copy, Debug)]
75
struct Scale {
76
    pub x: f64,
77
    pub y: f64,
78
}
79

            
80
impl Scale {
81
    #[allow(clippy::float_cmp)]
82
28
    pub fn is_identity(&self) -> bool {
83
28
        self.x == 1.0 && self.y == 1.0
84
28
    }
85
}
86

            
87
38
#[derive(Clone, Copy, Debug, PartialEq)]
88
struct Size {
89
19
    pub w: f64,
90
19
    pub h: f64,
91
}
92

            
93
impl Size {
94
257
    pub fn new(w: f64, h: f64) -> Self {
95
257
        Self { w, h }
96
257
    }
97
}
98

            
99
#[derive(Clone, Copy, Debug)]
100
enum ResizeStrategy {
101
    Scale(Scale),
102
    Fit {
103
        size: Size,
104
        keep_aspect_ratio: bool,
105
    },
106
    FitWidth(f64),
107
    FitHeight(f64),
108
    ScaleWithMaxSize {
109
        scale: Scale,
110
        max_width: Option<f64>,
111
        max_height: Option<f64>,
112
        keep_aspect_ratio: bool,
113
    },
114
}
115

            
116
impl ResizeStrategy {
117
113
    pub fn apply(self, input: &Size) -> Option<Size> {
118
113
        if input.w == 0.0 || input.h == 0.0 {
119
2
            return None;
120
        }
121

            
122
111
        let output_size = match self {
123
65
            ResizeStrategy::Scale(s) => Size::new(input.w * s.x, input.h * s.y),
124

            
125
            ResizeStrategy::Fit {
126
27
                size,
127
27
                keep_aspect_ratio,
128
            } => {
129
27
                if keep_aspect_ratio {
130
8
                    let aspect = AspectRatio::parse_str("xMinYMin meet").unwrap();
131
8
                    let rect = aspect.compute(
132
8
                        &ViewBox::from(Rect::from_size(input.w, input.h)),
133
8
                        &Rect::from_size(size.w, size.h),
134
                    );
135
8
                    Size::new(rect.width(), rect.height())
136
                } else {
137
19
                    size
138
                }
139
            }
140

            
141
2
            ResizeStrategy::FitWidth(w) => Size::new(w, input.h * w / input.w),
142

            
143
2
            ResizeStrategy::FitHeight(h) => Size::new(input.w * h / input.h, h),
144

            
145
            ResizeStrategy::ScaleWithMaxSize {
146
15
                scale,
147
15
                max_width,
148
15
                max_height,
149
15
                keep_aspect_ratio,
150
            } => {
151
15
                let scaled = Size::new(input.w * scale.x, input.h * scale.y);
152

            
153
15
                match (max_width, max_height, keep_aspect_ratio) {
154
1
                    (None, None, _) => scaled,
155

            
156
4
                    (Some(max_width), Some(max_height), false) => {
157
4
                        if scaled.w <= max_width && scaled.h <= max_height {
158
2
                            scaled
159
                        } else {
160
2
                            Size::new(max_width, max_height)
161
                        }
162
                    }
163

            
164
2
                    (Some(max_width), Some(max_height), true) => {
165
2
                        if scaled.w <= max_width && scaled.h <= max_height {
166
1
                            scaled
167
                        } else {
168
1
                            let aspect = AspectRatio::parse_str("xMinYMin meet").unwrap();
169
1
                            let rect = aspect.compute(
170
1
                                &ViewBox::from(Rect::from_size(scaled.w, scaled.h)),
171
1
                                &Rect::from_size(max_width, max_height),
172
                            );
173
1
                            Size::new(rect.width(), rect.height())
174
                        }
175
                    }
176

            
177
2
                    (Some(max_width), None, false) => {
178
2
                        if scaled.w <= max_width {
179
1
                            scaled
180
                        } else {
181
1
                            Size::new(max_width, scaled.h)
182
                        }
183
                    }
184

            
185
2
                    (Some(max_width), None, true) => {
186
2
                        if scaled.w <= max_width {
187
1
                            scaled
188
                        } else {
189
1
                            let factor = max_width / scaled.w;
190
1
                            Size::new(max_width, scaled.h * factor)
191
                        }
192
                    }
193

            
194
2
                    (None, Some(max_height), false) => {
195
2
                        if scaled.h <= max_height {
196
1
                            scaled
197
                        } else {
198
1
                            Size::new(scaled.w, max_height)
199
                        }
200
                    }
201

            
202
2
                    (None, Some(max_height), true) => {
203
2
                        if scaled.h <= max_height {
204
1
                            scaled
205
                        } else {
206
1
                            let factor = max_height / scaled.h;
207
1
                            Size::new(scaled.w * factor, max_height)
208
                        }
209
                    }
210
                }
211
            }
212
        };
213

            
214
111
        Some(output_size)
215
113
    }
216
}
217

            
218
enum Surface {
219
    Png(cairo::ImageSurface, OutputStream),
220
    #[cfg(system_deps_have_cairo_pdf)]
221
    Pdf(cairo::PdfSurface, Size),
222
    #[cfg(system_deps_have_cairo_ps)]
223
    Ps(cairo::PsSurface, Size),
224
    #[cfg(system_deps_have_cairo_svg)]
225
    Svg(cairo::SvgSurface),
226
}
227

            
228
impl Deref for Surface {
229
    type Target = cairo::Surface;
230

            
231
111
    fn deref(&self) -> &cairo::Surface {
232
166
        match self {
233
55
            Self::Png(surface, _) => surface,
234
            #[cfg(system_deps_have_cairo_pdf)]
235
76
            Self::Pdf(surface, _) => surface,
236
            #[cfg(system_deps_have_cairo_ps)]
237
20
            Self::Ps(surface, _) => surface,
238
            #[cfg(system_deps_have_cairo_svg)]
239
16
            Self::Svg(surface) => surface,
240
        }
241
111
    }
242
}
243

            
244
impl AsRef<cairo::Surface> for Surface {
245
87
    fn as_ref(&self) -> &cairo::Surface {
246
87
        self
247
87
    }
248
}
249

            
250
impl Surface {
251
84
    pub fn new(
252
        format: Format,
253
        size: Size,
254
        stream: OutputStream,
255
        unit: LengthUnit,
256
    ) -> Result<Self, Error> {
257
84
        match format {
258
57
            Format::Png => Self::new_for_png(size, stream),
259
15
            Format::Pdf => Self::new_for_pdf(size, stream, None),
260
1
            Format::Pdf1_7 => Self::new_for_pdf(size, stream, Some(cairo::PdfVersion::_1_7)),
261
1
            Format::Pdf1_6 => Self::new_for_pdf(size, stream, Some(cairo::PdfVersion::_1_6)),
262
1
            Format::Pdf1_5 => Self::new_for_pdf(size, stream, Some(cairo::PdfVersion::_1_5)),
263
1
            Format::Pdf1_4 => Self::new_for_pdf(size, stream, Some(cairo::PdfVersion::_1_4)),
264
2
            Format::Ps => Self::new_for_ps(size, stream, false),
265
2
            Format::Eps => Self::new_for_ps(size, stream, true),
266
4
            Format::Svg => Self::new_for_svg(size, stream, unit),
267
        }
268
84
    }
269

            
270
57
    fn new_for_png(size: Size, stream: OutputStream) -> Result<Self, Error> {
271
        // We use ceil() to avoid chopping off the last pixel if it is partially covered.
272
57
        let w = checked_i32(size.w.ceil())?;
273
56
        let h = checked_i32(size.h.ceil())?;
274
56
        let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w, h)?;
275
55
        Ok(Self::Png(surface, stream))
276
57
    }
277

            
278
    #[cfg(system_deps_have_cairo_pdf)]
279
19
    fn new_for_pdf(
280
        size: Size,
281
        stream: OutputStream,
282
        version: Option<cairo::PdfVersion>,
283
    ) -> Result<Self, Error> {
284
19
        let surface = cairo::PdfSurface::for_stream(size.w, size.h, stream.into_write())?;
285
19
        if let Some(ver) = version {
286
4
            surface.restrict(ver)?;
287
        }
288
19
        if let Some(date) = metadata::creation_date()? {
289
20
            surface.set_metadata(cairo::PdfMetadata::CreateDate, &date)?;
290
19
        }
291
16
        Ok(Self::Pdf(surface, size))
292
19
    }
293

            
294
    #[cfg(not(system_deps_have_cairo_pdf))]
295
    fn new_for_pdf(_size: Size, _stream: OutputStream) -> Result<Self, Error> {
296
        Err(Error("unsupported format".to_string()))
297
    }
298

            
299
    #[cfg(system_deps_have_cairo_ps)]
300
4
    fn new_for_ps(size: Size, stream: OutputStream, eps: bool) -> Result<Self, Error> {
301
4
        let surface = cairo::PsSurface::for_stream(size.w, size.h, stream.into_write())?;
302
4
        surface.set_eps(eps);
303
4
        Ok(Self::Ps(surface, size))
304
4
    }
305

            
306
    #[cfg(not(system_deps_have_cairo_ps))]
307
    fn new_for_ps(_size: Size, _stream: OutputStream, _eps: bool) -> Result<Self, Error> {
308
        Err(Error("unsupported format".to_string()))
309
    }
310

            
311
    #[cfg(system_deps_have_cairo_svg)]
312
4
    fn new_for_svg(size: Size, stream: OutputStream, unit: LengthUnit) -> Result<Self, Error> {
313
4
        let mut surface = cairo::SvgSurface::for_stream(size.w, size.h, stream.into_write())?;
314

            
315
4
        let svg_unit = match unit {
316
1
            LengthUnit::Cm => cairo::SvgUnit::Cm,
317
            LengthUnit::In => cairo::SvgUnit::In,
318
1
            LengthUnit::Mm => cairo::SvgUnit::Mm,
319
            LengthUnit::Pc => cairo::SvgUnit::Pc,
320
            LengthUnit::Pt => cairo::SvgUnit::Pt,
321
2
            _ => cairo::SvgUnit::User,
322
        };
323

            
324
4
        surface.set_document_unit(svg_unit);
325
4
        Ok(Self::Svg(surface))
326
4
    }
327

            
328
    #[cfg(not(system_deps_have_cairo_svg))]
329
    fn new_for_svg(_size: Size, _stream: OutputStream, u: LengthUnit) -> Result<Self, Error> {
330
        Err(Error("unsupported format".to_string()))
331
    }
332

            
333
    #[allow(clippy::too_many_arguments)] // yeah, yeah, we'll refactor it eventually
334
87
    pub fn render(
335
        &self,
336
        renderer: &CairoRenderer,
337
        left: f64,
338
        top: f64,
339
        final_size: Size,
340
        geometry: cairo::Rectangle,
341
        background_color: Option<Color>,
342
        id: Option<&str>,
343
    ) -> Result<(), Error> {
344
87
        let cr = cairo::Context::new(self)?;
345

            
346
87
        if let Some(color) = background_color {
347
9
            set_source_color_on_cairo(&cr, &color);
348
9
            cr.paint()?;
349
        }
350

            
351
87
        cr.translate(left, top);
352

            
353
        // Note that we don't scale the viewport; we change the cr's transform instead.  This
354
        // is because SVGs are rendered proportionally to fit within the viewport, regardless
355
        // of the viewport's proportions.  Rsvg-convert allows non-proportional scaling, so
356
        // we do that with a separate transform.
357

            
358
87
        let scale = Scale {
359
87
            x: final_size.w / geometry.width(),
360
87
            y: final_size.h / geometry.height(),
361
        };
362

            
363
87
        cr.scale(scale.x, scale.y);
364

            
365
87
        let viewport = cairo::Rectangle::new(0.0, 0.0, geometry.width(), geometry.height());
366

            
367
87
        match id {
368
83
            None => renderer.render_document(&cr, &viewport)?,
369
4
            Some(_) => renderer.render_element(&cr, id, &viewport)?,
370
        }
371

            
372
87
        if !matches!(self, Self::Png(_, _)) {
373
119
            cr.show_page()?;
374
        }
375

            
376
87
        Ok(())
377
87
    }
378

            
379
79
    pub fn finish(self) -> Result<(), Error> {
380
79
        match self {
381
55
            Self::Png(surface, stream) => surface.write_to_png(&mut stream.into_write())?,
382
48
            _ => self.finish_output_stream().map(|_| ())?,
383
        }
384

            
385
79
        Ok(())
386
79
    }
387
}
388

            
389
113
fn checked_i32(x: f64) -> Result<i32, cairo::Error> {
390
114
    cast::i32(x).map_err(|_| cairo::Error::InvalidSize)
391
113
}
392

            
393
mod metadata {
394
    use super::Error;
395
    use chrono::prelude::*;
396
    use std::env;
397
    use std::str::FromStr;
398

            
399
19
    pub fn creation_date() -> Result<Option<String>, Error> {
400
19
        match env::var("SOURCE_DATE_EPOCH") {
401
4
            Ok(epoch) => match i64::from_str(&epoch) {
402
1
                Ok(seconds) => {
403
1
                    let datetime = Utc.timestamp_opt(seconds, 0).unwrap();
404
1
                    Ok(Some(datetime.to_rfc3339()))
405
1
                }
406
3
                Err(e) => Err(error!("Environment variable $SOURCE_DATE_EPOCH: {}", e)),
407
4
            },
408
15
            Err(env::VarError::NotPresent) => Ok(None),
409
            Err(env::VarError::NotUnicode(_)) => Err(error!(
410
                "Environment variable $SOURCE_DATE_EPOCH is not valid Unicode"
411
            )),
412
        }
413
19
    }
414
}
415

            
416
struct Stdin;
417

            
418
impl Stdin {
419
73
    fn is_terminal(&self) -> bool {
420
73
        io::stdin().is_terminal()
421
73
    }
422

            
423
    #[cfg(unix)]
424
73
    pub fn as_gio_input_stream(&self) -> InputStream {
425
        use std::os::fd::AsRawFd;
426

            
427
73
        let raw_fd = io::stdin().as_raw_fd();
428
73
        let stream = unsafe { UnixInputStream::with_fd(raw_fd) };
429
73
        stream.upcast::<InputStream>()
430
73
    }
431

            
432
    #[cfg(windows)]
433
    pub fn as_gio_input_stream(&self) -> InputStream {
434
        let stream = unsafe { Win32InputStream::with_handle(io::stdin()) };
435
        stream.upcast::<InputStream>()
436
    }
437
}
438

            
439
struct Stdout;
440

            
441
impl Stdout {
442
    #[cfg(unix)]
443
80
    pub fn stream() -> OutputStream {
444
80
        let stream = unsafe { UnixOutputStream::with_fd(1) };
445
80
        stream.upcast::<OutputStream>()
446
80
    }
447

            
448
    #[cfg(windows)]
449
    pub fn stream() -> OutputStream {
450
        // Ideally, we could use a Win32OutputStream, but when it's used with a file redirect,
451
        // it gets buggy.
452
        // https://gitlab.gnome.org/GNOME/librsvg/-/issues/812
453
        let stream = WriteOutputStream::new(io::stdout());
454
        stream.upcast::<OutputStream>()
455
    }
456
}
457

            
458
#[derive(Clone, Debug)]
459
enum Input {
460
    Stdin,
461
24
    Named(PathOrUrl),
462
}
463

            
464
impl std::fmt::Display for Input {
465
3
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466
3
        match self {
467
3
            Input::Stdin => "stdin".fmt(f),
468
            Input::Named(p) => p.fmt(f),
469
        }
470
3
    }
471
}
472

            
473
#[derive(Clone, Debug)]
474
enum Output {
475
    Stdout,
476
    Path(PathBuf),
477
}
478

            
479
impl std::fmt::Display for Output {
480
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481
        match self {
482
            Output::Stdout => "stdout".fmt(f),
483
            Output::Path(p) => p.display().fmt(f),
484
        }
485
    }
486
}
487

            
488
// Keep this enum in sync with supported_formats in parse_args()
489
#[derive(Clone, Copy, Debug)]
490
enum Format {
491
    Png,
492
    Pdf,
493
    Pdf1_7,
494
    Pdf1_6,
495
    Pdf1_5,
496
    Pdf1_4,
497
    Ps,
498
    Eps,
499
    Svg,
500
}
501

            
502
struct Converter {
503
    pub dpi_x: Resolution,
504
    pub dpi_y: Resolution,
505
    pub zoom: Scale,
506
    pub width: Option<ULength<Horizontal>>,
507
    pub height: Option<ULength<Vertical>>,
508
    pub left: Option<Length<Horizontal>>,
509
    pub top: Option<Length<Vertical>>,
510
    pub page_size: Option<(ULength<Horizontal>, ULength<Vertical>)>,
511
    pub format: Format,
512
    pub export_id: Option<String>,
513
    pub keep_aspect_ratio: bool,
514
    pub background_color: Option<Color>,
515
    pub stylesheet: Option<PathBuf>,
516
    pub language: Language,
517
    pub unlimited: bool,
518
    pub keep_image_data: bool,
519
    pub input: Vec<Input>,
520
    pub output: Output,
521
    pub testing: bool,
522
}
523

            
524
impl Converter {
525
88
    pub fn convert(self) -> Result<(), Error> {
526
88
        let stylesheet = match self.stylesheet {
527
3
            Some(ref p) => std::fs::read_to_string(p)
528
                .map(Some)
529
4
                .map_err(|e| error!("Error reading stylesheet: {}", e))?,
530
85
            None => None,
531
        };
532

            
533
87
        let mut surface: Option<Surface> = None;
534

            
535
        // Use user units per default
536
87
        let mut unit = LengthUnit::Px;
537

            
538
26
        fn set_unit<N: Normalize, V: Validate>(
539
            l: CssLength<N, V>,
540
            p: &NormalizeParams,
541
            u: LengthUnit,
542
        ) -> f64 {
543
26
            match u {
544
6
                LengthUnit::Pt => l.to_points(p),
545
                LengthUnit::In => l.to_inches(p),
546
4
                LengthUnit::Cm => l.to_cm(p),
547
8
                LengthUnit::Mm => l.to_mm(p),
548
                LengthUnit::Pc => l.to_picas(p),
549
8
                _ => l.to_user(p),
550
            }
551
26
        }
552

            
553
        let stdin = Stdin;
554

            
555
87
        for (page_idx, input) in self.input.iter().enumerate() {
556
190
            let (stream, basefile) = match input {
557
                Input::Stdin => {
558
73
                    if stdin.is_terminal() {
559
                        eprintln!("rsvg-convert is reading from standard input.");
560
                        eprintln!("Type Control-C to exit if this is not what you expected.");
561
                    }
562

            
563
73
                    (stdin.as_gio_input_stream(), None)
564
                }
565

            
566
22
                Input::Named(p) => {
567
22
                    let file = p.get_gfile();
568
22
                    let stream = file
569
22
                        .read(None::<&Cancellable>)
570
22
                        .map_err(|e| error!("Error reading file \"{}\": {}", input, e))?;
571
22
                    (stream.upcast::<InputStream>(), Some(file))
572
22
                }
573
            };
574

            
575
190
            let mut handle = Loader::new()
576
95
                .with_unlimited_size(self.unlimited)
577
95
                .keep_image_data(self.keep_image_data)
578
95
                .read_stream(&stream, basefile.as_ref(), None::<&Cancellable>)
579
97
                .map_err(|e| error!("Error reading SVG {}: {}", input, e))?;
580

            
581
94
            if let Some(ref css) = stylesheet {
582
2
                handle
583
2
                    .set_stylesheet(css)
584
                    .map_err(|e| error!("Error applying stylesheet: {}", e))?;
585
            }
586

            
587
94
            let renderer = CairoRenderer::new(&handle)
588
94
                .with_dpi(self.dpi_x.0, self.dpi_y.0)
589
94
                .with_language(&self.language)
590
94
                .test_mode(self.testing);
591

            
592
94
            let geometry = natural_geometry(&renderer, input, self.export_id.as_deref())?;
593

            
594
93
            let natural_size = Size::new(geometry.width(), geometry.height());
595

            
596
93
            let params = NormalizeParams::from_dpi(Dpi::new(self.dpi_x.0, self.dpi_y.0));
597

            
598
            // Convert natural size and requested size to pixels or points, depending on the target format,
599
186
            let (natural_size, requested_width, requested_height, page_size) = match self.format {
600
                Format::Png => {
601
                    // PNG surface requires units in pixels
602
58
                    (
603
                        natural_size,
604
74
                        self.width.map(|l| l.to_user(&params)),
605
74
                        self.height.map(|l| l.to_user(&params)),
606
62
                        self.page_size.map(|(w, h)| Size {
607
2
                            w: w.to_user(&params),
608
2
                            h: h.to_user(&params),
609
2
                        }),
610
58
                    )
611
                }
612

            
613
                Format::Pdf
614
                | Format::Pdf1_7
615
                | Format::Pdf1_6
616
                | Format::Pdf1_5
617
                | Format::Pdf1_4
618
                | Format::Ps
619
                | Format::Eps => {
620
                    // These surfaces require units in points
621
31
                    unit = LengthUnit::Pt;
622

            
623
31
                    (
624
31
                        Size {
625
31
                            w: ULength::<Horizontal>::new(natural_size.w, LengthUnit::Px)
626
                                .to_points(&params),
627
31
                            h: ULength::<Vertical>::new(natural_size.h, LengthUnit::Px)
628
                                .to_points(&params),
629
                        },
630
39
                        self.width.map(|l| l.to_points(&params)),
631
39
                        self.height.map(|l| l.to_points(&params)),
632
39
                        self.page_size.map(|(w, h)| Size {
633
4
                            w: w.to_points(&params),
634
4
                            h: h.to_points(&params),
635
4
                        }),
636
                    )
637
31
                }
638

            
639
                Format::Svg => {
640
4
                    let (w_unit, h_unit) =
641
10
                        (self.width.map(|l| l.unit), self.height.map(|l| l.unit));
642

            
643
8
                    unit = match (w_unit, h_unit) {
644
1
                        (None, None) => LengthUnit::Px,
645
                        (None, u) | (u, None) => u.unwrap(),
646
3
                        (u1, u2) => {
647
3
                            if u1 == u2 {
648
2
                                u1.unwrap()
649
                            } else {
650
1
                                LengthUnit::Px
651
                            }
652
                        }
653
                    };
654

            
655
                    // Supported SVG units are px, in, cm, mm, pt, pc, ch
656
4
                    (
657
4
                        Size {
658
4
                            w: set_unit(
659
4
                                ULength::<Horizontal>::new(natural_size.w, LengthUnit::Px),
660
                                &params,
661
4
                                unit,
662
                            ),
663
4
                            h: set_unit(
664
4
                                ULength::<Vertical>::new(natural_size.h, LengthUnit::Px),
665
                                &params,
666
4
                                unit,
667
                            ),
668
                        },
669
7
                        self.width.map(|l| set_unit(l, &params, unit)),
670
7
                        self.height.map(|l| set_unit(l, &params, unit)),
671
6
                        self.page_size.map(|(w, h)| Size {
672
1
                            w: set_unit(w, &params, unit),
673
1
                            h: set_unit(h, &params, unit),
674
1
                        }),
675
                    )
676
4
                }
677
            };
678

            
679
93
            let strategy = match (requested_width, requested_height) {
680
                // when w and h are not specified, scale to the requested zoom (if any)
681
65
                (None, None) => ResizeStrategy::Scale(self.zoom),
682

            
683
                // when w and h are specified, but zoom is not, scale to the requested size
684
26
                (Some(width), Some(height)) if self.zoom.is_identity() => ResizeStrategy::Fit {
685
24
                    size: Size::new(width, height),
686
24
                    keep_aspect_ratio: self.keep_aspect_ratio,
687
24
                },
688

            
689
                // if only one between w and h is specified and there is no zoom, scale to the
690
                // requested w or h and use the same scaling factor for the other
691
1
                (Some(w), None) if self.zoom.is_identity() => ResizeStrategy::FitWidth(w),
692
1
                (None, Some(h)) if self.zoom.is_identity() => ResizeStrategy::FitHeight(h),
693

            
694
                // otherwise scale the image, but cap the zoom to match the requested size
695
2
                _ => ResizeStrategy::ScaleWithMaxSize {
696
2
                    scale: self.zoom,
697
                    max_width: requested_width,
698
                    max_height: requested_height,
699
2
                    keep_aspect_ratio: self.keep_aspect_ratio,
700
2
                },
701
            };
702

            
703
93
            let final_size = self.final_size(&strategy, &natural_size, input)?;
704

            
705
            // Create the surface once on the first input,
706
            // except for PDF, PS, and EPS, which allow differently-sized pages.
707
92
            let page_size = page_size.unwrap_or(final_size);
708
92
            let s = match &mut surface {
709
8
                Some(s) => {
710
8
                    match s {
711
                        #[cfg(system_deps_have_cairo_pdf)]
712
6
                        Surface::Pdf(pdf, size) => {
713
6
                            pdf.set_size(page_size.w, page_size.h).map_err(|e| {
714
                                error!(
715
                                    "Error setting PDF page #{} size {}: {}",
716
                                    page_idx + 1,
717
                                    input,
718
                                    e
719
                                )
720
                            })?;
721
6
                            *size = page_size;
722
6
                        }
723
                        #[cfg(system_deps_have_cairo_ps)]
724
2
                        Surface::Ps(ps, size) => {
725
2
                            ps.set_size(page_size.w, page_size.h);
726
2
                            *size = page_size;
727
2
                        }
728
                        _ => {}
729
                    }
730
8
                    s
731
8
                }
732
84
                surface @ None => surface.insert(self.create_surface(page_size, unit)?),
733
            };
734

            
735
92
            let left = self.left.map(|l| set_unit(l, &params, unit)).unwrap_or(0.0);
736
92
            let top = self.top.map(|l| set_unit(l, &params, unit)).unwrap_or(0.0);
737

            
738
87
            s.render(
739
                &renderer,
740
                left,
741
                top,
742
                final_size,
743
87
                geometry,
744
87
                self.background_color,
745
87
                self.export_id.as_deref(),
746
            )
747
87
            .map_err(|e| error!("Error rendering SVG {}: {}", input, e))?
748
95
        }
749

            
750
79
        if let Some(s) = surface.take() {
751
79
            s.finish()
752
79
                .map_err(|e| error!("Error saving output {}: {}", self.output, e))?
753
79
        };
754

            
755
79
        Ok(())
756
246
    }
757

            
758
93
    fn final_size(
759
        &self,
760
        strategy: &ResizeStrategy,
761
        natural_size: &Size,
762
        input: &Input,
763
    ) -> Result<Size, Error> {
764
186
        strategy
765
            .apply(natural_size)
766
94
            .ok_or_else(|| error!("The SVG {} has no dimensions", input))
767
93
    }
768

            
769
84
    fn create_surface(&self, size: Size, unit: LengthUnit) -> Result<Surface, Error> {
770
84
        let output_stream = match self.output {
771
80
            Output::Stdout => Stdout::stream(),
772
4
            Output::Path(ref p) => {
773
4
                let file = gio::File::for_path(p);
774
4
                let stream = file
775
4
                    .replace(None, false, FileCreateFlags::NONE, None::<&Cancellable>)
776
4
                    .map_err(|e| error!("Error opening output \"{}\": {}", self.output, e))?;
777
4
                stream.upcast::<OutputStream>()
778
4
            }
779
        };
780

            
781
84
        Surface::new(self.format, size, output_stream, unit)
782
84
    }
783
}
784

            
785
94
fn natural_geometry(
786
    renderer: &CairoRenderer,
787
    input: &Input,
788
    export_id: Option<&str>,
789
) -> Result<cairo::Rectangle, Error> {
790
282
    match export_id {
791
89
        None => renderer.legacy_layer_geometry(None),
792
5
        Some(id) => renderer.geometry_for_element(Some(id)),
793
    }
794
93
    .map(|(ink_r, _)| ink_r)
795
95
    .map_err(|e| match e {
796
1
        RenderingError::IdNotFound => error!(
797
            "File {} does not have an object with id \"{}\")",
798
            input,
799
1
            export_id.unwrap()
800
        ),
801
        _ => error!("Error rendering SVG {}: {}", input, e),
802
1
    })
803
94
}
804

            
805
109
fn build_cli() -> clap::Command {
806
109
    let supported_formats = vec![
807
        "png",
808
        #[cfg(system_deps_have_cairo_pdf)]
809
        "pdf",
810
        #[cfg(system_deps_have_cairo_pdf)]
811
        "pdf1.7",
812
        #[cfg(system_deps_have_cairo_pdf)]
813
        "pdf1.6",
814
        #[cfg(system_deps_have_cairo_pdf)]
815
        "pdf1.5",
816
        #[cfg(system_deps_have_cairo_pdf)]
817
        "pdf1.4",
818
        #[cfg(system_deps_have_cairo_ps)]
819
        "ps",
820
        #[cfg(system_deps_have_cairo_ps)]
821
        "eps",
822
        #[cfg(system_deps_have_cairo_svg)]
823
        "svg",
824
    ];
825

            
826
2943
    clap::Command::new("rsvg-convert")
827
        .version(concat!("version ", crate_version!()))
828
        .about("Convert SVG files to other image formats")
829
        .disable_version_flag(true)
830
109
        .disable_help_flag(true)
831
        .arg(
832
109
            clap::Arg::new("help")
833
                .short('?')
834
                .long("help")
835
                .help("Display the help")
836
109
                .action(clap::ArgAction::Help)
837
109
        )
838
        .arg(
839
109
            clap::Arg::new("version")
840
                .short('v')
841
                .long("version")
842
                .help("Display the version information")
843
109
                .action(clap::ArgAction::Version)
844
109
        )
845
        .arg(
846
109
            clap::Arg::new("res_x")
847
                .short('d')
848
                .long("dpi-x")
849
                .num_args(1)
850
                .value_name("number")
851
                .default_value("96")
852
                .value_parser(parse_resolution)
853
                .help("Pixels per inch")
854
109
                .action(clap::ArgAction::Set),
855
109
        )
856
        .arg(
857
109
            clap::Arg::new("res_y")
858
                .short('p')
859
                .long("dpi-y")
860
                .num_args(1)
861
                .value_name("number")
862
                .default_value("96")
863
                .value_parser(parse_resolution)
864
                .help("Pixels per inch")
865
109
                .action(clap::ArgAction::Set),
866
109
        )
867
        .arg(
868
109
            clap::Arg::new("zoom_x")
869
                .short('x')
870
                .long("x-zoom")
871
                .num_args(1)
872
                .value_name("number")
873
                .conflicts_with("zoom")
874
                .value_parser(parse_zoom_factor)
875
                .help("Horizontal zoom factor")
876
109
                .action(clap::ArgAction::Set),
877
109
        )
878
        .arg(
879
109
            clap::Arg::new("zoom_y")
880
                .short('y')
881
                .long("y-zoom")
882
                .num_args(1)
883
                .value_name("number")
884
                .conflicts_with("zoom")
885
                .value_parser(parse_zoom_factor)
886
                .help("Vertical zoom factor")
887
109
                .action(clap::ArgAction::Set),
888
109
        )
889
        .arg(
890
109
            clap::Arg::new("zoom")
891
                .short('z')
892
                .long("zoom")
893
                .num_args(1)
894
                .value_name("number")
895
                .value_parser(parse_zoom_factor)
896
                .help("Zoom factor")
897
109
                .action(clap::ArgAction::Set),
898
109
        )
899
        .arg(
900
109
            clap::Arg::new("size_x")
901
                .short('w')
902
                .long("width")
903
                .num_args(1)
904
                .value_name("length")
905
                .value_parser(parse_length::<Horizontal, Unsigned>)
906
                .help("Width [defaults to the width of the SVG]")
907
109
                .action(clap::ArgAction::Set),
908
109
        )
909
        .arg(
910
109
            clap::Arg::new("size_y")
911
                .short('h')
912
                .long("height")
913
                .num_args(1)
914
                .value_name("length")
915
                .value_parser(parse_length::<Vertical, Unsigned>)
916
                .help("Height [defaults to the height of the SVG]")
917
109
                .action(clap::ArgAction::Set),
918
109
        )
919
        .arg(
920
109
            clap::Arg::new("top")
921
                .long("top")
922
                .num_args(1)
923
                .value_name("length")
924
                .value_parser(parse_length::<Vertical, Signed>)
925
                .help("Distance between top edge of page and the image [defaults to 0]")
926
109
                .action(clap::ArgAction::Set),
927
109
        )
928
        .arg(
929
109
            clap::Arg::new("left")
930
                .long("left")
931
                .num_args(1)
932
                .value_name("length")
933
                .value_parser(parse_length::<Horizontal, Signed>)
934
                .help("Distance between left edge of page and the image [defaults to 0]")
935
109
                .action(clap::ArgAction::Set),
936
109
        )
937
        .arg(
938
109
            clap::Arg::new("page_width")
939
                .long("page-width")
940
                .num_args(1)
941
                .value_name("length")
942
                .value_parser(parse_length::<Horizontal, Unsigned>)
943
                .help("Width of output media [defaults to the width of the SVG]")
944
109
                .action(clap::ArgAction::Set),
945
109
        )
946
        .arg(
947
109
            clap::Arg::new("page_height")
948
                .long("page-height")
949
                .num_args(1)
950
                .value_name("length")
951
                .value_parser(parse_length::<Vertical, Unsigned>)
952
                .help("Height of output media [defaults to the height of the SVG]")
953
109
                .action(clap::ArgAction::Set),
954
109
        )
955
        .arg(
956
218
            clap::Arg::new("format")
957
                .short('f')
958
                .long("format")
959
109
                .num_args(1)
960
218
                .value_parser(clap::builder::PossibleValuesParser::new(supported_formats.as_slice()))
961
                .ignore_case(true)
962
                .default_value("png")
963
                .help("Output format")
964
109
                .action(clap::ArgAction::Set),
965
109
        )
966
        .arg(
967
218
            clap::Arg::new("output")
968
                .short('o')
969
                .long("output")
970
109
                .num_args(1)
971
218
                .value_parser(clap::value_parser!(PathBuf))
972
                .help("Output filename [defaults to stdout]")
973
109
                .action(clap::ArgAction::Set),
974
109
        )
975
        .arg(
976
218
            clap::Arg::new("export_id")
977
                .short('i')
978
109
                .long("export-id")
979
218
                .value_parser(clap::builder::NonEmptyStringValueParser::new())
980
                .value_name("object id")
981
                .help("SVG id of object to export [default is to export all objects]")
982
109
                .action(clap::ArgAction::Set),
983
109
        )
984
        .arg(
985
218
            clap::Arg::new("accept-language")
986
                .short('l')
987
109
                .long("accept-language")
988
218
                .value_parser(clap::builder::NonEmptyStringValueParser::new())
989
                .value_name("languages")
990
                .help("Languages to accept, for example \"es-MX,de,en\" [default uses language from the environment]")
991
109
                .action(clap::ArgAction::Set),
992
109
        )
993
        .arg(
994
109
            clap::Arg::new("keep_aspect")
995
                .short('a')
996
                .long("keep-aspect-ratio")
997
                .help("Preserve the aspect ratio")
998
109
                .action(clap::ArgAction::SetTrue),
999
109
        )
        .arg(
218
            clap::Arg::new("background")
                .short('b')
                .long("background-color")
                .num_args(1)
109
                .value_name("color")
218
                .value_parser(clap::builder::NonEmptyStringValueParser::new())
                .default_value("none")
                .help("Set the background color using a CSS color spec")
109
                .action(clap::ArgAction::Set),
109
        )
        .arg(
218
            clap::Arg::new("stylesheet")
                .short('s')
                .long("stylesheet")
109
            .num_args(1)
218
                .value_parser(clap::value_parser!(PathBuf))
                .value_name("filename.css")
                .help("Filename of CSS stylesheet to apply")
109
                .action(clap::ArgAction::Set),
109
        )
        .arg(
109
            clap::Arg::new("unlimited")
                .short('u')
                .long("unlimited")
                .help("Allow huge SVG files")
109
                .action(clap::ArgAction::SetTrue),
109
        )
        .arg(
109
            clap::Arg::new("keep_image_data")
                .long("keep-image-data")
                .help("Keep image data")
                .conflicts_with("no_keep_image_data")
109
                .action(clap::ArgAction::SetTrue),
109
        )
        .arg(
109
            clap::Arg::new("no_keep_image_data")
                .long("no-keep-image-data")
                .help("Do not keep image data")
                .conflicts_with("keep_image_data")
109
                .action(clap::ArgAction::SetTrue),
109
        )
        .arg(
109
            clap::Arg::new("testing")
                .long("testing")
                .help("Render images for librsvg's test suite")
                .hide(true)
109
                .action(clap::ArgAction::SetTrue),
109
        )
        .arg(
218
            clap::Arg::new("completion")
                .long("completion")
                .help("Output shell completion for the given shell")
                .num_args(1)
109
                .action(clap::ArgAction::Set)
218
                .value_parser(clap::value_parser!(Shell)),
109
        )
        .arg(
218
            clap::Arg::new("FILE")
218
                .value_parser(clap::value_parser!(OsString))
                .help("The input file(s) to convert, you can use - for stdin")
109
                .num_args(1..)
109
                .action(clap::ArgAction::Append),
109
        )
109
}
fn print_completions<G: Generator>(gen: G, cmd: &mut clap::Command) {
    clap_complete::generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout());
}
109
fn parse_args() -> Result<Converter, Error> {
109
    let cli = build_cli();
109
    let matches = cli.get_matches();
109
    if let Some(shell) = matches.get_one::<Shell>("completion").copied() {
        let mut cmd = build_cli();
        eprintln!("Generating completion file for {shell}");
        print_completions(shell, &mut cmd);
        std::process::exit(0);
    }
99
    let format_str: &String = matches
        .get_one("format")
        .expect("already provided default_value");
99
    let format = match_ignore_ascii_case! {
        format_str,
        "png" => Format::Png,
        "pdf" => Format::Pdf,
        "pdf1.7" => Format::Pdf1_7,
        "pdf1.6" => Format::Pdf1_6,
        "pdf1.5" => Format::Pdf1_5,
        "pdf1.4" => Format::Pdf1_4,
        "ps" => Format::Ps,
        "eps" => Format::Eps,
        "svg" => Format::Svg,
        _ => unreachable!("clap should already have the list of possible values"),
    };
99
    let keep_image_data = match format {
21
        Format::Ps | Format::Eps | Format::Pdf => !matches.get_flag("no_keep_image_data"),
78
        _ => matches.get_flag("keep_image_data"),
    };
99
    let language = match matches.get_one::<String>("accept-language") {
95
        None => Language::FromEnvironment,
4
        Some(s) => AcceptLanguage::parse(s)
            .map(Language::AcceptLanguage)
5
            .map_err(|e| clap::Error::raw(clap::error::ErrorKind::InvalidValue, e))?,
    };
98
    let background_str: &String = matches
        .get_one("background")
        .expect("already provided default_value");
98
    let background_color: Option<Color> = parse_background_color(background_str)
12
        .map_err(|e| clap::Error::raw(clap::error::ErrorKind::InvalidValue, e))?;
    // librsvg expects ids starting with '#', so it can lookup ids in externs like "subfile.svg#subid".
    // For the user's convenience, we prepend '#' automatically; we only support specifying ids from
    // the toplevel, and don't expect users to lookup things in externs.
5
    let lookup_id = |id: &String| {
5
        if id.starts_with('#') {
1
            id.clone()
        } else {
4
            format!("#{id}")
        }
5
    };
92
    let width: Option<ULength<Horizontal>> = matches.get_one("size_x").copied();
92
    let height: Option<ULength<Vertical>> = matches.get_one("size_y").copied();
92
    let left: Option<Length<Horizontal>> = matches.get_one("left").copied();
92
    let top: Option<Length<Vertical>> = matches.get_one("top").copied();
92
    let page_width: Option<ULength<Horizontal>> = matches.get_one("page_width").copied();
92
    let page_height: Option<ULength<Vertical>> = matches.get_one("page_height").copied();
92
    let page_size = match (page_width, page_height) {
85
        (None, None) => None,
        (Some(_), None) | (None, Some(_)) => {
2
            return Err(error!(
                "Please specify both the --page-width and --page-height options together."
            ));
        }
5
        (Some(w), Some(h)) => Some((w, h)),
    };
90
    let dpi_x = *matches
        .get_one::<Resolution>("res_x")
        .expect("already provided default_value");
90
    let dpi_y = *matches
        .get_one::<Resolution>("res_y")
        .expect("already provided default_value");
90
    let zoom: Option<ZoomFactor> = matches.get_one("zoom").copied();
90
    let zoom_x: Option<ZoomFactor> = matches.get_one("zoom_x").copied();
90
    let zoom_y: Option<ZoomFactor> = matches.get_one("zoom_y").copied();
90
    let input = match matches.get_many::<std::ffi::OsString>("FILE") {
17
        Some(values) => values
27
            .map(|f| PathOrUrl::from_os_str(f).map_err(Error))
27
            .map(|r| match r {
27
                Ok(p) if p.is_stdin_alias() => Ok(Input::Stdin),
24
                p => p.map(Input::Named),
27
            })
17
            .collect::<Result<Vec<Input>, Error>>()?,
73
        None => vec![Input::Stdin],
    };
190
    if input.iter().filter(|i| matches!(i, Input::Stdin)).count() > 1 {
1
        return Err(error!("Only one input file can be read from stdin."));
    }
89
    if input.len() > 1 && !matches!(format, Format::Ps | Format::Eps | Format::Pdf) {
1
        return Err(error!(
            "Multiple SVG files are only allowed for PDF and (E)PS output."
        ));
    }
88
    let export_id: Option<String> = matches.get_one::<String>("export_id").map(lookup_id);
88
    let output = match matches.get_one::<PathBuf>("output") {
84
        None => Output::Stdout,
4
        Some(path) => Output::Path(path.clone()),
    };
88
    Ok(Converter {
        dpi_x,
        dpi_y,
88
        zoom: Scale {
94
            x: zoom.or(zoom_x).map(|factor| factor.0).unwrap_or(1.0),
94
            y: zoom.or(zoom_y).map(|factor| factor.0).unwrap_or(1.0),
        },
        width,
        height,
        left,
        top,
88
        page_size,
88
        format,
88
        export_id,
88
        keep_aspect_ratio: matches.get_flag("keep_aspect"),
        background_color,
88
        stylesheet: matches.get_one("stylesheet").cloned(),
88
        unlimited: matches.get_flag("unlimited"),
88
        keep_image_data,
88
        language,
88
        input,
88
        output,
88
        testing: matches.get_flag("testing"),
    })
99
}
#[derive(Copy, Clone)]
struct Resolution(f64);
200
fn parse_resolution(v: &str) -> Result<Resolution, String> {
200
    match v.parse::<f64>() {
200
        Ok(res) if res > 0.0 => Ok(Resolution(res)),
2
        Ok(_) => Err(String::from("Invalid resolution")),
        Err(e) => Err(format!("{e}")),
    }
200
}
#[derive(Copy, Clone)]
struct ZoomFactor(f64);
10
fn parse_zoom_factor(v: &str) -> Result<ZoomFactor, String> {
10
    match v.parse::<f64>() {
9
        Ok(res) if res > 0.0 => Ok(ZoomFactor(res)),
1
        Ok(_) => Err(String::from("Invalid zoom factor")),
1
        Err(e) => Err(format!("{e}")),
    }
10
}
101
fn parse_background_color(s: &str) -> Result<Option<Color>, String> {
    match s {
101
        "none" | "None" => Ok(None),
24
        _ => <Color as Parse>::parse_str(s).map(Some).map_err(|e| {
7
            format!("Invalid value: The argument '{s}' can not be parsed as a CSS color value: {e}")
7
        }),
    }
101
}
75
fn is_absolute_unit(u: LengthUnit) -> bool {
    use LengthUnit::*;
75
    match u {
1
        Percent | Em | Ex | Ch => false,
74
        Px | In | Cm | Mm | Pt | Pc => true,
        _ => false,
    }
75
}
76
fn parse_length<N: Normalize, V: Validate>(s: &str) -> Result<CssLength<N, V>, String> {
228
    <CssLength<N, V> as Parse>::parse_str(s)
77
        .map_err(|_| format!("Invalid value: The argument '{s}' can not be parsed as a length"))
151
        .and_then(|l| {
75
            if is_absolute_unit(l.unit) {
74
                Ok(l)
            } else {
1
                Err(format!(
                    "Invalid value '{s}': supported units are px, in, cm, mm, pt, pc"
                ))
            }
75
        })
76
}
119
fn main() {
207
    if let Err(e) = parse_args().and_then(|converter| converter.convert()) {
40
        std::eprintln!("{e}");
20
        std::process::exit(1);
    }
79
}
#[cfg(test)]
mod color_tests {
    use super::*;
    #[test]
2
    fn valid_color_is_ok() {
1
        assert!(parse_background_color("Red").is_ok());
2
    }
    #[test]
2
    fn none_is_handled_as_transparent() {
1
        assert_eq!(parse_background_color("None").unwrap(), None,);
2
    }
    #[test]
2
    fn invalid_is_handled_as_invalid_value() {
1
        assert!(parse_background_color("foo").is_err());
2
    }
}
#[cfg(test)]
mod sizing_tests {
    use super::*;
    #[test]
2
    fn detects_empty_size() {
1
        let strategy = ResizeStrategy::Scale(Scale { x: 42.0, y: 42.0 });
1
        assert!(strategy.apply(&Size::new(0.0, 0.0)).is_none());
2
    }
    #[test]
2
    fn scale() {
1
        let strategy = ResizeStrategy::Scale(Scale { x: 2.0, y: 3.0 });
1
        assert_eq!(
1
            strategy.apply(&Size::new(1.0, 2.0)).unwrap(),
1
            Size::new(2.0, 6.0),
        );
2
    }
    #[test]
2
    fn fit_non_proportional() {
1
        let strategy = ResizeStrategy::Fit {
1
            size: Size::new(40.0, 10.0),
            keep_aspect_ratio: false,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(2.0, 1.0)).unwrap(),
1
            Size::new(40.0, 10.0),
        );
2
    }
    #[test]
2
    fn fit_proportional_wider_than_tall() {
1
        let strategy = ResizeStrategy::Fit {
1
            size: Size::new(40.0, 10.0),
            keep_aspect_ratio: true,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(2.0, 1.0)).unwrap(),
1
            Size::new(20.0, 10.0),
        );
2
    }
    #[test]
2
    fn fit_proportional_taller_than_wide() {
1
        let strategy = ResizeStrategy::Fit {
1
            size: Size::new(100.0, 50.0),
            keep_aspect_ratio: true,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(1.0, 2.0)).unwrap(),
1
            Size::new(25.0, 50.0),
        );
2
    }
    #[test]
2
    fn fit_width() {
1
        let strategy = ResizeStrategy::FitWidth(100.0);
1
        assert_eq!(
1
            strategy.apply(&Size::new(1.0, 2.0)).unwrap(),
1
            Size::new(100.0, 200.0),
        );
2
    }
    #[test]
2
    fn fit_height() {
1
        let strategy = ResizeStrategy::FitHeight(100.0);
1
        assert_eq!(
1
            strategy.apply(&Size::new(1.0, 2.0)).unwrap(),
1
            Size::new(50.0, 100.0),
        );
2
    }
    #[test]
2
    fn scale_no_max_size_non_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 2.0, y: 3.0 },
1
            max_width: None,
1
            max_height: None,
            keep_aspect_ratio: false,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(1.0, 2.0)).unwrap(),
1
            Size::new(2.0, 6.0),
        );
2
    }
    #[test]
2
    fn scale_with_max_width_and_height_fits_non_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 2.0, y: 3.0 },
1
            max_width: Some(10.0),
1
            max_height: Some(20.0),
            keep_aspect_ratio: false,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(4.0, 2.0)).unwrap(),
1
            Size::new(8.0, 6.0)
        );
2
    }
    #[test]
2
    fn scale_with_max_width_and_height_fits_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 2.0, y: 3.0 },
1
            max_width: Some(10.0),
1
            max_height: Some(20.0),
            keep_aspect_ratio: true,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(4.0, 2.0)).unwrap(),
1
            Size::new(8.0, 6.0)
        );
2
    }
    #[test]
2
    fn scale_with_max_width_and_height_doesnt_fit_non_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 10.0, y: 20.0 },
1
            max_width: Some(10.0),
1
            max_height: Some(20.0),
            keep_aspect_ratio: false,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(4.0, 5.0)).unwrap(),
1
            Size::new(10.0, 20.0)
        );
2
    }
    #[test]
2
    fn scale_with_max_width_and_height_doesnt_fit_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 10.0, y: 20.0 },
1
            max_width: Some(10.0),
1
            max_height: Some(15.0),
            keep_aspect_ratio: true,
        };
1
        assert_eq!(
            // this will end up with a 40:120 aspect ratio
1
            strategy.apply(&Size::new(4.0, 6.0)).unwrap(),
            // which should be shrunk to 1:3 that fits in (10, 15) per the max_width/max_height above
1
            Size::new(5.0, 15.0)
        );
2
    }
    #[test]
2
    fn scale_with_max_width_fits_non_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 5.0, y: 20.0 },
1
            max_width: Some(10.0),
1
            max_height: None,
            keep_aspect_ratio: false,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(1.0, 10.0)).unwrap(),
1
            Size::new(5.0, 200.0),
        );
2
    }
    #[test]
2
    fn scale_with_max_width_fits_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 5.0, y: 20.0 },
1
            max_width: Some(10.0),
1
            max_height: None,
            keep_aspect_ratio: true,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(1.0, 10.0)).unwrap(),
1
            Size::new(5.0, 200.0),
        );
2
    }
    #[test]
2
    fn scale_with_max_height_fits_non_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 20.0, y: 5.0 },
1
            max_width: None,
1
            max_height: Some(10.0),
            keep_aspect_ratio: false,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(10.0, 1.0)).unwrap(),
1
            Size::new(200.0, 5.0),
        );
2
    }
    #[test]
2
    fn scale_with_max_height_fits_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 20.0, y: 5.0 },
1
            max_width: None,
1
            max_height: Some(10.0),
            keep_aspect_ratio: true,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(10.0, 1.0)).unwrap(),
1
            Size::new(200.0, 5.0),
        );
2
    }
    #[test]
2
    fn scale_with_max_width_doesnt_fit_non_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 10.0, y: 20.0 },
1
            max_width: Some(10.0),
1
            max_height: None,
            keep_aspect_ratio: false,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(5.0, 10.0)).unwrap(),
1
            Size::new(10.0, 200.0),
        );
2
    }
    #[test]
2
    fn scale_with_max_width_doesnt_fit_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 10.0, y: 20.0 },
1
            max_width: Some(10.0),
1
            max_height: None,
            keep_aspect_ratio: true,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(5.0, 10.0)).unwrap(),
1
            Size::new(10.0, 40.0),
        );
2
    }
    #[test]
2
    fn scale_with_max_height_doesnt_fit_non_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 10.0, y: 20.0 },
1
            max_width: None,
1
            max_height: Some(10.0),
            keep_aspect_ratio: false,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(5.0, 10.0)).unwrap(),
1
            Size::new(50.0, 10.0),
        );
2
    }
    #[test]
2
    fn scale_with_max_height_doesnt_fit_proportional() {
1
        let strategy = ResizeStrategy::ScaleWithMaxSize {
1
            scale: Scale { x: 8.0, y: 20.0 },
1
            max_width: None,
1
            max_height: Some(10.0),
            keep_aspect_ratio: true,
        };
1
        assert_eq!(
1
            strategy.apply(&Size::new(5.0, 10.0)).unwrap(),
1
            Size::new(2.0, 10.0),
        );
2
    }
}