|
|
|
@ -1,8 +1,7 @@ |
|
|
|
// Copyright (c) Six Labors.
|
|
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
#nullable disable |
|
|
|
|
|
|
|
using SixLabors.ImageSharp.Advanced; |
|
|
|
using System.Diagnostics.CodeAnalysis; |
|
|
|
using SixLabors.ImageSharp.Compression.Zlib; |
|
|
|
using SixLabors.ImageSharp.Formats.Tiff.Compression; |
|
|
|
using SixLabors.ImageSharp.Formats.Tiff.Constants; |
|
|
|
@ -50,41 +49,22 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals |
|
|
|
/// </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>
|
|
|
|
/// Whether to skip metadata during encoding.
|
|
|
|
/// </summary>
|
|
|
|
private readonly bool skipMetadata; |
|
|
|
|
|
|
|
private readonly List<(long, uint)> frameMarkers = new(); |
|
|
|
private readonly List<(long, uint)> frameMarkers = []; |
|
|
|
|
|
|
|
/// <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(TiffEncoder options, MemoryAllocator memoryAllocator) |
|
|
|
/// <param name="configuration">The global configuration.</param>
|
|
|
|
public TiffEncoderCore(TiffEncoder options, Configuration configuration) |
|
|
|
{ |
|
|
|
this.memoryAllocator = memoryAllocator; |
|
|
|
this.configuration = configuration; |
|
|
|
this.memoryAllocator = configuration.MemoryAllocator; |
|
|
|
this.PhotometricInterpretation = options.PhotometricInterpretation; |
|
|
|
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; |
|
|
|
this.pixelSamplingStrategy = options.PixelSamplingStrategy; |
|
|
|
@ -135,35 +115,29 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals |
|
|
|
|
|
|
|
// Determine the correct values to encode with.
|
|
|
|
// EncoderOptions > Metadata > Default.
|
|
|
|
TiffBitsPerPixel? bitsPerPixel = this.BitsPerPixel ?? rootFrameTiffMetaData.BitsPerPixel; |
|
|
|
TiffBitsPerPixel bitsPerPixel = this.BitsPerPixel ?? rootFrameTiffMetaData.BitsPerPixel; |
|
|
|
|
|
|
|
TiffPhotometricInterpretation? photometricInterpretation = this.PhotometricInterpretation ?? rootFrameTiffMetaData.PhotometricInterpretation; |
|
|
|
TiffPhotometricInterpretation photometricInterpretation = this.PhotometricInterpretation ?? rootFrameTiffMetaData.PhotometricInterpretation; |
|
|
|
|
|
|
|
TiffPredictor predictor = |
|
|
|
this.HorizontalPredictor |
|
|
|
?? rootFrameTiffMetaData.Predictor |
|
|
|
?? DefaultPredictor; |
|
|
|
TiffPredictor predictor = this.HorizontalPredictor ?? rootFrameTiffMetaData.Predictor; |
|
|
|
|
|
|
|
TiffCompression compression = |
|
|
|
this.CompressionType |
|
|
|
?? rootFrameTiffMetaData.Compression |
|
|
|
?? DefaultCompression; |
|
|
|
TiffCompression compression = this.CompressionType ?? rootFrameTiffMetaData.Compression; |
|
|
|
|
|
|
|
// Make sure, the Encoder options makes sense in combination with each other.
|
|
|
|
this.SanitizeAndSetEncoderOptions(bitsPerPixel, image.PixelType.BitsPerPixel, photometricInterpretation, compression, predictor); |
|
|
|
// Make sure the Encoder options makes sense in combination with each other.
|
|
|
|
this.SanitizeAndSetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor); |
|
|
|
|
|
|
|
using TiffStreamWriter writer = new(stream); |
|
|
|
Span<byte> buffer = stackalloc byte[4]; |
|
|
|
|
|
|
|
long ifdMarker = WriteHeader(writer, buffer); |
|
|
|
|
|
|
|
Image<TPixel> metadataImage = image; |
|
|
|
Image<TPixel>? metadataImage = image; |
|
|
|
|
|
|
|
foreach (ImageFrame<TPixel> frame in image.Frames) |
|
|
|
{ |
|
|
|
cancellationToken.ThrowIfCancellationRequested(); |
|
|
|
|
|
|
|
ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker); |
|
|
|
ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, this.BitsPerPixel.Value, this.CompressionType.Value, ifdMarker); |
|
|
|
metadataImage = null; |
|
|
|
} |
|
|
|
|
|
|
|
@ -199,6 +173,8 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals |
|
|
|
/// <param name="frame">The tiff frame.</param>
|
|
|
|
/// <param name="imageMetadata">The image metadata (resolution values for each frame).</param>
|
|
|
|
/// <param name="image">The image (common metadata for root frame).</param>
|
|
|
|
/// <param name="bitsPerPixel">The bits per pixel.</param>
|
|
|
|
/// <param name="compression">The compression type.</param>
|
|
|
|
/// <param name="ifdOffset">The marker to write this IFD offset.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// The next IFD offset value.
|
|
|
|
@ -207,16 +183,18 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals |
|
|
|
TiffStreamWriter writer, |
|
|
|
ImageFrame<TPixel> frame, |
|
|
|
ImageMetadata imageMetadata, |
|
|
|
Image<TPixel> image, |
|
|
|
Image<TPixel>? image, |
|
|
|
TiffBitsPerPixel bitsPerPixel, |
|
|
|
TiffCompression compression, |
|
|
|
long ifdOffset) |
|
|
|
where TPixel : unmanaged, IPixel<TPixel> |
|
|
|
{ |
|
|
|
using TiffBaseCompressor compressor = TiffCompressorFactory.Create( |
|
|
|
this.CompressionType ?? TiffCompression.None, |
|
|
|
compression, |
|
|
|
writer.BaseStream, |
|
|
|
this.memoryAllocator, |
|
|
|
frame.Width, |
|
|
|
(int)this.BitsPerPixel, |
|
|
|
(int)bitsPerPixel, |
|
|
|
this.compressionLevel, |
|
|
|
this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); |
|
|
|
|
|
|
|
@ -229,7 +207,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals |
|
|
|
this.memoryAllocator, |
|
|
|
this.configuration, |
|
|
|
entriesCollector, |
|
|
|
(int)this.BitsPerPixel); |
|
|
|
(int)bitsPerPixel); |
|
|
|
|
|
|
|
int rowsPerStrip = CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow, this.CompressionType); |
|
|
|
|
|
|
|
@ -307,7 +285,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals |
|
|
|
} |
|
|
|
|
|
|
|
uint dataOffset = (uint)writer.Position + (uint)(6 + (entries.Count * 12)); |
|
|
|
List<byte[]> largeDataBlocks = new(); |
|
|
|
List<byte[]> largeDataBlocks = []; |
|
|
|
|
|
|
|
entries.Sort((a, b) => (ushort)a.Tag - (ushort)b.Tag); |
|
|
|
|
|
|
|
@ -354,135 +332,80 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals |
|
|
|
return nextIfdMarker; |
|
|
|
} |
|
|
|
|
|
|
|
[MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))] |
|
|
|
private void SanitizeAndSetEncoderOptions( |
|
|
|
TiffBitsPerPixel? bitsPerPixel, |
|
|
|
int inputBitsPerPixel, |
|
|
|
TiffPhotometricInterpretation? photometricInterpretation, |
|
|
|
TiffBitsPerPixel bitsPerPixel, |
|
|
|
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 (IsOneBitCompression(compression)) |
|
|
|
{ |
|
|
|
// 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; |
|
|
|
case TiffBitsPerPixel.Bit16: |
|
|
|
// Assume desire to encode as L16 grayscale
|
|
|
|
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); |
|
|
|
break; |
|
|
|
case TiffBitsPerPixel.Bit6: |
|
|
|
case TiffBitsPerPixel.Bit10: |
|
|
|
case TiffBitsPerPixel.Bit12: |
|
|
|
case TiffBitsPerPixel.Bit14: |
|
|
|
case TiffBitsPerPixel.Bit30: |
|
|
|
case TiffBitsPerPixel.Bit36: |
|
|
|
case TiffBitsPerPixel.Bit42: |
|
|
|
case TiffBitsPerPixel.Bit48: |
|
|
|
// Encoding not yet supported bits per pixel will default to 24 bits.
|
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None); |
|
|
|
break; |
|
|
|
case TiffBitsPerPixel.Bit64: |
|
|
|
// Encoding not yet supported bits per pixel will default to 32 bits.
|
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None); |
|
|
|
break; |
|
|
|
default: |
|
|
|
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
// Make sure 1 Bit compression is only used with 1 bit pixel type.
|
|
|
|
if (IsOneBitCompression(this.CompressionType) && this.BitsPerPixel != TiffBitsPerPixel.Bit1) |
|
|
|
{ |
|
|
|
// Invalid compression / bits per pixel combination, fallback to no compression.
|
|
|
|
this.CompressionType = DefaultCompression; |
|
|
|
} |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// If no photometric interpretation was chosen, the input image bit per pixel should be preserved.
|
|
|
|
if (!photometricInterpretation.HasValue) |
|
|
|
switch (bitsPerPixel) |
|
|
|
{ |
|
|
|
if (IsOneBitCompression(this.CompressionType)) |
|
|
|
{ |
|
|
|
// We need to make sure bits per pixel is set to Bit1 now. WhiteIsZero is set because its the default for bilevel compressed data.
|
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// At the moment only 8, 16 and 32 bits per pixel can be preserved by the tiff encoder.
|
|
|
|
if (inputBitsPerPixel == 8) |
|
|
|
{ |
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (inputBitsPerPixel == 16) |
|
|
|
{ |
|
|
|
// Assume desire to encode as L16 grayscale
|
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit16, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
switch (photometricInterpretation) |
|
|
|
{ |
|
|
|
case TiffPhotometricInterpretation.BlackIsZero: |
|
|
|
case TiffPhotometricInterpretation.WhiteIsZero: |
|
|
|
if (IsOneBitCompression(this.CompressionType)) |
|
|
|
{ |
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit1, photometricInterpretation, compression, TiffPredictor.None); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (inputBitsPerPixel == 16) |
|
|
|
case TiffBitsPerPixel.Bit1: |
|
|
|
if (IsOneBitCompression(compression)) |
|
|
|
{ |
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit16, photometricInterpretation, compression, predictor); |
|
|
|
return; |
|
|
|
// The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero.
|
|
|
|
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor); |
|
|
|
return; |
|
|
|
|
|
|
|
case TiffPhotometricInterpretation.PaletteColor: |
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor); |
|
|
|
return; |
|
|
|
|
|
|
|
case TiffPhotometricInterpretation.Rgb: |
|
|
|
// Make sure 1 Bit compression is only used with 1 bit pixel type.
|
|
|
|
if (IsOneBitCompression(this.CompressionType)) |
|
|
|
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, TiffPredictor.None); |
|
|
|
break; |
|
|
|
case TiffBitsPerPixel.Bit4: |
|
|
|
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, TiffPredictor.None); |
|
|
|
break; |
|
|
|
case TiffBitsPerPixel.Bit8: |
|
|
|
|
|
|
|
// Allow any combination of the below for 8 bit images.
|
|
|
|
if (photometricInterpretation is TiffPhotometricInterpretation.BlackIsZero |
|
|
|
or TiffPhotometricInterpretation.WhiteIsZero |
|
|
|
or TiffPhotometricInterpretation.PaletteColor) |
|
|
|
{ |
|
|
|
// Invalid compression / bits per pixel combination, fallback to no compression.
|
|
|
|
compression = DefaultCompression; |
|
|
|
this.SetEncoderOptions(bitsPerPixel, photometricInterpretation, compression, predictor); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, photometricInterpretation, compression, predictor); |
|
|
|
return; |
|
|
|
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.PaletteColor, compression, predictor); |
|
|
|
break; |
|
|
|
case TiffBitsPerPixel.Bit16: |
|
|
|
// Assume desire to encode as L16 grayscale
|
|
|
|
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor); |
|
|
|
break; |
|
|
|
case TiffBitsPerPixel.Bit6: |
|
|
|
case TiffBitsPerPixel.Bit10: |
|
|
|
case TiffBitsPerPixel.Bit12: |
|
|
|
case TiffBitsPerPixel.Bit14: |
|
|
|
case TiffBitsPerPixel.Bit30: |
|
|
|
case TiffBitsPerPixel.Bit36: |
|
|
|
case TiffBitsPerPixel.Bit42: |
|
|
|
case TiffBitsPerPixel.Bit48: |
|
|
|
// Encoding not yet supported bits per pixel will default to 24 bits.
|
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None); |
|
|
|
break; |
|
|
|
case TiffBitsPerPixel.Bit64: |
|
|
|
// Encoding not yet supported bits per pixel will default to 32 bits.
|
|
|
|
this.SetEncoderOptions(TiffBitsPerPixel.Bit32, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None); |
|
|
|
break; |
|
|
|
default: |
|
|
|
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor); |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
this.SetEncoderOptions(DefaultBitsPerPixel, DefaultPhotometricInterpretation, compression, predictor); |
|
|
|
// Make sure 1 Bit compression is only used with 1 bit pixel type.
|
|
|
|
if (IsOneBitCompression(this.CompressionType) && this.BitsPerPixel != TiffBitsPerPixel.Bit1) |
|
|
|
{ |
|
|
|
// Invalid compression / bits per pixel combination, fallback to no compression.
|
|
|
|
this.CompressionType = TiffCompression.None; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void SetEncoderOptions(TiffBitsPerPixel? bitsPerPixel, TiffPhotometricInterpretation? photometricInterpretation, TiffCompression compression, TiffPredictor predictor) |
|
|
|
[MemberNotNull(nameof(BitsPerPixel), nameof(PhotometricInterpretation), nameof(CompressionType), nameof(HorizontalPredictor))] |
|
|
|
private void SetEncoderOptions( |
|
|
|
TiffBitsPerPixel bitsPerPixel, |
|
|
|
TiffPhotometricInterpretation photometricInterpretation, |
|
|
|
TiffCompression compression, |
|
|
|
TiffPredictor predictor) |
|
|
|
{ |
|
|
|
this.BitsPerPixel = bitsPerPixel; |
|
|
|
this.PhotometricInterpretation = photometricInterpretation; |
|
|
|
|