1use std::fmt;
7use std::io::{self, Write};
8use std::time::Instant;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum ProcessingStage {
13 #[default]
15 Initializing,
16 Extracting,
18 Deskewing,
20 Normalizing,
22 ColorCorrecting,
24 Cropping,
26 Upscaling,
28 Finalizing,
30 WritingPdf,
32 OCR,
34 Completed,
36}
37
38impl ProcessingStage {
39 pub fn name(&self) -> &'static str {
41 match self {
42 ProcessingStage::Initializing => "Initializing",
43 ProcessingStage::Extracting => "Extracting",
44 ProcessingStage::Deskewing => "Deskewing",
45 ProcessingStage::Normalizing => "Normalizing",
46 ProcessingStage::ColorCorrecting => "ColorCorrecting",
47 ProcessingStage::Cropping => "Cropping",
48 ProcessingStage::Upscaling => "Upscaling",
49 ProcessingStage::Finalizing => "Finalizing",
50 ProcessingStage::WritingPdf => "WritingPdf",
51 ProcessingStage::OCR => "OCR",
52 ProcessingStage::Completed => "Completed",
53 }
54 }
55
56 pub fn description_ja(&self) -> &'static str {
58 match self {
59 ProcessingStage::Initializing => "初期化中",
60 ProcessingStage::Extracting => "抽出中",
61 ProcessingStage::Deskewing => "傾き補正中",
62 ProcessingStage::Normalizing => "正規化中",
63 ProcessingStage::ColorCorrecting => "色補正中",
64 ProcessingStage::Cropping => "クロップ中",
65 ProcessingStage::Upscaling => "AI高画質化中",
66 ProcessingStage::Finalizing => "最終処理中",
67 ProcessingStage::WritingPdf => "PDF生成中",
68 ProcessingStage::OCR => "文字認識中",
69 ProcessingStage::Completed => "完了",
70 }
71 }
72}
73
74impl fmt::Display for ProcessingStage {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 write!(f, "{} ({})", self.name(), self.description_ja())
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum OutputMode {
83 Quiet,
85 #[default]
87 Normal,
88 Verbose,
90 VeryVerbose,
92}
93
94impl OutputMode {
95 pub fn from_verbosity(level: u8) -> Self {
97 match level {
98 0 => OutputMode::Normal,
99 1 => OutputMode::Verbose,
100 _ => OutputMode::VeryVerbose,
101 }
102 }
103
104 pub fn should_show(&self, required: OutputMode) -> bool {
106 use OutputMode::*;
107 match (self, required) {
108 (Quiet, _) => false,
109 (Normal, Quiet | Normal) => true,
110 (Verbose, Quiet | Normal | Verbose) => true,
111 (VeryVerbose, _) => true,
112 _ => false,
113 }
114 }
115}
116
117const PROGRESS_BAR_WIDTH: usize = 40;
119
120pub fn build_progress_bar(percent: u8) -> String {
122 let percent = percent.min(100);
123 let filled = (percent as usize * PROGRESS_BAR_WIDTH) / 100;
124 let empty = PROGRESS_BAR_WIDTH - filled;
125 format!("[{}{}]", "=".repeat(filled), "-".repeat(empty))
126}
127
128#[derive(Debug)]
130pub struct ProgressTracker {
131 pub current_file: usize,
133 pub total_files: usize,
135 pub current_filename: String,
137 pub current_stage: ProcessingStage,
139 pub current_page: usize,
141 pub total_pages: usize,
143 pub current_item: String,
145 start_time: Instant,
147 output_mode: OutputMode,
149}
150
151impl Default for ProgressTracker {
152 fn default() -> Self {
153 Self::new(1, OutputMode::Normal)
154 }
155}
156
157impl ProgressTracker {
158 pub fn new(total_files: usize, output_mode: OutputMode) -> Self {
160 Self {
161 current_file: 0,
162 total_files,
163 current_filename: String::new(),
164 current_stage: ProcessingStage::Initializing,
165 current_page: 0,
166 total_pages: 0,
167 current_item: String::new(),
168 start_time: Instant::now(),
169 output_mode,
170 }
171 }
172
173 pub fn start_file(&mut self, file_number: usize, filename: &str) {
175 self.current_file = file_number;
176 self.current_filename = filename.to_string();
177 self.current_stage = ProcessingStage::Initializing;
178 self.current_page = 0;
179 self.total_pages = 0;
180 self.current_item.clear();
181 self.start_time = Instant::now();
182
183 if self.output_mode.should_show(OutputMode::Normal) {
184 self.print_file_header();
185 }
186 }
187
188 pub fn set_stage(&mut self, stage: ProcessingStage, total_pages: usize) {
190 self.current_stage = stage;
191 if total_pages > 0 {
192 self.total_pages = total_pages;
193 }
194 self.current_page = 0;
195
196 if self.output_mode.should_show(OutputMode::Normal) {
197 self.print_stage();
198 }
199 }
200
201 pub fn update_page(&mut self, page_number: usize, item_name: &str) {
203 self.current_page = page_number;
204 if !item_name.is_empty() {
205 self.current_item = item_name.to_string();
206 }
207
208 if self.output_mode.should_show(OutputMode::Verbose) {
209 self.print_progress();
210 }
211 }
212
213 pub fn complete_file(&mut self) {
215 self.current_stage = ProcessingStage::Completed;
216
217 if self.output_mode.should_show(OutputMode::Normal) {
218 let elapsed = self.start_time.elapsed();
219 println!(" Completed in {:.2}s", elapsed.as_secs_f64());
220 println!();
221 }
222 }
223
224 pub fn elapsed_secs(&self) -> f64 {
226 self.start_time.elapsed().as_secs_f64()
227 }
228
229 fn print_file_header(&self) {
231 println!();
232 println!("{}", "=".repeat(80));
233 println!(
234 "[File {}/{}] {}",
235 self.current_file, self.total_files, self.current_filename
236 );
237 println!("{}", "=".repeat(80));
238 }
239
240 fn print_stage(&self) {
242 println!(" Stage: {}", self.current_stage);
243 }
244
245 fn print_progress(&self) {
247 if self.total_pages > 0 && self.current_stage != ProcessingStage::Completed {
248 let percent = ((self.current_page as f64 / self.total_pages as f64) * 100.0) as u8;
249 let bar = build_progress_bar(percent);
250 print!(
251 "\r {} {:3}% ({}/{})",
252 bar, percent, self.current_page, self.total_pages
253 );
254 if self.output_mode.should_show(OutputMode::VeryVerbose) && !self.current_item.is_empty()
255 {
256 print!(" {}", self.current_item);
257 }
258 let _ = io::stdout().flush();
259 }
260 }
261
262 pub fn print_summary(
264 total_files: usize,
265 ok_count: usize,
266 skip_count: usize,
267 error_count: usize,
268 ) {
269 println!();
270 println!("{}", "=".repeat(80));
271 println!("Processing Summary");
272 println!("{}", "=".repeat(80));
273 println!(" Total files: {}", total_files);
274 println!(" Succeeded: {}", ok_count);
275 println!(" Skipped: {}", skip_count);
276 println!(" Errors: {}", error_count);
277 println!("{}", "=".repeat(80));
278 println!();
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
288 fn test_progress_tracker_new() {
289 let tracker = ProgressTracker::new(5, OutputMode::Normal);
290 assert_eq!(tracker.total_files, 5);
291 assert_eq!(tracker.current_file, 0);
292 assert_eq!(tracker.current_stage, ProcessingStage::Initializing);
293 }
294
295 #[test]
297 fn test_start_file() {
298 let mut tracker = ProgressTracker::new(3, OutputMode::Quiet);
299 tracker.start_file(1, "test.pdf");
300 assert_eq!(tracker.current_file, 1);
301 assert_eq!(tracker.current_filename, "test.pdf");
302 }
303
304 #[test]
306 fn test_set_stage() {
307 let mut tracker = ProgressTracker::new(1, OutputMode::Quiet);
308 tracker.set_stage(ProcessingStage::Extracting, 100);
309 assert_eq!(tracker.current_stage, ProcessingStage::Extracting);
310 assert_eq!(tracker.total_pages, 100);
311 }
312
313 #[test]
315 fn test_update_page() {
316 let mut tracker = ProgressTracker::new(1, OutputMode::Quiet);
317 tracker.set_stage(ProcessingStage::Deskewing, 50);
318 tracker.update_page(25, "page_025.png");
319 assert_eq!(tracker.current_page, 25);
320 assert_eq!(tracker.current_item, "page_025.png");
321 }
322
323 #[test]
325 fn test_complete_file() {
326 let mut tracker = ProgressTracker::new(1, OutputMode::Quiet);
327 tracker.start_file(1, "test.pdf");
328 tracker.complete_file();
329 assert_eq!(tracker.current_stage, ProcessingStage::Completed);
330 }
331
332 #[test]
334 fn test_processing_stage_name() {
335 assert_eq!(ProcessingStage::Initializing.name(), "Initializing");
336 assert_eq!(ProcessingStage::Extracting.name(), "Extracting");
337 assert_eq!(ProcessingStage::Deskewing.name(), "Deskewing");
338 assert_eq!(ProcessingStage::Normalizing.name(), "Normalizing");
339 assert_eq!(ProcessingStage::ColorCorrecting.name(), "ColorCorrecting");
340 assert_eq!(ProcessingStage::Cropping.name(), "Cropping");
341 assert_eq!(ProcessingStage::Upscaling.name(), "Upscaling");
342 assert_eq!(ProcessingStage::Finalizing.name(), "Finalizing");
343 assert_eq!(ProcessingStage::WritingPdf.name(), "WritingPdf");
344 assert_eq!(ProcessingStage::OCR.name(), "OCR");
345 assert_eq!(ProcessingStage::Completed.name(), "Completed");
346 }
347
348 #[test]
350 fn test_processing_stage_description_ja() {
351 assert_eq!(ProcessingStage::Initializing.description_ja(), "初期化中");
352 assert_eq!(ProcessingStage::Extracting.description_ja(), "抽出中");
353 assert_eq!(ProcessingStage::Deskewing.description_ja(), "傾き補正中");
354 assert_eq!(ProcessingStage::Completed.description_ja(), "完了");
355 }
356
357 #[test]
359 fn test_build_progress_bar() {
360 let bar_0 = build_progress_bar(0);
361 assert_eq!(bar_0, "[----------------------------------------]");
362
363 let bar_50 = build_progress_bar(50);
364 assert_eq!(bar_50, "[====================--------------------]");
365
366 let bar_100 = build_progress_bar(100);
367 assert_eq!(bar_100, "[========================================]");
368 }
369
370 #[test]
372 fn test_build_progress_bar_boundary() {
373 let bar_150 = build_progress_bar(150);
375 assert_eq!(bar_150, "[========================================]");
376
377 let bar_25 = build_progress_bar(25);
379 assert_eq!(bar_25, "[==========------------------------------]");
380
381 let bar_75 = build_progress_bar(75);
383 assert_eq!(bar_75, "[==============================----------]");
384 }
385
386 #[test]
388 fn test_output_mode_quiet() {
389 let mode = OutputMode::Quiet;
390 assert!(!mode.should_show(OutputMode::Quiet));
391 assert!(!mode.should_show(OutputMode::Normal));
392 assert!(!mode.should_show(OutputMode::Verbose));
393 }
394
395 #[test]
397 fn test_output_mode_verbose() {
398 let mode = OutputMode::Verbose;
399 assert!(mode.should_show(OutputMode::Quiet));
400 assert!(mode.should_show(OutputMode::Normal));
401 assert!(mode.should_show(OutputMode::Verbose));
402 assert!(!mode.should_show(OutputMode::VeryVerbose));
403 }
404
405 #[test]
407 fn test_elapsed_secs() {
408 let tracker = ProgressTracker::new(1, OutputMode::Quiet);
409 std::thread::sleep(std::time::Duration::from_millis(10));
410 let elapsed = tracker.elapsed_secs();
411 assert!(elapsed >= 0.01);
412 }
413
414 #[test]
417 fn test_output_mode_from_verbosity() {
418 assert_eq!(OutputMode::from_verbosity(0), OutputMode::Normal);
419 assert_eq!(OutputMode::from_verbosity(1), OutputMode::Verbose);
420 assert_eq!(OutputMode::from_verbosity(2), OutputMode::VeryVerbose);
421 assert_eq!(OutputMode::from_verbosity(10), OutputMode::VeryVerbose);
422 }
423
424 #[test]
425 fn test_processing_stage_display() {
426 let stage = ProcessingStage::Extracting;
427 let display = format!("{}", stage);
428 assert_eq!(display, "Extracting (抽出中)");
429 }
430
431 #[test]
432 fn test_processing_stage_default() {
433 let stage: ProcessingStage = Default::default();
434 assert_eq!(stage, ProcessingStage::Initializing);
435 }
436
437 #[test]
438 fn test_output_mode_default() {
439 let mode: OutputMode = Default::default();
440 assert_eq!(mode, OutputMode::Normal);
441 }
442
443 #[test]
444 fn test_progress_tracker_default() {
445 let tracker: ProgressTracker = Default::default();
446 assert_eq!(tracker.total_files, 1);
447 assert_eq!(tracker.current_file, 0);
448 }
449
450 #[test]
451 fn test_update_page_empty_item() {
452 let mut tracker = ProgressTracker::new(1, OutputMode::Quiet);
453 tracker.current_item = "previous.png".to_string();
454 tracker.update_page(10, "");
455 assert_eq!(tracker.current_page, 10);
456 assert_eq!(tracker.current_item, "previous.png"); }
458
459 #[test]
460 fn test_set_stage_zero_pages() {
461 let mut tracker = ProgressTracker::new(1, OutputMode::Quiet);
462 tracker.total_pages = 100;
463 tracker.set_stage(ProcessingStage::Deskewing, 0);
464 assert_eq!(tracker.total_pages, 100); }
466
467 #[test]
468 fn test_output_mode_very_verbose() {
469 let mode = OutputMode::VeryVerbose;
470 assert!(mode.should_show(OutputMode::Quiet));
471 assert!(mode.should_show(OutputMode::Normal));
472 assert!(mode.should_show(OutputMode::Verbose));
473 assert!(mode.should_show(OutputMode::VeryVerbose));
474 }
475
476 #[test]
477 fn test_output_mode_normal() {
478 let mode = OutputMode::Normal;
479 assert!(mode.should_show(OutputMode::Quiet));
480 assert!(mode.should_show(OutputMode::Normal));
481 assert!(!mode.should_show(OutputMode::Verbose));
482 assert!(!mode.should_show(OutputMode::VeryVerbose));
483 }
484}