1#![allow(clippy::upper_case_acronyms)]
2
3use std::{fmt, fs, io};
4use std::env::current_dir;
5use std::error::Error;
6use std::io::BufRead;
7use std::path::PathBuf;
8
9use futures::stream::StreamExt;
10use indicatif::{ProgressBar, ProgressStyle};
11use itertools::Itertools;
12use log::{debug, info};
13use reqwest::Client;
14
15pub use arguments::Arguments;
16use dezoomer::{TileFetchResult, ZoomLevel, ZoomLevelIter};
17use dezoomer::{Dezoomer, DezoomerError, DezoomerInput, ZoomLevels};
18use dezoomer::TileReference;
19pub use errors::ZoomError;
20use network::{client, fetch_uri};
21use output_file::get_outname;
22use tile::Tile;
23pub use vec2d::Vec2d;
24
25use crate::dezoomer::PageContents;
26use crate::encoder::tile_buffer::TileBuffer;
27use crate::network::TileDownloader;
28use crate::output_file::reserve_output_file;
29
30mod arguments;
31mod encoder;
32pub mod dezoomer;
33pub mod tile;
34mod vec2d;
35mod errors;
36mod output_file;
37mod network;
38
39pub mod auto;
40pub mod custom_yaml;
41pub mod dzi;
42pub mod generic;
43pub mod google_arts_and_culture;
44pub mod iiif;
45pub mod pff;
46pub mod zoomify;
47pub mod krpano;
48pub mod nypl;
49pub mod iipimage;
50mod json_utils;
51
52fn stdin_line() -> Result<String, ZoomError> {
53 let stdin = std::io::stdin();
54 let mut lines = stdin.lock().lines();
55 let first_line = lines.next().ok_or_else(|| {
56 let err_msg = "Encountered end of standard input while reading a line";
57 io::Error::new(io::ErrorKind::UnexpectedEof, err_msg)
58 })?;
59 Ok(first_line?)
60}
61
62async fn list_tiles(
63 dezoomer: &mut dyn Dezoomer,
64 http: &Client,
65 uri: &str,
66) -> Result<ZoomLevels, ZoomError> {
67 let mut i = DezoomerInput {
68 uri: String::from(uri),
69 contents: PageContents::Unknown,
70 };
71 loop {
72 match dezoomer.zoom_levels(&i) {
73 Ok(levels) => return Ok(levels),
74 Err(DezoomerError::NeedsData { uri }) => {
75 let contents = fetch_uri(&uri, http).await.into();
76 debug!("Response for metadata file '{}': {:?}", uri, &contents);
77 i.uri = uri;
78 i.contents = contents;
79 }
80 Err(e) => return Err(e.into()),
81 }
82 }
83}
84
85fn level_picker(mut levels: Vec<ZoomLevel>) -> Result<ZoomLevel, ZoomError> {
87 println!("Found the following zoom levels:");
88 for (i, level) in levels.iter().enumerate() {
89 println!("{: >2}. {}", i, level.name());
90 }
91 loop {
92 println!("Which level do you want to download? ");
93 let line = stdin_line()?;
94 if let Ok(idx) = line.parse::<usize>() {
95 if levels.get(idx).is_some() {
96 return Ok(levels.swap_remove(idx));
97 }
98 }
99 println!("'{}' is not a valid level number", line);
100 }
101}
102
103fn choose_level(mut levels: Vec<ZoomLevel>, args: &Arguments) -> Result<ZoomLevel, ZoomError> {
104 match levels.len() {
105 0 => Err(ZoomError::NoLevels),
106 1 => Ok(levels.swap_remove(0)),
107 _ => {
108 let pos = args
109 .best_size(levels.iter().filter_map(|l| l.size_hint()))
110 .and_then(|best_size| {
111 levels
112 .iter()
113 .find_position(|&l| l.size_hint() == Some(best_size))
114 });
115 if let Some((i, _)) = pos {
116 Ok(levels.swap_remove(i))
117 } else {
118 level_picker(levels)
119 }
120 }
121 }
122}
123
124fn progress_bar(n: usize) -> ProgressBar {
125 let progress = ProgressBar::new(n as u64);
126 progress.set_style(
127 ProgressStyle::default_bar()
128 .template("[ETA:{eta}] {bar:40.cyan/blue} {pos:>4}/{len:4} {msg}")
129 .expect("Invalid indicatif progress bar template")
130 .progress_chars("##-"),
131 );
132 progress
133}
134
135async fn find_zoomlevel(args: &Arguments) -> Result<ZoomLevel, ZoomError> {
136 let mut dezoomer = args.find_dezoomer()?;
137 let uri = args.choose_input_uri()?;
138 let http_client = client(args.headers(), args, Some(&uri))?;
139 info!("Trying to locate a zoomable image...");
140 let zoom_levels: Vec<ZoomLevel> = list_tiles(dezoomer.as_mut(), &http_client, &uri).await?;
141 info!("Found {} zoom levels", zoom_levels.len());
142 choose_level(zoom_levels, args)
143}
144
145pub async fn dezoomify(args: &Arguments) -> Result<PathBuf, ZoomError> {
146 let zoom_level = find_zoomlevel(args).await?;
147 let base_dir = current_dir()?;
148 let outname = get_outname(&args.outfile, &zoom_level.title(), &base_dir,zoom_level.size_hint());
149 let save_as = fs::canonicalize(outname.as_path()).unwrap_or_else(|_e| outname.clone());
150 reserve_output_file(&save_as)?;
151 let tile_buffer: TileBuffer = TileBuffer::new(save_as.clone(), args.compression).await?;
152 info!("Dezooming {}", zoom_level.name());
153 dezoomify_level(args, zoom_level, tile_buffer).await?;
154 Ok(save_as)
155}
156
157pub async fn dezoomify_level(
158 args: &Arguments,
159 mut zoom_level: ZoomLevel,
160 tile_buffer: TileBuffer,
161) -> Result<(), ZoomError> {
162 let level_headers = zoom_level.http_headers();
163 let downloader = TileDownloader {
164 http_client: client(level_headers.iter().chain(args.headers()), args, None)?,
165 post_process_fn: zoom_level.post_process_fn(),
166 retries: args.retries,
167 retry_delay: args.retry_delay,
168 tile_storage_folder: args.tile_storage_folder.clone(),
169 };
170
171 info!("Creating canvas");
172 let mut canvas = tile_buffer;
173
174 let progress = progress_bar(10);
175 let mut total_tiles = 0u64;
176 let mut successful_tiles = 0u64;
177
178
179 progress.set_message("Computing the URLs of the image tiles...");
180
181 let mut zoom_level_iter = ZoomLevelIter::new(&mut zoom_level);
182 let mut last_count = 0;
183 let mut last_successes = 0;
184 while let Some(tile_refs) = zoom_level_iter.next_tile_references() {
185 last_count = tile_refs.len() as u64;
186 total_tiles += last_count;
187 progress.set_length(total_tiles);
188
189 progress.set_message("Requesting the tiles...");
190
191 let mut stream = futures::stream::iter(tile_refs)
192 .map(|tile_ref: TileReference| downloader.download_tile(tile_ref))
193 .buffer_unordered(args.parallelism);
194
195 last_successes = 0;
196 let mut tile_size = None;
197
198 if let Some(size) = zoom_level_iter.size_hint() {
199 canvas.set_size(size).await?;
200 }
201
202 while let Some(tile_result) = stream.next().await {
203 debug!("Received tile result: {:?}", tile_result);
204 progress.inc(1);
205 let tile = match tile_result {
206 Ok(tile) => {
207 progress.set_message(format!("Loaded tile at {}", tile.position()));
208 tile_size.replace(tile.size());
209 last_successes += 1;
210 Some(tile)
211 }
212 Err(err) => {
213 progress.set_message(err.to_string());
215 let position = err.tile_reference.position;
216 tile_size.and_then(|tile_size| {
217 zoom_level_iter.size_hint().map(|canvas_size| {
218 let size = max_size_in_rect(position, tile_size, canvas_size);
219 Tile::empty(position, size)
220 })
221 })
222 }
223 };
224 if let Some(tile) = tile { canvas.add_tile(tile).await; }
225 }
226 successful_tiles += last_successes;
227 zoom_level_iter.set_fetch_result(TileFetchResult {
228 count: last_count,
229 successes: last_successes,
230 tile_size,
231 });
232 }
233
234 if successful_tiles == 0 { return Err(ZoomError::NoTile); }
235
236 progress.set_message("Downloaded all tiles. Finalizing the image file.");
237 canvas.finalize().await?;
238
239 progress.finish_with_message("Finished tile download");
240
241 if last_successes < last_count {
242 let destination = canvas.destination().to_string_lossy().to_string();
243 Err(ZoomError::PartialDownload { successful_tiles, total_tiles, destination })
244 } else {
245 Ok(())
246 }
247}
248
249#[derive(Debug)]
250pub struct TileDownloadError {
251 tile_reference: TileReference,
252 cause: ZoomError,
253}
254
255impl fmt::Display for TileDownloadError {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 write!(f, "Unable to download tile '{}'. Cause: {}", self.tile_reference.url, self.cause)
258 }
259}
260
261impl Error for TileDownloadError {}
262
263pub fn max_size_in_rect(position: Vec2d, tile_size: Vec2d, canvas_size: Vec2d) -> Vec2d {
265 (position + tile_size).min(canvas_size) - position
266}