// 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; } } }