port png transparency fixes from biggustave changes #346

This commit is contained in:
Eliot Jones
2021-08-14 14:47:23 -04:00
parent 50dc67c2ca
commit c77e2f7952
5 changed files with 114 additions and 25 deletions

View File

@@ -2,18 +2,43 @@
{
internal class Palette
{
public bool HasAlphaValues { get; private set; }
public byte[] Data { get; }
/// <summary>
/// Creates a palette object. Input palette data length from PLTE chunk must be a multiple of 3.
/// </summary>
public Palette(byte[] data)
{
Data = data;
Data = new byte[data.Length * 4 / 3];
var dataIndex = 0;
for (var i = 0; i < data.Length; i += 3)
{
Data[dataIndex++] = data[i];
Data[dataIndex++] = data[i + 1];
Data[dataIndex++] = data[i + 2];
Data[dataIndex++] = 255;
}
}
/// <summary>
/// Adds transparency values from tRNS chunk.
/// </summary>
public void SetAlphaValues(byte[] bytes)
{
HasAlphaValues = true;
for (var i = 0; i < bytes.Length; i++)
{
Data[i * 4 + 3] = bytes[i];
}
}
public Pixel GetPixel(int index)
{
var start = index * 3;
return new Pixel(Data[start], Data[start + 1], Data[start + 2], 255, false);
var start = index * 4;
return new Pixel(Data[start], Data[start + 1], Data[start + 2], Data[start + 3], false);
}
}
}

View File

@@ -9,6 +9,7 @@
internal class Png
{
private readonly RawPngData data;
private readonly bool hasTransparencyChunk;
/// <summary>
/// The header data from the PNG image.
@@ -28,12 +29,13 @@
/// <summary>
/// Whether the image has an alpha (transparency) layer.
/// </summary>
public bool HasAlphaChannel => (Header.ColorType & ColorType.AlphaChannelUsed) != 0;
public bool HasAlphaChannel => (Header.ColorType & ColorType.AlphaChannelUsed) != 0 || hasTransparencyChunk;
internal Png(ImageHeader header, RawPngData data)
internal Png(ImageHeader header, RawPngData data, bool hasTransparencyChunk)
{
Header = header;
this.data = data ?? throw new ArgumentNullException(nameof(data));
this.hasTransparencyChunk = hasTransparencyChunk;
}
/// <summary>

View File

@@ -7,7 +7,12 @@
internal static class PngOpener
{
public static Png Open(Stream stream, IChunkVisitor chunkVisitor = null)
public static Png Open(Stream stream, IChunkVisitor chunkVisitor = null) => Open(stream, new PngOpenerSettings
{
ChunkVisitor = chunkVisitor
});
public static Png Open(Stream stream, PngOpenerSettings settings)
{
if (stream == null)
{
@@ -41,7 +46,12 @@
{
if (hasEncounteredImageEnd)
{
throw new InvalidOperationException($"Found another chunk {header} after already reading the IEND chunk.");
if (settings?.DisallowTrailingData == true)
{
throw new InvalidOperationException($"Found another chunk {header} after already reading the IEND chunk.");
}
break;
}
var bytes = new byte[header.Length];
@@ -61,7 +71,11 @@
throw new InvalidOperationException($"Palette data must be multiple of 3, got {header.Length}.");
}
palette = new Palette(bytes);
// Ignore palette data unless the header.ColorType indicates that the image is paletted.
if (imageHeader.ColorType.HasFlag(ColorType.PaletteUsed))
{
palette = new Palette(bytes);
}
break;
case "IDAT":
@@ -74,6 +88,19 @@
throw new NotSupportedException($"Encountered critical header {header} which was not recognised.");
}
}
else
{
switch (header.Name)
{
case "tRNS":
// Add transparency to palette, if the PLTE chunk has been read.
if (palette != null)
{
palette.SetAlphaValues(bytes);
}
break;
}
}
read = stream.Read(crc, 0, crc.Length);
if (read != 4)
@@ -89,7 +116,7 @@
throw new InvalidOperationException($"CRC calculated {result} did not match file {crcActual} for chunk: {header.Name}.");
}
chunkVisitor?.Visit(stream, imageHeader, header, bytes, crc);
settings?.ChunkVisitor?.Visit(stream, imageHeader, header, bytes, crc);
}
memoryStream.Flush();
@@ -108,7 +135,7 @@
bytesOut = Decoder.Decode(bytesOut, imageHeader, bytesPerPixel, samplesPerPixel);
return new Png(imageHeader, new RawPngData(bytesOut, bytesPerPixel, imageHeader.Width, imageHeader.InterlaceMethod, palette, imageHeader.ColorType));
return new Png(imageHeader, new RawPngData(bytesOut, bytesPerPixel, palette, imageHeader), palette?.HasAlphaValues ?? false);
}
}

View File

@@ -0,0 +1,16 @@
namespace UglyToad.PdfPig.Images.Png
{
internal class PngOpenerSettings
{
/// <summary>
/// The code to execute whenever a chunk is read. Can be <see langword="null"/>.
/// </summary>
public IChunkVisitor ChunkVisitor { get; set; }
/// <summary>
/// Whether to throw if the image contains data after the image end marker.
/// <see langword="false"/> by default.
/// </summary>
public bool DisallowTrailingData { get; set; }
}
}

View File

@@ -2,7 +2,7 @@
{
using System;
/// <summary>
/// <summary>
/// Provides convenience methods for indexing into a raw byte array to extract pixel values.
/// </summary>
internal class RawPngData
@@ -13,17 +13,16 @@
private readonly Palette palette;
private readonly ColorType colorType;
private readonly int rowOffset;
private readonly int bitDepth;
/// <summary>
/// Create a new <see cref="RawPngData"/>.
/// </summary>
/// <param name="data">The decoded pixel data as bytes.</param>
/// <param name="bytesPerPixel">The number of bytes in each pixel.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="interlaceMethod">The interlace method used.</param>
/// <param name="palette">The palette for images using indexed colors.</param>
/// <param name="colorType">The color type.</param>
public RawPngData(byte[] data, int bytesPerPixel, int width, InterlaceMethod interlaceMethod, Palette palette, ColorType colorType)
/// <param name="palette">The palette for the image.</param>
/// <param name="imageHeader">The image header.</param>
public RawPngData(byte[] data, int bytesPerPixel, Palette palette, ImageHeader imageHeader)
{
if (width < 0)
{
@@ -32,25 +31,45 @@
this.data = data ?? throw new ArgumentNullException(nameof(data));
this.bytesPerPixel = bytesPerPixel;
this.width = width;
this.palette = palette;
this.colorType = colorType;
rowOffset = interlaceMethod == InterlaceMethod.Adam7 ? 0 : 1;
width = imageHeader.Width;
colorType = imageHeader.ColorType;
rowOffset = imageHeader.InterlaceMethod == InterlaceMethod.Adam7 ? 0 : 1;
bitDepth = imageHeader.BitDepth;
}
public Pixel GetPixel(int x, int y)
{
if (palette != null)
{
var pixelsPerByte = (8 / bitDepth);
var bytesInRow = (1 + (width / pixelsPerByte));
var byteIndexInRow = x / pixelsPerByte;
var paletteIndex = (1 + (y * bytesInRow)) + byteIndexInRow;
var b = data[paletteIndex];
if (bitDepth == 8)
{
return palette.GetPixel(b);
}
var withinByteIndex = x % pixelsPerByte;
var rightShift = 8 - ((withinByteIndex + 1) * bitDepth);
var indexActual = (b >> rightShift) & ((1 << bitDepth) - 1);
return palette.GetPixel(indexActual);
}
var rowStartPixel = (rowOffset + (rowOffset * y)) + (bytesPerPixel * width * y);
var pixelStartIndex = rowStartPixel + (bytesPerPixel * x);
var first = data[pixelStartIndex];
if (palette != null)
{
return palette.GetPixel(first);
}
switch (bytesPerPixel)
{
case 1: