Browse Source

Merge branch 'master' into pr/1629

pull/1629/head
James Jackson-South 5 years ago
parent
commit
5d2884e1b7
  1. 1
      ImageSharp.sln
  2. 7
      src/ImageSharp/Advanced/AotCompilerTools.cs
  3. 23
      src/ImageSharp/Common/ByteOrder.cs
  4. 50
      src/ImageSharp/Common/Helpers/UnitConverter.cs
  5. 2
      src/ImageSharp/Compression/Zlib/Adler32.cs
  6. 2
      src/ImageSharp/Compression/Zlib/Crc32.Lut.cs
  7. 2
      src/ImageSharp/Compression/Zlib/Crc32.cs
  8. 81
      src/ImageSharp/Compression/Zlib/DeflateCompressionLevel.cs
  9. 2
      src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs
  10. 2
      src/ImageSharp/Compression/Zlib/Deflater.cs
  11. 2
      src/ImageSharp/Compression/Zlib/DeflaterConstants.cs
  12. 2
      src/ImageSharp/Compression/Zlib/DeflaterEngine.cs
  13. 2
      src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs
  14. 2
      src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs
  15. 2
      src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs
  16. 0
      src/ImageSharp/Compression/Zlib/README.md
  17. 15
      src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs
  18. 2
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  19. 0
      src/ImageSharp/Compression/Zlib/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf
  20. 13
      src/ImageSharp/Configuration.cs
  21. 106
      src/ImageSharp/Formats/ImageExtensions.Save.cs
  22. 3
      src/ImageSharp/Formats/ImageExtensions.Save.tt
  23. 3
      src/ImageSharp/Formats/Png/PngCompressionLevel.cs
  24. 3
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  25. 4
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  26. 2
      src/ImageSharp/Formats/Tga/README.md
  27. 51
      src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs
  28. 62
      src/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs
  29. 40
      src/ImageSharp/Formats/Tiff/Compression/Compressors/LzwCompressor.cs
  30. 34
      src/ImageSharp/Formats/Tiff/Compression/Compressors/NoCompressor.cs
  31. 48
      src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsCompressor.cs
  32. 128
      src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs
  33. 592
      src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs
  34. 270
      src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffLzwEncoder.cs
  35. 63
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
  36. 95
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwString.cs
  37. 45
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs
  38. 79
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs
  39. 35
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs
  40. 96
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs
  41. 842
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs
  42. 90
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs
  43. 257
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffLzwDecoder.cs
  44. 36
      src/ImageSharp/Formats/Tiff/Compression/FaxCompressionOptions.cs
  45. 138
      src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs
  46. 62
      src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs
  47. 48
      src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompressor.cs
  48. 54
      src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs
  49. 64
      src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs
  50. 41
      src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs
  51. 54
      src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
  52. 94
      src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs
  53. 113
      src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
  54. 26
      src/ImageSharp/Formats/Tiff/Constants/TiffExtraSamples.cs
  55. 21
      src/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs
  56. 44
      src/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs
  57. 51
      src/ImageSharp/Formats/Tiff/Constants/TiffOrientation.cs
  58. 89
      src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs
  59. 31
      src/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs
  60. 28
      src/ImageSharp/Formats/Tiff/Constants/TiffPredictor.cs
  61. 41
      src/ImageSharp/Formats/Tiff/Constants/TiffSampleFormat.cs
  62. 26
      src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs
  63. 26
      src/ImageSharp/Formats/Tiff/Constants/TiffThresholding.cs
  64. 16
      src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs
  65. 47
      src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs
  66. 104
      src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
  67. 65
      src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs
  68. 28
      src/ImageSharp/Formats/Tiff/MetadataExtensions.cs
  69. 46
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor{TPixel}.cs
  70. 58
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor{TPixel}.cs
  71. 39
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs
  72. 50
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs
  73. 67
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs
  74. 43
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs
  75. 76
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs
  76. 64
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs
  77. 28
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBaseColorDecoder{TPixel}.cs
  78. 94
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
  79. 71
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
  80. 45
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor{TPixel}.cs
  81. 58
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor{TPixel}.cs
  82. 39
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs
  83. 50
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs
  84. 253
      src/ImageSharp/Formats/Tiff/README.md
  85. BIN
      src/ImageSharp/Formats/Tiff/T-REC-T.4-198811-S!!PDF-E.pdf
  86. BIN
      src/ImageSharp/Formats/Tiff/T-REC-T.6-198811-I!!PDF-E.pdf
  87. BIN
      src/ImageSharp/Formats/Tiff/TIFF-v6.pdf
  88. 31
      src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
  89. 36
      src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs
  90. 75
      src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs
  91. 19
      src/ImageSharp/Formats/Tiff/TiffConfigurationModule.cs
  92. 67
      src/ImageSharp/Formats/Tiff/TiffDecoder.cs
  93. 339
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  94. 133
      src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
  95. 281
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  96. 55
      src/ImageSharp/Formats/Tiff/TiffEncoder.cs
  97. 381
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  98. 378
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  99. 42
      src/ImageSharp/Formats/Tiff/TiffFormat.cs
  100. 113
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs

1
ImageSharp.sln

@ -1,4 +1,3 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28902.138

7
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
@ -56,7 +57,7 @@ namespace SixLabors.ImageSharp.Advanced
/// necessary methods to complete the SaveAsGif call. That's it, otherwise you should NEVER need this method!!!
/// </remarks>
[Preserve]
private static void SeedEverything()
private static void SeedPixelFormats()
{
try
{
@ -199,6 +200,7 @@ namespace SixLabors.ImageSharp.Advanced
default(JpegEncoderCore).Encode<TPixel>(default, default, default);
default(PngEncoderCore).Encode<TPixel>(default, default, default);
default(TgaEncoderCore).Encode<TPixel>(default, default, default);
default(TiffEncoderCore).Encode<TPixel>(default, default, default);
}
/// <summary>
@ -214,6 +216,7 @@ namespace SixLabors.ImageSharp.Advanced
default(JpegDecoderCore).Decode<TPixel>(default, default, default);
default(PngDecoderCore).Decode<TPixel>(default, default, default);
default(TgaDecoderCore).Decode<TPixel>(default, default, default);
default(TiffDecoderCore).Decode<TPixel>(default, default, default);
}
/// <summary>
@ -229,6 +232,7 @@ namespace SixLabors.ImageSharp.Advanced
AotCompileImageEncoder<TPixel, JpegEncoder>();
AotCompileImageEncoder<TPixel, PngEncoder>();
AotCompileImageEncoder<TPixel, TgaEncoder>();
AotCompileImageEncoder<TPixel, TiffEncoder>();
}
/// <summary>
@ -244,6 +248,7 @@ namespace SixLabors.ImageSharp.Advanced
AotCompileImageDecoder<TPixel, JpegDecoder>();
AotCompileImageDecoder<TPixel, PngDecoder>();
AotCompileImageDecoder<TPixel, TgaDecoder>();
AotCompileImageDecoder<TPixel, TiffDecoder>();
}
/// <summary>

23
src/ImageSharp/Common/ByteOrder.cs

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp
{
/// <summary>
/// The byte order of the data stream.
/// </summary>
public enum ByteOrder
{
/// <summary>
/// The big-endian byte order (Motorola).
/// Most-significant byte comes first, and ends with the least-significant byte.
/// </summary>
BigEndian,
/// <summary>
/// The little-endian byte order (Intel).
/// Least-significant byte comes first and ends with the most-significant byte.
/// </summary>
LittleEndian
}
}

50
src/ImageSharp/Common/Helpers/UnitConverter.cs

@ -30,6 +30,11 @@ namespace SixLabors.ImageSharp.Common.Helpers
/// </summary>
private const double InchesInMeter = 1 / 0.0254D;
/// <summary>
/// The default resolution unit value.
/// </summary>
private const PixelResolutionUnit DefaultResolutionUnit = PixelResolutionUnit.PixelsPerInch;
/// <summary>
/// Scales the value from centimeters to meters.
/// </summary>
@ -89,7 +94,50 @@ namespace SixLabors.ImageSharp.Common.Helpers
IExifValue<ushort> resolution = profile.GetValue(ExifTag.ResolutionUnit);
// EXIF is 1, 2, 3 so we minus "1" off the result.
return resolution is null ? default : (PixelResolutionUnit)(byte)(resolution.Value - 1);
return resolution is null ? DefaultResolutionUnit : (PixelResolutionUnit)(byte)(resolution.Value - 1);
}
/// <summary>
/// Sets the exif profile resolution values.
/// </summary>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="unit">The resolution unit.</param>
/// <param name="horizontal">The horizontal resolution value.</param>
/// <param name="vertical">The vertical resolution value.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void SetResolutionValues(ExifProfile exifProfile, PixelResolutionUnit unit, double horizontal, double vertical)
{
switch (unit)
{
case PixelResolutionUnit.AspectRatio:
case PixelResolutionUnit.PixelsPerInch:
case PixelResolutionUnit.PixelsPerCentimeter:
break;
case PixelResolutionUnit.PixelsPerMeter:
{
unit = PixelResolutionUnit.PixelsPerCentimeter;
horizontal = UnitConverter.MeterToCm(horizontal);
vertical = UnitConverter.MeterToCm(vertical);
}
break;
default:
unit = PixelResolutionUnit.PixelsPerInch;
break;
}
exifProfile.SetValue(ExifTag.ResolutionUnit, (ushort)(unit + 1));
if (unit == PixelResolutionUnit.AspectRatio)
{
exifProfile.RemoveValue(ExifTag.XResolution);
exifProfile.RemoveValue(ExifTag.YResolution);
}
else
{
exifProfile.SetValue(ExifTag.XResolution, new Rational(horizontal));
exifProfile.SetValue(ExifTag.YResolution, new Rational(vertical));
}
}
}
}

2
src/ImageSharp/Formats/Png/Zlib/Adler32.cs → src/ImageSharp/Compression/Zlib/Adler32.cs

@ -9,7 +9,7 @@ using System.Runtime.Intrinsics.X86;
#endif
#pragma warning disable IDE0007 // Use implicit type
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// Calculates the 32 bit Adler checksum of a given buffer according to

2
src/ImageSharp/Formats/Png/Zlib/Crc32.Lut.cs → src/ImageSharp/Compression/Zlib/Crc32.Lut.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <content>
/// Contains precalulated tables for scalar calculations.

2
src/ImageSharp/Formats/Png/Zlib/Crc32.cs → src/ImageSharp/Compression/Zlib/Crc32.cs

@ -9,7 +9,7 @@ using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// Calculates the 32 bit Cyclic Redundancy Check (CRC) checksum of a given buffer

81
src/ImageSharp/Compression/Zlib/DeflateCompressionLevel.cs

@ -0,0 +1,81 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// Provides enumeration of available deflate compression levels.
/// </summary>
public enum DeflateCompressionLevel
{
/// <summary>
/// Level 0. Equivalent to <see cref="NoCompression"/>.
/// </summary>
Level0 = 0,
/// <summary>
/// No compression. Equivalent to <see cref="Level0"/>.
/// </summary>
NoCompression = Level0,
/// <summary>
/// Level 1. Equivalent to <see cref="BestSpeed"/>.
/// </summary>
Level1 = 1,
/// <summary>
/// Best speed compression level.
/// </summary>
BestSpeed = Level1,
/// <summary>
/// Level 2.
/// </summary>
Level2 = 2,
/// <summary>
/// Level 3.
/// </summary>
Level3 = 3,
/// <summary>
/// Level 4.
/// </summary>
Level4 = 4,
/// <summary>
/// Level 5.
/// </summary>
Level5 = 5,
/// <summary>
/// Level 6. Equivalent to <see cref="DefaultCompression"/>.
/// </summary>
Level6 = 6,
/// <summary>
/// The default compression level. Equivalent to <see cref="Level6"/>.
/// </summary>
DefaultCompression = Level6,
/// <summary>
/// Level 7.
/// </summary>
Level7 = 7,
/// <summary>
/// Level 8.
/// </summary>
Level8 = 8,
/// <summary>
/// Level 9. Equivalent to <see cref="BestCompression"/>.
/// </summary>
Level9 = 9,
/// <summary>
/// Best compression level. Equivalent to <see cref="Level9"/>.
/// </summary>
BestCompression = Level9,
}
}

2
src/ImageSharp/Formats/Png/Zlib/DeflateThrowHelper.cs → src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs

@ -4,7 +4,7 @@
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
internal static class DeflateThrowHelper
{

2
src/ImageSharp/Formats/Png/Zlib/Deflater.cs → src/ImageSharp/Compression/Zlib/Deflater.cs

@ -5,7 +5,7 @@ using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// This class compresses input with the deflate algorithm described in RFC 1951.

2
src/ImageSharp/Formats/Png/Zlib/DeflaterConstants.cs → src/ImageSharp/Compression/Zlib/DeflaterConstants.cs

@ -4,7 +4,7 @@
// <auto-generated/>
using System;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// This class contains constants used for deflation.

2
src/ImageSharp/Formats/Png/Zlib/DeflaterEngine.cs → src/ImageSharp/Compression/Zlib/DeflaterEngine.cs

@ -6,7 +6,7 @@ using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// Strategies for deflater

2
src/ImageSharp/Formats/Png/Zlib/DeflaterHuffman.cs → src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs

@ -7,7 +7,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// Performs Deflate Huffman encoding.

2
src/ImageSharp/Formats/Png/Zlib/DeflaterOutputStream.cs → src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs

@ -5,7 +5,7 @@ using System;
using System.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// A special stream deflating or compressing the bytes that are

2
src/ImageSharp/Formats/Png/Zlib/DeflaterPendingBuffer.cs → src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs

@ -6,7 +6,7 @@ using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// Stores pending data for writing data to the Deflater.

0
src/ImageSharp/Formats/Png/Zlib/README.md → src/ImageSharp/Compression/Zlib/README.md

15
src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs → src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs

@ -4,9 +4,10 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// Provides methods and properties for compressing streams by using the Zlib Deflate algorithm.
@ -39,9 +40,19 @@ namespace SixLabors.ImageSharp.Formats.Png.Zlib
/// <summary>
/// The stream responsible for compressing the input stream.
/// </summary>
// private DeflateStream deflateStream;
private DeflaterOutputStream deflateStream;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibDeflateStream"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
/// <param name="stream">The stream to compress.</param>
/// <param name="level">The compression level.</param>
public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, DeflateCompressionLevel level)
: this(memoryAllocator, stream, (PngCompressionLevel)level)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZlibDeflateStream"/> class.
/// </summary>

2
src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs → src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs

@ -6,7 +6,7 @@ using System.IO;
using System.IO.Compression;
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Formats.Png.Zlib
namespace SixLabors.ImageSharp.Compression.Zlib
{
/// <summary>
/// Provides methods and properties for deframing streams from PNGs.

0
src/ImageSharp/Formats/Png/Zlib/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf → src/ImageSharp/Compression/Zlib/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf

13
src/ImageSharp/Configuration.cs

@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Processing;
@ -39,7 +40,7 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Initializes a new instance of the <see cref="Configuration" /> class.
/// </summary>
/// <param name="configurationModules">A collection of configuration modules to register</param>
/// <param name="configurationModules">A collection of configuration modules to register.</param>
public Configuration(params IConfigurationModule[] configurationModules)
{
if (configurationModules != null)
@ -77,7 +78,7 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Gets or sets the size of the buffer to use when working with streams.
/// Intitialized with <see cref="DefaultStreamProcessingBufferSize"/> by default.
/// Initialized with <see cref="DefaultStreamProcessingBufferSize"/> by default.
/// </summary>
public int StreamProcessingBufferSize
{
@ -94,9 +95,9 @@ namespace SixLabors.ImageSharp
}
/// <summary>
/// Gets a set of properties for the Congiguration.
/// Gets a set of properties for the Configuration.
/// </summary>
/// <remarks>This can be used for storing global settings and defaults to be accessable to processors.</remarks>
/// <remarks>This can be used for storing global settings and defaults to be accessible to processors.</remarks>
public IDictionary<object, object> Properties { get; } = new ConcurrentDictionary<object, object>();
/// <summary>
@ -180,6 +181,7 @@ namespace SixLabors.ImageSharp
/// <see cref="GifConfigurationModule"/>
/// <see cref="BmpConfigurationModule"/>.
/// <see cref="TgaConfigurationModule"/>.
/// <see cref="TiffConfigurationModule"/>.
/// </summary>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns>
internal static Configuration CreateDefaultInstance()
@ -189,7 +191,8 @@ namespace SixLabors.ImageSharp
new JpegConfigurationModule(),
new GifConfigurationModule(),
new BmpConfigurationModule(),
new TgaConfigurationModule());
new TgaConfigurationModule(),
new TiffConfigurationModule());
}
}
}

106
src/ImageSharp/Formats/ImageExtensions.Save.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
// <auto-generated />
@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
namespace SixLabors.ImageSharp
{
@ -535,5 +536,108 @@ namespace SixLabors.ImageSharp
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TgaFormat.Instance),
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Tiff format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsTiff(this Image source, string path) => SaveAsTiff(source, path, null);
/// <summary>
/// Saves the image to the given stream with the Tiff format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTiffAsync(this Image source, string path) => SaveAsTiffAsync(source, path, null);
/// <summary>
/// Saves the image to the given stream with the Tiff format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTiffAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsTiffAsync(source, path, null, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Tiff format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsTiff(this Image source, string path, TiffEncoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Tiff format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTiffAsync(this Image source, string path, TiffEncoder encoder, CancellationToken cancellationToken = default) =>
source.SaveAsync(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance),
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Tiff format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsTiff(this Image source, Stream stream)
=> SaveAsTiff(source, stream, null);
/// <summary>
/// Saves the image to the given stream with the Tiff format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTiffAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsTiffAsync(source, stream, null, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Tiff format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder)
=> source.Save(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Tiff format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder encoder, CancellationToken cancellationToken = default) =>
source.SaveAsync(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance),
cancellationToken);
}
}

3
src/ImageSharp/Formats/ImageExtensions.Save.tt

@ -1,4 +1,4 @@
<#@ template language="C#" #>
<#@ template language="C#" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
// Copyright (c) Six Labors.
@ -17,6 +17,7 @@ using SixLabors.ImageSharp.Advanced;
"Jpeg",
"Png",
"Tga",
"Tiff",
};
foreach (string fmt in formats)

3
src/ImageSharp/Formats/Png/PngCompressionLevel.cs

@ -1,11 +1,14 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.ComponentModel;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Provides enumeration of available PNG compression levels.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public enum PngCompressionLevel
{
/// <summary>

3
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -10,10 +10,9 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Png.Zlib;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;

4
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -8,11 +8,9 @@ using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Png.Zlib;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;

2
src/ImageSharp/Formats/README.md → src/ImageSharp/Formats/Tga/README.md

@ -1,4 +1,4 @@
# Encoder/Decoder for true vision targa files
# Encoder/Decoder for true vision targa files
Useful links for reference:

51
src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs

@ -0,0 +1,51 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression
{
internal static class BitWriterUtils
{
public static void WriteBits(Span<byte> buffer, int pos, uint count, byte value)
{
int bitPos = pos % 8;
int bufferPos = pos / 8;
int startIdx = bufferPos + bitPos;
int endIdx = (int)(startIdx + count);
if (value == 1)
{
for (int i = startIdx; i < endIdx; i++)
{
WriteBit(buffer, bufferPos, bitPos);
bitPos++;
if (bitPos >= 8)
{
bitPos = 0;
bufferPos++;
}
}
}
else
{
for (int i = startIdx; i < endIdx; i++)
{
WriteZeroBit(buffer, bufferPos, bitPos);
bitPos++;
if (bitPos >= 8)
{
bitPos = 0;
bufferPos++;
}
}
}
}
public static void WriteBit(Span<byte> buffer, int bufferPos, int bitPos) => buffer[bufferPos] |= (byte)(1 << (7 - bitPos));
public static void WriteZeroBit(Span<byte> buffer, int bufferPos, int bitPos) => buffer[bufferPos] = (byte)(buffer[bufferPos] & ~(1 << (7 - bitPos)));
}
}

62
src/ImageSharp/Formats/Tiff/Compression/Compressors/DeflateCompressor.cs

@ -0,0 +1,62 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
{
internal sealed class DeflateCompressor : TiffBaseCompressor
{
private readonly DeflateCompressionLevel compressionLevel;
private readonly MemoryStream memoryStream = new MemoryStream();
public DeflateCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor, DeflateCompressionLevel compressionLevel)
: base(output, allocator, width, bitsPerPixel, predictor)
=> this.compressionLevel = compressionLevel;
/// <inheritdoc/>
public override TiffCompression Method => TiffCompression.Deflate;
/// <inheritdoc/>
public override void Initialize(int rowsPerStrip)
{
}
/// <inheritdoc/>
public override void CompressStrip(Span<byte> rows, int height)
{
this.memoryStream.Seek(0, SeekOrigin.Begin);
using (var stream = new ZlibDeflateStream(this.Allocator, this.memoryStream, this.compressionLevel))
{
if (this.Predictor == TiffPredictor.Horizontal)
{
HorizontalPredictor.ApplyHorizontalPrediction(rows, this.BytesPerRow, this.BitsPerPixel);
}
stream.Write(rows);
stream.Flush();
}
int size = (int)this.memoryStream.Position;
#if !NETSTANDARD1_3
byte[] buffer = this.memoryStream.GetBuffer();
this.Output.Write(buffer, 0, size);
#else
this.memoryStream.SetLength(size);
this.memoryStream.Position = 0;
this.memoryStream.CopyTo(this.Output);
#endif
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
}

40
src/ImageSharp/Formats/Tiff/Compression/Compressors/LzwCompressor.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
{
internal sealed class LzwCompressor : TiffBaseCompressor
{
private TiffLzwEncoder lzwEncoder;
public LzwCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor)
: base(output, allocator, width, bitsPerPixel, predictor)
{
}
/// <inheritdoc/>
public override TiffCompression Method => TiffCompression.Lzw;
/// <inheritdoc/>
public override void Initialize(int rowsPerStrip) => this.lzwEncoder = new TiffLzwEncoder(this.Allocator);
/// <inheritdoc/>
public override void CompressStrip(Span<byte> rows, int height)
{
if (this.Predictor == TiffPredictor.Horizontal)
{
HorizontalPredictor.ApplyHorizontalPrediction(rows, this.BytesPerRow, this.BitsPerPixel);
}
this.lzwEncoder.Encode(rows, this.Output);
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.lzwEncoder?.Dispose();
}
}

34
src/ImageSharp/Formats/Tiff/Compression/Compressors/NoCompressor.cs

@ -0,0 +1,34 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
{
internal sealed class NoCompressor : TiffBaseCompressor
{
public NoCompressor(Stream output, MemoryAllocator memoryAllocator, int width, int bitsPerPixel)
: base(output, memoryAllocator, width, bitsPerPixel)
{
}
/// <inheritdoc/>
public override TiffCompression Method => TiffCompression.None;
/// <inheritdoc/>
public override void Initialize(int rowsPerStrip)
{
}
/// <inheritdoc/>
public override void CompressStrip(Span<byte> rows, int height) => this.Output.Write(rows);
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
}

48
src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsCompressor.cs

@ -0,0 +1,48 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
{
internal sealed class PackBitsCompressor : TiffBaseCompressor
{
private IManagedByteBuffer pixelData;
public PackBitsCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel)
: base(output, allocator, width, bitsPerPixel)
{
}
/// <inheritdoc/>
public override TiffCompression Method => TiffCompression.PackBits;
/// <inheritdoc/>
public override void Initialize(int rowsPerStrip)
{
int additionalBytes = ((this.BytesPerRow + 126) / 127) + 1;
this.pixelData = this.Allocator.AllocateManagedByteBuffer(this.BytesPerRow + additionalBytes);
}
/// <inheritdoc/>
public override void CompressStrip(Span<byte> rows, int height)
{
DebugGuard.IsTrue(rows.Length % height == 0, "Invalid height");
DebugGuard.IsTrue(this.BytesPerRow == rows.Length / height, "The widths must match");
Span<byte> span = this.pixelData.GetSpan();
for (int i = 0; i < height; i++)
{
Span<byte> row = rows.Slice(i * this.BytesPerRow, this.BytesPerRow);
int size = PackBitsWriter.PackBits(row, span);
this.Output.Write(span.Slice(0, size));
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.pixelData?.Dispose();
}
}

128
src/ImageSharp/Formats/Tiff/Compression/Compressors/PackBitsWriter.cs

@ -0,0 +1,128 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
{
/// <summary>
/// Pack Bits compression for tiff images. See Tiff Spec v6, section 9.
/// </summary>
internal static class PackBitsWriter
{
public static int PackBits(ReadOnlySpan<byte> rowSpan, Span<byte> compressedRowSpan)
{
int maxRunLength = 127;
int posInRowSpan = 0;
int bytesWritten = 0;
int literalRunLength = 0;
while (posInRowSpan < rowSpan.Length)
{
bool useReplicateRun = IsReplicateRun(rowSpan, posInRowSpan);
if (useReplicateRun)
{
if (literalRunLength > 0)
{
WriteLiteralRun(rowSpan, posInRowSpan, literalRunLength, compressedRowSpan, bytesWritten);
bytesWritten += literalRunLength + 1;
}
// Write a run with the same bytes.
int runLength = FindRunLength(rowSpan, posInRowSpan, maxRunLength);
WriteRun(rowSpan, posInRowSpan, runLength, compressedRowSpan, bytesWritten);
bytesWritten += 2;
literalRunLength = 0;
posInRowSpan += runLength;
continue;
}
literalRunLength++;
posInRowSpan++;
if (literalRunLength >= maxRunLength)
{
WriteLiteralRun(rowSpan, posInRowSpan, literalRunLength, compressedRowSpan, bytesWritten);
bytesWritten += literalRunLength + 1;
literalRunLength = 0;
}
}
if (literalRunLength > 0)
{
WriteLiteralRun(rowSpan, posInRowSpan, literalRunLength, compressedRowSpan, bytesWritten);
bytesWritten += literalRunLength + 1;
}
return bytesWritten;
}
private static void WriteLiteralRun(ReadOnlySpan<byte> rowSpan, int end, int literalRunLength, Span<byte> compressedRowSpan, int compressedRowPos)
{
DebugGuard.MustBeLessThanOrEqualTo(literalRunLength, 127, nameof(literalRunLength));
int literalRunStart = end - literalRunLength;
sbyte runLength = (sbyte)(literalRunLength - 1);
compressedRowSpan[compressedRowPos] = (byte)runLength;
rowSpan.Slice(literalRunStart, literalRunLength).CopyTo(compressedRowSpan.Slice(compressedRowPos + 1));
}
private static void WriteRun(ReadOnlySpan<byte> rowSpan, int start, int runLength, Span<byte> compressedRowSpan, int compressedRowPos)
{
DebugGuard.MustBeLessThanOrEqualTo(runLength, 127, nameof(runLength));
sbyte headerByte = (sbyte)(-runLength + 1);
compressedRowSpan[compressedRowPos] = (byte)headerByte;
compressedRowSpan[compressedRowPos + 1] = rowSpan[start];
}
private static bool IsReplicateRun(ReadOnlySpan<byte> rowSpan, int startPos)
{
// We consider run which has at least 3 same consecutive bytes a candidate for a run.
var startByte = rowSpan[startPos];
int count = 0;
for (int i = startPos + 1; i < rowSpan.Length; i++)
{
if (rowSpan[i] == startByte)
{
count++;
if (count >= 2)
{
return true;
}
}
else
{
break;
}
}
return false;
}
private static int FindRunLength(ReadOnlySpan<byte> rowSpan, int startPos, int maxRunLength)
{
var startByte = rowSpan[startPos];
int count = 1;
for (int i = startPos + 1; i < rowSpan.Length; i++)
{
if (rowSpan[i] == startByte)
{
count++;
}
else
{
break;
}
if (count == maxRunLength)
{
break;
}
}
return count;
}
}
}

592
src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs

@ -0,0 +1,592 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
{
/// <summary>
/// Bitwriter for writing compressed CCITT T4 1D data.
/// </summary>
internal sealed class T4BitCompressor : TiffBaseCompressor
{
private const uint WhiteZeroRunTermCode = 0x35;
private const uint BlackZeroRunTermCode = 0x37;
private static readonly uint[] MakeupRunLength =
{
64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792, 1856, 1920, 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560
};
private static readonly Dictionary<uint, uint> WhiteLen4TermCodes = new Dictionary<uint, uint>()
{
{ 2, 0x7 }, { 3, 0x8 }, { 4, 0xB }, { 5, 0xC }, { 6, 0xE }, { 7, 0xF }
};
private static readonly Dictionary<uint, uint> WhiteLen5TermCodes = new Dictionary<uint, uint>()
{
{ 8, 0x13 }, { 9, 0x14 }, { 10, 0x7 }, { 11, 0x8 }
};
private static readonly Dictionary<uint, uint> WhiteLen6TermCodes = new Dictionary<uint, uint>()
{
{ 1, 0x7 }, { 12, 0x8 }, { 13, 0x3 }, { 14, 0x34 }, { 15, 0x35 }, { 16, 0x2A }, { 17, 0x2B }
};
private static readonly Dictionary<uint, uint> WhiteLen7TermCodes = new Dictionary<uint, uint>()
{
{ 18, 0x27 }, { 19, 0xC }, { 20, 0x8 }, { 21, 0x17 }, { 22, 0x3 }, { 23, 0x4 }, { 24, 0x28 }, { 25, 0x2B }, { 26, 0x13 },
{ 27, 0x24 }, { 28, 0x18 }
};
private static readonly Dictionary<uint, uint> WhiteLen8TermCodes = new Dictionary<uint, uint>()
{
{ 0, WhiteZeroRunTermCode }, { 29, 0x2 }, { 30, 0x3 }, { 31, 0x1A }, { 32, 0x1B }, { 33, 0x12 }, { 34, 0x13 }, { 35, 0x14 },
{ 36, 0x15 }, { 37, 0x16 }, { 38, 0x17 }, { 39, 0x28 }, { 40, 0x29 }, { 41, 0x2A }, { 42, 0x2B }, { 43, 0x2C }, { 44, 0x2D },
{ 45, 0x4 }, { 46, 0x5 }, { 47, 0xA }, { 48, 0xB }, { 49, 0x52 }, { 50, 0x53 }, { 51, 0x54 }, { 52, 0x55 }, { 53, 0x24 },
{ 54, 0x25 }, { 55, 0x58 }, { 56, 0x59 }, { 57, 0x5A }, { 58, 0x5B }, { 59, 0x4A }, { 60, 0x4B }, { 61, 0x32 }, { 62, 0x33 },
{ 63, 0x34 }
};
private static readonly Dictionary<uint, uint> BlackLen2TermCodes = new Dictionary<uint, uint>()
{
{ 2, 0x3 }, { 3, 0x2 }
};
private static readonly Dictionary<uint, uint> BlackLen3TermCodes = new Dictionary<uint, uint>()
{
{ 1, 0x2 }, { 4, 0x3 }
};
private static readonly Dictionary<uint, uint> BlackLen4TermCodes = new Dictionary<uint, uint>()
{
{ 5, 0x3 }, { 6, 0x2 }
};
private static readonly Dictionary<uint, uint> BlackLen5TermCodes = new Dictionary<uint, uint>()
{
{ 7, 0x3 }
};
private static readonly Dictionary<uint, uint> BlackLen6TermCodes = new Dictionary<uint, uint>()
{
{ 8, 0x5 }, { 9, 0x4 }
};
private static readonly Dictionary<uint, uint> BlackLen7TermCodes = new Dictionary<uint, uint>()
{
{ 10, 0x4 }, { 11, 0x5 }, { 12, 0x7 }
};
private static readonly Dictionary<uint, uint> BlackLen8TermCodes = new Dictionary<uint, uint>()
{
{ 13, 0x4 }, { 14, 0x7 }
};
private static readonly Dictionary<uint, uint> BlackLen9TermCodes = new Dictionary<uint, uint>()
{
{ 15, 0x18 }
};
private static readonly Dictionary<uint, uint> BlackLen10TermCodes = new Dictionary<uint, uint>()
{
{ 0, BlackZeroRunTermCode }, { 16, 0x17 }, { 17, 0x18 }, { 18, 0x8 }
};
private static readonly Dictionary<uint, uint> BlackLen11TermCodes = new Dictionary<uint, uint>()
{
{ 19, 0x67 }, { 20, 0x68 }, { 21, 0x6C }, { 22, 0x37 }, { 23, 0x28 }, { 24, 0x17 }, { 25, 0x18 }
};
private static readonly Dictionary<uint, uint> BlackLen12TermCodes = new Dictionary<uint, uint>()
{
{ 26, 0xCA }, { 27, 0xCB }, { 28, 0xCC }, { 29, 0xCD }, { 30, 0x68 }, { 31, 0x69 }, { 32, 0x6A }, { 33, 0x6B }, { 34, 0xD2 },
{ 35, 0xD3 }, { 36, 0xD4 }, { 37, 0xD5 }, { 38, 0xD6 }, { 39, 0xD7 }, { 40, 0x6C }, { 41, 0x6D }, { 42, 0xDA }, { 43, 0xDB },
{ 44, 0x54 }, { 45, 0x55 }, { 46, 0x56 }, { 47, 0x57 }, { 48, 0x64 }, { 49, 0x65 }, { 50, 0x52 }, { 51, 0x53 }, { 52, 0x24 },
{ 53, 0x37 }, { 54, 0x38 }, { 55, 0x27 }, { 56, 0x28 }, { 57, 0x58 }, { 58, 0x59 }, { 59, 0x2B }, { 60, 0x2C }, { 61, 0x5A },
{ 62, 0x66 }, { 63, 0x67 }
};
private static readonly Dictionary<uint, uint> WhiteLen5MakeupCodes = new Dictionary<uint, uint>()
{
{ 64, 0x1B }, { 128, 0x12 }
};
private static readonly Dictionary<uint, uint> WhiteLen6MakeupCodes = new Dictionary<uint, uint>()
{
{ 192, 0x17 }, { 1664, 0x18 }
};
private static readonly Dictionary<uint, uint> WhiteLen8MakeupCodes = new Dictionary<uint, uint>()
{
{ 320, 0x36 }, { 384, 0x37 }, { 448, 0x64 }, { 512, 0x65 }, { 576, 0x68 }, { 640, 0x67 }
};
private static readonly Dictionary<uint, uint> WhiteLen7MakeupCodes = new Dictionary<uint, uint>()
{
{ 256, 0x37 }
};
private static readonly Dictionary<uint, uint> WhiteLen9MakeupCodes = new Dictionary<uint, uint>()
{
{ 704, 0xCC }, { 768, 0xCD }, { 832, 0xD2 }, { 896, 0xD3 }, { 960, 0xD4 }, { 1024, 0xD5 }, { 1088, 0xD6 },
{ 1152, 0xD7 }, { 1216, 0xD8 }, { 1280, 0xD9 }, { 1344, 0xDA }, { 1408, 0xDB }, { 1472, 0x98 }, { 1536, 0x99 },
{ 1600, 0x9A }, { 1728, 0x9B }
};
private static readonly Dictionary<uint, uint> WhiteLen11MakeupCodes = new Dictionary<uint, uint>()
{
{ 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD }
};
private static readonly Dictionary<uint, uint> WhiteLen12MakeupCodes = new Dictionary<uint, uint>()
{
{ 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C },
{ 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F }
};
private static readonly Dictionary<uint, uint> BlackLen10MakeupCodes = new Dictionary<uint, uint>()
{
{ 64, 0xF }
};
private static readonly Dictionary<uint, uint> BlackLen11MakeupCodes = new Dictionary<uint, uint>()
{
{ 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD }
};
private static readonly Dictionary<uint, uint> BlackLen12MakeupCodes = new Dictionary<uint, uint>()
{
{ 128, 0xC8 }, { 192, 0xC9 }, { 256, 0x5B }, { 320, 0x33 }, { 384, 0x34 }, { 448, 0x35 },
{ 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C },
{ 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F }
};
private static readonly Dictionary<uint, uint> BlackLen13MakeupCodes = new Dictionary<uint, uint>()
{
{ 512, 0x6C }, { 576, 0x6D }, { 640, 0x4A }, { 704, 0x4B }, { 768, 0x4C }, { 832, 0x4D }, { 896, 0x72 },
{ 960, 0x73 }, { 1024, 0x74 }, { 1088, 0x75 }, { 1152, 0x76 }, { 1216, 0x77 }, { 1280, 0x52 }, { 1344, 0x53 },
{ 1408, 0x54 }, { 1472, 0x55 }, { 1536, 0x5A }, { 1600, 0x5B }, { 1664, 0x64 }, { 1728, 0x65 }
};
/// <summary>
/// The modified huffman is basically the same as CCITT T4, but without EOL markers and padding at the end of the rows.
/// </summary>
private readonly bool useModifiedHuffman;
private IMemoryOwner<byte> compressedDataBuffer;
private int bytePosition;
private byte bitPosition;
/// <summary>
/// Initializes a new instance of the <see cref="T4BitCompressor" /> class.
/// </summary>
/// <param name="output">The output.</param>
/// <param name="allocator">The allocator.</param>
/// <param name="width">The width.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param>
/// <param name="useModifiedHuffman">Indicates if the modified huffman RLE should be used.</param>
public T4BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, bool useModifiedHuffman = false)
: base(output, allocator, width, bitsPerPixel)
{
this.bytePosition = 0;
this.bitPosition = 0;
this.useModifiedHuffman = useModifiedHuffman;
}
/// <inheritdoc/>
public override TiffCompression Method => this.useModifiedHuffman ? TiffCompression.Ccitt1D : TiffCompression.CcittGroup3Fax;
/// <inheritdoc/>
public override void Initialize(int rowsPerStrip)
{
// This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good.
int maxNeededBytes = this.Width * rowsPerStrip;
this.compressedDataBuffer = this.Allocator.Allocate<byte>(maxNeededBytes);
}
/// <summary>Writes a image compressed with CCITT T4 to the stream.</summary>
/// <param name="pixelsAsGray">The pixels as 8-bit gray array.</param>
/// <param name="height">The strip height.</param>
public override void CompressStrip(Span<byte> pixelsAsGray, int height)
{
DebugGuard.IsTrue(pixelsAsGray.Length / height == this.Width, "Values must be equals");
DebugGuard.IsTrue(pixelsAsGray.Length % height == 0, "Values must be equals");
this.compressedDataBuffer.Clear();
Span<byte> compressedData = this.compressedDataBuffer.GetSpan();
this.bytePosition = 0;
this.bitPosition = 0;
if (!this.useModifiedHuffman)
{
// An EOL code is expected at the start of the data.
this.WriteCode(12, 1, compressedData);
}
for (int y = 0; y < height; y++)
{
bool isWhiteRun = true;
bool isStartOrRow = true;
int x = 0;
Span<byte> row = pixelsAsGray.Slice(y * this.Width, this.Width);
while (x < this.Width)
{
uint runLength = 0;
for (int i = x; i < this.Width; i++)
{
if (isWhiteRun && row[i] != 255)
{
break;
}
if (isWhiteRun && row[i] == 255)
{
runLength++;
continue;
}
if (!isWhiteRun && row[i] != 0)
{
break;
}
if (!isWhiteRun && row[i] == 0)
{
runLength++;
}
}
if (isStartOrRow && runLength == 0)
{
this.WriteCode(8, WhiteZeroRunTermCode, compressedData);
isWhiteRun = false;
isStartOrRow = false;
continue;
}
uint code;
uint codeLength;
if (runLength <= 63)
{
code = this.GetTermCode(runLength, out codeLength, isWhiteRun);
this.WriteCode(codeLength, code, compressedData);
x += (int)runLength;
}
else
{
runLength = this.GetBestFittingMakeupRunLength(runLength);
code = this.GetMakeupCode(runLength, out codeLength, isWhiteRun);
this.WriteCode(codeLength, code, compressedData);
x += (int)runLength;
// If we are at the end of the line with a makeup code, we need to write a final term code with a length of zero.
if (x == this.Width)
{
if (isWhiteRun)
{
this.WriteCode(8, WhiteZeroRunTermCode, compressedData);
}
else
{
this.WriteCode(10, BlackZeroRunTermCode, compressedData);
}
}
continue;
}
isStartOrRow = false;
isWhiteRun = !isWhiteRun;
}
this.WriteEndOfLine(compressedData);
}
// Write the compressed data to the stream.
int bytesToWrite = this.bitPosition != 0 ? this.bytePosition + 1 : this.bytePosition;
this.Output.Write(compressedData.Slice(0, bytesToWrite));
}
protected override void Dispose(bool disposing) => this.compressedDataBuffer?.Dispose();
private void WriteEndOfLine(Span<byte> compressedData)
{
if (this.useModifiedHuffman)
{
// Check if padding is necessary.
if (this.bitPosition % 8 != 0)
{
// Skip padding bits, move to next byte.
this.bytePosition++;
this.bitPosition = 0;
}
}
else
{
// Write EOL.
this.WriteCode(12, 1, compressedData);
}
}
private void WriteCode(uint codeLength, uint code, Span<byte> compressedData)
{
while (codeLength > 0)
{
int bitNumber = (int)codeLength;
bool bit = (code & (1 << (bitNumber - 1))) != 0;
if (bit)
{
BitWriterUtils.WriteBit(compressedData, this.bytePosition, this.bitPosition);
}
else
{
BitWriterUtils.WriteZeroBit(compressedData, this.bytePosition, this.bitPosition);
}
this.bitPosition++;
if (this.bitPosition == 8)
{
this.bytePosition++;
this.bitPosition = 0;
}
codeLength--;
}
}
private uint GetBestFittingMakeupRunLength(uint runLength)
{
for (int i = 0; i < MakeupRunLength.Length - 1; i++)
{
if (MakeupRunLength[i] <= runLength && MakeupRunLength[i + 1] > runLength)
{
return MakeupRunLength[i];
}
}
return MakeupRunLength[MakeupRunLength.Length - 1];
}
private uint GetTermCode(uint runLength, out uint codeLength, bool isWhiteRun)
{
if (isWhiteRun)
{
return this.GetWhiteTermCode(runLength, out codeLength);
}
return this.GetBlackTermCode(runLength, out codeLength);
}
private uint GetMakeupCode(uint runLength, out uint codeLength, bool isWhiteRun)
{
if (isWhiteRun)
{
return this.GetWhiteMakeupCode(runLength, out codeLength);
}
return this.GetBlackMakeupCode(runLength, out codeLength);
}
private uint GetWhiteMakeupCode(uint runLength, out uint codeLength)
{
codeLength = 0;
if (WhiteLen5MakeupCodes.ContainsKey(runLength))
{
codeLength = 5;
return WhiteLen5MakeupCodes[runLength];
}
if (WhiteLen6MakeupCodes.ContainsKey(runLength))
{
codeLength = 6;
return WhiteLen6MakeupCodes[runLength];
}
if (WhiteLen7MakeupCodes.ContainsKey(runLength))
{
codeLength = 7;
return WhiteLen7MakeupCodes[runLength];
}
if (WhiteLen8MakeupCodes.ContainsKey(runLength))
{
codeLength = 8;
return WhiteLen8MakeupCodes[runLength];
}
if (WhiteLen9MakeupCodes.ContainsKey(runLength))
{
codeLength = 9;
return WhiteLen9MakeupCodes[runLength];
}
if (WhiteLen11MakeupCodes.ContainsKey(runLength))
{
codeLength = 11;
return WhiteLen11MakeupCodes[runLength];
}
if (WhiteLen12MakeupCodes.ContainsKey(runLength))
{
codeLength = 12;
return WhiteLen12MakeupCodes[runLength];
}
return 0;
}
private uint GetBlackMakeupCode(uint runLength, out uint codeLength)
{
codeLength = 0;
if (BlackLen10MakeupCodes.ContainsKey(runLength))
{
codeLength = 10;
return BlackLen10MakeupCodes[runLength];
}
if (BlackLen11MakeupCodes.ContainsKey(runLength))
{
codeLength = 11;
return BlackLen11MakeupCodes[runLength];
}
if (BlackLen12MakeupCodes.ContainsKey(runLength))
{
codeLength = 12;
return BlackLen12MakeupCodes[runLength];
}
if (BlackLen13MakeupCodes.ContainsKey(runLength))
{
codeLength = 13;
return BlackLen13MakeupCodes[runLength];
}
return 0;
}
private uint GetWhiteTermCode(uint runLength, out uint codeLength)
{
codeLength = 0;
if (WhiteLen4TermCodes.ContainsKey(runLength))
{
codeLength = 4;
return WhiteLen4TermCodes[runLength];
}
if (WhiteLen5TermCodes.ContainsKey(runLength))
{
codeLength = 5;
return WhiteLen5TermCodes[runLength];
}
if (WhiteLen6TermCodes.ContainsKey(runLength))
{
codeLength = 6;
return WhiteLen6TermCodes[runLength];
}
if (WhiteLen7TermCodes.ContainsKey(runLength))
{
codeLength = 7;
return WhiteLen7TermCodes[runLength];
}
if (WhiteLen8TermCodes.ContainsKey(runLength))
{
codeLength = 8;
return WhiteLen8TermCodes[runLength];
}
return 0;
}
private uint GetBlackTermCode(uint runLength, out uint codeLength)
{
codeLength = 0;
if (BlackLen2TermCodes.ContainsKey(runLength))
{
codeLength = 2;
return BlackLen2TermCodes[runLength];
}
if (BlackLen3TermCodes.ContainsKey(runLength))
{
codeLength = 3;
return BlackLen3TermCodes[runLength];
}
if (BlackLen4TermCodes.ContainsKey(runLength))
{
codeLength = 4;
return BlackLen4TermCodes[runLength];
}
if (BlackLen5TermCodes.ContainsKey(runLength))
{
codeLength = 5;
return BlackLen5TermCodes[runLength];
}
if (BlackLen6TermCodes.ContainsKey(runLength))
{
codeLength = 6;
return BlackLen6TermCodes[runLength];
}
if (BlackLen7TermCodes.ContainsKey(runLength))
{
codeLength = 7;
return BlackLen7TermCodes[runLength];
}
if (BlackLen8TermCodes.ContainsKey(runLength))
{
codeLength = 8;
return BlackLen8TermCodes[runLength];
}
if (BlackLen9TermCodes.ContainsKey(runLength))
{
codeLength = 9;
return BlackLen9TermCodes[runLength];
}
if (BlackLen10TermCodes.ContainsKey(runLength))
{
codeLength = 10;
return BlackLen10TermCodes[runLength];
}
if (BlackLen11TermCodes.ContainsKey(runLength))
{
codeLength = 11;
return BlackLen11TermCodes[runLength];
}
if (BlackLen12TermCodes.ContainsKey(runLength))
{
codeLength = 12;
return BlackLen12TermCodes[runLength];
}
return 0;
}
}
}

270
src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffLzwEncoder.cs

@ -0,0 +1,270 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.IO;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
{
/*
This implementation is a port of a java tiff encoder by Harald Kuhr: https://github.com/haraldk/TwelveMonkeys
Original licence:
BSD 3-Clause License
* Copyright (c) 2015, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
** Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/// <summary>
/// Encodes and compresses the image data using dynamic Lempel-Ziv compression.
/// </summary>
/// <remarks>
/// <para>
/// This code is based on the <see cref="LzwEncoder"/> used for GIF encoding. There is potential
/// for a shared implementation. Differences between the GIF and TIFF implementations of the LZW
/// encoding are: (i) The GIF implementation includes an initial 'data size' byte, whilst this is
/// always 8 for TIFF. (ii) The GIF implementation writes a number of sub-blocks with an initial
/// byte indicating the length of the sub-block. In TIFF the data is written as a single block
/// with no length indicator (this can be determined from the 'StripByteCounts' entry).
/// </para>
/// </remarks>
internal sealed class TiffLzwEncoder : IDisposable
{
// Clear: Re-initialize tables.
private static readonly int ClearCode = 256;
// End of Information.
private static readonly int EoiCode = 257;
private static readonly int MinBits = 9;
private static readonly int MaxBits = 12;
private static readonly int TableSize = 1 << MaxBits;
// A child is made up of a parent (or prefix) code plus a suffix byte
// and siblings are strings with a common parent(or prefix) and different suffix bytes.
private readonly IMemoryOwner<int> children;
private readonly IMemoryOwner<int> siblings;
private readonly IMemoryOwner<int> suffixes;
// Initial setup
private int parent;
private int bitsPerCode;
private int nextValidCode;
private int maxCode;
// Buffer for partial codes
private int bits;
private int bitPos;
private int bufferPosition;
/// <summary>
/// Initializes a new instance of the <see cref="TiffLzwEncoder"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
public TiffLzwEncoder(MemoryAllocator memoryAllocator)
{
this.children = memoryAllocator.Allocate<int>(TableSize);
this.siblings = memoryAllocator.Allocate<int>(TableSize);
this.suffixes = memoryAllocator.Allocate<int>(TableSize);
}
/// <summary>
/// Encodes and compresses the indexed pixels to the stream.
/// </summary>
/// <param name="data">The data to compress.</param>
/// <param name="stream">The stream to write to.</param>
public void Encode(Span<byte> data, Stream stream)
{
this.Reset();
Span<int> childrenSpan = this.children.GetSpan();
Span<int> suffixesSpan = this.suffixes.GetSpan();
Span<int> siblingsSpan = this.siblings.GetSpan();
int length = data.Length;
if (length == 0)
{
return;
}
if (this.parent == -1)
{
// Init stream.
this.WriteCode(stream, ClearCode);
this.parent = this.ReadNextByte(data);
}
while (this.bufferPosition < data.Length)
{
int value = this.ReadNextByte(data);
int child = childrenSpan[this.parent];
if (child > 0)
{
if (suffixesSpan[child] == value)
{
this.parent = child;
}
else
{
int sibling = child;
while (true)
{
if (siblingsSpan[sibling] > 0)
{
sibling = siblingsSpan[sibling];
if (suffixesSpan[sibling] == value)
{
this.parent = sibling;
break;
}
}
else
{
siblingsSpan[sibling] = (short)this.nextValidCode;
suffixesSpan[this.nextValidCode] = (short)value;
this.WriteCode(stream, this.parent);
this.parent = value;
this.nextValidCode++;
this.IncreaseCodeSizeOrResetIfNeeded(stream);
break;
}
}
}
}
else
{
childrenSpan[this.parent] = (short)this.nextValidCode;
suffixesSpan[this.nextValidCode] = (short)value;
this.WriteCode(stream, this.parent);
this.parent = value;
this.nextValidCode++;
this.IncreaseCodeSizeOrResetIfNeeded(stream);
}
}
// Write EOI when we are done.
this.WriteCode(stream, this.parent);
this.WriteCode(stream, EoiCode);
// Flush partial codes by writing 0 pad.
if (this.bitPos > 0)
{
this.WriteCode(stream, 0);
}
}
/// <inheritdoc />
public void Dispose()
{
this.children.Dispose();
this.siblings.Dispose();
this.suffixes.Dispose();
}
private void Reset()
{
this.children.Clear();
this.siblings.Clear();
this.suffixes.Clear();
this.parent = -1;
this.bitsPerCode = MinBits;
this.nextValidCode = EoiCode + 1;
this.maxCode = (1 << this.bitsPerCode) - 1;
this.bits = 0;
this.bitPos = 0;
this.bufferPosition = 0;
}
private byte ReadNextByte(Span<byte> data) => data[this.bufferPosition++];
private void IncreaseCodeSizeOrResetIfNeeded(Stream stream)
{
if (this.nextValidCode > this.maxCode)
{
if (this.bitsPerCode == MaxBits)
{
// Reset stream by writing Clear code.
this.WriteCode(stream, ClearCode);
// Reset tables.
this.ResetTables();
}
else
{
// Increase code size.
this.bitsPerCode++;
this.maxCode = MaxValue(this.bitsPerCode);
}
}
}
private void WriteCode(Stream stream, int code)
{
this.bits = (this.bits << this.bitsPerCode) | (code & this.maxCode);
this.bitPos += this.bitsPerCode;
while (this.bitPos >= 8)
{
int b = (this.bits >> (this.bitPos - 8)) & 0xff;
stream.WriteByte((byte)b);
this.bitPos -= 8;
}
this.bits &= BitmaskFor(this.bitPos);
}
private void ResetTables()
{
this.children.GetSpan().Fill(0);
this.siblings.GetSpan().Fill(0);
this.bitsPerCode = MinBits;
this.maxCode = MaxValue(this.bitsPerCode);
this.nextValidCode = EoiCode + 1;
}
private static int MaxValue(int codeLen) => (1 << codeLen) - 1;
private static int BitmaskFor(int bits) => MaxValue(bits);
}
}

63
src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs

@ -0,0 +1,63 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO.Compression;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Class to handle cases where TIFF image data is compressed using Deflate compression.
/// </summary>
/// <remarks>
/// Note that the 'OldDeflate' compression type is identical to the 'Deflate' compression type.
/// </remarks>
internal class DeflateTiffCompression : TiffBaseDecompressor
{
/// <summary>
/// Initializes a new instance of the <see cref="DeflateTiffCompression" /> class.
/// </summary>
/// <param name="memoryAllocator">The memoryAllocator to use for buffer allocations.</param>
/// <param name="width">The image width.</param>
/// <param name="bitsPerPixel">The bits used per pixel.</param>
/// <param name="predictor">The tiff predictor used.</param>
public DeflateTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor)
: base(memoryAllocator, width, bitsPerPixel, predictor)
{
}
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer)
{
long pos = stream.Position;
using (var deframeStream = new ZlibInflateStream(
stream,
() =>
{
int left = (int)(byteCount - (stream.Position - pos));
return left > 0 ? left : 0;
}))
{
deframeStream.AllocateNewBytes(byteCount, true);
DeflateStream dataStream = deframeStream.CompressedStream;
dataStream.Read(buffer, 0, buffer.Length);
}
if (this.Predictor == TiffPredictor.Horizontal)
{
HorizontalPredictor.Undo(buffer, this.Width, this.BitsPerPixel);
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
}

95
src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwString.cs

@ -0,0 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Represents a lzw string with a code word and a code length.
/// </summary>
public class LzwString
{
private static readonly LzwString Empty = new LzwString(0, 0, 0, null);
private readonly LzwString previous;
private readonly byte value;
/// <summary>
/// Initializes a new instance of the <see cref="LzwString"/> class.
/// </summary>
/// <param name="code">The code word.</param>
public LzwString(byte code)
: this(code, code, 1, null)
{
}
private LzwString(byte value, byte firstChar, int length, LzwString previous)
{
this.value = value;
this.FirstChar = firstChar;
this.Length = length;
this.previous = previous;
}
/// <summary>
/// Gets the code length;
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the first character of the codeword.
/// </summary>
public byte FirstChar { get; }
/// <summary>
/// Concatenates two code words.
/// </summary>
/// <param name="other">The code word to concatenate.</param>
/// <returns>A concatenated lzw string.</returns>
public LzwString Concatenate(byte other)
{
if (this == Empty)
{
return new LzwString(other);
}
return new LzwString(other, this.FirstChar, this.Length + 1, this);
}
/// <summary>
/// Writes decoded pixel to buffer at a given position.
/// </summary>
/// <param name="buffer">The buffer to write to.</param>
/// <param name="offset">The position to write to.</param>
/// <returns>The number of bytes written.</returns>
public int WriteTo(Span<byte> buffer, int offset)
{
if (this.Length == 0)
{
return 0;
}
if (this.Length == 1)
{
buffer[offset] = this.value;
return 1;
}
LzwString e = this;
int endIdx = this.Length - 1;
if (endIdx >= buffer.Length)
{
TiffThrowHelper.ThrowImageFormatException("Error reading lzw compressed stream. Either pixel buffer to write to is to small or code length is invalid!");
}
for (int i = endIdx; i >= 0; i--)
{
buffer[offset + i] = e.value;
e = e.previous;
}
return this.Length;
}
}
}

45
src/ImageSharp/Formats/Tiff/Compression/Decompressors/LzwTiffCompression.cs

@ -0,0 +1,45 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Class to handle cases where TIFF image data is compressed using LZW compression.
/// </summary>
internal class LzwTiffCompression : TiffBaseDecompressor
{
/// <summary>
/// Initializes a new instance of the <see cref="LzwTiffCompression" /> class.
/// </summary>
/// <param name="memoryAllocator">The memoryAllocator to use for buffer allocations.</param>
/// <param name="width">The image width.</param>
/// <param name="bitsPerPixel">The bits used per pixel.</param>
/// <param name="predictor">The tiff predictor used.</param>
public LzwTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor)
: base(memoryAllocator, width, bitsPerPixel, predictor)
{
}
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer)
{
var decoder = new TiffLzwDecoder(stream);
decoder.DecodePixels(buffer);
if (this.Predictor == TiffPredictor.Horizontal)
{
HorizontalPredictor.Undo(buffer, this.Width, this.BitsPerPixel);
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
}

79
src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs

@ -0,0 +1,79 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Class to handle cases where TIFF image data is compressed using Modified Huffman Compression.
/// </summary>
internal class ModifiedHuffmanTiffCompression : T4TiffCompression
{
private readonly byte whiteValue;
private readonly byte blackValue;
/// <summary>
/// Initializes a new instance of the <see cref="ModifiedHuffmanTiffCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="width">The image width.</param>
/// <param name="bitsPerPixel">The number of bits per pixel.</param>
/// <param name="photometricInterpretation">The photometric interpretation.</param>
public ModifiedHuffmanTiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPhotometricInterpretation photometricInterpretation)
: base(allocator, width, bitsPerPixel, FaxCompressionOptions.None, photometricInterpretation)
{
bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero;
this.whiteValue = (byte)(isWhiteZero ? 0 : 1);
this.blackValue = (byte)(isWhiteZero ? 1 : 0);
}
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer)
{
using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, eolPadding: false, isModifiedHuffman: true);
buffer.Clear();
uint bitsWritten = 0;
uint pixelsWritten = 0;
while (bitReader.HasMoreData)
{
bitReader.ReadNextRun();
if (bitReader.RunLength > 0)
{
if (bitReader.IsWhiteRun)
{
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue);
bitsWritten += bitReader.RunLength;
pixelsWritten += bitReader.RunLength;
}
else
{
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue);
bitsWritten += bitReader.RunLength;
pixelsWritten += bitReader.RunLength;
}
}
if (pixelsWritten % this.Width == 0)
{
bitReader.StartNewRow();
// Write padding bits, if necessary.
uint pad = 8 - (bitsWritten % 8);
if (pad != 8)
{
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, pad, 0);
bitsWritten += pad;
}
}
}
}
}
}

35
src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs

@ -0,0 +1,35 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Class to handle cases where TIFF image data is not compressed.
/// </summary>
internal class NoneTiffCompression : TiffBaseDecompressor
{
/// <summary>
/// Initializes a new instance of the <see cref="NoneTiffCompression" /> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="width">The width of the image.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param>
public NoneTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel)
: base(memoryAllocator, width, bitsPerPixel)
{
}
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer) => _ = stream.Read(buffer, 0, Math.Min(buffer.Length, byteCount));
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
}

96
src/ImageSharp/Formats/Tiff/Compression/Decompressors/PackBitsTiffCompression.cs

@ -0,0 +1,96 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Class to handle cases where TIFF image data is compressed using PackBits compression.
/// </summary>
internal class PackBitsTiffCompression : TiffBaseDecompressor
{
private IMemoryOwner<byte> compressedDataMemory;
/// <summary>
/// Initializes a new instance of the <see cref="PackBitsTiffCompression" /> class.
/// </summary>
/// <param name="memoryAllocator">The memoryAllocator to use for buffer allocations.</param>
/// <param name="width">The width of the image.</param>
/// <param name="bitsPerPixel">The number of bits per pixel.</param>
public PackBitsTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel)
: base(memoryAllocator, width, bitsPerPixel)
{
}
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer)
{
if (this.compressedDataMemory == null)
{
this.compressedDataMemory = this.Allocator.Allocate<byte>(byteCount);
}
else if (this.compressedDataMemory.Length() < byteCount)
{
this.compressedDataMemory.Dispose();
this.compressedDataMemory = this.Allocator.Allocate<byte>(byteCount);
}
Span<byte> compressedData = this.compressedDataMemory.GetSpan();
stream.Read(compressedData, 0, byteCount);
int compressedOffset = 0;
int decompressedOffset = 0;
while (compressedOffset < byteCount)
{
byte headerByte = compressedData[compressedOffset];
if (headerByte <= 127)
{
int literalOffset = compressedOffset + 1;
int literalLength = compressedData[compressedOffset] + 1;
if ((literalOffset + literalLength) > compressedData.Length)
{
TiffThrowHelper.ThrowImageFormatException("Tiff packbits compression error: not enough data.");
}
compressedData.Slice(literalOffset, literalLength).CopyTo(buffer.Slice(decompressedOffset));
compressedOffset += literalLength + 1;
decompressedOffset += literalLength;
}
else if (headerByte == 0x80)
{
compressedOffset += 1;
}
else
{
byte repeatData = compressedData[compressedOffset + 1];
int repeatLength = 257 - headerByte;
ArrayCopyRepeat(repeatData, buffer, decompressedOffset, repeatLength);
compressedOffset += 2;
decompressedOffset += repeatLength;
}
}
}
private static void ArrayCopyRepeat(byte value, Span<byte> destinationArray, int destinationIndex, int length)
{
for (int i = 0; i < length; i++)
{
destinationArray[i + destinationIndex] = value;
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.compressedDataMemory?.Dispose();
}
}

842
src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4BitReader.cs

@ -0,0 +1,842 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Bitreader for reading compressed CCITT T4 1D data.
/// </summary>
internal class T4BitReader : IDisposable
{
/// <summary>
/// Number of bits read.
/// </summary>
private int bitsRead;
/// <summary>
/// Current value.
/// </summary>
private uint value;
/// <summary>
/// Number of bits read for the current run value.
/// </summary>
private int curValueBitsRead;
/// <summary>
/// Byte position in the buffer.
/// </summary>
private ulong position;
/// <summary>
/// Indicates whether its the first line of data which is read from the image.
/// </summary>
private bool isFirstScanLine;
/// <summary>
/// Indicates whether we have found a termination code which signals the end of a run.
/// </summary>
private bool terminationCodeFound;
/// <summary>
/// We keep track if its the start of the row, because each run is expected to start with a white run.
/// If the image row itself starts with black, a white run of zero is expected.
/// </summary>
private bool isStartOfRow;
/// <summary>
/// Indicates whether the modified huffman compression, as specified in the TIFF spec in section 10, is used.
/// </summary>
private readonly bool isModifiedHuffmanRle;
/// <summary>
/// Indicates, if fill bits have been added as necessary before EOL codes such that EOL always ends on a byte boundary. Defaults to false.
/// </summary>
private readonly bool eolPadding;
private readonly int dataLength;
private const int MinCodeLength = 2;
private readonly int maxCodeLength = 13;
private static readonly Dictionary<uint, uint> WhiteLen4TermCodes = new Dictionary<uint, uint>()
{
{ 0x7, 2 }, { 0x8, 3 }, { 0xB, 4 }, { 0xC, 5 }, { 0xE, 6 }, { 0xF, 7 }
};
private static readonly Dictionary<uint, uint> WhiteLen5TermCodes = new Dictionary<uint, uint>()
{
{ 0x13, 8 }, { 0x14, 9 }, { 0x7, 10 }, { 0x8, 11 }
};
private static readonly Dictionary<uint, uint> WhiteLen6TermCodes = new Dictionary<uint, uint>()
{
{ 0x7, 1 }, { 0x8, 12 }, { 0x3, 13 }, { 0x34, 14 }, { 0x35, 15 }, { 0x2A, 16 }, { 0x2B, 17 }
};
private static readonly Dictionary<uint, uint> WhiteLen7TermCodes = new Dictionary<uint, uint>()
{
{ 0x27, 18 }, { 0xC, 19 }, { 0x8, 20 }, { 0x17, 21 }, { 0x3, 22 }, { 0x4, 23 }, { 0x28, 24 }, { 0x2B, 25 }, { 0x13, 26 },
{ 0x24, 27 }, { 0x18, 28 }
};
private static readonly Dictionary<uint, uint> WhiteLen8TermCodes = new Dictionary<uint, uint>()
{
{ 0x35, 0 }, { 0x2, 29 }, { 0x3, 30 }, { 0x1A, 31 }, { 0x1B, 32 }, { 0x12, 33 }, { 0x13, 34 }, { 0x14, 35 }, { 0x15, 36 },
{ 0x16, 37 }, { 0x17, 38 }, { 0x28, 39 }, { 0x29, 40 }, { 0x2A, 41 }, { 0x2B, 42 }, { 0x2C, 43 }, { 0x2D, 44 }, { 0x4, 45 },
{ 0x5, 46 }, { 0xA, 47 }, { 0xB, 48 }, { 0x52, 49 }, { 0x53, 50 }, { 0x54, 51 }, { 0x55, 52 }, { 0x24, 53 }, { 0x25, 54 },
{ 0x58, 55 }, { 0x59, 56 }, { 0x5A, 57 }, { 0x5B, 58 }, { 0x4A, 59 }, { 0x4B, 60 }, { 0x32, 61 }, { 0x33, 62 }, { 0x34, 63 }
};
private static readonly Dictionary<uint, uint> BlackLen2TermCodes = new Dictionary<uint, uint>()
{
{ 0x3, 2 }, { 0x2, 3 }
};
private static readonly Dictionary<uint, uint> BlackLen3TermCodes = new Dictionary<uint, uint>()
{
{ 0x2, 1 }, { 0x3, 4 }
};
private static readonly Dictionary<uint, uint> BlackLen4TermCodes = new Dictionary<uint, uint>()
{
{ 0x3, 5 }, { 0x2, 6 }
};
private static readonly Dictionary<uint, uint> BlackLen5TermCodes = new Dictionary<uint, uint>()
{
{ 0x3, 7 }
};
private static readonly Dictionary<uint, uint> BlackLen6TermCodes = new Dictionary<uint, uint>()
{
{ 0x5, 8 }, { 0x4, 9 }
};
private static readonly Dictionary<uint, uint> BlackLen7TermCodes = new Dictionary<uint, uint>()
{
{ 0x4, 10 }, { 0x5, 11 }, { 0x7, 12 }
};
private static readonly Dictionary<uint, uint> BlackLen8TermCodes = new Dictionary<uint, uint>()
{
{ 0x4, 13 }, { 0x7, 14 }
};
private static readonly Dictionary<uint, uint> BlackLen9TermCodes = new Dictionary<uint, uint>()
{
{ 0x18, 15 }
};
private static readonly Dictionary<uint, uint> BlackLen10TermCodes = new Dictionary<uint, uint>()
{
{ 0x37, 0 }, { 0x17, 16 }, { 0x18, 17 }, { 0x8, 18 }
};
private static readonly Dictionary<uint, uint> BlackLen11TermCodes = new Dictionary<uint, uint>()
{
{ 0x67, 19 }, { 0x68, 20 }, { 0x6C, 21 }, { 0x37, 22 }, { 0x28, 23 }, { 0x17, 24 }, { 0x18, 25 }
};
private static readonly Dictionary<uint, uint> BlackLen12TermCodes = new Dictionary<uint, uint>()
{
{ 0xCA, 26 }, { 0xCB, 27 }, { 0xCC, 28 }, { 0xCD, 29 }, { 0x68, 30 }, { 0x69, 31 }, { 0x6A, 32 }, { 0x6B, 33 }, { 0xD2, 34 },
{ 0xD3, 35 }, { 0xD4, 36 }, { 0xD5, 37 }, { 0xD6, 38 }, { 0xD7, 39 }, { 0x6C, 40 }, { 0x6D, 41 }, { 0xDA, 42 }, { 0xDB, 43 },
{ 0x54, 44 }, { 0x55, 45 }, { 0x56, 46 }, { 0x57, 47 }, { 0x64, 48 }, { 0x65, 49 }, { 0x52, 50 }, { 0x53, 51 }, { 0x24, 52 },
{ 0x37, 53 }, { 0x38, 54 }, { 0x27, 55 }, { 0x28, 56 }, { 0x58, 57 }, { 0x59, 58 }, { 0x2B, 59 }, { 0x2C, 60 }, { 0x5A, 61 },
{ 0x66, 62 }, { 0x67, 63 }
};
private static readonly Dictionary<uint, uint> WhiteLen5MakeupCodes = new Dictionary<uint, uint>()
{
{ 0x1B, 64 }, { 0x12, 128 }
};
private static readonly Dictionary<uint, uint> WhiteLen6MakeupCodes = new Dictionary<uint, uint>()
{
{ 0x17, 192 }, { 0x18, 1664 }
};
private static readonly Dictionary<uint, uint> WhiteLen8MakeupCodes = new Dictionary<uint, uint>()
{
{ 0x36, 320 }, { 0x37, 384 }, { 0x64, 448 }, { 0x65, 512 }, { 0x68, 576 }, { 0x67, 640 }
};
private static readonly Dictionary<uint, uint> WhiteLen7MakeupCodes = new Dictionary<uint, uint>()
{
{ 0x37, 256 }
};
private static readonly Dictionary<uint, uint> WhiteLen9MakeupCodes = new Dictionary<uint, uint>()
{
{ 0xCC, 704 }, { 0xCD, 768 }, { 0xD2, 832 }, { 0xD3, 896 }, { 0xD4, 960 }, { 0xD5, 1024 }, { 0xD6, 1088 },
{ 0xD7, 1152 }, { 0xD8, 1216 }, { 0xD9, 1280 }, { 0xDA, 1344 }, { 0xDB, 1408 }, { 0x98, 1472 }, { 0x99, 1536 },
{ 0x9A, 1600 }, { 0x9B, 1728 }
};
private static readonly Dictionary<uint, uint> WhiteLen11MakeupCodes = new Dictionary<uint, uint>()
{
{ 0x8, 1792 }, { 0xC, 1856 }, { 0xD, 1920 }
};
private static readonly Dictionary<uint, uint> WhiteLen12MakeupCodes = new Dictionary<uint, uint>()
{
{ 0x12, 1984 }, { 0x13, 2048 }, { 0x14, 2112 }, { 0x15, 2176 }, { 0x16, 2240 }, { 0x17, 2304 }, { 0x1C, 2368 },
{ 0x1D, 2432 }, { 0x1E, 2496 }, { 0x1F, 2560 }
};
private static readonly Dictionary<uint, uint> BlackLen10MakeupCodes = new Dictionary<uint, uint>()
{
{ 0xF, 64 }
};
private static readonly Dictionary<uint, uint> BlackLen11MakeupCodes = new Dictionary<uint, uint>()
{
{ 0x8, 1792 }, { 0xC, 1856 }, { 0xD, 1920 }
};
private static readonly Dictionary<uint, uint> BlackLen12MakeupCodes = new Dictionary<uint, uint>()
{
{ 0xC8, 128 }, { 0xC9, 192 }, { 0x5B, 256 }, { 0x33, 320 }, { 0x34, 384 }, { 0x35, 448 },
{ 0x12, 1984 }, { 0x13, 2048 }, { 0x14, 2112 }, { 0x15, 2176 }, { 0x16, 2240 }, { 0x17, 2304 }, { 0x1C, 2368 },
{ 0x1D, 2432 }, { 0x1E, 2496 }, { 0x1F, 2560 }
};
private static readonly Dictionary<uint, uint> BlackLen13MakeupCodes = new Dictionary<uint, uint>()
{
{ 0x6C, 512 }, { 0x6D, 576 }, { 0x4A, 640 }, { 0x4B, 704 }, { 0x4C, 768 }, { 0x4D, 832 }, { 0x72, 896 },
{ 0x73, 960 }, { 0x74, 1024 }, { 0x75, 1088 }, { 0x76, 1152 }, { 0x77, 1216 }, { 0x52, 1280 }, { 0x53, 1344 },
{ 0x54, 1408 }, { 0x55, 1472 }, { 0x5A, 1536 }, { 0x5B, 1600 }, { 0x64, 1664 }, { 0x65, 1728 }
};
/// <summary>
/// Initializes a new instance of the <see cref="T4BitReader" /> class.
/// </summary>
/// <param name="input">The compressed input stream.</param>
/// <param name="bytesToRead">The number of bytes to read from the stream.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="eolPadding">Indicates, if fill bits have been added as necessary before EOL codes such that EOL always ends on a byte boundary. Defaults to false.</param>
/// <param name="isModifiedHuffman">Indicates, if its the modified huffman code variation. Defaults to false.</param>
public T4BitReader(Stream input, int bytesToRead, MemoryAllocator allocator, bool eolPadding = false, bool isModifiedHuffman = false)
{
this.Data = allocator.Allocate<byte>(bytesToRead);
this.ReadImageDataFromStream(input, bytesToRead);
this.isModifiedHuffmanRle = isModifiedHuffman;
this.dataLength = bytesToRead;
this.bitsRead = 0;
this.value = 0;
this.curValueBitsRead = 0;
this.position = 0;
this.IsWhiteRun = true;
this.isFirstScanLine = true;
this.isStartOfRow = true;
this.terminationCodeFound = false;
this.RunLength = 0;
this.eolPadding = eolPadding;
if (this.eolPadding)
{
this.maxCodeLength = 24;
}
}
/// <summary>
/// Gets the compressed image data.
/// </summary>
public IMemoryOwner<byte> Data { get; }
/// <summary>
/// Gets a value indicating whether there is more data to read left.
/// </summary>
public bool HasMoreData
{
get
{
if (this.isModifiedHuffmanRle)
{
return this.position < (ulong)this.dataLength - 1 || (this.bitsRead > 0 && this.bitsRead < 7);
}
return this.position < (ulong)this.dataLength - 1;
}
}
/// <summary>
/// Gets a value indicating whether the current run is a white pixel run, otherwise its a black pixel run.
/// </summary>
public bool IsWhiteRun { get; private set; }
/// <summary>
/// Gets the number of pixels in the current run.
/// </summary>
public uint RunLength { get; private set; }
/// <summary>
/// Gets a value indicating whether the end of a pixel row has been reached.
/// </summary>
public bool IsEndOfScanLine
{
get
{
if (this.eolPadding)
{
return this.curValueBitsRead >= 12 && this.value == 1;
}
return this.curValueBitsRead == 12 && this.value == 1;
}
}
/// <summary>
/// Read the next run of pixels.
/// </summary>
public void ReadNextRun()
{
if (this.terminationCodeFound)
{
this.IsWhiteRun = !this.IsWhiteRun;
this.terminationCodeFound = false;
}
this.Reset();
if (this.isFirstScanLine && !this.isModifiedHuffmanRle)
{
// We expect an EOL before the first data.
this.value = this.ReadValue(this.eolPadding ? 16 : 12);
if (!this.IsEndOfScanLine)
{
TiffThrowHelper.ThrowImageFormatException("t4 parsing error: expected start of data marker not found");
}
this.Reset();
}
// A code word must have at least 2 bits.
this.value = this.ReadValue(MinCodeLength);
do
{
if (this.curValueBitsRead > this.maxCodeLength)
{
TiffThrowHelper.ThrowImageFormatException("t4 parsing error: invalid code length read");
}
bool isMakeupCode = this.IsMakeupCode();
if (isMakeupCode)
{
if (this.IsWhiteRun)
{
this.RunLength += this.WhiteMakeupCodeRunLength();
}
else
{
this.RunLength += this.BlackMakeupCodeRunLength();
}
this.isStartOfRow = false;
this.Reset(resetRunLength: false);
continue;
}
bool isTerminatingCode = this.IsTerminatingCode();
if (isTerminatingCode)
{
// Each line starts with a white run. If the image starts with black, a white run with length zero is written.
if (this.isStartOfRow && this.IsWhiteRun && this.WhiteTerminatingCodeRunLength() == 0)
{
this.IsWhiteRun = !this.IsWhiteRun;
this.Reset();
this.isStartOfRow = false;
continue;
}
if (this.IsWhiteRun)
{
this.RunLength += this.WhiteTerminatingCodeRunLength();
}
else
{
this.RunLength += this.BlackTerminatingCodeRunLength();
}
this.terminationCodeFound = true;
this.isStartOfRow = false;
break;
}
var currBit = this.ReadValue(1);
this.value = (this.value << 1) | currBit;
if (this.IsEndOfScanLine)
{
this.StartNewRow();
}
}
while (!this.IsEndOfScanLine);
this.isFirstScanLine = false;
}
public void StartNewRow()
{
// Each new row starts with a white run.
this.IsWhiteRun = true;
this.isStartOfRow = true;
this.terminationCodeFound = false;
if (this.isModifiedHuffmanRle)
{
int pad = 8 - (this.bitsRead % 8);
if (pad != 8)
{
// Skip padding bits, move to next byte.
this.position++;
this.bitsRead = 0;
}
}
}
/// <inheritdoc/>
public void Dispose() => this.Data.Dispose();
private uint WhiteTerminatingCodeRunLength()
{
switch (this.curValueBitsRead)
{
case 4:
{
return WhiteLen4TermCodes[this.value];
}
case 5:
{
return WhiteLen5TermCodes[this.value];
}
case 6:
{
return WhiteLen6TermCodes[this.value];
}
case 7:
{
return WhiteLen7TermCodes[this.value];
}
case 8:
{
return WhiteLen8TermCodes[this.value];
}
}
return 0;
}
private uint BlackTerminatingCodeRunLength()
{
switch (this.curValueBitsRead)
{
case 2:
{
return BlackLen2TermCodes[this.value];
}
case 3:
{
return BlackLen3TermCodes[this.value];
}
case 4:
{
return BlackLen4TermCodes[this.value];
}
case 5:
{
return BlackLen5TermCodes[this.value];
}
case 6:
{
return BlackLen6TermCodes[this.value];
}
case 7:
{
return BlackLen7TermCodes[this.value];
}
case 8:
{
return BlackLen8TermCodes[this.value];
}
case 9:
{
return BlackLen9TermCodes[this.value];
}
case 10:
{
return BlackLen10TermCodes[this.value];
}
case 11:
{
return BlackLen11TermCodes[this.value];
}
case 12:
{
return BlackLen12TermCodes[this.value];
}
}
return 0;
}
private uint WhiteMakeupCodeRunLength()
{
switch (this.curValueBitsRead)
{
case 5:
{
return WhiteLen5MakeupCodes[this.value];
}
case 6:
{
return WhiteLen6MakeupCodes[this.value];
}
case 7:
{
return WhiteLen7MakeupCodes[this.value];
}
case 8:
{
return WhiteLen8MakeupCodes[this.value];
}
case 9:
{
return WhiteLen9MakeupCodes[this.value];
}
case 11:
{
return WhiteLen11MakeupCodes[this.value];
}
case 12:
{
return WhiteLen12MakeupCodes[this.value];
}
}
return 0;
}
private uint BlackMakeupCodeRunLength()
{
switch (this.curValueBitsRead)
{
case 10:
{
return BlackLen10MakeupCodes[this.value];
}
case 11:
{
return BlackLen11MakeupCodes[this.value];
}
case 12:
{
return BlackLen12MakeupCodes[this.value];
}
case 13:
{
return BlackLen13MakeupCodes[this.value];
}
}
return 0;
}
private bool IsMakeupCode()
{
if (this.IsWhiteRun)
{
return this.IsWhiteMakeupCode();
}
return this.IsBlackMakeupCode();
}
private bool IsWhiteMakeupCode()
{
switch (this.curValueBitsRead)
{
case 5:
{
return WhiteLen5MakeupCodes.ContainsKey(this.value);
}
case 6:
{
return WhiteLen6MakeupCodes.ContainsKey(this.value);
}
case 7:
{
return WhiteLen7MakeupCodes.ContainsKey(this.value);
}
case 8:
{
return WhiteLen8MakeupCodes.ContainsKey(this.value);
}
case 9:
{
return WhiteLen9MakeupCodes.ContainsKey(this.value);
}
case 11:
{
return WhiteLen11MakeupCodes.ContainsKey(this.value);
}
case 12:
{
if (this.isModifiedHuffmanRle)
{
if (this.value == 1)
{
return true;
}
}
return WhiteLen12MakeupCodes.ContainsKey(this.value);
}
}
return false;
}
private bool IsBlackMakeupCode()
{
switch (this.curValueBitsRead)
{
case 10:
{
return BlackLen10MakeupCodes.ContainsKey(this.value);
}
case 11:
{
if (this.isModifiedHuffmanRle)
{
if (this.value == 0)
{
return true;
}
}
return BlackLen11MakeupCodes.ContainsKey(this.value);
}
case 12:
{
return BlackLen12MakeupCodes.ContainsKey(this.value);
}
case 13:
{
return BlackLen13MakeupCodes.ContainsKey(this.value);
}
}
return false;
}
private bool IsTerminatingCode()
{
if (this.IsWhiteRun)
{
return this.IsWhiteTerminatingCode();
}
return this.IsBlackTerminatingCode();
}
private bool IsWhiteTerminatingCode()
{
switch (this.curValueBitsRead)
{
case 4:
{
return WhiteLen4TermCodes.ContainsKey(this.value);
}
case 5:
{
return WhiteLen5TermCodes.ContainsKey(this.value);
}
case 6:
{
return WhiteLen6TermCodes.ContainsKey(this.value);
}
case 7:
{
return WhiteLen7TermCodes.ContainsKey(this.value);
}
case 8:
{
return WhiteLen8TermCodes.ContainsKey(this.value);
}
}
return false;
}
private bool IsBlackTerminatingCode()
{
switch (this.curValueBitsRead)
{
case 2:
{
return BlackLen2TermCodes.ContainsKey(this.value);
}
case 3:
{
return BlackLen3TermCodes.ContainsKey(this.value);
}
case 4:
{
return BlackLen4TermCodes.ContainsKey(this.value);
}
case 5:
{
return BlackLen5TermCodes.ContainsKey(this.value);
}
case 6:
{
return BlackLen6TermCodes.ContainsKey(this.value);
}
case 7:
{
return BlackLen7TermCodes.ContainsKey(this.value);
}
case 8:
{
return BlackLen8TermCodes.ContainsKey(this.value);
}
case 9:
{
return BlackLen9TermCodes.ContainsKey(this.value);
}
case 10:
{
return BlackLen10TermCodes.ContainsKey(this.value);
}
case 11:
{
return BlackLen11TermCodes.ContainsKey(this.value);
}
case 12:
{
return BlackLen12TermCodes.ContainsKey(this.value);
}
}
return false;
}
private void Reset(bool resetRunLength = true)
{
this.value = 0;
this.curValueBitsRead = 0;
if (resetRunLength)
{
this.RunLength = 0;
}
}
private uint ReadValue(int nBits)
{
Guard.MustBeGreaterThan(nBits, 0, nameof(nBits));
uint v = 0;
int shift = nBits;
while (shift-- > 0)
{
uint bit = this.GetBit();
v |= bit << shift;
this.curValueBitsRead++;
}
return v;
}
private uint GetBit()
{
if (this.bitsRead >= 8)
{
this.LoadNewByte();
}
Span<byte> dataSpan = this.Data.GetSpan();
int shift = 8 - this.bitsRead - 1;
var bit = (uint)((dataSpan[(int)this.position] & (1 << shift)) != 0 ? 1 : 0);
this.bitsRead++;
return bit;
}
private void LoadNewByte()
{
this.position++;
this.bitsRead = 0;
if (this.position >= (ulong)this.dataLength)
{
TiffThrowHelper.ThrowImageFormatException("tiff image has invalid t4 compressed data");
}
}
private void ReadImageDataFromStream(Stream input, int bytesToRead)
{
Span<byte> dataSpan = this.Data.GetSpan();
input.Read(dataSpan, 0, bytesToRead);
}
}
}

90
src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs

@ -0,0 +1,90 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/// <summary>
/// Class to handle cases where TIFF image data is compressed using CCITT T4 compression.
/// </summary>
internal class T4TiffCompression : TiffBaseDecompressor
{
private readonly FaxCompressionOptions faxCompressionOptions;
private readonly byte whiteValue;
private readonly byte blackValue;
/// <summary>
/// Initializes a new instance of the <see cref="T4TiffCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="width">The image width.</param>
/// <param name="bitsPerPixel">The number of bits per pixel.</param>
/// <param name="faxOptions">Fax compression options.</param>
/// <param name="photometricInterpretation">The photometric interpretation.</param>
public T4TiffCompression(MemoryAllocator allocator, int width, int bitsPerPixel, FaxCompressionOptions faxOptions, TiffPhotometricInterpretation photometricInterpretation)
: base(allocator, width, bitsPerPixel)
{
this.faxCompressionOptions = faxOptions;
bool isWhiteZero = photometricInterpretation == TiffPhotometricInterpretation.WhiteIsZero;
this.whiteValue = (byte)(isWhiteZero ? 0 : 1);
this.blackValue = (byte)(isWhiteZero ? 1 : 0);
}
/// <inheritdoc/>
protected override void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer)
{
if (this.faxCompressionOptions.HasFlag(FaxCompressionOptions.TwoDimensionalCoding))
{
TiffThrowHelper.ThrowNotSupported("TIFF CCITT 2D compression is not yet supported");
}
var eolPadding = this.faxCompressionOptions.HasFlag(FaxCompressionOptions.EolPadding);
using var bitReader = new T4BitReader(stream, byteCount, this.Allocator, eolPadding);
buffer.Clear();
uint bitsWritten = 0;
while (bitReader.HasMoreData)
{
bitReader.ReadNextRun();
if (bitReader.RunLength > 0)
{
if (bitReader.IsWhiteRun)
{
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.whiteValue);
bitsWritten += bitReader.RunLength;
}
else
{
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, bitReader.RunLength, this.blackValue);
bitsWritten += bitReader.RunLength;
}
}
if (bitReader.IsEndOfScanLine)
{
// Write padding bytes, if necessary.
uint pad = 8 - (bitsWritten % 8);
if (pad != 8)
{
BitWriterUtils.WriteBits(buffer, (int)bitsWritten, pad, 0);
bitsWritten += pad;
}
}
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
}

257
src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffLzwDecoder.cs

@ -0,0 +1,257 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
{
/*
This implementation is based on a port of a java tiff decoder by Harald Kuhr: https://github.com/haraldk/TwelveMonkeys
Original licence:
BSD 3-Clause License
* Copyright (c) 2015, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
** Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED.IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/// <summary>
/// Decompresses and decodes data using the dynamic LZW algorithms, see TIFF spec Section 13.
/// </summary>
internal sealed class TiffLzwDecoder
{
/// <summary>
/// The stream to decode.
/// </summary>
private readonly Stream stream;
/// <summary>
/// As soon as we use entry 4094 of the table (maxTableSize - 2), the lzw compressor write out a (12-bit) ClearCode.
/// At this point, the compressor reinitializes the string table and then writes out 9-bit codes again.
/// </summary>
private const int ClearCode = 256;
/// <summary>
/// End of Information.
/// </summary>
private const int EoiCode = 257;
/// <summary>
/// Minimum code length of 9 bits.
/// </summary>
private const int MinBits = 9;
/// <summary>
/// Maximum code length of 12 bits.
/// </summary>
private const int MaxBits = 12;
/// <summary>
/// Maximum table size of 4096.
/// </summary>
private const int TableSize = 1 << MaxBits;
private readonly LzwString[] table;
private int tableLength;
private int bitsPerCode;
private int oldCode = ClearCode;
private int maxCode;
private int bitMask;
private int maxString;
private bool eofReached;
private int nextData;
private int nextBits;
/// <summary>
/// Initializes a new instance of the <see cref="TiffLzwDecoder" /> class
/// and sets the stream, where the compressed data should be read from.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream" /> is null.</exception>
public TiffLzwDecoder(Stream stream)
{
Guard.NotNull(stream, nameof(stream));
this.stream = stream;
// TODO: Investigate a manner by which we can avoid this allocation.
this.table = new LzwString[TableSize];
for (int i = 0; i < 256; i++)
{
this.table[i] = new LzwString((byte)i);
}
this.Init();
}
private void Init()
{
// Table length is 256 + 2, because of special clear code and end of information code.
this.tableLength = 258;
this.bitsPerCode = MinBits;
this.bitMask = BitmaskFor(this.bitsPerCode);
this.maxCode = this.MaxCode();
this.maxString = 1;
}
/// <summary>
/// Decodes and decompresses all pixel indices from the stream.
/// </summary>
/// <param name="pixels">The pixel array to decode to.</param>
public void DecodePixels(Span<byte> pixels)
{
// Adapted from the pseudo-code example found in the TIFF 6.0 Specification, 1992.
// See Section 13: "LZW Compression"/"LZW Decoding", page 61+
int code;
int offset = 0;
while ((code = this.GetNextCode()) != EoiCode)
{
if (code == ClearCode)
{
this.Init();
code = this.GetNextCode();
if (code == EoiCode)
{
break;
}
if (this.table[code] == null)
{
TiffThrowHelper.ThrowImageFormatException($"Corrupted TIFF LZW: code {code} (table size: {this.tableLength})");
}
offset += this.table[code].WriteTo(pixels, offset);
}
else
{
if (this.table[this.oldCode] == null)
{
TiffThrowHelper.ThrowImageFormatException($"Corrupted TIFF LZW: code {this.oldCode} (table size: {this.tableLength})");
}
if (this.IsInTable(code))
{
offset += this.table[code].WriteTo(pixels, offset);
this.AddStringToTable(this.table[this.oldCode].Concatenate(this.table[code].FirstChar));
}
else
{
LzwString outString = this.table[this.oldCode].Concatenate(this.table[this.oldCode].FirstChar);
offset += outString.WriteTo(pixels, offset);
this.AddStringToTable(outString);
}
}
this.oldCode = code;
if (offset >= pixels.Length)
{
break;
}
}
}
private void AddStringToTable(LzwString lzwString)
{
if (this.tableLength > this.table.Length)
{
TiffThrowHelper.ThrowImageFormatException($"TIFF LZW with more than {MaxBits} bits per code encountered (table overflow)");
}
this.table[this.tableLength++] = lzwString;
if (this.tableLength > this.maxCode)
{
this.bitsPerCode++;
if (this.bitsPerCode > MaxBits)
{
// Continue reading MaxBits (12 bit) length codes.
this.bitsPerCode = MaxBits;
}
this.bitMask = BitmaskFor(this.bitsPerCode);
this.maxCode = this.MaxCode();
}
if (lzwString.Length > this.maxString)
{
this.maxString = lzwString.Length;
}
}
private int GetNextCode()
{
if (this.eofReached)
{
return EoiCode;
}
int read = this.stream.ReadByte();
if (read < 0)
{
this.eofReached = true;
return EoiCode;
}
this.nextData = (this.nextData << 8) | read;
this.nextBits += 8;
if (this.nextBits < this.bitsPerCode)
{
read = this.stream.ReadByte();
if (read < 0)
{
this.eofReached = true;
return EoiCode;
}
this.nextData = (this.nextData << 8) | read;
this.nextBits += 8;
}
int code = (this.nextData >> (this.nextBits - this.bitsPerCode)) & this.bitMask;
this.nextBits -= this.bitsPerCode;
return code;
}
private bool IsInTable(int code) => code < this.tableLength;
private int MaxCode() => this.bitMask - 1;
private static int BitmaskFor(int bits) => (1 << bits) - 1;
}
}

36
src/ImageSharp/Formats/Tiff/Compression/FaxCompressionOptions.cs

@ -0,0 +1,36 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression
{
/// <summary>
/// Fax compression options, see TIFF spec page 51f (T4Options).
/// </summary>
[Flags]
public enum FaxCompressionOptions : uint
{
/// <summary>
/// No options.
/// </summary>
None = 0,
/// <summary>
/// If set, 2-dimensional coding is used (otherwise 1-dimensional is assumed).
/// </summary>
TwoDimensionalCoding = 1,
/// <summary>
/// If set, uncompressed mode is used.
/// </summary>
UncompressedMode = 2,
/// <summary>
/// If set, fill bits have been added as necessary before EOL codes such that
/// EOL always ends on a byte boundary, thus ensuring an EOL-sequence of 1 byte
/// preceded by a zero nibble: xxxx-0000 0000-0001.
/// </summary>
EolPadding = 4
}
}

138
src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs

@ -0,0 +1,138 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression
{
/// <summary>
/// Methods for undoing the horizontal prediction used in combination with deflate and LZW compressed TIFF images.
/// </summary>
internal static class HorizontalPredictor
{
/// <summary>
/// Inverts the horizontal prediction.
/// </summary>
/// <param name="pixelBytes">Buffer with decompressed pixel data.</param>
/// <param name="width">The width of the image or strip.</param>
/// <param name="bitsPerPixel">Bits per pixel.</param>
public static void Undo(Span<byte> pixelBytes, int width, int bitsPerPixel)
{
if (bitsPerPixel == 8)
{
Undo8Bit(pixelBytes, width);
}
else if (bitsPerPixel == 24)
{
Undo24Bit(pixelBytes, width);
}
}
public static void ApplyHorizontalPrediction(Span<byte> rows, int width, int bitsPerPixel)
{
if (bitsPerPixel == 8)
{
ApplyHorizontalPrediction8Bit(rows, width);
}
else if (bitsPerPixel == 24)
{
ApplyHorizontalPrediction24Bit(rows, width);
}
}
/// <summary>
/// Applies a horizontal predictor to the rgb row.
/// Make use of the fact that many continuous-tone images rarely vary much in pixel value from one pixel to the next.
/// In such images, if we replace the pixel values by differences between consecutive pixels, many of the differences should be 0, plus
/// or minus 1, and so on.This reduces the apparent information content and allows LZW to encode the data more compactly.
/// </summary>
/// <param name="rows">The rgb pixel rows.</param>
/// <param name="width">The width.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private static void ApplyHorizontalPrediction24Bit(Span<byte> rows, int width)
{
DebugGuard.IsTrue(rows.Length % width == 0, "Values must be equals");
int height = rows.Length / width;
for (int y = 0; y < height; y++)
{
Span<byte> rowSpan = rows.Slice(y * width, width);
Span<Rgb24> rowRgb = MemoryMarshal.Cast<byte, Rgb24>(rowSpan);
for (int x = rowRgb.Length - 1; x >= 1; x--)
{
byte r = (byte)(rowRgb[x].R - rowRgb[x - 1].R);
byte g = (byte)(rowRgb[x].G - rowRgb[x - 1].G);
byte b = (byte)(rowRgb[x].B - rowRgb[x - 1].B);
var rgb = new Rgb24(r, g, b);
rowRgb[x].FromRgb24(rgb);
}
}
}
/// <summary>
/// Applies a horizontal predictor to a gray pixel row.
/// </summary>
/// <param name="rows">The gray pixel rows.</param>
/// <param name="width">The width.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private static void ApplyHorizontalPrediction8Bit(Span<byte> rows, int width)
{
DebugGuard.IsTrue(rows.Length % width == 0, "Values must be equals");
int height = rows.Length / width;
for (int y = 0; y < height; y++)
{
Span<byte> rowSpan = rows.Slice(y * width, width);
for (int x = rowSpan.Length - 1; x >= 1; x--)
{
rowSpan[x] -= rowSpan[x - 1];
}
}
}
private static void Undo8Bit(Span<byte> pixelBytes, int width)
{
int rowBytesCount = width;
int height = pixelBytes.Length / rowBytesCount;
for (int y = 0; y < height; y++)
{
Span<byte> rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount);
byte pixelValue = rowBytes[0];
for (int x = 1; x < width; x++)
{
pixelValue += rowBytes[x];
rowBytes[x] = pixelValue;
}
}
}
private static void Undo24Bit(Span<byte> pixelBytes, int width)
{
int rowBytesCount = width * 3;
int height = pixelBytes.Length / rowBytesCount;
for (int y = 0; y < height; y++)
{
Span<byte> rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount);
Span<Rgb24> rowRgb = MemoryMarshal.Cast<byte, Rgb24>(rowBytes).Slice(0, width);
ref Rgb24 rowRgbBase = ref MemoryMarshal.GetReference(rowRgb);
byte r = rowRgbBase.R;
byte g = rowRgbBase.G;
byte b = rowRgbBase.B;
for (int x = 1; x < rowRgb.Length; x++)
{
ref Rgb24 pixel = ref rowRgb[x];
r += pixel.R;
g += pixel.G;
b += pixel.B;
var rgb = new Rgb24(r, g, b);
pixel.FromRgb24(rgb);
}
}
}
}
}

62
src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompression.cs

@ -0,0 +1,62 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression
{
internal abstract class TiffBaseCompression : IDisposable
{
private bool isDisposed;
protected TiffBaseCompression(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None)
{
this.Allocator = allocator;
this.Width = width;
this.BitsPerPixel = bitsPerPixel;
this.Predictor = predictor;
this.BytesPerRow = ((width * bitsPerPixel) + 7) / 8;
}
/// <summary>
/// Gets the image width.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the bits per pixel.
/// </summary>
public int BitsPerPixel { get; }
/// <summary>
/// Gets the bytes per row.
/// </summary>
public int BytesPerRow { get; }
/// <summary>
/// Gets the predictor to use. Should only be used with deflate or lzw compression.
/// </summary>
public TiffPredictor Predictor { get; }
/// <summary>
/// Gets the memory allocator.
/// </summary>
protected MemoryAllocator Allocator { get; }
/// <inheritdoc />
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.isDisposed = true;
this.Dispose(true);
}
protected abstract void Dispose(bool disposing);
}
}

48
src/ImageSharp/Formats/Tiff/Compression/TiffBaseCompressor.cs

@ -0,0 +1,48 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression
{
internal abstract class TiffBaseCompressor : TiffBaseCompression
{
/// <summary>
/// Initializes a new instance of the <see cref="TiffBaseCompressor"/> class.
/// </summary>
/// <param name="output">The output stream to write the compressed image to.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="width">The image width.</param>
/// <param name="bitsPerPixel">Bits per pixel.</param>
/// <param name="predictor">The predictor to use (should only be used with deflate or lzw compression). Defaults to none.</param>
protected TiffBaseCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None)
: base(allocator, width, bitsPerPixel, predictor)
=> this.Output = output;
/// <summary>
/// Gets the compression method to use.
/// </summary>
public abstract TiffCompression Method { get; }
/// <summary>
/// Gets the output stream to write the compressed image to.
/// </summary>
public Stream Output { get; }
/// <summary>
/// Does any initialization required for the compression.
/// </summary>
/// <param name="rowsPerStrip">The number of rows per strip.</param>
public abstract void Initialize(int rowsPerStrip);
/// <summary>
/// Compresses a strip of the image.
/// </summary>
/// <param name="rows">Image rows to compress.</param>
/// <param name="height">Image height.</param>
public abstract void CompressStrip(Span<byte> rows, int height);
}
}

54
src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs

@ -0,0 +1,54 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression
{
/// <summary>
/// The base tiff decompressor class.
/// </summary>
internal abstract class TiffBaseDecompressor : TiffBaseCompression
{
protected TiffBaseDecompressor(MemoryAllocator allocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None)
: base(allocator, width, bitsPerPixel, predictor)
{
}
/// <summary>
/// Decompresses image data into the supplied buffer.
/// </summary>
/// <param name="stream">The <see cref="Stream" /> to read image data from.</param>
/// <param name="stripOffset">The strip offset of stream.</param>
/// <param name="stripByteCount">The number of bytes to read from the input stream.</param>
/// <param name="buffer">The output buffer for uncompressed data.</param>
public void Decompress(BufferedReadStream stream, uint stripOffset, uint stripByteCount, Span<byte> buffer)
{
if (stripByteCount > int.MaxValue)
{
TiffThrowHelper.ThrowImageFormatException("The StripByteCount value is too big.");
}
stream.Seek(stripOffset, SeekOrigin.Begin);
this.Decompress(stream, (int)stripByteCount, buffer);
if (stripOffset + stripByteCount < stream.Position)
{
TiffThrowHelper.ThrowImageFormatException("Out of range when reading a strip.");
}
}
/// <summary>
/// Decompresses image data into the supplied buffer.
/// </summary>
/// <param name="stream">The <see cref="Stream" /> to read image data from.</param>
/// <param name="byteCount">The number of bytes to read from the input stream.</param>
/// <param name="buffer">The output buffer for uncompressed data.</param>
protected abstract void Decompress(BufferedReadStream stream, int byteCount, Span<byte> buffer);
}
}

64
src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs

@ -0,0 +1,64 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression
{
internal static class TiffCompressorFactory
{
public static TiffBaseCompressor Create(
TiffCompression method,
Stream output,
MemoryAllocator allocator,
int width,
int bitsPerPixel,
DeflateCompressionLevel compressionLevel,
TiffPredictor predictor)
{
switch (method)
{
// The following compression types are not implemented in the encoder and will default to no compression instead.
case TiffCompression.ItuTRecT43:
case TiffCompression.ItuTRecT82:
case TiffCompression.Jpeg:
case TiffCompression.OldJpeg:
case TiffCompression.OldDeflate:
case TiffCompression.None:
DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set");
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new NoCompressor(output, allocator, width, bitsPerPixel);
case TiffCompression.PackBits:
DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set");
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new PackBitsCompressor(output, allocator, width, bitsPerPixel);
case TiffCompression.Deflate:
return new DeflateCompressor(output, allocator, width, bitsPerPixel, predictor, compressionLevel);
case TiffCompression.Lzw:
DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set");
return new LzwCompressor(output, allocator, width, bitsPerPixel, predictor);
case TiffCompression.CcittGroup3Fax:
DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set");
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new T4BitCompressor(output, allocator, width, bitsPerPixel, false);
case TiffCompression.Ccitt1D:
DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set");
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new T4BitCompressor(output, allocator, width, bitsPerPixel, true);
default:
throw TiffThrowHelper.NotSupportedCompressor(method.ToString());
}
}
}
}

41
src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs

@ -0,0 +1,41 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Compression
{
/// <summary>
/// Provides enumeration of the various TIFF compression types the decoder can handle.
/// </summary>
internal enum TiffDecoderCompressionType
{
/// <summary>
/// Image data is stored uncompressed in the TIFF file.
/// </summary>
None = 0,
/// <summary>
/// Image data is compressed using PackBits compression.
/// </summary>
PackBits = 1,
/// <summary>
/// Image data is compressed using Deflate compression.
/// </summary>
Deflate = 2,
/// <summary>
/// Image data is compressed using LZW compression.
/// </summary>
Lzw = 3,
/// <summary>
/// Image data is compressed using T4-encoding: CCITT T.4.
/// </summary>
T4 = 4,
/// <summary>
/// Image data is compressed using modified huffman compression.
/// </summary>
HuffmanRle = 5,
}
}

54
src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs

@ -0,0 +1,54 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Tiff.Compression
{
internal static class TiffDecompressorsFactory
{
public static TiffBaseDecompressor Create(
TiffDecoderCompressionType method,
MemoryAllocator allocator,
TiffPhotometricInterpretation photometricInterpretation,
int width,
int bitsPerPixel,
TiffPredictor predictor,
FaxCompressionOptions faxOptions)
{
switch (method)
{
case TiffDecoderCompressionType.None:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected");
return new NoneTiffCompression(allocator, width, bitsPerPixel);
case TiffDecoderCompressionType.PackBits:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected");
return new PackBitsTiffCompression(allocator, width, bitsPerPixel);
case TiffDecoderCompressionType.Deflate:
DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected");
return new DeflateTiffCompression(allocator, width, bitsPerPixel, predictor);
case TiffDecoderCompressionType.Lzw:
DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected");
return new LzwTiffCompression(allocator, width, bitsPerPixel, predictor);
case TiffDecoderCompressionType.T4:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new T4TiffCompression(allocator, width, bitsPerPixel, faxOptions, photometricInterpretation);
case TiffDecoderCompressionType.HuffmanRle:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new ModifiedHuffmanTiffCompression(allocator, width, bitsPerPixel, photometricInterpretation);
default:
throw TiffThrowHelper.NotSupportedDecompressor(nameof(method));
}
}
}
}

94
src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs

@ -0,0 +1,94 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Enumeration representing the compression formats defined by the Tiff file-format.
/// </summary>
public enum TiffCompression : ushort
{
/// <summary>
/// A invalid compression value.
/// </summary>
Invalid = 0,
/// <summary>
/// No compression.
/// </summary>
None = 1,
/// <summary>
/// CCITT Group 3 1-Dimensional Modified Huffman run-length encoding.
/// </summary>
Ccitt1D = 2,
/// <summary>
/// PackBits compression
/// </summary>
PackBits = 32773,
/// <summary>
/// T4-encoding: CCITT T.4 bi-level encoding (see Section 11 of the TIFF 6.0 specification).
/// </summary>
CcittGroup3Fax = 3,
/// <summary>
/// T6-encoding: CCITT T.6 bi-level encoding (see Section 11 of the TIFF 6.0 specification).
///
/// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead,
/// if this is choosen.
/// </summary>
CcittGroup4Fax = 4,
/// <summary>
/// LZW compression (see Section 13 of the TIFF 6.0 specification).
/// </summary>
Lzw = 5,
/// <summary>
/// JPEG compression - obsolete (see Section 22 of the TIFF 6.0 specification).
///
/// Note: The TIFF encoder does not support this compression and will default to use no compression instead,
/// if this is chosen.
/// </summary>
OldJpeg = 6,
/// <summary>
/// JPEG compression (see TIFF Specification, supplement 2).
///
/// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead,
/// if this is chosen.
/// </summary>
Jpeg = 7,
/// <summary>
/// Deflate compression, using zlib data format (see TIFF Specification, supplement 2).
/// </summary>
Deflate = 8,
/// <summary>
/// Deflate compression - old.
///
/// Note: The TIFF encoder does not support this compression and will default to use no compression instead,
/// if this is chosen.
/// </summary>
OldDeflate = 32946,
/// <summary>
/// ITU-T Rec. T.82 coding, applying ITU-T Rec. T.85 (JBIG) (see RFC2301).
///
/// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead,
/// if this is chosen.
/// </summary>
ItuTRecT82 = 9,
/// <summary>
/// ITU-T Rec. T.43 representation, using ITU-T Rec. T.82 (JBIG) (see RFC2301).
///
/// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead,
/// if this is chosen.
/// </summary>
ItuTRecT43 = 10
}
}

113
src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs

@ -0,0 +1,113 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Defines constants defined in the TIFF specification.
/// </summary>
internal static class TiffConstants
{
/// <summary>
/// Byte order markers for indicating little endian encoding.
/// </summary>
public const byte ByteOrderLittleEndian = 0x49;
/// <summary>
/// Byte order markers for indicating big endian encoding.
/// </summary>
public const byte ByteOrderBigEndian = 0x4D;
/// <summary>
/// Byte order markers for indicating little endian encoding.
/// </summary>
public const ushort ByteOrderLittleEndianShort = 0x4949;
/// <summary>
/// Byte order markers for indicating big endian encoding.
/// </summary>
public const ushort ByteOrderBigEndianShort = 0x4D4D;
/// <summary>
/// Magic number used within the image file header to identify a TIFF format file.
/// </summary>
public const ushort HeaderMagicNumber = 42;
/// <summary>
/// RowsPerStrip default value, which is effectively infinity.
/// </summary>
public const int RowsPerStripInfinity = 2147483647;
/// <summary>
/// Size (in bytes) of the TIFF file header.
/// </summary>
public const int SizeOfTiffHeader = 8;
/// <summary>
/// Size (in bytes) of each individual TIFF IFD entry
/// </summary>
public const int SizeOfIfdEntry = 12;
/// <summary>
/// Size (in bytes) of the Short and SShort data types
/// </summary>
public const int SizeOfShort = 2;
/// <summary>
/// Size (in bytes) of the Long and SLong data types
/// </summary>
public const int SizeOfLong = 4;
/// <summary>
/// Size (in bytes) of the Rational and SRational data types
/// </summary>
public const int SizeOfRational = 8;
/// <summary>
/// Size (in bytes) of the Float data type
/// </summary>
public const int SizeOfFloat = 4;
/// <summary>
/// Size (in bytes) of the Double data type
/// </summary>
public const int SizeOfDouble = 8;
/// <summary>
/// The default strip size is 8k.
/// </summary>
public const int DefaultStripSize = 8 * 1024;
/// <summary>
/// The bits per sample for 1 bit bicolor images.
/// </summary>
public static readonly ushort[] BitsPerSample1Bit = { 1 };
/// <summary>
/// The bits per sample for images with a 4 color palette.
/// </summary>
public static readonly ushort[] BitsPerSample4Bit = { 4 };
/// <summary>
/// The bits per sample for 8 bit images.
/// </summary>
public static readonly ushort[] BitsPerSample8Bit = { 8 };
/// <summary>
/// The bits per sample for images with 8 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb8Bit = { 8, 8, 8 };
/// <summary>
/// The list of mimetypes that equate to a tiff.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/tiff", "image/tiff-fx" };
/// <summary>
/// The list of file extensions that equate to a tiff.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "tiff", "tif" };
}
}

26
src/ImageSharp/Formats/Tiff/Constants/TiffExtraSamples.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Enumeration representing the possible uses of extra components in TIFF format files.
/// </summary>
internal enum TiffExtraSamples
{
/// <summary>
/// Unspecified data.
/// </summary>
Unspecified = 0,
/// <summary>
/// Associated alpha data (with pre-multiplied color).
/// </summary>
AssociatedAlpha = 1,
/// <summary>
/// Unassociated alpha data.
/// </summary>
UnassociatedAlpha = 2
}
}

21
src/ImageSharp/Formats/Tiff/Constants/TiffFillOrder.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Enumeration representing the fill orders defined by the Tiff file-format.
/// </summary>
internal enum TiffFillOrder : ushort
{
/// <summary>
/// Pixels with lower column values are stored in the higher-order bits of the byte.
/// </summary>
MostSignificantBitFirst = 1,
/// <summary>
/// Pixels with lower column values are stored in the lower-order bits of the byte.
/// </summary>
LeastSignificantBitFirst = 2
}
}

44
src/ImageSharp/Formats/Tiff/Constants/TiffNewSubfileType.cs

@ -0,0 +1,44 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Enumeration representing the sub-file types defined by the Tiff file-format.
/// </summary>
[Flags]
public enum TiffNewSubfileType : uint
{
/// <summary>
/// A full-resolution image.
/// </summary>
FullImage = 0,
/// <summary>
/// Reduced-resolution version of another image in this TIFF file.
/// </summary>
Preview = 1,
/// <summary>
/// A single page of a multi-page image.
/// </summary>
SinglePage = 2,
/// <summary>
/// A transparency mask for another image in this TIFF file.
/// </summary>
TransparencyMask = 4,
/// <summary>
/// Alternative reduced-resolution version of another image in this TIFF file (see DNG specification).
/// </summary>
AlternativePreview = 65536,
/// <summary>
/// Mixed raster content (see RFC2301).
/// </summary>
MixedRasterContent = 8
}
}

51
src/ImageSharp/Formats/Tiff/Constants/TiffOrientation.cs

@ -0,0 +1,51 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Enumeration representing the image orientations defined by the Tiff file-format.
/// </summary>
internal enum TiffOrientation
{
/// <summary>
/// The 0th row and 0th column represent the visual top and left-hand side of the image respectively.
/// </summary>
TopLeft = 1,
/// <summary>
/// The 0th row and 0th column represent the visual top and right-hand side of the image respectively.
/// </summary>
TopRight = 2,
/// <summary>
/// The 0th row and 0th column represent the visual bottom and right-hand side of the image respectively.
/// </summary>
BottomRight = 3,
/// <summary>
/// The 0th row and 0th column represent the visual bottom and left-hand side of the image respectively.
/// </summary>
BottomLeft = 4,
/// <summary>
/// The 0th row and 0th column represent the visual left-hand side and top of the image respectively.
/// </summary>
LeftTop = 5,
/// <summary>
/// The 0th row and 0th column represent the visual right-hand side and top of the image respectively.
/// </summary>
RightTop = 6,
/// <summary>
/// The 0th row and 0th column represent the visual right-hand side and bottom of the image respectively.
/// </summary>
RightBottom = 7,
/// <summary>
/// The 0th row and 0th column represent the visual left-hand side and bottom of the image respectively.
/// </summary>
LeftBottom = 8
}
}

89
src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs

@ -0,0 +1,89 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Enumeration representing the photometric interpretation formats defined by the Tiff file-format.
/// </summary>
public enum TiffPhotometricInterpretation : ushort
{
/// <summary>
/// Bilevel and grayscale: 0 is imaged as white. The maximum value is imaged as black.
///
/// Not supported by the TiffEncoder.
/// </summary>
WhiteIsZero = 0,
/// <summary>
/// Bilevel and grayscale: 0 is imaged as black. The maximum value is imaged as white.
/// </summary>
BlackIsZero = 1,
/// <summary>
/// RGB image.
/// </summary>
Rgb = 2,
/// <summary>
/// Palette Color.
/// </summary>
PaletteColor = 3,
/// <summary>
/// A transparency mask.
///
/// Not supported by the TiffEncoder.
/// </summary>
TransparencyMask = 4,
/// <summary>
/// Separated: usually CMYK (see Section 16 of the TIFF 6.0 specification).
///
/// Not supported by the TiffEncoder.
/// </summary>
Separated = 5,
/// <summary>
/// YCbCr (see Section 21 of the TIFF 6.0 specification).
///
/// Not supported by the TiffEncoder.
/// </summary>
YCbCr = 6,
/// <summary>
/// 1976 CIE L*a*b* (see Section 23 of the TIFF 6.0 specification).
///
/// Not supported by the TiffEncoder.
/// </summary>
CieLab = 8,
/// <summary>
/// ICC L*a*b* (see TIFF Specification, supplement 1).
///
/// Not supported by the TiffEncoder.
/// </summary>
IccLab = 9,
/// <summary>
/// ITU L*a*b* (see RFC2301).
///
/// Not supported by the TiffEncoder.
/// </summary>
ItuLab = 10,
/// <summary>
/// Color Filter Array (see the DNG specification).
///
/// Not supported by the TiffEncoder.
/// </summary>
ColorFilterArray = 32803,
/// <summary>
/// Linear Raw (see the DNG specification).
///
/// Not supported by the TiffEncoder.
/// </summary>
LinearRaw = 34892
}
}

31
src/ImageSharp/Formats/Tiff/Constants/TiffPlanarConfiguration.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Enumeration representing how the components of each pixel are stored the Tiff file-format.
/// </summary>
public enum TiffPlanarConfiguration : ushort
{
/// <summary>
/// Chunky format.
/// The component values for each pixel are stored contiguously.
/// The order of the components within the pixel is specified by
/// PhotometricInterpretation. For example, for RGB data, the data is stored as RGBRGBRGB.
/// </summary>
Chunky = 1,
/// <summary>
/// Planar format.
/// The components are stored in separate “component planes.” The
/// values in StripOffsets and StripByteCounts are then arranged as a 2-dimensional
/// array, with SamplesPerPixel rows and StripsPerImage columns. (All of the columns
/// for row 0 are stored first, followed by the columns of row 1, and so on.)
/// PhotometricInterpretation describes the type of data stored in each component
/// plane. For example, RGB data is stored with the Red components in one component
/// plane, the Green in another, and the Blue in another.
/// </summary>
Planar = 2
}
}

28
src/ImageSharp/Formats/Tiff/Constants/TiffPredictor.cs

@ -0,0 +1,28 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// A mathematical operator that is applied to the image data before an encoding scheme is applied.
/// </summary>
public enum TiffPredictor : ushort
{
/// <summary>
/// No prediction.
/// </summary>
None = 1,
/// <summary>
/// Horizontal differencing.
/// </summary>
Horizontal = 2,
/// <summary>
/// Floating point horizontal differencing.
///
/// Note: The Tiff Encoder does not yet support this. If this is chosen, the encoder will fallback to none.
/// </summary>
FloatingPoint = 3
}
}

41
src/ImageSharp/Formats/Tiff/Constants/TiffSampleFormat.cs

@ -0,0 +1,41 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Specifies how to interpret each data sample in a pixel.
/// </summary>
public enum TiffSampleFormat : ushort
{
/// <summary>
/// Unsigned integer data. Default value.
/// </summary>
UnsignedInteger = 1,
/// <summary>
/// Signed integer data.
/// </summary>
SignedInteger = 2,
/// <summary>
/// IEEE floating point data.
/// </summary>
Float = 3,
/// <summary>
/// Undefined data format.
/// </summary>
Undefined = 4,
/// <summary>
/// The complex int.
/// </summary>
ComplexInt = 5,
/// <summary>
/// The complex float.
/// </summary>
ComplexFloat = 6
}
}

26
src/ImageSharp/Formats/Tiff/Constants/TiffSubfileType.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Enumeration representing the sub-file types defined by the Tiff file-format.
/// </summary>
public enum TiffSubfileType : ushort
{
/// <summary>
/// Full-resolution image data.
/// </summary>
FullImage = 1,
/// <summary>
/// Reduced-resolution image data.
/// </summary>
Preview = 2,
/// <summary>
/// A single page of a multi-page image.
/// </summary>
SinglePage = 3
}
}

26
src/ImageSharp/Formats/Tiff/Constants/TiffThresholding.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.Constants
{
/// <summary>
/// Enumeration representing the thresholding applied to image data defined by the Tiff file-format.
/// </summary>
internal enum TiffThresholding
{
/// <summary>
/// No dithering or halftoning.
/// </summary>
None = 1,
/// <summary>
/// An ordered dither or halftone technique.
/// </summary>
Ordered = 2,
/// <summary>
/// A randomized process such as error diffusion.
/// </summary>
Random = 3
}
}

16
src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// Encapsulates the options for the <see cref="TiffDecoder"/>.
/// </summary>
internal interface ITiffDecoderOptions
{
/// <summary>
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
bool IgnoreMetadata { get; }
}
}

47
src/ImageSharp/Formats/Tiff/ITiffEncoderOptions.cs

@ -0,0 +1,47 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// Encapsulates the options for the <see cref="TiffEncoder"/>.
/// </summary>
internal interface ITiffEncoderOptions
{
/// <summary>
/// Gets the number of bits per pixel.
/// </summary>
TiffBitsPerPixel? BitsPerPixel { get; }
/// <summary>
/// Gets the compression type to use.
/// </summary>
TiffCompression? Compression { get; }
/// <summary>
/// Gets the compression level 1-9 for the deflate compression mode.
/// <remarks>Defaults to <see cref="DeflateCompressionLevel.DefaultCompression"/>.</remarks>
/// </summary>
DeflateCompressionLevel? CompressionLevel { get; }
/// <summary>
/// Gets the PhotometricInterpretation to use. Possible options are RGB, RGB with a color palette, gray or BiColor.
/// If no PhotometricInterpretation is specified or it is unsupported by the encoder, RGB will be used.
/// </summary>
TiffPhotometricInterpretation? PhotometricInterpretation { get; }
/// <summary>
/// Gets a value indicating which horizontal prediction to use. This can improve the compression ratio with deflate or lzw compression.
/// </summary>
TiffPredictor? HorizontalPredictor { get; }
/// <summary>
/// Gets the quantizer for creating a color palette image.
/// </summary>
IQuantizer Quantizer { get; }
}
}

104
src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs

@ -0,0 +1,104 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// The TIFF IFD reader class.
/// </summary>
internal class DirectoryReader
{
private readonly Stream stream;
private uint nextIfdOffset;
// used for sequential read big values (actual for multiframe big files)
// todo: different tags can link to the same data (stream offset) - investigate
private readonly SortedList<uint, Action> lazyLoaders = new SortedList<uint, Action>(new DuplicateKeyComparer<uint>());
public DirectoryReader(Stream stream) => this.stream = stream;
/// <summary>
/// Gets the byte order.
/// </summary>
public ByteOrder ByteOrder { get; private set; }
/// <summary>
/// Reads image file directories.
/// </summary>
/// <returns>Image file directories.</returns>
public IEnumerable<ExifProfile> Read()
{
this.ByteOrder = ReadByteOrder(this.stream);
this.nextIfdOffset = new HeaderReader(this.stream, this.ByteOrder).ReadFileHeader();
return this.ReadIfds();
}
private static ByteOrder ReadByteOrder(Stream stream)
{
var headerBytes = new byte[2];
stream.Read(headerBytes, 0, 2);
if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian)
{
return ByteOrder.LittleEndian;
}
else if (headerBytes[0] == TiffConstants.ByteOrderBigEndian && headerBytes[1] == TiffConstants.ByteOrderBigEndian)
{
return ByteOrder.BigEndian;
}
throw TiffThrowHelper.ThrowInvalidHeader();
}
private IEnumerable<ExifProfile> ReadIfds()
{
var readers = new List<EntryReader>();
while (this.nextIfdOffset != 0 && this.nextIfdOffset < this.stream.Length)
{
var reader = new EntryReader(this.stream, this.ByteOrder, this.nextIfdOffset, this.lazyLoaders);
reader.ReadTags();
this.nextIfdOffset = reader.NextIfdOffset;
readers.Add(reader);
}
// Sequential reading big values.
foreach (Action loader in this.lazyLoaders.Values)
{
loader();
}
var list = new List<ExifProfile>();
foreach (EntryReader reader in readers)
{
var profile = new ExifProfile(reader.Values, reader.InvalidTags);
list.Add(profile);
}
return list;
}
/// <summary>
/// <see cref="DuplicateKeyComparer{TKey}"/> used for possibility add a duplicate offsets (but tags don't duplicate).
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
private class DuplicateKeyComparer<TKey> : IComparer<TKey>
where TKey : IComparable
{
public int Compare(TKey x, TKey y)
{
int result = x.CompareTo(y);
// Handle equality as being greater.
return (result == 0) ? 1 : result;
}
}
}
}

65
src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs

@ -0,0 +1,65 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Tiff
{
internal class EntryReader : BaseExifReader
{
private readonly uint startOffset;
private readonly SortedList<uint, Action> lazyLoaders;
public EntryReader(Stream stream, ByteOrder byteOrder, uint ifdOffset, SortedList<uint, Action> lazyLoaders)
: base(stream)
{
this.IsBigEndian = byteOrder == ByteOrder.BigEndian;
this.startOffset = ifdOffset;
this.lazyLoaders = lazyLoaders;
}
public List<IExifValue> Values { get; } = new List<IExifValue>();
public uint NextIfdOffset { get; private set; }
public void ReadTags()
{
this.ReadValues(this.Values, this.startOffset);
this.NextIfdOffset = this.ReadUInt32();
this.ReadSubIfd(this.Values);
}
protected override void RegisterExtLoader(uint offset, Action reader) =>
this.lazyLoaders.Add(offset, reader);
}
internal class HeaderReader : BaseExifReader
{
public HeaderReader(Stream stream, ByteOrder byteOrder)
: base(stream) =>
this.IsBigEndian = byteOrder == ByteOrder.BigEndian;
public uint FirstIfdOffset { get; private set; }
public uint ReadFileHeader()
{
ushort magic = this.ReadUInt16();
if (magic != TiffConstants.HeaderMagicNumber)
{
TiffThrowHelper.ThrowInvalidHeader();
}
this.FirstIfdOffset = this.ReadUInt32();
return this.FirstIfdOffset;
}
protected override void RegisterExtLoader(uint offset, Action reader) => throw new NotSupportedException();
}
}

28
src/ImageSharp/Formats/Tiff/MetadataExtensions.cs

@ -0,0 +1,28 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Extension methods for the <see cref="ImageMetadata"/> type.
/// </summary>
public static partial class MetadataExtensions
{
/// <summary>
/// Gets the tiff format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="TiffMetadata"/>.</returns>
public static TiffMetadata GetTiffMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(TiffFormat.Instance);
/// <summary>
/// Gets the tiff format specific metadata for the image frame.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="TiffFrameMetadata"/>.</returns>
public static TiffFrameMetadata GetTiffMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(TiffFormat.Instance);
}
}

46
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor{TPixel}.cs

@ -0,0 +1,46 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'BlackIsZero' photometric interpretation (optimized for bilevel images).
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class BlackIsZero1TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
int offset = 0;
Color black = Color.Black;
Color white = Color.White;
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x += 8)
{
byte b = data[offset++];
int maxShift = Math.Min(left + width - x, 8);
for (int shift = 0; shift < maxShift; shift++)
{
int bit = (b >> (7 - shift)) & 1;
color.FromRgba32(bit == 0 ? black : white);
pixels[x + shift, y] = color;
}
}
}
}
}
}

58
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor{TPixel}.cs

@ -0,0 +1,58 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'BlackIsZero' photometric interpretation (optimized for 4-bit grayscale images).
/// </summary>
internal class BlackIsZero4TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
int offset = 0;
bool isOddWidth = (width & 1) == 1;
var l8 = default(L8);
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width - 1;)
{
byte byteData = data[offset++];
byte intensity1 = (byte)(((byteData & 0xF0) >> 4) * 17);
l8.PackedValue = intensity1;
color.FromL8(l8);
pixels[x++, y] = color;
byte intensity2 = (byte)((byteData & 0x0F) * 17);
l8.PackedValue = intensity2;
color.FromL8(l8);
pixels[x++, y] = color;
}
if (isOddWidth)
{
byte byteData = data[offset++];
byte intensity1 = (byte)(((byteData & 0xF0) >> 4) * 17);
l8.PackedValue = intensity1;
color.FromL8(l8);
pixels[left + width - 1, y] = color;
}
}
}
}
}

39
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs

@ -0,0 +1,39 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'BlackIsZero' photometric interpretation (optimized for 8-bit grayscale images).
/// </summary>
internal class BlackIsZero8TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
int offset = 0;
var l8 = default(L8);
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x++)
{
byte intensity = data[offset++];
l8.PackedValue = intensity;
color.FromL8(l8);
pixels[x, y] = color;
}
}
}
}
}

50
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'BlackIsZero' photometric interpretation (for all bit depths).
/// </summary>
internal class BlackIsZeroTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ushort bitsPerSample0;
private readonly float factor;
public BlackIsZeroTiffColor(ushort[] bitsPerSample)
{
this.bitsPerSample0 = bitsPerSample[0];
this.factor = (1 << this.bitsPerSample0) - 1.0f;
}
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
var bitReader = new BitReader(data);
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x++)
{
int value = bitReader.ReadBits(this.bitsPerSample0);
float intensity = value / this.factor;
color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f));
pixels[x, y] = color;
}
bitReader.NextRow();
}
}
}
}

67
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs

@ -0,0 +1,67 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'PaletteTiffColor' photometric interpretation (for all bit depths).
/// </summary>
internal class PaletteTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ushort bitsPerSample0;
private readonly TPixel[] palette;
/// <param name="bitsPerSample">The number of bits per sample for each pixel.</param>
/// <param name="colorMap">The RGB color lookup table to use for decoding the image.</param>
public PaletteTiffColor(ushort[] bitsPerSample, ushort[] colorMap)
{
this.bitsPerSample0 = bitsPerSample[0];
int colorCount = 1 << this.bitsPerSample0;
this.palette = GeneratePalette(colorMap, colorCount);
}
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var bitReader = new BitReader(data);
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x++)
{
int index = bitReader.ReadBits(this.bitsPerSample0);
pixels[x, y] = this.palette[index];
}
bitReader.NextRow();
}
}
private static TPixel[] GeneratePalette(ushort[] colorMap, int colorCount)
{
var palette = new TPixel[colorCount];
const int rOffset = 0;
int gOffset = colorCount;
int bOffset = colorCount * 2;
for (int i = 0; i < palette.Length; i++)
{
float r = colorMap[rOffset + i] / 65535F;
float g = colorMap[gOffset + i] / 65535F;
float b = colorMap[bOffset + i] / 65535F;
palette[i].FromVector4(new Vector4(r, g, b, 1.0f));
}
return palette;
}
}
}

43
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'RGB' photometric interpretation (optimized for 8-bit full color images).
/// </summary>
internal class Rgb888TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
int offset = 0;
var rgba = default(Rgba32);
for (int y = top; y < top + height; y++)
{
Span<TPixel> pixelRow = pixels.GetRowSpan(y);
for (int x = left; x < left + width; x++)
{
byte r = data[offset++];
byte g = data[offset++];
byte b = data[offset++];
rgba.PackedValue = (uint)(r | (g << 8) | (b << 16) | (0xff << 24));
color.FromRgba32(rgba);
pixelRow[x] = color;
}
}
}
}
}

76
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs

@ -0,0 +1,76 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'RGB' photometric interpretation with 'Planar' layout (for all bit depths).
/// </summary>
internal class RgbPlanarTiffColor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly float rFactor;
private readonly float gFactor;
private readonly float bFactor;
private readonly ushort bitsPerSampleR;
private readonly ushort bitsPerSampleG;
private readonly ushort bitsPerSampleB;
public RgbPlanarTiffColor(ushort[] bitsPerSample)
/* : base(bitsPerSample, null) */
{
this.bitsPerSampleR = bitsPerSample[0];
this.bitsPerSampleG = bitsPerSample[1];
this.bitsPerSampleB = bitsPerSample[2];
this.rFactor = (1 << this.bitsPerSampleR) - 1.0f;
this.gFactor = (1 << this.bitsPerSampleG) - 1.0f;
this.bFactor = (1 << this.bitsPerSampleB) - 1.0f;
}
/// <summary>
/// Decodes pixel data using the current photometric interpretation.
/// </summary>
/// <param name="data">The buffers to read image data from.</param>
/// <param name="pixels">The image buffer to write pixels to.</param>
/// <param name="left">The x-coordinate of the left-hand side of the image block.</param>
/// <param name="top">The y-coordinate of the top of the image block.</param>
/// <param name="width">The width of the image block.</param>
/// <param name="height">The height of the image block.</param>
public void Decode(IManagedByteBuffer[] data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
var rBitReader = new BitReader(data[0].GetSpan());
var gBitReader = new BitReader(data[1].GetSpan());
var bBitReader = new BitReader(data[2].GetSpan());
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x++)
{
float r = rBitReader.ReadBits(this.bitsPerSampleR) / this.rFactor;
float g = gBitReader.ReadBits(this.bitsPerSampleG) / this.gFactor;
float b = bBitReader.ReadBits(this.bitsPerSampleB) / this.bFactor;
color.FromVector4(new Vector4(r, g, b, 1.0f));
pixels[x, y] = color;
}
rBitReader.NextRow();
gBitReader.NextRow();
bBitReader.NextRow();
}
}
}
}

64
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs

@ -0,0 +1,64 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'RGB' photometric interpretation (for all bit depths).
/// </summary>
internal class RgbTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly float rFactor;
private readonly float gFactor;
private readonly float bFactor;
private readonly ushort bitsPerSampleR;
private readonly ushort bitsPerSampleG;
private readonly ushort bitsPerSampleB;
public RgbTiffColor(ushort[] bitsPerSample)
{
this.bitsPerSampleR = bitsPerSample[0];
this.bitsPerSampleG = bitsPerSample[1];
this.bitsPerSampleB = bitsPerSample[2];
this.rFactor = (1 << this.bitsPerSampleR) - 1.0f;
this.gFactor = (1 << this.bitsPerSampleG) - 1.0f;
this.bFactor = (1 << this.bitsPerSampleB) - 1.0f;
}
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
var bitReader = new BitReader(data);
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x++)
{
float r = bitReader.ReadBits(this.bitsPerSampleR) / this.rFactor;
float g = bitReader.ReadBits(this.bitsPerSampleG) / this.gFactor;
float b = bitReader.ReadBits(this.bitsPerSampleB) / this.bFactor;
color.FromVector4(new Vector4(r, g, b, 1.0f));
pixels[x, y] = color;
}
bitReader.NextRow();
}
}
}
}

28
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffBaseColorDecoder{TPixel}.cs

@ -0,0 +1,28 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// The base class for photometric interpretation decoders.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Decodes source raw pixel data using the current photometric interpretation.
/// </summary>
/// <param name="data">The buffer to read image data from.</param>
/// <param name="pixels">The image buffer to write pixels to.</param>
/// <param name="left">The x-coordinate of the left-hand side of the image block.</param>
/// <param name="top">The y-coordinate of the top of the image block.</param>
/// <param name="width">The width of the image block.</param>
/// <param name="height">The height of the image block.</param>
public abstract void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height);
}
}

94
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs

@ -0,0 +1,94 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
internal static class TiffColorDecoderFactory<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
public static TiffBaseColorDecoder<TPixel> Create(TiffColorType colorType, ushort[] bitsPerSample, ushort[] colorMap)
{
switch (colorType)
{
case TiffColorType.WhiteIsZero:
DebugGuard.IsTrue(bitsPerSample.Length == 1, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new WhiteIsZeroTiffColor<TPixel>(bitsPerSample);
case TiffColorType.WhiteIsZero1:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 1, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new WhiteIsZero1TiffColor<TPixel>();
case TiffColorType.WhiteIsZero4:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 4, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new WhiteIsZero4TiffColor<TPixel>();
case TiffColorType.WhiteIsZero8:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 8, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new WhiteIsZero8TiffColor<TPixel>();
case TiffColorType.BlackIsZero:
DebugGuard.IsTrue(bitsPerSample.Length == 1, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new BlackIsZeroTiffColor<TPixel>(bitsPerSample);
case TiffColorType.BlackIsZero1:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 1, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new BlackIsZero1TiffColor<TPixel>();
case TiffColorType.BlackIsZero4:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 4, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new BlackIsZero4TiffColor<TPixel>();
case TiffColorType.BlackIsZero8:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 8, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new BlackIsZero8TiffColor<TPixel>();
case TiffColorType.Rgb:
DebugGuard.NotNull(bitsPerSample, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.Rgb888:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[0] == 8
&& bitsPerSample[1] == 8
&& bitsPerSample[2] == 8,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new Rgb888TiffColor<TPixel>();
case TiffColorType.PaletteColor:
DebugGuard.NotNull(bitsPerSample, "bitsPerSample");
DebugGuard.NotNull(colorMap, "colorMap");
return new PaletteTiffColor<TPixel>(bitsPerSample, colorMap);
default:
throw TiffThrowHelper.InvalidColorType(colorType.ToString());
}
}
public static RgbPlanarTiffColor<TPixel> CreatePlanar(TiffColorType colorType, ushort[] bitsPerSample, ushort[] colorMap)
{
switch (colorType)
{
case TiffColorType.RgbPlanar:
DebugGuard.NotNull(bitsPerSample, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbPlanarTiffColor<TPixel>(bitsPerSample);
default:
throw TiffThrowHelper.InvalidColorType(colorType.ToString());
}
}
}
}

71
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Provides enumeration of the various TIFF photometric interpretation implementation types.
/// </summary>
internal enum TiffColorType
{
/// <summary>
/// Grayscale: 0 is imaged as black. The maximum value is imaged as white.
/// </summary>
BlackIsZero,
/// <summary>
/// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for bilevel images.
/// </summary>
BlackIsZero1,
/// <summary>
/// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 4-bit images.
/// </summary>
BlackIsZero4,
/// <summary>
/// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimized implementation for 8-bit images.
/// </summary>
BlackIsZero8,
/// <summary>
/// Grayscale: 0 is imaged as white. The maximum value is imaged as black.
/// </summary>
WhiteIsZero,
/// <summary>
/// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for bilevel images.
/// </summary>
WhiteIsZero1,
/// <summary>
/// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 4-bit images.
/// </summary>
WhiteIsZero4,
/// <summary>
/// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimized implementation for 8-bit images.
/// </summary>
WhiteIsZero8,
/// <summary>
/// Palette-color.
/// </summary>
PaletteColor,
/// <summary>
/// RGB Full Color.
/// </summary>
Rgb,
/// <summary>
/// RGB Full Color. Optimized implementation for 8-bit images.
/// </summary>
Rgb888,
/// <summary>
/// RGB Full Color. Planar configuration of data.
/// </summary>
RgbPlanar,
}
}

45
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor{TPixel}.cs

@ -0,0 +1,45 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'WhiteIsZero' photometric interpretation (optimized for bilevel images).
/// </summary>
internal class WhiteIsZero1TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
int offset = 0;
Color black = Color.Black;
Color white = Color.White;
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x += 8)
{
byte b = data[offset++];
int maxShift = Math.Min(left + width - x, 8);
for (int shift = 0; shift < maxShift; shift++)
{
int bit = (b >> (7 - shift)) & 1;
color.FromRgba32(bit == 0 ? white : black);
pixels[x + shift, y] = color;
}
}
}
}
}
}

58
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor{TPixel}.cs

@ -0,0 +1,58 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'WhiteIsZero' photometric interpretation (optimized for 4-bit grayscale images).
/// </summary>
internal class WhiteIsZero4TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
int offset = 0;
bool isOddWidth = (width & 1) == 1;
var l8 = default(L8);
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width - 1;)
{
byte byteData = data[offset++];
byte intensity1 = (byte)((15 - ((byteData & 0xF0) >> 4)) * 17);
l8.PackedValue = intensity1;
color.FromL8(l8);
pixels[x++, y] = color;
byte intensity2 = (byte)((15 - (byteData & 0x0F)) * 17);
l8.PackedValue = intensity2;
color.FromL8(l8);
pixels[x++, y] = color;
}
if (isOddWidth)
{
byte byteData = data[offset++];
byte intensity1 = (byte)((15 - ((byteData & 0xF0) >> 4)) * 17);
l8.PackedValue = intensity1;
color.FromL8(l8);
pixels[left + width - 1, y] = color;
}
}
}
}
}

39
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs

@ -0,0 +1,39 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'WhiteIsZero' photometric interpretation (optimized for 8-bit grayscale images).
/// </summary>
internal class WhiteIsZero8TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
int offset = 0;
var l8 = default(L8);
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x++)
{
byte intensity = (byte)(255 - data[offset++]);
l8.PackedValue = intensity;
color.FromL8(l8);
pixels[x, y] = color;
}
}
}
}
}

50
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'WhiteIsZero' photometric interpretation (for all bit depths).
/// </summary>
internal class WhiteIsZeroTiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ushort bitsPerSample0;
private readonly float factor;
public WhiteIsZeroTiffColor(ushort[] bitsPerSample)
{
this.bitsPerSample0 = bitsPerSample[0];
this.factor = (float)Math.Pow(2, this.bitsPerSample0) - 1.0f;
}
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
var bitReader = new BitReader(data);
for (int y = top; y < top + height; y++)
{
for (int x = left; x < left + width; x++)
{
int value = bitReader.ReadBits(this.bitsPerSample0);
float intensity = 1.0f - (value / this.factor);
color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f));
pixels[x, y] = color;
}
bitReader.NextRow();
}
}
}
}

253
src/ImageSharp/Formats/Tiff/README.md

@ -0,0 +1,253 @@
# ImageSharp TIFF codec
## References
- TIFF
- [TIFF 6.0 Specification](http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf),(http://www.npes.org/pdf/TIFF-v6.pdf)
- [TIFF Supplement 1](http://partners.adobe.com/public/developer/en/tiff/TIFFPM6.pdf)
- [TIFF Supplement 2](http://partners.adobe.com/public/developer/en/tiff/TIFFphotoshop.pdf)
- [TIFF Supplement 3](http://chriscox.org/TIFFTN3d1.pdf)
- [TIFF-F/FX Extension (RFC2301)](http://www.ietf.org/rfc/rfc2301.txt)
- [TIFF/EP Extension (Wikipedia)](https://en.wikipedia.org/wiki/TIFF/EP)
- [Adobe TIFF Pages](http://partners.adobe.com/public/developer/tiff/index.html)
- [Unofficial TIFF FAQ](http://www.awaresystems.be/imaging/tiff/faq.html)
- [CCITT T.4 Compression](https://www.itu.int/rec/T-REC-T.4-198811-S/_page.print)
- [CCITT T.6 Compression](https://www.itu.int/rec/T-REC-T.6/en)
- DNG
- [Adobe DNG Pages](https://helpx.adobe.com/photoshop/digital-negative.html)
- Metadata (EXIF)
- [EXIF 2.3 Specification](http://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf)
- Metadata (XMP)
- [Adobe XMP Pages](http://www.adobe.com/products/xmp.html)
- [Adobe XMP Developer Center](http://www.adobe.com/devnet/xmp.html)
## Implementation Status
- The Decoder and Encoder currently only supports a single frame per image.
- Some compression formats are not yet supported. See the list below.
### Deviations from the TIFF spec (to be fixed)
- Decoder
- A Baseline TIFF reader must skip over extra components (e.g. RGB with 4 samples per pixels)
- NB: Need to handle this for both planar and chunky data
- If the SampleFormat field is present and not 1 - fail gracefully if you cannot handle this
- Compression=None should treat 16/32-BitsPerSample for all samples as SHORT/LONG (for byte order and padding rows)
- Check Planar format data - is this encoded as strips in order RGBRGBRGB or RRRGGGBBB?
### Compression Formats
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------|
|None | Y | Y | |
|Ccitt1D | Y | Y | |
|PackBits | Y | Y | |
|CcittGroup3Fax | Y | Y | |
|CcittGroup4Fax | | | |
|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case |
|Old Jpeg | | | We should not even try to support this |
|Jpeg (Technote 2) | | | |
|Deflate (Technote 2) | Y | Y | Based on PNG Deflate. |
|Old Deflate (Technote 2) | | Y | |
### Photometric Interpretation Formats
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------|
|WhiteIsZero | Y | Y | General + 1/4/8-bit optimised implementations |
|BlackIsZero | Y | Y | General + 1/4/8-bit optimised implementations |
|Rgb (Chunky) | Y | Y | General + Rgb888 optimised implementation |
|Rgb (Planar) | | Y | General implementation only |
|PaletteColor | Y | Y | General implementation only |
|TransparencyMask | | | |
|Separated (TIFF Extension) | | | |
|YCbCr (TIFF Extension) | | | |
|CieLab (TIFF Extension) | | | |
|IccLab (TechNote 1) | | | |
### Baseline TIFF Tags
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------|
|NewSubfileType | | | |
|SubfileType | | | |
|ImageWidth | Y | Y | |
|ImageLength | Y | Y | |
|BitsPerSample | Y | Y | |
|Compression | Y | Y | |
|PhotometricInterpretation | Y | Y | |
|Thresholding | | | |
|CellWidth | | | |
|CellLength | | | |
|FillOrder | | - | Ignore. In practice is very uncommon, and is not recommended. |
|ImageDescription | Y | Y | |
|Make | Y | Y | |
|Model | Y | Y | |
|StripOffsets | Y | Y | |
|Orientation | | - | Ignore. Many readers ignore this tag. |
|SamplesPerPixel | Y | - | Currently ignored, as can be inferred from count of BitsPerSample |
|RowsPerStrip | Y | Y | |
|StripByteCounts | Y | Y | |
|MinSampleValue | | | |
|MaxSampleValue | | | |
|XResolution | Y | Y | |
|YResolution | Y | Y | |
|PlanarConfiguration | | Y | Encoding support only chunky. |
|FreeOffsets | | | |
|FreeByteCounts | | | |
|GrayResponseUnit | | | |
|GrayResponseCurve | | | |
|ResolutionUnit | Y | Y | |
|Software | Y | Y | |
|DateTime | Y | Y | |
|Artist | Y | Y | |
|HostComputer | Y | Y | |
|ColorMap | Y | Y | |
|ExtraSamples | | - | |
|Copyright | Y | Y | |
### Extension TIFF Tags
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------|
|NewSubfileType | | | |
|DocumentName | Y | Y | |
|PageName | | | |
|XPosition | | | |
|YPosition | | | |
|T4Options | | Y | |
|T6Options | | | |
|PageNumber | | | |
|TransferFunction | | | |
|Predictor | Y | Y | only Horizontal |
|WhitePoint | | | |
|PrimaryChromaticities | | | |
|HalftoneHints | | | |
|TileWidth | | - | |
|TileLength | | - | |
|TileOffsets | | - | |
|TileByteCounts | | - | |
|BadFaxLines | | | |
|CleanFaxData | | | |
|ConsecutiveBadFaxLines | | | |
|SubIFDs | | - | |
|InkSet | | | |
|InkNames | | | |
|NumberOfInks | | | |
|DotRange | | | |
|TargetPrinter | | | |
|SampleFormat | | - | |
|SMinSampleValue | | | |
|SMaxSampleValue | | | |
|TransferRange | | | |
|ClipPath | | | |
|XClipPathUnits | | | |
|YClipPathUnits | | | |
|Indexed | | | |
|JPEGTables | | | |
|OPIProxy | | | |
|GlobalParametersIFD | | | |
|ProfileType | | | |
|FaxProfile | | | |
|CodingMethods | | | |
|VersionYear | | | |
|ModeNumber | | | |
|Decode | | | |
|DefaultImageColor | | | |
|JPEGProc | | | |
|JPEGInterchangeFormat | | | |
|JPEGInterchangeFormatLength| | | |
|JPEGRestartInterval | | | |
|JPEGLosslessPredictors | | | |
|JPEGPointTransforms | | | |
|JPEGQTables | | | |
|JPEGDCTables | | | |
|JPEGACTables | | | |
|YCbCrCoefficients | | | |
|YCbCrSubSampling | | | |
|YCbCrPositioning | | | |
|ReferenceBlackWhite | | | |
|StripRowCounts | - | - | See RFC 2301 (File Format for Internet Fax). |
|XMP | Y | Y | |
|ImageID | | | |
|ImageLayer | | | |
### Private TIFF Tags
| |Encoder|Decoder|Comments |
|---------------------------|:-----:|:-----:|--------------------------|
|Wang Annotation | | | |
|MD FileTag | | | |
|MD ScalePixel | | | |
|MD ColorTable | | | |
|MD LabName | | | |
|MD SampleInfo | | | |
|MD PrepDate | | | |
|MD PrepTime | | | |
|MD FileUnits | | | |
|ModelPixelScaleTag | | | |
|IPTC | Y | Y | |
|INGR Packet Data Tag | | | |
|INGR Flag Registers | | | |
|IrasB Transformation Matrix| | | |
|ModelTiepointTag | | | |
|ModelTransformationTag | | | |
|Photoshop | | | |
|Exif IFD | | - | 0x8769 SubExif |
|ICC Profile | Y | Y | |
|GeoKeyDirectoryTag | | | |
|GeoDoubleParamsTag | | | |
|GeoAsciiParamsTag | | | |
|GPS IFD | | | |
|HylaFAX FaxRecvParams | | | |
|HylaFAX FaxSubAddress | | | |
|HylaFAX FaxRecvTime | | | |
|ImageSourceData | | | |
|Interoperability IFD | | | |
|GDAL_METADATA | | | |
|GDAL_NODATA | | | |
|Oce Scanjob Description | | | |
|Oce Application Selector | | | |
|Oce Identification Number | | | |
|Oce ImageLogic Characteristics| | | |
|DNGVersion | | | |
|DNGBackwardVersion | | | |
|UniqueCameraModel | | | |
|LocalizedCameraModel | | | |
|CFAPlaneColor | | | |
|CFALayout | | | |
|LinearizationTable | | | |
|BlackLevelRepeatDim | | | |
|BlackLevel | | | |
|BlackLevelDeltaH | | | |
|BlackLevelDeltaV | | | |
|WhiteLevel | | | |
|DefaultScale | | | |
|DefaultCropOrigin | | | |
|DefaultCropSize | | | |
|ColorMatrix1 | | | |
|ColorMatrix2 | | | |
|CameraCalibration1 | | | |
|CameraCalibration2 | | | |
|ReductionMatrix1 | | | |
|ReductionMatrix2 | | | |
|AnalogBalance | | | |
|AsShotNeutral | | | |
|AsShotWhiteXY | | | |
|BaselineExposure | | | |
|BaselineNoise | | | |
|BaselineSharpness | | | |
|BayerGreenSplit | | | |
|LinearResponseLimit | | | |
|CameraSerialNumber | | | |
|LensInfo | | | |
|ChromaBlurRadius | | | |
|AntiAliasStrength | | | |
|DNGPrivateData | | | |
|MakerNoteSafety | | | |
|CalibrationIlluminant1 | | | |
|CalibrationIlluminant2 | | | |
|BestQualityScale | | | |
|Alias Layer Metadata | | | |

BIN
src/ImageSharp/Formats/Tiff/T-REC-T.4-198811-S!!PDF-E.pdf

Binary file not shown.

BIN
src/ImageSharp/Formats/Tiff/T-REC-T.6-198811-I!!PDF-E.pdf

Binary file not shown.

BIN
src/ImageSharp/Formats/Tiff/TIFF-v6.pdf

Binary file not shown.

31
src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// Enumerates the available bits per pixel for the tiff format.
/// </summary>
public enum TiffBitsPerPixel
{
/// <summary>
/// 1 bit per pixel, for bi-color image.
/// </summary>
Bit1 = 1,
/// <summary>
/// 4 bits per pixel, for images with a color palette.
/// </summary>
Bit4 = 4,
/// <summary>
/// 8 bits per pixel, grayscale or color palette images.
/// </summary>
Bit8 = 8,
/// <summary>
/// 24 bits per pixel. One byte for each color channel.
/// </summary>
Bit24 = 24,
}
}

36
src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs

@ -0,0 +1,36 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// The number of bits per component.
/// </summary>
public enum TiffBitsPerSample
{
/// <summary>
/// The Bits per samples is not known.
/// </summary>
Unknown = 0,
/// <summary>
/// One bit per sample for bicolor images.
/// </summary>
Bit1 = 1,
/// <summary>
/// Four bits per sample for grayscale images with 16 different levels of gray or paletted images with a palette of 16 colors.
/// </summary>
Bit4 = 4,
/// <summary>
/// Eight bits per sample for grayscale images with 256 different levels of gray or paletted images with a palette of 256 colors.
/// </summary>
Bit8 = 8,
/// <summary>
/// 24 bits per sample, each color channel has 8 Bits.
/// </summary>
Bit24 = 24,
}
}

75
src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs

@ -0,0 +1,75 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
namespace SixLabors.ImageSharp.Formats.Tiff
{
internal static class TiffBitsPerSampleExtensions
{
/// <summary>
/// Gets the bits per channel array for a given BitsPerSample value, e,g, for RGB888: [8, 8, 8]
/// </summary>
/// <param name="tiffBitsPerSample">The tiff bits per sample.</param>
/// <returns>Bits per sample array.</returns>
public static ushort[] Bits(this TiffBitsPerSample tiffBitsPerSample)
{
switch (tiffBitsPerSample)
{
case TiffBitsPerSample.Bit1:
return TiffConstants.BitsPerSample1Bit;
case TiffBitsPerSample.Bit4:
return TiffConstants.BitsPerSample4Bit;
case TiffBitsPerSample.Bit8:
return TiffConstants.BitsPerSample8Bit;
case TiffBitsPerSample.Bit24:
return TiffConstants.BitsPerSampleRgb8Bit;
default:
return Array.Empty<ushort>();
}
}
/// <summary>
/// Maps an array of bits per sample to a concrete enum value.
/// </summary>
/// <param name="bitsPerSample">The bits per sample array.</param>
/// <returns>TiffBitsPerSample enum value.</returns>
public static TiffBitsPerSample GetBitsPerSample(this ushort[] bitsPerSample)
{
switch (bitsPerSample.Length)
{
case 3:
if (bitsPerSample[0] == TiffConstants.BitsPerSampleRgb8Bit[0] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb8Bit[1] &&
bitsPerSample[2] == TiffConstants.BitsPerSampleRgb8Bit[2])
{
return TiffBitsPerSample.Bit24;
}
break;
case 1:
if (bitsPerSample[0] == TiffConstants.BitsPerSample1Bit[0])
{
return TiffBitsPerSample.Bit1;
}
if (bitsPerSample[0] == TiffConstants.BitsPerSample4Bit[0])
{
return TiffBitsPerSample.Bit4;
}
if (bitsPerSample[0] == TiffConstants.BitsPerSample8Bit[0])
{
return TiffBitsPerSample.Bit8;
}
break;
}
return TiffBitsPerSample.Unknown;
}
}
}

19
src/ImageSharp/Formats/Tiff/TiffConfigurationModule.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the TIFF format.
/// </summary>
public sealed class TiffConfigurationModule : IConfigurationModule
{
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetEncoder(TiffFormat.Instance, new TiffEncoder());
configuration.ImageFormatsManager.SetDecoder(TiffFormat.Instance, new TiffDecoder());
configuration.ImageFormatsManager.AddImageFormatDetector(new TiffImageFormatDetector());
}
}
}

67
src/ImageSharp/Formats/Tiff/TiffDecoder.cs

@ -0,0 +1,67 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// Image decoder for generating an image out of a TIFF stream.
/// </summary>
public class TiffDecoder : IImageDecoder, ITiffDecoderOptions, IImageInfoDetector
{
/// <summary>
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
public bool IgnoreMetadata { get; set; }
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(Configuration configuration, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(stream, "stream");
var decoder = new TiffDecoderCore(configuration, this);
return decoder.Decode<TPixel>(configuration, stream);
}
/// <inheritdoc/>
public Image Decode(Configuration configuration, Stream stream) => this.Decode<Rgba32>(configuration, stream);
/// <inheritdoc/>
public Task<Image<TPixel>> DecodeAsync<TPixel>(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(stream, nameof(stream));
var decoder = new TiffDecoderCore(configuration, this);
return decoder.DecodeAsync<TPixel>(configuration, stream, cancellationToken);
}
/// <inheritdoc/>
public async Task<Image> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken)
=> await this.DecodeAsync<Rgba32>(configuration, stream, cancellationToken)
.ConfigureAwait(false);
/// <inheritdoc/>
public IImageInfo Identify(Configuration configuration, Stream stream)
{
Guard.NotNull(stream, nameof(stream));
var decoder = new TiffDecoderCore(configuration, this);
return decoder.Identify(configuration, stream);
}
/// <inheritdoc/>
public Task<IImageInfo> IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(stream, nameof(stream));
var decoder = new TiffDecoderCore(configuration, this);
return decoder.IdentifyAsync(configuration, stream, cancellationToken);
}
}
}

339
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -0,0 +1,339 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// Performs the tiff decoding operation.
/// </summary>
internal class TiffDecoderCore : IImageDecoderInternals
{
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
private readonly bool ignoreMetadata;
/// <summary>
/// The stream to decode from.
/// </summary>
private BufferedReadStream inputStream;
/// <summary>
/// Initializes a new instance of the <see cref="TiffDecoderCore" /> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="options">The decoder options.</param>
public TiffDecoderCore(Configuration configuration, ITiffDecoderOptions options)
{
options ??= new TiffDecoder();
this.Configuration = configuration ?? Configuration.Default;
this.ignoreMetadata = options.IgnoreMetadata;
this.memoryAllocator = this.Configuration.MemoryAllocator;
}
/// <summary>
/// Gets or sets the number of bits per component of the pixel format used to decode the image.
/// </summary>
public TiffBitsPerSample BitsPerSample { get; set; }
/// <summary>
/// Gets or sets the bits per pixel.
/// </summary>
public int BitsPerPixel { get; set; }
/// <summary>
/// Gets or sets the lookup table for RGB palette colored images.
/// </summary>
public ushort[] ColorMap { get; set; }
/// <summary>
/// Gets or sets the photometric interpretation implementation to use when decoding the image.
/// </summary>
public TiffColorType ColorType { get; set; }
/// <summary>
/// Gets or sets the compression used, when the image was encoded.
/// </summary>
public TiffDecoderCompressionType CompressionType { get; set; }
/// <summary>
/// Gets or sets the Fax specific compression options.
/// </summary>
public FaxCompressionOptions FaxCompressionOptions { get; set; }
/// <summary>
/// Gets or sets the planar configuration type to use when decoding the image.
/// </summary>
public TiffPlanarConfiguration PlanarConfiguration { get; set; }
/// <summary>
/// Gets or sets the photometric interpretation.
/// </summary>
public TiffPhotometricInterpretation PhotometricInterpretation { get; set; }
/// <summary>
/// Gets or sets the horizontal predictor.
/// </summary>
public TiffPredictor Predictor { get; set; }
/// <inheritdoc/>
public Configuration Configuration { get; }
/// <inheritdoc/>
public Size Dimensions { get; private set; }
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
this.inputStream = stream;
var reader = new DirectoryReader(stream);
IEnumerable<ExifProfile> directories = reader.Read();
var frames = new List<ImageFrame<TPixel>>();
foreach (ExifProfile ifd in directories)
{
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(ifd);
frames.Add(frame);
}
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder);
// TODO: Tiff frames can have different sizes
ImageFrame<TPixel> root = frames[0];
this.Dimensions = root.Size();
foreach (ImageFrame<TPixel> frame in frames)
{
if (frame.Size() != root.Size())
{
TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported");
}
}
return new Image<TPixel>(this.Configuration, metadata, frames);
}
/// <inheritdoc/>
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.inputStream = stream;
var reader = new DirectoryReader(stream);
IEnumerable<ExifProfile> directories = reader.Read();
ExifProfile rootFrameExifProfile = directories.First();
var rootMetadata = TiffFrameMetadata.Parse(rootFrameExifProfile);
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(reader.ByteOrder, rootFrameExifProfile);
int width = GetImageWidth(rootFrameExifProfile);
int height = GetImageHeight(rootFrameExifProfile);
return new ImageInfo(new PixelTypeInfo((int)rootMetadata.BitsPerPixel), width, height, metadata);
}
/// <summary>
/// Decodes the image data from a specified IFD.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="tags">The IFD tags.</param>
/// <returns>
/// The tiff frame.
/// </returns>
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ?
new ImageFrameMetadata() :
new ImageFrameMetadata { ExifProfile = tags, XmpProfile = tags.GetValue(ExifTag.XMP)?.Value };
TiffFrameMetadata tiffFrameMetaData = imageFrameMetaData.GetTiffMetadata();
TiffFrameMetadata.Parse(tiffFrameMetaData, tags);
this.VerifyAndParse(tags, tiffFrameMetaData);
int width = GetImageWidth(tags);
int height = GetImageHeight(tags);
var frame = new ImageFrame<TPixel>(this.Configuration, width, height, imageFrameMetaData);
int rowsPerStrip = tags.GetValue(ExifTag.RowsPerStrip) != null ? (int)tags.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity;
Number[] stripOffsets = tags.GetValue(ExifTag.StripOffsets)?.Value;
Number[] stripByteCounts = tags.GetValue(ExifTag.StripByteCounts)?.Value;
if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar)
{
this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts);
}
else
{
this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts);
}
return frame;
}
/// <summary>
/// Calculates the size (in bytes) for a pixel buffer using the determined color format.
/// </summary>
/// <param name="width">The width for the desired pixel buffer.</param>
/// <param name="height">The height for the desired pixel buffer.</param>
/// <param name="plane">The index of the plane for planar image configuration (or zero for chunky).</param>
/// <returns>The size (in bytes) of the required pixel buffer.</returns>
private int CalculateStripBufferSize(int width, int height, int plane = -1)
{
int bitsPerPixel;
if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
{
DebugGuard.IsTrue(plane == -1, "Expected Chunky planar.");
bitsPerPixel = this.BitsPerPixel;
}
else
{
bitsPerPixel = this.BitsPerSample.Bits()[plane];
}
int bytesPerRow = ((width * bitsPerPixel) + 7) / 8;
return bytesPerRow * height;
}
/// <summary>
/// Decodes the image data for strip encoded data.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame to decode data into.</param>
/// <param name="rowsPerStrip">The number of rows per strip of data.</param>
/// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param>
/// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param>
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts)
where TPixel : unmanaged, IPixel<TPixel>
{
int stripsPerPixel = this.BitsPerSample.Bits().Length;
int stripsPerPlane = stripOffsets.Length / stripsPerPixel;
int bitsPerPixel = this.BitsPerPixel;
Buffer2D<TPixel> pixels = frame.PixelBuffer;
var stripBuffers = new IManagedByteBuffer[stripsPerPixel];
try
{
for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++)
{
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex);
stripBuffers[stripIndex] = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize);
}
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions);
RgbPlanarTiffColor<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.CreatePlanar(this.ColorType, this.BitsPerSample.Bits(), this.ColorMap);
for (int i = 0; i < stripsPerPlane; i++)
{
int stripHeight = i < stripsPerPlane - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip;
for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++)
{
int stripIndex = (i * stripsPerPixel) + planeIndex;
decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffers[planeIndex].GetSpan());
}
colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, frame.Width, stripHeight);
}
}
finally
{
foreach (IManagedByteBuffer buf in stripBuffers)
{
buf?.Dispose();
}
}
}
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts)
where TPixel : unmanaged, IPixel<TPixel>
{
// If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip.
if (rowsPerStrip == TiffConstants.RowsPerStripInfinity)
{
rowsPerStrip = frame.Height;
}
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip);
int bitsPerPixel = this.BitsPerPixel;
using IManagedByteBuffer stripBuffer = this.memoryAllocator.AllocateManagedByteBuffer(uncompressedStripSize, AllocationOptions.Clean);
Buffer2D<TPixel> pixels = frame.PixelBuffer;
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions);
TiffBaseColorDecoder<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.Create(this.ColorType, this.BitsPerSample.Bits(), this.ColorMap);
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{
int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 ? rowsPerStrip : frame.Height % rowsPerStrip;
int top = rowsPerStrip * stripIndex;
if (top + stripHeight > frame.Height)
{
// Make sure we ignore any strips that are not needed for the image (if too many are present)
break;
}
decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripBuffer.GetSpan());
colorDecoder.Decode(stripBuffer.GetSpan(), pixels, 0, top, frame.Width, stripHeight);
}
}
/// <summary>
/// Gets the width of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image width.</returns>
private static int GetImageWidth(ExifProfile exifProfile)
{
IExifValue<Number> width = exifProfile.GetValue(ExifTag.ImageWidth);
if (width == null)
{
TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageWidth");
}
return (int)width.Value;
}
/// <summary>
/// Gets the height of the image frame.
/// </summary>
/// <param name="exifProfile">The image frame exif profile.</param>
/// <returns>The image height.</returns>
private static int GetImageHeight(ExifProfile exifProfile)
{
IExifValue<Number> height = exifProfile.GetValue(ExifTag.ImageLength);
if (height == null)
{
TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
}
return (int)height.Value;
}
}
}

133
src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs

@ -0,0 +1,133 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Linq;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// The decoder metadata creator.
/// </summary>
internal static class TiffDecoderMetadataCreator
{
public static ImageMetadata Create<TPixel>(List<ImageFrame<TPixel>> frames, bool ignoreMetadata, ByteOrder byteOrder)
where TPixel : unmanaged, IPixel<TPixel>
{
if (frames.Count < 1)
{
TiffThrowHelper.ThrowImageFormatException("Expected at least one frame.");
}
var imageMetaData = new ImageMetadata();
ExifProfile exifProfileRootFrame = frames[0].Metadata.ExifProfile;
SetResolution(imageMetaData, exifProfileRootFrame);
TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata();
tiffMetadata.ByteOrder = byteOrder;
if (!ignoreMetadata)
{
for (int i = 0; i < frames.Count; i++)
{
ImageFrame<TPixel> frame = frames[i];
ImageFrameMetadata frameMetaData = frame.Metadata;
if (TryGetIptc(frameMetaData.ExifProfile.Values, out byte[] iptcBytes))
{
frameMetaData.IptcProfile = new IptcProfile(iptcBytes);
}
IExifValue<byte[]> iccProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.IccProfile);
if (iccProfileBytes != null)
{
frameMetaData.IccProfile = new IccProfile(iccProfileBytes.Value);
}
}
}
return imageMetaData;
}
public static ImageMetadata Create(ByteOrder byteOrder, ExifProfile exifProfile)
{
var imageMetaData = new ImageMetadata();
SetResolution(imageMetaData, exifProfile);
TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata();
tiffMetadata.ByteOrder = byteOrder;
return imageMetaData;
}
private static void SetResolution(ImageMetadata imageMetaData, ExifProfile exifProfile)
{
imageMetaData.ResolutionUnits = exifProfile != null ? UnitConverter.ExifProfileToResolutionUnit(exifProfile) : PixelResolutionUnit.PixelsPerInch;
double? horizontalResolution = exifProfile?.GetValue(ExifTag.XResolution)?.Value.ToDouble();
if (horizontalResolution != null)
{
imageMetaData.HorizontalResolution = horizontalResolution.Value;
}
double? verticalResolution = exifProfile?.GetValue(ExifTag.YResolution)?.Value.ToDouble();
if (verticalResolution != null)
{
imageMetaData.VerticalResolution = verticalResolution.Value;
}
}
private static bool TryGetIptc(IReadOnlyList<IExifValue> exifValues, out byte[] iptcBytes)
{
iptcBytes = null;
IExifValue iptc = exifValues.FirstOrDefault(f => f.Tag == ExifTag.IPTC);
if (iptc != null)
{
if (iptc.DataType == ExifDataType.Byte || iptc.DataType == ExifDataType.Undefined)
{
iptcBytes = (byte[])iptc.GetValue();
return true;
}
// Some Encoders write the data type of IPTC as long.
if (iptc.DataType == ExifDataType.Long)
{
uint[] iptcValues = (uint[])iptc.GetValue();
iptcBytes = new byte[iptcValues.Length * 4];
Buffer.BlockCopy(iptcValues, 0, iptcBytes, 0, iptcValues.Length * 4);
if (iptcBytes[0] == 0x1c)
{
return true;
}
else if (iptcBytes[3] != 0x1c)
{
return false;
}
// Probably wrong endianess, swap byte order.
Span<byte> iptcBytesSpan = iptcBytes.AsSpan();
Span<byte> buffer = stackalloc byte[4];
for (int i = 0; i < iptcBytes.Length; i += 4)
{
iptcBytesSpan.Slice(i, 4).CopyTo(buffer);
iptcBytes[i] = buffer[3];
iptcBytes[i + 1] = buffer[2];
iptcBytes[i + 2] = buffer[1];
iptcBytes[i + 3] = buffer[0];
}
return true;
}
}
return false;
}
}
}

281
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -0,0 +1,281 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Linq;
using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// The decoder options parser.
/// </summary>
internal static class TiffDecoderOptionsParser
{
private const TiffPlanarConfiguration DefaultPlanarConfiguration = TiffPlanarConfiguration.Chunky;
/// <summary>
/// Determines the TIFF compression and color types, and reads any associated parameters.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="exifProfile">The exif profile of the frame to decode.</param>
/// <param name="frameMetadata">The IFD entries container to read the image format information for current frame.</param>
public static void VerifyAndParse(this TiffDecoderCore options, ExifProfile exifProfile, TiffFrameMetadata frameMetadata)
{
if (exifProfile.GetValue(ExifTag.TileOffsets)?.Value != null)
{
TiffThrowHelper.ThrowNotSupported("Tiled images are not supported.");
}
if (exifProfile.GetValue(ExifTag.ExtraSamples)?.Value != null)
{
TiffThrowHelper.ThrowNotSupported("ExtraSamples is not supported.");
}
TiffFillOrder fillOrder = (TiffFillOrder?)exifProfile.GetValue(ExifTag.FillOrder)?.Value ?? TiffFillOrder.MostSignificantBitFirst;
if (fillOrder != TiffFillOrder.MostSignificantBitFirst)
{
TiffThrowHelper.ThrowNotSupported("The lower-order bits of the byte FillOrder is not supported.");
}
if (frameMetadata.Predictor == TiffPredictor.FloatingPoint)
{
TiffThrowHelper.ThrowNotSupported("TIFF images with FloatingPoint horizontal predictor are not supported.");
}
TiffSampleFormat[] sampleFormat = exifProfile.GetValue(ExifTag.SampleFormat)?.Value?.Select(a => (TiffSampleFormat)a).ToArray();
if (sampleFormat != null)
{
foreach (TiffSampleFormat format in sampleFormat)
{
if (format != TiffSampleFormat.UnsignedInteger)
{
TiffThrowHelper.ThrowNotSupported("ImageSharp only supports the UnsignedInteger SampleFormat.");
}
}
}
if (exifProfile.GetValue(ExifTag.StripRowCounts)?.Value != null)
{
TiffThrowHelper.ThrowNotSupported("Variable-sized strips are not supported.");
}
VerifyRequiredFieldsArePresent(exifProfile, frameMetadata);
options.PlanarConfiguration = (TiffPlanarConfiguration?)exifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value ?? DefaultPlanarConfiguration;
options.Predictor = frameMetadata.Predictor ?? TiffPredictor.None;
options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb;
options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24;
options.BitsPerSample = GetBitsPerSample(frameMetadata.BitsPerPixel);
options.ParseColorType(exifProfile);
options.ParseCompression(frameMetadata.Compression, exifProfile);
}
private static void VerifyRequiredFieldsArePresent(ExifProfile exifProfile, TiffFrameMetadata frameMetadata)
{
if (exifProfile.GetValue(ExifTag.StripOffsets) == null)
{
TiffThrowHelper.ThrowImageFormatException("StripOffsets are missing and are required for decoding the TIFF image!");
}
if (exifProfile.GetValue(ExifTag.StripByteCounts) == null)
{
TiffThrowHelper.ThrowImageFormatException("StripByteCounts are missing and are required for decoding the TIFF image!");
}
if (frameMetadata.BitsPerPixel == null)
{
TiffThrowHelper.ThrowNotSupported("The TIFF BitsPerSample entry is missing which is required to decode the image!");
}
}
private static void ParseColorType(this TiffDecoderCore options, ExifProfile exifProfile)
{
switch (options.PhotometricInterpretation)
{
case TiffPhotometricInterpretation.WhiteIsZero:
{
if (options.BitsPerSample.Bits().Length != 1)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
}
switch (options.BitsPerSample)
{
case TiffBitsPerSample.Bit8:
{
options.ColorType = TiffColorType.WhiteIsZero8;
break;
}
case TiffBitsPerSample.Bit4:
{
options.ColorType = TiffColorType.WhiteIsZero4;
break;
}
case TiffBitsPerSample.Bit1:
{
options.ColorType = TiffColorType.WhiteIsZero1;
break;
}
default:
{
options.ColorType = TiffColorType.WhiteIsZero;
break;
}
}
break;
}
case TiffPhotometricInterpretation.BlackIsZero:
{
if (options.BitsPerSample.Bits().Length != 1)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
}
switch (options.BitsPerSample)
{
case TiffBitsPerSample.Bit8:
{
options.ColorType = TiffColorType.BlackIsZero8;
break;
}
case TiffBitsPerSample.Bit4:
{
options.ColorType = TiffColorType.BlackIsZero4;
break;
}
case TiffBitsPerSample.Bit1:
{
options.ColorType = TiffColorType.BlackIsZero1;
break;
}
default:
{
options.ColorType = TiffColorType.BlackIsZero;
break;
}
}
break;
}
case TiffPhotometricInterpretation.Rgb:
{
if (options.BitsPerSample.Bits().Length != 3)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
}
if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
{
options.ColorType = options.BitsPerSample == TiffBitsPerSample.Bit24 ? TiffColorType.Rgb888 : TiffColorType.Rgb;
}
else
{
options.ColorType = TiffColorType.RgbPlanar;
}
break;
}
case TiffPhotometricInterpretation.PaletteColor:
{
options.ColorMap = exifProfile.GetValue(ExifTag.ColorMap)?.Value;
if (options.ColorMap != null)
{
if (options.BitsPerSample.Bits().Length != 1)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
}
options.ColorType = TiffColorType.PaletteColor;
}
else
{
TiffThrowHelper.ThrowNotSupported("The TIFF ColorMap entry is missing for a palette color image.");
}
break;
}
default:
{
TiffThrowHelper.ThrowNotSupported($"The specified TIFF photometric interpretation is not supported: {options.PhotometricInterpretation}");
}
break;
}
}
private static void ParseCompression(this TiffDecoderCore options, TiffCompression? compression, ExifProfile exifProfile)
{
switch (compression)
{
case TiffCompression.None:
{
options.CompressionType = TiffDecoderCompressionType.None;
break;
}
case TiffCompression.PackBits:
{
options.CompressionType = TiffDecoderCompressionType.PackBits;
break;
}
case TiffCompression.Deflate:
case TiffCompression.OldDeflate:
{
options.CompressionType = TiffDecoderCompressionType.Deflate;
break;
}
case TiffCompression.Lzw:
{
options.CompressionType = TiffDecoderCompressionType.Lzw;
break;
}
case TiffCompression.CcittGroup3Fax:
{
options.CompressionType = TiffDecoderCompressionType.T4;
options.FaxCompressionOptions = exifProfile.GetValue(ExifTag.T4Options) != null ? (FaxCompressionOptions)exifProfile.GetValue(ExifTag.T4Options).Value : FaxCompressionOptions.None;
break;
}
case TiffCompression.Ccitt1D:
{
options.CompressionType = TiffDecoderCompressionType.HuffmanRle;
break;
}
default:
{
TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format {compression} is not supported");
break;
}
}
}
private static TiffBitsPerSample GetBitsPerSample(TiffBitsPerPixel? bitsPerPixel) => bitsPerPixel switch
{
TiffBitsPerPixel.Bit1 => TiffBitsPerSample.Bit1,
TiffBitsPerPixel.Bit4 => TiffBitsPerSample.Bit4,
TiffBitsPerPixel.Bit8 => TiffBitsPerSample.Bit8,
TiffBitsPerPixel.Bit24 => TiffBitsPerSample.Bit24,
_ => TiffBitsPerSample.Bit24,
};
}
}

55
src/ImageSharp/Formats/Tiff/TiffEncoder.cs

@ -0,0 +1,55 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// Encoder for writing the data image to a stream in TIFF format.
/// </summary>
public class TiffEncoder : IImageEncoder, ITiffEncoderOptions
{
/// <inheritdoc/>
public TiffBitsPerPixel? BitsPerPixel { get; set; }
/// <inheritdoc/>
public TiffCompression? Compression { get; set; }
/// <inheritdoc/>
public DeflateCompressionLevel? CompressionLevel { get; set; }
/// <inheritdoc/>
public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; }
/// <inheritdoc/>
public TiffPredictor? HorizontalPredictor { get; set; }
/// <inheritdoc/>
public IQuantizer Quantizer { get; set; }
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
var encode = new TiffEncoderCore(this, image.GetMemoryAllocator());
encode.Encode(image, stream);
}
/// <inheritdoc/>
public Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
var encoder = new TiffEncoderCore(this, image.GetMemoryAllocator());
return encoder.EncodeAsync(image, stream, cancellationToken);
}
}
}

381
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -0,0 +1,381 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Formats.Tiff.Writers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// Performs the TIFF encoding operation.
/// </summary>
internal sealed class TiffEncoderCore : IImageEncoderInternals
{
private static readonly ushort ByteOrderMarker = BitConverter.IsLittleEndian
? TiffConstants.ByteOrderLittleEndianShort
: TiffConstants.ByteOrderBigEndianShort;
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
private readonly byte[] buffer = new byte[4];
/// <summary>
/// The global configuration.
/// </summary>
private Configuration configuration;
/// <summary>
/// The quantizer for creating color palette image.
/// </summary>
private readonly IQuantizer quantizer;
/// <summary>
/// Sets the deflate compression level.
/// </summary>
private readonly DeflateCompressionLevel compressionLevel;
/// <summary>
/// The default predictor is None.
/// </summary>
private const TiffPredictor DefaultPredictor = TiffPredictor.None;
/// <summary>
/// The default bits per pixel is Bit24.
/// </summary>
private const TiffBitsPerPixel DefaultBitsPerPixel = TiffBitsPerPixel.Bit24;
/// <summary>
/// The default compression is None.
/// </summary>
private const TiffCompression DefaultCompression = TiffCompression.None;
/// <summary>
/// The default photometric interpretation is Rgb.
/// </summary>
private const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb;
/// <summary>
/// Initializes a new instance of the <see cref="TiffEncoderCore"/> class.
/// </summary>
/// <param name="options">The options for the encoder.</param>
/// <param name="memoryAllocator">The memory allocator.</param>
public TiffEncoderCore(ITiffEncoderOptions options, MemoryAllocator memoryAllocator)
{
this.memoryAllocator = memoryAllocator;
this.PhotometricInterpretation = options.PhotometricInterpretation;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
this.BitsPerPixel = options.BitsPerPixel;
this.HorizontalPredictor = options.HorizontalPredictor;
this.CompressionType = options.Compression;
this.compressionLevel = options.CompressionLevel ?? DeflateCompressionLevel.DefaultCompression;
}
/// <summary>
/// Gets the photometric interpretation implementation to use when encoding the image.
/// </summary>
internal TiffPhotometricInterpretation? PhotometricInterpretation { get; private set; }
/// <summary>
/// Gets or sets the compression implementation to use when encoding the image.
/// </summary>
internal TiffCompression? CompressionType { get; set; }
/// <summary>
/// Gets or sets a value indicating which horizontal predictor to use. This can improve the compression ratio with deflate compression.
/// </summary>
internal TiffPredictor? HorizontalPredictor { get; set; }
/// <summary>
/// Gets the bits per pixel.
/// </summary>
internal TiffBitsPerPixel? BitsPerPixel { get; private set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
this.configuration = image.GetConfiguration();
ImageFrameMetadata rootFrameMetaData = image.Frames.RootFrame.Metadata;
TiffFrameMetadata rootFrameTiffMetaData = rootFrameMetaData.GetTiffMetadata();
// Determine the correct values to encode with.
// EncoderOptions > Metadata > Default.
TiffBitsPerPixel? bitsPerPixel = this.BitsPerPixel ?? rootFrameTiffMetaData.BitsPerPixel;
TiffPhotometricInterpretation? photometricInterpretation = this.PhotometricInterpretation ?? rootFrameTiffMetaData.PhotometricInterpretation;
TiffPredictor predictor =
this.HorizontalPredictor
?? rootFrameTiffMetaData.Predictor
?? DefaultPredictor;
TiffCompression compression =
this.CompressionType
?? rootFrameTiffMetaData.Compression
?? DefaultCompression;
// Make sure, the Encoder options makes sense in combination with each other.
this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor);
using (var writer = new TiffStreamWriter(stream))
{
long firstIfdMarker = this.WriteHeader(writer);
// TODO: multiframing is not supported
this.WriteImage(writer, image, firstIfdMarker);
}
}
/// <summary>
/// Writes the TIFF file header.
/// </summary>
/// <param name="writer">The <see cref="TiffStreamWriter" /> to write data to.</param>
/// <returns>
/// The marker to write the first IFD offset.
/// </returns>
public long WriteHeader(TiffStreamWriter writer)
{
writer.Write(ByteOrderMarker);
writer.Write(TiffConstants.HeaderMagicNumber);
return writer.PlaceMarker();
}
/// <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>
private void WriteImage<TPixel>(TiffStreamWriter writer, Image<TPixel> image, long ifdOffset)
where TPixel : unmanaged, IPixel<TPixel>
{
var entriesCollector = new TiffEncoderEntriesCollector();
using TiffBaseCompressor compressor = TiffCompressorFactory.Create(
this.CompressionType ?? TiffCompression.None,
writer.BaseStream,
this.memoryAllocator,
image.Width,
(int)this.BitsPerPixel,
this.compressionLevel,
this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None);
using TiffBaseColorWriter<TPixel> colorWriter = TiffColorWriterFactory.Create(
this.PhotometricInterpretation,
image.Frames.RootFrame,
this.quantizer,
this.memoryAllocator,
this.configuration,
entriesCollector,
(int)this.BitsPerPixel);
int rowsPerStrip = this.CalcRowsPerStrip(image.Frames.RootFrame.Height, colorWriter.BytesPerRow);
colorWriter.Write(compressor, rowsPerStrip);
entriesCollector.ProcessImageFormat(this);
entriesCollector.ProcessGeneral(image);
writer.WriteMarker(ifdOffset, (uint)writer.Position);
long nextIfdMarker = this.WriteIfd(writer, entriesCollector.Entries);
}
/// <summary>
/// Calculates the number of rows written per strip.
/// </summary>
/// <param name="height">The height of the image.</param>
/// <param name="bytesPerRow">The number of bytes per row.</param>
/// <returns>Number of rows per strip.</returns>
private int CalcRowsPerStrip(int height, int bytesPerRow)
{
DebugGuard.MustBeGreaterThan(height, 0, nameof(height));
DebugGuard.MustBeGreaterThan(bytesPerRow, 0, nameof(bytesPerRow));
int rowsPerStrip = TiffConstants.DefaultStripSize / bytesPerRow;
if (rowsPerStrip > 0)
{
if (rowsPerStrip < height)
{
return rowsPerStrip;
}
return height;
}
return 1;
}
/// <summary>
/// Writes a TIFF IFD block.
/// </summary>
/// <param name="writer">The <see cref="BinaryWriter"/> to write data to.</param>
/// <param name="entries">The IFD entries to write to the file.</param>
/// <returns>The marker to write the next IFD offset (if present).</returns>
private long WriteIfd(TiffStreamWriter writer, List<IExifValue> entries)
{
if (entries.Count == 0)
{
TiffThrowHelper.ThrowArgumentException("There must be at least one entry per IFD.");
}
uint dataOffset = (uint)writer.Position + (uint)(6 + (entries.Count * 12));
var largeDataBlocks = new List<byte[]>();
entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag);
writer.Write((ushort)entries.Count);
foreach (IExifValue entry in entries)
{
writer.Write((ushort)entry.Tag);
writer.Write((ushort)entry.DataType);
writer.Write(ExifWriter.GetNumberOfComponents(entry));
uint length = ExifWriter.GetLength(entry);
if (length <= 4)
{
int sz = ExifWriter.WriteValue(entry, this.buffer, 0);
DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written");
writer.WritePadded(this.buffer.AsSpan(0, sz));
}
else
{
var raw = new byte[length];
int sz = ExifWriter.WriteValue(entry, raw, 0);
DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written");
largeDataBlocks.Add(raw);
writer.Write(dataOffset);
dataOffset += (uint)(raw.Length + (raw.Length % 2));
}
}
long nextIfdMarker = writer.PlaceMarker();
foreach (byte[] dataBlock in largeDataBlocks)
{
writer.Write(dataBlock);
if (dataBlock.Length % 2 == 1)
{
writer.Write(0);
}
}
return nextIfdMarker;
}
private void SanitizeAndSetEncoderOptions(TiffBitsPerPixel? bitsPerPixel, int inputBitsPerPixel, TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffPredictor predictor)
{
// BitsPerPixel should be the primary source of truth for the encoder options.
if (bitsPerPixel.HasValue)
{
switch (bitsPerPixel)
{
case TiffBitsPerPixel.Bit1:
if (compression == TiffCompression.Ccitt1D || compression == TiffCompression.CcittGroup3Fax || compression == TiffCompression.CcittGroup4Fax)
{
// The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero.
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None);
break;
}
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, TiffPredictor.None);
break;
case TiffBitsPerPixel.Bit4:
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, TiffPredictor.None);
break;
case TiffBitsPerPixel.Bit8:
this.SetEncoderOptions(bitsPerPixel, photometricInterpretation ?? TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
break;
default:
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor);
break;
}
return;
}
// If no photometric interpretation was chosen, the input image bit per pixel should be preserved.
if (!photometricInterpretation.HasValue)
{
// At the moment only 8 and 32 bits per pixel can be preserved by the tiff encoder.
if (inputBitsPerPixel == 8)
{
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
return;
}
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor);
return;
}
switch (photometricInterpretation)
{
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
if (this.CompressionType == TiffCompression.Ccitt1D ||
this.CompressionType == TiffCompression.CcittGroup3Fax ||
this.CompressionType == TiffCompression.CcittGroup4Fax)
{
this.SetEncoderOptions(TiffBitsPerPixel.Bit1, photometricInterpretation, compression, TiffPredictor.None);
return;
}
else
{
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor);
return;
}
case TiffPhotometricInterpretation.PaletteColor:
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor);
return;
case TiffPhotometricInterpretation.Rgb:
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, photometricInterpretation, compression, predictor);
return;
}
this.SetEncoderOptions(DefaultBitsPerPixel, DefaultPhotometricInterpretation, compression, predictor);
}
private void SetEncoderOptions(TiffBitsPerPixel? bitsPerPixel, TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffPredictor predictor)
{
this.BitsPerPixel = bitsPerPixel;
this.PhotometricInterpretation = photometricInterpretation;
this.CompressionType = compression;
this.HorizontalPredictor = predictor;
}
}
}

378
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -0,0 +1,378 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
{
internal class TiffEncoderEntriesCollector
{
private const string SoftwareValue = "ImageSharp";
public List<IExifValue> Entries { get; } = new List<IExifValue>();
public void ProcessGeneral<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
=> new GeneralProcessor(this).Process(image);
public void ProcessImageFormat(TiffEncoderCore encoder)
=> new ImageFormatProcessor(this).Process(encoder);
public void AddOrReplace(IExifValue entry)
{
int index = this.Entries.FindIndex(t => t.Tag == entry.Tag);
if (index >= 0)
{
this.Entries[index] = entry;
}
else
{
this.Entries.Add(entry);
}
}
private void Add(IExifValue entry) => this.Entries.Add(entry);
private class GeneralProcessor
{
private readonly TiffEncoderEntriesCollector collector;
public GeneralProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector;
public void Process<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrame<TPixel> rootFrame = image.Frames.RootFrame;
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile();
byte[] rootFrameXmpBytes = rootFrame.Metadata.XmpProfile;
var width = new ExifLong(ExifTagValue.ImageWidth)
{
Value = (uint)image.Width
};
var height = new ExifLong(ExifTagValue.ImageLength)
{
Value = (uint)image.Height
};
var software = new ExifString(ExifTagValue.Software)
{
Value = SoftwareValue
};
this.collector.Add(width);
this.collector.Add(height);
this.ProcessResolution(image.Metadata, rootFrameExifProfile);
this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpBytes);
this.ProcessMetadata(rootFrameExifProfile);
if (!this.collector.Entries.Exists(t => t.Tag == ExifTag.Software))
{
this.collector.Add(software);
}
}
private static bool IsPureMetadata(ExifTag tag)
{
switch ((ExifTagValue)(ushort)tag)
{
case ExifTagValue.DocumentName:
case ExifTagValue.ImageDescription:
case ExifTagValue.Make:
case ExifTagValue.Model:
case ExifTagValue.Software:
case ExifTagValue.DateTime:
case ExifTagValue.Artist:
case ExifTagValue.HostComputer:
case ExifTagValue.TargetPrinter:
case ExifTagValue.XMP:
case ExifTagValue.Rating:
case ExifTagValue.RatingPercent:
case ExifTagValue.ImageID:
case ExifTagValue.Copyright:
case ExifTagValue.MDLabName:
case ExifTagValue.MDSampleInfo:
case ExifTagValue.MDPrepDate:
case ExifTagValue.MDPrepTime:
case ExifTagValue.MDFileUnits:
case ExifTagValue.SEMInfo:
case ExifTagValue.XPTitle:
case ExifTagValue.XPComment:
case ExifTagValue.XPAuthor:
case ExifTagValue.XPKeywords:
case ExifTagValue.XPSubject:
return true;
default:
return false;
}
}
private void ProcessResolution(ImageMetadata imageMetadata, ExifProfile exifProfile)
{
UnitConverter.SetResolutionValues(
exifProfile,
imageMetadata.ResolutionUnits,
imageMetadata.HorizontalResolution,
imageMetadata.VerticalResolution);
this.collector.Add(exifProfile.GetValue(ExifTag.ResolutionUnit).DeepClone());
IExifValue xResolution = exifProfile.GetValue(ExifTag.XResolution)?.DeepClone();
IExifValue yResolution = exifProfile.GetValue(ExifTag.YResolution)?.DeepClone();
if (xResolution != null && yResolution != null)
{
this.collector.Add(xResolution);
this.collector.Add(yResolution);
}
}
private void ProcessMetadata(ExifProfile exifProfile)
{
foreach (IExifValue entry in exifProfile.Values)
{
// todo: skip subIfd
if (entry.DataType == ExifDataType.Ifd)
{
continue;
}
switch ((ExifTagValue)(ushort)entry.Tag)
{
case ExifTagValue.SubIFDOffset:
case ExifTagValue.GPSIFDOffset:
case ExifTagValue.SubIFDs:
case ExifTagValue.XMP:
case ExifTagValue.IPTC:
case ExifTagValue.IccProfile:
continue;
}
switch (ExifTags.GetPart(entry.Tag))
{
case ExifParts.ExifTags:
case ExifParts.GpsTags:
break;
case ExifParts.IfdTags:
if (!IsPureMetadata(entry.Tag))
{
continue;
}
break;
}
if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag))
{
this.collector.AddOrReplace(entry.DeepClone());
}
}
}
private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, byte[] xmpProfile)
{
if (exifProfile != null && exifProfile.Parts != ExifParts.None)
{
foreach (IExifValue entry in exifProfile.Values)
{
if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null)
{
ExifParts entryPart = ExifTags.GetPart(entry.Tag);
if (entryPart != ExifParts.None && exifProfile.Parts.HasFlag(entryPart))
{
this.collector.AddOrReplace(entry.DeepClone());
}
}
}
}
else
{
exifProfile.RemoveValue(ExifTag.SubIFDOffset);
}
if (imageMetadata.IptcProfile != null)
{
imageMetadata.IptcProfile.UpdateData();
var iptc = new ExifByteArray(ExifTagValue.IPTC, ExifDataType.Byte)
{
Value = imageMetadata.IptcProfile.Data
};
this.collector.Add(iptc);
}
else
{
exifProfile.RemoveValue(ExifTag.IPTC);
}
if (imageMetadata.IccProfile != null)
{
var icc = new ExifByteArray(ExifTagValue.IccProfile, ExifDataType.Undefined)
{
Value = imageMetadata.IccProfile.ToByteArray()
};
this.collector.Add(icc);
}
else
{
exifProfile.RemoveValue(ExifTag.IccProfile);
}
TiffMetadata tiffMetadata = imageMetadata.GetTiffMetadata();
if (xmpProfile != null)
{
var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte)
{
Value = xmpProfile
};
this.collector.Add(xmp);
}
else
{
exifProfile.RemoveValue(ExifTag.XMP);
}
}
}
private class ImageFormatProcessor
{
private readonly TiffEncoderEntriesCollector collector;
public ImageFormatProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector;
public void Process(TiffEncoderCore encoder)
{
var samplesPerPixel = new ExifLong(ExifTagValue.SamplesPerPixel)
{
Value = GetSamplesPerPixel(encoder)
};
ushort[] bitsPerSampleValue = GetBitsPerSampleValue(encoder);
var bitPerSample = new ExifShortArray(ExifTagValue.BitsPerSample)
{
Value = bitsPerSampleValue
};
ushort compressionType = GetCompressionType(encoder);
var compression = new ExifShort(ExifTagValue.Compression)
{
Value = compressionType
};
var photometricInterpretation = new ExifShort(ExifTagValue.PhotometricInterpretation)
{
Value = (ushort)encoder.PhotometricInterpretation
};
this.collector.AddOrReplace(samplesPerPixel);
this.collector.AddOrReplace(bitPerSample);
this.collector.AddOrReplace(compression);
this.collector.AddOrReplace(photometricInterpretation);
if (encoder.HorizontalPredictor == TiffPredictor.Horizontal)
{
if (encoder.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb ||
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor ||
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero)
{
var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal };
this.collector.AddOrReplace(predictor);
}
}
}
private static uint GetSamplesPerPixel(TiffEncoderCore encoder)
{
switch (encoder.PhotometricInterpretation)
{
case TiffPhotometricInterpretation.PaletteColor:
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
return 1;
case TiffPhotometricInterpretation.Rgb:
default:
return 3;
}
}
private static ushort[] GetBitsPerSampleValue(TiffEncoderCore encoder)
{
switch (encoder.PhotometricInterpretation)
{
case TiffPhotometricInterpretation.PaletteColor:
if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit4)
{
return TiffConstants.BitsPerSample4Bit;
}
else
{
return TiffConstants.BitsPerSample8Bit;
}
case TiffPhotometricInterpretation.Rgb:
return TiffConstants.BitsPerSampleRgb8Bit;
case TiffPhotometricInterpretation.WhiteIsZero:
if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit1)
{
return TiffConstants.BitsPerSample1Bit;
}
return TiffConstants.BitsPerSample8Bit;
case TiffPhotometricInterpretation.BlackIsZero:
if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit1)
{
return TiffConstants.BitsPerSample1Bit;
}
return TiffConstants.BitsPerSample8Bit;
default:
return TiffConstants.BitsPerSampleRgb8Bit;
}
}
private static ushort GetCompressionType(TiffEncoderCore encoder)
{
switch (encoder.CompressionType)
{
case TiffCompression.Deflate:
// Deflate is allowed for all modes.
return (ushort)TiffCompression.Deflate;
case TiffCompression.PackBits:
// PackBits is allowed for all modes.
return (ushort)TiffCompression.PackBits;
case TiffCompression.Lzw:
if (encoder.PhotometricInterpretation == TiffPhotometricInterpretation.Rgb ||
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.PaletteColor ||
encoder.PhotometricInterpretation == TiffPhotometricInterpretation.BlackIsZero)
{
return (ushort)TiffCompression.Lzw;
}
break;
case TiffCompression.CcittGroup3Fax:
return (ushort)TiffCompression.CcittGroup3Fax;
case TiffCompression.Ccitt1D:
return (ushort)TiffCompression.Ccitt1D;
}
return (ushort)TiffCompression.None;
}
}
}
}

42
src/ImageSharp/Formats/Tiff/TiffFormat.cs

@ -0,0 +1,42 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// Encapsulates the means to encode and decode Tiff images.
/// </summary>
public sealed class TiffFormat : IImageFormat<TiffMetadata, TiffFrameMetadata>
{
private TiffFormat()
{
}
/// <summary>
/// Gets the current instance.
/// </summary>
public static TiffFormat Instance { get; } = new TiffFormat();
/// <inheritdoc/>
public string Name => "TIFF";
/// <inheritdoc/>
public string DefaultMimeType => "image/tiff";
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => TiffConstants.MimeTypes;
/// <inheritdoc/>
public IEnumerable<string> FileExtensions => TiffConstants.FileExtensions;
/// <inheritdoc/>
public TiffMetadata CreateDefaultFormatMetadata() => new TiffMetadata();
/// <inheritdoc/>
public TiffFrameMetadata CreateDefaultFormatFrameMetadata() => new TiffFrameMetadata();
}
}

113
src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs

@ -0,0 +1,113 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// Provides Tiff specific metadata information for the frame.
/// </summary>
public class TiffFrameMetadata : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="TiffFrameMetadata"/> class.
/// </summary>
public TiffFrameMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TiffFrameMetadata"/> class.
/// </summary>
/// <param name="other">The other tiff frame metadata.</param>
private TiffFrameMetadata(TiffFrameMetadata other)
{
this.BitsPerPixel = other.BitsPerPixel;
this.Compression = other.Compression;
this.PhotometricInterpretation = other.PhotometricInterpretation;
this.Predictor = other.Predictor;
}
/// <summary>
/// Gets or sets the bits per pixel.
/// </summary>
public TiffBitsPerPixel? BitsPerPixel { get; set; }
/// <summary>
/// Gets or sets the compression scheme used on the image data.
/// </summary>
public TiffCompression? Compression { get; set; }
/// <summary>
/// Gets or sets the color space of the image data.
/// </summary>
public TiffPhotometricInterpretation? PhotometricInterpretation { get; set; }
/// <summary>
/// Gets or sets a mathematical operator that is applied to the image data before an encoding scheme is applied.
/// </summary>
public TiffPredictor? Predictor { get; set; }
/// <summary>
/// Returns a new <see cref="TiffFrameMetadata"/> instance parsed from the given Exif profile.
/// </summary>
/// <param name="profile">The Exif profile containing tiff frame directory tags to parse.
/// If null, a new instance is created and parsed instead.</param>
/// <returns>The <see cref="TiffFrameMetadata"/>.</returns>
internal static TiffFrameMetadata Parse(ExifProfile profile)
{
var meta = new TiffFrameMetadata();
Parse(meta, profile);
return meta;
}
/// <summary>
/// Parses the given Exif profile to populate the properties of the tiff frame meta data..
/// </summary>
/// <param name="meta">The tiff frame meta data.</param>
/// <param name="profile">The Exif profile containing tiff frame directory tags.</param>
internal static void Parse(TiffFrameMetadata meta, ExifProfile profile)
{
if (profile != null)
{
ushort[] bitsPerSample = profile.GetValue(ExifTag.BitsPerSample)?.Value;
meta.BitsPerPixel = BitsPerPixelFromBitsPerSample(bitsPerSample);
meta.Compression = (TiffCompression?)profile.GetValue(ExifTag.Compression)?.Value;
meta.PhotometricInterpretation =
(TiffPhotometricInterpretation?)profile.GetValue(ExifTag.PhotometricInterpretation)?.Value;
meta.Predictor = (TiffPredictor?)profile.GetValue(ExifTag.Predictor)?.Value;
profile.RemoveValue(ExifTag.BitsPerSample);
profile.RemoveValue(ExifTag.Compression);
profile.RemoveValue(ExifTag.PhotometricInterpretation);
profile.RemoveValue(ExifTag.Predictor);
}
}
/// <summary>
/// Gets the bits per pixel for the given bits per sample.
/// </summary>
/// <param name="bitsPerSample">The tiff bits per sample.</param>
/// <returns>Bits per pixel.</returns>
private static TiffBitsPerPixel? BitsPerPixelFromBitsPerSample(ushort[] bitsPerSample)
{
if (bitsPerSample == null)
{
return null;
}
int bitsPerPixel = 0;
foreach (ushort bits in bitsPerSample)
{
bitsPerPixel += bits;
}
return (TiffBitsPerPixel)bitsPerPixel;
}
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffFrameMetadata(this);
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save