Browse Source

Merge pull request #717 from carbon/png

Improve PNG CQ
af/merge-core
James Jackson-South 7 years ago
committed by GitHub
parent
commit
5bd7b7abfe
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      src/ImageSharp/Formats/Png/Adam7.cs
  2. 14
      src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
  3. 6
      src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
  4. 6
      src/ImageSharp/Formats/Png/Filters/SubFilter.cs
  5. 2
      src/ImageSharp/Formats/Png/PngBitDepth.cs
  6. 97
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  7. 21
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  8. 38
      src/ImageSharp/Formats/Png/PngHeader.cs
  9. 2
      src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs

56
src/ImageSharp/Formats/Png/Adam7.cs

@ -0,0 +1,56 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Constants and helper methods for the Adam7 interlacing algorithm.
/// </summary>
internal static class Adam7
{
/// <summary>
/// The amount to increment when processing each column per scanline for each interlaced pass.
/// </summary>
public static readonly int[] ColumnIncrement = { 8, 8, 4, 4, 2, 2, 1 };
/// <summary>
/// The index to start at when processing each column per scanline for each interlaced pass.
/// </summary>
public static readonly int[] FirstColumn = { 0, 4, 0, 2, 0, 1, 0 };
/// <summary>
/// The index to start at when processing each row per scanline for each interlaced pass.
/// </summary>
public static readonly int[] FirstRow = { 0, 0, 4, 0, 2, 0, 1 };
/// <summary>
/// The amount to increment when processing each row per scanline for each interlaced pass.
/// </summary>
public static readonly int[] RowIncrement = { 8, 8, 8, 4, 4, 2, 2 };
/// <summary>
/// Returns the correct number of columns for each interlaced pass.
/// </summary>
/// <param name="width">The line width.</param>
/// <param name="passIndex">The current pass index.</param>
/// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ComputeColumns(int width, int passIndex)
{
switch (passIndex)
{
case 0: return (width + 7) / 8;
case 1: return (width + 3) / 8;
case 2: return (width + 3) / 4;
case 3: return (width + 1) / 4;
case 4: return (width + 1) / 2;
case 5: return width / 2;
case 6: return width;
default: throw new ArgumentException($"Not a valid pass index: {passIndex}");
}
}
}
}

14
src/ImageSharp/Formats/Png/Filters/AverageFilter.cs

@ -30,7 +30,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
// Average(x) + floor((Raw(x-bpp)+Prior(x))/2)
int x = 1;
for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) {
for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x)
{
ref byte scan = ref Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref prevBaseRef, x);
scan = (byte)(scan + (above >> 1));
@ -68,7 +69,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
resultBaseRef = 3;
int x = 0;
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) {
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref prevBaseRef, x);
++x;
@ -77,7 +79,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
sum += ImageMaths.FastAbs(unchecked((sbyte)res));
}
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) {
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte left = Unsafe.Add(ref scanBaseRef, xLeft);
byte above = Unsafe.Add(ref prevBaseRef, x);
@ -97,9 +100,6 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
/// <param name="above">The above byte</param>
/// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Average(byte left, byte above)
{
return (left + above) >> 1;
}
private static int Average(byte left, byte above) => (left + above) >> 1;
}
}

6
src/ImageSharp/Formats/Png/Filters/PaethFilter.cs

@ -72,7 +72,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
resultBaseRef = 4;
int x = 0;
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) {
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte above = Unsafe.Add(ref prevBaseRef, x);
++x;
@ -81,7 +82,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
sum += ImageMaths.FastAbs(unchecked((sbyte)res));
}
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) {
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte left = Unsafe.Add(ref scanBaseRef, xLeft);
byte above = Unsafe.Add(ref prevBaseRef, x);

6
src/ImageSharp/Formats/Png/Filters/SubFilter.cs

@ -59,7 +59,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
resultBaseRef = 1;
int x = 0;
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */) {
for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
++x;
ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
@ -67,7 +68,8 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
sum += ImageMaths.FastAbs(unchecked((sbyte)res));
}
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */) {
for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
{
byte scan = Unsafe.Add(ref scanBaseRef, x);
byte prev = Unsafe.Add(ref scanBaseRef, xLeft);
++x;

2
src/ImageSharp/Formats/Png/PngBitDepth.cs

@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Provides enumeration for the available PNG bit depths.
/// </summary>
public enum PngBitDepth
public enum PngBitDepth : byte
{
/// <summary>
/// 1 bit per sample or per palette index (not per pixel).

97
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -37,26 +37,6 @@ namespace SixLabors.ImageSharp.Formats.Png
[PngColorType.RgbWithAlpha] = new byte[] { 8, 16 }
};
/// <summary>
/// The amount to increment when processing each column per scanline for each interlaced pass
/// </summary>
private static readonly int[] Adam7ColumnIncrement = { 8, 8, 4, 4, 2, 2, 1 };
/// <summary>
/// The index to start at when processing each column per scanline for each interlaced pass
/// </summary>
private static readonly int[] Adam7FirstColumn = { 0, 4, 0, 2, 0, 1, 0 };
/// <summary>
/// The index to start at when processing each row per scanline for each interlaced pass
/// </summary>
private static readonly int[] Adam7FirstRow = { 0, 0, 4, 0, 2, 0, 1 };
/// <summary>
/// The amount to increment when processing each row per scanline for each interlaced pass
/// </summary>
private static readonly int[] Adam7RowIncrement = { 8, 8, 8, 4, 4, 2, 2 };
/// <summary>
/// Reusable buffer for reading chunk types.
/// </summary>
@ -150,7 +130,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// The index of the current scanline being processed
/// </summary>
private int currentRow = Adam7FirstRow[0];
private int currentRow = Adam7.FirstRow[0];
/// <summary>
/// The current pass for an interlaced PNG
@ -635,7 +615,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{
while (true)
{
int numColumns = this.ComputeColumnsAdam7(this.pass);
int numColumns = Adam7.ComputeColumns(this.header.Width, this.pass);
if (numColumns == 0)
{
@ -691,11 +671,11 @@ namespace SixLabors.ImageSharp.Formats.Png
}
Span<TPixel> rowSpan = image.GetPixelRowSpan(this.currentRow);
this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, Adam7FirstColumn[this.pass], Adam7ColumnIncrement[this.pass]);
this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, Adam7.FirstColumn[this.pass], Adam7.ColumnIncrement[this.pass]);
this.SwapBuffers();
this.currentRow += Adam7RowIncrement[this.pass];
this.currentRow += Adam7.RowIncrement[this.pass];
}
this.pass++;
@ -703,7 +683,7 @@ namespace SixLabors.ImageSharp.Formats.Png
if (this.pass < 7)
{
this.currentRow = Adam7FirstRow[this.pass];
this.currentRow = Adam7.FirstRow[this.pass];
}
else
{
@ -943,17 +923,9 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="data">The <see cref="T:ReadOnlySpan{byte}"/> containing data.</param>
private void ReadHeaderChunk(PngMetaData pngMetaData, ReadOnlySpan<byte> data)
{
byte bitDepth = data[8];
this.header = new PngHeader(
width: BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)),
height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)),
bitDepth: bitDepth,
colorType: (PngColorType)data[9],
compressionMethod: data[10],
filterMethod: data[11],
interlaceMethod: (PngInterlaceMode)data[12]);
pngMetaData.BitDepth = (PngBitDepth)bitDepth;
this.header = PngHeader.Parse(data);
pngMetaData.BitDepth = (PngBitDepth)this.header.BitDepth;
pngMetaData.ColorType = this.header.ColorType;
}
@ -1062,9 +1034,7 @@ namespace SixLabors.ImageSharp.Formats.Png
return true;
}
int length = this.ReadChunkLength();
if (length == -1)
if (!this.TryReadChunkLength(out int length))
{
chunk = default;
@ -1078,9 +1048,7 @@ namespace SixLabors.ImageSharp.Formats.Png
// That lets us read one byte at a time until we reach a known chunk.
this.currentStream.Position -= 3;
length = this.ReadChunkLength();
if (length == -1)
if (!this.TryReadChunkLength(out length))
{
chunk = default;
@ -1180,14 +1148,9 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </exception>
private PngChunkType ReadChunkType()
{
int numBytes = this.currentStream.Read(this.chunkTypeBuffer, 0, 4);
if (numBytes >= 1 && numBytes <= 3)
{
throw new ImageFormatException("Image stream is not valid!");
}
return (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.chunkTypeBuffer.AsSpan());
return this.currentStream.Read(this.chunkTypeBuffer, 0, 4) == 4
? (PngChunkType)BinaryPrimitives.ReadUInt32BigEndian(this.chunkTypeBuffer.AsSpan())
: throw new ImageFormatException("Invalid PNG data.");
}
/// <summary>
@ -1196,38 +1159,18 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid.
/// </exception>
private int ReadChunkLength()
private bool TryReadChunkLength(out int result)
{
int numBytes = this.currentStream.Read(this.chunkLengthBuffer, 0, 4);
if (numBytes < 4)
if (this.currentStream.Read(this.chunkLengthBuffer, 0, 4) == 4)
{
return -1;
result = BinaryPrimitives.ReadInt32BigEndian(this.chunkLengthBuffer);
return true;
}
return BinaryPrimitives.ReadInt32BigEndian(this.chunkLengthBuffer);
}
result = default;
/// <summary>
/// Returns the correct number of columns for each interlaced pass.
/// </summary>
/// <param name="passIndex">Th current pass index</param>
/// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int ComputeColumnsAdam7(int passIndex)
{
int width = this.header.Width;
switch (passIndex)
{
case 0: return (width + 7) / 8;
case 1: return (width + 3) / 8;
case 2: return (width + 3) / 4;
case 3: return (width + 1) / 4;
case 4: return (width + 1) / 2;
case 5: return width / 2;
case 6: return width;
default: throw new ArgumentException($"Not a valid pass index: {passIndex}");
}
return false;
}
private void SwapBuffers()

21
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -606,16 +606,9 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="header">The <see cref="PngHeader"/>.</param>
private void WriteHeaderChunk(Stream stream, in PngHeader header)
{
BinaryPrimitives.WriteInt32BigEndian(this.chunkDataBuffer.AsSpan(0, 4), header.Width);
BinaryPrimitives.WriteInt32BigEndian(this.chunkDataBuffer.AsSpan(4, 4), header.Height);
header.WriteTo(this.chunkDataBuffer);
this.chunkDataBuffer[8] = header.BitDepth;
this.chunkDataBuffer[9] = (byte)header.ColorType;
this.chunkDataBuffer[10] = header.CompressionMethod;
this.chunkDataBuffer[11] = header.FilterMethod;
this.chunkDataBuffer[12] = (byte)header.InterlaceMethod;
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer, 0, 13);
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer, 0, PngHeader.Size);
}
/// <summary>
@ -697,28 +690,24 @@ namespace SixLabors.ImageSharp.Formats.Png
switch (meta.ResolutionUnits)
{
case PixelResolutionUnit.AspectRatio:
this.chunkDataBuffer[8] = 0;
BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(meta.HorizontalResolution));
BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(meta.VerticalResolution));
break;
case PixelResolutionUnit.PixelsPerInch:
this.chunkDataBuffer[8] = 1; // Per meter
BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(UnitConverter.InchToMeter(meta.HorizontalResolution)));
BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(UnitConverter.InchToMeter(meta.VerticalResolution)));
break;
case PixelResolutionUnit.PixelsPerCentimeter:
this.chunkDataBuffer[8] = 1; // Per meter
BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(UnitConverter.CmToMeter(meta.HorizontalResolution)));
BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(UnitConverter.CmToMeter(meta.VerticalResolution)));
break;
default:
this.chunkDataBuffer[8] = 1; // Per meter
BinaryPrimitives.WriteInt32BigEndian(hResolution, (int)Math.Round(meta.HorizontalResolution));
BinaryPrimitives.WriteInt32BigEndian(vResolution, (int)Math.Round(meta.VerticalResolution));
@ -782,26 +771,22 @@ namespace SixLabors.ImageSharp.Formats.Png
break;
case PngFilterMethod.Sub:
this.sub = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
break;
case PngFilterMethod.Up:
this.up = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
break;
case PngFilterMethod.Average:
this.average = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
break;
case PngFilterMethod.Paeth:
this.paeth = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
break;
case PngFilterMethod.Adaptive:
case PngFilterMethod.Adaptive:
this.sub = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
this.up = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);
this.average = this.memoryAllocator.AllocateManagedByteBuffer(resultLength, AllocationOptions.Clean);

38
src/ImageSharp/Formats/Png/PngHeader.cs

@ -1,6 +1,9 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
@ -8,6 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
internal readonly struct PngHeader
{
public const int Size = 13;
public PngHeader(
int width,
int height,
@ -74,5 +79,38 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Two values are currently defined: 0 (no interlace) or 1 (Adam7 interlace).
/// </summary>
public PngInterlaceMode InterlaceMethod { get; }
/// <summary>
/// Writes the header to the given buffer.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
public void WriteTo(Span<byte> buffer)
{
BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(0, 4), this.Width);
BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(4, 4), this.Height);
buffer[8] = this.BitDepth;
buffer[9] = (byte)this.ColorType;
buffer[10] = this.CompressionMethod;
buffer[11] = this.FilterMethod;
buffer[12] = (byte)this.InterlaceMethod;
}
/// <summary>
/// Parses the PngHeader from the given data buffer.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>The parsed PngHeader.</returns>
public static PngHeader Parse(ReadOnlySpan<byte> data)
{
return new PngHeader(
width: BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)),
height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)),
bitDepth: data[8],
colorType: (PngColorType)data[9],
compressionMethod: data[10],
filterMethod: data[11],
interlaceMethod: (PngInterlaceMode)data[12]);
}
}
}

2
src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs

@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
/// <summary>
/// Delegate to get more data once we've exhausted the current data remaining
/// </summary>
private Func<int> getData;
private readonly Func<int> getData;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.

Loading…
Cancel
Save