diff --git a/NAPS2.Escl.Server/EsclApiController.cs b/NAPS2.Escl.Server/EsclApiController.cs index 2e3beaede..5e0e2358a 100644 --- a/NAPS2.Escl.Server/EsclApiController.cs +++ b/NAPS2.Escl.Server/EsclApiController.cs @@ -39,6 +39,7 @@ internal class EsclApiController : WebApiController new XElement(PwgNs + "Version", caps.Version), new XElement(PwgNs + "MakeAndModel", caps.MakeAndModel), new XElement(PwgNs + "SerialNumber", caps.SerialNumber), + new XElement(ScanNs + "Manufacturer", caps.Manufacturer), new XElement(ScanNs + "UUID", caps.Uuid), new XElement(ScanNs + "AdminURI", ""), new XElement(ScanNs + "IconURI", iconUri), diff --git a/NAPS2.Escl/Client/CapabilitiesParser.cs b/NAPS2.Escl/Client/CapabilitiesParser.cs index ec9ee342c..fa1901f7a 100644 --- a/NAPS2.Escl/Client/CapabilitiesParser.cs +++ b/NAPS2.Escl/Client/CapabilitiesParser.cs @@ -31,6 +31,7 @@ internal static class CapabilitiesParser Version = root.Element(PwgNs + "Version")?.Value ?? EsclCapabilities.DEFAULT_VERSION, MakeAndModel = root.Element(PwgNs + "MakeAndModel")?.Value, SerialNumber = root.Element(PwgNs + "SerialNumber")?.Value, + Manufacturer = root.Element(ScanNs + "Manufacturer")?.Value, Uuid = root.Element(ScanNs + "UUID")?.Value, AdminUri = root.Element(ScanNs + "AdminURI")?.Value, IconUri = root.Element(ScanNs + "IconURI")?.Value, diff --git a/NAPS2.Escl/EsclCapabilities.cs b/NAPS2.Escl/EsclCapabilities.cs index 767e24562..b2e9255f2 100644 --- a/NAPS2.Escl/EsclCapabilities.cs +++ b/NAPS2.Escl/EsclCapabilities.cs @@ -7,6 +7,7 @@ public class EsclCapabilities public string Version { get; init; } = DEFAULT_VERSION; public string? MakeAndModel { get; init; } public string? SerialNumber { get; init; } + public string? Manufacturer { get; init; } public string? Uuid { get; init; } public string? AdminUri { get; init; } public string? IconUri { get; init; } diff --git a/NAPS2.Sdk/Scan/Internal/Escl/EsclScanDriver.cs b/NAPS2.Sdk/Scan/Internal/Escl/EsclScanDriver.cs index 60d827412..6de8e73a0 100644 --- a/NAPS2.Sdk/Scan/Internal/Escl/EsclScanDriver.cs +++ b/NAPS2.Sdk/Scan/Internal/Escl/EsclScanDriver.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Net.Http; using System.Net.Sockets; using System.Threading; @@ -53,9 +54,6 @@ internal class EsclScanDriver : IScanDriver { return; } - // TODO: When we implement scanner capabilities, store all the connection information in there so we can - // try and connect directly before querying for a potentially-updated-IP (and then back-propagate the new - // connection info). var id = service.Uuid; var name = string.IsNullOrEmpty(service.ScannerName) ? $"{ip}" @@ -73,37 +71,100 @@ internal class EsclScanDriver : IScanDriver } } - public Task GetCaps(ScanOptions options, CancellationToken cancelToken) + public async Task GetCaps(ScanOptions options, CancellationToken cancelToken) { - return Task.FromResult(new ScanCaps()); + if (cancelToken.IsCancellationRequested) return new ScanCaps(); + var client = await GetEsclClient(options, cancelToken); + if (client == null) return new ScanCaps(); + + try + { + var caps = await client.GetCapabilities(); + return new ScanCaps + { + MetadataCaps = new() + { + Model = caps.MakeAndModel, + Manufacturer = caps.Manufacturer, + SerialNumber = caps.SerialNumber, + IconUri = client.IconUri + }, + PaperSourceCaps = new() + { + SupportsFlatbed = caps.PlatenCaps != null, + SupportsFeeder = caps.AdfSimplexCaps != null, + SupportsDuplex = caps.AdfDuplexCaps != null, + CanCheckIfFeederHasPaper = true + }, + FlatbedCaps = MapCaps(caps.PlatenCaps), + FeederCaps = MapCaps(caps.AdfSimplexCaps), + DuplexCaps = MapCaps(caps.AdfDuplexCaps) + }; + } + catch (HttpRequestException ex) when (ex.InnerException is TaskCanceledException or SocketException) + { + // A connection timeout manifests as TaskCanceledException + _logger.LogError(ex, "Error connecting to ESCL device"); + throw new DeviceCommunicationException(); + } + catch (TaskCanceledException) + { + } + return new ScanCaps(); + } + + private PerSourceCaps? MapCaps(EsclInputCaps? caps) + { + if (caps == null) + { + return null; + } + return PerSourceCaps.UnionAll(caps.SettingProfiles.Select(profile => MapSettingProfile(caps, profile))); + } + + private PerSourceCaps MapSettingProfile(EsclInputCaps caps, EsclSettingProfile profile) + { + DpiCaps? dpiCaps = null; + if (profile.DiscreteResolutions.Count > 0) + { + dpiCaps = new DpiCaps + { + Values = profile.DiscreteResolutions + .Where(res => res.XResolution == res.YResolution) + .Select(res => res.XResolution).ToImmutableList() + }; + } + else if (profile.XResolutionRange != null && profile.YResolutionRange != null) + { + int min = Math.Max(profile.XResolutionRange.Min, profile.YResolutionRange.Min); + int max = Math.Min(profile.XResolutionRange.Max, profile.YResolutionRange.Max); + int step = Math.Max(profile.XResolutionRange.Step, profile.YResolutionRange.Step); + dpiCaps = DpiCaps.ForRange(min, max, step); + } + return new PerSourceCaps + { + DpiCaps = dpiCaps, + BitDepthCaps = new BitDepthCaps + { + SupportsColor = profile.ColorModes.Contains(EsclColorMode.RGB24), + SupportsGrayscale = profile.ColorModes.Contains(EsclColorMode.Grayscale8), + SupportsBlackAndWhite = profile.ColorModes.Contains(EsclColorMode.BlackAndWhite1) + }, + PageSizeCaps = caps.MaxWidth != null && caps.MaxHeight != null + ? new PageSizeCaps + { + ScanArea = new PageSize(caps.MaxWidth.Value / 300m, caps.MaxHeight.Value / 300m, PageSizeUnit.Inch) + } + : null + }; } public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action callback) { if (cancelToken.IsCancellationRequested) return; - - EsclClient client; - string deviceId = options.Device!.ID; - - if (deviceId.StartsWith("http://") || deviceId.StartsWith("https://")) - { - client = new EsclClient(new Uri(deviceId)); - // TODO: Handle device offline? - } - else - { - var service = await FindDeviceEsclService(options, cancelToken); - - if (cancelToken.IsCancellationRequested) return; - if (service == null) throw new DeviceOfflineException(); - - client = new EsclClient(service); - } - - client.SecurityPolicy = options.EsclOptions.SecurityPolicy; - client.Logger = _logger; - client.CancelToken = cancelToken; + var client = await GetEsclClient(options, cancelToken); + if (client == null) return; try { @@ -169,6 +230,31 @@ internal class EsclScanDriver : IScanDriver } } + private async Task GetEsclClient(ScanOptions options, CancellationToken cancelToken) + { + EsclClient client; + string deviceId = options.Device!.ID; + + if (deviceId.StartsWith("http://") || deviceId.StartsWith("https://")) + { + client = new EsclClient(new Uri(deviceId)); + // TODO: Handle device offline? + } + else + { + var service = await FindDeviceEsclService(options, cancelToken); + if (cancelToken.IsCancellationRequested) return null; + if (service == null) throw new DeviceOfflineException(); + client = new EsclClient(service); + } + + client.SecurityPolicy = options.EsclOptions.SecurityPolicy; + client.Logger = _logger; + client.CancelToken = cancelToken; + + return client; + } + private async Task CreateScanJobAndCorrectInvalidSettings(EsclClient client, EsclScanSettings scanSettings) { _logger.LogDebug("Creating ESCL job: format {Format}, source {Source}, mode {Mode}", diff --git a/NAPS2.Sdk/Scan/MetadataCaps.cs b/NAPS2.Sdk/Scan/MetadataCaps.cs index c02f59f27..842a3786f 100644 --- a/NAPS2.Sdk/Scan/MetadataCaps.cs +++ b/NAPS2.Sdk/Scan/MetadataCaps.cs @@ -25,11 +25,6 @@ public class MetadataCaps /// public string? SerialNumber { get; init; } - /// - /// The location note associated with the device. - /// - public string? Location { get; init; } - /// /// The URI for an icon associated with the device. /// diff --git a/NAPS2.Sdk/Scan/PerSourceCaps.cs b/NAPS2.Sdk/Scan/PerSourceCaps.cs index f98e4a7e4..4ecf19d4a 100644 --- a/NAPS2.Sdk/Scan/PerSourceCaps.cs +++ b/NAPS2.Sdk/Scan/PerSourceCaps.cs @@ -11,10 +11,11 @@ public class PerSourceCaps /// Gets an object representing the union of all possible option values allowed by the provided objects. /// This can be helpful when presenting the user with a single set of possible options for multiple sources. /// - public static PerSourceCaps UnionAll(ICollection caps) + public static PerSourceCaps UnionAll(IEnumerable caps) { + var capsColl = caps as ICollection ?? caps.ToList(); DpiCaps? dpiCaps = null; - foreach (var dpiValues in caps.Select(x => x.DpiCaps?.Values).WhereNotNull()) + foreach (var dpiValues in capsColl.Select(x => x.DpiCaps?.Values).WhereNotNull()) { dpiCaps = new DpiCaps { @@ -22,7 +23,7 @@ public class PerSourceCaps }; } BitDepthCaps? bitDepthCaps = null; - foreach (var bd in caps.Select(x => x.BitDepthCaps).WhereNotNull()) + foreach (var bd in capsColl.Select(x => x.BitDepthCaps).WhereNotNull()) { bitDepthCaps = new BitDepthCaps { @@ -32,7 +33,7 @@ public class PerSourceCaps }; } PageSizeCaps? pageSizeCaps = null; - foreach (var area in caps.Select(x => x.PageSizeCaps?.ScanArea).WhereNotNull()) + foreach (var area in capsColl.Select(x => x.PageSizeCaps?.ScanArea).WhereNotNull()) { pageSizeCaps = new PageSizeCaps {