From eec21fd89e17bfbeb22484a54c6dab9c98c98baf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 3 Sep 2018 22:40:59 +0100 Subject: [PATCH 01/14] Add derived format info types and allow persistance of palette lengths --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 21 +++--- src/ImageSharp/Formats/Bmp/BmpInfo.cs | 25 +++++++ src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 57 ++++++++++----- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 30 +++++--- src/ImageSharp/Formats/Gif/GifInfo.cs | 33 +++++++++ .../Gif/Sections/GifImageDescriptor.cs | 2 +- .../Formats/Jpeg/JpegDecoderCore.cs | 7 +- src/ImageSharp/Formats/Jpeg/JpegInfo.cs | 25 +++++++ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 8 +-- src/ImageSharp/Formats/Png/PngInfo.cs | 25 +++++++ src/ImageSharp/ImageInfo.cs | 12 ++-- src/ImageSharp/MetaData/ImageFrameMetaData.cs | 13 ++-- .../Processors/Quantization/IQuantizer.cs | 9 +++ .../OctreeFrameQuantizer{TPixel}.cs | 32 +++++---- .../Quantization/OctreeQuantizer.cs | 23 ++++-- .../PaletteFrameQuantizer{TPixel}.cs | 2 + .../Quantization/PaletteQuantizer.cs | 20 ++++-- .../Quantization/WuFrameQuantizer{TPixel}.cs | 16 ++++- .../Processors/Quantization/WuQuantizer.cs | 21 ++++-- .../Formats/Gif/GifEncoderTests.cs | 39 +++++++++++ tests/ImageSharp.Tests/ImageInfoTests.cs | 70 ++++++++++++++++++- .../MetaData/ImageFrameMetaDataTests.cs | 21 ++++-- tests/ImageSharp.Tests/TestImages.cs | 3 +- .../ImageProviders/FileProvider.cs | 62 +++++++++------- .../SystemDrawingReferenceDecoder.cs | 11 ++- tests/Images/Input/Gif/leo.gif | 3 + 26 files changed, 466 insertions(+), 124 deletions(-) create mode 100644 src/ImageSharp/Formats/Bmp/BmpInfo.cs create mode 100644 src/ImageSharp/Formats/Gif/GifInfo.cs create mode 100644 src/ImageSharp/Formats/Jpeg/JpegInfo.cs create mode 100644 src/ImageSharp/Formats/Png/PngInfo.cs create mode 100644 tests/Images/Input/Gif/leo.gif diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index d67beb0368..4cb524ecb4 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; +using SixLabors.Primitives; namespace SixLabors.ImageSharp.Formats.Bmp { @@ -164,7 +165,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp public IImageInfo Identify(Stream stream) { this.ReadImageHeaders(stream, out _, out _); - return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metaData); + + var size = new Size(this.infoHeader.Width, this.infoHeader.Height); + return new BmpInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), size, this.metaData); } /// @@ -175,10 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Whether the bitmap is inverted. /// The representing the inverted value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Invert(int y, int height, bool inverted) - { - return (!inverted) ? height - y - 1 : y; - } + private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y; /// /// Calculates the amount of bytes to pad a row. @@ -206,10 +206,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The masked and shifted value /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static byte GetBytesFrom5BitValue(int value) - { - return (byte)((value << 3) | (value >> 2)); - } + private static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2)); /// /// Looks up color values and builds the image from de-compressed RLE8 data. @@ -524,8 +521,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp } // Resolution is stored in PPM. - var meta = new ImageMetaData(); - meta.ResolutionUnits = PixelResolutionUnit.PixelsPerMeter; + var meta = new ImageMetaData + { + ResolutionUnits = PixelResolutionUnit.PixelsPerMeter + }; if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0) { meta.HorizontalResolution = this.infoHeader.XPelsPerMeter; diff --git a/src/ImageSharp/Formats/Bmp/BmpInfo.cs b/src/ImageSharp/Formats/Bmp/BmpInfo.cs new file mode 100644 index 0000000000..dfffe94db7 --- /dev/null +++ b/src/ImageSharp/Formats/Bmp/BmpInfo.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Formats.Bmp +{ + /// + /// Contains information about the bmp including dimensions, pixel type information and additional metadata. + /// + public class BmpInfo : ImageInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The image pixel type information. + /// The size of the image in pixels. + /// The images metadata. + internal BmpInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData) + : base(pixelType, size, metaData) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 2a4d981ebb..e791517521 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -55,6 +55,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private GifGraphicControlExtension graphicsControlExtension; + /// + /// The image desciptor. + /// + private GifImageDescriptor imageDescriptor; + /// /// The metadata /// @@ -120,8 +125,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } else if (nextFlag == GifConstants.ExtensionIntroducer) { - int label = stream.ReadByte(); - switch (label) + switch (stream.ReadByte()) { case GifConstants.GraphicControlLabel: this.ReadGraphicalControlExtension(); @@ -178,13 +182,11 @@ namespace SixLabors.ImageSharp.Formats.Gif { if (nextFlag == GifConstants.ImageLabel) { - // Skip image block - this.Skip(0); + this.ReadImageDescriptor(); } else if (nextFlag == GifConstants.ExtensionIntroducer) { - int label = stream.ReadByte(); - switch (label) + switch (stream.ReadByte()) { case GifConstants.GraphicControlLabel: @@ -224,7 +226,17 @@ namespace SixLabors.ImageSharp.Formats.Gif this.globalColorTable?.Dispose(); } - return new ImageInfo(new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height, this.metaData); + GifColorTableMode colorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag + ? GifColorTableMode.Global + : GifColorTableMode.Local; + + var size = new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height); + + return new GifInfo( + colorTableMode, + new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), + size, + this.metaData); } /// @@ -238,14 +250,13 @@ namespace SixLabors.ImageSharp.Formats.Gif } /// - /// Reads the image descriptor + /// Reads the image descriptor. /// - /// - private GifImageDescriptor ReadImageDescriptor() + private void ReadImageDescriptor() { this.stream.Read(this.buffer, 0, 9); - return GifImageDescriptor.Parse(this.buffer); + this.imageDescriptor = GifImageDescriptor.Parse(this.buffer); } /// @@ -312,25 +323,25 @@ namespace SixLabors.ImageSharp.Formats.Gif private void ReadFrame(ref Image image, ref ImageFrame previousFrame) where TPixel : struct, IPixel { - GifImageDescriptor imageDescriptor = this.ReadImageDescriptor(); + this.ReadImageDescriptor(); IManagedByteBuffer localColorTable = null; IManagedByteBuffer indices = null; try { // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. - if (imageDescriptor.LocalColorTableFlag) + if (this.imageDescriptor.LocalColorTableFlag) { - int length = imageDescriptor.LocalColorTableSize * 3; + int length = this.imageDescriptor.LocalColorTableSize * 3; localColorTable = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(length, AllocationOptions.Clean); this.stream.Read(localColorTable.Array, 0, length); } - indices = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(imageDescriptor.Width * imageDescriptor.Height, AllocationOptions.Clean); + indices = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(this.imageDescriptor.Width * this.imageDescriptor.Height, AllocationOptions.Clean); - this.ReadFrameIndices(imageDescriptor, indices.GetSpan()); + this.ReadFrameIndices(this.imageDescriptor, indices.GetSpan()); ReadOnlySpan colorTable = MemoryMarshal.Cast((localColorTable ?? this.globalColorTable).GetSpan()); - this.ReadFrameColors(ref image, ref previousFrame, indices.GetSpan(), colorTable, imageDescriptor); + this.ReadFrameColors(ref image, ref previousFrame, indices.GetSpan(), colorTable, this.imageDescriptor); // Skip any remaining blocks this.Skip(0); @@ -508,6 +519,18 @@ namespace SixLabors.ImageSharp.Formats.Gif meta.FrameDelay = this.graphicsControlExtension.DelayTime; } + // Frames can either use the global table or their own local table. + if (this.logicalScreenDescriptor.GlobalColorTableFlag + && this.logicalScreenDescriptor.GlobalColorTableSize > 0) + { + meta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize; + } + else if (this.imageDescriptor.LocalColorTableFlag + && this.imageDescriptor.LocalColorTableSize > 0) + { + meta.ColorTableLength = this.imageDescriptor.LocalColorTableSize; + } + meta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 5532900355..ab0ee1fb6f 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -146,7 +146,8 @@ namespace SixLabors.ImageSharp.Formats.Gif } else { - using (QuantizedFrame paletteQuantized = palleteQuantizer.CreateFrameQuantizer(() => quantized.Palette).QuantizeFrame(frame)) + using (QuantizedFrame paletteQuantized + = palleteQuantizer.CreateFrameQuantizer(() => quantized.Palette).QuantizeFrame(frame)) { this.WriteImageData(paletteQuantized, stream); } @@ -157,13 +158,25 @@ namespace SixLabors.ImageSharp.Formats.Gif private void EncodeLocal(Image image, QuantizedFrame quantized, Stream stream) where TPixel : struct, IPixel { + ImageFrame previousFrame = null; foreach (ImageFrame frame in image.Frames) { if (quantized is null) { - quantized = this.quantizer.CreateFrameQuantizer().QuantizeFrame(frame); + // Allow each frame to be encoded at whatever color depth the frame designates if set. + if (previousFrame != null + && previousFrame.MetaData.ColorTableLength != frame.MetaData.ColorTableLength + && frame.MetaData.ColorTableLength > 0) + { + quantized = this.quantizer.CreateFrameQuantizer(frame.MetaData.ColorTableLength).QuantizeFrame(frame); + } + else + { + quantized = this.quantizer.CreateFrameQuantizer().QuantizeFrame(frame); + } } + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); this.WriteGraphicalControlExtension(frame.MetaData, this.GetTransparentIndex(quantized), stream); this.WriteImageDescriptor(frame, true, stream); this.WriteColorTable(quantized, stream); @@ -171,6 +184,7 @@ namespace SixLabors.ImageSharp.Formats.Gif quantized?.Dispose(); quantized = null; // So next frame can regenerate it + previousFrame = frame; } } @@ -210,10 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The stream to write to. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WriteHeader(Stream stream) - { - stream.Write(GifConstants.MagicNumber, 0, GifConstants.MagicNumber.Length); - } + private void WriteHeader(Stream stream) => stream.Write(GifConstants.MagicNumber, 0, GifConstants.MagicNumber.Length); /// /// Writes the logical screen descriptor to the stream. @@ -226,7 +237,7 @@ namespace SixLabors.ImageSharp.Formats.Gif private void WriteLogicalScreenDescriptor(Image image, int transparencyIndex, bool useGlobalTable, Stream stream) where TPixel : struct, IPixel { - byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1); + byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth, false, this.bitDepth - 1); // The Pixel Aspect Ratio is defined to be the quotient of the pixel's // width over its height. The value range in this field allows @@ -382,7 +393,7 @@ namespace SixLabors.ImageSharp.Formats.Gif localColorTableFlag: hasColorTable, interfaceFlag: false, sortFlag: false, - localColorTableSize: (byte)this.bitDepth); + localColorTableSize: this.bitDepth - 1); var descriptor = new GifImageDescriptor( left: 0, @@ -407,7 +418,8 @@ namespace SixLabors.ImageSharp.Formats.Gif { int pixelCount = image.Palette.Length; - int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3; // The maximium number of colors for the bit depth + // The maximium number of colors for the bit depth + int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3; Rgb24 rgb = default; using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) diff --git a/src/ImageSharp/Formats/Gif/GifInfo.cs b/src/ImageSharp/Formats/Gif/GifInfo.cs new file mode 100644 index 0000000000..1c345a576b --- /dev/null +++ b/src/ImageSharp/Formats/Gif/GifInfo.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Formats.Gif +{ + /// + /// Contains information about the bmp including dimensions, pixel type information and additional metadata. + /// + public class GifInfo : ImageInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The color table mode. + /// The image pixel type information. + /// The size of the image in pixels. + /// The images metadata. + internal GifInfo( + GifColorTableMode colorTableMode, + PixelTypeInfo pixelType, + Size size, + ImageMetaData metaData) + : base(pixelType, size, metaData) => this.ColorTableMode = colorTableMode; + + /// + /// Gets the color table mode. + /// + public GifColorTableMode ColorTableMode { get; } + } +} diff --git a/src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs b/src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs index c5360729e8..e2f5bee78e 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs @@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Formats.Gif return MemoryMarshal.Cast(buffer)[0]; } - public static byte GetPackedValue(bool localColorTableFlag, bool interfaceFlag, bool sortFlag, byte localColorTableSize) + public static byte GetPackedValue(bool localColorTableFlag, bool interfaceFlag, bool sortFlag, int localColorTableSize) { /* Local Color Table Flag | 1 Bit diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 7561afa1ef..fb717d3fb8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -234,7 +234,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.InitExifProfile(); this.InitIccProfile(); this.InitDerivedMetaDataProperties(); - return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData); + + return new JpegInfo(new PixelTypeInfo(this.BitsPerPixel), new Size(this.ImageWidth, this.ImageHeight), this.MetaData); } /// @@ -899,9 +900,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The values [MethodImpl(MethodImplOptions.AggressiveInlining)] private void BuildHuffmanTable(HuffmanTables tables, int index, ReadOnlySpan codeLengths, ReadOnlySpan values) - { - tables[index] = new HuffmanTable(this.configuration.MemoryAllocator, codeLengths, values); - } + => tables[index] = new HuffmanTable(this.configuration.MemoryAllocator, codeLengths, values); /// /// Reads a from the stream advancing it by two bytes diff --git a/src/ImageSharp/Formats/Jpeg/JpegInfo.cs b/src/ImageSharp/Formats/Jpeg/JpegInfo.cs new file mode 100644 index 0000000000..8e9b6257c3 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/JpegInfo.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jpeg +{ + /// + /// Contains information about the bmp including dimensions, pixel type information and additional metadata. + /// + public class JpegInfo : ImageInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The image pixel type information. + /// The size of the image in pixels. + /// The images metadata. + internal JpegInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData) + : base(pixelType, size, metaData) + { + } + } +} diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index aa96b926ca..9032560160 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -10,7 +10,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.Memory; @@ -18,6 +17,7 @@ using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; +using SixLabors.Primitives; namespace SixLabors.ImageSharp.Formats.Png { @@ -349,7 +349,7 @@ namespace SixLabors.ImageSharp.Formats.Png throw new ImageFormatException("PNG Image does not contain a header chunk"); } - return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); + return new PngInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new Size(this.header.Width, this.header.Height), metadata); } /// @@ -360,9 +360,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] private static byte ReadByteLittleEndian(ReadOnlySpan buffer, int offset) - { - return (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF)); - } + => (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF)); /// /// Attempts to convert a byte array to a new array where each value in the original array is represented by the diff --git a/src/ImageSharp/Formats/Png/PngInfo.cs b/src/ImageSharp/Formats/Png/PngInfo.cs new file mode 100644 index 0000000000..2bb0b16b70 --- /dev/null +++ b/src/ImageSharp/Formats/Png/PngInfo.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Formats.Png +{ + /// + /// Contains information about the bmp including dimensions, pixel type information and additional metadata. + /// + public class PngInfo : ImageInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The image pixel type information. + /// The size of the image in pixels. + /// The images metadata. + internal PngInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData) + : base(pixelType, size, metaData) + { + } + } +} diff --git a/src/ImageSharp/ImageInfo.cs b/src/ImageSharp/ImageInfo.cs index 6f894cb599..eed1c66918 100644 --- a/src/ImageSharp/ImageInfo.cs +++ b/src/ImageSharp/ImageInfo.cs @@ -3,26 +3,26 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.MetaData; +using SixLabors.Primitives; namespace SixLabors.ImageSharp { /// /// Contains information about the image including dimensions, pixel type information and additional metadata /// - internal sealed class ImageInfo : IImageInfo + public abstract class ImageInfo : IImageInfo { /// /// Initializes a new instance of the class. /// /// The image pixel type information. - /// The width of the image in pixels. - /// The height of the image in pixels. + /// The size of the image in pixels. /// The images metadata. - public ImageInfo(PixelTypeInfo pixelType, int width, int height, ImageMetaData metaData) + protected ImageInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData) { this.PixelType = pixelType; - this.Width = width; - this.Height = height; + this.Width = size.Width; + this.Height = size.Height; this.MetaData = metaData; } diff --git a/src/ImageSharp/MetaData/ImageFrameMetaData.cs b/src/ImageSharp/MetaData/ImageFrameMetaData.cs index 47a2fb775f..f83e092c9f 100644 --- a/src/ImageSharp/MetaData/ImageFrameMetaData.cs +++ b/src/ImageSharp/MetaData/ImageFrameMetaData.cs @@ -28,10 +28,18 @@ namespace SixLabors.ImageSharp.MetaData { DebugGuard.NotNull(other, nameof(other)); + this.ColorTableLength = other.ColorTableLength; this.FrameDelay = other.FrameDelay; this.DisposalMethod = other.DisposalMethod; } + /// + /// Gets or sets the length of the color table for paletted images. + /// If not 0, then this field indicates the maximum number of colors to use when quantizing the + /// image frame. + /// + public int ColorTableLength { get; set; } + /// /// Gets or sets the frame delay for animated images. /// If not 0, when utilized in Gif animation, this field specifies the number of hundredths (1/100) of a second to @@ -51,9 +59,6 @@ namespace SixLabors.ImageSharp.MetaData /// Clones this ImageFrameMetaData. /// /// The cloned instance. - public ImageFrameMetaData Clone() - { - return new ImageFrameMetaData(this); - } + public ImageFrameMetaData Clone() => new ImageFrameMetaData(this); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs index 0f6846d1bf..3da09cde09 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs @@ -23,5 +23,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The IFrameQuantizer CreateFrameQuantizer() where TPixel : struct, IPixel; + + /// + /// Creates the generic frame quantizer + /// + /// The pixel format. + /// The maximum number of colors to hold in the color palette. + /// The + IFrameQuantizer CreateFrameQuantizer(int maxColors) + where TPixel : struct, IPixel; } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index 3eac70eea5..39546d63f7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Maximum allowed color depth /// - private readonly byte colors; + private readonly int colors; /// /// Stores the tree @@ -43,9 +43,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// the second pass quantizes a color based on the nodes in the tree /// public OctreeFrameQuantizer(OctreeQuantizer quantizer) + : this(quantizer, quantizer.MaxColors) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The octree quantizer. + /// The maximum number of colors to hold in the color palette. + /// + /// The Octree quantizer is a two pass algorithm. The initial pass sets up the Octree, + /// the second pass quantizes a color based on the nodes in the tree + /// + public OctreeFrameQuantizer(OctreeQuantizer quantizer, int maxColors) : base(quantizer, false) { - this.colors = (byte)quantizer.MaxColors; + this.colors = maxColors; this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8)); } @@ -261,13 +275,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization [MethodImpl(MethodImplOptions.AggressiveInlining)] public TPixel[] Palletize(int colorCount) { - while (this.Leaves > colorCount) + while (this.Leaves > colorCount - 1) { this.Reduce(); } // Now palletize the nodes - var palette = new TPixel[colorCount + 1]; + var palette = new TPixel[colorCount]; int paletteIndex = 0; this.root.ConstructPalette(palette, ref paletteIndex); @@ -285,10 +299,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetPaletteIndex(ref TPixel pixel, ref Rgba32 rgba) - { - return this.root.GetPaletteIndex(ref pixel, 0, ref rgba); - } + public int GetPaletteIndex(ref TPixel pixel, ref Rgba32 rgba) => this.root.GetPaletteIndex(ref pixel, 0, ref rgba); /// /// Keep track of the previous node that was quantized @@ -297,10 +308,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The node last quantized /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected void TrackPrevious(OctreeNode node) - { - this.previousNode = node; - } + protected void TrackPrevious(OctreeNode node) => this.previousNode = node; /// /// Reduce the depth of the tree diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs index 385f6246f8..22bb5223f0 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs @@ -15,6 +15,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class OctreeQuantizer : IQuantizer { + /// + /// The default maximum number of colors to use when quantizing the image. + /// + public const int DefaultMaxColors = 256; + /// /// Initializes a new instance of the class. /// @@ -26,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Initializes a new instance of the class. /// - /// The maximum number of colors to hold in the color palette + /// The maximum number of colors to hold in the color palette. public OctreeQuantizer(int maxColors) : this(GetDiffuser(true), maxColors) { @@ -37,7 +42,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Whether to apply dithering to the output image public OctreeQuantizer(bool dither) - : this(GetDiffuser(dither), 255) + : this(GetDiffuser(dither), DefaultMaxColors) { } @@ -46,7 +51,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The error diffusion algorithm, if any, to apply to the output image public OctreeQuantizer(IErrorDiffuser diffuser) - : this(diffuser, 255) + : this(diffuser, DefaultMaxColors) { } @@ -57,10 +62,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The maximum number of colors to hold in the color palette public OctreeQuantizer(IErrorDiffuser diffuser, int maxColors) { - Guard.MustBeBetweenOrEqualTo(maxColors, 1, 255, nameof(maxColors)); - this.Diffuser = diffuser; - this.MaxColors = maxColors; + this.MaxColors = maxColors.Clamp(1, DefaultMaxColors); } /// @@ -76,6 +79,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization where TPixel : struct, IPixel => new OctreeFrameQuantizer(this); + /// + public IFrameQuantizer CreateFrameQuantizer(int maxColors) + where TPixel : struct, IPixel + { + maxColors = maxColors.Clamp(1, DefaultMaxColors); + return new OctreeFrameQuantizer(this, maxColors); + } + private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null; } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index 8df81b426f..cdf3514e2d 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -36,6 +36,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public PaletteFrameQuantizer(PaletteQuantizer quantizer, TPixel[] colors) : base(quantizer, true) { + // TODO: Why is this value constrained? Gif has limitations but theoretically + // we might want to reduce the palette of an image to greater than that limitation. Guard.MustBeBetweenOrEqualTo(colors.Length, 1, 256, nameof(colors)); this.palette = colors; this.paletteVector = new Vector4[this.palette.Length]; diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index 8ae9177185..27ef05dfe9 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -37,10 +37,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// /// The error diffusion algorithm, if any, to apply to the output image - public PaletteQuantizer(IErrorDiffuser diffuser) - { - this.Diffuser = diffuser; - } + public PaletteQuantizer(IErrorDiffuser diffuser) => this.Diffuser = diffuser; /// public IErrorDiffuser Diffuser { get; } @@ -50,6 +47,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization where TPixel : struct, IPixel => this.CreateFrameQuantizer(() => NamedColors.WebSafePalette); + /// + public IFrameQuantizer CreateFrameQuantizer(int maxColors) + where TPixel : struct, IPixel + { + TPixel[] websafe = NamedColors.WebSafePalette; + int max = Math.Min(maxColors, websafe.Length); + + if (max != websafe.Length) + { + return this.CreateFrameQuantizer(() => NamedColors.WebSafePalette.AsSpan(0, max).ToArray()); + } + + return this.CreateFrameQuantizer(() => websafe); + } + /// /// Gets the palette to use to quantize the image. /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 021dc62fbf..d71221b9d6 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -3,7 +3,6 @@ using System; using System.Buffers; -using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -128,11 +127,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// the second pass quantizes a color based on the position in the histogram. /// public WuFrameQuantizer(WuQuantizer quantizer) - : base(quantizer, false) + : this(quantizer, quantizer.MaxColors) { - this.colors = quantizer.MaxColors; } + /// + /// Initializes a new instance of the class. + /// + /// The wu quantizer. + /// The maximum number of colors to hold in the color palette. + /// + /// The Wu quantizer is a two pass algorithm. The initial pass sets up the 3-D color histogram, + /// the second pass quantizes a color based on the position in the histogram. + /// + public WuFrameQuantizer(WuQuantizer quantizer, int maxColors) + : base(quantizer, false) => this.colors = maxColors; + /// public override QuantizedFrame QuantizeFrame(ImageFrame image) { diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs index 3aa1f4c5e6..5123e737d3 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs @@ -14,6 +14,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WuQuantizer : IQuantizer { + /// + /// The default maximum number of colors to use when quantizing the image. + /// + public const int DefaultMaxColors = 256; + /// /// Initializes a new instance of the class. /// @@ -36,7 +41,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Whether to apply dithering to the output image public WuQuantizer(bool dither) - : this(GetDiffuser(dither), 255) + : this(GetDiffuser(dither), DefaultMaxColors) { } @@ -45,7 +50,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The error diffusion algorithm, if any, to apply to the output image public WuQuantizer(IErrorDiffuser diffuser) - : this(diffuser, 255) + : this(diffuser, DefaultMaxColors) { } @@ -56,10 +61,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The maximum number of colors to hold in the color palette public WuQuantizer(IErrorDiffuser diffuser, int maxColors) { - Guard.MustBeBetweenOrEqualTo(maxColors, 1, 255, nameof(maxColors)); - this.Diffuser = diffuser; - this.MaxColors = maxColors; + this.MaxColors = maxColors.Clamp(1, DefaultMaxColors); } /// @@ -75,6 +78,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization where TPixel : struct, IPixel => new WuFrameQuantizer(this); + /// + public IFrameQuantizer CreateFrameQuantizer(int maxColors) + where TPixel : struct, IPixel + { + maxColors = maxColors.Clamp(1, DefaultMaxColors); + return new WuFrameQuantizer(this, maxColors); + } + private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null; } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index e9104ef8d9..11fa4d5d5f 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -179,5 +179,44 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif Assert.True(fileInfoGlobal.Length < fileInfoLocal.Length); } } + + [Fact] + public void NonMutatingEncodePreservesPaletteCount() + { + using (var inStream = new MemoryStream(TestFile.Create(TestImages.Gif.Leo).Bytes)) + using (var outStream = new MemoryStream()) + { + var info = (GifInfo)Image.Identify(inStream); + GifColorTableMode colorMode = info.ColorTableMode; + inStream.Position = 0; + + var image = Image.Load(inStream); + var encoder = new GifEncoder() + { + ColorTableMode = colorMode, + Quantizer = new OctreeQuantizer(image.Frames.RootFrame.MetaData.ColorTableLength) + }; + + image.Save(outStream, encoder); + outStream.Position = 0; + + var cloneInfo = (GifInfo)Image.Identify(outStream); + outStream.Position = 0; + var clone = Image.Load(outStream); + + // Gifiddle and Cyotek GifInfo say this image has 64 colors. + Assert.Equal(64, image.Frames.RootFrame.MetaData.ColorTableLength); + Assert.Equal(info.ColorTableMode, cloneInfo.ColorTableMode); + + for (int i = 0; i < image.Frames.Count; i++) + { + Assert.Equal(image.Frames[i].MetaData.ColorTableLength, clone.Frames[i].MetaData.ColorTableLength); + Assert.Equal(image.Frames[i].MetaData.FrameDelay, clone.Frames[i].MetaData.FrameDelay); + } + + image.Dispose(); + clone.Dispose(); + } + } } } diff --git a/tests/ImageSharp.Tests/ImageInfoTests.cs b/tests/ImageSharp.Tests/ImageInfoTests.cs index 91f6804c0f..d46e340521 100644 --- a/tests/ImageSharp.Tests/ImageInfoTests.cs +++ b/tests/ImageSharp.Tests/ImageInfoTests.cs @@ -2,6 +2,10 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.MetaData; using SixLabors.Primitives; @@ -12,7 +16,7 @@ namespace SixLabors.ImageSharp.Tests public class ImageInfoTests { [Fact] - public void ImageInfoInitializesCorrectly() + public void JpegInfoInitializesCorrectly() { const int Width = 50; const int Height = 60; @@ -21,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests var pixelType = new PixelTypeInfo(8); var meta = new ImageMetaData(); - var info = new ImageInfo(pixelType, Width, Height, meta); + var info = new JpegInfo(pixelType, size, meta); Assert.Equal(pixelType, info.PixelType); Assert.Equal(Width, info.Width); @@ -30,5 +34,67 @@ namespace SixLabors.ImageSharp.Tests Assert.Equal(rectangle, info.Bounds()); Assert.Equal(meta, info.MetaData); } + + [Fact] + public void BmpInfoInitializesCorrectly() + { + const int Width = 50; + const int Height = 60; + var size = new Size(Width, Height); + var rectangle = new Rectangle(0, 0, Width, Height); + var pixelType = new PixelTypeInfo(8); + var meta = new ImageMetaData(); + + var info = new BmpInfo(pixelType, size, meta); + + Assert.Equal(pixelType, info.PixelType); + Assert.Equal(Width, info.Width); + Assert.Equal(Height, info.Height); + Assert.Equal(size, info.Size()); + Assert.Equal(rectangle, info.Bounds()); + Assert.Equal(meta, info.MetaData); + } + + [Fact] + public void PngInfoInitializesCorrectly() + { + const int Width = 50; + const int Height = 60; + var size = new Size(Width, Height); + var rectangle = new Rectangle(0, 0, Width, Height); + var pixelType = new PixelTypeInfo(8); + var meta = new ImageMetaData(); + + var info = new PngInfo(pixelType, size, meta); + + Assert.Equal(pixelType, info.PixelType); + Assert.Equal(Width, info.Width); + Assert.Equal(Height, info.Height); + Assert.Equal(size, info.Size()); + Assert.Equal(rectangle, info.Bounds()); + Assert.Equal(meta, info.MetaData); + } + + [Fact] + public void GifInfoInitializesCorrectly() + { + const GifColorTableMode mode = GifColorTableMode.Local; + const int Width = 50; + const int Height = 60; + var size = new Size(Width, Height); + var rectangle = new Rectangle(0, 0, Width, Height); + var pixelType = new PixelTypeInfo(8); + var meta = new ImageMetaData(); + + var info = new GifInfo(mode, pixelType, size, meta); + + Assert.Equal(mode, info.ColorTableMode); + Assert.Equal(pixelType, info.PixelType); + Assert.Equal(Width, info.Width); + Assert.Equal(Height, info.Height); + Assert.Equal(size, info.Size()); + Assert.Equal(rectangle, info.Bounds()); + Assert.Equal(meta, info.MetaData); + } } } diff --git a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs index 507401398e..6e18fe2537 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.MetaData; using Xunit; @@ -16,14 +15,22 @@ namespace SixLabors.ImageSharp.Tests [Fact] public void ConstructorImageFrameMetaData() { - ImageFrameMetaData metaData = new ImageFrameMetaData(); - metaData.FrameDelay = 42; - metaData.DisposalMethod = DisposalMethod.RestoreToBackground; + const int frameDelay = 42; + const int colorTableLength = 128; + const DisposalMethod disposalMethod = DisposalMethod.RestoreToBackground; - ImageFrameMetaData clone = new ImageFrameMetaData(metaData); + var metaData = new ImageFrameMetaData + { + FrameDelay = frameDelay, + ColorTableLength = colorTableLength, + DisposalMethod = disposalMethod + }; - Assert.Equal(42, clone.FrameDelay); - Assert.Equal(DisposalMethod.RestoreToBackground, clone.DisposalMethod); + var clone = new ImageFrameMetaData(metaData); + + Assert.Equal(frameDelay, clone.FrameDelay); + Assert.Equal(colorTableLength, clone.ColorTableLength); + Assert.Equal(disposalMethod, clone.DisposalMethod); } } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 1ee3f96757..7a82a618cb 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -201,6 +201,7 @@ namespace SixLabors.ImageSharp.Tests public const string Cheers = "Gif/cheers.gif"; public const string Trans = "Gif/trans.gif"; public const string Kumin = "Gif/kumin.gif"; + public const string Leo = "Gif/leo.gif"; public const string Ratio4x1 = "Gif/base_4x1.gif"; public const string Ratio1x4 = "Gif/base_1x4.gif"; @@ -211,7 +212,7 @@ namespace SixLabors.ImageSharp.Tests public const string BadDescriptorWidth = "Gif/issues/issue403_baddescriptorwidth.gif"; } - public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Ratio4x1, Ratio1x4 }; + public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 }; } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs index 6475547a06..3ed696c472 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs @@ -55,9 +55,20 @@ namespace SixLabors.ImageSharp.Tests public bool Equals(Key other) { - if (other is null) return false; - if (ReferenceEquals(this, other)) return true; - if (!this.commonValues.Equals(other.commonValues)) return false; + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (!this.commonValues.Equals(other.commonValues)) + { + return false; + } if (this.decoderParameters.Count != other.decoderParameters.Count) { @@ -66,8 +77,7 @@ namespace SixLabors.ImageSharp.Tests foreach (KeyValuePair kv in this.decoderParameters) { - object otherVal; - if (!other.decoderParameters.TryGetValue(kv.Key, out otherVal)) + if (!other.decoderParameters.TryGetValue(kv.Key, out object otherVal)) { return false; } @@ -81,26 +91,29 @@ namespace SixLabors.ImageSharp.Tests public override bool Equals(object obj) { - if (obj is null) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + return this.Equals((Key)obj); } - public override int GetHashCode() - { - return this.commonValues.GetHashCode(); - } + public override int GetHashCode() => this.commonValues.GetHashCode(); - public static bool operator ==(Key left, Key right) - { - return Equals(left, right); - } + public static bool operator ==(Key left, Key right) => Equals(left, right); - public static bool operator !=(Key left, Key right) - { - return !Equals(left, right); - } + public static bool operator !=(Key left, Key right) => !Equals(left, right); } private static readonly ConcurrentDictionary> cache = new ConcurrentDictionary>(); @@ -111,10 +124,7 @@ namespace SixLabors.ImageSharp.Tests { } - public FileProvider(string filePath) - { - this.FilePath = filePath; - } + public FileProvider(string filePath) => this.FilePath = filePath; /// /// Gets the file path relative to the "~/tests/images" folder @@ -135,12 +145,12 @@ namespace SixLabors.ImageSharp.Tests if (!TestEnvironment.Is64BitProcess) { - return LoadImage(decoder); + return this.LoadImage(decoder); } var key = new Key(this.PixelType, this.FilePath, decoder); - Image cachedImage = cache.GetOrAdd(key, fn => { return LoadImage(decoder); }); + Image cachedImage = cache.GetOrAdd(key, _ => this.LoadImage(decoder)); return cachedImage.Clone(); } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index 427a565424..3696accdd3 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -48,7 +48,16 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs using (var sourceBitmap = new System.Drawing.Bitmap(stream)) { var pixelType = new PixelTypeInfo(System.Drawing.Image.GetPixelFormatSize(sourceBitmap.PixelFormat)); - return new ImageInfo(pixelType, sourceBitmap.Width, sourceBitmap.Height, new ImageMetaData()); + var size = new SixLabors.Primitives.Size(sourceBitmap.Width, sourceBitmap.Height); + return new SystemDrawingInfo(pixelType, size, new ImageMetaData()); + } + } + + private class SystemDrawingInfo : ImageInfo + { + public SystemDrawingInfo(PixelTypeInfo pixelType, SixLabors.Primitives.Size size, ImageMetaData metaData) + : base(pixelType, size, metaData) + { } } } diff --git a/tests/Images/Input/Gif/leo.gif b/tests/Images/Input/Gif/leo.gif new file mode 100644 index 0000000000..8cf7078380 --- /dev/null +++ b/tests/Images/Input/Gif/leo.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ee2bf4404165d534dcbfaebece0eee2a93999c47aec26850a7021b8c5d25f5c +size 454544 From 3644451e6d5d6d53d58506c75a160ed445e01d46 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 5 Sep 2018 20:07:19 +0100 Subject: [PATCH 02/14] Use dictionaries to store format specific metadata --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 5 +- src/ImageSharp/Formats/Bmp/BmpInfo.cs | 25 ------ .../Formats/Gif/GifColorTableMode.cs | 2 +- src/ImageSharp/Formats/Gif/GifConstants.cs | 18 +++- src/ImageSharp/Formats/Gif/GifDecoder.cs | 1 + src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 89 +++++++++++++------ ...DisposalMethod.cs => GifDisposalMethod.cs} | 2 +- src/ImageSharp/Formats/Gif/GifEncoder.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 54 +++++------ .../Formats/Gif/GifFrameMetaData.cs | 33 +++++++ src/ImageSharp/Formats/Gif/GifInfo.cs | 33 ------- src/ImageSharp/Formats/Gif/GifMetaData.cs | 29 ++++++ .../Formats/Gif/GifMetaDataExtensions.cs | 49 ++++++++++ .../Formats/Gif/IGifDecoderOptions.cs | 1 + .../Formats/Gif/IGifEncoderOptions.cs | 2 +- .../Sections/GifGraphicControlExtension.cs | 4 +- .../GifNetscapeLoopingApplicationExtension.cs | 44 +++++++++ .../Formats/Jpeg/JpegDecoderCore.cs | 2 +- src/ImageSharp/Formats/Jpeg/JpegInfo.cs | 25 ------ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 3 +- src/ImageSharp/Formats/Png/PngInfo.cs | 25 ------ src/ImageSharp/ImageInfo.cs | 12 +-- .../Gif => MetaData}/FrameDecodingMode.cs | 2 +- src/ImageSharp/MetaData/ImageFrameMetaData.cs | 67 +++++++++----- src/ImageSharp/MetaData/ImageMetaData.cs | 69 ++++++++++---- .../Formats/Gif/GifEncoderTests.cs | 21 +++-- .../GifGraphicControlExtensionTests.cs | 8 +- tests/ImageSharp.Tests/ImageInfoTests.cs | 70 +-------------- .../MetaData/ImageFrameMetaDataTests.cs | 14 +-- .../MetaData/ImageMetaDataTests.cs | 2 - .../SystemDrawingReferenceDecoder.cs | 11 +-- 31 files changed, 399 insertions(+), 325 deletions(-) delete mode 100644 src/ImageSharp/Formats/Bmp/BmpInfo.cs rename src/ImageSharp/Formats/Gif/{DisposalMethod.cs => GifDisposalMethod.cs} (97%) create mode 100644 src/ImageSharp/Formats/Gif/GifFrameMetaData.cs delete mode 100644 src/ImageSharp/Formats/Gif/GifInfo.cs create mode 100644 src/ImageSharp/Formats/Gif/GifMetaData.cs create mode 100644 src/ImageSharp/Formats/Gif/GifMetaDataExtensions.cs create mode 100644 src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs delete mode 100644 src/ImageSharp/Formats/Jpeg/JpegInfo.cs delete mode 100644 src/ImageSharp/Formats/Png/PngInfo.cs rename src/ImageSharp/{Formats/Gif => MetaData}/FrameDecodingMode.cs (91%) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 4cb524ecb4..385c79896e 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -10,7 +10,6 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; -using SixLabors.Primitives; namespace SixLabors.ImageSharp.Formats.Bmp { @@ -165,9 +164,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp public IImageInfo Identify(Stream stream) { this.ReadImageHeaders(stream, out _, out _); - - var size = new Size(this.infoHeader.Width, this.infoHeader.Height); - return new BmpInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), size, this.metaData); + return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metaData); } /// diff --git a/src/ImageSharp/Formats/Bmp/BmpInfo.cs b/src/ImageSharp/Formats/Bmp/BmpInfo.cs deleted file mode 100644 index dfffe94db7..0000000000 --- a/src/ImageSharp/Formats/Bmp/BmpInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.MetaData; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Formats.Bmp -{ - /// - /// Contains information about the bmp including dimensions, pixel type information and additional metadata. - /// - public class BmpInfo : ImageInfo - { - /// - /// Initializes a new instance of the class. - /// - /// The image pixel type information. - /// The size of the image in pixels. - /// The images metadata. - internal BmpInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData) - : base(pixelType, size, metaData) - { - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifColorTableMode.cs b/src/ImageSharp/Formats/Gif/GifColorTableMode.cs index aa41928633..95b3335626 100644 --- a/src/ImageSharp/Formats/Gif/GifColorTableMode.cs +++ b/src/ImageSharp/Formats/Gif/GifColorTableMode.cs @@ -4,7 +4,7 @@ namespace SixLabors.ImageSharp.Formats.Gif { /// - /// Provides enumeration for the available Gif color table modes. + /// Provides enumeration for the available color table modes. /// public enum GifColorTableMode { diff --git a/src/ImageSharp/Formats/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs index 0dbd39b992..cc80b0cce2 100644 --- a/src/ImageSharp/Formats/Gif/GifConstants.cs +++ b/src/ImageSharp/Formats/Gif/GifConstants.cs @@ -26,6 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// internal static readonly byte[] MagicNumber = Encoding.UTF8.GetBytes(FileType + FileVersion); + /// + /// Gets the key used for storing and retriving metadata. + /// + public const string MetaDataKey = FileType; + /// /// The extension block introducer !. /// @@ -41,20 +46,25 @@ namespace SixLabors.ImageSharp.Formats.Gif /// public const byte ApplicationExtensionLabel = 0xFF; + /// + /// The application block size. + /// + public const byte ApplicationBlockSize = 11; + /// /// The application identification. /// - public const string ApplicationIdentification = "NETSCAPE2.0"; + public const string NetscapeApplicationIdentification = "NETSCAPE2.0"; /// /// The ASCII encoded application identification bytes. /// - internal static readonly byte[] ApplicationIdentificationBytes = Encoding.UTF8.GetBytes(ApplicationIdentification); + internal static readonly byte[] NetscapeApplicationIdentificationBytes = Encoding.UTF8.GetBytes(NetscapeApplicationIdentification); /// - /// The application block size. + /// The Netscape looping application sub block size. /// - public const byte ApplicationBlockSize = 11; + public const byte NetscapeLoopingSubBlockSize = 3; /// /// The comment label. diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index ac451a3550..42c76d6400 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -3,6 +3,7 @@ using System.IO; using System.Text; +using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Gif diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index e791517521..092ac859df 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -61,10 +61,15 @@ namespace SixLabors.ImageSharp.Formats.Gif private GifImageDescriptor imageDescriptor; /// - /// The metadata + /// The abstract metadata. /// private ImageMetaData metaData; + /// + /// The gif specific metadata. + /// + private GifMetaData gifMetaData; + /// /// Initializes a new instance of the class. /// @@ -134,11 +139,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.ReadComments(); break; case GifConstants.ApplicationExtensionLabel: - - // The application extension length should be 11 but we've got test images that incorrectly - // set this to 252. - int appLength = stream.ReadByte(); - this.Skip(appLength); // No need to read. + this.ReadApplicationExtension(); break; case GifConstants.PlainTextLabel: int plainLength = stream.ReadByte(); @@ -163,6 +164,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.globalColorTable?.Dispose(); } + image?.MetaData.AddOrUpdateGifMetaData(this.gifMetaData); return image; } @@ -197,11 +199,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.ReadComments(); break; case GifConstants.ApplicationExtensionLabel: - - // The application extension length should be 11 but we've got test images that incorrectly - // set this to 252. - int appLength = stream.ReadByte(); - this.Skip(appLength); // No need to read. + this.ReadApplicationExtension(); break; case GifConstants.PlainTextLabel: int plainLength = stream.ReadByte(); @@ -226,16 +224,11 @@ namespace SixLabors.ImageSharp.Formats.Gif this.globalColorTable?.Dispose(); } - GifColorTableMode colorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag - ? GifColorTableMode.Global - : GifColorTableMode.Local; - - var size = new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height); - - return new GifInfo( - colorTableMode, + this.metaData.AddOrUpdateGifMetaData(this.gifMetaData); + return new ImageInfo( new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), - size, + this.logicalScreenDescriptor.Width, + this.logicalScreenDescriptor.Height, this.metaData); } @@ -269,6 +262,41 @@ namespace SixLabors.ImageSharp.Formats.Gif this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer); } + /// + /// Reads the application extension block parsing any animation information + /// if present. + /// + private void ReadApplicationExtension() + { + int appLength = this.stream.ReadByte(); + + // If the length is 11 then it's a valid extension and most likely + // a NETSCAPE or ANIMEXTS extension. We want the loop count from this. + if (appLength == GifConstants.ApplicationBlockSize) + { + this.stream.Skip(appLength); + int subBlockSize = this.stream.ReadByte(); + + // TODO: There's also a NETSCAPE buffer extension. + // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension + if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) + { + this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); + this.gifMetaData.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; + this.stream.Skip(1); // Skip the terminator. + return; + } + + // Could be XMP or something else not supported yet. + // Back up and skip. + this.stream.Position -= appLength + 1; + this.Skip(appLength); + return; + } + + this.Skip(appLength); // Not supported by any known decoder. + } + /// /// Skips the designated number of bytes in the stream. /// @@ -399,7 +427,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } else { - if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious) + if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious) { prevFrame = previousFrame; } @@ -482,7 +510,7 @@ namespace SixLabors.ImageSharp.Formats.Gif previousFrame = currentFrame ?? image.Frames.RootFrame; - if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToBackground) + if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToBackground) { this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height); } @@ -514,24 +542,26 @@ namespace SixLabors.ImageSharp.Formats.Gif [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetFrameMetaData(ImageFrameMetaData meta) { + var gifMeta = new GifFrameMetaData(); if (this.graphicsControlExtension.DelayTime > 0) { - meta.FrameDelay = this.graphicsControlExtension.DelayTime; + gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime; } // Frames can either use the global table or their own local table. if (this.logicalScreenDescriptor.GlobalColorTableFlag && this.logicalScreenDescriptor.GlobalColorTableSize > 0) { - meta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize; + gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize; } else if (this.imageDescriptor.LocalColorTableFlag && this.imageDescriptor.LocalColorTableSize > 0) { - meta.ColorTableLength = this.imageDescriptor.LocalColorTableSize; + gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize; } - meta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; + gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; + meta.AddOrUpdateGifFrameMetaData(gifMeta); } /// @@ -575,10 +605,17 @@ namespace SixLabors.ImageSharp.Formats.Gif } this.metaData = meta; + this.gifMetaData = new GifMetaData + { + ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag + ? GifColorTableMode.Global + : GifColorTableMode.Local + }; if (this.logicalScreenDescriptor.GlobalColorTableFlag) { int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; + this.gifMetaData.GlobalColorTableLength = globalColorTableLength; this.globalColorTable = this.MemoryAllocator.AllocateManagedByteBuffer(globalColorTableLength, AllocationOptions.Clean); diff --git a/src/ImageSharp/Formats/Gif/DisposalMethod.cs b/src/ImageSharp/Formats/Gif/GifDisposalMethod.cs similarity index 97% rename from src/ImageSharp/Formats/Gif/DisposalMethod.cs rename to src/ImageSharp/Formats/Gif/GifDisposalMethod.cs index 5d3e1b4d89..982340db66 100644 --- a/src/ImageSharp/Formats/Gif/DisposalMethod.cs +++ b/src/ImageSharp/Formats/Gif/GifDisposalMethod.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// in an animation sequence. /// section 23 /// - public enum DisposalMethod + public enum GifDisposalMethod { /// /// No disposal specified. diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index e8e28ccdd8..9f376044d3 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Gets or sets the color table mode: Global or local. /// - public GifColorTableMode ColorTableMode { get; set; } + public GifColorTableMode? ColorTableMode { get; set; } /// public void Encode(Image image, Stream stream) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index ab0ee1fb6f..6f0068f351 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers.Binary; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -44,7 +43,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The color table mode: Global or local. /// - private readonly GifColorTableMode colorTableMode; + private GifColorTableMode? colorTableMode; /// /// A flag indicating whether to ingore the metadata when writing the image. @@ -56,6 +55,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private int bitDepth; + /// + /// Gif specific meta data. + /// + private GifMetaData gifMetaData; + /// /// Initializes a new instance of the class. /// @@ -66,7 +70,6 @@ namespace SixLabors.ImageSharp.Formats.Gif this.memoryAllocator = memoryAllocator; this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding; this.quantizer = options.Quantizer; - this.colorTableMode = options.ColorTableMode; this.ignoreMetadata = options.IgnoreMetadata; } @@ -82,6 +85,10 @@ namespace SixLabors.ImageSharp.Formats.Gif Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); + this.gifMetaData = image.MetaData.GetGifMetaData() ?? new GifMetaData(); + this.colorTableMode = this.colorTableMode ?? this.gifMetaData.ColorTableMode; + bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global); + // Quantize the image returning a palette. QuantizedFrame quantized = this.quantizer.CreateFrameQuantizer().QuantizeFrame(image.Frames.RootFrame); @@ -94,7 +101,6 @@ namespace SixLabors.ImageSharp.Formats.Gif // Write the LSD. int index = this.GetTransparentIndex(quantized); - bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global); this.WriteLogicalScreenDescriptor(image, index, useGlobalTable, stream); if (useGlobalTable) @@ -108,7 +114,7 @@ namespace SixLabors.ImageSharp.Formats.Gif // Write application extension to allow additional frames. if (image.Frames.Count > 1) { - this.WriteApplicationExtension(stream, image.MetaData.RepeatCount); + this.WriteApplicationExtension(stream, this.gifMetaData.RepeatCount); } if (useGlobalTable) @@ -136,8 +142,8 @@ namespace SixLabors.ImageSharp.Formats.Gif for (int i = 0; i < image.Frames.Count; i++) { ImageFrame frame = image.Frames[i]; - - this.WriteGraphicalControlExtension(frame.MetaData, transparencyIndex, stream); + GifFrameMetaData frameMetaData = frame.MetaData.GetGifFrameMetaData() ?? new GifFrameMetaData(); + this.WriteGraphicalControlExtension(frameMetaData, transparencyIndex, stream); this.WriteImageDescriptor(frame, false, stream); if (i == 0) @@ -159,16 +165,18 @@ namespace SixLabors.ImageSharp.Formats.Gif where TPixel : struct, IPixel { ImageFrame previousFrame = null; + GifFrameMetaData previousMeta = null; foreach (ImageFrame frame in image.Frames) { + GifFrameMetaData meta = frame.MetaData.GetGifFrameMetaData() ?? new GifFrameMetaData(); if (quantized is null) { // Allow each frame to be encoded at whatever color depth the frame designates if set. if (previousFrame != null - && previousFrame.MetaData.ColorTableLength != frame.MetaData.ColorTableLength - && frame.MetaData.ColorTableLength > 0) + && previousMeta.ColorTableLength != meta.ColorTableLength + && meta.ColorTableLength > 0) { - quantized = this.quantizer.CreateFrameQuantizer(frame.MetaData.ColorTableLength).QuantizeFrame(frame); + quantized = this.quantizer.CreateFrameQuantizer(meta.ColorTableLength).QuantizeFrame(frame); } else { @@ -177,7 +185,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); - this.WriteGraphicalControlExtension(frame.MetaData, this.GetTransparentIndex(quantized), stream); + this.WriteGraphicalControlExtension(meta, this.GetTransparentIndex(quantized), stream); this.WriteImageDescriptor(frame, true, stream); this.WriteColorTable(quantized, stream); this.WriteImageData(quantized, stream); @@ -185,6 +193,7 @@ namespace SixLabors.ImageSharp.Formats.Gif quantized?.Dispose(); quantized = null; // So next frame can regenerate it previousFrame = frame; + previousMeta = meta; } } @@ -290,25 +299,8 @@ namespace SixLabors.ImageSharp.Formats.Gif // Application Extension Header if (repeatCount != 1) { - this.buffer[0] = GifConstants.ExtensionIntroducer; - this.buffer[1] = GifConstants.ApplicationExtensionLabel; - this.buffer[2] = GifConstants.ApplicationBlockSize; - - // Write NETSCAPE2.0 - GifConstants.ApplicationIdentificationBytes.AsSpan().CopyTo(this.buffer.AsSpan(3, 11)); - - // Application Data ---- - this.buffer[14] = 3; // Application block length - this.buffer[15] = 1; // Data sub-block index (always 1) - - // 0 means loop indefinitely. Count is set as play n + 1 times. - repeatCount = (ushort)Math.Max(0, repeatCount - 1); - - BinaryPrimitives.WriteUInt16LittleEndian(this.buffer.AsSpan(16, 2), repeatCount); // Repeat count for images. - - this.buffer[18] = GifConstants.Terminator; // Terminator - - stream.Write(this.buffer, 0, 19); + var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount); + this.WriteExtension(loopingExtension, stream); } } @@ -348,7 +340,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// The metadata of the image or frame. /// The index of the color in the color palette to make transparent. /// The stream to write to. - private void WriteGraphicalControlExtension(ImageFrameMetaData metaData, int transparencyIndex, Stream stream) + private void WriteGraphicalControlExtension(GifFrameMetaData metaData, int transparencyIndex, Stream stream) { byte packedValue = GifGraphicControlExtension.GetPackedValue( disposalMethod: metaData.DisposalMethod, diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs b/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs new file mode 100644 index 0000000000..cc04d48314 --- /dev/null +++ b/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Gif +{ + /// + /// Provides Gif specific metadata information for the image frame. + /// + public class GifFrameMetaData + { + /// + /// Gets or sets the length of the color table for paletted images. + /// If not 0, then this field indicates the maximum number of colors to use when quantizing the + /// image frame. + /// + public int ColorTableLength { get; set; } + + /// + /// Gets or sets the frame delay for animated images. + /// If not 0, when utilized in Gif animation, this field specifies the number of hundredths (1/100) of a second to + /// wait before continuing with the processing of the Data Stream. + /// The clock starts ticking immediately after the graphic is rendered. + /// + public int FrameDelay { get; set; } + + /// + /// Gets or sets the disposal method for animated images. + /// Primarily used in Gif animation, this field indicates the way in which the graphic is to + /// be treated after being displayed. + /// + public GifDisposalMethod DisposalMethod { get; set; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifInfo.cs b/src/ImageSharp/Formats/Gif/GifInfo.cs deleted file mode 100644 index 1c345a576b..0000000000 --- a/src/ImageSharp/Formats/Gif/GifInfo.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.MetaData; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Formats.Gif -{ - /// - /// Contains information about the bmp including dimensions, pixel type information and additional metadata. - /// - public class GifInfo : ImageInfo - { - /// - /// Initializes a new instance of the class. - /// - /// The color table mode. - /// The image pixel type information. - /// The size of the image in pixels. - /// The images metadata. - internal GifInfo( - GifColorTableMode colorTableMode, - PixelTypeInfo pixelType, - Size size, - ImageMetaData metaData) - : base(pixelType, size, metaData) => this.ColorTableMode = colorTableMode; - - /// - /// Gets the color table mode. - /// - public GifColorTableMode ColorTableMode { get; } - } -} diff --git a/src/ImageSharp/Formats/Gif/GifMetaData.cs b/src/ImageSharp/Formats/Gif/GifMetaData.cs new file mode 100644 index 0000000000..f58f5dff3e --- /dev/null +++ b/src/ImageSharp/Formats/Gif/GifMetaData.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Gif +{ + /// + /// Provides Gif specific metadata information for the image. + /// + public class GifMetaData + { + /// + /// Gets or sets the number of times any animation is repeated. + /// + /// 0 means to repeat indefinitely, count is set as play n + 1 times + /// + /// + public ushort RepeatCount { get; set; } + + /// + /// Gets or sets the color table mode. + /// + public GifColorTableMode ColorTableMode { get; set; } + + /// + /// Gets or sets the length of the global color table if present. + /// + public int GlobalColorTableLength { get; set; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifMetaDataExtensions.cs b/src/ImageSharp/Formats/Gif/GifMetaDataExtensions.cs new file mode 100644 index 0000000000..04b542d2d6 --- /dev/null +++ b/src/ImageSharp/Formats/Gif/GifMetaDataExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData; + +namespace SixLabors.ImageSharp.Formats.Gif +{ + /// + /// Extension methods for storing meta data specific to Gif images. + /// + public static class GifMetaDataExtensions + { + /// + /// Adds or updates the Gif specific meta data to the image. + /// + /// The image meta data. + /// The gif meta data. + public static void AddOrUpdateGifMetaData(this ImageMetaData meta, GifMetaData value) => meta.AddOrUpdateMetaData(GifConstants.MetaDataKey, value); + + /// + /// Gets the Gif format specific meta data from the image. + /// + /// The image meta data. + /// The or null. + public static GifMetaData GetGifMetaData(this ImageMetaData meta) + { + meta.TryGetMetaData(GifConstants.MetaDataKey, out GifMetaData value); + return value; + } + + /// + /// Adds or updates the Gif specific meta data to the image frame. + /// + /// The image meta data. + /// The gif meta data. + public static void AddOrUpdateGifFrameMetaData(this ImageFrameMetaData meta, GifFrameMetaData value) => meta.AddOrUpdateMetaData(GifConstants.MetaDataKey, value); + + /// + /// Gets the Gif format specific meta data from the image frame. + /// + /// The image meta data. + /// The or null. + public static GifFrameMetaData GetGifFrameMetaData(this ImageFrameMetaData meta) + { + meta.TryGetMetaData(GifConstants.MetaDataKey, out GifFrameMetaData value); + return value; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs index e99f09add3..42c202a3d9 100644 --- a/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs +++ b/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Text; +using SixLabors.ImageSharp.MetaData; namespace SixLabors.ImageSharp.Formats.Gif { diff --git a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs index bad6e0031b..7dd558c803 100644 --- a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs +++ b/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs @@ -29,6 +29,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Gets the color table mode: Global or local. /// - GifColorTableMode ColorTableMode { get; } + GifColorTableMode? ColorTableMode { get; } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs index 7ec5f20309..cb548d687d 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Gets the disposal method which indicates the way in which the /// graphic is to be treated after being displayed. /// - public DisposalMethod DisposalMethod => (DisposalMethod)((this.Packed & 0x1C) >> 2); + public GifDisposalMethod DisposalMethod => (GifDisposalMethod)((this.Packed & 0x1C) >> 2); /// /// Gets a value indicating whether transparency flag is to be set. @@ -77,7 +77,7 @@ namespace SixLabors.ImageSharp.Formats.Gif return MemoryMarshal.Cast(buffer)[0]; } - public static byte GetPackedValue(DisposalMethod disposalMethod, bool userInputFlag = false, bool transparencyFlag = false) + public static byte GetPackedValue(GifDisposalMethod disposalMethod, bool userInputFlag = false, bool transparencyFlag = false) { /* Reserved | 3 Bits diff --git a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs new file mode 100644 index 0000000000..49a52edf6a --- /dev/null +++ b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Gif +{ + internal readonly struct GifNetscapeLoopingApplicationExtension : IGifExtension + { + public GifNetscapeLoopingApplicationExtension(ushort repeatCount) => this.RepeatCount = repeatCount; + + public byte Label => GifConstants.ApplicationExtensionLabel; + + /// + /// Gets the repeat count. + /// 0 means loop indefinitely. Count is set as play n + 1 times. + /// + public ushort RepeatCount { get; } + + public static GifNetscapeLoopingApplicationExtension Parse(ReadOnlySpan buffer) + { + ushort repeatCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(0, 2)); + return new GifNetscapeLoopingApplicationExtension(repeatCount); + } + + public int WriteTo(Span buffer) + { + buffer[0] = GifConstants.ApplicationBlockSize; + + // Write NETSCAPE2.0 + GifConstants.NetscapeApplicationIdentificationBytes.AsSpan().CopyTo(buffer.Slice(1, 11)); + + // Application Data ---- + buffer[12] = 3; // Application block length (always 3) + buffer[13] = 1; // Data sub-block indentity (always 1) + + // 0 means loop indefinitely. Count is set as play n + 1 times. + BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(14, 2), this.RepeatCount); + + return 16; // Length - Introducer + Label + Terminator. + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index fb717d3fb8..8c5230a1c4 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -235,7 +235,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.InitIccProfile(); this.InitDerivedMetaDataProperties(); - return new JpegInfo(new PixelTypeInfo(this.BitsPerPixel), new Size(this.ImageWidth, this.ImageHeight), this.MetaData); + return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData); } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegInfo.cs b/src/ImageSharp/Formats/Jpeg/JpegInfo.cs deleted file mode 100644 index 8e9b6257c3..0000000000 --- a/src/ImageSharp/Formats/Jpeg/JpegInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.MetaData; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Formats.Jpeg -{ - /// - /// Contains information about the bmp including dimensions, pixel type information and additional metadata. - /// - public class JpegInfo : ImageInfo - { - /// - /// Initializes a new instance of the class. - /// - /// The image pixel type information. - /// The size of the image in pixels. - /// The images metadata. - internal JpegInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData) - : base(pixelType, size, metaData) - { - } - } -} diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 9032560160..be1914174b 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -17,7 +17,6 @@ using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; -using SixLabors.Primitives; namespace SixLabors.ImageSharp.Formats.Png { @@ -349,7 +348,7 @@ namespace SixLabors.ImageSharp.Formats.Png throw new ImageFormatException("PNG Image does not contain a header chunk"); } - return new PngInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new Size(this.header.Width, this.header.Height), metadata); + return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); } /// diff --git a/src/ImageSharp/Formats/Png/PngInfo.cs b/src/ImageSharp/Formats/Png/PngInfo.cs deleted file mode 100644 index 2bb0b16b70..0000000000 --- a/src/ImageSharp/Formats/Png/PngInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.MetaData; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Formats.Png -{ - /// - /// Contains information about the bmp including dimensions, pixel type information and additional metadata. - /// - public class PngInfo : ImageInfo - { - /// - /// Initializes a new instance of the class. - /// - /// The image pixel type information. - /// The size of the image in pixels. - /// The images metadata. - internal PngInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData) - : base(pixelType, size, metaData) - { - } - } -} diff --git a/src/ImageSharp/ImageInfo.cs b/src/ImageSharp/ImageInfo.cs index eed1c66918..6f894cb599 100644 --- a/src/ImageSharp/ImageInfo.cs +++ b/src/ImageSharp/ImageInfo.cs @@ -3,26 +3,26 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.MetaData; -using SixLabors.Primitives; namespace SixLabors.ImageSharp { /// /// Contains information about the image including dimensions, pixel type information and additional metadata /// - public abstract class ImageInfo : IImageInfo + internal sealed class ImageInfo : IImageInfo { /// /// Initializes a new instance of the class. /// /// The image pixel type information. - /// The size of the image in pixels. + /// The width of the image in pixels. + /// The height of the image in pixels. /// The images metadata. - protected ImageInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData) + public ImageInfo(PixelTypeInfo pixelType, int width, int height, ImageMetaData metaData) { this.PixelType = pixelType; - this.Width = size.Width; - this.Height = size.Height; + this.Width = width; + this.Height = height; this.MetaData = metaData; } diff --git a/src/ImageSharp/Formats/Gif/FrameDecodingMode.cs b/src/ImageSharp/MetaData/FrameDecodingMode.cs similarity index 91% rename from src/ImageSharp/Formats/Gif/FrameDecodingMode.cs rename to src/ImageSharp/MetaData/FrameDecodingMode.cs index 05791c92e5..2863fbf8f9 100644 --- a/src/ImageSharp/Formats/Gif/FrameDecodingMode.cs +++ b/src/ImageSharp/MetaData/FrameDecodingMode.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -namespace SixLabors.ImageSharp.Formats.Gif +namespace SixLabors.ImageSharp.MetaData { /// /// Enumerated frame process modes to apply to multi-frame images. diff --git a/src/ImageSharp/MetaData/ImageFrameMetaData.cs b/src/ImageSharp/MetaData/ImageFrameMetaData.cs index f83e092c9f..55678789e5 100644 --- a/src/ImageSharp/MetaData/ImageFrameMetaData.cs +++ b/src/ImageSharp/MetaData/ImageFrameMetaData.cs @@ -1,7 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.Formats.Gif; +using System; +using System.Collections.Generic; namespace SixLabors.ImageSharp.MetaData { @@ -10,6 +11,8 @@ namespace SixLabors.ImageSharp.MetaData /// public sealed class ImageFrameMetaData { + private readonly Dictionary metaData = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// /// Initializes a new instance of the class. /// @@ -28,37 +31,57 @@ namespace SixLabors.ImageSharp.MetaData { DebugGuard.NotNull(other, nameof(other)); - this.ColorTableLength = other.ColorTableLength; - this.FrameDelay = other.FrameDelay; - this.DisposalMethod = other.DisposalMethod; + foreach (KeyValuePair meta in other.metaData) + { + this.metaData.Add(meta.Key, meta.Value); + } } /// - /// Gets or sets the length of the color table for paletted images. - /// If not 0, then this field indicates the maximum number of colors to use when quantizing the - /// image frame. + /// Clones this ImageFrameMetaData. /// - public int ColorTableLength { get; set; } + /// The cloned instance. + public ImageFrameMetaData Clone() => new ImageFrameMetaData(this); /// - /// Gets or sets the frame delay for animated images. - /// If not 0, when utilized in Gif animation, this field specifies the number of hundredths (1/100) of a second to - /// wait before continuing with the processing of the Data Stream. - /// The clock starts ticking immediately after the graphic is rendered. + /// Adds or updates the specified key and value to the . /// - public int FrameDelay { get; set; } + /// The key of the metadata to add. + /// The value of the element to add. + /// key is null. + /// value is null. + /// An element with the same key already exists in the . + public void AddOrUpdateMetaData(string key, object value) + { + // Don't think this needs to be threadsafe. + Guard.NotNull(value, nameof(value)); + this.metaData[key] = value; + } /// - /// Gets or sets the disposal method for animated images. - /// Primarily used in Gif animation, this field indicates the way in which the graphic is to - /// be treated after being displayed. + /// Gets the metadata value associated with the specified key. /// - public DisposalMethod DisposalMethod { get; set; } + /// The type of value. + /// The key of the value to get. + /// + /// When this method returns, contains the metadata value associated with the specified key, + /// if the key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the contains an element with + /// the specified key; otherwise, false. + /// + public bool TryGetMetaData(string key, out T value) + { + if (this.metaData.TryGetValue(key, out object meta)) + { + value = (T)meta; + return true; + } - /// - /// Clones this ImageFrameMetaData. - /// - /// The cloned instance. - public ImageFrameMetaData Clone() => new ImageFrameMetaData(this); + value = default; + return false; + } } } \ No newline at end of file diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index 40880bd085..8233798c22 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.MetaData.Profiles.Icc; @@ -24,6 +25,7 @@ namespace SixLabors.ImageSharp.MetaData /// public const double DefaultVerticalResolution = 96; + private readonly Dictionary metaData = new Dictionary(StringComparer.OrdinalIgnoreCase); private double horizontalResolution; private double verticalResolution; @@ -48,7 +50,11 @@ namespace SixLabors.ImageSharp.MetaData this.HorizontalResolution = other.HorizontalResolution; this.VerticalResolution = other.VerticalResolution; this.ResolutionUnits = other.ResolutionUnits; - this.RepeatCount = other.RepeatCount; + + foreach (KeyValuePair meta in other.metaData) + { + this.metaData.Add(meta.Key, meta.Value); + } foreach (ImageProperty property in other.Properties) { @@ -125,10 +131,51 @@ namespace SixLabors.ImageSharp.MetaData public IList Properties { get; } = new List(); /// - /// Gets or sets the number of times any animation is repeated. - /// 0 means to repeat indefinitely. + /// Adds or updates the specified key and value to the . + /// + /// The key of the metadata to add. + /// The value of the element to add. + /// key is null. + /// value is null. + /// An element with the same key already exists in the . + public void AddOrUpdateMetaData(string key, object value) + { + // Don't think this needs to be threadsafe. + Guard.NotNull(value, nameof(value)); + this.metaData[key] = value; + } + + /// + /// Gets the metadata value associated with the specified key. /// - public ushort RepeatCount { get; set; } + /// The type of value. + /// The key of the value to get. + /// + /// When this method returns, contains the metadata value associated with the specified key, + /// if the key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the contains an element with + /// the specified key; otherwise, false. + /// + public bool TryGetMetaData(string key, out T value) + { + if (this.metaData.TryGetValue(key, out object meta)) + { + value = (T)meta; + return true; + } + + value = default; + return false; + } + + /// + /// Clones this into a new instance + /// + /// The cloned metadata instance + public ImageMetaData Clone() => new ImageMetaData(this); /// /// Looks up a property with the provided name. @@ -153,21 +200,9 @@ namespace SixLabors.ImageSharp.MetaData return false; } - /// - /// Clones this into a new instance - /// - /// The cloned metadata instance - public ImageMetaData Clone() - { - return new ImageMetaData(this); - } - /// /// Synchronizes the profiles with the current meta data. /// - internal void SyncProfiles() - { - this.ExifProfile?.Sync(this); - } + internal void SyncProfiles() => this.ExifProfile?.Sync(this); } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 11fa4d5d5f..2b08bf20de 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -186,32 +186,37 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif using (var inStream = new MemoryStream(TestFile.Create(TestImages.Gif.Leo).Bytes)) using (var outStream = new MemoryStream()) { - var info = (GifInfo)Image.Identify(inStream); - GifColorTableMode colorMode = info.ColorTableMode; inStream.Position = 0; var image = Image.Load(inStream); + GifMetaData metaData = image.MetaData.GetGifMetaData(); + GifFrameMetaData frameMetaData = image.Frames.RootFrame.MetaData.GetGifFrameMetaData(); + GifColorTableMode colorMode = metaData.ColorTableMode; var encoder = new GifEncoder() { ColorTableMode = colorMode, - Quantizer = new OctreeQuantizer(image.Frames.RootFrame.MetaData.ColorTableLength) + Quantizer = new OctreeQuantizer(frameMetaData.ColorTableLength) }; image.Save(outStream, encoder); outStream.Position = 0; - var cloneInfo = (GifInfo)Image.Identify(outStream); outStream.Position = 0; var clone = Image.Load(outStream); + GifMetaData cloneMetaData = clone.MetaData.GetGifMetaData(); + Assert.Equal(metaData.ColorTableMode, cloneMetaData.ColorTableMode); + // Gifiddle and Cyotek GifInfo say this image has 64 colors. - Assert.Equal(64, image.Frames.RootFrame.MetaData.ColorTableLength); - Assert.Equal(info.ColorTableMode, cloneInfo.ColorTableMode); + Assert.Equal(64, frameMetaData.ColorTableLength); for (int i = 0; i < image.Frames.Count; i++) { - Assert.Equal(image.Frames[i].MetaData.ColorTableLength, clone.Frames[i].MetaData.ColorTableLength); - Assert.Equal(image.Frames[i].MetaData.FrameDelay, clone.Frames[i].MetaData.FrameDelay); + GifFrameMetaData ifm = image.Frames[i].MetaData.GetGifFrameMetaData(); + GifFrameMetaData cifm = clone.Frames[i].MetaData.GetGifFrameMetaData(); + + Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength); + Assert.Equal(ifm.FrameDelay, cifm.FrameDelay); } image.Dispose(); diff --git a/tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs b/tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs index 2790b1a576..dc0da5e2db 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs @@ -12,10 +12,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif [Fact] public void TestPackedValue() { - Assert.Equal(0, GifGraphicControlExtension.GetPackedValue(DisposalMethod.Unspecified, false, false)); - Assert.Equal(11, GifGraphicControlExtension.GetPackedValue(DisposalMethod.RestoreToBackground, true, true)); - Assert.Equal(4, GifGraphicControlExtension.GetPackedValue(DisposalMethod.NotDispose, false, false)); - Assert.Equal(14, GifGraphicControlExtension.GetPackedValue(DisposalMethod.RestoreToPrevious, true, false)); + Assert.Equal(0, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.Unspecified, false, false)); + Assert.Equal(11, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.RestoreToBackground, true, true)); + Assert.Equal(4, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.NotDispose, false, false)); + Assert.Equal(14, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.RestoreToPrevious, true, false)); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/ImageInfoTests.cs b/tests/ImageSharp.Tests/ImageInfoTests.cs index d46e340521..91f6804c0f 100644 --- a/tests/ImageSharp.Tests/ImageInfoTests.cs +++ b/tests/ImageSharp.Tests/ImageInfoTests.cs @@ -2,10 +2,6 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.MetaData; using SixLabors.Primitives; @@ -16,7 +12,7 @@ namespace SixLabors.ImageSharp.Tests public class ImageInfoTests { [Fact] - public void JpegInfoInitializesCorrectly() + public void ImageInfoInitializesCorrectly() { const int Width = 50; const int Height = 60; @@ -25,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests var pixelType = new PixelTypeInfo(8); var meta = new ImageMetaData(); - var info = new JpegInfo(pixelType, size, meta); + var info = new ImageInfo(pixelType, Width, Height, meta); Assert.Equal(pixelType, info.PixelType); Assert.Equal(Width, info.Width); @@ -34,67 +30,5 @@ namespace SixLabors.ImageSharp.Tests Assert.Equal(rectangle, info.Bounds()); Assert.Equal(meta, info.MetaData); } - - [Fact] - public void BmpInfoInitializesCorrectly() - { - const int Width = 50; - const int Height = 60; - var size = new Size(Width, Height); - var rectangle = new Rectangle(0, 0, Width, Height); - var pixelType = new PixelTypeInfo(8); - var meta = new ImageMetaData(); - - var info = new BmpInfo(pixelType, size, meta); - - Assert.Equal(pixelType, info.PixelType); - Assert.Equal(Width, info.Width); - Assert.Equal(Height, info.Height); - Assert.Equal(size, info.Size()); - Assert.Equal(rectangle, info.Bounds()); - Assert.Equal(meta, info.MetaData); - } - - [Fact] - public void PngInfoInitializesCorrectly() - { - const int Width = 50; - const int Height = 60; - var size = new Size(Width, Height); - var rectangle = new Rectangle(0, 0, Width, Height); - var pixelType = new PixelTypeInfo(8); - var meta = new ImageMetaData(); - - var info = new PngInfo(pixelType, size, meta); - - Assert.Equal(pixelType, info.PixelType); - Assert.Equal(Width, info.Width); - Assert.Equal(Height, info.Height); - Assert.Equal(size, info.Size()); - Assert.Equal(rectangle, info.Bounds()); - Assert.Equal(meta, info.MetaData); - } - - [Fact] - public void GifInfoInitializesCorrectly() - { - const GifColorTableMode mode = GifColorTableMode.Local; - const int Width = 50; - const int Height = 60; - var size = new Size(Width, Height); - var rectangle = new Rectangle(0, 0, Width, Height); - var pixelType = new PixelTypeInfo(8); - var meta = new ImageMetaData(); - - var info = new GifInfo(mode, pixelType, size, meta); - - Assert.Equal(mode, info.ColorTableMode); - Assert.Equal(pixelType, info.PixelType); - Assert.Equal(Width, info.Width); - Assert.Equal(Height, info.Height); - Assert.Equal(size, info.Size()); - Assert.Equal(rectangle, info.Bounds()); - Assert.Equal(meta, info.MetaData); - } } } diff --git a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs index 6e18fe2537..c250d973b5 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs @@ -17,20 +17,24 @@ namespace SixLabors.ImageSharp.Tests { const int frameDelay = 42; const int colorTableLength = 128; - const DisposalMethod disposalMethod = DisposalMethod.RestoreToBackground; + const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground; - var metaData = new ImageFrameMetaData + var gifFrameMetaData = new GifFrameMetaData { FrameDelay = frameDelay, ColorTableLength = colorTableLength, DisposalMethod = disposalMethod }; + var metaData = new ImageFrameMetaData(); + metaData.AddOrUpdateGifFrameMetaData(gifFrameMetaData); + var clone = new ImageFrameMetaData(metaData); + GifFrameMetaData cloneGifFrameMetaData = clone.GetGifFrameMetaData(); - Assert.Equal(frameDelay, clone.FrameDelay); - Assert.Equal(colorTableLength, clone.ColorTableLength); - Assert.Equal(disposalMethod, clone.DisposalMethod); + Assert.Equal(frameDelay, cloneGifFrameMetaData.FrameDelay); + Assert.Equal(colorTableLength, cloneGifFrameMetaData.ColorTableLength); + Assert.Equal(disposalMethod, cloneGifFrameMetaData.DisposalMethod); } } } diff --git a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs index 8934ebc361..d681c90ba2 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs @@ -28,7 +28,6 @@ namespace SixLabors.ImageSharp.Tests metaData.HorizontalResolution = 4; metaData.VerticalResolution = 2; metaData.Properties.Add(imageProperty); - metaData.RepeatCount = 1; ImageMetaData clone = metaData.Clone(); @@ -36,7 +35,6 @@ namespace SixLabors.ImageSharp.Tests Assert.Equal(4, clone.HorizontalResolution); Assert.Equal(2, clone.VerticalResolution); Assert.Equal(imageProperty, clone.Properties[0]); - Assert.Equal(1, clone.RepeatCount); } [Fact] diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index 3696accdd3..427a565424 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -48,16 +48,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs using (var sourceBitmap = new System.Drawing.Bitmap(stream)) { var pixelType = new PixelTypeInfo(System.Drawing.Image.GetPixelFormatSize(sourceBitmap.PixelFormat)); - var size = new SixLabors.Primitives.Size(sourceBitmap.Width, sourceBitmap.Height); - return new SystemDrawingInfo(pixelType, size, new ImageMetaData()); - } - } - - private class SystemDrawingInfo : ImageInfo - { - public SystemDrawingInfo(PixelTypeInfo pixelType, SixLabors.Primitives.Size size, ImageMetaData metaData) - : base(pixelType, size, metaData) - { + return new ImageInfo(pixelType, sourceBitmap.Width, sourceBitmap.Height, new ImageMetaData()); } } } From 72d1d277b278d2c30a1b96832745812807afdac6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 6 Sep 2018 10:44:10 +0100 Subject: [PATCH 03/14] Fix local color table encoding. --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 3 ++- .../Formats/Gif/Sections/GifImageDescriptor.cs | 2 +- .../Gif/Sections/GifImageDescriptorTests.cs | 14 +++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 6f0068f351..b373446589 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -71,6 +71,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding; this.quantizer = options.Quantizer; this.ignoreMetadata = options.IgnoreMetadata; + this.colorTableMode = options.ColorTableMode; } /// @@ -246,7 +247,7 @@ namespace SixLabors.ImageSharp.Formats.Gif private void WriteLogicalScreenDescriptor(Image image, int transparencyIndex, bool useGlobalTable, Stream stream) where TPixel : struct, IPixel { - byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth, false, this.bitDepth - 1); + byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1); // The Pixel Aspect Ratio is defined to be the quotient of the pixel's // width over its height. The value range in this field allows diff --git a/src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs b/src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs index e2f5bee78e..c3504dfe7b 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs @@ -108,7 +108,7 @@ namespace SixLabors.ImageSharp.Formats.Gif value |= 1 << 5; } - value |= (byte)(localColorTableSize - 1); + value |= (byte)localColorTableSize; return value; } diff --git a/tests/ImageSharp.Tests/Formats/Gif/Sections/GifImageDescriptorTests.cs b/tests/ImageSharp.Tests/Formats/Gif/Sections/GifImageDescriptorTests.cs index 4ef4c12d97..6a90c0c277 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/Sections/GifImageDescriptorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/Sections/GifImageDescriptorTests.cs @@ -12,13 +12,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif [Fact] public void TestPackedValue() { - Assert.Equal(128, GifImageDescriptor.GetPackedValue(true, false, false, 1)); // localColorTable - Assert.Equal(64, GifImageDescriptor.GetPackedValue(false, true, false, 1)); // interfaceFlag - Assert.Equal(32, GifImageDescriptor.GetPackedValue(false, false, true, 1)); // sortFlag - Assert.Equal(224, GifImageDescriptor.GetPackedValue(true, true, true, 1)); // all - Assert.Equal(7, GifImageDescriptor.GetPackedValue(false, false, false, 8)); - Assert.Equal(227, GifImageDescriptor.GetPackedValue(true, true, true, 4)); - Assert.Equal(231, GifImageDescriptor.GetPackedValue(true, true, true, 8)); + Assert.Equal(129, GifImageDescriptor.GetPackedValue(true, false, false, 1)); // localColorTable + Assert.Equal(65, GifImageDescriptor.GetPackedValue(false, true, false, 1)); // interfaceFlag + Assert.Equal(33, GifImageDescriptor.GetPackedValue(false, false, true, 1)); // sortFlag + Assert.Equal(225, GifImageDescriptor.GetPackedValue(true, true, true, 1)); // all + Assert.Equal(8, GifImageDescriptor.GetPackedValue(false, false, false, 8)); + Assert.Equal(228, GifImageDescriptor.GetPackedValue(true, true, true, 4)); + Assert.Equal(232, GifImageDescriptor.GetPackedValue(true, true, true, 8)); } } } \ No newline at end of file From dc4ec30a14d82bd901ccb257a2de4277d0d52e80 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 7 Sep 2018 22:01:18 +0100 Subject: [PATCH 04/14] Strong type meta query + format singletons --- .../Formats/Bmp/BmpConfigurationModule.cs | 8 +-- src/ImageSharp/Formats/Bmp/BmpFormat.cs | 14 ++++-- .../Formats/Bmp/BmpImageFormatDetector.cs | 2 +- src/ImageSharp/Formats/Bmp/BmpMetaData.cs | 15 ++++++ src/ImageSharp/Formats/Bmp/ImageExtensions.cs | 2 +- .../Formats/Gif/GifConfigurationModule.cs | 9 ++-- src/ImageSharp/Formats/Gif/GifConstants.cs | 5 -- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 6 +-- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 6 +-- src/ImageSharp/Formats/Gif/GifFormat.cs | 14 ++++-- .../Formats/Gif/GifFrameMetaData.cs | 4 +- .../Formats/Gif/GifImageFormatDetector.cs | 2 +- src/ImageSharp/Formats/Gif/GifMetaData.cs | 4 +- .../Formats/Gif/GifMetaDataExtensions.cs | 49 ------------------- src/ImageSharp/Formats/Gif/ImageExtensions.cs | 2 +- src/ImageSharp/Formats/ImageFormatBase{T}.cs | 38 ++++++++++++++ .../Formats/Jpeg/ImageExtensions.cs | 2 +- .../Formats/Jpeg/JpegConfigurationModule.cs | 9 ++-- src/ImageSharp/Formats/Jpeg/JpegFormat.cs | 14 ++++-- .../Formats/Jpeg/JpegImageFormatDetector.cs | 2 +- src/ImageSharp/Formats/Jpeg/JpegMetaData.cs | 15 ++++++ src/ImageSharp/Formats/Png/ImageExtensions.cs | 2 +- .../Formats/Png/PngConfigurationModule.cs | 4 +- src/ImageSharp/Formats/Png/PngFormat.cs | 14 ++++-- .../Formats/Png/PngImageFormatDetector.cs | 2 +- src/ImageSharp/Formats/Png/PngMetaData.cs | 15 ++++++ src/ImageSharp/ImageFormats.cs | 37 -------------- .../MetaData/IImageFormatFrameMetaData.cs | 13 +++++ .../MetaData/IImageFormatMetaData.cs | 13 +++++ src/ImageSharp/MetaData/ImageFrameMetaData.cs | 30 +++++------- src/ImageSharp/MetaData/ImageMetaData.cs | 30 +++++------- tests/ImageSharp.Tests/ConfigurationTests.cs | 13 ++--- .../Formats/GeneralFormatTests.cs | 2 +- .../Formats/Gif/GifEncoderTests.cs | 10 ++-- .../Formats/ImageFormatManagerTests.cs | 4 +- .../MetaData/ImageFrameMetaDataTests.cs | 4 +- .../TestUtilities/TestEnvironment.Formats.cs | 4 +- 37 files changed, 221 insertions(+), 198 deletions(-) create mode 100644 src/ImageSharp/Formats/Bmp/BmpMetaData.cs delete mode 100644 src/ImageSharp/Formats/Gif/GifMetaDataExtensions.cs create mode 100644 src/ImageSharp/Formats/ImageFormatBase{T}.cs create mode 100644 src/ImageSharp/Formats/Jpeg/JpegMetaData.cs create mode 100644 src/ImageSharp/Formats/Png/PngMetaData.cs delete mode 100644 src/ImageSharp/ImageFormats.cs create mode 100644 src/ImageSharp/MetaData/IImageFormatFrameMetaData.cs create mode 100644 src/ImageSharp/MetaData/IImageFormatMetaData.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpConfigurationModule.cs b/src/ImageSharp/Formats/Bmp/BmpConfigurationModule.cs index 956acc1578..57117cc075 100644 --- a/src/ImageSharp/Formats/Bmp/BmpConfigurationModule.cs +++ b/src/ImageSharp/Formats/Bmp/BmpConfigurationModule.cs @@ -9,11 +9,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp public sealed class BmpConfigurationModule : IConfigurationModule { /// - public void Configure(Configuration config) + public void Configure(Configuration configuration) { - config.ImageFormatsManager.SetEncoder(ImageFormats.Bmp, new BmpEncoder()); - config.ImageFormatsManager.SetDecoder(ImageFormats.Bmp, new BmpDecoder()); - config.ImageFormatsManager.AddImageFormatDetector(new BmpImageFormatDetector()); + configuration.ImageFormatsManager.SetEncoder(BmpFormat.Instance, new BmpEncoder()); + configuration.ImageFormatsManager.SetDecoder(BmpFormat.Instance, new BmpDecoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new BmpImageFormatDetector()); } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Bmp/BmpFormat.cs b/src/ImageSharp/Formats/Bmp/BmpFormat.cs index 64c6574c1e..de3f6f3f44 100644 --- a/src/ImageSharp/Formats/Bmp/BmpFormat.cs +++ b/src/ImageSharp/Formats/Bmp/BmpFormat.cs @@ -8,18 +8,22 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Registers the image encoders, decoders and mime type detectors for the bmp format. /// - internal sealed class BmpFormat : IImageFormat + internal sealed class BmpFormat : ImageFormatBase { + private BmpFormat() + { + } + /// - public string Name => "BMP"; + public override string Name => "BMP"; /// - public string DefaultMimeType => "image/bmp"; + public override string DefaultMimeType => "image/bmp"; /// - public IEnumerable MimeTypes => BmpConstants.MimeTypes; + public override IEnumerable MimeTypes => BmpConstants.MimeTypes; /// - public IEnumerable FileExtensions => BmpConstants.FileExtensions; + public override IEnumerable FileExtensions => BmpConstants.FileExtensions; } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs b/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs index bb884019b7..6a740d47d1 100644 --- a/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public IImageFormat DetectFormat(ReadOnlySpan header) { - return this.IsSupportedFileFormat(header) ? ImageFormats.Bmp : null; + return this.IsSupportedFileFormat(header) ? BmpFormat.Instance : null; } private bool IsSupportedFileFormat(ReadOnlySpan header) diff --git a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs new file mode 100644 index 0000000000..1e350200a4 --- /dev/null +++ b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData; + +namespace SixLabors.ImageSharp.Formats.Bmp +{ + /// + /// Provides Bmp specific metadata information for the image. + /// + public class BmpMetaData : IImageFormatMetaData + { + // TODO: Analyse what properties we would like to preserve. + } +} diff --git a/src/ImageSharp/Formats/Bmp/ImageExtensions.cs b/src/ImageSharp/Formats/Bmp/ImageExtensions.cs index 57e4615bad..aa1c353db2 100644 --- a/src/ImageSharp/Formats/Bmp/ImageExtensions.cs +++ b/src/ImageSharp/Formats/Bmp/ImageExtensions.cs @@ -34,6 +34,6 @@ namespace SixLabors.ImageSharp /// Thrown if the stream is null. public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) where TPixel : struct, IPixel - => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ImageFormats.Bmp)); + => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance)); } } diff --git a/src/ImageSharp/Formats/Gif/GifConfigurationModule.cs b/src/ImageSharp/Formats/Gif/GifConfigurationModule.cs index 0bb62779eb..861d3e0368 100644 --- a/src/ImageSharp/Formats/Gif/GifConfigurationModule.cs +++ b/src/ImageSharp/Formats/Gif/GifConfigurationModule.cs @@ -9,12 +9,11 @@ namespace SixLabors.ImageSharp.Formats.Gif public sealed class GifConfigurationModule : IConfigurationModule { /// - public void Configure(Configuration config) + public void Configure(Configuration configuration) { - config.ImageFormatsManager.SetEncoder(ImageFormats.Gif, new GifEncoder()); - config.ImageFormatsManager.SetDecoder(ImageFormats.Gif, new GifDecoder()); - - config.ImageFormatsManager.AddImageFormatDetector(new GifImageFormatDetector()); + configuration.ImageFormatsManager.SetEncoder(GifFormat.Instance, new GifEncoder()); + configuration.ImageFormatsManager.SetDecoder(GifFormat.Instance, new GifDecoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new GifImageFormatDetector()); } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs index cc80b0cce2..8167d0d2e0 100644 --- a/src/ImageSharp/Formats/Gif/GifConstants.cs +++ b/src/ImageSharp/Formats/Gif/GifConstants.cs @@ -26,11 +26,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// internal static readonly byte[] MagicNumber = Encoding.UTF8.GetBytes(FileType + FileVersion); - /// - /// Gets the key used for storing and retriving metadata. - /// - public const string MetaDataKey = FileType; - /// /// The extension block introducer !. /// diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 092ac859df..f503812641 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -164,7 +164,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.globalColorTable?.Dispose(); } - image?.MetaData.AddOrUpdateGifMetaData(this.gifMetaData); + image?.MetaData.AddOrUpdateFormatMetaData(GifFormat.Instance, this.gifMetaData); return image; } @@ -224,7 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.globalColorTable?.Dispose(); } - this.metaData.AddOrUpdateGifMetaData(this.gifMetaData); + this.metaData.AddOrUpdateFormatMetaData(GifFormat.Instance, this.gifMetaData); return new ImageInfo( new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), this.logicalScreenDescriptor.Width, @@ -561,7 +561,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; - meta.AddOrUpdateGifFrameMetaData(gifMeta); + meta.AddOrUpdateFormatMetaData(GifFormat.Instance, gifMeta); } /// diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index b373446589..20f2f1d336 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp.Formats.Gif Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - this.gifMetaData = image.MetaData.GetGifMetaData() ?? new GifMetaData(); + this.gifMetaData = image.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); this.colorTableMode = this.colorTableMode ?? this.gifMetaData.ColorTableMode; bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global); @@ -143,7 +143,7 @@ namespace SixLabors.ImageSharp.Formats.Gif for (int i = 0; i < image.Frames.Count; i++) { ImageFrame frame = image.Frames[i]; - GifFrameMetaData frameMetaData = frame.MetaData.GetGifFrameMetaData() ?? new GifFrameMetaData(); + GifFrameMetaData frameMetaData = frame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); this.WriteGraphicalControlExtension(frameMetaData, transparencyIndex, stream); this.WriteImageDescriptor(frame, false, stream); @@ -169,7 +169,7 @@ namespace SixLabors.ImageSharp.Formats.Gif GifFrameMetaData previousMeta = null; foreach (ImageFrame frame in image.Frames) { - GifFrameMetaData meta = frame.MetaData.GetGifFrameMetaData() ?? new GifFrameMetaData(); + GifFrameMetaData meta = frame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); if (quantized is null) { // Allow each frame to be encoded at whatever color depth the frame designates if set. diff --git a/src/ImageSharp/Formats/Gif/GifFormat.cs b/src/ImageSharp/Formats/Gif/GifFormat.cs index 6353690f47..2d14abb444 100644 --- a/src/ImageSharp/Formats/Gif/GifFormat.cs +++ b/src/ImageSharp/Formats/Gif/GifFormat.cs @@ -8,18 +8,22 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Registers the image encoders, decoders and mime type detectors for the gif format. /// - internal sealed class GifFormat : IImageFormat + internal sealed class GifFormat : ImageFormatBase { + private GifFormat() + { + } + /// - public string Name => "GIF"; + public override string Name => "GIF"; /// - public string DefaultMimeType => "image/gif"; + public override string DefaultMimeType => "image/gif"; /// - public IEnumerable MimeTypes => GifConstants.MimeTypes; + public override IEnumerable MimeTypes => GifConstants.MimeTypes; /// - public IEnumerable FileExtensions => GifConstants.FileExtensions; + public override IEnumerable FileExtensions => GifConstants.FileExtensions; } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs b/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs index cc04d48314..a7b68e6e85 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.MetaData; + namespace SixLabors.ImageSharp.Formats.Gif { /// /// Provides Gif specific metadata information for the image frame. /// - public class GifFrameMetaData + public class GifFrameMetaData : IImageFormatFrameMetaData { /// /// Gets or sets the length of the color table for paletted images. diff --git a/src/ImageSharp/Formats/Gif/GifImageFormatDetector.cs b/src/ImageSharp/Formats/Gif/GifImageFormatDetector.cs index bfbd334b01..b8f9a03f1a 100644 --- a/src/ImageSharp/Formats/Gif/GifImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Gif/GifImageFormatDetector.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// public IImageFormat DetectFormat(ReadOnlySpan header) { - return this.IsSupportedFileFormat(header) ? ImageFormats.Gif : null; + return this.IsSupportedFileFormat(header) ? GifFormat.Instance : null; } private bool IsSupportedFileFormat(ReadOnlySpan header) diff --git a/src/ImageSharp/Formats/Gif/GifMetaData.cs b/src/ImageSharp/Formats/Gif/GifMetaData.cs index f58f5dff3e..cc0716ec57 100644 --- a/src/ImageSharp/Formats/Gif/GifMetaData.cs +++ b/src/ImageSharp/Formats/Gif/GifMetaData.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.MetaData; + namespace SixLabors.ImageSharp.Formats.Gif { /// /// Provides Gif specific metadata information for the image. /// - public class GifMetaData + public class GifMetaData : IImageFormatMetaData { /// /// Gets or sets the number of times any animation is repeated. diff --git a/src/ImageSharp/Formats/Gif/GifMetaDataExtensions.cs b/src/ImageSharp/Formats/Gif/GifMetaDataExtensions.cs deleted file mode 100644 index 04b542d2d6..0000000000 --- a/src/ImageSharp/Formats/Gif/GifMetaDataExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.MetaData; - -namespace SixLabors.ImageSharp.Formats.Gif -{ - /// - /// Extension methods for storing meta data specific to Gif images. - /// - public static class GifMetaDataExtensions - { - /// - /// Adds or updates the Gif specific meta data to the image. - /// - /// The image meta data. - /// The gif meta data. - public static void AddOrUpdateGifMetaData(this ImageMetaData meta, GifMetaData value) => meta.AddOrUpdateMetaData(GifConstants.MetaDataKey, value); - - /// - /// Gets the Gif format specific meta data from the image. - /// - /// The image meta data. - /// The or null. - public static GifMetaData GetGifMetaData(this ImageMetaData meta) - { - meta.TryGetMetaData(GifConstants.MetaDataKey, out GifMetaData value); - return value; - } - - /// - /// Adds or updates the Gif specific meta data to the image frame. - /// - /// The image meta data. - /// The gif meta data. - public static void AddOrUpdateGifFrameMetaData(this ImageFrameMetaData meta, GifFrameMetaData value) => meta.AddOrUpdateMetaData(GifConstants.MetaDataKey, value); - - /// - /// Gets the Gif format specific meta data from the image frame. - /// - /// The image meta data. - /// The or null. - public static GifFrameMetaData GetGifFrameMetaData(this ImageFrameMetaData meta) - { - meta.TryGetMetaData(GifConstants.MetaDataKey, out GifFrameMetaData value); - return value; - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/ImageExtensions.cs b/src/ImageSharp/Formats/Gif/ImageExtensions.cs index 1c41285a97..8ddd4247e1 100644 --- a/src/ImageSharp/Formats/Gif/ImageExtensions.cs +++ b/src/ImageSharp/Formats/Gif/ImageExtensions.cs @@ -34,6 +34,6 @@ namespace SixLabors.ImageSharp /// Thrown if the stream is null. public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) where TPixel : struct, IPixel - => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ImageFormats.Gif)); + => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance)); } } diff --git a/src/ImageSharp/Formats/ImageFormatBase{T}.cs b/src/ImageSharp/Formats/ImageFormatBase{T}.cs new file mode 100644 index 0000000000..85273db2be --- /dev/null +++ b/src/ImageSharp/Formats/ImageFormatBase{T}.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Formats +{ + /// + /// The base class for all image formats. + /// Inheriting classes should implement the singleton pattern by creating a private constructor. + /// + /// The type of image format. + public abstract class ImageFormatBase : IImageFormat + where T : class, IImageFormat + { + private static readonly Lazy Lazy = new Lazy(CreateInstance); + + /// + /// Gets the current instance. + /// + public static T Instance => Lazy.Value; + + /// + public abstract string Name { get; } + + /// + public abstract string DefaultMimeType { get; } + + /// + public abstract IEnumerable MimeTypes { get; } + + /// + public abstract IEnumerable FileExtensions { get; } + + private static T CreateInstance() => (T)Activator.CreateInstance(typeof(T), true); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs b/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs index 1d3be063dd..cb7fc19446 100644 --- a/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs +++ b/src/ImageSharp/Formats/Jpeg/ImageExtensions.cs @@ -34,6 +34,6 @@ namespace SixLabors.ImageSharp /// Thrown if the stream is null. public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) where TPixel : struct, IPixel - => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ImageFormats.Jpeg)); + => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance)); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegConfigurationModule.cs b/src/ImageSharp/Formats/Jpeg/JpegConfigurationModule.cs index c3bf801ac8..9840a2ae88 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegConfigurationModule.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegConfigurationModule.cs @@ -9,12 +9,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public sealed class JpegConfigurationModule : IConfigurationModule { /// - public void Configure(Configuration config) + public void Configure(Configuration configuration) { - config.ImageFormatsManager.SetEncoder(ImageFormats.Jpeg, new JpegEncoder()); - config.ImageFormatsManager.SetDecoder(ImageFormats.Jpeg, new JpegDecoder()); - - config.ImageFormatsManager.AddImageFormatDetector(new JpegImageFormatDetector()); + configuration.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder()); + configuration.ImageFormatsManager.SetDecoder(JpegFormat.Instance, new JpegDecoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new JpegImageFormatDetector()); } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/JpegFormat.cs b/src/ImageSharp/Formats/Jpeg/JpegFormat.cs index 9a18f14d30..21757dcb3d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegFormat.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegFormat.cs @@ -8,18 +8,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Registers the image encoders, decoders and mime type detectors for the jpeg format. /// - internal sealed class JpegFormat : IImageFormat + internal sealed class JpegFormat : ImageFormatBase { + private JpegFormat() + { + } + /// - public string Name => "JPEG"; + public override string Name => "JPEG"; /// - public string DefaultMimeType => "image/jpeg"; + public override string DefaultMimeType => "image/jpeg"; /// - public IEnumerable MimeTypes => JpegConstants.MimeTypes; + public override IEnumerable MimeTypes => JpegConstants.MimeTypes; /// - public IEnumerable FileExtensions => JpegConstants.FileExtensions; + public override IEnumerable FileExtensions => JpegConstants.FileExtensions; } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/JpegImageFormatDetector.cs b/src/ImageSharp/Formats/Jpeg/JpegImageFormatDetector.cs index e25957efcf..7594f44770 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegImageFormatDetector.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public IImageFormat DetectFormat(ReadOnlySpan header) { - return this.IsSupportedFileFormat(header) ? ImageFormats.Jpeg : null; + return this.IsSupportedFileFormat(header) ? JpegFormat.Instance : null; } private bool IsSupportedFileFormat(ReadOnlySpan header) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs b/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs new file mode 100644 index 0000000000..eec0efa809 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData; + +namespace SixLabors.ImageSharp.Formats.Jpeg +{ + /// + /// Provides Jpeg specific metadata information for the image. + /// + public class JpegMetaData : IImageFormatMetaData + { + // TODO: Analyse what properties we would like to preserve. + } +} diff --git a/src/ImageSharp/Formats/Png/ImageExtensions.cs b/src/ImageSharp/Formats/Png/ImageExtensions.cs index a65845e02d..c73ec6f57e 100644 --- a/src/ImageSharp/Formats/Png/ImageExtensions.cs +++ b/src/ImageSharp/Formats/Png/ImageExtensions.cs @@ -35,6 +35,6 @@ namespace SixLabors.ImageSharp /// Thrown if the stream is null. public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) where TPixel : struct, IPixel - => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ImageFormats.Png)); + => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance)); } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngConfigurationModule.cs b/src/ImageSharp/Formats/Png/PngConfigurationModule.cs index 64dad23bca..3c9fddbad4 100644 --- a/src/ImageSharp/Formats/Png/PngConfigurationModule.cs +++ b/src/ImageSharp/Formats/Png/PngConfigurationModule.cs @@ -11,8 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Png /// public void Configure(Configuration configuration) { - configuration.ImageFormatsManager.SetEncoder(ImageFormats.Png, new PngEncoder()); - configuration.ImageFormatsManager.SetDecoder(ImageFormats.Png, new PngDecoder()); + configuration.ImageFormatsManager.SetEncoder(PngFormat.Instance, new PngEncoder()); + configuration.ImageFormatsManager.SetDecoder(PngFormat.Instance, new PngDecoder()); configuration.ImageFormatsManager.AddImageFormatDetector(new PngImageFormatDetector()); } } diff --git a/src/ImageSharp/Formats/Png/PngFormat.cs b/src/ImageSharp/Formats/Png/PngFormat.cs index 142f660712..00ebbde527 100644 --- a/src/ImageSharp/Formats/Png/PngFormat.cs +++ b/src/ImageSharp/Formats/Png/PngFormat.cs @@ -8,18 +8,22 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Registers the image encoders, decoders and mime type detectors for the png format. /// - internal sealed class PngFormat : IImageFormat + internal sealed class PngFormat : ImageFormatBase { + private PngFormat() + { + } + /// - public string Name => "PNG"; + public override string Name => "PNG"; /// - public string DefaultMimeType => "image/png"; + public override string DefaultMimeType => "image/png"; /// - public IEnumerable MimeTypes => PngConstants.MimeTypes; + public override IEnumerable MimeTypes => PngConstants.MimeTypes; /// - public IEnumerable FileExtensions => PngConstants.FileExtensions; + public override IEnumerable FileExtensions => PngConstants.FileExtensions; } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngImageFormatDetector.cs b/src/ImageSharp/Formats/Png/PngImageFormatDetector.cs index c1c039a1be..5deed86e30 100644 --- a/src/ImageSharp/Formats/Png/PngImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Png/PngImageFormatDetector.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// public IImageFormat DetectFormat(ReadOnlySpan header) { - return this.IsSupportedFileFormat(header) ? ImageFormats.Png : null; + return this.IsSupportedFileFormat(header) ? PngFormat.Instance : null; } private bool IsSupportedFileFormat(ReadOnlySpan header) diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs new file mode 100644 index 0000000000..1d76cf580a --- /dev/null +++ b/src/ImageSharp/Formats/Png/PngMetaData.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.MetaData; + +namespace SixLabors.ImageSharp.Formats.Png +{ + /// + /// Provides Png specific metadata information for the image. + /// + public class PngMetaData : IImageFormatMetaData + { + // TODO: Analyse what properties we would like to preserve. + } +} diff --git a/src/ImageSharp/ImageFormats.cs b/src/ImageSharp/ImageFormats.cs deleted file mode 100644 index bc437e5a5e..0000000000 --- a/src/ImageSharp/ImageFormats.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Png; - -namespace SixLabors.ImageSharp -{ - /// - /// The static collection of all the default image formats - /// - public static class ImageFormats - { - /// - /// The format details for the jpegs. - /// - public static readonly IImageFormat Jpeg = new JpegFormat(); - - /// - /// The format details for the pngs. - /// - public static readonly IImageFormat Png = new PngFormat(); - - /// - /// The format details for the gifs. - /// - public static readonly IImageFormat Gif = new GifFormat(); - - /// - /// The format details for the bitmaps. - /// - public static readonly IImageFormat Bmp = new BmpFormat(); - } -} \ No newline at end of file diff --git a/src/ImageSharp/MetaData/IImageFormatFrameMetaData.cs b/src/ImageSharp/MetaData/IImageFormatFrameMetaData.cs new file mode 100644 index 0000000000..5df6fa433d --- /dev/null +++ b/src/ImageSharp/MetaData/IImageFormatFrameMetaData.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.MetaData +{ + /// + /// Encapsulates the format specific metadata of an image frame. + /// This interface exists to allow type saftey and avoid the performance overhead of parsing attributes. + /// + public interface IImageFormatFrameMetaData + { + } +} \ No newline at end of file diff --git a/src/ImageSharp/MetaData/IImageFormatMetaData.cs b/src/ImageSharp/MetaData/IImageFormatMetaData.cs new file mode 100644 index 0000000000..ded8df8013 --- /dev/null +++ b/src/ImageSharp/MetaData/IImageFormatMetaData.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.MetaData +{ + /// + /// Encapsulates the format specific metadata of an image. + /// This interface exists to allow type saftey and avoid the performance overhead of parsing attributes. + /// + public interface IImageFormatMetaData + { + } +} \ No newline at end of file diff --git a/src/ImageSharp/MetaData/ImageFrameMetaData.cs b/src/ImageSharp/MetaData/ImageFrameMetaData.cs index 55678789e5..31bad29b81 100644 --- a/src/ImageSharp/MetaData/ImageFrameMetaData.cs +++ b/src/ImageSharp/MetaData/ImageFrameMetaData.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.MetaData { @@ -11,7 +12,7 @@ namespace SixLabors.ImageSharp.MetaData /// public sealed class ImageFrameMetaData { - private readonly Dictionary metaData = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary metaData = new Dictionary(); /// /// Initializes a new instance of the class. @@ -31,7 +32,7 @@ namespace SixLabors.ImageSharp.MetaData { DebugGuard.NotNull(other, nameof(other)); - foreach (KeyValuePair meta in other.metaData) + foreach (KeyValuePair meta in other.metaData) { this.metaData.Add(meta.Key, meta.Value); } @@ -51,7 +52,7 @@ namespace SixLabors.ImageSharp.MetaData /// key is null. /// value is null. /// An element with the same key already exists in the . - public void AddOrUpdateMetaData(string key, object value) + public void AddOrUpdateFormatMetaData(IImageFormat key, IImageFormatFrameMetaData value) { // Don't think this needs to be threadsafe. Guard.NotNull(value, nameof(value)); @@ -61,27 +62,22 @@ namespace SixLabors.ImageSharp.MetaData /// /// Gets the metadata value associated with the specified key. /// - /// The type of value. + /// The type of metadata. /// The key of the value to get. - /// - /// When this method returns, contains the metadata value associated with the specified key, - /// if the key is found; otherwise, the default value for the type of the value parameter. - /// This parameter is passed uninitialized. - /// /// - /// true if the contains an element with - /// the specified key; otherwise, false. + /// The . /// - public bool TryGetMetaData(string key, out T value) + public T GetOrAddFormatMetaData(IImageFormat key) + where T : IImageFormatFrameMetaData, new() { - if (this.metaData.TryGetValue(key, out object meta)) + if (this.metaData.TryGetValue(key, out IImageFormatFrameMetaData meta)) { - value = (T)meta; - return true; + return (T)meta; } - value = default; - return false; + var newMeta = new T(); + this.AddOrUpdateFormatMetaData(key, newMeta); + return newMeta; } } } \ No newline at end of file diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index 8233798c22..d907e8b8a5 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.MetaData.Profiles.Icc; @@ -25,7 +26,7 @@ namespace SixLabors.ImageSharp.MetaData /// public const double DefaultVerticalResolution = 96; - private readonly Dictionary metaData = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary metaData = new Dictionary(); private double horizontalResolution; private double verticalResolution; @@ -51,7 +52,7 @@ namespace SixLabors.ImageSharp.MetaData this.VerticalResolution = other.VerticalResolution; this.ResolutionUnits = other.ResolutionUnits; - foreach (KeyValuePair meta in other.metaData) + foreach (KeyValuePair meta in other.metaData) { this.metaData.Add(meta.Key, meta.Value); } @@ -138,7 +139,7 @@ namespace SixLabors.ImageSharp.MetaData /// key is null. /// value is null. /// An element with the same key already exists in the . - public void AddOrUpdateMetaData(string key, object value) + public void AddOrUpdateFormatMetaData(IImageFormat key, IImageFormatMetaData value) { // Don't think this needs to be threadsafe. Guard.NotNull(value, nameof(value)); @@ -148,27 +149,22 @@ namespace SixLabors.ImageSharp.MetaData /// /// Gets the metadata value associated with the specified key. /// - /// The type of value. + /// The type of metadata. /// The key of the value to get. - /// - /// When this method returns, contains the metadata value associated with the specified key, - /// if the key is found; otherwise, the default value for the type of the value parameter. - /// This parameter is passed uninitialized. - /// /// - /// true if the contains an element with - /// the specified key; otherwise, false. + /// The . /// - public bool TryGetMetaData(string key, out T value) + public T GetOrAddFormatMetaData(IImageFormat key) + where T : IImageFormatMetaData, new() { - if (this.metaData.TryGetValue(key, out object meta)) + if (this.metaData.TryGetValue(key, out IImageFormatMetaData meta)) { - value = (T)meta; - return true; + return (T)meta; } - value = default; - return false; + var newMeta = new T(); + this.AddOrUpdateFormatMetaData(key, newMeta); + return newMeta; } /// diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index 4bec25f7a2..6a8479b2b2 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using Moq; +using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.IO; using Xunit; // ReSharper disable InconsistentNaming @@ -37,19 +38,13 @@ namespace SixLabors.ImageSharp.Tests /// Test that the default configuration is not null. /// [Fact] - public void TestDefaultConfigurationIsNotNull() - { - Assert.True(Configuration.Default != null); - } + public void TestDefaultConfigurationIsNotNull() => Assert.True(Configuration.Default != null); /// /// Test that the default configuration read origin options is set to begin. /// [Fact] - public void TestDefaultConfigurationReadOriginIsCurrent() - { - Assert.True(Configuration.Default.ReadOrigin == ReadOrigin.Current); - } + public void TestDefaultConfigurationReadOriginIsCurrent() => Assert.True(Configuration.Default.ReadOrigin == ReadOrigin.Current); /// /// Test that the default configuration parallel options max degrees of parallelism matches the @@ -101,7 +96,7 @@ namespace SixLabors.ImageSharp.Tests Assert.Equal(count, config.ImageFormats.Count()); - config.ImageFormatsManager.AddImageFormat(ImageFormats.Bmp); + config.ImageFormatsManager.AddImageFormat(BmpFormat.Instance); Assert.Equal(count, config.ImageFormats.Count()); } diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index 23b806767c..f8b035ca83 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Tests using (Image image = file.CreateImage()) { string filename = path + "/" + file.FileNameWithoutExtension + ".txt"; - File.WriteAllText(filename, image.ToBase64String(ImageFormats.Png)); + File.WriteAllText(filename, image.ToBase64String(PngFormat.Instance)); } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 2b08bf20de..4ddcded5ea 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -189,8 +189,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif inStream.Position = 0; var image = Image.Load(inStream); - GifMetaData metaData = image.MetaData.GetGifMetaData(); - GifFrameMetaData frameMetaData = image.Frames.RootFrame.MetaData.GetGifFrameMetaData(); + GifMetaData metaData = image.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData frameMetaData = image.Frames.RootFrame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); GifColorTableMode colorMode = metaData.ColorTableMode; var encoder = new GifEncoder() { @@ -204,7 +204,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif outStream.Position = 0; var clone = Image.Load(outStream); - GifMetaData cloneMetaData = clone.MetaData.GetGifMetaData(); + GifMetaData cloneMetaData = clone.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); Assert.Equal(metaData.ColorTableMode, cloneMetaData.ColorTableMode); // Gifiddle and Cyotek GifInfo say this image has 64 colors. @@ -212,8 +212,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif for (int i = 0; i < image.Frames.Count; i++) { - GifFrameMetaData ifm = image.Frames[i].MetaData.GetGifFrameMetaData(); - GifFrameMetaData cifm = clone.Frames[i].MetaData.GetGifFrameMetaData(); + GifFrameMetaData ifm = image.Frames[i].MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData cifm = clone.Frames[i].MetaData.GetOrAddFormatMetaData(GifFormat.Instance); Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength); Assert.Equal(ifm.FrameDelay, cifm.FrameDelay); diff --git a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs index f10a4ce842..464b1564b8 100644 --- a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs +++ b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs @@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Tests }); Assert.Throws(() => { - this.DefaultFormatsManager.SetEncoder(ImageFormats.Bmp, null); + this.DefaultFormatsManager.SetEncoder(BmpFormat.Instance, null); }); Assert.Throws(() => { @@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Tests }); Assert.Throws(() => { - this.DefaultFormatsManager.SetDecoder(ImageFormats.Bmp, null); + this.DefaultFormatsManager.SetDecoder(BmpFormat.Instance, null); }); Assert.Throws(() => { diff --git a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs index c250d973b5..00d03dce4c 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs @@ -27,10 +27,10 @@ namespace SixLabors.ImageSharp.Tests }; var metaData = new ImageFrameMetaData(); - metaData.AddOrUpdateGifFrameMetaData(gifFrameMetaData); + metaData.AddOrUpdateFormatMetaData(GifFormat.Instance, gifFrameMetaData); var clone = new ImageFrameMetaData(metaData); - GifFrameMetaData cloneGifFrameMetaData = clone.GetGifFrameMetaData(); + GifFrameMetaData cloneGifFrameMetaData = clone.GetOrAddFormatMetaData(GifFormat.Instance); Assert.Equal(frameDelay, cloneGifFrameMetaData.FrameDelay); Assert.Equal(colorTableLength, cloneGifFrameMetaData.ColorTableLength); diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index 90c999f7cd..334b6552ac 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -62,13 +62,13 @@ namespace SixLabors.ImageSharp.Tests IImageEncoder bmpEncoder = IsWindows ? (IImageEncoder)SystemDrawingReferenceEncoder.Bmp : new BmpEncoder(); cfg.ConfigureCodecs( - ImageFormats.Png, + PngFormat.Instance, MagickReferenceDecoder.Instance, pngEncoder, new PngImageFormatDetector()); cfg.ConfigureCodecs( - ImageFormats.Bmp, + BmpFormat.Instance, SystemDrawingReferenceDecoder.Instance, bmpEncoder, new BmpImageFormatDetector()); From 9f5fd5605bb09094011952bb0f964f4c7759990c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 9 Sep 2018 20:42:53 +0100 Subject: [PATCH 05/14] Much better type safety. --- src/ImageSharp/Formats/Bmp/BmpFormat.cs | 18 ++++++--- src/ImageSharp/Formats/Bmp/BmpMetaData.cs | 4 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 4 +- src/ImageSharp/Formats/Gif/GifFormat.cs | 21 +++++++--- .../Formats/Gif/GifFrameMetaData.cs | 4 +- src/ImageSharp/Formats/Gif/GifMetaData.cs | 4 +- src/ImageSharp/Formats/IImageFormat.cs | 32 +++++++++++++++- src/ImageSharp/Formats/ImageFormatBase{T}.cs | 38 ------------------- src/ImageSharp/Formats/Jpeg/JpegFormat.cs | 18 ++++++--- src/ImageSharp/Formats/Jpeg/JpegMetaData.cs | 4 +- src/ImageSharp/Formats/Png/PngFormat.cs | 18 ++++++--- src/ImageSharp/Formats/Png/PngMetaData.cs | 4 +- .../MetaData/IImageFormatFrameMetaData.cs | 13 ------- .../MetaData/IImageFormatMetaData.cs | 13 ------- src/ImageSharp/MetaData/ImageFrameMetaData.cs | 26 ++++++++----- src/ImageSharp/MetaData/ImageMetaData.cs | 22 ++++++----- .../Formats/Gif/GifEncoderTests.cs | 8 ++-- .../MetaData/ImageFrameMetaDataTests.cs | 2 +- 18 files changed, 126 insertions(+), 127 deletions(-) delete mode 100644 src/ImageSharp/Formats/ImageFormatBase{T}.cs delete mode 100644 src/ImageSharp/MetaData/IImageFormatFrameMetaData.cs delete mode 100644 src/ImageSharp/MetaData/IImageFormatMetaData.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpFormat.cs b/src/ImageSharp/Formats/Bmp/BmpFormat.cs index de3f6f3f44..665f492daf 100644 --- a/src/ImageSharp/Formats/Bmp/BmpFormat.cs +++ b/src/ImageSharp/Formats/Bmp/BmpFormat.cs @@ -8,22 +8,30 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Registers the image encoders, decoders and mime type detectors for the bmp format. /// - internal sealed class BmpFormat : ImageFormatBase + internal sealed class BmpFormat : IImageFormat { private BmpFormat() { } + /// + /// Gets the current instance. + /// + public static BmpFormat Instance { get; } = new BmpFormat(); + + /// + public string Name => "BMP"; + /// - public override string Name => "BMP"; + public string DefaultMimeType => "image/bmp"; /// - public override string DefaultMimeType => "image/bmp"; + public IEnumerable MimeTypes => BmpConstants.MimeTypes; /// - public override IEnumerable MimeTypes => BmpConstants.MimeTypes; + public IEnumerable FileExtensions => BmpConstants.FileExtensions; /// - public override IEnumerable FileExtensions => BmpConstants.FileExtensions; + public BmpMetaData CreateDefaultFormatMetaData() => new BmpMetaData(); } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs index 1e350200a4..aa60f38662 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs @@ -1,14 +1,12 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.MetaData; - namespace SixLabors.ImageSharp.Formats.Bmp { /// /// Provides Bmp specific metadata information for the image. /// - public class BmpMetaData : IImageFormatMetaData + public class BmpMetaData { // TODO: Analyse what properties we would like to preserve. } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 20f2f1d336..7a880b0f96 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -143,7 +143,7 @@ namespace SixLabors.ImageSharp.Formats.Gif for (int i = 0; i < image.Frames.Count; i++) { ImageFrame frame = image.Frames[i]; - GifFrameMetaData frameMetaData = frame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData frameMetaData = frame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); this.WriteGraphicalControlExtension(frameMetaData, transparencyIndex, stream); this.WriteImageDescriptor(frame, false, stream); @@ -169,7 +169,7 @@ namespace SixLabors.ImageSharp.Formats.Gif GifFrameMetaData previousMeta = null; foreach (ImageFrame frame in image.Frames) { - GifFrameMetaData meta = frame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData meta = frame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); if (quantized is null) { // Allow each frame to be encoded at whatever color depth the frame designates if set. diff --git a/src/ImageSharp/Formats/Gif/GifFormat.cs b/src/ImageSharp/Formats/Gif/GifFormat.cs index 2d14abb444..f91269ceda 100644 --- a/src/ImageSharp/Formats/Gif/GifFormat.cs +++ b/src/ImageSharp/Formats/Gif/GifFormat.cs @@ -8,22 +8,33 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Registers the image encoders, decoders and mime type detectors for the gif format. /// - internal sealed class GifFormat : ImageFormatBase + internal sealed class GifFormat : IImageFormat { private GifFormat() { } + /// + /// Gets the current instance. + /// + public static GifFormat Instance { get; } = new GifFormat(); + + /// + public string Name => "GIF"; + + /// + public string DefaultMimeType => "image/gif"; + /// - public override string Name => "GIF"; + public IEnumerable MimeTypes => GifConstants.MimeTypes; /// - public override string DefaultMimeType => "image/gif"; + public IEnumerable FileExtensions => GifConstants.FileExtensions; /// - public override IEnumerable MimeTypes => GifConstants.MimeTypes; + public GifMetaData CreateDefaultFormatMetaData() => new GifMetaData(); /// - public override IEnumerable FileExtensions => GifConstants.FileExtensions; + public GifFrameMetaData CreateDefaultFormatFrameMetaData() => new GifFrameMetaData(); } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs b/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs index a7b68e6e85..cc04d48314 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs @@ -1,14 +1,12 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.MetaData; - namespace SixLabors.ImageSharp.Formats.Gif { /// /// Provides Gif specific metadata information for the image frame. /// - public class GifFrameMetaData : IImageFormatFrameMetaData + public class GifFrameMetaData { /// /// Gets or sets the length of the color table for paletted images. diff --git a/src/ImageSharp/Formats/Gif/GifMetaData.cs b/src/ImageSharp/Formats/Gif/GifMetaData.cs index cc0716ec57..f58f5dff3e 100644 --- a/src/ImageSharp/Formats/Gif/GifMetaData.cs +++ b/src/ImageSharp/Formats/Gif/GifMetaData.cs @@ -1,14 +1,12 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.MetaData; - namespace SixLabors.ImageSharp.Formats.Gif { /// /// Provides Gif specific metadata information for the image. /// - public class GifMetaData : IImageFormatMetaData + public class GifMetaData { /// /// Gets or sets the number of times any animation is repeated. diff --git a/src/ImageSharp/Formats/IImageFormat.cs b/src/ImageSharp/Formats/IImageFormat.cs index 15bdc73a84..9720352750 100644 --- a/src/ImageSharp/Formats/IImageFormat.cs +++ b/src/ImageSharp/Formats/IImageFormat.cs @@ -6,7 +6,37 @@ using System.Collections.Generic; namespace SixLabors.ImageSharp.Formats { /// - /// Describes an image format. + /// Defines the contract for an image format containing metadata with multiple frames. + /// + /// The type of format metadata. + /// The type of format frame metadata. + public interface IImageFormat : IImageFormat + where TFormatMetaData : class + where TFormatFrameMetaData : class + { + /// + /// Creates a default instance of the format frame metadata. + /// + /// The . + TFormatFrameMetaData CreateDefaultFormatFrameMetaData(); + } + + /// + /// Defines the contract for an image format containing metadata. + /// + /// The type of format metadata. + public interface IImageFormat : IImageFormat + where TFormatMetaData : class + { + /// + /// Creates a default instance of the format metadata. + /// + /// The . + TFormatMetaData CreateDefaultFormatMetaData(); + } + + /// + /// Defines the contract for an image format. /// public interface IImageFormat { diff --git a/src/ImageSharp/Formats/ImageFormatBase{T}.cs b/src/ImageSharp/Formats/ImageFormatBase{T}.cs deleted file mode 100644 index 85273db2be..0000000000 --- a/src/ImageSharp/Formats/ImageFormatBase{T}.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Collections.Generic; - -namespace SixLabors.ImageSharp.Formats -{ - /// - /// The base class for all image formats. - /// Inheriting classes should implement the singleton pattern by creating a private constructor. - /// - /// The type of image format. - public abstract class ImageFormatBase : IImageFormat - where T : class, IImageFormat - { - private static readonly Lazy Lazy = new Lazy(CreateInstance); - - /// - /// Gets the current instance. - /// - public static T Instance => Lazy.Value; - - /// - public abstract string Name { get; } - - /// - public abstract string DefaultMimeType { get; } - - /// - public abstract IEnumerable MimeTypes { get; } - - /// - public abstract IEnumerable FileExtensions { get; } - - private static T CreateInstance() => (T)Activator.CreateInstance(typeof(T), true); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/JpegFormat.cs b/src/ImageSharp/Formats/Jpeg/JpegFormat.cs index 21757dcb3d..94c0895b9d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegFormat.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegFormat.cs @@ -8,22 +8,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Registers the image encoders, decoders and mime type detectors for the jpeg format. /// - internal sealed class JpegFormat : ImageFormatBase + internal sealed class JpegFormat : IImageFormat { private JpegFormat() { } + /// + /// Gets the current instance. + /// + public static JpegFormat Instance { get; } = new JpegFormat(); + + /// + public string Name => "JPEG"; + /// - public override string Name => "JPEG"; + public string DefaultMimeType => "image/jpeg"; /// - public override string DefaultMimeType => "image/jpeg"; + public IEnumerable MimeTypes => JpegConstants.MimeTypes; /// - public override IEnumerable MimeTypes => JpegConstants.MimeTypes; + public IEnumerable FileExtensions => JpegConstants.FileExtensions; /// - public override IEnumerable FileExtensions => JpegConstants.FileExtensions; + public JpegMetaData CreateDefaultFormatMetaData() => new JpegMetaData(); } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs b/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs index eec0efa809..b05f9fa15a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs @@ -1,14 +1,12 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.MetaData; - namespace SixLabors.ImageSharp.Formats.Jpeg { /// /// Provides Jpeg specific metadata information for the image. /// - public class JpegMetaData : IImageFormatMetaData + public class JpegMetaData { // TODO: Analyse what properties we would like to preserve. } diff --git a/src/ImageSharp/Formats/Png/PngFormat.cs b/src/ImageSharp/Formats/Png/PngFormat.cs index 00ebbde527..b5223cb5a8 100644 --- a/src/ImageSharp/Formats/Png/PngFormat.cs +++ b/src/ImageSharp/Formats/Png/PngFormat.cs @@ -8,22 +8,30 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Registers the image encoders, decoders and mime type detectors for the png format. /// - internal sealed class PngFormat : ImageFormatBase + internal sealed class PngFormat : IImageFormat { private PngFormat() { } + /// + /// Gets the current instance. + /// + public static PngFormat Instance { get; } = new PngFormat(); + + /// + public string Name => "PNG"; + /// - public override string Name => "PNG"; + public string DefaultMimeType => "image/png"; /// - public override string DefaultMimeType => "image/png"; + public IEnumerable MimeTypes => PngConstants.MimeTypes; /// - public override IEnumerable MimeTypes => PngConstants.MimeTypes; + public IEnumerable FileExtensions => PngConstants.FileExtensions; /// - public override IEnumerable FileExtensions => PngConstants.FileExtensions; + public PngMetaData CreateDefaultFormatMetaData() => new PngMetaData(); } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs index 1d76cf580a..90dbb83b6f 100644 --- a/src/ImageSharp/Formats/Png/PngMetaData.cs +++ b/src/ImageSharp/Formats/Png/PngMetaData.cs @@ -1,14 +1,12 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.MetaData; - namespace SixLabors.ImageSharp.Formats.Png { /// /// Provides Png specific metadata information for the image. /// - public class PngMetaData : IImageFormatMetaData + public class PngMetaData { // TODO: Analyse what properties we would like to preserve. } diff --git a/src/ImageSharp/MetaData/IImageFormatFrameMetaData.cs b/src/ImageSharp/MetaData/IImageFormatFrameMetaData.cs deleted file mode 100644 index 5df6fa433d..0000000000 --- a/src/ImageSharp/MetaData/IImageFormatFrameMetaData.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.MetaData -{ - /// - /// Encapsulates the format specific metadata of an image frame. - /// This interface exists to allow type saftey and avoid the performance overhead of parsing attributes. - /// - public interface IImageFormatFrameMetaData - { - } -} \ No newline at end of file diff --git a/src/ImageSharp/MetaData/IImageFormatMetaData.cs b/src/ImageSharp/MetaData/IImageFormatMetaData.cs deleted file mode 100644 index ded8df8013..0000000000 --- a/src/ImageSharp/MetaData/IImageFormatMetaData.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.MetaData -{ - /// - /// Encapsulates the format specific metadata of an image. - /// This interface exists to allow type saftey and avoid the performance overhead of parsing attributes. - /// - public interface IImageFormatMetaData - { - } -} \ No newline at end of file diff --git a/src/ImageSharp/MetaData/ImageFrameMetaData.cs b/src/ImageSharp/MetaData/ImageFrameMetaData.cs index 31bad29b81..07d4bdb05e 100644 --- a/src/ImageSharp/MetaData/ImageFrameMetaData.cs +++ b/src/ImageSharp/MetaData/ImageFrameMetaData.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.MetaData /// public sealed class ImageFrameMetaData { - private readonly Dictionary metaData = new Dictionary(); + private readonly Dictionary metaData = new Dictionary(); /// /// Initializes a new instance of the class. @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.MetaData { DebugGuard.NotNull(other, nameof(other)); - foreach (KeyValuePair meta in other.metaData) + foreach (KeyValuePair meta in other.metaData) { this.metaData.Add(meta.Key, meta.Value); } @@ -47,12 +47,16 @@ namespace SixLabors.ImageSharp.MetaData /// /// Adds or updates the specified key and value to the . /// + /// The type of format metadata. + /// The type of format frame metadata. /// The key of the metadata to add. /// The value of the element to add. /// key is null. /// value is null. /// An element with the same key already exists in the . - public void AddOrUpdateFormatMetaData(IImageFormat key, IImageFormatFrameMetaData value) + public void AddOrUpdateFormatMetaData(IImageFormat key, TFormatFrameMetaData value) + where TFormatMetaData : class + where TFormatFrameMetaData : class { // Don't think this needs to be threadsafe. Guard.NotNull(value, nameof(value)); @@ -62,20 +66,22 @@ namespace SixLabors.ImageSharp.MetaData /// /// Gets the metadata value associated with the specified key. /// - /// The type of metadata. + /// The type of format metadata. + /// The type of format frame metadata. /// The key of the value to get. /// - /// The . + /// The . /// - public T GetOrAddFormatMetaData(IImageFormat key) - where T : IImageFormatFrameMetaData, new() + public TFormatFrameMetaData GetOrAddFormatMetaData(IImageFormat key) + where TFormatMetaData : class + where TFormatFrameMetaData : class { - if (this.metaData.TryGetValue(key, out IImageFormatFrameMetaData meta)) + if (this.metaData.TryGetValue(key, out object meta)) { - return (T)meta; + return (TFormatFrameMetaData)meta; } - var newMeta = new T(); + TFormatFrameMetaData newMeta = key.CreateDefaultFormatFrameMetaData(); this.AddOrUpdateFormatMetaData(key, newMeta); return newMeta; } diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index d907e8b8a5..aa084728ca 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.MetaData /// public const double DefaultVerticalResolution = 96; - private readonly Dictionary metaData = new Dictionary(); + private readonly Dictionary metaData = new Dictionary(); private double horizontalResolution; private double verticalResolution; @@ -52,7 +52,7 @@ namespace SixLabors.ImageSharp.MetaData this.VerticalResolution = other.VerticalResolution; this.ResolutionUnits = other.ResolutionUnits; - foreach (KeyValuePair meta in other.metaData) + foreach (KeyValuePair meta in other.metaData) { this.metaData.Add(meta.Key, meta.Value); } @@ -134,12 +134,14 @@ namespace SixLabors.ImageSharp.MetaData /// /// Adds or updates the specified key and value to the . /// + /// The type of format metadata. /// The key of the metadata to add. /// The value of the element to add. /// key is null. /// value is null. /// An element with the same key already exists in the . - public void AddOrUpdateFormatMetaData(IImageFormat key, IImageFormatMetaData value) + public void AddOrUpdateFormatMetaData(IImageFormat key, TFormatMetaData value) + where TFormatMetaData : class { // Don't think this needs to be threadsafe. Guard.NotNull(value, nameof(value)); @@ -149,20 +151,20 @@ namespace SixLabors.ImageSharp.MetaData /// /// Gets the metadata value associated with the specified key. /// - /// The type of metadata. + /// The type of metadata. /// The key of the value to get. /// - /// The . + /// The . /// - public T GetOrAddFormatMetaData(IImageFormat key) - where T : IImageFormatMetaData, new() + public TFormatMetaData GetOrAddFormatMetaData(IImageFormat key) + where TFormatMetaData : class { - if (this.metaData.TryGetValue(key, out IImageFormatMetaData meta)) + if (this.metaData.TryGetValue(key, out object meta)) { - return (T)meta; + return (TFormatMetaData)meta; } - var newMeta = new T(); + TFormatMetaData newMeta = key.CreateDefaultFormatMetaData(); this.AddOrUpdateFormatMetaData(key, newMeta); return newMeta; } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 4ddcded5ea..0c32689f09 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -189,8 +189,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif inStream.Position = 0; var image = Image.Load(inStream); - GifMetaData metaData = image.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); - GifFrameMetaData frameMetaData = image.Frames.RootFrame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifMetaData metaData = image.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData frameMetaData = image.Frames.RootFrame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); GifColorTableMode colorMode = metaData.ColorTableMode; var encoder = new GifEncoder() { @@ -212,8 +212,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif for (int i = 0; i < image.Frames.Count; i++) { - GifFrameMetaData ifm = image.Frames[i].MetaData.GetOrAddFormatMetaData(GifFormat.Instance); - GifFrameMetaData cifm = clone.Frames[i].MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData ifm = image.Frames[i].MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData cifm = clone.Frames[i].MetaData.GetOrAddFormatMetaData(GifFormat.Instance); Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength); Assert.Equal(ifm.FrameDelay, cifm.FrameDelay); diff --git a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs index 00d03dce4c..54441f0cbf 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs @@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Tests metaData.AddOrUpdateFormatMetaData(GifFormat.Instance, gifFrameMetaData); var clone = new ImageFrameMetaData(metaData); - GifFrameMetaData cloneGifFrameMetaData = clone.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData cloneGifFrameMetaData = clone.GetOrAddFormatMetaData(GifFormat.Instance); Assert.Equal(frameDelay, cloneGifFrameMetaData.FrameDelay); Assert.Equal(colorTableLength, cloneGifFrameMetaData.ColorTableLength); From d46e275072699fde6f4f7260740b96e2c5a6e62c Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 11 Sep 2018 00:31:35 +0200 Subject: [PATCH 06/14] minor code cleanup --- src/ImageSharp/Formats/IImageFormat.cs | 58 +++++++++---------- src/ImageSharp/MetaData/ImageFrameMetaData.cs | 14 +++-- src/ImageSharp/MetaData/ImageMetaData.cs | 10 ++-- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/ImageSharp/Formats/IImageFormat.cs b/src/ImageSharp/Formats/IImageFormat.cs index 9720352750..94191c1493 100644 --- a/src/ImageSharp/Formats/IImageFormat.cs +++ b/src/ImageSharp/Formats/IImageFormat.cs @@ -6,26 +6,36 @@ using System.Collections.Generic; namespace SixLabors.ImageSharp.Formats { /// - /// Defines the contract for an image format containing metadata with multiple frames. + /// Defines the contract for an image format. /// - /// The type of format metadata. - /// The type of format frame metadata. - public interface IImageFormat : IImageFormat - where TFormatMetaData : class - where TFormatFrameMetaData : class + public interface IImageFormat { /// - /// Creates a default instance of the format frame metadata. + /// Gets the name that describes this image format. /// - /// The . - TFormatFrameMetaData CreateDefaultFormatFrameMetaData(); + string Name { get; } + + /// + /// Gets the default mimetype that the image foramt uses + /// + string DefaultMimeType { get; } + + /// + /// Gets all the mimetypes that have been used by this image foramt. + /// + IEnumerable MimeTypes { get; } + + /// + /// Gets the file extensions this image format commonly uses. + /// + IEnumerable FileExtensions { get; } } /// /// Defines the contract for an image format containing metadata. /// /// The type of format metadata. - public interface IImageFormat : IImageFormat + public interface IImageFormat : IImageFormat where TFormatMetaData : class { /// @@ -36,28 +46,18 @@ namespace SixLabors.ImageSharp.Formats } /// - /// Defines the contract for an image format. + /// Defines the contract for an image format containing metadata with multiple frames. /// - public interface IImageFormat + /// The type of format metadata. + /// The type of format frame metadata. + public interface IImageFormat : IImageFormat + where TFormatMetaData : class + where TFormatFrameMetaData : class { /// - /// Gets the name that describes this image format. - /// - string Name { get; } - - /// - /// Gets the default mimetype that the image foramt uses - /// - string DefaultMimeType { get; } - - /// - /// Gets all the mimetypes that have been used by this image foramt. - /// - IEnumerable MimeTypes { get; } - - /// - /// Gets the file extensions this image format commonly uses. + /// Creates a default instance of the format frame metadata. /// - IEnumerable FileExtensions { get; } + /// The . + TFormatFrameMetaData CreateDefaultFormatFrameMetaData(); } } \ No newline at end of file diff --git a/src/ImageSharp/MetaData/ImageFrameMetaData.cs b/src/ImageSharp/MetaData/ImageFrameMetaData.cs index 07d4bdb05e..908a0e6246 100644 --- a/src/ImageSharp/MetaData/ImageFrameMetaData.cs +++ b/src/ImageSharp/MetaData/ImageFrameMetaData.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.MetaData /// public sealed class ImageFrameMetaData { - private readonly Dictionary metaData = new Dictionary(); + private readonly Dictionary formatMetaData = new Dictionary(); /// /// Initializes a new instance of the class. @@ -32,9 +32,9 @@ namespace SixLabors.ImageSharp.MetaData { DebugGuard.NotNull(other, nameof(other)); - foreach (KeyValuePair meta in other.metaData) + foreach (KeyValuePair meta in other.formatMetaData) { - this.metaData.Add(meta.Key, meta.Value); + this.formatMetaData.Add(meta.Key, meta.Value); } } @@ -54,13 +54,15 @@ namespace SixLabors.ImageSharp.MetaData /// key is null. /// value is null. /// An element with the same key already exists in the . - public void AddOrUpdateFormatMetaData(IImageFormat key, TFormatFrameMetaData value) + public void AddOrUpdateFormatMetaData( + IImageFormat key, + TFormatFrameMetaData value) where TFormatMetaData : class where TFormatFrameMetaData : class { // Don't think this needs to be threadsafe. Guard.NotNull(value, nameof(value)); - this.metaData[key] = value; + this.formatMetaData[key] = value; } /// @@ -76,7 +78,7 @@ namespace SixLabors.ImageSharp.MetaData where TFormatMetaData : class where TFormatFrameMetaData : class { - if (this.metaData.TryGetValue(key, out object meta)) + if (this.formatMetaData.TryGetValue(key, out object meta)) { return (TFormatFrameMetaData)meta; } diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index aa084728ca..74c00cf8a5 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.MetaData /// public const double DefaultVerticalResolution = 96; - private readonly Dictionary metaData = new Dictionary(); + private readonly Dictionary formatMetaData = new Dictionary(); private double horizontalResolution; private double verticalResolution; @@ -52,9 +52,9 @@ namespace SixLabors.ImageSharp.MetaData this.VerticalResolution = other.VerticalResolution; this.ResolutionUnits = other.ResolutionUnits; - foreach (KeyValuePair meta in other.metaData) + foreach (KeyValuePair meta in other.formatMetaData) { - this.metaData.Add(meta.Key, meta.Value); + this.formatMetaData.Add(meta.Key, meta.Value); } foreach (ImageProperty property in other.Properties) @@ -145,7 +145,7 @@ namespace SixLabors.ImageSharp.MetaData { // Don't think this needs to be threadsafe. Guard.NotNull(value, nameof(value)); - this.metaData[key] = value; + this.formatMetaData[key] = value; } /// @@ -159,7 +159,7 @@ namespace SixLabors.ImageSharp.MetaData public TFormatMetaData GetOrAddFormatMetaData(IImageFormat key) where TFormatMetaData : class { - if (this.metaData.TryGetValue(key, out object meta)) + if (this.formatMetaData.TryGetValue(key, out object meta)) { return (TFormatMetaData)meta; } From 8ccea04cdb122e72be79d1453632ea1d37162d37 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Sep 2018 11:41:52 +0100 Subject: [PATCH 07/14] Use metadata as fallback forpng encoder options. --- .../Formats/Png/IPngEncoderOptions.cs | 18 ++---- src/ImageSharp/Formats/Png/PngChunkType.cs | 52 ++++++++-------- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 58 ++++++++++++++---- src/ImageSharp/Formats/Png/PngEncoder.cs | 14 ++--- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 61 ++++++++++--------- src/ImageSharp/Formats/Png/PngMetaData.cs | 18 +++++- 6 files changed, 129 insertions(+), 92 deletions(-) diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index f3231fa22a..77bc9f7a05 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -14,12 +14,12 @@ namespace SixLabors.ImageSharp.Formats.Png /// Gets the number of bits per sample or per palette index (not per pixel). /// Not all values are allowed for all values. /// - PngBitDepth BitDepth { get; } + PngBitDepth? BitDepth { get; } /// /// Gets the color type /// - PngColorType ColorType { get; } + PngColorType? ColorType { get; } /// /// Gets the filter method. @@ -33,15 +33,13 @@ namespace SixLabors.ImageSharp.Formats.Png int CompressionLevel { get; } /// - /// Gets the gamma value, that will be written - /// the the stream, when the property - /// is set to true. The default value is 2.2F. + /// Gets the gamma value, that will be written the the image. /// /// The gamma value of the image. - float Gamma { get; } + float? Gamma { get; } /// - /// Gets quantizer for reducing the color count. + /// Gets the quantizer for reducing the color count. /// IQuantizer Quantizer { get; } @@ -49,11 +47,5 @@ namespace SixLabors.ImageSharp.Formats.Png /// Gets the transparency threshold. /// byte Threshold { get; } - - /// - /// Gets a value indicating whether this instance should write - /// gamma information to the stream. The default value is false. - /// - bool WriteGamma { get; } } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs index e0844ca6b8..7654c17014 100644 --- a/src/ImageSharp/Formats/Png/PngChunkType.cs +++ b/src/ImageSharp/Formats/Png/PngChunkType.cs @@ -8,58 +8,58 @@ namespace SixLabors.ImageSharp.Formats.Png /// internal enum PngChunkType : uint { - /// - /// The first chunk in a png file. Can only exists once. Contains - /// common information like the width and the height of the image or - /// the used compression method. - /// - Header = 0x49484452U, // IHDR - - /// - /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte - /// series in the RGB format. - /// - Palette = 0x504C5445U, // PLTE - /// /// The IDAT chunk contains the actual image data. The image can contains more /// than one chunk of this type. All chunks together are the whole image. /// - Data = 0x49444154U, // IDAT + Data = 0x49444154U, /// /// This chunk must appear last. It marks the end of the PNG data stream. /// The chunk's data field is empty. /// - End = 0x49454E44U, // IEND + End = 0x49454E44U, /// - /// This chunk specifies that the image uses simple transparency: - /// either alpha values associated with palette entries (for indexed-color images) - /// or a single transparent color (for grayscale and true color images). + /// The first chunk in a png file. Can only exists once. Contains + /// common information like the width and the height of the image or + /// the used compression method. /// - PaletteAlpha = 0x74524E53U, // tRNS + Header = 0x49484452U, /// - /// Textual information that the encoder wishes to record with the image can be stored in - /// tEXt chunks. Each tEXt chunk contains a keyword and a text string. + /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte + /// series in the RGB format. + /// + Palette = 0x504C5445U, + + /// + /// The eXIf data chunk which contains the Exif profile. /// - Text = 0x74455874U, // tEXt + Exif = 0x65584966U, /// /// This chunk specifies the relationship between the image samples and the desired /// display output intensity. /// - Gamma = 0x67414D41U, // gAMA + Gamma = 0x67414D41U, /// /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. /// - Physical = 0x70485973U, // pHYs + Physical = 0x70485973U, /// - /// The data chunk which contains the Exif profile. + /// Textual information that the encoder wishes to record with the image can be stored in + /// tEXt chunks. Each tEXt chunk contains a keyword and a text string. + /// + Text = 0x74455874U, + + /// + /// This chunk specifies that the image uses simple transparency: + /// either alpha values associated with palette entries (for indexed-color images) + /// or a single transparent color (for grayscale and true color images). /// - Exif = 0x65584966U // eXIf + PaletteAlpha = 0x74524E53U } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index be1914174b..4bc4833011 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -216,7 +216,9 @@ namespace SixLabors.ImageSharp.Formats.Png public Image Decode(Stream stream) where TPixel : struct, IPixel { - var metadata = new ImageMetaData(); + var metaData = new ImageMetaData(); + var pngMetaData = new PngMetaData(); + metaData.AddOrUpdateFormatMetaData(PngFormat.Instance, pngMetaData); this.currentStream = stream; this.currentStream.Skip(8); Image image = null; @@ -231,16 +233,19 @@ namespace SixLabors.ImageSharp.Formats.Png switch (chunk.Type) { case PngChunkType.Header: - this.ReadHeaderChunk(chunk.Data.Array); + this.ReadHeaderChunk(pngMetaData, chunk.Data.Array); this.ValidateHeader(); break; case PngChunkType.Physical: - this.ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); + this.ReadPhysicalChunk(metaData, chunk.Data.GetSpan()); + break; + case PngChunkType.Gamma: + this.ReadGammaChunk(pngMetaData, chunk.Data.GetSpan()); break; case PngChunkType.Data: if (image is null) { - this.InitializeImage(metadata, out image); + this.InitializeImage(metaData, out image); } deframeStream.AllocateNewBytes(chunk.Length); @@ -259,14 +264,14 @@ namespace SixLabors.ImageSharp.Formats.Png this.AssignTransparentMarkers(alpha); break; case PngChunkType.Text: - this.ReadTextChunk(metadata, chunk.Data.Array, chunk.Length); + this.ReadTextChunk(metaData, chunk.Data.Array, chunk.Length); break; case PngChunkType.Exif: if (!this.ignoreMetadata) { byte[] exifData = new byte[chunk.Length]; Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length); - metadata.ExifProfile = new ExifProfile(exifData); + metaData.ExifProfile = new ExifProfile(exifData); } break; @@ -302,7 +307,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// The containing image data. public IImageInfo Identify(Stream stream) { - var metadata = new ImageMetaData(); + var metaData = new ImageMetaData(); + var pngMetaData = new PngMetaData(); + metaData.AddOrUpdateFormatMetaData(PngFormat.Instance, pngMetaData); this.currentStream = stream; this.currentStream.Skip(8); try @@ -314,17 +321,20 @@ namespace SixLabors.ImageSharp.Formats.Png switch (chunk.Type) { case PngChunkType.Header: - this.ReadHeaderChunk(chunk.Data.Array); + this.ReadHeaderChunk(pngMetaData, chunk.Data.Array); this.ValidateHeader(); break; case PngChunkType.Physical: - this.ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); + this.ReadPhysicalChunk(metaData, chunk.Data.GetSpan()); + break; + case PngChunkType.Gamma: + this.ReadGammaChunk(pngMetaData, chunk.Data.GetSpan()); break; case PngChunkType.Data: this.SkipChunkDataAndCrc(chunk); break; case PngChunkType.Text: - this.ReadTextChunk(metadata, chunk.Data.Array, chunk.Length); + this.ReadTextChunk(metaData, chunk.Data.Array, chunk.Length); break; case PngChunkType.End: this.isEndChunkReached = true; @@ -348,7 +358,7 @@ namespace SixLabors.ImageSharp.Formats.Png throw new ImageFormatException("PNG Image does not contain a header chunk"); } - return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata); + return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metaData); } /// @@ -427,6 +437,18 @@ namespace SixLabors.ImageSharp.Formats.Png metadata.VerticalResolution = vResolution; } + /// + /// Reads the data chunk containing gamma data. + /// + /// The metadata to read to. + /// The data containing physical data. + private void ReadGammaChunk(PngMetaData pngMetadata, ReadOnlySpan data) + { + // The value is encoded as a 4-byte unsigned integer, representing gamma times 100000. + // For example, a gamma of 1/2.2 would be stored as 45455. + pngMetadata.Gamma = BinaryPrimitives.ReadUInt32BigEndian(data) / 100_000F; + } + /// /// Initializes the image and various buffers needed for processing /// @@ -1267,17 +1289,27 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Reads a header chunk from the data. /// + /// The png metadata. /// The containing data. - private void ReadHeaderChunk(ReadOnlySpan data) + private void ReadHeaderChunk(PngMetaData pngMetaData, ReadOnlySpan data) { + byte bitDepth = data[8]; this.header = new PngHeader( width: BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)), height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)), - bitDepth: data[8], + bitDepth: bitDepth, colorType: (PngColorType)data[9], compressionMethod: data[10], filterMethod: data[11], interlaceMethod: (PngInterlaceMode)data[12]); + + // TODO: Figure out how we can determine the number of colors and support more bit depths. + if (bitDepth == 8 || bitDepth == 16) + { + pngMetaData.BitDepth = (PngBitDepth)bitDepth; + } + + pngMetaData.ColorType = this.header.ColorType; } /// diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 109e6ad770..435d0abbc9 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -4,6 +4,7 @@ using System.IO; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Png @@ -17,12 +18,12 @@ namespace SixLabors.ImageSharp.Formats.Png /// Gets or sets the number of bits per sample or per palette index (not per pixel). /// Not all values are allowed for all values. /// - public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8; + public PngBitDepth? BitDepth { get; set; } /// /// Gets or sets the color type. /// - public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha; + public PngColorType? ColorType { get; set; } /// /// Gets or sets the filter method. @@ -36,18 +37,15 @@ namespace SixLabors.ImageSharp.Formats.Png public int CompressionLevel { get; set; } = 6; /// - /// Gets or sets the gamma value, that will be written - /// the the stream, when the property - /// is set to true. The default value is 2.2F. + /// Gets or sets the gamma value, that will be written the the image. /// - /// The gamma value of the image. - public float Gamma { get; set; } = 2.2F; + public float? Gamma { get; set; } /// /// Gets or sets quantizer for reducing the color count. /// Defaults to the /// - public IQuantizer Quantizer { get; set; } = new WuQuantizer(); + public IQuantizer Quantizer { get; set; } = KnownQuantizers.Wu; /// /// Gets or sets the transparency threshold. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index ffe29aecae..9d9de71b14 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -4,7 +4,6 @@ using System; using System.Buffers.Binary; using System.IO; -using System.Linq; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Png.Filters; @@ -45,49 +44,49 @@ namespace SixLabors.ImageSharp.Formats.Png private readonly Crc32 crc = new Crc32(); /// - /// The png bit depth + /// The png filter method. /// - private readonly PngBitDepth pngBitDepth; + private readonly PngFilterMethod pngFilterMethod; /// - /// Gets or sets a value indicating whether to use 16 bit encoding for supported color types. + /// The quantizer for reducing the color count. /// - private readonly bool use16Bit; + private readonly IQuantizer quantizer; /// - /// The png color type. + /// Gets or sets the CompressionLevel value /// - private readonly PngColorType pngColorType; + private readonly int compressionLevel; /// - /// The png filter method. + /// Gets or sets the alpha threshold value /// - private readonly PngFilterMethod pngFilterMethod; + private readonly byte threshold; /// - /// The quantizer for reducing the color count. + /// Gets or sets a value indicating whether to write the gamma chunk /// - private readonly IQuantizer quantizer; + private bool writeGamma; /// - /// Gets or sets the CompressionLevel value + /// The png bit depth /// - private readonly int compressionLevel; + private PngBitDepth? pngBitDepth; /// - /// Gets or sets the Gamma value + /// Gets or sets a value indicating whether to use 16 bit encoding for supported color types. /// - private readonly float gamma; + private bool use16Bit; /// - /// Gets or sets the Threshold value + /// The png color type. /// - private readonly byte threshold; + private PngColorType? pngColorType; /// - /// Gets or sets a value indicating whether to Write Gamma + /// Gets or sets the Gamma value /// - private readonly bool writeGamma; + private float? gamma; /// /// The image width. @@ -158,14 +157,12 @@ namespace SixLabors.ImageSharp.Formats.Png { this.memoryAllocator = memoryAllocator; this.pngBitDepth = options.BitDepth; - this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16); this.pngColorType = options.ColorType; this.pngFilterMethod = options.FilterMethod; this.compressionLevel = options.CompressionLevel; this.gamma = options.Gamma; this.quantizer = options.Quantizer; this.threshold = options.Threshold; - this.writeGamma = options.WriteGamma; } /// @@ -183,6 +180,16 @@ namespace SixLabors.ImageSharp.Formats.Png this.width = image.Width; this.height = image.Height; + // Always take the encoder options over the metadata values. + PngMetaData pngMetaData = image.MetaData.GetOrAddFormatMetaData(PngFormat.Instance); + this.gamma = this.gamma ?? pngMetaData.Gamma; + this.writeGamma = this.gamma > 0; + this.pngColorType = this.pngColorType ?? pngMetaData.ColorType; + + // TODO: We don't take full advantage of this information yet. + this.pngBitDepth = this.pngBitDepth ?? pngMetaData.BitDepth; + this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16); + stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); QuantizedFrame quantized = null; @@ -217,7 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Png width: image.Width, height: image.Height, bitDepth: this.bitDepth, - colorType: this.pngColorType, + colorType: this.pngColorType.Value, compressionMethod: 0, // None filterMethod: 0, interlaceMethod: 0); // TODO: Can't write interlaced yet. @@ -781,10 +788,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// Writes the chunk end to the stream. /// /// The containing image data. - private void WriteEndChunk(Stream stream) - { - this.WriteChunk(stream, PngChunkType.End, null); - } + private void WriteEndChunk(Stream stream) => this.WriteChunk(stream, PngChunkType.End, null); /// /// Writes a chunk to the stream. @@ -792,10 +796,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The to write to. /// The type of chunk to write. /// The containing data. - private void WriteChunk(Stream stream, PngChunkType type, byte[] data) - { - this.WriteChunk(stream, type, data, 0, data?.Length ?? 0); - } + private void WriteChunk(Stream stream, PngChunkType type, byte[] data) => this.WriteChunk(stream, type, data, 0, data?.Length ?? 0); /// /// Writes a chunk of a specified length to the stream at the given offset. diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs index 90dbb83b6f..1eb3cdad6a 100644 --- a/src/ImageSharp/Formats/Png/PngMetaData.cs +++ b/src/ImageSharp/Formats/Png/PngMetaData.cs @@ -8,6 +8,20 @@ namespace SixLabors.ImageSharp.Formats.Png /// public class PngMetaData { - // TODO: Analyse what properties we would like to preserve. + /// + /// Gets or sets the number of bits per sample or per palette index (not per pixel). + /// Not all values are allowed for all values. + /// + public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8; + + /// + /// Gets or sets the color type. + /// + public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha; + + /// + /// Gets or sets the gamma value for the image. + /// + public float Gamma { get; set; } } -} +} \ No newline at end of file From 551c03f16a33b7a1a3f9e6446e9119519fe6446c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Sep 2018 15:58:42 +0100 Subject: [PATCH 08/14] Preserve BmpBitsPerPixel --- src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs | 6 +-- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 17 +++++-- src/ImageSharp/Formats/Bmp/BmpEncoder.cs | 2 +- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 17 ++++--- src/ImageSharp/Formats/Bmp/BmpMetaData.cs | 7 ++- .../Formats/Bmp/IBmpEncoderOptions.cs | 2 +- .../Formats/Bmp/BmpEncoderTests.cs | 45 ++++++++++++++----- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Bmp/rgb32.bmp | 3 ++ 9 files changed, 71 insertions(+), 29 deletions(-) create mode 100644 tests/Images/Input/Bmp/rgb32.bmp diff --git a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs index 0029a6b68d..618999c87d 100644 --- a/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs +++ b/src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs @@ -6,16 +6,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Enumerates the available bits per pixel for bitmap. /// - public enum BmpBitsPerPixel + public enum BmpBitsPerPixel : short { /// /// 24 bits per pixel. Each pixel consists of 3 bytes. /// - Pixel24 = 3, + Pixel24 = 24, /// /// 32 bits per pixel. Each pixel consists of 4 bytes. /// - Pixel32 = 4 + Pixel32 = 32 } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 385c79896e..3bb44f1d06 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -536,6 +536,17 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.metaData = meta; + short bitsPerPixel = this.infoHeader.BitsPerPixel; + var bmpMetaData = new BmpMetaData(); + this.metaData.AddOrUpdateFormatMetaData(BmpFormat.Instance, bmpMetaData); + + // We can only encode at these bit rates so far. + if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24) + || bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32)) + { + bmpMetaData.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel; + } + // skip the remaining header because we can't read those parts this.stream.Skip(skipAmount); } @@ -581,9 +592,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp if (this.infoHeader.ClrUsed == 0) { - if (this.infoHeader.BitsPerPixel == 1 || - this.infoHeader.BitsPerPixel == 4 || - this.infoHeader.BitsPerPixel == 8) + if (this.infoHeader.BitsPerPixel == 1 + || this.infoHeader.BitsPerPixel == 4 + || this.infoHeader.BitsPerPixel == 8) { colorMapSize = (int)Math.Pow(2, this.infoHeader.BitsPerPixel) * 4; } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index 23b01ae9e8..b1a66accec 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Gets or sets the number of bits per pixel. /// - public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24; + public BmpBitsPerPixel? BitsPerPixel { get; set; } /// public void Encode(Image image, Stream stream) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index b49b8a8959..7a09a47f78 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -21,10 +21,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// private int padding; - private readonly BmpBitsPerPixel bitsPerPixel; - private readonly MemoryAllocator memoryAllocator; + private BmpBitsPerPixel? bitsPerPixel; + /// /// Initializes a new instance of the class. /// @@ -48,10 +48,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - // Cast to int will get the bytes per pixel - short bpp = (short)(8 * (int)this.bitsPerPixel); + BmpMetaData bmpMetaData = image.MetaData.GetOrAddFormatMetaData(BmpFormat.Instance); + this.bitsPerPixel = this.bitsPerPixel ?? bmpMetaData.BitsPerPixel; + + short bpp = (short)this.bitsPerPixel; int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32); - this.padding = bytesPerLine - (image.Width * (int)this.bitsPerPixel); + this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F)); // Set Resolution. ImageMetaData meta = image.MetaData; @@ -145,10 +147,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp } } - private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) - { - return this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, this.padding); - } + private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, this.padding); /// /// Writes the 32bit color palette to the stream. diff --git a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs index aa60f38662..3d678c13e1 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetaData.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetaData.cs @@ -8,6 +8,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public class BmpMetaData { - // TODO: Analyse what properties we would like to preserve. + /// + /// Gets or sets the number of bits per pixel. + /// + public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24; + + // TODO: Colors used once we support encoding palette bmps. } } diff --git a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs index 56952f0356..f62504d08f 100644 --- a/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs +++ b/src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs @@ -12,6 +12,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Gets the number of bits per pixel. /// - BmpBitsPerPixel BitsPerPixel { get; } + BmpBitsPerPixel? BitsPerPixel { get; } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index d887d23ade..c75c656919 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -28,10 +28,14 @@ namespace SixLabors.ImageSharp.Tests { TestImages.Bmp.RLE, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } }; - public BmpEncoderTests(ITestOutputHelper output) + public static readonly TheoryData BmpBitsPerPixelFiles = + new TheoryData { - this.Output = output; - } + { TestImages.Bmp.Car, BmpBitsPerPixel.Pixel24 }, + { TestImages.Bmp.Bit32Rgb, BmpBitsPerPixel.Pixel32 } + }; + + public BmpEncoderTests(ITestOutputHelper output) => this.Output = output; private ITestOutputHelper Output { get; } @@ -61,13 +65,35 @@ namespace SixLabors.ImageSharp.Tests } [Theory] - [WithTestPatternImages(nameof(BitsPerPixel), 24, 24, PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24)] - public void Encode_IsNotBoundToSinglePixelType(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) - where TPixel : struct, IPixel + [MemberData(nameof(BmpBitsPerPixelFiles))] + public void Encode_PreserveBitsPerPixel(string imagePath, BmpBitsPerPixel bmpBitsPerPixel) { - TestBmpEncoderCore(provider, bitsPerPixel); + var options = new BmpEncoder(); + + var testFile = TestFile.Create(imagePath); + using (Image input = testFile.CreateImage()) + { + using (var memStream = new MemoryStream()) + { + input.Save(memStream, options); + + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + BmpMetaData meta = output.MetaData.GetOrAddFormatMetaData(BmpFormat.Instance); + + Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); + } + } + } } + + [Theory] + [WithTestPatternImages(nameof(BitsPerPixel), 24, 24, PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24)] + public void Encode_IsNotBoundToSinglePixelType(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) + where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel); + [Theory] [WithTestPatternImages(nameof(BitsPerPixel), 48, 24, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel), 47, 8, PixelTypes.Rgba32)] @@ -75,10 +101,7 @@ namespace SixLabors.ImageSharp.Tests [WithSolidFilledImages(nameof(BitsPerPixel), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel), 7, 5, PixelTypes.Rgba32)] public void Encode_WorksWithDifferentSizes(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) - where TPixel : struct, IPixel - { - TestBmpEncoderCore(provider, bitsPerPixel); - } + where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel); private static void TestBmpEncoderCore(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : struct, IPixel diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index df015f7556..acfad042eb 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -178,6 +178,7 @@ namespace SixLabors.ImageSharp.Tests public const string Bit8Inverted = "Bmp/test8-inverted.bmp"; public const string Bit16 = "Bmp/test16.bmp"; public const string Bit16Inverted = "Bmp/test16-inverted.bmp"; + public const string Bit32Rgb = "Bmp/rgb32.bmp"; public static readonly string[] All = { diff --git a/tests/Images/Input/Bmp/rgb32.bmp b/tests/Images/Input/Bmp/rgb32.bmp new file mode 100644 index 0000000000..bc4c47c9e5 --- /dev/null +++ b/tests/Images/Input/Bmp/rgb32.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4c79cb8ffd2f1c096af27f9d82b5feaa0aa2cb049c791e0f6251de0435066e5 +size 32566 From bd5b0c07eb0d2703b45897746463e9460f33c0d2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Sep 2018 19:38:23 +0100 Subject: [PATCH 09/14] Png now correctly encodes 1, 2, 4 bit images --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 18 ++++--- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 4 +- src/ImageSharp/Formats/Png/PngBitDepth.cs | 15 ++++++ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 11 ++-- src/ImageSharp/Formats/Png/PngEncoder.cs | 3 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 51 +++++++++++++++---- .../Formats/Bmp/BmpEncoderTests.cs | 1 - .../Formats/Png/PngEncoderTests.cs | 31 +++++++++++ 9 files changed, 103 insertions(+), 33 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 3c48488ecc..cacaca0bb7 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -36,10 +36,15 @@ namespace SixLabors.ImageSharp /// The /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetBitsNeededForColorDepth(int colors) - { - return Math.Max(1, (int)Math.Ceiling(Math.Log(colors, 2))); - } + public static int GetBitsNeededForColorDepth(int colors) => Math.Max(1, (int)Math.Ceiling(Math.Log(colors, 2))); + + /// + /// Returns how many colors will be created by the specified number of bits. + /// + /// The bit depth. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetColorCountForBitDepth(int bitDepth) => (int)Math.Pow(2, bitDepth); /// /// Implementation of 1D Gaussian G(x) function @@ -132,10 +137,7 @@ namespace SixLabors.ImageSharp /// The bounding . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rectangle GetBoundingRectangle(Point topLeft, Point bottomRight) - { - return new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y); - } + public static Rectangle GetBoundingRectangle(Point topLeft, Point bottomRight) => new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y); /// /// Finds the bounding rectangle based on the first instance of any color component other diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 3bb44f1d06..a574d5178d 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -596,7 +596,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp || this.infoHeader.BitsPerPixel == 4 || this.infoHeader.BitsPerPixel == 8) { - colorMapSize = (int)Math.Pow(2, this.infoHeader.BitsPerPixel) * 4; + colorMapSize = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel) * 4; } } else diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 7a880b0f96..f2e0eab5c2 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp.Formats.Gif Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - this.gifMetaData = image.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + this.gifMetaData = image.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); this.colorTableMode = this.colorTableMode ?? this.gifMetaData.ColorTableMode; bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global); @@ -412,7 +412,7 @@ namespace SixLabors.ImageSharp.Formats.Gif int pixelCount = image.Palette.Length; // The maximium number of colors for the bit depth - int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3; + int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3; Rgb24 rgb = default; using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) diff --git a/src/ImageSharp/Formats/Png/PngBitDepth.cs b/src/ImageSharp/Formats/Png/PngBitDepth.cs index 0c22a4c913..396f2c1608 100644 --- a/src/ImageSharp/Formats/Png/PngBitDepth.cs +++ b/src/ImageSharp/Formats/Png/PngBitDepth.cs @@ -9,6 +9,21 @@ namespace SixLabors.ImageSharp.Formats.Png /// public enum PngBitDepth { + /// + /// 1 bit per sample or per palette index (not per pixel). + /// + Bit1 = 1, + + /// + /// 2 bits per sample or per palette index (not per pixel). + /// + Bit2 = 2, + + /// + /// 4 bits per sample or per palette index (not per pixel). + /// + Bit4 = 4, + /// /// 8 bits per sample or per palette index (not per pixel). /// diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 4bc4833011..3daee991c9 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -730,7 +730,7 @@ namespace SixLabors.ImageSharp.Formats.Png { case PngColorType.Grayscale: - int factor = 255 / ((int)Math.Pow(2, this.header.BitDepth) - 1); + int factor = 255 / (ImageMaths.GetColorCountForBitDepth(this.header.BitDepth) - 1); if (!this.hasTrans) { @@ -952,7 +952,7 @@ namespace SixLabors.ImageSharp.Formats.Png { case PngColorType.Grayscale: - int factor = 255 / ((int)Math.Pow(2, this.header.BitDepth) - 1); + int factor = 255 / (ImageMaths.GetColorCountForBitDepth(this.header.BitDepth) - 1); if (!this.hasTrans) { @@ -1303,12 +1303,7 @@ namespace SixLabors.ImageSharp.Formats.Png filterMethod: data[11], interlaceMethod: (PngInterlaceMode)data[12]); - // TODO: Figure out how we can determine the number of colors and support more bit depths. - if (bitDepth == 8 || bitDepth == 16) - { - pngMetaData.BitDepth = (PngBitDepth)bitDepth; - } - + pngMetaData.BitDepth = (PngBitDepth)bitDepth; pngMetaData.ColorType = this.header.ColorType; } diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 435d0abbc9..05d687a888 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -4,7 +4,6 @@ using System.IO; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Png @@ -45,7 +44,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// Gets or sets quantizer for reducing the color count. /// Defaults to the /// - public IQuantizer Quantizer { get; set; } = KnownQuantizers.Wu; + public IQuantizer Quantizer { get; set; } /// /// Gets or sets the transparency threshold. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 9d9de71b14..20fc8b8e36 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -48,11 +48,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// private readonly PngFilterMethod pngFilterMethod; - /// - /// The quantizer for reducing the color count. - /// - private readonly IQuantizer quantizer; - /// /// Gets or sets the CompressionLevel value /// @@ -63,6 +58,11 @@ namespace SixLabors.ImageSharp.Formats.Png /// private readonly byte threshold; + /// + /// The quantizer for reducing the color count. + /// + private IQuantizer quantizer; + /// /// Gets or sets a value indicating whether to write the gamma chunk /// @@ -185,8 +185,6 @@ namespace SixLabors.ImageSharp.Formats.Png this.gamma = this.gamma ?? pngMetaData.Gamma; this.writeGamma = this.gamma > 0; this.pngColorType = this.pngColorType ?? pngMetaData.ColorType; - - // TODO: We don't take full advantage of this information yet. this.pngBitDepth = this.pngBitDepth ?? pngMetaData.BitDepth; this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16); @@ -196,17 +194,27 @@ namespace SixLabors.ImageSharp.Formats.Png ReadOnlySpan quantizedPixelsSpan = default; if (this.pngColorType == PngColorType.Palette) { + byte bits; + + // Use the metadata to determine what quantization depth to use if no quantizer has been set. + if (this.quantizer == null) + { + bits = (byte)Math.Min(8u, (short)this.pngBitDepth); + int colorSize = ImageMaths.GetColorCountForBitDepth(bits); + this.quantizer = new WuQuantizer(colorSize); + } + // Create quantized frame returning the palette and set the bit depth. quantized = this.quantizer.CreateFrameQuantizer().QuantizeFrame(image.Frames.RootFrame); quantizedPixelsSpan = quantized.GetPixelSpan(); - byte bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); + bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk if (bits == 3) { bits = 4; } - else if (bits >= 5 || bits <= 7) + else if (bits >= 5 && bits <= 7) { bits = 8; } @@ -556,7 +564,7 @@ namespace SixLabors.ImageSharp.Formats.Png byte pixelCount = palette.Length.ToByte(); // Get max colors for bit depth. - int colorTableLength = (int)Math.Pow(2, header.BitDepth) * 3; + int colorTableLength = ImageMaths.GetColorCountForBitDepth(header.BitDepth) * 3; Rgba32 rgba = default; bool anyAlpha = false; @@ -700,7 +708,7 @@ namespace SixLabors.ImageSharp.Formats.Png private void WriteDataChunks(ImageFrame pixels, ReadOnlySpan quantizedPixelsSpan, Stream stream) where TPixel : struct, IPixel { - this.bytesPerScanline = this.width * this.bytesPerPixel; + this.bytesPerScanline = this.CalculateScanlineLength(this.width); int resultLength = this.bytesPerScanline + 1; this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean); @@ -828,5 +836,26 @@ namespace SixLabors.ImageSharp.Formats.Png stream.Write(this.buffer, 0, 4); // write the crc } + + /// + /// Calculates the scanline length. + /// + /// The width of the row. + /// + /// The representing the length. + /// + private int CalculateScanlineLength(int width) + { + int mod = this.bitDepth == 16 ? 16 : 8; + int scanlineLength = width * this.bitDepth * this.bytesPerPixel; + + int amount = scanlineLength % mod; + if (amount != 0) + { + scanlineLength += mod - amount; + } + + return scanlineLength / mod; + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index c75c656919..311b28f2d5 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -88,7 +88,6 @@ namespace SixLabors.ImageSharp.Tests } } - [Theory] [WithTestPatternImages(nameof(BitsPerPixel), 24, 24, PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24)] public void Encode_IsNotBoundToSinglePixelType(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 62de45064a..c9435a37dc 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -23,6 +23,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png // The images are an exact match. Maybe the submodule isn't updating? private const float ToleranceThresholdForPaletteEncoder = 1.3F / 100; + public static readonly TheoryData PngBitDepthFiles = + new TheoryData + { + { TestImages.Png.Rgb48Bpp, PngBitDepth.Bit16 }, + { TestImages.Png.Bpp1, PngBitDepth.Bit1 } + }; + /// /// All types except Palette /// @@ -290,5 +297,29 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } } + + [Theory] + [MemberData(nameof(PngBitDepthFiles))] + public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth) + { + var options = new PngEncoder(); + + var testFile = TestFile.Create(imagePath); + using (Image input = testFile.CreateImage()) + { + using (var memStream = new MemoryStream()) + { + input.Save(memStream, options); + + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + PngMetaData meta = output.MetaData.GetOrAddFormatMetaData(PngFormat.Instance); + + Assert.Equal(pngBitDepth, meta.BitDepth); + } + } + } + } } } \ No newline at end of file From 06b9c542488ecab108128dfde7b74bb9b15647c0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 12 Sep 2018 08:30:42 +0100 Subject: [PATCH 10/14] Simplify metadata API --- src/ImageSharp/Common/Helpers/ImageMaths.cs | 2 +- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 3 +-- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 2 +- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 15 ++++------- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 6 ++--- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 6 ++--- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- src/ImageSharp/MetaData/ImageFrameMetaData.cs | 25 ++----------------- src/ImageSharp/MetaData/ImageMetaData.cs | 21 ++-------------- .../Formats/Bmp/BmpEncoderTests.cs | 2 +- .../Formats/Gif/GifEncoderTests.cs | 10 ++++---- .../Formats/Png/PngEncoderTests.cs | 4 +-- .../MetaData/ImageFrameMetaDataTests.cs | 14 ++++------- 13 files changed, 31 insertions(+), 81 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index cacaca0bb7..c15e0a7329 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp /// The bit depth. /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetColorCountForBitDepth(int bitDepth) => (int)Math.Pow(2, bitDepth); + public static int GetColorCountForBitDepth(int bitDepth) => 1 << bitDepth; /// /// Implementation of 1D Gaussian G(x) function diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index a574d5178d..71852acddd 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -537,8 +537,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.metaData = meta; short bitsPerPixel = this.infoHeader.BitsPerPixel; - var bmpMetaData = new BmpMetaData(); - this.metaData.AddOrUpdateFormatMetaData(BmpFormat.Instance, bmpMetaData); + var bmpMetaData = this.metaData.GetFormatMetaData(BmpFormat.Instance); // We can only encode at these bit rates so far. if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 7a09a47f78..4ffaf39506 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - BmpMetaData bmpMetaData = image.MetaData.GetOrAddFormatMetaData(BmpFormat.Instance); + BmpMetaData bmpMetaData = image.MetaData.GetFormatMetaData(BmpFormat.Instance); this.bitsPerPixel = this.bitsPerPixel ?? bmpMetaData.BitsPerPixel; short bpp = (short)this.bitsPerPixel; diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index f503812641..207f126f9e 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -164,7 +164,6 @@ namespace SixLabors.ImageSharp.Formats.Gif this.globalColorTable?.Dispose(); } - image?.MetaData.AddOrUpdateFormatMetaData(GifFormat.Instance, this.gifMetaData); return image; } @@ -224,7 +223,6 @@ namespace SixLabors.ImageSharp.Formats.Gif this.globalColorTable?.Dispose(); } - this.metaData.AddOrUpdateFormatMetaData(GifFormat.Instance, this.gifMetaData); return new ImageInfo( new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), this.logicalScreenDescriptor.Width, @@ -542,7 +540,7 @@ namespace SixLabors.ImageSharp.Formats.Gif [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetFrameMetaData(ImageFrameMetaData meta) { - var gifMeta = new GifFrameMetaData(); + GifFrameMetaData gifMeta = meta.GetFormatMetaData(GifFormat.Instance); if (this.graphicsControlExtension.DelayTime > 0) { gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime; @@ -561,7 +559,6 @@ namespace SixLabors.ImageSharp.Formats.Gif } gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; - meta.AddOrUpdateFormatMetaData(GifFormat.Instance, gifMeta); } /// @@ -605,12 +602,10 @@ namespace SixLabors.ImageSharp.Formats.Gif } this.metaData = meta; - this.gifMetaData = new GifMetaData - { - ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag - ? GifColorTableMode.Global - : GifColorTableMode.Local - }; + this.gifMetaData = meta.GetFormatMetaData(GifFormat.Instance); + this.gifMetaData.ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag + ? GifColorTableMode.Global + : GifColorTableMode.Local; if (this.logicalScreenDescriptor.GlobalColorTableFlag) { diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index f2e0eab5c2..a516b5fefe 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp.Formats.Gif Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - this.gifMetaData = image.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + this.gifMetaData = image.MetaData.GetFormatMetaData(GifFormat.Instance); this.colorTableMode = this.colorTableMode ?? this.gifMetaData.ColorTableMode; bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global); @@ -143,7 +143,7 @@ namespace SixLabors.ImageSharp.Formats.Gif for (int i = 0; i < image.Frames.Count; i++) { ImageFrame frame = image.Frames[i]; - GifFrameMetaData frameMetaData = frame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData frameMetaData = frame.MetaData.GetFormatMetaData(GifFormat.Instance); this.WriteGraphicalControlExtension(frameMetaData, transparencyIndex, stream); this.WriteImageDescriptor(frame, false, stream); @@ -169,7 +169,7 @@ namespace SixLabors.ImageSharp.Formats.Gif GifFrameMetaData previousMeta = null; foreach (ImageFrame frame in image.Frames) { - GifFrameMetaData meta = frame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData meta = frame.MetaData.GetFormatMetaData(GifFormat.Instance); if (quantized is null) { // Allow each frame to be encoded at whatever color depth the frame designates if set. diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 3daee991c9..7837c2da5c 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -217,8 +217,7 @@ namespace SixLabors.ImageSharp.Formats.Png where TPixel : struct, IPixel { var metaData = new ImageMetaData(); - var pngMetaData = new PngMetaData(); - metaData.AddOrUpdateFormatMetaData(PngFormat.Instance, pngMetaData); + var pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance); this.currentStream = stream; this.currentStream.Skip(8); Image image = null; @@ -308,8 +307,7 @@ namespace SixLabors.ImageSharp.Formats.Png public IImageInfo Identify(Stream stream) { var metaData = new ImageMetaData(); - var pngMetaData = new PngMetaData(); - metaData.AddOrUpdateFormatMetaData(PngFormat.Instance, pngMetaData); + var pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance); this.currentStream = stream; this.currentStream.Skip(8); try diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 20fc8b8e36..906e6d0003 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -181,7 +181,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.height = image.Height; // Always take the encoder options over the metadata values. - PngMetaData pngMetaData = image.MetaData.GetOrAddFormatMetaData(PngFormat.Instance); + PngMetaData pngMetaData = image.MetaData.GetFormatMetaData(PngFormat.Instance); this.gamma = this.gamma ?? pngMetaData.Gamma; this.writeGamma = this.gamma > 0; this.pngColorType = this.pngColorType ?? pngMetaData.ColorType; diff --git a/src/ImageSharp/MetaData/ImageFrameMetaData.cs b/src/ImageSharp/MetaData/ImageFrameMetaData.cs index 908a0e6246..4b819e2013 100644 --- a/src/ImageSharp/MetaData/ImageFrameMetaData.cs +++ b/src/ImageSharp/MetaData/ImageFrameMetaData.cs @@ -44,27 +44,6 @@ namespace SixLabors.ImageSharp.MetaData /// The cloned instance. public ImageFrameMetaData Clone() => new ImageFrameMetaData(this); - /// - /// Adds or updates the specified key and value to the . - /// - /// The type of format metadata. - /// The type of format frame metadata. - /// The key of the metadata to add. - /// The value of the element to add. - /// key is null. - /// value is null. - /// An element with the same key already exists in the . - public void AddOrUpdateFormatMetaData( - IImageFormat key, - TFormatFrameMetaData value) - where TFormatMetaData : class - where TFormatFrameMetaData : class - { - // Don't think this needs to be threadsafe. - Guard.NotNull(value, nameof(value)); - this.formatMetaData[key] = value; - } - /// /// Gets the metadata value associated with the specified key. /// @@ -74,7 +53,7 @@ namespace SixLabors.ImageSharp.MetaData /// /// The . /// - public TFormatFrameMetaData GetOrAddFormatMetaData(IImageFormat key) + public TFormatFrameMetaData GetFormatMetaData(IImageFormat key) where TFormatMetaData : class where TFormatFrameMetaData : class { @@ -84,7 +63,7 @@ namespace SixLabors.ImageSharp.MetaData } TFormatFrameMetaData newMeta = key.CreateDefaultFormatFrameMetaData(); - this.AddOrUpdateFormatMetaData(key, newMeta); + this.formatMetaData[key] = newMeta; return newMeta; } } diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index 74c00cf8a5..7e74157e70 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -131,23 +131,6 @@ namespace SixLabors.ImageSharp.MetaData /// public IList Properties { get; } = new List(); - /// - /// Adds or updates the specified key and value to the . - /// - /// The type of format metadata. - /// The key of the metadata to add. - /// The value of the element to add. - /// key is null. - /// value is null. - /// An element with the same key already exists in the . - public void AddOrUpdateFormatMetaData(IImageFormat key, TFormatMetaData value) - where TFormatMetaData : class - { - // Don't think this needs to be threadsafe. - Guard.NotNull(value, nameof(value)); - this.formatMetaData[key] = value; - } - /// /// Gets the metadata value associated with the specified key. /// @@ -156,7 +139,7 @@ namespace SixLabors.ImageSharp.MetaData /// /// The . /// - public TFormatMetaData GetOrAddFormatMetaData(IImageFormat key) + public TFormatMetaData GetFormatMetaData(IImageFormat key) where TFormatMetaData : class { if (this.formatMetaData.TryGetValue(key, out object meta)) @@ -165,7 +148,7 @@ namespace SixLabors.ImageSharp.MetaData } TFormatMetaData newMeta = key.CreateDefaultFormatMetaData(); - this.AddOrUpdateFormatMetaData(key, newMeta); + this.formatMetaData[key] = newMeta; return newMeta; } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 311b28f2d5..b9f855cf12 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests memStream.Position = 0; using (var output = Image.Load(memStream)) { - BmpMetaData meta = output.MetaData.GetOrAddFormatMetaData(BmpFormat.Instance); + BmpMetaData meta = output.MetaData.GetFormatMetaData(BmpFormat.Instance); Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 0c32689f09..4a17f867f3 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -189,8 +189,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif inStream.Position = 0; var image = Image.Load(inStream); - GifMetaData metaData = image.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); - GifFrameMetaData frameMetaData = image.Frames.RootFrame.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifMetaData metaData = image.MetaData.GetFormatMetaData(GifFormat.Instance); + GifFrameMetaData frameMetaData = image.Frames.RootFrame.MetaData.GetFormatMetaData(GifFormat.Instance); GifColorTableMode colorMode = metaData.ColorTableMode; var encoder = new GifEncoder() { @@ -204,7 +204,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif outStream.Position = 0; var clone = Image.Load(outStream); - GifMetaData cloneMetaData = clone.MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifMetaData cloneMetaData = clone.MetaData.GetFormatMetaData(GifFormat.Instance); Assert.Equal(metaData.ColorTableMode, cloneMetaData.ColorTableMode); // Gifiddle and Cyotek GifInfo say this image has 64 colors. @@ -212,8 +212,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif for (int i = 0; i < image.Frames.Count; i++) { - GifFrameMetaData ifm = image.Frames[i].MetaData.GetOrAddFormatMetaData(GifFormat.Instance); - GifFrameMetaData cifm = clone.Frames[i].MetaData.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData ifm = image.Frames[i].MetaData.GetFormatMetaData(GifFormat.Instance); + GifFrameMetaData cifm = clone.Frames[i].MetaData.GetFormatMetaData(GifFormat.Instance); Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength); Assert.Equal(ifm.FrameDelay, cifm.FrameDelay); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index c9435a37dc..0508ac8c26 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -228,7 +228,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png : image; float paletteToleranceHack = 80f / paletteSize; - paletteToleranceHack = paletteToleranceHack * paletteToleranceHack; + paletteToleranceHack *= paletteToleranceHack; ImageComparer comparer = pngColorType == PngColorType.Palette ? ImageComparer.Tolerant(ToleranceThresholdForPaletteEncoder * paletteToleranceHack) : ImageComparer.Exact; @@ -314,7 +314,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png memStream.Position = 0; using (var output = Image.Load(memStream)) { - PngMetaData meta = output.MetaData.GetOrAddFormatMetaData(PngFormat.Instance); + PngMetaData meta = output.MetaData.GetFormatMetaData(PngFormat.Instance); Assert.Equal(pngBitDepth, meta.BitDepth); } diff --git a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs index 54441f0cbf..0a0ca1efa4 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs @@ -19,18 +19,14 @@ namespace SixLabors.ImageSharp.Tests const int colorTableLength = 128; const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground; - var gifFrameMetaData = new GifFrameMetaData - { - FrameDelay = frameDelay, - ColorTableLength = colorTableLength, - DisposalMethod = disposalMethod - }; - var metaData = new ImageFrameMetaData(); - metaData.AddOrUpdateFormatMetaData(GifFormat.Instance, gifFrameMetaData); + GifFrameMetaData gifFrameMetaData = metaData.GetFormatMetaData(GifFormat.Instance); + gifFrameMetaData.FrameDelay = frameDelay; + gifFrameMetaData.ColorTableLength = colorTableLength; + gifFrameMetaData.DisposalMethod = disposalMethod; var clone = new ImageFrameMetaData(metaData); - GifFrameMetaData cloneGifFrameMetaData = clone.GetOrAddFormatMetaData(GifFormat.Instance); + GifFrameMetaData cloneGifFrameMetaData = clone.GetFormatMetaData(GifFormat.Instance); Assert.Equal(frameDelay, cloneGifFrameMetaData.FrameDelay); Assert.Equal(colorTableLength, cloneGifFrameMetaData.ColorTableLength); From ffc0c7dd19034de9cef1275344397f2eef54b223 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 12 Sep 2018 18:10:06 +0100 Subject: [PATCH 11/14] Can now preserve jpeg quality :heart: --- .../Components/Decoder/QualityEvaluator.cs | 140 ++++++++++++++++++ .../Formats/Jpeg/IJpegEncoderOptions.cs | 7 +- .../Formats/Jpeg/JpegDecoderCore.cs | 14 +- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 7 +- .../Formats/Jpeg/JpegEncoderCore.cs | 33 ++--- src/ImageSharp/Formats/Jpeg/JpegMetaData.cs | 7 +- .../Formats/Jpg/JpegDecoderTests.MetaData.cs | 37 +++++ .../Formats/Jpg/JpegEncoderTests.cs | 72 ++++----- 8 files changed, 232 insertions(+), 85 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs new file mode 100644 index 0000000000..4e11568c8d --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Provides methods to evaluate the quality of an image. + /// Ported from + /// + internal static class QualityEvaluator + { + private static readonly int[] Hash = new int[101] + { + 1020, 1015, 932, 848, 780, 735, 702, 679, 660, 645, + 632, 623, 613, 607, 600, 594, 589, 585, 581, 571, + 555, 542, 529, 514, 494, 474, 457, 439, 424, 410, + 397, 386, 373, 364, 351, 341, 334, 324, 317, 309, + 299, 294, 287, 279, 274, 267, 262, 257, 251, 247, + 243, 237, 232, 227, 222, 217, 213, 207, 202, 198, + 192, 188, 183, 177, 173, 168, 163, 157, 153, 148, + 143, 139, 132, 128, 125, 119, 115, 108, 104, 99, + 94, 90, 84, 79, 74, 70, 64, 59, 55, 49, + 45, 40, 34, 30, 25, 20, 15, 11, 6, 4, + 0 + }; + + private static readonly int[] Sums = new int[101] + { + 32640, 32635, 32266, 31495, 30665, 29804, 29146, 28599, 28104, + 27670, 27225, 26725, 26210, 25716, 25240, 24789, 24373, 23946, + 23572, 22846, 21801, 20842, 19949, 19121, 18386, 17651, 16998, + 16349, 15800, 15247, 14783, 14321, 13859, 13535, 13081, 12702, + 12423, 12056, 11779, 11513, 11135, 10955, 10676, 10392, 10208, + 9928, 9747, 9564, 9369, 9193, 9017, 8822, 8639, 8458, + 8270, 8084, 7896, 7710, 7527, 7347, 7156, 6977, 6788, + 6607, 6422, 6236, 6054, 5867, 5684, 5495, 5305, 5128, + 4945, 4751, 4638, 4442, 4248, 4065, 3888, 3698, 3509, + 3326, 3139, 2957, 2775, 2586, 2405, 2216, 2037, 1846, + 1666, 1483, 1297, 1109, 927, 735, 554, 375, 201, + 128, 0 + }; + + private static readonly int[] Hash1 = new int[101] + { + 510, 505, 422, 380, 355, 338, 326, 318, 311, 305, + 300, 297, 293, 291, 288, 286, 284, 283, 281, 280, + 279, 278, 277, 273, 262, 251, 243, 233, 225, 218, + 211, 205, 198, 193, 186, 181, 177, 172, 168, 164, + 158, 156, 152, 148, 145, 142, 139, 136, 133, 131, + 129, 126, 123, 120, 118, 115, 113, 110, 107, 105, + 102, 100, 97, 94, 92, 89, 87, 83, 81, 79, + 76, 74, 70, 68, 66, 63, 61, 57, 55, 52, + 50, 48, 44, 42, 39, 37, 34, 31, 29, 26, + 24, 21, 18, 16, 13, 11, 8, 6, 3, 2, + 0 + }; + + private static readonly int[] Sums1 = new int[101] + { + 16320, 16315, 15946, 15277, 14655, 14073, 13623, 13230, 12859, + 12560, 12240, 11861, 11456, 11081, 10714, 10360, 10027, 9679, + 9368, 9056, 8680, 8331, 7995, 7668, 7376, 7084, 6823, + 6562, 6345, 6125, 5939, 5756, 5571, 5421, 5240, 5086, + 4976, 4829, 4719, 4616, 4463, 4393, 4280, 4166, 4092, + 3980, 3909, 3835, 3755, 3688, 3621, 3541, 3467, 3396, + 3323, 3247, 3170, 3096, 3021, 2952, 2874, 2804, 2727, + 2657, 2583, 2509, 2437, 2362, 2290, 2211, 2136, 2068, + 1996, 1915, 1858, 1773, 1692, 1620, 1552, 1477, 1398, + 1326, 1251, 1179, 1109, 1031, 961, 884, 814, 736, + 667, 592, 518, 441, 369, 292, 221, 151, 86, + 64, 0 + }; + + /// + /// Returns an estimated quality of the image based on the quantization tables. + /// + /// The quantization tables. + /// The . + public static int EstimateQuality(Block8x8F[] quantizationTables) + { + int quality = 75; + float sum = 0; + + for (int i = 0; i < quantizationTables.Length; i++) + { + ref Block8x8F qTable = ref quantizationTables[i]; + for (int j = 0; j < Block8x8F.Size; j++) + { + sum += qTable[j]; + } + } + + ref Block8x8F qTable0 = ref quantizationTables[0]; + ref Block8x8F qTable1 = ref quantizationTables[1]; + + if (!qTable0.Equals(default)) + { + if (!qTable1.Equals(default)) + { + quality = (int)(qTable0[2] + + qTable0[53] + + qTable1[0] + + qTable1[Block8x8F.Size - 1]); + + for (int i = 0; i < 100; i++) + { + if (quality < Hash[i] && sum < Sums[i]) + { + continue; + } + + if (((quality <= Hash[i]) && (sum <= Sums[i])) || (i >= 50)) + { + return i + 1; + } + } + } + else + { + quality = (int)(qTable0[2] + qTable0[53]); + + for (int i = 0; i < 100; i++) + { + if (quality < Hash1[i] && sum < Sums1[i]) + { + continue; + } + + if (((quality <= Hash1[i]) && (sum <= Sums1[i])) || (i >= 50)) + { + return i + 1; + } + } + } + } + + return quality; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs index 4076b7da82..53108de934 100644 --- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs +++ b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs @@ -8,17 +8,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// internal interface IJpegEncoderOptions { - /// - /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. - /// - bool IgnoreMetadata { get; } - /// /// Gets the quality, that will be used to encode the image. Quality /// index must be between 0 and 100 (compression from max to min). /// /// The quality of the jpg image from 0 to 100. - int Quality { get; } + int? Quality { get; } /// /// Gets the subsample ration, that will be used to encode the image. diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 3ea24809d8..011b6100dc 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -259,11 +259,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.InputStream.Read(this.markerBuffer, 0, 2); byte marker = this.markerBuffer[1]; fileMarker = new JpegFileMarker(marker, (int)this.InputStream.Position - 2); + this.QuantizationTables = new Block8x8F[4]; // Only assign what we need if (!metadataOnly) { - this.QuantizationTables = new Block8x8F[4]; this.dcHuffmanTables = new HuffmanTables(); this.acHuffmanTables = new HuffmanTables(); this.fastACTables = new FastACTables(this.configuration.MemoryAllocator); @@ -314,15 +314,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg break; case JpegConstants.Markers.DQT: - if (metadataOnly) - { - this.InputStream.Skip(remaining); - } - else - { - this.ProcessDefineQuantizationTablesMarker(remaining); - } - + this.ProcessDefineQuantizationTablesMarker(remaining); break; case JpegConstants.Markers.DRI: @@ -708,6 +700,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { throw new ImageFormatException("DQT has wrong length"); } + + this.MetaData.GetFormatMetaData(JpegFormat.Instance).Quality = QualityEvaluator.EstimateQuality(this.QuantizationTables); } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 0f389dee0f..d649d30418 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -11,17 +11,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions { - /// - /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. - /// - public bool IgnoreMetadata { get; set; } - /// /// Gets or sets the quality, that will be used to encode the image. Quality /// index must be between 0 and 100 (compression from max to min). /// Defaults to 75. /// - public int Quality { get; set; } = 75; + public int? Quality { get; set; } /// /// Gets or sets the subsample ration, that will be used to encode the image. diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index f7b6fe9967..32c50d2a08 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -123,19 +123,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private readonly byte[] huffmanBuffer = new byte[179]; /// - /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. + /// Gets or sets the subsampling method to use. /// - private readonly bool ignoreMetadata; + private JpegSubsample? subsample; /// /// The quality, that will be used to encode the image. /// - private readonly int quality; - - /// - /// Gets or sets the subsampling method to use. - /// - private readonly JpegSubsample? subsample; + private readonly int? quality; /// /// The accumulated bits to write to the stream. @@ -168,11 +163,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The options public JpegEncoderCore(IJpegEncoderOptions options) { - // System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1. - this.quality = options.Quality.Clamp(1, 100); - this.subsample = options.Subsample ?? (this.quality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420); - - this.ignoreMetadata = options.IgnoreMetadata; + this.quality = options.Quality; + this.subsample = options.Subsample; } /// @@ -195,15 +187,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.outputStream = stream; + // System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1. + int qlty = (this.quality ?? image.MetaData.GetFormatMetaData(JpegFormat.Instance).Quality).Clamp(1, 100); + this.subsample = this.subsample ?? (qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420); + // Convert from a quality rating to a scaling factor. int scale; - if (this.quality < 50) + if (qlty < 50) { - scale = 5000 / this.quality; + scale = 5000 / qlty; } else { - scale = 200 - (this.quality * 2); + scale = 200 - (qlty * 2); } // Initialize the quantization tables. @@ -767,11 +763,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private void WriteProfiles(Image image) where TPixel : struct, IPixel { - if (this.ignoreMetadata) - { - return; - } - image.MetaData.SyncProfiles(); this.WriteExifProfile(image.MetaData.ExifProfile); this.WriteIccProfile(image.MetaData.IccProfile); diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs b/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs index b05f9fa15a..bd7232e602 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetaData.cs @@ -8,6 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetaData { - // TODO: Analyse what properties we would like to preserve. + /// + /// Gets or sets the encoded quality. + /// + public int Quality { get; set; } = 75; } -} +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.MetaData.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.MetaData.cs index 2e67c06c18..4810985f11 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.MetaData.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.MetaData.cs @@ -49,6 +49,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch } }; + public static readonly TheoryData QualityFiles = + new TheoryData + { + { TestImages.Jpeg.Baseline.Calliphora, 80}, + { TestImages.Jpeg.Progressive.Fb, 75 } + }; + [Theory] [MemberData(nameof(MetaDataTestData))] public void MetaDataIsParsedCorrectly( @@ -101,6 +108,36 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } } + [Theory] + [MemberData(nameof(QualityFiles))] + public void Identify_VerifyQuality(string imagePath, int quality) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + var decoder = new JpegDecoder(); + IImageInfo image = decoder.Identify(Configuration.Default, stream); + JpegMetaData meta = image.MetaData.GetFormatMetaData(JpegFormat.Instance); + Assert.Equal(quality, meta.Quality); + } + } + + [Theory] + [MemberData(nameof(QualityFiles))] + public void Decode_VerifyQuality(string imagePath, int quality) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + var decoder = new JpegDecoder(); + using (Image image = decoder.Decode(Configuration.Default, stream)) + { + JpegMetaData meta = image.MetaData.GetFormatMetaData(JpegFormat.Instance); + Assert.Equal(quality, meta.Quality); + } + } + } + private static void TestImageInfo(string imagePath, IImageDecoder decoder, bool useIdentify, Action test) { var testFile = TestFile.Create(imagePath); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index a31ae37b75..598d99274a 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -14,6 +14,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { public class JpegEncoderTests { + public static readonly TheoryData QualityFiles = + new TheoryData + { + { TestImages.Jpeg.Baseline.Calliphora, 80}, + { TestImages.Jpeg.Progressive.Fb, 75 } + }; + public static readonly TheoryData BitsPerPixel_Quality = new TheoryData { @@ -34,6 +41,29 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch } }; + [Theory] + [MemberData(nameof(QualityFiles))] + public void Encode_PreserveQuality(string imagePath, int quality) + { + var options = new JpegEncoder(); + + var testFile = TestFile.Create(imagePath); + using (Image input = testFile.CreateImage()) + { + using (var memStream = new MemoryStream()) + { + input.Save(memStream, options); + + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + JpegMetaData meta = output.MetaData.GetFormatMetaData(JpegFormat.Instance); + Assert.Equal(quality, meta.Quality); + } + } + } + } + [Theory] [WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 73, 71, PixelTypes.Rgba32)] @@ -43,18 +73,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)] public void EncodeBaseline_WorksWithDifferentSizes(TestImageProvider provider, JpegSubsample subsample, int quality) - where TPixel : struct, IPixel - { - TestJpegEncoderCore(provider, subsample, quality); - } + where TPixel : struct, IPixel => TestJpegEncoderCore(provider, subsample, quality); [Theory] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)] public void EncodeBaseline_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegSubsample subsample, int quality) - where TPixel : struct, IPixel - { - TestJpegEncoderCore(provider, subsample, quality); - } + where TPixel : struct, IPixel => TestJpegEncoderCore(provider, subsample, quality); /// /// Anton's SUPER-SCIENTIFIC tolerance threshold calculation @@ -103,38 +127,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void IgnoreMetadata_ControlsIfExifProfileIsWritten(bool ignoreMetaData) - { - var encoder = new JpegEncoder() - { - IgnoreMetadata = ignoreMetaData - }; - - using (Image input = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateImage()) - { - using (var memStream = new MemoryStream()) - { - input.Save(memStream, encoder); - - memStream.Position = 0; - using (var output = Image.Load(memStream)) - { - if (ignoreMetaData) - { - Assert.Null(output.MetaData.ExifProfile); - } - else - { - Assert.NotNull(output.MetaData.ExifProfile); - } - } - } - } - } - [Fact] public void Quality_0_And_1_Are_Identical() { From bce72007971152d848cbdf075e890548c2ad546f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 13 Sep 2018 16:32:58 +0100 Subject: [PATCH 12/14] Make formats public + minor cleanup. --- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 22 +++---- src/ImageSharp/Formats/Bmp/BmpFormat.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoder.cs | 5 -- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 59 +++++++++---------- src/ImageSharp/Formats/Gif/GifFormat.cs | 2 +- .../Formats/Gif/IGifEncoderOptions.cs | 5 -- .../Formats/Jpeg/JpegEncoderCore.cs | 29 +++++---- src/ImageSharp/Formats/Jpeg/JpegFormat.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoder.cs | 6 -- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 26 ++++---- src/ImageSharp/Formats/Png/PngFormat.cs | 2 +- src/ImageSharp/ImageFrame{TPixel}.cs | 23 ++------ .../Formats/Gif/GifEncoderTests.cs | 16 ++--- 13 files changed, 82 insertions(+), 117 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 4ffaf39506..44e42528cf 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -48,7 +48,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - BmpMetaData bmpMetaData = image.MetaData.GetFormatMetaData(BmpFormat.Instance); + ImageMetaData metaData = image.MetaData; + BmpMetaData bmpMetaData = metaData.GetFormatMetaData(BmpFormat.Instance); this.bitsPerPixel = this.bitsPerPixel ?? bmpMetaData.BitsPerPixel; short bpp = (short)this.bitsPerPixel; @@ -56,31 +57,30 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F)); // Set Resolution. - ImageMetaData meta = image.MetaData; int hResolution = 0; int vResolution = 0; - if (meta.ResolutionUnits != PixelResolutionUnit.AspectRatio) + if (metaData.ResolutionUnits != PixelResolutionUnit.AspectRatio) { - if (meta.HorizontalResolution > 0 && meta.VerticalResolution > 0) + if (metaData.HorizontalResolution > 0 && metaData.VerticalResolution > 0) { - switch (meta.ResolutionUnits) + switch (metaData.ResolutionUnits) { case PixelResolutionUnit.PixelsPerInch: - hResolution = (int)Math.Round(UnitConverter.InchToMeter(meta.HorizontalResolution)); - vResolution = (int)Math.Round(UnitConverter.InchToMeter(meta.VerticalResolution)); + hResolution = (int)Math.Round(UnitConverter.InchToMeter(metaData.HorizontalResolution)); + vResolution = (int)Math.Round(UnitConverter.InchToMeter(metaData.VerticalResolution)); break; case PixelResolutionUnit.PixelsPerCentimeter: - hResolution = (int)Math.Round(UnitConverter.CmToMeter(meta.HorizontalResolution)); - vResolution = (int)Math.Round(UnitConverter.CmToMeter(meta.VerticalResolution)); + hResolution = (int)Math.Round(UnitConverter.CmToMeter(metaData.HorizontalResolution)); + vResolution = (int)Math.Round(UnitConverter.CmToMeter(metaData.VerticalResolution)); break; case PixelResolutionUnit.PixelsPerMeter: - hResolution = (int)Math.Round(meta.HorizontalResolution); - vResolution = (int)Math.Round(meta.VerticalResolution); + hResolution = (int)Math.Round(metaData.HorizontalResolution); + vResolution = (int)Math.Round(metaData.VerticalResolution); break; } diff --git a/src/ImageSharp/Formats/Bmp/BmpFormat.cs b/src/ImageSharp/Formats/Bmp/BmpFormat.cs index 665f492daf..a5eaab8ebf 100644 --- a/src/ImageSharp/Formats/Bmp/BmpFormat.cs +++ b/src/ImageSharp/Formats/Bmp/BmpFormat.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Registers the image encoders, decoders and mime type detectors for the bmp format. /// - internal sealed class BmpFormat : IImageFormat + public sealed class BmpFormat : IImageFormat { private BmpFormat() { diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 9f376044d3..4210b08765 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -14,11 +14,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions { - /// - /// Gets or sets a value indicating whether the metadata should be ignored when the image is being encoded. - /// - public bool IgnoreMetadata { get; set; } = false; - /// /// Gets or sets the encoding that should be used when writing comments. /// diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index a516b5fefe..ae0366e6e6 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -45,11 +45,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private GifColorTableMode? colorTableMode; - /// - /// A flag indicating whether to ingore the metadata when writing the image. - /// - private readonly bool ignoreMetadata; - /// /// The number of bits requires to store the color palette. /// @@ -70,7 +65,6 @@ namespace SixLabors.ImageSharp.Formats.Gif this.memoryAllocator = memoryAllocator; this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding; this.quantizer = options.Quantizer; - this.ignoreMetadata = options.IgnoreMetadata; this.colorTableMode = options.ColorTableMode; } @@ -86,7 +80,8 @@ namespace SixLabors.ImageSharp.Formats.Gif Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - this.gifMetaData = image.MetaData.GetFormatMetaData(GifFormat.Instance); + ImageMetaData metaData = image.MetaData; + this.gifMetaData = metaData.GetFormatMetaData(GifFormat.Instance); this.colorTableMode = this.colorTableMode ?? this.gifMetaData.ColorTableMode; bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global); @@ -102,7 +97,7 @@ namespace SixLabors.ImageSharp.Formats.Gif // Write the LSD. int index = this.GetTransparentIndex(quantized); - this.WriteLogicalScreenDescriptor(image, index, useGlobalTable, stream); + this.WriteLogicalScreenDescriptor(metaData, image.Width, image.Height, index, useGlobalTable, stream); if (useGlobalTable) { @@ -110,7 +105,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } // Write the comments. - this.WriteComments(image.MetaData, stream); + this.WriteComments(metaData, stream); // Write application extension to allow additional frames. if (image.Frames.Count > 1) @@ -143,7 +138,8 @@ namespace SixLabors.ImageSharp.Formats.Gif for (int i = 0; i < image.Frames.Count; i++) { ImageFrame frame = image.Frames[i]; - GifFrameMetaData frameMetaData = frame.MetaData.GetFormatMetaData(GifFormat.Instance); + ImageFrameMetaData metaData = frame.MetaData; + GifFrameMetaData frameMetaData = metaData.GetFormatMetaData(GifFormat.Instance); this.WriteGraphicalControlExtension(frameMetaData, transparencyIndex, stream); this.WriteImageDescriptor(frame, false, stream); @@ -169,15 +165,16 @@ namespace SixLabors.ImageSharp.Formats.Gif GifFrameMetaData previousMeta = null; foreach (ImageFrame frame in image.Frames) { - GifFrameMetaData meta = frame.MetaData.GetFormatMetaData(GifFormat.Instance); + ImageFrameMetaData metaData = frame.MetaData; + GifFrameMetaData frameMetaData = metaData.GetFormatMetaData(GifFormat.Instance); if (quantized is null) { // Allow each frame to be encoded at whatever color depth the frame designates if set. if (previousFrame != null - && previousMeta.ColorTableLength != meta.ColorTableLength - && meta.ColorTableLength > 0) + && previousMeta.ColorTableLength != frameMetaData.ColorTableLength + && frameMetaData.ColorTableLength > 0) { - quantized = this.quantizer.CreateFrameQuantizer(meta.ColorTableLength).QuantizeFrame(frame); + quantized = this.quantizer.CreateFrameQuantizer(frameMetaData.ColorTableLength).QuantizeFrame(frame); } else { @@ -186,7 +183,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); - this.WriteGraphicalControlExtension(meta, this.GetTransparentIndex(quantized), stream); + this.WriteGraphicalControlExtension(frameMetaData, this.GetTransparentIndex(quantized), stream); this.WriteImageDescriptor(frame, true, stream); this.WriteColorTable(quantized, stream); this.WriteImageData(quantized, stream); @@ -194,7 +191,7 @@ namespace SixLabors.ImageSharp.Formats.Gif quantized?.Dispose(); quantized = null; // So next frame can regenerate it previousFrame = frame; - previousMeta = meta; + previousMeta = frameMetaData; } } @@ -239,13 +236,19 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Writes the logical screen descriptor to the stream. /// - /// The pixel format. - /// The image to encode. + /// The image metadata. + /// The image width. + /// The image height. /// The transparency index to set the default background index to. /// Whether to use a global or local color table. /// The stream to write to. - private void WriteLogicalScreenDescriptor(Image image, int transparencyIndex, bool useGlobalTable, Stream stream) - where TPixel : struct, IPixel + private void WriteLogicalScreenDescriptor( + ImageMetaData metaData, + int width, + int height, + int transparencyIndex, + bool useGlobalTable, + Stream stream) { byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1); @@ -258,13 +261,12 @@ namespace SixLabors.ImageSharp.Formats.Gif // 1..255 - Value used in the computation. // // Aspect Ratio = (Pixel Aspect Ratio + 15) / 64 - ImageMetaData meta = image.MetaData; byte ratio = 0; - if (meta.ResolutionUnits == PixelResolutionUnit.AspectRatio) + if (metaData.ResolutionUnits == PixelResolutionUnit.AspectRatio) { - double hr = meta.HorizontalResolution; - double vr = meta.VerticalResolution; + double hr = metaData.HorizontalResolution; + double vr = metaData.VerticalResolution; if (hr != vr) { if (hr > vr) @@ -279,8 +281,8 @@ namespace SixLabors.ImageSharp.Formats.Gif } var descriptor = new GifLogicalScreenDescriptor( - width: (ushort)image.Width, - height: (ushort)image.Height, + width: (ushort)width, + height: (ushort)height, packed: packedValue, backgroundColorIndex: unchecked((byte)transparencyIndex), ratio); @@ -312,11 +314,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// The stream to write to. private void WriteComments(ImageMetaData metadata, Stream stream) { - if (this.ignoreMetadata) - { - return; - } - if (!metadata.TryGetProperty(GifConstants.Comments, out ImageProperty property) || string.IsNullOrEmpty(property.Value)) { return; diff --git a/src/ImageSharp/Formats/Gif/GifFormat.cs b/src/ImageSharp/Formats/Gif/GifFormat.cs index f91269ceda..07d4a54547 100644 --- a/src/ImageSharp/Formats/Gif/GifFormat.cs +++ b/src/ImageSharp/Formats/Gif/GifFormat.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Registers the image encoders, decoders and mime type detectors for the gif format. /// - internal sealed class GifFormat : IImageFormat + public sealed class GifFormat : IImageFormat { private GifFormat() { diff --git a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs index 7dd558c803..4b3c28a92c 100644 --- a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs +++ b/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs @@ -11,11 +11,6 @@ namespace SixLabors.ImageSharp.Formats.Gif /// internal interface IGifEncoderOptions { - /// - /// Gets a value indicating whether the metadata should be ignored when the image is being encoded. - /// - bool IgnoreMetadata { get; } - /// /// Gets the text encoding used to write comments. /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 32c50d2a08..a4677ba2b7 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -186,9 +186,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } this.outputStream = stream; + ImageMetaData metaData = image.MetaData; // System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1. - int qlty = (this.quality ?? image.MetaData.GetFormatMetaData(JpegFormat.Instance).Quality).Clamp(1, 100); + int qlty = (this.quality ?? metaData.GetFormatMetaData(JpegFormat.Instance).Quality).Clamp(1, 100); this.subsample = this.subsample ?? (qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420); // Convert from a quality rating to a scaling factor. @@ -210,10 +211,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int componentCount = 3; // Write the Start Of Image marker. - this.WriteApplicationHeader(image.MetaData); + this.WriteApplicationHeader(metaData); // Write Exif and ICC profiles - this.WriteProfiles(image); + this.WriteProfiles(metaData); // Write the quantization tables. this.WriteDefineQuantizationTables(); @@ -620,6 +621,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void WriteExifProfile(ExifProfile exifProfile) { + if (exifProfile is null) + { + return; + } + const int MaxBytesApp1 = 65533; const int MaxBytesWithExifId = 65527; @@ -758,14 +764,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Writes the metadata profiles to the image. /// - /// The image. - /// The pixel format. - private void WriteProfiles(Image image) - where TPixel : struct, IPixel + /// The image meta data. + private void WriteProfiles(ImageMetaData metaData) { - image.MetaData.SyncProfiles(); - this.WriteExifProfile(image.MetaData.ExifProfile); - this.WriteIccProfile(image.MetaData.IccProfile); + if (metaData is null) + { + return; + } + + metaData.SyncProfiles(); + this.WriteExifProfile(metaData.ExifProfile); + this.WriteIccProfile(metaData.IccProfile); } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegFormat.cs b/src/ImageSharp/Formats/Jpeg/JpegFormat.cs index 94c0895b9d..6b23ceac7a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegFormat.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegFormat.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Registers the image encoders, decoders and mime type detectors for the jpeg format. /// - internal sealed class JpegFormat : IImageFormat + public sealed class JpegFormat : IImageFormat { private JpegFormat() { diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 05d687a888..f47a6518f0 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -51,12 +51,6 @@ namespace SixLabors.ImageSharp.Formats.Png /// public byte Threshold { get; set; } = 255; - /// - /// Gets or sets a value indicating whether this instance should write - /// gamma information to the stream. The default value is false. - /// - public bool WriteGamma { get; set; } - /// /// Encodes the image to the specified stream from the . /// diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 906e6d0003..0b47c1c63e 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -181,7 +181,8 @@ namespace SixLabors.ImageSharp.Formats.Png this.height = image.Height; // Always take the encoder options over the metadata values. - PngMetaData pngMetaData = image.MetaData.GetFormatMetaData(PngFormat.Instance); + ImageMetaData metaData = image.MetaData; + PngMetaData pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance); this.gamma = this.gamma ?? pngMetaData.Gamma; this.writeGamma = this.gamma > 0; this.pngColorType = this.pngColorType ?? pngMetaData.ColorType; @@ -245,9 +246,9 @@ namespace SixLabors.ImageSharp.Formats.Png this.WritePaletteChunk(stream, header, quantized); } - this.WritePhysicalChunk(stream, image); + this.WritePhysicalChunk(stream, metaData); this.WriteGammaChunk(stream); - this.WriteExifChunk(stream, image); + this.WriteExifChunk(stream, metaData); this.WriteDataChunks(image.Frames.RootFrame, quantizedPixelsSpan, stream); this.WriteEndChunk(stream); stream.Flush(); @@ -611,11 +612,9 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Writes the physical dimension information to the stream. /// - /// The pixel format. /// The containing image data. - /// The image. - private void WritePhysicalChunk(Stream stream, Image image) - where TPixel : struct, IPixel + /// The image meta data. + private void WritePhysicalChunk(Stream stream, ImageMetaData meta) { // The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. It contains: // Pixels per unit, X axis: 4 bytes (unsigned integer) @@ -627,7 +626,6 @@ namespace SixLabors.ImageSharp.Formats.Png // 1: unit is the meter // // When the unit specifier is 0, the pHYs chunk defines pixel aspect ratio only; the actual size of the pixels remains unspecified. - ImageMetaData meta = image.MetaData; Span hResolution = this.chunkDataBuffer.AsSpan(0, 4); Span vResolution = this.chunkDataBuffer.AsSpan(4, 4); @@ -668,16 +666,14 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Writes the eXIf chunk to the stream, if any EXIF Profile values are present in the meta data. /// - /// The pixel format. /// The containing image data. - /// The image. - private void WriteExifChunk(Stream stream, Image image) - where TPixel : struct, IPixel + /// The image meta data. + private void WriteExifChunk(Stream stream, ImageMetaData meta) { - if (image.MetaData.ExifProfile?.Values.Count > 0) + if (meta.ExifProfile?.Values.Count > 0) { - image.MetaData.SyncProfiles(); - this.WriteChunk(stream, PngChunkType.Exif, image.MetaData.ExifProfile.ToByteArray()); + meta.SyncProfiles(); + this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray()); } } diff --git a/src/ImageSharp/Formats/Png/PngFormat.cs b/src/ImageSharp/Formats/Png/PngFormat.cs index b5223cb5a8..210e2a837d 100644 --- a/src/ImageSharp/Formats/Png/PngFormat.cs +++ b/src/ImageSharp/Formats/Png/PngFormat.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Registers the image encoders, decoders and mime type detectors for the png format. /// - internal sealed class PngFormat : IImageFormat + public sealed class PngFormat : IImageFormat { private PngFormat() { diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index a3971fe9ce..132ab598e5 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -84,12 +84,11 @@ namespace SixLabors.ImageSharp Guard.NotNull(configuration, nameof(configuration)); Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - Guard.NotNull(metaData, nameof(metaData)); this.configuration = configuration; this.MemoryAllocator = configuration.MemoryAllocator; this.PixelBuffer = this.MemoryAllocator.Allocate2D(width, height); - this.MetaData = metaData; + this.MetaData = metaData ?? new ImageFrameMetaData(); this.Clear(configuration.GetParallelOptions(), backgroundColor); } @@ -201,10 +200,7 @@ namespace SixLabors.ImageSharp /// The y-coordinate of the pixel. Must be greater than or equal to zero and less than the height of the image. /// The at the specified position. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal ref TPixel GetPixelReference(int x, int y) - { - return ref this.PixelBuffer[x, y]; - } + internal ref TPixel GetPixelReference(int x, int y) => ref this.PixelBuffer[x, y]; /// /// Copies the pixels to a of the same size. @@ -249,10 +245,7 @@ namespace SixLabors.ImageSharp } /// - public override string ToString() - { - return $"ImageFrame<{typeof(TPixel).Name}>: {this.Width}x{this.Height}"; - } + public override string ToString() => $"ImageFrame<{typeof(TPixel).Name}>: {this.Width}x{this.Height}"; /// /// Returns a copy of the image frame in the given pixel format. @@ -309,15 +302,9 @@ namespace SixLabors.ImageSharp /// Clones the current instance. /// /// The - internal ImageFrame Clone() - { - return new ImageFrame(this.configuration, this); - } + internal ImageFrame Clone() => new ImageFrame(this.configuration, this); /// - void IDisposable.Dispose() - { - this.Dispose(); - } + void IDisposable.Dispose() => this.Dispose(); } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 4a17f867f3..c5c971962c 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -55,10 +55,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif [MemberData(nameof(RatioFiles))] public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) { - var options = new GifEncoder() - { - IgnoreMetadata = false - }; + var options = new GifEncoder(); var testFile = TestFile.Create(imagePath); using (Image input = testFile.CreateImage()) @@ -82,10 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif [Fact] public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten() { - var options = new GifEncoder() - { - IgnoreMetadata = false - }; + var options = new GifEncoder(); var testFile = TestFile.Create(TestImages.Gif.Rings); @@ -109,15 +103,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif [Fact] public void Encode_IgnoreMetadataIsTrue_CommentsAreNotWritten() { - var options = new GifEncoder() - { - IgnoreMetadata = true - }; + var options = new GifEncoder(); var testFile = TestFile.Create(TestImages.Gif.Rings); using (Image input = testFile.CreateImage()) { + input.MetaData.Properties.Clear(); using (var memStream = new MemoryStream()) { input.SaveAsGif(memStream, options); From 385b3fe68876a927670b88e468cb455cb45baf1a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 17 Sep 2018 23:02:02 +0100 Subject: [PATCH 13/14] Fix png quantization and reduce Wu memory pressure --- .../Common/Extensions/ComparableExtensions.cs | 11 -- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 4 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 19 ++-- .../Quantization/WuFrameQuantizer{TPixel}.cs | 101 +++++++++--------- 4 files changed, 61 insertions(+), 74 deletions(-) diff --git a/src/ImageSharp/Common/Extensions/ComparableExtensions.cs b/src/ImageSharp/Common/Extensions/ComparableExtensions.cs index d6dade7703..1b0f8ad095 100644 --- a/src/ImageSharp/Common/Extensions/ComparableExtensions.cs +++ b/src/ImageSharp/Common/Extensions/ComparableExtensions.cs @@ -137,17 +137,6 @@ namespace SixLabors.ImageSharp return value; } - /// - /// Converts an to a first restricting the value between the - /// minimum and maximum allowable ranges. - /// - /// The this method extends. - /// The - public static byte ToByte(this int value) - { - return (byte)value.Clamp(0, 255); - } - /// /// Converts an to a first restricting the value between the /// minimum and maximum allowable ranges. diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 7837c2da5c..9281927012 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -217,7 +217,7 @@ namespace SixLabors.ImageSharp.Formats.Png where TPixel : struct, IPixel { var metaData = new ImageMetaData(); - var pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance); + PngMetaData pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance); this.currentStream = stream; this.currentStream.Skip(8); Image image = null; @@ -307,7 +307,7 @@ namespace SixLabors.ImageSharp.Formats.Png public IImageInfo Identify(Stream stream) { var metaData = new ImageMetaData(); - var pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance); + PngMetaData pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance); this.currentStream = stream; this.currentStream.Skip(8); try diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 0b47c1c63e..e4d2fc510d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -243,7 +243,7 @@ namespace SixLabors.ImageSharp.Formats.Png // Collect the indexed pixel data if (quantized != null) { - this.WritePaletteChunk(stream, header, quantized); + this.WritePaletteChunk(stream, quantized); } this.WritePhysicalChunk(stream, metaData); @@ -555,30 +555,27 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// The pixel format. /// The containing image data. - /// The . /// The quantized frame. - private void WritePaletteChunk(Stream stream, in PngHeader header, QuantizedFrame quantized) + private void WritePaletteChunk(Stream stream, QuantizedFrame quantized) where TPixel : struct, IPixel { // Grab the palette and write it to the stream. TPixel[] palette = quantized.Palette; - byte pixelCount = palette.Length.ToByte(); - - // Get max colors for bit depth. - int colorTableLength = ImageMaths.GetColorCountForBitDepth(header.BitDepth) * 3; + int paletteLength = Math.Min(palette.Length, 256); + int colorTableLength = paletteLength * 3; Rgba32 rgba = default; bool anyAlpha = false; using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) - using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(pixelCount)) + using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength)) { Span colorTableSpan = colorTable.GetSpan(); Span alphaTableSpan = alphaTable.GetSpan(); Span quantizedSpan = quantized.GetPixelSpan(); - for (byte i = 0; i < pixelCount; i++) + for (int i = 0; i < paletteLength; i++) { - if (quantizedSpan.IndexOf(i) > -1) + if (quantizedSpan.IndexOf((byte)i) > -1) { int offset = i * 3; palette[i].ToRgba32(ref rgba); @@ -604,7 +601,7 @@ namespace SixLabors.ImageSharp.Formats.Png // Write the transparency data if (anyAlpha) { - this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaTable.Array, 0, pixelCount); + this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaTable.Array, 0, paletteLength); } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index d71221b9d6..5cd3d53ea2 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -46,12 +46,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The index bits. /// - private const int IndexBits = 6; + private const int IndexBits = 5; /// /// The index alpha bits. /// - private const int IndexAlphaBits = 3; + private const int IndexAlphaBits = 5; /// /// The index count. @@ -201,57 +201,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization return this.palette; } - /// - /// Quantizes the pixel - /// - /// The rgba used to quantize the pixel input - private void QuantizePixel(ref Rgba32 rgba) - { - // Add the color to a 3-D color histogram. - int r = rgba.R >> (8 - IndexBits); - int g = rgba.G >> (8 - IndexBits); - int b = rgba.B >> (8 - IndexBits); - int a = rgba.A >> (8 - IndexAlphaBits); - - int index = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1); - - Span vwtSpan = this.vwt.GetSpan(); - Span vmrSpan = this.vmr.GetSpan(); - Span vmgSpan = this.vmg.GetSpan(); - Span vmbSpan = this.vmb.GetSpan(); - Span vmaSpan = this.vma.GetSpan(); - Span m2Span = this.m2.GetSpan(); - - vwtSpan[index]++; - vmrSpan[index] += rgba.R; - vmgSpan[index] += rgba.G; - vmbSpan[index] += rgba.B; - vmaSpan[index] += rgba.A; - - var vector = new Vector4(rgba.R, rgba.G, rgba.B, rgba.A); - m2Span[index] += Vector4.Dot(vector, vector); - } - /// protected override void FirstPass(ImageFrame source, int width, int height) { - // Build up the 3-D color histogram - // Loop through each row - for (int y = 0; y < height; y++) - { - Span row = source.GetPixelRowSpan(y); - ref TPixel scanBaseRef = ref MemoryMarshal.GetReference(row); - - // And loop through each column - Rgba32 rgba = default; - for (int x = 0; x < width; x++) - { - ref TPixel pixel = ref Unsafe.Add(ref scanBaseRef, x); - pixel.ToRgba32(ref rgba); - this.QuantizePixel(ref rgba); - } - } - + this.Build3DHistogram(source, width, height); this.Get3DMoments(source.MemoryAllocator); this.BuildCube(); } @@ -466,6 +419,54 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } } + /// + /// Builds a 3-D color histogram of counts, r/g/b, c^2. + /// + /// The source data. + /// The width in pixels of the image. + /// The height in pixels of the image. + private void Build3DHistogram(ImageFrame source, int width, int height) + { + // Build up the 3-D color histogram + // Loop through each row + Span vwtSpan = this.vwt.GetSpan(); + Span vmrSpan = this.vmr.GetSpan(); + Span vmgSpan = this.vmg.GetSpan(); + Span vmbSpan = this.vmb.GetSpan(); + Span vmaSpan = this.vma.GetSpan(); + Span m2Span = this.m2.GetSpan(); + + for (int y = 0; y < height; y++) + { + Span row = source.GetPixelRowSpan(y); + ref TPixel scanBaseRef = ref MemoryMarshal.GetReference(row); + + // And loop through each column + Rgba32 rgba = default; + for (int x = 0; x < width; x++) + { + ref TPixel pixel = ref Unsafe.Add(ref scanBaseRef, x); + pixel.ToRgba32(ref rgba); + + int r = rgba.R >> (8 - IndexBits); + int g = rgba.G >> (8 - IndexBits); + int b = rgba.B >> (8 - IndexBits); + int a = rgba.A >> (8 - IndexAlphaBits); + + int index = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1); + + vwtSpan[index]++; + vmrSpan[index] += rgba.R; + vmgSpan[index] += rgba.G; + vmbSpan[index] += rgba.B; + vmaSpan[index] += rgba.A; + + var vector = new Vector4(rgba.R, rgba.G, rgba.B, rgba.A); + m2Span[index] += Vector4.Dot(vector, vector); + } + } + } + /// /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// From 6a73c5c1dffe536f03be39f1222b8ec921bcced7 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 17 Sep 2018 23:26:59 +0100 Subject: [PATCH 14/14] Minor optimizations to Wu --- .../Quantization/WuFrameQuantizer{TPixel}.cs | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 5cd3d53ea2..13bc057da8 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization where TPixel : struct, IPixel { // TODO: The WuFrameQuantizer code is rising several questions: - // - Do we really need to ALWAYS allocate the whole table of size TableLength? (~ 2471625 * sizeof(long) * 5 bytes ) + // - Do we really need to ALWAYS allocate the whole table of size TableLength? (~ 2471625 * sizeof(long) * 5 bytes ) JS. I'm afraid so. // - Isn't an AOS ("array of structures") layout more efficient & more readable than SOA ("structure of arrays") for this particular use case? // (T, R, G, B, A, M2) could be grouped together! // - It's a frequently used class, we need tests! (So we can optimize safely.) There are tests in the original!!! We should just adopt them! @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization private const int IndexBits = 5; /// - /// The index alpha bits. + /// The index alpha bits. Keep separate for now to allow easy adjustment. /// private const int IndexAlphaBits = 5; @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1; /// - /// The table length. + /// The table length. Now 1185921. /// private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; @@ -179,18 +179,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization if (this.palette is null) { this.palette = new TPixel[this.colors]; + Span vwtSpan = this.vwt.GetSpan(); + Span vmrSpan = this.vmr.GetSpan(); + Span vmgSpan = this.vmg.GetSpan(); + Span vmbSpan = this.vmb.GetSpan(); + Span vmaSpan = this.vma.GetSpan(); + for (int k = 0; k < this.colors; k++) { this.Mark(ref this.colorCube[k], (byte)k); - float weight = Volume(ref this.colorCube[k], this.vwt.GetSpan()); + float weight = Volume(ref this.colorCube[k], vwtSpan); if (MathF.Abs(weight) > Constants.Epsilon) { - float r = Volume(ref this.colorCube[k], this.vmr.GetSpan()); - float g = Volume(ref this.colorCube[k], this.vmg.GetSpan()); - float b = Volume(ref this.colorCube[k], this.vmb.GetSpan()); - float a = Volume(ref this.colorCube[k], this.vma.GetSpan()); + float r = Volume(ref this.colorCube[k], vmrSpan); + float g = Volume(ref this.colorCube[k], vmgSpan); + float b = Volume(ref this.colorCube[k], vmbSpan); + float a = Volume(ref this.colorCube[k], vmaSpan); ref TPixel color = ref this.palette[k]; color.PackFromScaledVector4(new Vector4(r, g, b, a) / weight / 255F); @@ -427,8 +433,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The height in pixels of the image. private void Build3DHistogram(ImageFrame source, int width, int height) { - // Build up the 3-D color histogram - // Loop through each row Span vwtSpan = this.vwt.GetSpan(); Span vmrSpan = this.vmr.GetSpan(); Span vmgSpan = this.vmg.GetSpan(); @@ -436,6 +440,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Span vmaSpan = this.vma.GetSpan(); Span m2Span = this.m2.GetSpan(); + // Build up the 3-D color histogram + // Loop through each row for (int y = 0; y < height; y++) { Span row = source.GetPixelRowSpan(y); @@ -632,22 +638,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The . private float Maximize(ref Box cube, int direction, int first, int last, out int cut, float wholeR, float wholeG, float wholeB, float wholeA, float wholeW) { - long baseR = Bottom(ref cube, direction, this.vmr.GetSpan()); - long baseG = Bottom(ref cube, direction, this.vmg.GetSpan()); - long baseB = Bottom(ref cube, direction, this.vmb.GetSpan()); - long baseA = Bottom(ref cube, direction, this.vma.GetSpan()); - long baseW = Bottom(ref cube, direction, this.vwt.GetSpan()); + Span vwtSpan = this.vwt.GetSpan(); + Span vmrSpan = this.vmr.GetSpan(); + Span vmgSpan = this.vmg.GetSpan(); + Span vmbSpan = this.vmb.GetSpan(); + Span vmaSpan = this.vma.GetSpan(); + + long baseR = Bottom(ref cube, direction, vmrSpan); + long baseG = Bottom(ref cube, direction, vmgSpan); + long baseB = Bottom(ref cube, direction, vmbSpan); + long baseA = Bottom(ref cube, direction, vmaSpan); + long baseW = Bottom(ref cube, direction, vwtSpan); float max = 0F; cut = -1; for (int i = first; i < last; i++) { - float halfR = baseR + Top(ref cube, direction, i, this.vmr.GetSpan()); - float halfG = baseG + Top(ref cube, direction, i, this.vmg.GetSpan()); - float halfB = baseB + Top(ref cube, direction, i, this.vmb.GetSpan()); - float halfA = baseA + Top(ref cube, direction, i, this.vma.GetSpan()); - float halfW = baseW + Top(ref cube, direction, i, this.vwt.GetSpan()); + float halfR = baseR + Top(ref cube, direction, i, vmrSpan); + float halfG = baseG + Top(ref cube, direction, i, vmgSpan); + float halfB = baseB + Top(ref cube, direction, i, vmbSpan); + float halfA = baseA + Top(ref cube, direction, i, vmaSpan); + float halfW = baseW + Top(ref cube, direction, i, vwtSpan); if (MathF.Abs(halfW) < Constants.Epsilon) {