/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.IO;
using System.IO.Compression;
namespace Vision.BinPickingCell
{
///
/// Minimal PNG decoder for 8-bit RGBA images. Handles the output of
/// the in-repo PngEncoder used by the OpenUSD capture provider
/// (single IHDR + single or few IDATs + IEND, colour-type 6, bit-depth
/// 8, filter type 0 or 1 per row) and any similarly-conformant PNG.
/// Adds no NuGet dependency; the DEFLATE step goes through the BCL's
/// .
///
///
/// The decoder is deliberately restricted to what the sample diagnostic
/// needs. It rejects interlaced streams, palette images, and 16-bit
/// depths with ; the OpenUSD
/// provider only ever emits the RGBA8 non-interlaced flavour.
///
internal static class PngDecoder
{
public static (byte[] Rgba, int Width, int Height) Decode(byte[] png)
{
if (png == null)
{
throw new ArgumentNullException(nameof(png));
}
if (png.Length < 8 ||
png[0] != 0x89 ||
png[1] != (byte)'P' ||
png[2] != (byte)'N' ||
png[3] != (byte)'G' ||
png[4] != 0x0D ||
png[5] != 0x0A ||
png[6] != 0x1A ||
png[7] != 0x0A)
{
throw new InvalidDataException("Not a PNG stream.");
}
int width = 0;
int height = 0;
byte bitDepth = 0;
byte colourType = 0;
byte interlace = 0;
using var idat = new MemoryStream();
int offset = 8;
while (offset + 8 <= png.Length)
{
int length = ReadUInt32BE(png, offset);
string chunkType = System.Text.Encoding.ASCII.GetString(png, offset + 4, 4);
int dataStart = offset + 8;
if (chunkType == "IHDR")
{
width = ReadUInt32BE(png, dataStart);
height = ReadUInt32BE(png, dataStart + 4);
bitDepth = png[dataStart + 8];
colourType = png[dataStart + 9];
interlace = png[dataStart + 12];
}
else if (chunkType == "IDAT")
{
idat.Write(png, dataStart, length);
}
else if (chunkType == "IEND")
{
break;
}
offset = dataStart + length + 4;
}
if (bitDepth != 8 || colourType != 6)
{
throw new NotSupportedException(
$"PNG must be 8-bit RGBA (colour type 6, bit depth 8); got type={colourType}, depth={bitDepth}.");
}
if (interlace != 0)
{
throw new NotSupportedException("Interlaced PNGs are not supported by the sample decoder.");
}
if (width <= 0 || height <= 0)
{
throw new InvalidDataException("PNG dimensions are invalid.");
}
byte[] zlibBytes = idat.ToArray();
if (zlibBytes.Length < 2)
{
throw new InvalidDataException("IDAT chunk is too short.");
}
byte[] rawFiltered = InflateZlib(zlibBytes);
int rowBytes = width * 4;
int expected = height * (rowBytes + 1);
if (rawFiltered.Length != expected)
{
throw new InvalidDataException(
$"Decoded filtered size {rawFiltered.Length} does not match {expected}.");
}
byte[] rgba = new byte[height * rowBytes];
Unfilter(rawFiltered, rgba, width, height);
return (rgba, width, height);
}
private static byte[] InflateZlib(byte[] zlib)
{
using var input = new MemoryStream(zlib, index: 2, count: zlib.Length - 6, writable: false);
using var deflate = new DeflateStream(input, CompressionMode.Decompress, leaveOpen: false);
using var output = new MemoryStream();
deflate.CopyTo(output);
return output.ToArray();
}
private static void Unfilter(byte[] filtered, byte[] rgba, int width, int height)
{
int rowBytes = width * 4;
for (int y = 0; y < height; y++)
{
int srcRow = y * (rowBytes + 1);
int dstRow = y * rowBytes;
byte type = filtered[srcRow];
for (int x = 0; x < rowBytes; x++)
{
byte value = filtered[srcRow + 1 + x];
byte left = x >= 4 ? rgba[dstRow + x - 4] : (byte)0;
byte up = y > 0 ? rgba[((y - 1) * rowBytes) + x] : (byte)0;
byte upLeft = y > 0 && x >= 4 ? rgba[((y - 1) * rowBytes) + x - 4] : (byte)0;
rgba[dstRow + x] = type switch
{
0 => value,
1 => (byte)(value + left),
2 => (byte)(value + up),
3 => (byte)(value + ((left + up) / 2)),
4 => (byte)(value + Paeth(left, up, upLeft)),
_ => throw new NotSupportedException($"Unknown PNG row filter {type}.")
};
}
}
}
private static byte Paeth(byte left, byte up, byte upLeft)
{
int p = left + up - upLeft;
int pa = Math.Abs(p - left);
int pb = Math.Abs(p - up);
int pc = Math.Abs(p - upLeft);
if (pa <= pb && pa <= pc)
{
return left;
}
if (pb <= pc)
{
return up;
}
return upLeft;
}
private static int ReadUInt32BE(byte[] source, int offset)
{
return (source[offset] << 24) |
(source[offset + 1] << 16) |
(source[offset + 2] << 8) |
source[offset + 3];
}
}
}