From e2669707c67e0b8642dca2a1a01e4809e1f02968 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 24 Nov 2020 19:58:15 +0100 Subject: [PATCH] First attempt writing uncompressed tiff --- src/ImageSharp/Formats/Tiff/README.md | 2 +- src/ImageSharp/Formats/Tiff/TiffEncoder.cs | 3 +- .../Formats/Tiff/TiffEncoderCore.cs | 166 +++++++++++++++--- .../Formats/Tiff/Utils/TiffWriter.cs | 70 ++++++-- .../Formats/Tiff/TiffEncoderHeaderTests.cs | 16 +- .../Formats/Tiff/Utils/TiffWriterTests.cs | 87 ++++----- 6 files changed, 248 insertions(+), 96 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index f2fa861a25..636e08a32e 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -46,7 +46,7 @@ |CcittGroup3Fax | | Y | | |CcittGroup4Fax | | | | |Lzw | | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case | -|Old Jpeg | | | | +|Old Jpeg | | | We should not even try to support this | |Jpeg (Technote 2) | | | | |Deflate (Technote 2) | | Y | | |Old Deflate (Technote 2) | | Y | | diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index 4c0d5dff8a..18c0d12a0f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff @@ -17,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel { - var encode = new TiffEncoderCore(this); + var encode = new TiffEncoderCore(this, image.GetMemoryAllocator()); encode.Encode(image, stream); } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 8aa0edb978..0350f42a47 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -4,7 +4,8 @@ using System; using System.Collections.Generic; using System.IO; -using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -15,12 +16,29 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// internal sealed class TiffEncoderCore { + /// + /// The amount to pad each row by in bytes. + /// + private int padding; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private Configuration configuration; + /// /// Initializes a new instance of the class. /// /// The options for the encoder. - public TiffEncoderCore(ITiffEncoderOptions options) + /// The memory allocator. + public TiffEncoderCore(ITiffEncoderOptions options, MemoryAllocator memoryAllocator) { + this.memoryAllocator = memoryAllocator; options = options ?? new TiffEncoder(); } @@ -46,10 +64,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - using (var writer = new TiffWriter(stream)) + this.configuration = image.GetConfiguration(); + + // TODO: bits per pixel hardcoded to 24 for the start. + short bpp = 24; + int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32); + this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F)); + + using (var writer = new TiffWriter(stream, this.memoryAllocator, this.configuration)) { long firstIfdMarker = this.WriteHeader(writer); - //// todo: multiframing is not support + + // TODO: multiframing is not support long nextIfdMarker = this.WriteImage(writer, image, firstIfdMarker); } } @@ -72,6 +98,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff return firstIfdMarker; } + /// + /// Writes all data required to define an image. + /// + /// The pixel format. + /// The to write data to. + /// The to encode from. + /// The marker to write this IFD offset. + /// The marker to write the next IFD offset (if present). + public long WriteImage(TiffWriter writer, Image image, long ifdOffset) + where TPixel : unmanaged, IPixel + { + var ifdEntries = new List(); + + // Write the image bytes to the steam. + var imageDataStart = (uint)writer.Position; + int imageData = writer.WriteRgbImageData(image, this.padding); + + // Write info's about the image to the stream. + this.AddImageFormat(image, ifdEntries, imageDataStart, imageData); + writer.WriteMarker(ifdOffset, (uint)writer.Position); + long nextIfdMarker = this.WriteIfd(writer, ifdEntries); + + return nextIfdMarker + imageData; + } + /// /// Writes a TIFF IFD block. /// @@ -98,8 +149,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff writer.Write((ushort)entry.DataType); writer.Write(ExifWriter.GetNumberOfComponents(entry)); - uint lenght = ExifWriter.GetLength(entry); - var raw = new byte[lenght]; + uint length = ExifWriter.GetLength(entry); + var raw = new byte[length]; int sz = ExifWriter.WriteValue(entry, raw, 0); DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written"); if (raw.Length <= 4) @@ -130,36 +181,95 @@ namespace SixLabors.ImageSharp.Formats.Tiff } /// - /// Writes all data required to define an image + /// Adds image format information to the specified IFD. /// /// The pixel format. - /// The to write data to. /// The to encode from. - /// The marker to write this IFD offset. - /// The marker to write the next IFD offset (if present). - public long WriteImage(TiffWriter writer, Image image, long ifdOffset) - where TPixel : unmanaged, IPixel + /// The image format entries to add to the IFD. + /// The start of the image data in the stream. + /// The image data in bytes to write. + public void AddImageFormat(Image image, List ifdEntries, uint imageDataStartOffset, int imageDataBytes) + where TPixel : unmanaged, IPixel { - var ifdEntries = new List(); + var width = new ExifLong(ExifTagValue.ImageWidth) + { + Value = (uint)image.Width + }; - this.AddImageFormat(image, ifdEntries); + var height = new ExifLong(ExifTagValue.ImageLength) + { + Value = (uint)image.Height + }; - writer.WriteMarker(ifdOffset, (uint)writer.Position); - long nextIfdMarker = this.WriteIfd(writer, ifdEntries); + var bitPerSample = new ExifShortArray(ExifTagValue.BitsPerSample) + { + Value = new ushort[] { 8, 8, 8 } + }; - return nextIfdMarker; - } + var compression = new ExifShort(ExifTagValue.Compression) + { + // TODO: for the start, no compression is used. + Value = (ushort)TiffCompression.None + }; - /// - /// Adds image format information to the specified IFD. - /// - /// The pixel format. - /// The to encode from. - /// The image format entries to add to the IFD. - public void AddImageFormat(Image image, List ifdEntries) - where TPixel : unmanaged, IPixel - { - throw new NotImplementedException(); + var photometricInterpretation = new ExifShort(ExifTagValue.PhotometricInterpretation) + { + // TODO: only rgb for now. + Value = (ushort)TiffPhotometricInterpretation.Rgb + }; + + var stripOffsets = new ExifLongArray(ExifTagValue.StripOffsets) + { + // TODO: we only write one image strip for the start. + Value = new uint[] { imageDataStartOffset } + }; + + var samplesPerPixel = new ExifLong(ExifTagValue.SamplesPerPixel) + { + Value = 3 + }; + + var rowsPerStrip = new ExifLong(ExifTagValue.RowsPerStrip) + { + // TODO: all rows in one strip for the start + Value = (uint)image.Height + }; + + var stripByteCounts = new ExifLongArray(ExifTagValue.StripByteCounts) + { + Value = new[] { (uint)(imageDataBytes) } + }; + + var xResolution = new ExifRational(ExifTagValue.XResolution) + { + // TODO: what to use here as a default? + Value = Rational.FromDouble(1.0d) + }; + + var yResolution = new ExifRational(ExifTagValue.YResolution) + { + // TODO: what to use here as a default? + Value = Rational.FromDouble(1.0d) + }; + + var resolutionUnit = new ExifShort(ExifTagValue.ResolutionUnit) + { + // TODO: what to use here as default? + Value = 0 + }; + + ifdEntries.Add(width); + ifdEntries.Add(height); + ifdEntries.Add(bitPerSample); + ifdEntries.Add(compression); + ifdEntries.Add(photometricInterpretation); + ifdEntries.Add(stripOffsets); + ifdEntries.Add(samplesPerPixel); + ifdEntries.Add(rowsPerStrip); + ifdEntries.Add(stripByteCounts); + ifdEntries.Add(xResolution); + ifdEntries.Add(yResolution); + ifdEntries.Add(resolutionUnit); } } } diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs index 7501e314ab..1908d38ae8 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff { @@ -14,15 +16,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff { private readonly Stream output; + private readonly MemoryAllocator memoryAllocator; + + private readonly Configuration configuration; + private readonly byte[] paddingBytes = new byte[4]; private readonly List references = new List(); - /// Initializes a new instance of the class. + /// + /// Initializes a new instance of the class. + /// /// The output stream. - public TiffWriter(Stream output) + /// The memory allocator. + /// The configuration. + public TiffWriter(Stream output, MemoryAllocator memoryMemoryAllocator, Configuration configuration) { this.output = output; + this.memoryAllocator = memoryMemoryAllocator; + this.configuration = configuration; } /// @@ -35,7 +47,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public long Position => this.output.Position; - /// Writes an empty four bytes to the stream, returning the offset to be written later. + /// + /// Writes an empty four bytes to the stream, returning the offset to be written later. + /// /// The offset to be written later public long PlaceMarker() { @@ -44,21 +58,27 @@ namespace SixLabors.ImageSharp.Formats.Tiff return offset; } - /// Writes an array of bytes to the current stream. + /// + /// Writes an array of bytes to the current stream. + /// /// The bytes to write. public void Write(byte[] value) { this.output.Write(value, 0, value.Length); } - /// Writes a byte to the current stream. + /// + /// Writes a byte to the current stream. + /// /// The byte to write. public void Write(byte value) { this.output.Write(new byte[] { value }, 0, 1); } - /// Writes a two-byte unsigned integer to the current stream. + /// + /// Writes a two-byte unsigned integer to the current stream. + /// /// The two-byte unsigned integer to write. public void Write(ushort value) { @@ -66,7 +86,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.output.Write(bytes, 0, 2); } - /// Writes a four-byte unsigned integer to the current stream. + /// + /// Writes a four-byte unsigned integer to the current stream. + /// /// The four-byte unsigned integer to write. public void Write(uint value) { @@ -74,7 +96,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.output.Write(bytes, 0, 4); } - /// Writes an array of bytes to the current stream, padded to four-bytes. + /// + /// Writes an array of bytes to the current stream, padded to four-bytes. + /// /// The bytes to write. public void WritePadded(byte[] value) { @@ -86,7 +110,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - /// Writes a four-byte unsigned integer to the specified marker in the stream. + /// + /// Writes a four-byte unsigned integer to the specified marker in the stream. + /// /// The offset returned when placing the marker /// The four-byte unsigned integer to write. public void WriteMarker(long offset, uint value) @@ -97,6 +123,30 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.output.Seek(currentOffset, SeekOrigin.Begin); } + /// + /// Writes the image data as RGB to the stream. + /// + /// The pixel data. + /// The image to write to the stream. + /// The padding bytes for each row. + /// The number of bytes written + public int WriteRgbImageData(Image image, int padding) + where TPixel : unmanaged, IPixel + { + using IManagedByteBuffer row = this.AllocateRow(image.Width, 3, padding); + Span rowSpan = row.GetSpan(); + for (int y = 0; y < image.Height; y++) + { + Span pixelRow = image.GetPixelRowSpan(y); + PixelOperations.Instance.ToRgb24Bytes(this.configuration, pixelRow, rowSpan, pixelRow.Length); + this.output.Write(rowSpan); + } + + return image.Width * image.Height * 3; + } + + private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel, int padding) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, padding); + /// /// Disposes instance, ensuring any unwritten data is flushed. /// @@ -105,4 +155,4 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.output.Flush(); } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs index 2af1b52250..91166bf2d6 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs @@ -3,6 +3,7 @@ using System.IO; using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Memory; using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Tiff @@ -10,13 +11,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Trait("Category", "Tiff")] public class TiffEncoderHeaderTests { + private static readonly MemoryAllocator MemoryAllocator = new ArrayPoolMemoryAllocator(); + private static readonly Configuration Configuration = Configuration.Default; + [Fact] public void WriteHeader_WritesValidHeader() { - MemoryStream stream = new MemoryStream(); - TiffEncoderCore encoder = new TiffEncoderCore(null); + var stream = new MemoryStream(); + var encoder = new TiffEncoderCore(null, MemoryAllocator); - using (TiffWriter writer = new TiffWriter(stream)) + using (var writer = new TiffWriter(stream, MemoryAllocator, Configuration)) { long firstIfdMarker = encoder.WriteHeader(writer); } @@ -27,10 +31,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Fact] public void WriteHeader_ReturnsFirstIfdMarker() { - MemoryStream stream = new MemoryStream(); - TiffEncoderCore encoder = new TiffEncoderCore(null); + var stream = new MemoryStream(); + var encoder = new TiffEncoderCore(null, MemoryAllocator); - using (TiffWriter writer = new TiffWriter(stream)) + using (var writer = new TiffWriter(stream, MemoryAllocator, Configuration)) { long firstIfdMarker = encoder.WriteHeader(writer); Assert.Equal(4, firstIfdMarker); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs index a3e865519c..9023fe3e02 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs @@ -3,6 +3,7 @@ using System.IO; using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Memory; using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Tiff @@ -10,41 +11,35 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Trait("Category", "Tiff")] public class TiffWriterTests { + private static readonly MemoryAllocator MemoryAllocator = new ArrayPoolMemoryAllocator(); + private static readonly Configuration Configuration = Configuration.Default; + [Fact] public void IsLittleEndian_IsTrueOnWindows() { - MemoryStream stream = new MemoryStream(); - - using (TiffWriter writer = new TiffWriter(stream)) - { - Assert.True(writer.IsLittleEndian); - } + using var stream = new MemoryStream(); + using var writer = new TiffWriter(stream, MemoryAllocator, Configuration); + Assert.True(writer.IsLittleEndian); } [Theory] - [InlineData(new byte[] {}, 0)] + [InlineData(new byte[] { }, 0)] [InlineData(new byte[] { 42 }, 1)] [InlineData(new byte[] { 1, 2, 3, 4, 5 }, 5)] public void Position_EqualsTheStreamPosition(byte[] data, long expectedResult) { - MemoryStream stream = new MemoryStream(); - - using (TiffWriter writer = new TiffWriter(stream)) - { - writer.Write(data); - Assert.Equal(writer.Position, expectedResult); - } + using var stream = new MemoryStream(); + using var writer = new TiffWriter(stream, MemoryAllocator, Configuration); + writer.Write(data); + Assert.Equal(writer.Position, expectedResult); } [Fact] public void Write_WritesByte() { - MemoryStream stream = new MemoryStream(); - - using (TiffWriter writer = new TiffWriter(stream)) - { - writer.Write((byte)42); - } + using var stream = new MemoryStream(); + using var writer = new TiffWriter(stream, MemoryAllocator, Configuration); + writer.Write((byte)42); Assert.Equal(new byte[] { 42 }, stream.ToArray()); } @@ -52,12 +47,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Fact] public void Write_WritesByteArray() { - MemoryStream stream = new MemoryStream(); - - using (TiffWriter writer = new TiffWriter(stream)) - { - writer.Write(new byte[] { 2, 4, 6, 8 }); - } + using var stream = new MemoryStream(); + using var writer = new TiffWriter(stream, MemoryAllocator, Configuration); + writer.Write(new byte[] { 2, 4, 6, 8 }); Assert.Equal(new byte[] { 2, 4, 6, 8 }, stream.ToArray()); } @@ -65,12 +57,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Fact] public void Write_WritesUInt16() { - MemoryStream stream = new MemoryStream(); - - using (TiffWriter writer = new TiffWriter(stream)) - { - writer.Write((ushort)1234); - } + using var stream = new MemoryStream(); + using var writer = new TiffWriter(stream, MemoryAllocator, Configuration); + writer.Write((ushort)1234); Assert.Equal(new byte[] { 0xD2, 0x04 }, stream.ToArray()); } @@ -78,12 +67,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Fact] public void Write_WritesUInt32() { - MemoryStream stream = new MemoryStream(); - - using (TiffWriter writer = new TiffWriter(stream)) - { - writer.Write((uint)12345678); - } + using var stream = new MemoryStream(); + using var writer = new TiffWriter(stream, MemoryAllocator, Configuration); + writer.Write((uint)12345678); Assert.Equal(new byte[] { 0x4E, 0x61, 0xBC, 0x00 }, stream.ToArray()); } @@ -97,12 +83,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [InlineData(new byte[] { 2, 4, 6, 8, 10, 12 }, new byte[] { 2, 4, 6, 8, 10, 12 })] public void WritePadded_WritesByteArray(byte[] bytes, byte[] expectedResult) { - MemoryStream stream = new MemoryStream(); - - using (TiffWriter writer = new TiffWriter(stream)) - { - writer.WritePadded(bytes); - } + using var stream = new MemoryStream(); + using var writer = new TiffWriter(stream, MemoryAllocator, Configuration); + writer.WritePadded(bytes); Assert.Equal(expectedResult, stream.ToArray()); } @@ -110,9 +93,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Fact] public void WriteMarker_WritesToPlacedPosition() { - MemoryStream stream = new MemoryStream(); + using var stream = new MemoryStream(); - using (TiffWriter writer = new TiffWriter(stream)) + using (var writer = new TiffWriter(stream, MemoryAllocator, Configuration)) { writer.Write((uint)0x11111111); long marker = writer.PlaceMarker(); @@ -123,10 +106,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff writer.Write((uint)0x44444444); } - Assert.Equal(new byte[] { 0x11, 0x11, 0x11, 0x11, - 0x78, 0x56, 0x34, 0x12, - 0x33, 0x33, 0x33, 0x33, - 0x44, 0x44, 0x44, 0x44 }, stream.ToArray()); + Assert.Equal( + new byte[] + { + 0x11, 0x11, 0x11, 0x11, + 0x78, 0x56, 0x34, 0x12, + 0x33, 0x33, 0x33, 0x33, + 0x44, 0x44, 0x44, 0x44 + }, stream.ToArray()); } } }