From b8a5be0aaa47ca40423b8e61c977d7a9375430c0 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sat, 15 Jul 2017 22:27:19 +0100 Subject: [PATCH] Cloneing Image Processor --- ...DefaultInternalImageProcessorApplicator.cs | 16 +- .../Image/ICloningImageProcessor.cs | 37 ++++ src/ImageSharp/Image/ImageBase{TPixel}.cs | 21 ++ src/ImageSharp/Image/ImageFrame{TPixel}.cs | 23 ++- src/ImageSharp/MetaData/ImageFrameMetaData.cs | 9 + src/ImageSharp/MetaData/ImageMetaData.cs | 9 + .../Processors/CLoneingImageProcessor.cs | 166 ++++++++++++++++ .../Transforms/ResamplingWeightedProcessor.cs | 8 +- .../Processors/Transforms/ResizeProcessor.cs | 180 ++++++++++-------- 9 files changed, 376 insertions(+), 93 deletions(-) create mode 100644 src/ImageSharp/Image/ICloningImageProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/CLoneingImageProcessor.cs diff --git a/src/ImageSharp/DefaultInternalImageProcessorApplicator.cs b/src/ImageSharp/DefaultInternalImageProcessorApplicator.cs index 4dda4f070f..ba476ba335 100644 --- a/src/ImageSharp/DefaultInternalImageProcessorApplicator.cs +++ b/src/ImageSharp/DefaultInternalImageProcessorApplicator.cs @@ -54,9 +54,19 @@ namespace ImageSharp { if (!this.mutate && this.destination == null) { - // TODO check if the processor implements a special interface and if it does then allow it to take - // over and crereate the clone on behalf ImageOperations class. - this.destination = this.source.Clone(); + // this will only work if the first processor applied is the cloning one thus + // realistically for this optermissation to work the resize must the first processor + // applied any only up processors will take the douple data path. + if (processor is ICloningImageProcessor) + { + var cloningProcessor = (ICloningImageProcessor)processor; + this.destination = cloningProcessor.CloneAndApply(this.source, rectangle); + return this; + } + else + { + this.destination = this.source.Clone(); + } } processor.Apply(this.destination, rectangle); diff --git a/src/ImageSharp/Image/ICloningImageProcessor.cs b/src/ImageSharp/Image/ICloningImageProcessor.cs new file mode 100644 index 0000000000..8906063f9c --- /dev/null +++ b/src/ImageSharp/Image/ICloningImageProcessor.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Processing +{ + using System; + using System.Threading.Tasks; + + using ImageSharp.PixelFormats; + using SixLabors.Primitives; + + /// + /// Encapsulates methods to alter the pixels of an image. + /// + /// The pixel format. + public interface ICloningImageProcessor : IImageProcessor + where TPixel : struct, IPixel + { + /// + /// Applies the process to the specified portion of the specified . + /// + /// The source image. Cannot be null. + /// + /// The structure that specifies the portion of the image object to draw. + /// + /// + /// is null. + /// + /// + /// doesnt fit the dimension of the image. + /// + /// Returns the cloned image after thre processor has been applied to it. + Image CloneAndApply(Image source, Rectangle sourceRectangle); + } +} diff --git a/src/ImageSharp/Image/ImageBase{TPixel}.cs b/src/ImageSharp/Image/ImageBase{TPixel}.cs index 95d5375185..7d965cb050 100644 --- a/src/ImageSharp/Image/ImageBase{TPixel}.cs +++ b/src/ImageSharp/Image/ImageBase{TPixel}.cs @@ -244,6 +244,27 @@ namespace ImageSharp this.PixelBuffer = newPixels; } + /// + /// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer. + /// + /// The pixel source. + internal void SwapPixelsData(ImageBase pixelSource) + { + Guard.NotNull(pixelSource, nameof(pixelSource)); + + int newWidth = pixelSource.Width; + int newHeight = pixelSource.Height; + TPixel[] newPixels = pixelSource.PixelBuffer; + + pixelSource.PixelBuffer = this.PixelBuffer; + pixelSource.Width = this.Width; + pixelSource.Height = this.Height; + + this.Width = newWidth; + this.Height = newHeight; + this.PixelBuffer = newPixels; + } + /// /// Clones the image /// diff --git a/src/ImageSharp/Image/ImageFrame{TPixel}.cs b/src/ImageSharp/Image/ImageFrame{TPixel}.cs index c8cad16482..1e55d7b3b3 100644 --- a/src/ImageSharp/Image/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/Image/ImageFrame{TPixel}.cs @@ -28,6 +28,23 @@ namespace ImageSharp public ImageFrame(Configuration configuration, int width, int height) : base(configuration, width, height) { + this.MetaData = new ImageFrameMetaData(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The configuration providing initialization code which allows extending the library. + /// + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The metadata of the frame. + public ImageFrame(Configuration configuration, int width, int height, ImageFrameMetaData metadata) + : base(configuration, width, height) + { + Guard.NotNull(metadata, nameof(metadata)); + this.MetaData = metadata; } /// @@ -38,6 +55,7 @@ namespace ImageSharp public ImageFrame(int width, int height) : this(null, width, height) { + this.MetaData = new ImageFrameMetaData(); } /// @@ -47,6 +65,7 @@ namespace ImageSharp internal ImageFrame(ImageBase image) : base(image) { + this.MetaData = new ImageFrameMetaData(); } /// @@ -62,12 +81,12 @@ namespace ImageSharp /// /// Gets the meta data of the frame. /// - public ImageFrameMetaData MetaData { get; private set; } = new ImageFrameMetaData(); + public ImageFrameMetaData MetaData { get; private set; } /// public override string ToString() { - return $"ImageFrame: {this.Width}x{this.Height}"; + return $"ImageFrame<{typeof(TPixel).Name}>: {this.Width}x{this.Height}"; } /// diff --git a/src/ImageSharp/MetaData/ImageFrameMetaData.cs b/src/ImageSharp/MetaData/ImageFrameMetaData.cs index b55bfd1ad1..9004fbd1df 100644 --- a/src/ImageSharp/MetaData/ImageFrameMetaData.cs +++ b/src/ImageSharp/MetaData/ImageFrameMetaData.cs @@ -39,5 +39,14 @@ namespace ImageSharp /// public DisposalMethod DisposalMethod { get; set; } + + /// + /// Clones this ImageFrameMetaData. + /// + /// The cloned instance. + public ImageFrameMetaData Clone() + { + return new ImageFrameMetaData(this); + } } } diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index 5361b486d4..a5b36f884d 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -132,6 +132,15 @@ namespace ImageSharp /// public ushort RepeatCount { get; set; } + /// + /// Clones this into a new instance + /// + /// The cloned metadata instance + public ImageMetaData Clone() + { + return new ImageMetaData(this); + } + /// /// Synchronizes the profiles with the current meta data. /// diff --git a/src/ImageSharp/Processing/Processors/CLoneingImageProcessor.cs b/src/ImageSharp/Processing/Processors/CLoneingImageProcessor.cs new file mode 100644 index 0000000000..0b6dbd3ed2 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/CLoneingImageProcessor.cs @@ -0,0 +1,166 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Processing +{ + using System; + using System.Threading.Tasks; + + using ImageSharp.PixelFormats; + using SixLabors.Primitives; + + /// + /// Allows the application of processors to images. + /// + /// The pixel format. + internal abstract class CloneingImageProcessor : IImageProcessor, ICloningImageProcessor + where TPixel : struct, IPixel + { + /// + public virtual ParallelOptions ParallelOptions { get; set; } + + /// + public virtual bool Compand { get; set; } = false; + + /// + public Image CloneAndApply(Image source, Rectangle sourceRectangle) + { + if (this.ParallelOptions == null) + { + this.ParallelOptions = source.Configuration.ParallelOptions; + } + + try + { + Image clone = this.CreateDestination(source, sourceRectangle); + + if (clone.Frames.Count != source.Frames.Count) + { + throw new ImageProcessingException($"An error occured when processing the image using {this.GetType().Name}. The processor changed the number of frames."); + } + + this.BeforeImageApply(source, clone, sourceRectangle); + + this.BeforeApply(source, clone, sourceRectangle); + this.OnApply(source, clone, sourceRectangle); + this.AfterApply(source, clone, sourceRectangle); + + for (int i = 0; i < source.Frames.Count; i++) + { + ImageFrame sourceFrame = source.Frames[i]; + ImageFrame clonedFrame = clone.Frames[i]; + + this.BeforeApply(sourceFrame, clonedFrame, sourceRectangle); + + this.OnApply(sourceFrame, clonedFrame, sourceRectangle); + this.AfterApply(sourceFrame, clonedFrame, sourceRectangle); + } + + this.AfterImageApply(source, clone, sourceRectangle); + + return clone; + } +#if DEBUG + catch (Exception) + { + throw; +#else + catch (Exception ex) + { + throw new ImageProcessingException($"An error occured when processing the image using {this.GetType().Name}. See the inner exception for more detail.", ex); +#endif + } + } + + /// + public void Apply(Image source, Rectangle sourceRectangle) + { + using (Image cloned = this.CloneAndApply(source, sourceRectangle)) + { + // we now need to move the pixel data/size data from one image base to another + if (cloned.Frames.Count != source.Frames.Count) + { + throw new ImageProcessingException($"An error occured when processing the image using {this.GetType().Name}. The processor changed the number of frames."); + } + + source.SwapPixelsData(cloned); + for (int i = 0; i < source.Frames.Count; i++) + { + source.Frames[i].SwapPixelsData(cloned.Frames[i]); + } + } + } + + /// + /// Generates the clone of the source image that operatinos should be applied to. + /// + /// The source image. Cannot be null. + /// The source rectangle. + /// The cloned image. + protected virtual Image CreateDestination(Image source, Rectangle sourceRectangle) + { + return source.Clone(); + } + + /// + /// This method is called before the process is applied to prepare the processor. + /// + /// The source image. Cannot be null. + /// The cloned/destination image. Cannot be null. + /// + /// The structure that specifies the portion of the image object to draw. + /// + protected virtual void BeforeImageApply(Image source, Image destination, Rectangle sourceRectangle) + { + } + + /// + /// This method is called before the process is applied to prepare the processor. + /// + /// The source image. Cannot be null. + /// The cloned/destination image. Cannot be null. + /// + /// The structure that specifies the portion of the image object to draw. + /// + protected virtual void BeforeApply(ImageBase source, ImageBase destination, Rectangle sourceRectangle) + { + } + + /// + /// Applies the process to the specified portion of the specified at the specified location + /// and with the specified size. + /// + /// The source image. Cannot be null. + /// The cloned/destination image. Cannot be null. + /// + /// The structure that specifies the portion of the image object to draw. + /// + protected abstract void OnApply(ImageBase source, ImageBase destination, Rectangle sourceRectangle); + + /// + /// This method is called after the process is applied to prepare the processor. + /// + /// The source image. Cannot be null. + /// The cloned/destination image. Cannot be null. + /// + /// The structure that specifies the portion of the image object to draw. + /// + protected virtual void AfterApply(ImageBase source, ImageBase destination, Rectangle sourceRectangle) + { + } + + /// + /// This method is called after the process is applied to prepare the processor. + /// + /// The source image. Cannot be null. + /// The cloned/destination image. Cannot be null. + /// + /// The structure that specifies the portion of the image object to draw. + /// + protected virtual void AfterImageApply(Image source, Image destination, Rectangle sourceRectangle) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs index 0f1e166fdc..957f917be5 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs @@ -16,7 +16,7 @@ namespace ImageSharp.Processing.Processors /// Adapted from /// /// The pixel format. - internal abstract partial class ResamplingWeightedProcessor : ImageProcessor + internal abstract partial class ResamplingWeightedProcessor : CloneingImageProcessor where TPixel : struct, IPixel { /// @@ -140,7 +140,7 @@ namespace ImageSharp.Processing.Processors } /// - protected override void BeforeApply(ImageBase source, Rectangle sourceRectangle) + protected override void BeforeApply(ImageBase source, ImageBase destination, Rectangle sourceRectangle) { if (!(this.Sampler is NearestNeighborResampler)) { @@ -155,9 +155,9 @@ namespace ImageSharp.Processing.Processors } /// - protected override void AfterApply(ImageBase source, Rectangle sourceRectangle) + protected override void AfterApply(ImageBase source, ImageBase destination, Rectangle sourceRectangle) { - base.AfterApply(source, sourceRectangle); + base.AfterApply(source, destination, sourceRectangle); this.HorizontalWeights?.Dispose(); this.HorizontalWeights = null; this.VerticalWeights?.Dispose(); diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index be1680cf1c..65fbb65fb0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs @@ -46,11 +46,33 @@ namespace ImageSharp.Processing.Processors } /// - protected override unsafe void OnApply(ImageBase source, Rectangle sourceRectangle) + protected override Image CreateDestination(Image source, Rectangle sourceRectangle) + { + // we will always be creating the clone even for mutate because thatsa the way this base processor works + // ------------ + // for resize we know we are going to populate every pixel with fresh data and we want a different target size so + // lets manually clone an empty set of images at the correct target and then have the base class processs them in. + // turn. + var image = new Image(source.Configuration, this.Width, this.Height, source.MetaData.Clone()); + + // now 'clone' the ImageFrames + foreach (ImageFrame sourceFrame in source.Frames) + { + var targetFrame = new ImageFrame(sourceFrame.Configuration, this.Width, this.Height, sourceFrame.MetaData.Clone()); + image.Frames.Add(targetFrame); + } + + return image; + } + + /// + protected override unsafe void OnApply(ImageBase source, ImageBase cloned, Rectangle sourceRectangle) { // Jump out, we'll deal with that later. - if (source.Width == this.Width && source.Height == this.Height && sourceRectangle == this.ResizeRectangle) + if (source.Width == cloned.Width && source.Height == cloned.Height && sourceRectangle == this.ResizeRectangle) { + // the cloned will be blank here copy all the pixel data over + source.Pixels.CopyTo(cloned.Pixels); return; } @@ -74,29 +96,24 @@ namespace ImageSharp.Processing.Processors float widthFactor = sourceRectangle.Width / (float)this.ResizeRectangle.Width; float heightFactor = sourceRectangle.Height / (float)this.ResizeRectangle.Height; - using (var targetPixels = new PixelAccessor(width, height)) - { - Parallel.For( - minY, - maxY, - this.ParallelOptions, - y => + Parallel.For( + minY, + maxY, + this.ParallelOptions, + y => + { + // Y coordinates of source points + Span sourceRow = source.GetRowSpan((int)(((y - startY) * heightFactor) + sourceY)); + Span targetRow = cloned.GetRowSpan(y); + + for (int x = minX; x < maxX; x++) { - // Y coordinates of source points - Span sourceRow = source.GetRowSpan((int)(((y - startY) * heightFactor) + sourceY)); - Span targetRow = targetPixels.GetRowSpan(y); + // X coordinates of source points + targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)]; + } + }); - for (int x = minX; x < maxX; x++) - { - // X coordinates of source points - targetRow[x] = sourceRow[(int)(((x - startX) * widthFactor) + sourceX)]; - } - }); - - // Break out now. - source.SwapPixelsBuffers(targetPixels); - return; - } + return; } // Interpolate the image using the calculated weights. @@ -105,82 +122,77 @@ namespace ImageSharp.Processing.Processors // are the upper and lower bounds of the source rectangle. // TODO: Using a transposed variant of 'firstPassPixels' could eliminate the need for the WeightsWindow.ComputeWeightedColumnSum() method, and improve speed! - using (var targetPixels = new PixelAccessor(width, height)) + using (var firstPassPixels = new Buffer2D(width, source.Height)) { - using (var firstPassPixels = new Buffer2D(width, source.Height)) - { - firstPassPixels.Clear(); - - Parallel.For( - 0, - sourceRectangle.Bottom, - this.ParallelOptions, - y => + firstPassPixels.Clear(); + + Parallel.For( + 0, + sourceRectangle.Bottom, + this.ParallelOptions, + y => + { + // TODO: Without Parallel.For() this buffer object could be reused: + using (var tempRowBuffer = new Buffer(source.Width)) { - // TODO: Without Parallel.For() this buffer object could be reused: - using (var tempRowBuffer = new Buffer(source.Width)) - { - Span firstPassRow = firstPassPixels.GetRowSpan(y); - Span sourceRow = source.GetRowSpan(y); - PixelOperations.Instance.ToVector4(sourceRow, tempRowBuffer, sourceRow.Length); + Span firstPassRow = firstPassPixels.GetRowSpan(y); + Span sourceRow = source.GetRowSpan(y); + PixelOperations.Instance.ToVector4(sourceRow, tempRowBuffer, sourceRow.Length); - if (this.Compand) + if (this.Compand) + { + for (int x = minX; x < maxX; x++) { - for (int x = minX; x < maxX; x++) - { - WeightsWindow window = this.HorizontalWeights.Weights[x - startX]; - firstPassRow[x] = window.ComputeExpandedWeightedRowSum(tempRowBuffer, sourceX); - } + WeightsWindow window = this.HorizontalWeights.Weights[x - startX]; + firstPassRow[x] = window.ComputeExpandedWeightedRowSum(tempRowBuffer, sourceX); } - else + } + else + { + for (int x = minX; x < maxX; x++) { - for (int x = minX; x < maxX; x++) - { - WeightsWindow window = this.HorizontalWeights.Weights[x - startX]; - firstPassRow[x] = window.ComputeWeightedRowSum(tempRowBuffer, sourceX); - } + WeightsWindow window = this.HorizontalWeights.Weights[x - startX]; + firstPassRow[x] = window.ComputeWeightedRowSum(tempRowBuffer, sourceX); } } - }); - - // Now process the rows. - Parallel.For( - minY, - maxY, - this.ParallelOptions, - y => - { - // Ensure offsets are normalised for cropping and padding. - WeightsWindow window = this.VerticalWeights.Weights[y - startY]; - Span targetRow = targetPixels.GetRowSpan(y); + } + }); - if (this.Compand) + // Now process the rows. + Parallel.For( + minY, + maxY, + this.ParallelOptions, + y => + { + // Ensure offsets are normalised for cropping and padding. + WeightsWindow window = this.VerticalWeights.Weights[y - startY]; + Span targetRow = cloned.GetRowSpan(y); + + if (this.Compand) + { + for (int x = 0; x < width; x++) { - for (int x = 0; x < width; x++) - { - // Destination color components - Vector4 destination = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY); - destination = destination.Compress(); + // Destination color components + Vector4 destination = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY); + destination = destination.Compress(); - ref TPixel pixel = ref targetRow[x]; - pixel.PackFromVector4(destination); - } + ref TPixel pixel = ref targetRow[x]; + pixel.PackFromVector4(destination); } - else + } + else + { + for (int x = 0; x < width; x++) { - for (int x = 0; x < width; x++) - { - // Destination color components - Vector4 destination = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY); + // Destination color components + Vector4 destination = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY); - ref TPixel pixel = ref targetRow[x]; - pixel.PackFromVector4(destination); - } + ref TPixel pixel = ref targetRow[x]; + pixel.PackFromVector4(destination); } - }); - } - - source.SwapPixelsBuffers(targetPixels); + } + }); } } }