From 6e30ec6cc930f477523a4c9f1c7e59a369b44c17 Mon Sep 17 00:00:00 2001 From: Ben Olden-Cooligan Date: Thu, 29 Nov 2018 01:54:18 -0500 Subject: [PATCH] Storage wip (not compiling yet) --- NAPS2.Lib.Common/Modules/ConsoleModule.cs | 1 - NAPS2.Lib.Common/StaticConfiguration.cs | 7 + .../ImportExport/DirectImportOperation.cs | 15 +- .../ImportExport/Images/ImageImporter.cs | 30 +- .../Images/SaveImagesOperation.cs | 6 +- NAPS2.Sdk/ImportExport/Images/TiffHelper.cs | 8 +- .../ImportExport/Pdf/PdfSharpImporter.cs | 77 ++-- .../ImportExport/PrintDocumentPrinter.cs | 5 +- NAPS2.Sdk/NAPS2.Sdk.csproj | 12 +- NAPS2.Sdk/Scan/Images/IBlankDetector.cs | 6 +- NAPS2.Sdk/Scan/Images/ImageScaleHelper.cs | 31 -- .../Scan/Images/NullThumbnailRenderer.cs | 19 - NAPS2.Sdk/Scan/Images/ScannedImage.cs | 129 +++--- NAPS2.Sdk/Scan/Images/ScannedImageHelper.cs | 50 +- NAPS2.Sdk/Scan/Images/ScannedImageList.cs | 3 +- NAPS2.Sdk/Scan/Images/ScannedImageRenderer.cs | 31 +- NAPS2.Sdk/Scan/Images/Storage/FileStorage.cs | 8 +- .../Scan/Images/Storage/FileStorageManager.cs | 17 +- .../Scan/Images/Storage/GdiFileConverter.cs | 24 +- NAPS2.Sdk/Scan/Images/Storage/GdiStorage.cs | 21 +- .../Scan/Images/Storage/GdiStorageFactory.cs | 32 +- .../Scan/Images/Storage/GdiTransformer.cs | 435 ++++++++++++++++++ NAPS2.Sdk/Scan/Images/Storage/IFileStorage.cs | 11 + .../Scan/Images/Storage/IImageMetadata.cs | 26 ++ .../Images/Storage/IImageMetadataFactory.cs | 11 + .../Scan/Images/Storage/IMemoryStorage.cs | 12 + .../Images/Storage/IMemoryStorageFactory.cs | 36 +- .../Scan/Images/Storage/IStorageConverter.cs | 2 +- NAPS2.Sdk/Scan/Images/Storage/ITransformer.cs | 12 + .../Scan/Images/Storage/PdfFileStorage.cs | 2 +- .../Storage/RecoverableImageMetadata.cs | 73 +++ .../Images/Storage/RecoveryStorageManager.cs | 67 ++- .../Images/Storage/StorageConvertParams.cs | 6 +- .../Scan/Images/Storage/StorageManager.cs | 73 ++- .../Scan/Images/Storage/StubImageMetadata.cs | 32 ++ .../Storage/StubImageMetadataFactory.cs | 11 + .../Scan/Images/ThresholdBlankDetector.cs | 33 +- NAPS2.Sdk/Scan/Images/ThumbnailRenderer.cs | 95 +--- .../Images/Transforms/BlackWhiteTransform.cs | 16 - .../Images/Transforms/BrightnessTransform.cs | 8 - .../Images/Transforms/ContrastTransform.cs | 28 -- .../Scan/Images/Transforms/CropTransform.cs | 22 - .../Scan/Images/Transforms/HueTransform.cs | 21 - .../Images/Transforms/RotationTransform.cs | 48 +- .../Images/Transforms/SaturationTransform.cs | 49 -- .../Scan/Images/Transforms/ScaleTransform.cs | 14 + .../Images/Transforms/SharpenTransform.cs | 111 ----- .../Images/Transforms/ThumbnailTransform.cs | 13 + NAPS2.Sdk/Scan/Images/Transforms/Transform.cs | 51 +- .../Transforms/TrueContrastTransform.cs | 14 - NAPS2.Sdk/Scan/PatchCodeDetector.cs | 10 +- NAPS2.Sdk/Scan/Sane/SaneScanDriver.cs | 5 +- NAPS2.Sdk/Scan/Stub/StubScanDriver.cs | 3 +- NAPS2.Sdk/Scan/Twain/Legacy/TwainApi.cs | 3 +- NAPS2.Sdk/Scan/Twain/TwainWrapper.cs | 21 +- NAPS2.Sdk/Scan/Wia/WiaScanOperation.cs | 17 +- NAPS2.Sdk/WinForms/FDesktop.cs | 7 +- NAPS2.Sdk/WinForms/FViewer.cs | 3 +- NAPS2.Sdk/WinForms/ImageForm.cs | 6 +- NAPS2.Sdk/WinForms/ThumbnailList.cs | 3 +- NAPS2.Sdk/Worker/WorkerCallback.cs | 3 +- 61 files changed, 1175 insertions(+), 770 deletions(-) delete mode 100644 NAPS2.Sdk/Scan/Images/ImageScaleHelper.cs delete mode 100644 NAPS2.Sdk/Scan/Images/NullThumbnailRenderer.cs create mode 100644 NAPS2.Sdk/Scan/Images/Storage/GdiTransformer.cs create mode 100644 NAPS2.Sdk/Scan/Images/Storage/IFileStorage.cs create mode 100644 NAPS2.Sdk/Scan/Images/Storage/IImageMetadata.cs create mode 100644 NAPS2.Sdk/Scan/Images/Storage/IImageMetadataFactory.cs create mode 100644 NAPS2.Sdk/Scan/Images/Storage/ITransformer.cs create mode 100644 NAPS2.Sdk/Scan/Images/Storage/RecoverableImageMetadata.cs create mode 100644 NAPS2.Sdk/Scan/Images/Storage/StubImageMetadata.cs create mode 100644 NAPS2.Sdk/Scan/Images/Storage/StubImageMetadataFactory.cs create mode 100644 NAPS2.Sdk/Scan/Images/Transforms/ScaleTransform.cs create mode 100644 NAPS2.Sdk/Scan/Images/Transforms/ThumbnailTransform.cs diff --git a/NAPS2.Lib.Common/Modules/ConsoleModule.cs b/NAPS2.Lib.Common/Modules/ConsoleModule.cs index 0881f4131..3cfc9787b 100644 --- a/NAPS2.Lib.Common/Modules/ConsoleModule.cs +++ b/NAPS2.Lib.Common/Modules/ConsoleModule.cs @@ -20,7 +20,6 @@ namespace NAPS2.DI.Modules Bind().To(); Bind().To(); Bind().To(); - Bind().To(); } } } diff --git a/NAPS2.Lib.Common/StaticConfiguration.cs b/NAPS2.Lib.Common/StaticConfiguration.cs index ab9961b1f..7101ce397 100644 --- a/NAPS2.Lib.Common/StaticConfiguration.cs +++ b/NAPS2.Lib.Common/StaticConfiguration.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using NAPS2.Config; using NAPS2.ImportExport.Pdf; using NAPS2.Logging; using NAPS2.Ocr; using NAPS2.Platform; +using NAPS2.Scan.Images.Storage; using NLog; namespace NAPS2.DI @@ -34,6 +36,11 @@ namespace NAPS2.DI GhostscriptManager.BasePath = basePath; OcrManager.Default = new OcrManager(basePath); + + var recoveryFolderPath = Path.Combine(Paths.Recovery, Path.GetRandomFileName()); + var rsm = new RecoveryStorageManager(recoveryFolderPath); + FileStorageManager.Default = rsm; + StorageManager.ImageMetadataFactory = rsm; } } } diff --git a/NAPS2.Sdk/ImportExport/DirectImportOperation.cs b/NAPS2.Sdk/ImportExport/DirectImportOperation.cs index f18b6f122..e5c342691 100644 --- a/NAPS2.Sdk/ImportExport/DirectImportOperation.cs +++ b/NAPS2.Sdk/ImportExport/DirectImportOperation.cs @@ -7,17 +7,18 @@ using NAPS2.Lang.Resources; using NAPS2.Logging; using NAPS2.Operation; using NAPS2.Scan.Images; -using NAPS2.Util; +using NAPS2.Scan.Images.Storage; +using NAPS2.Scan.Images.Transforms; namespace NAPS2.ImportExport { public class DirectImportOperation : OperationBase { - private readonly ThumbnailRenderer thumbnailRenderer; + private readonly ScannedImageRenderer scannedImageRenderer; - public DirectImportOperation(ThumbnailRenderer thumbnailRenderer) + public DirectImportOperation(ScannedImageRenderer scannedImageRenderer) { - this.thumbnailRenderer = thumbnailRenderer; + this.scannedImageRenderer = scannedImageRenderer; AllowCancel = true; AllowBackground = true; @@ -40,16 +41,16 @@ namespace NAPS2.ImportExport try { ScannedImage img; - using (var bitmap = new Bitmap(Path.Combine(data.RecoveryFolder, ir.FileName))) + using (var storage = StorageManager.ConvertToMemory(new FileStorage(Path.Combine(data.RecoveryFolder, ir.FileName)), new StorageConvertParams())) { - img = new ScannedImage(bitmap, ir.BitDepth, ir.HighQuality, -1); + img = new ScannedImage(storage, ir.BitDepth, ir.HighQuality, -1); } foreach (var transform in ir.TransformList) { img.AddTransform(transform); } // TODO: Don't bother, here, in recovery, etc. - img.SetThumbnail(await thumbnailRenderer.RenderThumbnail(img)); + img.SetThumbnail(StorageManager.PerformTransform(await scannedImageRenderer.Render(img), new ThumbnailTransform())); imageCallback(img); Status.CurrentProgress++; diff --git a/NAPS2.Sdk/ImportExport/Images/ImageImporter.cs b/NAPS2.Sdk/ImportExport/Images/ImageImporter.cs index 0e6335758..c1c318a52 100644 --- a/NAPS2.Sdk/ImportExport/Images/ImageImporter.cs +++ b/NAPS2.Sdk/ImportExport/Images/ImageImporter.cs @@ -8,6 +8,8 @@ using System.Threading.Tasks; using NAPS2.Logging; using NAPS2.Scan; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; +using NAPS2.Scan.Images.Transforms; using NAPS2.Util; namespace NAPS2.ImportExport.Images @@ -34,10 +36,11 @@ namespace NAPS2.ImportExport.Images return; } - Bitmap toImport; + IEnumerable toImport; + int frameCount; try { - toImport = new Bitmap(filePath); + toImport = StorageManager.MemoryStorageFactory.DecodeMultiple(filePath, out frameCount); } catch (Exception e) { @@ -46,28 +49,27 @@ namespace NAPS2.ImportExport.Images throw; } - using (toImport) + foreach (var frame in toImport) { - int frameCount = toImport.GetFrameCount(FrameDimension.Page); - int i = 0; - foreach (var frameIndex in importParams.Slice.Indices(frameCount)) + using (frame) { + int i = 0; progressCallback(i++, frameCount); if (cancelToken.IsCancellationRequested) { source.Done(); return; } - - toImport.SelectActiveFrame(FrameDimension.Page, frameIndex); - var image = new ScannedImage(toImport, ScanBitDepth.C24Bit, IsLossless(toImport.RawFormat), -1); + + var image = new ScannedImage(frame, ScanBitDepth.C24Bit, frame.IsOriginalLossless, -1); if (!importParams.NoThumbnails) { - image.SetThumbnail(thumbnailRenderer.RenderThumbnail(toImport)); + image.SetThumbnail(StorageManager.PerformTransform(frame, new ThumbnailTransform())); } + if (importParams.DetectPatchCodes) { - image.PatchCode = PatchCodeDetector.Detect(toImport); + image.PatchCode = PatchCodeDetector.Detect(frame); } source.Put(image); @@ -75,6 +77,7 @@ namespace NAPS2.ImportExport.Images progressCallback(frameCount, frameCount); } + source.Done(); } catch(Exception e) @@ -84,10 +87,5 @@ namespace NAPS2.ImportExport.Images }, TaskCreationOptions.LongRunning); return source; } - - private bool IsLossless(ImageFormat format) - { - return Equals(format, ImageFormat.Bmp) || Equals(format, ImageFormat.Png); - } } } diff --git a/NAPS2.Sdk/ImportExport/Images/SaveImagesOperation.cs b/NAPS2.Sdk/ImportExport/Images/SaveImagesOperation.cs index 02de80cbf..27e9ab3c4 100644 --- a/NAPS2.Sdk/ImportExport/Images/SaveImagesOperation.cs +++ b/NAPS2.Sdk/ImportExport/Images/SaveImagesOperation.cs @@ -12,6 +12,7 @@ using NAPS2.Lang.Resources; using NAPS2.Logging; using NAPS2.Operation; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; using NAPS2.Util; namespace NAPS2.ImportExport.Images @@ -173,14 +174,15 @@ namespace NAPS2.ImportExport.Images var encoder = ImageCodecInfo.GetImageEncoders().First(x => x.FormatID == ImageFormat.Jpeg.Guid); var encoderParams = new EncoderParameters(1); encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, quality); - using (Bitmap bitmap = await scannedImageRenderer.Render(snapshot)) + // TODO: Something more generic + using (Bitmap bitmap = ((GdiStorage) await scannedImageRenderer.Render(snapshot)).Bitmap) { bitmap.Save(path, encoder, encoderParams); } } else { - using (Bitmap bitmap = await scannedImageRenderer.Render(snapshot)) + using (Bitmap bitmap = ((GdiStorage)await scannedImageRenderer.Render(snapshot)).Bitmap) { bitmap.Save(path, format); } diff --git a/NAPS2.Sdk/ImportExport/Images/TiffHelper.cs b/NAPS2.Sdk/ImportExport/Images/TiffHelper.cs index c9c80bbfe..0e34722b1 100644 --- a/NAPS2.Sdk/ImportExport/Images/TiffHelper.cs +++ b/NAPS2.Sdk/ImportExport/Images/TiffHelper.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; using NAPS2.Util; namespace NAPS2.ImportExport.Images @@ -39,7 +40,8 @@ namespace NAPS2.ImportExport.Images { var iparams = new EncoderParameters(1); Encoder iparam = Encoder.Compression; - using (var bitmap = await scannedImageRenderer.Render(snapshots[0])) + // TODO: More generic (?) + using (var bitmap = ((GdiStorage)await scannedImageRenderer.Render(snapshots[0])).Bitmap) { ValidateBitmap(bitmap); var iparamPara = new EncoderParameter(iparam, (long)GetEncoderValue(compression, bitmap)); @@ -54,7 +56,7 @@ namespace NAPS2.ImportExport.Images var compressionEncoder = Encoder.Compression; File.Delete(location); - using (var bitmap0 = await scannedImageRenderer.Render(snapshots[0])) + using (var bitmap0 = ((GdiStorage)await scannedImageRenderer.Render(snapshots[0])).Bitmap) { ValidateBitmap(bitmap0); encoderParams.Param[0] = new EncoderParameter(compressionEncoder, (long)GetEncoderValue(compression, bitmap0)); @@ -74,7 +76,7 @@ namespace NAPS2.ImportExport.Images return false; } - using (var bitmap = await scannedImageRenderer.Render(snapshots[i])) + using (var bitmap = ((GdiStorage)await scannedImageRenderer.Render(snapshots[i])).Bitmap) { ValidateBitmap(bitmap); encoderParams.Param[0] = new EncoderParameter(compressionEncoder, (long)GetEncoderValue(compression, bitmap)); diff --git a/NAPS2.Sdk/ImportExport/Pdf/PdfSharpImporter.cs b/NAPS2.Sdk/ImportExport/Pdf/PdfSharpImporter.cs index d7acddb96..e5ec9c3f6 100644 --- a/NAPS2.Sdk/ImportExport/Pdf/PdfSharpImporter.cs +++ b/NAPS2.Sdk/ImportExport/Pdf/PdfSharpImporter.cs @@ -13,6 +13,8 @@ using NAPS2.Lang.Resources; using NAPS2.Logging; using NAPS2.Scan; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; +using NAPS2.Scan.Images.Transforms; using NAPS2.Util; using PdfSharp.Pdf; using PdfSharp.Pdf.Advanced; @@ -179,19 +181,22 @@ namespace NAPS2.ImportExport.Pdf private async Task ExportRawPdfPage(PdfPage page, ImportParams importParams) { - string pdfPath = Path.Combine(Paths.Temp, Path.GetRandomFileName()); + string pdfPath = FileStorageManager.Default.NextFilePath(); var document = new PdfDocument(); document.Pages.Add(page); document.Save(pdfPath); - var image = ScannedImage.FromSinglePagePdf(pdfPath, false); + // TODO: It would make sense to have in-memory PDFs be an option. + // TODO: Really, ConvertToBacking should convert PdfStorage -> PdfFileStorage. + // TODO: Then we wouldn't need a static FileStorageManager. + var image = new ScannedImage(new PdfFileStorage(pdfPath)); if (!importParams.NoThumbnails || importParams.DetectPatchCodes) { using (var bitmap = await scannedImageRenderer.Render(image)) { if (!importParams.NoThumbnails) { - image.SetThumbnail(thumbnailRenderer.RenderThumbnail(bitmap)); + image.SetThumbnail(StorageManager.PerformTransform(bitmap, new ThumbnailTransform())); } if (importParams.DetectPatchCodes) { @@ -207,17 +212,17 @@ namespace NAPS2.ImportExport.Pdf // Fortunately JPEG has native support in PDF and exporting an image is just writing the stream to a file. using (var memoryStream = new MemoryStream(imageBytes)) { - using (var bitmap = new Bitmap(memoryStream)) + using (var storage = StorageManager.MemoryStorageFactory.Decode(memoryStream, ".jpg")) { - bitmap.SafeSetResolution(bitmap.Width / (float)page.Width.Inch, bitmap.Height / (float)page.Height.Inch); - var image = new ScannedImage(bitmap, ScanBitDepth.C24Bit, false, -1); + storage.SetResolution(storage.Width / (float)page.Width.Inch, storage.Height / (float)page.Height.Inch); + var image = new ScannedImage(storage, ScanBitDepth.C24Bit, false, -1); if (!importParams.NoThumbnails) { - image.SetThumbnail(thumbnailRenderer.RenderThumbnail(bitmap)); + image.SetThumbnail(StorageManager.PerformTransform(storage, new ThumbnailTransform())); } if (importParams.DetectPatchCodes) { - image.PatchCode = PatchCodeDetector.Detect(bitmap); + image.PatchCode = PatchCodeDetector.Detect(storage); } return image; } @@ -232,51 +237,51 @@ namespace NAPS2.ImportExport.Pdf var buffer = imageObject.Stream.UnfilteredValue; - Bitmap bitmap; + IMemoryStorage storage; ScanBitDepth bitDepth; switch (bitsPerComponent) { case 8: - bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb); + storage = StorageManager.MemoryStorageFactory.FromDimensions(width, height, StoragePixelFormat.RGB24); bitDepth = ScanBitDepth.C24Bit; - RgbToBitmapUnmanaged(height, width, bitmap, buffer); + RgbToBitmapUnmanaged(storage, buffer); break; case 1: - bitmap = new Bitmap(width, height, PixelFormat.Format1bppIndexed); + storage = StorageManager.MemoryStorageFactory.FromDimensions(width, height, StoragePixelFormat.BW1); bitDepth = ScanBitDepth.BlackWhite; - BlackAndWhiteToBitmapUnmanaged(height, width, bitmap, buffer); + BlackAndWhiteToBitmapUnmanaged(storage, buffer); break; default: throw new NotImplementedException("Unsupported image encoding (expected 24 bpp or 1bpp)"); } - using (bitmap) + using (storage) { - bitmap.SafeSetResolution(bitmap.Width / (float)page.Width.Inch, bitmap.Height / (float)page.Height.Inch); - var image = new ScannedImage(bitmap, bitDepth, true, -1); + storage.SetResolution(storage.Width / (float)page.Width.Inch, storage.Height / (float)page.Height.Inch); + var image = new ScannedImage(storage, bitDepth, true, -1); if (!importParams.NoThumbnails) { - image.SetThumbnail(thumbnailRenderer.RenderThumbnail(bitmap)); + image.SetThumbnail(StorageManager.PerformTransform(storage, new ThumbnailTransform())); } if (importParams.DetectPatchCodes) { - image.PatchCode = PatchCodeDetector.Detect(bitmap); + image.PatchCode = PatchCodeDetector.Detect(storage); } return image; } } - private static void RgbToBitmapUnmanaged(int height, int width, Bitmap bitmap, byte[] rgbBuffer) + private static void RgbToBitmapUnmanaged(IMemoryStorage storage, byte[] rgbBuffer) { - BitmapData data = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); + var data = storage.Lock(out var scan0, out var stride); try { - for (int y = 0; y < height; y++) + for (int y = 0; y < storage.Height; y++) { - for (int x = 0; x < width; x++) + for (int x = 0; x < storage.Width; x++) { - IntPtr pixelData = data.Scan0 + y * data.Stride + x * 3; - int bufferIndex = (y * width + x) * 3; + IntPtr pixelData = scan0 + y * stride + x * 3; + int bufferIndex = (y * storage.Width + x) * 3; Marshal.WriteByte(pixelData, rgbBuffer[bufferIndex + 2]); Marshal.WriteByte(pixelData + 1, rgbBuffer[bufferIndex + 1]); Marshal.WriteByte(pixelData + 2, rgbBuffer[bufferIndex]); @@ -285,28 +290,28 @@ namespace NAPS2.ImportExport.Pdf } finally { - bitmap.UnlockBits(data); + storage.Unlock(data); } } - private static void BlackAndWhiteToBitmapUnmanaged(int height, int width, Bitmap bitmap, byte[] bwBuffer) + private static void BlackAndWhiteToBitmapUnmanaged(IMemoryStorage storage, byte[] bwBuffer) { - BitmapData data = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed); + var data = storage.Lock(out var scan0, out var stride); try { - int bytesPerRow = (width - 1) / 8 + 1; - for (int y = 0; y < height; y++) + int bytesPerRow = (storage.Width - 1) / 8 + 1; + for (int y = 0; y < storage.Height; y++) { for (int x = 0; x < bytesPerRow; x++) { - IntPtr pixelData = data.Scan0 + y * data.Stride + x; + IntPtr pixelData = scan0 + y * stride + x; Marshal.WriteByte(pixelData, bwBuffer[y * bytesPerRow + x]); } } } finally { - bitmap.UnlockBits(data); + storage.Unlock(data); } } @@ -357,18 +362,18 @@ namespace NAPS2.ImportExport.Pdf Write(stream, TiffTrailer); stream.Seek(0, SeekOrigin.Begin); - using (Bitmap bitmap = (Bitmap)Image.FromStream(stream)) + using (var storage = StorageManager.MemoryStorageFactory.Decode(stream, ".tiff")) { - bitmap.SafeSetResolution(bitmap.Width / (float)page.Width.Inch, bitmap.Height / (float)page.Height.Inch); + storage.SetResolution(storage.Width / (float)page.Width.Inch, storage.Height / (float)page.Height.Inch); - var image = new ScannedImage(bitmap, ScanBitDepth.BlackWhite, true, -1); + var image = new ScannedImage(storage, ScanBitDepth.BlackWhite, true, -1); if (!importParams.NoThumbnails) { - image.SetThumbnail(thumbnailRenderer.RenderThumbnail(bitmap)); + image.SetThumbnail(StorageManager.PerformTransform(storage, new ThumbnailTransform())); } if (importParams.DetectPatchCodes) { - image.PatchCode = PatchCodeDetector.Detect(bitmap); + image.PatchCode = PatchCodeDetector.Detect(storage); } return image; } diff --git a/NAPS2.Sdk/ImportExport/PrintDocumentPrinter.cs b/NAPS2.Sdk/ImportExport/PrintDocumentPrinter.cs index 22a428967..65884b410 100644 --- a/NAPS2.Sdk/ImportExport/PrintDocumentPrinter.cs +++ b/NAPS2.Sdk/ImportExport/PrintDocumentPrinter.cs @@ -8,6 +8,7 @@ using System.Windows.Forms; using NAPS2.Lang.Resources; using NAPS2.Logging; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; using NAPS2.Scan.Images.Transforms; using NAPS2.Util; @@ -88,7 +89,7 @@ namespace NAPS2.ImportExport if (Math.Sign(image.Width - image.Height) != Math.Sign(pb.Width - pb.Height)) { // Flip portrait/landscape to match output - image = new RotationTransform(90).Perform(image); + image = StorageManager.PerformTransform(image, new RotationTransform(90)); } // Fit the image into the output rect while maintaining its aspect ratio @@ -96,7 +97,7 @@ namespace NAPS2.ImportExport ? new Rectangle(pb.Left, pb.Top, image.Width * pb.Height / image.Height, pb.Height) : new Rectangle(pb.Left, pb.Top, pb.Width, image.Height * pb.Width / image.Width); - e.Graphics.DrawImage(image, rect); + e.Graphics.DrawImage(StorageManager.Convert(image).Bitmap, rect); } finally { diff --git a/NAPS2.Sdk/NAPS2.Sdk.csproj b/NAPS2.Sdk/NAPS2.Sdk.csproj index ca1e3735f..5efd7ab25 100644 --- a/NAPS2.Sdk/NAPS2.Sdk.csproj +++ b/NAPS2.Sdk/NAPS2.Sdk.csproj @@ -191,6 +191,11 @@ + + + + + @@ -201,10 +206,15 @@ + + + + + @@ -345,7 +355,6 @@ - @@ -470,7 +479,6 @@ - diff --git a/NAPS2.Sdk/Scan/Images/IBlankDetector.cs b/NAPS2.Sdk/Scan/Images/IBlankDetector.cs index 1abc3a952..45b64c051 100644 --- a/NAPS2.Sdk/Scan/Images/IBlankDetector.cs +++ b/NAPS2.Sdk/Scan/Images/IBlankDetector.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.Linq; +using NAPS2.Scan.Images.Storage; namespace NAPS2.Scan.Images { public interface IBlankDetector { - bool IsBlank(Bitmap bitmap, int whiteThresholdNorm, int coverageThresholdNorm); + bool IsBlank(IMemoryStorage bitmap, int whiteThresholdNorm, int coverageThresholdNorm); - bool ExcludePage(Bitmap bitmap, ScanProfile scanProfile); + bool ExcludePage(IMemoryStorage bitmap, ScanProfile scanProfile); } } diff --git a/NAPS2.Sdk/Scan/Images/ImageScaleHelper.cs b/NAPS2.Sdk/Scan/Images/ImageScaleHelper.cs deleted file mode 100644 index 7a2d68f37..000000000 --- a/NAPS2.Sdk/Scan/Images/ImageScaleHelper.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.Linq; -using NAPS2.Util; - -namespace NAPS2.Scan.Images -{ - internal static class ImageScaleHelper - { - public static Bitmap ScaleImage(Image original, double scaleFactor) - { - double realWidth = original.Width / scaleFactor; - double realHeight = original.Height / scaleFactor; - - double horizontalRes = original.HorizontalResolution / scaleFactor; - double verticalRes = original.VerticalResolution / scaleFactor; - - var result = new Bitmap((int)realWidth, (int)realHeight, PixelFormat.Format24bppRgb); - using (Graphics g = Graphics.FromImage(result)) - { - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - g.DrawImage(original, 0, 0, (int)realWidth, (int)realHeight); - result.SafeSetResolution((float)horizontalRes, (float)verticalRes); - return result; - } - } - } -} \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Images/NullThumbnailRenderer.cs b/NAPS2.Sdk/Scan/Images/NullThumbnailRenderer.cs deleted file mode 100644 index ea84f43b7..000000000 --- a/NAPS2.Sdk/Scan/Images/NullThumbnailRenderer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Threading.Tasks; -using NAPS2.Config; - -namespace NAPS2.Scan.Images -{ - public class NullThumbnailRenderer : ThumbnailRenderer - { - public NullThumbnailRenderer(ScannedImageRenderer scannedImageRenderer) - : base(scannedImageRenderer) - { - } - - public override Bitmap RenderThumbnail(Bitmap b, int size) => null; - } -} diff --git a/NAPS2.Sdk/Scan/Images/ScannedImage.cs b/NAPS2.Sdk/Scan/Images/ScannedImage.cs index c2f16f647..aa503799e 100644 --- a/NAPS2.Sdk/Scan/Images/ScannedImage.cs +++ b/NAPS2.Sdk/Scan/Images/ScannedImage.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Runtime.Serialization; using System.Threading.Tasks; using NAPS2.Recovery; +using NAPS2.Scan.Images.Storage; using NAPS2.Scan.Images.Transforms; using NAPS2.Util; @@ -16,71 +17,86 @@ namespace NAPS2.Scan.Images { public class ScannedImage : IDisposable { - // Store the base image and metadata on disk using a separate class to manage lifetime - // If NAPS2 crashes, the image data can be recovered by the next instance of NAPS2 to start - private readonly RecoveryImage recoveryImage; - - // Store a base image and transform pair (rather than doing the actual transform on the base image) - // so that JPEG degradation is minimized when multiple rotations/flips are performed - private readonly List transformList; - - private Bitmap thumbnail; + private IMemoryStorage thumbnail; private int thumbnailState; private int transformState; private bool disposed; private int snapshotCount; - public static ScannedImage FromSinglePagePdf(string pdfPath, bool copy) + //public static ScannedImage FromSinglePagePdf(string pdfPath, bool copy) + //{ + // return new ScannedImage(pdfPath, copy); + //} + + //public ScannedImage(Bitmap img, ScanBitDepth bitDepth, bool highQuality, int quality) + //{ + // string tempFilePath = ScannedImageHelper.SaveSmallestBitmap(img, bitDepth, highQuality, quality, out ImageFormat fileFormat); + + // transformList = new List(); + // recoveryImage = RecoveryImage.CreateNew(fileFormat, bitDepth, highQuality, transformList); + + // File.Move(tempFilePath, recoveryImage.FilePath); + + // recoveryImage.Save(); + //} + + //public ScannedImage(RecoveryIndexImage recoveryIndexImage) + //{ + // recoveryImage = RecoveryImage.LoadExisting(recoveryIndexImage); + // transformList = recoveryImage.IndexImage.TransformList; + //} + + //private ScannedImage(string pdfPath, bool copy) + //{ + // transformList = new List(); + // recoveryImage = RecoveryImage.CreateNew(null, ScanBitDepth.C24Bit, false, transformList); + + // if (copy) + // { + // File.Copy(pdfPath, recoveryImage.FilePath); + // } + // else + // { + // File.Move(pdfPath, recoveryImage.FilePath); + // } + + // recoveryImage.Save(); + //} + + public ScannedImage(IStorage storage) : this(storage, new StorageConvertParams()) { - return new ScannedImage(pdfPath, copy); } - public ScannedImage(Bitmap img, ScanBitDepth bitDepth, bool highQuality, int quality) + public ScannedImage(IStorage storage, StorageConvertParams convertParams) { - string tempFilePath = ScannedImageHelper.SaveSmallestBitmap(img, bitDepth, highQuality, quality, out ImageFormat fileFormat); - - transformList = new List(); - recoveryImage = RecoveryImage.CreateNew(fileFormat, bitDepth, highQuality, transformList); - - File.Move(tempFilePath, recoveryImage.FilePath); - - recoveryImage.Save(); + BackingStorage = StorageManager.ConvertToBacking(storage, convertParams); + Metadata = StorageManager.ImageMetadataFactory.CreateMetadata(BackingStorage); + Metadata.Commit(); } - public ScannedImage(RecoveryIndexImage recoveryIndexImage) + public ScannedImage(IStorage storage, IImageMetadata metadata, StorageConvertParams convertParams) { - recoveryImage = RecoveryImage.LoadExisting(recoveryIndexImage); - transformList = recoveryImage.IndexImage.TransformList; + BackingStorage = StorageManager.ConvertToBacking(storage, convertParams); + Metadata = metadata; } - private ScannedImage(string pdfPath, bool copy) + public ScannedImage(IStorage storage, ScanBitDepth bitDepth, bool highQuality, int quality) { - transformList = new List(); - recoveryImage = RecoveryImage.CreateNew(null, ScanBitDepth.C24Bit, false, transformList); - - if (copy) - { - File.Copy(pdfPath, recoveryImage.FilePath); - } - else - { - File.Move(pdfPath, recoveryImage.FilePath); - } - - recoveryImage.Save(); + BackingStorage = StorageManager.ConvertToBacking(storage, new StorageConvertParams { Lossless = highQuality, LossyQuality = quality }); + Metadata = StorageManager.ImageMetadataFactory.CreateMetadata(BackingStorage); + // TODO: Is this stuff really needed in metadata? + Metadata.BitDepth = bitDepth; + Metadata.Lossless = highQuality; + Metadata.Commit(); } + public IStorage BackingStorage { get; } + + public IImageMetadata Metadata { get; } + public PatchCode PatchCode { get; set; } - public ImageFormat FileFormat => recoveryImage.FileFormat; - - public RecoveryIndexImage RecoveryIndexImage => recoveryImage.IndexImage; - - public string RecoveryFilePath => recoveryImage.FilePath; - - public long Size => new FileInfo(recoveryImage.FilePath).Length; - public void Dispose() { lock (this) @@ -90,7 +106,7 @@ namespace NAPS2.Scan.Images if (snapshotCount != 0) return; // Delete the image data on disk - recoveryImage?.Dispose(); + BackingStorage?.Dispose(); if (thumbnail != null) { thumbnail.Dispose(); @@ -106,13 +122,13 @@ namespace NAPS2.Scan.Images lock (this) { // Also updates the recovery index since they reference the same list - if (!Transform.AddOrSimplify(transformList, transform)) + if (!Transform.AddOrSimplify(Metadata.TransformList, transform)) { return; } transformState++; } - recoveryImage.Save(); + Metadata.Commit(); ThumbnailInvalidated?.Invoke(this, new EventArgs()); } @@ -120,26 +136,26 @@ namespace NAPS2.Scan.Images { lock (this) { - if (transformList.Count == 0) + if (Metadata.TransformList.Count == 0) { return; } - transformList.Clear(); + Metadata.TransformList.Clear(); transformState++; } - recoveryImage.Save(); + Metadata.Commit(); ThumbnailInvalidated?.Invoke(this, new EventArgs()); } - public Bitmap GetThumbnail() + public IMemoryStorage GetThumbnail() { lock (this) { - return (Bitmap) thumbnail?.Clone(); + return thumbnail?.Clone(); } } - public void SetThumbnail(Bitmap bitmap, int? state = null) + public void SetThumbnail(IMemoryStorage bitmap, int? state = null) { lock (this) { @@ -160,7 +176,8 @@ namespace NAPS2.Scan.Images public void MovedTo(int index) { - recoveryImage.Move(index); + Metadata.Index = index; + Metadata.Commit(); } public Snapshot Preserve() => new Snapshot(this); @@ -181,7 +198,7 @@ namespace NAPS2.Scan.Images } source.snapshotCount++; Source = source; - TransformList = source.transformList.ToList(); + TransformList = source.Metadata.TransformList.ToList(); TransformState = source.transformState; } } diff --git a/NAPS2.Sdk/Scan/Images/ScannedImageHelper.cs b/NAPS2.Sdk/Scan/Images/ScannedImageHelper.cs index ce6392c1c..fd8cfbad8 100644 --- a/NAPS2.Sdk/Scan/Images/ScannedImageHelper.cs +++ b/NAPS2.Sdk/Scan/Images/ScannedImageHelper.cs @@ -7,6 +7,7 @@ using System.Linq; using NAPS2.Config; using NAPS2.Ocr; using NAPS2.Operation; +using NAPS2.Scan.Images.Storage; using NAPS2.Scan.Images.Transforms; using NAPS2.Util; @@ -101,29 +102,28 @@ namespace NAPS2.Scan.Images return tempFilePath; } - private readonly ThumbnailRenderer thumbnailRenderer; private readonly IOperationFactory operationFactory; private readonly IOperationProgress operationProgress; private readonly OcrRequestQueue ocrRequestQueue; private readonly OcrManager ocrManager; - public ScannedImageHelper(ThumbnailRenderer thumbnailRenderer, IOperationFactory operationFactory, IOperationProgress operationProgress, OcrRequestQueue ocrRequestQueue, OcrManager ocrManager) + public ScannedImageHelper(IOperationFactory operationFactory, IOperationProgress operationProgress, OcrRequestQueue ocrRequestQueue, OcrManager ocrManager) { - this.thumbnailRenderer = thumbnailRenderer; this.operationFactory = operationFactory; this.operationProgress = operationProgress; this.ocrRequestQueue = ocrRequestQueue; this.ocrManager = ocrManager; } - public Bitmap PostProcessStep1(Image output, ScanProfile profile, bool supportsNativeUI = true) + public IMemoryStorage PostProcessStep1(IMemoryStorage output, ScanProfile profile, bool supportsNativeUI = true) { double scaleFactor = 1; if (!profile.UseNativeUI || !supportsNativeUI) { scaleFactor = profile.AfterScanScale.ToIntScaleFactor(); } - var result = ImageScaleHelper.ScaleImage(output, scaleFactor); + // TODO: Scale + var result = output;//ImageScaleHelper.ScaleImage(output, scaleFactor); if ((!profile.UseNativeUI || !supportsNativeUI) && (profile.ForcePageSize || profile.ForcePageSizeCrop)) { @@ -139,31 +139,31 @@ namespace NAPS2.Scan.Images { if (profile.ForcePageSizeCrop) { - result = new CropTransform + result = StorageManager.PerformTransform(result, new CropTransform { - Right = (int) ((width - (float) pageDimensions.HeightInInches()) * output.HorizontalResolution), - Bottom = (int) ((height - (float) pageDimensions.WidthInInches()) * output.VerticalResolution) - }.Perform(result); + Right = (int)((width - (float)pageDimensions.HeightInInches()) * output.HorizontalResolution), + Bottom = (int)((height - (float)pageDimensions.WidthInInches()) * output.VerticalResolution) + }); } else { - result.SafeSetResolution((float) (output.Width / pageDimensions.HeightInInches()), - (float) (output.Height / pageDimensions.WidthInInches())); + result.SetResolution((float)(output.Width / pageDimensions.HeightInInches()), + (float)(output.Height / pageDimensions.WidthInInches())); } } else { if (profile.ForcePageSizeCrop) { - result = new CropTransform + result = StorageManager.PerformTransform(result, new CropTransform { - Right = (int) ((width - (float) pageDimensions.WidthInInches()) * output.HorizontalResolution), - Bottom = (int) ((height - (float) pageDimensions.HeightInInches()) * output.VerticalResolution) - }.Perform(result); + Right = (int)((width - (float)pageDimensions.WidthInInches()) * output.HorizontalResolution), + Bottom = (int)((height - (float)pageDimensions.HeightInInches()) * output.VerticalResolution) + }); } else { - result.SafeSetResolution((float)(output.Width / pageDimensions.WidthInInches()), (float)(output.Height / pageDimensions.HeightInInches())); + result.SetResolution((float)(output.Width / pageDimensions.WidthInInches()), (float)(output.Height / pageDimensions.HeightInInches())); } } } @@ -171,11 +171,11 @@ namespace NAPS2.Scan.Images return result; } - public void PostProcessStep2(ScannedImage image, Bitmap bitmap, ScanProfile profile, ScanParams scanParams, int pageNumber, bool supportsNativeUI = true) + public void PostProcessStep2(ScannedImage image, IMemoryStorage bitmap, ScanProfile profile, ScanParams scanParams, int pageNumber, bool supportsNativeUI = true) { if (!scanParams.NoThumbnails) { - image.SetThumbnail(thumbnailRenderer.RenderThumbnail(bitmap)); + image.SetThumbnail(StorageManager.PerformTransform(bitmap, new ThumbnailTransform())); } if (scanParams.SkipPostProcessing) { @@ -220,13 +220,13 @@ namespace NAPS2.Scan.Images return scanParams.DoOcr ?? (ocrEnabled && afterScanning); } - public string SaveForBackgroundOcr(Bitmap bitmap, ScanParams scanParams) + public string SaveForBackgroundOcr(IMemoryStorage bitmap, ScanParams scanParams) { if (ShouldDoBackgroundOcr(scanParams)) { - string tempPath = Path.Combine(Paths.Temp, Path.GetRandomFileName()); - bitmap.Save(tempPath); - return tempPath; + var fileStorage = StorageManager.Convert(bitmap, new StorageConvertParams { Temporary = true }); + // TODO: Maybe return the storage rather than the path + return fileStorage.FullPath; } return null; } @@ -249,14 +249,14 @@ namespace NAPS2.Scan.Images } } - private void AddTransformAndUpdateThumbnail(ScannedImage image, ref Bitmap bitmap, Transform transform) + private void AddTransformAndUpdateThumbnail(ScannedImage image, ref IMemoryStorage bitmap, Transform transform) { image.AddTransform(transform); var thumbnail = image.GetThumbnail(); if (thumbnail != null) { - bitmap = transform.Perform(bitmap); - image.SetThumbnail(thumbnailRenderer.RenderThumbnail(bitmap)); + bitmap = StorageManager.PerformTransform(bitmap, transform); + image.SetThumbnail(StorageManager.PerformTransform(bitmap, new ThumbnailTransform())); } } } diff --git a/NAPS2.Sdk/Scan/Images/ScannedImageList.cs b/NAPS2.Sdk/Scan/Images/ScannedImageList.cs index da5707099..f420c3d30 100644 --- a/NAPS2.Sdk/Scan/Images/ScannedImageList.cs +++ b/NAPS2.Sdk/Scan/Images/ScannedImageList.cs @@ -4,6 +4,7 @@ using System.Drawing; using System.Linq; using System.Threading.Tasks; using NAPS2.Recovery; +using NAPS2.Scan.Images.Storage; using NAPS2.Scan.Images.Transforms; using NAPS2.Util; @@ -279,7 +280,7 @@ namespace NAPS2.Scan.Images var thumb = img.GetThumbnail(); if (thumb != null) { - img.SetThumbnail(transform.Perform(thumb)); + img.SetThumbnail(StorageManager.PerformTransform(thumb, transform)); } } } diff --git a/NAPS2.Sdk/Scan/Images/ScannedImageRenderer.cs b/NAPS2.Sdk/Scan/Images/ScannedImageRenderer.cs index c270568f6..7d734ef3e 100644 --- a/NAPS2.Sdk/Scan/Images/ScannedImageRenderer.cs +++ b/NAPS2.Sdk/Scan/Images/ScannedImageRenderer.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using NAPS2.ImportExport.Pdf; +using NAPS2.Scan.Images.Storage; using NAPS2.Scan.Images.Transforms; namespace NAPS2.Scan.Images @@ -20,7 +21,7 @@ namespace NAPS2.Scan.Images this.pdfRenderer = pdfRenderer; } - public async Task Render(ScannedImage image, int outputSize = 0) + public async Task Render(ScannedImage image, int outputSize = 0) { using (var snapshot = image.Preserve()) { @@ -28,38 +29,20 @@ namespace NAPS2.Scan.Images } } - public async Task Render(ScannedImage.Snapshot snapshot, int outputSize = 0) + public async Task Render(ScannedImage.Snapshot snapshot, int outputSize = 0) { return await Task.Factory.StartNew(() => { - var bitmap = snapshot.Source.FileFormat == null - ? pdfRenderer.Render(snapshot.Source.RecoveryFilePath).Single() - : new Bitmap(snapshot.Source.RecoveryFilePath); + var storage = StorageManager.ConvertToMemory(snapshot.Source.BackingStorage, new StorageConvertParams()); if (outputSize > 0) { - bitmap = ShrinkBitmap(bitmap, outputSize); + double scaleFactor = Math.Min(outputSize / (double)storage.Height, outputSize / (double)storage.Width); + storage = StorageManager.PerformTransform(storage, new ScaleTransform { ScaleFactor = scaleFactor }); } - return Transform.PerformAll(bitmap, snapshot.TransformList); + return StorageManager.PerformAllTransforms(storage, snapshot.TransformList); }); } - private Bitmap ShrinkBitmap(Bitmap bitmap, int outputSize) - { - double scaleFactor = Math.Min(outputSize / (double)bitmap.Height, outputSize / (double)bitmap.Width); - if (scaleFactor >= 1) - { - return bitmap; - } - var bitmap2 = new Bitmap((int)Math.Round(bitmap.Width * scaleFactor), (int)Math.Round(bitmap.Height * scaleFactor)); - using (var g = Graphics.FromImage(bitmap2)) - { - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - g.DrawImage(bitmap, new Rectangle(Point.Empty, bitmap2.Size), new Rectangle(Point.Empty, bitmap.Size), GraphicsUnit.Pixel); - } - bitmap.Dispose(); - return bitmap2; - } - public async Task RenderToStream(ScannedImage image) { using (var snapshot = image.Preserve()) diff --git a/NAPS2.Sdk/Scan/Images/Storage/FileStorage.cs b/NAPS2.Sdk/Scan/Images/Storage/FileStorage.cs index 2c1d84746..6cd55c5b6 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/FileStorage.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/FileStorage.cs @@ -5,13 +5,10 @@ using System.Linq; namespace NAPS2.Scan.Images.Storage { - public class FileStorage : IStorage + public class FileStorage : IFileStorage { - private readonly FileStorageManager fileStorageManager; - - public FileStorage(FileStorageManager fileStorageManager, string fullPath) + public FileStorage(string fullPath) { - this.fileStorageManager = fileStorageManager; FullPath = fullPath ?? throw new ArgumentNullException(nameof(fullPath)); } @@ -22,7 +19,6 @@ namespace NAPS2.Scan.Images.Storage try { File.Delete(FullPath); - fileStorageManager.Detach(FullPath); } catch (IOException) { diff --git a/NAPS2.Sdk/Scan/Images/Storage/FileStorageManager.cs b/NAPS2.Sdk/Scan/Images/Storage/FileStorageManager.cs index 1243d48cf..59b923610 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/FileStorageManager.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/FileStorageManager.cs @@ -7,15 +7,14 @@ namespace NAPS2.Scan.Images.Storage { public class FileStorageManager { + private static FileStorageManager _default = new FileStorageManager(); + + public static FileStorageManager Default + { + get => _default; + set => _default = value ?? throw new ArgumentNullException(nameof(value)); + } + public virtual string NextFilePath() => Path.Combine(Paths.Temp, Path.GetRandomFileName()); - - public virtual void Attach(string path) - { - // TODO: Separate all this stuff out completely. - } - - public virtual void Detach(string path) - { - } } } diff --git a/NAPS2.Sdk/Scan/Images/Storage/GdiFileConverter.cs b/NAPS2.Sdk/Scan/Images/Storage/GdiFileConverter.cs index 1d3298b99..3b10a78f5 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/GdiFileConverter.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/GdiFileConverter.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.IO; using System.Linq; namespace NAPS2.Scan.Images.Storage { - public class GdiFileConverter : IStorageConverter, IStorageConverter + public class GdiFileConverter : + IStorageConverter, + IStorageConverter { private readonly FileStorageManager fileStorageManager; @@ -16,11 +19,20 @@ namespace NAPS2.Scan.Images.Storage public FileStorage Convert(GdiStorage input, StorageConvertParams convertParams) { - // TODO: Save smallest - string ext = convertParams.HighQuality ? ".png" : ".jpg"; - var path = fileStorageManager.NextFilePath() + ext; - input.Bitmap.Save(path); - return new FileStorage(fileStorageManager, path); + if (convertParams.Temporary) + { + var path = Path.Combine(Paths.Temp, Path.GetRandomFileName()); + input.Bitmap.Save(path); + return new FileStorage(path); + } + else + { + // TODO: Save smallest + string ext = convertParams.Lossless ? ".png" : ".jpg"; + var path = fileStorageManager.NextFilePath() + ext; + input.Bitmap.Save(path); + return new FileStorage(path); + } } public GdiStorage Convert(FileStorage input, StorageConvertParams convertParams) => new GdiStorage(new Bitmap(input.FullPath)); diff --git a/NAPS2.Sdk/Scan/Images/Storage/GdiStorage.cs b/NAPS2.Sdk/Scan/Images/Storage/GdiStorage.cs index 25af25dd7..906b31b7a 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/GdiStorage.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/GdiStorage.cs @@ -19,6 +19,18 @@ namespace NAPS2.Scan.Images.Storage public int Height => Bitmap.Height; + public float HorizontalResolution => Bitmap.HorizontalResolution; + + public float VerticalResolution => Bitmap.VerticalResolution; + + public void SetResolution(float xDpi, float yDpi) + { + if (xDpi > 0 && yDpi > 0) + { + Bitmap.SetResolution(xDpi, yDpi); + } + } + public StoragePixelFormat PixelFormat { get @@ -37,11 +49,13 @@ namespace NAPS2.Scan.Images.Storage } } + public bool IsOriginalLossless => Equals(Bitmap.RawFormat, ImageFormat.Bmp) || Equals(Bitmap.RawFormat, ImageFormat.Png); + public object Lock(out IntPtr scan0, out int stride) { var bitmapData = Bitmap.LockBits(new Rectangle(0, 0, Bitmap.Width, Bitmap.Height), ImageLockMode.ReadWrite, Bitmap.PixelFormat); scan0 = bitmapData.Scan0; - stride = bitmapData.Stride; + stride = Math.Abs(bitmapData.Stride); return bitmapData; } @@ -55,5 +69,10 @@ namespace NAPS2.Scan.Images.Storage { Bitmap.Dispose(); } + + public IMemoryStorage Clone() + { + return new GdiStorage((Bitmap)Bitmap.Clone()); + } } } diff --git a/NAPS2.Sdk/Scan/Images/Storage/GdiStorageFactory.cs b/NAPS2.Sdk/Scan/Images/Storage/GdiStorageFactory.cs index 4faa36ac9..fc74959dd 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/GdiStorageFactory.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/GdiStorageFactory.cs @@ -9,9 +9,37 @@ namespace NAPS2.Scan.Images.Storage { public class GdiStorageFactory : IMemoryStorageFactory { - public IStorage FromBmpStream(Stream stream) => new GdiStorage(new Bitmap(stream)); + public IMemoryStorage Decode(Stream stream, string ext) => new GdiStorage(new Bitmap(stream)); - public IStorage FromDimensions(int width, int height, StoragePixelFormat pixelFormat) => new GdiStorage(new Bitmap(width, height, GdiPixelFormat(pixelFormat))); + public IMemoryStorage Decode(string path) => new GdiStorage(new Bitmap(path)); + + public IEnumerable DecodeMultiple(Stream stream, string ext, out int count) + { + var bitmap = new Bitmap(stream); + count = bitmap.GetFrameCount(FrameDimension.Page); + return EnumerateFrames(bitmap, count); + } + + public IEnumerable DecodeMultiple(string path, out int count) + { + var bitmap = new Bitmap(path); + count = bitmap.GetFrameCount(FrameDimension.Page); + return EnumerateFrames(bitmap, count); + } + + private IEnumerable EnumerateFrames(Bitmap bitmap, int count) + { + using (bitmap) + { + for (int i = 0; i < count; i++) + { + bitmap.SelectActiveFrame(FrameDimension.Page, i); + yield return new GdiStorage((Bitmap) bitmap.Clone()); + } + } + } + + public IMemoryStorage FromDimensions(int width, int height, StoragePixelFormat pixelFormat) => new GdiStorage(new Bitmap(width, height, GdiPixelFormat(pixelFormat))); private PixelFormat GdiPixelFormat(StoragePixelFormat pixelFormat) { diff --git a/NAPS2.Sdk/Scan/Images/Storage/GdiTransformer.cs b/NAPS2.Sdk/Scan/Images/Storage/GdiTransformer.cs new file mode 100644 index 000000000..015eeaac4 --- /dev/null +++ b/NAPS2.Sdk/Scan/Images/Storage/GdiTransformer.cs @@ -0,0 +1,435 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Linq; +using System.Runtime.InteropServices; +using NAPS2.Scan.Images.Transforms; +using NAPS2.Util; + +namespace NAPS2.Scan.Images.Storage +{ + public class GdiTransformer : + ITransformer, + ITransformer, + ITransformer, + ITransformer, + ITransformer, + ITransformer, + ITransformer, + ITransformer, + ITransformer, + ITransformer, + ITransformer + { + public GdiStorage PerformTransform(GdiStorage storage, BrightnessTransform transform) + { + var bitmap = storage.Bitmap; + float brightnessAdjusted = transform.Brightness / 1000f; + EnsurePixelFormat(ref bitmap); + UnsafeImageOps.ChangeBrightness(bitmap, brightnessAdjusted); + return new GdiStorage(bitmap); + } + + public GdiStorage PerformTransform(GdiStorage storage, ContrastTransform transform) + { + float contrastAdjusted = transform.Contrast / 1000f + 1.0f; + + var bitmap = storage.Bitmap; + EnsurePixelFormat(ref bitmap); + using (var g = Graphics.FromImage(bitmap)) + { + var attrs = new ImageAttributes(); + attrs.SetColorMatrix(new ColorMatrix + { + Matrix00 = contrastAdjusted, + Matrix11 = contrastAdjusted, + Matrix22 = contrastAdjusted + }); + g.DrawImage(bitmap, + new Rectangle(0, 0, bitmap.Width, bitmap.Height), + 0, + 0, + bitmap.Width, + bitmap.Height, + GraphicsUnit.Pixel, + attrs); + } + return storage; + } + + public GdiStorage PerformTransform(GdiStorage storage, TrueContrastTransform transform) + { + // convert +/-1000 input range to a logarithmic scaled multiplier + float contrastAdjusted = (float)Math.Pow(2.718281f, transform.Contrast / 500.0f); + // see http://docs.rainmeter.net/tips/colormatrix-guide/ for offset & matrix calculation + float offset = (1.0f - contrastAdjusted) / 2.0f; + + var bitmap = storage.Bitmap; + EnsurePixelFormat(ref bitmap); + UnsafeImageOps.ChangeContrast(bitmap, contrastAdjusted, offset); + // TODO: Actually need to create a new storage. Change signature of EnsurePixelFormat. + return storage; + } + + public GdiStorage PerformTransform(GdiStorage storage, HueTransform transform) + { + if (storage.PixelFormat != StoragePixelFormat.RGB24 && storage.PixelFormat != StoragePixelFormat.ARGB32) + { + // No need to handle 1bpp since hue shifts are null transforms + return storage; + } + + float hueShiftAdjusted = transform.HueShift / 2000f * 360; + if (hueShiftAdjusted < 0) + { + hueShiftAdjusted += 360; + } + + UnsafeImageOps.HueShift(storage.Bitmap, hueShiftAdjusted); + + return storage; + } + + public GdiStorage PerformTransform(GdiStorage storage, SaturationTransform transform) + { + double saturationAdjusted = transform.Saturation / 1000.0 + 1; + + var bitmap = storage.Bitmap; + EnsurePixelFormat(ref bitmap); + int bytesPerPixel; + if (bitmap.PixelFormat == PixelFormat.Format24bppRgb) + { + bytesPerPixel = 3; + } + else if (bitmap.PixelFormat == PixelFormat.Format32bppArgb) + { + bytesPerPixel = 4; + } + else + { + return storage; + } + + var data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, bitmap.PixelFormat); + var stride = Math.Abs(data.Stride); + for (int y = 0; y < data.Height; y++) + { + for (int x = 0; x < data.Width; x++) + { + int r = Marshal.ReadByte(data.Scan0 + stride * y + x * bytesPerPixel); + int g = Marshal.ReadByte(data.Scan0 + stride * y + x * bytesPerPixel + 1); + int b = Marshal.ReadByte(data.Scan0 + stride * y + x * bytesPerPixel + 2); + + Color c = Color.FromArgb(255, r, g, b); + ColorHelper.ColorToHSL(c, out double h, out double s, out double v); + + s = Math.Min(s * saturationAdjusted, 1); + + c = ColorHelper.ColorFromHSL(h, s, v); + + Marshal.WriteByte(data.Scan0 + stride * y + x * bytesPerPixel, c.R); + Marshal.WriteByte(data.Scan0 + stride * y + x * bytesPerPixel + 1, c.G); + Marshal.WriteByte(data.Scan0 + stride * y + x * bytesPerPixel + 2, c.B); + } + } + bitmap.UnlockBits(data); + + return storage; + } + + public GdiStorage PerformTransform(GdiStorage storage, SharpenTransform transform) + { + double sharpnessAdjusted = transform.Sharpness / 1000.0; + + var bitmap = storage.Bitmap; + EnsurePixelFormat(ref bitmap); + int bytesPerPixel; + if (bitmap.PixelFormat == PixelFormat.Format24bppRgb) + { + bytesPerPixel = 3; + } + else if (bitmap.PixelFormat == PixelFormat.Format32bppArgb) + { + bytesPerPixel = 4; + } + else + { + return storage; + } + + // From https://stackoverflow.com/a/17596299 + + int width = bitmap.Width; + int height = bitmap.Height; + + // Create sharpening filter. + const int filterSize = 5; + + var filter = new double[,] + { + {-1, -1, -1, -1, -1}, + {-1, 2, 2, 2, -1}, + {-1, 2, 16, 2, -1}, + {-1, 2, 2, 2, -1}, + {-1, -1, -1, -1, -1} + }; + + double bias = 1.0 - sharpnessAdjusted; + double factor = sharpnessAdjusted / 16.0; + + const int s = filterSize / 2; + + var result = new Color[bitmap.Width, bitmap.Height]; + + // Lock image bits for read/write. + BitmapData pbits = bitmap.LockBits(new Rectangle(0, 0, width, height), + ImageLockMode.ReadWrite, + bitmap.PixelFormat); + + // Declare an array to hold the bytes of the bitmap. + int bytes = pbits.Stride * height; + var rgbValues = new byte[bytes]; + + // Copy the RGB values into the array. + Marshal.Copy(pbits.Scan0, rgbValues, 0, bytes); + + int rgb; + // Fill the color array with the new sharpened color values. + for (int x = s; x < width - s; x++) + { + for (int y = s; y < height - s; y++) + { + double red = 0.0, green = 0.0, blue = 0.0; + + for (int filterX = 0; filterX < filterSize; filterX++) + { + for (int filterY = 0; filterY < filterSize; filterY++) + { + int imageX = (x - s + filterX + width) % width; + int imageY = (y - s + filterY + height) % height; + + rgb = imageY * pbits.Stride + bytesPerPixel * imageX; + + red += rgbValues[rgb + 2] * filter[filterX, filterY]; + green += rgbValues[rgb + 1] * filter[filterX, filterY]; + blue += rgbValues[rgb + 0] * filter[filterX, filterY]; + } + + rgb = y * pbits.Stride + bytesPerPixel * x; + + int r = Math.Min(Math.Max((int)(factor * red + (bias * rgbValues[rgb + 2])), 0), 255); + int g = Math.Min(Math.Max((int)(factor * green + (bias * rgbValues[rgb + 1])), 0), 255); + int b = Math.Min(Math.Max((int)(factor * blue + (bias * rgbValues[rgb + 0])), 0), 255); + + result[x, y] = Color.FromArgb(r, g, b); + } + } + } + + // Update the image with the sharpened pixels. + for (int x = s; x < width - s; x++) + { + for (int y = s; y < height - s; y++) + { + rgb = y * pbits.Stride + bytesPerPixel * x; + + rgbValues[rgb + 2] = result[x, y].R; + rgbValues[rgb + 1] = result[x, y].G; + rgbValues[rgb + 0] = result[x, y].B; + } + } + + // Copy the RGB values back to the bitmap. + Marshal.Copy(rgbValues, 0, pbits.Scan0, bytes); + // Release image bits. + bitmap.UnlockBits(pbits); + + return storage; + } + + public GdiStorage PerformTransform(GdiStorage storage, RotationTransform transform) + { + + if (Math.Abs(transform.Angle - 0.0) < RotationTransform.TOLERANCE) + { + return storage; + } + if (Math.Abs(transform.Angle - 90.0) < RotationTransform.TOLERANCE) + { + storage.Bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone); + return storage; + } + if (Math.Abs(transform.Angle - 180.0) < RotationTransform.TOLERANCE) + { + storage.Bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone); + return storage; + } + if (Math.Abs(transform.Angle - 270.0) < RotationTransform.TOLERANCE) + { + storage.Bitmap.RotateFlip(RotateFlipType.Rotate270FlipNone); + return storage; + } + Bitmap result; + if (transform.Angle > 45.0 && transform.Angle < 135.0 || transform.Angle > 225.0 && transform.Angle < 315.0) + { + result = new Bitmap(storage.Height, storage.Width); + result.SafeSetResolution(storage.VerticalResolution, storage.HorizontalResolution); + } + else + { + result = new Bitmap(storage.Width, storage.Height); + result.SafeSetResolution(storage.HorizontalResolution, storage.VerticalResolution); + } + using (var g = Graphics.FromImage(result)) + { + g.Clear(Color.White); + g.TranslateTransform(result.Width / 2.0f, result.Height / 2.0f); + g.RotateTransform((float)transform.Angle); + g.TranslateTransform(-storage.Width / 2.0f, -storage.Height / 2.0f); + g.DrawImage(storage.Bitmap, new Rectangle(0, 0, storage.Width, storage.Height)); + } + OptimizePixelFormat(storage.Bitmap, ref result); + storage.Dispose(); + return new GdiStorage(result); + } + + public GdiStorage PerformTransform(GdiStorage storage, CropTransform transform) + { + double xScale = storage.Width / (double)(transform.OriginalWidth ?? storage.Width), + yScale = storage.Height / (double)(transform.OriginalHeight ?? storage.Height); + + int width = Math.Max(storage.Width - (int)Math.Round((transform.Left + transform.Right) * xScale), 1); + int height = Math.Max(storage.Height - (int)Math.Round((transform.Top + transform.Bottom) * yScale), 1); + var result = new Bitmap(width, height, PixelFormat.Format24bppRgb); + result.SafeSetResolution(storage.HorizontalResolution, storage.VerticalResolution); + using (var g = Graphics.FromImage(result)) + { + g.Clear(Color.White); + int x = (int) Math.Round(-transform.Left * xScale); + int y = (int) Math.Round(-transform.Top * yScale); + g.DrawImage(storage.Bitmap, new Rectangle(x, y, storage.Width, storage.Height)); + } + OptimizePixelFormat(storage.Bitmap, ref result); + storage.Dispose(); + return new GdiStorage(result); + } + + public GdiStorage PerformTransform(GdiStorage storage, ScaleTransform transform) + { + double realWidth = storage.Width / transform.ScaleFactor; + double realHeight = storage.Height / transform.ScaleFactor; + + double horizontalRes = storage.HorizontalResolution / transform.ScaleFactor; + double verticalRes = storage.VerticalResolution / transform.ScaleFactor; + + var result = new Bitmap((int)realWidth, (int)realHeight, PixelFormat.Format24bppRgb); + using (Graphics g = Graphics.FromImage(result)) + { + g.InterpolationMode = InterpolationMode.HighQualityBicubic; + g.DrawImage(storage.Bitmap, 0, 0, (int)realWidth, (int)realHeight); + result.SafeSetResolution((float)horizontalRes, (float)verticalRes); + return new GdiStorage(result); + } + } + + public GdiStorage PerformTransform(GdiStorage storage, BlackWhiteTransform transform) + { + if (storage.PixelFormat != StoragePixelFormat.RGB24 && storage.PixelFormat != StoragePixelFormat.ARGB32) + { + return storage; + } + + var monoBitmap = UnsafeImageOps.ConvertTo1Bpp(storage.Bitmap, transform.Threshold); + storage.Dispose(); + + return new GdiStorage(monoBitmap); + } + + /// + /// Gets a bitmap resized to fit within a thumbnail rectangle, including a border around the picture. + /// + /// The bitmap to resize. + /// The maximum width and height of the thumbnail. + /// The thumbnail bitmap. + public GdiStorage PerformTransform(GdiStorage storage, ThumbnailTransform transform) + { + var result = new Bitmap(transform.Size, transform.Size); + using (Graphics g = Graphics.FromImage(result)) + { + // The location and dimensions of the old bitmap, scaled and positioned within the thumbnail bitmap + int left, top, width, height; + + // We want a nice thumbnail, so use the maximum quality interpolation + g.InterpolationMode = InterpolationMode.HighQualityBicubic; + + if (storage.Width > storage.Height) + { + // Fill the new bitmap's width + width = transform.Size; + left = 0; + // Scale the drawing height to match the original bitmap's aspect ratio + height = (int)(storage.Height * (transform.Size / (double)storage.Width)); + // Center the drawing vertically + top = (transform.Size - height) / 2; + } + else + { + // Fill the new bitmap's height + height = transform.Size; + top = 0; + // Scale the drawing width to match the original bitmap's aspect ratio + width = (int)(storage.Width * (transform.Size / (double)storage.Height)); + // Center the drawing horizontally + left = (transform.Size - width) / 2; + } + + // Draw the original bitmap onto the new bitmap, using the calculated location and dimensions + // Note that there may be some padding if the aspect ratios don't match + var destRect = new RectangleF(left, top, width, height); + var srcRect = new RectangleF(0, 0, storage.Width, storage.Height); + g.DrawImage(storage.Bitmap, destRect, srcRect, GraphicsUnit.Pixel); + // Draw a border around the orignal bitmap's content, inside the padding + g.DrawRectangle(Pens.Black, left, top, width - 1, height - 1); + } + + return new GdiStorage(result); + } + + /// + /// If the provided bitmap is 1-bit (black and white), replace it with a 24-bit bitmap so that image transforms will work. If the bitmap is replaced, the original is disposed. + /// + /// The bitmap that may be replaced. + protected static void EnsurePixelFormat(ref Bitmap bitmap) + { + if (bitmap.PixelFormat == PixelFormat.Format1bppIndexed) + { + // Copy B&W over to grayscale + var bitmap2 = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format24bppRgb); + bitmap2.SafeSetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution); + using (var g = Graphics.FromImage(bitmap2)) + { + g.DrawImage(bitmap, 0, 0); + } + bitmap.Dispose(); + bitmap = bitmap2; + } + } + + /// + /// If the original bitmap is 1-bit (black and white), optimize the result by making it 1-bit too. + /// + /// The original bitmap that is used to determine whether the result should be black and white. + /// The result that may be replaced. + protected static void OptimizePixelFormat(Bitmap original, ref Bitmap result) + { + if (original.PixelFormat == PixelFormat.Format1bppIndexed) + { + var bitmap2 = (Bitmap)BitmapHelper.CopyToBpp(result, 1).Clone(); + result.Dispose(); + result = bitmap2; + } + } + } +} diff --git a/NAPS2.Sdk/Scan/Images/Storage/IFileStorage.cs b/NAPS2.Sdk/Scan/Images/Storage/IFileStorage.cs new file mode 100644 index 000000000..3c13b0712 --- /dev/null +++ b/NAPS2.Sdk/Scan/Images/Storage/IFileStorage.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NAPS2.Scan.Images.Storage +{ + public interface IFileStorage : IStorage + { + string FullPath { get; } + } +} diff --git a/NAPS2.Sdk/Scan/Images/Storage/IImageMetadata.cs b/NAPS2.Sdk/Scan/Images/Storage/IImageMetadata.cs new file mode 100644 index 000000000..a1d852aa5 --- /dev/null +++ b/NAPS2.Sdk/Scan/Images/Storage/IImageMetadata.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NAPS2.Scan.Images.Transforms; + +namespace NAPS2.Scan.Images.Storage +{ + public interface IImageMetadata : IDisposable + { + List TransformList { get; set; } + + int Index { get; set; } + + ScanBitDepth BitDepth { get; set; } + + bool Lossless { get; set; } + + void Commit(); + + bool CanSerialize { get; } + + byte[] Serialize(IStorage storage); + + IStorage Deserialize(byte[] serializedData); + } +} diff --git a/NAPS2.Sdk/Scan/Images/Storage/IImageMetadataFactory.cs b/NAPS2.Sdk/Scan/Images/Storage/IImageMetadataFactory.cs new file mode 100644 index 000000000..7d910b954 --- /dev/null +++ b/NAPS2.Sdk/Scan/Images/Storage/IImageMetadataFactory.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NAPS2.Scan.Images.Storage +{ + public interface IImageMetadataFactory + { + IImageMetadata CreateMetadata(IStorage storage); + } +} diff --git a/NAPS2.Sdk/Scan/Images/Storage/IMemoryStorage.cs b/NAPS2.Sdk/Scan/Images/Storage/IMemoryStorage.cs index 00a01c87a..3d11a5699 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/IMemoryStorage.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/IMemoryStorage.cs @@ -1,19 +1,31 @@ using System; using System.Collections.Generic; +using System.Drawing.Imaging; using System.Linq; namespace NAPS2.Scan.Images.Storage { + // TODO: Maybe just call this IImage. public interface IMemoryStorage : IStorage { int Width { get; } int Height { get; } + float HorizontalResolution { get; } + + float VerticalResolution { get; } + + void SetResolution(float xDpi, float yDpi); + StoragePixelFormat PixelFormat { get; } + bool IsOriginalLossless { get; } + object Lock(out IntPtr scan0, out int stride); void Unlock(object state); + + IMemoryStorage Clone(); } } diff --git a/NAPS2.Sdk/Scan/Images/Storage/IMemoryStorageFactory.cs b/NAPS2.Sdk/Scan/Images/Storage/IMemoryStorageFactory.cs index 8b6d5bb22..83dd8cd5e 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/IMemoryStorageFactory.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/IMemoryStorageFactory.cs @@ -7,8 +7,40 @@ namespace NAPS2.Scan.Images.Storage { public interface IMemoryStorageFactory { - IStorage FromBmpStream(Stream stream); + /// + /// Decodes an image from the given stream and file extension. + /// + /// The image data, in a common format (JPEG, PNG, etc). + /// A file extension hinting at the image format. When possible, the contents of the stream should be used to definitively determine the image format. + /// + IMemoryStorage Decode(Stream stream, string ext); - IStorage FromDimensions(int width, int height, StoragePixelFormat pixelFormat); + /// + /// Decodes an image from the given file path. + /// + /// The image path. + /// + IMemoryStorage Decode(string path); + + /// + /// Decodes an image from the given stream and file extension. + /// If there are multiple images (e.g. TIFF), multiple results will be returned; + /// however, only the enumerator's current IStorage is guaranteed to be valid. + /// + /// The image data, in a common format (JPEG, PNG, etc). + /// A file extension hinting at the image format. When possible, the contents of the stream should be used to definitively determine the image format. + /// The number of returned images. + /// + IEnumerable DecodeMultiple(Stream stream, string ext, out int count); + + /// + /// Decodes an image from the given file path. + /// + /// The image path. + /// The number of returned images. + /// + IEnumerable DecodeMultiple(string path, out int count); + + IMemoryStorage FromDimensions(int width, int height, StoragePixelFormat pixelFormat); } } diff --git a/NAPS2.Sdk/Scan/Images/Storage/IStorageConverter.cs b/NAPS2.Sdk/Scan/Images/Storage/IStorageConverter.cs index baf0ba5f2..788e04c47 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/IStorageConverter.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/IStorageConverter.cs @@ -4,7 +4,7 @@ using System.Linq; namespace NAPS2.Scan.Images.Storage { - public interface IStorageConverter + public interface IStorageConverter where TStorage1 : IStorage where TStorage2 : IStorage { TStorage2 Convert(TStorage1 input, StorageConvertParams convertParams); } diff --git a/NAPS2.Sdk/Scan/Images/Storage/ITransformer.cs b/NAPS2.Sdk/Scan/Images/Storage/ITransformer.cs new file mode 100644 index 000000000..1db99779c --- /dev/null +++ b/NAPS2.Sdk/Scan/Images/Storage/ITransformer.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NAPS2.Scan.Images.Transforms; + +namespace NAPS2.Scan.Images.Storage +{ + public interface ITransformer where TStorage : IMemoryStorage where TTransform : Transform + { + TStorage PerformTransform(TStorage storage, TTransform transform); + } +} diff --git a/NAPS2.Sdk/Scan/Images/Storage/PdfFileStorage.cs b/NAPS2.Sdk/Scan/Images/Storage/PdfFileStorage.cs index 7cea34ee7..76540aa3d 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/PdfFileStorage.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/PdfFileStorage.cs @@ -5,7 +5,7 @@ using System.Linq; namespace NAPS2.Scan.Images.Storage { - public class PdfFileStorage : IStorage + public class PdfFileStorage : IFileStorage { public PdfFileStorage(string fullPath) { diff --git a/NAPS2.Sdk/Scan/Images/Storage/RecoverableImageMetadata.cs b/NAPS2.Sdk/Scan/Images/Storage/RecoverableImageMetadata.cs new file mode 100644 index 000000000..0c6db9cd5 --- /dev/null +++ b/NAPS2.Sdk/Scan/Images/Storage/RecoverableImageMetadata.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NAPS2.Recovery; +using NAPS2.Scan.Images.Transforms; + +namespace NAPS2.Scan.Images.Storage +{ + public class RecoverableImageMetadata : IImageMetadata + { + private readonly RecoveryStorageManager rsm; + private RecoveryIndexImage indexImage; + + public RecoverableImageMetadata(RecoveryStorageManager rsm, RecoveryIndexImage indexImage) + { + this.rsm = rsm; + // TODO: Maybe not a constructor param? + this.indexImage = indexImage; + } + + public List TransformList + { + get => indexImage.TransformList; + set => indexImage.TransformList = value; + } + + public int Index + { + get => rsm.Index.Images.IndexOf(indexImage); + set + { + // TODO: Locking + rsm.Index.Images.Remove(indexImage); + rsm.Index.Images.Insert(value, indexImage); + } + } + + public ScanBitDepth BitDepth + { + get => indexImage.BitDepth; + set => indexImage.BitDepth = value; + } + + public bool Lossless + { + get => indexImage.HighQuality; + set => indexImage.HighQuality = value; + } + + public void Commit() + { + rsm.Commit(); + } + + public bool CanSerialize => true; + + public byte[] Serialize(IStorage storage) + { + throw new NotImplementedException(); + } + + public IStorage Deserialize(byte[] serializedData) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + rsm.Index.Images.Remove(indexImage); + // TODO: Commit? + } + } +} diff --git a/NAPS2.Sdk/Scan/Images/Storage/RecoveryStorageManager.cs b/NAPS2.Sdk/Scan/Images/Storage/RecoveryStorageManager.cs index 3ae941beb..0bc73afed 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/RecoveryStorageManager.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/RecoveryStorageManager.cs @@ -1,27 +1,78 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; using System.Linq; using NAPS2.Config; using NAPS2.Recovery; namespace NAPS2.Scan.Images.Storage { - public class RecoveryStorageManager : FileStorageManager + public class RecoveryStorageManager : FileStorageManager, IImageMetadataFactory { + public const string LOCK_FILE_NAME = ".lock"; + + private readonly string recoveryFolderPath; + + private int fileNumber; + private bool folderCreated; + private FileInfo folderLockFile; + private Stream folderLock; private ConfigManager indexConfigManager; public RecoveryStorageManager(string recoveryFolderPath) { - indexConfigManager = new ConfigManager("index.xml", recoveryFolderPath, null, RecoveryIndex.Create); + this.recoveryFolderPath = recoveryFolderPath; } - public override void Detach(string path) + public RecoveryIndex Index { - //lock (this) - //{ - // indexConfigManager.Config.Images.Remove(IndexImage); - // indexConfigManager.Save(); - //} + get + { + EnsureFolderCreated(); + return indexConfigManager.Config; + } + } + + public override string NextFilePath() + { + string fileName = $"{Process.GetCurrentProcess().Id}_{(++fileNumber).ToString("D5", CultureInfo.InvariantCulture)}"; + return Path.Combine(recoveryFolderPath, fileName); + } + + private void EnsureFolderCreated() + { + if (!folderCreated) + { + var folder = new DirectoryInfo(recoveryFolderPath); + folder.Create(); + folderLockFile = new FileInfo(Path.Combine(recoveryFolderPath, LOCK_FILE_NAME)); + folderLock = folderLockFile.Open(FileMode.CreateNew, FileAccess.Write, FileShare.None); + indexConfigManager = new ConfigManager("index.xml", recoveryFolderPath, null, RecoveryIndex.Create); + folderCreated = true; + } + } + + public void Commit() + { + // TODO: Clean up when all contents are removed + + EnsureFolderCreated(); + indexConfigManager.Save(); + } + + public IImageMetadata CreateMetadata(IStorage storage) + { + var fileStorage = storage as IFileStorage; + if (fileStorage == null) + { + throw new ArgumentException("RecoveryStorageManager can only used with IFileStorage."); + } + return new RecoverableImageMetadata(this, new RecoveryIndexImage + { + FileName = Path.GetFileName(fileStorage.FullPath) + }); } } } diff --git a/NAPS2.Sdk/Scan/Images/Storage/StorageConvertParams.cs b/NAPS2.Sdk/Scan/Images/Storage/StorageConvertParams.cs index 9530efa8f..66de4aaa8 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/StorageConvertParams.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/StorageConvertParams.cs @@ -6,6 +6,10 @@ namespace NAPS2.Scan.Images.Storage { public class StorageConvertParams { - public bool HighQuality { get; set; } + public bool Temporary { get; set; } + + public bool Lossless { get; set; } + + public int LossyQuality { get; set; } } } diff --git a/NAPS2.Sdk/Scan/Images/Storage/StorageManager.cs b/NAPS2.Sdk/Scan/Images/Storage/StorageManager.cs index 8d5b1e7ba..113a2917d 100644 --- a/NAPS2.Sdk/Scan/Images/Storage/StorageManager.cs +++ b/NAPS2.Sdk/Scan/Images/Storage/StorageManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using NAPS2.Scan.Images.Transforms; using NAPS2.Util; namespace NAPS2.Scan.Images.Storage @@ -18,38 +19,84 @@ namespace NAPS2.Scan.Images.Storage public static IMemoryStorageFactory MemoryStorageFactory { get; set; } = new GdiStorageFactory(); + public static IImageMetadataFactory ImageMetadataFactory { get; set; } + + private static readonly Dictionary<(Type, Type), (object, MethodInfo)> Transformers = new Dictionary<(Type, Type), (object, MethodInfo)>(); + + public static void RegisterTransformer(object transformer) + { + foreach (var interfaceType in transformer.GetType().GetInterfaces().Where(x => x.Name == "ITransformer")) + { + var genericArgs = interfaceType.GetGenericArguments(); + Transformers.Add((genericArgs[0], genericArgs[1]), (transformer, interfaceType.GetMethod("PerformTransform"))); + } + + } + + public static IMemoryStorage PerformTransform(IMemoryStorage storage, Transform transform) + { + try + { + var (transformer, perform) = Transformers[(storage.GetType(), transform.GetType())]; + return (IMemoryStorage)perform.Invoke(transformer, new object[] { storage, transform }); + } + catch (KeyNotFoundException) + { + throw new ArgumentException($"No transformer exists for {storage.GetType().Name} and {transform.GetType().Name}"); + } + } + + public static IMemoryStorage PerformAllTransforms(IMemoryStorage storage, IEnumerable transforms) + { + return transforms.Aggregate(storage, PerformTransform); + } + private static readonly Dictionary<(Type, Type), (object, MethodInfo)> Converters = new Dictionary<(Type, Type), (object, MethodInfo)>(); - public static void RegisterConverter(IStorageConverter converter) + public static void RegisterConverter(IStorageConverter converter) where TStorage1 : IStorage where TStorage2 : IStorage { Converters.Add((typeof(TStorage1), typeof(TStorage2)), (converter, typeof(IStorageConverter).GetMethod("Convert"))); } - public static IStorage ConvertToBacking(IStorage storage) + public static IStorage ConvertToBacking(IStorage storage, StorageConvertParams convertParams) { if (BackingStorageTypes.Contains(storage.GetType())) { return storage; } - return Convert(storage, PreferredBackingStorageType); + return Convert(storage, PreferredBackingStorageType, convertParams); } - public static IStorage ConvertToMemory(IStorage storage) + public static IMemoryStorage ConvertToMemory(IStorage storage, StorageConvertParams convertParams) { - if (storage is IMemoryStorage) + if (storage is IMemoryStorage memStorage) + { + return memStorage; + } + return (IMemoryStorage)Convert(storage, PreferredMemoryStorageType, convertParams); + } + + public static TStorage Convert(IStorage storage) + { + return (TStorage)Convert(storage, typeof(TStorage), new StorageConvertParams()); + } + + public static TStorage Convert(IStorage storage, StorageConvertParams convertParams) + { + return (TStorage)Convert(storage, typeof(TStorage), convertParams); + } + + public static IStorage Convert(IStorage storage, Type type, StorageConvertParams convertParams) + { + if (storage.GetType() == type) { return storage; } - return Convert(storage, PreferredMemoryStorageType); - } - - public static IStorage Convert(IStorage storage, Type type) - { - // TODO: Dispose old storage? + // TODO: Dispose old storage? Consider ownership. Possibility: Clone/Dispose ref counts. try { var (converter, convert) = Converters[(storage.GetType(), type)]; - return (IStorage) convert.Invoke(converter, new object[] {storage}); + return (IStorage)convert.Invoke(converter, new object[] { storage, convertParams }); } catch (KeyNotFoundException) { @@ -62,6 +109,8 @@ namespace NAPS2.Scan.Images.Storage var gdiFileConverter = new GdiFileConverter(new FileStorageManager()); RegisterConverter(gdiFileConverter); RegisterConverter(gdiFileConverter); + var gdiTransformer = new GdiTransformer(); + RegisterTransformer(gdiTransformer); } } } diff --git a/NAPS2.Sdk/Scan/Images/Storage/StubImageMetadata.cs b/NAPS2.Sdk/Scan/Images/Storage/StubImageMetadata.cs new file mode 100644 index 000000000..9404c15cb --- /dev/null +++ b/NAPS2.Sdk/Scan/Images/Storage/StubImageMetadata.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NAPS2.Scan.Images.Transforms; + +namespace NAPS2.Scan.Images.Storage +{ + public class StubImageMetadata : IImageMetadata + { + public List TransformList { get; set; } + + public int Index { get; set; } + + public ScanBitDepth BitDepth { get; set; } + + public bool Lossless { get; set; } + + public void Commit() + { + } + + public bool CanSerialize => true; + + public byte[] Serialize(IStorage storage) => throw new InvalidOperationException(); + + public IStorage Deserialize(byte[] serializedData) => throw new InvalidOperationException(); + + public void Dispose() + { + } + } +} diff --git a/NAPS2.Sdk/Scan/Images/Storage/StubImageMetadataFactory.cs b/NAPS2.Sdk/Scan/Images/Storage/StubImageMetadataFactory.cs new file mode 100644 index 000000000..ccea06c19 --- /dev/null +++ b/NAPS2.Sdk/Scan/Images/Storage/StubImageMetadataFactory.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NAPS2.Scan.Images.Storage +{ + public class StubImageMetadataFactory : IImageMetadataFactory + { + public IImageMetadata CreateMetadata(IStorage storage) => new StubImageMetadata(); + } +} diff --git a/NAPS2.Sdk/Scan/Images/ThresholdBlankDetector.cs b/NAPS2.Sdk/Scan/Images/ThresholdBlankDetector.cs index 61b6d1256..7f70f4695 100644 --- a/NAPS2.Sdk/Scan/Images/ThresholdBlankDetector.cs +++ b/NAPS2.Sdk/Scan/Images/ThresholdBlankDetector.cs @@ -4,6 +4,7 @@ using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Runtime.InteropServices; +using NAPS2.Scan.Images.Storage; namespace NAPS2.Scan.Images { @@ -16,23 +17,28 @@ namespace NAPS2.Scan.Images private const double COVERAGE_THRESHOLD_MIN = 0.00; private const double COVERAGE_THRESHOLD_MAX = 0.01; - public bool IsBlank(Bitmap bitmap, int whiteThresholdNorm, int coverageThresholdNorm) + public bool IsBlank(IMemoryStorage bitmap, int whiteThresholdNorm, int coverageThresholdNorm) { - if (bitmap.PixelFormat == PixelFormat.Format1bppIndexed) + if (bitmap.PixelFormat == StoragePixelFormat.BW1) { - using (var bitmap2 = BitmapHelper.CopyToBpp(bitmap, 8)) + // TODO: Make more generic + if (!(bitmap is GdiStorage gdiStorage)) { - return IsBlankRGB(bitmap2, whiteThresholdNorm, coverageThresholdNorm); + throw new InvalidOperationException("Patch code detection only supported for GdiStorage"); + } + using (var bitmap2 = BitmapHelper.CopyToBpp(gdiStorage.Bitmap, 8)) + { + return IsBlankRGB(new GdiStorage(bitmap2), whiteThresholdNorm, coverageThresholdNorm); } } - if (bitmap.PixelFormat != PixelFormat.Format24bppRgb) + if (bitmap.PixelFormat != StoragePixelFormat.RGB24) { return false; } return IsBlankRGB(bitmap, whiteThresholdNorm, coverageThresholdNorm); } - private static bool IsBlankRGB(Bitmap bitmap, int whiteThresholdNorm, int coverageThresholdNorm) + private static bool IsBlankRGB(IMemoryStorage bitmap, int whiteThresholdNorm, int coverageThresholdNorm) { var whiteThreshold = (int)Math.Round(WHITE_THRESHOLD_MIN + (whiteThresholdNorm / 100.0) * (WHITE_THRESHOLD_MAX - WHITE_THRESHOLD_MIN)); var coverageThreshold = COVERAGE_THRESHOLD_MIN + (coverageThresholdNorm / 100.0) * (COVERAGE_THRESHOLD_MAX - COVERAGE_THRESHOLD_MIN); @@ -40,14 +46,13 @@ namespace NAPS2.Scan.Images long totalPixels = bitmap.Width * bitmap.Height; long matchPixels = 0; - var data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, bitmap.PixelFormat); - var stride = Math.Abs(data.Stride); - var bytes = new byte[stride * data.Height]; - Marshal.Copy(data.Scan0, bytes, 0, bytes.Length); - bitmap.UnlockBits(data); - for (int x = 0; x < data.Width; x++) + var data = bitmap.Lock(out var scan0, out var stride); + var bytes = new byte[stride * bitmap.Height]; + Marshal.Copy(scan0, bytes, 0, bytes.Length); + bitmap.Unlock(data); + for (int x = 0; x < bitmap.Width; x++) { - for (int y = 0; y < data.Height; y++) + for (int y = 0; y < bitmap.Height; y++) { int r = bytes[stride * y + x * 3]; int g = bytes[stride * y + x * 3 + 1]; @@ -65,7 +70,7 @@ namespace NAPS2.Scan.Images return coverage < coverageThreshold; } - public bool ExcludePage(Bitmap bitmap, ScanProfile scanProfile) + public bool ExcludePage(IMemoryStorage bitmap, ScanProfile scanProfile) { return scanProfile.ExcludeBlankPages && IsBlank(bitmap, scanProfile.BlankPageWhiteThreshold, scanProfile.BlankPageCoverageThreshold); } diff --git a/NAPS2.Sdk/Scan/Images/ThumbnailRenderer.cs b/NAPS2.Sdk/Scan/Images/ThumbnailRenderer.cs index aaed0f8ac..c31e6ca88 100644 --- a/NAPS2.Sdk/Scan/Images/ThumbnailRenderer.cs +++ b/NAPS2.Sdk/Scan/Images/ThumbnailRenderer.cs @@ -51,88 +51,21 @@ namespace NAPS2.Scan.Images } return (size - 832) / 96 + 16; } - - private readonly ScannedImageRenderer scannedImageRenderer; - public ThumbnailRenderer(ScannedImageRenderer scannedImageRenderer) - { - this.scannedImageRenderer = scannedImageRenderer; - } + //public Task RenderThumbnail(ScannedImage scannedImage, int size) + //{ + // using (var snapshot = scannedImage.Preserve()) + // { + // return RenderThumbnail(snapshot, size); + // } + //} - public Task RenderThumbnail(ScannedImage scannedImage) - { - return RenderThumbnail(scannedImage, UserConfig.Current.ThumbnailSize); - } - - public Task RenderThumbnail(ScannedImage scannedImage, int size) - { - using (var snapshot = scannedImage.Preserve()) - { - return RenderThumbnail(snapshot, size); - } - } - - public async Task RenderThumbnail(ScannedImage.Snapshot snapshot, int size) - { - using (var bitmap = await scannedImageRenderer.Render(snapshot, snapshot.TransformList.Count == 0 ? 0 : size * OVERSAMPLE)) - { - return RenderThumbnail(bitmap, size); - } - } - - public Bitmap RenderThumbnail(Bitmap b) - { - return RenderThumbnail(b, UserConfig.Current.ThumbnailSize); - } - - /// - /// Gets a bitmap resized to fit within a thumbnail rectangle, including a border around the picture. - /// - /// The bitmap to resize. - /// The maximum width and height of the thumbnail. - /// The thumbnail bitmap. - public virtual Bitmap RenderThumbnail(Bitmap b, int size) - { - var result = new Bitmap(size, size); - using (Graphics g = Graphics.FromImage(result)) - { - // The location and dimensions of the old bitmap, scaled and positioned within the thumbnail bitmap - int left, top, width, height; - - // We want a nice thumbnail, so use the maximum quality interpolation - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - - if (b.Width > b.Height) - { - // Fill the new bitmap's width - width = size; - left = 0; - // Scale the drawing height to match the original bitmap's aspect ratio - height = (int)(b.Height * (size / (double)b.Width)); - // Center the drawing vertically - top = (size - height) / 2; - } - else - { - // Fill the new bitmap's height - height = size; - top = 0; - // Scale the drawing width to match the original bitmap's aspect ratio - width = (int)(b.Width * (size / (double)b.Height)); - // Center the drawing horizontally - left = (size - width) / 2; - } - - // Draw the original bitmap onto the new bitmap, using the calculated location and dimensions - // Note that there may be some padding if the aspect ratios don't match - var destRect = new RectangleF(left, top, width, height); - var srcRect = new RectangleF(0, 0, b.Width, b.Height); - g.DrawImage(b, destRect, srcRect, GraphicsUnit.Pixel); - // Draw a border around the orignal bitmap's content, inside the padding - g.DrawRectangle(Pens.Black, left, top, width - 1, height - 1); - } - - return result; - } + //public async Task RenderThumbnail(ScannedImage.Snapshot snapshot, int size) + //{ + // using (var bitmap = await scannedImageRenderer.Render(snapshot, snapshot.TransformList.Count == 0 ? 0 : size * OVERSAMPLE)) + // { + // return RenderThumbnail(bitmap, size); + // } + //} } } \ No newline at end of file diff --git a/NAPS2.Sdk/Scan/Images/Transforms/BlackWhiteTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/BlackWhiteTransform.cs index 7529fdc65..65c94e091 100644 --- a/NAPS2.Sdk/Scan/Images/Transforms/BlackWhiteTransform.cs +++ b/NAPS2.Sdk/Scan/Images/Transforms/BlackWhiteTransform.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.Linq; -using System.Runtime.InteropServices; namespace NAPS2.Scan.Images.Transforms { @@ -11,18 +8,5 @@ namespace NAPS2.Scan.Images.Transforms public class BlackWhiteTransform : Transform { public int Threshold { get; set; } - - public override Bitmap Perform(Bitmap bitmap) - { - if (bitmap.PixelFormat != PixelFormat.Format24bppRgb && bitmap.PixelFormat != PixelFormat.Format32bppArgb) - { - return bitmap; - } - - var monoBitmap = UnsafeImageOps.ConvertTo1Bpp(bitmap, Threshold); - bitmap.Dispose(); - - return monoBitmap; - } } } diff --git a/NAPS2.Sdk/Scan/Images/Transforms/BrightnessTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/BrightnessTransform.cs index b6c3a1a4c..26b158f1f 100644 --- a/NAPS2.Sdk/Scan/Images/Transforms/BrightnessTransform.cs +++ b/NAPS2.Sdk/Scan/Images/Transforms/BrightnessTransform.cs @@ -10,14 +10,6 @@ namespace NAPS2.Scan.Images.Transforms { public int Brightness { get; set; } - public override Bitmap Perform(Bitmap bitmap) - { - float brightnessAdjusted = Brightness / 1000f; - EnsurePixelFormat(ref bitmap); - UnsafeImageOps.ChangeBrightness(bitmap, brightnessAdjusted); - return bitmap; - } - public override bool IsNull => Brightness == 0; } } diff --git a/NAPS2.Sdk/Scan/Images/Transforms/ContrastTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/ContrastTransform.cs index ef8883c4f..42850abe5 100644 --- a/NAPS2.Sdk/Scan/Images/Transforms/ContrastTransform.cs +++ b/NAPS2.Sdk/Scan/Images/Transforms/ContrastTransform.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.Linq; namespace NAPS2.Scan.Images.Transforms @@ -11,32 +9,6 @@ namespace NAPS2.Scan.Images.Transforms { public int Contrast { get; set; } - public override Bitmap Perform(Bitmap bitmap) - { - float contrastAdjusted = Contrast / 1000f + 1.0f; - - EnsurePixelFormat(ref bitmap); - using (var g = Graphics.FromImage(bitmap)) - { - var attrs = new ImageAttributes(); - attrs.SetColorMatrix(new ColorMatrix - { - Matrix00 = contrastAdjusted, - Matrix11 = contrastAdjusted, - Matrix22 = contrastAdjusted - }); - g.DrawImage(bitmap, - new Rectangle(0, 0, bitmap.Width, bitmap.Height), - 0, - 0, - bitmap.Width, - bitmap.Height, - GraphicsUnit.Pixel, - attrs); - } - return bitmap; - } - public override bool IsNull => Contrast == 0; } } diff --git a/NAPS2.Sdk/Scan/Images/Transforms/CropTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/CropTransform.cs index 658444e45..7729ee865 100644 --- a/NAPS2.Sdk/Scan/Images/Transforms/CropTransform.cs +++ b/NAPS2.Sdk/Scan/Images/Transforms/CropTransform.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.Linq; -using NAPS2.Util; namespace NAPS2.Scan.Images.Transforms { @@ -18,25 +15,6 @@ namespace NAPS2.Scan.Images.Transforms public int? OriginalWidth { get; set; } public int? OriginalHeight { get; set; } - public override Bitmap Perform(Bitmap bitmap) - { - double xScale = bitmap.Width / (double)(OriginalWidth ?? bitmap.Width), - yScale = bitmap.Height / (double)(OriginalHeight ?? bitmap.Height); - - int width = Math.Max(bitmap.Width - (int)Math.Round((Left + Right) * xScale), 1); - int height = Math.Max(bitmap.Height - (int)Math.Round((Top + Bottom) * yScale), 1); - var result = new Bitmap(width, height, PixelFormat.Format24bppRgb); - result.SafeSetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution); - using (var g = Graphics.FromImage(result)) - { - g.Clear(Color.White); - g.DrawImage(bitmap, new Rectangle((int)Math.Round(-Left * xScale), (int)Math.Round(-Top * yScale), bitmap.Width, bitmap.Height)); - } - OptimizePixelFormat(bitmap, ref result); - bitmap.Dispose(); - return result; - } - public override bool CanSimplify(Transform other) => other is CropTransform other2 && OriginalHeight.HasValue && OriginalWidth.HasValue && other2.OriginalHeight.HasValue && other2.OriginalWidth.HasValue; diff --git a/NAPS2.Sdk/Scan/Images/Transforms/HueTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/HueTransform.cs index 5cddf0784..0624bde01 100644 --- a/NAPS2.Sdk/Scan/Images/Transforms/HueTransform.cs +++ b/NAPS2.Sdk/Scan/Images/Transforms/HueTransform.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.Linq; namespace NAPS2.Scan.Images.Transforms @@ -11,25 +9,6 @@ namespace NAPS2.Scan.Images.Transforms { public int HueShift { get; set; } - public override Bitmap Perform(Bitmap bitmap) - { - if (bitmap.PixelFormat != PixelFormat.Format24bppRgb && bitmap.PixelFormat != PixelFormat.Format32bppArgb) - { - // No need to handle 1bpp since hue shifts are null transforms - return bitmap; - } - - float hueShiftAdjusted = HueShift / 2000f * 360; - if (hueShiftAdjusted < 0) - { - hueShiftAdjusted += 360; - } - - UnsafeImageOps.HueShift(bitmap, hueShiftAdjusted); - - return bitmap; - } - public override bool CanSimplify(Transform other) => other is HueTransform; public override Transform Simplify(Transform other) diff --git a/NAPS2.Sdk/Scan/Images/Transforms/RotationTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/RotationTransform.cs index bf06ed435..f37fca6d7 100644 --- a/NAPS2.Sdk/Scan/Images/Transforms/RotationTransform.cs +++ b/NAPS2.Sdk/Scan/Images/Transforms/RotationTransform.cs @@ -2,14 +2,13 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; -using NAPS2.Util; namespace NAPS2.Scan.Images.Transforms { [Serializable] public class RotationTransform : Transform { - private const double TOLERANCE = 0.001; + public const double TOLERANCE = 0.001; public static double NormalizeAngle(double angle) { @@ -64,51 +63,6 @@ namespace NAPS2.Scan.Images.Transforms set => angle = NormalizeAngle(value); } - public override Bitmap Perform(Bitmap bitmap) - { - if (Math.Abs(Angle - 0.0) < TOLERANCE) - { - return bitmap; - } - if (Math.Abs(Angle - 90.0) < TOLERANCE) - { - bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone); - return bitmap; - } - if (Math.Abs(Angle - 180.0) < TOLERANCE) - { - bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone); - return bitmap; - } - if (Math.Abs(Angle - 270.0) < TOLERANCE) - { - bitmap.RotateFlip(RotateFlipType.Rotate270FlipNone); - return bitmap; - } - Bitmap result; - if (Angle > 45.0 && Angle < 135.0 || Angle > 225.0 && Angle < 315.0) - { - result = new Bitmap(bitmap.Height, bitmap.Width); - result.SafeSetResolution(bitmap.VerticalResolution, bitmap.HorizontalResolution); - } - else - { - result = new Bitmap(bitmap.Width, bitmap.Height); - result.SafeSetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution); - } - using (var g = Graphics.FromImage(result)) - { - g.Clear(Color.White); - g.TranslateTransform(result.Width / 2.0f, result.Height / 2.0f); - g.RotateTransform((float)Angle); - g.TranslateTransform(-bitmap.Width / 2.0f, -bitmap.Height / 2.0f); - g.DrawImage(bitmap, new Rectangle(0, 0, bitmap.Width, bitmap.Height)); - } - OptimizePixelFormat(bitmap, ref result); - bitmap.Dispose(); - return result; - } - public override bool CanSimplify(Transform other) => other is RotationTransform; public override Transform Simplify(Transform other) diff --git a/NAPS2.Sdk/Scan/Images/Transforms/SaturationTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/SaturationTransform.cs index d821dd1e3..4f6095ff8 100644 --- a/NAPS2.Sdk/Scan/Images/Transforms/SaturationTransform.cs +++ b/NAPS2.Sdk/Scan/Images/Transforms/SaturationTransform.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.Linq; -using System.Runtime.InteropServices; namespace NAPS2.Scan.Images.Transforms { @@ -12,52 +9,6 @@ namespace NAPS2.Scan.Images.Transforms { public int Saturation { get; set; } - public override Bitmap Perform(Bitmap bitmap) - { - double saturationAdjusted = Saturation / 1000.0 + 1; - - EnsurePixelFormat(ref bitmap); - int bytesPerPixel; - if (bitmap.PixelFormat == PixelFormat.Format24bppRgb) - { - bytesPerPixel = 3; - } - else if (bitmap.PixelFormat == PixelFormat.Format32bppArgb) - { - bytesPerPixel = 4; - } - else - { - return bitmap; - } - - var data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, bitmap.PixelFormat); - var stride = Math.Abs(data.Stride); - for (int y = 0; y < data.Height; y++) - { - for (int x = 0; x < data.Width; x++) - { - int r = Marshal.ReadByte(data.Scan0 + stride * y + x * bytesPerPixel); - int g = Marshal.ReadByte(data.Scan0 + stride * y + x * bytesPerPixel + 1); - int b = Marshal.ReadByte(data.Scan0 + stride * y + x * bytesPerPixel + 2); - - Color c = Color.FromArgb(255, r, g, b); - ColorHelper.ColorToHSL(c, out double h, out double s, out double v); - - s = Math.Min(s * saturationAdjusted, 1); - - c = ColorHelper.ColorFromHSL(h, s, v); - - Marshal.WriteByte(data.Scan0 + stride * y + x * bytesPerPixel, c.R); - Marshal.WriteByte(data.Scan0 + stride * y + x * bytesPerPixel + 1, c.G); - Marshal.WriteByte(data.Scan0 + stride * y + x * bytesPerPixel + 2, c.B); - } - } - bitmap.UnlockBits(data); - - return bitmap; - } - public override bool IsNull => Saturation == 0; } } diff --git a/NAPS2.Sdk/Scan/Images/Transforms/ScaleTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/ScaleTransform.cs new file mode 100644 index 000000000..cefc6f39e --- /dev/null +++ b/NAPS2.Sdk/Scan/Images/Transforms/ScaleTransform.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NAPS2.Scan.Images.Transforms +{ + [Serializable] + public class ScaleTransform : Transform + { + public double ScaleFactor { get; set; } + + public override bool IsNull => ScaleFactor == 1; + } +} diff --git a/NAPS2.Sdk/Scan/Images/Transforms/SharpenTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/SharpenTransform.cs index 707b59e65..16c492137 100644 --- a/NAPS2.Sdk/Scan/Images/Transforms/SharpenTransform.cs +++ b/NAPS2.Sdk/Scan/Images/Transforms/SharpenTransform.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.Linq; -using System.Runtime.InteropServices; namespace NAPS2.Scan.Images.Transforms { @@ -12,114 +9,6 @@ namespace NAPS2.Scan.Images.Transforms { public int Sharpness { get; set; } - public override Bitmap Perform(Bitmap bitmap) - { - double sharpnessAdjusted = Sharpness / 1000.0; - - EnsurePixelFormat(ref bitmap); - int bytesPerPixel; - if (bitmap.PixelFormat == PixelFormat.Format24bppRgb) - { - bytesPerPixel = 3; - } else if (bitmap.PixelFormat == PixelFormat.Format32bppArgb) - { - bytesPerPixel = 4; - } - else - { - return bitmap; - } - - // From https://stackoverflow.com/a/17596299 - - int width = bitmap.Width; - int height = bitmap.Height; - - // Create sharpening filter. - const int filterSize = 5; - - var filter = new double[,] - { - {-1, -1, -1, -1, -1}, - {-1, 2, 2, 2, -1}, - {-1, 2, 16, 2, -1}, - {-1, 2, 2, 2, -1}, - {-1, -1, -1, -1, -1} - }; - - double bias = 1.0 - sharpnessAdjusted; - double factor = sharpnessAdjusted / 16.0; - - const int s = filterSize / 2; - - var result = new Color[bitmap.Width, bitmap.Height]; - - // Lock image bits for read/write. - BitmapData pbits = bitmap.LockBits(new Rectangle(0, 0, width, height), - ImageLockMode.ReadWrite, - bitmap.PixelFormat); - - // Declare an array to hold the bytes of the bitmap. - int bytes = pbits.Stride * height; - var rgbValues = new byte[bytes]; - - // Copy the RGB values into the array. - Marshal.Copy(pbits.Scan0, rgbValues, 0, bytes); - - int rgb; - // Fill the color array with the new sharpened color values. - for (int x = s; x < width - s; x++) - { - for (int y = s; y < height - s; y++) - { - double red = 0.0, green = 0.0, blue = 0.0; - - for (int filterX = 0; filterX < filterSize; filterX++) - { - for (int filterY = 0; filterY < filterSize; filterY++) - { - int imageX = (x - s + filterX + width) % width; - int imageY = (y - s + filterY + height) % height; - - rgb = imageY * pbits.Stride + bytesPerPixel * imageX; - - red += rgbValues[rgb + 2] * filter[filterX, filterY]; - green += rgbValues[rgb + 1] * filter[filterX, filterY]; - blue += rgbValues[rgb + 0] * filter[filterX, filterY]; - } - - rgb = y * pbits.Stride + bytesPerPixel * x; - - int r = Math.Min(Math.Max((int)(factor * red + (bias * rgbValues[rgb + 2])), 0), 255); - int g = Math.Min(Math.Max((int)(factor * green + (bias * rgbValues[rgb + 1])), 0), 255); - int b = Math.Min(Math.Max((int)(factor * blue + (bias * rgbValues[rgb + 0])), 0), 255); - - result[x, y] = Color.FromArgb(r, g, b); - } - } - } - - // Update the image with the sharpened pixels. - for (int x = s; x < width - s; x++) - { - for (int y = s; y < height - s; y++) - { - rgb = y * pbits.Stride + bytesPerPixel * x; - - rgbValues[rgb + 2] = result[x, y].R; - rgbValues[rgb + 1] = result[x, y].G; - rgbValues[rgb + 0] = result[x, y].B; - } - } - - // Copy the RGB values back to the bitmap. - Marshal.Copy(rgbValues, 0, pbits.Scan0, bytes); - // Release image bits. - bitmap.UnlockBits(pbits); - - return bitmap; - } - public override bool IsNull => Sharpness == 0; } } diff --git a/NAPS2.Sdk/Scan/Images/Transforms/ThumbnailTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/ThumbnailTransform.cs new file mode 100644 index 000000000..918442a78 --- /dev/null +++ b/NAPS2.Sdk/Scan/Images/Transforms/ThumbnailTransform.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NAPS2.Config; + +namespace NAPS2.Scan.Images.Transforms +{ + [Serializable] + public class ThumbnailTransform : Transform + { + public int Size { get; set; } = UserConfig.Current.ThumbnailSize; + } +} diff --git a/NAPS2.Sdk/Scan/Images/Transforms/Transform.cs b/NAPS2.Sdk/Scan/Images/Transforms/Transform.cs index c8faf7152..8dcd91eab 100644 --- a/NAPS2.Sdk/Scan/Images/Transforms/Transform.cs +++ b/NAPS2.Sdk/Scan/Images/Transforms/Transform.cs @@ -21,11 +21,6 @@ namespace NAPS2.Scan.Images.Transforms [Serializable] public abstract class Transform { - public static Bitmap PerformAll(Bitmap bitmap, IEnumerable transforms) - { - return transforms.Aggregate(bitmap, (current, t) => t.Perform(current)); - } - public static bool AddOrSimplify(IList transformList, Transform transform) { if (transform.IsNull) @@ -51,51 +46,7 @@ namespace NAPS2.Scan.Images.Transforms } return true; } - - /// - /// If the provided bitmap is 1-bit (black and white), replace it with a 24-bit bitmap so that image transforms will work. If the bitmap is replaced, the original is disposed. - /// - /// The bitmap that may be replaced. - protected static void EnsurePixelFormat(ref Bitmap bitmap) - { - if (bitmap.PixelFormat == PixelFormat.Format1bppIndexed) - { - // Copy B&W over to grayscale - var bitmap2 = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format24bppRgb); - bitmap2.SafeSetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution); - using (var g = Graphics.FromImage(bitmap2)) - { - g.DrawImage(bitmap, 0, 0); - } - bitmap.Dispose(); - bitmap = bitmap2; - } - } - - /// - /// If the original bitmap is 1-bit (black and white), optimize the result by making it 1-bit too. - /// - /// The original bitmap that is used to determine whether the result should be black and white. - /// The result that may be replaced. - protected static void OptimizePixelFormat(Bitmap original, ref Bitmap result) - { - if (original.PixelFormat == PixelFormat.Format1bppIndexed) - { - var bitmap2 = (Bitmap)BitmapHelper.CopyToBpp(result, 1).Clone(); - result.Dispose(); - result = bitmap2; - } - } - - /// - /// Returns a bitmap with the result of the transform. - /// May be the same bitmap object if the transform can be performed in-place. - /// The original bitmap is disposed otherwise. - /// - /// The bitmap to transform. - /// - public abstract Bitmap Perform(Bitmap bitmap); - + /// /// Determines if this transform performed after another transform can be combined to form a single transform. /// diff --git a/NAPS2.Sdk/Scan/Images/Transforms/TrueContrastTransform.cs b/NAPS2.Sdk/Scan/Images/Transforms/TrueContrastTransform.cs index 047880930..f175175f7 100644 --- a/NAPS2.Sdk/Scan/Images/Transforms/TrueContrastTransform.cs +++ b/NAPS2.Sdk/Scan/Images/Transforms/TrueContrastTransform.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.Linq; namespace NAPS2.Scan.Images.Transforms @@ -10,18 +8,6 @@ namespace NAPS2.Scan.Images.Transforms { public int Contrast { get; set; } - public override Bitmap Perform(Bitmap bitmap) - { - // convert +/-1000 input range to a logarithmic scaled multiplier - float contrastAdjusted = (float) Math.Pow(2.718281f, Contrast / 500.0f); - // see http://docs.rainmeter.net/tips/colormatrix-guide/ for offset & matrix calculation - float offset = (1.0f - contrastAdjusted) / 2.0f; - - EnsurePixelFormat(ref bitmap); - UnsafeImageOps.ChangeContrast(bitmap, contrastAdjusted, offset); - return bitmap; - } - public override bool IsNull => Contrast == 0; } } diff --git a/NAPS2.Sdk/Scan/PatchCodeDetector.cs b/NAPS2.Sdk/Scan/PatchCodeDetector.cs index b57f4ee72..b8fb4773a 100644 --- a/NAPS2.Sdk/Scan/PatchCodeDetector.cs +++ b/NAPS2.Sdk/Scan/PatchCodeDetector.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; +using NAPS2.Scan.Images.Storage; using ZXing; namespace NAPS2.Scan @@ -12,10 +13,15 @@ namespace NAPS2.Scan /// public class PatchCodeDetector { - public static PatchCode Detect(Bitmap bitmap) + public static PatchCode Detect(IMemoryStorage bitmap) { + // TODO: Make more generic + if (!(bitmap is GdiStorage gdiStorage)) + { + throw new InvalidOperationException("Patch code detection only supported for GdiStorage"); + } IBarcodeReader reader = new BarcodeReader(); - var barcodeResult = reader.Decode(bitmap); + var barcodeResult = reader.Decode(gdiStorage.Bitmap); if (barcodeResult != null) { switch (barcodeResult.Text) diff --git a/NAPS2.Sdk/Scan/Sane/SaneScanDriver.cs b/NAPS2.Sdk/Scan/Sane/SaneScanDriver.cs index 5ce831acd..cddbaae4e 100644 --- a/NAPS2.Sdk/Scan/Sane/SaneScanDriver.cs +++ b/NAPS2.Sdk/Scan/Sane/SaneScanDriver.cs @@ -11,6 +11,7 @@ using NAPS2.Logging; using NAPS2.Platform; using NAPS2.Scan.Exceptions; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; using NAPS2.Scan.Images.Transforms; using NAPS2.Util; using NAPS2.WinForms; @@ -253,7 +254,7 @@ namespace NAPS2.Scan.Sane return (null, true); } using (stream) - using (var output = Image.FromStream(stream)) + using (var output = StorageManager.MemoryStorageFactory.Decode(stream, ".bmp")) using (var result = scannedImageHelper.PostProcessStep1(output, ScanProfile, false)) { if (blankDetector.ExcludePage(result, ScanProfile)) @@ -263,7 +264,7 @@ namespace NAPS2.Scan.Sane // By converting to 1bpp here we avoid the Win32 call in the BitmapHelper conversion // This converter also has the side effect of working even if the scanner doesn't support Lineart - using (var encoded = ScanProfile.BitDepth == ScanBitDepth.BlackWhite ? UnsafeImageOps.ConvertTo1Bpp(result, -ScanProfile.Brightness) : result) + using (var encoded = ScanProfile.BitDepth == ScanBitDepth.BlackWhite ? StorageManager.PerformTransform(result, new BlackWhiteTransform { Threshold = -ScanProfile.Brightness }) : result) { var image = new ScannedImage(encoded, ScanProfile.BitDepth, ScanProfile.MaxQuality, ScanProfile.Quality); scannedImageHelper.PostProcessStep2(image, result, ScanProfile, ScanParams, 1, false); diff --git a/NAPS2.Sdk/Scan/Stub/StubScanDriver.cs b/NAPS2.Sdk/Scan/Stub/StubScanDriver.cs index f93ca400e..c4f85b409 100644 --- a/NAPS2.Sdk/Scan/Stub/StubScanDriver.cs +++ b/NAPS2.Sdk/Scan/Stub/StubScanDriver.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; namespace NAPS2.Scan.Stub { @@ -74,7 +75,7 @@ namespace NAPS2.Scan.Stub g.FillRectangle(Brushes.LightGray, 0, 0, bitmap.Width, bitmap.Height); g.DrawString((_number++).ToString("G"), new Font("Times New Roman", 80), Brushes.Black, 0, 350); } - var image = new ScannedImage(bitmap, ScanBitDepth.C24Bit, ScanProfile.MaxQuality, ScanProfile.Quality); + var image = new ScannedImage(new GdiStorage(bitmap), ScanBitDepth.C24Bit, ScanProfile.MaxQuality, ScanProfile.Quality); return image; } diff --git a/NAPS2.Sdk/Scan/Twain/Legacy/TwainApi.cs b/NAPS2.Sdk/Scan/Twain/Legacy/TwainApi.cs index 7457cbe2c..c8210704a 100644 --- a/NAPS2.Sdk/Scan/Twain/Legacy/TwainApi.cs +++ b/NAPS2.Sdk/Scan/Twain/Legacy/TwainApi.cs @@ -7,6 +7,7 @@ using System.Windows.Forms; using NAPS2.Logging; using NAPS2.Scan.Exceptions; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; using NAPS2.Util; using NAPS2.WinForms; @@ -129,7 +130,7 @@ namespace NAPS2.Scan.Twain.Legacy using (Bitmap bmp = DibUtils.BitmapFromDib(img, out bitcount)) { - Bitmaps.Add(new ScannedImage(bmp, bitcount == 1 ? ScanBitDepth.BlackWhite : ScanBitDepth.C24Bit, settings.MaxQuality, settings.Quality)); + Bitmaps.Add(new ScannedImage(new GdiStorage(bmp), bitcount == 1 ? ScanBitDepth.BlackWhite : ScanBitDepth.C24Bit, settings.MaxQuality, settings.Quality)); } } form.Close(); diff --git a/NAPS2.Sdk/Scan/Twain/TwainWrapper.cs b/NAPS2.Sdk/Scan/Twain/TwainWrapper.cs index 15e7351e5..51155a412 100644 --- a/NAPS2.Sdk/Scan/Twain/TwainWrapper.cs +++ b/NAPS2.Sdk/Scan/Twain/TwainWrapper.cs @@ -12,6 +12,7 @@ using NAPS2.Logging; using NAPS2.Platform; using NAPS2.Scan.Exceptions; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; using NAPS2.WinForms; using NTwain; using NTwain.Data; @@ -147,7 +148,7 @@ namespace NAPS2.Scan.Twain pageNumber++; using (var output = twainImpl == TwainImpl.MemXfer ? GetBitmapFromMemXFer(eventArgs.MemoryData, eventArgs.ImageInfo) - : Image.FromStream(eventArgs.GetNativeImageStream())) + : StorageManager.MemoryStorageFactory.Decode(eventArgs.GetNativeImageStream(), ".bmp")) { using (var result = scannedImageHelper.PostProcessStep1(output, scanProfile)) { @@ -156,7 +157,7 @@ namespace NAPS2.Scan.Twain return; } - var bitDepth = output.PixelFormat == PixelFormat.Format1bppIndexed + var bitDepth = output.PixelFormat == StoragePixelFormat.BW1 ? ScanBitDepth.BlackWhite : ScanBitDepth.C24Bit; var image = new ScannedImage(result, bitDepth, scanProfile.MaxQuality, scanProfile.Quality); @@ -318,21 +319,21 @@ namespace NAPS2.Scan.Twain } } - private static Bitmap GetBitmapFromMemXFer(byte[] memoryData, TWImageInfo imageInfo) + private static IMemoryStorage GetBitmapFromMemXFer(byte[] memoryData, TWImageInfo imageInfo) { int bytesPerPixel = memoryData.Length / (imageInfo.ImageWidth * imageInfo.ImageLength); - PixelFormat pixelFormat = bytesPerPixel == 0 ? PixelFormat.Format1bppIndexed : PixelFormat.Format24bppRgb; + var pixelFormat = bytesPerPixel == 0 ? StoragePixelFormat.BW1: StoragePixelFormat.RGB24; int imageWidth = imageInfo.ImageWidth; int imageHeight = imageInfo.ImageLength; - var bitmap = new Bitmap(imageWidth, imageHeight, pixelFormat); - var data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, bitmap.PixelFormat); + var bitmap = StorageManager.MemoryStorageFactory.FromDimensions(imageWidth, imageHeight, pixelFormat); + var data = bitmap.Lock(out var scan0, out var stride); try { byte[] source = memoryData; if (bytesPerPixel == 1) { // No 8-bit greyscale format, so we have to transform into 24-bit - int rowWidth = data.Stride; + int rowWidth = stride; int originalRowWidth = source.Length / imageHeight; byte[] source2 = new byte[rowWidth * imageHeight]; for (int row = 0; row < imageHeight; row++) @@ -349,7 +350,7 @@ namespace NAPS2.Scan.Twain else if (bytesPerPixel == 3) { // Colors are provided as BGR, they need to be swapped to RGB - int rowWidth = data.Stride; + int rowWidth = stride; for (int row = 0; row < imageHeight; row++) { for (int col = 0; col < imageWidth; col++) @@ -359,11 +360,11 @@ namespace NAPS2.Scan.Twain } } } - Marshal.Copy(source, 0, data.Scan0, source.Length); + Marshal.Copy(source, 0, scan0, source.Length); } finally { - bitmap.UnlockBits(data); + bitmap.Unlock(data); } return bitmap; } diff --git a/NAPS2.Sdk/Scan/Wia/WiaScanOperation.cs b/NAPS2.Sdk/Scan/Wia/WiaScanOperation.cs index 262818d92..1f73c92aa 100644 --- a/NAPS2.Sdk/Scan/Wia/WiaScanOperation.cs +++ b/NAPS2.Sdk/Scan/Wia/WiaScanOperation.cs @@ -10,6 +10,7 @@ using NAPS2.Logging; using NAPS2.Operation; using NAPS2.Scan.Exceptions; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; using NAPS2.Scan.Wia.Native; using NAPS2.Util; using NAPS2.Worker; @@ -137,7 +138,7 @@ namespace NAPS2.Scan.Wia } } - private void ProduceImage(ScannedImageSource.Concrete source, Image output, ref int pageNumber) + private void ProduceImage(ScannedImageSource.Concrete source, IMemoryStorage output, ref int pageNumber) { using (var result = scannedImageHelper.PostProcessStep1(output, ScanProfile)) { @@ -179,13 +180,13 @@ namespace NAPS2.Scan.Wia foreach (var path in paths) { using (var stream = new FileStream(path, FileMode.Open)) - using (var output = Image.FromStream(stream)) { - int frameCount = output.GetFrameCount(FrameDimension.Page); - for (int i = 0; i < frameCount; i++) + foreach (var storage in StorageManager.MemoryStorageFactory.DecodeMultiple(stream, Path.GetExtension(path), out _)) { - output.SelectActiveFrame(FrameDimension.Page, i); - ProduceImage(source, output, ref pageNumber); + using (storage) + { + ProduceImage(source, storage, ref pageNumber); + } } } } @@ -228,9 +229,9 @@ namespace NAPS2.Scan.Wia try { using (args.Stream) - using (var output = Image.FromStream(args.Stream)) + using (var storage = StorageManager.MemoryStorageFactory.Decode(args.Stream, ".bmp")) { - ProduceImage(source, output, ref pageNumber); + ProduceImage(source, storage, ref pageNumber); } } catch (Exception e) diff --git a/NAPS2.Sdk/WinForms/FDesktop.cs b/NAPS2.Sdk/WinForms/FDesktop.cs index 65b7ad7ea..f04055a05 100644 --- a/NAPS2.Sdk/WinForms/FDesktop.cs +++ b/NAPS2.Sdk/WinForms/FDesktop.cs @@ -26,6 +26,7 @@ using NAPS2.Recovery; using NAPS2.Scan; using NAPS2.Scan.Exceptions; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; using NAPS2.Scan.Wia; using NAPS2.Scan.Wia.Native; using NAPS2.Update; @@ -1701,7 +1702,7 @@ namespace NAPS2.WinForms } if (includeBitmap) { - using (var firstBitmap = await scannedImageRenderer.Render(imageList[0])) + using (var firstBitmap = ((GdiStorage) await scannedImageRenderer.Render(imageList[0])).Bitmap) { ido.SetData(DataFormats.Bitmap, true, new Bitmap(firstBitmap)); ido.SetData(DataFormats.Rtf, true, await RtfEncodeImages(firstBitmap, imageList)); @@ -1721,7 +1722,7 @@ namespace NAPS2.WinForms } foreach (var img in images.Skip(1)) { - using (var bitmap = await scannedImageRenderer.Render(img)) + using (var bitmap = ((GdiStorage)await scannedImageRenderer.Render(img)).Bitmap) { if (!AppendRtfEncodedImage(bitmap, img.FileFormat, sb, true)) { @@ -1863,7 +1864,7 @@ namespace NAPS2.WinForms { continue; } - + next.SetThumbnail(thumb, snapshot.TransformState); } fallback.Reset(); diff --git a/NAPS2.Sdk/WinForms/FViewer.cs b/NAPS2.Sdk/WinForms/FViewer.cs index 03ca04b42..2c12e38aa 100644 --- a/NAPS2.Sdk/WinForms/FViewer.cs +++ b/NAPS2.Sdk/WinForms/FViewer.cs @@ -11,6 +11,7 @@ using NAPS2.Lang.Resources; using NAPS2.Operation; using NAPS2.Platform; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; using NAPS2.Util; namespace NAPS2.WinForms @@ -109,7 +110,7 @@ namespace NAPS2.WinForms tiffViewer1.Image?.Dispose(); tiffViewer1.Image = null; var newImage = await scannedImageRenderer.Render(ImageList.Images[ImageIndex]); - tiffViewer1.Image = newImage; + tiffViewer1.Image = ((GdiStorage)newImage).Bitmap; } protected override void Dispose(bool disposing) diff --git a/NAPS2.Sdk/WinForms/ImageForm.cs b/NAPS2.Sdk/WinForms/ImageForm.cs index 01c898a1f..65b69bf1f 100644 --- a/NAPS2.Sdk/WinForms/ImageForm.cs +++ b/NAPS2.Sdk/WinForms/ImageForm.cs @@ -4,6 +4,7 @@ using System.Drawing; using System.Linq; using System.Windows.Forms; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; using NAPS2.Scan.Images.Transforms; using NAPS2.Util; using Timer = System.Threading.Timer; @@ -54,7 +55,8 @@ namespace NAPS2.WinForms { if (!transform.IsNull) { - result = transform.Perform(result); + // TODO: Maybe the working images etc. should be storage + result = ((GdiStorage)StorageManager.PerformTransform(new GdiStorage(result), transform)).Bitmap; } } return result; @@ -90,7 +92,7 @@ namespace NAPS2.WinForms Size = new Size(600, 600); var maxDimen = Screen.AllScreens.Max(s => Math.Max(s.WorkingArea.Height, s.WorkingArea.Width)); - workingImage = await scannedImageRenderer.Render(Image, maxDimen * 2); + workingImage = ((GdiStorage)await scannedImageRenderer.Render(Image, maxDimen * 2)).Bitmap; if (closed) { workingImage?.Dispose(); diff --git a/NAPS2.Sdk/WinForms/ThumbnailList.cs b/NAPS2.Sdk/WinForms/ThumbnailList.cs index 14c5c2232..6eb01840b 100644 --- a/NAPS2.Sdk/WinForms/ThumbnailList.cs +++ b/NAPS2.Sdk/WinForms/ThumbnailList.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Windows.Forms; using NAPS2.Platform; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; namespace NAPS2.WinForms { @@ -195,7 +196,7 @@ namespace NAPS2.WinForms { lock (this) { - var thumb = img.GetThumbnail(); + var thumb = ((GdiStorage)img.GetThumbnail()).Bitmap; if (thumb == null) { return RenderPlaceholder(); diff --git a/NAPS2.Sdk/Worker/WorkerCallback.cs b/NAPS2.Sdk/Worker/WorkerCallback.cs index 7f24c6bc8..1b70099c6 100644 --- a/NAPS2.Sdk/Worker/WorkerCallback.cs +++ b/NAPS2.Sdk/Worker/WorkerCallback.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using NAPS2.Recovery; using NAPS2.Scan.Images; +using NAPS2.Scan.Images.Storage; namespace NAPS2.Worker { @@ -17,7 +18,7 @@ namespace NAPS2.Worker var scannedImage = new ScannedImage(image); if (thumbnail != null) { - scannedImage.SetThumbnail(new Bitmap(new MemoryStream(thumbnail))); + scannedImage.SetThumbnail(StorageManager.MemoryStorageFactory.Decode(new MemoryStream(thumbnail), ".bmp")); } ImageCallback?.Invoke(scannedImage, tempImageFilePath); }