Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

APNG support #2511

Merged
merged 25 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
45f6f5b
Implement APNG decoder
Poker-sang Aug 12, 2023
7b6c32d
implement APNG encoder
Poker-sang Aug 12, 2023
01caebd
Add UnitTest
Poker-sang Aug 13, 2023
6f9525e
Merge branch 'main' into main
Poker-sang Aug 15, 2023
64a0ff0
Fix simple issues in review
Poker-sang Aug 17, 2023
e7eed49
Merge branch 'main' of github.com:Poker-sang/ImageSharp
Poker-sang Aug 17, 2023
bf308b7
Merge branch 'main' into main
Poker-sang Aug 17, 2023
6e54822
Fix review
Poker-sang Aug 17, 2023
c253f39
Fix offset
Poker-sang Aug 17, 2023
1464064
Fix: replace lambda with method
Poker-sang Aug 17, 2023
316a839
Optimize code
Poker-sang Aug 18, 2023
a6b8abe
remove set to null from disposal
Poker-sang Aug 22, 2023
b0dc908
Merge branch 'main' into main
JimBobSquarePants Sep 12, 2023
5a711a8
Merge remote-tracking branch 'upstream/main'
JimBobSquarePants Oct 17, 2023
aada974
Refactor and cleanup
JimBobSquarePants Oct 17, 2023
564c3d1
Fix encoding
JimBobSquarePants Oct 17, 2023
3bc12e4
Fix failing tests
JimBobSquarePants Oct 17, 2023
0385ad0
Fix header bit depth assignment.
JimBobSquarePants Oct 19, 2023
5ed6f24
Reintroduce scanline optimizations
JimBobSquarePants Oct 23, 2023
bc5b6c5
Add alpha blending support
JimBobSquarePants Oct 23, 2023
8455275
Handle disposal methods.
JimBobSquarePants Oct 23, 2023
56588d3
Use region for alpha blending
JimBobSquarePants Oct 23, 2023
66f444d
Fix alpha blending and add tests
JimBobSquarePants Oct 30, 2023
14a95a8
Rename properties and add metadata tests
JimBobSquarePants Oct 31, 2023
b4e9805
Update PngDecoderTests.cs
JimBobSquarePants Oct 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ namespace SixLabors.ImageSharp.Formats.Png.Chunks;
/// </summary>
public int YOffset { get; }

/// <summary>
/// Gets the X limit at which to render the following frame
/// </summary>
public uint XLimit => (uint)(this.XOffset + this.Width);

/// <summary>
/// Gets the Y limit at which to render the following frame
/// </summary>
public uint YLimit => (uint)(this.YOffset + this.Height);

/// <summary>
/// Gets the frame delay fraction numerator
/// </summary>
Expand Down Expand Up @@ -104,14 +114,14 @@ public void Validate(PngHeader hdr)
PngThrowHelper.ThrowInvalidParameter(this.Height, "Expected > 0");
}

if (this.XOffset + this.Width > hdr.Width)
if (this.XLimit > hdr.Width)
{
PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The sum > {nameof(PngHeader)}.{nameof(PngHeader.Width)}");
PngThrowHelper.ThrowInvalidParameter(this.XOffset, this.Width, $"The sum of them > {nameof(PngHeader)}.{nameof(PngHeader.Width)}");
}

if (this.YOffset + this.Height > hdr.Height)
if (this.YLimit > hdr.Height)
{
PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, "The sum > PngHeader.Height");
PngThrowHelper.ThrowInvalidParameter(this.YOffset, this.Height, $"The sum of them > {nameof(PngHeader)}.{nameof(PngHeader.Height)}");
}
}

Expand Down
73 changes: 41 additions & 32 deletions src/ImageSharp/Formats/Png/PngDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
this.currentStream.Position += 4; // Skip sequence number
return length - 4;
},
lastFrameControl.Value,
cancellationToken);
lastFrameControl = null;
break;
Expand All @@ -237,7 +238,9 @@ public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken
this.InitializeImage(metadata, lastFrameControl, out image);
}

this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, cancellationToken);
FrameControl frameControl = lastFrameControl ?? new(0, this.header.Width, this.header.Height, 0, 0, 0, 0, default, default);

this.ReadScanlines(chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, frameControl, cancellationToken);
lastFrameControl = null;
break;
case PngChunkType.Palette:
Expand Down Expand Up @@ -682,8 +685,9 @@ private int CalculateScanlineLength(int width)
/// <param name="image"> The pixel data.</param>
/// <param name="pngMetadata">The png metadata</param>
/// <param name="getData">A delegate to get more data from the inner stream for <see cref="ZlibInflateStream"/>.</param>
/// <param name="frameControl">The frame control</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void ReadScanlines<TPixel>(int chunkLength, ImageFrame<TPixel> image, PngMetadata pngMetadata, Func<int> getData, CancellationToken cancellationToken)
private void ReadScanlines<TPixel>(int chunkLength, ImageFrame<TPixel> image, PngMetadata pngMetadata, Func<int> getData, FrameControl frameControl, CancellationToken cancellationToken)
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
where TPixel : unmanaged, IPixel<TPixel>
{
using ZlibInflateStream deframeStream = new(this.currentStream, getData);
Expand All @@ -692,28 +696,29 @@ private void ReadScanlines<TPixel>(int chunkLength, ImageFrame<TPixel> image, Pn

if (this.header.InterlaceMethod is PngInterlaceMode.Adam7)
{
this.DecodeInterlacedPixelData(dataStream, image, pngMetadata, cancellationToken);
this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, cancellationToken);
}
else
{
this.DecodePixelData(dataStream, image, pngMetadata, cancellationToken);
this.DecodePixelData(frameControl, dataStream, image, pngMetadata, cancellationToken);
}
}

/// <summary>
/// Decodes the raw pixel data row by row
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="image">The image to decode to.</param>
/// <param name="pngMetadata">The png metadata</param>
/// <param name="cancellationToken">The CancellationToken</param>
private void DecodePixelData<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
private void DecodePixelData<TPixel>(FrameControl frameControl, DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int currentRow = Adam7.FirstRow[0];
int currentRow = frameControl.YOffset;
int currentRowBytesRead = 0;
int height = image.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? frameMetadata) ? frameMetadata.Height : this.header.Height;
int height = frameControl.Height;
while (currentRow < height)
Poker-sang marked this conversation as resolved.
Show resolved Hide resolved
{
cancellationToken.ThrowIfCancellationRequested();
Expand Down Expand Up @@ -757,7 +762,7 @@ private void DecodePixelData<TPixel>(DeflateStream compressedStream, ImageFrame<
break;
}

this.ProcessDefilteredScanline(currentRow, scanlineSpan, image, pngMetadata);
this.ProcessDefilteredScanline(frameControl, currentRow, scanlineSpan, image, pngMetadata);

this.SwapScanlineBuffers();
currentRow++;
Expand All @@ -769,23 +774,19 @@ private void DecodePixelData<TPixel>(DeflateStream compressedStream, ImageFrame<
/// <see href="https://github.com/juehv/DentalImageViewer/blob/8a1a4424b15d6cc453b5de3f273daf3ff5e3a90d/DentalImageViewer/lib/jiu-0.14.3/net/sourceforge/jiu/codecs/PNGCodec.java"/>
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="image">The current image.</param>
/// <param name="pngMetadata">The png metadata.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void DecodeInterlacedPixelData<TPixel>(DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
private void DecodeInterlacedPixelData<TPixel>(FrameControl frameControl, DeflateStream compressedStream, ImageFrame<TPixel> image, PngMetadata pngMetadata, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
int currentRow = Adam7.FirstRow[0];
int currentRow = Adam7.FirstRow[0] + frameControl.YOffset;
int currentRowBytesRead = 0;
int pass = 0;
int width = this.header.Width;
int height = this.header.Height;
if (image.Metadata.TryGetPngFrameMetadata(out PngFrameMetadata? frameMetadata))
{
width = frameMetadata.Width;
height = frameMetadata.Height;
}
int width = frameControl.Width;
int height = frameControl.Height;

Buffer2D<TPixel> imageBuffer = image.PixelBuffer;
while (true)
Expand Down Expand Up @@ -848,7 +849,7 @@ private void DecodeInterlacedPixelData<TPixel>(DeflateStream compressedStream, I
}

Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(currentRow);
this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]);
this.ProcessInterlacedDefilteredScanline(frameControl, this.scanline.GetSpan(), rowSpan, pngMetadata, pixelOffset: Adam7.FirstColumn[pass], increment: Adam7.ColumnIncrement[pass]);

this.SwapScanlineBuffers();

Expand All @@ -874,11 +875,12 @@ private void DecodeInterlacedPixelData<TPixel>(DeflateStream compressedStream, I
/// Processes the de-filtered scanline filling the image pixel data
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="currentRow">The index of the current scanline being processed.</param>
/// <param name="defilteredScanline">The de-filtered scanline</param>
/// <param name="pixels">The image</param>
/// <param name="pngMetadata">The png metadata.</param>
private void ProcessDefilteredScanline<TPixel>(int currentRow, ReadOnlySpan<byte> defilteredScanline, ImageFrame<TPixel> pixels, PngMetadata pngMetadata)
private void ProcessDefilteredScanline<TPixel>(FrameControl frameControl, int currentRow, ReadOnlySpan<byte> defilteredScanline, ImageFrame<TPixel> pixels, PngMetadata pngMetadata)
where TPixel : unmanaged, IPixel<TPixel>
{
Span<TPixel> rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(currentRow);
Expand All @@ -902,7 +904,8 @@ private void ProcessDefilteredScanline<TPixel>(int currentRow, ReadOnlySpan<byte
{
case PngColorType.Grayscale:
PngScanlineProcessor.ProcessGrayscaleScanline(
this.header,
this.header.BitDepth,
frameControl,
scanlineSpan,
rowSpan,
pngMetadata.HasTransparency,
Expand All @@ -913,7 +916,8 @@ private void ProcessDefilteredScanline<TPixel>(int currentRow, ReadOnlySpan<byte

case PngColorType.GrayscaleWithAlpha:
PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline(
this.header,
this.header.BitDepth,
frameControl,
scanlineSpan,
rowSpan,
(uint)this.bytesPerPixel,
Expand All @@ -923,7 +927,7 @@ private void ProcessDefilteredScanline<TPixel>(int currentRow, ReadOnlySpan<byte

case PngColorType.Palette:
PngScanlineProcessor.ProcessPaletteScanline(
this.header,
frameControl,
scanlineSpan,
rowSpan,
this.palette,
Expand All @@ -933,8 +937,8 @@ private void ProcessDefilteredScanline<TPixel>(int currentRow, ReadOnlySpan<byte

case PngColorType.Rgb:
PngScanlineProcessor.ProcessRgbScanline(
this.configuration,
this.header,
this.header.BitDepth,
frameControl,
scanlineSpan,
rowSpan,
this.bytesPerPixel,
Expand All @@ -947,8 +951,8 @@ private void ProcessDefilteredScanline<TPixel>(int currentRow, ReadOnlySpan<byte

case PngColorType.RgbWithAlpha:
PngScanlineProcessor.ProcessRgbaScanline(
this.configuration,
this.header,
this.header.BitDepth,
frameControl,
scanlineSpan,
rowSpan,
this.bytesPerPixel,
Expand All @@ -967,12 +971,13 @@ private void ProcessDefilteredScanline<TPixel>(int currentRow, ReadOnlySpan<byte
/// Processes the interlaced de-filtered scanline filling the image pixel data
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frameControl">The frame control</param>
/// <param name="defilteredScanline">The de-filtered scanline</param>
/// <param name="rowSpan">The current image row.</param>
/// <param name="pngMetadata">The png metadata.</param>
/// <param name="pixelOffset">The column start index. Always 0 for none interlaced images.</param>
/// <param name="increment">The column increment. Always 1 for none interlaced images.</param>
private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScanline, Span<TPixel> rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1)
private void ProcessInterlacedDefilteredScanline<TPixel>(FrameControl frameControl, ReadOnlySpan<byte> defilteredScanline, Span<TPixel> rowSpan, PngMetadata pngMetadata, int pixelOffset = 0, int increment = 1)
where TPixel : unmanaged, IPixel<TPixel>
{
// Trim the first marker byte from the buffer
Expand All @@ -994,7 +999,8 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi
{
case PngColorType.Grayscale:
PngScanlineProcessor.ProcessInterlacedGrayscaleScanline(
this.header,
this.header.BitDepth,
frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
Expand All @@ -1007,7 +1013,8 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi

case PngColorType.GrayscaleWithAlpha:
PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline(
this.header,
this.header.BitDepth,
frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
Expand All @@ -1019,7 +1026,7 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi

case PngColorType.Palette:
PngScanlineProcessor.ProcessInterlacedPaletteScanline(
this.header,
frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
Expand All @@ -1031,7 +1038,8 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi

case PngColorType.Rgb:
PngScanlineProcessor.ProcessInterlacedRgbScanline(
this.header,
this.header.BitDepth,
frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
Expand All @@ -1046,7 +1054,8 @@ private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defi

case PngColorType.RgbWithAlpha:
PngScanlineProcessor.ProcessInterlacedRgbaScanline(
this.header,
this.header.BitDepth,
frameControl,
scanlineSpan,
rowSpan,
(uint)pixelOffset,
Expand Down