Storage wip (not compiling yet)

This commit is contained in:
Ben Olden-Cooligan 2018-11-29 01:54:18 -05:00
parent 744bc6a5fd
commit 6e30ec6cc9
61 changed files with 1175 additions and 770 deletions

View File

@ -20,7 +20,6 @@ namespace NAPS2.DI.Modules
Bind<IOverwritePrompt>().To<ConsoleOverwritePrompt>();
Bind<IOperationProgress>().To<ConsoleOperationProgress>();
Bind<IComponentInstallPrompt>().To<ConsoleComponentInstallPrompt>();
Bind<ThumbnailRenderer>().To<NullThumbnailRenderer>();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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++;

View File

@ -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<IMemoryStorage> 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);
}
}
}

View File

@ -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);
}

View File

@ -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));

View File

@ -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<ScannedImage> 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;
}

View File

@ -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<GdiStorage>(image).Bitmap, rect);
}
finally
{

View File

@ -191,6 +191,11 @@
<Compile Include="Platform\DefaultRuntimeCompat.cs" />
<Compile Include="Scan\Exceptions\DriverNotSupportedException.cs" />
<Compile Include="Scan\Images\ScannedImageSource.cs" />
<Compile Include="Scan\Images\Storage\GdiTransformer.cs" />
<Compile Include="Scan\Images\Storage\IFileStorage.cs" />
<Compile Include="Scan\Images\Storage\IImageMetadata.cs" />
<Compile Include="Scan\Images\Storage\IImageMetadataFactory.cs" />
<Compile Include="Scan\Images\Storage\ITransformer.cs" />
<Compile Include="Scan\Images\Storage\PdfFileStorage.cs" />
<Compile Include="Scan\Images\Storage\FileStorageManager.cs" />
<Compile Include="Scan\Images\Storage\GdiFileConverter.cs" />
@ -201,10 +206,15 @@
<Compile Include="Scan\Images\Storage\IStorage.cs" />
<Compile Include="Scan\Images\Storage\IStorageConverter.cs" />
<Compile Include="Scan\Images\Storage\IMemoryStorageFactory.cs" />
<Compile Include="Scan\Images\Storage\RecoverableImageMetadata.cs" />
<Compile Include="Scan\Images\Storage\RecoveryStorageManager.cs" />
<Compile Include="Scan\Images\Storage\StorageConvertParams.cs" />
<Compile Include="Scan\Images\Storage\StorageManager.cs" />
<Compile Include="Scan\Images\Storage\StoragePixelFormat.cs" />
<Compile Include="Scan\Images\Storage\StubImageMetadata.cs" />
<Compile Include="Scan\Images\Storage\StubImageMetadataFactory.cs" />
<Compile Include="Scan\Images\Transforms\ThumbnailTransform.cs" />
<Compile Include="Scan\Images\Transforms\ScaleTransform.cs" />
<Compile Include="Scan\KeyValueScanOptions.cs" />
<Compile Include="Scan\Exceptions\SaneNotAvailableException.cs" />
<Compile Include="Scan\Sane\SaneOption.cs" />
@ -345,7 +355,6 @@
<Compile Include="Scan\Batch\BatchSettings.cs" />
<Compile Include="Scan\Exceptions\NoDuplexSupportException.cs" />
<Compile Include="Scan\Images\IBlankDetector.cs" />
<Compile Include="Scan\Images\NullThumbnailRenderer.cs" />
<Compile Include="Scan\Images\ThresholdBlankDetector.cs" />
<Compile Include="Scan\Images\Transforms\BlackWhiteTransform.cs" />
<Compile Include="Scan\Images\Transforms\ColorHelper.cs" />
@ -470,7 +479,6 @@
<Compile Include="Scan\Images\ScannedImageHelper.cs" />
<Compile Include="Scan\Images\ScannedImageList.cs" />
<Compile Include="Scan\Images\ThumbnailRenderer.cs" />
<Compile Include="Scan\Images\ImageScaleHelper.cs" />
<Compile Include="Scan\IScanDriver.cs" />
<Compile Include="Scan\IScanPerformer.cs" />
<Compile Include="Scan\LocalizedDescriptionAttribute.cs" />

View File

@ -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);
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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<Transform> 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<Transform>();
// 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<Transform>();
// 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<Transform>();
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<Transform>();
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;
}
}

View File

@ -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<FileStorage>(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()));
}
}
}

View File

@ -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));
}
}
}

View File

@ -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<Bitmap> Render(ScannedImage image, int outputSize = 0)
public async Task<IMemoryStorage> Render(ScannedImage image, int outputSize = 0)
{
using (var snapshot = image.Preserve())
{
@ -28,38 +29,20 @@ namespace NAPS2.Scan.Images
}
}
public async Task<Bitmap> Render(ScannedImage.Snapshot snapshot, int outputSize = 0)
public async Task<IMemoryStorage> 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<Stream> RenderToStream(ScannedImage image)
{
using (var snapshot = image.Preserve())

View File

@ -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)
{

View File

@ -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)
{
}
}
}

View File

@ -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<GdiStorage, FileStorage>, IStorageConverter<FileStorage, GdiStorage>
public class GdiFileConverter :
IStorageConverter<GdiStorage, FileStorage>,
IStorageConverter<FileStorage, GdiStorage>
{
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));

View File

@ -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());
}
}
}

View File

@ -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<IMemoryStorage> DecodeMultiple(Stream stream, string ext, out int count)
{
var bitmap = new Bitmap(stream);
count = bitmap.GetFrameCount(FrameDimension.Page);
return EnumerateFrames(bitmap, count);
}
public IEnumerable<IMemoryStorage> DecodeMultiple(string path, out int count)
{
var bitmap = new Bitmap(path);
count = bitmap.GetFrameCount(FrameDimension.Page);
return EnumerateFrames(bitmap, count);
}
private IEnumerable<IMemoryStorage> 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)
{

View File

@ -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<GdiStorage, BrightnessTransform>,
ITransformer<GdiStorage, ContrastTransform>,
ITransformer<GdiStorage, TrueContrastTransform>,
ITransformer<GdiStorage, HueTransform>,
ITransformer<GdiStorage, SaturationTransform>,
ITransformer<GdiStorage, SharpenTransform>,
ITransformer<GdiStorage, RotationTransform>,
ITransformer<GdiStorage, CropTransform>,
ITransformer<GdiStorage, BlackWhiteTransform>,
ITransformer<GdiStorage, ThumbnailTransform>,
ITransformer<GdiStorage, ScaleTransform>
{
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);
}
/// <summary>
/// Gets a bitmap resized to fit within a thumbnail rectangle, including a border around the picture.
/// </summary>
/// <param name="storage">The bitmap to resize.</param>
/// <param name="transform">The maximum width and height of the thumbnail.</param>
/// <returns>The thumbnail bitmap.</returns>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="bitmap">The bitmap that may be replaced.</param>
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;
}
}
/// <summary>
/// If the original bitmap is 1-bit (black and white), optimize the result by making it 1-bit too.
/// </summary>
/// <param name="original">The original bitmap that is used to determine whether the result should be black and white.</param>
/// <param name="result">The result that may be replaced.</param>
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;
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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<Transform> 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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -7,8 +7,40 @@ namespace NAPS2.Scan.Images.Storage
{
public interface IMemoryStorageFactory
{
IStorage FromBmpStream(Stream stream);
/// <summary>
/// Decodes an image from the given stream and file extension.
/// </summary>
/// <param name="stream">The image data, in a common format (JPEG, PNG, etc).</param>
/// <param name="ext">A file extension hinting at the image format. When possible, the contents of the stream should be used to definitively determine the image format.</param>
/// <returns></returns>
IMemoryStorage Decode(Stream stream, string ext);
IStorage FromDimensions(int width, int height, StoragePixelFormat pixelFormat);
/// <summary>
/// Decodes an image from the given file path.
/// </summary>
/// <param name="path">The image path.</param>
/// <returns></returns>
IMemoryStorage Decode(string path);
/// <summary>
/// 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.
/// </summary>
/// <param name="stream">The image data, in a common format (JPEG, PNG, etc).</param>
/// <param name="ext">A file extension hinting at the image format. When possible, the contents of the stream should be used to definitively determine the image format.</param>
/// <param name="count">The number of returned images.</param>
/// <returns></returns>
IEnumerable<IMemoryStorage> DecodeMultiple(Stream stream, string ext, out int count);
/// <summary>
/// Decodes an image from the given file path.
/// </summary>
/// <param name="path">The image path.</param>
/// <param name="count">The number of returned images.</param>
/// <returns></returns>
IEnumerable<IMemoryStorage> DecodeMultiple(string path, out int count);
IMemoryStorage FromDimensions(int width, int height, StoragePixelFormat pixelFormat);
}
}

View File

@ -4,7 +4,7 @@ using System.Linq;
namespace NAPS2.Scan.Images.Storage
{
public interface IStorageConverter<in TStorage1, out TStorage2>
public interface IStorageConverter<in TStorage1, out TStorage2> where TStorage1 : IStorage where TStorage2 : IStorage
{
TStorage2 Convert(TStorage1 input, StorageConvertParams convertParams);
}

View File

@ -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<TStorage, in TTransform> where TStorage : IMemoryStorage where TTransform : Transform
{
TStorage PerformTransform(TStorage storage, TTransform transform);
}
}

View File

@ -5,7 +5,7 @@ using System.Linq;
namespace NAPS2.Scan.Images.Storage
{
public class PdfFileStorage : IStorage
public class PdfFileStorage : IFileStorage
{
public PdfFileStorage(string fullPath)
{

View File

@ -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<Transform> 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?
}
}
}

View File

@ -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<RecoveryIndex> indexConfigManager;
public RecoveryStorageManager(string recoveryFolderPath)
{
indexConfigManager = new ConfigManager<RecoveryIndex>("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<RecoveryIndex>("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)
});
}
}
}

View File

@ -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; }
}
}

View File

@ -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<Transform> 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<TStorage1, TStorage2>(IStorageConverter<TStorage1, TStorage2> converter)
public static void RegisterConverter<TStorage1, TStorage2>(IStorageConverter<TStorage1, TStorage2> converter) where TStorage1 : IStorage where TStorage2 : IStorage
{
Converters.Add((typeof(TStorage1), typeof(TStorage2)), (converter, typeof(IStorageConverter<TStorage1, TStorage2>).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<TStorage>(IStorage storage)
{
return (TStorage)Convert(storage, typeof(TStorage), new StorageConvertParams());
}
public static TStorage Convert<TStorage>(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<GdiStorage, FileStorage>(gdiFileConverter);
RegisterConverter<FileStorage, GdiStorage>(gdiFileConverter);
var gdiTransformer = new GdiTransformer();
RegisterTransformer(gdiTransformer);
}
}
}

View File

@ -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<Transform> 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()
{
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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<Bitmap> RenderThumbnail(ScannedImage scannedImage, int size)
//{
// using (var snapshot = scannedImage.Preserve())
// {
// return RenderThumbnail(snapshot, size);
// }
//}
public Task<Bitmap> RenderThumbnail(ScannedImage scannedImage)
{
return RenderThumbnail(scannedImage, UserConfig.Current.ThumbnailSize);
}
public Task<Bitmap> RenderThumbnail(ScannedImage scannedImage, int size)
{
using (var snapshot = scannedImage.Preserve())
{
return RenderThumbnail(snapshot, size);
}
}
public async Task<Bitmap> 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);
}
/// <summary>
/// Gets a bitmap resized to fit within a thumbnail rectangle, including a border around the picture.
/// </summary>
/// <param name="b">The bitmap to resize.</param>
/// <param name="size">The maximum width and height of the thumbnail.</param>
/// <returns>The thumbnail bitmap.</returns>
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<Bitmap> RenderThumbnail(ScannedImage.Snapshot snapshot, int size)
//{
// using (var bitmap = await scannedImageRenderer.Render(snapshot, snapshot.TransformList.Count == 0 ? 0 : size * OVERSAMPLE))
// {
// return RenderThumbnail(bitmap, size);
// }
//}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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)

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -21,11 +21,6 @@ namespace NAPS2.Scan.Images.Transforms
[Serializable]
public abstract class Transform
{
public static Bitmap PerformAll(Bitmap bitmap, IEnumerable<Transform> transforms)
{
return transforms.Aggregate(bitmap, (current, t) => t.Perform(current));
}
public static bool AddOrSimplify(IList<Transform> transformList, Transform transform)
{
if (transform.IsNull)
@ -51,51 +46,7 @@ namespace NAPS2.Scan.Images.Transforms
}
return true;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="bitmap">The bitmap that may be replaced.</param>
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;
}
}
/// <summary>
/// If the original bitmap is 1-bit (black and white), optimize the result by making it 1-bit too.
/// </summary>
/// <param name="original">The original bitmap that is used to determine whether the result should be black and white.</param>
/// <param name="result">The result that may be replaced.</param>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="bitmap">The bitmap to transform.</param>
/// <returns></returns>
public abstract Bitmap Perform(Bitmap bitmap);
/// <summary>
/// Determines if this transform performed after another transform can be combined to form a single transform.
/// </summary>

View File

@ -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;
}
}

View File

@ -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
/// </summary>
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)

View File

@ -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);

View File

@ -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;
}

View File

@ -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();

View File

@ -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;
}

View File

@ -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)

View File

@ -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();

View File

@ -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)

View File

@ -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();

View File

@ -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();

View File

@ -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);
}