Browse Source

Add premultiplied alpha

Former-commit-id: 346fe4d5a27014ff153447113eee4406c9b9d07e
Former-commit-id: fd5c29c65e88e8ba292879950f70ea072848753a
Former-commit-id: 8c2da3d64065b5884117016885a26c5913b932de
af/merge-core
James Jackson-South 10 years ago
parent
commit
fdc681a263
  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 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?

32
src/ImageProcessor/Colors/Color.cs

@ -10,7 +10,8 @@ namespace ImageProcessor
using System.Numerics;
/// <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>
/// <remarks>
/// 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 ;
}
/// <summary>
@ -478,7 +480,7 @@ namespace ImageProcessor
/// <see href="http://www.4p8.com/eric.brasseur/gamma.html#formulas"/>
/// <see href="http://entropymine.com/imageworsener/srgbformula/"/>
/// </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>
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://entropymine.com/imageworsener/srgbformula/"/>
/// </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>
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"/>
/// that contains premultiplied alpha.
/// </summary>
/// <param name="r">The red component of this <see cref="Color"/>.</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>
/// <param name="color">The <see cref="Color"/> to convert.</param>
/// <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>
/// Converts a premultipled alpha <see cref="Color"/> to a <see cref="Color"/>
/// that contains non-premultiplied alpha.
/// </summary>
/// <param name="r">The red component of this <see cref="Color"/>.</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>
/// <param name="color">The <see cref="Color"/> to convert.</param>
/// <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)
{
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>

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));
}
/// <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>
/// Combines the given image together with the current one by blending their pixels.
/// </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.
// 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

4
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.

14
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);

16
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

24
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

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
/// the stream; otherwise, <c>false</c>.
/// </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>
/// 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;
}
}
}

18
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;

6
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);
}

1
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",

1
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;

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

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