Browse Source

First attempt writing uncompressed tiff

pull/1457/head
Brian Popow 6 years ago
parent
commit
e2669707c6
  1. 2
      src/ImageSharp/Formats/Tiff/README.md
  2. 3
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  3. 166
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  4. 70
      src/ImageSharp/Formats/Tiff/Utils/TiffWriter.cs
  5. 16
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs
  6. 87
      tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffWriterTests.cs

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

3
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<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
var encode = new TiffEncoderCore(this);
var encode = new TiffEncoderCore(this, image.GetMemoryAllocator());
encode.Encode(image, stream);
}

166
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
/// </summary>
internal sealed class TiffEncoderCore
{
/// <summary>
/// The amount to pad each row by in bytes.
/// </summary>
private int padding;
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The global configuration.
/// </summary>
private Configuration configuration;
/// <summary>
/// Initializes a new instance of the <see cref="TiffEncoderCore"/> class.
/// </summary>
/// <param name="options">The options for the encoder.</param>
public TiffEncoderCore(ITiffEncoderOptions options)
/// <param name="memoryAllocator">The memory allocator.</param>
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;
}
/// <summary>
/// Writes all data required to define an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="writer">The <see cref="BinaryWriter"/> to write data to.</param>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="ifdOffset">The marker to write this IFD offset.</param>
/// <returns>The marker to write the next IFD offset (if present).</returns>
public long WriteImage<TPixel>(TiffWriter writer, Image<TPixel> image, long ifdOffset)
where TPixel : unmanaged, IPixel<TPixel>
{
var ifdEntries = new List<IExifValue>();
// 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;
}
/// <summary>
/// Writes a TIFF IFD block.
/// </summary>
@ -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
}
/// <summary>
/// Writes all data required to define an image
/// Adds image format information to the specified IFD.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="writer">The <see cref="BinaryWriter"/> to write data to.</param>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="ifdOffset">The marker to write this IFD offset.</param>
/// <returns>The marker to write the next IFD offset (if present).</returns>
public long WriteImage<TPixel>(TiffWriter writer, Image<TPixel> image, long ifdOffset)
where TPixel : unmanaged, IPixel<TPixel>
/// <param name="ifdEntries">The image format entries to add to the IFD.</param>
/// <param name="imageDataStartOffset">The start of the image data in the stream.</param>
/// <param name="imageDataBytes">The image data in bytes to write.</param>
public void AddImageFormat<TPixel>(Image<TPixel> image, List<IExifValue> ifdEntries, uint imageDataStartOffset, int imageDataBytes)
where TPixel : unmanaged, IPixel<TPixel>
{
var ifdEntries = new List<IExifValue>();
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
};
/// <summary>
/// Adds image format information to the specified IFD.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="ifdEntries">The image format entries to add to the IFD.</param>
public void AddImageFormat<TPixel>(Image<TPixel> image, List<IExifValue> ifdEntries)
where TPixel : unmanaged, IPixel<TPixel>
{
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);
}
}
}

70
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<long> references = new List<long>();
/// <summary>Initializes a new instance of the <see cref="TiffWriter"/> class.</summary>
/// <summary>
/// Initializes a new instance of the <see cref="TiffWriter"/> class.
/// </summary>
/// <param name="output">The output stream.</param>
public TiffWriter(Stream output)
/// <param name="memoryMemoryAllocator">The memory allocator.</param>
/// <param name="configuration">The configuration.</param>
public TiffWriter(Stream output, MemoryAllocator memoryMemoryAllocator, Configuration configuration)
{
this.output = output;
this.memoryAllocator = memoryMemoryAllocator;
this.configuration = configuration;
}
/// <summary>
@ -35,7 +47,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
public long Position => this.output.Position;
/// <summary>Writes an empty four bytes to the stream, returning the offset to be written later.</summary>
/// <summary>
/// Writes an empty four bytes to the stream, returning the offset to be written later.
/// </summary>
/// <returns>The offset to be written later</returns>
public long PlaceMarker()
{
@ -44,21 +58,27 @@ namespace SixLabors.ImageSharp.Formats.Tiff
return offset;
}
/// <summary>Writes an array of bytes to the current stream.</summary>
/// <summary>
/// Writes an array of bytes to the current stream.
/// </summary>
/// <param name="value">The bytes to write.</param>
public void Write(byte[] value)
{
this.output.Write(value, 0, value.Length);
}
/// <summary>Writes a byte to the current stream.</summary>
/// <summary>
/// Writes a byte to the current stream.
/// </summary>
/// <param name="value">The byte to write.</param>
public void Write(byte value)
{
this.output.Write(new byte[] { value }, 0, 1);
}
/// <summary>Writes a two-byte unsigned integer to the current stream.</summary>
/// <summary>
/// Writes a two-byte unsigned integer to the current stream.
/// </summary>
/// <param name="value">The two-byte unsigned integer to write.</param>
public void Write(ushort value)
{
@ -66,7 +86,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.output.Write(bytes, 0, 2);
}
/// <summary>Writes a four-byte unsigned integer to the current stream.</summary>
/// <summary>
/// Writes a four-byte unsigned integer to the current stream.
/// </summary>
/// <param name="value">The four-byte unsigned integer to write.</param>
public void Write(uint value)
{
@ -74,7 +96,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.output.Write(bytes, 0, 4);
}
/// <summary>Writes an array of bytes to the current stream, padded to four-bytes.</summary>
/// <summary>
/// Writes an array of bytes to the current stream, padded to four-bytes.
/// </summary>
/// <param name="value">The bytes to write.</param>
public void WritePadded(byte[] value)
{
@ -86,7 +110,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
}
/// <summary>Writes a four-byte unsigned integer to the specified marker in the stream.</summary>
/// <summary>
/// Writes a four-byte unsigned integer to the specified marker in the stream.
/// </summary>
/// <param name="offset">The offset returned when placing the marker</param>
/// <param name="value">The four-byte unsigned integer to write.</param>
public void WriteMarker(long offset, uint value)
@ -97,6 +123,30 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.output.Seek(currentOffset, SeekOrigin.Begin);
}
/// <summary>
/// Writes the image data as RGB to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel data.</typeparam>
/// <param name="image">The image to write to the stream.</param>
/// <param name="padding">The padding bytes for each row.</param>
/// <returns>The number of bytes written</returns>
public int WriteRgbImageData<TPixel>(Image<TPixel> image, int padding)
where TPixel : unmanaged, IPixel<TPixel>
{
using IManagedByteBuffer row = this.AllocateRow(image.Width, 3, padding);
Span<byte> rowSpan = row.GetSpan();
for (int y = 0; y < image.Height; y++)
{
Span<TPixel> pixelRow = image.GetPixelRowSpan(y);
PixelOperations<TPixel>.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);
/// <summary>
/// Disposes <see cref="TiffWriter"/> instance, ensuring any unwritten data is flushed.
/// </summary>
@ -105,4 +155,4 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.output.Flush();
}
}
}
}

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

87
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());
}
}
}

Loading…
Cancel
Save