dezoomify_rs/
arguments.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use regex::Regex;
5use structopt::StructOpt;
6
7use crate::dezoomer::Dezoomer;
8
9use super::{auto, stdin_line, Vec2d, ZoomError};
10
11#[derive(StructOpt, Debug)]
12#[structopt(author, about)]
13pub struct Arguments {
14    /// Input URL or local file name
15    pub input_uri: Option<String>,
16
17    /// File to which the resulting image should be saved
18    #[structopt(parse(from_os_str))]
19    pub outfile: Option<PathBuf>,
20
21    /// Name of the dezoomer to use
22    #[structopt(short, long, default_value = "auto")]
23    dezoomer: String,
24
25    /// If several zoom levels are available, then select the largest one
26    #[structopt(short, long)]
27    pub largest: bool,
28
29    /// If several zoom levels are available, then select the one with the largest width that
30    /// is inferior to max-width.
31    #[structopt(short = "w", long = "max-width")]
32    max_width: Option<u32>,
33
34    /// If several zoom levels are available, then select the one with the largest height that
35    /// is inferior to max-height.
36    #[structopt(short = "h", long = "max-height")]
37    max_height: Option<u32>,
38
39    /// Degree of parallelism to use. At most this number of
40    /// tiles will be downloaded at the same time.
41    #[structopt(short = "n", long = "parallelism", default_value = "16")]
42    pub parallelism: usize,
43
44    /// Number of new attempts to make when a tile load fails
45    /// before giving up. Setting this to 0 is useful to speed up the
46    /// generic dezoomer, which relies on failed tile loads to detect the
47    /// dimensions of the image. On the contrary, if a server is not reliable,
48    /// set this value to a higher number.
49    #[structopt(short = "r", long = "retries", default_value = "1")]
50    pub retries: usize,
51
52    /// Amount of time to wait before retrying a request that failed.
53    /// Applies only to the first retry. Subsequent retries follow an
54    /// exponential backoff strategy: each one is twice as long as
55    /// the previous one.
56    #[structopt(long, default_value = "2s", parse(try_from_str = parse_duration))]
57    pub retry_delay: Duration,
58
59    /// A number between 0 and 100 expressing how much to compress the output image.
60    /// For lossy output formats such as jpeg, this affects the quality of the resulting image.
61    /// 0 means less compression, 100 means more compression.
62    /// Currently affects only the JPEG and PNG encoders.
63    #[structopt(long, default_value = "20")]
64    pub compression: u8,
65
66    /// Sets an HTTP header to use on requests.
67    /// This option can be repeated in order to set multiple headers.
68    /// You can use `-H "Referer: URL"` where URL is the URL of the website's
69    /// viewer page in order to let the site think you come from the legitimate viewer.
70    #[structopt(
71    short = "H",
72    long = "header",
73    parse(try_from_str = parse_header),
74    number_of_values = 1
75    )]
76    pub headers: Vec<(String, String)>,
77
78    /// Maximum number of idle connections per host allowed at the same time
79    #[structopt(long, default_value = "32")]
80    pub max_idle_per_host: usize,
81
82    /// Whether to accept connecting to insecure HTTPS servers
83    #[structopt(long)]
84    pub accept_invalid_certs: bool,
85
86    /// Maximum time between the beginning of a request and the end of a response before
87    ///the request should be interrupted and considered failed
88    #[structopt(long, default_value = "30s", parse(try_from_str = parse_duration))]
89    pub timeout: Duration,
90
91    /// Time after which we should give up when trying to connect to a server
92    #[structopt(long = "connect-timeout", default_value = "6s", parse(try_from_str = parse_duration))]
93    pub connect_timeout: Duration,
94
95    /// Level of logging verbosity. Set it to "debug" to get all logging messages.
96    #[structopt(long, default_value = "warn")]
97    pub logging: String,
98
99    /// A place to store the image tiles when after they are downloaded and decrypted.
100    /// By default, tiles are not stored to disk (which is faster), but using a tile cache allows
101    /// retrying partially failed downloads, or stitching the tiles with an external program.
102    #[structopt(short = "c", long = "tile-cache")]
103    pub tile_storage_folder: Option<PathBuf>,
104}
105
106impl Default for Arguments {
107    fn default() -> Self {
108        Arguments {
109            input_uri: None,
110            outfile: None,
111            dezoomer: "auto".to_string(),
112            largest: false,
113            max_width: None,
114            max_height: None,
115            parallelism: 16,
116            retries: 1,
117            compression: 20,
118            retry_delay: Duration::from_secs(2),
119            headers: vec![],
120            max_idle_per_host: 32,
121            accept_invalid_certs: false,
122            timeout: Duration::from_secs(30),
123            connect_timeout: Duration::from_secs(6),
124            logging: "warn".to_string(),
125            tile_storage_folder: None
126        }
127    }
128}
129
130impl Arguments {
131    pub fn choose_input_uri(&self) -> Result<String, ZoomError> {
132        match &self.input_uri {
133            Some(uri) => Ok(uri.clone()),
134            None => {
135                println!("Enter an URL or a path to a tiles.yaml file: ");
136                stdin_line()
137            }
138        }
139    }
140    pub fn find_dezoomer(&self) -> Result<Box<dyn Dezoomer>, ZoomError> {
141        auto::all_dezoomers(true)
142            .into_iter()
143            .find(|d| d.name() == self.dezoomer)
144            .ok_or_else(|| ZoomError::NoSuchDezoomer {
145                name: self.dezoomer.clone(),
146            })
147    }
148    pub fn best_size<I: Iterator<Item = Vec2d>>(&self, sizes: I) -> Option<Vec2d> {
149        if self.largest {
150            sizes.max_by_key(|s| s.area())
151        } else if self.max_width.is_some() || self.max_height.is_some() {
152            sizes
153                .filter(|s| {
154                    self.max_width.map(|w| s.x <= w).unwrap_or(true)
155                        && self.max_height.map(|h| s.y <= h).unwrap_or(true)
156                })
157                .max_by_key(|s| s.area())
158        } else {
159            None
160        }
161    }
162
163    pub fn headers(&self) -> impl Iterator<Item = (&String, &String)> {
164        self.headers.iter().map(|(k, v)| (k, v))
165    }
166}
167
168fn parse_header(s: &str) -> Result<(String, String), &'static str> {
169    let vals: Vec<&str> = s.splitn(2, ':').map(str::trim).collect();
170    if let [key, value] = vals[..] {
171        Ok((key.into(), value.into()))
172    } else {
173        Err("Invalid header format. Expected 'Name: Value'")
174    }
175}
176
177fn parse_duration(s: &str) -> Result<Duration, &'static str> {
178    let err_msg = "Invalid duration. \
179                        A duration is a number followed by a unit, such as '10ms' or '5s'";
180    let re = Regex::new(r"^(\d+)\s*(min|s|ms|ns)$").unwrap();
181    let caps = re.captures(s).ok_or(err_msg)?;
182    let val: u64 = caps[1].parse().map_err(|_| err_msg)?;
183    match &caps[2] {
184        "min" => Ok(Duration::from_secs(60 * val)),
185        "s" => Ok(Duration::from_secs(val)),
186        "ms" => Ok(Duration::from_millis(val)),
187        "ns" => Ok(Duration::from_nanos(val)),
188        _ => Err(err_msg)
189    }
190}
191
192
193#[test]
194fn test_headers_and_input() -> Result<(), structopt::clap::Error> {
195    let args: Arguments = StructOpt::from_iter_safe(
196        [
197            "dezoomify-rs",
198            "--header",
199            "Referer: http://test.com",
200            "--header",
201            "User-Agent: custom",
202            "--header",
203            "A:B",
204            "input-url",
205        ]
206        .iter(),
207    )?;
208    assert_eq!(args.input_uri, Some("input-url".into()));
209    assert_eq!(
210        args.headers,
211        vec![
212            ("Referer".into(), "http://test.com".into()),
213            ("User-Agent".into(), "custom".into()),
214            ("A".into(), "B".into()),
215        ]
216    );
217    Ok(())
218}
219
220#[test]
221fn test_parse_duration() {
222    assert_eq!(parse_duration("2s"), Ok(Duration::from_secs(2)));
223    assert_eq!(parse_duration("29 s"), Ok(Duration::from_secs(29)));
224    assert_eq!(parse_duration("2min"), Ok(Duration::from_secs(120)));
225    assert_eq!(parse_duration("1000 ms"), Ok(Duration::from_secs(1)));
226    assert!(parse_duration("1 2 ms").is_err());
227    assert!(parse_duration("1 s s").is_err());
228    assert!(parse_duration("ms").is_err());
229    assert!(parse_duration("1j").is_err());
230    assert!(parse_duration("").is_err());
231}