From fdc681a263ece34e718ff1c1fde603f86c4f09bb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Nov 2015 18:16:23 +1100 Subject: [PATCH] Add premultiplied alpha Former-commit-id: 346fe4d5a27014ff153447113eee4406c9b9d07e Former-commit-id: fd5c29c65e88e8ba292879950f70ea072848753a Former-commit-id: 8c2da3d64065b5884117016885a26c5913b932de --- README.md | 2 +- src/ImageProcessor/Colors/Color.cs | 32 +++++----- src/ImageProcessor/Filters/BackgroundColor.cs | 61 +++++++++++++++++++ .../Filters/ImageFilterExtensions.cs | 12 ++++ .../Formats/Gif/GifDecoderCore.cs | 2 + .../Formats/Gif/Quantizer/Quantizer.cs | 4 +- src/ImageProcessor/Formats/Jpg/JpegEncoder.cs | 14 ++++- .../Formats/Png/GrayscaleReader.cs | 16 +++-- .../Formats/Png/PaletteIndexReader.cs | 24 ++++++-- src/ImageProcessor/Formats/Png/PngEncoder.cs | 51 ++++++++++------ .../Formats/Png/TrueColorReader.cs | 18 ++++-- .../Processors/Filters/FilterTests.cs | 6 +- .../Processors/ProcessorTestBase.cs | 1 + .../Processors/Samplers/SamplerTests.cs | 1 + .../Formats/Png/blur.png.REMOVED.git-id | 1 + 15 files changed, 187 insertions(+), 58 deletions(-) create mode 100644 src/ImageProcessor/Filters/BackgroundColor.cs create mode 100644 tests/ImageProcessor.Tests/TestImages/Formats/Png/blur.png.REMOVED.git-id diff --git a/README.md b/README.md index bada7219f..528015329 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **This branch contains the new cross platform version of ImageProcessor**. -This is a complete rewrite from the ground up to allow the processing of images without the use of `System.Drawing` using a portable class library (PCL). It's still in early stages but progress has been pretty quick. +This is a complete rewrite from the ground up to allow the processing of images without the use of `System.Drawing` using a cross-platform class library. It's still in early stages but progress has been pretty quick. ###Why am I writing this? diff --git a/src/ImageProcessor/Colors/Color.cs b/src/ImageProcessor/Colors/Color.cs index 799ae7e3f..4f6ca7eff 100644 --- a/src/ImageProcessor/Colors/Color.cs +++ b/src/ImageProcessor/Colors/Color.cs @@ -10,7 +10,8 @@ namespace ImageProcessor using System.Numerics; /// - /// Represents a four-component color using red, green, blue, and alpha data. + /// Represents a four-component color using red, green, blue, and alpha data. + /// Each component is stored in premultiplied format multiplied by the alpha component. /// /// /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, @@ -470,7 +471,8 @@ namespace ImageProcessor { amount = amount.Clamp(0f, 1f); - return (from * (1 - amount)) + (to * amount); + //return (from * (1 - amount)) + (to * amount); + return (from * (1 - amount)) + to ; } /// @@ -478,7 +480,7 @@ namespace ImageProcessor /// /// /// - /// The whos signal to compress. + /// The whose signal to compress. /// The . public static Color Compand(Color linear) { @@ -495,7 +497,7 @@ namespace ImageProcessor /// /// /// - /// The whos signal to expand. + /// The whose signal to expand. /// The . public static Color InverseCompand(Color gamma) { @@ -511,33 +513,29 @@ namespace ImageProcessor /// Converts a non-premultipled alpha to a /// that contains premultiplied alpha. /// - /// The red component of this . - /// The green component of this . - /// The blue component of this . - /// The alpha component of this . + /// The to convert. /// The . - public static Color FromNonPremultiplied(float r, float g, float b, float a) + public static Color FromNonPremultiplied(Color color) { - return new Color(r * a, g * a, b * a, a); + float a = color.A; + return new Color(color.R * a, color.G * a, color.B * a, a); } /// /// Converts a premultipled alpha to a /// that contains non-premultiplied alpha. /// - /// The red component of this . - /// The green component of this . - /// The blue component of this . - /// The alpha component of this . + /// The to convert. /// The . - public static Color ToNonPremultiplied(float r, float g, float b, float a) + public static Color ToNonPremultiplied(Color color) { + float a = color.A; if (Math.Abs(a) < Epsilon) { - return new Color(r, g, b, a); + return new Color(color.R, color.G, color.B, a); } - return new Color(r / a, g / a, b / a, a); + return new Color(color.R / a, color.G / a, color.B / a, a); } /// diff --git a/src/ImageProcessor/Filters/BackgroundColor.cs b/src/ImageProcessor/Filters/BackgroundColor.cs new file mode 100644 index 000000000..0f844f7a5 --- /dev/null +++ b/src/ImageProcessor/Filters/BackgroundColor.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor.Filters +{ + using System.Threading.Tasks; + + /// + /// Sets the background color of the image. + /// + public class BackgroundColor : ParallelImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// The to set the background color to. + public BackgroundColor(Color color) + { + this.Value = Color.FromNonPremultiplied(color); + } + + /// + /// Gets the background color value. + /// + public Color Value { get; } + + /// + protected override void Apply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle, int startY, int endY) + { + int sourceY = sourceRectangle.Y; + int sourceBottom = sourceRectangle.Bottom; + int startX = sourceRectangle.X; + int endX = sourceRectangle.Right; + Color backgroundColor = this.Value; + + Parallel.For( + startY, + endY, + y => + { + if (y >= sourceY && y < sourceBottom) + { + for (int x = startX; x < endX; x++) + { + Color color = source[x, y]; + + // TODO: Fix this nonesense. + if (color.A < .9) + { + color = Color.Lerp(color, backgroundColor, .5f); + } + + target[x, y] = color; + } + } + }); + } + } +} diff --git a/src/ImageProcessor/Filters/ImageFilterExtensions.cs b/src/ImageProcessor/Filters/ImageFilterExtensions.cs index a322105b5..f435829b2 100644 --- a/src/ImageProcessor/Filters/ImageFilterExtensions.cs +++ b/src/ImageProcessor/Filters/ImageFilterExtensions.cs @@ -35,6 +35,18 @@ namespace ImageProcessor.Filters return source.Process(rectangle, new Alpha(percent)); } + /// + /// Combines the given image together with the current one by blending their pixels. + /// + /// The image this method extends. + /// The image to blend with the currently processing image. + /// The opacity of the image image to blend. Must be between 0 and 100. + /// The . + public static Image BackgroundColor(this Image source, Color color) + { + return source.Process(source.Bounds, new BackgroundColor(color)); + } + /// /// Combines the given image together with the current one by blending their pixels. /// diff --git a/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs b/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs index 077f28483..ee69a854b 100644 --- a/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs @@ -354,6 +354,8 @@ namespace ImageProcessor.Formats { // We divide by 255 as we will store the colors in our floating point format. // Stored in r-> g-> b-> a order. + // Gifs don't store alpha transparency so we don't need to convert to + // premultiplied. int indexOffset = index * 3; this.currentFrame[offset + 0] = colorTable[indexOffset] / 255f; // r this.currentFrame[offset + 1] = colorTable[indexOffset + 1] / 255f; // g diff --git a/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs b/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs index c601b2e12..039f9f82c 100644 --- a/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs +++ b/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs @@ -79,7 +79,7 @@ namespace ImageProcessor.Formats for (int x = 0; x < width; x++) { // Now I have the pixel, call the FirstPassQuantize function... - this.InitialQuantizePixel(source[x, y]); + this.InitialQuantizePixel(Color.ToNonPremultiplied(source[x, y])); } } } @@ -107,7 +107,7 @@ namespace ImageProcessor.Formats for (int x = 0; x < width; x++) { // Implicit cast here from Color. - Bgra32 sourcePixel = source[x, y]; + Bgra32 sourcePixel = Color.ToNonPremultiplied(source[x, y]); // Check if this is the same as the last pixel. If so use that value // rather than calculating it again. This is an inexpensive optimization. diff --git a/src/ImageProcessor/Formats/Jpg/JpegEncoder.cs b/src/ImageProcessor/Formats/Jpg/JpegEncoder.cs index 2da1fe11f..ed0a69a64 100644 --- a/src/ImageProcessor/Formats/Jpg/JpegEncoder.cs +++ b/src/ImageProcessor/Formats/Jpg/JpegEncoder.cs @@ -105,9 +105,17 @@ namespace ImageProcessor.Formats int start = x * 3; int source = ((y * pixelWidth) + x) * 4; - samples[start] = (byte)(sourcePixels[source].Clamp(0, 1) * 255); - samples[start + 1] = (byte)(sourcePixels[source + 1].Clamp(0, 1) * 255); - samples[start + 2] = (byte)(sourcePixels[source + 2].Clamp(0, 1) * 255); + // Convert to non-premultiplied color. + float r = sourcePixels[source]; + float g = sourcePixels[source + 1]; + float b = sourcePixels[source + 2]; + float a = sourcePixels[source + 3]; + + Bgra32 color = Color.ToNonPremultiplied(new Color(r, g, b, a)); + + samples[start] = color.R; + samples[start + 1] = color.G; + samples[start + 2] = color.B; } rows[y] = new SampleRow(samples, pixelWidth, 8, 3); diff --git a/src/ImageProcessor/Formats/Png/GrayscaleReader.cs b/src/ImageProcessor/Formats/Png/GrayscaleReader.cs index 0392458a0..4dfc69d9a 100644 --- a/src/ImageProcessor/Formats/Png/GrayscaleReader.cs +++ b/src/ImageProcessor/Formats/Png/GrayscaleReader.cs @@ -47,10 +47,18 @@ namespace ImageProcessor.Formats { offset = ((this.row * header.Width) + x) * 4; - pixels[offset] = newScanline[x * 2] / 255f; - pixels[offset + 1] = newScanline[x * 2] / 255f; - pixels[offset + 2] = newScanline[x * 2] / 255f; - pixels[offset + 3] = newScanline[(x * 2) + 1] / 255f; + // We want to convert to premultiplied alpha here. + float r = newScanline[x * 2] / 255f; + float g = newScanline[x * 2] / 255f; + float b = newScanline[x * 2] / 255f; + float a = newScanline[(x * 2) + 1] / 255f; + + Color premultiplied = Color.FromNonPremultiplied(new Color(r, g, b, a)); + + pixels[offset] = premultiplied.R; + pixels[offset + 1] = premultiplied.G; + pixels[offset + 2] = premultiplied.B; + pixels[offset + 3] = premultiplied.A; } } else diff --git a/src/ImageProcessor/Formats/Png/PaletteIndexReader.cs b/src/ImageProcessor/Formats/Png/PaletteIndexReader.cs index 90b2053ac..af087701b 100644 --- a/src/ImageProcessor/Formats/Png/PaletteIndexReader.cs +++ b/src/ImageProcessor/Formats/Png/PaletteIndexReader.cs @@ -55,13 +55,25 @@ namespace ImageProcessor.Formats offset = ((this.row * header.Width) + i) * 4; int pixelOffset = index * 3; + + float r = newScanline[pixelOffset] / 255f; + float g = newScanline[pixelOffset + 1] / 255f; + float b = newScanline[pixelOffset + 2] / 255f; + float a = this.paletteAlpha.Length > index + ? this.paletteAlpha[index] / 255f + : 1; - pixels[offset] = this.palette[pixelOffset] / 255f; - pixels[offset + 1] = this.palette[pixelOffset + 1] / 255f; - pixels[offset + 2] = this.palette[pixelOffset + 2] / 255f; - pixels[offset + 3] = this.paletteAlpha.Length > index - ? this.paletteAlpha[index] / 255f - : 1; + Color color = new Color(r, g, b, a); + if (color.A < 1) + { + // We want to convert to premultiplied alpha here. + color = Color.FromNonPremultiplied(color); + } + + pixels[offset] = color.R; + pixels[offset + 1] = color.G; + pixels[offset + 2] = color.B; + pixels[offset + 3] = color.A; } } else diff --git a/src/ImageProcessor/Formats/Png/PngEncoder.cs b/src/ImageProcessor/Formats/Png/PngEncoder.cs index 6ac2b0f71..e4501a956 100644 --- a/src/ImageProcessor/Formats/Png/PngEncoder.cs +++ b/src/ImageProcessor/Formats/Png/PngEncoder.cs @@ -46,7 +46,9 @@ namespace ImageProcessor.Formats /// true if the image should be written uncompressed to /// the stream; otherwise, false. /// - public bool IsWritingUncompressed { get; set; } + // TODO: We can't quickly return a color to non-premultiplied with this method. + // Should we remove? + //public bool IsWritingUncompressed { get; set; } /// /// Gets or sets a value indicating whether this instance is writing @@ -113,14 +115,14 @@ namespace ImageProcessor.Formats this.WritePhysicalChunk(stream, image); this.WriteGammaChunk(stream); - if (this.IsWritingUncompressed) - { - this.WriteDataChunksFast(stream, image); - } - else - { - this.WriteDataChunks(stream, image); - } + //if (this.IsWritingUncompressed) + //{ + // this.WriteDataChunksFast(stream, image); + //} + //else + //{ + this.WriteDataChunks(stream, image); + //} this.WriteEndChunk(stream); stream.Flush(); @@ -318,19 +320,34 @@ namespace ImageProcessor.Formats // Calculate the offset for the original pixel array. int pixelOffset = ((y * imageBase.Width) + x) * 4; - data[dataOffset] = (byte)(pixels[pixelOffset].Clamp(0, 1) * 255); - data[dataOffset + 1] = (byte)(pixels[pixelOffset + 1].Clamp(0, 1) * 255); - data[dataOffset + 2] = (byte)(pixels[pixelOffset + 2].Clamp(0, 1) * 255); - data[dataOffset + 3] = (byte)(pixels[pixelOffset + 3].Clamp(0, 1) * 255); + // Convert to non-premultiplied color. + float r = pixels[pixelOffset]; + float g = pixels[pixelOffset + 1]; + float b = pixels[pixelOffset + 2]; + float a = pixels[pixelOffset + 3]; + + Bgra32 color = Color.ToNonPremultiplied(new Color(r, g, b, a)); + + data[dataOffset] = color.R; + data[dataOffset + 1] = color.G; + data[dataOffset + 2] = color.B; + data[dataOffset + 3] = color.A; if (y > 0) { int lastOffset = (((y - 1) * imageBase.Width) + x) * 4; - data[dataOffset] -= (byte)(pixels[lastOffset].Clamp(0, 1) * 255); - data[dataOffset + 1] -= (byte)(pixels[lastOffset + 1].Clamp(0, 1) * 255); - data[dataOffset + 2] -= (byte)(pixels[lastOffset + 2].Clamp(0, 1) * 255); - data[dataOffset + 3] -= (byte)(pixels[lastOffset + 3].Clamp(0, 1) * 255); + r = pixels[lastOffset]; + g = pixels[lastOffset + 1]; + b = pixels[lastOffset + 2]; + a = pixels[lastOffset + 3]; + + color = Color.ToNonPremultiplied(new Color(r, g, b, a)); + + data[dataOffset] -= color.R; + data[dataOffset + 1] -= color.G; + data[dataOffset + 2] -= color.B; + data[dataOffset + 3] -= color.A; } } } diff --git a/src/ImageProcessor/Formats/Png/TrueColorReader.cs b/src/ImageProcessor/Formats/Png/TrueColorReader.cs index 2a9f46198..c292fcd56 100644 --- a/src/ImageProcessor/Formats/Png/TrueColorReader.cs +++ b/src/ImageProcessor/Formats/Png/TrueColorReader.cs @@ -44,10 +44,18 @@ namespace ImageProcessor.Formats { offset = ((this.row * header.Width) + (x >> 2)) * 4; - pixels[offset + 0] = newScanline[x] / 255f; - pixels[offset + 1] = newScanline[x + 1] / 255f; - pixels[offset + 2] = newScanline[x + 2] / 255f; - pixels[offset + 3] = newScanline[x + 3] / 255f; + // We want to convert to premultiplied alpha here. + float r = newScanline[x] / 255f; + float g = newScanline[x + 1] / 255f; + float b = newScanline[x + 2] / 255f; + float a = newScanline[x + 3] / 255f; + + Color premultiplied = Color.FromNonPremultiplied(new Color(r, g, b, a)); + + pixels[offset] = premultiplied.R; + pixels[offset + 1] = premultiplied.G; + pixels[offset + 2] = premultiplied.B; + pixels[offset + 3] = premultiplied.A; } } else @@ -57,7 +65,7 @@ namespace ImageProcessor.Formats offset = ((this.row * header.Width) + x) * 4; int pixelOffset = x * 3; - pixels[offset + 0] = newScanline[pixelOffset] / 255f; + pixels[offset] = newScanline[pixelOffset] / 255f; pixels[offset + 1] = newScanline[pixelOffset + 1] / 255f; pixels[offset + 2] = newScanline[pixelOffset + 2] / 255f; pixels[offset + 3] = 1; diff --git a/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs b/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs index 35029e8d9..584b62a5e 100644 --- a/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs +++ b/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs @@ -46,9 +46,9 @@ namespace ImageProcessor.Tests [MemberData("Filters")] public void FilterImage(string name, IImageProcessor processor) { - if (!Directory.Exists("Filtered")) + if (!Directory.Exists("TestOutput/Filtered")) { - Directory.CreateDirectory("Filtered"); + Directory.CreateDirectory("TestOutput/Filtered"); } foreach (string file in Files) @@ -58,7 +58,7 @@ namespace ImageProcessor.Tests Stopwatch watch = Stopwatch.StartNew(); Image image = new Image(stream); string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); - using (FileStream output = File.OpenWrite($"Filtered/{ Path.GetFileName(filename) }")) + using (FileStream output = File.OpenWrite($"TestOutput/Filtered/{ Path.GetFileName(filename) }")) { image.Process(processor).Save(output); } diff --git a/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs b/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs index 1bc117e54..8d274ae95 100644 --- a/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs +++ b/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs @@ -30,6 +30,7 @@ namespace ImageProcessor.Tests //"TestImages/Formats/Jpg/greyscale.jpg", //"TestImages/Formats/Bmp/Car.bmp", "TestImages/Formats/Png/cballs.png", + "TestImages/Formats/Png/blur.png", //"TestImages/Formats/Png/cmyk.png", //"TestImages/Formats/Png/gamma-1.0-or-2.2.png", "TestImages/Formats/Png/splash.png", diff --git a/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs b/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs index e171341f8..88d5b7a38 100644 --- a/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs +++ b/tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs @@ -4,6 +4,7 @@ namespace ImageProcessor.Tests using System.Diagnostics; using System.IO; + using ImageProcessor.Filters; using ImageProcessor.Samplers; using Xunit; diff --git a/tests/ImageProcessor.Tests/TestImages/Formats/Png/blur.png.REMOVED.git-id b/tests/ImageProcessor.Tests/TestImages/Formats/Png/blur.png.REMOVED.git-id new file mode 100644 index 000000000..7bcc08db2 --- /dev/null +++ b/tests/ImageProcessor.Tests/TestImages/Formats/Png/blur.png.REMOVED.git-id @@ -0,0 +1 @@ +8b5342317c64603069b6b7227edeb96f6acf6c29 \ No newline at end of file