Remove render extensions in favor of using ImageContext

This should be okay in general and allows us to inject an IPdfRenderer for rendering PDF-based images.
This commit is contained in:
Ben Olden-Cooligan 2022-06-14 22:16:35 -07:00
parent b9d1ebe499
commit cbcfd2de82
19 changed files with 85 additions and 62 deletions

View File

@ -5,33 +5,6 @@ namespace NAPS2.Images.Gdi;
public static class GdiExtensions
{
public static IMemoryImage RenderToImage(this ProcessedImage processedImage)
{
return new GdiImage(processedImage.RenderToBitmap());
}
public static Bitmap RenderToBitmap(this ProcessedImage processedImage)
{
// TODO: Need to take transforms into account
switch (processedImage.Storage)
{
// TODO: We probably want to support PDFs somehow (which presumably use fileStorage?)
case ImageFileStorage fileStorage:
// Rather than creating a bitmap from the file directly, instead we read it into memory first.
// This ensures we don't accidentally keep a lock on the storage file, which would cause an error if we
// try to delete it before the bitmap is disposed.
// This is less efficient in the case where the bitmap is guaranteed to be disposed quickly, but for now
// that seems like a reasonable tradeoff to avoid a whole class of hard-to-diagnose errors.
var stream = new MemoryStream(File.ReadAllBytes(fileStorage.FullPath));
return new Bitmap(stream);
case MemoryStreamImageStorage memoryStreamStorage:
return new Bitmap(memoryStreamStorage.Stream);
case GdiImage image:
return image.Clone().AsBitmap();
}
throw new ArgumentException("Unsupported image storage: " + processedImage.Storage);
}
public static Bitmap AsBitmap(this IMemoryImage image)
{
var gdiImage = image as GdiImage ?? throw new ArgumentException("Expected a GdiImage", nameof(image));

View File

@ -58,7 +58,32 @@ public class GdiImageContext : ImageContext
}
}
public override IMemoryImage Render(ProcessedImage processedImage) => processedImage.RenderToImage();
public override IMemoryImage Render(ProcessedImage processedImage)
{
return new GdiImage(RenderToBitmap(processedImage));
}
public Bitmap RenderToBitmap(ProcessedImage processedImage)
{
// TODO: Need to take transforms into account
switch (processedImage.Storage)
{
// TODO: We probably want to support PDFs somehow (which presumably use fileStorage?)
case ImageFileStorage fileStorage:
// Rather than creating a bitmap from the file directly, instead we read it into memory first.
// This ensures we don't accidentally keep a lock on the storage file, which would cause an error if we
// try to delete it before the bitmap is disposed.
// This is less efficient in the case where the bitmap is guaranteed to be disposed quickly, but for now
// that seems like a reasonable tradeoff to avoid a whole class of hard-to-diagnose errors.
var stream = new MemoryStream(File.ReadAllBytes(fileStorage.FullPath));
return new Bitmap(stream);
case MemoryStreamImageStorage memoryStreamStorage:
return new Bitmap(memoryStreamStorage.Stream);
case GdiImage image:
return image.Clone().AsBitmap();
}
throw new ArgumentException("Unsupported image storage: " + processedImage.Storage);
}
public override IMemoryImage Create(int width, int height, ImagePixelFormat pixelFormat)
{

View File

@ -5,6 +5,9 @@ namespace NAPS2.Images.Storage;
public abstract class ImageContext
{
// TODO: Ideally this class should be fully stateless. Maybe a protected virtual method to provide transformers.
// TODO: Using lazy static state if needed.
// TODO: Although we may need an IPdfRenderer...
private readonly Dictionary<(Type, Type), (object, MethodInfo)> _transformers = new();
protected ImageContext(Type imageType)

View File

@ -9,7 +9,8 @@ public class FileStorageSample
{
public static async Task Run()
{
ScanningContext scanningContext = new ScanningContext(new GdiImageContext());
GdiImageContext imageContext = new GdiImageContext();
ScanningContext scanningContext = new ScanningContext(imageContext);
// To save memory, we can store scanned images on disk after initial processing.
// This will put files in the system temp folder by default, which can be
@ -30,20 +31,20 @@ public class FileStorageSample
// excessive amount of memory, since it is all stored on disk until rendered.
// This is just for illustration purposes; in real code you usually want to
// process images as they come rather than waiting for the full scan.
List<ProcessedImage> renderableImages = await controller.Scan(options).ToList();
List<ProcessedImage> processedImages = await controller.Scan(options).ToList();
try
{
foreach (var renderableImage in renderableImages)
foreach (var processedImage in processedImages)
{
// This seamlessly loads the image data from disk.
using Bitmap bitmap = renderableImage.RenderToBitmap();
using Bitmap bitmap = imageContext.RenderToBitmap(processedImage);
// TODO: Do something with the bitmap
}
}
finally
{
foreach (var scannedImage in renderableImages)
foreach (var scannedImage in processedImages)
{
// This cleanly deletes any data from the filesystem.
scannedImage.Dispose();

View File

@ -11,7 +11,8 @@ public class ScanToBitmapSample
{
// We configure scanned images to be stored in GDI+ format, which uses
// System.Drawing.Bitmap internally.
ScanningContext scanningContext = new ScanningContext(new GdiImageContext());
GdiImageContext imageContext = new GdiImageContext();
ScanningContext scanningContext = new ScanningContext(imageContext);
// To select a device and scan, you need a controller.
ScanController controller = new ScanController(scanningContext);
@ -40,11 +41,11 @@ public class ScanToBitmapSample
// ScannedImageSource has several different methods to help you consume images.
// ForEach allows you to asynchronously process images as they arrive.
await imageSource.ForEach(async renderableImage =>
await imageSource.ForEach(async processedImage =>
{
// Make sure ScannedImage and rendered images are disposed after use
using (renderableImage)
using (Bitmap bitmap = renderableImage.RenderToBitmap())
using (processedImage)
using (Bitmap bitmap = imageContext.RenderToBitmap(processedImage))
{
// TODO: Do something with the bitmap
}

View File

@ -81,7 +81,7 @@ public class ImageImporterTests : ContextualTexts
Assert.Equal(BitDepth.Color, result[0].Metadata.BitDepth);
ImageAsserts.Similar(
new GdiImage(ImageImporterTestsData.color_image),
result[0].RenderToImage(),
ImageContext.Render(result[0]),
ImageAsserts.GENERAL_RMSE_THRESHOLD);
AssertUsesRecoveryStorage(result[2].Storage, "00003.jpg");
@ -89,7 +89,7 @@ public class ImageImporterTests : ContextualTexts
Assert.Equal(BitDepth.Color, result[2].Metadata.BitDepth);
ImageAsserts.Similar(
new GdiImage(ImageImporterTestsData.stock_cat),
result[2].RenderToImage(),
ImageContext.Render(result[2]),
ImageAsserts.GENERAL_RMSE_THRESHOLD);
result[0].Dispose();

View File

@ -28,7 +28,7 @@ public class DeskewOperation : OperationBase
return await Pipeline.For(images, CancelToken).RunParallel(async img =>
{
using var processedImage = img.GetClonedImage();
var image = processedImage.RenderToImage();
var image = _imageContext.Render(processedImage);
try
{
CancelToken.ThrowIfCancellationRequested();

View File

@ -15,7 +15,7 @@ public class ThumbnailRenderer
public IMemoryImage Render(ProcessedImage processedImage, int outputSize)
{
var image = processedImage.RenderToImage();
var image = _imageContext.Render(processedImage);
var transformList = processedImage.TransformState.Transforms;
if (!processedImage.TransformState.IsEmpty)
{

View File

@ -17,8 +17,10 @@ public class AutoSaver
private readonly PdfExporter _pdfExporter;
private readonly OverwritePrompt _overwritePrompt;
private readonly ScopedConfig _config;
private readonly TiffHelper _tiffHelper;
private readonly ImageContext _imageContext;
public AutoSaver(IConfigProvider<PdfSettings> pdfSettingsProvider, IConfigProvider<ImageSettings> imageSettingsProvider, OcrEngineManager ocrEngineManager, OcrRequestQueue ocrRequestQueue, ErrorOutput errorOutput, DialogHelper dialogHelper, OperationProgress operationProgress, ISaveNotify notify, PdfExporter pdfExporter, OverwritePrompt overwritePrompt, ScopedConfig config)
public AutoSaver(IConfigProvider<PdfSettings> pdfSettingsProvider, IConfigProvider<ImageSettings> imageSettingsProvider, OcrEngineManager ocrEngineManager, OcrRequestQueue ocrRequestQueue, ErrorOutput errorOutput, DialogHelper dialogHelper, OperationProgress operationProgress, ISaveNotify notify, PdfExporter pdfExporter, OverwritePrompt overwritePrompt, ScopedConfig config, TiffHelper tiffHelper, ImageContext imageContext)
{
_pdfSettingsProvider = pdfSettingsProvider;
_imageSettingsProvider = imageSettingsProvider;
@ -29,6 +31,8 @@ public class AutoSaver
_pdfExporter = pdfExporter;
_overwritePrompt = overwritePrompt;
_config = config;
_tiffHelper = tiffHelper;
_imageContext = imageContext;
}
public ScannedImageSource Save(AutoSaveSettings settings, ScannedImageSource source)
@ -161,7 +165,7 @@ public class AutoSaver
}
else
{
var op = new SaveImagesOperation(_overwritePrompt, new TiffHelper());
var op = new SaveImagesOperation(_imageContext, _overwritePrompt, _tiffHelper);
if (op.Start(subPath, placeholders, images, _imageSettingsProvider))
{
_operationProgress.ShowProgress(op);

View File

@ -37,10 +37,12 @@ public class DirectImportOperation : OperationBase
// TODO: Don't bother, here, in recovery, etc.
if (img.PostProcessingData.Thumbnail == null && importParams.ThumbnailSize.HasValue)
{
var renderedImage = _scanningContext.ImageContext.Render(img);
var thumbnail = _scanningContext.ImageContext.PerformTransform(renderedImage,
new ThumbnailTransform(importParams.ThumbnailSize.Value));
img = img.WithPostProcessingData(img.PostProcessingData with
{
Thumbnail = _scanningContext.ImageContext.PerformTransform(img.RenderToImage(),
new ThumbnailTransform(importParams.ThumbnailSize.Value))
Thumbnail = thumbnail
}, true);
}
imageCallback(img);

View File

@ -9,11 +9,13 @@ namespace NAPS2.ImportExport.Images;
// TODO: Avoid GDI dependency
public class SaveImagesOperation : OperationBase
{
private readonly ImageContext _imageContext;
private readonly OverwritePrompt _overwritePrompt;
private readonly TiffHelper _tiffHelper;
public SaveImagesOperation(OverwritePrompt overwritePrompt, TiffHelper tiffHelper)
public SaveImagesOperation(ImageContext imageContext, OverwritePrompt overwritePrompt, TiffHelper tiffHelper)
{
_imageContext = imageContext;
_overwritePrompt = overwritePrompt;
_tiffHelper = tiffHelper;
@ -155,12 +157,12 @@ public class SaveImagesOperation : OperationBase
var encoderParams = new EncoderParameters(1);
encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, quality);
// TODO: Something more generic
using Bitmap bitmap = image.RenderToBitmap();
using Bitmap bitmap = ((GdiImageContext)_imageContext).RenderToBitmap(image);
bitmap.Save(path, encoder, encoderParams);
}
else
{
using Bitmap bitmap = image.RenderToBitmap();
using Bitmap bitmap = ((GdiImageContext)_imageContext).RenderToBitmap(image);;
bitmap.Save(path, format);
}
}

View File

@ -8,6 +8,13 @@ namespace NAPS2.ImportExport.Images;
public class TiffHelper
{
private readonly ImageContext _imageContext;
public TiffHelper(ImageContext imageContext)
{
_imageContext = imageContext;
}
public async Task<bool> SaveMultipage(IList<ProcessedImage> images, string location, TiffCompression compression, ProgressHandler progressCallback, CancellationToken cancelToken)
{
try
@ -27,7 +34,7 @@ public class TiffHelper
var iparams = new EncoderParameters(1);
Encoder iparam = Encoder.Compression;
// TODO: More generic (?)
using var bitmap = images[0].RenderToBitmap();
using var bitmap = ((GdiImageContext)_imageContext).RenderToBitmap(images[0]);
ValidateBitmap(bitmap);
var iparamPara = new EncoderParameter(iparam, (long)GetEncoderValue(compression, bitmap));
iparams.Param[0] = iparamPara;
@ -40,7 +47,7 @@ public class TiffHelper
var compressionEncoder = Encoder.Compression;
File.Delete(location);
using var bitmap0 = images[0].RenderToBitmap();
using var bitmap0 = ((GdiImageContext)_imageContext).RenderToBitmap(images[0]);
ValidateBitmap(bitmap0);
encoderParams.Param[0] = new EncoderParameter(compressionEncoder, (long)GetEncoderValue(compression, bitmap0));
encoderParams.Param[1] = new EncoderParameter(saveEncoder, (long)EncoderValue.MultiFrame);
@ -56,7 +63,7 @@ public class TiffHelper
return false;
}
using var bitmap = images[i].RenderToBitmap();
using var bitmap = ((GdiImageContext)_imageContext).RenderToBitmap(images[i]);
ValidateBitmap(bitmap);
encoderParams.Param[0] = new EncoderParameter(compressionEncoder, (long)GetEncoderValue(compression, bitmap));
encoderParams.Param[1] = new EncoderParameter(saveEncoder, (long)EncoderValue.FrameDimensionPage);

View File

@ -129,7 +129,7 @@ public class PdfSharpExporter : PdfExporter
{
// TODO: Dedup from other method
var format = image.Metadata.Lossless ? ImageFileFormat.Png : ImageFileFormat.Jpeg;
using var renderedImage = image.RenderToImage();
using var renderedImage = _scanningContext.ImageContext.Render(image);
using Stream stream = renderedImage.SaveToMemoryStream(format);
using var img = XImage.FromStream(stream);
if (cancelToken.IsCancellationRequested)
@ -185,7 +185,7 @@ public class PdfSharpExporter : PdfExporter
string tempImageFilePath = Path.Combine(Paths.Temp, Path.GetRandomFileName());
var format = image.Metadata.Lossless ? ImageFileFormat.Png : ImageFileFormat.Jpeg;
using (var renderedImage = image.RenderToImage())
using (var renderedImage = _scanningContext.ImageContext.Render(image))
using (var stream = renderedImage.SaveToMemoryStream(format))
using (var pdfImage = XImage.FromStream(stream))
{

View File

@ -196,7 +196,7 @@ public class PdfSharpImporter : IPdfImporter
TransformState.Empty);
return _importPostProcessor.AddPostProcessingData(
image,
image.RenderToImage(),
_imageContext.Render(image),
importParams.ThumbnailSize,
importParams.BarcodeDetectionOptions,
true);

View File

@ -72,7 +72,7 @@ public class PrintDocumentPrinter : IScannedImagePrinter
int i = 0;
printDocument.PrintPage += (sender, e) =>
{
var image = imagesToPrint[i].RenderToImage();
var image = _imageContext.Render(imagesToPrint[i]);
try
{
var pb = e.PageBounds;

View File

@ -164,7 +164,7 @@ public class RecoveryManager
}
processedImage = _importPostProcessor.AddPostProcessingData(processedImage,
processedImage.RenderToImage(),
_imageContext.Render(processedImage),
recoveryParams.ThumbnailSize,
new BarcodeDetectionOptions(),
true);

View File

@ -99,7 +99,8 @@ namespace NAPS2.WinForms
_tiffViewer1.Image?.Dispose();
_tiffViewer1.Image = null;
using var imageToRender = CurrentImage.GetClonedImage();
_tiffViewer1.Image = imageToRender.RenderToBitmap();
// TODO: Should we avoid this cast somehow? Inject the GDI context directly? (Ctrl+Shift+F similar too)
_tiffViewer1.Image = ((GdiImageContext)_imageContext).RenderToBitmap(imageToRender);
}
protected override void Dispose(bool disposing)

View File

@ -10,15 +10,17 @@ namespace NAPS2.WinForms;
public class ImageClipboard
{
private readonly ImageContext _imageContext;
private readonly ImageTransfer _imageTransfer;
public ImageClipboard(ImageContext imageContext)
: this(imageContext, new ImageTransfer(imageContext))
{
_imageTransfer = new ImageTransfer(imageContext);
}
public ImageClipboard(ImageTransfer imageTransfer)
public ImageClipboard(ImageContext imageContext, ImageTransfer imageTransfer)
{
_imageContext = imageContext;
_imageTransfer = imageTransfer;
}
@ -36,7 +38,8 @@ public class ImageClipboard
// Slow path for more full-featured copying
if (includeBitmap)
{
using var firstBitmap = imageList[0].RenderToBitmap();
// TODO: More generic?
using var firstBitmap = ((GdiImageContext)_imageContext).RenderToBitmap(imageList[0]);
Clipboard.Instance.Image = firstBitmap.ToEto();
Clipboard.Instance.SetString(await RtfEncodeImages(firstBitmap, imageList), "Rich Text Format");
}
@ -53,7 +56,7 @@ public class ImageClipboard
}
foreach (var img in images.Skip(1))
{
using var bitmap = img.RenderToBitmap();
using var bitmap = ((GdiImageContext)_imageContext).RenderToBitmap(img);
// TODO: Is this the right format?
if (!AppendRtfEncodedImage(bitmap, bitmap.RawFormat, sb, true))
{

View File

@ -86,7 +86,8 @@ namespace NAPS2.WinForms
var maxDimen = Screen.AllScreens.Max(s => Math.Max(s.WorkingArea.Height, s.WorkingArea.Width));
// TODO: Limit to maxDimen * 2
using var imageToRender = Image.GetClonedImage();
workingImage = imageToRender.RenderToBitmap();
// TODO: More generic or avoid the cast somehow? In general how do we integrate with eto?
workingImage = ((GdiImageContext)_imageContext).RenderToBitmap(imageToRender);
if (_closed)
{
workingImage?.Dispose();