Browse Source

Merge branch 'main' into js/decoder-attempt-2

pull/2301/head
James Jackson-South 4 years ago
committed by GitHub
parent
commit
391442b38b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      src/ImageSharp/Formats/Tiff/README.md
  2. 475
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  3. 63
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  4. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs
  5. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs
  6. 4
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs
  7. 6
      tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs
  8. 13
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  9. 2
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs
  10. 6
      tests/ImageSharp.Tests/TestImages.cs
  11. 3
      tests/Images/Input/Tiff/quad-tile.tiff
  12. 3
      tests/Images/Input/Tiff/rgb_uncompressed_tiled_chunky.tiff
  13. 3
      tests/Images/Input/Tiff/rgb_uncompressed_tiled_planar.tiff
  14. 3
      tests/Images/Input/Tiff/tiled.tiff

22
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

475
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<ExifProfile> 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<TPixel> 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;
}
/// <summary>
/// Decodes the image data for Tiff's which arrange the pixel data in stripes.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param>
/// <param name="frame">The image frame to decode into.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeImageWithStrips<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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<ulong> ConvertNumbers(Array array, out Span<ulong> span)
{
if (array is Number[] numbers)
{
IMemoryOwner<ulong> memory = this.memoryAllocator.Allocate<ulong>(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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="width">The width for the desired pixel buffer.</param>
/// <param name="height">The height for the desired pixel buffer.</param>
/// <param name="plane">The index of the plane for planar image configuration (or zero for chunky).</param>
/// <returns>The size (in bytes) of the required pixel buffer.</returns>
private int CalculateStripBufferSize(int width, int height, int plane = -1)
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param>
/// <param name="frame">The image frame to decode into.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeImageWithTiles<TPixel>(ExifProfile tags, ImageFrame<TPixel> frame, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
DebugGuard.MustBeLessThanOrEqualTo(plane, 3, nameof(plane));
Buffer2D<TPixel> 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<ulong> tileOffsetsMemory = this.ConvertNumbers(tilesOffsetsArray, out Span<ulong> tileOffsets);
using IMemoryOwner<ulong> tileByteCountsMemory = this.ConvertNumbers(tilesByteCountsArray, out Span<ulong> 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;
}
/// <summary>
@ -373,30 +384,8 @@ internal class TiffDecoderCore : IImageDecoderInternals
stripBuffers[stripIndex] = this.memoryAllocator.Allocate<byte>(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<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.CreatePlanar(
this.ColorType,
this.BitsPerSample,
this.ExtraSamplesType,
this.ColorMap,
this.ReferenceBlackAndWhite,
this.YcbcrCoefficients,
this.YcbcrSubSampling,
this.byteOrder);
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel);
TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>();
for (int i = 0; i < stripsPerPlane; i++)
{
@ -455,32 +444,8 @@ internal class TiffDecoderCore : IImageDecoderInternals
Span<byte> stripBufferSpan = stripBuffer.GetSpan();
Buffer2D<TPixel> 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<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.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<TPixel>(frame.Width, bitsPerPixel);
TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>();
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{
@ -509,6 +474,299 @@ internal class TiffDecoderCore : IImageDecoderInternals
}
}
/// <summary>
/// Decodes the image data for Tiff's which arrange the pixel data in tiles and the planar configuration.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode into.</param>
/// <param name="tileWidth">The width in pixels of the tile.</param>
/// <param name="tileLength">The height in pixels of the tile.</param>
/// <param name="tilesAcross">The number of tiles horizontally.</param>
/// <param name="tilesDown">The number of tiles vertically.</param>
/// <param name="tileOffsets">The tile offsets.</param>
/// <param name="tileByteCounts">The tile byte counts.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeTilesPlanar<TPixel>(
ImageFrame<TPixel> frame,
int tileWidth,
int tileLength,
int tilesAcross,
int tilesDown,
Span<ulong> tileOffsets,
Span<ulong> tileByteCounts,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> 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<byte>[] tilesBuffers = new IMemoryOwner<byte>[channels];
try
{
int bytesPerTileRow = RoundUpToMultipleOfEight(tileWidth * bitsPerPixel);
int uncompressedTilesSize = bytesPerTileRow * tileLength;
for (int i = 0; i < tilesBuffers.Length; i++)
{
tilesBuffers[i] = this.memoryAllocator.Allocate<byte>(uncompressedTilesSize, AllocationOptions.Clean);
}
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel);
TiffBasePlanarColorDecoder<TPixel> colorDecoder = this.CreatePlanarColorDecoder<TPixel>();
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<byte> tileBufferSpan = tilesBuffers[i].GetSpan();
for (int y = 0; y < tileLength; y++)
{
int currentRowOffset = y * tileWidth;
Span<byte> 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<byte> buf in tilesBuffers)
{
buf?.Dispose();
}
}
}
/// <summary>
/// Decodes the image data for Tiff's which arrange the pixel data in tiles and the chunky configuration.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode into.</param>
/// <param name="tileWidth">The width in pixels of the tile.</param>
/// <param name="tileLength">The height in pixels of the tile.</param>
/// <param name="tilesAcross">The number of tiles horizontally.</param>
/// <param name="tilesDown">The number of tiles vertically.</param>
/// <param name="tileOffsets">The tile offsets.</param>
/// <param name="tileByteCounts">The tile byte counts.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeTilesChunky<TPixel>(
ImageFrame<TPixel> frame,
int tileWidth,
int tileLength,
int tilesAcross,
int tilesDown,
Span<ulong> tileOffsets,
Span<ulong> tileByteCounts,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> 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<byte> tileBuffer = this.memoryAllocator.Allocate<byte>(uncompressedTilesSize, AllocationOptions.Clean);
using IMemoryOwner<byte> uncompressedPixelBuffer = this.memoryAllocator.Allocate<byte>(tilesDown * tileLength * bytesPerRow, AllocationOptions.Clean);
Span<byte> tileBufferSpan = tileBuffer.GetSpan();
Span<byte> uncompressedPixelBufferSpan = uncompressedPixelBuffer.GetSpan();
using TiffBaseDecompressor decompressor = this.CreateDecompressor<TPixel>(frame.Width, bitsPerPixel);
TiffBaseColorDecoder<TPixel> colorDecoder = this.CreateChunkyColorDecoder<TPixel>();
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<byte> 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<TPixel> CreateChunkyColorDecoder<TPixel>()
where TPixel : unmanaged, IPixel<TPixel> =>
TiffColorDecoderFactory<TPixel>.Create(
this.configuration,
this.memoryAllocator,
this.ColorType,
this.BitsPerSample,
this.ExtraSamplesType,
this.ColorMap,
this.ReferenceBlackAndWhite,
this.YcbcrCoefficients,
this.YcbcrSubSampling,
this.byteOrder);
private TiffBasePlanarColorDecoder<TPixel> CreatePlanarColorDecoder<TPixel>()
where TPixel : unmanaged, IPixel<TPixel> =>
TiffColorDecoderFactory<TPixel>.CreatePlanar(
this.ColorType,
this.BitsPerSample,
this.ExtraSamplesType,
this.ColorMap,
this.ReferenceBlackAndWhite,
this.YcbcrCoefficients,
this.YcbcrSubSampling,
this.byteOrder);
private TiffBaseDecompressor CreateDecompressor<TPixel>(int frameWidth, int bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel> =>
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<ulong> ConvertNumbers(Array array, out Span<ulong> span)
{
if (array is Number[] numbers)
{
IMemoryOwner<ulong> memory = this.memoryAllocator.Allocate<ulong>(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;
}
/// <summary>
/// Calculates the size (in bytes) for a pixel buffer using the determined color format.
/// </summary>
/// <param name="width">The width for the desired pixel buffer.</param>
/// <param name="height">The height for the desired pixel buffer.</param>
/// <param name="plane">The index of the plane for planar image configuration (or zero for chunky).</param>
/// <returns>The size (in bytes) of the required pixel buffer.</returns>
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;
}
/// <summary>
/// Gets the width of the image frame.
/// </summary>
@ -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);
}

63
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -21,13 +21,9 @@ internal static class TiffDecoderOptionsParser
/// <param name="options">The options.</param>
/// <param name="exifProfile">The exif profile of the frame to decode.</param>
/// <param name="frameMetadata">The IFD entries container to read the image format information for current frame.</param>
public static void VerifyAndParse(this TiffDecoderCore options, ExifProfile exifProfile, TiffFrameMetadata frameMetadata)
/// <returns>True, if the image uses tiles. Otherwise the images has strip's.</returns>
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)
/// <summary>
/// Verifies that all required fields for decoding are present.
/// </summary>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="frameMetadata">The frame metadata.</param>
/// <param name="planarConfiguration">The planar configuration. Either planar or chunky.</param>
/// <returns>True, if the image uses tiles. Otherwise the images has strip's.</returns>
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)

5
src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs

@ -21,11 +21,6 @@ public abstract partial class ExifTag
/// </summary>
public static ExifTag<uint[]> ColorResponseUnit { get; } = new ExifTag<uint[]>(ExifTagValue.ColorResponseUnit);
/// <summary>
/// Gets the TileOffsets exif tag.
/// </summary>
public static ExifTag<uint[]> TileOffsets { get; } = new ExifTag<uint[]>(ExifTagValue.TileOffsets);
/// <summary>
/// Gets the SMinSampleValue exif tag.
/// </summary>

5
src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.NumberArray.cs

@ -21,6 +21,11 @@ public abstract partial class ExifTag
/// </summary>
public static ExifTag<Number[]> TileByteCounts { get; } = new ExifTag<Number[]>(ExifTagValue.TileByteCounts);
/// <summary>
/// Gets the TileOffsets exif tag.
/// </summary>
public static ExifTag<Number[]> TileOffsets { get; } = new ExifTag<Number[]>(ExifTagValue.TileOffsets);
/// <summary>
/// Gets the ImageLayer exif tag.
/// </summary>

4
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);

6
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(BigTIFFLong8Tiles, PixelTypes.Rgba32)]
public void ThrowsNotSupported<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => Assert.Throws<NotSupportedException>(() => provider.GetImage(TiffDecoder));
[Theory]
[WithFile(Damaged_MinIsWhite_RLE, PixelTypes.Rgba32)]
[WithFile(Damaged_MinIsBlack_RLE, PixelTypes.Rgba32)]

13
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<TPixel>(TestImageProvider<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(Tiled, PixelTypes.Rgba32)]
[WithFile(QuadTile, PixelTypes.Rgba32)]
[WithFile(TiledChunky, PixelTypes.Rgba32)]
[WithFile(TiledPlanar, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_Tiled<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(Rgba8BitPlanarUnassociatedAlpha, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_Planar_32Bit<TPixel>(TestImageProvider<TPixel> provider)

2
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 }
};

6
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";

3
tests/Images/Input/Tiff/quad-tile.tiff

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab5e5c87cd575472c6fc3e0d5824ebc818b88bf6e5e4aff3afe66f8725351a09
size 209220

3
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

3
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

3
tests/Images/Input/Tiff/tiled.tiff

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f07be69e33985e7bcf6305eb74e3f23b124dc75509d192697df789318913174b
size 31357
Loading…
Cancel
Save