diff --git a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
index dbda4d73c9..c88c23ecb5 100644
--- a/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngFrameMetadata.cs
@@ -30,7 +30,7 @@ private PngFrameMetadata(PngFrameMetadata other)
///
/// Gets or sets the frame delay for animated images.
- /// If not 0, when utilized in Png animation, this field specifies the number of hundredths (1/100) of a second to
+ /// If not 0, when utilized in Png animation, this field specifies the number of seconds to
/// wait before continuing with the processing of the Data Stream.
/// The clock starts ticking immediately after the graphic is rendered.
///
diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs
index 03cd639ad3..86b5c19d21 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs
@@ -28,20 +28,20 @@ protected TiffBaseDecompressor(MemoryAllocator memoryAllocator, int width, int b
/// Decompresses image data into the supplied buffer.
///
/// The to read image data from.
- /// The strip offset of stream.
- /// The number of bytes to read from the input stream.
+ /// The data offset within the stream.
+ /// The number of bytes to read from the input stream.
/// The height of the strip.
/// The output buffer for uncompressed data.
/// The token to monitor cancellation.
- public void Decompress(BufferedReadStream stream, ulong stripOffset, ulong stripByteCount, int stripHeight, Span buffer, CancellationToken cancellationToken)
+ public void Decompress(BufferedReadStream stream, ulong offset, ulong count, int stripHeight, Span buffer, CancellationToken cancellationToken)
{
- DebugGuard.MustBeLessThanOrEqualTo(stripOffset, (ulong)long.MaxValue, nameof(stripOffset));
- DebugGuard.MustBeLessThanOrEqualTo(stripByteCount, (ulong)long.MaxValue, nameof(stripByteCount));
+ DebugGuard.MustBeLessThanOrEqualTo(offset, (ulong)long.MaxValue, nameof(offset));
+ DebugGuard.MustBeLessThanOrEqualTo(count, (ulong)int.MaxValue, nameof(count));
- stream.Seek((long)stripOffset, SeekOrigin.Begin);
- this.Decompress(stream, (int)stripByteCount, stripHeight, buffer, cancellationToken);
+ stream.Seek((long)offset, SeekOrigin.Begin);
+ this.Decompress(stream, (int)count, stripHeight, buffer, cancellationToken);
- if ((long)stripOffset + (long)stripByteCount < stream.Position)
+ if ((long)offset + (long)count < stream.Position)
{
TiffThrowHelper.ThrowImageFormatException("Out of range when reading a strip.");
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index e66f0f3765..134f8e2aa0 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -5,6 +5,7 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Tiff.Compression;
+using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
using SixLabors.ImageSharp.IO;
@@ -403,8 +404,14 @@ private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStr
{
for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++)
{
- int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex);
- stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize);
+ ulong uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex);
+
+ if (uncompressedStripSize > int.MaxValue)
+ {
+ TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images.");
+ }
+
+ stripBuffers[stripIndex] = this.memoryAllocator.Allocate((int)uncompressedStripSize);
}
using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel);
@@ -460,26 +467,97 @@ private void DecodeStripsChunky(ImageFrame frame, int rowsPerStr
rowsPerStrip = frame.Height;
}
- int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip);
+ ulong uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip);
int bitsPerPixel = this.BitsPerPixel;
-
- using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate(uncompressedStripSize, AllocationOptions.Clean);
- Span stripBufferSpan = stripBuffer.GetSpan();
Buffer2D pixels = frame.PixelBuffer;
- using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel);
+ int width = frame.Width;
+ int height = frame.Height;
+
+ using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel);
TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder();
+ // There exists in this world TIFF files with uncompressed strips larger than Int32.MaxValue.
+ // We can read them, but we cannot allocate a buffer that large to hold the uncompressed data.
+ // In this scenario we fall back to reading and decoding one row at a time.
+ //
+ // The NoneTiffCompression decompressor can be used to read individual rows since we have
+ // a guarantee that each row required the same number of bytes.
+ if (decompressor is NoneTiffCompression none && uncompressedStripSize > int.MaxValue)
+ {
+ ulong bytesPerRowU = this.CalculateStripBufferSize(frame.Width, 1);
+
+ // This should never happen, but we check just to be sure.
+ if (bytesPerRowU > int.MaxValue)
+ {
+ TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images.");
+ }
+
+ int bytesPerRow = (int)bytesPerRowU;
+ using IMemoryOwner rowBufferOwner = this.memoryAllocator.Allocate(bytesPerRow, AllocationOptions.Clean);
+ Span rowBuffer = rowBufferOwner.GetSpan();
+ for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0
+ ? rowsPerStrip
+ : height % rowsPerStrip;
+
+ int top = rowsPerStrip * stripIndex;
+ if (top + stripHeight > height)
+ {
+ break;
+ }
+
+ ulong baseOffset = stripOffsets[stripIndex];
+ ulong available = stripByteCounts[stripIndex];
+ ulong required = (ulong)bytesPerRow * (ulong)stripHeight;
+ if (available < required)
+ {
+ break;
+ }
+
+ for (int r = 0; r < stripHeight; r++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ ulong rowOffset = baseOffset + ((ulong)r * (ulong)bytesPerRow);
+
+ // Use the NoneTiffCompression decompressor to read exactly one row.
+ none.Decompress(
+ this.inputStream,
+ rowOffset,
+ (ulong)bytesPerRow,
+ 1,
+ rowBuffer,
+ cancellationToken);
+
+ colorDecoder.Decode(rowBuffer, pixels, 0, top + r, width, 1);
+ }
+ }
+
+ return;
+ }
+
+ if (uncompressedStripSize > int.MaxValue)
+ {
+ TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images.");
+ }
+
+ using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate((int)uncompressedStripSize, AllocationOptions.Clean);
+ Span stripBufferSpan = stripBuffer.GetSpan();
+
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
- int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0
+ int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0
? rowsPerStrip
- : frame.Height % rowsPerStrip;
+ : height % rowsPerStrip;
int top = rowsPerStrip * stripIndex;
- if (top + stripHeight > frame.Height)
+ if (top + stripHeight > height)
{
// Make sure we ignore any strips that are not needed for the image (if too many are present).
break;
@@ -493,7 +571,7 @@ private void DecodeStripsChunky(ImageFrame frame, int rowsPerStr
stripBufferSpan,
cancellationToken);
- colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight);
+ colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight);
}
}
@@ -753,7 +831,7 @@ private IMemoryOwner ConvertNumbers(Array array, out Span span)
/// 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)
+ private ulong CalculateStripBufferSize(int width, int height, int plane = -1)
{
DebugGuard.MustBeLessThanOrEqualTo(plane, 3, nameof(plane));
@@ -786,8 +864,8 @@ private int CalculateStripBufferSize(int width, int height, int plane = -1)
}
}
- int bytesPerRow = ((width * bitsPerPixel) + 7) / 8;
- return bytesPerRow * height;
+ ulong bytesPerRow = (((ulong)width * (ulong)bitsPerPixel) + 7) / 8;
+ return bytesPerRow * (ulong)height;
}
///
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
index db349b8b69..26edbbb51f 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
@@ -637,6 +637,14 @@ private static void ParseCompression(this TiffDecoderCore options, TiffCompressi
{
options.CompressionType = TiffDecoderCompressionType.Jpeg;
+ // Some tiff encoder set this to values different from [1, 1]. The jpeg decoder already handles this,
+ // so we set this always to [1, 1], see: https://github.com/SixLabors/ImageSharp/issues/2679
+ if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.YcbcrSubSampling != null)
+ {
+ options.YcbcrSubSampling[0] = 1;
+ options.YcbcrSubSampling[1] = 1;
+ }
+
if (options.PhotometricInterpretation is TiffPhotometricInterpretation.YCbCr && options.JpegTables is null)
{
// Note: Setting PhotometricInterpretation and color type to RGB here, since the jpeg decoder will handle the conversion of the pixel data.
diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
index b9f58c3d84..359b380b22 100644
--- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
@@ -32,6 +32,11 @@ internal class WebpAnimationDecoder : IDisposable
///
private readonly uint maxFrames;
+ ///
+ /// Whether to skip metadata.
+ ///
+ private readonly bool skipMetadata;
+
///
/// The area to restore.
///
@@ -63,15 +68,85 @@ internal class WebpAnimationDecoder : IDisposable
/// The memory allocator.
/// The global configuration.
/// The maximum number of frames to decode. Inclusive.
+ /// Whether to skip metadata.
/// The flag to decide how to handle the background color in the Animation Chunk.
- public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames, BackgroundColorHandling backgroundColorHandling)
+ public WebpAnimationDecoder(
+ MemoryAllocator memoryAllocator,
+ Configuration configuration,
+ uint maxFrames,
+ bool skipMetadata,
+ BackgroundColorHandling backgroundColorHandling)
{
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
this.maxFrames = maxFrames;
+ this.skipMetadata = skipMetadata;
this.backgroundColorHandling = backgroundColorHandling;
}
+ ///
+ /// Reads the animated webp image information from the specified stream.
+ ///
+ /// The stream, where the image should be decoded from. Cannot be null.
+ /// The bits per pixel.
+ /// The webp features.
+ /// The width of the image.
+ /// The height of the image.
+ /// The size of the image data in bytes.
+ public ImageInfo Identify(
+ BufferedReadStream stream,
+ int bitsPerPixel,
+ WebpFeatures features,
+ uint width,
+ uint height,
+ uint completeDataSize)
+ {
+ List framesMetadata = new();
+ this.metadata = new ImageMetadata();
+ this.webpMetadata = this.metadata.GetWebpMetadata();
+ this.webpMetadata.RepeatCount = features.AnimationLoopCount;
+
+ this.webpMetadata.BackgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
+ ? Color.Transparent
+ : features.AnimationBackgroundColor!.Value;
+
+ Span buffer = stackalloc byte[4];
+ uint frameCount = 0;
+ int remainingBytes = (int)completeDataSize;
+ while (remainingBytes > 0)
+ {
+ WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
+ remainingBytes -= 4;
+ switch (chunkType)
+ {
+ case WebpChunkType.FrameData:
+
+ ImageFrameMetadata frameMetadata = new();
+ uint dataSize = ReadFrameInfo(stream, ref frameMetadata);
+ framesMetadata.Add(frameMetadata);
+
+ remainingBytes -= (int)dataSize;
+ break;
+ case WebpChunkType.Xmp:
+ case WebpChunkType.Exif:
+ WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, this.metadata, this.skipMetadata, buffer);
+ break;
+ default:
+
+ // Specification explicitly states to ignore unknown chunks.
+ // We do not support writing these chunks at present.
+ break;
+ }
+
+ if (stream.Position == stream.Length || ++frameCount == this.maxFrames)
+ {
+ break;
+ }
+ }
+
+ return new ImageInfo(new PixelTypeInfo(bitsPerPixel), new Size((int)width, (int)height), this.metadata, framesMetadata);
+ }
+
///
/// Decodes the animated webp image from the specified stream.
///
@@ -127,10 +202,12 @@ public Image Decode(
break;
case WebpChunkType.Xmp:
case WebpChunkType.Exif:
- WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, false, buffer);
+ WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, this.skipMetadata, buffer);
break;
default:
- WebpThrowHelper.ThrowImageFormatException("Read unexpected webp chunk data");
+
+ // Specification explicitly states to ignore unknown chunks.
+ // We do not support writing these chunks at present.
break;
}
@@ -143,6 +220,26 @@ public Image Decode(
return image!;
}
+ ///
+ /// Reads frame information from the specified stream and updates the provided frame metadata.
+ ///
+ /// The stream from which to read the frame information. Must support reading and seeking.
+ /// A reference to the structure that will be updated with the parsed frame metadata.
+ /// The number of bytes read from the stream while parsing the frame information.
+ private static uint ReadFrameInfo(BufferedReadStream stream, ref ImageFrameMetadata frameMetadata)
+ {
+ WebpFrameData frameData = WebpFrameData.Parse(stream);
+ SetFrameMetadata(frameMetadata, frameData);
+
+ // Size of the frame header chunk.
+ const int chunkHeaderSize = 16;
+
+ uint remaining = frameData.DataSize - chunkHeaderSize;
+ stream.Skip((int)remaining);
+
+ return remaining;
+ }
+
///
/// Reads an individual webp frame.
///
diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
index 839798b4d7..6926928ea9 100644
--- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
+++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
+using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
@@ -343,9 +344,22 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
}
- if (metadata.ExifProfile != null)
+ if (metadata.ExifProfile == null)
{
- metadata.ExifProfile = new ExifProfile(exifData);
+ ExifProfile exifProfile = new(exifData);
+
+ // Set the resolution from the metadata.
+ double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution);
+ double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution);
+
+ if (horizontalValue > 0 && verticalValue > 0)
+ {
+ metadata.HorizontalResolution = horizontalValue;
+ metadata.VerticalResolution = verticalValue;
+ metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile);
+ }
+
+ metadata.ExifProfile = exifProfile;
}
break;
@@ -357,10 +371,7 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
}
- if (metadata.XmpProfile != null)
- {
- metadata.XmpProfile = new XmpProfile(xmpData);
- }
+ metadata.XmpProfile ??= new XmpProfile(xmpData);
break;
default:
@@ -370,6 +381,16 @@ public static void ParseOptionalChunks(BufferedReadStream stream, WebpChunkType
}
}
+ private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag tag)
+ {
+ if (exifProfile.TryGetValue(tag, out IExifValue? resolution))
+ {
+ return resolution.Value.ToDouble();
+ }
+
+ return 0;
+ }
+
///
/// Determines if the chunk type is an optional VP8X chunk.
///
diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
index 21e1b55cfc..362677eeee 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
@@ -89,7 +89,9 @@ protected override Image Decode(BufferedReadStream stream, Cance
this.memoryAllocator,
this.configuration,
this.maxFrames,
+ this.skipMetadata,
this.backgroundColorHandling);
+
return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
}
@@ -101,6 +103,7 @@ protected override Image Decode(BufferedReadStream stream, Cance
this.webImageInfo.Vp8LBitReader,
this.memoryAllocator,
this.configuration);
+
losslessDecoder.Decode(pixels, image.Width, image.Height);
}
else
@@ -109,6 +112,7 @@ protected override Image Decode(BufferedReadStream stream, Cance
this.webImageInfo.Vp8BitReader,
this.memoryAllocator,
this.configuration);
+
lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo, this.alphaData);
}
@@ -131,11 +135,29 @@ protected override Image Decode(BufferedReadStream stream, Cance
///
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
- ReadImageHeader(stream, stackalloc byte[4]);
-
+ uint fileSize = ReadImageHeader(stream, stackalloc byte[4]);
ImageMetadata metadata = new();
+
using (this.webImageInfo = this.ReadVp8Info(stream, metadata, true))
{
+ if (this.webImageInfo.Features is { Animation: true })
+ {
+ using WebpAnimationDecoder animationDecoder = new(
+ this.memoryAllocator,
+ this.configuration,
+ this.maxFrames,
+ this.skipMetadata,
+ this.backgroundColorHandling);
+
+ return animationDecoder.Identify(
+ stream,
+ (int)this.webImageInfo.BitsPerPixel,
+ this.webImageInfo.Features,
+ this.webImageInfo.Width,
+ this.webImageInfo.Height,
+ fileSize);
+ }
+
return new ImageInfo(
new PixelTypeInfo((int)this.webImageInfo.BitsPerPixel),
new Size((int)this.webImageInfo.Width, (int)this.webImageInfo.Height),
@@ -208,6 +230,8 @@ private WebpImageInfo ReadVp8Info(BufferedReadStream stream, ImageMetadata metad
}
else if (WebpChunkParsingUtils.IsOptionalVp8XChunk(chunkType))
{
+ // ANIM chunks appear before EXIF and XMP chunks.
+ // Return after parsing an ANIM chunk - The animated decoder will handle the rest.
bool isAnimationChunk = this.ParseOptionalExtendedChunks(stream, metadata, chunkType, features, ignoreAlpha, buffer);
if (isAnimationChunk)
{
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index d66a8def41..64f22d1201 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -705,6 +705,23 @@ public void TiffDecoder_CanDecode_TiledWithNonEqualWidthAndHeight(TestIm
public void TiffDecoder_CanDecode_BiColorWithMissingBitsPerSample(TestImageProvider provider)
where TPixel : unmanaged, IPixel => TestTiffDecoder(provider);
+ // https://github.com/SixLabors/ImageSharp/issues/2679
+ [Theory]
+ [WithFile(Issues2679, PixelTypes.Rgba32)]
+ public void TiffDecoder_CanDecode_JpegCompressedWithIssue2679(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(TiffDecoder.Instance);
+
+ // The image is handcrafted to simulate issue 2679. ImageMagick will throw an expection here and wont decode,
+ // so we compare to reference output instead.
+ image.DebugSave(provider);
+ image.CompareToReferenceOutput(
+ ImageComparer.Exact,
+ provider,
+ appendPixelTypeToFileName: false);
+ }
+
[Theory]
[WithFile(JpegCompressedGray0000539558, PixelTypes.Rgba32)]
public void TiffDecoder_ThrowsException_WithCircular_IFD_Offsets(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
index c8d780cc20..cb4bc5ca90 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
@@ -314,6 +314,21 @@ public void Decode_AnimatedLossless_VerifyAllFrames(TestImageProvider(TestImageProvider provider)
@@ -331,6 +346,21 @@ public void Decode_AnimatedLossy_VerifyAllFrames(TestImageProvider(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 39b90b28a4..97a0de2c48 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -1112,6 +1112,7 @@ public static class Tiff
public const string Issues2255 = "Tiff/Issues/Issue2255.png";
public const string Issues2435 = "Tiff/Issues/Issue2435.tiff";
public const string Issues2587 = "Tiff/Issues/Issue2587.tiff";
+ public const string Issues2679 = "Tiff/Issues/Issue2679.tiff";
public const string JpegCompressedGray0000539558 = "Tiff/Issues/JpegCompressedGray-0000539558.tiff";
public const string Tiled0000023664 = "Tiff/Issues/tiled-0000023664.tiff";
public const string ExtraSamplesUnspecified = "Tiff/Issues/ExtraSamplesUnspecified.tif";
diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png
new file mode 100644
index 0000000000..ef602fbc9d
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_JpegCompressedWithIssue2679_Issue2679.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0e1ebab8bf31eb8125c6e46152fdc8935fba1630f53131d2f46cfa7e55eaac8e
+size 80288
diff --git a/tests/Images/Input/Tiff/Issues/Issue2679.tiff b/tests/Images/Input/Tiff/Issues/Issue2679.tiff
new file mode 100644
index 0000000000..1bc49f079c
--- /dev/null
+++ b/tests/Images/Input/Tiff/Issues/Issue2679.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:feb938396b9d5b4c258244197ba382937a52c93f72cc91081c7e6810e4a3b94c
+size 6136