diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs
index 196d77ad77..c31a2c1c94 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoder.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs
@@ -5,7 +5,6 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.IO;
-using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs
new file mode 100644
index 0000000000..5b793c35de
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
+using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
+{
+ ///
+ /// Spectral converter for gray TIFF's which use the JPEG compression.
+ ///
+ /// The type of the pixel.
+ internal sealed class GrayJpegSpectralConverter : SpectralConverter
+ where TPixel : unmanaged, IPixel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration.
+ public GrayJpegSpectralConverter(Configuration configuration)
+ : base(configuration)
+ {
+ }
+
+ ///
+ protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(JpegColorSpace.Grayscale, frame.Precision);
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
index ce7820ccf9..cfbc32f4f6 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
@@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
-using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
@@ -55,17 +54,41 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
using var jpegDecoder = new JpegDecoderCore(this.configuration, new JpegDecoder());
- // If the PhotometricInterpretation is YCbCr we explicitly assume the JPEG data is in RGB color space.
- // There seems no other way to determine that the JPEG data is RGB colorspace (no APP14 marker, componentId's are not RGB).
- using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ?
- new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration);
- var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None);
- jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
- scanDecoder.ResetInterval = 0;
- jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None);
+ switch (this.photometricInterpretation)
+ {
+ case TiffPhotometricInterpretation.BlackIsZero:
+ case TiffPhotometricInterpretation.WhiteIsZero:
+ {
+ using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(this.configuration);
+ var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, CancellationToken.None);
+ jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray);
+ jpegDecoder.ParseStream(stream, scanDecoderGray, CancellationToken.None);
- // TODO: Should we pass through the CancellationToken from the tiff decoder?
- CopyImageBytesToBuffer(buffer, spectralConverter.GetPixelBuffer(CancellationToken.None));
+ // TODO: Should we pass through the CancellationToken from the tiff decoder?
+ CopyImageBytesToBuffer(buffer, spectralConverterGray.GetPixelBuffer(CancellationToken.None));
+ break;
+ }
+
+ // If the PhotometricInterpretation is YCbCr we explicitly assume the JPEG data is in RGB color space.
+ // There seems no other way to determine that the JPEG data is RGB colorspace (no APP14 marker, componentId's are not RGB).
+ case TiffPhotometricInterpretation.YCbCr:
+ case TiffPhotometricInterpretation.Rgb:
+ {
+ using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ?
+ new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration);
+ var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None);
+ jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
+ jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None);
+
+ // TODO: Should we pass through the CancellationToken from the tiff decoder?
+ CopyImageBytesToBuffer(buffer, spectralConverter.GetPixelBuffer(CancellationToken.None));
+ break;
+ }
+
+ default:
+ TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported");
+ break;
+ }
}
else
{
@@ -86,6 +109,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
}
}
+ private static void CopyImageBytesToBuffer(Span buffer, Buffer2D pixelBuffer)
+ {
+ int offset = 0;
+ for (int y = 0; y < pixelBuffer.Height; y++)
+ {
+ Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y);
+ Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan);
+ rgbBytes.CopyTo(buffer.Slice(offset));
+ offset += rgbBytes.Length;
+ }
+ }
+
///
protected override void Dispose(bool disposing)
{
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
index 001480542f..a83518064d 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
-using System.Threading;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters;
using SixLabors.ImageSharp.PixelFormats;
diff --git a/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs
index 5372384397..d6d1bb8a4c 100644
--- a/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs
+++ b/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using SixLabors.ImageSharp.Metadata;
+
namespace SixLabors.ImageSharp.Formats.Tiff
{
///
@@ -12,5 +14,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
///
bool IgnoreMetadata { get; }
+
+ ///
+ /// Gets the decoding mode for multi-frame images.
+ ///
+ FrameDecodingMode DecodingMode { get; }
}
}
diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md
index 488701e318..51e84ef558 100644
--- a/src/ImageSharp/Formats/Tiff/README.md
+++ b/src/ImageSharp/Formats/Tiff/README.md
@@ -25,7 +25,7 @@
## Implementation Status
-- The Decoder currently only supports a single frame per image.
+- The Decoder currently only supports decoding multiframe images, which have the same dimensions.
- Some compression formats are not yet supported. See the list below.
### Deviations from the TIFF spec (to be fixed)
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs
index 9d52e34dfe..b4d7520199 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs
@@ -4,6 +4,7 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
@@ -18,6 +19,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
public bool IgnoreMetadata { get; set; }
+ ///
+ /// Gets or sets the decoding mode for multi-frame images.
+ ///
+ public FrameDecodingMode DecodingMode { get; set; }
+
///
public Image Decode(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 05c5358f59..cd06282f18 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -13,7 +13,6 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
-using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
@@ -33,6 +32,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
private readonly bool ignoreMetadata;
+ ///
+ /// Gets the decoding mode for multi-frame images
+ ///
+ private FrameDecodingMode decodingMode;
+
///
/// The stream to decode from.
///
@@ -59,6 +63,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.Configuration = configuration ?? Configuration.Default;
this.ignoreMetadata = options.IgnoreMetadata;
+ this.decodingMode = options.DecodingMode;
this.memoryAllocator = this.Configuration.MemoryAllocator;
}
@@ -160,11 +165,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff
cancellationToken.ThrowIfCancellationRequested();
ImageFrame frame = this.DecodeFrame(ifd, cancellationToken);
frames.Add(frame);
+
+ if (this.decodingMode is FrameDecodingMode.First)
+ {
+ break;
+ }
}
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder, reader.IsBigTiff);
- // TODO: Tiff frames can have different sizes
+ // TODO: Tiff frames can have different sizes.
ImageFrame root = frames[0];
this.Dimensions = root.Size();
foreach (ImageFrame frame in frames)
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index ea0544acf4..e4f4d26b85 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -4,6 +4,7 @@
// ReSharper disable InconsistentNaming
using System;
using System.IO;
+using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
@@ -365,11 +366,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffDecoder_CanDecode_PackBitsCompressed(TestImageProvider provider)
where TPixel : unmanaged, IPixel => TestTiffDecoder(provider);
+ [Theory]
+ [WithFile(MultiFrameMipMap, PixelTypes.Rgba32)]
+ public void CanDecodeJustOneFrame(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image image = provider.GetImage(new TiffDecoder() { DecodingMode = FrameDecodingMode.First }))
+ {
+ Assert.Equal(1, image.Frames.Count);
+ }
+ }
+
[Theory]
[WithFile(RgbJpegCompressed, PixelTypes.Rgba32)]
[WithFile(RgbWithStripsJpegCompressed, PixelTypes.Rgba32)]
[WithFile(YCbCrJpegCompressed, PixelTypes.Rgba32)]
[WithFile(RgbJpegCompressedNoJpegTable, PixelTypes.Rgba32)]
+ [WithFile(GrayscaleJpegCompressed, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_JpegCompressed(TestImageProvider provider)
where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false);
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 9ea3c09f16..aa4314b8e0 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -766,6 +766,7 @@ namespace SixLabors.ImageSharp.Tests
public const string GrayscaleDeflateMultistrip = "Tiff/grayscale_deflate_multistrip.tiff";
public const string GrayscaleUncompressed = "Tiff/grayscale_uncompressed.tiff";
+ public const string GrayscaleJpegCompressed = "Tiff/JpegCompressedGray.tiff";
public const string PaletteDeflateMultistrip = "Tiff/palette_grayscale_deflate_multistrip.tiff";
public const string PaletteUncompressed = "Tiff/palette_uncompressed.tiff";
public const string RgbDeflate = "Tiff/rgb_deflate.tiff";
@@ -867,6 +868,7 @@ namespace SixLabors.ImageSharp.Tests
public const string MultiframeDeflateWithPreview = "Tiff/multipage_deflate_withPreview.tiff";
public const string MultiframeDifferentSize = "Tiff/multipage_differentSize.tiff";
public const string MultiframeDifferentVariants = "Tiff/multipage_differentVariants.tiff";
+ public const string MultiFrameMipMap = "Tiff/SKC1H3.tiff";
public const string LittleEndianByteOrder = "Tiff/little_endian.tiff";
diff --git a/tests/Images/Input/Tiff/JpegCompressedGray.tiff b/tests/Images/Input/Tiff/JpegCompressedGray.tiff
new file mode 100644
index 0000000000..e7feed15a8
--- /dev/null
+++ b/tests/Images/Input/Tiff/JpegCompressedGray.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:868afd018d025ed7636f1155c1b1f64ba8a36153b56c7598e8dee18ce770cd5a
+size 539660
diff --git a/tests/Images/Input/Tiff/SKC1H3.tiff b/tests/Images/Input/Tiff/SKC1H3.tiff
new file mode 100644
index 0000000000..9f9a50fdd6
--- /dev/null
+++ b/tests/Images/Input/Tiff/SKC1H3.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:938bbf1c0f8bdbea0c632bb8d51c1150f757f88b3779d7fa18c296a3a3f61e9b
+size 13720193