Skip to content

Commit

Permalink
Png working, needs some refinement and additional work for correctnes…
Browse files Browse the repository at this point in the history
…s in all color profiles.
  • Loading branch information
juliusfriedman committed Oct 19, 2024
1 parent a2f5561 commit f38817e
Show file tree
Hide file tree
Showing 124 changed files with 577 additions and 92 deletions.
55 changes: 55 additions & 0 deletions Codecs/Image/Png/Codec.Png/Chunk.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Media.Common;

namespace Codec.Png;

public class Chunk : MemorySegment
{
public Chunk(byte[] array, int offset)
: base(array, offset)
{
}

public Chunk(string chunkType, int chunkSize)
: base(new MemorySegment(ChunkHeader.ChunkHeaderLength + Binary.BytesPerInteger + chunkSize))
{
ChunkType = chunkType;
ChunkSize = chunkSize;
}

public ChunkHeader Header => new ChunkHeader(Array, Offset);

public int ChunkSize
{
get { return (int)Header.Length; }
set { Header.Length = (uint)value; }
}

public string ChunkType
{
get { return Header.Name; }
set { Header.Name = value; }
}

public MemorySegment Data => new MemorySegment(Array, Offset + ChunkHeader.ChunkHeaderLength, (int)Header.Length);

public int Crc
{
get { return Binary.Read32(Array, Offset + ChunkHeader.ChunkHeaderLength + ChunkSize, Binary.IsBigEndian); }
set { Binary.Write32(Array, Offset + ChunkHeader.ChunkHeaderLength + ChunkSize, Binary.IsBigEndian, value); }
}

public MemorySegment CrcData => new(Array, Offset + ChunkHeader.ChunkHeaderLength + ChunkSize, Binary.BytesPerInteger);

internal static Chunk ReadChunk(Stream inputStream)
{
ChunkHeader header = new ChunkHeader();
if (ChunkHeader.ChunkHeaderLength != inputStream.Read(header.Array, header.Offset, ChunkHeader.ChunkHeaderLength))
throw new InvalidDataException("Not enough bytes for chunk length.");
var chunk = new Chunk(header.Name, (int)header.Length);
if (header.Length != inputStream.Read(chunk.Data.Array, chunk.Data.Offset, (int)header.Length))
throw new InvalidDataException("Not enough bytes for chunk data.");
if (Binary.BytesPerInteger != inputStream.Read(chunk.CrcData.Array, chunk.CrcData.Offset, chunk.CrcData.Count))
throw new InvalidDataException("Not enough bytes for CrcData.");
return chunk;
}
}
55 changes: 55 additions & 0 deletions Codecs/Image/Png/Codec.Png/ChunkHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Media.Common;
using System.Text;

namespace Codec.Png;

public class ChunkHeader : MemorySegment
{
public const int ChunkHeaderLength = 8;

public ChunkHeader()
: this(new byte[ChunkHeaderLength], 0)
{
}

public ChunkHeader(byte[] array, int offset)
: base(array, offset, ChunkHeaderLength)
{
}

public uint Length
{
get => Binary.ReadU32(Array, Offset, Binary.IsLittleEndian);
set => Binary.Write32(Array, Offset, Binary.IsLittleEndian, value);
}

public uint Type
{
get => Binary.ReadU32(Array, Offset + Binary.BytesPerInteger, Binary.IsLittleEndian);
set => Binary.Write32(Array, Offset + Binary.BytesPerInteger, Binary.IsLittleEndian, value);
}

public string Name
{
get { return Encoding.ASCII.GetString(Array, Offset + Binary.BytesPerInteger, Binary.BytesPerInteger); }
set { Encoding.ASCII.GetBytes(value, 0, Binary.BytesPerInteger, Array, Offset + Binary.BytesPerInteger); }
}

/// <summary>
/// Whether the chunk is critical (must be read by all readers) or ancillary (may be ignored).
/// </summary>
public bool IsCritical => char.IsUpper(Name[0]);

/// <summary>
/// A public chunk is one that is defined in the International Standard or is registered in the list of public chunk types maintained by the Registration Authority.
/// Applications can also define private (unregistered) chunk types for their own purposes.
/// </summary>
public bool IsPublic => char.IsUpper(Name[1]);

public bool IsPrivate => char.IsUpper(Name[2]);

/// <summary>
/// Whether the (if unrecognized) chunk is safe to copy.
/// </summary>
public bool IsSafeToCopy => char.IsUpper(Name[3]);
}
26 changes: 24 additions & 2 deletions Codecs/Image/Png/Codec.Png/PngCodec.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Text;
using Media.Codec;
using Media.Codec.Interfaces;
Expand Down Expand Up @@ -28,14 +29,35 @@ public PngCodec()

public int Encode(PngImage image, Stream outputStream)
{
var position = outputStream.Position;
// Implement PNG encoding logic here
throw new NotImplementedException();
image.Save(outputStream);
return (int)(outputStream.Position - position);
}

public PngImage Decode(Stream inputStream)
{
// Implement PNG decoding logic here
throw new NotImplementedException();
return PngImage.FromStream(inputStream);
}

public static IEnumerable<Chunk> ReadChunks(Stream inputStream)
{
// Read and validate the PNG signature
using MemorySegment bytes = new MemorySegment(new byte[Binary.BytesPerLong]);
if (Binary.BytesPerLong != inputStream.Read(bytes.Array, bytes.Offset, bytes.Count))
throw new InvalidDataException("Not enough bytes for PNGSignature.");
ulong signature = Binary.ReadU64(bytes.Array, bytes.Offset, Binary.IsLittleEndian);
if (signature != PngImage.PNGSignature)
throw new InvalidDataException("The provided stream is not a valid PNG file.");

while (inputStream.Position < inputStream.Length)
{
var chunk = Chunk.ReadChunk(inputStream);
if (chunk == null)
yield break;
yield return chunk;
}
}
}
}
94 changes: 44 additions & 50 deletions Codecs/Image/Png/Codec.Png/PngImage.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System.IO.Compression;
using System;
using System.IO.Compression;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks.Sources;
using Media.Codecs.Image;
using Media.Common;

Expand Down Expand Up @@ -57,7 +60,7 @@ private PngImage(ImageFormat imageFormat, int width, int height, MemorySegment d
public static PngImage FromStream(Stream stream)
{
// Read and validate the PNG signature
MemorySegment bytes = new MemorySegment(new byte[Binary.BytesPerLong]);
using MemorySegment bytes = new MemorySegment(new byte[Binary.BytesPerLong]);
if (Binary.BytesPerLong != stream.Read(bytes.Array, bytes.Offset, bytes.Count))
throw new InvalidDataException("Not enough bytes for PNGSignature.");
ulong signature = Binary.ReadU64(bytes.Array, bytes.Offset, Binary.IsLittleEndian);
Expand All @@ -69,17 +72,15 @@ public static PngImage FromStream(Stream stream)
ImageFormat? imageFormat = default;
MemorySegment? dataSegment = default;
byte colorType = default;
ChunkHeader chunkHeader;
while (stream.Position < stream.Length)
{
if (Binary.BytesPerInteger != stream.Read(bytes.Array, bytes.Offset, Binary.BytesPerInteger))
throw new InvalidDataException("Not enough bytes for chunk length.");

int chunkLength = Binary.Read32(bytes.Array, bytes.Offset, Binary.IsLittleEndian);
chunkHeader = new ChunkHeader(bytes.Array, bytes.Offset);

if (Binary.BytesPerInteger != stream.Read(bytes.Array, bytes.Offset, Binary.BytesPerInteger))
throw new InvalidDataException("Not enough bytes for chunk type.");
if (ChunkHeader.ChunkHeaderLength != stream.Read(bytes.Array, bytes.Offset, ChunkHeader.ChunkHeaderLength))
throw new InvalidDataException("Not enough bytes for chunk length.");

string chunkType = Encoding.ASCII.GetString(bytes.Array, bytes.Offset, Binary.BytesPerInteger);
string chunkType = chunkHeader.Name;

if (chunkType == "IHDR")
{
Expand Down Expand Up @@ -111,19 +112,24 @@ public static PngImage FromStream(Stream stream)

// Create the image format based on the IHDR data
imageFormat = CreateImageFormat(bitDepth, colorType);

stream.Seek(Binary.BytesPerInteger, SeekOrigin.Current); // Skip the CRC

}
else if (chunkType == "IDAT")
{
// Read the image data
dataSegment = new MemorySegment(chunkLength);
dataSegment = new MemorySegment(chunkHeader.Length);

if(chunkHeader.Length != stream.Read(dataSegment.Array, dataSegment.Offset, dataSegment.Count))
throw new InvalidDataException("Not enough bytes for IDAT.");

if(chunkLength != stream.Read(dataSegment.Array, dataSegment.Offset, dataSegment.Count))
throw new InvalidDataException("Not enough bytes for IDAT.");
stream.Seek(Binary.BytesPerInteger, SeekOrigin.Current); // Skip the CRC
}
else
{
// Skip the chunk data and CRC
stream.Seek(chunkLength, SeekOrigin.Current);
stream.Seek(chunkHeader.Length + Binary.BytesPerInteger, SeekOrigin.Current);
}
}

Expand All @@ -143,64 +149,52 @@ public void Save(Stream stream)
//Should implement like MarkerReader and MarkerWriter in Codec.Jpeg

// Write the IHDR chunk
WriteChunk(stream, "IHDR", WriteIHDRChunk);
WriteIHDRChunk(stream);

// Write the IDAT chunk
WriteChunk(stream, "IDAT", WriteIDATChunk);
WriteIDATChunk(stream);

// Write the IEND chunk
WriteChunk(stream, "IEND", WriteIENDChunk);
WriteIENDChunk(stream);
}

private void WriteIHDRChunk(Stream stream)
{
stream.Write(Binary.GetBytes(Width, Binary.IsLittleEndian));
stream.Write(Binary.GetBytes(Height, Binary.IsLittleEndian));
stream.WriteByte((byte)ImageFormat.Size);
stream.WriteByte(ColorType);
stream.WriteByte(0); // Compression method
stream.WriteByte(0); // Filter method
stream.WriteByte(0); // Interlace method
}

private void WriteChunk(Stream writer, string chunkType, Action<Stream> writeChunkData)
{
using (MemoryStream ms = new MemoryStream())
{
// Write the chunk data to the MemoryStream
writeChunkData(ms);
byte[] chunkData = ms.ToArray();

// Write the length of the chunk data (big-endian)
writer.Write(BitConverter.GetBytes(chunkData.Length).Reverse().ToArray());

// Write the chunk type
writer.Write(Encoding.ASCII.GetBytes(chunkType));

// Write the chunk data
writer.Write(chunkData);

// Calculate and write the CRC
writer.Write(CalculateCrc(Encoding.ASCII.GetBytes(chunkType).Concat(chunkData)));
}
using var ihdr = new Chunk("IHDR", 13);
var offset = ihdr.Data.Offset;
Binary.Write32(ihdr.Data.Array, offset, Binary.IsLittleEndian, Width);
offset += Binary.BytesPerInteger;
Binary.Write32(ihdr.Data.Array, offset, Binary.IsLittleEndian, Height);
offset += Binary.BytesPerInteger;
Binary.Write8(ihdr.Data.Array, offset++, Binary.IsBigEndian, (byte)ImageFormat.Size);
Binary.Write8(ihdr.Data.Array, offset++, Binary.IsBigEndian, ColorType);
Binary.Write8(ihdr.Data.Array, offset++, Binary.IsBigEndian, 0);
Binary.Write8(ihdr.Data.Array, offset++, Binary.IsBigEndian, 0);
Binary.Write8(ihdr.Data.Array, offset++, Binary.IsBigEndian, 0);
stream.Write(ihdr.Array, ihdr.Offset, ihdr.Count);
}

private void WriteIDATChunk(Stream stream)
private void WriteIDATChunk(Stream stream, CompressionLevel compressionLevel = CompressionLevel.Optimal)
{
using (MemoryStream ms = new MemoryStream())
Chunk idat;
using (MemoryStream ms = new MemoryStream(Data.Count))
{
using (DeflateStream deflateStream = new DeflateStream(ms, CompressionLevel.Optimal, true))
using (DeflateStream deflateStream = new DeflateStream(ms, compressionLevel, true))
{
deflateStream.Write(Data.Array, Data.Offset, Data.Count);
}
ms.Seek(0, SeekOrigin.Begin);
ms.CopyTo(stream);
ms.TryGetBuffer(out var buffer);
idat = new Chunk("IDAT", buffer.Count);
buffer.CopyTo(idat.Data.Array, idat.Data.Offset);
}
stream.Write(idat.Array, idat.Offset, idat.Count);
}

private void WriteIENDChunk(Stream stream)
{
// IEND chunk has no data
var iend = new Chunk("IEND", 0);
stream.Write(iend.Array, iend.Offset, iend.Count);
}

public MemorySegment GetPixelDataAt(int x, int y)
Expand Down
Loading

0 comments on commit f38817e

Please sign in to comment.