diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs index 18c0f2e0d3..f13cb91b17 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs @@ -74,8 +74,17 @@ private static ByteOrder ReadByteOrder(Stream stream) private List ReadIfds(bool isBigTiff) { List readers = []; + + // TIFF allows directories at any offset, so a chain that goes backwards in the file is valid. + // Only a directory offset that has already been read means the chain loops. + HashSet visitedOffsets = []; while (this.nextIfdOffset != 0 && this.nextIfdOffset < (ulong)this.stream.Length) { + if (!visitedOffsets.Add(this.nextIfdOffset)) + { + TiffThrowHelper.ThrowImageFormatException("TIFF image contains circular directory offsets"); + } + EntryReader reader = new(this.stream, this.ByteOrder, this.allocator); reader.ReadTags(isBigTiff, this.nextIfdOffset); @@ -90,11 +99,6 @@ private List ReadIfds(bool isBigTiff) } } - if (this.nextIfdOffset >= reader.NextIfdOffset && reader.NextIfdOffset != 0) - { - TiffThrowHelper.ThrowImageFormatException("TIFF image contains circular directory offsets"); - } - this.nextIfdOffset = reader.NextIfdOffset; readers.Add(reader); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 413e970836..ce8789628e 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -184,7 +184,6 @@ protected override Image Decode(BufferedReadStream stream, Cance } } - this.Dimensions = frames[0].Size; ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff); return new Image(this.configuration, metadata, frames); } @@ -253,18 +252,30 @@ private ImageFrame DecodeFrame(ExifProfile tags, Size? size, Can } else { + // Record the image size before the root frame is allocated, so that a failure to allocate + // any buffer of the image reports its real dimensions. size = new Size(width, height); + this.Dimensions = size.Value; } ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, metadata.FrameMetadata); - if (isTiled) + try { - this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken); + if (isTiled) + { + this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken); + } + else + { + this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken); + } } - else + catch { - this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken); + // The frame is not owned by an image yet, so it must be released here. + frame.Dispose(); + throw; } // Only RGB-compatible color types can be converted here because the TPixel-based ICC profile conversion @@ -323,14 +334,13 @@ private ImageFrame DecodeFrame(ExifProfile tags, Size? size, Can private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - int rowsPerStrip; + // A missing RowsPerStrip defaults to 2^32 - 1, and any value of at least the image height means that + // the entire image is one strip. Limiting the value to the height keeps the strip buffer sizes to the + // rows that actually exist, for both the chunky and the planar layout. + int rowsPerStrip = height; if (tags.TryGetValue(ExifTag.RowsPerStrip, out IExifValue value)) { - rowsPerStrip = (int)value.Value; - } - else - { - rowsPerStrip = TiffConstants.RowsPerStripInfinity; + rowsPerStrip = Math.Min((int)value.Value, height); } Array stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue(); @@ -451,13 +461,32 @@ private void DecodeStripsPlanar( Buffer2D pixels = frame.PixelBuffer; + using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata); + + // As for chunky data, an uncompressed strip of a plane that is larger than the allocator allows for a single + // buffer is read and decoded one row at a time. Every row of a plane holds the same number of bytes, so the + // position of each row in the strip is known without decoding the rows before it. + bool readRows = false; + if (decompressor is NoneTiffCompression && this.CanReadStripRows()) + { + for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) + { + if (this.CalculateStripBufferSize(width, rowsPerStrip, planeIndex) > (ulong)this.memoryAllocator.SingleBufferAllocationLimitBytes) + { + readRows = true; + break; + } + } + } + + int rowsPerRead = readRows ? 1 : rowsPerStrip; IMemoryOwner[] stripBuffers = new IMemoryOwner[stripsPerPixel]; try { for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++) { - ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex); + ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerRead, stripIndex); if (uncompressedStripSize > int.MaxValue) { @@ -467,7 +496,6 @@ private void DecodeStripsPlanar( stripBuffers[stripIndex] = this.memoryAllocator.Allocate((int)uncompressedStripSize); } - using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata); TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(frame.Metadata); for (int i = 0; i < stripsPerPlane; i++) @@ -476,21 +504,39 @@ private void DecodeStripsPlanar( int stripHeight = i < stripsPerPlane - 1 || height % rowsPerStrip == 0 ? rowsPerStrip : height % rowsPerStrip; - int stripIndex = i; - for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) + for (int row = 0; row < stripHeight; row += rowsPerRead) { - decompressor.Decompress( - this.inputStream, - stripOffsets[stripIndex], - stripByteCounts[stripIndex], - stripHeight, - stripBuffers[planeIndex].GetSpan(), - cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); - stripIndex += stripsPerPlane; - } + int readHeight = Math.Min(rowsPerRead, stripHeight - row); + int stripIndex = i; + for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++) + { + ulong offset = stripOffsets[stripIndex]; + ulong byteCount = stripByteCounts[stripIndex]; + if (readRows) + { + // Read the one row at its position in the strip of this plane. A strip that is shorter + // than its rows is read only up to its end, as when the strip is read whole. + ulong bytesPerRow = this.CalculateStripBufferSize(width, 1, planeIndex); + ulong rowStart = (ulong)row * bytesPerRow; + offset += rowStart; + byteCount = byteCount > rowStart ? Math.Min(byteCount - rowStart, bytesPerRow) : 0; + } + + decompressor.Decompress( + this.inputStream, + offset, + byteCount, + readHeight, + stripBuffers[planeIndex].GetSpan(), + cancellationToken); + + stripIndex += stripsPerPlane; + } - colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, width, stripHeight); + colorDecoder.Decode(stripBuffers, pixels, 0, (rowsPerStrip * i) + row, width, readHeight); + } } } finally @@ -523,12 +569,6 @@ private void DecodeStripsChunky( CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. - if (rowsPerStrip == TiffConstants.RowsPerStripInfinity) - { - rowsPerStrip = height; - } - ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip); int bitsPerPixel = this.BitsPerPixel; @@ -536,13 +576,16 @@ private void DecodeStripsChunky( TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(frame.Metadata); Buffer2D pixels = frame.PixelBuffer; - // 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. + // There exists in this world TIFF files with uncompressed strips larger than the allocator allows + // for a single buffer, up to and beyond Int32.MaxValue bytes. We can read them, but we cannot allocate + // a buffer that large to hold the uncompressed data, even when the decoded image fits in memory. // 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) + if (decompressor is NoneTiffCompression none + && uncompressedStripSize > (ulong)this.memoryAllocator.SingleBufferAllocationLimitBytes + && this.CanReadStripRows()) { ulong bytesPerRowU = this.CalculateStripBufferSize(width, 1); @@ -966,6 +1009,20 @@ private ulong CalculateStripBufferSize(int width, int height, int plane = -1) return bytesPerRow * (ulong)height; } + /// + /// Gets a value indicating whether every row of uncompressed strip data can be decoded on its own, + /// which allows a strip to be read one row at a time. + /// + /// + /// YCbCr data with chroma subsampling stores the samples of a block of rows together, so its rows cannot be + /// located or decoded separately and the strip must be decoded whole. + /// + /// if the rows of a strip can be decoded one at a time. + private bool CanReadStripRows() + => this.PhotometricInterpretation != TiffPhotometricInterpretation.YCbCr + || this.YcbcrSubSampling is null + || (this.YcbcrSubSampling[0] == 1 && this.YcbcrSubSampling[1] == 1); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8); } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderStripLayoutTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderStripLayoutTests.cs new file mode 100644 index 0000000000..8974b82e02 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderStripLayoutTests.cs @@ -0,0 +1,454 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff; + +/// +/// Decodes hand-built TIFF files with unusual but valid strip and directory layouts, very large strips, and malformed data. +/// +[Collection("RunSerial")] +[Trait("Format", "Tiff")] +[ValidateDisposedMemoryAllocations] +public class TiffDecoderStripLayoutTests +{ + /// + /// A missing RowsPerStrip, or one larger than the image height, means the whole image is one strip, + /// for both chunky and planar data. + /// + /// Whether the samples are stored planar. + /// The image width. + /// The RowsPerStrip value, or to omit the entry. + [Theory] + [InlineData(false, 4000, null)] + [InlineData(false, 4000, 100000u)] + [InlineData(false, 4000, uint.MaxValue)] + [InlineData(true, 4, null)] + [InlineData(true, 4, 2u)] + [InlineData(true, 4000, null)] + [InlineData(true, 4000, 100000u)] + [InlineData(true, 4000, uint.MaxValue)] + public void Decode_RowsPerStripMissingOrAboveHeight_DecodesAllRows(bool planar, int width, uint? rowsPerStrip) + { + const int height = 2; + TiffPage page = TiffPage.Create(width, height, [8, 8, 8], TiffPhotometricInterpretation.Rgb, planar, rowsPerStrip, CreateRgbStrips(width, height, 1, planar, height)); + + using Image image = Decode(BuildTiff([page])); + + AssertRgb24Pixels(image); + } + + /// + /// An uncompressed strip larger than the single buffer allocation limit is read in rows, so the image + /// decodes whenever its pixel buffer fits in memory, with the same pixels as without the limit. + /// + /// Whether the samples are stored planar. + /// The number of rows per strip. + [Theory] + [InlineData(false, 700)] + [InlineData(false, 600)] + [InlineData(true, 700)] + [InlineData(true, 600)] + public void Decode_UncompressedStripAboveBufferLimit_DecodesAllRows(bool planar, int rowsPerStrip) + { + // With 3 x 16-bit samples, a strip of 600 rows needs 3.7 MB (1.2 MB per plane), more than the 1 MB + // allocation limit, while the decoded L8 image needs only 0.7 MB. + const int width = 1024; + const int height = 700; + TiffPage page = TiffPage.Create(width, height, [16, 16, 16], TiffPhotometricInterpretation.Rgb, planar, (uint)rowsPerStrip, CreateRgbStrips(width, height, 2, planar, rowsPerStrip)); + byte[] tiff = BuildTiff([page]); + + using Image expected = Decode(tiff); + using Image image = Decode(tiff, CreateConfiguration(1)); + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (expected[x, y] != image[x, y]) + { + Assert.Fail($"Pixel ({x}, {y}) is {image[x, y]}, expected {expected[x, y]}."); + } + } + } + } + + /// + /// A frame that does not fit in the allocation limit reports its real dimensions. + /// + [Fact] + public void Decode_FrameAboveAllocationLimit_ThrowsWithRealDimensions() + { + TiffPage page = TiffPage.Create(20000, 15000, [16, 16, 16, 16], TiffPhotometricInterpretation.Rgb, false, null, [new byte[4096]]); + + InvalidImageContentException ex = Assert.Throws(() => Decode(BuildTiff([page]), CreateConfiguration(100)).Dispose()); + + Assert.Contains("20000x15000", ex.Message, StringComparison.Ordinal); + } + + /// + /// TIFF allows directories at any file offset, so a chain that goes backwards in the file is valid. + /// + [Fact] + public void Decode_DirectoriesStoredInReverseFileOrder_DecodesAllFrames() + { + TiffPage first = TiffPage.Create(2, 2, [8], TiffPhotometricInterpretation.BlackIsZero, false, 2, [[10, 11, 12, 13]]); + TiffPage second = TiffPage.Create(2, 2, [8], TiffPhotometricInterpretation.BlackIsZero, false, 2, [[20, 21, 22, 23]]); + + using Image image = Decode(BuildTiff([first, second], reverseIfdOrder: true)); + + Assert.Equal(2, image.Frames.Count); + Assert.Equal(10, image.Frames[0][0, 0].PackedValue); + Assert.Equal(13, image.Frames[0][1, 1].PackedValue); + Assert.Equal(20, image.Frames[1][0, 0].PackedValue); + Assert.Equal(23, image.Frames[1][1, 1].PackedValue); + } + + /// + /// A directory chain that returns to a directory it has already read is a loop and fails fast. + /// + /// The number of directories in the loop. + /// Whether the directories are stored in reverse file order. + [Theory] + [InlineData(1, false)] + [InlineData(2, false)] + [InlineData(2, true)] + [InlineData(3, true)] + public void Decode_DirectoryChainRevisitsDirectory_ThrowsImageFormatException(int pageCount, bool reverseIfdOrder) + { + TiffPage[] pages = new TiffPage[pageCount]; + for (int i = 0; i < pageCount; i++) + { + pages[i] = TiffPage.Create(2, 2, [8], TiffPhotometricInterpretation.BlackIsZero, false, 2, [new byte[4]]); + } + + byte[] tiff = BuildTiff(pages, reverseIfdOrder, loopToFirstIfd: true); + + ImageFormatException ex = Assert.Throws(() => Decode(tiff).Dispose()); + Assert.Contains("circular", ex.Message, StringComparison.Ordinal); + } + + /// + /// Gets the expected value of a sample of the synthesised images. + /// + /// The x coordinate. + /// The y coordinate. + /// The channel index. + /// The sample value. + private static byte Sample(int x, int y, int channel) => (byte)((x * 7) + (y * 101) + (channel * 53)); + + /// + /// Asserts that every pixel of the image matches . + /// + /// The decoded image. + private static void AssertRgb24Pixels(Image image) + { + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + Rgb24 expected = new(Sample(x, y, 0), Sample(x, y, 1), Sample(x, y, 2)); + if (!expected.Equals(image[x, y])) + { + Assert.Fail($"Pixel ({x}, {y}) is {image[x, y]}, expected {expected}."); + } + } + } + } + + /// + /// Creates the strip data of an RGB image whose samples are , stored as + /// 8-bit samples, or as 16-bit samples holding the same value in both bytes. Planar strips are ordered by plane. + /// + /// The image width. + /// The image height. + /// The bytes per sample, 1 or 2. + /// Whether the samples are stored planar. + /// The number of rows per strip. + /// The strips. + private static byte[][] CreateRgbStrips(int width, int height, int bytesPerSample, bool planar, int rowsPerStrip) + { + int stripsPerPlane = (height + rowsPerStrip - 1) / rowsPerStrip; + int planes = planar ? 3 : 1; + int samplesPerPixel = planar ? 1 : 3; + byte[][] strips = new byte[planes * stripsPerPlane][]; + for (int plane = 0; plane < planes; plane++) + { + for (int strip = 0; strip < stripsPerPlane; strip++) + { + int top = strip * rowsPerStrip; + int rows = Math.Min(rowsPerStrip, height - top); + byte[] data = new byte[width * rows * samplesPerPixel * bytesPerSample]; + for (int y = 0; y < rows; y++) + { + for (int x = 0; x < width; x++) + { + for (int sample = 0; sample < samplesPerPixel; sample++) + { + byte value = Sample(x, top + y, plane + sample); + int index = (((y * width) + x) * samplesPerPixel) + sample; + data.AsSpan(index * bytesPerSample, bytesPerSample).Fill(value); + } + } + } + + strips[(plane * stripsPerPlane) + strip] = data; + } + } + + return strips; + } + + /// + /// Decodes a TIFF file from memory. + /// + /// The pixel format. + /// The encoded TIFF. + /// The configuration, or for the default. + /// The decoded image. + private static Image Decode(byte[] tiff, Configuration configuration = null) + where TPixel : unmanaged, IPixel + { + using MemoryStream stream = new(tiff, false); + return Image.Load(new DecoderOptions { Configuration = configuration ?? Configuration.Default }, stream); + } + + /// + /// Creates a configuration whose memory allocator has the given allocation limit. + /// + /// The allocation limit in megabytes. + /// The configuration. + private static Configuration CreateConfiguration(int allocationLimitMegabytes) + { + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { AllocationLimitMegabytes = allocationLimitMegabytes }); + return configuration; + } + + /// + /// Writes a little-endian TIFF. The strip data of every page follows the header, and the directories follow + /// the strip data in page order, or in reverse page order when is set. + /// + /// The pages. + /// Whether to store the directories in reverse page order. + /// Whether the last directory links back to the first one. + /// The encoded TIFF. + private static byte[] BuildTiff(TiffPage[] pages, bool reverseIfdOrder = false, bool loopToFirstIfd = false) + { + List bytes = [0x49, 0x49, 42, 0, 0, 0, 0, 0]; + + uint[][] stripOffsets = new uint[pages.Length][]; + for (int i = 0; i < pages.Length; i++) + { + stripOffsets[i] = new uint[pages[i].Strips.Length]; + for (int s = 0; s < pages[i].Strips.Length; s++) + { + stripOffsets[i][s] = (uint)bytes.Count; + bytes.AddRange(pages[i].Strips[s]); + } + } + + uint[] ifdOffsets = new uint[pages.Length]; + int[] nextIfdPositions = new int[pages.Length]; + for (int n = 0; n < pages.Length; n++) + { + int i = reverseIfdOrder ? pages.Length - 1 - n : n; + + // Directories must start on a word boundary. + if ((bytes.Count & 1) != 0) + { + bytes.Add(0); + } + + ifdOffsets[i] = (uint)bytes.Count; + List<(ExifTagValue Tag, ExifDataType Type, uint[] Values)> entries = pages[i].GetEntries(stripOffsets[i]); + + // Values that do not fit in an entry are stored straight after the directory. + int valuesOffset = bytes.Count + 2 + (entries.Count * 12) + 4; + List values = []; + AddUInt16(bytes, (ushort)entries.Count); + foreach ((ExifTagValue tag, ExifDataType type, uint[] entryValues) in entries) + { + AddUInt16(bytes, (ushort)tag); + AddUInt16(bytes, (ushort)type); + AddUInt32(bytes, (uint)entryValues.Length); + + List target = bytes; + if (entryValues.Length * (type == ExifDataType.Short ? 2 : 4) > 4) + { + AddUInt32(bytes, (uint)(valuesOffset + values.Count)); + target = values; + } + + int start = target.Count; + foreach (uint value in entryValues) + { + if (type == ExifDataType.Short) + { + AddUInt16(target, (ushort)value); + } + else + { + AddUInt32(target, value); + } + } + + while (target == bytes && bytes.Count - start < 4) + { + bytes.Add(0); + } + } + + nextIfdPositions[i] = bytes.Count; + AddUInt32(bytes, 0); + bytes.AddRange(values); + } + + byte[] tiff = [.. bytes]; + BinaryPrimitives.WriteUInt32LittleEndian(tiff.AsSpan(4), ifdOffsets[0]); + for (int i = 0; i < pages.Length; i++) + { + uint next = i + 1 < pages.Length ? ifdOffsets[i + 1] : loopToFirstIfd ? ifdOffsets[0] : 0; + BinaryPrimitives.WriteUInt32LittleEndian(tiff.AsSpan(nextIfdPositions[i]), next); + } + + return tiff; + } + + /// + /// Appends a little-endian 16-bit value. + /// + /// The target. + /// The value. + private static void AddUInt16(List bytes, ushort value) + { + bytes.Add((byte)value); + bytes.Add((byte)(value >> 8)); + } + + /// + /// Appends a little-endian 32-bit value. + /// + /// The target. + /// The value. + private static void AddUInt32(List bytes, uint value) + { + AddUInt16(bytes, (ushort)value); + AddUInt16(bytes, (ushort)(value >> 16)); + } + + /// + /// Describes one uncompressed page of a synthesised TIFF. + /// + private sealed class TiffPage + { + /// + /// Gets the image width. + /// + public int Width { get; private init; } + + /// + /// Gets the image height. + /// + public int Height { get; private init; } + + /// + /// Gets the bits of each sample; the length is the number of samples per pixel. + /// + public ushort[] BitsPerSample { get; private init; } + + /// + /// Gets the photometric interpretation. + /// + public TiffPhotometricInterpretation Photometric { get; private init; } + + /// + /// Gets a value indicating whether the samples are stored planar. + /// + public bool Planar { get; private init; } + + /// + /// Gets the RowsPerStrip value, or to omit the entry. + /// + public uint? RowsPerStrip { get; private init; } + + /// + /// Gets the strip data. + /// + public byte[][] Strips { get; private init; } + + /// + /// Creates a page. + /// + /// The image width. + /// The image height. + /// The bits of each sample. + /// The photometric interpretation. + /// Whether the samples are stored planar. + /// The RowsPerStrip value, or to omit the entry. + /// The strip data. + /// The page. + public static TiffPage Create(int width, int height, ushort[] bitsPerSample, TiffPhotometricInterpretation photometric, bool planar, uint? rowsPerStrip, byte[][] strips) + => new() + { + Width = width, + Height = height, + BitsPerSample = bitsPerSample, + Photometric = photometric, + Planar = planar, + RowsPerStrip = rowsPerStrip, + Strips = strips, + }; + + /// + /// Gets the directory entries of the page, sorted by tag. + /// + /// The file offset of each strip. + /// The entries. + public List<(ExifTagValue Tag, ExifDataType Type, uint[] Values)> GetEntries(uint[] stripOffsets) + { + uint[] bitsPerSample = new uint[this.BitsPerSample.Length]; + for (int i = 0; i < bitsPerSample.Length; i++) + { + bitsPerSample[i] = this.BitsPerSample[i]; + } + + uint[] stripByteCounts = new uint[this.Strips.Length]; + for (int i = 0; i < stripByteCounts.Length; i++) + { + stripByteCounts[i] = (uint)this.Strips[i].Length; + } + + List<(ExifTagValue Tag, ExifDataType Type, uint[] Values)> entries = + [ + (ExifTagValue.ImageWidth, ExifDataType.Long, [(uint)this.Width]), + (ExifTagValue.ImageLength, ExifDataType.Long, [(uint)this.Height]), + (ExifTagValue.BitsPerSample, ExifDataType.Short, bitsPerSample), + (ExifTagValue.Compression, ExifDataType.Short, [(uint)TiffCompression.None]), + (ExifTagValue.PhotometricInterpretation, ExifDataType.Short, [(uint)this.Photometric]), + (ExifTagValue.StripOffsets, ExifDataType.Long, stripOffsets), + (ExifTagValue.SamplesPerPixel, ExifDataType.Short, [(uint)this.BitsPerSample.Length]), + (ExifTagValue.StripByteCounts, ExifDataType.Long, stripByteCounts), + ]; + + if (this.RowsPerStrip.HasValue) + { + entries.Add((ExifTagValue.RowsPerStrip, ExifDataType.Long, [this.RowsPerStrip.Value])); + } + + if (this.Planar) + { + entries.Add((ExifTagValue.PlanarConfiguration, ExifDataType.Short, [(uint)TiffPlanarConfiguration.Planar])); + } + + entries.Sort((a, b) => a.Tag.CompareTo(b.Tag)); + return entries; + } + } +}