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
121 changes: 82 additions & 39 deletions src/ImageSharp/Formats/Png/PngDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
break;
case PngChunkType.FrameData:
{
ValidateFrameDataChunk(chunk);

if (frameCount > this.maxFrames)
{
goto EOF;
Expand Down Expand Up @@ -258,10 +260,10 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
currentFrameControl ??= new FrameControl((uint)this.header.Width, (uint)this.header.Height);
if (image is null)
{
// PLTE and tRNS precede IDAT. Validate the required indexed palette before
// allocating pixel storage, outside recovery from compressed-data errors.
this.AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
this.InitializeImage(metadata, currentFrameControl.Value, out image);

// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
}

this.ReadScanlines(
Expand Down Expand Up @@ -413,6 +415,8 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok

break;
case PngChunkType.FrameData:
ValidateFrameDataChunk(chunk);

if (frameCount > this.maxFrames)
{
// Must skip the chunk data even when we've hit maxFrames, because TryReadChunk
Expand Down Expand Up @@ -454,7 +458,7 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok
InitializeFrameMetadata(framesMetadata, currentFrameControl.Value);

// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
this.AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
}

this.SkipChunkDataAndCrc(chunk);
Expand Down Expand Up @@ -1331,15 +1335,22 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(
}

/// <summary>
/// Decodes and assigns the color palette to the metadata
/// Assigns the palette to metadata and rejects indexed images without a complete palette entry.
/// </summary>
/// <param name="palette">The palette buffer.</param>
/// <param name="alpha">The alpha palette buffer.</param>
/// <param name="pngMetadata">The png metadata.</param>
private static void AssignColorPalette(ReadOnlySpan<byte> palette, ReadOnlySpan<byte> alpha, PngMetadata pngMetadata)
private void AssignColorPalette(ReadOnlySpan<byte> palette, ReadOnlySpan<byte> alpha, PngMetadata pngMetadata)
{
if (palette.Length == 0)
// Indexed samples require at least one complete RGB entry. Reject a missing table before scanline
// recovery can suppress the error in IgnoreImageData mode. Other color types have an optional palette.
if (palette.Length < Unsafe.SizeOf<Rgb24>())
{
if (this.pngColorType is PngColorType.Palette)
{
PngThrowHelper.ThrowMissingPalette();
}

return;
}

Expand Down Expand Up @@ -1380,9 +1391,11 @@ private void AssignTransparentMarkers(ReadOnlySpan<byte> alpha, PngMetadata pngM
{
if (this.header.BitDepth == 16)
{
ushort rc = BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2]);
ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2));
ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2));
// tRNS samples are stored big-endian like all other PNG multi-byte values,
// matching the byte order used for the 16-bit samples in the image data.
ushort rc = BinaryPrimitives.ReadUInt16BigEndian(alpha[..2]);
ushort gc = BinaryPrimitives.ReadUInt16BigEndian(alpha.Slice(2, 2));
ushort bc = BinaryPrimitives.ReadUInt16BigEndian(alpha.Slice(4, 2));

pngMetadata.TransparentColor = Color.FromPixel(new Rgb48(rc, gc, bc));
return;
Expand All @@ -1400,7 +1413,7 @@ private void AssignTransparentMarkers(ReadOnlySpan<byte> alpha, PngMetadata pngM
{
if (this.header.BitDepth == 16)
{
pngMetadata.TransparentColor = Color.FromPixel(new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2])));
pngMetadata.TransparentColor = Color.FromPixel(new L16(BinaryPrimitives.ReadUInt16BigEndian(alpha[..2])));
}
else
{
Expand Down Expand Up @@ -2183,24 +2196,32 @@ private int ReadNextDataChunk()

Span<byte> buffer = stackalloc byte[20];

int length = this.currentStream.Read(buffer, 0, 4);
if (length == 0)
// The compressed stream is the concatenation of the payloads of all consecutive data chunks, and the
// specification permits zero-length data chunks. A return value of 0 tells the inflater that the
// compressed stream has ended, so empty chunks are skipped rather than reported.
while (true)
{
return 0;
}
// Skip the CRC of the previous chunk.
int length = this.currentStream.Read(buffer, 0, 4);
if (length == 0 || !this.TryReadChunk(buffer, out PngChunk chunk))
{
return 0;
}

if (this.TryReadChunk(buffer, out PngChunk chunk))
{
if (chunk.Type is PngChunkType.Data or PngChunkType.FrameData)
if (chunk.Type is not PngChunkType.Data)
{
chunk.Data?.Dispose();
return chunk.Length;
// Only IDAT payloads belong to this stream. Frame data must return to the outer loop
// for its own framing checks, outside recovery from compressed-data errors.
this.nextChunk = chunk;
return 0;
}

this.nextChunk = chunk;
chunk.Data?.Dispose();
if (chunk.Length > 0)
{
return chunk.Length;
}
}

return 0;
}

/// <summary>
Expand All @@ -2216,26 +2237,34 @@ private int ReadNextFrameDataChunk()

Span<byte> buffer = stackalloc byte[20];

int length = this.currentStream.Read(buffer, 0, 4);
if (length == 0)
{
return 0;
}

if (this.TryReadChunk(buffer, out PngChunk chunk))
// As with data chunks, frame data chunks with no payload after the sequence number are valid and are
// skipped, so that a return value of 0 only ever means that the frame's compressed stream has ended.
while (true)
{
if (chunk.Type is PngChunkType.FrameData)
// Skip the CRC of the previous chunk.
int length = this.currentStream.Read(buffer, 0, 4);
if (length == 0 || !this.TryReadChunk(buffer, out PngChunk chunk))
{
chunk.Data?.Dispose();
return 0;
}

this.currentStream.Position += 4; // Skip sequence number
return chunk.Length - 4;
if (chunk.Type is not PngChunkType.FrameData || chunk.Length < sizeof(uint))
{
// A short fdAT cannot supply compressed data. Let the outer chunk loop validate it outside
// image-data recovery, just as that loop handles the next frame control or end chunk.
this.nextChunk = chunk;
return 0;
}

this.nextChunk = chunk;
}
chunk.Data?.Dispose();

return 0;
this.currentStream.Position += 4; // Skip sequence number
int dataLength = chunk.Length - 4;
if (dataLength > 0)
{
return dataLength;
}
}
}

/// <summary>
Expand All @@ -2249,20 +2278,34 @@ private void SkipRemainingFrameDataChunks(Span<byte> buffer)
{
while (this.TryReadChunk(buffer, out PngChunk chunk))
{
if (chunk.Type is PngChunkType.FrameData)
if (chunk.Type is PngChunkType.FrameData && chunk.Length >= sizeof(uint))
{
chunk.Data?.Dispose();
this.SkipChunkDataAndCrc(chunk);
}
else
{
// Not a FrameData chunk; store it so the next TryReadChunk call returns it.
// Preserve other chunks, including malformed fdAT, for validation by the outer chunk loop.
this.nextChunk = chunk;
return;
}
}
}

/// <summary>
/// Checks the required sequence number before consuming a frame data chunk.
/// </summary>
/// <param name="chunk">The frame data chunk.</param>
private static void ValidateFrameDataChunk(in PngChunk chunk)
{
// A missing sequence number is a structural error in every integrity mode. Call this from the outer
// chunk loop, outside the scope that permits recovery from compressed image-data errors.
if (chunk.Length < sizeof(uint))
{
PngThrowHelper.ThrowInvalidImageContentException("The frame data chunk does not contain a sequence number.");
}
}

/// <summary>
/// Reads a chunk from the stream.
/// </summary>
Expand Down
9 changes: 5 additions & 4 deletions src/ImageSharp/Formats/Png/PngEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1247,10 +1247,11 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
{
if (this.use16Bit)
{
// tRNS samples are stored big-endian like all other PNG multi-byte values.
Rgb48 rgb = pngMetadata.TransparentColor.Value.ToPixel<Rgb48>();
BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B);
BinaryPrimitives.WriteUInt16BigEndian(alpha, rgb.R);
BinaryPrimitives.WriteUInt16BigEndian(alpha.Slice(2, 2), rgb.G);
BinaryPrimitives.WriteUInt16BigEndian(alpha.Slice(4, 2), rgb.B);

this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6);
}
Expand All @@ -1269,7 +1270,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata)
if (this.use16Bit)
{
L16 l16 = pngMetadata.TransparentColor.Value.ToPixel<L16>();
BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue);
BinaryPrimitives.WriteUInt16BigEndian(alpha, l16.PackedValue);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2);
}
else
Expand Down
Loading
Loading