PNG/RawPngData.cs
2024-11-03 21:34:26 +00:00

120 lines
No EOL
4.3 KiB
C#

namespace Uwaa.PNG;
/// <summary>
/// Provides convenience methods for indexing into a raw byte array to extract pixel values.
/// </summary>
internal class RawPngData
{
readonly byte[] data;
readonly int bytesPerPixel;
readonly int width;
readonly Palette? palette;
readonly ColorType colorType;
readonly int rowOffset;
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="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)
{
throw new ArgumentOutOfRangeException($"Width must be greater than or equal to 0, got {width}.");
}
this.data = data ?? throw new ArgumentNullException(nameof(data));
this.bytesPerPixel = bytesPerPixel;
this.palette = palette;
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)
{
int pixelsPerByte = 8 / bitDepth;
int bytesInRow = 1 + (width / pixelsPerByte);
int byteIndexInRow = x / pixelsPerByte;
int paletteIndex = 1 + (y * bytesInRow) + byteIndexInRow;
byte b = data[paletteIndex];
if (bitDepth == 8)
return palette.GetPixel(b);
int withinByteIndex = x % pixelsPerByte;
int rightShift = 8 - ((withinByteIndex + 1) * bitDepth);
int indexActual = (b >> rightShift) & ((1 << bitDepth) - 1);
return palette.GetPixel(indexActual);
}
int rowStartPixel = rowOffset + (rowOffset * y) + (bytesPerPixel * width * y);
int pixelStartIndex = rowStartPixel + (bytesPerPixel * x);
byte first = data[pixelStartIndex];
switch (bytesPerPixel)
{
case 1:
return new Pixel(first);
case 2:
switch (colorType)
{
case ColorType.None:
{
byte second = data[pixelStartIndex + 1];
byte value = ToSingleByte(first, second);
return new Pixel(value, value, value, 255);
}
default:
return new Pixel(first, first, first, data[pixelStartIndex + 1]);
}
case 3:
return new Pixel(first, data[pixelStartIndex + 1], data[pixelStartIndex + 2], 255);
case 4:
switch (colorType)
{
case ColorType.None | ColorType.AlphaChannelUsed:
{
byte second = data[pixelStartIndex + 1];
byte firstAlpha = data[pixelStartIndex + 2];
byte secondAlpha = data[pixelStartIndex + 3];
byte gray = ToSingleByte(first, second);
byte alpha = ToSingleByte(firstAlpha, secondAlpha);
return new Pixel(gray, gray, gray, alpha);
}
default:
return new Pixel(first, data[pixelStartIndex + 1], data[pixelStartIndex + 2], data[pixelStartIndex + 3]);
}
case 6:
return new Pixel(first, data[pixelStartIndex + 2], data[pixelStartIndex + 4], 255);
case 8:
return new Pixel(first, data[pixelStartIndex + 2], data[pixelStartIndex + 4], data[pixelStartIndex + 6]);
default:
throw new InvalidOperationException($"Unreconized number of bytes per pixel: {bytesPerPixel}.");
}
}
static byte ToSingleByte(byte first, byte second)
{
int us = (first << 8) + second;
byte result = (byte)Math.Round(255 * us / (double)ushort.MaxValue);
return result;
}
}