diff --git a/.gitignore b/.gitignore index 740321b79..6572e8060 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,6 @@ TestCloud/ .vs/ project.lock.json -tools/ .nugetapikey .mygetapikey *.xam diff --git a/Samples/Sample.Android/Sample.Android.csproj b/Samples/Sample.Android/Sample.Android.csproj index b0c9c7d8b..8454a3062 100644 --- a/Samples/Sample.Android/Sample.Android.csproj +++ b/Samples/Sample.Android/Sample.Android.csproj @@ -89,7 +89,6 @@ - 1.3.0.4 diff --git a/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj b/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj index dcacc25c9..8d720a563 100644 --- a/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj +++ b/Samples/Sample.Forms/Sample.Forms.Android/Sample.Forms.Android.csproj @@ -109,4 +109,9 @@ + + + + + \ No newline at end of file diff --git a/Samples/Sample.Forms/Sample.Forms/Sample.Forms.csproj b/Samples/Sample.Forms/Sample.Forms/Sample.Forms.csproj index 8eaf1848a..19de08fda 100644 --- a/Samples/Sample.Forms/Sample.Forms/Sample.Forms.csproj +++ b/Samples/Sample.Forms/Sample.Forms/Sample.Forms.csproj @@ -13,7 +13,7 @@ - + diff --git a/Tools/ImageReceiver/ImageReceiver.csproj b/Tools/ImageReceiver/ImageReceiver.csproj new file mode 100644 index 000000000..c78c9c7e8 --- /dev/null +++ b/Tools/ImageReceiver/ImageReceiver.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Tools/ImageReceiver/Program.cs b/Tools/ImageReceiver/Program.cs new file mode 100644 index 000000000..896d25c19 --- /dev/null +++ b/Tools/ImageReceiver/Program.cs @@ -0,0 +1,37 @@ + +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +var picNum = 0; + +var imagePath = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!, "Images"); +Directory.CreateDirectory(imagePath); +Console.WriteLine($"Saving images to: {imagePath}"); + + +app.MapPost("/", async context => +{ + try + { + var filePrefix = "File"; + if (context.Request.Headers.TryGetValue("FileName", out var newFilePrefix)) + { + filePrefix = newFilePrefix; + } + + using (var fs = File.Create(Path.Combine(imagePath, $"{filePrefix}_{picNum}.bmp"))) + { + picNum++; + await context.Request.Body.CopyToAsync(fs); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Failed to save File_{picNum - 1}.bmp"); + Console.Error.WriteLine(ex.Message); + } + + context.Response.StatusCode = 200; +}); + +app.Run(); \ No newline at end of file diff --git a/Tools/ImageReceiver/Properties/launchSettings.json b/Tools/ImageReceiver/Properties/launchSettings.json new file mode 100644 index 000000000..76b7d4176 --- /dev/null +++ b/Tools/ImageReceiver/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:63208", + "sslPort": 0 + } + }, + "profiles": { + "ImageReceiver": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5390", + "dotnetRunMessages": true + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Tools/ImageReceiver/appsettings.Development.json b/Tools/ImageReceiver/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Tools/ImageReceiver/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Tools/ImageReceiver/appsettings.json b/Tools/ImageReceiver/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/Tools/ImageReceiver/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj b/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj index 95095e523..374cb7d3e 100644 --- a/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj +++ b/ZXing.Net.Mobile.Forms/ZXing.Net.Mobile.Forms.csproj @@ -89,7 +89,7 @@ - + diff --git a/ZXing.Net.Mobile.sln b/ZXing.Net.Mobile.sln index b33050cd7..c4914e227 100644 --- a/ZXing.Net.Mobile.sln +++ b/ZXing.Net.Mobile.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29806.167 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZXing.Net.Mobile", "ZXing.Net.Mobile\ZXing.Net.Mobile.csproj", "{8B7A8AB6-35A4-4C9C-83E1-96BA8BE3C941}" EndProject @@ -32,6 +32,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Forms.UWP", "Samples EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Forms.Tizen", "Samples\Sample.Forms\Sample.Forms.Tizen\Sample.Forms.Tizen.csproj", "{9CBD2F34-9649-48DE-9B35-0D1291F4E714}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{09527748-5F83-45A9-B1A4-DB3C85D136FB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageReceiver", "Tools\ImageReceiver\ImageReceiver.csproj", "{47387C60-8776-44DE-A73E-8D6C72D9BC43}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -686,6 +690,62 @@ Global {9CBD2F34-9649-48DE-9B35-0D1291F4E714}.Release|x64.Build.0 = Release|Any CPU {9CBD2F34-9649-48DE-9B35-0D1291F4E714}.Release|x86.ActiveCfg = Release|Any CPU {9CBD2F34-9649-48DE-9B35-0D1291F4E714}.Release|x86.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|ARM.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|ARM64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|ARM64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|iPhone.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|x64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|x64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|x86.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.AppStore|x86.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|ARM.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|ARM.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|ARM64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|iPhone.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|x64.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|x64.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|x86.ActiveCfg = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Debug|x86.Build.0 = Debug|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|Any CPU.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|ARM.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|ARM.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|ARM64.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|ARM64.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|iPhone.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|iPhone.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|x64.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|x64.Build.0 = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|x86.ActiveCfg = Release|Any CPU + {47387C60-8776-44DE-A73E-8D6C72D9BC43}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -699,6 +759,7 @@ Global {CFF9673E-1188-4646-BCC7-F7601F3A1D1A} = {E0DF8E5D-AF49-43C9-9921-D67268E75964} {96FBFDD1-F91A-44F6-962A-51E1AC7AAC63} = {E0DF8E5D-AF49-43C9-9921-D67268E75964} {9CBD2F34-9649-48DE-9B35-0D1291F4E714} = {E0DF8E5D-AF49-43C9-9921-D67268E75964} + {47387C60-8776-44DE-A73E-8D6C72D9BC43} = {09527748-5F83-45A9-B1A4-DB3C85D136FB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {887F72FF-99D0-4882-A73A-A913A0534F0C} diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs index b8939d2b8..75a40dc68 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraAnalyzer.android.cs @@ -1,155 +1,156 @@ using System; using System.Threading.Tasks; +using Android.Content; using Android.Views; -using ApxLabs.FastAndroidCamera; +using ZXing.Net.Mobile.Android; namespace ZXing.Mobile.CameraAccess { - public class CameraAnalyzer - { - readonly CameraController cameraController; - readonly CameraEventsListener cameraEventListener; - Task processingTask; - DateTime lastPreviewAnalysis = DateTime.UtcNow; - bool wasScanned; - readonly IScannerSessionHost scannerHost; - BarcodeReaderGeneric barcodeReader; - - public CameraAnalyzer(SurfaceView surfaceView, IScannerSessionHost scannerHost) - { - this.scannerHost = scannerHost; - cameraEventListener = new CameraEventsListener(); - cameraController = new CameraController(surfaceView, cameraEventListener, scannerHost); - Torch = new Torch(cameraController, surfaceView.Context); - } - - public Action BarcodeFound; - - public Torch Torch { get; } - - public bool IsAnalyzing { get; private set; } - - public void PauseAnalysis() - => IsAnalyzing = false; - - public void ResumeAnalysis() - => IsAnalyzing = true; - - public void ShutdownCamera() - { - IsAnalyzing = false; - cameraEventListener.OnPreviewFrameReady -= HandleOnPreviewFrameReady; - cameraController.ShutdownCamera(); - } - - public void SetupCamera() - { - cameraEventListener.OnPreviewFrameReady += HandleOnPreviewFrameReady; - cameraController.SetupCamera(); - barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); - } - - public void AutoFocus() - => cameraController.AutoFocus(); - - public void AutoFocus(int x, int y) - => cameraController.AutoFocus(x, y); - - public void RefreshCamera() - => cameraController.RefreshCamera(); - - bool CanAnalyzeFrame - { - get - { - if (!IsAnalyzing) - return false; - - //Check and see if we're still processing a previous frame - // todo: check if we can run as many as possible or mby run two analyzers at once (Vision + ZXing) - if (processingTask != null && !processingTask.IsCompleted) - return false; - - var elapsedTimeMs = (DateTime.UtcNow - lastPreviewAnalysis).TotalMilliseconds; - if (elapsedTimeMs < scannerHost.ScanningOptions.DelayBetweenAnalyzingFrames) - return false; - - // Delay a minimum between scans - if (wasScanned && elapsedTimeMs < scannerHost.ScanningOptions.DelayBetweenContinuousScans) - return false; - - return true; - } - } - - void HandleOnPreviewFrameReady(object sender, FastJavaByteArray fastArray) - { - if (!CanAnalyzeFrame) - return; - - wasScanned = false; - lastPreviewAnalysis = DateTime.UtcNow; - - processingTask = Task.Run(() => - { - try - { - DecodeFrame(fastArray); - } - catch (Exception ex) - { - Console.WriteLine(ex); - } - }).ContinueWith(task => - { - if (task.IsFaulted) - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "DecodeFrame exception occurs"); - }, TaskContinuationOptions.OnlyOnFaulted); - } - - void DecodeFrame(FastJavaByteArray fastArray) - { - var resolution = cameraController.CameraResolution; - var width = resolution.Width; - var height = resolution.Height; - - var rotate = false; - var newWidth = width; - var newHeight = height; - - // use last value for performance gain - var cDegrees = cameraController.LastCameraDisplayOrientationDegree; - - if (cDegrees == 90 || cDegrees == 270) - { - rotate = true; - newWidth = height; - newHeight = width; - } - - var start = PerformanceCounter.Start(); - - LuminanceSource fast = new FastJavaByteArrayYUVLuminanceSource(fastArray, width, height, 0, 0, width, height); // _area.Left, _area.Top, _area.Width, _area.Height); - if (rotate) - fast = fast.rotateCounterClockwise(); - - var result = barcodeReader.Decode(fast); - - fastArray.Dispose(); - fastArray = null; - - PerformanceCounter.Stop(start, - "Decode Time: {0} ms (width: " + width + ", height: " + height + ", degrees: " + cDegrees + ", rotate: " + - rotate + ")"); - - if (result != null) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Barcode Found"); - - wasScanned = true; - BarcodeFound?.Invoke(result); - return; - } - } - } -} \ No newline at end of file + public class CameraAnalyzer + { + readonly Context context; + readonly CameraController cameraController; + readonly CameraEventsListener cameraEventListener; + readonly DeviceOrientationEventListener orientationEventListener; + Task processingTask; + DateTime lastPreviewAnalysis = DateTime.UtcNow; + bool wasScanned; + readonly IScannerSessionHost scannerHost; + BarcodeReaderGeneric barcodeReader; + + public CameraAnalyzer(SurfaceView surfaceView, IScannerSessionHost scannerHost) + { + context = surfaceView.Context; + this.scannerHost = scannerHost; + cameraEventListener = new CameraEventsListener(); + orientationEventListener = new DeviceOrientationEventListener(context, Android.Hardware.SensorDelay.Normal); + cameraController = new CameraController(surfaceView, cameraEventListener, scannerHost); + Torch = new Torch(cameraController, surfaceView.Context); + } + + public Action BarcodeFound; + + public Torch Torch { get; } + + public bool IsAnalyzing { get; private set; } + + public void PauseAnalysis() + => IsAnalyzing = false; + + public void ResumeAnalysis() + => IsAnalyzing = true; + + public void ShutdownCamera() + { + IsAnalyzing = false; + cameraEventListener.OnPreviewFrameReady -= HandleOnPreviewFrameReady; + orientationEventListener.Disable(); + cameraController.ShutdownCamera(); + } + + public void SetupCamera() + { + cameraEventListener.OnPreviewFrameReady += HandleOnPreviewFrameReady; + if (orientationEventListener.CanDetectOrientation()) + { + orientationEventListener.Enable(); + } + + barcodeReader = scannerHost.ScanningOptions.BuildBarcodeReader(); + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Created Barcode Reader"); + + cameraController.SetupCamera(); + } + + public void AutoFocus() + => cameraController.AutoFocus(); + + public void AutoFocus(int x, int y) + => cameraController.AutoFocus(x, y); + + public void RefreshCamera() + { + cameraController.RefreshCamera(); + } + + bool CanAnalyzeFrame + { + get + { + if (!IsAnalyzing) + return false; + + //Check and see if we're still processing a previous frame + // todo: check if we can run as many as possible or mby run two analyzers at once (Vision + ZXing) + if (processingTask != null && !processingTask.IsCompleted) + return false; + + var elapsedTimeMs = (DateTime.UtcNow - lastPreviewAnalysis).TotalMilliseconds; + if (elapsedTimeMs < scannerHost.ScanningOptions.DelayBetweenAnalyzingFrames) + return false; + + // Delay a minimum between scans + if (wasScanned && elapsedTimeMs < scannerHost.ScanningOptions.DelayBetweenContinuousScans) + return false; + + return true; + } + } + + void HandleOnPreviewFrameReady(object sender, CapturedImageData data) + { + if (!CanAnalyzeFrame) + return; + + wasScanned = false; + lastPreviewAnalysis = DateTime.UtcNow; + + processingTask = Task.Run(() => + { + try + { + DecodeFrame(data); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + }).ContinueWith(task => + { + if (task.IsFaulted) + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "DecodeFrame exception occurs"); + }, TaskContinuationOptions.OnlyOnFaulted); + } + + void DecodeFrame(CapturedImageData data) + { + var start = PerformanceCounter.Start(); + var orientationData = new DeviceOrientationData(context.Resources.Configuration.Orientation, orientationEventListener.Orientation, cameraController.SensorRotation); + var source = new PlanarNV21LuminanceSource(data.Matrix, data.Width, data.Height, orientationData, (!barcodeReader.AutoRotate && orientationEventListener.IsEnabled)); + var initPerformance = PerformanceCounter.Stop(start); + + //DebugHelper.SendNV21toJPEGToEndpoint(source.Matrix, source.Width, source.Height, "https://local.imagereceiver.ip:5390"); + + start = PerformanceCounter.Start(); + var result = barcodeReader.Decode(source); + Android.Util.Log.Debug( + MobileBarcodeScanner.TAG, + "Decode Time: {0} ms (Width: {1}, Height: {2}, Rotations (S/D): {3} / {4}), Source setup: {5} ms", + PerformanceCounter.Stop(start).Milliseconds, + data.Width, + data.Height, + orientationData.SensorRotation, + orientationData.DeviceOrientation, + initPerformance.Milliseconds); + + if (result != null) + { + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Barcode Found"); + + wasScanned = true; + BarcodeFound?.Invoke(result); + return; + } + } + } +} diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraCaptureStateListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraCaptureStateListener.android.cs new file mode 100644 index 000000000..5fb1b5b9d --- /dev/null +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraCaptureStateListener.android.cs @@ -0,0 +1,22 @@ +using System; +using Android.Hardware.Camera2; + +namespace ZXing.Mobile.CameraAccess +{ + public class CameraCaptureStateListener : CameraCaptureSession.StateCallback + { + public Action OnConfigureFailedAction; + + public Action OnConfiguredAction; + + public override void OnConfigureFailed(CameraCaptureSession session) + { + OnConfigureFailedAction?.Invoke(session); + } + + public override void OnConfigured(CameraCaptureSession session) + { + OnConfiguredAction?.Invoke(session); + } + } +} diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs index 71354dae5..c35b0cce9 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraController.android.cs @@ -1,435 +1,472 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Android.Content; using Android.Graphics; -using Android.Hardware; +using Android.Hardware.Camera2; +using Android.Hardware.Camera2.Params; +using Android.Media; using Android.OS; using Android.Runtime; +using Android.Util; using Android.Views; -using ApxLabs.FastAndroidCamera; -using Camera = Android.Hardware.Camera; +using Java.Lang; namespace ZXing.Mobile.CameraAccess { - public class CameraController - { - readonly Context context; - readonly ISurfaceHolder holder; - readonly SurfaceView surfaceView; - readonly CameraEventsListener cameraEventListener; - int cameraId; - IScannerSessionHost scannerHost; - - public CameraController(SurfaceView surfaceView, CameraEventsListener cameraEventListener, IScannerSessionHost scannerHost) - { - context = surfaceView.Context; - holder = surfaceView.Holder; - this.surfaceView = surfaceView; - this.cameraEventListener = cameraEventListener; - this.scannerHost = scannerHost; - } - - public Camera Camera { get; private set; } - - public CameraResolution CameraResolution { get; private set; } - - public int LastCameraDisplayOrientationDegree { get; private set; } - - public void RefreshCamera() - { - if (holder == null) return; - - ApplyCameraSettings(); - - try - { - Camera.SetPreviewDisplay(holder); - Camera.StartPreview(); - } - catch (Exception ex) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, ex.ToString()); - } - } - - public void SetupCamera() - { - if (Camera != null) - return; - - var perf = PerformanceCounter.Start(); - OpenCamera(); - PerformanceCounter.Stop(perf, "Setup Camera took {0}ms"); - - if (Camera == null) return; - - perf = PerformanceCounter.Start(); - ApplyCameraSettings(); - - try - { - Camera.SetPreviewDisplay(holder); - - - var previewParameters = Camera.GetParameters(); - var previewSize = previewParameters.PreviewSize; - var bitsPerPixel = ImageFormat.GetBitsPerPixel(previewParameters.PreviewFormat); - - - var bufferSize = (previewSize.Width * previewSize.Height * bitsPerPixel) / 8; - const int NUM_PREVIEW_BUFFERS = 5; - for (uint i = 0; i < NUM_PREVIEW_BUFFERS; ++i) - { - using (var buffer = new FastJavaByteArray(bufferSize)) - Camera.AddCallbackBuffer(buffer); - } - - Camera.StartPreview(); - - Camera.SetNonMarshalingPreviewCallback(cameraEventListener); - } - catch (Exception ex) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, ex.ToString()); - return; - } - finally - { - PerformanceCounter.Stop(perf, "Setup Camera Parameters took {0}ms"); - } - - // Docs suggest if Auto or Macro modes, we should invoke AutoFocus at least once - var currentFocusMode = Camera.GetParameters().FocusMode; - if (currentFocusMode == Camera.Parameters.FocusModeAuto - || currentFocusMode == Camera.Parameters.FocusModeMacro) - AutoFocus(); - } - - public void AutoFocus() - { - AutoFocus(0, 0, false); - } - - public void AutoFocus(int x, int y) - { - // The bounds for focus areas are actually -1000 to 1000 - // So we need to translate the touch coordinates to this scale - var focusX = x / surfaceView.Width * 2000 - 1000; - var focusY = y / surfaceView.Height * 2000 - 1000; - - // Call the autofocus with our coords - AutoFocus(focusX, focusY, true); - } - - public void ShutdownCamera() - { - if (Camera == null) return; - - // camera release logic takes about 0.005 sec so there is no need in async releasing - var perf = PerformanceCounter.Start(); - try - { - try - { - Camera.StopPreview(); - Camera.SetNonMarshalingPreviewCallback(null); - - //Camera.SetPreviewCallback(null); - - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, $"Calling SetPreviewDisplay: null"); - Camera.SetPreviewDisplay(null); - } - catch (Exception ex) - { - Android.Util.Log.Error(MobileBarcodeScanner.TAG, ex.ToString()); - } - Camera.Release(); - Camera = null; - } - catch (Exception e) - { - Android.Util.Log.Error(MobileBarcodeScanner.TAG, e.ToString()); - } - - PerformanceCounter.Stop(perf, "Shutdown camera took {0}ms"); - } - - void OpenCamera() - { - try - { - var version = Build.VERSION.SdkInt; - - if (version >= BuildVersionCodes.Gingerbread) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Checking Number of cameras..."); - - var numCameras = Camera.NumberOfCameras; - var camInfo = new Camera.CameraInfo(); - var found = false; - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Found " + numCameras + " cameras..."); - - var whichCamera = CameraFacing.Back; - - if (scannerHost.ScanningOptions.UseFrontCameraIfAvailable.HasValue && - scannerHost.ScanningOptions.UseFrontCameraIfAvailable.Value) - whichCamera = CameraFacing.Front; - - for (var i = 0; i < numCameras; i++) - { - Camera.GetCameraInfo(i, camInfo); - if (camInfo.Facing == whichCamera) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, - "Found " + whichCamera + " Camera, opening..."); - Camera = Camera.Open(i); - cameraId = i; - found = true; - break; - } - } - - if (!found) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, - "Finding " + whichCamera + " camera failed, opening camera 0..."); - Camera = Camera.Open(0); - cameraId = 0; - } - } - else - { - Camera = Camera.Open(); - } - - //if (Camera != null) - // Camera.SetPreviewCallback(_cameraEventListener); - //else - // MobileBarcodeScanner.LogWarn(MobileBarcodeScanner.TAG, "Camera is null :("); - } - catch (Exception ex) - { - ShutdownCamera(); - MobileBarcodeScanner.LogError("Setup Error: {0}", ex); - } - } - - void ApplyCameraSettings() - { - if (Camera == null) - { - OpenCamera(); - } - - // do nothing if something wrong with camera - if (Camera == null) return; - - var parameters = Camera.GetParameters(); - parameters.PreviewFormat = ImageFormatType.Nv21; - - var supportedFocusModes = parameters.SupportedFocusModes; - if (scannerHost.ScanningOptions.DisableAutofocus) - parameters.FocusMode = Camera.Parameters.FocusModeFixed; - else if (Build.VERSION.SdkInt >= BuildVersionCodes.IceCreamSandwich && - supportedFocusModes.Contains(Camera.Parameters.FocusModeContinuousPicture)) - parameters.FocusMode = Camera.Parameters.FocusModeContinuousPicture; - else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeContinuousVideo)) - parameters.FocusMode = Camera.Parameters.FocusModeContinuousVideo; - else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeAuto)) - parameters.FocusMode = Camera.Parameters.FocusModeAuto; - else if (supportedFocusModes.Contains(Camera.Parameters.FocusModeFixed)) - parameters.FocusMode = Camera.Parameters.FocusModeFixed; - - var selectedFps = parameters.SupportedPreviewFpsRange.FirstOrDefault(); - if (selectedFps != null) - { - // This will make sure we select a range with the highest maximum fps - // which still has the lowest minimum fps (Widest Range) - foreach (var fpsRange in parameters.SupportedPreviewFpsRange) - { - if (fpsRange[1] > selectedFps[1] || fpsRange[1] == selectedFps[1] && fpsRange[0] < selectedFps[0]) - selectedFps = fpsRange; - } - parameters.SetPreviewFpsRange(selectedFps[0], selectedFps[1]); - } - - CameraResolution resolution = null; - var supportedPreviewSizes = parameters.SupportedPreviewSizes; - if (supportedPreviewSizes != null) - { - var availableResolutions = supportedPreviewSizes.Select(sps => new CameraResolution - { - Width = sps.Width, - Height = sps.Height - }); - - // Try and get a desired resolution from the options selector - resolution = scannerHost.ScanningOptions.GetResolution(availableResolutions.ToList()); - - // If the user did not specify a resolution, let's try and find a suitable one - if (resolution == null) - { - foreach (var sps in supportedPreviewSizes) - { - if (sps.Width >= 640 && sps.Width <= 1000 && sps.Height >= 360 && sps.Height <= 1000) - { - resolution = new CameraResolution - { - Width = sps.Width, - Height = sps.Height - }; - break; - } - } - } - } - - // Google Glass requires this fix to display the camera output correctly - if (Build.Model.Contains("Glass")) - { - resolution = new CameraResolution - { - Width = 640, - Height = 360 - }; - // Glass requires 30fps - parameters.SetPreviewFpsRange(30000, 30000); - } - - // Hopefully a resolution was selected at some point - if (resolution != null) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, - "Selected Resolution: " + resolution.Width + "x" + resolution.Height); - - CameraResolution = resolution; - parameters.SetPreviewSize(resolution.Width, resolution.Height); - } - - Camera.SetParameters(parameters); - - SetCameraDisplayOrientation(); - } - - void AutoFocus(int x, int y, bool useCoordinates) - { - if (Camera == null) return; - - if (scannerHost.ScanningOptions.DisableAutofocus) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Disabled"); - return; - } - - var cameraParams = Camera.GetParameters(); - - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Requested"); - - // Cancel any previous requests - Camera.CancelAutoFocus(); - - try - { - // If we want to use coordinates - // Also only if our camera supports Auto focus mode - // Since FocusAreas only really work with FocusModeAuto set - if (useCoordinates - && cameraParams.SupportedFocusModes.Contains(Camera.Parameters.FocusModeAuto)) - { - // Let's give the touched area a 20 x 20 minimum size rect to focus on - // So we'll offset -10 from the center of the touch and then - // make a rect of 20 to give an area to focus on based on the center of the touch - x = x - 10; - y = y - 10; - - // Ensure we don't go over the -1000 to 1000 limit of focus area - if (x >= 1000) - x = 980; - if (x < -1000) - x = -1000; - if (y >= 1000) - y = 980; - if (y < -1000) - y = -1000; - - // Explicitly set FocusModeAuto since Focus areas only work with this setting - cameraParams.FocusMode = Camera.Parameters.FocusModeAuto; - // Add our focus area - cameraParams.FocusAreas = new List - { - new Camera.Area(new Rect(x, y, x + 20, y + 20), 1000) - }; - Camera.SetParameters(cameraParams); - } - - // Finally autofocus (weather we used focus areas or not) - Camera.AutoFocus(cameraEventListener); - } - catch (Exception ex) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Failed: {0}", ex); - } - } - - void SetCameraDisplayOrientation() - { - var degrees = GetCameraDisplayOrientation(); - LastCameraDisplayOrientationDegree = degrees; - - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Changing Camera Orientation to: " + degrees); - - try - { - Camera.SetDisplayOrientation(degrees); - } - catch (Exception ex) - { - Android.Util.Log.Error(MobileBarcodeScanner.TAG, ex.ToString()); - } - } - - int GetCameraDisplayOrientation() - { - int degrees; - var windowManager = context.GetSystemService(Context.WindowService).JavaCast(); - var display = windowManager.DefaultDisplay; - var rotation = display.Rotation; - - switch (rotation) - { - case SurfaceOrientation.Rotation0: - degrees = 0; - break; - case SurfaceOrientation.Rotation90: - degrees = 90; - break; - case SurfaceOrientation.Rotation180: - degrees = 180; - break; - case SurfaceOrientation.Rotation270: - degrees = 270; - break; - default: - throw new ArgumentOutOfRangeException(); - } - - var info = new Camera.CameraInfo(); - Camera.GetCameraInfo(cameraId, info); - - int correctedDegrees; - if (info.Facing == CameraFacing.Front) - { - correctedDegrees = (info.Orientation + degrees) % 360; - correctedDegrees = (360 - correctedDegrees) % 360; // compensate the mirror - } - else - { - // back-facing - correctedDegrees = (info.Orientation - degrees + 360) % 360; - } - - return correctedDegrees; - } - } -} \ No newline at end of file + public class CameraController + { + readonly Context context; + readonly ISurfaceHolder holder; + readonly SurfaceView surfaceView; + readonly CameraEventsListener cameraEventListener; + readonly IScannerSessionHost scannerHost; + readonly CameraStateCallback cameraStateCallback; + + CameraManager cameraManager; + ImageReader imageReader; + bool flashSupported; + Handler backgroundHandler; + CaptureRequest.Builder previewBuilder; + CameraCaptureSession previewSession; + CaptureRequest previewRequest; + HandlerThread backgroundThread; + Size[] supportedSizes; + + public string CameraId { get; private set; } + + public bool OpeningCamera { get; private set; } + + public CameraDevice Camera { get; private set; } + + public Size DisplaySize { get; private set; } + + public Size IdealPhotoSize { get; private set; } + + public int SensorRotation { get; private set; } + + public CameraController(SurfaceView surfaceView, CameraEventsListener cameraEventListener, IScannerSessionHost scannerHost) + { + context = surfaceView.Context; + holder = surfaceView.Holder; + this.surfaceView = surfaceView; + this.cameraEventListener = cameraEventListener; + this.scannerHost = scannerHost; + cameraStateCallback = new CameraStateCallback() + { + OnErrorAction = (camera, error) => + { + camera.Close(); + + Camera = null; + OpeningCamera = false; + + Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "Error on opening camera: " + error); + }, + OnOpenedAction = camera => + { + Camera = camera; + StartPreview(); + AutoFocus(); + OpeningCamera = false; + }, + OnDisconnectedAction = camera => + { + camera.Close(); + Camera = null; + OpeningCamera = false; + } + }; + } + + public void RefreshCamera() + { + if (Camera is null || previewRequest is null || previewSession is null || previewBuilder is null) return; + + SetUpCameraOutputs(); + previewRequest.Dispose(); + previewSession.Dispose(); + previewBuilder.Dispose(); + StartPreview(); + } + + public void SetupCamera() + { + StartBackgroundThread(); + + OpenCamera(); + } + + public void ShutdownCamera() + { + if (Camera != null) + Camera.Close(); + + StopBackgroundThread(); + } + + public void AutoFocus() + { + AutoFocus(0, 0, false); + } + + public void AutoFocus(int x, int y) + { + // Call the autofocus with our coords + AutoFocus(x, y, true); + } + + void AutoFocus(int x, int y, bool useCoordinates) + { + if (Camera == null) return; + + try + { + if (scannerHost.ScanningOptions.DisableAutofocus) + { + Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Disabled"); + return; + } + + var characteristics = cameraManager.GetCameraCharacteristics(CameraId.ToString()); + var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); + var supportedFocusModes = ((int[])characteristics + .Get(CameraCharacteristics.ControlAfAvailableModes)) + .Select(x => (ControlAFMode)x); + + Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Requested"); + + // If we want to use coordinates + // Also only if our camera supports Auto focus mode + // Since FocusAreas only really work with FocusModeAuto set + if (supportedFocusModes.Contains(ControlAFMode.ContinuousVideo)) + { + previewBuilder.Set(CaptureRequest.ControlAfTrigger, (int)ControlAFTrigger.Cancel); + previewBuilder.Set(CaptureRequest.ControlAfMode, (int)ControlAFMode.ContinuousVideo); + } + else if (useCoordinates && supportedFocusModes.Contains(ControlAFMode.Auto)) + { + // Let's give the touched area a 20 x 20 minimum size rect to focus on + // So we'll offset -10 from the center of the touch and then + // make a rect of 20 to give an area to focus on based on the center of the touch + x = x - 10; + y = y - 10; + + // Explicitly set FocusModeAuto since Focus areas only work with this setting + previewBuilder.Set(CaptureRequest.ControlAfMode, (int)ControlAFMode.Auto); + // Add our focus area + previewBuilder.Set(CaptureRequest.ControlAfRegions, new MeteringRectangle[] + { + new MeteringRectangle(x, y, x + 20, y + 20, 1000) + }); + + previewBuilder.Set(CaptureRequest.ControlAeRegions, new MeteringRectangle[] + { + new MeteringRectangle(x, y, x + 20, y + 20, 1000) + }); + + // Finally autofocus (weather we used focus areas or not) + previewBuilder.Set(CaptureRequest.ControlAfTrigger, (int)ControlAFTrigger.Start); + } + + UpdatePreview(); + } + catch (System.Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus Failed: {0}", ex); + } + } + + void SetUpCameraOutputs() + { + try + { + cameraManager = (CameraManager)context.GetSystemService(Context.CameraService); + + var cameraIds = cameraManager.GetCameraIdList(); + + CameraId = cameraIds[0]; + + var whichCamera = LensFacing.Back; + + if (scannerHost.ScanningOptions.UseFrontCameraIfAvailable.HasValue && + scannerHost.ScanningOptions.UseFrontCameraIfAvailable.Value) + whichCamera = LensFacing.Front; + + for (var i = 0; i < cameraIds.Length; i++) + { + var cameraCharacteristics = cameraManager.GetCameraCharacteristics(cameraIds[i]); + + var facing = (Integer)cameraCharacteristics.Get(CameraCharacteristics.LensFacing); + if (facing != null && facing.IntValue() == (int)whichCamera) + { + CameraId = cameraIds[i]; + break; + } + } + + var characteristics = cameraManager.GetCameraCharacteristics(CameraId); + var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); + + if (characteristics != null && supportedSizes == null) + supportedSizes = ((StreamConfigurationMap)characteristics + .Get(CameraCharacteristics.ScalerStreamConfigurationMap)) + .GetOutputSizes((int)ImageFormatType.Yuv420888); + + if (supportedSizes is null || supportedSizes.Length == 0) + { + Log.Debug(MobileBarcodeScanner.TAG, "Failed to get supported output sizes"); + return; + } + + var wm = context.GetSystemService(Context.WindowService).JavaCast(); + var display = wm.DefaultDisplay; + var point = new Point(); + display.GetSize(point); + DisplaySize = new Size(point.X, point.Y); + + IdealPhotoSize = DisplaySize.Width > DisplaySize.Height ? GetOptimalSize(supportedSizes, DisplaySize) : GetOptimalSize(supportedSizes, DisplaySize, true); + + imageReader = ImageReader.NewInstance(IdealPhotoSize.Width, IdealPhotoSize.Height, ImageFormatType.Yuv420888, 8); + + flashSupported = HasFlash(characteristics); + SensorRotation = GetSensorRotation(characteristics); + + imageReader.SetOnImageAvailableListener(cameraEventListener, backgroundHandler); + + } + catch (System.Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "Could not setup camera outputs" + ex); + } + } + + bool HasFlash(CameraCharacteristics characteristics) + { + var available = (Java.Lang.Boolean)characteristics.Get(CameraCharacteristics.FlashInfoAvailable); + if (available == null) + { + return false; + } + else + { + return (bool)available; + } + } + + int GetSensorRotation(CameraCharacteristics characteristics) + { + var rotation = (int?)characteristics.Get(CameraCharacteristics.SensorOrientation); + if (rotation == null) + { + return 0; + } + else + { + + return rotation.Value; + } + } + + public void OpenCamera() + { + if (context == null || OpeningCamera) + { + return; + } + + try + { + OpeningCamera = true; + + SetUpCameraOutputs(); + + cameraManager.OpenCamera(CameraId, cameraStateCallback, backgroundHandler); + } + catch (System.Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "Error on opening camera" + ex); + } + } + + Size GetOptimalPreviewSize(SurfaceView surface) + { + var width = surface.Width > surface.Height ? surface.Width : surface.Height; + var height = surface.Width > surface.Height ? surface.Height : surface.Width; + var aspectRatio = (double)width / (double)height; + + var characteristics = cameraManager.GetCameraCharacteristics(CameraId); + var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); + var availableSizes = ((StreamConfigurationMap)characteristics + .Get(CameraCharacteristics.ScalerStreamConfigurationMap)) + .GetOutputSizes(Class.FromType(typeof(ISurfaceHolder))); + var availableAspectRatios = availableSizes.Select(x => (x, (double)x.Width / (double)x.Height)); + + var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); + var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(availableSizes.Count() / 2); + var matches = bestMatches.OrderBy(m => m.Item2).ThenByDescending(x => x.x.Width).ThenByDescending(x => x.x.Height); + return matches.First().x; + } + + void SetupHolderSize() + { + if (Camera is null || holder is null || imageReader is null || backgroundHandler is null) return; + + var optimalPreviewSize = GetOptimalPreviewSize(surfaceView); + if (Looper.MyLooper() == Looper.MainLooper) + { + holder.SetFixedSize(optimalPreviewSize.Width, optimalPreviewSize.Height); + } + else + { + var sizeSetResetEvent = new ManualResetEventSlim(false); + using (var handler = new Handler(Looper.MainLooper)) + { + handler.Post(() => + { + holder.SetFixedSize(optimalPreviewSize.Width, optimalPreviewSize.Height); + sizeSetResetEvent.Set(); + }); + } + + sizeSetResetEvent.Wait(); + sizeSetResetEvent.Reset(); + } + } + + public void StartPreview() + { + if (Camera is null || holder is null || imageReader is null || backgroundHandler is null) return; + + try + { + SetupHolderSize(); + + // This is needed bc otherwise the preview is sometimes distorted + System.Threading.Thread.Sleep(30); + + var surfaces = new List + { + holder.Surface, + imageReader.Surface + }; + + previewBuilder = Camera.CreateCaptureRequest(CameraTemplate.Preview); + foreach (var surface in surfaces) + { + previewBuilder.AddTarget(surface); + } + + Camera.CreateCaptureSession(surfaces, + new CameraCaptureStateListener + { + OnConfigureFailedAction = session => + { + }, + OnConfiguredAction = session => + { + if (previewSession != null) + previewSession.Dispose(); + + previewSession = session; + UpdatePreview(); + } + }, + backgroundHandler); + } + catch (System.Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "Error on starting preview" + ex); + } + } + + void UpdatePreview() + { + if (Camera is null || previewSession is null) return; + + try + { + SetupHolderSize(); + + if (previewRequest != null) + previewRequest.Dispose(); + + previewRequest = previewBuilder.Build(); + previewSession.SetRepeatingRequest(previewRequest, null, backgroundHandler); + } + catch (System.Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "Error on updating preview" + ex); + } + } + + Size GetOptimalSize(IList sizes, Size referenceSize, bool flipWidthWithHeight = false) => flipWidthWithHeight ? GetOptimalSize(sizes, referenceSize.Height, referenceSize.Width) : GetOptimalSize(sizes, referenceSize.Width, referenceSize.Height); + + Size GetOptimalSize(IList sizes, int width, int height) + { + const int minimumSize = 500; + const int maximumSize = 1280; + if (sizes is null) return null; + + var aspectRatio = (double)width / (double)height; + var availableAspectRatios = sizes.Select(x => (x, (double)x.Width / (double)x.Height)); + + var differences = availableAspectRatios.Select(x => (x.x, System.Math.Abs(x.Item2 - aspectRatio))); + var bestMatches = differences.OrderBy(x => x.Item2).ThenBy(x => System.Math.Abs(x.x.Width - width)).ThenBy(x => System.Math.Abs(x.x.Height - height)).Take(sizes.Count / 2); + var orderedMatches = bestMatches.OrderBy(x => x.x.Width).ThenBy(x => x.x.Height); + var matches = orderedMatches.Where(m => (m.x.Height >= minimumSize || m.x.Width >= minimumSize) && (m.x.Height <= maximumSize || m.x.Width <= maximumSize)); + + if (matches.Count() == 0) + { + matches = orderedMatches; + } + + return matches.First().x; + } + + void StartBackgroundThread() + { + backgroundThread = new HandlerThread("CameraBackgroundThread"); + backgroundThread.Start(); + backgroundHandler = new Handler(backgroundThread.Looper); + } + + public void EnableTorch(bool state) + { + try + { + if (!flashSupported || previewBuilder is null) return; + + if (state) + { + previewBuilder.Set(CaptureRequest.ControlAeMode, (int)ControlAEMode.On); + previewBuilder.Set(CaptureRequest.FlashMode, (int)FlashMode.Torch); + } + else + previewBuilder.Set(CaptureRequest.FlashMode, (int)FlashMode.Off); + + UpdatePreview(); + } + catch (System.Exception ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "Error on enabling torch" + ex); + } + } + + void StopBackgroundThread() + { + try + { + backgroundThread?.QuitSafely(); + backgroundThread?.Join(); + backgroundThread = null; + backgroundHandler = null; + } + catch (InterruptedException ex) + { + Log.Debug(MobileBarcodeScanner.TAG, "Error stopping background threads: " + ex); + } + } + } +} diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs index e42eaf560..d32e58db8 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraEventsListener.android.cs @@ -1,29 +1,120 @@ using System; -using Android.Hardware; -using ApxLabs.FastAndroidCamera; +using Android.Media; +using Java.Nio; +using static Android.Media.ImageReader; namespace ZXing.Mobile.CameraAccess { - public class CameraEventsListener : Java.Lang.Object, INonMarshalingPreviewCallback, Camera.IAutoFocusCallback - { - public event EventHandler OnPreviewFrameReady; - - public void OnPreviewFrame(IntPtr data, Camera camera) - { - if (data != null && data != IntPtr.Zero) - { - using (var fastArray = new FastJavaByteArray(data)) - { - OnPreviewFrameReady?.Invoke(this, fastArray); - - camera.AddCallbackBuffer(fastArray); - } - } - } - - public void OnAutoFocus(bool success, Camera camera) - { - Android.Util.Log.Debug(MobileBarcodeScanner.TAG, "AutoFocus {0}", success ? "Succeeded" : "Failed"); - } - } + public class CameraEventsListener : Java.Lang.Object, IOnImageAvailableListener + { + public event EventHandler OnPreviewFrameReady; + + public CameraEventsListener() + { + } + + public void OnImageAvailable(ImageReader reader) + { + Image image = null; + try + { + image = reader.AcquireLatestImage(); + + if (image is null) return; + + var bytes = Yuv420888toNv21(image); + OnPreviewFrameReady?.Invoke(null, new CapturedImageData(bytes, image.Width, image.Height)); + } + finally + { + image?.Close(); + } + } + + //https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21 + byte[] Yuv420888toNv21(Image image) + { + var width = image.Width; + var height = image.Height; + var ySize = width * height; + var uvSize = width * height / 4; + + var nv21 = new byte[ySize + uvSize * 2]; + var planes = image.GetPlanes(); + + var yBuffer = planes[0].Buffer; // Y + var uBuffer = planes[1].Buffer; // U + var vBuffer = planes[2].Buffer; // V + + var yArray = new byte[yBuffer.Limit()]; + yBuffer.Get(yArray, 0, yArray.Length); + + var uArray = new byte[uBuffer.Limit()]; + uBuffer.Get(uArray, 0, uArray.Length); + + var vArray = new byte[vBuffer.Limit()]; + vBuffer.Get(vArray, 0, vArray.Length); + + var rowStride = planes[0].RowStride; + var pos = 0; + + if (rowStride == width) + { + // likely + Array.Copy(yArray, 0, nv21, 0, ySize); + pos += ySize; + } + else + { + var yBufferPos = -rowStride; // not an actual position + for (; pos < ySize; pos += width) + { + yBufferPos += rowStride; + Array.Copy(yArray, yBufferPos, nv21, pos, width); + } + } + + rowStride = planes[2].RowStride; + var pixelStride = planes[2].PixelStride; + + if (pixelStride == 2 && rowStride == width && uArray[0] == vArray[1]) + { + // maybe V and U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0] + var savePixel = vArray[1]; + try + { + vArray[1] = (byte)~savePixel; + if (uArray[0] == (sbyte)~savePixel) + { + vArray[1] = savePixel; + Array.Copy(vArray, 0, nv21, ySize, 1); + Array.Copy(vArray, 0, nv21, ySize + 1, uArray.Length); + + return nv21; // shortcut + } + } + catch (ReadOnlyBufferException) + { + // unfortunately, we cannot check if vBuffer and uBuffer overlap + } + + // unfortunately, the check failed. We must save U and V pixel by pixel + vArray[1] = savePixel; + } + + // other optimizations could check if (pixelStride == 1) or (pixelStride == 2), + // but performance gain would be less significant + for (var row = 0; row < height / 2; row++) + { + for (var col = 0; col < width / 2; col++) + { + var vuPos = col * pixelStride + row * rowStride; + nv21[pos++] = vArray[vuPos]; + nv21[pos++] = uArray[vuPos]; + } + } + + return nv21; + } + } } diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CameraStateCallback.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CameraStateCallback.android.cs new file mode 100644 index 000000000..804553d2f --- /dev/null +++ b/ZXing.Net.Mobile/Android/CameraAccess/CameraStateCallback.android.cs @@ -0,0 +1,22 @@ +using System; +using Android.Hardware.Camera2; +using Android.Runtime; + +namespace ZXing.Mobile.CameraAccess +{ + public class CameraStateCallback : CameraDevice.StateCallback + { + public Action OnDisconnectedAction; + public Action OnErrorAction; + public Action OnOpenedAction; + + public override void OnDisconnected(CameraDevice camera) + => OnDisconnectedAction?.Invoke(camera); + + public override void OnError(CameraDevice camera, [GeneratedEnum] CameraError error) + => OnErrorAction?.Invoke(camera, error); + + public override void OnOpened(CameraDevice camera) + => OnOpenedAction?.Invoke(camera); + } +} diff --git a/ZXing.Net.Mobile/Android/CameraAccess/CapturedImageData.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/CapturedImageData.android.cs new file mode 100644 index 000000000..b7ac0e3f8 --- /dev/null +++ b/ZXing.Net.Mobile/Android/CameraAccess/CapturedImageData.android.cs @@ -0,0 +1,18 @@ +namespace ZXing.Mobile.CameraAccess +{ + public class CapturedImageData + { + public byte[] Matrix { get; private set; } + + public int Width { get; private set; } + + public int Height { get; private set; } + + public CapturedImageData(byte[] matrix, int width, int height) + { + Matrix = matrix; + Width = width; + Height = height; + } + } +} diff --git a/ZXing.Net.Mobile/Android/CameraAccess/Torch.android.cs b/ZXing.Net.Mobile/Android/CameraAccess/Torch.android.cs index faacb42f5..5f4b5c431 100644 --- a/ZXing.Net.Mobile/Android/CameraAccess/Torch.android.cs +++ b/ZXing.Net.Mobile/Android/CameraAccess/Torch.android.cs @@ -1,94 +1,75 @@ using Android.Content; using Android.Content.PM; using Android.Hardware; +using Android.Hardware.Camera2; +using Android.Hardware.Camera2.Params; namespace ZXing.Mobile.CameraAccess { - public class Torch - { - readonly CameraController cameraController; - readonly Context context; - bool? hasTorch; - - public Torch(CameraController cameraController, Context context) - { - this.cameraController = cameraController; - this.context = context; - } - - public bool IsSupported - { - get - { - if (hasTorch.HasValue) - return hasTorch.Value; - - if (!context.PackageManager.HasSystemFeature(PackageManager.FeatureCameraFlash)) - { - Android.Util.Log.Info(MobileBarcodeScanner.TAG, "Flash not supported on this device"); - return false; - } - - if (cameraController.Camera == null) - { - Android.Util.Log.Info(MobileBarcodeScanner.TAG, "Run camera first"); - return false; - } - - var p = cameraController.Camera.GetParameters(); - var supportedFlashModes = p.SupportedFlashModes; - - if ((supportedFlashModes != null) - && (supportedFlashModes.Contains(Camera.Parameters.FlashModeTorch) - || supportedFlashModes.Contains(Camera.Parameters.FlashModeOn))) - hasTorch = ZXing.Net.Mobile.Android.PermissionsHandler.IsTorchPermissionDeclared(); - - return hasTorch != null && hasTorch.Value; - } - } - - public bool IsEnabled { get; private set; } - - public void TurnOn() => Enable(true); - - public void TurnOff() => Enable(false); - - public void Toggle() => Enable(!IsEnabled); - - private void Enable(bool state) - { - if (!IsSupported || IsEnabled == state) - return; - - if (cameraController.Camera == null) - { - Android.Util.Log.Info(MobileBarcodeScanner.TAG, "NULL Camera, cannot toggle torch"); - return; - } - - var parameters = cameraController.Camera.GetParameters(); - var supportedFlashModes = parameters.SupportedFlashModes; - - var flashMode = string.Empty; - if (state) - { - if (supportedFlashModes.Contains(Camera.Parameters.FlashModeTorch)) - flashMode = Camera.Parameters.FlashModeTorch; - else if (supportedFlashModes.Contains(Camera.Parameters.FlashModeOn)) - flashMode = Camera.Parameters.FlashModeOn; - } - else - { - if (supportedFlashModes != null && supportedFlashModes.Contains(Camera.Parameters.FlashModeOff)) - flashMode = Camera.Parameters.FlashModeOff; - } - - if (!string.IsNullOrEmpty(flashMode)) - { - parameters.FlashMode = flashMode; - cameraController.Camera.SetParameters(parameters); - IsEnabled = state; - } - } - } -} \ No newline at end of file + public class Torch + { + readonly CameraController cameraController; + readonly Context context; + CameraManager cameraManager; + bool? hasTorch; + + public Torch(CameraController cameraController, Context context) + { + this.cameraController = cameraController; + this.context = context; + cameraManager = (CameraManager)context.GetSystemService(Context.CameraService); + } + + public bool IsSupported + { + get + { + if (hasTorch.HasValue) + return hasTorch.Value; + + if (!context.PackageManager.HasSystemFeature(PackageManager.FeatureCameraFlash)) + { + Android.Util.Log.Info(MobileBarcodeScanner.TAG, "Flash not supported on this device"); + return false; + } + + if (cameraController.Camera == null) + { + Android.Util.Log.Info(MobileBarcodeScanner.TAG, "Run camera first"); + return false; + } + + var characteristics = cameraManager.GetCameraCharacteristics(cameraController.CameraId.ToString()); + var cameraHasTorch = ((Java.Lang.Boolean)characteristics.Get(CameraCharacteristics.FlashInfoAvailable)).BooleanValue(); + + if (cameraHasTorch) + hasTorch = ZXing.Net.Mobile.Android.PermissionsHandler.IsTorchPermissionDeclared(); + + return hasTorch != null && hasTorch.Value; + } + } + + public bool IsEnabled { get; private set; } + + public void TurnOn() => Enable(true); + + public void TurnOff() => Enable(false); + + public void Toggle() => Enable(!IsEnabled); + + private void Enable(bool state) + { + if (!IsSupported || IsEnabled == state) + return; + + if (cameraController.Camera == null) + { + Android.Util.Log.Info(MobileBarcodeScanner.TAG, "NULL Camera, cannot toggle torch"); + return; + } + + cameraController.EnableTorch(state); + IsEnabled = state; + } + } +} diff --git a/ZXing.Net.Mobile/Android/DebugHelper.android.cs b/ZXing.Net.Mobile/Android/DebugHelper.android.cs new file mode 100644 index 000000000..13a7763d6 --- /dev/null +++ b/ZXing.Net.Mobile/Android/DebugHelper.android.cs @@ -0,0 +1,72 @@ +using System.IO; +using System.Net.Http; +using Android.Graphics; +using ZXing.Mobile.CameraAccess; + +namespace ZXing.Net.Mobile.Android +{ +#if DEBUG + // Do not use this in production. + // This is a simple helping class to use with the ImageReceiver Tool + public static class DebugHelper + { + static int sendThreshold = 18; + static int sendCount; + + // Used to check NV21 Output + static byte[] NV21toJPEG(byte[] nv21, int width, int height) + { + using (var mems = new MemoryStream()) + { + var yuv = new YuvImage(nv21, ImageFormatType.Nv21, width, height, null); + yuv.CompressToJpeg(new Rect(0, 0, width, height), 100, mems); + return mems.ToArray(); + } + } + + public static void SendBytesToEndpoint(byte[] data, string endpoint, bool ignoreThreshold = false, params (string Key, string Value)[] headers) + { + if (!ignoreThreshold) + { + if (sendCount < sendThreshold) + { + sendCount++; + return; + } + + sendCount = 0; + } + + var httpClient = GetHttpClient(); + var request = new HttpRequestMessage(HttpMethod.Post, endpoint); + request.Content = new ByteArrayContent(data); + + foreach (var header in headers) + { + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } + + httpClient.SendAsync(request); + } + + public static void SendNV21toJPEGToEndpoint(byte[] data, int width, int height, string endpoint, bool ignoreThreshold = false, params (string Key, string Value)[] headers) + { + var jpeg = NV21toJPEG(data, width, height); + SendBytesToEndpoint(jpeg, endpoint, ignoreThreshold, headers); + } + + public static void SendNV21toJPEGToEndpoint(CapturedImageData data, string endpoint, bool ignoreThreshold = false, params (string Key, string Value)[] headers) + => SendNV21toJPEGToEndpoint(data.Matrix, data.Width, data.Height, endpoint, ignoreThreshold, headers); + + static HttpClient GetHttpClient() + { + var handler = new HttpClientHandler(); + // we dont need to validate certificates + handler.ClientCertificateOptions = ClientCertificateOption.Manual; + handler.ServerCertificateCustomValidationCallback = (_, __, ___, ____) => true; + + return new HttpClient(handler); + } + } +#endif +} diff --git a/ZXing.Net.Mobile/Android/DeviceOrientationData.android.cs b/ZXing.Net.Mobile/Android/DeviceOrientationData.android.cs new file mode 100644 index 000000000..cf72ecb83 --- /dev/null +++ b/ZXing.Net.Mobile/Android/DeviceOrientationData.android.cs @@ -0,0 +1,20 @@ +using Android.Content.Res; + +namespace ZXing.Net.Mobile.Android +{ + public class DeviceOrientationData + { + public Orientation OrientationMode { get; } + + public int DeviceOrientation { get; } + + public int SensorRotation { get; } + + public DeviceOrientationData(Orientation orientationMode, int orientation, int sensorRotation) + { + OrientationMode = orientationMode; + DeviceOrientation = orientation; + SensorRotation = sensorRotation; + } + } +} diff --git a/ZXing.Net.Mobile/Android/DeviceOrientationEventListener.android..cs b/ZXing.Net.Mobile/Android/DeviceOrientationEventListener.android..cs new file mode 100644 index 000000000..e03175646 --- /dev/null +++ b/ZXing.Net.Mobile/Android/DeviceOrientationEventListener.android..cs @@ -0,0 +1,42 @@ +using Android.Content; +using Android.Hardware; +using Android.Runtime; +using Android.Views; + +namespace ZXing.Net.Mobile.Android +{ + public class DeviceOrientationEventListener : OrientationEventListener + { + public int Orientation { get; private set; } + + public bool IsEnabled { get; private set; } = false; + + public DeviceOrientationEventListener(Context context) + : base(context) + { + } + + public DeviceOrientationEventListener(Context context, [GeneratedEnum] SensorDelay rate) + : base(context, rate) + { + } + + public override void OnOrientationChanged(int orientation) + { + if (orientation != OrientationUnknown) + Orientation = orientation; + } + + public override void Enable() + { + base.Enable(); + IsEnabled = true; + } + + public override void Disable() + { + base.Disable(); + IsEnabled = false; + } + } +} diff --git a/ZXing.Net.Mobile/Android/FastJavaArrayEx.android.cs b/ZXing.Net.Mobile/Android/FastJavaArrayEx.android.cs deleted file mode 100644 index 14584e001..000000000 --- a/ZXing.Net.Mobile/Android/FastJavaArrayEx.android.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using ApxLabs.FastAndroidCamera; - -namespace ZXing.Mobile -{ - public static class FastJavaArrayEx - { - public static void BlockCopyTo(this FastJavaByteArray self, int sourceIndex, byte[] array, int arrayIndex, int length) - { - unsafe - { - Marshal.Copy(new IntPtr(self.Raw + sourceIndex), array, arrayIndex, Math.Min(length, Math.Min(self.Count, array.Length - arrayIndex))); - } - } - } -} \ No newline at end of file diff --git a/ZXing.Net.Mobile/Android/FastJavaByteArrayYUVLuminanceSource.android.cs b/ZXing.Net.Mobile/Android/FastJavaByteArrayYUVLuminanceSource.android.cs deleted file mode 100644 index 1137377ac..000000000 --- a/ZXing.Net.Mobile/Android/FastJavaByteArrayYUVLuminanceSource.android.cs +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2009 ZXing authors - * Modifications copyright 2016 kasper@byolimit.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -using ApxLabs.FastAndroidCamera; - -using System; - -namespace ZXing.Mobile -{ - /// - /// This object extends LuminanceSource around an array of YUV data returned from the camera driver, - /// with the option to crop to a rectangle within the full data. This can be used to exclude - /// superfluous pixels around the perimeter and speed up decoding. - /// It works for any pixel format where the Y channel is planar and appears first, including - /// YCbCr_420_SP and YCbCr_422_SP. - /// - /// - /// Builds upon PlanarYUVLuminanceSource from ZXing.NET, which is a .Net port of ZXing. The original code - /// was authored by - /// @author dswitkin@google.com (Daniel Switkin) - /// - public sealed class FastJavaByteArrayYUVLuminanceSource : BaseLuminanceSource - { - private readonly FastJavaByteArray _yuv; - private readonly int _dataWidth; - private readonly int _dataHeight; - private readonly int _left; - private readonly int _top; - - /// - /// Initializes a new instance of the class. - /// - /// The yuv data. - /// Width of the data. - /// Height of the data. - /// The left. - /// The top. - /// The width. - /// The height. - /// if set to true [reverse horiz]. - public FastJavaByteArrayYUVLuminanceSource(FastJavaByteArray yuv, - int dataWidth, - int dataHeight, - int left, - int top, - int width, - int height) - : base(width, height) - { - if (left < 0) - throw new ArgumentException("Negative value", nameof(left)); - - if (top < 0) - throw new ArgumentException("Negative value", nameof(top)); - - if (width < 0) - throw new ArgumentException("Negative value", nameof(width)); - - if (height < 0) - throw new ArgumentException("Negative value", nameof(height)); - - if (left + width > dataWidth || top + height > dataHeight) - { - throw new ArgumentException("Crop rectangle does not fit within image data."); - } - - _yuv = yuv; - _dataWidth = dataWidth; - _dataHeight = dataHeight; - _left = left; - _top = top; - } - - /// - /// Fetches one row of luminance data from the underlying platform's bitmap. Values range from - /// 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have - /// to bitwise and with 0xff for each value. It is preferable for implementations of this method - /// to only fetch this row rather than the whole image, since no 2D Readers may be installed and - /// getMatrix() may never be called. - /// - /// The row to fetch, 0 <= y < Height. - /// An optional preallocated array. If null or too small, it will be ignored. - /// Always use the returned object, and ignore the .length of the array. - /// - /// An array containing the luminance data of the requested row. - /// - override public byte[] getRow(int y, byte[] row) - { - if (y < 0 || y >= Height) - throw new ArgumentException("Requested row is outside the image: " + y, nameof(y)); - - var width = Width; - if (row == null || row.Length < width) - row = new byte[width]; // ensure we have room for the row - - var offset = (y + _top) * _dataWidth + _left; - _yuv.BlockCopyTo(offset, row, 0, width); - return row; - } - - override public byte[] Matrix - { - get - { - var width = Width; - var height = Height; - - var area = width * height; - var matrix = new byte[area]; - var inputOffset = _top * _dataWidth + _left; - - // If the width matches the full width of the underlying data, perform a single copy. - if (width == _dataWidth) - { - _yuv.BlockCopyTo(inputOffset, matrix, 0, area); - return matrix; - } - - // Otherwise copy one cropped row at a time. - for (var y = 0; y < height; y++) - { - var outputOffset = y * width; - _yuv.BlockCopyTo(inputOffset, matrix, outputOffset, width); - inputOffset += _dataWidth; - } - return matrix; - } - } - - /// Whether this subclass supports cropping. - override public bool CropSupported - => true; - - /// - /// Returns a new object with cropped image data. Implementations may keep a reference to the - /// original data rather than a copy. Only callable if CropSupported is true. - /// - /// The left coordinate, 0 <= left < Width. - /// The top coordinate, 0 <= top <= Height. - /// The width of the rectangle to crop. - /// The height of the rectangle to crop. - /// - /// A cropped version of this object. - /// - override public LuminanceSource crop(int left, int top, int width, int height) - => new FastJavaByteArrayYUVLuminanceSource(_yuv, - _dataWidth, - _dataHeight, - _left + left, - _top + top, - width, - height); - - // Called when rotating. - // todo: This partially defeats the purpose as we traffic in byte[] luminances - protected override LuminanceSource CreateLuminanceSource(byte[] newLuminances, int width, int height) - => new PlanarYUVLuminanceSource(newLuminances, width, height, 0, 0, width, height, false); - } -} \ No newline at end of file diff --git a/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs b/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs new file mode 100644 index 000000000..0ac6936e0 --- /dev/null +++ b/ZXing.Net.Mobile/Android/PlanarNV21LuminanceSource.android.cs @@ -0,0 +1,131 @@ +using System; +using Android.Content.Res; + +namespace ZXing.Net.Mobile.Android +{ + public class PlanarNV21LuminanceSource : BaseLuminanceSource + { + DeviceOrientationData orientationData = null; + + public override bool CropSupported => false; + + public override bool RotateSupported => true; + + + public PlanarNV21LuminanceSource(byte[] nv21Data, int width, int height, DeviceOrientationData orientationData, bool useOrientationDataToCorrect) + : base(width, height) + { + this.orientationData = orientationData; + base.luminances = nv21Data; + Width = width; + Height = height; + + if (useOrientationDataToCorrect && orientationData == null) + throw new ArgumentNullException($"{orientationData} can't be null when correction is requested"); + + if (orientationData != null && useOrientationDataToCorrect) + ValidateRotation(); + } + + public PlanarNV21LuminanceSource(byte[] nv21Data, int width, int height) + : this(nv21Data, width, height, null, false) + { + } + + void ValidateRotation() + { + if (orientationData.SensorRotation % 90 != 0) // we don't support weird sensor orientations + { + return; + } + + var rotateBy = 0; + if (orientationData.OrientationMode == Orientation.Landscape) + { + if (orientationData.DeviceOrientation >= 180) // Navigation on the left, Header on the left (270°) + { + rotateBy = 270; // 270 + 90 = 360 || 360 zeros out so nothing to do + } + else if (orientationData.DeviceOrientation < 180) // Navigation on the left, Header on the Right (90°) + { + rotateBy = 90; + } + } + else + { + if (orientationData.DeviceOrientation > 175 && orientationData.DeviceOrientation < 185) // Upside down and not landscape mode + { + rotateBy = 180; + } + } + + rotateBy += orientationData.SensorRotation; + rotateBy %= 360; // Normalize + + var rotateResult = RotateNV21(luminances, Width, Height, rotateBy); + luminances = rotateResult.NV21; + Width = rotateResult.Width; + Height = rotateResult.Height; + } + + protected override LuminanceSource CreateLuminanceSource(byte[] newLuminances, int width, int height) + => new PlanarNV21LuminanceSource(newLuminances, width, height, orientationData, false); + + public override LuminanceSource rotateCounterClockwise() => GetRotatedLuminanceSource(270); + + public LuminanceSource RotateClockwise() => GetRotatedLuminanceSource(90); + + public LuminanceSource Mirror() => GetRotatedLuminanceSource(180); + + LuminanceSource GetRotatedLuminanceSource(int rotation) + { + var rotateResult = RotateNV21(luminances, Width, Height, rotation); + return CreateLuminanceSource(rotateResult.NV21, rotateResult.Width, rotateResult.Height); + } + + // https://stackoverflow.com/questions/6853401/camera-pixels-rotated/31425229#31425229 + public static (byte[] NV21, int Width, int Height) RotateNV21(byte[] nv21, int width, int height, int rotation) + { + if (rotation == 0) + return (nv21, width, height); + + if (rotation % 90 != 0 || rotation < 0 || rotation > 270) + { + throw new ArgumentException("0 <= rotation < 360, rotation % 90 == 0"); + } + + var output = new byte[nv21.Length]; + var frameSize = width * height; + var swap = rotation % 180 != 0; + var xflip = rotation % 270 != 0; + var yflip = rotation >= 180; + + for (var row = 0; row < height; row++) + { + for (var col = 0; col < width; col++) + { + var yInPos = row * width + col; + var uInPos = frameSize + (row >> 1) * width + (col & ~1); + var vInPos = uInPos + 1; + + var widthOut = swap ? height : width; + var heightOut = swap ? width : height; + var colSwapped = swap ? row : col; + var rowSwapped = swap ? col : row; + var colOut = xflip ? widthOut - colSwapped - 1 : colSwapped; + var rowOut = yflip ? heightOut - rowSwapped - 1 : rowSwapped; + + var yOutPos = rowOut * widthOut + colOut; + var uOutPos = frameSize + (rowOut >> 1) * widthOut + (colOut & ~1); + var vOutPos = uOutPos + 1; + + output[yOutPos] = (byte)(0xff & nv21[yInPos]); + output[uOutPos] = (byte)(0xff & nv21[uInPos]); + output[vOutPos] = (byte)(0xff & nv21[vInPos]); + } + } + + return (output, swap ? height : width, swap ? width : height); + } + } +} diff --git a/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs b/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs index 0fe0c87f1..4c9049e49 100644 --- a/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs +++ b/ZXing.Net.Mobile/Android/ZXingSurfaceView.android.cs @@ -5,156 +5,166 @@ using Android.Graphics; using ZXing.Mobile.CameraAccess; using ZXing.Net.Mobile.Android; +using System.Threading.Tasks; +using System.Threading; namespace ZXing.Mobile { - public class ZXingSurfaceView : SurfaceView, ISurfaceHolderCallback, IScannerView, IScannerSessionHost - { - public ZXingSurfaceView(Context context, MobileBarcodeScanningOptions options) - : base(context) - { - ScanningOptions = options ?? new MobileBarcodeScanningOptions(); - Init(); - } - - protected ZXingSurfaceView(IntPtr javaReference, JniHandleOwnership transfer) - : base(javaReference, transfer) => Init(); - - bool addedHolderCallback = false; - - void Init() - { - if (cameraAnalyzer == null) - cameraAnalyzer = new CameraAnalyzer(this, this); - - cameraAnalyzer.ResumeAnalysis(); - - if (!addedHolderCallback) - { - Holder.AddCallback(this); - Holder.SetType(SurfaceType.PushBuffers); - addedHolderCallback = true; - } - } - - public async void SurfaceCreated(ISurfaceHolder holder) - { - await PermissionsHandler.RequestPermissionsAsync(); - - cameraAnalyzer.SetupCamera(); - - surfaceCreated = true; - } - - public async void SurfaceChanged(ISurfaceHolder holder, Format format, int wx, int hx) - => cameraAnalyzer.RefreshCamera(); - - public async void SurfaceDestroyed(ISurfaceHolder holder) - { - try - { - if (addedHolderCallback) - { - Holder.RemoveCallback(this); - addedHolderCallback = false; - } - } - catch { } - - cameraAnalyzer.ShutdownCamera(); - } - - public override bool OnTouchEvent(MotionEvent e) - { - var r = base.OnTouchEvent(e); - - switch (e.Action) - { - case MotionEventActions.Down: - return true; - case MotionEventActions.Up: - var touchX = e.GetX(); - var touchY = e.GetY(); - AutoFocus((int)touchX, (int)touchY); - break; - } - - return r; - } - - public void AutoFocus() - => cameraAnalyzer.AutoFocus(); - - public void AutoFocus(int x, int y) - => cameraAnalyzer.AutoFocus(x, y); - - public void StartScanning(Action scanResultCallback, MobileBarcodeScanningOptions options = null) - { - cameraAnalyzer.SetupCamera(); - - ScanningOptions = options ?? MobileBarcodeScanningOptions.Default; - - cameraAnalyzer.BarcodeFound = (result) => - scanResultCallback?.Invoke(result); - cameraAnalyzer.ResumeAnalysis(); - } - - public void StopScanning() - => cameraAnalyzer.ShutdownCamera(); - - public void PauseAnalysis() - => cameraAnalyzer.PauseAnalysis(); - - public void ResumeAnalysis() - => cameraAnalyzer.ResumeAnalysis(); - - public void Torch(bool on) - { - if (on) - cameraAnalyzer.Torch.TurnOn(); - else - cameraAnalyzer.Torch.TurnOff(); - } - - public void ToggleTorch() - => cameraAnalyzer.Torch.Toggle(); - - public MobileBarcodeScanningOptions ScanningOptions { get; set; } - - public bool IsTorchOn => cameraAnalyzer.Torch.IsEnabled; - - public bool IsAnalyzing => cameraAnalyzer.IsAnalyzing; - - CameraAnalyzer cameraAnalyzer; - bool surfaceCreated; - - public bool HasTorch => cameraAnalyzer.Torch.IsSupported; - - protected override void OnAttachedToWindow() - { - base.OnAttachedToWindow(); - - // Reinit things - Init(); - } - - protected override void OnWindowVisibilityChanged(ViewStates visibility) - { - base.OnWindowVisibilityChanged(visibility); - if (visibility == ViewStates.Visible) - Init(); - } - - public override async void OnWindowFocusChanged(bool hasWindowFocus) - { - base.OnWindowFocusChanged(hasWindowFocus); - - if (!hasWindowFocus) - return; - - //only refresh the camera if the surface has already been created. Fixed #569 - if (surfaceCreated) - cameraAnalyzer.RefreshCamera(); - } - } + public class ZXingSurfaceView : SurfaceView, ISurfaceHolderCallback, IScannerView, IScannerSessionHost + { + public ZXingSurfaceView(Context context, MobileBarcodeScanningOptions options) + : base(context) + { + ScanningOptions = options ?? new MobileBarcodeScanningOptions(); + Init(); + } + + protected ZXingSurfaceView(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) => Init(); + + bool addedHolderCallback = false; + + void Init() + { + if (cameraAnalyzer == null) + cameraAnalyzer = new CameraAnalyzer(this, this); + + cameraAnalyzer.ResumeAnalysis(); + + if (!addedHolderCallback) + { + Holder.AddCallback(this); + Holder.SetType(SurfaceType.PushBuffers); + addedHolderCallback = true; + } + } + + public async void SurfaceCreated(ISurfaceHolder holder) + { + await PermissionsHandler.RequestPermissionsAsync(); + + cameraAnalyzer.SetupCamera(); + + surfaceCreated = true; + surfaceCreatedResetEvent.Set(); + } + + public async void SurfaceChanged(ISurfaceHolder holder, Format format, int wx, int hx) + { + cameraAnalyzer.RefreshCamera(); + } + + public async void SurfaceDestroyed(ISurfaceHolder holder) + { + try + { + if (addedHolderCallback) + { + Holder.RemoveCallback(this); + addedHolderCallback = false; + } + } + catch { } + + cameraAnalyzer.ShutdownCamera(); + } + + public override bool OnTouchEvent(MotionEvent e) + { + var r = base.OnTouchEvent(e); + + switch (e.Action) + { + case MotionEventActions.Down: + return true; + case MotionEventActions.Up: + var touchX = e.GetX(); + var touchY = e.GetY(); + AutoFocus((int)touchX, (int)touchY); + break; + } + + return r; + } + + public void AutoFocus() + => cameraAnalyzer.AutoFocus(); + + public void AutoFocus(int x, int y) + => cameraAnalyzer.AutoFocus(x, y); + + public void StartScanning(Action scanResultCallback, MobileBarcodeScanningOptions options = null) + { + Task.Run(() => + { + surfaceCreatedResetEvent.Wait(); + surfaceCreatedResetEvent.Reset(); + + ScanningOptions = options ?? MobileBarcodeScanningOptions.Default; + + cameraAnalyzer.BarcodeFound = (result) => + scanResultCallback?.Invoke(result); + cameraAnalyzer.ResumeAnalysis(); + }); + } + + public void StopScanning() + => cameraAnalyzer.ShutdownCamera(); + + public void PauseAnalysis() + => cameraAnalyzer.PauseAnalysis(); + + public void ResumeAnalysis() + => cameraAnalyzer.ResumeAnalysis(); + + public void Torch(bool on) + { + if (on) + cameraAnalyzer.Torch.TurnOn(); + else + cameraAnalyzer.Torch.TurnOff(); + } + + public void ToggleTorch() + => cameraAnalyzer.Torch.Toggle(); + + public MobileBarcodeScanningOptions ScanningOptions { get; set; } + + public bool IsTorchOn => cameraAnalyzer.Torch.IsEnabled; + + public bool IsAnalyzing => cameraAnalyzer.IsAnalyzing; + + CameraAnalyzer cameraAnalyzer; + bool surfaceCreated; + ManualResetEventSlim surfaceCreatedResetEvent = new ManualResetEventSlim(false); + + public bool HasTorch => cameraAnalyzer.Torch.IsSupported; + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + + // Reinit things + Init(); + } + + protected override void OnWindowVisibilityChanged(ViewStates visibility) + { + base.OnWindowVisibilityChanged(visibility); + if (visibility == ViewStates.Visible) + Init(); + } + + public override async void OnWindowFocusChanged(bool hasWindowFocus) + { + base.OnWindowFocusChanged(hasWindowFocus); + + if (!hasWindowFocus) + return; + + //only refresh the camera if the surface has already been created. Fixed #569 + if (surfaceCreated) + cameraAnalyzer.RefreshCamera(); + } + } } diff --git a/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs b/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs index 2ab08614a..14f0f69ee 100644 --- a/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs +++ b/ZXing.Net.Mobile/MobileBarcodeScanningOptions.shared.cs @@ -1,121 +1,118 @@ -using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using ZXing; namespace ZXing.Mobile { - public class MobileBarcodeScanningOptions - { - /// - /// Camera resolution selector delegate, must return the selected Resolution from the list of available resolutions - /// - public delegate CameraResolution CameraResolutionSelectorDelegate(List availableResolutions); + public class MobileBarcodeScanningOptions + { + /// + /// Camera resolution selector delegate, must return the selected Resolution from the list of available resolutions + /// + public delegate CameraResolution CameraResolutionSelectorDelegate(List availableResolutions); - public MobileBarcodeScanningOptions() - { - PossibleFormats = new List(); - //this.AutoRotate = true; - DelayBetweenAnalyzingFrames = 150; - InitialDelayBeforeAnalyzingFrames = 300; - DelayBetweenContinuousScans = 1000; - UseNativeScanning = false; - } + public MobileBarcodeScanningOptions() + { + PossibleFormats = new List(); + //this.AutoRotate = true; + DelayBetweenAnalyzingFrames = 150; + InitialDelayBeforeAnalyzingFrames = 300; + DelayBetweenContinuousScans = 1000; + UseNativeScanning = false; + } - public CameraResolutionSelectorDelegate CameraResolutionSelector { get; set; } + public CameraResolutionSelectorDelegate CameraResolutionSelector { get; set; } - public IEnumerable PossibleFormats { get; set; } + public IEnumerable PossibleFormats { get; set; } - public bool? TryHarder { get; set; } + public bool? TryHarder { get; set; } - public bool? PureBarcode { get; set; } + public bool? PureBarcode { get; set; } - public bool? AutoRotate { get; set; } + public bool? AutoRotate { get; set; } - public bool? UseCode39ExtendedMode { get; set; } + public bool? UseCode39ExtendedMode { get; set; } - public string CharacterSet { get; set; } + public string CharacterSet { get; set; } - public bool? TryInverted { get; set; } + public bool? TryInverted { get; set; } - public bool? UseFrontCameraIfAvailable { get; set; } + public bool? UseFrontCameraIfAvailable { get; set; } - public bool? AssumeGS1 { get; set; } + public bool? AssumeGS1 { get; set; } - public bool DisableAutofocus { get; set; } + public bool DisableAutofocus { get; set; } - public bool UseNativeScanning { get; set; } + public bool UseNativeScanning { get; set; } - public int DelayBetweenContinuousScans { get; set; } + public int DelayBetweenContinuousScans { get; set; } - public int DelayBetweenAnalyzingFrames { get; set; } - public int InitialDelayBeforeAnalyzingFrames { get; set; } + public int DelayBetweenAnalyzingFrames { get; set; } + public int InitialDelayBeforeAnalyzingFrames { get; set; } - public static MobileBarcodeScanningOptions Default - { - get { return new MobileBarcodeScanningOptions(); } - } + public static MobileBarcodeScanningOptions Default + { + get { return new MobileBarcodeScanningOptions(); } + } - public BarcodeReaderGeneric BuildBarcodeReader() - { - var reader = new BarcodeReaderGeneric(); - if (TryHarder.HasValue) - reader.Options.TryHarder = TryHarder.Value; - if (PureBarcode.HasValue) - reader.Options.PureBarcode = PureBarcode.Value; - if (AutoRotate.HasValue) - reader.AutoRotate = AutoRotate.Value; - if (UseCode39ExtendedMode.HasValue) - reader.Options.UseCode39ExtendedMode = UseCode39ExtendedMode.Value; - if (!string.IsNullOrEmpty(CharacterSet)) - reader.Options.CharacterSet = CharacterSet; - if (TryInverted.HasValue) - reader.TryInverted = TryInverted.Value; - if (AssumeGS1.HasValue) - reader.Options.AssumeGS1 = AssumeGS1.Value; + public BarcodeReaderGeneric BuildBarcodeReader() + { + var reader = new BarcodeReaderGeneric(); + if (TryHarder.HasValue) + reader.Options.TryHarder = TryHarder.Value; + if (PureBarcode.HasValue) + reader.Options.PureBarcode = PureBarcode.Value; + if (AutoRotate.HasValue) + reader.AutoRotate = AutoRotate.Value; + if (UseCode39ExtendedMode.HasValue) + reader.Options.UseCode39ExtendedMode = UseCode39ExtendedMode.Value; + if (!string.IsNullOrEmpty(CharacterSet)) + reader.Options.CharacterSet = CharacterSet; + if (TryInverted.HasValue) + reader.Options.TryInverted = TryInverted.Value; + if (AssumeGS1.HasValue) + reader.Options.AssumeGS1 = AssumeGS1.Value; - if (PossibleFormats?.Any() ?? false) - { - reader.Options.PossibleFormats = new List(); + if (PossibleFormats?.Any() ?? false) + { + reader.Options.PossibleFormats = new List(); - foreach (var pf in PossibleFormats) - reader.Options.PossibleFormats.Add(pf); - } + foreach (var pf in PossibleFormats) + reader.Options.PossibleFormats.Add(pf); + } - return reader; - } + return reader; + } - public MultiFormatReader BuildMultiFormatReader() - { - var reader = new MultiFormatReader(); + public MultiFormatReader BuildMultiFormatReader() + { + var reader = new MultiFormatReader(); - var hints = new Dictionary(); + var hints = new Dictionary(); - if (TryHarder.HasValue && TryHarder.Value) - hints.Add(DecodeHintType.TRY_HARDER, TryHarder.Value); - if (PureBarcode.HasValue && PureBarcode.Value) - hints.Add(DecodeHintType.PURE_BARCODE, PureBarcode.Value); + if (TryHarder.HasValue && TryHarder.Value) + hints.Add(DecodeHintType.TRY_HARDER, TryHarder.Value); + if (PureBarcode.HasValue && PureBarcode.Value) + hints.Add(DecodeHintType.PURE_BARCODE, PureBarcode.Value); - if (PossibleFormats?.Any() ?? false) - hints.Add(DecodeHintType.POSSIBLE_FORMATS, PossibleFormats); + if (PossibleFormats?.Any() ?? false) + hints.Add(DecodeHintType.POSSIBLE_FORMATS, PossibleFormats); - reader.Hints = hints; + reader.Hints = hints; - return reader; - } + return reader; + } - public CameraResolution GetResolution(List availableResolutions) - { - CameraResolution r = null; + public CameraResolution GetResolution(List availableResolutions) + { + CameraResolution r = null; - var dg = CameraResolutionSelector; + var dg = CameraResolutionSelector; - if (dg != null) - r = dg(availableResolutions); + if (dg != null) + r = dg(availableResolutions); - return r; - } - } + return r; + } + } } diff --git a/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj b/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj index 029245573..5b9e6215d 100644 --- a/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj +++ b/ZXing.Net.Mobile/ZXing.Net.Mobile.csproj @@ -86,7 +86,6 @@ - @@ -106,6 +105,6 @@ - + \ No newline at end of file