Skip to content

Commit

Permalink
Twain capabilities
Browse files Browse the repository at this point in the history
  • Loading branch information
cyanfish committed Jul 28, 2024
1 parent 395afa9 commit e5d4810
Show file tree
Hide file tree
Showing 15 changed files with 220 additions and 11 deletions.
1 change: 1 addition & 0 deletions NAPS2.Sdk/Remoting/Worker/WorkerService.proto
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ service WorkerService {
rpc GetCaps (GetCapsRequest) returns (GetCapsResponse) {}
rpc Scan (ScanRequest) returns (stream ScanResponse) {}
rpc TwainGetDeviceList (GetDeviceListRequest) returns (GetDeviceListResponse) {}
rpc TwainGetCaps (GetCapsRequest) returns (GetCapsResponse) {}
rpc TwainScan (TwainScanRequest) returns (stream TwainScanResponse) {}
rpc StopWorker (StopWorkerRequest) returns (StopWorkerResponse) {}
}
Expand Down
12 changes: 10 additions & 2 deletions NAPS2.Sdk/Remoting/Worker/WorkerServiceAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ public async Task GetDevices(ScanOptions options, CancellationToken cancelToken,
public async Task<ScanCaps?> GetCaps(ScanOptions options, CancellationToken cancelToken)
{
var req = new GetCapsRequest { OptionsXml = options.ToXml() };
var resp = await _client.GetCapsAsync(req);
var resp = await _client.GetCapsAsync(req, cancellationToken: cancelToken);
RemotingHelper.HandleErrors(resp.Error);
return resp.ScanCapsXml.FromXml<ScanCaps>();
return string.IsNullOrEmpty(resp.ScanCapsXml) ? null : resp.ScanCapsXml.FromXml<ScanCaps>();
}

public async Task Scan(ScanningContext scanningContext, ScanOptions options, CancellationToken cancelToken,
Expand Down Expand Up @@ -209,6 +209,14 @@ public async Task<List<ScanDevice>> TwainGetDeviceList(ScanOptions options)
return resp.DeviceListXml.FromXml<List<ScanDevice>>();
}

public async Task<ScanCaps?> TwainGetCaps(ScanOptions options)
{
var req = new GetCapsRequest { OptionsXml = options.ToXml() };
var resp = await _client.TwainGetCapsAsync(req);
RemotingHelper.HandleErrors(resp.Error);
return string.IsNullOrEmpty(resp.ScanCapsXml) ? null : resp.ScanCapsXml.FromXml<ScanCaps>();
}

public ProcessedImage ImportPostProcess(ScanningContext scanningContext, ProcessedImage img, int? thumbnailSize,
BarcodeDetectionOptions barcodeDetectionOptions)
{
Expand Down
33 changes: 33 additions & 0 deletions NAPS2.Sdk/Remoting/Worker/WorkerServiceImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ await _remoteScanController.GetDevices(scanOptions,
await sequencedWriter.WaitForCompletion();
}

public override async Task<GetCapsResponse> GetCaps(GetCapsRequest request, ServerCallContext context)
{
using var callRef = StartCall();
try
{
var scanOptions = request.OptionsXml.FromXml<ScanOptions>();
var caps = await _remoteScanController.GetCaps(scanOptions, context.CancellationToken);
return new GetCapsResponse { ScanCapsXml = caps?.ToXml() ?? "" };
}
catch (Exception e)
{
return new GetCapsResponse { Error = RemotingHelper.ToError(e) };
}
}

public override async Task Scan(ScanRequest request, IServerStreamWriter<ScanResponse> responseStream,
ServerCallContext context)
{
Expand Down Expand Up @@ -327,6 +342,24 @@ public override async Task<GetDeviceListResponse> TwainGetDeviceList(GetDeviceLi
}
}

public override async Task<GetCapsResponse> TwainGetCaps(GetCapsRequest request, ServerCallContext context)
{
using var callRef = StartCall();
try
{
var options = request.OptionsXml.FromXml<ScanOptions>();
var caps = await _twainController.GetCaps(options);
return new GetCapsResponse
{
ScanCapsXml = caps?.ToXml() ?? ""
};
}
catch (Exception e)
{
return new GetCapsResponse { Error = RemotingHelper.ToError(e) };
}
}

public override async Task TwainScan(TwainScanRequest request,
IServerStreamWriter<TwainScanResponse> responseStream, ServerCallContext context)
{
Expand Down
7 changes: 6 additions & 1 deletion NAPS2.Sdk/Scan/BitDepthCaps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ public record BitDepthCaps(
bool SupportsColor,
bool SupportsGrayscale,
bool SupportsBlackAndWhite
);
)
{
private BitDepthCaps() : this(false, false, false)
{
}
}
7 changes: 6 additions & 1 deletion NAPS2.Sdk/Scan/DpiCaps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ public record DpiCaps(
int Min,
int Max,
int Step
);
)
{
private DpiCaps() : this(null, 0, 0, 0)
{
}
}
1 change: 1 addition & 0 deletions NAPS2.Sdk/Scan/Internal/Twain/ITwainController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ namespace NAPS2.Scan.Internal.Twain;
internal interface ITwainController
{
Task<List<ScanDevice>> GetDeviceList(ScanOptions options);
Task<ScanCaps?> GetCaps(ScanOptions options);
Task StartScan(ScanOptions options, ITwainEvents twainEvents, CancellationToken cancelToken);
}
117 changes: 116 additions & 1 deletion NAPS2.Sdk/Scan/Internal/Twain/LocalTwainController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#if !MAC
using System.Reflection;
using System.Collections.Immutable;
using System.Threading;
using Microsoft.Extensions.Logging;
using NAPS2.Scan.Exceptions;
Expand Down Expand Up @@ -85,6 +85,121 @@ private List<ScanDevice> InternalGetDeviceList(ScanOptions options)
}
}

public Task<ScanCaps?> GetCaps(ScanOptions options)
{
if (options.TwainOptions.Dsm != TwainDsm.Old)
{
TwainDsmSetup.Run();
}
return Task.Run(() =>
{
var caps = InternalGetCaps(options);
if (options.TwainOptions.Dsm != TwainDsm.Old && caps == null)
{
// Fall back to OldDsm in case of no devices
// This is primarily for Citrix support, which requires using twain_32.dll for TWAIN passthrough
caps = InternalGetCaps(options);
}

return caps;
});
}

private ScanCaps? InternalGetCaps(ScanOptions options)
{
PlatformInfo.Current.PreferNewDSM = options.TwainOptions.Dsm != TwainDsm.Old;
var session = new TwainSession(TwainAppId);
// TODO: Standardize on custom hook?
#if NET6_0_OR_GREATER
if (!OperatingSystem.IsWindows()) throw new InvalidOperationException("Windows-only");
session.Open(new Win32MessageLoopHook(_logger));
#else
session.Open();
#endif
try
{
var ds = session.GetSources().FirstOrDefault(ds => ds.Name == options.Device!.ID);
if (ds == null) return null;
try
{
var rc = ds.Open();
if (rc != ReturnCode.Success)
{
_logger.LogDebug("Couldn't open TWAIN data source for capabilities, return code {RC}", rc);
return null;
}
try
{
var feederCap = ds.Capabilities.CapFeederEnabled;

feederCap.SetValue(BoolType.False);
bool supportsFlatbed = feederCap.GetCurrent() == BoolType.False;
var flatbedCaps = supportsFlatbed ? GetPerSourceCaps(ds) : null;

feederCap.SetValue(BoolType.True);
bool supportsFeeder = feederCap.GetCurrent() == BoolType.True;
var feederCaps = supportsFeeder ? GetPerSourceCaps(ds) : null;

bool supportsDuplex = supportsFeeder && ds.Capabilities.CapDuplex.GetCurrent() != Duplex.None;

return new ScanCaps(
new MetadataCaps(
Manufacturer: ds.Manufacturer,
Model: ds.Name,
SerialNumber: ds.Capabilities.CapSerialNumber.GetCurrent()
),
new PaperSourceCaps(supportsFlatbed, supportsFeeder, supportsDuplex,
ds.Capabilities.CapAutomaticSenseMedium.IsSupported ||
ds.Capabilities.CapFeederLoaded.IsSupported),
flatbedCaps,
feederCaps,
supportsDuplex ? feederCaps : null
);
}
finally
{
ds.Close();
}
}
catch (Exception e)
{
_logger.LogError(e, "Error getting TWAIN capabilities");
return null;
}
}
finally
{
try
{
session.Close();
}
catch (Exception e)
{
_logger.LogError(e, "Error closing TWAIN session for capabilities");
}
}
}

private PerSourceCaps GetPerSourceCaps(DataSource ds)
{
var xRes = ds.Capabilities.ICapXResolution.GetValues().Select(x => (int) x.Whole);
var yRes = ds.Capabilities.ICapYResolution.GetValues().Select(x => (int) x.Whole);
var dpiCaps = new DpiCaps(xRes.Intersect(yRes).ToImmutableList(), 0, 0, 0);
var pixelTypes = ds.Capabilities.ICapPixelType.GetValues().ToList();
var bitDepthCaps = new BitDepthCaps(
pixelTypes.Contains(PixelType.RGB),
pixelTypes.Contains(PixelType.Gray),
pixelTypes.Contains(PixelType.BlackWhite));
var w = ds.Capabilities.ICapPhysicalWidth.GetCurrent();
var h = ds.Capabilities.ICapPhysicalHeight.GetCurrent();
var scanAreaSize = new PageSize(
decimal.Round(w.Whole + w.Fraction / 65536m, 4),
decimal.Round(h.Whole + h.Fraction / 65536m, 4),
PageSizeUnit.Inch);
var pageSizeCaps = new PageSizeCaps(scanAreaSize);
return new PerSourceCaps(dpiCaps, bitDepthCaps, pageSizeCaps);
}

public async Task StartScan(ScanOptions options, ITwainEvents twainEvents, CancellationToken cancelToken)
{
if (options.TwainOptions.Dsm != TwainDsm.Old)
Expand Down
6 changes: 6 additions & 0 deletions NAPS2.Sdk/Scan/Internal/Twain/RemoteTwainController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public async Task<List<ScanDevice>> GetDeviceList(ScanOptions options)
return await workerContext.Service.TwainGetDeviceList(options);
}

public async Task<ScanCaps?> GetCaps(ScanOptions options)
{
using var workerContext = CreateWorker(options);
return await workerContext.Service.TwainGetCaps(options);
}

public async Task StartScan(ScanOptions options, ITwainEvents twainEvents, CancellationToken cancelToken)
{
using var workerContext = CreateWorker(options);
Expand Down
5 changes: 5 additions & 0 deletions NAPS2.Sdk/Scan/Internal/Twain/StubTwainController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ public Task<List<ScanDevice>> GetDeviceList(ScanOptions options)
throw new NotSupportedException();
}

public Task<ScanCaps?> GetCaps(ScanOptions options)
{
throw new NotSupportedException();
}

public Task StartScan(ScanOptions options, ITwainEvents twainEvents, CancellationToken cancelToken)
{
throw new NotSupportedException();
Expand Down
7 changes: 6 additions & 1 deletion NAPS2.Sdk/Scan/Internal/Twain/TwainScanDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ public Task GetDevices(ScanOptions options, CancellationToken cancelToken, Actio

public Task<ScanCaps?> GetCaps(ScanOptions options, CancellationToken cancelToken)
{
return Task.FromResult<ScanCaps?>(null);
CheckArch(options);
return Task.Run(async () =>
{
var controller = GetTwainController(options);
return await controller.GetCaps(options);
});
}

public Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents,
Expand Down
7 changes: 6 additions & 1 deletion NAPS2.Sdk/Scan/MetadataCaps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ public record MetadataCaps(
string? SerialNumber = null,
string? Location = null,
string? IconUri = null
);
)
{
private MetadataCaps() : this(null, null, null, null, null, null)
{
}
}
7 changes: 6 additions & 1 deletion NAPS2.Sdk/Scan/PageSizeCaps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ namespace NAPS2.Scan;

public record PageSizeCaps(
PageSize ScanAreaSize
);
)
{
private PageSizeCaps() : this(new PageSize(0, 0, PageSizeUnit.Inch))
{
}
}
7 changes: 6 additions & 1 deletion NAPS2.Sdk/Scan/PaperSourceCaps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ public record PaperSourceCaps(
bool SupportsFeeder,
bool SupportsDuplex,
bool CanCheckIfFeederHasPaper
);
)
{
private PaperSourceCaps() : this(false, false, false, false)
{
}
}
7 changes: 6 additions & 1 deletion NAPS2.Sdk/Scan/PerSourceCaps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ public record PerSourceCaps(
DpiCaps? DpiCaps,
BitDepthCaps? BitDepthCaps,
PageSizeCaps? PageSizeCaps
);
)
{
private PerSourceCaps() : this(null, null, null)
{
}
}
7 changes: 6 additions & 1 deletion NAPS2.Sdk/Scan/ScanCaps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ public record ScanCaps(
PerSourceCaps? FlatbedCaps,
PerSourceCaps? FeederCaps,
PerSourceCaps? DuplexCaps
);
)
{
private ScanCaps() : this(null, null, null, null, null)
{
}
}

0 comments on commit e5d4810

Please sign in to comment.