Improve scan error handling

This commit is contained in:
Ben Olden-Cooligan 2019-07-14 11:17:05 -04:00
parent eaa90f6d7f
commit 02ecc927e5
11 changed files with 151 additions and 28 deletions

View File

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using NAPS2.Config;
using NAPS2.Images;
using NAPS2.Images.Storage;
namespace NAPS2.Sdk.Tests
@ -48,6 +50,11 @@ namespace NAPS2.Sdk.Tests
ImageContext.ImageMetadataFactory = rsm;
}
public ScannedImage CreateScannedImage()
{
return ImageContext.CreateScannedImage(new GdiImage(new Bitmap(100, 100)));
}
public virtual void Dispose()
{
rsm?.ForceReleaseLock();

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NAPS2.Images;
using NAPS2.Scan;
using NAPS2.Scan.Experimental;
using NAPS2.Scan.Experimental.Internal;
namespace NAPS2.Sdk.Tests.Mocks
{
internal class StubScanBridge : IScanBridge
{
public List<ScanDevice> MockDevices { get; set; } = new List<ScanDevice>();
public List<ScannedImage> MockOutput { get; set; } = new List<ScannedImage>();
public List<ScanDevice> GetDeviceList(ScanOptions options) => MockDevices;
public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents, Action<ScannedImage, PostProcessingContext> callback)
{
await Task.Run(() =>
{
foreach (var img in MockOutput)
{
callback(img, new PostProcessingContext());
}
});
}
}
}

View File

@ -131,6 +131,7 @@
<DependentUpon>PdfiumTestsData.resx</DependentUpon>
</Compile>
<Compile Include="Mocks\MockScanDriver.cs" />
<Compile Include="Mocks\StubScanBridge.cs" />
<Compile Include="Scan\AutoSaveTests.cs" />
<Compile Include="Config\ProfileSerializerTestsData.Designer.cs">
<AutoGen>True</AutoGen>
@ -139,6 +140,7 @@
</Compile>
<Compile Include="ContextualTexts.cs" />
<Compile Include="Scan\RemoteScanControllerTests.cs" />
<Compile Include="Scan\ScanErrorHandling.cs" />
<Compile Include="Serialization\XmlSerializerTests.cs" />
<Compile Include="Util\NaturalStringComparerTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

View File

@ -178,10 +178,5 @@ namespace NAPS2.Sdk.Tests.Scan
new BitmapRenderer(ImageContext),
new StubConfigProvider<CommonConfig>(InternalDefaults.GetCommonConfig()));
}
private ScannedImage CreateScannedImage()
{
return ImageContext.CreateScannedImage(new GdiImage(new Bitmap(100, 100)));
}
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using Moq;
using NAPS2.Images;
using NAPS2.Scan.Experimental;
using NAPS2.Scan.Experimental.Internal;
using NAPS2.Sdk.Tests.Mocks;
using Xunit;
namespace NAPS2.Sdk.Tests.Scan
{
public class ScanErrorHandling : ContextualTexts
{
[Fact]
public void InvalidOptions()
{
var localPostProcessor = new Mock<ILocalPostProcessor>();
var bridgeFactory = new Mock<IScanBridgeFactory>();
var controller = new ScanController(localPostProcessor.Object, new ScanOptionsValidator(), bridgeFactory.Object);
var invalidOptions = new ScanOptions { Dpi = -1 };
Assert.Throws<ArgumentException>(() => controller.Scan(invalidOptions));
}
[Fact]
public async void CreateScanBridge()
{
var localPostProcessor = new Mock<ILocalPostProcessor>();
var bridgeFactory = new Mock<IScanBridgeFactory>();
var controller = new ScanController(localPostProcessor.Object, new ScanOptionsValidator(), bridgeFactory.Object);
bridgeFactory.Setup(factory => factory.Create(It.IsAny<ScanOptions>())).Throws<InvalidOperationException>();
var source = controller.Scan(new ScanOptions());
await Assert.ThrowsAsync<InvalidOperationException>(source.ToList);
}
[Fact]
public async void LocalPostProcess()
{
var localPostProcessor = new Mock<ILocalPostProcessor>();
var bridge = new StubScanBridge { MockOutput = new List<ScannedImage> { CreateScannedImage() } };
var bridgeFactory = new Mock<IScanBridgeFactory>();
var controller = new ScanController(localPostProcessor.Object, new ScanOptionsValidator(), bridgeFactory.Object);
bridgeFactory.Setup(factory => factory.Create(It.IsAny<ScanOptions>())).Returns(bridge);
localPostProcessor.Setup(pp => pp.PostProcess(It.IsAny<ScannedImage>(), It.IsAny<ScanOptions>(), It.IsAny<PostProcessingContext>()))
.Throws<InvalidOperationException>();
var source = controller.Scan(new ScanOptions());
await Assert.ThrowsAsync<InvalidOperationException>(source.ToList);
}
}
}

View File

@ -36,7 +36,7 @@ namespace NAPS2.Images
{
if (Completed)
{
throw new InvalidOperationException("Sink is already in the completed state");
return;
}
Completed = true;
images.Last().SetResult(null);

View File

@ -298,7 +298,7 @@
<Compile Include="Scan\Experimental\ScanStartEventArgs.cs" />
<Compile Include="Scan\Experimental\TwainOptions.cs" />
<Compile Include="Scan\Experimental\WiaOptions.cs" />
<Compile Include="Scan\Experimental\Internal\WorkerProcessScanBridge.cs" />
<Compile Include="Scan\Experimental\Internal\WorkerScanBridge.cs" />
<Compile Include="Scan\KeyValueScanOptions.cs" />
<Compile Include="Scan\Exceptions\SaneNotAvailableException.cs" />
<Compile Include="Scan\Sane\SaneOption.cs" />

View File

@ -49,6 +49,8 @@ namespace NAPS2.Scan.Experimental.Internal
}
// TODO: Validate DoOcr based on OcrParams
// TODO: Do we need to validate the presence of a device?
// TODO: Probably more things as well.
return options;
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NAPS2.Images;
using NAPS2.Scan.Experimental.Internal;
@ -39,7 +40,6 @@ namespace NAPS2.Scan.Experimental
public ScannedImageSource Scan(ScanOptions options, CancellationToken cancelToken = default)
{
options = scanOptionsValidator.ValidateAll(options);
var bridge = scanBridgeFactory.Create(options);
var sink = new ScannedImageSink();
int pageNumber = 0;
@ -51,25 +51,27 @@ namespace NAPS2.Scan.Experimental
void PageEndCallback(ScannedImage image) => PageEnd?.Invoke(this, new PageEndEventArgs(pageNumber, image));
ScanStartCallback();
bridge.Scan(options, cancelToken, new ScanEvents(PageStartCallback, PageProgressCallback), (scannedImage, postProcessingContext) =>
Task.Run(async () =>
{
localPostProcessor.PostProcess(scannedImage, options, postProcessingContext);
sink.PutImage(scannedImage);
PageEndCallback(scannedImage);
}).ContinueWith(t =>
{
if (t.IsFaulted)
{
sink.SetError(t.Exception);
ScanErrorCallback(t.Exception);
}
else
try
{
var bridge = scanBridgeFactory.Create(options);
await bridge.Scan(options, cancelToken, new ScanEvents(PageStartCallback, PageProgressCallback),
(scannedImage, postProcessingContext) =>
{
localPostProcessor.PostProcess(scannedImage, options, postProcessingContext);
sink.PutImage(scannedImage);
PageEndCallback(scannedImage);
});
sink.SetCompleted();
ScanEndCallback(sink.AsSource());
}
catch (Exception ex)
{
sink.SetError(ex);
ScanErrorCallback(ex);
}
ScanEndCallback(sink.AsSource());
});
return sink.AsSource();
}

View File

@ -6,7 +6,9 @@ using System.Threading.Tasks;
using NAPS2.Config;
using NAPS2.Images;
using NAPS2.ImportExport;
using NAPS2.Logging;
using NAPS2.Operation;
using NAPS2.Scan.Exceptions;
using NAPS2.Scan.Wia.Native;
using NAPS2.Util;
using NAPS2.WinForms;
@ -20,14 +22,17 @@ namespace NAPS2.Scan.Experimental
private readonly OperationProgress operationProgress;
private readonly AutoSaver autoSaver;
private readonly IProfileManager profileManager;
private readonly ErrorOutput errorOutput;
public ScanPerformer(IFormFactory formFactory, ConfigProvider<CommonConfig> configProvider, OperationProgress operationProgress, AutoSaver autoSaver, IProfileManager profileManager)
public ScanPerformer(IFormFactory formFactory, ConfigProvider<CommonConfig> configProvider, OperationProgress operationProgress, AutoSaver autoSaver,
IProfileManager profileManager, ErrorOutput errorOutput)
{
this.formFactory = formFactory;
this.configProvider = configProvider;
this.operationProgress = operationProgress;
this.autoSaver = autoSaver;
this.profileManager = profileManager;
this.errorOutput = errorOutput;
}
public ScannedImageSource PerformScan(ScanProfile scanProfile, ScanParams scanParams, IntPtr dialogParent = default,
@ -39,11 +44,13 @@ namespace NAPS2.Scan.Experimental
// User cancelled out of a dialog
return ScannedImageSource.Empty;
}
var controller = new ScanController();
var op = new ScanOperation(options.Device, options.PaperSource);
controller.PageStart += (sender, args) => op.NextPage(args.PageNumber);
controller.ScanEnd += (sender, args) => op.Completed();
controller.ScanError += (sender, args) => HandleError(args.Exception);
TranslateProgress(controller, op);
ShowOperation(op, scanParams);
@ -55,7 +62,29 @@ namespace NAPS2.Scan.Experimental
{
source = autoSaver.Save(scanProfile.AutoSaveSettings, source);
}
return source;
// Errors are handled by the ScanError callback
return SwallowErrors(source);
}
private void HandleError(Exception error)
{
if (error is ScanDriverUnknownException)
{
Log.ErrorException(error.Message, error.InnerException);
errorOutput?.DisplayError(error.Message, error);
}
else
{
errorOutput?.DisplayError(error.Message);
}
}
private ScannedImageSource SwallowErrors(ScannedImageSource source)
{
var sink = new ScannedImageSink();
source.ForEach(img => sink.PutImage(img)).ContinueWith(t => sink.SetCompleted());
return sink.AsSource();
}
private void ShowOperation(ScanOperation op, ScanParams scanParams)
@ -90,9 +119,9 @@ namespace NAPS2.Scan.Experimental
var options = new ScanOptions
{
Driver = scanProfile.DriverName == "wia" ? Driver.Wia
: scanProfile.DriverName == "sane" ? Driver.Sane
: scanProfile.DriverName == "twain" ? Driver.Twain
: Driver.Default,
: scanProfile.DriverName == "sane" ? Driver.Sane
: scanProfile.DriverName == "twain" ? Driver.Twain
: Driver.Default,
WiaOptions =
{
WiaVersion = scanProfile.WiaVersion,
@ -146,12 +175,13 @@ namespace NAPS2.Scan.Experimental
Device = null, // Set after
PageSize = null, // Set after
};
PageDimensions pageDimensions = scanProfile.PageSize.PageDimensions() ?? scanProfile.CustomPageSize;
if (pageDimensions == null)
{
throw new ArgumentException("No page size specified");
}
options.PageSize = new PageSize(pageDimensions.Width, pageDimensions.Height, pageDimensions.Unit);
// If a device wasn't specified, prompt the user to pick one
@ -167,6 +197,7 @@ namespace NAPS2.Scan.Experimental
{
return null;
}
options.Device = new ScanDevice(wiaDevice.Id(), wiaDevice.Name());
}
}
@ -180,6 +211,7 @@ namespace NAPS2.Scan.Experimental
{
return null;
}
options.Device = deviceForm.SelectedDevice;
}