// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Buffers;
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
{
///
/// Performs the tiff decoding operation.
///
internal class TiffDecoderCore : IImageDecoderInternals
{
///
/// Used for allocating memory during processing operations.
///
private readonly MemoryAllocator memoryAllocator;
///
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
///
private readonly bool ignoreMetadata;
///
/// The stream to decode from.
///
private BufferedReadStream inputStream;
///
/// Indicates the byte order of the stream.
///
private ByteOrder byteOrder;
///
/// Initializes a new instance of the class.
///
/// The configuration.
/// The decoder options.
public TiffDecoderCore(Configuration configuration, ITiffDecoderOptions options)
{
options ??= new TiffDecoder();
this.Configuration = configuration ?? Configuration.Default;
this.ignoreMetadata = options.IgnoreMetadata;
this.memoryAllocator = this.Configuration.MemoryAllocator;
}
///
/// Gets or sets the bits per sample.
///
public TiffBitsPerSample BitsPerSample { get; set; }
///
/// Gets or sets the bits per pixel.
///
public int BitsPerPixel { get; set; }
///
/// Gets or sets the lookup table for RGB palette colored images.
///
public ushort[] ColorMap { get; set; }
///
/// Gets or sets the photometric interpretation implementation to use when decoding the image.
///
public TiffColorType ColorType { get; set; }
///
/// Gets or sets the compression used, when the image was encoded.
///
public TiffDecoderCompressionType CompressionType { get; set; }
///
/// Gets or sets the Fax specific compression options.
///
public FaxCompressionOptions FaxCompressionOptions { get; set; }
///
/// Gets or sets the planar configuration type to use when decoding the image.
///
public TiffPlanarConfiguration PlanarConfiguration { get; set; }
///
/// Gets or sets the photometric interpretation.
///
public TiffPhotometricInterpretation PhotometricInterpretation { get; set; }
///
/// Gets or sets the horizontal predictor.
///
public TiffPredictor Predictor { get; set; }
///
public Configuration Configuration { get; }
///
public Size Dimensions { get; private set; }
///
public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
this.inputStream = stream;
var reader = new DirectoryReader(stream);
IEnumerable directories = reader.Read();
this.byteOrder = reader.ByteOrder;
var frames = new List>();
foreach (ExifProfile ifd in directories)
{
ImageFrame frame = this.DecodeFrame(ifd);
frames.Add(frame);
}
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder);
// TODO: Tiff frames can have different sizes
ImageFrame root = frames[0];
this.Dimensions = root.Size();
foreach (ImageFrame frame in frames)
{
if (frame.Size() != root.Size())
{
TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported");
}
}
return new Image(this.Configuration, metadata, frames);
}
///
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.inputStream = stream;
var reader = new DirectoryReader(stream);
IEnumerable 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);
}
///
/// Decodes the image data from a specified IFD.
///
/// The pixel format.
/// The IFD tags.
///
/// The tiff frame.
///
private ImageFrame DecodeFrame(ExifProfile tags)
where TPixel : unmanaged, IPixel
{
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(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;
}
///
/// Calculates the size (in bytes) for a pixel buffer using the determined color format.
///
/// The width for the desired pixel buffer.
/// The height for the desired pixel buffer.
/// The index of the plane for planar image configuration (or zero for chunky).
/// The size (in bytes) of the required pixel buffer.
private int CalculateStripBufferSize(int width, int height, int plane = -1)
{
DebugGuard.MustBeLessThanOrEqualTo(plane, 3, nameof(plane));
int bitsPerPixel = 0;
if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
{
DebugGuard.IsTrue(plane == -1, "Expected Chunky planar.");
bitsPerPixel = this.BitsPerPixel;
}
else
{
switch (plane)
{
case 0:
bitsPerPixel = this.BitsPerSample.Channel0;
break;
case 1:
bitsPerPixel = this.BitsPerSample.Channel1;
break;
case 2:
bitsPerPixel = this.BitsPerSample.Channel2;
break;
default:
TiffThrowHelper.ThrowNotSupported("More then 3 color channels are not supported");
break;
}
}
int bytesPerRow = ((width * bitsPerPixel) + 7) / 8;
return bytesPerRow * height;
}
///
/// Decodes the image data for strip encoded data.
///
/// The pixel format.
/// The image frame to decode data into.
/// The number of rows per strip of data.
/// An array of byte offsets to each strip in the image.
/// An array of the size of each strip (in bytes).
private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts)
where TPixel : unmanaged, IPixel
{
int stripsPerPixel = this.BitsPerSample.Channels;
int stripsPerPlane = stripOffsets.Length / stripsPerPixel;
int bitsPerPixel = this.BitsPerPixel;
Buffer2D pixels = frame.PixelBuffer;
var stripBuffers = new IMemoryOwner[stripsPerPixel];
try
{
for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++)
{
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex);
stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize);
}
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions);
RgbPlanarTiffColor colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, 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 (IMemoryOwner buf in stripBuffers)
{
buf?.Dispose();
}
}
}
private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts)
where TPixel : unmanaged, IPixel
{
// If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip.
if (rowsPerStrip == TiffConstants.RowsPerStripInfinity)
{
rowsPerStrip = frame.Height;
}
int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip);
int bitsPerPixel = this.BitsPerPixel;
using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate(uncompressedStripSize, AllocationOptions.Clean);
System.Span stripBufferSpan = stripBuffer.GetSpan();
Buffer2D pixels = frame.PixelBuffer;
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(
this.CompressionType,
this.memoryAllocator,
this.PhotometricInterpretation,
frame.Width,
bitsPerPixel,
this.Predictor,
this.FaxCompressionOptions);
TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder);
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], stripBufferSpan);
colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight);
}
}
///
/// Gets the width of the image frame.
///
/// The image frame exif profile.
/// The image width.
private static int GetImageWidth(ExifProfile exifProfile)
{
IExifValue width = exifProfile.GetValue(ExifTag.ImageWidth);
if (width == null)
{
TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageWidth");
}
return (int)width.Value;
}
///
/// Gets the height of the image frame.
///
/// The image frame exif profile.
/// The image height.
private static int GetImageHeight(ExifProfile exifProfile)
{
IExifValue height = exifProfile.GetValue(ExifTag.ImageLength);
if (height == null)
{
TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageLength");
}
return (int)height.Value;
}
}
}