c97b162f636e32c71d923fdd6c60201c26583b11
[zxing.git] / iphone / Classes / DecoderViewController.m
1 //
2 //  DecoderViewController.m
3 //  ZXing
4 //
5 //  Created by Christian Brunschen on 22/05/2008.
6 /*
7  * Copyright 2008 ZXing authors
8  *
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  *      http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  */
21
22 #import "DecoderViewController.h"
23 #import "Decoder.h"
24 #import "NSString+HTML.h"
25 #import "ResultParser.h"
26 #import "ParsedResult.h"
27 #import "ResultAction.h"
28
29 #import "Database.h"
30 #import "ArchiveController.h"
31 #import "MessageViewController.h"
32 #import "Scan.h"
33 #import "TwoDDecoderResult.h"
34
35 // Michael Jurewitz, Dec 16, 2009 6:32 PM writes:
36 // https://devforums.apple.com/message/149553
37 // Notice Regarding UIGetScreenImage()
38 // After carefully considering the issue, Apple is now allowing applications to
39 // use the function UIGetScreenImage() to programmatically capture the current
40 // screen contents.
41 // Note that a future release of iPhone OS may provide a public API equivalent
42 // of this functionality.  At such time, all applications using
43 // UIGetScreenImage() will be required to adopt the public API.
44 CGImageRef MyCGImageCopyScreenContents(void) {
45    extern CGImageRef UIGetScreenImage(void);
46    return UIGetScreenImage(); /* already retained */
47 }
48
49 @interface DecoderViewController()
50 - (void)takeScreenshot;
51 @end
52
53 @implementation DecoderViewController
54
55 @synthesize cameraBarItem;
56 @synthesize libraryBarItem;
57 @synthesize savedPhotosBarItem;
58 @synthesize archiveBarItem;
59 @synthesize actionBarItem;
60
61 @synthesize messageView;
62 @synthesize messageTextView;
63 @synthesize messageHelpButton;
64 @synthesize imageView;
65 @synthesize picker;
66 @synthesize toolbar;
67
68 @synthesize decoder;
69 @synthesize result;
70 @synthesize actions;
71
72 @synthesize resultPointViews;
73
74 - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
75   if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) {
76     // Initialization code
77     self.title =
78         NSLocalizedString(@"DecoderViewController AppTitle", @"Barcode Scanner");
79
80     Decoder *d = [[Decoder alloc] init];
81     self.decoder = d;
82     d.delegate = self;
83     [d release];
84     resultPointViews = [[NSMutableArray alloc] init];
85   }
86   return self;
87 }
88
89 - (void) messageReady:(id)sender {
90   MessageViewController *messageController = sender;
91   [[self navigationController] pushViewController:messageController animated:true];
92   [messageController release];
93 }
94
95 - (void) messageFailed:(id)sender {
96   MessageViewController *messageController = sender;
97   NSLog(@"Failed to load message!");
98   [messageController release];
99 }
100
101 - (void) showHints:(id)sender {
102   NSLog(@"Showing Hints!");
103
104   MessageViewController *hintsController =
105       [[MessageViewController alloc] initWithMessageFilename:@"Hints"
106                                                       target:self
107                                                    onSuccess:@selector(messageReady:)
108                                                    onFailure:@selector(messageFailed:)];
109   hintsController.title =
110       NSLocalizedString(@"DecoderViewController Hints MessageViewController title", @"Hints");
111   [hintsController view];
112 }
113
114 - (void) showAbout:(id)sender {
115   NSLog(@"Showing About!");
116
117   MessageViewController *aboutController =
118       [[MessageViewController alloc] initWithMessageFilename:@"About"
119                                                       target:self
120                                                    onSuccess:@selector(messageReady:)
121                                                    onFailure:@selector(messageFailed:)];
122   aboutController.title =
123       NSLocalizedString(@"DecoderViewController About MessageViewController title", @"About");
124   [aboutController view];
125 }
126
127
128 #define HELP_BUTTON_WIDTH (44.0)
129 #define HELP_BUTTON_HEIGHT (55.0)
130
131
132 #define FONT_NAME @"TimesNewRomanPSMT"
133 #define FONT_SIZE 16.0
134
135 - (void) reset {
136   self.result = nil;
137   [self clearImageView];
138   [self updateToolbar];
139   [self showMessage:NSLocalizedString(@"DecoderViewController take or choose picture",
140       @"Please take or choose a picture containing a barcode") helpButton:YES];
141 }
142
143 // Implement loadView if you want to create a view hierarchy programmatically
144 - (void)loadView {
145   [super loadView];
146
147   CGRect messageViewFrame = imageView.frame;
148   UIView *mView = [[UIView alloc] initWithFrame:messageViewFrame];
149   mView.backgroundColor = [UIColor darkGrayColor];
150   mView.alpha = 0.9;
151   mView.autoresizingMask = UIViewAutoresizingFlexibleHeight |
152   UIViewAutoresizingFlexibleWidth |
153   UIViewAutoresizingFlexibleTopMargin;
154
155   UITextView *mTextView = [[UITextView alloc] initWithFrame:messageViewFrame];
156   mTextView.autoresizingMask = UIViewAutoresizingFlexibleHeight |
157   UIViewAutoresizingFlexibleWidth;
158   mTextView.editable = false;
159   mTextView.scrollEnabled = true;
160   mTextView.font = [UIFont fontWithName:FONT_NAME size:FONT_SIZE];
161   mTextView.textColor = [UIColor whiteColor];
162   mTextView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:0.0];
163   mTextView.textAlignment = UITextAlignmentLeft;
164   mTextView.alpha = 1.0;
165   [mView addSubview:mTextView];
166
167   UIButton *mHelpButton = [[UIButton buttonWithType:UIButtonTypeInfoLight] retain];
168   mHelpButton.frame = CGRectMake(messageViewFrame.size.width - HELP_BUTTON_WIDTH, 0.0, HELP_BUTTON_WIDTH, HELP_BUTTON_HEIGHT);
169
170   mHelpButton.backgroundColor = [UIColor clearColor];
171   [mHelpButton setUserInteractionEnabled:YES];
172   [mHelpButton addTarget:self action:@selector(showHints:) forControlEvents:UIControlEventTouchUpInside];
173
174   self.messageHelpButton = mHelpButton;
175   [mHelpButton release];
176
177   self.messageTextView = mTextView;
178   [mTextView release];
179
180   self.messageView = mView;
181   [mView release];
182
183   [self.view addSubview:self.messageView];
184
185   // add the 'About' button at the top-right of the navigation bar
186   UIBarButtonItem *aboutButton =
187   [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"DecoderViewController about button title", @"About")
188                                    style:UIBarButtonItemStyleBordered
189                                   target:self
190                                   action:@selector(showAbout:)];
191   self.navigationItem.rightBarButtonItem = aboutButton;
192   [aboutButton release];
193
194   [self reset];
195 }
196
197 - (void) updateToolbar {
198   self.cameraBarItem.enabled = [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera];
199   self.savedPhotosBarItem.enabled = [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeSavedPhotosAlbum];
200   self.libraryBarItem.enabled = [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary];
201   self.archiveBarItem.enabled = true;
202   self.actionBarItem.enabled = (self.result != nil) && ([self.result actions] != nil) && ([self.result actions].count > 0);
203 }
204
205
206 // If you need to do additional setup after loading the view, override viewDidLoad.
207 - (void)viewDidLoad {
208   [super viewDidLoad];
209 }
210
211
212 - (void)clearImageView {
213   imageView.image = nil;
214   for (UIView *view in resultPointViews) {
215     [view removeFromSuperview];
216   }
217   [resultPointViews removeAllObjects];
218 }
219
220 - (void)pickAndDecodeFromSource:(UIImagePickerControllerSourceType) sourceType {
221   [self reset];
222
223   // Create the Image Picker
224   if ([UIImagePickerController isSourceTypeAvailable:sourceType]) {
225     UIImagePickerController* aPicker =
226         [[[UIImagePickerController alloc] init] autorelease];
227     aPicker.sourceType = sourceType;
228     aPicker.delegate = self;
229     self.picker = aPicker;
230
231     // [[NSUserDefaults standardUserDefaults] boolForKey:@"allowEditing"];
232     BOOL isCamera = (sourceType == UIImagePickerControllerSourceTypeCamera);
233     picker.allowsEditing = !isCamera;
234     if (isCamera) {
235       picker.showsCameraControls = NO;
236       UIButton *cancelButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
237       NSString *cancelString =
238           NSLocalizedString(@"DecoderViewController cancel button title", @"");
239       CGFloat height = [UIFont systemFontSize];
240       CGSize size = [cancelString sizeWithFont:[UIFont systemFontOfSize:height]];
241       [cancelButton setTitle:cancelString forState:UIControlStateNormal];
242       CGRect appFrame = [[UIScreen mainScreen] bounds];
243       static const int kMargin = 10;
244       static const int kInternalXMargin = 10;
245       static const int kInternalYMargin = 10;
246       CGRect frame = CGRectMake(kMargin,
247         appFrame.size.height - (height + 2*kInternalYMargin + kMargin),
248         2*kInternalXMargin + size.width,
249         height + 2*kInternalYMargin);
250       [cancelButton setFrame:frame];
251       [cancelButton addTarget:self
252                        action:@selector(cancel:)
253              forControlEvents:UIControlEventTouchUpInside];
254       picker.cameraOverlayView = cancelButton;
255       // The camera takes quite a while to start up. Hence the 2 second delay.
256       [self performSelector:@selector(takeScreenshot)
257                  withObject:nil
258                  afterDelay:2.0];
259     }
260
261     // Picker is displayed asynchronously.
262     [self presentModalViewController:picker animated:YES];
263   } else {
264     NSLog(@"Attempted to pick an image with illegal source type '%d'", sourceType);
265   }
266 }
267
268 - (IBAction)pickAndDecode:(id) sender {
269   UIImagePickerControllerSourceType sourceType;
270   int i = [sender tag];
271
272   switch (i) {
273     case 0: sourceType = UIImagePickerControllerSourceTypeCamera; break;
274     case 1: sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum; break;
275     case 2: sourceType = UIImagePickerControllerSourceTypePhotoLibrary; break;
276     default: sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
277   }
278   [self pickAndDecodeFromSource:sourceType];
279 }
280
281
282 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
283   // Return YES for supported orientations
284   return (interfaceOrientation == UIInterfaceOrientationPortrait);
285 }
286
287
288 - (void)didReceiveMemoryWarning {
289   [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
290   // Release anything that's not essential, such as cached data
291 }
292
293 - (void)dealloc {
294   [decoder release];
295   [self clearImageView];
296   [imageView release];
297   [actionBarItem release];
298   [cameraBarItem release];
299   [libraryBarItem release];
300   [savedPhotosBarItem release];
301   [archiveBarItem release];
302   [toolbar release];
303   [picker release];
304   [actions release];
305   [resultPointViews release];
306
307   [super dealloc];
308 }
309
310 - (void)showMessage:(NSString *)message helpButton:(BOOL)showHelpButton {
311 #ifdef DEBUG
312   NSLog(@"Showing message '%@' %@ help Button", message, showHelpButton ? @"with" : @"without");
313 #endif
314
315   CGSize imageMaxSize = imageView.bounds.size;
316   if (showHelpButton) {
317     imageMaxSize.width -= messageHelpButton.frame.size.width;
318   }
319   CGSize size = [message sizeWithFont:messageTextView.font constrainedToSize:imageMaxSize lineBreakMode:UILineBreakModeWordWrap];
320   float height = 20.0 + fmin(100.0, size.height);
321   if (showHelpButton) {
322     height = fmax(HELP_BUTTON_HEIGHT, height);
323   }
324
325   CGRect messageFrame = imageView.bounds;
326   messageFrame.origin.y = CGRectGetMaxY(messageFrame) - height;
327   messageFrame.size.height = height;
328   [self.messageView setFrame:messageFrame];
329   messageView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
330   CGRect messageViewBounds = [messageView bounds];
331
332   self.messageTextView.text = message;
333   messageTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
334   if (showHelpButton) {
335     CGRect textViewFrame;
336     CGRect helpButtonFrame;
337
338     CGRectDivide(messageViewBounds, &helpButtonFrame, &textViewFrame, HELP_BUTTON_WIDTH, CGRectMaxXEdge);
339     [self.messageTextView setFrame:textViewFrame];
340
341     [messageHelpButton setFrame:helpButtonFrame];
342     messageHelpButton.alpha = 1.0;
343     messageHelpButton.enabled = YES;
344     messageHelpButton.autoresizingMask =
345       UIViewAutoresizingFlexibleLeftMargin |
346       UIViewAutoresizingFlexibleTopMargin;
347     [messageView addSubview:messageHelpButton];
348   } else {
349     [messageHelpButton removeFromSuperview];
350     messageHelpButton.alpha = 0.0;
351     messageHelpButton.enabled = NO;
352
353     [self.messageTextView setFrame:messageViewBounds];
354   }
355 }
356
357 // DecoderDelegate methods
358
359 - (void)decoder:(Decoder *)decoder willDecodeImage:(UIImage *)image usingSubset:(UIImage *)subset{
360   [self clearImageView];
361   [self.imageView setImage:subset];
362   [self showMessage:[NSString stringWithFormat:NSLocalizedString(@"DecoderViewController MessageWhileDecodingWithDimensions", @"Decoding image (%.0fx%.0f) ..."), image.size.width, image.size.height]
363      helpButton:NO];
364 }
365
366 - (void)decoder:(Decoder *)decoder
367   decodingImage:(UIImage *)image
368     usingSubset:(UIImage *)subset
369        progress:(NSString *)message {
370   [self clearImageView];
371   [self.imageView setImage:subset];
372   [self showMessage:message helpButton:NO];
373 }
374
375 - (void)presentResultForString:(NSString *)resultString {
376   self.result = [ResultParser parsedResultForString:resultString];
377   [self showMessage:[self.result stringForDisplay] helpButton:NO];
378   self.actions = self.result.actions;
379 #ifdef DEBUG
380   NSLog(@"result has %d actions", actions ? 0 : actions.count);
381 #endif
382   [self updateToolbar];
383 }
384
385 - (void)presentResultPoints:(NSArray *)resultPoints
386                    forImage:(UIImage *)image
387                 usingSubset:(UIImage *)subset {
388   // simply add the points to the image view
389   imageView.image = subset;
390   for (NSValue *pointValue in resultPoints) {
391     [imageView addResultPoint:[pointValue CGPointValue]];
392   }
393 }
394
395 - (void)decoder:(Decoder *)decoder didDecodeImage:(UIImage *)image usingSubset:(UIImage *)subset withResult:(TwoDDecoderResult *)twoDResult {
396   self.picker = nil;
397   [self presentResultForString:twoDResult.text];
398
399   [self presentResultPoints:twoDResult.points forImage:image usingSubset:subset];
400
401   // save the scan to the shared database
402   [[Database sharedDatabase] addScanWithText:twoDResult.text];
403
404   [self performResultAction:self];
405 }
406
407 - (void)decoder:(Decoder *)decoder failedToDecodeImage:(UIImage *)image usingSubset:(UIImage *)subset reason:(NSString *)reason {
408   if (self.picker && UIImagePickerControllerSourceTypeCamera == self.picker.sourceType) {
409     // If we are using the camera, and the user hasn't manually cancelled,
410     // take another snapshot and try to decode it.
411     [self takeScreenshot];
412   } else {
413     [self showMessage:reason helpButton:YES];
414     [self updateToolbar];
415   }
416 }
417
418
419 - (void)willAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
420   [super willAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
421
422   if (imageView.image) {
423     /*
424     CGRect viewBounds = imageView.bounds;
425     CGSize imageSize = imageView.image.size;
426     float scale = fmin(viewBounds.size.width / imageSize.width,
427                        viewBounds.size.height / imageSize.height);
428     float xOffset = (viewBounds.size.width - scale * imageSize.width) / 2.0;
429     float yOffset = (viewBounds.size.height - scale * imageSize.height) / 2.0;
430      */
431
432     for (UIView *view in resultPointViews) {
433       view.alpha = 0.0;
434     }
435   }
436 }
437
438 - (void)willAnimateSecondHalfOfRotationFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation duration:(NSTimeInterval)duration {
439   [super willAnimateSecondHalfOfRotationFromInterfaceOrientation:fromInterfaceOrientation duration:duration];
440
441   if (imageView.image) {
442     /*
443     CGRect viewBounds = imageView.bounds;
444     CGSize imageSize = imageView.image.size;
445     float scale = fmin(viewBounds.size.width / imageSize.width,
446                        viewBounds.size.height / imageSize.height);
447     float xOffset = (viewBounds.size.width - scale * imageSize.width) / 2.0;
448     float yOffset = (viewBounds.size.height - scale * imageSize.height) / 2.0;
449      */
450
451     for (UIView *view in resultPointViews) {
452       view.alpha = 1.0;
453     }
454   }
455 }
456
457 - (void)cancel:(id)sender {
458   self.picker = nil;
459 }
460
461 - (void)takeScreenshot {
462   if (picker) {
463     CGImageRef cgScreen = MyCGImageCopyScreenContents();
464     if (cgScreen) {
465       CGRect croppedFrame = CGRectMake(0, 0, CGImageGetWidth(cgScreen),
466           CGImageGetHeight(cgScreen) - (10+toolbar.bounds.size.height));
467       CGImageRef cgCropped = CGImageCreateWithImageInRect(cgScreen, croppedFrame);
468       if (cgCropped) {
469         UIImage *screenshot = [UIImage imageWithCGImage:cgCropped];
470         CGImageRelease(cgCropped);
471         [self.decoder decodeImage:screenshot];
472       }
473       CGImageRelease(cgScreen);
474     }
475   }
476 }
477
478 // UIImagePickerControllerDelegate methods
479
480 - (void)imagePickerController:(UIImagePickerController *)aPicker
481 didFinishPickingMediaWithInfo:(NSDictionary *)info {
482   UIImage *imageToDecode =
483     [info objectForKey:UIImagePickerControllerEditedImage];
484   if (!imageToDecode) {
485     imageToDecode = [info objectForKey:UIImagePickerControllerOriginalImage];
486   }
487   CGSize size = [imageToDecode size];
488   CGRect cropRect = CGRectMake(0.0, 0.0, size.width, size.height);
489   
490 #ifdef DEBUG
491   NSLog(@"picked image size = (%f, %f)", size.width, size.height);
492 #endif
493   NSString *systemVersion = [[UIDevice currentDevice] systemVersion];
494
495   NSValue *cropRectValue = [info objectForKey:UIImagePickerControllerCropRect];
496   if (cropRectValue) {
497     UIImage *originalImage = [info objectForKey:UIImagePickerControllerOriginalImage];
498     if (originalImage) {
499 #ifdef DEBUG
500       NSLog(@"original image size = (%f, %f)", originalImage.size.width, originalImage.size.height);
501 #endif
502        cropRect = [cropRectValue CGRectValue];
503 #ifdef DEBUG
504       NSLog(@"crop rect = (%f, %f) x (%f, %f)", CGRectGetMinX(cropRect), CGRectGetMinY(cropRect), CGRectGetWidth(cropRect), CGRectGetHeight(cropRect));
505 #endif
506       if (([picker sourceType] == UIImagePickerControllerSourceTypeSavedPhotosAlbum) &&
507           [@"2.1" isEqualToString:systemVersion]) {
508         // adjust crop rect to work around bug in iPhone OS 2.1 when selecting from the photo roll
509         cropRect.origin.x *= 2.5;
510         cropRect.origin.y *= 2.5;
511         cropRect.size.width *= 2.5;
512         cropRect.size.height *= 2.5;
513 #ifdef DEBUG
514         NSLog(@"2.1-adjusted crop rect = (%f, %f) x (%f, %f)", CGRectGetMinX(cropRect), CGRectGetMinY(cropRect), CGRectGetWidth(cropRect), CGRectGetHeight(cropRect));
515 #endif
516       }
517
518       imageToDecode = originalImage;
519     }
520   }
521
522   [imageToDecode retain];
523   self.picker = nil;
524   [self.decoder decodeImage:imageToDecode cropRect:cropRect];
525   [imageToDecode release];
526 }
527
528
529 - (void)imagePickerControllerDidCancel:(UIImagePickerController *)aPicker {
530   self.picker = nil;
531 }
532
533 - (void)setPicker:(UIImagePickerController *)aPicker {
534   if (picker != aPicker) {
535     [picker dismissModalViewControllerAnimated:YES];
536     picker = [aPicker retain];
537     [self updateToolbar];
538   }
539 }
540
541 - (void)navigationController:(UINavigationController *)navigationController
542        didShowViewController:(UIViewController *)viewController
543                     animated:(BOOL)animated {
544   // no-op
545 }
546
547 - (void)navigationController:(UINavigationController *)navigationController
548       willShowViewController:(UIViewController *)viewController
549                     animated:(BOOL)animated {
550   // no-op
551 }
552
553 - (void)performAction:(ResultAction *)action {
554   [action performActionWithController:self shouldConfirm:NO];
555 }
556
557 - (void)confirmAndPerformAction:(ResultAction *)action {
558   [action performActionWithController:self shouldConfirm:YES];
559 }
560
561
562 - (IBAction)performResultAction:(id)sender {
563   if (self.result == nil) {
564     NSLog(@"no result to perform an action on!");
565     return;
566   }
567
568   if (self.actions == nil || self.actions.count == 0) {
569     NSLog(@"result has no actions to perform!");
570     return;
571   }
572
573   if (self.actions.count == 1) {
574     ResultAction *action = [self.actions lastObject];
575 #ifdef DEBUG
576     NSLog(@"Result has the single action, (%@)  '%@', performing it",
577           NSStringFromClass([action class]), [action title]);
578 #endif
579     [self performSelector:@selector(confirmAndPerformAction:)
580                  withObject:action
581                  afterDelay:0.0];
582   } else {
583 #ifdef DEBUG
584     NSLog(@"Result has multiple actions, popping up an action sheet");
585 #endif
586     UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithFrame:self.view.bounds];
587
588     for (ResultAction *action in self.actions) {
589       [actionSheet addButtonWithTitle:[action title]];
590     }
591
592     int cancelIndex = [actionSheet addButtonWithTitle:NSLocalizedString(@"DecoderViewController cancel button title", @"Cancel")];
593     actionSheet.cancelButtonIndex = cancelIndex;
594
595     actionSheet.delegate = self;
596
597     [actionSheet showFromToolbar:self.toolbar];
598   }
599 }
600
601 - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
602   if (buttonIndex < self.actions.count) {
603     int actionIndex = buttonIndex;
604     ResultAction *action = [self.actions objectAtIndex:actionIndex];
605     [self performSelector:@selector(performAction:)
606                  withObject:action
607                  afterDelay:0.0];
608   }
609 }
610
611 - (IBAction)showArchive:(id)sender {
612   ArchiveController *archiveController = [[ArchiveController alloc] initWithDecoderViewController:self];
613   [[self navigationController] pushViewController:archiveController animated:true];
614   [archiveController release];
615 }
616
617 - (void)showScan:(Scan *)scan {
618   [self clearImageView];
619   [self presentResultForString:scan.text];
620   [[self navigationController] popToViewController:self animated:YES];
621 }
622
623 @end