mirror of
https://github.com/cyanfish/naps2.git
synced 2024-09-11 15:26:55 +03:00
WIP: Apple scan driver based on ImageCaptureCore
This commit is contained in:
parent
bd7d19b876
commit
c8f724efd5
@ -14,7 +14,6 @@
|
||||
<string>en</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string></string>
|
||||
<!-- TODO: Somehow the build process is generating this icon file rather than using the one we already have in the project folder -->
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Icon.icns</string>
|
||||
</dict>
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace NAPS2.Images.Mac;
|
||||
|
||||
// TODO: We might need to dispose things more aggressively
|
||||
public class MacImage : IMemoryImage
|
||||
{
|
||||
public MacImage(ImageContext imageContext, NSImage image)
|
||||
|
57
NAPS2.Sdk/Scan/Internal/Apple/AppleScanDriver.cs
Normal file
57
NAPS2.Sdk/Scan/Internal/Apple/AppleScanDriver.cs
Normal file
@ -0,0 +1,57 @@
|
||||
#if MAC
|
||||
using System.Threading;
|
||||
using ImageCaptureCore;
|
||||
using NAPS2.Scan.Exceptions;
|
||||
|
||||
namespace NAPS2.Scan.Internal.Apple;
|
||||
|
||||
internal class AppleScanDriver : IScanDriver
|
||||
{
|
||||
private readonly ScanningContext _scanningContext;
|
||||
|
||||
public AppleScanDriver(ScanningContext scanningContext)
|
||||
{
|
||||
_scanningContext = scanningContext;
|
||||
}
|
||||
|
||||
public async Task<List<ScanDevice>> GetDeviceList(ScanOptions options)
|
||||
{
|
||||
using var reader = new DeviceReader();
|
||||
reader.Start();
|
||||
await Task.Delay(2000);
|
||||
return reader.Devices.Select(x => new ScanDevice(x.Uuid, x.Name)).ToList();
|
||||
}
|
||||
|
||||
public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents,
|
||||
Action<IMemoryImage> callback)
|
||||
{
|
||||
using var reader = new DeviceReader();
|
||||
using var device = await GetDevice(reader, options.Device!);
|
||||
using var oper =
|
||||
new DeviceOperator(_scanningContext, device, options, cancelToken, scanEvents, callback);
|
||||
await oper.Scan();
|
||||
}
|
||||
|
||||
private async Task<ICScannerDevice> GetDevice(DeviceReader reader, ScanDevice scanDevice)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<ICScannerDevice>();
|
||||
reader.DeviceFound += (_, args) =>
|
||||
{
|
||||
if (args.Device.Uuid == scanDevice.ID)
|
||||
{
|
||||
tcs.TrySetResult(args.Device);
|
||||
}
|
||||
};
|
||||
reader.Start();
|
||||
Task.Delay(2000).ContinueWith(_ => tcs.TrySetCanceled()).AssertNoAwait();
|
||||
try
|
||||
{
|
||||
return await tcs.Task;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new DeviceException(SdkResources.DeviceOffline);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
243
NAPS2.Sdk/Scan/Internal/Apple/DeviceOperator.cs
Normal file
243
NAPS2.Sdk/Scan/Internal/Apple/DeviceOperator.cs
Normal file
@ -0,0 +1,243 @@
|
||||
#if MAC
|
||||
using System.Threading;
|
||||
using CoreGraphics;
|
||||
using Foundation;
|
||||
using ImageCaptureCore;
|
||||
using NAPS2.Images.Bitwise;
|
||||
using NAPS2.Scan.Exceptions;
|
||||
|
||||
namespace NAPS2.Scan.Internal.Apple;
|
||||
|
||||
internal class DeviceOperator : ICScannerDeviceDelegate
|
||||
{
|
||||
private readonly ScanningContext _scanningContext;
|
||||
private readonly ICScannerDevice _device;
|
||||
private readonly ScanOptions _options;
|
||||
private readonly IScanEvents _scanEvents;
|
||||
private readonly Action<IMemoryImage> _callback;
|
||||
private readonly TaskCompletionSource _openSessionTcs = new();
|
||||
private readonly TaskCompletionSource _readyTcs = new();
|
||||
private TaskCompletionSource<ICScannerFunctionalUnit> _unitTcs = new();
|
||||
private readonly TaskCompletionSource _scanTcs = new();
|
||||
private readonly TaskCompletionSource _closeTcs = new();
|
||||
private MemoryStream? _buffer;
|
||||
|
||||
public DeviceOperator(ScanningContext scanningContext, ICScannerDevice device, ScanOptions options,
|
||||
CancellationToken cancelToken, IScanEvents scanEvents, Action<IMemoryImage> callback)
|
||||
{
|
||||
_scanningContext = scanningContext;
|
||||
_device = device;
|
||||
_options = options;
|
||||
_scanEvents = scanEvents;
|
||||
_callback = callback;
|
||||
|
||||
cancelToken.Register(() =>
|
||||
{
|
||||
_openSessionTcs.TrySetCanceled();
|
||||
_readyTcs.TrySetCanceled();
|
||||
_unitTcs.TrySetCanceled();
|
||||
_scanTcs.TrySetCanceled();
|
||||
_closeTcs.TrySetCanceled();
|
||||
});
|
||||
}
|
||||
|
||||
public override void DidOpenSession(ICDevice device, NSError? error)
|
||||
{
|
||||
SetResultOrError(_openSessionTcs, error);
|
||||
}
|
||||
|
||||
public override void DidBecomeReady(ICDevice device)
|
||||
{
|
||||
_readyTcs.TrySetResult();
|
||||
}
|
||||
|
||||
public override void DidCloseSession(ICDevice device, NSError? error)
|
||||
{
|
||||
SetResultOrError(_closeTcs, error);
|
||||
}
|
||||
|
||||
public override void DidReceiveStatusInformation(ICDevice device, NSDictionary<NSString, NSObject> status)
|
||||
{
|
||||
var state = status[ICStatusNotificationKeys.NotificationKey] as NSString;
|
||||
Console.WriteLine($"{nameof(DidReceiveStatusInformation)}: Status: {status} State {state}");
|
||||
|
||||
if (state == ICScannerStatus.WarmingUp)
|
||||
{
|
||||
_scanEvents.PageStart();
|
||||
}
|
||||
}
|
||||
|
||||
public override void DidEncounterError(ICDevice device, NSError? error)
|
||||
{
|
||||
var ex = new DeviceException(error.Description);
|
||||
// TODO: Put these in a list or something
|
||||
_openSessionTcs.TrySetException(ex);
|
||||
_readyTcs.TrySetException(ex);
|
||||
_unitTcs.TrySetException(ex);
|
||||
_scanTcs.TrySetException(ex);
|
||||
_closeTcs.TrySetException(ex);
|
||||
}
|
||||
|
||||
// TODO: This will be called if the scanner is in use. We can consider waiting a couple seconds for the scanner
|
||||
// TODO: to become available before sending a busy error.
|
||||
public override void DidBecomeAvailable(ICScannerDevice scanner)
|
||||
{
|
||||
Console.WriteLine($"{nameof(DidBecomeAvailable)}: {scanner}");
|
||||
}
|
||||
|
||||
public override void DidSelectFunctionalUnit(
|
||||
ICScannerDevice scanner, ICScannerFunctionalUnit functionalUnit, NSError? error)
|
||||
{
|
||||
SetResultOrError(_unitTcs, functionalUnit, error);
|
||||
}
|
||||
|
||||
public override void DidScanToBandData(ICScannerDevice scanner, ICScannerBandData data)
|
||||
{
|
||||
var (pixelFormat, subPixelType) = (data.PixelDataType, data.NumComponents, data.BitsPerComponent) switch
|
||||
{
|
||||
(ICScannerPixelDataType.BW, 1, 1) => (ImagePixelFormat.BW1, SubPixelType.Bit),
|
||||
(ICScannerPixelDataType.Gray, 1, 8) => (ImagePixelFormat.Gray8, SubPixelType.Gray),
|
||||
(ICScannerPixelDataType.Rgb, 3, 8) => (ImagePixelFormat.RGB24, SubPixelType.Rgb),
|
||||
(ICScannerPixelDataType.Rgb, 4, 8) => (ImagePixelFormat.ARGB32, SubPixelType.Rgba),
|
||||
_ => (ImagePixelFormat.Unsupported, null)
|
||||
};
|
||||
if (pixelFormat == ImagePixelFormat.Unsupported)
|
||||
{
|
||||
// TODO: Set errors
|
||||
return;
|
||||
}
|
||||
var bufferInfo = new PixelInfo(
|
||||
(int) data.FullImageWidth,
|
||||
(int) data.FullImageHeight,
|
||||
subPixelType!,
|
||||
(int) data.BytesPerRow);
|
||||
_buffer ??= new MemoryStream((int) bufferInfo.Length);
|
||||
data.DataBuffer!.AsStream().CopyTo(_buffer);
|
||||
// TODO: The buffer gets written pretty much all at once, at least for escl - maybe we can/should reuse TwainProgressEstimator
|
||||
_scanEvents.PageProgress(_buffer.Length / (double) bufferInfo.Length);
|
||||
if (_buffer.Length >= bufferInfo.Length)
|
||||
{
|
||||
var image = _scanningContext.ImageContext.Create(
|
||||
(int) data.FullImageWidth, (int) data.FullImageHeight, pixelFormat);
|
||||
new CopyBitwiseImageOp().Perform(_buffer.GetBuffer(), bufferInfo, image);
|
||||
_callback(image);
|
||||
_buffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void DidCompleteScan(ICScannerDevice scanner, NSError? error)
|
||||
{
|
||||
SetResultOrError(_scanTcs, error);
|
||||
}
|
||||
|
||||
private void SetResultOrError(TaskCompletionSource tcs, NSError? error)
|
||||
{
|
||||
if (error != null)
|
||||
{
|
||||
tcs.TrySetException(GetException(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.TrySetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetResultOrError<T>(TaskCompletionSource<T> tcs, T value, NSError? error)
|
||||
{
|
||||
if (error != null)
|
||||
{
|
||||
tcs.TrySetException(GetException(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.TrySetResult(value);
|
||||
}
|
||||
}
|
||||
|
||||
private Exception GetException(NSError error)
|
||||
{
|
||||
return new DeviceException(error.LocalizedDescription);
|
||||
}
|
||||
|
||||
public override void DidRemoveDevice(ICDevice device)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task Scan()
|
||||
{
|
||||
try
|
||||
{
|
||||
_device.Delegate = this;
|
||||
_device.RequestOpenSession();
|
||||
await _openSessionTcs.Task;
|
||||
await _readyTcs.Task;
|
||||
var unit = await SelectUnit(_options.PaperSource == PaperSource.Flatbed
|
||||
? ICScannerFunctionalUnitType.Flatbed
|
||||
: ICScannerFunctionalUnitType.DocumentFeeder);
|
||||
SetScanArea(unit);
|
||||
// TODO: Check supported resolutions?
|
||||
unit.Resolution = (nuint) _options.Dpi;
|
||||
unit.BitDepth = _options.BitDepth == BitDepth.BlackAndWhite
|
||||
? ICScannerBitDepth.Bits1
|
||||
: ICScannerBitDepth.Bits8;
|
||||
unit.PixelDataType = _options.BitDepth switch
|
||||
{
|
||||
BitDepth.BlackAndWhite => ICScannerPixelDataType.BW,
|
||||
BitDepth.Grayscale => ICScannerPixelDataType.Gray,
|
||||
_ => ICScannerPixelDataType.Rgb
|
||||
};
|
||||
_device.TransferMode = ICScannerTransferMode.MemoryBased;
|
||||
// TODO: increase? or maybe not as this could still be useful progress for twain scanners
|
||||
_device.MaxMemoryBandSize = 65536;
|
||||
_device.RequestScan();
|
||||
await _scanTcs.Task;
|
||||
_device.RequestCloseSession();
|
||||
await _closeTcs.Task;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// TODO: Cancellation not working
|
||||
_device.CancelScan();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_device.HasOpenSession)
|
||||
{
|
||||
_device.RequestCloseSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetScanArea(ICScannerFunctionalUnit unit)
|
||||
{
|
||||
unit.MeasurementUnit = ICScannerMeasurementUnit.Inches;
|
||||
var maxSize = unit.PhysicalSize;
|
||||
var width = Math.Min((double) _options.PageSize!.WidthInInches, maxSize.Width);
|
||||
var height = Math.Min((double) _options.PageSize.HeightInInches, maxSize.Height);
|
||||
var deltaX = maxSize.Width - width;
|
||||
var offsetX = _options.PageAlign switch
|
||||
{
|
||||
HorizontalAlign.Left => deltaX,
|
||||
HorizontalAlign.Center => deltaX / 2,
|
||||
_ => 0
|
||||
};
|
||||
unit.ScanArea = new CGRect(offsetX, 0, offsetX + width, height);
|
||||
}
|
||||
|
||||
private async Task<ICScannerFunctionalUnit> SelectUnit(ICScannerFunctionalUnitType unitType)
|
||||
{
|
||||
// TODO: Can we clean this up at all?
|
||||
var availableUnits = _device.AvailableFunctionalUnitTypes.Select(x =>
|
||||
(ICScannerFunctionalUnitType) (int) x).ToList();
|
||||
await _unitTcs.Task;
|
||||
_unitTcs = new TaskCompletionSource<ICScannerFunctionalUnit>();
|
||||
if (availableUnits.Contains(unitType))
|
||||
{
|
||||
_device.RequestSelectFunctionalUnit(unitType);
|
||||
var result = await _unitTcs.Task;
|
||||
return result;
|
||||
}
|
||||
return _device.SelectedFunctionalUnit;
|
||||
}
|
||||
}
|
||||
#endif
|
54
NAPS2.Sdk/Scan/Internal/Apple/DeviceReader.cs
Normal file
54
NAPS2.Sdk/Scan/Internal/Apple/DeviceReader.cs
Normal file
@ -0,0 +1,54 @@
|
||||
#if MAC
|
||||
using System.Collections.Immutable;
|
||||
using ImageCaptureCore;
|
||||
|
||||
namespace NAPS2.Scan.Internal.Apple;
|
||||
|
||||
internal class DeviceReader : ICDeviceBrowserDelegate
|
||||
{
|
||||
private readonly ICDeviceBrowser _browser = new();
|
||||
|
||||
public DeviceReader()
|
||||
{
|
||||
_browser.Delegate = this;
|
||||
_browser.BrowsedDeviceTypeMask =
|
||||
ICBrowsedDeviceType.Scanner | ICBrowsedDeviceType.Local | ICBrowsedDeviceType.Remote;
|
||||
}
|
||||
|
||||
public ImmutableList<ICScannerDevice> Devices { get; private set; } = ImmutableList<ICScannerDevice>.Empty;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_browser.Start();
|
||||
}
|
||||
|
||||
public event EventHandler<DeviceEventArgs>? DeviceFound;
|
||||
|
||||
public override void DidAddDevice(ICDeviceBrowser browser, ICDevice device, bool moreComing)
|
||||
{
|
||||
// TODO: Use moreComing
|
||||
if (device.Type.HasFlag(ICDeviceType.Scanner))
|
||||
{
|
||||
var scannerDevice = (ICScannerDevice) device;
|
||||
Devices = Devices.Add(scannerDevice);
|
||||
DeviceFound?.Invoke(this, new DeviceEventArgs(scannerDevice));
|
||||
}
|
||||
}
|
||||
|
||||
public override void DidRemoveDevice(ICDeviceBrowser browser, ICDevice device, bool moreGoing)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// _browser.Stop();
|
||||
// _browser.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
internal record DeviceEventArgs(ICScannerDevice Device);
|
||||
}
|
||||
#endif
|
@ -1,8 +1,4 @@
|
||||
using NAPS2.Scan.Internal.Sane;
|
||||
using NAPS2.Scan.Internal.Twain;
|
||||
using NAPS2.Scan.Internal.Wia;
|
||||
|
||||
namespace NAPS2.Scan.Internal;
|
||||
namespace NAPS2.Scan.Internal;
|
||||
|
||||
internal class ScanDriverFactory : IScanDriverFactory
|
||||
{
|
||||
@ -17,18 +13,23 @@ internal class ScanDriverFactory : IScanDriverFactory
|
||||
{
|
||||
switch (options.Driver)
|
||||
{
|
||||
#if !MAC
|
||||
#if MAC
|
||||
case Driver.Apple:
|
||||
return new Apple.AppleScanDriver(_scanningContext);
|
||||
#else
|
||||
case Driver.Wia:
|
||||
return new WiaScanDriver(_scanningContext);
|
||||
return new Wia.WiaScanDriver(_scanningContext);
|
||||
case Driver.Twain:
|
||||
return options.TwainOptions.Adapter == TwainAdapter.Legacy
|
||||
? new LegacyTwainScanDriver()
|
||||
: new TwainScanDriver(_scanningContext);
|
||||
? new Twain.LegacyTwainScanDriver()
|
||||
: new Twain.TwainScanDriver(_scanningContext);
|
||||
#endif
|
||||
case Driver.Sane:
|
||||
return new SaneScanDriver(_scanningContext);
|
||||
return new Sane.SaneScanDriver(_scanningContext);
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported driver: {options.Driver}");
|
||||
};
|
||||
throw new NotSupportedException(
|
||||
$"Unsupported driver: {options.Driver}. " +
|
||||
"Make sure you're using the right framework target (e.g. net6-macos10.15 for the Apple driver).");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user