From 34ae604b29aa523fc6b4dc3efe1a42233fbf03ba Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 4 Nov 2022 20:17:06 +0100 Subject: [PATCH 1/7] Add support for decoding tiled tiff images --- src/ImageSharp/Formats/Tiff/README.md | 22 +- .../Formats/Tiff/TiffDecoderCore.cs | 395 ++++++++++++++---- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 63 ++- .../Profiles/Exif/Tags/ExifTag.LongArray.cs | 5 - .../Profiles/Exif/Tags/ExifTag.NumberArray.cs | 5 + .../Profiles/Exif/Values/ExifValues.cs | 4 +- 6 files changed, 378 insertions(+), 116 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 99594b4c54..8f4cd83639 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -46,18 +46,20 @@ ### Photometric Interpretation Formats -| |Encoder|Decoder|Comments | -|---------------------------|:-----:|:-----:|-------------------------------------------| +| |Encoder|Decoder|Comments | +|---------------------------|:-----:|:-----:|------------------------------------------------| |WhiteIsZero | Y | Y | General + 1/4/8-bit optimised implementations. | |BlackIsZero | Y | Y | General + 1/4/8-bit optimised implementations. | -|Rgb (Chunky) | Y | Y | General + Rgb888 optimised implementation.| -|Rgb (Planar) | | Y | General implementation only. | -|PaletteColor | Y | Y | General implementation only. | -|TransparencyMask | | | | -|Separated (TIFF Extension) | | Y | | -|YCbCr (TIFF Extension) | | Y | | -|CieLab (TIFF Extension) | | Y | | -|IccLab (TechNote 1) | | | | +|Rgb (Chunky) | Y | Y | General + Rgb888 optimised implementation. | +|Rgb (Planar) | | Y | General implementation only. | +|PaletteColor | Y | Y | General implementation only. | +|TransparencyMask | | | | +|Separated (TIFF Extension) | | Y | | +|YCbCr (TIFF Extension) | | Y | | +|CieLab (TIFF Extension) | | Y | | +|IccLab (TechNote 1) | | | | +|CMYK | | Y | | +|Tiled Images | | Y | | ### Baseline TIFF Tags diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 59d8b6ecb6..0324759824 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -214,11 +214,11 @@ internal class TiffDecoderCore : IImageDecoderInternals public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.inputStream = stream; - var reader = new DirectoryReader(stream, this.configuration.MemoryAllocator); + DirectoryReader reader = new(stream, this.configuration.MemoryAllocator); IEnumerable directories = reader.Read(); ExifProfile rootFrameExifProfile = directories.First(); - var rootMetadata = TiffFrameMetadata.Parse(rootFrameExifProfile); + TiffFrameMetadata rootMetadata = TiffFrameMetadata.Parse(rootFrameExifProfile); ImageMetadata metadata = TiffDecoderMetadataCreator.Create(reader.ByteOrder, reader.IsBigTiff, rootFrameExifProfile); int width = GetImageWidth(rootFrameExifProfile); @@ -246,13 +246,37 @@ internal class TiffDecoderCore : IImageDecoderInternals TiffFrameMetadata tiffFrameMetaData = imageFrameMetaData.GetTiffMetadata(); TiffFrameMetadata.Parse(tiffFrameMetaData, tags); - this.VerifyAndParse(tags, tiffFrameMetaData); + bool isTiled = this.VerifyAndParse(tags, tiffFrameMetaData); int width = GetImageWidth(tags); int height = GetImageHeight(tags); ImageFrame frame = new(this.configuration, width, height, imageFrameMetaData); - int rowsPerStrip = tags.GetValue(ExifTag.RowsPerStrip) != null ? (int)tags.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; + if (isTiled) + { + this.DecodeImageWithTiles(tags, frame, cancellationToken); + } + else + { + this.DecodeImageWithStrips(tags, frame, cancellationToken); + } + + return frame; + } + + /// + /// Decodes the image data for Tiff's which arrange the pixel data in stripes. + /// + /// The pixel format. + /// The IFD tags. + /// The image frame to decode into. + /// The token to monitor cancellation. + private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + int rowsPerStrip = tags.GetValue(ExifTag.RowsPerStrip) != null + ? (int)tags.GetValue(ExifTag.RowsPerStrip).Value + : TiffConstants.RowsPerStripInfinity; Array stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue(); Array stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue(); @@ -278,71 +302,35 @@ internal class TiffDecoderCore : IImageDecoderInternals stripByteCounts, cancellationToken); } - - return frame; - } - - private IMemoryOwner ConvertNumbers(Array array, out Span span) - { - if (array is Number[] numbers) - { - IMemoryOwner memory = this.memoryAllocator.Allocate(numbers.Length); - span = memory.GetSpan(); - for (int i = 0; i < numbers.Length; i++) - { - span[i] = (uint)numbers[i]; - } - - return memory; - } - - DebugGuard.IsTrue(array is ulong[], $"Expected {nameof(UInt64)} array."); - span = (ulong[])array; - return null; } /// - /// Calculates the size (in bytes) for a pixel buffer using the determined color format. + /// Decodes the image data for Tiff's which arrange the pixel data in tiles. /// - /// The width for the desired pixel buffer. - /// The height for the desired pixel buffer. - /// The index of the plane for planar image configuration (or zero for chunky). - /// The size (in bytes) of the required pixel buffer. - private int CalculateStripBufferSize(int width, int height, int plane = -1) + /// The pixel format. + /// The IFD tags. + /// The image frame to decode into. + /// The token to monitor cancellation. + private void DecodeImageWithTiles(ExifProfile tags, ImageFrame frame, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel { - DebugGuard.MustBeLessThanOrEqualTo(plane, 3, nameof(plane)); + Buffer2D pixels = frame.PixelBuffer; + int width = pixels.Width; + int height = pixels.Height; - int bitsPerPixel = 0; + int tileWidth = (int)tags.GetValue(ExifTag.TileWidth).Value; + int tileLength = (int)tags.GetValue(ExifTag.TileLength).Value; + int tilesAcross = (width + tileWidth - 1) / tileWidth; + int tilesDown = (height + tileLength - 1) / tileLength; - if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky) + if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) { - DebugGuard.IsTrue(plane == -1, "Expected Chunky planar."); - bitsPerPixel = this.BitsPerPixel; + this.DecodeTilesPlanar(tags, frame, tileWidth, tileLength, tilesAcross, tilesDown, cancellationToken); } else { - switch (plane) - { - case 0: - bitsPerPixel = this.BitsPerSample.Channel0; - break; - case 1: - bitsPerPixel = this.BitsPerSample.Channel1; - break; - case 2: - bitsPerPixel = this.BitsPerSample.Channel2; - break; - case 3: - bitsPerPixel = this.BitsPerSample.Channel2; - break; - default: - TiffThrowHelper.ThrowNotSupported("More then 4 color channels are not supported"); - break; - } + this.DecodeTilesChunky(tags, frame, tileWidth, tileLength, tilesAcross, tilesDown, cancellationToken); } - - int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; - return bytesPerRow * height; } /// @@ -373,20 +361,7 @@ internal class TiffDecoderCore : IImageDecoderInternals stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); } - using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( - this.Options, - this.CompressionType, - this.memoryAllocator, - this.PhotometricInterpretation, - frame.Width, - bitsPerPixel, - this.ColorType, - this.Predictor, - this.FaxCompressionOptions, - this.JpegTables, - this.OldJpegCompressionStartOfImageMarker.GetValueOrDefault(), - this.FillOrder, - this.byteOrder); + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar( this.ColorType, @@ -455,20 +430,7 @@ internal class TiffDecoderCore : IImageDecoderInternals Span stripBufferSpan = stripBuffer.GetSpan(); Buffer2D pixels = frame.PixelBuffer; - using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( - this.Options, - this.CompressionType, - this.memoryAllocator, - this.PhotometricInterpretation, - frame.Width, - bitsPerPixel, - this.ColorType, - this.Predictor, - this.FaxCompressionOptions, - this.JpegTables, - this.OldJpegCompressionStartOfImageMarker.GetValueOrDefault(), - this.FillOrder, - this.byteOrder); + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( this.configuration, @@ -509,6 +471,269 @@ internal class TiffDecoderCore : IImageDecoderInternals } } + /// + /// Decodes the image data for Tiff's which arrange the pixel data in tiles and the planar configuration. + /// + /// The pixel format. + /// The IFD tags. + /// The image frame to decode into. + /// The width in pixels of the tile. + /// The height in pixels of the tile. + /// The number of tiles horizontally. + /// The number of tiles vertically. + /// The token to monitor cancellation. + private void DecodeTilesPlanar(ExifProfile tags, ImageFrame frame, int tileWidth, int tileLength, int tilesAcross, int tilesDown, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Buffer2D pixels = frame.PixelBuffer; + int width = pixels.Width; + int height = pixels.Height; + int bitsPerPixel = this.BitsPerPixel; + + Array tilesOffsetsArray = (Array)tags.GetValueInternal(ExifTag.TileOffsets).GetValue(); + Array tilesByteCountsArray = (Array)tags.GetValueInternal(ExifTag.TileByteCounts).GetValue(); + using IMemoryOwner tileOffsetsMemory = this.ConvertNumbers(tilesOffsetsArray, out Span tileOffsets); + using IMemoryOwner tileByteCountsMemory = this.ConvertNumbers(tilesByteCountsArray, out Span tileByteCounts); + + int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; + int bytesPerTileRow = ((tileWidth * bitsPerPixel) + 7) / 8; + int uncompressedTilesSize = bytesPerTileRow * tileLength; + using IMemoryOwner tileBuffer = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean); + using IMemoryOwner uncompressedPixelBuffer = this.memoryAllocator.Allocate(tilesDown * tileLength * bytesPerRow, AllocationOptions.Clean); + Span tileBufferSpan = tileBuffer.GetSpan(); + Span uncompressedPixelBufferSpan = uncompressedPixelBuffer.GetSpan(); + + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + + TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( + this.configuration, + this.memoryAllocator, + this.ColorType, + this.BitsPerSample, + this.ExtraSamplesType, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.YcbcrSubSampling, + this.byteOrder); + + int tileIndex = 0; + for (int tileY = 0; tileY < tilesDown; tileY++) + { + int uncompressedPixelBufferOffset = tileY * tileLength * bytesPerRow; + int remainingPixelsInRow = width; + for (int tileX = 0; tileX < tilesAcross; tileX++) + { + cancellationToken.ThrowIfCancellationRequested(); + + bool isLastHorizontalTile = tileX == tilesAcross - 1; + + decompressor.Decompress( + this.inputStream, + tileOffsets[tileIndex], + tileByteCounts[tileIndex], + tileLength, + tileBufferSpan, + cancellationToken); + + int tileBufferOffset = 0; + uncompressedPixelBufferOffset += bytesPerTileRow * tileX; + int bytesToCopy = isLastHorizontalTile ? ((bitsPerPixel * remainingPixelsInRow) + 7) / 8 : bytesPerTileRow; + for (int y = 0; y < tileLength; y++) + { + Span uncompressedPixelRow = uncompressedPixelBufferSpan.Slice(uncompressedPixelBufferOffset, bytesToCopy); + tileBufferSpan.Slice(tileBufferOffset, bytesToCopy).CopyTo(uncompressedPixelRow); + tileBufferOffset += bytesPerTileRow; + uncompressedPixelBufferOffset += bytesPerRow; + } + + remainingPixelsInRow -= tileWidth; + tileIndex++; + } + } + + colorDecoder.Decode(uncompressedPixelBufferSpan, pixels, 0, 0, width, height); + } + + /// + /// Decodes the image data for Tiff's which arrange the pixel data in tiles and the chunky configuration. + /// + /// The pixel format. + /// The IFD tags. + /// The image frame to decode into. + /// The width in pixels of the tile. + /// The height in pixels of the tile. + /// The number of tiles horizontally. + /// The number of tiles vertically. + /// The token to monitor cancellation. + private void DecodeTilesChunky(ExifProfile tags, ImageFrame frame, int tileWidth, int tileLength, int tilesAcross, int tilesDown, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Buffer2D pixels = frame.PixelBuffer; + int width = pixels.Width; + int height = pixels.Height; + int bitsPerPixel = this.BitsPerPixel; + + Array tilesOffsetsArray; + Array tilesByteCountsArray; + IExifValue tilesOffsetsExifValue = tags.GetValueInternal(ExifTag.TileOffsets); + IExifValue tilesByteCountsExifValue = tags.GetValueInternal(ExifTag.TileByteCounts); + if (tilesOffsetsExifValue is null) + { + tilesOffsetsExifValue = tags.GetValueInternal(ExifTag.StripOffsets); + tilesByteCountsExifValue = tags.GetValueInternal(ExifTag.StripByteCounts); + tilesOffsetsArray = (Array)tilesOffsetsExifValue.GetValue(); + tilesByteCountsArray = (Array)tilesByteCountsExifValue.GetValue(); + } + else + { + tilesOffsetsArray = (Array)tilesOffsetsExifValue.GetValue(); + tilesByteCountsArray = (Array)tilesByteCountsExifValue.GetValue(); + } + + using IMemoryOwner tileOffsetsMemory = this.ConvertNumbers(tilesOffsetsArray, out Span tileOffsets); + using IMemoryOwner tileByteCountsMemory = this.ConvertNumbers(tilesByteCountsArray, out Span tileByteCounts); + + int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; + int bytesPerTileRow = ((tileWidth * bitsPerPixel) + 7) / 8; + int uncompressedTilesSize = bytesPerTileRow * tileLength; + using IMemoryOwner tileBuffer = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean); + using IMemoryOwner uncompressedPixelBuffer = this.memoryAllocator.Allocate(tilesDown * tileLength * bytesPerRow, AllocationOptions.Clean); + Span tileBufferSpan = tileBuffer.GetSpan(); + Span uncompressedPixelBufferSpan = uncompressedPixelBuffer.GetSpan(); + + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + + TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( + this.configuration, + this.memoryAllocator, + this.ColorType, + this.BitsPerSample, + this.ExtraSamplesType, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.YcbcrSubSampling, + this.byteOrder); + + int tileIndex = 0; + for (int tileY = 0; tileY < tilesDown; tileY++) + { + int remainingPixelsInRow = width; + for (int tileX = 0; tileX < tilesAcross; tileX++) + { + cancellationToken.ThrowIfCancellationRequested(); + + int uncompressedPixelBufferOffset = tileY * tileLength * bytesPerRow; + bool isLastHorizontalTile = tileX == tilesAcross - 1; + + decompressor.Decompress( + this.inputStream, + tileOffsets[tileIndex], + tileByteCounts[tileIndex], + tileLength, + tileBufferSpan, + cancellationToken); + + int tileBufferOffset = 0; + uncompressedPixelBufferOffset += bytesPerTileRow * tileX; + int bytesToCopy = isLastHorizontalTile ? ((bitsPerPixel * remainingPixelsInRow) + 7) / 8 : bytesPerTileRow; + for (int y = 0; y < tileLength; y++) + { + Span uncompressedPixelRow = uncompressedPixelBufferSpan.Slice(uncompressedPixelBufferOffset, bytesToCopy); + tileBufferSpan.Slice(tileBufferOffset, bytesToCopy).CopyTo(uncompressedPixelRow); + tileBufferOffset += bytesPerTileRow; + uncompressedPixelBufferOffset += bytesPerRow; + } + + remainingPixelsInRow -= tileWidth; + tileIndex++; + } + } + + colorDecoder.Decode(uncompressedPixelBufferSpan, pixels, 0, 0, width, height); + } + + private TiffBaseDecompressor CreateDecompressor(int frameWidth, int bitsPerPixel) + where TPixel : unmanaged, IPixel => + TiffDecompressorsFactory.Create( + this.Options, + this.CompressionType, + this.memoryAllocator, + this.PhotometricInterpretation, + frameWidth, + bitsPerPixel, + this.ColorType, + this.Predictor, + this.FaxCompressionOptions, + this.JpegTables, + this.OldJpegCompressionStartOfImageMarker.GetValueOrDefault(), + this.FillOrder, + this.byteOrder); + + private IMemoryOwner ConvertNumbers(Array array, out Span span) + { + if (array is Number[] numbers) + { + IMemoryOwner memory = this.memoryAllocator.Allocate(numbers.Length); + span = memory.GetSpan(); + for (int i = 0; i < numbers.Length; i++) + { + span[i] = (uint)numbers[i]; + } + + return memory; + } + + DebugGuard.IsTrue(array is ulong[], $"Expected {nameof(UInt64)} array."); + span = (ulong[])array; + return null; + } + + /// + /// Calculates the size (in bytes) for a pixel buffer using the determined color format. + /// + /// The width for the desired pixel buffer. + /// The height for the desired pixel buffer. + /// The index of the plane for planar image configuration (or zero for chunky). + /// The size (in bytes) of the required pixel buffer. + private int CalculateStripBufferSize(int width, int height, int plane = -1) + { + DebugGuard.MustBeLessThanOrEqualTo(plane, 3, nameof(plane)); + + int bitsPerPixel = 0; + + if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky) + { + DebugGuard.IsTrue(plane == -1, "Expected Chunky planar."); + bitsPerPixel = this.BitsPerPixel; + } + else + { + switch (plane) + { + case 0: + bitsPerPixel = this.BitsPerSample.Channel0; + break; + case 1: + bitsPerPixel = this.BitsPerSample.Channel1; + break; + case 2: + bitsPerPixel = this.BitsPerSample.Channel2; + break; + case 3: + bitsPerPixel = this.BitsPerSample.Channel2; + break; + default: + TiffThrowHelper.ThrowNotSupported("More then 4 color channels are not supported"); + break; + } + } + + int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; + return bytesPerRow * height; + } + /// /// Gets the width of the image frame. /// diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 5a4baafa42..384e2389c1 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -21,13 +21,9 @@ internal static class TiffDecoderOptionsParser /// The options. /// The exif profile of the frame to decode. /// The IFD entries container to read the image format information for current frame. - public static void VerifyAndParse(this TiffDecoderCore options, ExifProfile exifProfile, TiffFrameMetadata frameMetadata) + /// True, if the image uses tiles. Otherwise the images has strip's. + public static bool VerifyAndParse(this TiffDecoderCore options, ExifProfile exifProfile, TiffFrameMetadata frameMetadata) { - if (exifProfile.GetValueInternal(ExifTag.TileOffsets) is not null || exifProfile.GetValueInternal(ExifTag.TileByteCounts) is not null) - { - TiffThrowHelper.ThrowNotSupported("Tiled images are not supported."); - } - IExifValue extraSamplesExifValue = exifProfile.GetValueInternal(ExifTag.ExtraSamples); if (extraSamplesExifValue is not null) { @@ -86,8 +82,6 @@ internal static class TiffDecoderOptionsParser TiffThrowHelper.ThrowNotSupported("Variable-sized strips are not supported."); } - VerifyRequiredFieldsArePresent(exifProfile, frameMetadata); - options.PlanarConfiguration = (TiffPlanarConfiguration?)exifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value ?? DefaultPlanarConfiguration; options.Predictor = frameMetadata.Predictor ?? TiffPredictor.None; options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb; @@ -103,24 +97,65 @@ internal static class TiffDecoderOptionsParser options.ParseColorType(exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile); + + bool isTiled = VerifyRequiredFieldsArePresent(exifProfile, frameMetadata, options.PlanarConfiguration); + + return isTiled; } - private static void VerifyRequiredFieldsArePresent(ExifProfile exifProfile, TiffFrameMetadata frameMetadata) + /// + /// Verifies that all required fields for decoding are present. + /// + /// The exif profile. + /// The frame metadata. + /// The planar configuration. Either planar or chunky. + /// True, if the image uses tiles. Otherwise the images has strip's. + private static bool VerifyRequiredFieldsArePresent(ExifProfile exifProfile, TiffFrameMetadata frameMetadata, TiffPlanarConfiguration planarConfiguration) { - if (exifProfile.GetValueInternal(ExifTag.StripOffsets) is null) + bool isTiled = false; + if (exifProfile.GetValueInternal(ExifTag.TileWidth) is not null || exifProfile.GetValueInternal(ExifTag.TileLength) is not null) { - TiffThrowHelper.ThrowImageFormatException("StripOffsets are missing and are required for decoding the TIFF image!"); - } + if (planarConfiguration == TiffPlanarConfiguration.Planar && exifProfile.GetValueInternal(ExifTag.TileOffsets) is null) + { + TiffThrowHelper.ThrowImageFormatException("TileOffsets are missing and are required for decoding the TIFF image!"); + } + + if (planarConfiguration == TiffPlanarConfiguration.Chunky && exifProfile.GetValueInternal(ExifTag.TileOffsets) is null && exifProfile.GetValueInternal(ExifTag.StripOffsets) is null) + { + TiffThrowHelper.ThrowImageFormatException("TileOffsets are missing and are required for decoding the TIFF image!"); + } - if (exifProfile.GetValueInternal(ExifTag.StripByteCounts) is null) + if (exifProfile.GetValueInternal(ExifTag.TileWidth) is null) + { + TiffThrowHelper.ThrowImageFormatException("TileWidth are missing and are required for decoding the TIFF image!"); + } + + if (exifProfile.GetValueInternal(ExifTag.TileLength) is null) + { + TiffThrowHelper.ThrowImageFormatException("TileLength are missing and are required for decoding the TIFF image!"); + } + + isTiled = true; + } + else { - TiffThrowHelper.ThrowImageFormatException("StripByteCounts are missing and are required for decoding the TIFF image!"); + if (exifProfile.GetValueInternal(ExifTag.StripOffsets) is null) + { + TiffThrowHelper.ThrowImageFormatException("StripOffsets are missing and are required for decoding the TIFF image!"); + } + + if (exifProfile.GetValueInternal(ExifTag.StripByteCounts) is null) + { + TiffThrowHelper.ThrowImageFormatException("StripByteCounts are missing and are required for decoding the TIFF image!"); + } } if (frameMetadata.BitsPerPixel == null) { TiffThrowHelper.ThrowNotSupported("The TIFF BitsPerSample entry is missing which is required to decode the image!"); } + + return isTiled; } private static void ParseColorType(this TiffDecoderCore options, ExifProfile exifProfile) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs index 29de869435..741df50f2d 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs @@ -21,11 +21,6 @@ public abstract partial class ExifTag /// public static ExifTag ColorResponseUnit { get; } = new ExifTag(ExifTagValue.ColorResponseUnit); - /// - /// Gets the TileOffsets exif tag. - /// - public static ExifTag TileOffsets { get; } = new ExifTag(ExifTagValue.TileOffsets); - /// /// Gets the SMinSampleValue exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs index ecd4d87d5a..cd89681413 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs @@ -21,6 +21,11 @@ public abstract partial class ExifTag /// public static ExifTag TileByteCounts { get; } = new ExifTag(ExifTagValue.TileByteCounts); + /// + /// Gets the TileOffsets exif tag. + /// + public static ExifTag TileOffsets { get; } = new ExifTag(ExifTagValue.TileOffsets); + /// /// Gets the ImageLayer exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index 93720fbe94..2302a66819 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -130,8 +130,6 @@ internal static partial class ExifValues return new ExifLongArray(ExifTag.FreeByteCounts); case ExifTagValue.ColorResponseUnit: return new ExifLongArray(ExifTag.ColorResponseUnit); - case ExifTagValue.TileOffsets: - return new ExifLongArray(ExifTag.TileOffsets); case ExifTagValue.SMinSampleValue: return new ExifLongArray(ExifTag.SMinSampleValue); case ExifTagValue.SMaxSampleValue: @@ -176,6 +174,8 @@ internal static partial class ExifValues return new ExifNumberArray(ExifTag.StripOffsets); case ExifTagValue.TileByteCounts: return new ExifNumberArray(ExifTag.TileByteCounts); + case ExifTagValue.TileOffsets: + return new ExifNumberArray(ExifTag.TileOffsets); case ExifTagValue.ImageLayer: return new ExifNumberArray(ExifTag.ImageLayer); From 0327e1d332179fea4e051a14c2ec275c36d69035 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 5 Nov 2022 17:33:20 +0100 Subject: [PATCH 2/7] Avoid some code duplication --- .../Formats/Tiff/TiffDecoderCore.cs | 77 +++++++++++-------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 0324759824..6d55d9981c 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -323,13 +323,35 @@ internal class TiffDecoderCore : IImageDecoderInternals int tilesAcross = (width + tileWidth - 1) / tileWidth; int tilesDown = (height + tileLength - 1) / tileLength; + Array tilesOffsetsArray; + Array tilesByteCountsArray; + IExifValue tilesOffsetsExifValue = tags.GetValueInternal(ExifTag.TileOffsets); + IExifValue tilesByteCountsExifValue = tags.GetValueInternal(ExifTag.TileByteCounts); + if (tilesOffsetsExifValue is null) + { + // Note: This is against the spec, but libTiff seems to handle it this way. + // TIFF 6.0 says: "Do not use both strip- oriented and tile-oriented fields in the same TIFF file". + tilesOffsetsExifValue = tags.GetValueInternal(ExifTag.StripOffsets); + tilesByteCountsExifValue = tags.GetValueInternal(ExifTag.StripByteCounts); + tilesOffsetsArray = (Array)tilesOffsetsExifValue.GetValue(); + tilesByteCountsArray = (Array)tilesByteCountsExifValue.GetValue(); + } + else + { + tilesOffsetsArray = (Array)tilesOffsetsExifValue.GetValue(); + tilesByteCountsArray = (Array)tilesByteCountsExifValue.GetValue(); + } + + using IMemoryOwner tileOffsetsMemory = this.ConvertNumbers(tilesOffsetsArray, out Span tileOffsets); + using IMemoryOwner tileByteCountsMemory = this.ConvertNumbers(tilesByteCountsArray, out Span tileByteCounts); + if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) { - this.DecodeTilesPlanar(tags, frame, tileWidth, tileLength, tilesAcross, tilesDown, cancellationToken); + this.DecodeTilesPlanar(frame, tileWidth, tileLength, tilesAcross, tilesDown, tileOffsets, tileByteCounts, cancellationToken); } else { - this.DecodeTilesChunky(tags, frame, tileWidth, tileLength, tilesAcross, tilesDown, cancellationToken); + this.DecodeTilesChunky(frame, tileWidth, tileLength, tilesAcross, tilesDown, tileOffsets, tileByteCounts, cancellationToken); } } @@ -475,14 +497,23 @@ internal class TiffDecoderCore : IImageDecoderInternals /// Decodes the image data for Tiff's which arrange the pixel data in tiles and the planar configuration. /// /// The pixel format. - /// The IFD tags. /// The image frame to decode into. /// The width in pixels of the tile. /// The height in pixels of the tile. /// The number of tiles horizontally. /// The number of tiles vertically. + /// The tile offsets. + /// The tile byte counts. /// The token to monitor cancellation. - private void DecodeTilesPlanar(ExifProfile tags, ImageFrame frame, int tileWidth, int tileLength, int tilesAcross, int tilesDown, CancellationToken cancellationToken) + private void DecodeTilesPlanar( + ImageFrame frame, + int tileWidth, + int tileLength, + int tilesAcross, + int tilesDown, + Span tileOffsets, + Span tileByteCounts, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Buffer2D pixels = frame.PixelBuffer; @@ -490,11 +521,6 @@ internal class TiffDecoderCore : IImageDecoderInternals int height = pixels.Height; int bitsPerPixel = this.BitsPerPixel; - Array tilesOffsetsArray = (Array)tags.GetValueInternal(ExifTag.TileOffsets).GetValue(); - Array tilesByteCountsArray = (Array)tags.GetValueInternal(ExifTag.TileByteCounts).GetValue(); - using IMemoryOwner tileOffsetsMemory = this.ConvertNumbers(tilesOffsetsArray, out Span tileOffsets); - using IMemoryOwner tileByteCountsMemory = this.ConvertNumbers(tilesByteCountsArray, out Span tileByteCounts); - int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; int bytesPerTileRow = ((tileWidth * bitsPerPixel) + 7) / 8; int uncompressedTilesSize = bytesPerTileRow * tileLength; @@ -559,14 +585,23 @@ internal class TiffDecoderCore : IImageDecoderInternals /// Decodes the image data for Tiff's which arrange the pixel data in tiles and the chunky configuration. /// /// The pixel format. - /// The IFD tags. /// The image frame to decode into. /// The width in pixels of the tile. /// The height in pixels of the tile. /// The number of tiles horizontally. /// The number of tiles vertically. + /// The tile offsets. + /// The tile byte counts. /// The token to monitor cancellation. - private void DecodeTilesChunky(ExifProfile tags, ImageFrame frame, int tileWidth, int tileLength, int tilesAcross, int tilesDown, CancellationToken cancellationToken) + private void DecodeTilesChunky( + ImageFrame frame, + int tileWidth, + int tileLength, + int tilesAcross, + int tilesDown, + Span tileOffsets, + Span tileByteCounts, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Buffer2D pixels = frame.PixelBuffer; @@ -574,26 +609,6 @@ internal class TiffDecoderCore : IImageDecoderInternals int height = pixels.Height; int bitsPerPixel = this.BitsPerPixel; - Array tilesOffsetsArray; - Array tilesByteCountsArray; - IExifValue tilesOffsetsExifValue = tags.GetValueInternal(ExifTag.TileOffsets); - IExifValue tilesByteCountsExifValue = tags.GetValueInternal(ExifTag.TileByteCounts); - if (tilesOffsetsExifValue is null) - { - tilesOffsetsExifValue = tags.GetValueInternal(ExifTag.StripOffsets); - tilesByteCountsExifValue = tags.GetValueInternal(ExifTag.StripByteCounts); - tilesOffsetsArray = (Array)tilesOffsetsExifValue.GetValue(); - tilesByteCountsArray = (Array)tilesByteCountsExifValue.GetValue(); - } - else - { - tilesOffsetsArray = (Array)tilesOffsetsExifValue.GetValue(); - tilesByteCountsArray = (Array)tilesByteCountsExifValue.GetValue(); - } - - using IMemoryOwner tileOffsetsMemory = this.ConvertNumbers(tilesOffsetsArray, out Span tileOffsets); - using IMemoryOwner tileByteCountsMemory = this.ConvertNumbers(tilesByteCountsArray, out Span tileByteCounts); - int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; int bytesPerTileRow = ((tileWidth * bitsPerPixel) + 7) / 8; int uncompressedTilesSize = bytesPerTileRow * tileLength; From c850e2d41b288eba66420c7539466da02df49988 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 7 Nov 2022 17:17:11 +0100 Subject: [PATCH 3/7] Fix decoding tiled tiff's with planar configuration --- .../Formats/Tiff/TiffDecoderCore.cs | 125 +++++++++++------- 1 file changed, 78 insertions(+), 47 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 6d55d9981c..6bdbf442b3 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -520,65 +520,96 @@ internal class TiffDecoderCore : IImageDecoderInternals int width = pixels.Width; int height = pixels.Height; int bitsPerPixel = this.BitsPerPixel; + int channels = this.BitsPerSample.Channels; + int tilesPerChannel = tileOffsets.Length / channels; - int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; - int bytesPerTileRow = ((tileWidth * bitsPerPixel) + 7) / 8; - int uncompressedTilesSize = bytesPerTileRow * tileLength; - using IMemoryOwner tileBuffer = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean); - using IMemoryOwner uncompressedPixelBuffer = this.memoryAllocator.Allocate(tilesDown * tileLength * bytesPerRow, AllocationOptions.Clean); - Span tileBufferSpan = tileBuffer.GetSpan(); - Span uncompressedPixelBufferSpan = uncompressedPixelBuffer.GetSpan(); - - using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + IMemoryOwner[] tilesBuffers = new IMemoryOwner[channels]; - TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( - this.configuration, - this.memoryAllocator, - this.ColorType, - this.BitsPerSample, - this.ExtraSamplesType, - this.ColorMap, - this.ReferenceBlackAndWhite, - this.YcbcrCoefficients, - this.YcbcrSubSampling, - this.byteOrder); - - int tileIndex = 0; - for (int tileY = 0; tileY < tilesDown; tileY++) + try { - int uncompressedPixelBufferOffset = tileY * tileLength * bytesPerRow; - int remainingPixelsInRow = width; - for (int tileX = 0; tileX < tilesAcross; tileX++) + int bytesPerTileRow = ((tileWidth * bitsPerPixel) + 7) / 8; + int uncompressedTilesSize = bytesPerTileRow * tileLength; + for (int i = 0; i < tilesBuffers.Length; i++) { - cancellationToken.ThrowIfCancellationRequested(); + tilesBuffers[i] = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean); + } - bool isLastHorizontalTile = tileX == tilesAcross - 1; + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); - decompressor.Decompress( - this.inputStream, - tileOffsets[tileIndex], - tileByteCounts[tileIndex], - tileLength, - tileBufferSpan, - cancellationToken); + TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar( + this.ColorType, + this.BitsPerSample, + this.ExtraSamplesType, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.YcbcrSubSampling, + this.byteOrder); - int tileBufferOffset = 0; - uncompressedPixelBufferOffset += bytesPerTileRow * tileX; - int bytesToCopy = isLastHorizontalTile ? ((bitsPerPixel * remainingPixelsInRow) + 7) / 8 : bytesPerTileRow; - for (int y = 0; y < tileLength; y++) + int tileIndex = 0; + int remainingPixelsInColumn = height; + for (int tileY = 0; tileY < tilesDown; tileY++) + { + int remainingPixelsInRow = width; + int pixelColumnOffset = tileY * tileLength; + bool isLastVerticalTile = tileY == tilesDown - 1; + for (int tileX = 0; tileX < tilesAcross; tileX++) { - Span uncompressedPixelRow = uncompressedPixelBufferSpan.Slice(uncompressedPixelBufferOffset, bytesToCopy); - tileBufferSpan.Slice(tileBufferOffset, bytesToCopy).CopyTo(uncompressedPixelRow); - tileBufferOffset += bytesPerTileRow; - uncompressedPixelBufferOffset += bytesPerRow; + int pixelRowOffset = tileX * tileWidth; + bool isLastHorizontalTile = tileX == tilesAcross - 1; + int tileIndexForChannel = tileIndex; + for (int i = 0; i < channels; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + decompressor.Decompress( + this.inputStream, + tileOffsets[tileIndexForChannel], + tileByteCounts[tileIndexForChannel], + tileLength, + tilesBuffers[i].GetSpan(), + cancellationToken); + + tileIndexForChannel += tilesPerChannel; + } + + if (isLastHorizontalTile && remainingPixelsInRow < tileWidth) + { + // Adjust pixel data in the tile buffer to fit the smaller then usual tile width. + for (int i = 0; i < channels; i++) + { + Span tileBufferSpan = tilesBuffers[i].GetSpan(); + for (int y = 0; y < tileLength; y++) + { + int currentRowOffset = y * tileWidth; + Span adjustedRow = tileBufferSpan.Slice(y * remainingPixelsInRow, remainingPixelsInRow); + tileBufferSpan.Slice(currentRowOffset, remainingPixelsInRow).CopyTo(adjustedRow); + } + } + } + + colorDecoder.Decode( + tilesBuffers, + pixels, + pixelRowOffset, + pixelColumnOffset, + isLastHorizontalTile ? remainingPixelsInRow : tileWidth, + isLastVerticalTile ? remainingPixelsInColumn : tileLength); + + remainingPixelsInRow -= tileWidth; + tileIndex++; } - remainingPixelsInRow -= tileWidth; - tileIndex++; + remainingPixelsInColumn -= tileLength; + } + } + finally + { + foreach (IMemoryOwner buf in tilesBuffers) + { + buf?.Dispose(); } } - - colorDecoder.Decode(uncompressedPixelBufferSpan, pixels, 0, 0, width, height); } /// From 3a46222c458dee751f4299861fb7faf3164fba5b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 7 Nov 2022 17:29:25 +0100 Subject: [PATCH 4/7] Add tests for tiled images --- .../Formats/Tiff/TiffDecoderTests.cs | 12 ++++++++++-- tests/ImageSharp.Tests/TestImages.cs | 6 ++++++ tests/Images/Input/Tiff/quad-tile.tiff | 3 +++ .../Input/Tiff/rgb_uncompressed_tiled_chunky.tiff | 3 +++ .../Input/Tiff/rgb_uncompressed_tiled_planar.tiff | 3 +++ tests/Images/Input/Tiff/tiled.tiff | 3 +++ 6 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/Images/Input/Tiff/quad-tile.tiff create mode 100644 tests/Images/Input/Tiff/rgb_uncompressed_tiled_chunky.tiff create mode 100644 tests/Images/Input/Tiff/rgb_uncompressed_tiled_planar.tiff create mode 100644 tests/Images/Input/Tiff/tiled.tiff diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 2dd56f079f..166637a704 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -51,7 +51,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester [InlineData(RgbLzwNoPredictorMultistripMotorola, ImageSharp.ByteOrder.BigEndian)] public void ByteOrder(string imagePath, ByteOrder expectedByteOrder) { - var testFile = TestFile.Create(imagePath); + TestFile testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { IImageInfo info = Image.Identify(stream); @@ -61,7 +61,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester stream.Seek(0, SeekOrigin.Begin); - using var img = Image.Load(stream); + using Image img = Image.Load(stream); Assert.Equal(expectedByteOrder, img.Metadata.GetTiffMetadata().ByteOrder); } } @@ -80,6 +80,14 @@ public class TiffDecoderTests : TiffDecoderBaseTester public void TiffDecoder_CanDecode_Planar(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Tiled, PixelTypes.Rgba32)] + [WithFile(QuadTile, PixelTypes.Rgba32)] + [WithFile(TiledChunky, PixelTypes.Rgba32)] + [WithFile(TiledPlanar, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Tiled(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Rgba8BitPlanarUnassociatedAlpha, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_Planar_32Bit(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index d47f1136d5..3867bfdbcc 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -889,6 +889,12 @@ public static class TestImages public const string Flower32BitGrayPredictorBigEndian = "Tiff/flower-minisblack-32_msb_deflate_predictor.tiff"; public const string Flower32BitGrayPredictorLittleEndian = "Tiff/flower-minisblack-32_lsb_deflate_predictor.tiff"; + // Tiled images. + public const string Tiled = "Tiff/tiled.tiff"; + public const string QuadTile = "Tiff/quad-tile.tiff"; + public const string TiledChunky = "Tiff/rgb_uncompressed_tiled_chunky.tiff"; + public const string TiledPlanar = "Tiff/rgb_uncompressed_tiled_planar.tiff"; + // Images with alpha channel. public const string Rgba2BitUnassociatedAlpha = "Tiff/RgbaUnassociatedAlpha2bit.tiff"; public const string Rgba3BitUnassociatedAlpha = "Tiff/RgbaUnassociatedAlpha3bit.tiff"; diff --git a/tests/Images/Input/Tiff/quad-tile.tiff b/tests/Images/Input/Tiff/quad-tile.tiff new file mode 100644 index 0000000000..9f93f25fef --- /dev/null +++ b/tests/Images/Input/Tiff/quad-tile.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab5e5c87cd575472c6fc3e0d5824ebc818b88bf6e5e4aff3afe66f8725351a09 +size 209220 diff --git a/tests/Images/Input/Tiff/rgb_uncompressed_tiled_chunky.tiff b/tests/Images/Input/Tiff/rgb_uncompressed_tiled_chunky.tiff new file mode 100644 index 0000000000..ef4421b25b --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_uncompressed_tiled_chunky.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42bf0a62b8d5de300c0f284c23b5ac1fc7ae9487beeaa3f2ea5a1f6c0c48ced6 +size 339070 diff --git a/tests/Images/Input/Tiff/rgb_uncompressed_tiled_planar.tiff b/tests/Images/Input/Tiff/rgb_uncompressed_tiled_planar.tiff new file mode 100644 index 0000000000..4eb52b3a4f --- /dev/null +++ b/tests/Images/Input/Tiff/rgb_uncompressed_tiled_planar.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ab10d6714142608d0e003d0de1d9f573af996254772f609107815a938141b57 +size 339178 diff --git a/tests/Images/Input/Tiff/tiled.tiff b/tests/Images/Input/Tiff/tiled.tiff new file mode 100644 index 0000000000..63c1e50a16 --- /dev/null +++ b/tests/Images/Input/Tiff/tiled.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f07be69e33985e7bcf6305eb74e3f23b124dc75509d192697df789318913174b +size 31357 From 0345d0271ecef71f736290ded435cc736976a397 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 7 Nov 2022 17:33:53 +0100 Subject: [PATCH 5/7] Avoid code duplication creating the color decoders --- .../Formats/Tiff/TiffDecoderCore.cs | 74 ++++++++----------- .../Formats/Tiff/TiffDecoderTests.cs | 1 - 2 files changed, 30 insertions(+), 45 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 6bdbf442b3..15234d8ac5 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -384,16 +384,7 @@ internal class TiffDecoderCore : IImageDecoderInternals } using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); - - TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar( - this.ColorType, - this.BitsPerSample, - this.ExtraSamplesType, - this.ColorMap, - this.ReferenceBlackAndWhite, - this.YcbcrCoefficients, - this.YcbcrSubSampling, - this.byteOrder); + TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(); for (int i = 0; i < stripsPerPlane; i++) { @@ -453,18 +444,7 @@ internal class TiffDecoderCore : IImageDecoderInternals Buffer2D pixels = frame.PixelBuffer; using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); - - TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( - this.configuration, - this.memoryAllocator, - this.ColorType, - this.BitsPerSample, - this.ExtraSamplesType, - this.ColorMap, - this.ReferenceBlackAndWhite, - this.YcbcrCoefficients, - this.YcbcrSubSampling, - this.byteOrder); + TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { @@ -535,16 +515,7 @@ internal class TiffDecoderCore : IImageDecoderInternals } using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); - - TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar( - this.ColorType, - this.BitsPerSample, - this.ExtraSamplesType, - this.ColorMap, - this.ReferenceBlackAndWhite, - this.YcbcrCoefficients, - this.YcbcrSubSampling, - this.byteOrder); + TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(); int tileIndex = 0; int remainingPixelsInColumn = height; @@ -649,18 +620,7 @@ internal class TiffDecoderCore : IImageDecoderInternals Span uncompressedPixelBufferSpan = uncompressedPixelBuffer.GetSpan(); using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); - - TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( - this.configuration, - this.memoryAllocator, - this.ColorType, - this.BitsPerSample, - this.ExtraSamplesType, - this.ColorMap, - this.ReferenceBlackAndWhite, - this.YcbcrCoefficients, - this.YcbcrSubSampling, - this.byteOrder); + TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); int tileIndex = 0; for (int tileY = 0; tileY < tilesDown; tileY++) @@ -700,6 +660,32 @@ internal class TiffDecoderCore : IImageDecoderInternals colorDecoder.Decode(uncompressedPixelBufferSpan, pixels, 0, 0, width, height); } + private TiffBaseColorDecoder CreateChunkyColorDecoder() + where TPixel : unmanaged, IPixel => + TiffColorDecoderFactory.Create( + this.configuration, + this.memoryAllocator, + this.ColorType, + this.BitsPerSample, + this.ExtraSamplesType, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.YcbcrSubSampling, + this.byteOrder); + + private TiffBasePlanarColorDecoder CreatePlanarColorDecoder() + where TPixel : unmanaged, IPixel => + TiffColorDecoderFactory.CreatePlanar( + this.ColorType, + this.BitsPerSample, + this.ExtraSamplesType, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.YcbcrSubSampling, + this.byteOrder); + private TiffBaseDecompressor CreateDecompressor(int frameWidth, int bitsPerPixel) where TPixel : unmanaged, IPixel => TiffDecompressorsFactory.Create( diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 166637a704..ba5b77baf3 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -18,7 +18,6 @@ public class TiffDecoderTests : TiffDecoderBaseTester public static readonly string[] MultiframeTestImages = Multiframes; [Theory] - [WithFile(RgbUncompressedTiled, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] public void ThrowsNotSupported(TestImageProvider provider) From d914c3c400fdaace5033c62048e6015ec2efa895 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 7 Nov 2022 19:23:58 +0100 Subject: [PATCH 6/7] Fix failing tests --- tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs | 6 +----- .../Metadata/Profiles/Exif/Values/ExifValuesTests.cs | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs index 8bc3ceeb45..62ca6da3de 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs @@ -27,14 +27,10 @@ public class BigTiffDecoderTests : TiffDecoderBaseTester [WithFile(Indexed8_LZW, PixelTypes.Rgba32)] [WithFile(MinIsBlack, PixelTypes.Rgba32)] [WithFile(MinIsWhite, PixelTypes.Rgba32)] + [WithFile(BigTIFFLong8Tiles, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); - [Theory] - [WithFile(BigTIFFLong8Tiles, PixelTypes.Rgba32)] - public void ThrowsNotSupported(TestImageProvider provider) - where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder)); - [Theory] [WithFile(Damaged_MinIsWhite_RLE, PixelTypes.Rgba32)] [WithFile(Damaged_MinIsBlack_RLE, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs index 1c53bc9dd1..1adb7bd556 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs @@ -64,7 +64,6 @@ public class ExifValuesTests { ExifTag.FreeOffsets }, { ExifTag.FreeByteCounts }, { ExifTag.ColorResponseUnit }, - { ExifTag.TileOffsets }, { ExifTag.SMinSampleValue }, { ExifTag.SMaxSampleValue }, { ExifTag.JPEGQTables }, @@ -92,6 +91,7 @@ public class ExifValuesTests { ExifTag.StripOffsets }, { ExifTag.StripByteCounts }, { ExifTag.TileByteCounts }, + { ExifTag.TileOffsets }, { ExifTag.ImageLayer } }; From 8fe8948fa134b2810d8720c955ff26524a9c0136 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 8 Nov 2022 19:12:11 +0100 Subject: [PATCH 7/7] Review suggestions --- src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 15234d8ac5..76b486416b 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Tiff.Compression; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; @@ -507,7 +508,7 @@ internal class TiffDecoderCore : IImageDecoderInternals try { - int bytesPerTileRow = ((tileWidth * bitsPerPixel) + 7) / 8; + int bytesPerTileRow = RoundUpToMultipleOfEight(tileWidth * bitsPerPixel); int uncompressedTilesSize = bytesPerTileRow * tileLength; for (int i = 0; i < tilesBuffers.Length; i++) { @@ -611,8 +612,8 @@ internal class TiffDecoderCore : IImageDecoderInternals int height = pixels.Height; int bitsPerPixel = this.BitsPerPixel; - int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; - int bytesPerTileRow = ((tileWidth * bitsPerPixel) + 7) / 8; + int bytesPerRow = RoundUpToMultipleOfEight(width * bitsPerPixel); + int bytesPerTileRow = RoundUpToMultipleOfEight(tileWidth * bitsPerPixel); int uncompressedTilesSize = bytesPerTileRow * tileLength; using IMemoryOwner tileBuffer = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean); using IMemoryOwner uncompressedPixelBuffer = this.memoryAllocator.Allocate(tilesDown * tileLength * bytesPerRow, AllocationOptions.Clean); @@ -643,7 +644,7 @@ internal class TiffDecoderCore : IImageDecoderInternals int tileBufferOffset = 0; uncompressedPixelBufferOffset += bytesPerTileRow * tileX; - int bytesToCopy = isLastHorizontalTile ? ((bitsPerPixel * remainingPixelsInRow) + 7) / 8 : bytesPerTileRow; + int bytesToCopy = isLastHorizontalTile ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : bytesPerTileRow; for (int y = 0; y < tileLength; y++) { Span uncompressedPixelRow = uncompressedPixelBufferSpan.Slice(uncompressedPixelBufferOffset, bytesToCopy); @@ -799,4 +800,7 @@ internal class TiffDecoderCore : IImageDecoderInternals return (int)height.Value; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8); }