summaryrefslogtreecommitdiffstats
path: root/iphone/ZBarReaderView.m
diff options
context:
space:
mode:
Diffstat (limited to 'iphone/ZBarReaderView.m')
-rw-r--r--iphone/ZBarReaderView.m596
1 files changed, 596 insertions, 0 deletions
diff --git a/iphone/ZBarReaderView.m b/iphone/ZBarReaderView.m
new file mode 100644
index 0000000..60fc18d
--- /dev/null
+++ b/iphone/ZBarReaderView.m
@@ -0,0 +1,596 @@
+//------------------------------------------------------------------------
+// Copyright 2010 (c) Jeff Brown <spadix@users.sourceforge.net>
+//
+// This file is part of the ZBar Bar Code Reader.
+//
+// The ZBar Bar Code Reader is free software; you can redistribute it
+// and/or modify it under the terms of the GNU Lesser Public License as
+// published by the Free Software Foundation; either version 2.1 of
+// the License, or (at your option) any later version.
+//
+// The ZBar Bar Code Reader is distributed in the hope that it will be
+// useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser Public License for more details.
+//
+// You should have received a copy of the GNU Lesser Public License
+// along with the ZBar Bar Code Reader; if not, write to the Free
+// Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+// Boston, MA 02110-1301 USA
+//
+// http://sourceforge.net/projects/zbar
+//------------------------------------------------------------------------
+
+#import <ZBarSDK/ZBarReaderView.h>
+
+#define MODULE ZBarReaderView
+#import "debug.h"
+
+// silence warning
+@interface ZBarReaderViewImpl : NSObject
+@end
+
+@implementation ZBarReaderView
+
+@synthesize readerDelegate, tracksSymbols, trackingColor, torchMode, showsFPS,
+ zoom, maxZoom, scanCrop, previewTransform, captureReader;
+@dynamic scanner, allowsPinchZoom, enableCache, device, session;
+
++ (id) alloc
+{
+ if(self == [ZBarReaderView class]) {
+ // this is an abstract wrapper for implementation selected
+ // at compile time. replace with concrete subclass.
+ return((id)[ZBarReaderViewImpl alloc]);
+ }
+ return([super alloc]);
+}
+
+- (void) initSubviews
+{
+ assert(preview);
+
+ overlay = [CALayer new];
+ overlay.backgroundColor = [UIColor clearColor].CGColor;
+ [preview addSublayer: overlay];
+
+#ifndef NDEBUG
+ overlay.borderWidth = 2;
+ overlay.borderColor = [UIColor colorWithRed: 1
+ green: 0
+ blue: 0
+ alpha: .5].CGColor;
+ cropLayer = [CALayer new];
+ cropLayer.backgroundColor = [UIColor clearColor].CGColor;
+ cropLayer.borderWidth = 2;
+ cropLayer.borderColor = [UIColor colorWithRed: 0
+ green: 0
+ blue: 1
+ alpha: .5].CGColor;
+ [overlay addSublayer: cropLayer];
+#endif
+
+ tracking = [CALayer new];
+ tracking.opacity = 0;
+ tracking.borderWidth = 1;
+ tracking.backgroundColor = [UIColor clearColor].CGColor;
+ [overlay addSublayer: tracking];
+
+ trackingColor = [[UIColor greenColor]
+ retain];
+ tracking.borderColor = trackingColor.CGColor;
+
+ fpsView = [UIView new];
+ fpsView.backgroundColor = [UIColor colorWithWhite: 0
+ alpha: .333];
+ fpsView.layer.cornerRadius = 12;
+ fpsView.hidden = YES;
+ [self addSubview: fpsView];
+
+ fpsLabel = [[UILabel alloc]
+ initWithFrame: CGRectMake(0, 0, 80, 32)];
+ fpsLabel.backgroundColor = [UIColor clearColor];
+ fpsLabel.textColor = [UIColor colorWithRed: .333
+ green: .666
+ blue: 1
+ alpha: 1];
+ fpsLabel.font = [UIFont systemFontOfSize: 18];
+ fpsLabel.textAlignment = UITextAlignmentRight;
+ [fpsView addSubview: fpsLabel];
+
+ self.zoom = 1.25;
+}
+
+- (void) _initWithImageScanner: (ZBarImageScanner*) scanner
+{
+ assert(scanner);
+
+ tracksSymbols = YES;
+ interfaceOrientation = UIInterfaceOrientationPortrait;
+ torchMode = 2; // AVCaptureTorchModeAuto
+ scanCrop = effectiveCrop = CGRectMake(0, 0, 1, 1);
+ imageScale = 1;
+ previewTransform = CGAffineTransformIdentity;
+ maxZoom = 2;
+
+ pinch = [[UIPinchGestureRecognizer alloc]
+ initWithTarget: self
+ action: @selector(handlePinch)];
+ [self addGestureRecognizer: pinch];
+}
+
+- (id) initWithImageScanner: (ZBarImageScanner*) scanner
+{
+ self = [super initWithFrame: CGRectMake(0, 0, 320, 426)];
+ if(!self)
+ return(nil);
+
+ self.backgroundColor = [UIColor blackColor];
+ self.contentMode = UIViewContentModeScaleAspectFill;
+ self.clipsToBounds = YES;
+ self.autoresizingMask =
+ UIViewAutoresizingFlexibleWidth |
+ UIViewAutoresizingFlexibleHeight;
+
+ [self _initWithImageScanner: scanner];
+ return(self);
+}
+
+- (id) init
+{
+ ZBarImageScanner *scanner =
+ [[ZBarImageScanner new]
+ autorelease];
+ self = [self initWithImageScanner: scanner];
+ if(!self)
+ return(nil);
+
+ [scanner setSymbology: 0
+ config: ZBAR_CFG_X_DENSITY
+ to: 3];
+ [scanner setSymbology: 0
+ config: ZBAR_CFG_Y_DENSITY
+ to: 3];
+ return(self);
+}
+
+- (id) initWithCoder: (NSCoder*) decoder
+{
+ self = [super initWithCoder: decoder];
+ if(!self)
+ return(nil);
+ ZBarImageScanner *scanner =
+ [[ZBarImageScanner new]
+ autorelease];
+ [self _initWithImageScanner: scanner];
+
+ [scanner setSymbology: 0
+ config: ZBAR_CFG_X_DENSITY
+ to: 3];
+ [scanner setSymbology: 0
+ config: ZBAR_CFG_Y_DENSITY
+ to: 3];
+ return(self);
+}
+
+- (void) dealloc
+{
+ [preview removeFromSuperlayer];
+ [preview release];
+ preview = nil;
+ [overlay release];
+ overlay = nil;
+ [cropLayer release];
+ cropLayer = nil;
+ [tracking release];
+ tracking = nil;
+ [trackingColor release];
+ trackingColor = nil;
+ [fpsLabel release];
+ fpsLabel = nil;
+ [fpsView release];
+ fpsView = nil;
+ [pinch release];
+ pinch = nil;
+ [super dealloc];
+}
+
+- (void) resetTracking
+{
+ [tracking removeAllAnimations];
+ [CATransaction begin];
+ [CATransaction setDisableActions: YES];
+ CGSize size = overlay.bounds.size;
+ CGRect crop = effectiveCrop;
+ tracking.frame = CGRectMake(crop.origin.x * size.width,
+ crop.origin.y * size.height,
+ crop.size.width * size.width,
+ crop.size.height * size.height);
+ tracking.opacity = 0;
+ [CATransaction commit];
+}
+
+- (void) updateCrop
+{
+}
+
+static inline CGFloat rotationForInterfaceOrientation (int orient)
+{
+ // resolve camera/device image orientation to view/interface orientation
+ switch(orient)
+ {
+ case UIInterfaceOrientationLandscapeLeft:
+ return(M_PI_2);
+ case UIInterfaceOrientationPortraitUpsideDown:
+ return(M_PI);
+ case UIInterfaceOrientationLandscapeRight:
+ return(3 * M_PI_2);
+ case UIInterfaceOrientationPortrait:
+ return(2 * M_PI);
+ }
+ return(0);
+}
+
+- (void) layoutSubviews
+{
+ CGRect bounds = self.bounds;
+ if(!bounds.size.width || !bounds.size.height)
+ return;
+
+ [CATransaction begin];
+ if(animationDuration) {
+ [CATransaction setAnimationDuration: animationDuration];
+ [CATransaction setAnimationTimingFunction:
+ [CAMediaTimingFunction functionWithName:
+ kCAMediaTimingFunctionEaseInEaseOut]];
+ }
+ else
+ [CATransaction setDisableActions: YES];
+
+ [super layoutSubviews];
+ fpsView.frame = CGRectMake(bounds.size.width - 80, bounds.size.height - 32,
+ 80 + 12, 32 + 12);
+
+ // orient view bounds to match camera image
+ CGSize psize;
+ if(UIInterfaceOrientationIsPortrait(interfaceOrientation))
+ psize = CGSizeMake(bounds.size.height, bounds.size.width);
+ else
+ psize = bounds.size;
+
+ // calculate scale from view coordinates to image coordinates
+ // FIXME assumes AVLayerVideoGravityResizeAspectFill
+ CGFloat scalex = imageSize.width / psize.width;
+ CGFloat scaley = imageSize.height / psize.height;
+ imageScale = (scalex < scaley) ? scalex : scaley;
+ if(!imageScale)
+ imageScale = 1;
+ // apply zoom
+ imageScale /= zoom;
+
+ // scale crop by zoom factor
+ CGFloat z = 1 / zoom;
+ CGFloat t = (1 - z) / 2;
+ CGRect zoomCrop =
+ CGRectMake(scanCrop.origin.x * z + t,
+ scanCrop.origin.y * z + t,
+ scanCrop.size.width * z,
+ scanCrop.size.height * z);
+
+ // convert effective preview area to normalized image coordinates
+ CGRect previewCrop;
+ if(scalex < scaley && imageSize.height)
+ previewCrop.size =
+ CGSizeMake(z, psize.height * imageScale / imageSize.height);
+ else if(imageSize.width)
+ previewCrop.size =
+ CGSizeMake(psize.width * imageScale / imageSize.width, z);
+ else
+ previewCrop.size = CGSizeMake(1, 1);
+ previewCrop.origin = CGPointMake((1 - previewCrop.size.width) / 2,
+ (1 - previewCrop.size.height) / 2);
+
+ // clip crop to visible preview area
+ effectiveCrop = CGRectIntersection(zoomCrop, previewCrop);
+ if(CGRectIsNull(effectiveCrop))
+ effectiveCrop = zoomCrop;
+
+ // size preview to match image in view coordinates
+ CGFloat viewScale = 1 / imageScale;
+ if(imageSize.width && imageSize.height)
+ psize = CGSizeMake(imageSize.width * viewScale,
+ imageSize.height * viewScale);
+
+ preview.bounds = CGRectMake(0, 0, psize.height, psize.width);
+ // center preview in view
+ preview.position = CGPointMake(bounds.size.width / 2,
+ bounds.size.height / 2);
+
+ CGFloat angle = rotationForInterfaceOrientation(interfaceOrientation);
+ CATransform3D xform =
+ CATransform3DMakeAffineTransform(previewTransform);
+ preview.transform = CATransform3DRotate(xform, angle, 0, 0, 1);
+
+ // scale overlay to match actual image
+ if(imageSize.width && imageSize.height)
+ overlay.bounds = CGRectMake(0, 0, imageSize.width, imageSize.height);
+ else
+ overlay.bounds = CGRectMake(0, 0, psize.width, psize.height);
+ // center overlay in preview
+ overlay.position = CGPointMake(psize.height / 2, psize.width / 2);
+
+ // image coordinates rotated from preview
+ xform = CATransform3DMakeRotation(M_PI_2, 0, 0, 1);
+ overlay.transform = CATransform3DScale(xform, viewScale, viewScale, 1);
+ tracking.borderWidth = imageScale;
+
+#ifndef NDEBUG
+ preview.backgroundColor = [UIColor yellowColor].CGColor;
+ overlay.borderWidth = 2 * imageScale;
+ cropLayer.borderWidth = 2 * imageScale;
+ cropLayer.frame = CGRectMake(effectiveCrop.origin.x * imageSize.width,
+ effectiveCrop.origin.y * imageSize.height,
+ effectiveCrop.size.width * imageSize.width,
+ effectiveCrop.size.height * imageSize.height);
+ zlog(@"layoutSubviews: bounds=%@ orient=%d image=%@ crop=%@ zoom=%g\n"
+ @"=> preview=%@ crop=(z%@ p%@ %@ i%@) scale=%g %c %g = 1/%g",
+ NSStringFromCGSize(bounds.size), interfaceOrientation,
+ NSStringFromCGSize(imageSize), NSStringFromCGRect(scanCrop), zoom,
+ NSStringFromCGSize(psize), NSStringFromCGRect(zoomCrop),
+ NSStringFromCGRect(previewCrop), NSStringFromCGRect(effectiveCrop),
+ NSStringFromCGRect(cropLayer.frame),
+ scalex, (scalex > scaley) ? '>' : '<', scaley, viewScale);
+#endif
+
+ [self resetTracking];
+ [self updateCrop];
+
+ [CATransaction commit];
+ animationDuration = 0;
+}
+
+- (void) setImageSize: (CGSize) size
+{
+ zlog(@"imageSize=%@", NSStringFromCGSize(size));
+ imageSize = size;
+
+ // FIXME bug in AVCaptureVideoPreviewLayer fails to update preview location
+ preview.bounds = CGRectMake(0, 0, size.width, size.height);
+
+ [self setNeedsLayout];
+}
+
+- (void) willRotateToInterfaceOrientation: (UIInterfaceOrientation) orient
+ duration: (NSTimeInterval) duration
+{
+ if(interfaceOrientation != orient) {
+ zlog(@"orient=%d #%g", orient, duration);
+ interfaceOrientation = orient;
+ animationDuration = duration;
+ }
+}
+
+- (void) setScanCrop: (CGRect) r
+{
+ if(CGRectEqualToRect(scanCrop, r))
+ return;
+ scanCrop = r;
+ [self setNeedsLayout];
+}
+
+- (void) setTracksSymbols: (BOOL) track
+{
+ if(track == tracksSymbols)
+ return;
+ tracksSymbols = track;
+ [self resetTracking];
+}
+
+- (BOOL) allowsPinchZoom
+{
+ return(pinch.enabled);
+}
+
+- (void) setAllowsPinchZoom: (BOOL) enabled
+{
+ pinch.enabled = enabled;
+}
+
+- (void) setTrackingColor: (UIColor*) color
+{
+ if(!color)
+ return;
+ [color retain];
+ [trackingColor release];
+ trackingColor = color;
+ tracking.borderColor = color.CGColor;
+}
+
+- (void) setShowsFPS: (BOOL) show
+{
+ if(show == showsFPS)
+ return;
+ fpsView.hidden = !show;
+}
+
+- (void) setZoom: (CGFloat) z
+{
+ if(z < 1.0)
+ z = 1.0;
+ if(z > maxZoom)
+ z = maxZoom;
+ if(z == zoom)
+ return;
+ zoom = z;
+
+ [self setNeedsLayout];
+}
+
+- (void) setZoom: (CGFloat) z
+ animated: (BOOL) animated
+{
+ [CATransaction begin];
+ if(animated) {
+ [CATransaction setAnimationDuration: .1];
+ [CATransaction setAnimationTimingFunction:
+ [CAMediaTimingFunction functionWithName:
+ kCAMediaTimingFunctionLinear]];
+ }
+ else
+ [CATransaction setDisableActions: YES];
+ // FIXME animate from current value
+ self.zoom = z;
+ [self layoutIfNeeded];
+ [CATransaction commit];
+}
+
+- (void) setPreviewTransform: (CGAffineTransform) xfrm
+{
+ previewTransform = xfrm;
+ [self setNeedsLayout];
+}
+
+- (void) start
+{
+ if(started)
+ return;
+ started = YES;
+
+ [self resetTracking];
+ fpsLabel.text = @"--- fps ";
+
+ [[UIDevice currentDevice]
+ beginGeneratingDeviceOrientationNotifications];
+}
+
+- (void) stop
+{
+ if(!started)
+ return;
+ started = NO;
+
+ [[UIDevice currentDevice]
+ endGeneratingDeviceOrientationNotifications];
+}
+
+- (void) flushCache
+{
+}
+
+// UIGestureRecognizer callback
+
+- (void) handlePinch
+{
+ if(pinch.state == UIGestureRecognizerStateBegan)
+ zoom0 = zoom;
+ CGFloat z = zoom0 * pinch.scale;
+ [self setZoom: z
+ animated: YES];
+
+ if((zoom < 1.5) != (z < 1.5)) {
+ int d = (z < 1.5) ? 3 : 2;
+ ZBarImageScanner *scanner = self.scanner;
+ @synchronized(scanner) {
+ [scanner setSymbology: 0
+ config: ZBAR_CFG_X_DENSITY
+ to: d];
+ [scanner setSymbology: 0
+ config: ZBAR_CFG_Y_DENSITY
+ to: d];
+ }
+ }
+}
+
+- (void) updateTracking: (CALayer*) trk
+ withSymbol: (ZBarSymbol*) sym
+{
+ if(!sym)
+ return;
+
+ CGRect r = sym.bounds;
+ if(r.size.width <= 32 && r.size.height <= 32)
+ return;
+ r = CGRectInset(r, -24, -24);
+
+ CALayer *current = trk.presentationLayer;
+ CGPoint cp = current.position;
+ CGPoint p = CGPointMake(CGRectGetMidX(r), CGRectGetMidY(r));
+ p = CGPointMake((p.x * 3 + cp.x) / 4, (p.y * 3 + cp.y) / 4);
+
+ CGRect cr = current.bounds;
+ r.origin = cr.origin;
+ r.size.width = (r.size.width * 3 + cr.size.width) / 4;
+ r.size.height = (r.size.height * 3 + cr.size.height) / 4;
+
+ CAMediaTimingFunction *linear =
+ [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear];
+
+ CABasicAnimation *resize =
+ [CABasicAnimation animationWithKeyPath: @"bounds"];
+ resize.fromValue = [NSValue valueWithCGRect: cr];
+ resize.toValue = [NSValue valueWithCGRect: r];
+ resize.duration = .2;
+ resize.timingFunction = linear;
+ resize.fillMode = kCAFillModeForwards;
+ resize.removedOnCompletion = NO;
+
+ CABasicAnimation *move =
+ [CABasicAnimation animationWithKeyPath: @"position"];
+ move.fromValue = [NSValue valueWithCGPoint: cp];
+ move.toValue = [NSValue valueWithCGPoint: p];
+ move.duration = .2;
+ move.timingFunction = linear;
+ move.fillMode = kCAFillModeForwards;
+ move.removedOnCompletion = NO;
+
+ CABasicAnimation *on =
+ [CABasicAnimation animationWithKeyPath: @"opacity"];
+ on.fromValue = [NSNumber numberWithDouble: current.opacity];
+ on.toValue = [NSNumber numberWithDouble: 1];
+ on.duration = .2;
+ on.timingFunction = linear;
+ on.fillMode = kCAFillModeForwards;
+ on.removedOnCompletion = NO;
+
+ CABasicAnimation *off = nil;
+ if(!TARGET_IPHONE_SIMULATOR) {
+ off = [CABasicAnimation animationWithKeyPath: @"opacity"];
+ off.fromValue = [NSNumber numberWithDouble: 1];
+ off.toValue = [NSNumber numberWithDouble: 0];
+ off.beginTime = .5;
+ off.duration = .5;
+ off.timingFunction = linear;
+ }
+
+ CAAnimationGroup *group = [CAAnimationGroup animation];
+ group.animations = [NSArray arrayWithObjects: resize, move, on, off, nil];
+ group.duration = 1;
+ group.fillMode = kCAFillModeForwards;
+ group.removedOnCompletion = !TARGET_IPHONE_SIMULATOR;
+ [trk addAnimation: group
+ forKey: @"tracking"];
+}
+
+- (void) didTrackSymbols: (ZBarSymbolSet*) syms
+{
+ if(!tracksSymbols)
+ return;
+
+ int n = syms.count;
+ assert(n);
+ if(!n)
+ return;
+
+ ZBarSymbol *sym = nil;
+ for(ZBarSymbol *s in syms)
+ if(!sym || s.type == ZBAR_QRCODE || s.quality > sym.quality)
+ sym = s;
+ assert(sym);
+ if(!sym)
+ return;
+
+ [self updateTracking: tracking
+ withSymbol: sym];
+}
+
+@end