Browse Source

Add premultiplied alpha

Former-commit-id: 19e9707988c7eea57137289d395c61a3c64dfcbc
Former-commit-id: 7345f59fdb18429fd71d617c493ef6b9a2e006fa
Former-commit-id: 2f7b5f2b751c0c26fc588d7fddcb93f67844d5a5
af/merge-core
James Jackson-South 11 years ago
parent
commit
1a449f0629
  1. 2
      README.md
  2. 32
      src/ImageProcessor/Colors/Color.cs
  3. 61
      src/ImageProcessor/Filters/BackgroundColor.cs
  4. 12
      src/ImageProcessor/Filters/ImageFilterExtensions.cs
  5. 2
      src/ImageProcessor/Formats/Gif/GifDecoderCore.cs
  6. 4
      src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs
  7. 14
      src/ImageProcessor/Formats/Jpg/JpegEncoder.cs
  8. 16
      src/ImageProcessor/Formats/Png/GrayscaleReader.cs
  9. 24
      src/ImageProcessor/Formats/Png/PaletteIndexReader.cs
  10. 51
      src/ImageProcessor/Formats/Png/PngEncoder.cs
  11. 18
      src/ImageProcessor/Formats/Png/TrueColorReader.cs
  12. 6
      tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs
  13. 1
      tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs
  14. 1
      tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs
  15. 1
      tests/ImageProcessor.Tests/TestImages/Formats/Png/blur.png.REMOVED.git-id

2
README.md

@ -2,7 +2,7 @@
**This branch contains the new cross platform version of ImageProcessor**. **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? ###Why am I writing this?

32
src/ImageProcessor/Colors/Color.cs

@ -10,7 +10,8 @@ namespace ImageProcessor
using System.Numerics; using System.Numerics;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, /// 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); amount = amount.Clamp(0f, 1f);
return (from * (1 - amount)) + (to * amount); //return (from * (1 - amount)) + (to * amount);
return (from * (1 - amount)) + to ;
} }
/// <summary> /// <summary>
@ -478,7 +480,7 @@ namespace ImageProcessor
/// <see href="http://www.4p8.com/eric.brasseur/gamma.html#formulas"/> /// <see href="http://www.4p8.com/eric.brasseur/gamma.html#formulas"/>
/// <see href="http://entropymine.com/imageworsener/srgbformula/"/> /// <see href="http://entropymine.com/imageworsener/srgbformula/"/>
/// </summary> /// </summary>
/// <param name="linear">The <see cref="Color"/> whos signal to compress.</param> /// <param name="linear">The <see cref="Color"/> whose signal to compress.</param>
/// <returns>The <see cref="Color"/>.</returns> /// <returns>The <see cref="Color"/>.</returns>
public static Color Compand(Color linear) public static Color Compand(Color linear)
{ {
@ -495,7 +497,7 @@ namespace ImageProcessor
/// <see href="http://www.4p8.com/eric.brasseur/gamma.html#formulas"/> /// <see href="http://www.4p8.com/eric.brasseur/gamma.html#formulas"/>
/// <see href="http://entropymine.com/imageworsener/srgbformula/"/> /// <see href="http://entropymine.com/imageworsener/srgbformula/"/>
/// </summary> /// </summary>
/// <param name="gamma">The <see cref="Color"/> whos signal to expand.</param> /// <param name="gamma">The <see cref="Color"/> whose signal to expand.</param>
/// <returns>The <see cref="Color"/>.</returns> /// <returns>The <see cref="Color"/>.</returns>
public static Color InverseCompand(Color gamma) public static Color InverseCompand(Color gamma)
{ {
@ -511,33 +513,29 @@ namespace ImageProcessor
/// Converts a non-premultipled alpha <see cref="Color"/> to a <see cref="Color"/> /// Converts a non-premultipled alpha <see cref="Color"/> to a <see cref="Color"/>
/// that contains premultiplied alpha. /// that contains premultiplied alpha.
/// </summary> /// </summary>
/// <param name="r">The red component of this <see cref="Color"/>.</param> /// <param name="color">The <see cref="Color"/> to convert.</param>
/// <param name="g">The green component of this <see cref="Color"/>.</param>
/// <param name="b">The blue component of this <see cref="Color"/>.</param>
/// <param name="a">The alpha component of this <see cref="Color"/>.</param>
/// <returns>The <see cref="Color"/>.</returns> /// <returns>The <see cref="Color"/>.</returns>
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);
} }
/// <summary> /// <summary>
/// Converts a premultipled alpha <see cref="Color"/> to a <see cref="Color"/> /// Converts a premultipled alpha <see cref="Color"/> to a <see cref="Color"/>
/// that contains non-premultiplied alpha. /// that contains non-premultiplied alpha.
/// </summary> /// </summary>
/// <param name="r">The red component of this <see cref="Color"/>.</param> /// <param name="color">The <see cref="Color"/> to convert.</param>
/// <param name="g">The green component of this <see cref="Color"/>.</param>
/// <param name="b">The blue component of this <see cref="Color"/>.</param>
/// <param name="a">The alpha component of this <see cref="Color"/>.</param>
/// <returns>The <see cref="Color"/>.</returns> /// <returns>The <see cref="Color"/>.</returns>
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) 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);
} }
/// <summary> /// <summary>

61
src/ImageProcessor/Filters/BackgroundColor.cs

@ -0,0 +1,61 @@
// <copyright file="Blend.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessor.Filters
{
using System.Threading.Tasks;
/// <summary>
/// Sets the background color of the image.
/// </summary>
public class BackgroundColor : ParallelImageProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="Blend"/> class.
/// </summary>
/// <param name="color">The <see cref="Color"/> to set the background color to.</param>
public BackgroundColor(Color color)
{
this.Value = Color.FromNonPremultiplied(color);
}
/// <summary>
/// Gets the background color value.
/// </summary>
public Color Value { get; }
/// <inheritdoc/>
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;
}
}
});
}
}
}

12
src/ImageProcessor/Filters/ImageFilterExtensions.cs

@ -35,6 +35,18 @@ namespace ImageProcessor.Filters
return source.Process(rectangle, new Alpha(percent)); return source.Process(rectangle, new Alpha(percent));
} }
/// <summary>
/// Combines the given image together with the current one by blending their pixels.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="image">The image to blend with the currently processing image.</param>
/// <param name="percent">The opacity of the image image to blend. Must be between 0 and 100.</param>
/// <returns>The <see cref="Image"/>.</returns>
public static Image BackgroundColor(this Image source, Color color)
{
return source.Process(source.Bounds, new BackgroundColor(color));
}
/// <summary> /// <summary>
/// Combines the given image together with the current one by blending their pixels. /// Combines the given image together with the current one by blending their pixels.
/// </summary> /// </summary>

2
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. // We divide by 255 as we will store the colors in our floating point format.
// Stored in r-> g-> b-> a order. // 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; int indexOffset = index * 3;
this.currentFrame[offset + 0] = colorTable[indexOffset] / 255f; // r this.currentFrame[offset + 0] = colorTable[indexOffset] / 255f; // r
this.currentFrame[offset + 1] = colorTable[indexOffset + 1] / 255f; // g this.currentFrame[offset + 1] = colorTable[indexOffset + 1] / 255f; // g

4
src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs

@ -79,7 +79,7 @@ namespace ImageProcessor.Formats
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
// Now I have the pixel, call the FirstPassQuantize function... // 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++) for (int x = 0; x < width; x++)
{ {
// Implicit cast here from Color. // 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 // 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. // rather than calculating it again. This is an inexpensive optimization.

14
src/ImageProcessor/Formats/Jpg/JpegEncoder.cs

@ -105,9 +105,17 @@ namespace ImageProcessor.Formats
int start = x * 3; int start = x * 3;
int source = ((y * pixelWidth) + x) * 4; int source = ((y * pixelWidth) + x) * 4;
samples[start] = (byte)(sourcePixels[source].Clamp(0, 1) * 255); // Convert to non-premultiplied color.
samples[start + 1] = (byte)(sourcePixels[source + 1].Clamp(0, 1) * 255); float r = sourcePixels[source];
samples[start + 2] = (byte)(sourcePixels[source + 2].Clamp(0, 1) * 255); 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); rows[y] = new SampleRow(samples, pixelWidth, 8, 3);

16
src/ImageProcessor/Formats/Png/GrayscaleReader.cs

@ -47,10 +47,18 @@ namespace ImageProcessor.Formats
{ {
offset = ((this.row * header.Width) + x) * 4; offset = ((this.row * header.Width) + x) * 4;
pixels[offset] = newScanline[x * 2] / 255f; // We want to convert to premultiplied alpha here.
pixels[offset + 1] = newScanline[x * 2] / 255f; float r = newScanline[x * 2] / 255f;
pixels[offset + 2] = newScanline[x * 2] / 255f; float g = newScanline[x * 2] / 255f;
pixels[offset + 3] = newScanline[(x * 2) + 1] / 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 else

24
src/ImageProcessor/Formats/Png/PaletteIndexReader.cs

@ -55,13 +55,25 @@ namespace ImageProcessor.Formats
offset = ((this.row * header.Width) + i) * 4; offset = ((this.row * header.Width) + i) * 4;
int pixelOffset = index * 3; 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; Color color = new Color(r, g, b, a);
pixels[offset + 1] = this.palette[pixelOffset + 1] / 255f; if (color.A < 1)
pixels[offset + 2] = this.palette[pixelOffset + 2] / 255f; {
pixels[offset + 3] = this.paletteAlpha.Length > index // We want to convert to premultiplied alpha here.
? this.paletteAlpha[index] / 255f color = Color.FromNonPremultiplied(color);
: 1; }
pixels[offset] = color.R;
pixels[offset + 1] = color.G;
pixels[offset + 2] = color.B;
pixels[offset + 3] = color.A;
} }
} }
else else

51
src/ImageProcessor/Formats/Png/PngEncoder.cs

@ -46,7 +46,9 @@ namespace ImageProcessor.Formats
/// <c>true</c> if the image should be written uncompressed to /// <c>true</c> if the image should be written uncompressed to
/// the stream; otherwise, <c>false</c>. /// the stream; otherwise, <c>false</c>.
/// </value> /// </value>
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; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this instance is writing /// Gets or sets a value indicating whether this instance is writing
@ -113,14 +115,14 @@ namespace ImageProcessor.Formats
this.WritePhysicalChunk(stream, image); this.WritePhysicalChunk(stream, image);
this.WriteGammaChunk(stream); this.WriteGammaChunk(stream);
if (this.IsWritingUncompressed) //if (this.IsWritingUncompressed)
{ //{
this.WriteDataChunksFast(stream, image); // this.WriteDataChunksFast(stream, image);
} //}
else //else
{ //{
this.WriteDataChunks(stream, image); this.WriteDataChunks(stream, image);
} //}
this.WriteEndChunk(stream); this.WriteEndChunk(stream);
stream.Flush(); stream.Flush();
@ -318,19 +320,34 @@ namespace ImageProcessor.Formats
// Calculate the offset for the original pixel array. // Calculate the offset for the original pixel array.
int pixelOffset = ((y * imageBase.Width) + x) * 4; int pixelOffset = ((y * imageBase.Width) + x) * 4;
data[dataOffset] = (byte)(pixels[pixelOffset].Clamp(0, 1) * 255); // Convert to non-premultiplied color.
data[dataOffset + 1] = (byte)(pixels[pixelOffset + 1].Clamp(0, 1) * 255); float r = pixels[pixelOffset];
data[dataOffset + 2] = (byte)(pixels[pixelOffset + 2].Clamp(0, 1) * 255); float g = pixels[pixelOffset + 1];
data[dataOffset + 3] = (byte)(pixels[pixelOffset + 3].Clamp(0, 1) * 255); 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) if (y > 0)
{ {
int lastOffset = (((y - 1) * imageBase.Width) + x) * 4; int lastOffset = (((y - 1) * imageBase.Width) + x) * 4;
data[dataOffset] -= (byte)(pixels[lastOffset].Clamp(0, 1) * 255); r = pixels[lastOffset];
data[dataOffset + 1] -= (byte)(pixels[lastOffset + 1].Clamp(0, 1) * 255); g = pixels[lastOffset + 1];
data[dataOffset + 2] -= (byte)(pixels[lastOffset + 2].Clamp(0, 1) * 255); b = pixels[lastOffset + 2];
data[dataOffset + 3] -= (byte)(pixels[lastOffset + 3].Clamp(0, 1) * 255); 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;
} }
} }
} }

18
src/ImageProcessor/Formats/Png/TrueColorReader.cs

@ -44,10 +44,18 @@ namespace ImageProcessor.Formats
{ {
offset = ((this.row * header.Width) + (x >> 2)) * 4; offset = ((this.row * header.Width) + (x >> 2)) * 4;
pixels[offset + 0] = newScanline[x] / 255f; // We want to convert to premultiplied alpha here.
pixels[offset + 1] = newScanline[x + 1] / 255f; float r = newScanline[x] / 255f;
pixels[offset + 2] = newScanline[x + 2] / 255f; float g = newScanline[x + 1] / 255f;
pixels[offset + 3] = newScanline[x + 3] / 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 else
@ -57,7 +65,7 @@ namespace ImageProcessor.Formats
offset = ((this.row * header.Width) + x) * 4; offset = ((this.row * header.Width) + x) * 4;
int pixelOffset = x * 3; int pixelOffset = x * 3;
pixels[offset + 0] = newScanline[pixelOffset] / 255f; pixels[offset] = newScanline[pixelOffset] / 255f;
pixels[offset + 1] = newScanline[pixelOffset + 1] / 255f; pixels[offset + 1] = newScanline[pixelOffset + 1] / 255f;
pixels[offset + 2] = newScanline[pixelOffset + 2] / 255f; pixels[offset + 2] = newScanline[pixelOffset + 2] / 255f;
pixels[offset + 3] = 1; pixels[offset + 3] = 1;

6
tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs

@ -46,9 +46,9 @@ namespace ImageProcessor.Tests
[MemberData("Filters")] [MemberData("Filters")]
public void FilterImage(string name, IImageProcessor processor) 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) foreach (string file in Files)
@ -58,7 +58,7 @@ namespace ImageProcessor.Tests
Stopwatch watch = Stopwatch.StartNew(); Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream); Image image = new Image(stream);
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); 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); image.Process(processor).Save(output);
} }

1
tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs

@ -30,6 +30,7 @@ namespace ImageProcessor.Tests
//"TestImages/Formats/Jpg/greyscale.jpg", //"TestImages/Formats/Jpg/greyscale.jpg",
//"TestImages/Formats/Bmp/Car.bmp", //"TestImages/Formats/Bmp/Car.bmp",
"TestImages/Formats/Png/cballs.png", "TestImages/Formats/Png/cballs.png",
"TestImages/Formats/Png/blur.png",
//"TestImages/Formats/Png/cmyk.png", //"TestImages/Formats/Png/cmyk.png",
//"TestImages/Formats/Png/gamma-1.0-or-2.2.png", //"TestImages/Formats/Png/gamma-1.0-or-2.2.png",
"TestImages/Formats/Png/splash.png", "TestImages/Formats/Png/splash.png",

1
tests/ImageProcessor.Tests/Processors/Samplers/SamplerTests.cs

@ -4,6 +4,7 @@ namespace ImageProcessor.Tests
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using ImageProcessor.Filters;
using ImageProcessor.Samplers; using ImageProcessor.Samplers;
using Xunit; using Xunit;

1
tests/ImageProcessor.Tests/TestImages/Formats/Png/blur.png.REMOVED.git-id

@ -0,0 +1 @@
8b5342317c64603069b6b7227edeb96f6acf6c29
Loading…
Cancel
Save