diff --git a/Samples/Sample.Android/MainActivity.cs b/Samples/Sample.Android/MainActivity.cs index 0d5f17364..a6da23515 100644 --- a/Samples/Sample.Android/MainActivity.cs +++ b/Samples/Sample.Android/MainActivity.cs @@ -45,7 +45,10 @@ protected override void OnCreate(Bundle bundle) scanner.BottomText = "Wait for the barcode to automatically scan!"; //Start scanning - var result = await scanner.Scan(); + var result = await scanner.Scan( new MobileBarcodeScanningOptions + { + ScanningArea = ScanningArea.From(0f, 0.49f, 1f, 0.51f) + }); HandleScanResult(result); }; diff --git a/Samples/Sample.Forms/Sample.Forms/HomePage.cs b/Samples/Sample.Forms/Sample.Forms/HomePage.cs index 90f4fbd85..0ddb8ded8 100644 --- a/Samples/Sample.Forms/Sample.Forms/HomePage.cs +++ b/Samples/Sample.Forms/Sample.Forms/HomePage.cs @@ -4,6 +4,8 @@ using System.Text; using System.Threading.Tasks; using Xamarin.Forms; +using ZXing; +using ZXing.Mobile; using ZXing.Net.Mobile.Forms; namespace Sample.Forms @@ -17,6 +19,7 @@ public class HomePage : ContentPage Button buttonScanContinuousCustomPage; Button buttonScanCustomPage; Button buttonGenerateBarcode; + Button buttonScanMiddle1D; public HomePage() : base() @@ -130,8 +133,36 @@ public HomePage() : base() await Navigation.PushAsync(new BarcodePage()); }; + buttonScanMiddle1D = new Button + { + Text = "Scan 1D only in the middle (Android and iOS only)", + AutomationId = "barcodeMiddleScan1D" + }; + buttonScanMiddle1D.Clicked += async delegate + { + scanPage = new ZXingScannerPage(new MobileBarcodeScanningOptions + { + ScanningArea = ScanningArea.From(0f, 0.49f, 1f, 0.51f), + PossibleFormats = new List { BarcodeFormat.All_1D } + }); + + scanPage.OnScanResult += (result) => + { + scanPage.IsScanning = false; + + Device.BeginInvokeOnMainThread(async () => + { + await Navigation.PopAsync(); + await DisplayAlert("Scanned Barcode", result.Text, "OK"); + }); + }; + + await Navigation.PushAsync(scanPage); + }; + var stack = new StackLayout(); stack.Children.Add(buttonScanDefaultOverlay); + stack.Children.Add(buttonScanMiddle1D); stack.Children.Add(buttonScanCustomOverlay); stack.Children.Add(buttonScanContinuously); stack.Children.Add(buttonScanCustomPage); diff --git a/Samples/Sample.iOS/HomeViewController.cs b/Samples/Sample.iOS/HomeViewController.cs index 7287c6950..80d45d715 100644 --- a/Samples/Sample.iOS/HomeViewController.cs +++ b/Samples/Sample.iOS/HomeViewController.cs @@ -1,10 +1,6 @@ using System; using MonoTouch.Dialog; - -using Foundation; -using CoreGraphics; using UIKit; - using ZXing; using ZXing.Mobile; using System.Collections.Generic; @@ -25,10 +21,27 @@ public override void ViewDidLoad() //Create a new instance of our scanner scanner = new MobileBarcodeScanner(this.NavigationController); - Root = new RootElement("ZXing.Net.Mobile") { + Root = new RootElement("ZXing.Net.Mobile") + { new Section { + new StyledStringElement("Scan with Default View", async () => + { + //Tell our scanner to use the default overlay + scanner.UseCustomOverlay = false; + //We can customize the top and bottom text of the default overlay + scanner.TopText = "Hold camera up to barcode to scan"; + scanner.BottomText = "Barcode will automatically scan"; + + //Start scanning + var result = await scanner.Scan(new MobileBarcodeScanningOptions + { + ScanningArea = ScanningArea.From(0f, 0.3f, 1f, 0.7f) + }); - new StyledStringElement ("Scan with Default View", async () => { + HandleScanResult(result); + }), + new StyledStringElement("Scan with Default View using laser point", async () => + { //Tell our scanner to use the default overlay scanner.UseCustomOverlay = false; //We can customize the top and bottom text of the default overlay @@ -36,7 +49,10 @@ public override void ViewDidLoad() scanner.BottomText = "Barcode will automatically scan"; //Start scanning - var result = await scanner.Scan (); + var result = await scanner.Scan(new MobileBarcodeScanningOptions + { + ScanningArea = ScanningArea.From(0f, 0.49f, 1f, 0.51f) + }); HandleScanResult(result); }), @@ -145,4 +161,4 @@ public void UITestBackdoorScan(string param) }); } } -} +} \ No newline at end of file diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index 6d4f5c2be..cd073b8ae 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Android.Views; using ApxLabs.FastAndroidCamera; @@ -112,8 +111,6 @@ void DecodeFrame(FastJavaByteArray fastArray) var width = cameraParameters.PreviewSize.Width; var height = cameraParameters.PreviewSize.Height; - var barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); - var rotate = false; var newWidth = width; var newHeight = height; @@ -131,10 +128,33 @@ void DecodeFrame(FastJavaByteArray fastArray) ZXing.Result result = null; var start = PerformanceCounter.Start(); - LuminanceSource fast = new FastJavaByteArrayYUVLuminanceSource(fastArray, width, height, 0, 0, width, height); // _area.Left, _area.Top, _area.Width, _area.Height); + var scanningRect = scannerHost.ScanningOptions.ScanningArea; + if (rotate) + { + scanningRect = scanningRect.RotateCounterClockwise(); + } + + var left = (int) (width * scanningRect.StartX); + var top = (int) (height * scanningRect.StartY); + var endHeight = (int) (scanningRect.EndY * height) - top; + var endWidth = (int) (scanningRect.EndX * width) - left; + + LuminanceSource fast = + new FastJavaByteArrayYUVLuminanceSource( + fastArray, + width, + height, + left, + top, + endWidth, + endHeight); + if (rotate) + { fast = fast.rotateCounterClockwise(); + } + var barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); result = barcodeReader.Decode(fast); fastArray.Dispose(); diff --git a/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs b/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs index 789c8aa6a..580b0f019 100644 --- a/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs +++ b/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs @@ -21,12 +21,19 @@ public MobileBarcodeScanningOptions() InitialDelayBeforeAnalyzingFrames = 300; DelayBetweenContinuousScans = 1000; UseNativeScanning = false; + ScanningArea = ScanningArea.Default; } public CameraResolutionSelectorDelegate CameraResolutionSelector { get; set; } public IEnumerable PossibleFormats { get; set; } + /// + /// Narrow chosen scanning area.
+ /// Works only on iOS and Android + ///
+ public ScanningArea ScanningArea { get; set; } + public bool? TryHarder { get; set; } public bool? PureBarcode { get; set; } @@ -118,4 +125,4 @@ public CameraResolution GetResolution(List availableResolution return r; } } -} +} \ No newline at end of file diff --git a/ZXing.Net.Mobile/ScanningArea.cs b/ZXing.Net.Mobile/ScanningArea.cs new file mode 100644 index 000000000..7710edf2a --- /dev/null +++ b/ZXing.Net.Mobile/ScanningArea.cs @@ -0,0 +1,107 @@ +using System; + +namespace ZXing.Mobile +{ + /// + /// Representation of restricted scanning area in PERCENTAGE. + /// Allowed values: 0 <= value <= 1 AND startY != endY + /// Values of startY and endY are ABSOLUTE to image that means if use values of + /// startY:0.49 and endY:0.51 we will scan only 2% of the whole image + /// starting at 49% and finishing at 51% of the image height. + /// + public class ScanningArea + { + public float StartX { get; } + public float StartY { get; } + public float EndX { get; } + public float EndY { get; } + + ScanningArea(float startX, float startY, float endX, float endY) + { + //if difference between parameters is less than 1% we assume those are equal + if (Math.Abs(startY - endY) < 0.01f) + { + throw new ArgumentException($"Values of {nameof(startY)} and {nameof(endY)} cannot be the same"); + } + + //if difference between parameters is less than 1% we assume those are equal + if (Math.Abs(startX - endX) < 0.01f) + { + throw new ArgumentException($"Values of {nameof(startX)} and {nameof(endX)} cannot be the same"); + } + + //Reverse values instead of throwing argument exception + if (startY > endY) + { + var temp = endY; + endY = startY; + startY = temp; + } + + if (startX > endX) + { + var temp = endX; + endX = startX; + startX = temp; + } + + if (startY < 0) + { + startY = 0; + } + + if (endY > 1) + { + endY = 1; + } + + if (startX < 0) + { + startX = 0; + } + + if (endX > 1) + { + endX = 1; + } + + StartY = startY; + EndY = endY; + StartX = startX; + EndX = endX; + } + + public ScanningArea RotateCounterClockwise() + { + var startX = StartY; + var startY = EndX; + var endX = EndY; + var endY = StartX; + + if (startY > endY) + { + startY = 1f - startY; + endY = 1 - endY; + } + + if (startX > endX) + { + startX = 1 - startX; + endX = 1 - endX; + } + + return new ScanningArea(startX, startY, endX, endY); + } + + + static ScanningArea _default = new ScanningArea(0f, 0f, 1f, 1f); + + /// + /// Returns value that represents whole image. + /// + public static ScanningArea Default => _default; + + public static ScanningArea From(float startX, float startY, float endX, float endY) => + new ScanningArea(startX, startY, endX, endY); + } +} \ No newline at end of file diff --git a/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj b/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj index 1eaa83f3d..d8f27b0e3 100644 --- a/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj +++ b/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj @@ -49,9 +49,13 @@ + + + + @@ -126,4 +130,7 @@ + + + \ No newline at end of file diff --git a/ZXing.Net.Mobile/iOS/CVPixelBufferBGRA32LuminanceSource.ios.cs b/ZXing.Net.Mobile/iOS/CVPixelBufferBGRA32LuminanceSource.ios.cs index 23867d963..9206a136b 100644 --- a/ZXing.Net.Mobile/iOS/CVPixelBufferBGRA32LuminanceSource.ios.cs +++ b/ZXing.Net.Mobile/iOS/CVPixelBufferBGRA32LuminanceSource.ios.cs @@ -1,34 +1,202 @@ using System; -using System.Runtime.InteropServices; -using CoreVideo; -using ZXing; +using ZXing.Mobile.iOS.Helpers; namespace ZXing.Mobile { public class CVPixelBufferBGRA32LuminanceSource : BaseLuminanceSource { - public unsafe CVPixelBufferBGRA32LuminanceSource(byte* cvPixelByteArray, int cvPixelByteArrayLength, int width, int height) - : base(width, height) => CalculateLuminance(cvPixelByteArray, cvPixelByteArrayLength); + private readonly bool shouldRotate; - public CVPixelBufferBGRA32LuminanceSource(byte[] luminances, int width, int height) : base(luminances, width, height) + public CVPixelBufferBGRA32LuminanceSource( + Span cvPixelByteArray, + bool shouldRotate, + int originalImageWidth, + int originalImageHeight, + ScanningArea scanningArea) + : base(0, 0) // this is not an mistake. We calculate those values later on { + this.shouldRotate = shouldRotate; + + var destinationRect = PrepareDestinationImageRect(scanningArea, originalImageWidth, originalImageHeight); + SetupLuminanceArray(destinationRect); + + CalculateLuminance(cvPixelByteArray, originalImageWidth, destinationRect); } - unsafe void CalculateLuminance(byte* rgbRawBytes, int bytesLen) + protected CVPixelBufferBGRA32LuminanceSource(byte[] luminances, int width, int height) : base(luminances, width, + height) { - for (int rgbIndex = 0, luminanceIndex = 0; rgbIndex < bytesLen && luminanceIndex < luminances.Length; luminanceIndex++) + } + + void CalculateLuminance(Span rgbRawBytes, int originalImageWidth, Rect destinationRect) + { + if (shouldRotate) + { + CalculateLuminanceWithCroppingAndRotation(rgbRawBytes, originalImageWidth, destinationRect); + } + else { - // Calculate luminance cheaply, favoring green. - var b = rgbRawBytes[rgbIndex++]; - var g = rgbRawBytes[rgbIndex++]; - var r = rgbRawBytes[rgbIndex++]; - var alpha = rgbRawBytes[rgbIndex++]; - var luminance = (byte)((RChannelWeight * r + GChannelWeight * g + BChannelWeight * b) >> ChannelWeight); - luminances[luminanceIndex] = (byte)(((luminance * alpha) >> 8) + (255 * (255 - alpha) >> 8)); + CalculateLuminanceWithCropping(rgbRawBytes, originalImageWidth, destinationRect); } } protected override LuminanceSource CreateLuminanceSource(byte[] newLuminances, int width, int height) => new CVPixelBufferBGRA32LuminanceSource(newLuminances, width, height); + + + void CalculateLuminanceWithCroppingAndRotation( + Span rgbRawBytes, + int originalImageWidth, + Rect destinationRect) + { + for (int x = 0, y = 0, destinationX = 0, destinationY = 0, rgbIndex = 0; rgbIndex < rgbRawBytes.Length; x++) + { + //Follow the current Y. Because image in memory is represented row after row + //we increment Y each time we've read whole line + if (x == originalImageWidth) + { + x = 0; + ++y; + } + + //Check if the current coordinates are outside of destination ScanningArea. + //We flip the values because of rotation + if (destinationRect.Outside(y, x)) + { + //Pixel in memory is represented by 4 bytes (BGRA) therefore we skip whole pixel. + rgbIndex += 4; + continue; + } + + //Because of the rotation and consecutive reading row by row of original image + //we fulfill destination image column by column. + if (destinationY == destinationRect.Height) + { + destinationY = 0; + destinationX++; + } + + var index = destinationX + (destinationY * destinationRect.Width); + luminances[index] = CalculateLuminance(rgbRawBytes, ref rgbIndex); + + //Because of the rotation and consecutive reading row by row of original image + //we fulfill destination image column by column. + destinationY++; + } + } + + void CalculateLuminanceWithCropping( + Span rgbRawBytes, + int originalImageWidth, + Rect destinationRect) + { + for (int x = 0, y = 0, destinationX = 0, destinationY = 0, rgbIndex = 0; rgbIndex < rgbRawBytes.Length; x++) + { + if (x == originalImageWidth) + { + x = 0; + ++y; + } + + //Check if the current coordinates are outside of destination ScanningArea. + if (destinationRect.Outside(x, y)) + { + //Pixel in memory is represented by 4 bytes (BGRA) therefore we skip whole pixel. + rgbIndex += 4; + continue; + } + + //Fill data row by row + if (destinationX == destinationRect.Width) + { + destinationX = 0; + destinationY++; + } + + var index = destinationX + (destinationY * destinationRect.Width); + luminances[index] = CalculateLuminance(rgbRawBytes, ref rgbIndex); + + destinationX++; + } + } + + byte CalculateLuminance(Span rgbRawBytes, ref int rgbIndex) + { + // Calculate luminance cheaply, favoring green. + var b = rgbRawBytes[rgbIndex++]; + var g = rgbRawBytes[rgbIndex++]; + var r = rgbRawBytes[rgbIndex++]; + var alpha = rgbRawBytes[rgbIndex++]; + var luminance = (byte) ((RChannelWeight * r + GChannelWeight * g + BChannelWeight * b) >> + ChannelWeight); + + return (byte) (((luminance * alpha) >> 8) + (255 * (255 - alpha) >> 8)); + } + + Rect PrepareDestinationImageRect(ScanningArea area, int width, int height) + { + int left, top, right, bottom = 0; + + if (shouldRotate) + { + //this ones are flipped because we are rotating destination image + left = (int) (height * area.StartX); + right = (int) (height * area.EndX); + + top = (int) (width * area.StartY); + bottom = (int) (width * area.EndY); + + //Flip values because we are rotating destination image + var temp = height; + height = width; + width = temp; + } + else + { + left = (int) (width * area.StartX); + right = (int) (width * area.EndX); + + top = (int) (height * area.StartY); + bottom = (int) (height * area.EndY); + } + + //Internally ZXing.Net uses minimum 15 rows, so we want to match that + const int minimalAmountOfRows = 16; + if (bottom - top < minimalAmountOfRows) + { + var croppedHeight = bottom - top; + if (croppedHeight % 2 != 0) + { + ++croppedHeight; + } + + var diff = croppedHeight >> 1; + + //Compensate the difference both from bottom and top + if (bottom + diff < height) + { + bottom += diff; + top -= diff; + } + else + { + //to prevent bottom coordinate to go outside of the scope + //we move the additional pixels above. + var rest = Math.Abs(height - bottom - diff); + top -= diff + rest; + bottom += rest; + } + } + + + return new Rect(left, top, right, bottom); + } + + void SetupLuminanceArray(Rect rect) + { + Width = rect.Width; + Height = rect.Height; + luminances = new byte[Width * Height]; + } } -} +} \ No newline at end of file diff --git a/ZXing.Net.Mobile/iOS/Helpers/Rect.cs b/ZXing.Net.Mobile/iOS/Helpers/Rect.cs new file mode 100644 index 000000000..35be25ed4 --- /dev/null +++ b/ZXing.Net.Mobile/iOS/Helpers/Rect.cs @@ -0,0 +1,29 @@ +namespace ZXing.Mobile.iOS.Helpers +{ + class Rect + { + public readonly int Left; + public readonly int Right; + public readonly int Top; + public readonly int Bottom; + + public readonly int Width; + public readonly int Height; + + public Rect(int left, int top, int right, int bottom) + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + + Width = Right - Left; + Height = Bottom - Top; + } + + public bool Outside(int x, int y) + { + return Left > x || Right <= x || Top > y || Bottom <= y; + } + } +} \ No newline at end of file diff --git a/ZXing.Net.Mobile/iOS/ZXingScannerView.ios.cs b/ZXing.Net.Mobile/iOS/ZXingScannerView.ios.cs index 406cd0b4c..96dc7a6f2 100644 --- a/ZXing.Net.Mobile/iOS/ZXingScannerView.ios.cs +++ b/ZXing.Net.Mobile/iOS/ZXingScannerView.ios.cs @@ -14,9 +14,6 @@ using ObjCRuntime; using UIKit; -using ZXing.Common; -using ZXing.Mobile; - namespace ZXing.Mobile { public class ZXingScannerView : UIView, IZXingScanner, IScannerSessionHost @@ -232,7 +229,7 @@ bool SetupCaptureSession() var barcodeReader = ScanningOptions.BuildBarcodeReader(); - outputRecorder = new OutputRecorder(this, img => + outputRecorder = new OutputRecorder(shouldRotatePreviewBuffer, this, (img ) => { var ls = img; @@ -242,10 +239,6 @@ bool SetupCaptureSession() try { var perfDecode = PerformanceCounter.Start(); - - if (shouldRotatePreviewBuffer) - ls = ls.rotateCounterClockwise(); - var result = barcodeReader.Decode(ls); PerformanceCounter.Stop(perfDecode, "Decode Time: {0} ms"); @@ -402,14 +395,16 @@ public void Focus(PointF pointOfInterest) public class OutputRecorder : AVCaptureVideoDataOutputSampleBufferDelegate { - public OutputRecorder(IScannerSessionHost scannerHost, Func handleImage) : base() + public OutputRecorder(bool shouldRotateCounterClockwise, IScannerSessionHost scannerHost, Func handleImage) : base() { + this.shouldRotateCounterClockwise = shouldRotateCounterClockwise; this.handleImage = handleImage; this.scannerHost = scannerHost; } IScannerSessionHost scannerHost; - Func handleImage; + Func handleImage; + readonly bool shouldRotateCounterClockwise; DateTime lastAnalysis = DateTime.MinValue; volatile bool working = false; @@ -463,11 +458,14 @@ public override void DidOutputSampleBuffer(AVCaptureOutput captureOutput, CMSamp // Let's access the raw underlying data and create a luminance source from it unsafe { - var rawData = (byte*)pixelBuffer.BaseAddress.ToPointer(); - var rawDatalen = (int)(pixelBuffer.Height * pixelBuffer.Width * 4); //This drops 8 bytes from the original length to give us the expected length - - luminanceSource = new CVPixelBufferBGRA32LuminanceSource(rawData, rawDatalen, (int)pixelBuffer.Width, (int)pixelBuffer.Height); - } + var rawData = new Span(pixelBuffer.BaseAddress.ToPointer(), (int)(pixelBuffer.Width * pixelBuffer.Height * 4)); + luminanceSource = new CVPixelBufferBGRA32LuminanceSource( + rawData, + shouldRotateCounterClockwise, + (int)pixelBuffer.Width, + (int)pixelBuffer.Height, + scannerHost.ScanningOptions.ScanningArea); + } if (handleImage(luminanceSource)) wasScanned = true;