Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions PngSharp.Tests/ExifChunkTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using PngSharp.Api;
using PngSharp.Spec.Chunks.IHDR;
using PngSharp.Spec.Chunks.eXIf;
using Xunit;
using static PngSharp.Tests.PngTestHelpers;

namespace PngSharp.Tests;

public class ExifChunkTests
{
[Fact]
public void RoundTrip_Exif_BigEndian_Preserved()
{
byte[] exifData = [0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08];
var png = Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha))
.WithExif(new ExifChunkData { Data = exifData })
.WithPixelData(new byte[2 * 2 * 4])
.Build();

var decoded = RoundTrip(png);

Assert.NotNull(decoded.Exif);
Assert.Equal(exifData, decoded.Exif.Value.Data);
}

[Fact]
public void RoundTrip_Exif_LittleEndian_Preserved()
{
byte[] exifData = [0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00];
var png = Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha))
.WithExif(new ExifChunkData { Data = exifData })
.WithPixelData(new byte[2 * 2 * 4])
.Build();

var decoded = RoundTrip(png);

Assert.NotNull(decoded.Exif);
Assert.Equal(exifData, decoded.Exif.Value.Data);
}

[Fact]
public void Builder_Exif_TooShort_Throws()
{
Assert.Throws<InvalidOperationException>(() =>
Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha))
.WithExif(new ExifChunkData { Data = [0x4D, 0x4D, 0x00] }) // 3 bytes, min is 4
.WithPixelData(new byte[2 * 2 * 4])
.Build());
}

[Fact]
public void Builder_Exif_InvalidByteOrderMark_Throws()
{
Assert.Throws<InvalidOperationException>(() =>
Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha))
.WithExif(new ExifChunkData { Data = [0x00, 0x00, 0x00, 0x00] })
.WithPixelData(new byte[2 * 2 * 4])
.Build());
}

[Fact]
public void RoundTrip_NoExif_ReturnsNull()
{
var png = Png.CreateRgba(2, 2, new byte[2 * 2 * 4]);
var decoded = RoundTrip(png);
Assert.Null(decoded.Exif);
}
}
102 changes: 102 additions & 0 deletions PngSharp.Tests/IccpChunkTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using PngSharp.Api;
using PngSharp.Spec.Chunks.IHDR;
using PngSharp.Spec.Chunks.iCCP;
using PngSharp.Spec.Chunks.sRGB;
using Xunit;
using static PngSharp.Tests.PngTestHelpers;

namespace PngSharp.Tests;

public class IccpChunkTests
{
private static readonly byte[] SampleProfile = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];

[Fact]
public void RoundTrip_Iccp_Preserved()
{
var content = new IccpChunkContent { ProfileName = "TestProfile", RawProfile = SampleProfile };
var iccp = IccpChunkData.Encode(content);
var png = Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha))
.WithIccp(iccp)
.WithPixelData(new byte[2 * 2 * 4])
.Build();

var decoded = RoundTrip(png);

Assert.NotNull(decoded.Iccp);
var decodedContent = decoded.Iccp.Value.Decode();
Assert.Equal("TestProfile", decodedContent.ProfileName);
Assert.Equal(SampleProfile, decodedContent.RawProfile);
}

[Fact]
public void RoundTrip_Iccp_CompressedDataPreserved()
{
var iccp = IccpChunkData.Encode(new IccpChunkContent { ProfileName = "MyProfile", RawProfile = SampleProfile });
var png = Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColor))
.WithIccp(iccp)
.WithPixelData(new byte[2 * 2 * 3])
.Build();

var decoded = RoundTrip(png);

Assert.NotNull(decoded.Iccp);
Assert.Equal(iccp.CompressedProfile, decoded.Iccp.Value.CompressedProfile);
}

[Fact]
public void Builder_Iccp_WithSrgb_Throws()
{
var iccp = IccpChunkData.Encode(new IccpChunkContent { ProfileName = "Test", RawProfile = SampleProfile });
Assert.Throws<InvalidOperationException>(() =>
Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha))
.WithIccp(iccp)
.WithSrgb(new SrgbChunkData { RenderingIntent = RenderingIntent.Perceptual })
.WithPixelData(new byte[2 * 2 * 4])
.Build());
}

[Fact]
public void Builder_Iccp_EmptyProfileName_Throws()
{
Assert.Throws<InvalidOperationException>(() =>
Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha))
.WithIccp(new IccpChunkData { ProfileName = "", CompressedProfile = [1, 2, 3] })
.WithPixelData(new byte[2 * 2 * 4])
.Build());
}

[Fact]
public void Builder_Iccp_ProfileNameTooLong_Throws()
{
Assert.Throws<InvalidOperationException>(() =>
Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha))
.WithIccp(new IccpChunkData { ProfileName = new string('A', 80), CompressedProfile = [1, 2, 3] })
.WithPixelData(new byte[2 * 2 * 4])
.Build());
}

[Fact]
public void Builder_Iccp_EmptyCompressedData_Throws()
{
Assert.Throws<InvalidOperationException>(() =>
Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha))
.WithIccp(new IccpChunkData { ProfileName = "Test", CompressedProfile = [] })
.WithPixelData(new byte[2 * 2 * 4])
.Build());
}

[Fact]
public void RoundTrip_NoIccp_ReturnsNull()
{
var png = Png.CreateRgba(2, 2, new byte[2 * 2 * 4]);
var decoded = RoundTrip(png);
Assert.Null(decoded.Iccp);
}
}
133 changes: 133 additions & 0 deletions PngSharp.Tests/SbitChunkTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using PngSharp.Api;
using PngSharp.Spec.Chunks.IHDR;
using PngSharp.Spec.Chunks.PLTE;
using PngSharp.Spec.Chunks.sBIT;
using Xunit;
using static PngSharp.Tests.PngTestHelpers;

namespace PngSharp.Tests;

public class SbitChunkTests
{
[Fact]
public void RoundTrip_Sbit_TrueColorWithAlpha_Preserved()
{
byte[] sbitData = [8, 8, 8, 8];
var png = Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColorWithAlpha))
.WithSbit(new SbitChunkData { Data = sbitData })
.WithPixelData(new byte[2 * 2 * 4])
.Build();

var decoded = RoundTrip(png);

Assert.NotNull(decoded.Sbit);
Assert.Equal(sbitData, decoded.Sbit.Value.Data);
}

[Fact]
public void RoundTrip_Sbit_TrueColor_Preserved()
{
byte[] sbitData = [5, 6, 5];
var png = Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColor))
.WithSbit(new SbitChunkData { Data = sbitData })
.WithPixelData(new byte[2 * 2 * 3])
.Build();

var decoded = RoundTrip(png);

Assert.NotNull(decoded.Sbit);
Assert.Equal(sbitData, decoded.Sbit.Value.Data);
}

[Fact]
public void RoundTrip_Sbit_Grayscale_Preserved()
{
byte[] sbitData = [5];
var png = Png.Builder()
.WithIhdr(CreateIhdr(ColorType.Grayscale))
.WithSbit(new SbitChunkData { Data = sbitData })
.WithPixelData(new byte[2 * 2])
.Build();

var decoded = RoundTrip(png);

Assert.NotNull(decoded.Sbit);
Assert.Equal(sbitData, decoded.Sbit.Value.Data);
}

[Fact]
public void RoundTrip_Sbit_GrayscaleWithAlpha_Preserved()
{
byte[] sbitData = [5, 8];
var png = Png.Builder()
.WithIhdr(CreateIhdr(ColorType.GrayscaleWithAlpha))
.WithSbit(new SbitChunkData { Data = sbitData })
.WithPixelData(new byte[2 * 2 * 2])
.Build();

var decoded = RoundTrip(png);

Assert.NotNull(decoded.Sbit);
Assert.Equal(sbitData, decoded.Sbit.Value.Data);
}

[Fact]
public void RoundTrip_Sbit_IndexedColor_Preserved()
{
byte[] sbitData = [5, 6, 5];
var png = Png.Builder()
.WithIhdr(CreateIhdr(ColorType.IndexedColor))
.WithPlte(new PlteChunkData { Entries = [255, 0, 0, 0, 255, 0, 0, 0, 255, 128, 128, 128] })
.WithSbit(new SbitChunkData { Data = sbitData })
.WithPixelData(new byte[2 * 2])
.Build();

var decoded = RoundTrip(png);

Assert.NotNull(decoded.Sbit);
Assert.Equal(sbitData, decoded.Sbit.Value.Data);
}

[Fact]
public void Builder_Sbit_WrongSizeForColorType_Throws()
{
Assert.Throws<InvalidOperationException>(() =>
Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColor))
.WithSbit(new SbitChunkData { Data = [8, 8] }) // should be 3 bytes
.WithPixelData(new byte[2 * 2 * 3])
.Build());
}

[Fact]
public void Builder_Sbit_ZeroValue_Throws()
{
Assert.Throws<InvalidOperationException>(() =>
Png.Builder()
.WithIhdr(CreateIhdr(ColorType.TrueColor))
.WithSbit(new SbitChunkData { Data = [0, 8, 8] })
.WithPixelData(new byte[2 * 2 * 3])
.Build());
}

[Fact]
public void Builder_Sbit_ExceedsBitDepth_Throws()
{
Assert.Throws<InvalidOperationException>(() =>
Png.Builder()
.WithIhdr(CreateIhdr(ColorType.Grayscale))
.WithSbit(new SbitChunkData { Data = [9] }) // 8-bit depth, max is 8
.WithPixelData(new byte[2 * 2])
.Build());
}

[Fact]
public void RoundTrip_NoSbit_ReturnsNull()
{
var png = Png.CreateRgba(2, 2, new byte[2 * 2 * 4]);
var decoded = RoundTrip(png);
Assert.Null(decoded.Sbit);
}
}
6 changes: 6 additions & 0 deletions PngSharp/Api/IRawPng.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
using PngSharp.Spec.Chunks.cHRM;
using PngSharp.Spec.Chunks.tIME;
using PngSharp.Spec.Chunks.tRNS;
using PngSharp.Spec.Chunks.sBIT;
using PngSharp.Spec.Chunks.iCCP;
using PngSharp.Spec.Chunks.eXIf;

namespace PngSharp.Api;

Expand All @@ -29,6 +32,9 @@ public interface IRawPng
ChrmChunkData? Chrm { get; }
TimeChunkData? Time { get; }
BkgdChunkData? Bkgd { get; }
SbitChunkData? Sbit { get; }
IccpChunkData? Iccp { get; }
ExifChunkData? Exif { get; }
IReadOnlyList<TextChunk> TxtChunks { get; }
IReadOnlyList<ZTextChunk> ZTxtChunks { get; }
IReadOnlyList<ITextChunk> ITxtChunks { get; }
Expand Down
6 changes: 6 additions & 0 deletions PngSharp/Api/IRawPngBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
using PngSharp.Spec.Chunks.cHRM;
using PngSharp.Spec.Chunks.tIME;
using PngSharp.Spec.Chunks.tRNS;
using PngSharp.Spec.Chunks.sBIT;
using PngSharp.Spec.Chunks.iCCP;
using PngSharp.Spec.Chunks.eXIf;

namespace PngSharp.Api;

Expand All @@ -23,6 +26,9 @@ public interface IRawPngBuilder
IRawPngBuilder WithChrm(ChrmChunkData chrm);
IRawPngBuilder WithTime(TimeChunkData time);
IRawPngBuilder WithBkgd(BkgdChunkData bkgd);
IRawPngBuilder WithSbit(SbitChunkData sbit);
IRawPngBuilder WithIccp(IccpChunkData iccp);
IRawPngBuilder WithExif(ExifChunkData exif);
IRawPngBuilder WithTxtChunk(TextChunk textChunk);
IRawPngBuilder WithZTxtChunk(ZTextChunk textChunk);
IRawPngBuilder WithITxtChunk(ITextChunk textChunk);
Expand Down
3 changes: 3 additions & 0 deletions PngSharp/Api/Png.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public static IRawPng DecodeFromStream(Stream inputStream)
Chrm = decoder.Chrm,
Time = decoder.Time,
Bkgd = decoder.Bkgd,
Sbit = decoder.Sbit,
Iccp = decoder.Iccp,
Exif = decoder.Exif,
TxtChunks = decoder.TxtChunks,
ZTxtChunks = decoder.ZTxtChunks,
ITxtChunks = decoder.ITxtChunks,
Expand Down
6 changes: 6 additions & 0 deletions PngSharp/Decoder/PngDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
using PngSharp.Spec.Chunks.cHRM;
using PngSharp.Spec.Chunks.tIME;
using PngSharp.Spec.Chunks.tRNS;
using PngSharp.Spec.Chunks.sBIT;
using PngSharp.Spec.Chunks.iCCP;
using PngSharp.Spec.Chunks.eXIf;

namespace PngSharp.Decoder;

Expand All @@ -28,6 +31,9 @@ internal sealed class PngDecoder : IDisposable, IAsyncDisposable
public ChrmChunkData? Chrm { get; set; }
public TimeChunkData? Time { get; set; }
public BkgdChunkData? Bkgd { get; set; }
public SbitChunkData? Sbit { get; set; }
public IccpChunkData? Iccp { get; set; }
public ExifChunkData? Exif { get; set; }
public List<TextChunk> TxtChunks { get; } = [];
public List<ZTextChunk> ZTxtChunks { get; } = [];
public List<ITextChunk> ITxtChunks { get; } = [];
Expand Down
Loading
Loading