Browse Source

Merge branch 'master' into sw/fake-async-codecs

pull/1196/head
James Jackson-South 6 years ago
parent
commit
84c7958e23
  1. 8
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs
  2. 4
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  3. 17
      src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
  4. 44
      src/ImageSharp/Formats/Png/PngChunkFilter.cs
  5. 13
      src/ImageSharp/Formats/Png/PngEncoder.cs
  6. 95
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  7. 12
      src/ImageSharp/Formats/Png/PngEncoderOptions.cs
  8. 7
      src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
  9. 22
      src/ImageSharp/Formats/Png/PngTransparentColorMode.cs
  10. 16
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs
  11. 328
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs
  12. 226
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  13. 16
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs

8
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the GNU Affero General Public License, Version 3.
using System;
@ -28,12 +28,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Gets or sets the number of scanlines within the frame.
/// </summary>
public short Scanlines { get; set; }
public int Scanlines { get; set; }
/// <summary>
/// Gets or sets the number of samples per scanline.
/// </summary>
public short SamplesPerLine { get; set; }
public int SamplesPerLine { get; set; }
/// <summary>
/// Gets or sets the number of components within a frame. In progressive frames this value can range from only 1 to 4.
@ -105,4 +105,4 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
}
}
}
}
}

4
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -887,8 +887,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
Extended = frameMarker.Marker == JpegConstants.Markers.SOF1,
Progressive = frameMarker.Marker == JpegConstants.Markers.SOF2,
Precision = this.temp[0],
Scanlines = (short)((this.temp[1] << 8) | this.temp[2]),
SamplesPerLine = (short)((this.temp[3] << 8) | this.temp[4]),
Scanlines = (this.temp[1] << 8) | this.temp[2],
SamplesPerLine = (this.temp[3] << 8) | this.temp[4],
ComponentCount = this.temp[5]
};

17
src/ImageSharp/Formats/Png/IPngEncoderOptions.cs

@ -57,5 +57,22 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets a value indicating whether this instance should write an Adam7 interlaced image.
/// </summary>
PngInterlaceMode? InterlaceMethod { get; }
/// <summary>
/// Gets a value indicating whether the metadata should be ignored when the image is being encoded.
/// When set to true, all ancillary chunks will be skipped.
/// </summary>
bool IgnoreMetadata { get; }
/// <summary>
/// Gets the chunk filter method. This allows to filter ancillary chunks.
/// </summary>
PngChunkFilter? ChunkFilter { get; }
/// <summary>
/// Gets a value indicating whether fully transparent pixels that may contain R, G, B values which are not 0,
/// should be converted to transparent black, which can yield in better compression in some cases.
/// </summary>
PngTransparentColorMode TransparentColorMode { get; }
}
}

44
src/ImageSharp/Formats/Png/PngChunkFilter.cs

@ -0,0 +1,44 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the GNU Affero General Public License, Version 3.
using System;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Provides enumeration of available PNG optimization methods.
/// </summary>
[Flags]
public enum PngChunkFilter
{
/// <summary>
/// With the None filter, all chunks will be written.
/// </summary>
None = 0,
/// <summary>
/// Excludes the physical dimension information chunk from encoding.
/// </summary>
ExcludePhysicalChunk = 1 << 0,
/// <summary>
/// Excludes the gamma information chunk from encoding.
/// </summary>
ExcludeGammaChunk = 1 << 1,
/// <summary>
/// Excludes the eXIf chunk from encoding.
/// </summary>
ExcludeExifChunk = 1 << 2,
/// <summary>
/// Excludes the tTXt, iTXt or zTXt chunk from encoding.
/// </summary>
ExcludeTextChunks = 1 << 3,
/// <summary>
/// All ancillary chunks will be excluded.
/// </summary>
ExcludeAll = ~None
}
}

13
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -35,14 +35,21 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <inheritdoc/>
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
/// <inheritdoc/>
public byte Threshold { get; set; } = byte.MaxValue;
/// <inheritdoc/>
public PngInterlaceMode? InterlaceMethod { get; set; }
/// <inheritdoc/>
public PngChunkFilter? ChunkFilter { get; set; }
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }
/// <inheritdoc/>
public PngTransparentColorMode TransparentColorMode { get; set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>

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

@ -166,10 +166,18 @@ namespace SixLabors.ImageSharp.Formats.Png
this.height = image.Height;
ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetPngMetadata();
PngMetadata pngMetadata = metadata.GetFormatMetadata(PngFormat.Instance);
PngEncoderOptionsHelpers.AdjustOptions<TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
IndexedImageFrame<TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
Image<TPixel> clonedImage = null;
bool clearTransparency = this.options.TransparentColorMode == PngTransparentColorMode.Clear;
if (clearTransparency)
{
clonedImage = image.Clone();
ClearTransparentPixels(clonedImage);
}
IndexedImageFrame<TPixel> quantized = this.CreateQuantizedImage(image, clonedImage);
stream.Write(PngConstants.HeaderBytes);
@ -180,11 +188,13 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
this.WriteDataChunks(image.Frames.RootFrame, quantized, stream);
this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
this.WriteEndChunk(stream);
stream.Flush();
quantized?.Dispose();
clonedImage?.Dispose();
}
/// <inheritdoc />
@ -205,6 +215,55 @@ namespace SixLabors.ImageSharp.Formats.Png
this.filterBuffer = null;
}
/// <summary>
/// Convert transparent pixels, to transparent black pixels, which can yield to better compression in some cases.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The cloned image where the transparent pixels will be changed.</param>
private static void ClearTransparentPixels<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 rgba32 = default;
for (int y = 0; y < image.Height; y++)
{
Span<TPixel> span = image.GetPixelRowSpan(y);
for (int x = 0; x < image.Width; x++)
{
span[x].ToRgba32(ref rgba32);
if (rgba32.A == 0)
{
span[x].FromRgba32(Color.Transparent);
}
}
}
}
/// <summary>
/// Creates the quantized image and sets calculates and sets the bit depth.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The image to quantize.</param>
/// <param name="clonedImage">Cloned image with transparent pixels are changed to black.</param>
/// <returns>The quantized image.</returns>
private IndexedImageFrame<TPixel> CreateQuantizedImage<TPixel>(Image<TPixel> image, Image<TPixel> clonedImage)
where TPixel : unmanaged, IPixel<TPixel>
{
IndexedImageFrame<TPixel> quantized;
if (this.options.TransparentColorMode == PngTransparentColorMode.Clear)
{
quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, clonedImage);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
}
else
{
quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, quantized);
}
return quantized;
}
/// <summary>Collects a row of grayscale pixels.</summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="rowSpan">The image row span.</param>
@ -627,6 +686,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="meta">The image metadata.</param>
private void WritePhysicalChunk(Stream stream, ImageMetadata meta)
{
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludePhysicalChunk) == PngChunkFilter.ExcludePhysicalChunk)
{
return;
}
PhysicalChunkData.FromMetadata(meta).WriteTo(this.chunkDataBuffer);
this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer, 0, PhysicalChunkData.Size);
@ -639,6 +703,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="meta">The image metadata.</param>
private void WriteExifChunk(Stream stream, ImageMetadata meta)
{
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeExifChunk) == PngChunkFilter.ExcludeExifChunk)
{
return;
}
if (meta.ExifProfile is null || meta.ExifProfile.Values.Count == 0)
{
return;
@ -656,6 +725,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="meta">The image metadata.</param>
private void WriteTextChunks(Stream stream, PngMetadata meta)
{
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
{
return;
}
const int MaxLatinCode = 255;
for (int i = 0; i < meta.TextData.Count; i++)
{
@ -748,6 +822,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
private void WriteGammaChunk(Stream stream)
{
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeGammaChunk) == PngChunkFilter.ExcludeGammaChunk)
{
return;
}
if (this.options.Gamma > 0)
{
// 4-byte unsigned integer of gamma * 100,000.
@ -817,7 +896,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The image.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param>
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream)
private void WriteDataChunks<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
byte[] buffer;
@ -915,8 +994,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
private void EncodePixels<TPixel>(Image<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
int bytesPerScanline = this.CalculateScanlineLength(this.width);
int resultLength = bytesPerScanline + 1;
@ -939,7 +1018,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="pixels">The pixels.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7Pixels<TPixel>(ImageFrame<TPixel> pixels, ZlibDeflateStream deflateStream)
private void EncodeAdam7Pixels<TPixel>(Image<TPixel> pixels, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = pixels.Width;

12
src/ImageSharp/Formats/Png/PngEncoderOptions.cs

@ -29,6 +29,9 @@ namespace SixLabors.ImageSharp.Formats.Png
this.Quantizer = source.Quantizer;
this.Threshold = source.Threshold;
this.InterlaceMethod = source.InterlaceMethod;
this.ChunkFilter = source.ChunkFilter;
this.IgnoreMetadata = source.IgnoreMetadata;
this.TransparentColorMode = source.TransparentColorMode;
}
/// <inheritdoc/>
@ -57,5 +60,14 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <inheritdoc/>
public PngInterlaceMode? InterlaceMethod { get; set; }
/// <inheritdoc/>
public PngChunkFilter? ChunkFilter { get; set; }
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }
/// <inheritdoc/>
public PngTransparentColorMode TransparentColorMode { get; set; }
}
}

7
src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs

@ -40,6 +40,11 @@ namespace SixLabors.ImageSharp.Formats.Png
use16Bit = options.BitDepth == PngBitDepth.Bit16;
bytesPerPixel = CalculateBytesPerPixel(options.ColorType, use16Bit);
if (options.IgnoreMetadata)
{
options.ChunkFilter = PngChunkFilter.ExcludeAll;
}
// Ensure we are not allowing impossible combinations.
if (!PngConstants.ColorTypes.ContainsKey(options.ColorType.Value))
{
@ -89,11 +94,9 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="options">The options.</param>
/// <param name="image">The image.</param>
/// <param name="quantizedFrame">The quantized frame.</param>
public static byte CalculateBitDepth<TPixel>(
PngEncoderOptions options,
Image<TPixel> image,
IndexedImageFrame<TPixel> quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel>
{

22
src/ImageSharp/Formats/Png/PngTransparentColorMode.cs

@ -0,0 +1,22 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the GNU Affero General Public License, Version 3.
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Enum indicating how the transparency should be handled on encoding.
/// </summary>
public enum PngTransparentColorMode
{
/// <summary>
/// The transparency will be kept as is.
/// </summary>
Preserve = 0,
/// <summary>
/// Converts fully transparent pixels that may contain R, G, B values which are not 0,
/// to transparent black, which can yield in better compression in some cases.
/// </summary>
Clear = 1,
}
}

16
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeHelper.cs

@ -72,7 +72,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
// case ResizeMode.Stretch:
default:
return (new Size(width, height), new Rectangle(0, 0, width, height));
return (new Size(Sanitize(width), Sanitize(height)), new Rectangle(0, 0, Sanitize(width), Sanitize(height)));
}
}
@ -143,7 +143,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
// Target image width and height can be different to the rectangle width and height.
return (new Size(width, height), new Rectangle(targetX, targetY, targetWidth, targetHeight));
return (new Size(Sanitize(width), Sanitize(height)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight)));
}
// Switch to pad mode to downscale and calculate from there.
@ -253,7 +253,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
// Target image width and height can be different to the rectangle width and height.
return (new Size(width, height), new Rectangle(targetX, targetY, targetWidth, targetHeight));
return (new Size(Sanitize(width), Sanitize(height)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight)));
}
private static (Size, Rectangle) CalculateMaxRectangle(
@ -282,7 +282,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
// Replace the size to match the rectangle.
return (new Size(targetWidth, targetHeight), new Rectangle(0, 0, targetWidth, targetHeight));
return (new Size(Sanitize(targetWidth), Sanitize(targetHeight)), new Rectangle(0, 0, Sanitize(targetWidth), Sanitize(targetHeight)));
}
private static (Size, Rectangle) CalculateMinRectangle(
@ -330,7 +330,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
// Replace the size to match the rectangle.
return (new Size(targetWidth, targetHeight), new Rectangle(0, 0, targetWidth, targetHeight));
return (new Size(Sanitize(targetWidth), Sanitize(targetHeight)), new Rectangle(0, 0, Sanitize(targetWidth), Sanitize(targetHeight)));
}
private static (Size, Rectangle) CalculatePadRectangle(
@ -398,7 +398,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
// Target image width and height can be different to the rectangle width and height.
return (new Size(width, height), new Rectangle(targetX, targetY, targetWidth, targetHeight));
return (new Size(Sanitize(width), Sanitize(height)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight)));
}
private static (Size, Rectangle) CalculateManualRectangle(
@ -419,9 +419,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int targetHeight = targetRectangle.Height > 0 ? targetRectangle.Height : height;
// Target image width and height can be different to the rectangle width and height.
return (new Size(width, height), new Rectangle(targetX, targetY, targetWidth, targetHeight));
return (new Size(Sanitize(width), Sanitize(height)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight)));
}
private static void ThrowInvalid(string message) => throw new InvalidOperationException(message);
private static int Sanitize(int input) => Math.Max(1, input);
}
}

328
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs

@ -0,0 +1,328 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the GNU Affero General Public License, Version 3.
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
public partial class PngEncoderTests
{
[Fact]
public void HeaderChunk_ComesFirst()
{
// arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
// act
input.Save(memStream, PngEncoder);
// assert
memStream.Position = 0;
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.Equal(PngChunkType.Header, type);
}
[Fact]
public void EndChunk_IsLast()
{
// arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
// act
input.Save(memStream, PngEncoder);
// assert
memStream.Position = 0;
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
bool endChunkFound = false;
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.False(endChunkFound);
if (type == PngChunkType.End)
{
endChunkFound = true;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
}
}
[Theory]
[InlineData(PngChunkType.Gamma)]
[InlineData(PngChunkType.Chroma)]
[InlineData(PngChunkType.EmbeddedColorProfile)]
[InlineData(PngChunkType.SignificantBits)]
[InlineData(PngChunkType.StandardRgbColourSpace)]
public void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj)
{
// arrange
var chunkType = (PngChunkType)chunkTypeObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
// act
input.Save(memStream, PngEncoder);
// assert
memStream.Position = 0;
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
bool palFound = false;
bool dataFound = false;
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
if (chunkType == type)
{
Assert.False(palFound || dataFound, $"{chunkType} chunk should come before data and palette chunk");
}
switch (type)
{
case PngChunkType.Data:
dataFound = true;
break;
case PngChunkType.Palette:
palFound = true;
break;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
}
}
[Theory]
[InlineData(PngChunkType.Physical)]
[InlineData(PngChunkType.SuggestedPalette)]
public void Chunk_ComesBeforeIDat(object chunkTypeObj)
{
// arrange
var chunkType = (PngChunkType)chunkTypeObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
// act
input.Save(memStream, PngEncoder);
// assert
memStream.Position = 0;
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
bool dataFound = false;
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
if (chunkType == type)
{
Assert.False(dataFound, $"{chunkType} chunk should come before data chunk");
}
if (type == PngChunkType.Data)
{
dataFound = true;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
}
}
[Fact]
public void IgnoreMetadata_WillExcludeAllAncillaryChunks()
{
// arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
var encoder = new PngEncoder() { IgnoreMetadata = true, TextCompressionThreshold = 8 };
var expectedChunkTypes = new Dictionary<PngChunkType, bool>()
{
{ PngChunkType.Header, false },
{ PngChunkType.Palette, false },
{ PngChunkType.Data, false },
{ PngChunkType.End, false }
};
var excludedChunkTypes = new List<PngChunkType>()
{
PngChunkType.Gamma,
PngChunkType.Exif,
PngChunkType.Physical,
PngChunkType.Text,
PngChunkType.InternationalText,
PngChunkType.CompressedText,
};
// act
input.Save(memStream, encoder);
// assert
Assert.True(excludedChunkTypes.Count > 0);
memStream.Position = 0;
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded");
if (expectedChunkTypes.ContainsKey(chunkType))
{
expectedChunkTypes[chunkType] = true;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
}
// all expected chunk types should have been seen at least once.
foreach (PngChunkType chunkType in expectedChunkTypes.Keys)
{
Assert.True(expectedChunkTypes[chunkType], $"We expect {chunkType} chunk to be present at least once");
}
}
[Theory]
[InlineData(PngChunkFilter.ExcludeGammaChunk)]
[InlineData(PngChunkFilter.ExcludeExifChunk)]
[InlineData(PngChunkFilter.ExcludePhysicalChunk)]
[InlineData(PngChunkFilter.ExcludeTextChunks)]
[InlineData(PngChunkFilter.ExcludeAll)]
public void ExcludeFilter_Works(object filterObj)
{
// arrange
var chunkFilter = (PngChunkFilter)filterObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
var encoder = new PngEncoder() { ChunkFilter = chunkFilter, TextCompressionThreshold = 8 };
var expectedChunkTypes = new Dictionary<PngChunkType, bool>()
{
{ PngChunkType.Header, false },
{ PngChunkType.Gamma, false },
{ PngChunkType.Palette, false },
{ PngChunkType.InternationalText, false },
{ PngChunkType.Text, false },
{ PngChunkType.CompressedText, false },
{ PngChunkType.Exif, false },
{ PngChunkType.Physical, false },
{ PngChunkType.Data, false },
{ PngChunkType.End, false }
};
var excludedChunkTypes = new List<PngChunkType>();
switch (chunkFilter)
{
case PngChunkFilter.ExcludeGammaChunk:
excludedChunkTypes.Add(PngChunkType.Gamma);
expectedChunkTypes.Remove(PngChunkType.Gamma);
break;
case PngChunkFilter.ExcludeExifChunk:
excludedChunkTypes.Add(PngChunkType.Exif);
expectedChunkTypes.Remove(PngChunkType.Exif);
break;
case PngChunkFilter.ExcludePhysicalChunk:
excludedChunkTypes.Add(PngChunkType.Physical);
expectedChunkTypes.Remove(PngChunkType.Physical);
break;
case PngChunkFilter.ExcludeTextChunks:
excludedChunkTypes.Add(PngChunkType.Text);
excludedChunkTypes.Add(PngChunkType.InternationalText);
excludedChunkTypes.Add(PngChunkType.CompressedText);
expectedChunkTypes.Remove(PngChunkType.Text);
expectedChunkTypes.Remove(PngChunkType.InternationalText);
expectedChunkTypes.Remove(PngChunkType.CompressedText);
break;
case PngChunkFilter.ExcludeAll:
excludedChunkTypes.Add(PngChunkType.Gamma);
excludedChunkTypes.Add(PngChunkType.Exif);
excludedChunkTypes.Add(PngChunkType.Physical);
excludedChunkTypes.Add(PngChunkType.Text);
excludedChunkTypes.Add(PngChunkType.InternationalText);
excludedChunkTypes.Add(PngChunkType.CompressedText);
expectedChunkTypes.Remove(PngChunkType.Gamma);
expectedChunkTypes.Remove(PngChunkType.Exif);
expectedChunkTypes.Remove(PngChunkType.Physical);
expectedChunkTypes.Remove(PngChunkType.Text);
expectedChunkTypes.Remove(PngChunkType.InternationalText);
expectedChunkTypes.Remove(PngChunkType.CompressedText);
break;
}
// act
input.Save(memStream, encoder);
// assert
Assert.True(excludedChunkTypes.Count > 0);
memStream.Position = 0;
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.False(excludedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been excluded");
if (expectedChunkTypes.ContainsKey(chunkType))
{
expectedChunkTypes[chunkType] = true;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
}
// all expected chunk types should have been seen at least once.
foreach (PngChunkType chunkType in expectedChunkTypes.Keys)
{
Assert.True(expectedChunkTypes[chunkType], $"We expect {chunkType} chunk to be present at least once");
}
}
[Fact]
public void ExcludeFilter_WithNone_DoesNotExcludeChunks()
{
// arrange
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
var encoder = new PngEncoder() { ChunkFilter = PngChunkFilter.None, TextCompressionThreshold = 8 };
var expectedChunkTypes = new List<PngChunkType>()
{
PngChunkType.Header,
PngChunkType.Gamma,
PngChunkType.Palette,
PngChunkType.InternationalText,
PngChunkType.Text,
PngChunkType.CompressedText,
PngChunkType.Exif,
PngChunkType.Physical,
PngChunkType.Data,
PngChunkType.End,
};
// act
input.Save(memStream, encoder);
memStream.Position = 0;
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8); // Skip header.
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var chunkType = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.True(expectedChunkTypes.Contains(chunkType), $"{chunkType} chunk should have been present");
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
}
}
}
}

226
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -2,8 +2,6 @@
// Licensed under the GNU Affero General Public License, Version 3.
// ReSharper disable InconsistentNaming
using System;
using System.Buffers.Binary;
using System.IO;
using System.Linq;
@ -18,7 +16,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
public class PngEncoderTests
public partial class PngEncoderTests
{
private static PngEncoder PngEncoder => new PngEncoder();
@ -215,6 +213,40 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
[Theory]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Rgb, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.Rgb, PngBitDepth.Bit16)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit1)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit2)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit4)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit1)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit2)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit4)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb48, PngColorType.Grayscale, PngBitDepth.Bit16)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit16)]
public void WorksWithAllBitDepthsAndExcludeAllFilter<TPixel>(TestImageProvider<TPixel> provider, PngColorType pngColorType, PngBitDepth pngBitDepth)
where TPixel : unmanaged, IPixel<TPixel>
{
foreach (PngInterlaceMode interlaceMode in InterlaceMode)
{
TestPngEncoderCore(
provider,
pngColorType,
PngFilterMethod.Adaptive,
pngBitDepth,
interlaceMode,
appendPngColorType: true,
appendPixelType: true,
appendPngBitDepth: true,
optimizeMethod: PngChunkFilter.ExcludeAll);
}
}
[Theory]
[WithBlankImages(1, 1, PixelTypes.A8, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)]
[WithBlankImages(1, 1, PixelTypes.Argb32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)]
@ -358,6 +390,66 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
[Theory]
[InlineData(PngColorType.Palette)]
[InlineData(PngColorType.RgbWithAlpha)]
[InlineData(PngColorType.GrayscaleWithAlpha)]
public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType colorType)
{
// arrange
var image = new Image<Rgba32>(50, 50);
var encoder = new PngEncoder()
{
TransparentColorMode = PngTransparentColorMode.Clear,
ColorType = colorType
};
Rgba32 rgba32 = Color.Blue;
for (int y = 0; y < image.Height; y++)
{
System.Span<Rgba32> rowSpan = image.GetPixelRowSpan(y);
// Half of the test image should be transparent.
if (y > 25)
{
rgba32.A = 0;
}
for (int x = 0; x < image.Width; x++)
{
rowSpan[x].FromRgba32(rgba32);
}
}
// act
using var memStream = new MemoryStream();
image.Save(memStream, encoder);
// assert
memStream.Position = 0;
using var actual = Image.Load<Rgba32>(memStream);
Rgba32 expectedColor = Color.Blue;
if (colorType == PngColorType.Grayscale || colorType == PngColorType.GrayscaleWithAlpha)
{
var luminance = ImageMaths.Get8BitBT709Luminance(expectedColor.R, expectedColor.G, expectedColor.B);
expectedColor = new Rgba32(luminance, luminance, luminance);
}
for (int y = 0; y < actual.Height; y++)
{
System.Span<Rgba32> rowSpan = actual.GetPixelRowSpan(y);
if (y > 25)
{
expectedColor = Color.Transparent;
}
for (int x = 0; x < actual.Width; x++)
{
Assert.Equal(expectedColor, rowSpan[x]);
}
}
}
[Theory]
[MemberData(nameof(PngTrnsFiles))]
public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType)
@ -411,126 +503,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
[Fact]
public void HeaderChunk_ComesFirst()
{
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
// Skip header.
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8);
BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.Equal(PngChunkType.Header, type);
}
[Fact]
public void EndChunk_IsLast()
{
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
// Skip header.
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8);
bool endChunkFound = false;
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
Assert.False(endChunkFound);
if (type == PngChunkType.End)
{
endChunkFound = true;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
}
}
[Theory]
[InlineData(PngChunkType.Gamma)]
[InlineData(PngChunkType.Chroma)]
[InlineData(PngChunkType.EmbeddedColorProfile)]
[InlineData(PngChunkType.SignificantBits)]
[InlineData(PngChunkType.StandardRgbColourSpace)]
public void Chunk_ComesBeforePlteAndIDat(object chunkTypeObj)
{
var chunkType = (PngChunkType)chunkTypeObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
// Skip header.
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8);
bool palFound = false;
bool dataFound = false;
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
if (chunkType == type)
{
Assert.False(palFound || dataFound, $"{chunkType} chunk should come before data and palette chunk");
}
switch (type)
{
case PngChunkType.Data:
dataFound = true;
break;
case PngChunkType.Palette:
palFound = true;
break;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
}
}
[Theory]
[InlineData(PngChunkType.Physical)]
[InlineData(PngChunkType.SuggestedPalette)]
public void Chunk_ComesBeforeIDat(object chunkTypeObj)
{
var chunkType = (PngChunkType)chunkTypeObj;
var testFile = TestFile.Create(TestImages.Png.PngWithMetadata);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
// Skip header.
Span<byte> bytesSpan = memStream.ToArray().AsSpan(8);
bool dataFound = false;
while (bytesSpan.Length > 0)
{
int length = BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(0, 4));
var type = (PngChunkType)BinaryPrimitives.ReadInt32BigEndian(bytesSpan.Slice(4, 4));
if (chunkType == type)
{
Assert.False(dataFound, $"{chunkType} chunk should come before data chunk");
}
if (type == PngChunkType.Data)
{
dataFound = true;
}
bytesSpan = bytesSpan.Slice(4 + 4 + length + 4);
}
}
[Theory]
[WithTestPatternImages(587, 821, PixelTypes.Rgba32)]
[WithTestPatternImages(677, 683, PixelTypes.Rgba32)]
@ -564,8 +536,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
bool appendPixelType = false,
bool appendCompressionLevel = false,
bool appendPaletteSize = false,
bool appendPngBitDepth = false)
where TPixel : unmanaged, IPixel<TPixel>
bool appendPngBitDepth = false,
PngChunkFilter optimizeMethod = PngChunkFilter.None)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
@ -576,7 +549,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
CompressionLevel = compressionLevel,
BitDepth = bitDepth,
Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = paletteSize }),
InterlaceMethod = interlaceMode
InterlaceMethod = interlaceMode,
ChunkFilter = optimizeMethod,
};
string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty;

16
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs

@ -605,5 +605,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
image.Mutate(x => x.Resize(image.Width / 2, image.Height / 2));
}
}
[Fact]
public void Issue1195()
{
using (var image = new Image<Rgba32>(2, 300))
{
var size = new Size(50, 50);
image.Mutate(x => x
.Resize(
new ResizeOptions
{
Size = size,
Mode = ResizeMode.Max
}));
}
}
}
}

Loading…
Cancel
Save