mirror of
https://github.com/cyanfish/naps2.git
synced 2024-11-13 06:27:11 +03:00
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:
parent
b9d1ebe499
commit
cbcfd2de82
@ -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));
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -196,7 +196,7 @@ public class PdfSharpImporter : IPdfImporter
|
||||
TransformState.Empty);
|
||||
return _importPostProcessor.AddPostProcessingData(
|
||||
image,
|
||||
image.RenderToImage(),
|
||||
_imageContext.Render(image),
|
||||
importParams.ThumbnailSize,
|
||||
importParams.BarcodeDetectionOptions,
|
||||
true);
|
||||
|
@ -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;
|
||||
|
@ -164,7 +164,7 @@ public class RecoveryManager
|
||||
}
|
||||
|
||||
processedImage = _importPostProcessor.AddPostProcessingData(processedImage,
|
||||
processedImage.RenderToImage(),
|
||||
_imageContext.Render(processedImage),
|
||||
recoveryParams.ThumbnailSize,
|
||||
new BarcodeDetectionOptions(),
|
||||
true);
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user