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..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; @@ -214,11 +215,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 +247,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 +303,57 @@ 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 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; + + 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(); + } - int bitsPerPixel = 0; + using IMemoryOwner tileOffsetsMemory = this.ConvertNumbers(tilesOffsetsArray, out Span tileOffsets); + using IMemoryOwner tileByteCountsMemory = this.ConvertNumbers(tilesByteCountsArray, out Span tileByteCounts); - if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky) + if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) { - DebugGuard.IsTrue(plane == -1, "Expected Chunky planar."); - bitsPerPixel = this.BitsPerPixel; + this.DecodeTilesPlanar(frame, tileWidth, tileLength, tilesAcross, tilesDown, tileOffsets, tileByteCounts, 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(frame, tileWidth, tileLength, tilesAcross, tilesDown, tileOffsets, tileByteCounts, cancellationToken); } - - int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; - return bytesPerRow * height; } /// @@ -373,30 +384,8 @@ 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); - - TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar( - this.ColorType, - this.BitsPerSample, - this.ExtraSamplesType, - this.ColorMap, - this.ReferenceBlackAndWhite, - this.YcbcrCoefficients, - this.YcbcrSubSampling, - this.byteOrder); + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(); for (int i = 0; i < stripsPerPlane; i++) { @@ -455,32 +444,8 @@ 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); - - TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( - this.configuration, - this.memoryAllocator, - this.ColorType, - this.BitsPerSample, - this.ExtraSamplesType, - this.ColorMap, - this.ReferenceBlackAndWhite, - this.YcbcrCoefficients, - this.YcbcrSubSampling, - this.byteOrder); + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { @@ -509,6 +474,299 @@ 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 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( + ImageFrame frame, + int tileWidth, + int tileLength, + int tilesAcross, + int tilesDown, + Span tileOffsets, + Span tileByteCounts, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Buffer2D pixels = frame.PixelBuffer; + int width = pixels.Width; + int height = pixels.Height; + int bitsPerPixel = this.BitsPerPixel; + int channels = this.BitsPerSample.Channels; + int tilesPerChannel = tileOffsets.Length / channels; + + IMemoryOwner[] tilesBuffers = new IMemoryOwner[channels]; + + try + { + int bytesPerTileRow = RoundUpToMultipleOfEight(tileWidth * bitsPerPixel); + int uncompressedTilesSize = bytesPerTileRow * tileLength; + for (int i = 0; i < tilesBuffers.Length; i++) + { + tilesBuffers[i] = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean); + } + + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(); + + 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++) + { + 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++; + } + + remainingPixelsInColumn -= tileLength; + } + } + finally + { + foreach (IMemoryOwner buf in tilesBuffers) + { + buf?.Dispose(); + } + } + } + + /// + /// Decodes the image data for Tiff's which arrange the pixel data in tiles and the chunky configuration. + /// + /// The pixel format. + /// 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( + ImageFrame frame, + int tileWidth, + int tileLength, + int tilesAcross, + int tilesDown, + Span tileOffsets, + Span tileByteCounts, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Buffer2D pixels = frame.PixelBuffer; + int width = pixels.Width; + int height = pixels.Height; + int bitsPerPixel = this.BitsPerPixel; + + 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); + Span tileBufferSpan = tileBuffer.GetSpan(); + Span uncompressedPixelBufferSpan = uncompressedPixelBuffer.GetSpan(); + + using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); + + 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 ? RoundUpToMultipleOfEight(bitsPerPixel * remainingPixelsInRow) : 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 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( + 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. /// @@ -542,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); } 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); 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/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 2dd56f079f..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) @@ -51,7 +50,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 +60,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 +79,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/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 } }; 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