From f1d9188253ff9f83fac9a1fd371309d3109169ea Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Mon, 7 Jun 2021 08:22:37 +0300 Subject: [PATCH 01/78] Implement encoding multi-frame tiff images 111 --- .../Common/Helpers/UnitConverter.cs | 28 ++- .../Formats/Tiff/TiffEncoderCore.cs | 66 ++++++-- .../Tiff/TiffEncoderEntriesCollector.cs | 160 ++++++++++-------- .../Formats/Tiff/Writers/TiffStreamWriter.cs | 2 - 4 files changed, 152 insertions(+), 104 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/UnitConverter.cs b/src/ImageSharp/Common/Helpers/UnitConverter.cs index efc0e0e15..03d50c025 100644 --- a/src/ImageSharp/Common/Helpers/UnitConverter.cs +++ b/src/ImageSharp/Common/Helpers/UnitConverter.cs @@ -100,12 +100,11 @@ namespace SixLabors.ImageSharp.Common.Helpers /// /// Sets the exif profile resolution values. /// - /// The exif profile. /// The resolution unit. /// The horizontal resolution value. /// The vertical resolution value. [MethodImpl(InliningOptions.ShortMethod)] - public static void SetResolutionValues(ExifProfile exifProfile, PixelResolutionUnit unit, double horizontal, double vertical) + public static (ushort, double?, double?) AdjustToExif(PixelResolutionUnit unit, double horizontal, double vertical) { switch (unit) { @@ -114,30 +113,25 @@ namespace SixLabors.ImageSharp.Common.Helpers case PixelResolutionUnit.PixelsPerCentimeter: break; case PixelResolutionUnit.PixelsPerMeter: - { - unit = PixelResolutionUnit.PixelsPerCentimeter; - horizontal = UnitConverter.MeterToCm(horizontal); - vertical = UnitConverter.MeterToCm(vertical); - } + { + unit = PixelResolutionUnit.PixelsPerCentimeter; + horizontal = MeterToCm(horizontal); + vertical = MeterToCm(vertical); + } - break; + break; default: unit = PixelResolutionUnit.PixelsPerInch; break; } - exifProfile.SetValue(ExifTag.ResolutionUnit, (ushort)(unit + 1)); - + ushort exifUnit = (ushort)(unit + 1); if (unit == PixelResolutionUnit.AspectRatio) { - exifProfile.RemoveValue(ExifTag.XResolution); - exifProfile.RemoveValue(ExifTag.YResolution); - } - else - { - exifProfile.SetValue(ExifTag.XResolution, new Rational(horizontal)); - exifProfile.SetValue(ExifTag.YResolution, new Rational(vertical)); + return (exifUnit, null, null); } + + return (exifUnit, horizontal, vertical); } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 2273d759f..9e7284aca 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -74,6 +74,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// private const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + private readonly List<(long, uint)> frameMarkers = new List<(long, uint)>(); + /// /// Initializes a new instance of the class. /// @@ -147,13 +149,30 @@ namespace SixLabors.ImageSharp.Formats.Tiff // Make sure, the Encoder options makes sense in combination with each other. this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor); - using (var writer = new TiffStreamWriter(stream)) + using var writer = new TiffStreamWriter(stream); + long ifdMarker = this.WriteHeader(writer); + + Image metadataImage = image; + foreach (ImageFrame frame in image.Frames) { - long firstIfdMarker = this.WriteHeader(writer); + var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile.GetValue(ExifTag.SubfileType)?.Value ?? 0); + + if (subfileType != TiffNewSubfileType.FullImage) + { + continue; + } - // TODO: multiframing is not supported - this.WriteImage(writer, image, firstIfdMarker); + ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker); + metadataImage = null; } + + long currentOffset = writer.BaseStream.Position; + foreach ((long, uint) marker in this.frameMarkers) + { + writer.WriteMarker(marker.Item1, marker.Item2); + } + + writer.BaseStream.Seek(currentOffset, SeekOrigin.Begin); } /// @@ -174,41 +193,56 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// Writes all data required to define an image. /// /// The pixel format. - /// The to write data to. - /// The to encode from. + /// The to write data to. + /// The tiff frame. + /// The image metadata (resolution values for each frame). + /// The image (common metadata for root frame). /// The marker to write this IFD offset. - private void WriteImage(TiffStreamWriter writer, Image image, long ifdOffset) + /// + /// The next IFD offset value. + /// + private long WriteFrame( + TiffStreamWriter writer, + ImageFrame frame, + ImageMetadata imageMetadata, + Image image, + long ifdOffset) where TPixel : unmanaged, IPixel { - var entriesCollector = new TiffEncoderEntriesCollector(); - using TiffBaseCompressor compressor = TiffCompressorFactory.Create( this.CompressionType ?? TiffCompression.None, writer.BaseStream, this.memoryAllocator, - image.Width, + frame.Width, (int)this.BitsPerPixel, this.compressionLevel, this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); + var entriesCollector = new TiffEncoderEntriesCollector(); using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create( this.PhotometricInterpretation, - image.Frames.RootFrame, + frame, this.quantizer, this.memoryAllocator, this.configuration, entriesCollector, (int)this.BitsPerPixel); - int rowsPerStrip = this.CalcRowsPerStrip(image.Frames.RootFrame.Height, colorWriter.BytesPerRow); + int rowsPerStrip = this.CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow); colorWriter.Write(compressor, rowsPerStrip); + if (image != null) + { + entriesCollector.ProcessMetadata(image); + } + + entriesCollector.ProcessFrameInfo(frame, imageMetadata); entriesCollector.ProcessImageFormat(this); - entriesCollector.ProcessGeneral(image); - writer.WriteMarker(ifdOffset, (uint)writer.Position); - long nextIfdMarker = this.WriteIfd(writer, entriesCollector.Entries); + this.frameMarkers.Add((ifdOffset, (uint)writer.Position)); + + return this.WriteIfd(writer, entriesCollector.Entries); } /// @@ -272,7 +306,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff } else { - var raw = new byte[length]; + byte[] raw = new byte[length]; int sz = ExifWriter.WriteValue(entry, raw, 0); DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written"); largeDataBlocks.Add(raw); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 43a086849..b00dcc967 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -6,7 +6,6 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff { @@ -16,9 +15,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff public List Entries { get; } = new List(); - public void ProcessGeneral(Image image) - where TPixel : unmanaged, IPixel - => new GeneralProcessor(this).Process(image); + public void ProcessMetadata(Image image) + => new MetadataProcessor(this).Process(image); + + public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) + => new FrameInfoProcessor(this).Process(frame, imageMetadata); public void ProcessImageFormat(TiffEncoderCore encoder) => new ImageFormatProcessor(this).Process(encoder); @@ -38,44 +39,35 @@ namespace SixLabors.ImageSharp.Formats.Tiff private void Add(IExifValue entry) => this.Entries.Add(entry); - private class GeneralProcessor + private abstract class BaseProcessor { - private readonly TiffEncoderEntriesCollector collector; + public BaseProcessor(TiffEncoderEntriesCollector collector) => this.Collector = collector; - public GeneralProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; + protected TiffEncoderEntriesCollector Collector { get; } + } - public void Process(Image image) - where TPixel : unmanaged, IPixel + private class MetadataProcessor : BaseProcessor + { + public MetadataProcessor(TiffEncoderEntriesCollector collector) + : base(collector) { - ImageFrame rootFrame = image.Frames.RootFrame; - ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); - byte[] rootFrameXmpBytes = rootFrame.Metadata.XmpProfile; - - var width = new ExifLong(ExifTagValue.ImageWidth) - { - Value = (uint)image.Width - }; - - var height = new ExifLong(ExifTagValue.ImageLength) - { - Value = (uint)image.Height - }; - - var software = new ExifString(ExifTagValue.Software) - { - Value = SoftwareValue - }; + } - this.collector.AddOrReplace(width); - this.collector.AddOrReplace(height); + public void Process(Image image) + { + ImageFrame rootFrame = image.Frames.RootFrame; + ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); + byte[] foorFrameXmpBytes = rootFrame.Metadata.XmpProfile; - this.ProcessResolution(image.Metadata, rootFrameExifProfile); - this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpBytes); + this.ProcessProfiles(image.Metadata, rootFrameExifProfile, foorFrameXmpBytes); this.ProcessMetadata(rootFrameExifProfile); - if (!this.collector.Entries.Exists(t => t.Tag == ExifTag.Software)) + if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) { - this.collector.Add(software); + this.Collector.Add(new ExifString(ExifTagValue.Software) + { + Value = SoftwareValue + }); } } @@ -114,26 +106,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - private void ProcessResolution(ImageMetadata imageMetadata, ExifProfile exifProfile) - { - UnitConverter.SetResolutionValues( - exifProfile, - imageMetadata.ResolutionUnits, - imageMetadata.HorizontalResolution, - imageMetadata.VerticalResolution); - - this.collector.Add(exifProfile.GetValue(ExifTag.ResolutionUnit).DeepClone()); - - IExifValue xResolution = exifProfile.GetValue(ExifTag.XResolution)?.DeepClone(); - IExifValue yResolution = exifProfile.GetValue(ExifTag.YResolution)?.DeepClone(); - - if (xResolution != null && yResolution != null) - { - this.collector.Add(xResolution); - this.collector.Add(yResolution); - } - } - private void ProcessMetadata(ExifProfile exifProfile) { foreach (IExifValue entry in exifProfile.Values) @@ -170,9 +142,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } - if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag)) + if (!this.Collector.Entries.Exists(t => t.Tag == entry.Tag)) { - this.collector.AddOrReplace(entry.DeepClone()); + this.Collector.AddOrReplace(entry.DeepClone()); } } } @@ -183,12 +155,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff { foreach (IExifValue entry in exifProfile.Values) { - if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null) + if (!this.Collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null) { ExifParts entryPart = ExifTags.GetPart(entry.Tag); if (entryPart != ExifParts.None && exifProfile.Parts.HasFlag(entryPart)) { - this.collector.AddOrReplace(entry.DeepClone()); + this.Collector.AddOrReplace(entry.DeepClone()); } } } @@ -206,7 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = imageMetadata.IptcProfile.Data }; - this.collector.Add(iptc); + this.Collector.Add(iptc); } else { @@ -220,7 +192,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = imageMetadata.IccProfile.ToByteArray() }; - this.collector.Add(icc); + this.Collector.Add(icc); } else { @@ -234,7 +206,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = xmpProfile }; - this.collector.Add(xmp); + this.Collector.Add(xmp); } else { @@ -243,11 +215,61 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - private class ImageFormatProcessor + private class FrameInfoProcessor : BaseProcessor { - private readonly TiffEncoderEntriesCollector collector; + public FrameInfoProcessor(TiffEncoderEntriesCollector collector) + : base(collector) + { + } + + public void Process(ImageFrame frame, ImageMetadata imageMetadata) + { + this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth) + { + Value = (uint)frame.Width + }); + + this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength) + { + Value = (uint)frame.Height + }); + + this.ProcessResolution(imageMetadata); + } + + private void ProcessResolution(ImageMetadata imageMetadata) + { + (ushort, double?, double?) exifValues = UnitConverter.AdjustToExif( + imageMetadata.ResolutionUnits, + imageMetadata.HorizontalResolution, + imageMetadata.VerticalResolution); - public ImageFormatProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; + this.Collector.Add(new ExifShort(ExifTagValue.ResolutionUnit) + { + Value = exifValues.Item1 + }); + + if (exifValues.Item2 != null && exifValues.Item3 != null) + { + this.Collector.Add(new ExifRational(ExifTagValue.XResolution) + { + Value = new Rational(exifValues.Item2.Value) + }); + + this.Collector.Add(new ExifRational(ExifTagValue.YResolution) + { + Value = new Rational(exifValues.Item3.Value) + }); + } + } + } + + private class ImageFormatProcessor : BaseProcessor + { + public ImageFormatProcessor(TiffEncoderEntriesCollector collector) + : base(collector) + { + } public void Process(TiffEncoderCore encoder) { @@ -278,11 +300,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = (ushort)encoder.PhotometricInterpretation }; - this.collector.AddOrReplace(planarConfig); - this.collector.AddOrReplace(samplesPerPixel); - this.collector.AddOrReplace(bitPerSample); - this.collector.AddOrReplace(compression); - this.collector.AddOrReplace(photometricInterpretation); + this.Collector.AddOrReplace(planarConfig); + this.Collector.AddOrReplace(samplesPerPixel); + this.Collector.AddOrReplace(bitPerSample); + this.Collector.AddOrReplace(compression); + this.Collector.AddOrReplace(photometricInterpretation); if (encoder.HorizontalPredictor == TiffPredictor.Horizontal) { @@ -292,7 +314,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff { var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; - this.collector.AddOrReplace(predictor); + this.Collector.AddOrReplace(predictor); } } } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs index 05a1ca7a2..8c83f41cc 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs @@ -126,10 +126,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers /// The four-byte unsigned integer to write. public void WriteMarker(long offset, uint value) { - long currentOffset = this.BaseStream.Position; this.BaseStream.Seek(offset, SeekOrigin.Begin); this.Write(value); - this.BaseStream.Seek(currentOffset, SeekOrigin.Begin); } /// From 4c1e7182bf29f75c200899caa9c44cc33ad885fa Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 26 Jun 2021 14:30:05 +0300 Subject: [PATCH 02/78] Add multi-frame tests --- .../Formats/Tiff/TiffEncoderBaseTester.cs | 114 ++++++++++++++++++ .../Tiff/TiffEncoderMultiframeTests.cs | 30 +++++ .../Formats/Tiff/TiffEncoderTests.cs | 100 +-------------- 3 files changed, 145 insertions(+), 99 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs create mode 100644 tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs new file mode 100644 index 000000000..cdbecf124 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public abstract class TiffEncoderBaseTester + { + private static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); + + protected static void TestStripLength( + TestImageProvider provider, + TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression, + bool useExactComparer = true, + float compareTolerance = 0.01f) + where TPixel : unmanaged, IPixel + { + // arrange + var tiffEncoder = new TiffEncoder() { PhotometricInterpretation = photometricInterpretation, Compression = compression }; + using Image input = provider.GetImage(); + using var memStream = new MemoryStream(); + TiffFrameMetadata inputMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata(); + TiffCompression inputCompression = inputMeta.Compression ?? TiffCompression.None; + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(memStream); + ExifProfile exifProfileOutput = output.Frames.RootFrame.Metadata.ExifProfile; + TiffFrameMetadata outputMeta = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + ImageFrame rootFrame = output.Frames.RootFrame; + + Number rowsPerStrip = exifProfileOutput.GetValue(ExifTag.RowsPerStrip) != null ? exifProfileOutput.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; + Assert.True(output.Height > (int)rowsPerStrip); + Assert.True(exifProfileOutput.GetValue(ExifTag.StripOffsets)?.Value.Length > 1); + Number[] stripByteCounts = exifProfileOutput.GetValue(ExifTag.StripByteCounts)?.Value; + Assert.NotNull(stripByteCounts); + Assert.True(stripByteCounts.Length > 1); + Assert.NotNull(outputMeta.BitsPerPixel); + + foreach (Number sz in stripByteCounts) + { + Assert.True((uint)sz <= TiffConstants.DefaultStripSize); + } + + // For uncompressed more accurate test. + if (compression == TiffCompression.None) + { + for (int i = 0; i < stripByteCounts.Length - 1; i++) + { + // The difference must be less than one row. + int stripBytes = (int)stripByteCounts[i]; + int widthBytes = ((int)outputMeta.BitsPerPixel + 7) / 8 * rootFrame.Width; + + Assert.True((TiffConstants.DefaultStripSize - stripBytes) < widthBytes); + } + } + + // Compare with reference. + TestTiffEncoderCore( + provider, + inputMeta.BitsPerPixel, + photometricInterpretation, + inputCompression, + useExactComparer: useExactComparer, + compareTolerance: compareTolerance); + } + + protected static void TestTiffEncoderCore( + TestImageProvider provider, + TiffBitsPerPixel? bitsPerPixel, + TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression = TiffCompression.None, + TiffPredictor predictor = TiffPredictor.None, + bool useExactComparer = true, + float compareTolerance = 0.001f, + IImageDecoder imageDecoder = null) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + var encoder = new TiffEncoder + { + PhotometricInterpretation = photometricInterpretation, + BitsPerPixel = bitsPerPixel, + Compression = compression, + HorizontalPredictor = predictor + }; + + // Does DebugSave & load reference CompareToReferenceInput(): + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), + referenceDecoder: imageDecoder ?? ReferenceDecoder); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs new file mode 100644 index 000000000..7f2acc2e1 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +using static SixLabors.ImageSharp.Tests.TestImages.Tiff; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public class TiffEncoderMultiframeTests : TiffEncoderBaseTester + { + [Theory] + [WithFile(MultiframeLzwPredictor, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); + + [Theory] + [WithFile(MultiframeDeflateWithPreview, PixelTypes.Rgba32)] + [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] + [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_NotSupport(TestImageProvider provider) + where TPixel : unmanaged, IPixel => Assert.Throws(() => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 09505692f..95013088e 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -2,14 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System.IO; - -using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Constants; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit; @@ -18,10 +13,8 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff { [Trait("Format", "Tiff")] - public class TiffEncoderTests + public class TiffEncoderTests : TiffEncoderBaseTester { - private static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); - [Theory] [InlineData(null, TiffBitsPerPixel.Bit24)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffBitsPerPixel.Bit24)] @@ -450,96 +443,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff var encoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation }; image.DebugSave(provider, encoder); } - - private static void TestStripLength( - TestImageProvider provider, - TiffPhotometricInterpretation photometricInterpretation, - TiffCompression compression, - bool useExactComparer = true, - float compareTolerance = 0.01f) - where TPixel : unmanaged, IPixel - { - // arrange - var tiffEncoder = new TiffEncoder() { PhotometricInterpretation = photometricInterpretation, Compression = compression }; - using Image input = provider.GetImage(); - using var memStream = new MemoryStream(); - TiffFrameMetadata inputMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata(); - TiffCompression inputCompression = inputMeta.Compression ?? TiffCompression.None; - - // act - input.Save(memStream, tiffEncoder); - - // assert - memStream.Position = 0; - using var output = Image.Load(memStream); - ExifProfile exifProfileOutput = output.Frames.RootFrame.Metadata.ExifProfile; - TiffFrameMetadata outputMeta = output.Frames.RootFrame.Metadata.GetTiffMetadata(); - ImageFrame rootFrame = output.Frames.RootFrame; - - Number rowsPerStrip = exifProfileOutput.GetValue(ExifTag.RowsPerStrip) != null ? exifProfileOutput.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; - Assert.True(output.Height > (int)rowsPerStrip); - Assert.True(exifProfileOutput.GetValue(ExifTag.StripOffsets)?.Value.Length > 1); - Number[] stripByteCounts = exifProfileOutput.GetValue(ExifTag.StripByteCounts)?.Value; - Assert.NotNull(stripByteCounts); - Assert.True(stripByteCounts.Length > 1); - Assert.NotNull(outputMeta.BitsPerPixel); - - foreach (Number sz in stripByteCounts) - { - Assert.True((uint)sz <= TiffConstants.DefaultStripSize); - } - - // For uncompressed more accurate test. - if (compression == TiffCompression.None) - { - for (int i = 0; i < stripByteCounts.Length - 1; i++) - { - // The difference must be less than one row. - int stripBytes = (int)stripByteCounts[i]; - int widthBytes = ((int)outputMeta.BitsPerPixel + 7) / 8 * rootFrame.Width; - - Assert.True((TiffConstants.DefaultStripSize - stripBytes) < widthBytes); - } - } - - // Compare with reference. - TestTiffEncoderCore( - provider, - inputMeta.BitsPerPixel, - photometricInterpretation, - inputCompression, - useExactComparer: useExactComparer, - compareTolerance: compareTolerance); - } - - private static void TestTiffEncoderCore( - TestImageProvider provider, - TiffBitsPerPixel? bitsPerPixel, - TiffPhotometricInterpretation photometricInterpretation, - TiffCompression compression = TiffCompression.None, - TiffPredictor predictor = TiffPredictor.None, - bool useExactComparer = true, - float compareTolerance = 0.001f, - IImageDecoder imageDecoder = null) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(); - var encoder = new TiffEncoder - { - PhotometricInterpretation = photometricInterpretation, - BitsPerPixel = bitsPerPixel, - Compression = compression, - HorizontalPredictor = predictor - }; - - // Does DebugSave & load reference CompareToReferenceInput(): - image.VerifyEncoder( - provider, - "tiff", - bitsPerPixel, - encoder, - useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), - referenceDecoder: imageDecoder ?? ReferenceDecoder); - } } } From c661fd4bd805fc3520c885f9e02aa34eb8196d83 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 3 Jul 2021 12:10:11 +0300 Subject: [PATCH 03/78] Add test --- .../Formats/Tiff/TiffEncoderCore.cs | 2 +- .../Formats/Tiff/TiffEncoderBaseTester.cs | 2 +- .../Tiff/TiffEncoderMultiframeTests.cs | 37 ++++++++++++++++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 9e7284aca..3e785f612 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -155,7 +155,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Image metadataImage = image; foreach (ImageFrame frame in image.Frames) { - var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile.GetValue(ExifTag.SubfileType)?.Value ?? 0); + var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile?.GetValue(ExifTag.SubfileType)?.Value ?? (int)TiffNewSubfileType.FullImage); if (subfileType != TiffNewSubfileType.FullImage) { diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs index cdbecf124..71d366369 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Trait("Format", "Tiff")] public abstract class TiffEncoderBaseTester { - private static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); + protected static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); protected static void TestStripLength( TestImageProvider provider, diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs index 7f2acc2e1..c9f670ea3 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs @@ -2,10 +2,12 @@ // Licensed under the Apache License, Version 2.0. using System; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.PixelFormats; - +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; using static SixLabors.ImageSharp.Tests.TestImages.Tiff; @@ -13,6 +15,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff { [Trait("Format", "Tiff")] + [Trait("Format", "Tiff.m")] public class TiffEncoderMultiframeTests : TiffEncoderBaseTester { [Theory] @@ -21,10 +24,40 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); [Theory] - [WithFile(MultiframeDeflateWithPreview, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] public void TiffEncoder_EncodeMultiframe_NotSupport(TestImageProvider provider) where TPixel : unmanaged, IPixel => Assert.Throws(() => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb)); + + [Theory] + [WithFile(MultiframeDeflateWithPreview, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_WithoutPreview_ProblemTest(TestImageProvider provider) + where TPixel : unmanaged, IPixel => Assert.Throws(() => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb)); + + [Theory] + [WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_Create(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using var image = provider.GetImage(); + using var image2 = new Image(image.Width, image.Height, Color.Green.ToRgba32()); + + image.Frames.AddFrame(image2.Frames.RootFrame); + + TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Bit24; + var encoder = new TiffEncoder + { + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + BitsPerPixel = bitsPerPixel, + Compression = TiffCompression.Deflate + }; + + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + ImageComparer.Exact); + } } } From de8ef67d6ab41d84122a2595d97d1ee92cc9a94f Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 3 Jul 2021 12:28:37 +0300 Subject: [PATCH 04/78] Build fix --- src/ImageSharp/Common/Helpers/UnitConverter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/UnitConverter.cs b/src/ImageSharp/Common/Helpers/UnitConverter.cs index 03d50c025..6bb9460e3 100644 --- a/src/ImageSharp/Common/Helpers/UnitConverter.cs +++ b/src/ImageSharp/Common/Helpers/UnitConverter.cs @@ -113,13 +113,13 @@ namespace SixLabors.ImageSharp.Common.Helpers case PixelResolutionUnit.PixelsPerCentimeter: break; case PixelResolutionUnit.PixelsPerMeter: - { + { unit = PixelResolutionUnit.PixelsPerCentimeter; horizontal = MeterToCm(horizontal); vertical = MeterToCm(vertical); - } + } - break; + break; default: unit = PixelResolutionUnit.PixelsPerInch; break; From 02990ac59ac7e341a98997e14e8148784d5c6391 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 3 Jul 2021 12:37:31 +0300 Subject: [PATCH 05/78] Cleanup --- .../Formats/Tiff/TiffEncoderMultiframeTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs index c9f670ea3..403160772 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs @@ -2,11 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -15,7 +13,6 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff { [Trait("Format", "Tiff")] - [Trait("Format", "Tiff.m")] public class TiffEncoderMultiframeTests : TiffEncoderBaseTester { [Theory] From 3fe4e7f96115e8b26115fba600d1ae27021e3850 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 3 Jul 2021 13:23:33 +0300 Subject: [PATCH 06/78] Test fixes --- src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs | 2 +- src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 3e785f612..b87b927a9 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -169,7 +169,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff long currentOffset = writer.BaseStream.Position; foreach ((long, uint) marker in this.frameMarkers) { - writer.WriteMarker(marker.Item1, marker.Item2); + writer.WriteMarkerFast(marker.Item1, marker.Item2); } writer.BaseStream.Seek(currentOffset, SeekOrigin.Begin); diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs index 8c83f41cc..138274d3f 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs @@ -125,6 +125,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers /// The offset returned when placing the marker /// The four-byte unsigned integer to write. public void WriteMarker(long offset, uint value) + { + long back = this.BaseStream.Position; + this.BaseStream.Seek(offset, SeekOrigin.Begin); + this.Write(value); + this.BaseStream.Seek(back, SeekOrigin.Begin); + } + + public void WriteMarkerFast(long offset, uint value) { this.BaseStream.Seek(offset, SeekOrigin.Begin); this.Write(value); From aeeb7db0bcdf8c5c203c40f64c7768521d2660ef Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 3 Jul 2021 13:31:32 +0300 Subject: [PATCH 07/78] Double tags fixing --- src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index b00dcc967..1b042eec0 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -244,19 +244,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff imageMetadata.HorizontalResolution, imageMetadata.VerticalResolution); - this.Collector.Add(new ExifShort(ExifTagValue.ResolutionUnit) + this.Collector.AddOrReplace(new ExifShort(ExifTagValue.ResolutionUnit) { Value = exifValues.Item1 }); if (exifValues.Item2 != null && exifValues.Item3 != null) { - this.Collector.Add(new ExifRational(ExifTagValue.XResolution) + this.Collector.AddOrReplace(new ExifRational(ExifTagValue.XResolution) { Value = new Rational(exifValues.Item2.Value) }); - this.Collector.Add(new ExifRational(ExifTagValue.YResolution) + this.Collector.AddOrReplace(new ExifRational(ExifTagValue.YResolution) { Value = new Rational(exifValues.Item3.Value) }); From aaaf1242c6a985a49c9e85a57f2ec5a6e20f1891 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 3 Jul 2021 13:44:52 +0300 Subject: [PATCH 08/78] Resolution test fixes --- tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index c80d9fc16..f9535607c 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -278,8 +278,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff PixelResolutionUnit resolutionUnitInput = UnitConverter.ExifProfileToResolutionUnit(exifProfileInput); PixelResolutionUnit resolutionUnitEncoded = UnitConverter.ExifProfileToResolutionUnit(encodedImageExifProfile); Assert.Equal(resolutionUnitInput, resolutionUnitEncoded); - Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution), encodedImageExifProfile.GetValue(ExifTag.XResolution)); - Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution), encodedImageExifProfile.GetValue(ExifTag.YResolution)); + Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble()); + Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble()); Assert.Equal(xmpProfileInput, encodedImageXmpProfile); From b16301b631520a778f2af29acfbeeeebad177bcc Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 10:19:09 +0300 Subject: [PATCH 09/78] Simplified quantization table scaling in the encoder --- src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 135048aa4..60c209c52 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -702,19 +702,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg for (int j = 0; j < Block8x8F.Size; j++) { - int x = unscaledQuant[j]; - x = ((x * scale) + 50) / 100; - if (x < 1) - { - x = 1; - } - - if (x > 255) - { - x = 255; - } - - quant[j] = x; + int scaled = ((unscaledQuant[j] * scale) + 50) / 100; + quant[j] = Math.Clamp(scaled, 1, 255); } } } From 51e13667909bdd8dc0ad89ecb28ec257495e588e Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 10:19:22 +0300 Subject: [PATCH 10/78] Added debug guard to the estimate quality --- .../Formats/Jpeg/Components/Decoder/QualityEvaluator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs index 938459b88..8c014ecda 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder @@ -78,6 +78,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// The . public static int EstimateQuality(Block8x8F[] quantizationTables) { + DebugGuard.MustBeGreaterThan(quantizationTables.Length, 2, nameof(quantizationTables)); + int quality = 75; float sum = 0; @@ -141,4 +143,4 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder return quality; } } -} \ No newline at end of file +} From 1d781da19326ef775a000ebae05663c6d4b981a2 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 12:45:18 +0300 Subject: [PATCH 11/78] Added comments to DQT marker parser, DQT exceptions now provide better info messages --- .../Jpeg/Components/Decoder/JpegComponent.cs | 2 +- .../Formats/Jpeg/JpegDecoderCore.cs | 45 +++++++++---------- .../Formats/Jpeg/JpegThrowHelper.cs | 5 ++- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs index 614e96e54..95223c444 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder if (quantizationTableIndex > 3) { - JpegThrowHelper.ThrowBadQuantizationTable(); + JpegThrowHelper.ThrowBadQuantizationTableIndex(quantizationTableIndex); } this.QuantizationTableIndex = quantizationTableIndex; diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 922e9797c..3896aa293 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -755,26 +755,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { while (remaining > 0) { - bool done = false; - remaining--; + // 1 byte: quantization table spec + // bit 0..3: table index (0..3) + // bit 4..7: table precision (0 = 8 bit, 1 = 16 bit) int quantizationTableSpec = stream.ReadByte(); int tableIndex = quantizationTableSpec & 15; + int tablePrecision = quantizationTableSpec >> 4; - // Max index. 4 Tables max. + // Validate: if (tableIndex > 3) { - JpegThrowHelper.ThrowBadQuantizationTable(); + JpegThrowHelper.ThrowBadQuantizationTableIndex(tableIndex); } - switch (quantizationTableSpec >> 4) + remaining--; + switch (tablePrecision) { + // 8 bit values case 0: { - // 8 bit values + // Validate: 8 bit table needs exactly 64 bytes if (remaining < 64) { - done = true; - break; + JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); } stream.Read(this.temp, 0, 64); @@ -785,16 +788,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { table[j] = this.temp[j]; } + + break; } - break; + // 16 bit values case 1: { - // 16 bit values + // Validate: 16 bit table needs exactly 128 bytes if (remaining < 128) { - done = true; - break; + JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); } stream.Read(this.temp, 0, 128); @@ -805,26 +809,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { table[j] = (this.temp[2 * j] << 8) | this.temp[(2 * j) + 1]; } - } - break; + break; + } + // Unknown precision - error default: { - JpegThrowHelper.ThrowBadQuantizationTable(); + JpegThrowHelper.ThrowBadQuantizationTablePrecision(tablePrecision); break; } } - - if (done) - { - break; - } - } - - if (remaining != 0) - { - JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DQT), remaining); } this.Metadata.GetFormatMetadata(JpegFormat.Instance).Quality = QualityEvaluator.EstimateQuality(this.QuantizationTables); diff --git a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs index cc75870e1..1b5362275 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs @@ -36,7 +36,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public static void ThrowBadMarker(string marker, int length) => throw new InvalidImageContentException($"Marker {marker} has bad length {length}."); [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowBadQuantizationTable() => throw new InvalidImageContentException("Bad Quantization Table index."); + public static void ThrowBadQuantizationTableIndex(int index) => throw new InvalidImageContentException($"Bad Quantization Table index {index}."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowBadQuantizationTablePrecision(int precision) => throw new InvalidImageContentException($"Unknown Quantization Table precision {precision}."); [MethodImpl(InliningOptions.ColdPath)] public static void ThrowBadSampling() => throw new InvalidImageContentException("Bad sampling factor."); From 15d77ddf26a1adb90ef19f0d84413c2145ae289f Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 17:42:11 +0300 Subject: [PATCH 12/78] EstimateQuality now return if given tables are standard --- .../Components/Decoder/QualityEvaluator.cs | 24 ++++++++++++------- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs index 8c014ecda..e9d67d1ba 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs @@ -72,15 +72,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder }; /// - /// Returns an estimated quality of the image based on the quantization tables. + /// Returns a jpeg quality parameter based on the quantization tables. /// /// The quantization tables. - /// The . - public static int EstimateQuality(Block8x8F[] quantizationTables) + /// Jpeg quality parameter + /// indicating if given quantization tables are equal to standard ITU spec. + public static bool EstimateQuality(Block8x8F[] quantizationTables, out int quality) { DebugGuard.MustBeGreaterThan(quantizationTables.Length, 2, nameof(quantizationTables)); - int quality = 75; + quality = 75; + float sum = 0; for (int i = 0; i < quantizationTables.Length; i++) @@ -115,9 +117,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder continue; } - if (((quality <= Hash[i]) && (sum <= Sums[i])) || (i >= 50)) + bool sumHashCondition = (quality <= Hash[i]) && (sum <= Sums[i]); + if (sumHashCondition || (i >= 50)) { - return i + 1; + quality = i + 1; + return sumHashCondition; } } } @@ -132,15 +136,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder continue; } - if (((quality <= Hash1[i]) && (sum <= Sums1[i])) || (i >= 50)) + bool sumHashCondition = (quality <= Hash1[i]) && (sum <= Sums1[i]); + if (sumHashCondition || (i >= 50)) { - return i + 1; + quality = i + 1; + return sumHashCondition; } } } } - return quality; + return false; } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 8131f74d2..b95dd5644 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// index must be between 0 and 100 (compression from max to min). /// Defaults to 75. /// - public int? Quality { get; set; } + public int? Quality { get; set; } = 75; /// /// Gets or sets the subsample ration, that will be used to encode the image. From 664d7f366f6eafda5f6c590ab316c15430e7e21a Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 22:47:08 +0300 Subject: [PATCH 13/78] Initial new quality estimator --- .../Components/Decoder/QualityEvaluator.cs | 152 ------------------ .../Formats/Jpeg/Components/Quantization.cs | 128 +++++++++++++++ .../Formats/Jpeg/JpegDecoderCore.cs | 39 ++++- .../Formats/Jpeg/JpegEncoderCore.cs | 40 +---- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 36 ++++- 5 files changed, 199 insertions(+), 196 deletions(-) delete mode 100644 src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Quantization.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs deleted file mode 100644 index e9d67d1ba..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) Six Labors. -// 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 a jpeg quality parameter based on the quantization tables. - /// - /// The quantization tables. - /// Jpeg quality parameter - /// indicating if given quantization tables are equal to standard ITU spec. - public static bool EstimateQuality(Block8x8F[] quantizationTables, out int quality) - { - DebugGuard.MustBeGreaterThan(quantizationTables.Length, 2, nameof(quantizationTables)); - - quality = 75; - - float sum = 0; - - for (int i = 0; i < quantizationTables.Length; i++) - { - ref Block8x8F qTable = ref quantizationTables[i]; - - if (!qTable.Equals(default)) - { - 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; - } - - bool sumHashCondition = (quality <= Hash[i]) && (sum <= Sums[i]); - if (sumHashCondition || (i >= 50)) - { - quality = i + 1; - return sumHashCondition; - } - } - } - else - { - quality = (int)(qTable0[2] + qTable0[53]); - - for (int i = 0; i < 100; i++) - { - if (quality < Hash1[i] && sum < Sums1[i]) - { - continue; - } - - bool sumHashCondition = (quality <= Hash1[i]) && (sum <= Sums1[i]); - if (sumHashCondition || (i >= 50)) - { - quality = i + 1; - return sumHashCondition; - } - } - } - } - - return false; - } - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs new file mode 100644 index 000000000..347155805 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components +{ + /// + /// Provides methods and properties related to jpeg quantization. + /// + internal static class Quantization + { + /// + /// Threshold at which given luminance quantization table should not be considered 'standard'. + /// + /// + /// Jpeg does not define either 'quality' nor 'standard quantization table' properties + /// so this is purely a practical value derived from tests. + /// + public const double StandardLuminanceTableVarianceThreshold = 10.0; + + /// + /// Threshold at which given luminance quantization table should not be considered 'standard'. + /// + /// + /// Jpeg does not define either 'quality' nor 'standard quantization table' properties + /// so this is purely a practical value derived from tests. + /// + public const double StandardChrominanceTableVarianceThreshold = 10.0; + + /// + /// Gets the unscaled luminance quantization table in zig-zag order. Each + /// encoder copies and scales the tables according to its quality parameter. + /// The values are derived from ITU section K.1 after converting from natural to + /// zig-zag order. + /// + // The C# compiler emits this as a compile-time constant embedded in the PE file. + // This is effectively compiled down to: return new ReadOnlySpan(&data, length) + // More details can be found: https://github.com/dotnet/roslyn/pull/24621 + public static ReadOnlySpan UnscaledQuant_Luminance => new byte[] + { + 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, + 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, + 57, 51, 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80, + 109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112, + 100, 120, 92, 101, 103, 99, + }; + + /// + /// Gets the unscaled chrominance quantization table in zig-zag order. Each + /// encoder copies and scales the tables according to its quality parameter. + /// The values are derived from ITU section K.1 after converting from natural to + /// zig-zag order. + /// + // The C# compiler emits this as a compile-time constant embedded in the PE file. + // This is effectively compiled down to: return new ReadOnlySpan(&data, length) + // More details can be found: https://github.com/dotnet/roslyn/pull/24621 + public static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] + { + 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + }; + + // https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 + public static void EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality, out double variance) + { + // This method can be SIMD'ified if standard table is injected as Block8x8F + // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8 + double comparePercent; + double sumPercent = 0; + double sumPercentSqr = 0; + + bool allOnes = true; + + for (int i = 0; i < Block8x8F.Size; i++) + { + float coeff = table[i]; + int coeffInteger = (int)coeff; + + // coefficients are actually int16 casted to float numbers so there's no truncating error + if (coeffInteger != 0) + { + comparePercent = 100.0 * (table[i] / target[i]); + } + else + { + comparePercent = 999.99; + } + + sumPercent += comparePercent; + sumPercentSqr += comparePercent * comparePercent; + + // Check just in case entire table are ones (Quality 100) + if (coeffInteger != 1) + { + allOnes = false; + } + } + + // Perform some statistical analysis of the quality factor + // to determine the likelihood of the current quantization + // table being a scaled version of the "standard" tables. + // If the variance is high, it is unlikely to be the case. + sumPercent /= 64.0; + sumPercentSqr /= 64.0; + variance = sumPercentSqr - (sumPercent * sumPercent); + + // Generate the equivalent IJQ "quality" factor + if (allOnes) + { + quality = 100; + } + else if (sumPercent <= 100.0) + { + quality = (200 - sumPercent) / 2; + } + else + { + quality = 5000.0 / sumPercent; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 3896aa293..b86772d81 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -753,6 +753,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining) { + JpegMetadata jpegMetadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance); + while (remaining > 0) { // 1 byte: quantization table spec @@ -769,6 +771,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } remaining--; + + // Decoding single 8x8 table + ref Block8x8F table = ref this.QuantizationTables[tableIndex]; switch (tablePrecision) { // 8 bit values @@ -783,7 +788,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg stream.Read(this.temp, 0, 64); remaining -= 64; - ref Block8x8F table = ref this.QuantizationTables[tableIndex]; for (int j = 0; j < 64; j++) { table[j] = this.temp[j]; @@ -804,7 +808,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg stream.Read(this.temp, 0, 128); remaining -= 128; - ref Block8x8F table = ref this.QuantizationTables[tableIndex]; for (int j = 0; j < 64; j++) { table[j] = (this.temp[2 * j] << 8) | this.temp[(2 * j) + 1]; @@ -820,9 +823,37 @@ namespace SixLabors.ImageSharp.Formats.Jpeg break; } } - } - this.Metadata.GetFormatMetadata(JpegFormat.Instance).Quality = QualityEvaluator.EstimateQuality(this.QuantizationTables); + // Estimating quality + switch (tableIndex) + { + // luminance table + case 0: + { + Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Luminance, out double quality, out double variance); + jpegMetadata.LumaQuality = quality; + if (variance <= Quantization.StandardLuminanceTableVarianceThreshold) + { + jpegMetadata.lumaQuantizationTable = table.RoundAsInt16Block(); + } + + break; + } + + // chrominance table + case 1: + { + Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Chrominance, out double quality, out double variance); + jpegMetadata.ChromaQuality = quality; + if (variance <= Quantization.StandardChrominanceTableVarianceThreshold) + { + jpegMetadata.chromaQuantizationTable = table.RoundAsInt16Block(); + } + + break; + } + } + } } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 60c209c52..f3fddd9e0 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -64,44 +64,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.colorType = options.ColorType; } - /// - /// Gets the unscaled quantization tables in zig-zag order. Each - /// encoder copies and scales the tables according to its quality parameter. - /// The values are derived from section K.1 after converting from natural to - /// zig-zag order. - /// - // The C# compiler emits this as a compile-time constant embedded in the PE file. - // This is effectively compiled down to: return new ReadOnlySpan(&data, length) - // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - private static ReadOnlySpan UnscaledQuant_Luminance => new byte[] - { - // Luminance. - 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, - 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, - 57, 51, 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80, - 109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112, - 100, 120, 92, 101, 103, 99, - }; - - /// - /// Gets the unscaled quantization tables in zig-zag order. Each - /// encoder copies and scales the tables according to its quality parameter. - /// The values are derived from section K.1 after converting from natural to - /// zig-zag order. - /// - // The C# compiler emits this as a compile-time constant embedded in the PE file. - // This is effectively compiled down to: return new ReadOnlySpan(&data, length) - // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - private static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] - { - // Chrominance. - 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, - 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - }; - /// /// Encode writes the image to the jpeg baseline format with the given options. /// @@ -698,7 +660,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant) { DebugGuard.MustBeBetweenOrEqualTo(i, 0, 1, nameof(i)); - ReadOnlySpan unscaledQuant = (i == 0) ? UnscaledQuant_Luminance : UnscaledQuant_Chrominance; + ReadOnlySpan unscaledQuant = (i == 0) ? Quantization.UnscaledQuant_Luminance : Quantization.UnscaledQuant_Chrominance; for (int j = 0; j < Block8x8F.Size; j++) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 9670d167e..0a05aac17 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; +using SixLabors.ImageSharp.Formats.Jpeg.Components; + namespace SixLabors.ImageSharp.Formats.Jpeg { /// @@ -8,6 +11,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { + /// + /// Luminance qunatization table derived from jpeg image. + /// + /// + /// Would be null if jpeg was encoded using table from ITU spec + /// + internal Block8x8? lumaQuantizationTable; + + /// + /// Luminance qunatization table derived from jpeg image. + /// + /// + /// Would be null if jpeg was encoded using table from ITU spec + /// + internal Block8x8? chromaQuantizationTable; + + internal double LumaQuality; + + internal double ChromaQuality; + /// /// Initializes a new instance of the class. /// @@ -23,12 +46,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { this.Quality = other.Quality; this.ColorType = other.ColorType; + this.lumaQuantizationTable = other.lumaQuantizationTable; + this.chromaQuantizationTable = other.chromaQuantizationTable; } /// /// Gets or sets the encoded quality. /// - public int Quality { get; set; } = 75; + public int Quality + { + get => (int)Math.Round((this.LumaQuality + this.ChromaQuality) / 2f); + set + { + double halfValue = value / 2.0; + this.LumaQuality = halfValue; + this.ChromaQuality = halfValue; + } + } /// /// Gets or sets the encoded quality. From 7772ff12ee136090711c20ed1742d7869a3d3ab6 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 22:57:04 +0300 Subject: [PATCH 14/78] Added comments, docs and improved estimation performances with intrinsics (not tested) --- .../Formats/Jpeg/Components/Block8x8F.cs | 41 +++++++++++++++++++ .../Formats/Jpeg/Components/Quantization.cs | 33 ++++++++------- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 8 ++++ 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index 8ca7b0c80..d55dfced7 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -830,5 +830,46 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components d.V7R.W = this.V7R.W; } } + + /// + /// Compares entire 8x8 block to a single scalar value. + /// + /// Value to compare to. + public bool EqualsToScalar(int value) + { +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported) + { + const int equalityMask = unchecked((int)0b1111_1111_1111_1111_1111_1111_1111_1111); + + var targetVector = Vector256.Create(value); + ref Vector256 blockStride = ref this.V0; + + for (int i = 0; i < RowCount; i++) + { + Vector256 areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector); + if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask) + { + return false; + } + } + + return true; + } +#endif + { + ref float scalars = ref Unsafe.As(ref this); + + for (int i = 0; i < Size; i++) + { + if ((int)Unsafe.Add(ref scalars, i) != value) + { + return false; + } + } + + return true; + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 347155805..0c9a0ca41 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -69,37 +69,44 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 public static void EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality, out double variance) { - // This method can be SIMD'ified if standard table is injected as Block8x8F - // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8 + // This method can be SIMD'ified if standard table is injected as Block8x8F. + // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. double comparePercent; double sumPercent = 0; double sumPercentSqr = 0; - bool allOnes = true; + // Corner case - all 1's => 100 quality + // It would fail to deduce using algorithm below without this check + if (table.EqualsToScalar(1)) + { + // While this is a 100% to be 100 quality, any given table can be scaled to all 1's. + // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit'. + quality = 100; + variance = 0; + return; + } for (int i = 0; i < Block8x8F.Size; i++) { float coeff = table[i]; int coeffInteger = (int)coeff; - // coefficients are actually int16 casted to float numbers so there's no truncating error + // Coefficients are actually int16 casted to float numbers so there's no truncating error. if (coeffInteger != 0) { comparePercent = 100.0 * (table[i] / target[i]); } else { + // No 'valid' quantization table should contain zero at any position + // while this is okay to decode with, it will throw DivideByZeroException at encoding proces stage. + // Not sure what to do here, we can't throw as this technically correct + // but this will screw up the encoder. comparePercent = 999.99; } sumPercent += comparePercent; sumPercentSqr += comparePercent * comparePercent; - - // Check just in case entire table are ones (Quality 100) - if (coeffInteger != 1) - { - allOnes = false; - } } // Perform some statistical analysis of the quality factor @@ -111,11 +118,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components variance = sumPercentSqr - (sumPercent * sumPercent); // Generate the equivalent IJQ "quality" factor - if (allOnes) - { - quality = 100; - } - else if (sumPercent <= 100.0) + if (sumPercent <= 100.0) { quality = (200 - sumPercent) / 2; } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 0a05aac17..1b43f26f0 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -46,8 +46,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { this.Quality = other.Quality; this.ColorType = other.ColorType; + this.lumaQuantizationTable = other.lumaQuantizationTable; this.chromaQuantizationTable = other.chromaQuantizationTable; + this.LumaQuality = other.LumaQuality; + this.ChromaQuality = other.ChromaQuality; } /// @@ -64,6 +67,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } + /// + /// Gets a value indicating whether jpeg was encoded using ITU section spec K.1 quantization tables + /// + public bool ItuSpecQuantization => !this.lumaQuantizationTable.HasValue && !this.chromaQuantizationTable.HasValue; + /// /// Gets or sets the encoded quality. /// From ca29541e4bd3dfed8568e9c8990c209472538dba Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 23:19:44 +0300 Subject: [PATCH 15/78] Fixed standard quality logic --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 9 ++------- .../Formats/Jpg/JpegDecoderTests.Metadata.cs | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 1b43f26f0..cd94c5d5f 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -58,13 +58,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public int Quality { - get => (int)Math.Round((this.LumaQuality + this.ChromaQuality) / 2f); - set - { - double halfValue = value / 2.0; - this.LumaQuality = halfValue; - this.ChromaQuality = halfValue; - } + get => (int)Math.Round(this.LumaQuality); + set => this.LumaQuality = value; } /// diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index f47ae5522..9d4aea453 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { { TestImages.Jpeg.Baseline.Calliphora, 80 }, { TestImages.Jpeg.Progressive.Fb, 75 }, - { TestImages.Jpeg.Issues.IncorrectQuality845, 99 } + { TestImages.Jpeg.Issues.IncorrectQuality845, 98 } }; [Theory] From 2fd703d40346258b4a2b396becc599e100bd63d4 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 23:24:44 +0300 Subject: [PATCH 16/78] Fixed 75 default quality from the encoder --- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index b95dd5644..8131f74d2 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// 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. From ee424d4f8739d2e869f05d1c89aa43b4b22b7040 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 23:44:45 +0300 Subject: [PATCH 17/78] Revert "Simplified quantization table scaling in the encoder" This reverts commit b16301b631520a778f2af29acfbeeeebad177bcc. --- src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index f3fddd9e0..871148335 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -664,8 +664,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg for (int j = 0; j < Block8x8F.Size; j++) { - int scaled = ((unscaledQuant[j] * scale) + 50) / 100; - quant[j] = Math.Clamp(scaled, 1, 255); + int x = unscaledQuant[j]; + x = ((x * scale) + 50) / 100; + if (x < 1) + { + x = 1; + } + + if (x > 255) + { + x = 255; + } + + quant[j] = x; } } } From d50f4b51ca7e7242e25fa212088b84ef0b2dae1b Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 17 Jul 2021 23:45:15 +0300 Subject: [PATCH 18/78] Fixed compilation errors --- .../Formats/Jpeg/JpegDecoderCore.cs | 4 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 46 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index b86772d81..613a1117c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -834,7 +834,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg jpegMetadata.LumaQuality = quality; if (variance <= Quantization.StandardLuminanceTableVarianceThreshold) { - jpegMetadata.lumaQuantizationTable = table.RoundAsInt16Block(); + jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); } break; @@ -847,7 +847,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg jpegMetadata.ChromaQuality = quality; if (variance <= Quantization.StandardChrominanceTableVarianceThreshold) { - jpegMetadata.chromaQuantizationTable = table.RoundAsInt16Block(); + jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); } break; diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index cd94c5d5f..51a65d96d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,26 +11,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { - /// - /// Luminance qunatization table derived from jpeg image. - /// - /// - /// Would be null if jpeg was encoded using table from ITU spec - /// - internal Block8x8? lumaQuantizationTable; - - /// - /// Luminance qunatization table derived from jpeg image. - /// - /// - /// Would be null if jpeg was encoded using table from ITU spec - /// - internal Block8x8? chromaQuantizationTable; - - internal double LumaQuality; - - internal double ChromaQuality; - /// /// Initializes a new instance of the class. /// @@ -47,12 +27,32 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.Quality = other.Quality; this.ColorType = other.ColorType; - this.lumaQuantizationTable = other.lumaQuantizationTable; - this.chromaQuantizationTable = other.chromaQuantizationTable; + this.LumaQuantizationTable = other.LumaQuantizationTable; + this.ChromaQuantizationTable = other.ChromaQuantizationTable; this.LumaQuality = other.LumaQuality; this.ChromaQuality = other.ChromaQuality; } + /// + /// Gets or sets luminance qunatization table derived from jpeg image. + /// + /// + /// Would be null if jpeg was encoded using table from ITU spec + /// + internal Block8x8? LumaQuantizationTable { get; set; } + + /// + /// Gets or sets chrominance qunatization table derived from jpeg image. + /// + /// + /// Would be null if jpeg was encoded using table from ITU spec + /// + internal Block8x8? ChromaQuantizationTable { get; set; } + + internal double LumaQuality { get; set; } + + internal double ChromaQuality { get; set; } + /// /// Gets or sets the encoded quality. /// @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Gets a value indicating whether jpeg was encoded using ITU section spec K.1 quantization tables /// - public bool ItuSpecQuantization => !this.lumaQuantizationTable.HasValue && !this.chromaQuantizationTable.HasValue; + public bool ItuSpecQuantization => !this.LumaQuantizationTable.HasValue && !this.ChromaQuantizationTable.HasValue; /// /// Gets or sets the encoded quality. From 54105e32391f3d84b07ae98aef4bb1dacd5eef6f Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 18 Jul 2021 14:38:10 +0300 Subject: [PATCH 19/78] Added docs to the metadata properties --- .../Formats/Jpeg/JpegDecoderCore.cs | 4 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 46 ++++++++++++++----- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 613a1117c..8d8ad3dad 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -831,7 +831,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case 0: { Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Luminance, out double quality, out double variance); - jpegMetadata.LumaQuality = quality; + jpegMetadata.LuminanceQuality = quality; if (variance <= Quantization.StandardLuminanceTableVarianceThreshold) { jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); @@ -844,7 +844,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case 1: { Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Chrominance, out double quality, out double variance); - jpegMetadata.ChromaQuality = quality; + jpegMetadata.ChrominanceQuality = quality; if (variance <= Quantization.StandardChrominanceTableVarianceThreshold) { jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 51a65d96d..da435fc7e 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -29,8 +29,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.LumaQuantizationTable = other.LumaQuantizationTable; this.ChromaQuantizationTable = other.ChromaQuantizationTable; - this.LumaQuality = other.LumaQuality; - this.ChromaQuality = other.ChromaQuality; + this.LuminanceQuality = other.LuminanceQuality; + this.ChrominanceQuality = other.ChrominanceQuality; } /// @@ -49,24 +49,48 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// internal Block8x8? ChromaQuantizationTable { get; set; } - internal double LumaQuality { get; set; } + /// + /// Gets or sets the jpeg luminance quality. + /// + /// + /// This value might not be accurate if it was calculated during jpeg decoding + /// with non-complient ITU quantization tables. + /// + public double LuminanceQuality { get; set; } - internal double ChromaQuality { get; set; } + /// + /// Gets or sets the jpeg chrominance quality. + /// + /// + /// This value might not be accurate if it was calculated during jpeg decoding + /// with non-complient ITU quantization tables. + /// + public double ChrominanceQuality { get; set; } + + /// + /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. + /// + public bool UsesStandardLuminanceTable => !this.LumaQuantizationTable.HasValue; + + /// + /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. + /// + public bool UsesStandardChrominanceTable => !this.ChromaQuantizationTable.HasValue; /// /// Gets or sets the encoded quality. /// + [Obsolete("Use LumanQuality and ChromaQuality instead. Quality is now separated for luminance and chrominance data.")] public int Quality { - get => (int)Math.Round(this.LumaQuality); - set => this.LumaQuality = value; + get => (int)Math.Round(this.LuminanceQuality + this.ChrominanceQuality); + set + { + this.LuminanceQuality = value; + this.ChrominanceQuality = value; + } } - /// - /// Gets a value indicating whether jpeg was encoded using ITU section spec K.1 quantization tables - /// - public bool ItuSpecQuantization => !this.LumaQuantizationTable.HasValue && !this.ChromaQuantizationTable.HasValue; - /// /// Gets or sets the encoded quality. /// From 174628306c92b50664a961b0eff287106372b51d Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 18 Jul 2021 15:27:48 +0300 Subject: [PATCH 20/78] Added docs --- .../Formats/Jpeg/Components/Quantization.cs | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 0c9a0ca41..a7af76a65 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -13,7 +13,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components internal static class Quantization { /// - /// Threshold at which given luminance quantization table should not be considered 'standard'. + /// Threshold at which given luminance quantization table should be considered 'standard'. + /// Bigger the variance - more likely it to be a non-ITU complient table. /// /// /// Jpeg does not define either 'quality' nor 'standard quantization table' properties @@ -22,7 +23,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components public const double StandardLuminanceTableVarianceThreshold = 10.0; /// - /// Threshold at which given luminance quantization table should not be considered 'standard'. + /// Threshold at which given chrominance quantization table should be considered 'standard'. + /// Bigger the variance - more likely it to be a non-ITU complient table. /// /// /// Jpeg does not define either 'quality' nor 'standard quantization table' properties @@ -66,8 +68,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components 99, 99, 99, 99, 99, 99, 99, 99, }; - // https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 - public static void EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality, out double variance) + /// Ported from JPEGsnoop: + /// https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 + /// + /// Estimates jpeg quality based on quantization table. + /// + /// + /// This technically can be used with any given table but internal decoder code uses ITU spec tables: + /// and . + /// + /// Input quantization table. + /// Quantization to estimate against. + /// Variance threshold after which given table is considered non-complient. + /// Estimated quality + /// indicating if given table is target-complient + private static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, out double quality) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. @@ -80,10 +95,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components if (table.EqualsToScalar(1)) { // While this is a 100% to be 100 quality, any given table can be scaled to all 1's. - // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit'. + // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' will affect result filesize drastically. + // Quality=100 shouldn't be used in usual use case. quality = 100; - variance = 0; - return; + return true; } for (int i = 0; i < Block8x8F.Size; i++) @@ -115,7 +130,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // If the variance is high, it is unlikely to be the case. sumPercent /= 64.0; sumPercentSqr /= 64.0; - variance = sumPercentSqr - (sumPercent * sumPercent); + double variance = sumPercentSqr - (sumPercent * sumPercent); // Generate the equivalent IJQ "quality" factor if (sumPercent <= 100.0) @@ -126,6 +141,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { quality = 5000.0 / sumPercent; } + + return variance <= varianceThreshold; } + + /// + /// Estimates jpeg luminance quality. + /// + /// Luminance quantization table. + /// Output jpeg quality. + /// indicating if given table is ITU-complient. + public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out double quality) + => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, StandardLuminanceTableVarianceThreshold, out quality); + + /// + /// Estimates jpeg chrominance quality. + /// + /// Chrominance quantization table. + /// Output jpeg quality. + /// indicating if given table is ITU-complient. + public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out double quality) + => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, StandardChrominanceTableVarianceThreshold, out quality); } } From 4b20325746098c265ae9af80e04cdda5f47b2c34 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 18 Jul 2021 15:42:39 +0300 Subject: [PATCH 21/78] Commented obsolete attribute for quality --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index da435fc7e..a768f6651 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -80,10 +80,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Gets or sets the encoded quality. /// - [Obsolete("Use LumanQuality and ChromaQuality instead. Quality is now separated for luminance and chrominance data.")] + // [Obsolete("Use LumanQuality and ChromaQuality instead. Quality is now separated for luminance and chrominance data.")] public int Quality { - get => (int)Math.Round(this.LuminanceQuality + this.ChrominanceQuality); + get => (int)Math.Round((this.LuminanceQuality + this.ChrominanceQuality) / 2); set { this.LuminanceQuality = value; From 2494131cfa6d37ac280b10965fea17b6d834d112 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 18 Jul 2021 22:55:26 +0300 Subject: [PATCH 22/78] Fixed invalid quality estimation --- .../Formats/Jpeg/Components/Quantization.cs | 48 +++++++++++++++-- .../Formats/Jpeg/JpegDecoderCore.cs | 16 +++--- .../Formats/Jpeg/JpegEncoderCore.cs | 51 ++----------------- 3 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index a7af76a65..b87c538a9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Text; namespace SixLabors.ImageSharp.Formats.Jpeg.Components @@ -20,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. /// - public const double StandardLuminanceTableVarianceThreshold = 10.0; + private const double StandardLuminanceTableVarianceThreshold = 10.0; /// /// Threshold at which given chrominance quantization table should be considered 'standard'. @@ -30,7 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. /// - public const double StandardChrominanceTableVarianceThreshold = 10.0; + private const double StandardChrominanceTableVarianceThreshold = 10.0; /// /// Gets the unscaled luminance quantization table in zig-zag order. Each @@ -41,7 +42,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // The C# compiler emits this as a compile-time constant embedded in the PE file. // This is effectively compiled down to: return new ReadOnlySpan(&data, length) // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - public static ReadOnlySpan UnscaledQuant_Luminance => new byte[] + private static ReadOnlySpan UnscaledQuant_Luminance => new byte[] { 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, @@ -59,7 +60,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // The C# compiler emits this as a compile-time constant embedded in the PE file. // This is effectively compiled down to: return new ReadOnlySpan(&data, length) // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - public static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] + private static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] { 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, @@ -82,7 +83,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Variance threshold after which given table is considered non-complient. /// Estimated quality /// indicating if given table is target-complient - private static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, out double quality) + public static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, out double quality) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. @@ -151,6 +152,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Luminance quantization table. /// Output jpeg quality. /// indicating if given table is ITU-complient. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out double quality) => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, StandardLuminanceTableVarianceThreshold, out quality); @@ -160,7 +162,43 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Chrominance quantization table. /// Output jpeg quality. /// indicating if given table is ITU-complient. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out double quality) => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, StandardChrominanceTableVarianceThreshold, out quality); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int QualityToScale(int quality) + => quality < 50 ? 5000 / quality : 200 - (quality * 2); + + private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) + { + Block8x8F table = default; + for (int j = 0; j < Block8x8F.Size; j++) + { + int x = unscaledTable[j]; + x = ((x * scale) + 50) / 100; + if (x < 1) + { + x = 1; + } + + if (x > 255) + { + x = 255; + } + + table[j] = x; + } + + return table; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Block8x8F ScaleLuminanceTable(int quality) + => ScaleQuantizationTable(scale: QualityToScale(quality), UnscaledQuant_Luminance); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Block8x8F ScaleChrominanceTable(int quality) + => ScaleQuantizationTable(scale: QualityToScale(quality), UnscaledQuant_Chrominance); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 8d8ad3dad..09b40e09d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -830,26 +830,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // luminance table case 0: { - Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Luminance, out double quality, out double variance); - jpegMetadata.LuminanceQuality = quality; - if (variance <= Quantization.StandardLuminanceTableVarianceThreshold) + // if quantization table is non-complient to stardard itu table + // we can't reacreate it later with calculated quality as this is an approximation + // so we save it in the metadata + if (!Quantization.EstimateLuminanceQuality(ref table, out double quality)) { jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); } + jpegMetadata.LuminanceQuality = quality; break; } // chrominance table case 1: { - Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Chrominance, out double quality, out double variance); - jpegMetadata.ChrominanceQuality = quality; - if (variance <= Quantization.StandardChrominanceTableVarianceThreshold) + // if quantization table is non-complient to stardard itu table + // we can't reacreate it later with calculated quality as this is an approximation + // so we save it in the metadata + if (!Quantization.EstimateChrominanceQuality(ref table, out double quality)) { jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); } + jpegMetadata.ChrominanceQuality = quality; break; } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 871148335..c829e0972 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -94,27 +94,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int qlty = Numerics.Clamp(this.quality ?? metadata.GetJpegMetadata().Quality, 1, 100); this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; - // Convert from a quality rating to a scaling factor. - int scale; - if (qlty < 50) - { - scale = 5000 / qlty; - } - else - { - scale = 200 - (qlty * 2); - } - // Initialize the quantization tables. // TODO: This looks ugly, should we write chrominance table for luminance-only images? // If not - this can code can be simplified - Block8x8F luminanceQuantTable = default; - Block8x8F chrominanceQuantTable = default; - InitQuantizationTable(0, scale, ref luminanceQuantTable); - if (componentCount > 1) - { - InitQuantizationTable(1, scale, ref chrominanceQuantTable); - } + Block8x8F luminanceQuantTable = Quantization.ScaleLuminanceTable(qlty); + Block8x8F chrominanceQuantTable = Quantization.ScaleChrominanceTable(qlty); // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); @@ -138,10 +122,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg var scanEncoder = new HuffmanScanEncoder(stream); if (this.colorType == JpegColorType.Luminance) { + // luminance quantization table only scanEncoder.EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken); } else { + // luminance and chrominance quantization tables switch (this.subsample) { case JpegSubsample.Ratio444: @@ -650,34 +636,5 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.buffer[3] = (byte)(length & 0xff); this.outputStream.Write(this.buffer, 0, 4); } - - /// - /// Initializes quantization table. - /// - /// The quantization index. - /// The scaling factor. - /// The quantization table. - private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant) - { - DebugGuard.MustBeBetweenOrEqualTo(i, 0, 1, nameof(i)); - ReadOnlySpan unscaledQuant = (i == 0) ? Quantization.UnscaledQuant_Luminance : Quantization.UnscaledQuant_Chrominance; - - for (int j = 0; j < Block8x8F.Size; j++) - { - int x = unscaledQuant[j]; - x = ((x * scale) + 50) / 100; - if (x < 1) - { - x = 1; - } - - if (x > 255) - { - x = 255; - } - - quant[j] = x; - } - } } } From bfbde71d881228f96b7cd227422c95278015faef Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sun, 18 Jul 2021 23:08:45 +0300 Subject: [PATCH 23/78] remove subfileType filtering --- src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index b87b927a9..d7c9848a4 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -157,11 +157,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff { var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile?.GetValue(ExifTag.SubfileType)?.Value ?? (int)TiffNewSubfileType.FullImage); - if (subfileType != TiffNewSubfileType.FullImage) - { - continue; - } - ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker); metadataImage = null; } From cad7340d5b47d0ecc897583b2d90525e49ac9fd7 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sun, 18 Jul 2021 23:16:59 +0300 Subject: [PATCH 24/78] Add multi-frame tests --- .../Tiff/TiffEncoderMultiframeTests.cs | 131 +++++++++++++++++- 1 file changed, 125 insertions(+), 6 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs index 403160772..aeca38c5c 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs @@ -28,17 +28,54 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(MultiframeDeflateWithPreview, PixelTypes.Rgba32)] - public void TiffEncoder_EncodeMultiframe_WithoutPreview_ProblemTest(TestImageProvider provider) - where TPixel : unmanaged, IPixel => Assert.Throws(() => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb)); + public void TiffEncoder_EncodeMultiframe_WithPreview(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); [Theory] - [WithFile(RgbLzwNoPredictor, PixelTypes.Rgba32)] - public void TiffEncoder_EncodeMultiframe_Create(TestImageProvider provider) + [WithFile(TestImages.Gif.Receipt, PixelTypes.Rgb24)] + [WithFile(TestImages.Gif.Issues.BadDescriptorWidth, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_Convert(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit48, TiffPhotometricInterpretation.Rgb); + + [Theory] + [WithFile(MultiframeLzwPredictor, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_RemoveFrames(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Assert.True(image.Frames.Count > 1); + + image.Frames.RemoveFrame(0); + + TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Bit24; + var encoder = new TiffEncoder + { + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + BitsPerPixel = bitsPerPixel, + Compression = TiffCompression.Deflate + }; + + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + ImageComparer.Exact); + } + + [Theory] + [WithFile(TestImages.Png.Bike, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_AddFrames(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using var image = provider.GetImage(); - using var image2 = new Image(image.Width, image.Height, Color.Green.ToRgba32()); + using Image image = provider.GetImage(); + Assert.Equal(1, image.Frames.Count); + using var image1 = new Image(image.Width, image.Height, Color.Green.ToRgba32()); + + using var image2 = new Image(image.Width, image.Height, Color.Yellow.ToRgba32()); + + image.Frames.AddFrame(image1.Frames.RootFrame); image.Frames.AddFrame(image2.Frames.RootFrame); TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Bit24; @@ -49,6 +86,88 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Compression = TiffCompression.Deflate }; + using (var ms = new System.IO.MemoryStream()) + { + image.Save(ms, encoder); + + ms.Position = 0; + using var output = Image.Load(ms); + + Assert.Equal(3, output.Frames.Count); + + ImageFrame frame1 = output.Frames[1]; + ImageFrame frame2 = output.Frames[2]; + + Assert.Equal(Color.Green.ToRgba32(), frame1[10, 10]); + Assert.Equal(Color.Yellow.ToRgba32(), frame2[10, 10]); + + Assert.Equal(TiffCompression.Deflate, frame1.Metadata.GetTiffMetadata().Compression); + Assert.Equal(TiffCompression.Deflate, frame1.Metadata.GetTiffMetadata().Compression); + + Assert.Equal(TiffPhotometricInterpretation.Rgb, frame1.Metadata.GetTiffMetadata().PhotometricInterpretation); + Assert.Equal(TiffPhotometricInterpretation.Rgb, frame2.Metadata.GetTiffMetadata().PhotometricInterpretation); + } + + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + ImageComparer.Exact); + } + + [Theory] + [WithBlankImages(100, 100, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_Create(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + using var image0 = new Image(image.Width, image.Height, Color.Red.ToRgba32()); + + using var image1 = new Image(image.Width, image.Height, Color.Green.ToRgba32()); + + using var image2 = new Image(image.Width, image.Height, Color.Yellow.ToRgba32()); + + image.Frames.AddFrame(image0.Frames.RootFrame); + image.Frames.AddFrame(image1.Frames.RootFrame); + image.Frames.AddFrame(image2.Frames.RootFrame); + image.Frames.RemoveFrame(0); + + TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Bit8; + var encoder = new TiffEncoder + { + PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor, + BitsPerPixel = bitsPerPixel, + Compression = TiffCompression.Lzw + }; + + using (var ms = new System.IO.MemoryStream()) + { + image.Save(ms, encoder); + + ms.Position = 0; + using var output = Image.Load(ms); + + Assert.Equal(3, output.Frames.Count); + + ImageFrame frame0 = output.Frames[0]; + ImageFrame frame1 = output.Frames[1]; + ImageFrame frame2 = output.Frames[2]; + + Assert.Equal(Color.Red.ToRgba32(), frame0[10, 10]); + Assert.Equal(Color.Green.ToRgba32(), frame1[10, 10]); + Assert.Equal(Color.Yellow.ToRgba32(), frame2[10, 10]); + + Assert.Equal(TiffCompression.Lzw, frame0.Metadata.GetTiffMetadata().Compression); + Assert.Equal(TiffCompression.Lzw, frame1.Metadata.GetTiffMetadata().Compression); + Assert.Equal(TiffCompression.Lzw, frame1.Metadata.GetTiffMetadata().Compression); + + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frame0.Metadata.GetTiffMetadata().PhotometricInterpretation); + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frame1.Metadata.GetTiffMetadata().PhotometricInterpretation); + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frame2.Metadata.GetTiffMetadata().PhotometricInterpretation); + } + image.VerifyEncoder( provider, "tiff", From 961e3b1d6471eef8cfebdb7113cccd28a9667390 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Mon, 19 Jul 2021 01:14:14 +0300 Subject: [PATCH 25/78] Added tests for variance thresholds --- .../Formats/Jpeg/Components/Quantization.cs | 51 ++++++++++---- .../Formats/Jpg/QuantizationTests.cs | 68 +++++++++++++++++++ 2 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index b87c538a9..3087551d0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -13,6 +13,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// internal static class Quantization { + /// + /// Upper bound (inclusive) for jpeg quality setting. + /// + public const int MaxQualityFactor = 100; + + /// + /// Lower bound (inclusive) for jpeg quality setting. + /// + public const int MinQualityFactor = 1; + + /// + /// Represents lowest quality setting which can be estimated with enough confidence. + /// Any quality below it results in a highly compressed jpeg image + /// which shouldn't use standard itu quantization tables for re-encoding. + /// + public const int QualityEstimationConfidenceThreshold = 25; + /// /// Threshold at which given luminance quantization table should be considered 'standard'. /// Bigger the variance - more likely it to be a non-ITU complient table. @@ -21,7 +38,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. /// - private const double StandardLuminanceTableVarianceThreshold = 10.0; + public const double StandardLuminanceTableVarianceThreshold = 10.0; /// /// Threshold at which given chrominance quantization table should be considered 'standard'. @@ -31,7 +48,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. /// - private const double StandardChrominanceTableVarianceThreshold = 10.0; + public const double StandardChrominanceTableVarianceThreshold = 10.0; /// /// Gets the unscaled luminance quantization table in zig-zag order. Each @@ -42,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // The C# compiler emits this as a compile-time constant embedded in the PE file. // This is effectively compiled down to: return new ReadOnlySpan(&data, length) // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - private static ReadOnlySpan UnscaledQuant_Luminance => new byte[] + public static ReadOnlySpan UnscaledQuant_Luminance => new byte[] { 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, @@ -60,7 +77,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // The C# compiler emits this as a compile-time constant embedded in the PE file. // This is effectively compiled down to: return new ReadOnlySpan(&data, length) // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - private static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] + public static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] { 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, @@ -72,7 +89,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Ported from JPEGsnoop: /// https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 /// - /// Estimates jpeg quality based on quantization table. + /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// /// This technically can be used with any given table but internal decoder code uses ITU spec tables: @@ -80,10 +97,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// /// Input quantization table. /// Quantization to estimate against. - /// Variance threshold after which given table is considered non-complient. /// Estimated quality /// indicating if given table is target-complient - public static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, out double quality) + public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. @@ -99,7 +115,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' will affect result filesize drastically. // Quality=100 shouldn't be used in usual use case. quality = 100; - return true; + return 0; } for (int i = 0; i < Block8x8F.Size; i++) @@ -131,7 +147,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // If the variance is high, it is unlikely to be the case. sumPercent /= 64.0; sumPercentSqr /= 64.0; - double variance = sumPercentSqr - (sumPercent * sumPercent); // Generate the equivalent IJQ "quality" factor if (sumPercent <= 100.0) @@ -143,28 +158,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components quality = 5000.0 / sumPercent; } - return variance <= varianceThreshold; + return sumPercentSqr - (sumPercent * sumPercent); } /// - /// Estimates jpeg luminance quality. + /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// Luminance quantization table. /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out double quality) - => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, StandardLuminanceTableVarianceThreshold, out quality); + { + double variance = EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, out quality); + return variance <= StandardLuminanceTableVarianceThreshold; + } /// - /// Estimates jpeg chrominance quality. + /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// Chrominance quantization table. /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out double quality) - => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, StandardChrominanceTableVarianceThreshold, out quality); + { + double variance = EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, out quality); + return variance <= StandardChrominanceTableVarianceThreshold; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int QualityToScale(int quality) @@ -172,6 +193,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) { + DebugGuard.MustBeBetweenOrEqualTo(scale, MinQualityFactor, MaxQualityFactor, nameof(scale)); + Block8x8F table = default; for (int j = 0; j < Block8x8F.Size; j++) { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs new file mode 100644 index 000000000..1870c39fa --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using Xunit; +using Xunit.Abstractions; + +using JpegQuantization = SixLabors.ImageSharp.Formats.Jpeg.Components.Quantization; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public class QuantizationTests + { + public QuantizationTests(ITestOutputHelper output) + { + this.Output = output; + } + + private ITestOutputHelper Output { get; } + + //[Fact(Skip = "Debug only, enable manually!")] + [Fact] + public void PrintVariancesFromStandardTables_Luminance() + { + this.Output.WriteLine("Variances for Luminance table.\nQuality levels 25-100:"); + + double minVariance = double.MaxValue; + double maxVariance = double.MinValue; + + for (int q = JpegQuantization.QualityEstimationConfidenceThreshold; q <= JpegQuantization.MaxQualityFactor; q++) + { + Block8x8F table = JpegQuantization.ScaleLuminanceTable(q); + double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Luminance, out double quality); + + minVariance = Math.Min(minVariance, variance); + maxVariance = Math.Max(maxVariance, variance); + + this.Output.WriteLine($"q={q}\t{variance}\test. q: {quality}"); + } + + this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); + } + + //[Fact(Skip = "Debug only, enable manually!")] + [Fact] + public void PrintVariancesFromStandardTables_Chrominance() + { + this.Output.WriteLine("Variances for Chrominance table.\nQuality levels 25-100:"); + + double minVariance = double.MaxValue; + double maxVariance = double.MinValue; + for (int q = JpegQuantization.QualityEstimationConfidenceThreshold; q <= JpegQuantization.MaxQualityFactor; q++) + { + Block8x8F table = JpegQuantization.ScaleChrominanceTable(q); + double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Chrominance, out double quality); + + minVariance = Math.Min(minVariance, variance); + maxVariance = Math.Max(maxVariance, variance); + + this.Output.WriteLine($"q={q}\t{variance}\test. q: {quality}"); + } + + this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); + } + } +} From d537ede70a43988a7b243f8077e771aece5f7f2f Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Mon, 19 Jul 2021 19:53:04 +0300 Subject: [PATCH 26/78] Updated thresholds --- src/ImageSharp/Formats/Jpeg/Components/Quantization.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 3087551d0..dc1801f6d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -37,8 +37,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. + /// For actual variances output against standard table see tests at Formats.Jpg.QuantizationTests.PrintVariancesFromStandardTables_*. + /// Actual value is 2.3629059983706604, truncated unsignificant part. /// - public const double StandardLuminanceTableVarianceThreshold = 10.0; + public const double StandardLuminanceTableVarianceThreshold = 2.36291; /// /// Threshold at which given chrominance quantization table should be considered 'standard'. @@ -47,8 +49,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. + /// For actual variances output against standard table see tests at Formats.Jpg.QuantizationTests.PrintVariancesFromStandardTables_*. + /// Actual value is 0.8949631033036098, truncated unsignificant part. /// - public const double StandardChrominanceTableVarianceThreshold = 10.0; + public const double StandardChrominanceTableVarianceThreshold = 0.894963; /// /// Gets the unscaled luminance quantization table in zig-zag order. Each From e40313cc5eeda1da3b103c005aa00115ef998d1d Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Mon, 19 Jul 2021 20:19:14 +0300 Subject: [PATCH 27/78] Made quality metadata int, added tests for standard tables --- .../Formats/Jpeg/Components/Quantization.cs | 17 +++++--- .../Formats/Jpeg/JpegDecoderCore.cs | 4 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 6 +-- .../Formats/Jpg/QuantizationTests.cs | 42 +++++++++++++++---- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index dc1801f6d..fb477dda8 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -28,7 +28,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Any quality below it results in a highly compressed jpeg image /// which shouldn't use standard itu quantization tables for re-encoding. /// - public const int QualityEstimationConfidenceThreshold = 25; + public const int QualityEstimationConfidenceLowerThreshold = 25; + + /// + /// Represents highest quality setting which can be estimated with enough confidence. + /// + public const int QualityEstimationConfidenceUpperThreshold = 98; /// /// Threshold at which given luminance quantization table should be considered 'standard'. @@ -103,7 +108,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Quantization to estimate against. /// Estimated quality /// indicating if given table is target-complient - public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality) + public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out int quality) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. @@ -155,11 +160,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // Generate the equivalent IJQ "quality" factor if (sumPercent <= 100.0) { - quality = (200 - sumPercent) / 2; + quality = (int)Math.Round((200 - sumPercent) / 2); } else { - quality = 5000.0 / sumPercent; + quality = (int)Math.Round(5000.0 / sumPercent); } return sumPercentSqr - (sumPercent * sumPercent); @@ -172,7 +177,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out double quality) + public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out int quality) { double variance = EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, out quality); return variance <= StandardLuminanceTableVarianceThreshold; @@ -185,7 +190,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out double quality) + public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out int quality) { double variance = EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, out quality); return variance <= StandardChrominanceTableVarianceThreshold; diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 09b40e09d..b58e99a10 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -833,7 +833,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // if quantization table is non-complient to stardard itu table // we can't reacreate it later with calculated quality as this is an approximation // so we save it in the metadata - if (!Quantization.EstimateLuminanceQuality(ref table, out double quality)) + if (!Quantization.EstimateLuminanceQuality(ref table, out int quality)) { jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); } @@ -848,7 +848,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // if quantization table is non-complient to stardard itu table // we can't reacreate it later with calculated quality as this is an approximation // so we save it in the metadata - if (!Quantization.EstimateChrominanceQuality(ref table, out double quality)) + if (!Quantization.EstimateChrominanceQuality(ref table, out int quality)) { jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index a768f6651..e6183705d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -56,7 +56,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public double LuminanceQuality { get; set; } + public int LuminanceQuality { get; set; } /// /// Gets or sets the jpeg chrominance quality. @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public double ChrominanceQuality { get; set; } + public int ChrominanceQuality { get; set; } /// /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. @@ -83,7 +83,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // [Obsolete("Use LumanQuality and ChromaQuality instead. Quality is now separated for luminance and chrominance data.")] public int Quality { - get => (int)Math.Round((this.LuminanceQuality + this.ChrominanceQuality) / 2); + get => (int)Math.Round((this.LuminanceQuality + this.ChrominanceQuality) / 2f); set { this.LuminanceQuality = value; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs index 1870c39fa..8ed14bd81 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -20,8 +20,37 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg private ITestOutputHelper Output { get; } - //[Fact(Skip = "Debug only, enable manually!")] [Fact] + public void QualityEstimationFromStandardEncoderTables_Luminance() + { + int firstIndex = JpegQuantization.QualityEstimationConfidenceLowerThreshold; + int lastIndex = JpegQuantization.QualityEstimationConfidenceUpperThreshold; + for (int quality = firstIndex; quality <= lastIndex; quality++) + { + Block8x8F table = JpegQuantization.ScaleLuminanceTable(quality); + bool isStrandard = JpegQuantization.EstimateLuminanceQuality(ref table, out int actualQuality); + + Assert.True(isStrandard, $"Standard table is estimated to be non-spec complient at quality level {quality}"); + Assert.Equal(quality, actualQuality); + } + } + + [Fact] + public void QualityEstimationFromStandardEncoderTables_Chrominance() + { + int firstIndex = JpegQuantization.QualityEstimationConfidenceLowerThreshold; + int lastIndex = JpegQuantization.QualityEstimationConfidenceUpperThreshold; + for (int quality = firstIndex; quality <= lastIndex; quality++) + { + Block8x8F table = JpegQuantization.ScaleChrominanceTable(quality); + bool isStrandard = JpegQuantization.EstimateChrominanceQuality(ref table, out int actualQuality); + + Assert.True(isStrandard, $"Standard table is estimated to be non-spec complient at quality level {quality}"); + Assert.Equal(quality, actualQuality); + } + } + + [Fact(Skip = "Debug only, enable manually!")] public void PrintVariancesFromStandardTables_Luminance() { this.Output.WriteLine("Variances for Luminance table.\nQuality levels 25-100:"); @@ -29,10 +58,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg double minVariance = double.MaxValue; double maxVariance = double.MinValue; - for (int q = JpegQuantization.QualityEstimationConfidenceThreshold; q <= JpegQuantization.MaxQualityFactor; q++) + for (int q = JpegQuantization.QualityEstimationConfidenceLowerThreshold; q <= JpegQuantization.MaxQualityFactor; q++) { Block8x8F table = JpegQuantization.ScaleLuminanceTable(q); - double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Luminance, out double quality); + double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Luminance, out int quality); minVariance = Math.Min(minVariance, variance); maxVariance = Math.Max(maxVariance, variance); @@ -43,18 +72,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); } - //[Fact(Skip = "Debug only, enable manually!")] - [Fact] + [Fact(Skip = "Debug only, enable manually!")] public void PrintVariancesFromStandardTables_Chrominance() { this.Output.WriteLine("Variances for Chrominance table.\nQuality levels 25-100:"); double minVariance = double.MaxValue; double maxVariance = double.MinValue; - for (int q = JpegQuantization.QualityEstimationConfidenceThreshold; q <= JpegQuantization.MaxQualityFactor; q++) + for (int q = JpegQuantization.QualityEstimationConfidenceLowerThreshold; q <= JpegQuantization.MaxQualityFactor; q++) { Block8x8F table = JpegQuantization.ScaleChrominanceTable(q); - double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Chrominance, out double quality); + double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Chrominance, out int quality); minVariance = Math.Min(minVariance, variance); maxVariance = Math.Max(maxVariance, variance); From 81b5e7fce0c008425e7939080f7c318d5c6b4b97 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 20 Jul 2021 01:25:10 +0300 Subject: [PATCH 28/78] Removed obsolete attribute --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index e6183705d..0579e7b5e 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -80,7 +80,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Gets or sets the encoded quality. /// - // [Obsolete("Use LumanQuality and ChromaQuality instead. Quality is now separated for luminance and chrominance data.")] public int Quality { get => (int)Math.Round((this.LuminanceQuality + this.ChrominanceQuality) / 2f); From cf1ad8edc2144a479850b53a0a7a76b02a861386 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 20 Jul 2021 16:42:26 +0300 Subject: [PATCH 29/78] (WIP) quality --- .../Formats/Jpeg/IJpegEncoderOptions.cs | 13 +++++-- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 34 +++++++++++++++++- .../Formats/Jpeg/JpegEncoderCore.cs | 35 +++++++++++++------ 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs index cceed407c..d2921ad4c 100644 --- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs +++ b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs @@ -9,11 +9,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg internal interface IJpegEncoderOptions { /// - /// Gets the quality, that will be used to encode the image. Quality - /// index must be between 0 and 100 (compression from max to min). + /// Gets the quality, that will be used to encode the luminance data of 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? LuminanceQuality { get; } + + /// + /// Gets the quality, that will be used to encode the chrominance data of 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? ChrominanceQuality { get; } /// /// Gets the subsample ration, that will be used to encode the image. diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 8131f74d2..27597a0f9 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -18,7 +19,38 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// index must be between 0 and 100 (compression from max to min). /// Defaults to 75. /// - public int? Quality { get; set; } + public int? Quality + { + [Obsolete("This accessor will soon be deprecated. Use LuminanceQuality and ChrominanceQuality getters instead.", error: false)] + get + { + const int defaultQuality = 75; + + int lumaQuality = this.LuminanceQuality ?? defaultQuality; + int chromaQuality = this.LuminanceQuality ?? lumaQuality; + return (int)Math.Round((lumaQuality + chromaQuality) / 2f); + } + + set + { + this.LuminanceQuality = value; + this.ChrominanceQuality = value; + } + } + + /// + /// Gets or sets the quality, that will be used to encode luminance image data. + /// Quality index must be between 0 and 100 (compression from max to min). + /// Defaults to 75. + /// + public int? LuminanceQuality { get; set; } + + /// + /// Gets or sets the quality, that will be used to encode chrominance image data. + /// Quality index must be between 0 and 100 (compression from max to min). + /// Defaults to 75. + /// + public int? ChrominanceQuality { 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 c829e0972..4c81c58dd 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -23,6 +23,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals { + /// + /// Default JPEG encoding quality for both luminance and chominance tables. + /// + private const int DefaultQualityValue = 75; + /// /// The number of quantization tables. /// @@ -41,7 +46,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The quality, that will be used to encode the image. /// - private readonly int? quality; + private readonly int? luminanceQuality; + + /// + /// The quality, that will be used to encode the image. + /// + private readonly int? chrominanceQuality; /// /// Gets or sets the subsampling method to use. @@ -59,7 +69,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The options public JpegEncoderCore(IJpegEncoderOptions options) { - this.quality = options.Quality; + this.luminanceQuality = options.LuminanceQuality; + this.chrominanceQuality = options.ChrominanceQuality; this.subsample = options.Subsample; this.colorType = options.ColorType; } @@ -86,19 +97,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.outputStream = stream; ImageMetadata metadata = image.Metadata; + JpegMetadata jpegMetadata = metadata.GetJpegMetadata(); // Compute number of components based on color type in options. int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3; - // System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1. - int qlty = Numerics.Clamp(this.quality ?? metadata.GetJpegMetadata().Quality, 1, 100); - this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; - // Initialize the quantization tables. - // TODO: This looks ugly, should we write chrominance table for luminance-only images? - // If not - this can code can be simplified - Block8x8F luminanceQuantTable = Quantization.ScaleLuminanceTable(qlty); - Block8x8F chrominanceQuantTable = Quantization.ScaleChrominanceTable(qlty); + // TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that + int lumaQuality = Numerics.Clamp(this.luminanceQuality ?? jpegMetadata.LuminanceQuality, 1, 100); + Block8x8F luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + Block8x8F chrominanceQuantTable = default; + if (componentCount > 1) + { + int chromaQuality = Numerics.Clamp(this.chrominanceQuality ?? jpegMetadata.ChrominanceQuality, 1, 100); + this.subsample ??= chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + + chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + } // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); From eaeab7a03ae88e92c503d32725907783d404425e Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 20 Jul 2021 17:13:17 +0300 Subject: [PATCH 30/78] (WIP) quality --- .../Formats/Jpeg/JpegEncoderCore.cs | 47 ++++++++++++++----- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 16 +++++-- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 4c81c58dd..593937b92 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -102,18 +102,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Compute number of components based on color type in options. int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3; - // Initialize the quantization tables. // TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that - int lumaQuality = Numerics.Clamp(this.luminanceQuality ?? jpegMetadata.LuminanceQuality, 1, 100); - Block8x8F luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); - Block8x8F chrominanceQuantTable = default; - if (componentCount > 1) - { - int chromaQuality = Numerics.Clamp(this.chrominanceQuality ?? jpegMetadata.ChrominanceQuality, 1, 100); - this.subsample ??= chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; - - chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); - } + // Initialize the quantization tables. + this.InitQuantizationTables(componentCount, jpegMetadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable); // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); @@ -651,5 +642,39 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.buffer[3] = (byte)(length & 0xff); this.outputStream.Write(this.buffer, 0, 4); } + + /// + /// Initializes quntization tables. + /// + /// Color components count. + /// Jpeg metadata instance. + /// Output luminance quantization table. + /// Output chrominance quantization table. + private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) + { + // We take quality values in a hierarchical order: + // 1. Check if encoder has set quality + // 2. Check if metadata has special table for encoding + // 3. Check if metadata has set quality + // 4. Take default quality value - 75 + int lumaQuality = Numerics.Clamp( + this.luminanceQuality ?? metadata.LuminanceQuality ?? DefaultQualityValue, + min: 1, + max: 100); + + luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + chrominanceQuantTable = default; + if (componentCount > 1) + { + int chromaQuality = Numerics.Clamp( + this.chrominanceQuality ?? metadata.ChrominanceQuality ?? DefaultQualityValue, + min: 1, + max: 100); + + this.subsample ??= chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + + chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + } + } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 0579e7b5e..8b3332ef8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -24,7 +24,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The metadata to create an instance from. private JpegMetadata(JpegMetadata other) { - this.Quality = other.Quality; this.ColorType = other.ColorType; this.LumaQuantizationTable = other.LumaQuantizationTable; @@ -56,7 +55,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int LuminanceQuality { get; set; } + public int? LuminanceQuality { get; set; } /// /// Gets or sets the jpeg chrominance quality. @@ -65,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int ChrominanceQuality { get; set; } + public int? ChrominanceQuality { get; set; } /// /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. @@ -82,7 +81,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public int Quality { - get => (int)Math.Round((this.LuminanceQuality + this.ChrominanceQuality) / 2f); + [Obsolete("This accessor will soon be deprecated. Use LuminanceQuality and ChrominanceQuality getters instead.", error: false)] + get + { + const int defaultQuality = 75; + + int lumaQuality = this.LuminanceQuality ?? defaultQuality; + int chromaQuality = this.LuminanceQuality ?? lumaQuality; + return (int)Math.Round((lumaQuality + chromaQuality) / 2f); + } + set { this.LuminanceQuality = value; From 3fdefa7aeff7e9be1ed62911af315210a64bfaed Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 20 Jul 2021 17:29:49 +0300 Subject: [PATCH 31/78] Encoder now uses appropriate quality or provied quantization tables --- .../Formats/Jpeg/JpegDecoderCore.cs | 4 +- .../Formats/Jpeg/JpegEncoderCore.cs | 66 ++++++++++++++----- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 8 +-- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index b58e99a10..cf21dd226 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -835,7 +835,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // so we save it in the metadata if (!Quantization.EstimateLuminanceQuality(ref table, out int quality)) { - jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); + jpegMetadata.LuminanceQuantizationTable = table; } jpegMetadata.LuminanceQuality = quality; @@ -850,7 +850,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // so we save it in the metadata if (!Quantization.EstimateChrominanceQuality(ref table, out int quality)) { - jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); + jpegMetadata.ChromaQuantizationTable = table; } jpegMetadata.ChrominanceQuality = quality; diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 593937b92..fea24111c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -646,34 +646,66 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Initializes quntization tables. /// + /// + /// We take quality values in a hierarchical order: + /// 1. Check if encoder has set quality + /// 2. Check if metadata has special table for encoding + /// 3. Check if metadata has set quality + /// 4. Take default quality value - 75 + /// /// Color components count. /// Jpeg metadata instance. /// Output luminance quantization table. /// Output chrominance quantization table. private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) { - // We take quality values in a hierarchical order: - // 1. Check if encoder has set quality - // 2. Check if metadata has special table for encoding - // 3. Check if metadata has set quality - // 4. Take default quality value - 75 - int lumaQuality = Numerics.Clamp( - this.luminanceQuality ?? metadata.LuminanceQuality ?? DefaultQualityValue, - min: 1, - max: 100); - - luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + // encoder quality + if (this.luminanceQuality.HasValue) + { + int lumaQuality = Numerics.Clamp(this.luminanceQuality.Value, 1, 100); + luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + } + + // non-standard table + else if (metadata.LuminanceQuantizationTable.HasValue) + { + luminanceQuantTable = metadata.LuminanceQuantizationTable.Value; + } + + // metadata or default quality + else + { + int lumaQuality = Numerics.Clamp(metadata.LuminanceQuality ?? DefaultQualityValue, 1, 100); + luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + } + chrominanceQuantTable = default; if (componentCount > 1) { - int chromaQuality = Numerics.Clamp( - this.chrominanceQuality ?? metadata.ChrominanceQuality ?? DefaultQualityValue, - min: 1, - max: 100); + int chromaQuality; - this.subsample ??= chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + // encoder quality + if (this.chrominanceQuality.HasValue) + { + chromaQuality = Numerics.Clamp(this.chrominanceQuality.Value, 1, 100); + chrominanceQuantTable = Quantization.ScaleLuminanceTable(chromaQuality); + } + + // non-standard table + else if (metadata.ChromaQuantizationTable.HasValue) + { + chromaQuality = metadata.ChrominanceQuality.Value; + chrominanceQuantTable = metadata.ChromaQuantizationTable.Value; + } + + // metadata or default quality + else + { + chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality ?? DefaultQualityValue, 1, 100); + chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + } - chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 8b3332ef8..4a58c6946 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { this.ColorType = other.ColorType; - this.LumaQuantizationTable = other.LumaQuantizationTable; + this.LuminanceQuantizationTable = other.LuminanceQuantizationTable; this.ChromaQuantizationTable = other.ChromaQuantizationTable; this.LuminanceQuality = other.LuminanceQuality; this.ChrominanceQuality = other.ChrominanceQuality; @@ -38,7 +38,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Would be null if jpeg was encoded using table from ITU spec /// - internal Block8x8? LumaQuantizationTable { get; set; } + internal Block8x8F? LuminanceQuantizationTable { get; set; } /// /// Gets or sets chrominance qunatization table derived from jpeg image. @@ -46,7 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Would be null if jpeg was encoded using table from ITU spec /// - internal Block8x8? ChromaQuantizationTable { get; set; } + internal Block8x8F? ChromaQuantizationTable { get; set; } /// /// Gets or sets the jpeg luminance quality. @@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. /// - public bool UsesStandardLuminanceTable => !this.LumaQuantizationTable.HasValue; + public bool UsesStandardLuminanceTable => !this.LuminanceQuantizationTable.HasValue; /// /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. From 376d190d2a81fb672446a7eb3c30d530f003a1d8 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 20 Jul 2021 21:05:04 +0300 Subject: [PATCH 32/78] Fixed encoder metadata usage --- .../Formats/Jpeg/JpegEncoderCore.cs | 32 +------- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 80 ++++++++++++------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index fea24111c..bc3a5f1b5 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -23,11 +23,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// internal sealed unsafe class JpegEncoderCore : IImageEncoderInternals { - /// - /// Default JPEG encoding quality for both luminance and chominance tables. - /// - private const int DefaultQualityValue = 75; - /// /// The number of quantization tables. /// @@ -659,50 +654,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Output chrominance quantization table. private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) { - // encoder quality if (this.luminanceQuality.HasValue) { int lumaQuality = Numerics.Clamp(this.luminanceQuality.Value, 1, 100); luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); } - - // non-standard table - else if (metadata.LuminanceQuantizationTable.HasValue) - { - luminanceQuantTable = metadata.LuminanceQuantizationTable.Value; - } - - // metadata or default quality else { - int lumaQuality = Numerics.Clamp(metadata.LuminanceQuality ?? DefaultQualityValue, 1, 100); - luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + luminanceQuantTable = metadata.LuminanceQuantizationTable; } chrominanceQuantTable = default; if (componentCount > 1) { int chromaQuality; - - // encoder quality if (this.chrominanceQuality.HasValue) { chromaQuality = Numerics.Clamp(this.chrominanceQuality.Value, 1, 100); chrominanceQuantTable = Quantization.ScaleLuminanceTable(chromaQuality); } - - // non-standard table - else if (metadata.ChromaQuantizationTable.HasValue) - { - chromaQuality = metadata.ChrominanceQuality.Value; - chrominanceQuantTable = metadata.ChromaQuantizationTable.Value; - } - - // metadata or default quality else { - chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality ?? DefaultQualityValue, 1, 100); - chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); + chromaQuality = metadata.ChrominanceQuality; + chrominanceQuantTable = metadata.ChromaQuantizationTable; } this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 4a58c6946..6b7f42bb2 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,6 +11,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { + /// + /// Default JPEG quality for both luminance and chominance tables. + /// + private const int DefaultQualityValue = 75; + + private Block8x8F? lumaQuantTable; + private Block8x8F? chromaQuantTable; + + private int? lumaQuality; + private int? chromaQuality; + /// /// Initializes a new instance of the class. /// @@ -33,20 +44,40 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Gets or sets luminance qunatization table derived from jpeg image. + /// Gets or sets luminance qunatization table for jpeg image. /// - /// - /// Would be null if jpeg was encoded using table from ITU spec - /// - internal Block8x8F? LuminanceQuantizationTable { get; set; } + internal Block8x8F LuminanceQuantizationTable + { + get + { + if (this.lumaQuantTable.HasValue) + { + return this.lumaQuantTable.Value; + } + + return Quantization.ScaleLuminanceTable(this.LuminanceQuality); + } + + set => this.lumaQuantTable = value; + } /// - /// Gets or sets chrominance qunatization table derived from jpeg image. + /// Gets or sets chrominance qunatization table for jpeg image. /// - /// - /// Would be null if jpeg was encoded using table from ITU spec - /// - internal Block8x8F? ChromaQuantizationTable { get; set; } + internal Block8x8F ChromaQuantizationTable + { + get + { + if (this.chromaQuantTable.HasValue) + { + return this.chromaQuantTable.Value; + } + + return Quantization.ScaleChrominanceTable(this.ChrominanceQuality); + } + + set => this.chromaQuantTable = value; + } /// /// Gets or sets the jpeg luminance quality. @@ -55,7 +86,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int? LuminanceQuality { get; set; } + public int LuminanceQuality + { + get => this.lumaQuality ?? DefaultQualityValue; + set => this.lumaQuality = value; + } /// /// Gets or sets the jpeg chrominance quality. @@ -64,30 +99,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int? ChrominanceQuality { get; set; } - - /// - /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. - /// - public bool UsesStandardLuminanceTable => !this.LuminanceQuantizationTable.HasValue; - - /// - /// Gets a value indicating whether jpeg luminance data was encoded using ITU complient quantization table. - /// - public bool UsesStandardChrominanceTable => !this.ChromaQuantizationTable.HasValue; + public int ChrominanceQuality + { + get => this.chromaQuality ?? DefaultQualityValue; + set => this.chromaQuality = value; + } /// /// Gets or sets the encoded quality. /// public int Quality { - [Obsolete("This accessor will soon be deprecated. Use LuminanceQuality and ChrominanceQuality getters instead.", error: false)] get { - const int defaultQuality = 75; - - int lumaQuality = this.LuminanceQuality ?? defaultQuality; - int chromaQuality = this.LuminanceQuality ?? lumaQuality; + int lumaQuality = this.lumaQuality ?? DefaultQualityValue; + int chromaQuality = this.chromaQuality ?? lumaQuality; return (int)Math.Round((lumaQuality + chromaQuality) / 2f); } From e9fddd0a48b2a9367c8c40d2a2b57ab1c08d7852 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 21 Jul 2021 00:27:31 +0300 Subject: [PATCH 33/78] Fixed subsample assignment in the encoder --- src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index bc3a5f1b5..9c66cfe0c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -679,7 +679,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg chrominanceQuantTable = metadata.ChromaQuantizationTable; } - this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + if (!this.subsample.HasValue) + { + this.subsample = chromaQuality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; + } } } } From a9a4f6f51d32682543a671914c70553b82e38b09 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 22 Jul 2021 05:59:53 +0300 Subject: [PATCH 34/78] Removed luma/chroma quality from the encoder --- .../Formats/Jpeg/IJpegEncoderOptions.cs | 15 ++----- src/ImageSharp/Formats/Jpeg/JpegEncoder.cs | 40 +------------------ .../Formats/Jpeg/JpegEncoderCore.cs | 4 +- 3 files changed, 8 insertions(+), 51 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs index d2921ad4c..a9f564b45 100644 --- a/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs +++ b/src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs @@ -9,18 +9,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg internal interface IJpegEncoderOptions { /// - /// Gets the quality, that will be used to encode the luminance data of the image. - /// Quality index must be between 0 and 100 (compression from max to min). + /// 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. /// - /// The quality of the jpg image from 0 to 100. - int? LuminanceQuality { get; } - - /// - /// Gets the quality, that will be used to encode the chrominance data of 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? ChrominanceQuality { get; } + public int? Quality { get; set; } /// /// Gets the subsample ration, that will be used to encode the image. diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs index 27597a0f9..5e199b420 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -14,43 +13,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions { - /// - /// 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 - { - [Obsolete("This accessor will soon be deprecated. Use LuminanceQuality and ChrominanceQuality getters instead.", error: false)] - get - { - const int defaultQuality = 75; - - int lumaQuality = this.LuminanceQuality ?? defaultQuality; - int chromaQuality = this.LuminanceQuality ?? lumaQuality; - return (int)Math.Round((lumaQuality + chromaQuality) / 2f); - } - - set - { - this.LuminanceQuality = value; - this.ChrominanceQuality = value; - } - } - - /// - /// Gets or sets the quality, that will be used to encode luminance image data. - /// Quality index must be between 0 and 100 (compression from max to min). - /// Defaults to 75. - /// - public int? LuminanceQuality { get; set; } - - /// - /// Gets or sets the quality, that will be used to encode chrominance image data. - /// Quality index must be between 0 and 100 (compression from max to min). - /// Defaults to 75. - /// - public int? ChrominanceQuality { get; set; } + /// + 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 9c66cfe0c..248772b28 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -64,8 +64,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The options public JpegEncoderCore(IJpegEncoderOptions options) { - this.luminanceQuality = options.LuminanceQuality; - this.chrominanceQuality = options.ChrominanceQuality; + this.luminanceQuality = options.Quality; + this.chrominanceQuality = options.Quality; this.subsample = options.Subsample; this.colorType = options.ColorType; } From 5bdc5a0a1544d95cfc01d81312604a5e9e7331fe Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 22 Jul 2021 06:35:22 +0300 Subject: [PATCH 35/78] Added remarks --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 6b7f42bb2..92f3a92ba 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -108,6 +108,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Gets or sets the encoded quality. /// + /// + /// Note that jpeg image can have different quality for luminance and chrominance components. + /// This property return average for both qualities and sets both qualities to the given value. + /// public int Quality { get From 74534665dd476becce31d5601090969a688568aa Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 22 Jul 2021 06:35:31 +0300 Subject: [PATCH 36/78] Fixed comments --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 92f3a92ba..a4f968d7c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -129,7 +129,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Gets or sets the encoded quality. + /// Gets or sets the color type. /// public JpegColorType? ColorType { get; set; } From 45292fbc09779ac8d7ed092987eda5088f344674 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 22 Jul 2021 06:36:40 +0300 Subject: [PATCH 37/78] Fixed clamping --- src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 248772b28..69ac5de71 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -675,7 +675,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } else { - chromaQuality = metadata.ChrominanceQuality; + chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality, 1, 100); chrominanceQuantTable = metadata.ChromaQuantizationTable; } From 404639ff8c7e3dad659841d04fb54bb47b017de7 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Fri, 23 Jul 2021 17:48:34 +0300 Subject: [PATCH 38/78] Final API improvements --- .../Formats/Jpeg/Components/Quantization.cs | 5 ++++ .../Formats/Jpeg/JpegEncoderCore.cs | 2 +- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 28 ++++--------------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index fb477dda8..fc602b7f8 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -23,6 +23,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// public const int MinQualityFactor = 1; + /// + /// Default JPEG quality for both luminance and chominance tables. + /// + public const int DefaultQualityFactor = 75; + /// /// Represents lowest quality setting which can be estimated with enough confidence. /// Any quality below it results in a highly compressed jpeg image diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 69ac5de71..828e03de7 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -675,7 +675,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } else { - chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality, 1, 100); + chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality ?? Quantization.DefaultQualityFactor, 1, 100); chrominanceQuantTable = metadata.ChromaQuantizationTable; } diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index a4f968d7c..0d95599e2 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,17 +11,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { - /// - /// Default JPEG quality for both luminance and chominance tables. - /// - private const int DefaultQualityValue = 75; - private Block8x8F? lumaQuantTable; private Block8x8F? chromaQuantTable; - private int? lumaQuality; - private int? chromaQuality; - /// /// Initializes a new instance of the class. /// @@ -55,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return this.lumaQuantTable.Value; } - return Quantization.ScaleLuminanceTable(this.LuminanceQuality); + return Quantization.ScaleLuminanceTable(this.LuminanceQuality ?? Quantization.DefaultQualityFactor); } set => this.lumaQuantTable = value; @@ -73,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return this.chromaQuantTable.Value; } - return Quantization.ScaleChrominanceTable(this.ChrominanceQuality); + return Quantization.ScaleChrominanceTable(this.ChrominanceQuality ?? Quantization.DefaultQualityFactor); } set => this.chromaQuantTable = value; @@ -86,11 +78,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int LuminanceQuality - { - get => this.lumaQuality ?? DefaultQualityValue; - set => this.lumaQuality = value; - } + public int? LuminanceQuality { get; set; } /// /// Gets or sets the jpeg chrominance quality. @@ -99,11 +87,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int ChrominanceQuality - { - get => this.chromaQuality ?? DefaultQualityValue; - set => this.chromaQuality = value; - } + public int? ChrominanceQuality { get; set; } /// /// Gets or sets the encoded quality. @@ -116,8 +100,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { get { - int lumaQuality = this.lumaQuality ?? DefaultQualityValue; - int chromaQuality = this.chromaQuality ?? lumaQuality; + int lumaQuality = this.LuminanceQuality ?? Quantization.DefaultQualityFactor; + int chromaQuality = this.ChrominanceQuality ?? lumaQuality; return (int)Math.Round((lumaQuality + chromaQuality) / 2f); } From 0e1aa6a9775d14d5ce7f930238a02171efd18071 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 27 Jul 2021 16:58:20 +0300 Subject: [PATCH 39/78] Made Quality nullable --- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 0d95599e2..77d27ee93 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int? LuminanceQuality { get; set; } + internal int? LuminanceQuality { get; set; } /// /// Gets or sets the jpeg chrominance quality. @@ -87,22 +87,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - public int? ChrominanceQuality { get; set; } + internal int? ChrominanceQuality { get; set; } /// /// Gets or sets the encoded quality. /// /// /// Note that jpeg image can have different quality for luminance and chrominance components. - /// This property return average for both qualities and sets both qualities to the given value. + /// This property returns maximum value of luma/chroma qualities. /// - public int Quality + public int? Quality { get { - int lumaQuality = this.LuminanceQuality ?? Quantization.DefaultQualityFactor; - int chromaQuality = this.ChrominanceQuality ?? lumaQuality; - return (int)Math.Round((lumaQuality + chromaQuality) / 2f); + // Jpeg always has a luminance table thus it must have a luminance quality derived from it + if (!this.LuminanceQuality.HasValue) + { + return null; + } + + // Jpeg might not have a chrominance table + if (!this.ChrominanceQuality.HasValue) + { + return this.LuminanceQuality.Value; + } + + // Theoretically, luma quality would always be greater or equal to chroma quality + // But we've already encountered images which can have higher quality of chroma components + return Math.Max(this.LuminanceQuality.Value, this.ChrominanceQuality.Value); } set From 7a99d6f8a318da7b9cddf3ec1600926b8917d2f5 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Tue, 27 Jul 2021 23:46:54 +0300 Subject: [PATCH 40/78] [Rollback] Removed table inheritance for jpeg re-encoding --- .../Formats/Jpeg/Components/Quantization.cs | 51 +++---------- .../Formats/Jpeg/JpegDecoderCore.cs | 20 +---- .../Formats/Jpeg/JpegEncoderCore.cs | 39 ++++------ src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 76 ++++++------------- .../Formats/Jpg/QuantizationTests.cs | 60 +-------------- 5 files changed, 56 insertions(+), 190 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index fc602b7f8..7e528d056 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -40,30 +40,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// public const int QualityEstimationConfidenceUpperThreshold = 98; - /// - /// Threshold at which given luminance quantization table should be considered 'standard'. - /// Bigger the variance - more likely it to be a non-ITU complient table. - /// - /// - /// Jpeg does not define either 'quality' nor 'standard quantization table' properties - /// so this is purely a practical value derived from tests. - /// For actual variances output against standard table see tests at Formats.Jpg.QuantizationTests.PrintVariancesFromStandardTables_*. - /// Actual value is 2.3629059983706604, truncated unsignificant part. - /// - public const double StandardLuminanceTableVarianceThreshold = 2.36291; - - /// - /// Threshold at which given chrominance quantization table should be considered 'standard'. - /// Bigger the variance - more likely it to be a non-ITU complient table. - /// - /// - /// Jpeg does not define either 'quality' nor 'standard quantization table' properties - /// so this is purely a practical value derived from tests. - /// For actual variances output against standard table see tests at Formats.Jpg.QuantizationTests.PrintVariancesFromStandardTables_*. - /// Actual value is 0.8949631033036098, truncated unsignificant part. - /// - public const double StandardChrominanceTableVarianceThreshold = 0.894963; - /// /// Gets the unscaled luminance quantization table in zig-zag order. Each /// encoder copies and scales the tables according to its quality parameter. @@ -113,25 +89,24 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Quantization to estimate against. /// Estimated quality /// indicating if given table is target-complient - public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out int quality) + public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. double comparePercent; double sumPercent = 0; - double sumPercentSqr = 0; // Corner case - all 1's => 100 quality // It would fail to deduce using algorithm below without this check if (table.EqualsToScalar(1)) { // While this is a 100% to be 100 quality, any given table can be scaled to all 1's. - // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' will affect result filesize drastically. + // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' which will affect result filesize drastically. // Quality=100 shouldn't be used in usual use case. - quality = 100; - return 0; + return 100; } + int quality; for (int i = 0; i < Block8x8F.Size; i++) { float coeff = table[i]; @@ -152,7 +127,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components } sumPercent += comparePercent; - sumPercentSqr += comparePercent * comparePercent; } // Perform some statistical analysis of the quality factor @@ -160,7 +134,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // table being a scaled version of the "standard" tables. // If the variance is high, it is unlikely to be the case. sumPercent /= 64.0; - sumPercentSqr /= 64.0; // Generate the equivalent IJQ "quality" factor if (sumPercent <= 100.0) @@ -172,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components quality = (int)Math.Round(5000.0 / sumPercent); } - return sumPercentSqr - (sumPercent * sumPercent); + return quality; } /// @@ -182,11 +155,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out int quality) - { - double variance = EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, out quality); - return variance <= StandardLuminanceTableVarianceThreshold; - } + public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable) + => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance); /// /// Estimates jpeg quality based on quantization table in zig-zag order. @@ -195,11 +165,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out int quality) - { - double variance = EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, out quality); - return variance <= StandardChrominanceTableVarianceThreshold; - } + public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable) + => EstimateQuality(ref chrominanceTable, UnscaledQuant_Luminance); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int QualityToScale(int quality) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index cf21dd226..b6d5aafd1 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -830,30 +830,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // luminance table case 0: { - // if quantization table is non-complient to stardard itu table - // we can't reacreate it later with calculated quality as this is an approximation - // so we save it in the metadata - if (!Quantization.EstimateLuminanceQuality(ref table, out int quality)) - { - jpegMetadata.LuminanceQuantizationTable = table; - } - - jpegMetadata.LuminanceQuality = quality; + jpegMetadata.LuminanceQuality = Quantization.EstimateLuminanceQuality(ref table); break; } // chrominance table case 1: { - // if quantization table is non-complient to stardard itu table - // we can't reacreate it later with calculated quality as this is an approximation - // so we save it in the metadata - if (!Quantization.EstimateChrominanceQuality(ref table, out int quality)) - { - jpegMetadata.ChromaQuantizationTable = table; - } - - jpegMetadata.ChrominanceQuality = quality; + jpegMetadata.ChrominanceQuality = Quantization.EstimateChrominanceQuality(ref table); break; } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 828e03de7..88d96f554 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -41,12 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The quality, that will be used to encode the image. /// - private readonly int? luminanceQuality; - - /// - /// The quality, that will be used to encode the image. - /// - private readonly int? chrominanceQuality; + private readonly int? quality; /// /// Gets or sets the subsampling method to use. @@ -64,8 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The options public JpegEncoderCore(IJpegEncoderOptions options) { - this.luminanceQuality = options.Quality; - this.chrominanceQuality = options.Quality; + this.quality = options.Quality; this.subsample = options.Subsample; this.colorType = options.ColorType; } @@ -654,30 +648,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Output chrominance quantization table. private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) { - if (this.luminanceQuality.HasValue) + int lumaQuality; + int chromaQuality; + if (this.quality.HasValue) { - int lumaQuality = Numerics.Clamp(this.luminanceQuality.Value, 1, 100); - luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + lumaQuality = this.quality.Value; + chromaQuality = this.quality.Value; } else { - luminanceQuantTable = metadata.LuminanceQuantizationTable; + lumaQuality = metadata.LuminanceQuality; + chromaQuality = metadata.ChrominanceQuality; } + // Luminance + lumaQuality = Numerics.Clamp(lumaQuality, 1, 100); + luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + + // Chrominance chrominanceQuantTable = default; if (componentCount > 1) { - int chromaQuality; - if (this.chrominanceQuality.HasValue) - { - chromaQuality = Numerics.Clamp(this.chrominanceQuality.Value, 1, 100); - chrominanceQuantTable = Quantization.ScaleLuminanceTable(chromaQuality); - } - else - { - chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality ?? Quantization.DefaultQualityFactor, 1, 100); - chrominanceQuantTable = metadata.ChromaQuantizationTable; - } + chromaQuality = Numerics.Clamp(chromaQuality, 1, 100); + chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); if (!this.subsample.HasValue) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 77d27ee93..1b17bdce7 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,8 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { - private Block8x8F? lumaQuantTable; - private Block8x8F? chromaQuantTable; + private int? luminanceQuality; + private int? chrominanceQuality; /// /// Initializes a new instance of the class. @@ -29,46 +29,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { this.ColorType = other.ColorType; - this.LuminanceQuantizationTable = other.LuminanceQuantizationTable; - this.ChromaQuantizationTable = other.ChromaQuantizationTable; - this.LuminanceQuality = other.LuminanceQuality; - this.ChrominanceQuality = other.ChrominanceQuality; - } - - /// - /// Gets or sets luminance qunatization table for jpeg image. - /// - internal Block8x8F LuminanceQuantizationTable - { - get - { - if (this.lumaQuantTable.HasValue) - { - return this.lumaQuantTable.Value; - } - - return Quantization.ScaleLuminanceTable(this.LuminanceQuality ?? Quantization.DefaultQualityFactor); - } - - set => this.lumaQuantTable = value; - } - - /// - /// Gets or sets chrominance qunatization table for jpeg image. - /// - internal Block8x8F ChromaQuantizationTable - { - get - { - if (this.chromaQuantTable.HasValue) - { - return this.chromaQuantTable.Value; - } - - return Quantization.ScaleChrominanceTable(this.ChrominanceQuality ?? Quantization.DefaultQualityFactor); - } - - set => this.chromaQuantTable = value; + this.luminanceQuality = other.luminanceQuality; + this.chrominanceQuality = other.chrominanceQuality; } /// @@ -78,7 +40,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - internal int? LuminanceQuality { get; set; } + internal int LuminanceQuality + { + get => this.luminanceQuality ?? Quantization.DefaultQualityFactor; + set => this.luminanceQuality = value; + } /// /// Gets or sets the jpeg chrominance quality. @@ -87,7 +53,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - internal int? ChrominanceQuality { get; set; } + internal int ChrominanceQuality + { + get => this.chrominanceQuality ?? Quantization.DefaultQualityFactor; + set => this.chrominanceQuality = value; + } /// /// Gets or sets the encoded quality. @@ -96,25 +66,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Note that jpeg image can have different quality for luminance and chrominance components. /// This property returns maximum value of luma/chroma qualities. /// - public int? Quality + public int Quality { get { // Jpeg always has a luminance table thus it must have a luminance quality derived from it - if (!this.LuminanceQuality.HasValue) + if (!this.luminanceQuality.HasValue) { - return null; + return Quantization.DefaultQualityFactor; } - // Jpeg might not have a chrominance table - if (!this.ChrominanceQuality.HasValue) + int lumaQuality = this.luminanceQuality.Value; + + // Jpeg might not have a chrominance table - return luminance quality (grayscale images) + if (!this.chrominanceQuality.HasValue) { - return this.LuminanceQuality.Value; + return lumaQuality; } + int chromaQuality = this.chrominanceQuality.Value; + // Theoretically, luma quality would always be greater or equal to chroma quality // But we've already encountered images which can have higher quality of chroma components - return Math.Max(this.LuminanceQuality.Value, this.ChrominanceQuality.Value); + return Math.Max(lumaQuality, chromaQuality); } set diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs index 8ed14bd81..2f673ef2f 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -13,13 +13,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Trait("Format", "Jpg")] public class QuantizationTests { - public QuantizationTests(ITestOutputHelper output) - { - this.Output = output; - } - - private ITestOutputHelper Output { get; } - [Fact] public void QualityEstimationFromStandardEncoderTables_Luminance() { @@ -28,10 +21,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg for (int quality = firstIndex; quality <= lastIndex; quality++) { Block8x8F table = JpegQuantization.ScaleLuminanceTable(quality); - bool isStrandard = JpegQuantization.EstimateLuminanceQuality(ref table, out int actualQuality); + int estimatedQuality = JpegQuantization.EstimateLuminanceQuality(ref table); - Assert.True(isStrandard, $"Standard table is estimated to be non-spec complient at quality level {quality}"); - Assert.Equal(quality, actualQuality); + Assert.True(quality.Equals(estimatedQuality), $"Failed to estimate luminance quality for standard table at quality level {quality}"); } } @@ -43,54 +35,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg for (int quality = firstIndex; quality <= lastIndex; quality++) { Block8x8F table = JpegQuantization.ScaleChrominanceTable(quality); - bool isStrandard = JpegQuantization.EstimateChrominanceQuality(ref table, out int actualQuality); + int estimatedQuality = JpegQuantization.EstimateChrominanceQuality(ref table); - Assert.True(isStrandard, $"Standard table is estimated to be non-spec complient at quality level {quality}"); - Assert.Equal(quality, actualQuality); + Assert.True(quality.Equals(estimatedQuality), $"Failed to estimate chrominance quality for standard table at quality level {quality}"); } } - - [Fact(Skip = "Debug only, enable manually!")] - public void PrintVariancesFromStandardTables_Luminance() - { - this.Output.WriteLine("Variances for Luminance table.\nQuality levels 25-100:"); - - double minVariance = double.MaxValue; - double maxVariance = double.MinValue; - - for (int q = JpegQuantization.QualityEstimationConfidenceLowerThreshold; q <= JpegQuantization.MaxQualityFactor; q++) - { - Block8x8F table = JpegQuantization.ScaleLuminanceTable(q); - double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Luminance, out int quality); - - minVariance = Math.Min(minVariance, variance); - maxVariance = Math.Max(maxVariance, variance); - - this.Output.WriteLine($"q={q}\t{variance}\test. q: {quality}"); - } - - this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); - } - - [Fact(Skip = "Debug only, enable manually!")] - public void PrintVariancesFromStandardTables_Chrominance() - { - this.Output.WriteLine("Variances for Chrominance table.\nQuality levels 25-100:"); - - double minVariance = double.MaxValue; - double maxVariance = double.MinValue; - for (int q = JpegQuantization.QualityEstimationConfidenceLowerThreshold; q <= JpegQuantization.MaxQualityFactor; q++) - { - Block8x8F table = JpegQuantization.ScaleChrominanceTable(q); - double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Chrominance, out int quality); - - minVariance = Math.Min(minVariance, variance); - maxVariance = Math.Max(maxVariance, variance); - - this.Output.WriteLine($"q={q}\t{variance}\test. q: {quality}"); - } - - this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); - } } } From c64ac5f8ecaa63707f72eaabf8a9ba94032feb04 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 00:19:52 +0300 Subject: [PATCH 41/78] Comments, quantization table scaling with clamp call --- .../Formats/Jpeg/Components/Quantization.cs | 15 ++------------- src/ImageSharp/Formats/Jpeg/JpegMetadata.cs | 7 +++++++ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 7e528d056..394ad71af 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -179,19 +179,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components Block8x8F table = default; for (int j = 0; j < Block8x8F.Size; j++) { - int x = unscaledTable[j]; - x = ((x * scale) + 50) / 100; - if (x < 1) - { - x = 1; - } - - if (x > 255) - { - x = 255; - } - - table[j] = x; + int x = ((unscaledTable[j] * scale) + 50) / 100; + table[j] = Numerics.Clamp(x, 1, 255); } return table; diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 1b17bdce7..0a4b970f4 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,7 +11,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { + /// + /// Backing field for + /// private int? luminanceQuality; + + /// + /// Backing field for + /// private int? chrominanceQuality; /// From e205bea7025257b809b63bc31fb4a1166a164510 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 00:58:36 +0300 Subject: [PATCH 42/78] Tests --- .../Formats/Jpg/Block8x8FTests.cs | 91 +++++++++++++++++++ .../Formats/Jpg/JpegMetadataTests.cs | 39 ++++++++ 2 files changed, 130 insertions(+) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs index 4effc52b2..c68b0ffa8 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs @@ -493,5 +493,96 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Equal(data[i], dest[i]); } } + + [Fact] + public void EqualsToScalar_AllOne() + { + static void RunTest() + { + // Fill matrix with valid value + Block8x8F block = default; + for (int i = 0; i < Block8x8F.Size; i++) + { + block[i] = 1; + } + + bool isEqual = block.EqualsToScalar(1); + Assert.True(isEqual); + } + + // 2 paths: + // 1. DisableFMA - call avx implementation + // 3. DisableAvx2 - call fallback code of float implementation + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + + [Theory] + [InlineData(10)] + public void EqualsToScalar_OneOffEachPosition(int equalsTo) + { + static void RunTest(string serializedEqualsTo) + { + int equalsTo = FeatureTestRunner.Deserialize(serializedEqualsTo); + int offValue = 0; + + // Fill matrix with valid value + Block8x8F block = default; + for (int i = 0; i < Block8x8F.Size; i++) + { + block[i] = equalsTo; + } + + // Assert with invalid values at different positions + for (int i = 0; i < Block8x8F.Size; i++) + { + block[i] = offValue; + + bool isEqual = block.EqualsToScalar(equalsTo); + Assert.False(isEqual, $"False equality:\n{block}"); + + // restore valid value for next iteration assertion + block[i] = equalsTo; + } + } + + // 2 paths: + // 1. DisableFMA - call avx implementation + // 3. DisableAvx2 - call fallback code of float implementation + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + equalsTo, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } + + [Theory] + [InlineData(39)] + public void EqualsToScalar_Valid(int equalsTo) + { + static void RunTest(string serializedEqualsTo) + { + int equalsTo = FeatureTestRunner.Deserialize(serializedEqualsTo); + + // Fill matrix with valid value + Block8x8F block = default; + for (int i = 0; i < Block8x8F.Size; i++) + { + block[i] = equalsTo; + } + + // Assert + bool isEqual = block.EqualsToScalar(equalsTo); + Assert.True(isEqual); + } + + // 2 paths: + // 1. DisableFMA - call avx implementation + // 3. DisableAvx2 - call fallback code of float implementation + FeatureTestRunner.RunWithHwIntrinsicsFeature( + RunTest, + equalsTo, + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs index 503ede129..56bf207b9 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs @@ -21,5 +21,44 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.False(meta.Quality.Equals(clone.Quality)); Assert.False(meta.ColorType.Equals(clone.ColorType)); } + + [Fact] + public void Quality_DefaultQuality() + { + var meta = new JpegMetadata(); + + Assert.Equal(meta.Quality, ImageSharp.Formats.Jpeg.Components.Quantization.DefaultQualityFactor); + } + + [Fact] + public void Quality_LuminanceOnlyQuality() + { + int quality = 50; + + var meta = new JpegMetadata { LuminanceQuality = quality }; + + Assert.Equal(meta.Quality, quality); + } + + [Fact] + public void Quality_BothComponentsQuality() + { + int quality = 50; + + var meta = new JpegMetadata { LuminanceQuality = quality, ChrominanceQuality = quality }; + + Assert.Equal(meta.Quality, quality); + } + + [Fact] + public void Quality_ReturnsMaxQuality() + { + int qualityLuma = 50; + int qualityChroma = 30; + + var meta = new JpegMetadata { LuminanceQuality = qualityLuma, ChrominanceQuality = qualityChroma }; + + Assert.Equal(meta.Quality, qualityLuma); + } } } From 99b24fefd35d18bb47c2007c8f50268877eb15c3 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 01:26:14 +0300 Subject: [PATCH 43/78] Added images to quality estimation tests --- .../ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs | 4 +++- tests/ImageSharp.Tests/TestImages.cs | 2 ++ tests/Images/Input/Jpg/baseline/forest_bridge.jpg | 3 +++ tests/Images/Input/Jpg/progressive/winter.jpg | 3 +++ 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/Images/Input/Jpg/baseline/forest_bridge.jpg create mode 100644 tests/Images/Input/Jpg/progressive/winter.jpg diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index 9d4aea453..403eeaf90 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -55,7 +55,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { { TestImages.Jpeg.Baseline.Calliphora, 80 }, { TestImages.Jpeg.Progressive.Fb, 75 }, - { TestImages.Jpeg.Issues.IncorrectQuality845, 98 } + { TestImages.Jpeg.Issues.IncorrectQuality845, 98 }, + { TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, 89 }, + { TestImages.Jpeg.Progressive.Winter, 80 } }; [Theory] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 6d2f65f57..d7fd3b569 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -157,6 +157,7 @@ namespace SixLabors.ImageSharp.Tests public const string Fb = "Jpg/progressive/fb.jpg"; public const string Progress = "Jpg/progressive/progress.jpg"; public const string Festzug = "Jpg/progressive/Festzug.jpg"; + public const string Winter = "Jpg/progressive/winter.jpg"; public static class Bad { @@ -198,6 +199,7 @@ namespace SixLabors.ImageSharp.Tests public const string Iptc = "Jpg/baseline/iptc.jpg"; public const string App13WithEmptyIptc = "Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg"; public const string HistogramEqImage = "Jpg/baseline/640px-Unequalized_Hawkes_Bay_NZ.jpg"; + public const string ForestBridgeDifferentComponentsQuality = "Jpg/baseline/forest_bridge.jpg"; public static readonly string[] All = { diff --git a/tests/Images/Input/Jpg/baseline/forest_bridge.jpg b/tests/Images/Input/Jpg/baseline/forest_bridge.jpg new file mode 100644 index 000000000..a487bb9e7 --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/forest_bridge.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56b3db3d0e146ee7fe27f8fbda4bccc1483e18104bfc747cac75a2ec03d65647 +size 1936782 diff --git a/tests/Images/Input/Jpg/progressive/winter.jpg b/tests/Images/Input/Jpg/progressive/winter.jpg new file mode 100644 index 000000000..bc08d8be0 --- /dev/null +++ b/tests/Images/Input/Jpg/progressive/winter.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d377b70cedfb9d25f1ae0244dcf2edb000540aa4a8925cce57f810f7efd0dc84 +size 234976 From 906ac84d42e9298982cc0d90205c8e64e7730bd3 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 01:38:13 +0300 Subject: [PATCH 44/78] Fixed invalid chrominance quality estimation --- src/ImageSharp/Formats/Jpeg/Components/Quantization.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 394ad71af..74fb19f54 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -166,7 +166,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable) - => EstimateQuality(ref chrominanceTable, UnscaledQuant_Luminance); + => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int QualityToScale(int quality) From 7463b49e7b9bfb5d74af511610dbfc9e893d13ef Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 01:43:06 +0300 Subject: [PATCH 45/78] Used nint in Unsafe.Add calls --- src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index d55dfced7..4cf8be44f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -845,7 +845,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components var targetVector = Vector256.Create(value); ref Vector256 blockStride = ref this.V0; - for (int i = 0; i < RowCount; i++) + for (nint i = 0; i < RowCount; i++) { Vector256 areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector); if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask) @@ -860,7 +860,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { ref float scalars = ref Unsafe.As(ref this); - for (int i = 0; i < Size; i++) + for (nint i = 0; i < Size; i++) { if ((int)Unsafe.Add(ref scalars, i) != value) { From eb6888a3787b7d2f92f31125b4e244527bf5514e Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 01:47:12 +0300 Subject: [PATCH 46/78] Fixed warnings --- .../Formats/Jpeg/Components/Quantization.cs | 11 +++-------- src/ImageSharp/Formats/Jpeg/JpegDecoder.cs | 2 -- .../ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs | 2 -- .../ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs | 1 - 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 74fb19f54..8e5f928b0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Text; namespace SixLabors.ImageSharp.Formats.Jpeg.Components { @@ -87,8 +85,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// /// Input quantization table. /// Quantization to estimate against. - /// Estimated quality - /// indicating if given table is target-complient + /// Estimated quality public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target) { // This method can be SIMD'ified if standard table is injected as Block8x8F. @@ -152,8 +149,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// Luminance quantization table. - /// Output jpeg quality. - /// indicating if given table is ITU-complient. + /// Estimated quality [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable) => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance); @@ -162,8 +158,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// Chrominance quantization table. - /// Output jpeg quality. - /// indicating if given table is ITU-complient. + /// Estimated quality [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable) => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance); diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 39b8e492f..b0bdbf0ed 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -4,8 +4,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs index 2f673ef2f..03f7020c0 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -1,10 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.Formats.Jpeg.Components; using Xunit; -using Xunit.Abstractions; using JpegQuantization = SixLabors.ImageSharp.Formats.Jpeg.Components.Quantization; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 0b819bf13..40b9e6867 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Linq; -using System.Threading; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; From 8b12623f7bcace057b5340d3848a58416d7a3c3e Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 01:50:04 +0300 Subject: [PATCH 47/78] Revert "Used nint in Unsafe.Add calls" This reverts commit 7463b49e7b9bfb5d74af511610dbfc9e893d13ef. --- src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index 4cf8be44f..d55dfced7 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -845,7 +845,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components var targetVector = Vector256.Create(value); ref Vector256 blockStride = ref this.V0; - for (nint i = 0; i < RowCount; i++) + for (int i = 0; i < RowCount; i++) { Vector256 areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector); if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask) @@ -860,7 +860,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { ref float scalars = ref Unsafe.As(ref this); - for (nint i = 0; i < Size; i++) + for (int i = 0; i < Size; i++) { if ((int)Unsafe.Add(ref scalars, i) != value) { From 84e52e767aafbb07d3222c6764a70ecd762a0c61 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 03:10:02 +0300 Subject: [PATCH 48/78] Moved quality debug check to a proper place --- src/ImageSharp/Formats/Jpeg/Components/Quantization.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 8e5f928b0..2ff56c63b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -165,12 +165,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int QualityToScale(int quality) - => quality < 50 ? 5000 / quality : 200 - (quality * 2); + { + DebugGuard.MustBeBetweenOrEqualTo(quality, MinQualityFactor, MaxQualityFactor, nameof(quality)); + + return quality < 50 ? (5000 / quality) : (200 - (quality * 2)); + } private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) { - DebugGuard.MustBeBetweenOrEqualTo(scale, MinQualityFactor, MaxQualityFactor, nameof(scale)); - Block8x8F table = default; for (int j = 0; j < Block8x8F.Size; j++) { From eaf40fc63918d30ce63e08866f6fc07c346557d3 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 03:13:18 +0300 Subject: [PATCH 49/78] nint usage --- shared-infrastructure | 2 +- src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-infrastructure b/shared-infrastructure index 9b94ebc4b..847a4e4c8 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 9b94ebc4be9b7a8d7620c257e6ee485455973332 +Subproject commit 847a4e4c8443fabafdd5c0a5fcf5fc3a32ab1f73 diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index d55dfced7..4cf8be44f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -845,7 +845,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components var targetVector = Vector256.Create(value); ref Vector256 blockStride = ref this.V0; - for (int i = 0; i < RowCount; i++) + for (nint i = 0; i < RowCount; i++) { Vector256 areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector); if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask) @@ -860,7 +860,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { ref float scalars = ref Unsafe.As(ref this); - for (int i = 0; i < Size; i++) + for (nint i = 0; i < Size; i++) { if ((int)Unsafe.Add(ref scalars, i) != value) { From 66604f40cdb04d5db86f6283a8ae31e2d7332b8a Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 28 Jul 2021 04:47:44 +0300 Subject: [PATCH 50/78] Revert "nint usage" This reverts commit eaf40fc63918d30ce63e08866f6fc07c346557d3. --- shared-infrastructure | 2 +- src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-infrastructure b/shared-infrastructure index 847a4e4c8..9b94ebc4b 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 847a4e4c8443fabafdd5c0a5fcf5fc3a32ab1f73 +Subproject commit 9b94ebc4be9b7a8d7620c257e6ee485455973332 diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index 4cf8be44f..d55dfced7 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -845,7 +845,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components var targetVector = Vector256.Create(value); ref Vector256 blockStride = ref this.V0; - for (nint i = 0; i < RowCount; i++) + for (int i = 0; i < RowCount; i++) { Vector256 areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector); if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask) @@ -860,7 +860,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { ref float scalars = ref Unsafe.As(ref this); - for (nint i = 0; i < Size; i++) + for (int i = 0; i < Size; i++) { if ((int)Unsafe.Add(ref scalars, i) != value) { From 941df950e13971167f99b76dda9b9f0518e0e0f7 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Fri, 30 Jul 2021 19:43:03 +0300 Subject: [PATCH 51/78] ExifResolutionValues struct --- src/ImageSharp/Common/Helpers/UnitConverter.cs | 9 +++++---- .../Formats/Tiff/TiffEncoderEntriesCollector.cs | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/UnitConverter.cs b/src/ImageSharp/Common/Helpers/UnitConverter.cs index 6bb9460e3..7ea64aa62 100644 --- a/src/ImageSharp/Common/Helpers/UnitConverter.cs +++ b/src/ImageSharp/Common/Helpers/UnitConverter.cs @@ -98,13 +98,14 @@ namespace SixLabors.ImageSharp.Common.Helpers } /// - /// Sets the exif profile resolution values. + /// Gets the exif profile resolution values. /// /// The resolution unit. /// The horizontal resolution value. /// The vertical resolution value. + /// [MethodImpl(InliningOptions.ShortMethod)] - public static (ushort, double?, double?) AdjustToExif(PixelResolutionUnit unit, double horizontal, double vertical) + public static ExifResolutionValues GetExifResolutionValues(PixelResolutionUnit unit, double horizontal, double vertical) { switch (unit) { @@ -128,10 +129,10 @@ namespace SixLabors.ImageSharp.Common.Helpers ushort exifUnit = (ushort)(unit + 1); if (unit == PixelResolutionUnit.AspectRatio) { - return (exifUnit, null, null); + return new ExifResolutionValues(exifUnit, null, null); } - return (exifUnit, horizontal, vertical); + return new ExifResolutionValues(exifUnit, horizontal, vertical); } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 1b042eec0..15694978f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -239,26 +239,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff private void ProcessResolution(ImageMetadata imageMetadata) { - (ushort, double?, double?) exifValues = UnitConverter.AdjustToExif( + ExifResolutionValues resolution = UnitConverter.GetExifResolutionValues( imageMetadata.ResolutionUnits, imageMetadata.HorizontalResolution, imageMetadata.VerticalResolution); this.Collector.AddOrReplace(new ExifShort(ExifTagValue.ResolutionUnit) { - Value = exifValues.Item1 + Value = resolution.ResolutionUnit }); - if (exifValues.Item2 != null && exifValues.Item3 != null) + if (resolution.VerticalResolution.HasValue && resolution.HorizontalResolution.HasValue) { this.Collector.AddOrReplace(new ExifRational(ExifTagValue.XResolution) { - Value = new Rational(exifValues.Item2.Value) + Value = new Rational(resolution.HorizontalResolution.Value) }); this.Collector.AddOrReplace(new ExifRational(ExifTagValue.YResolution) { - Value = new Rational(exifValues.Item3.Value) + Value = new Rational(resolution.VerticalResolution.Value) }); } } From c58a385103d5d08cfb693ba70e26d4132b612cca Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Fri, 30 Jul 2021 19:46:33 +0300 Subject: [PATCH 52/78] add missed file --- .../Common/Helpers/ExifResolutionValues.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/ImageSharp/Common/Helpers/ExifResolutionValues.cs diff --git a/src/ImageSharp/Common/Helpers/ExifResolutionValues.cs b/src/ImageSharp/Common/Helpers/ExifResolutionValues.cs new file mode 100644 index 000000000..b6a628608 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/ExifResolutionValues.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Common.Helpers +{ + internal readonly struct ExifResolutionValues + { + public ExifResolutionValues(ushort resolutionUnit, double? horizontalResolution, double? verticalResolution) + { + this.ResolutionUnit = resolutionUnit; + this.HorizontalResolution = horizontalResolution; + this.VerticalResolution = verticalResolution; + } + + public ushort ResolutionUnit { get; } + + public double? HorizontalResolution { get; } + + public double? VerticalResolution { get; } + } +} From 5d888bef8f733bd67476d5c711dab26fa32777e1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 2 Aug 2021 15:46:00 +0200 Subject: [PATCH 53/78] Tiff decoder now respects byte order for 16 bit gray images --- .../BlackIsZero16TiffColor{TPixel}.cs | 48 +++++++++++++++++++ .../Rgb161616TiffColor{TPixel}.cs | 11 ++--- .../TiffColorDecoderFactory{TPixel}.cs | 5 ++ .../TiffColorType.cs | 5 ++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 6 +++ .../Formats/Tiff/Utils/TiffUtils.cs | 18 +++++++ .../Formats/Tiff/TiffDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Tiff/flower-minisblack-16_lsb.tiff | 3 ++ 9 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs create mode 100644 src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs create mode 100644 tests/Images/Input/Tiff/flower-minisblack-16_lsb.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs new file mode 100644 index 000000000..13a570dbc --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'BlackIsZero' photometric interpretation for 16-bit grayscale images. + /// + internal class BlackIsZero16TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public BlackIsZero16TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + int offset = 0; + + var l16 = default(L16); + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + ushort intensity = TiffUtils.ConvertToShort(data.Slice(offset, 2), this.isBigEndian); + offset += 2; + + l16.PackedValue = intensity; + color.FromL16(l16); + + pixels[x, y] = color; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index 635be95f4..b0106cc09 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -3,6 +3,7 @@ using System; using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -36,11 +37,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int x = left; x < left + width; x++) { - ulong r = this.ConvertToShort(data.Slice(offset, 2)); + ulong r = TiffUtils.ConvertToShort(data.Slice(offset, 2), this.isBigEndian); offset += 2; - ulong g = this.ConvertToShort(data.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToShort(data.Slice(offset, 2), this.isBigEndian); offset += 2; - ulong b = this.ConvertToShort(data.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToShort(data.Slice(offset, 2), this.isBigEndian); offset += 2; rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); @@ -50,9 +51,5 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } } - - private ushort ConvertToShort(ReadOnlySpan buffer) => this.isBigEndian - ? BinaryPrimitives.ReadUInt16BigEndian(buffer) - : BinaryPrimitives.ReadUInt16LittleEndian(buffer); } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 8e711d3eb..ad9c478d2 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -52,6 +52,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new BlackIsZero8TiffColor(); + case TiffColorType.BlackIsZero16: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 16, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero16TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgb: DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbTiffColor(bitsPerSample); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 517926c23..3c003cf92 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -28,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// BlackIsZero8, + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 16-bit images. + /// + BlackIsZero16, + /// /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 288f01cd1..0ac9fd7c9 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -154,6 +154,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff switch (bitsPerChannel) { + case 16: + { + options.ColorType = TiffColorType.BlackIsZero16; + break; + } + case 8: { options.ColorType = TiffColorType.BlackIsZero8; diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs new file mode 100644 index 000000000..5a31b231e --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Tiff.Utils +{ + /// + /// Helper methods for TIFF decoding. + /// + internal static class TiffUtils + { + public static ushort ConvertToShort(ReadOnlySpan buffer, bool isBigEndian) => isBigEndian + ? BinaryPrimitives.ReadUInt16BigEndian(buffer) + : BinaryPrimitives.ReadUInt16LittleEndian(buffer); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ab53ca156..95d5437e2 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -140,6 +140,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] + [WithFile(Flower16BitGrayLittleEndian, PixelTypes.Rgba32)] [WithFile(Flower16BitGray, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_16Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index c54c82d7d..d5aeba0bb 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -582,6 +582,7 @@ namespace SixLabors.ImageSharp.Tests public const string Flower12BitGray = "Tiff/flower-minisblack-12.tiff"; public const string Flower14BitGray = "Tiff/flower-minisblack-14.tiff"; public const string Flower16BitGray = "Tiff/flower-minisblack-16.tiff"; + public const string Flower16BitGrayLittleEndian = "Tiff/flower-minisblack-16_lsb.tiff"; public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff"; diff --git a/tests/Images/Input/Tiff/flower-minisblack-16_lsb.tiff b/tests/Images/Input/Tiff/flower-minisblack-16_lsb.tiff new file mode 100644 index 000000000..62061bfaf --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-16_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3806304a5453a6ec8a6795bc77b967b9aa8593288af36bbf9802f22ee27869e +size 6588 From cc16677172175c5554df37807e4ca47e0afb2f53 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 2 Aug 2021 16:13:45 +0200 Subject: [PATCH 54/78] Tiff decoder now respects byte order for 16 bit gray images with white is zero --- .../TiffColorDecoderFactory{TPixel}.cs | 5 ++ .../TiffColorType.cs | 5 ++ .../WhiteIsZero16TiffColor{TPixel}.cs | 48 +++++++++++++++++++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 6 +++ .../Formats/Tiff/TiffDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Tiff/flower-miniswhite-16_lsb.tiff | 3 ++ 7 files changed, 69 insertions(+) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-miniswhite-16_lsb.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index ad9c478d2..25b441ab0 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -32,6 +32,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new WhiteIsZero8TiffColor(); + case TiffColorType.WhiteIsZero16: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 16, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero16TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.BlackIsZero: DebugGuard.IsTrue(bitsPerSample.Channels == 1, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 3c003cf92..331065d27 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -53,6 +53,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// WhiteIsZero8, + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 16-bit images. + /// + WhiteIsZero16, + /// /// Palette-color. /// diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs new file mode 100644 index 000000000..d84efef66 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'WhiteIsZero' photometric interpretation for 16-bit grayscale images. + /// + internal class WhiteIsZero16TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public WhiteIsZero16TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + int offset = 0; + + var l16 = default(L16); + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToShort(data.Slice(offset, 2), this.isBigEndian)); + offset += 2; + + l16.PackedValue = intensity; + color.FromL16(l16); + + pixels[x, y] = color; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 0ac9fd7c9..14c527a34 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -111,6 +111,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff switch (bitsPerChannel) { + case 16: + { + options.ColorType = TiffColorType.WhiteIsZero16; + break; + } + case 8: { options.ColorType = TiffColorType.WhiteIsZero8; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 95d5437e2..9346c4bfd 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -142,6 +142,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(Flower16BitGrayLittleEndian, PixelTypes.Rgba32)] [WithFile(Flower16BitGray, PixelTypes.Rgba32)] + [WithFile(Flower16BitGrayMinIsWhiteLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_16Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index d5aeba0bb..212bb94e3 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -583,6 +583,7 @@ namespace SixLabors.ImageSharp.Tests public const string Flower14BitGray = "Tiff/flower-minisblack-14.tiff"; public const string Flower16BitGray = "Tiff/flower-minisblack-16.tiff"; public const string Flower16BitGrayLittleEndian = "Tiff/flower-minisblack-16_lsb.tiff"; + public const string Flower16BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-16_lsb.tiff"; public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff"; diff --git a/tests/Images/Input/Tiff/flower-miniswhite-16_lsb.tiff b/tests/Images/Input/Tiff/flower-miniswhite-16_lsb.tiff new file mode 100644 index 000000000..ec9ceb184 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-16_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:435c92b453587e1943940111b66afabf70307beb0e1d65e9701fd9bb753eead2 +size 6588 From a1ee0d638d01b2d4c18ebfe233f354a63261ee5b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 2 Aug 2021 21:15:16 +0200 Subject: [PATCH 55/78] Decoding 16bit rgb planar now also respects byte order --- .../Rgb161616TiffColor{TPixel}.cs | 1 - .../Rgb16PlanarTiffColor{TPixel}.cs | 53 +++++++++++++++++++ .../RgbPlanarTiffColor{TPixel}.cs | 4 +- .../TiffBasePlanarColorDecoder{TPixel}.cs | 28 ++++++++++ .../TiffColorDecoderFactory{TPixel}.cs | 7 ++- .../Formats/Tiff/TiffDecoderCore.cs | 2 +- .../Formats/Tiff/TiffDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Tiff/flower-rgb-planar-16_lsb.tiff | 3 ++ 9 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-rgb-planar-16_lsb.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index b0106cc09..89cf1cb48 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..75b0b3a8b --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout for all 16 bit. + /// + internal class Rgb16PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb16PlanarTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + System.Span redData = data[0].GetSpan(); + System.Span greenData = data[1].GetSpan(); + System.Span blueData = data[2].GetSpan(); + + int offset = 0; + var rgba = default(Rgba64); + for (int y = top; y < top + height; y++) + { + for (int x = left; x < left + width; x++) + { + ulong r = TiffUtils.ConvertToShort(redData.Slice(offset, 2), this.isBigEndian); + ulong g = TiffUtils.ConvertToShort(greenData.Slice(offset, 2), this.isBigEndian); + ulong b = TiffUtils.ConvertToShort(blueData.Slice(offset, 2), this.isBigEndian); + + offset += 2; + + rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); + color.FromRgba64(rgba); + pixels[x, y] = color; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs index 3400bd65d..5df68ee59 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// /// Implements the 'RGB' photometric interpretation with 'Planar' layout (for all bit depths). /// - internal class RgbPlanarTiffColor + internal class RgbPlanarTiffColor : TiffBasePlanarColorDecoder where TPixel : unmanaged, IPixel { private readonly float rFactor; @@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// The y-coordinate of the top of the image block. /// The width of the image block. /// The height of the image block. - public void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) { var color = default(TPixel); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs new file mode 100644 index 000000000..57d8588ce --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBasePlanarColorDecoder{TPixel}.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// The base class for planar color decoders. + /// + /// The pixel format. + internal abstract class TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + /// + /// Decodes source raw pixel data using the current photometric interpretation. + /// + /// The buffers to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + public abstract void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height); + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 25b441ab0..0c93998c4 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -145,12 +145,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } - public static RgbPlanarTiffColor CreatePlanar(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap) + public static TiffBasePlanarColorDecoder CreatePlanar(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) { switch (colorType) { case TiffColorType.RgbPlanar: DebugGuard.IsTrue(colorMap == null, "colorMap"); + if (bitsPerSample.Channel0 == 16 && bitsPerSample.Channel1 == 16 && bitsPerSample.Channel2 == 16) + { + return new Rgb16PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + } + return new RgbPlanarTiffColor(bitsPerSample); default: diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 484e182c5..72f2336a8 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -266,7 +266,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions); - RgbPlanarTiffColor colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap); + TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); for (int i = 0; i < stripsPerPlane; i++) { diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 9346c4bfd..105b871da 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -166,6 +166,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerRgb161616Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb161616Planar, PixelTypes.Rgba32)] + [WithFile(FlowerRgb161616PlanarLittleEndian, PixelTypes.Rgba32)] [WithFile(Issues1716Rgb161616BitLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_48Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 212bb94e3..e0ae8f63d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -563,6 +563,7 @@ namespace SixLabors.ImageSharp.Tests public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff"; public const string FlowerRgb161616Contiguous = "Tiff/flower-rgb-contig-16.tiff"; public const string FlowerRgb161616Planar = "Tiff/flower-rgb-planar-16.tiff"; + public const string FlowerRgb161616PlanarLittleEndian = "Tiff/flower-rgb-planar-16_lsb.tiff"; public const string FlowerRgb141414Contiguous = "Tiff/flower-rgb-contig-14.tiff"; public const string FlowerRgb141414Planar = "Tiff/flower-rgb-planar-14.tiff"; public const string FlowerRgb101010Contiguous = "Tiff/flower-rgb-contig-10.tiff"; diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-16_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-16_lsb.tiff new file mode 100644 index 000000000..425ea42ef --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-16_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46a60552a7ff37f2c16c43e030e7180872af712f5d9c9c7673e2547049af3da9 +size 19168 From 6b538f89671d67a6115d4c472281351851bc67af Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 3 Aug 2021 14:05:14 +0200 Subject: [PATCH 56/78] Check for isBigEndian per row not per pixel --- .../BlackIsZero16TiffColor{TPixel}.cs | 22 ++++++++---- .../Rgb161616TiffColor{TPixel}.cs | 34 +++++++++++++------ .../Rgb16PlanarTiffColor{TPixel}.cs | 30 +++++++++++----- .../WhiteIsZero16TiffColor{TPixel}.cs | 22 ++++++++---- .../Formats/Tiff/Utils/TiffUtils.cs | 30 ++++++++++++++-- 5 files changed, 105 insertions(+), 33 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs index 13a570dbc..79017d8ce 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -32,15 +32,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var l16 = default(L16); for (int y = top; y < top + height; y++) { - for (int x = left; x < left + width; x++) + if (this.isBigEndian) { - ushort intensity = TiffUtils.ConvertToShort(data.Slice(offset, 2), this.isBigEndian); - offset += 2; + for (int x = left; x < left + width; x++) + { + ushort intensity = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); + offset += 2; - l16.PackedValue = intensity; - color.FromL16(l16); + pixels[x, y] = TiffUtils.ColorFromL16(l16, intensity, color); + } + } + else + { + for (int x = left; x < left + width; x++) + { + ushort intensity = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); + offset += 2; - pixels[x, y] = color; + pixels[x, y] = TiffUtils.ColorFromL16(l16, intensity, color); + } } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index 89cf1cb48..d2cb65cd5 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -34,19 +34,33 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { Span pixelRow = pixels.GetRowSpan(y); - for (int x = left; x < left + width; x++) + if (this.isBigEndian) { - ulong r = TiffUtils.ConvertToShort(data.Slice(offset, 2), this.isBigEndian); - offset += 2; - ulong g = TiffUtils.ConvertToShort(data.Slice(offset, 2), this.isBigEndian); - offset += 2; - ulong b = TiffUtils.ConvertToShort(data.Slice(offset, 2), this.isBigEndian); - offset += 2; + for (int x = left; x < left + width; x++) + { + ulong r = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); + offset += 2; + ulong g = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); + offset += 2; + ulong b = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); + offset += 2; - rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); - color.FromRgba64(rgba); + pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + } + } + else + { + for (int x = left; x < left + width; x++) + { + ulong r = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); + offset += 2; + ulong g = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); + offset += 2; + ulong b = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); + offset += 2; - pixelRow[x] = color; + pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + } } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs index 75b0b3a8b..43b985c90 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -35,17 +35,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var rgba = default(Rgba64); for (int y = top; y < top + height; y++) { - for (int x = left; x < left + width; x++) + if (this.isBigEndian) { - ulong r = TiffUtils.ConvertToShort(redData.Slice(offset, 2), this.isBigEndian); - ulong g = TiffUtils.ConvertToShort(greenData.Slice(offset, 2), this.isBigEndian); - ulong b = TiffUtils.ConvertToShort(blueData.Slice(offset, 2), this.isBigEndian); + for (int x = left; x < left + width; x++) + { + ulong r = TiffUtils.ConvertToShortBigEndian(redData.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToShortBigEndian(greenData.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToShortBigEndian(blueData.Slice(offset, 2)); - offset += 2; + offset += 2; - rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); - color.FromRgba64(rgba); - pixels[x, y] = color; + pixels[x, y] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + } + } + else + { + for (int x = left; x < left + width; x++) + { + ulong r = TiffUtils.ConvertToShortLittleEndian(redData.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToShortLittleEndian(greenData.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToShortLittleEndian(blueData.Slice(offset, 2)); + + offset += 2; + + pixels[x, y] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + } } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs index d84efef66..7a0aae267 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs @@ -32,15 +32,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var l16 = default(L16); for (int y = top; y < top + height; y++) { - for (int x = left; x < left + width; x++) + if (this.isBigEndian) { - ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToShort(data.Slice(offset, 2), this.isBigEndian)); - offset += 2; + for (int x = left; x < left + width; x++) + { + ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2))); + offset += 2; - l16.PackedValue = intensity; - color.FromL16(l16); + pixels[x, y] = TiffUtils.ColorFromL16(l16, intensity, color); + } + } + else + { + for (int x = left; x < left + width; x++) + { + ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2))); + offset += 2; - pixels[x, y] = color; + pixels[x, y] = TiffUtils.ColorFromL16(l16, intensity, color); + } } } } diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index 5a31b231e..4f769c047 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -3,6 +3,8 @@ using System; using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.Utils { @@ -11,8 +13,30 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils /// internal static class TiffUtils { - public static ushort ConvertToShort(ReadOnlySpan buffer, bool isBigEndian) => isBigEndian - ? BinaryPrimitives.ReadUInt16BigEndian(buffer) - : BinaryPrimitives.ReadUInt16LittleEndian(buffer); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ConvertToShortBigEndian(ReadOnlySpan buffer) => + BinaryPrimitives.ReadUInt16BigEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ConvertToShortLittleEndian(ReadOnlySpan buffer) => + BinaryPrimitives.ReadUInt16LittleEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorFromL16(L16 l16, ushort intensity, TPixel color) + where TPixel : unmanaged, IPixel + { + l16.PackedValue = intensity; + color.FromL16(l16); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorFromRgba64(Rgba64 rgba, ulong r, ulong g, ulong b, TPixel color) + where TPixel : unmanaged, IPixel + { + rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); + color.FromRgba64(rgba); + return color; + } } } From 0ae95f17d970f4ba663434d5f7be4a6cf7c862a6 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 3 Aug 2021 14:34:03 +0200 Subject: [PATCH 57/78] Use pixel row span to access pixels --- .../BlackIsZero16TiffColor{TPixel}.cs | 5 +++-- .../BlackIsZero8TiffColor{TPixel}.cs | 9 +++------ .../BlackIsZeroTiffColor{TPixel}.cs | 3 ++- .../PaletteTiffColor{TPixel}.cs | 3 ++- .../Rgb16PlanarTiffColor{TPixel}.cs | 5 +++-- .../RgbPlanarTiffColor{TPixel}.cs | 4 +++- .../PhotometricInterpretation/RgbTiffColor{TPixel}.cs | 3 ++- .../WhiteIsZero16TiffColor{TPixel}.cs | 5 +++-- .../WhiteIsZero8TiffColor{TPixel}.cs | 9 +++------ .../WhiteIsZeroTiffColor{TPixel}.cs | 3 ++- src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs | 9 +++++++++ 11 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs index 79017d8ce..735dad1e2 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -32,6 +32,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var l16 = default(L16); for (int y = top; y < top + height; y++) { + Span pixelRow = pixels.GetRowSpan(y); if (this.isBigEndian) { for (int x = left; x < left + width; x++) @@ -39,7 +40,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ushort intensity = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); offset += 2; - pixels[x, y] = TiffUtils.ColorFromL16(l16, intensity, color); + pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); } } else @@ -49,7 +50,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ushort intensity = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); offset += 2; - pixels[x, y] = TiffUtils.ColorFromL16(l16, intensity, color); + pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs index 096f0449b..3a0f4771b 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; - +using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -24,14 +24,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var l8 = default(L8); for (int y = top; y < top + height; y++) { + Span pixelRow = pixels.GetRowSpan(y); for (int x = left; x < left + width; x++) { byte intensity = data[offset++]; - - l8.PackedValue = intensity; - color.FromL8(l8); - - pixels[x, y] = color; + pixelRow[x] = TiffUtils.ColorFromL8(l8, intensity, color); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs index a4e5e45df..79ee04e29 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs @@ -34,13 +34,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { + Span pixelRow = pixels.GetRowSpan(y); for (int x = left; x < left + width; x++) { int value = bitReader.ReadBits(this.bitsPerSample0); float intensity = value / this.factor; color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f)); - pixels[x, y] = color; + pixelRow[x] = color; } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs index 796227953..c14ff2385 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs @@ -35,10 +35,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { + Span pixelRow = pixels.GetRowSpan(y); for (int x = left; x < left + width; x++) { int index = bitReader.ReadBits(this.bitsPerSample0); - pixels[x, y] = this.palette[index]; + pixelRow[x] = this.palette[index]; } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs index 43b985c90..3bad6c78c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -35,6 +35,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var rgba = default(Rgba64); for (int y = top; y < top + height; y++) { + System.Span pixelRow = pixels.GetRowSpan(y); if (this.isBigEndian) { for (int x = left; x < left + width; x++) @@ -45,7 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 2; - pixels[x, y] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); } } else @@ -58,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 2; - pixels[x, y] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs index 5df68ee59..11611907b 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Buffers; using System.Numerics; using SixLabors.ImageSharp.Formats.Tiff.Utils; @@ -57,6 +58,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { + Span pixelRow = pixels.GetRowSpan(y); for (int x = left; x < left + width; x++) { float r = rBitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; @@ -64,7 +66,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation float b = bBitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; color.FromVector4(new Vector4(r, g, b, 1.0f)); - pixels[x, y] = color; + pixelRow[x] = color; } rBitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs index 259bb8efa..01574c7aa 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs @@ -47,6 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { + Span pixelRow = pixels.GetRowSpan(y); for (int x = left; x < left + width; x++) { float r = bitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; @@ -54,7 +55,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation float b = bitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; color.FromVector4(new Vector4(r, g, b, 1.0f)); - pixels[x, y] = color; + pixelRow[x] = color; } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs index 7a0aae267..8bda36899 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs @@ -32,6 +32,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var l16 = default(L16); for (int y = top; y < top + height; y++) { + Span pixelRow = pixels.GetRowSpan(y); if (this.isBigEndian) { for (int x = left; x < left + width; x++) @@ -39,7 +40,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2))); offset += 2; - pixels[x, y] = TiffUtils.ColorFromL16(l16, intensity, color); + pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); } } else @@ -49,7 +50,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2))); offset += 2; - pixels[x, y] = TiffUtils.ColorFromL16(l16, intensity, color); + pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs index 1b141f9f6..eba6f6b72 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; - +using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -24,14 +24,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var l8 = default(L8); for (int y = top; y < top + height; y++) { + Span pixelRow = pixels.GetRowSpan(y); for (int x = left; x < left + width; x++) { byte intensity = (byte)(255 - data[offset++]); - - l8.PackedValue = intensity; - color.FromL8(l8); - - pixels[x, y] = color; + pixelRow[x] = TiffUtils.ColorFromL8(l8, intensity, color); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs index 04b6f98e5..5fb5fefb4 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs @@ -34,13 +34,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { + Span pixelRow = pixels.GetRowSpan(y); for (int x = left; x < left + width; x++) { int value = bitReader.ReadBits(this.bitsPerSample0); float intensity = 1.0f - (value / this.factor); color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f)); - pixels[x, y] = color; + pixelRow[x] = color; } bitReader.NextRow(); diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index 4f769c047..0a6e539b8 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -21,6 +21,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils public static ushort ConvertToShortLittleEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt16LittleEndian(buffer); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorFromL8(L8 l8, byte intensity, TPixel color) + where TPixel : unmanaged, IPixel + { + l8.PackedValue = intensity; + color.FromL8(l8); + return color; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorFromL16(L16 l16, ushort intensity, TPixel color) where TPixel : unmanaged, IPixel From cbb5aafa77c5208a3e17ad17eb1f9d6278e4be3f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 3 Aug 2021 23:25:20 +1000 Subject: [PATCH 58/78] Do not use static default options --- .../Processing/Processors/Quantization/OctreeQuantizer.cs | 4 +--- .../Processing/Processors/Quantization/PaletteQuantizer.cs | 3 +-- .../Processors/Quantization/WebSafePaletteQuantizer.cs | 6 +----- .../Processors/Quantization/WernerPaletteQuantizer.cs | 4 +--- .../Processing/Processors/Quantization/WuQuantizer.cs | 4 +--- 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs index 861697594..c1b695f65 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs @@ -11,14 +11,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class OctreeQuantizer : IQuantizer { - private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); - /// /// Initializes a new instance of the class /// using the default . /// public OctreeQuantizer() - : this(DefaultOptions) + : this(new QuantizerOptions()) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index 4f73f4ac8..5da674cc9 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -11,7 +11,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class PaletteQuantizer : IQuantizer { - private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); private readonly ReadOnlyMemory colorPalette; /// @@ -19,7 +18,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The color palette. public PaletteQuantizer(ReadOnlyMemory palette) - : this(palette, DefaultOptions) + : this(palette, new QuantizerOptions()) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs index 5dda17dc6..e717152f9 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.Processing.Processors.Dithering; - namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// @@ -10,13 +8,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WebSafePaletteQuantizer : PaletteQuantizer { - private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); - /// /// Initializes a new instance of the class. /// public WebSafePaletteQuantizer() - : this(DefaultOptions) + : this(new QuantizerOptions()) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs index 6675263df..8a96f8ecc 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs @@ -9,13 +9,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WernerPaletteQuantizer : PaletteQuantizer { - private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); - /// /// Initializes a new instance of the class. /// public WernerPaletteQuantizer() - : this(DefaultOptions) + : this(new QuantizerOptions()) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs index 95adb7e5d..337948bef 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs @@ -10,14 +10,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public class WuQuantizer : IQuantizer { - private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions(); - /// /// Initializes a new instance of the class /// using the default . /// public WuQuantizer() - : this(DefaultOptions) + : this(new QuantizerOptions()) { } From 522a879a1826a1fb47aafae6eaa61ccd7ab5562f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 3 Aug 2021 15:55:49 +0200 Subject: [PATCH 59/78] Avoid using defaults, because of issue with netcore2.1 in Release mode --- .../BlackIsZero16TiffColor{TPixel}.cs | 6 ++++-- .../Rgb161616TiffColor{TPixel}.cs | 5 ++++- .../Rgb16PlanarTiffColor{TPixel}.cs | 6 +++++- .../WhiteIsZero16TiffColor{TPixel}.cs | 6 ++++-- src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs | 7 +++++++ 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs index 735dad1e2..e80b1d99d 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -25,11 +25,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + L16 l16 = TiffUtils.L16Default; var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); int offset = 0; - - var l16 = default(L16); for (int y = top; y < top + height; y++) { Span pixelRow = pixels.GetRowSpan(y); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index d2cb65cd5..2e10916c0 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -25,11 +25,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + Rgba64 rgba = TiffUtils.Rgba64Default; var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); int offset = 0; - var rgba = default(Rgba64); for (int y = top; y < top + height; y++) { Span pixelRow = pixels.GetRowSpan(y); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs index 3bad6c78c..6adaab8bd 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Buffers; +using System.Numerics; using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -25,14 +26,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + Rgba64 rgba = TiffUtils.Rgba64Default; var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); System.Span redData = data[0].GetSpan(); System.Span greenData = data[1].GetSpan(); System.Span blueData = data[2].GetSpan(); int offset = 0; - var rgba = default(Rgba64); for (int y = top; y < top + height; y++) { System.Span pixelRow = pixels.GetRowSpan(y); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs index 8bda36899..3c7bfc99c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs @@ -25,11 +25,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + L16 l16 = TiffUtils.L16Default; var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); int offset = 0; - - var l16 = default(L16); for (int y = top; y < top + height; y++) { Span pixelRow = pixels.GetRowSpan(y); diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index 0a6e539b8..a0ba40f4e 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -3,6 +3,7 @@ using System; using System.Buffers.Binary; +using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; @@ -13,6 +14,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils /// internal static class TiffUtils { + public static Vector4 Vector4Default { get; } = new Vector4(0.0f, 0.0f, 0.0f, 0.0f); + + public static Rgba64 Rgba64Default { get; } = new Rgba64(0, 0, 0, 0); + + public static L16 L16Default { get; } = new L16(0); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort ConvertToShortBigEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt16BigEndian(buffer); From 547a90780fa4c4d20784d4a368ff8f6e7f5de374 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Aug 2021 00:19:03 +1000 Subject: [PATCH 60/78] Handle default instances. #1583 --- .../Processors/Dithering/ErrorDither.cs | 9 ++++++++ .../Processors/Dithering/OrderedDither.cs | 9 ++++++++ .../Processors/Quantization/QuantizerTests.cs | 23 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 1a107c2cf..35347933d 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -95,6 +95,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { + if (this == default) + { + ThrowDefaultInstance(); + } + int offsetY = bounds.Top; int offsetX = bounds.Left; float scale = quantizer.Options.DitherScale; @@ -210,5 +215,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// public override int GetHashCode() => HashCode.Combine(this.offset, this.matrix); + + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowDefaultInstance() + => throw new ImageProcessingException("Cannot use the default value type instance to dither."); } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index c317ddf02..b8eb0e390 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -109,6 +109,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TFrameQuantizer : struct, IQuantizer where TPixel : unmanaged, IPixel { + if (this == default) + { + ThrowDefaultInstance(); + } + int spread = CalculatePaletteSpread(destination.Palette.Length); float scale = quantizer.Options.DitherScale; @@ -201,5 +206,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public override int GetHashCode() => HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); + + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowDefaultInstance() + => throw new ImageProcessingException("Cannot use the default value type instance to dither."); } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs index af1d7f3f3..41d860246 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs @@ -5,6 +5,7 @@ using System; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Dithering; using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -152,6 +153,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization new WuQuantizer(OrderedDitherOptions), }; + public static readonly TheoryData DefaultInstanceDitherers + = new TheoryData + { + default(ErrorDither), + default(OrderedDither) + }; + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05F); [Theory] @@ -217,5 +225,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization testOutputDetails: testOutputDetails, appendPixelTypeToFileName: false); } + + [Theory] + [MemberData(nameof(DefaultInstanceDitherers))] + public void ShouldThrowForDefaultDitherInstance(IDither dither) + { + void Command() + { + using var image = new Image(10, 10); + var quantizer = new WebSafePaletteQuantizer(); + quantizer.Options.Dither = dither; + image.Mutate(x => x.Quantize(quantizer)); + } + + Assert.Throws(Command); + } } } From 8238c8fd6fd3414abae22adfcccda8537b45bbe5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Aug 2021 00:25:22 +1000 Subject: [PATCH 61/78] Add additional checks --- src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs | 2 ++ src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 35347933d..09ce4ebd9 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -27,6 +27,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public ErrorDither(in DenseMatrix matrix, int offset) { + Guard.MustBeGreaterThan(offset, 0, nameof(offset)); + this.matrix = matrix; this.offset = offset; } diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index b8eb0e390..2b5c7fc6b 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -24,6 +24,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering [MethodImpl(InliningOptions.ShortMethod)] public OrderedDither(uint length) { + Guard.MustBeGreaterThan(length, 0, nameof(length)); + DenseMatrix ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length); // Create a new matrix to run against, that pre-thresholds the values. From f1834c7816d90597c8681e777954a5fc7816ac2c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 3 Aug 2021 16:40:33 +0200 Subject: [PATCH 62/78] Avoid bounds checks --- .../BlackIsZero16TiffColor{TPixel}.cs | 6 +++--- .../BlackIsZero8TiffColor{TPixel}.cs | 4 ++-- .../BlackIsZeroTiffColor{TPixel}.cs | 4 ++-- .../PaletteTiffColor{TPixel}.cs | 4 ++-- .../Rgb161616TiffColor{TPixel}.cs | 4 ++-- .../Rgb16PlanarTiffColor{TPixel}.cs | 14 +++++++------- .../Rgb888TiffColor{TPixel}.cs | 4 ++-- .../RgbPlanarTiffColor{TPixel}.cs | 4 ++-- .../RgbTiffColor{TPixel}.cs | 4 ++-- .../WhiteIsZero16TiffColor{TPixel}.cs | 6 +++--- .../WhiteIsZero8TiffColor{TPixel}.cs | 4 ++-- .../WhiteIsZeroTiffColor{TPixel}.cs | 4 ++-- 12 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs index e80b1d99d..a31ff7a9f 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -34,10 +34,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); if (this.isBigEndian) { - for (int x = left; x < left + width; x++) + for (int x = 0; x < pixelRow.Length; x++) { ushort intensity = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); offset += 2; @@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } else { - for (int x = left; x < left + width; x++) + for (int x = 0; x < pixelRow.Length; x++) { ushort intensity = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); offset += 2; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs index 3a0f4771b..ea2608f6f 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var l8 = default(L8); for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { byte intensity = data[offset++]; pixelRow[x] = TiffUtils.ColorFromL8(l8, intensity, color); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs index 79ee04e29..9956db523 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs @@ -34,8 +34,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { int value = bitReader.ReadBits(this.bitsPerSample0); float intensity = value / this.factor; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs index c14ff2385..b392fe1a3 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs @@ -35,8 +35,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { int index = bitReader.ReadBits(this.bitsPerSample0); pixelRow[x] = this.palette[index]; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index 2e10916c0..00dc9fd43 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -35,11 +35,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); if (this.isBigEndian) { - for (int x = left; x < left + width; x++) + for (int x = 0; x < pixelRow.Length; x++) { ulong r = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); offset += 2; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs index 6adaab8bd..c24fada54 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Buffers; -using System.Numerics; using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -32,17 +32,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var color = default(TPixel); color.FromVector4(TiffUtils.Vector4Default); - System.Span redData = data[0].GetSpan(); - System.Span greenData = data[1].GetSpan(); - System.Span blueData = data[2].GetSpan(); + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); int offset = 0; for (int y = top; y < top + height; y++) { - System.Span pixelRow = pixels.GetRowSpan(y); + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); if (this.isBigEndian) { - for (int x = left; x < left + width; x++) + for (int x = 0; x < pixelRow.Length; x++) { ulong r = TiffUtils.ConvertToShortBigEndian(redData.Slice(offset, 2)); ulong g = TiffUtils.ConvertToShortBigEndian(greenData.Slice(offset, 2)); @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } else { - for (int x = left; x < left + width; x++) + for (int x = 0; x < pixelRow.Length; x++) { ulong r = TiffUtils.ConvertToShortLittleEndian(redData.Slice(offset, 2)); ulong g = TiffUtils.ConvertToShortLittleEndian(greenData.Slice(offset, 2)); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs index e45863a57..536dece8e 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs @@ -24,9 +24,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var rgba = default(Rgba32); for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); - for (int x = left; x < left + width; x++) + for (int x = 0; x < pixelRow.Length; x++) { byte r = data[offset++]; byte g = data[offset++]; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs index 11611907b..b442c4ae4 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs @@ -58,8 +58,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { float r = rBitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; float g = gBitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs index 01574c7aa..1377598cc 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs @@ -47,8 +47,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { float r = bitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; float g = bitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs index 3c7bfc99c..a6b5151d7 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs @@ -34,10 +34,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); if (this.isBigEndian) { - for (int x = left; x < left + width; x++) + for (int x = 0; x < pixelRow.Length; x++) { ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2))); offset += 2; @@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } else { - for (int x = left; x < left + width; x++) + for (int x = 0; x < pixelRow.Length; x++) { ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2))); offset += 2; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs index eba6f6b72..6a6c2af22 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs @@ -24,8 +24,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var l8 = default(L8); for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { byte intensity = (byte)(255 - data[offset++]); pixelRow[x] = TiffUtils.ColorFromL8(l8, intensity, color); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs index 5fb5fefb4..912955964 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs @@ -34,8 +34,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); - for (int x = left; x < left + width; x++) + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) { int value = bitReader.ReadBits(this.bitsPerSample0); float intensity = 1.0f - (value / this.factor); From 9e12a72a55487dd877613208abf01c91c3069b48 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 3 Aug 2021 16:57:03 +0200 Subject: [PATCH 63/78] Fix loop bounds for little endian, add little endian test file --- .../PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs | 2 +- tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Tiff/flower-rgb-contig-16_lsb.tiff | 3 +++ 4 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-16_lsb.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index 00dc9fd43..4b34d5a0d 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } else { - for (int x = left; x < left + width; x++) + for (int x = 0; x < pixelRow.Length; x++) { ulong r = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); offset += 2; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 105b871da..7ee6e70e6 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -165,6 +165,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerRgb161616Contiguous, PixelTypes.Rgba32)] + [WithFile(FlowerRgb161616ContiguousLittleEndian, PixelTypes.Rgba32)] [WithFile(FlowerRgb161616Planar, PixelTypes.Rgba32)] [WithFile(FlowerRgb161616PlanarLittleEndian, PixelTypes.Rgba32)] [WithFile(Issues1716Rgb161616BitLittleEndian, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index e0ae8f63d..e7641a7ca 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -562,6 +562,7 @@ namespace SixLabors.ImageSharp.Tests public const string Rgb4BitPalette = "Tiff/bike_colorpalette_4bit.tiff"; public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff"; public const string FlowerRgb161616Contiguous = "Tiff/flower-rgb-contig-16.tiff"; + public const string FlowerRgb161616ContiguousLittleEndian = "Tiff/flower-rgb-contig-16_lsb.tiff"; public const string FlowerRgb161616Planar = "Tiff/flower-rgb-planar-16.tiff"; public const string FlowerRgb161616PlanarLittleEndian = "Tiff/flower-rgb-planar-16_lsb.tiff"; public const string FlowerRgb141414Contiguous = "Tiff/flower-rgb-contig-14.tiff"; diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-16_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-16_lsb.tiff new file mode 100644 index 000000000..967d8bbf3 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-16_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0951a9c2207eb6864b6a19ec8513a28a874adddb37c3c06b9fd07831372924e3 +size 19150 From f868b7a2aa2a7a2cf2f9836fd8c84e5dbfe08544 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 3 Aug 2021 17:21:08 +0200 Subject: [PATCH 64/78] Add min is white 16 bit gray big endian test file --- tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Tiff/flower-miniswhite-16.tiff | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 tests/Images/Input/Tiff/flower-miniswhite-16.tiff diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 7ee6e70e6..3bf1c25f3 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -143,6 +143,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [WithFile(Flower16BitGrayLittleEndian, PixelTypes.Rgba32)] [WithFile(Flower16BitGray, PixelTypes.Rgba32)] [WithFile(Flower16BitGrayMinIsWhiteLittleEndian, PixelTypes.Rgba32)] + [WithFile(Flower16BitGrayMinIsWhiteBigEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_16Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index e7641a7ca..9c2418436 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -586,6 +586,7 @@ namespace SixLabors.ImageSharp.Tests public const string Flower16BitGray = "Tiff/flower-minisblack-16.tiff"; public const string Flower16BitGrayLittleEndian = "Tiff/flower-minisblack-16_lsb.tiff"; public const string Flower16BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-16_lsb.tiff"; + public const string Flower16BitGrayMinIsWhiteBigEndian = "Tiff/flower-miniswhite-16.tiff"; public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff"; diff --git a/tests/Images/Input/Tiff/flower-miniswhite-16.tiff b/tests/Images/Input/Tiff/flower-miniswhite-16.tiff new file mode 100644 index 000000000..83266873c --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-16.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f2c2afd8f1645717087bd2edbc3e8a46b88a54a4996c0e9350fdd652b5c382 +size 6588 From 6e9cff93f4f37f2f257b6d065d9ed0edb6fb28c9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 3 Aug 2021 20:31:48 +0200 Subject: [PATCH 65/78] Add support for decoding 24bit per channel color tiff with contiguous pixel data --- .../BlackIsZero16TiffColor{TPixel}.cs | 4 +- .../Rgb161616TiffColor{TPixel}.cs | 12 +-- .../Rgb16PlanarTiffColor{TPixel}.cs | 12 +-- .../Rgb242424TiffColor{TPixel}.cs | 89 +++++++++++++++++++ .../TiffColorDecoderFactory{TPixel}.cs | 10 +++ .../TiffColorType.cs | 5 ++ .../WhiteIsZero16TiffColor{TPixel}.cs | 4 +- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 4 + .../Formats/Tiff/Utils/TiffUtils.cs | 12 ++- .../Formats/Tiff/TiffDecoderTests.cs | 6 ++ tests/ImageSharp.Tests/TestImages.cs | 2 + .../Input/Tiff/flower-rgb-contig-24.tiff | 3 + .../Input/Tiff/flower-rgb-contig-24_lsb.tiff | 3 + 13 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-24.tiff create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-24_lsb.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs index a31ff7a9f..2a52e8bff 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - ushort intensity = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); + ushort intensity = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); offset += 2; pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - ushort intensity = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); + ushort intensity = TiffUtils.ConvertToUShortLittleEndian(data.Slice(offset, 2)); offset += 2; pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index 4b34d5a0d..86ac94f55 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -41,11 +41,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - ulong r = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); + ulong r = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); offset += 2; - ulong g = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); offset += 2; - ulong b = TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); offset += 2; pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); @@ -55,11 +55,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - ulong r = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); + ulong r = TiffUtils.ConvertToUShortLittleEndian(data.Slice(offset, 2)); offset += 2; - ulong g = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToUShortLittleEndian(data.Slice(offset, 2)); offset += 2; - ulong b = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToUShortLittleEndian(data.Slice(offset, 2)); offset += 2; pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs index c24fada54..20053eb8a 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -44,9 +44,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - ulong r = TiffUtils.ConvertToShortBigEndian(redData.Slice(offset, 2)); - ulong g = TiffUtils.ConvertToShortBigEndian(greenData.Slice(offset, 2)); - ulong b = TiffUtils.ConvertToShortBigEndian(blueData.Slice(offset, 2)); + ulong r = TiffUtils.ConvertToUShortBigEndian(redData.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToUShortBigEndian(greenData.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToUShortBigEndian(blueData.Slice(offset, 2)); offset += 2; @@ -57,9 +57,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - ulong r = TiffUtils.ConvertToShortLittleEndian(redData.Slice(offset, 2)); - ulong g = TiffUtils.ConvertToShortLittleEndian(greenData.Slice(offset, 2)); - ulong b = TiffUtils.ConvertToShortLittleEndian(blueData.Slice(offset, 2)); + ulong r = TiffUtils.ConvertToUShortLittleEndian(redData.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToUShortLittleEndian(greenData.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToUShortLittleEndian(blueData.Slice(offset, 2)); offset += 2; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs new file mode 100644 index 000000000..7d13fdcdf --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 16 bits for each channel. + /// + internal class Rgb242424TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb242424TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + int offset = 0; + float scale = 1.0f / 0xFFFFFF; + byte[] buffer = new byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong r = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong g = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong b = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + var colorVector = new Vector4(r * scale, g * scale, b * scale, 1.0f); + color.FromVector4(colorVector); + + pixelRow[x] = color; + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong r = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong g = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong b = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + var colorVector = new Vector4(r * scale, g * scale, b * scale, 1.0f); + color.FromVector4(colorVector); + + pixelRow[x] = color; + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 0c93998c4..ed9a14b2a 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -136,6 +136,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb161616TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgb242424: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 24 + && bitsPerSample.Channel1 == 24 + && bitsPerSample.Channel0 == 24, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb242424TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.PaletteColor: DebugGuard.NotNull(colorMap, "colorMap"); return new PaletteTiffColor(bitsPerSample, colorMap); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 331065d27..b49bcc219 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -103,6 +103,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// Rgb161616, + /// + /// RGB color image with 24 bits for each channel. + /// + Rgb242424, + /// /// RGB Full Color. Planar configuration of data. /// diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs index a6b5151d7..18b5300b2 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToShortBigEndian(data.Slice(offset, 2))); + ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2))); offset += 2; pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2))); + ushort intensity = (ushort)(ushort.MaxValue - TiffUtils.ConvertToUShortLittleEndian(data.Slice(offset, 2))); offset += 2; pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 14c527a34..4563e2317 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -206,6 +206,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff ushort bitsPerChannel = options.BitsPerSample.Channel0; switch (bitsPerChannel) { + case 24: + options.ColorType = TiffColorType.Rgb242424; + break; + case 16: options.ColorType = TiffColorType.Rgb161616; break; diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index a0ba40f4e..9514e4301 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -21,12 +21,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils public static L16 L16Default { get; } = new L16(0); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ushort ConvertToShortBigEndian(ReadOnlySpan buffer) => - BinaryPrimitives.ReadUInt16BigEndian(buffer); + public static ushort ConvertToUShortBigEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt16BigEndian(buffer); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ushort ConvertToShortLittleEndian(ReadOnlySpan buffer) => - BinaryPrimitives.ReadUInt16LittleEndian(buffer); + public static ushort ConvertToUShortLittleEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt16LittleEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ConvertToUIntBigEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt32BigEndian(buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ConvertToUIntLittleEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt32LittleEndian(buffer); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorFromL8(L8 l8, byte intensity, TPixel color) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 3bf1c25f3..9cda1bdac 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -173,6 +173,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_48Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(FlowerRgb242424Contiguous, PixelTypes.Rgba32)] + [WithFile(FlowerRgb242424ContiguousLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_72Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(GrayscaleDeflateMultistrip, PixelTypes.Rgba32)] [WithFile(RgbDeflateMultistrip, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 9c2418436..1f98c30a3 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -561,6 +561,8 @@ namespace SixLabors.ImageSharp.Tests public const string RgbPalette = "Tiff/rgb_palette.tiff"; public const string Rgb4BitPalette = "Tiff/bike_colorpalette_4bit.tiff"; public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff"; + public const string FlowerRgb242424Contiguous = "Tiff/flower-rgb-contig-24.tiff"; + public const string FlowerRgb242424ContiguousLittleEndian = "Tiff/flower-rgb-contig-24_lsb.tiff"; public const string FlowerRgb161616Contiguous = "Tiff/flower-rgb-contig-16.tiff"; public const string FlowerRgb161616ContiguousLittleEndian = "Tiff/flower-rgb-contig-16_lsb.tiff"; public const string FlowerRgb161616Planar = "Tiff/flower-rgb-planar-16.tiff"; diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-24.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-24.tiff new file mode 100644 index 000000000..9145c21db --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-24.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6368a704b0a629239024f6fbfb30723fa317593ef36ddba05d76302530bd974 +size 28568 diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-24_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-24_lsb.tiff new file mode 100644 index 000000000..40cf1c9b8 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-24_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbb2b4ca6d7eeee4737c6963c99ef68fb6971cf6ccee463427a8246574bc6440 +size 28632 From 28c1356a765c9601cae4be7322a7a2746d1a3b62 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 3 Aug 2021 21:21:59 +0200 Subject: [PATCH 66/78] Add support for decoding 24bit per channel color tiff with planar pixel data --- .../Rgb16PlanarTiffColor{TPixel}.cs | 2 +- .../Rgb24PlanarTiffColor{TPixel}.cs | 87 +++++++++++++++++++ .../TiffColorDecoderFactory{TPixel}.cs | 5 ++ .../Formats/Tiff/TiffDecoderTests.cs | 2 + tests/ImageSharp.Tests/TestImages.cs | 2 + .../Input/Tiff/flower-rgb-planar-24.tiff | 3 + .../Input/Tiff/flower-rgb-planar-24_lsb.tiff | 3 + 7 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-rgb-planar-24.tiff create mode 100644 tests/Images/Input/Tiff/flower-rgb-planar-24_lsb.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs index 20053eb8a..9a6d4631a 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { /// - /// Implements the 'RGB' photometric interpretation with 'Planar' layout for all 16 bit. + /// Implements the 'RGB' photometric interpretation with 'Planar' layout for each color channel with 16 bit. /// internal class Rgb16PlanarTiffColor : TiffBasePlanarColorDecoder where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..c322b35b3 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout for each color channel with 24 bit. + /// + internal class Rgb24PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb24PlanarTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + float scale = 1.0f / 0xFFFFFF; + byte[] buffer = new byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + redData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong r = TiffUtils.ConvertToUIntBigEndian(buffer); + greenData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong g = TiffUtils.ConvertToUIntBigEndian(buffer); + blueData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong b = TiffUtils.ConvertToUIntBigEndian(buffer); + + offset += 3; + + var colorVector = new Vector4(r * scale, g * scale, b * scale, 1.0f); + color.FromVector4(colorVector); + + pixelRow[x] = color; + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + redData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong r = TiffUtils.ConvertToUIntLittleEndian(buffer); + greenData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong g = TiffUtils.ConvertToUIntLittleEndian(buffer); + blueData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong b = TiffUtils.ConvertToUIntLittleEndian(buffer); + + offset += 3; + + var colorVector = new Vector4(r * scale, g * scale, b * scale, 1.0f); + color.FromVector4(colorVector); + + pixelRow[x] = color; + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index ed9a14b2a..1167d4784 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -166,6 +166,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new Rgb16PlanarTiffColor(byteOrder == ByteOrder.BigEndian); } + if (bitsPerSample.Channel0 == 24 && bitsPerSample.Channel1 == 24 && bitsPerSample.Channel2 == 24) + { + return new Rgb24PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + } + return new RgbPlanarTiffColor(bitsPerSample); default: diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 9cda1bdac..91fc69950 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -176,6 +176,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerRgb242424Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb242424ContiguousLittleEndian, PixelTypes.Rgba32)] + [WithFile(FlowerRgb242424Planar, PixelTypes.Rgba32)] + [WithFile(FlowerRgb242424PlanarLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_72Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 1f98c30a3..2f03e067a 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -561,6 +561,8 @@ namespace SixLabors.ImageSharp.Tests public const string RgbPalette = "Tiff/rgb_palette.tiff"; public const string Rgb4BitPalette = "Tiff/bike_colorpalette_4bit.tiff"; public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff"; + public const string FlowerRgb242424Planar = "Tiff/flower-rgb-planar-24.tiff"; + public const string FlowerRgb242424PlanarLittleEndian = "Tiff/flower-rgb-planar-24_lsb.tiff"; public const string FlowerRgb242424Contiguous = "Tiff/flower-rgb-contig-24.tiff"; public const string FlowerRgb242424ContiguousLittleEndian = "Tiff/flower-rgb-contig-24_lsb.tiff"; public const string FlowerRgb161616Contiguous = "Tiff/flower-rgb-contig-16.tiff"; diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-24.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-24.tiff new file mode 100644 index 000000000..b0b41901c --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-24.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:752452ac51ad1e836fb81267ab708ff81cf81a4c7e00daeed703f67782b563ec +size 28586 diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-24_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-24_lsb.tiff new file mode 100644 index 000000000..c615089fd --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-24_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72f27af4fe177ebe47bef2af64723497d5a5f44808424bedfc2012fe4e3fc34e +size 28586 From 8e8123c598dc7ff8d09877643ba2d37bf6a68081 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Aug 2021 10:06:16 +1000 Subject: [PATCH 67/78] Add sanitation for dithering methods. --- .../Processors/Dithering/ErrorDither.cs | 5 +++++ .../Processors/Dithering/OrderedDither.cs | 5 +++++ .../Processors/Dithering/DitherTests.cs | 20 +++++++++++++++++++ .../Processors/Quantization/QuantizerTests.cs | 8 ++++---- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 09ce4ebd9..0fe2d4b2c 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -129,6 +129,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel { + if (this == default) + { + ThrowDefaultInstance(); + } + float scale = processor.DitherScale; for (int y = bounds.Top; y < bounds.Bottom; y++) { diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 2b5c7fc6b..2f5a5cf85 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -141,6 +141,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor where TPixel : unmanaged, IPixel { + if (this == default) + { + ThrowDefaultInstance(); + } + int spread = CalculatePaletteSpread(processor.Palette.Length); float scale = processor.DitherScale; diff --git a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs index 37443a5b4..0465cae94 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs @@ -43,6 +43,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Dithering { KnownDitherings.Ordered3x3, nameof(KnownDitherings.Ordered3x3) } }; + public static readonly TheoryData DefaultInstanceDitherers + = new TheoryData + { + default(ErrorDither), + default(OrderedDither) + }; + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05f); private static IDither DefaultDitherer => KnownDitherings.Bayer4x4; @@ -175,5 +182,18 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Dithering c => c.Dither(dither), name); } + + [Theory] + [MemberData(nameof(DefaultInstanceDitherers))] + public void ShouldThrowForDefaultDitherInstance(IDither dither) + { + void Command() + { + using var image = new Image(10, 10); + image.Mutate(x => x.Dither(dither)); + } + + Assert.Throws(Command); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs index 41d860246..c99e10138 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs @@ -155,10 +155,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization public static readonly TheoryData DefaultInstanceDitherers = new TheoryData - { - default(ErrorDither), - default(OrderedDither) - }; + { + default(ErrorDither), + default(OrderedDither) + }; private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05F); From e700b972fd3c9a554fe3dac2bce8564c44d3e268 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 3 Aug 2021 22:04:21 +0200 Subject: [PATCH 68/78] Add support for decoding gray 24 bit tiff's --- .../BlackIsZero24TiffColor{TPixel}.cs | 65 +++++++++++++++++++ .../Rgb242424TiffColor{TPixel}.cs | 12 +--- .../Rgb24PlanarTiffColor{TPixel}.cs | 12 +--- .../TiffColorDecoderFactory{TPixel}.cs | 10 +++ .../TiffColorType.cs | 10 +++ .../WhiteIsZero24TiffColor{TPixel}.cs | 64 ++++++++++++++++++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 16 ++++- .../Formats/Tiff/Utils/TiffUtils.cs | 26 +++++++- .../Formats/Tiff/TiffDecoderTests.cs | 5 ++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Tiff/flower-minisblack-24.tiff | 3 + 11 files changed, 199 insertions(+), 25 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-minisblack-24.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs new file mode 100644 index 000000000..9dc989c38 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'BlackIsZero' photometric interpretation for 24-bit grayscale images. + /// + internal class BlackIsZero24TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public BlackIsZero24TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + byte[] buffer = new byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong intensity = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(intensity, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong intensity = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(intensity, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs index 7d13fdcdf..ce8d2db64 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Numerics; using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -31,7 +30,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation var color = default(TPixel); color.FromVector4(TiffUtils.Vector4Default); int offset = 0; - float scale = 1.0f / 0xFFFFFF; byte[] buffer = new byte[4]; int bufferStartIdx = this.isBigEndian ? 1 : 0; @@ -55,10 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ulong b = TiffUtils.ConvertToUIntBigEndian(buffer); offset += 3; - var colorVector = new Vector4(r * scale, g * scale, b * scale, 1.0f); - color.FromVector4(colorVector); - - pixelRow[x] = color; + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(r, g, b, color); } } else @@ -77,10 +72,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ulong b = TiffUtils.ConvertToUIntLittleEndian(buffer); offset += 3; - var colorVector = new Vector4(r * scale, g * scale, b * scale, 1.0f); - color.FromVector4(colorVector); - - pixelRow[x] = color; + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(r, g, b, color); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs index c322b35b3..cd94f8e81 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs @@ -3,7 +3,6 @@ using System; using System.Buffers; -using System.Numerics; using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -31,7 +30,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); color.FromVector4(TiffUtils.Vector4Default); - float scale = 1.0f / 0xFFFFFF; byte[] buffer = new byte[4]; int bufferStartIdx = this.isBigEndian ? 1 : 0; @@ -56,10 +54,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 3; - var colorVector = new Vector4(r * scale, g * scale, b * scale, 1.0f); - color.FromVector4(colorVector); - - pixelRow[x] = color; + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(r, g, b, color); } } else @@ -75,10 +70,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 3; - var colorVector = new Vector4(r * scale, g * scale, b * scale, 1.0f); - color.FromVector4(colorVector); - - pixelRow[x] = color; + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(r, g, b, color); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 1167d4784..20b3b1340 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -37,6 +37,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new WhiteIsZero16TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.WhiteIsZero24: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 24, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero24TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.BlackIsZero: DebugGuard.IsTrue(bitsPerSample.Channels == 1, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); @@ -62,6 +67,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new BlackIsZero16TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.BlackIsZero24: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 24, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero24TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgb: DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbTiffColor(bitsPerSample); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index b49bcc219..81418df29 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -33,6 +33,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// BlackIsZero16, + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 24-bit images. + /// + BlackIsZero24, + /// /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. /// @@ -58,6 +63,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// WhiteIsZero16, + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 24-bit images. + /// + WhiteIsZero24, + /// /// Palette-color. /// diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs new file mode 100644 index 000000000..143a684c2 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'WhiteIsZero' photometric interpretation for 24-bit grayscale images. + /// + internal class WhiteIsZero24TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public WhiteIsZero24TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + byte[] buffer = new byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong intensity = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(intensity, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + ulong intensity = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + pixelRow[x] = TiffUtils.ColorScaleTo24Bit(intensity, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 4563e2317..2186f2f9a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -104,13 +104,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff } ushort bitsPerChannel = options.BitsPerSample.Channel0; - if (bitsPerChannel > 16) + if (bitsPerChannel > 24) { TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported."); } switch (bitsPerChannel) { + case 24: + { + options.ColorType = TiffColorType.WhiteIsZero24; + break; + } + case 16: { options.ColorType = TiffColorType.WhiteIsZero16; @@ -153,13 +159,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff } ushort bitsPerChannel = options.BitsPerSample.Channel0; - if (bitsPerChannel > 16) + if (bitsPerChannel > 24) { TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported."); } switch (bitsPerChannel) { + case 24: + { + options.ColorType = TiffColorType.BlackIsZero24; + break; + } + case 16: { options.ColorType = TiffColorType.BlackIsZero16; diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index 9514e4301..c8fd98021 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -14,6 +14,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils /// internal static class TiffUtils { + private const float Scale = 1.0f / 0xFFFFFF; + public static Vector4 Vector4Default { get; } = new Vector4(0.0f, 0.0f, 0.0f, 0.0f); public static Rgba64 Rgba64Default { get; } = new Rgba64(0, 0, 0, 0); @@ -41,6 +43,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils return color; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorFromRgba64(Rgba64 rgba, ulong r, ulong g, ulong b, TPixel color) + where TPixel : unmanaged, IPixel + { + rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); + color.FromRgba64(rgba); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo24Bit(ulong r, ulong g, ulong b, TPixel color) + where TPixel : unmanaged, IPixel + { + var colorVector = new Vector4(r * Scale, g * Scale, b * Scale, 1.0f); + color.FromVector4(colorVector); + return color; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorFromL16(L16 l16, ushort intensity, TPixel color) where TPixel : unmanaged, IPixel @@ -51,11 +71,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TPixel ColorFromRgba64(Rgba64 rgba, ulong r, ulong g, ulong b, TPixel color) + public static TPixel ColorScaleTo24Bit(ulong intensity, TPixel color) where TPixel : unmanaged, IPixel { - rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); - color.FromRgba64(rgba); + var colorVector = new Vector4(intensity * Scale, intensity * Scale, intensity * Scale, 1.0f); + color.FromVector4(colorVector); return color; } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 91fc69950..90d17c959 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -147,6 +147,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_16Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Flower24BitGray, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_24Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 2f03e067a..641ff4671 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -588,6 +588,7 @@ namespace SixLabors.ImageSharp.Tests public const string Flower12BitGray = "Tiff/flower-minisblack-12.tiff"; public const string Flower14BitGray = "Tiff/flower-minisblack-14.tiff"; public const string Flower16BitGray = "Tiff/flower-minisblack-16.tiff"; + public const string Flower24BitGray = "Tiff/flower-minisblack-24.tiff"; public const string Flower16BitGrayLittleEndian = "Tiff/flower-minisblack-16_lsb.tiff"; public const string Flower16BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-16_lsb.tiff"; public const string Flower16BitGrayMinIsWhiteBigEndian = "Tiff/flower-miniswhite-16.tiff"; diff --git a/tests/Images/Input/Tiff/flower-minisblack-24.tiff b/tests/Images/Input/Tiff/flower-minisblack-24.tiff new file mode 100644 index 000000000..7f9dd009d --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-24.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe2d4e0d99bdfade966e27bd9583bce39bebb90efa8e7f768ce3cec69aa306e2 +size 9770 From c34a1ed0247c131af590feabb2603e40b3a3164d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 4 Aug 2021 08:57:10 +0200 Subject: [PATCH 69/78] Add support for decoding gray 32 bit tiff's --- .../BlackIsZero24TiffColor{TPixel}.cs | 1 - .../BlackIsZero32TiffColor{TPixel}.cs | 61 +++++++++++++++++++ .../TiffColorDecoderFactory{TPixel}.cs | 10 +++ .../TiffColorType.cs | 10 +++ .../WhiteIsZero32TiffColor{TPixel}.cs | 60 ++++++++++++++++++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 16 ++++- .../Formats/Tiff/Utils/TiffUtils.cs | 17 +++++- .../Formats/Tiff/TiffDecoderTests.cs | 29 ++++++--- tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Tiff/flower-minisblack-32.tiff | 3 + 10 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-minisblack-32.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs index 9dc989c38..813d7beb5 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Numerics; using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs new file mode 100644 index 000000000..862756bc4 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'BlackIsZero' photometric interpretation for 32-bit grayscale images. + /// + internal class BlackIsZero32TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public BlackIsZero32TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong intensity = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(intensity, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong intensity = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(intensity, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 20b3b1340..34866b58f 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -42,6 +42,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new WhiteIsZero24TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.WhiteIsZero32: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new WhiteIsZero32TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.BlackIsZero: DebugGuard.IsTrue(bitsPerSample.Channels == 1, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); @@ -72,6 +77,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new BlackIsZero24TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.BlackIsZero32: + DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new BlackIsZero32TiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgb: DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbTiffColor(bitsPerSample); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 81418df29..23f031173 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -38,6 +38,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// BlackIsZero24, + /// + /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 32-bit images. + /// + BlackIsZero32, + /// /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. /// @@ -68,6 +73,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// WhiteIsZero24, + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 32-bit images. + /// + WhiteIsZero32, + /// /// Palette-color. /// diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs new file mode 100644 index 000000000..007174003 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'WhiteIsZero' photometric interpretation for 32-bit grayscale images. + /// + internal class WhiteIsZero32TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public WhiteIsZero32TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong intensity = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(intensity, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong intensity = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(intensity, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 2186f2f9a..1162addee 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -104,13 +104,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff } ushort bitsPerChannel = options.BitsPerSample.Channel0; - if (bitsPerChannel > 24) + if (bitsPerChannel > 32) { TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported."); } switch (bitsPerChannel) { + case 32: + { + options.ColorType = TiffColorType.WhiteIsZero32; + break; + } + case 24: { options.ColorType = TiffColorType.WhiteIsZero24; @@ -159,13 +165,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff } ushort bitsPerChannel = options.BitsPerSample.Channel0; - if (bitsPerChannel > 24) + if (bitsPerChannel > 32) { TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported."); } switch (bitsPerChannel) { + case 32: + { + options.ColorType = TiffColorType.BlackIsZero32; + break; + } + case 24: { options.ColorType = TiffColorType.BlackIsZero24; diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index c8fd98021..95c4a542f 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -14,7 +14,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils /// internal static class TiffUtils { - private const float Scale = 1.0f / 0xFFFFFF; + private const float Scale24Bit = 1.0f / 0xFFFFFF; + + private const float Scale32Bit = 1.0f / 0xFFFFFFFF; public static Vector4 Vector4Default { get; } = new Vector4(0.0f, 0.0f, 0.0f, 0.0f); @@ -56,7 +58,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils public static TPixel ColorScaleTo24Bit(ulong r, ulong g, ulong b, TPixel color) where TPixel : unmanaged, IPixel { - var colorVector = new Vector4(r * Scale, g * Scale, b * Scale, 1.0f); + var colorVector = new Vector4(r * Scale24Bit, g * Scale24Bit, b * Scale24Bit, 1.0f); color.FromVector4(colorVector); return color; } @@ -74,7 +76,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils public static TPixel ColorScaleTo24Bit(ulong intensity, TPixel color) where TPixel : unmanaged, IPixel { - var colorVector = new Vector4(intensity * Scale, intensity * Scale, intensity * Scale, 1.0f); + var colorVector = new Vector4(intensity * Scale24Bit, intensity * Scale24Bit, intensity * Scale24Bit, 1.0f); + color.FromVector4(colorVector); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32Bit(ulong intensity, TPixel color) + where TPixel : unmanaged, IPixel + { + var colorVector = new Vector4(intensity * Scale32Bit, intensity * Scale32Bit, intensity * Scale32Bit, 1.0f); color.FromVector4(colorVector); return color; } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 90d17c959..ae02cc988 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -107,36 +107,44 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(Flower2BitGray, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_2Bit(TestImageProvider provider) + public void TiffDecoder_CanDecode_2Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] [WithFile(FlowerRgb222Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb222Planar, PixelTypes.Rgba32)] - [WithFile(Flower6BitGray, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_6Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Flower6BitGray, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_6Bit_Gray(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Flower8BitGray, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_8Bit(TestImageProvider provider) + public void TiffDecoder_CanDecode_8Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] [WithFile(Flower10BitGray, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_10Bit(TestImageProvider provider) + public void TiffDecoder_CanDecode_10Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] [WithFile(FlowerRgb444Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb444Planar, PixelTypes.Rgba32)] - [WithFile(Flower12BitGray, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_12Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Flower12BitGray, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_12Bit_Gray(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Flower14BitGray, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_14Bit(TestImageProvider provider) + public void TiffDecoder_CanDecode_14Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] @@ -144,12 +152,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [WithFile(Flower16BitGray, PixelTypes.Rgba32)] [WithFile(Flower16BitGrayMinIsWhiteLittleEndian, PixelTypes.Rgba32)] [WithFile(Flower16BitGrayMinIsWhiteBigEndian, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_16Bit(TestImageProvider provider) + public void TiffDecoder_CanDecode_16Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] [WithFile(Flower24BitGray, PixelTypes.Rgba32)] - public void TiffDecoder_CanDecode_24Bit(TestImageProvider provider) + public void TiffDecoder_CanDecode_24Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); [Theory] @@ -158,6 +166,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_30Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Flower32BitGray, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_32Bit_Gray(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(FlowerRgb121212Contiguous, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_36Bit(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 641ff4671..9005c98fa 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -589,6 +589,7 @@ namespace SixLabors.ImageSharp.Tests public const string Flower14BitGray = "Tiff/flower-minisblack-14.tiff"; public const string Flower16BitGray = "Tiff/flower-minisblack-16.tiff"; public const string Flower24BitGray = "Tiff/flower-minisblack-24.tiff"; + public const string Flower32BitGray = "Tiff/flower-minisblack-32.tiff"; public const string Flower16BitGrayLittleEndian = "Tiff/flower-minisblack-16_lsb.tiff"; public const string Flower16BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-16_lsb.tiff"; public const string Flower16BitGrayMinIsWhiteBigEndian = "Tiff/flower-miniswhite-16.tiff"; diff --git a/tests/Images/Input/Tiff/flower-minisblack-32.tiff b/tests/Images/Input/Tiff/flower-minisblack-32.tiff new file mode 100644 index 000000000..b64616ed2 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-32.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6677c372a449fe0324b148385cf0ebaaf33ab4563484ae89831dfeacd80d7c93 +size 12885 From 9c585c48993d29cf36d4dd9ac892536b35b01ceb Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 4 Aug 2021 09:09:06 +0200 Subject: [PATCH 70/78] Add support for decoding 32bit per channel color tiff with contiguous pixel data --- .../Rgb242424TiffColor{TPixel}.cs | 2 +- .../Rgb323232TiffColor{TPixel}.cs | 73 +++++++++++++++++++ .../TiffColorDecoderFactory{TPixel}.cs | 10 +++ .../TiffColorType.cs | 5 ++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 4 + .../Formats/Tiff/Utils/TiffUtils.cs | 9 +++ .../Formats/Tiff/TiffDecoderTests.cs | 5 ++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Tiff/flower-rgb-contig-32.tiff | 3 + 9 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-32.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs index ce8d2db64..abc3a82a9 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs @@ -9,7 +9,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { /// - /// Implements the 'RGB' photometric interpretation with 16 bits for each channel. + /// Implements the 'RGB' photometric interpretation with 24 bits for each channel. /// internal class Rgb242424TiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs new file mode 100644 index 000000000..e2ba085e1 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 32 bits for each channel. + /// + internal class Rgb323232TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb323232TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + int offset = 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + ulong g = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + ulong b = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(r, g, b, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + ulong g = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + ulong b = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(r, g, b, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 34866b58f..a34b1ad3e 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -166,6 +166,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb242424TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgb323232: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 32 + && bitsPerSample.Channel1 == 32 + && bitsPerSample.Channel0 == 32, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.PaletteColor: DebugGuard.NotNull(colorMap, "colorMap"); return new PaletteTiffColor(bitsPerSample, colorMap); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 23f031173..ffc5a8167 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -128,6 +128,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// Rgb242424, + /// + /// RGB color image with 32 bits for each channel. + /// + Rgb323232, + /// /// RGB Full Color. Planar configuration of data. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 1162addee..ceb2a3c94 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -230,6 +230,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff ushort bitsPerChannel = options.BitsPerSample.Channel0; switch (bitsPerChannel) { + case 32: + options.ColorType = TiffColorType.Rgb323232; + break; + case 24: options.ColorType = TiffColorType.Rgb242424; break; diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index 95c4a542f..f2872858c 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -63,6 +63,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils return color; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32Bit(ulong r, ulong g, ulong b, TPixel color) + where TPixel : unmanaged, IPixel + { + var colorVector = new Vector4(r * Scale32Bit, g * Scale32Bit, b * Scale32Bit, 1.0f); + color.FromVector4(colorVector); + return color; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorFromL16(L16 l16, ushort intensity, TPixel color) where TPixel : unmanaged, IPixel diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ae02cc988..19237b4b9 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -199,6 +199,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_72Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(FlowerRgb323232Contiguous, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_96Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(GrayscaleDeflateMultistrip, PixelTypes.Rgba32)] [WithFile(RgbDeflateMultistrip, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 9005c98fa..bb22cbcc8 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -561,6 +561,7 @@ namespace SixLabors.ImageSharp.Tests public const string RgbPalette = "Tiff/rgb_palette.tiff"; public const string Rgb4BitPalette = "Tiff/bike_colorpalette_4bit.tiff"; public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff"; + public const string FlowerRgb323232Contiguous = "Tiff/flower-rgb-contig-32.tiff"; public const string FlowerRgb242424Planar = "Tiff/flower-rgb-planar-24.tiff"; public const string FlowerRgb242424PlanarLittleEndian = "Tiff/flower-rgb-planar-24_lsb.tiff"; public const string FlowerRgb242424Contiguous = "Tiff/flower-rgb-contig-24.tiff"; diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-32.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-32.tiff new file mode 100644 index 000000000..28461d8d8 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-32.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7b9da8ec44da84fc89aed1ad221a5eb130a1f233a1ff8a4a15b41898a0e364f +size 38027 From f118b8117448794f0ea4ef660b92f6c3978fa8dd Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 4 Aug 2021 12:36:19 +0200 Subject: [PATCH 71/78] Fix issue calculating the stripIndex for planar tiff's --- src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs | 4 ++-- tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 6 ++++++ tests/ImageSharp.Tests/TestImages.cs | 2 ++ tests/Images/Input/Tiff/flower-rgb-planar-08-15strips.tiff | 3 +++ tests/Images/Input/Tiff/flower-rgb-planar-08-6strips.tiff | 3 +++ 5 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 tests/Images/Input/Tiff/flower-rgb-planar-08-15strips.tiff create mode 100644 tests/Images/Input/Tiff/flower-rgb-planar-08-6strips.tiff diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 72f2336a8..011d03779 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -272,11 +272,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff { int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip; + int stripIndex = i; for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) { - int stripIndex = (i * stripsPerPixel) + planeIndex; - decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffers[planeIndex].GetSpan()); + stripIndex += stripsPerPlane; } colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 3bf1c25f3..67892b14b 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -84,6 +84,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_Uncompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(FlowerRgb888Planar6Strips, PixelTypes.Rgba32)] + [WithFile(FlowerRgb888Planar15Strips, PixelTypes.Rgba32)] + public void TiffDecoder_Planar(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] [WithFile(PaletteDeflateMultistrip, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 9c2418436..a42e84650 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -574,6 +574,8 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff"; public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff"; public const string FlowerRgb222Planar = "Tiff/flower-rgb-planar-02.tiff"; + public const string FlowerRgb888Planar6Strips = "Tiff/flower-rgb-planar-08-6strips.tiff"; + public const string FlowerRgb888Planar15Strips = "Tiff/flower-rgb-planar-08-15strips.tiff"; public const string Flower2BitGray = "Tiff/flower-minisblack-02.tiff"; public const string Flower2BitPalette = "Tiff/flower-palette-02.tiff"; public const string Flower4BitPalette = "Tiff/flower-palette-04.tiff"; diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-08-15strips.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-08-15strips.tiff new file mode 100644 index 000000000..1a8deed21 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-08-15strips.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a49cf47fdf2ea43e5cb5a473523e50222fb13ff6a66bda2e4bdd5796f66140d8 +size 9770 diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-08-6strips.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-08-6strips.tiff new file mode 100644 index 000000000..1a8deed21 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-08-6strips.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a49cf47fdf2ea43e5cb5a473523e50222fb13ff6a66bda2e4bdd5796f66140d8 +size 9770 From 8c7ee589e635d8e7be9ccf0213d46eccb0f72155 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 4 Aug 2021 14:19:17 +0200 Subject: [PATCH 72/78] Add support for decoding 32bit per channel color tiff with planar pixel data --- .../Rgb32PlanarTiffColor{TPixel}.cs | 71 +++++++++++++++++++ .../TiffColorDecoderFactory{TPixel}.cs | 22 +++--- .../TiffColorType.cs | 19 ++++- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 17 ++++- .../Formats/Tiff/TiffDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Tiff/flower-rgb-planar-32.tiff | 3 + 7 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-rgb-planar-32.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..a7432549c --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with 'Planar' layout for each color channel with 32 bit. + /// + internal class Rgb32PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgb32PlanarTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromVector4(TiffUtils.Vector4Default); + + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntBigEndian(redData.Slice(offset, 4)); + ulong g = TiffUtils.ConvertToUIntBigEndian(greenData.Slice(offset, 4)); + ulong b = TiffUtils.ConvertToUIntBigEndian(blueData.Slice(offset, 4)); + + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(r, g, b, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntLittleEndian(redData.Slice(offset, 4)); + ulong g = TiffUtils.ConvertToUIntLittleEndian(greenData.Slice(offset, 4)); + ulong b = TiffUtils.ConvertToUIntLittleEndian(blueData.Slice(offset, 4)); + + offset += 4; + + pixelRow[x] = TiffUtils.ColorScaleTo32Bit(r, g, b, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index a34b1ad3e..9b3fb058b 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -189,19 +189,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { switch (colorType) { - case TiffColorType.RgbPlanar: + case TiffColorType.Rgb888Planar: DebugGuard.IsTrue(colorMap == null, "colorMap"); - if (bitsPerSample.Channel0 == 16 && bitsPerSample.Channel1 == 16 && bitsPerSample.Channel2 == 16) - { - return new Rgb16PlanarTiffColor(byteOrder == ByteOrder.BigEndian); - } + return new RgbPlanarTiffColor(bitsPerSample); - if (bitsPerSample.Channel0 == 24 && bitsPerSample.Channel1 == 24 && bitsPerSample.Channel2 == 24) - { - return new Rgb24PlanarTiffColor(byteOrder == ByteOrder.BigEndian); - } + case TiffColorType.Rgb161616Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb16PlanarTiffColor(byteOrder == ByteOrder.BigEndian); - return new RgbPlanarTiffColor(bitsPerSample); + case TiffColorType.Rgb242424Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb24PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + + case TiffColorType.Rgb323232Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgb32PlanarTiffColor(byteOrder == ByteOrder.BigEndian); default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index ffc5a8167..dc47dc8cd 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -134,8 +134,23 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Rgb323232, /// - /// RGB Full Color. Planar configuration of data. + /// RGB Full Color. Planar configuration of data. 8 Bit per color channel. /// - RgbPlanar, + Rgb888Planar, + + /// + /// RGB Full Color. Planar configuration of data. 16 Bit per color channel. + /// + Rgb161616Planar, + + /// + /// RGB Full Color. Planar configuration of data. 24 Bit per color channel. + /// + Rgb242424Planar, + + /// + /// RGB Full Color. Planar configuration of data. 32 Bit per color channel. + /// + Rgb323232Planar, } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index ceb2a3c94..dadf8d707 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -270,7 +270,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff } else { - options.ColorType = TiffColorType.RgbPlanar; + ushort bitsPerChannel = options.BitsPerSample.Channel0; + switch (bitsPerChannel) + { + case 32: + options.ColorType = TiffColorType.Rgb323232Planar; + break; + case 24: + options.ColorType = TiffColorType.Rgb242424Planar; + break; + case 16: + options.ColorType = TiffColorType.Rgb161616Planar; + break; + default: + options.ColorType = TiffColorType.Rgb888Planar; + break; + } } break; diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 19237b4b9..8001701ff 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -201,6 +201,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerRgb323232Contiguous, PixelTypes.Rgba32)] + [WithFile(FlowerRgb323232Planar, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_96Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index bb22cbcc8..c5dec8efc 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -562,6 +562,7 @@ namespace SixLabors.ImageSharp.Tests public const string Rgb4BitPalette = "Tiff/bike_colorpalette_4bit.tiff"; public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff"; public const string FlowerRgb323232Contiguous = "Tiff/flower-rgb-contig-32.tiff"; + public const string FlowerRgb323232Planar = "Tiff/flower-rgb-planar-32.tiff"; public const string FlowerRgb242424Planar = "Tiff/flower-rgb-planar-24.tiff"; public const string FlowerRgb242424PlanarLittleEndian = "Tiff/flower-rgb-planar-24_lsb.tiff"; public const string FlowerRgb242424Contiguous = "Tiff/flower-rgb-contig-24.tiff"; diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-32.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-32.tiff new file mode 100644 index 000000000..a84b4ab37 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-32.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a718ae37d6d7a5bb5702cc75350f6feec3e9cdcd7e22aaa4753c7fe9c2db9aae +size 38035 From 8f1e43a95d35f398a5166b6272df54f4c325f083 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 4 Aug 2021 16:06:12 +0200 Subject: [PATCH 73/78] Additional test images --- tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 4 ++++ tests/ImageSharp.Tests/TestImages.cs | 9 +++++++-- tests/Images/Input/Tiff/flower-minisblack-24.tiff | 2 +- tests/Images/Input/Tiff/flower-minisblack-24_lsb.tiff | 3 +++ tests/Images/Input/Tiff/flower-minisblack-32_lsb.tiff | 3 +++ tests/Images/Input/Tiff/flower-rgb-contig-32_lsb.tiff | 3 +++ tests/Images/Input/Tiff/flower-rgb-planar-32_lsb.tiff | 3 +++ 7 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 tests/Images/Input/Tiff/flower-minisblack-24_lsb.tiff create mode 100644 tests/Images/Input/Tiff/flower-minisblack-32_lsb.tiff create mode 100644 tests/Images/Input/Tiff/flower-rgb-contig-32_lsb.tiff create mode 100644 tests/Images/Input/Tiff/flower-rgb-planar-32_lsb.tiff diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 8001701ff..9b6c40285 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -157,6 +157,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(Flower24BitGray, PixelTypes.Rgba32)] + [WithFile(Flower24BitGrayLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_24Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); @@ -168,6 +169,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(Flower32BitGray, PixelTypes.Rgba32)] + [WithFile(Flower32BitGrayLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_32Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); @@ -201,7 +203,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerRgb323232Contiguous, PixelTypes.Rgba32)] + [WithFile(FlowerRgb323232ContiguousLittleEndian, PixelTypes.Rgba32)] [WithFile(FlowerRgb323232Planar, PixelTypes.Rgba32)] + [WithFile(FlowerRgb323232PlanarLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_96Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index c5dec8efc..a8c42113f 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -562,7 +562,9 @@ namespace SixLabors.ImageSharp.Tests public const string Rgb4BitPalette = "Tiff/bike_colorpalette_4bit.tiff"; public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff"; public const string FlowerRgb323232Contiguous = "Tiff/flower-rgb-contig-32.tiff"; + public const string FlowerRgb323232ContiguousLittleEndian = "Tiff/flower-rgb-contig-32_lsb.tiff"; public const string FlowerRgb323232Planar = "Tiff/flower-rgb-planar-32.tiff"; + public const string FlowerRgb323232PlanarLittleEndian = "Tiff/flower-rgb-planar-32_lsb.tiff"; public const string FlowerRgb242424Planar = "Tiff/flower-rgb-planar-24.tiff"; public const string FlowerRgb242424PlanarLittleEndian = "Tiff/flower-rgb-planar-24_lsb.tiff"; public const string FlowerRgb242424Contiguous = "Tiff/flower-rgb-contig-24.tiff"; @@ -590,11 +592,14 @@ namespace SixLabors.ImageSharp.Tests public const string Flower12BitGray = "Tiff/flower-minisblack-12.tiff"; public const string Flower14BitGray = "Tiff/flower-minisblack-14.tiff"; public const string Flower16BitGray = "Tiff/flower-minisblack-16.tiff"; - public const string Flower24BitGray = "Tiff/flower-minisblack-24.tiff"; - public const string Flower32BitGray = "Tiff/flower-minisblack-32.tiff"; public const string Flower16BitGrayLittleEndian = "Tiff/flower-minisblack-16_lsb.tiff"; public const string Flower16BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-16_lsb.tiff"; public const string Flower16BitGrayMinIsWhiteBigEndian = "Tiff/flower-miniswhite-16.tiff"; + public const string Flower24BitGray = "Tiff/flower-minisblack-24.tiff"; + public const string Flower24BitGrayLittleEndian = "Tiff/flower-minisblack-24_lsb.tiff"; + public const string Flower32BitGray = "Tiff/flower-minisblack-32.tiff"; + public const string Flower32BitGrayLittleEndian = "Tiff/flower-minisblack-32_lsb.tiff"; + public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff"; diff --git a/tests/Images/Input/Tiff/flower-minisblack-24.tiff b/tests/Images/Input/Tiff/flower-minisblack-24.tiff index 7f9dd009d..1fddb22e3 100644 --- a/tests/Images/Input/Tiff/flower-minisblack-24.tiff +++ b/tests/Images/Input/Tiff/flower-minisblack-24.tiff @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe2d4e0d99bdfade966e27bd9583bce39bebb90efa8e7f768ce3cec69aa306e2 +oid sha256:6b5a96942ee27a2b25d3cbb8bdd05239be71f84acc4d63c95380841a8a67befd size 9770 diff --git a/tests/Images/Input/Tiff/flower-minisblack-24_lsb.tiff b/tests/Images/Input/Tiff/flower-minisblack-24_lsb.tiff new file mode 100644 index 000000000..7f9dd009d --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-24_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe2d4e0d99bdfade966e27bd9583bce39bebb90efa8e7f768ce3cec69aa306e2 +size 9770 diff --git a/tests/Images/Input/Tiff/flower-minisblack-32_lsb.tiff b/tests/Images/Input/Tiff/flower-minisblack-32_lsb.tiff new file mode 100644 index 000000000..cc3be01d2 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-minisblack-32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e37a4455e6b61e32720af99127b82aacdc907be91b8ed1d8e1a1f06d6a853211 +size 12885 diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-32_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-32_lsb.tiff new file mode 100644 index 000000000..c602b5c4a --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-contig-32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0876580f9c5d8e13656210582137104daba137c99d55eafb5ebbfa418efa6525 +size 38027 diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-32_lsb.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-32_lsb.tiff new file mode 100644 index 000000000..5caa0886e --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-planar-32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2241683d74e9a52c5077870731e7bd5a7e7558c2a04fd0edf57da3a583044442 +size 38035 From f20a16593c14c59081ae56f156f6be61f06c4497 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 4 Aug 2021 16:27:30 +0200 Subject: [PATCH 74/78] Only DebugSave testimages with bit depth >= 24 --- .../Formats/Tiff/TiffDecoderTests.cs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 9b6c40285..b0b2a6b56 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -159,7 +159,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [WithFile(Flower24BitGray, PixelTypes.Rgba32)] [WithFile(Flower24BitGrayLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_24Bit_Gray(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } [Theory] [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)] @@ -171,7 +176,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [WithFile(Flower32BitGray, PixelTypes.Rgba32)] [WithFile(Flower32BitGrayLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_32Bit_Gray(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } [Theory] [WithFile(FlowerRgb121212Contiguous, PixelTypes.Rgba32)] @@ -199,7 +209,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [WithFile(FlowerRgb242424Planar, PixelTypes.Rgba32)] [WithFile(FlowerRgb242424PlanarLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_72Bit(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } [Theory] [WithFile(FlowerRgb323232Contiguous, PixelTypes.Rgba32)] @@ -207,7 +222,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [WithFile(FlowerRgb323232Planar, PixelTypes.Rgba32)] [WithFile(FlowerRgb323232PlanarLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_96Bit(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } [Theory] [WithFile(GrayscaleDeflateMultistrip, PixelTypes.Rgba32)] From 9b1276d2dae3cedd5dcc52395298f74f18a1b141 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 4 Aug 2021 18:04:42 +0200 Subject: [PATCH 75/78] Add test images for miniswhite 32 bit --- tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 2 ++ tests/ImageSharp.Tests/TestImages.cs | 2 ++ tests/Images/Input/Tiff/flower-miniswhite-32.tiff | 3 +++ tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff | 3 +++ 4 files changed, 10 insertions(+) create mode 100644 tests/Images/Input/Tiff/flower-miniswhite-32.tiff create mode 100644 tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index b0b2a6b56..504892ca2 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -175,6 +175,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(Flower32BitGray, PixelTypes.Rgba32)] [WithFile(Flower32BitGrayLittleEndian, PixelTypes.Rgba32)] + [WithFile(Flower32BitGrayMinIsWhite, PixelTypes.Rgba32)] + [WithFile(Flower32BitGrayMinIsWhiteLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_32Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a8c42113f..ad2dc1f59 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -599,6 +599,8 @@ namespace SixLabors.ImageSharp.Tests public const string Flower24BitGrayLittleEndian = "Tiff/flower-minisblack-24_lsb.tiff"; public const string Flower32BitGray = "Tiff/flower-minisblack-32.tiff"; public const string Flower32BitGrayLittleEndian = "Tiff/flower-minisblack-32_lsb.tiff"; + public const string Flower32BitGrayMinIsWhite = "Tiff/flower-miniswhite-32.tiff"; + public const string Flower32BitGrayMinIsWhiteLittleEndian = "Tiff/flower-miniswhite-32_lsb.tiff"; public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; diff --git a/tests/Images/Input/Tiff/flower-miniswhite-32.tiff b/tests/Images/Input/Tiff/flower-miniswhite-32.tiff new file mode 100644 index 000000000..f8cf87553 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-32.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:514417ead3d6c5c6ca33374ef0bb6ecbe5f875a266519d4cbaa4a6b91033d243 +size 12778 diff --git a/tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff b/tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff new file mode 100644 index 000000000..8c99dda7f --- /dev/null +++ b/tests/Images/Input/Tiff/flower-miniswhite-32_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64c948aa03bc4a24cd1d68bb18b5031c119936154a90f1cb1d9aaabd854c5d9b +size 12778 From c61ce85fb7eb4959d747dd55650836e80f04c605 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 5 Aug 2021 11:11:06 +0200 Subject: [PATCH 76/78] Throw exception if bits per channel for rgb are not equal --- src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index dadf8d707..8590203a7 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -220,11 +220,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff case TiffPhotometricInterpretation.Rgb: { - if (options.BitsPerSample.Channels != 3) + TiffBitsPerSample bitsPerSample = options.BitsPerSample; + if (bitsPerSample.Channels != 3) { TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); } + if (!(bitsPerSample.Channel0 == bitsPerSample.Channel1 && bitsPerSample.Channel1 == bitsPerSample.Channel2)) + { + TiffThrowHelper.ThrowNotSupported("Only BitsPerSample with equal bits per channel are supported."); + } + if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky) { ushort bitsPerChannel = options.BitsPerSample.Channel0; From 4e5dec9f4f75f779456d43134729b73f2213acee Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 5 Aug 2021 12:47:03 +0200 Subject: [PATCH 77/78] Introduce variable for buffer.AsSpan(bufferStartIdx) --- .../BlackIsZero24TiffColor{TPixel}.cs | 5 +++-- .../Rgb242424TiffColor{TPixel}.cs | 13 +++++++------ .../Rgb24PlanarTiffColor{TPixel}.cs | 13 +++++++------ .../WhiteIsZero24TiffColor{TPixel}.cs | 5 +++-- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs index 813d7beb5..ec07abd5c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs @@ -32,6 +32,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation byte[] buffer = new byte[4]; int bufferStartIdx = this.isBigEndian ? 1 : 0; + Span bufferSpan = buffer.AsSpan(bufferStartIdx); int offset = 0; for (int y = top; y < top + height; y++) { @@ -40,7 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + data.Slice(offset, 3).CopyTo(bufferSpan); ulong intensity = TiffUtils.ConvertToUIntBigEndian(buffer); offset += 3; @@ -51,7 +52,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + data.Slice(offset, 3).CopyTo(bufferSpan); ulong intensity = TiffUtils.ConvertToUIntLittleEndian(buffer); offset += 3; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs index abc3a82a9..3be0540a0 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs @@ -33,6 +33,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation byte[] buffer = new byte[4]; int bufferStartIdx = this.isBigEndian ? 1 : 0; + Span bufferSpan = buffer.AsSpan(bufferStartIdx); for (int y = top; y < top + height; y++) { Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); @@ -41,15 +42,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + data.Slice(offset, 3).CopyTo(bufferSpan); ulong r = TiffUtils.ConvertToUIntBigEndian(buffer); offset += 3; - data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + data.Slice(offset, 3).CopyTo(bufferSpan); ulong g = TiffUtils.ConvertToUIntBigEndian(buffer); offset += 3; - data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + data.Slice(offset, 3).CopyTo(bufferSpan); ulong b = TiffUtils.ConvertToUIntBigEndian(buffer); offset += 3; @@ -60,15 +61,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + data.Slice(offset, 3).CopyTo(bufferSpan); ulong r = TiffUtils.ConvertToUIntLittleEndian(buffer); offset += 3; - data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + data.Slice(offset, 3).CopyTo(bufferSpan); ulong g = TiffUtils.ConvertToUIntLittleEndian(buffer); offset += 3; - data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + data.Slice(offset, 3).CopyTo(bufferSpan); ulong b = TiffUtils.ConvertToUIntLittleEndian(buffer); offset += 3; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs index cd94f8e81..9c3e57e2a 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs @@ -36,6 +36,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Span redData = data[0].GetSpan(); Span greenData = data[1].GetSpan(); Span blueData = data[2].GetSpan(); + Span bufferSpan = buffer.AsSpan(bufferStartIdx); int offset = 0; for (int y = top; y < top + height; y++) @@ -45,11 +46,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - redData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + redData.Slice(offset, 3).CopyTo(bufferSpan); ulong r = TiffUtils.ConvertToUIntBigEndian(buffer); - greenData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + greenData.Slice(offset, 3).CopyTo(bufferSpan); ulong g = TiffUtils.ConvertToUIntBigEndian(buffer); - blueData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + blueData.Slice(offset, 3).CopyTo(bufferSpan); ulong b = TiffUtils.ConvertToUIntBigEndian(buffer); offset += 3; @@ -61,11 +62,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - redData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + redData.Slice(offset, 3).CopyTo(bufferSpan); ulong r = TiffUtils.ConvertToUIntLittleEndian(buffer); - greenData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + greenData.Slice(offset, 3).CopyTo(bufferSpan); ulong g = TiffUtils.ConvertToUIntLittleEndian(buffer); - blueData.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + blueData.Slice(offset, 3).CopyTo(bufferSpan); ulong b = TiffUtils.ConvertToUIntLittleEndian(buffer); offset += 3; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs index 143a684c2..b1088732b 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs @@ -32,6 +32,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation byte[] buffer = new byte[4]; int bufferStartIdx = this.isBigEndian ? 1 : 0; + Span bufferSpan = buffer.AsSpan(bufferStartIdx); int offset = 0; for (int y = top; y < top + height; y++) { @@ -40,7 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + data.Slice(offset, 3).CopyTo(bufferSpan); ulong intensity = TiffUtils.ConvertToUIntBigEndian(buffer); offset += 3; @@ -51,7 +52,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { for (int x = 0; x < pixelRow.Length; x++) { - data.Slice(offset, 3).CopyTo(buffer.AsSpan(bufferStartIdx)); + data.Slice(offset, 3).CopyTo(bufferSpan); ulong intensity = TiffUtils.ConvertToUIntLittleEndian(buffer); offset += 3; From 01202b62c381be6bf1724601d0ebf4c1673c38fa Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 6 Aug 2021 14:39:58 +0200 Subject: [PATCH 78/78] Use bulk pixel conversion for some tiff color decoders --- .../BlackIsZero16TiffColor{TPixel}.cs | 22 ++++++++++----- .../BlackIsZero8TiffColor{TPixel}.cs | 21 ++++++++------ .../Rgb161616TiffColor{TPixel}.cs | 26 +++++++++-------- .../Rgb888TiffColor{TPixel}.cs | 28 ++++++++----------- .../TiffColorDecoderFactory{TPixel}.cs | 10 +++---- .../Formats/Tiff/TiffDecoderCore.cs | 2 +- .../BlackIsZeroTiffColorTests.cs | 2 +- .../RgbTiffColorTests.cs | 2 +- 8 files changed, 62 insertions(+), 51 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs index a31ff7a9f..1cddc9b63 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -16,11 +16,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { private readonly bool isBigEndian; + private readonly Configuration configuration; + /// /// Initializes a new instance of the class. /// + /// The configuration. /// if set to true decodes the pixel data as big endian, otherwise as little endian. - public BlackIsZero16TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + public BlackIsZero16TiffColor(Configuration configuration, bool isBigEndian) + { + this.configuration = configuration; + this.isBigEndian = isBigEndian; + } /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) @@ -47,13 +54,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } else { - for (int x = 0; x < pixelRow.Length; x++) - { - ushort intensity = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); - offset += 2; + int byteCount = pixelRow.Length * 2; + PixelOperations.Instance.FromL16Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); - pixelRow[x] = TiffUtils.ColorFromL16(l16, intensity, color); - } + offset += byteCount; } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs index ea2608f6f..f62cf2952 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using SixLabors.ImageSharp.Formats.Tiff.Utils; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -14,22 +13,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal class BlackIsZero8TiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { + private readonly Configuration configuration; + + public BlackIsZero8TiffColor(Configuration configuration) => this.configuration = configuration; + /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { - var color = default(TPixel); - int offset = 0; - var l8 = default(L8); for (int y = top; y < top + height; y++) { Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); - for (int x = 0; x < pixelRow.Length; x++) - { - byte intensity = data[offset++]; - pixelRow[x] = TiffUtils.ColorFromL8(l8, intensity, color); - } + int byteCount = pixelRow.Length; + PixelOperations.Instance.FromL8Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + offset += byteCount; } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index 4b34d5a0d..e0d558a14 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -16,11 +16,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { private readonly bool isBigEndian; + private readonly Configuration configuration; + /// /// Initializes a new instance of the class. /// + /// The configuration. /// if set to true decodes the pixel data as big endian, otherwise as little endian. - public Rgb161616TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + public Rgb161616TiffColor(Configuration configuration, bool isBigEndian) + { + this.configuration = configuration; + this.isBigEndian = isBigEndian; + } /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) @@ -53,17 +60,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } else { - for (int x = 0; x < pixelRow.Length; x++) - { - ulong r = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); - offset += 2; - ulong g = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); - offset += 2; - ulong b = TiffUtils.ConvertToShortLittleEndian(data.Slice(offset, 2)); - offset += 2; + int byteCount = pixelRow.Length * 6; + PixelOperations.Instance.FromRgb48Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); - pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); - } + offset += byteCount; } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs index 536dece8e..2a86eb2ee 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; - using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -14,29 +13,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal class Rgb888TiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { + private readonly Configuration configuration; + + public Rgb888TiffColor(Configuration configuration) => this.configuration = configuration; + /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { - var color = default(TPixel); - int offset = 0; - var rgba = default(Rgba32); for (int y = top; y < top + height; y++) { Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); - - for (int x = 0; x < pixelRow.Length; x++) - { - byte r = data[offset++]; - byte g = data[offset++]; - byte b = data[offset++]; - - rgba.PackedValue = (uint)(r | (g << 8) | (b << 16) | (0xff << 24)); - color.FromRgba32(rgba); - - pixelRow[x] = color; - } + int byteCount = pixelRow.Length * 3; + PixelOperations.Instance.FromRgb24Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + offset += byteCount; } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 0c93998c4..97d3d8665 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal static class TiffColorDecoderFactory where TPixel : unmanaged, IPixel { - public static TiffBaseColorDecoder Create(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) + public static TiffBaseColorDecoder Create(Configuration configuration, TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) { switch (colorType) { @@ -55,12 +55,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation case TiffColorType.BlackIsZero8: DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 8, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); - return new BlackIsZero8TiffColor(); + return new BlackIsZero8TiffColor(configuration); case TiffColorType.BlackIsZero16: DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 16, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); - return new BlackIsZero16TiffColor(byteOrder == ByteOrder.BigEndian); + return new BlackIsZero16TiffColor(configuration, byteOrder == ByteOrder.BigEndian); case TiffColorType.Rgb: DebugGuard.IsTrue(colorMap == null, "colorMap"); @@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation && bitsPerSample.Channel0 == 8, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); - return new Rgb888TiffColor(); + return new Rgb888TiffColor(configuration); case TiffColorType.Rgb101010: DebugGuard.IsTrue( @@ -134,7 +134,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation && bitsPerSample.Channel0 == 16, "bitsPerSample"); DebugGuard.IsTrue(colorMap == null, "colorMap"); - return new Rgb161616TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + return new Rgb161616TiffColor(configuration, isBigEndian: byteOrder == ByteOrder.BigEndian); case TiffColorType.PaletteColor: DebugGuard.NotNull(colorMap, "colorMap"); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 011d03779..63185619d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -316,7 +316,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.Predictor, this.FaxCompressionOptions); - TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); + TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.Configuration, this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs index 769ab850e..38611c6f3 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs @@ -188,7 +188,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation { AssertDecode(expectedResult, pixels => { - new BlackIsZero8TiffColor().Decode(inputData, pixels, left, top, width, height); + new BlackIsZero8TiffColor(Configuration.Default).Decode(inputData, pixels, left, top, width, height); }); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs index 9adf59e48..f9f633106 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs @@ -179,7 +179,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation { AssertDecode(expectedResult, pixels => { - new Rgb888TiffColor().Decode(inputData, pixels, left, top, width, height); + new Rgb888TiffColor(Configuration.Default).Decode(inputData, pixels, left, top, width, height); }); } }