diff --git a/src/ImageSharp/Common/Helpers/ExifResolutionValues.cs b/src/ImageSharp/Common/Helpers/ExifResolutionValues.cs new file mode 100644 index 0000000000..b6a628608b --- /dev/null +++ b/src/ImageSharp/Common/Helpers/ExifResolutionValues.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Common.Helpers +{ + internal readonly struct ExifResolutionValues + { + public ExifResolutionValues(ushort resolutionUnit, double? horizontalResolution, double? verticalResolution) + { + this.ResolutionUnit = resolutionUnit; + this.HorizontalResolution = horizontalResolution; + this.VerticalResolution = verticalResolution; + } + + public ushort ResolutionUnit { get; } + + public double? HorizontalResolution { get; } + + public double? VerticalResolution { get; } + } +} diff --git a/src/ImageSharp/Common/Helpers/UnitConverter.cs b/src/ImageSharp/Common/Helpers/UnitConverter.cs index efc0e0e152..7ea64aa624 100644 --- a/src/ImageSharp/Common/Helpers/UnitConverter.cs +++ b/src/ImageSharp/Common/Helpers/UnitConverter.cs @@ -98,14 +98,14 @@ namespace SixLabors.ImageSharp.Common.Helpers } /// - /// Sets the exif profile resolution values. + /// Gets the exif profile resolution values. /// - /// The exif profile. /// The resolution unit. /// The horizontal resolution value. /// The vertical resolution value. + /// [MethodImpl(InliningOptions.ShortMethod)] - public static void SetResolutionValues(ExifProfile exifProfile, PixelResolutionUnit unit, double horizontal, double vertical) + public static ExifResolutionValues GetExifResolutionValues(PixelResolutionUnit unit, double horizontal, double vertical) { switch (unit) { @@ -115,9 +115,9 @@ namespace SixLabors.ImageSharp.Common.Helpers break; case PixelResolutionUnit.PixelsPerMeter: { - unit = PixelResolutionUnit.PixelsPerCentimeter; - horizontal = UnitConverter.MeterToCm(horizontal); - vertical = UnitConverter.MeterToCm(vertical); + unit = PixelResolutionUnit.PixelsPerCentimeter; + horizontal = MeterToCm(horizontal); + vertical = MeterToCm(vertical); } break; @@ -126,18 +126,13 @@ namespace SixLabors.ImageSharp.Common.Helpers break; } - exifProfile.SetValue(ExifTag.ResolutionUnit, (ushort)(unit + 1)); - + ushort exifUnit = (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)); + return new ExifResolutionValues(exifUnit, null, null); } + + return new ExifResolutionValues(exifUnit, horizontal, vertical); } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index 2273d759f0..d7c9848a44 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -74,6 +74,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// private const TiffPhotometricInterpretation DefaultPhotometricInterpretation = TiffPhotometricInterpretation.Rgb; + private readonly List<(long, uint)> frameMarkers = new List<(long, uint)>(); + /// /// Initializes a new instance of the class. /// @@ -147,13 +149,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff // 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)) + using var writer = new TiffStreamWriter(stream); + long ifdMarker = this.WriteHeader(writer); + + Image metadataImage = image; + foreach (ImageFrame frame in image.Frames) { - long firstIfdMarker = this.WriteHeader(writer); + var subfileType = (TiffNewSubfileType)(frame.Metadata.ExifProfile?.GetValue(ExifTag.SubfileType)?.Value ?? (int)TiffNewSubfileType.FullImage); - // TODO: multiframing is not supported - this.WriteImage(writer, image, firstIfdMarker); + ifdMarker = this.WriteFrame(writer, frame, image.Metadata, metadataImage, ifdMarker); + metadataImage = null; + } + + long currentOffset = writer.BaseStream.Position; + foreach ((long, uint) marker in this.frameMarkers) + { + writer.WriteMarkerFast(marker.Item1, marker.Item2); } + + writer.BaseStream.Seek(currentOffset, SeekOrigin.Begin); } /// @@ -174,41 +188,56 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// Writes all data required to define an image. /// /// The pixel format. - /// The to write data to. - /// The to encode from. + /// The to write data to. + /// The tiff frame. + /// The image metadata (resolution values for each frame). + /// The image (common metadata for root frame). /// The marker to write this IFD offset. - private void WriteImage(TiffStreamWriter writer, Image image, long ifdOffset) + /// + /// The next IFD offset value. + /// + private long WriteFrame( + TiffStreamWriter writer, + ImageFrame frame, + ImageMetadata imageMetadata, + Image image, + long ifdOffset) where TPixel : unmanaged, IPixel { - var entriesCollector = new TiffEncoderEntriesCollector(); - using TiffBaseCompressor compressor = TiffCompressorFactory.Create( this.CompressionType ?? TiffCompression.None, writer.BaseStream, this.memoryAllocator, - image.Width, + frame.Width, (int)this.BitsPerPixel, this.compressionLevel, this.HorizontalPredictor == TiffPredictor.Horizontal ? this.HorizontalPredictor.Value : TiffPredictor.None); + var entriesCollector = new TiffEncoderEntriesCollector(); using TiffBaseColorWriter colorWriter = TiffColorWriterFactory.Create( this.PhotometricInterpretation, - image.Frames.RootFrame, + frame, this.quantizer, this.memoryAllocator, this.configuration, entriesCollector, (int)this.BitsPerPixel); - int rowsPerStrip = this.CalcRowsPerStrip(image.Frames.RootFrame.Height, colorWriter.BytesPerRow); + int rowsPerStrip = this.CalcRowsPerStrip(frame.Height, colorWriter.BytesPerRow); colorWriter.Write(compressor, rowsPerStrip); + if (image != null) + { + entriesCollector.ProcessMetadata(image); + } + + entriesCollector.ProcessFrameInfo(frame, imageMetadata); entriesCollector.ProcessImageFormat(this); - entriesCollector.ProcessGeneral(image); - writer.WriteMarker(ifdOffset, (uint)writer.Position); - long nextIfdMarker = this.WriteIfd(writer, entriesCollector.Entries); + this.frameMarkers.Add((ifdOffset, (uint)writer.Position)); + + return this.WriteIfd(writer, entriesCollector.Entries); } /// @@ -272,7 +301,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff } else { - var raw = new byte[length]; + byte[] raw = new byte[length]; int sz = ExifWriter.WriteValue(entry, raw, 0); DebugGuard.IsTrue(sz == raw.Length, "Incorrect number of bytes written"); largeDataBlocks.Add(raw); diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 43a0868496..15694978fc 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -6,7 +6,6 @@ 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 { @@ -16,9 +15,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff public List Entries { get; } = new List(); - public void ProcessGeneral(Image image) - where TPixel : unmanaged, IPixel - => new GeneralProcessor(this).Process(image); + public void ProcessMetadata(Image image) + => new MetadataProcessor(this).Process(image); + + public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) + => new FrameInfoProcessor(this).Process(frame, imageMetadata); public void ProcessImageFormat(TiffEncoderCore encoder) => new ImageFormatProcessor(this).Process(encoder); @@ -38,44 +39,35 @@ namespace SixLabors.ImageSharp.Formats.Tiff private void Add(IExifValue entry) => this.Entries.Add(entry); - private class GeneralProcessor + private abstract class BaseProcessor { - private readonly TiffEncoderEntriesCollector collector; + public BaseProcessor(TiffEncoderEntriesCollector collector) => this.Collector = collector; - public GeneralProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; + protected TiffEncoderEntriesCollector Collector { get; } + } - public void Process(Image image) - where TPixel : unmanaged, IPixel + private class MetadataProcessor : BaseProcessor + { + public MetadataProcessor(TiffEncoderEntriesCollector collector) + : base(collector) { - ImageFrame 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.AddOrReplace(width); - this.collector.AddOrReplace(height); + public void Process(Image image) + { + ImageFrame rootFrame = image.Frames.RootFrame; + ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); + byte[] foorFrameXmpBytes = rootFrame.Metadata.XmpProfile; - this.ProcessResolution(image.Metadata, rootFrameExifProfile); - this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpBytes); + this.ProcessProfiles(image.Metadata, rootFrameExifProfile, foorFrameXmpBytes); this.ProcessMetadata(rootFrameExifProfile); - if (!this.collector.Entries.Exists(t => t.Tag == ExifTag.Software)) + if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) { - this.collector.Add(software); + this.Collector.Add(new ExifString(ExifTagValue.Software) + { + Value = SoftwareValue + }); } } @@ -114,26 +106,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - 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) @@ -170,9 +142,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } - if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag)) + if (!this.Collector.Entries.Exists(t => t.Tag == entry.Tag)) { - this.collector.AddOrReplace(entry.DeepClone()); + this.Collector.AddOrReplace(entry.DeepClone()); } } } @@ -183,12 +155,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff { foreach (IExifValue entry in exifProfile.Values) { - if (!this.collector.Entries.Exists(t => t.Tag == entry.Tag) && entry.GetValue() != null) + 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()); + this.Collector.AddOrReplace(entry.DeepClone()); } } } @@ -206,7 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = imageMetadata.IptcProfile.Data }; - this.collector.Add(iptc); + this.Collector.Add(iptc); } else { @@ -220,7 +192,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = imageMetadata.IccProfile.ToByteArray() }; - this.collector.Add(icc); + this.Collector.Add(icc); } else { @@ -234,7 +206,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = xmpProfile }; - this.collector.Add(xmp); + this.Collector.Add(xmp); } else { @@ -243,11 +215,61 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - private class ImageFormatProcessor + private class FrameInfoProcessor : BaseProcessor { - private readonly TiffEncoderEntriesCollector collector; + public FrameInfoProcessor(TiffEncoderEntriesCollector collector) + : base(collector) + { + } + + public void Process(ImageFrame frame, ImageMetadata imageMetadata) + { + this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageWidth) + { + Value = (uint)frame.Width + }); + + this.Collector.AddOrReplace(new ExifLong(ExifTagValue.ImageLength) + { + Value = (uint)frame.Height + }); + + this.ProcessResolution(imageMetadata); + } + + private void ProcessResolution(ImageMetadata imageMetadata) + { + ExifResolutionValues resolution = UnitConverter.GetExifResolutionValues( + imageMetadata.ResolutionUnits, + imageMetadata.HorizontalResolution, + imageMetadata.VerticalResolution); - public ImageFormatProcessor(TiffEncoderEntriesCollector collector) => this.collector = collector; + this.Collector.AddOrReplace(new ExifShort(ExifTagValue.ResolutionUnit) + { + Value = resolution.ResolutionUnit + }); + + if (resolution.VerticalResolution.HasValue && resolution.HorizontalResolution.HasValue) + { + this.Collector.AddOrReplace(new ExifRational(ExifTagValue.XResolution) + { + Value = new Rational(resolution.HorizontalResolution.Value) + }); + + this.Collector.AddOrReplace(new ExifRational(ExifTagValue.YResolution) + { + Value = new Rational(resolution.VerticalResolution.Value) + }); + } + } + } + + private class ImageFormatProcessor : BaseProcessor + { + public ImageFormatProcessor(TiffEncoderEntriesCollector collector) + : base(collector) + { + } public void Process(TiffEncoderCore encoder) { @@ -278,11 +300,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff Value = (ushort)encoder.PhotometricInterpretation }; - this.collector.AddOrReplace(planarConfig); - this.collector.AddOrReplace(samplesPerPixel); - this.collector.AddOrReplace(bitPerSample); - this.collector.AddOrReplace(compression); - this.collector.AddOrReplace(photometricInterpretation); + this.Collector.AddOrReplace(planarConfig); + this.Collector.AddOrReplace(samplesPerPixel); + this.Collector.AddOrReplace(bitPerSample); + this.Collector.AddOrReplace(compression); + this.Collector.AddOrReplace(photometricInterpretation); if (encoder.HorizontalPredictor == TiffPredictor.Horizontal) { @@ -292,7 +314,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff { var predictor = new ExifShort(ExifTagValue.Predictor) { Value = (ushort)TiffPredictor.Horizontal }; - this.collector.AddOrReplace(predictor); + this.Collector.AddOrReplace(predictor); } } } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs index 05a1ca7a24..138274d3f7 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffStreamWriter.cs @@ -126,10 +126,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers /// The four-byte unsigned integer to write. public void WriteMarker(long offset, uint value) { - long currentOffset = this.BaseStream.Position; + long back = this.BaseStream.Position; + this.BaseStream.Seek(offset, SeekOrigin.Begin); + this.Write(value); + this.BaseStream.Seek(back, SeekOrigin.Begin); + } + + public void WriteMarkerFast(long offset, uint value) + { this.BaseStream.Seek(offset, SeekOrigin.Begin); this.Write(value); - this.BaseStream.Seek(currentOffset, SeekOrigin.Begin); } /// diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs new file mode 100644 index 0000000000..71d3663692 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderBaseTester.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public abstract class TiffEncoderBaseTester + { + protected static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); + + protected static void TestStripLength( + TestImageProvider provider, + TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression, + bool useExactComparer = true, + float compareTolerance = 0.01f) + where TPixel : unmanaged, IPixel + { + // arrange + var tiffEncoder = new TiffEncoder() { PhotometricInterpretation = photometricInterpretation, Compression = compression }; + using Image input = provider.GetImage(); + using var memStream = new MemoryStream(); + TiffFrameMetadata inputMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata(); + TiffCompression inputCompression = inputMeta.Compression ?? TiffCompression.None; + + // act + input.Save(memStream, tiffEncoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(memStream); + ExifProfile exifProfileOutput = output.Frames.RootFrame.Metadata.ExifProfile; + TiffFrameMetadata outputMeta = output.Frames.RootFrame.Metadata.GetTiffMetadata(); + ImageFrame rootFrame = output.Frames.RootFrame; + + Number rowsPerStrip = exifProfileOutput.GetValue(ExifTag.RowsPerStrip) != null ? exifProfileOutput.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; + Assert.True(output.Height > (int)rowsPerStrip); + Assert.True(exifProfileOutput.GetValue(ExifTag.StripOffsets)?.Value.Length > 1); + Number[] stripByteCounts = exifProfileOutput.GetValue(ExifTag.StripByteCounts)?.Value; + Assert.NotNull(stripByteCounts); + Assert.True(stripByteCounts.Length > 1); + Assert.NotNull(outputMeta.BitsPerPixel); + + foreach (Number sz in stripByteCounts) + { + Assert.True((uint)sz <= TiffConstants.DefaultStripSize); + } + + // For uncompressed more accurate test. + if (compression == TiffCompression.None) + { + for (int i = 0; i < stripByteCounts.Length - 1; i++) + { + // The difference must be less than one row. + int stripBytes = (int)stripByteCounts[i]; + int widthBytes = ((int)outputMeta.BitsPerPixel + 7) / 8 * rootFrame.Width; + + Assert.True((TiffConstants.DefaultStripSize - stripBytes) < widthBytes); + } + } + + // Compare with reference. + TestTiffEncoderCore( + provider, + inputMeta.BitsPerPixel, + photometricInterpretation, + inputCompression, + useExactComparer: useExactComparer, + compareTolerance: compareTolerance); + } + + protected static void TestTiffEncoderCore( + TestImageProvider provider, + TiffBitsPerPixel? bitsPerPixel, + TiffPhotometricInterpretation photometricInterpretation, + TiffCompression compression = TiffCompression.None, + TiffPredictor predictor = TiffPredictor.None, + bool useExactComparer = true, + float compareTolerance = 0.001f, + IImageDecoder imageDecoder = null) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + var encoder = new TiffEncoder + { + PhotometricInterpretation = photometricInterpretation, + BitsPerPixel = bitsPerPixel, + Compression = compression, + HorizontalPredictor = predictor + }; + + // Does DebugSave & load reference CompareToReferenceInput(): + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), + referenceDecoder: imageDecoder ?? ReferenceDecoder); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs new file mode 100644 index 0000000000..aeca38c5c1 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderMultiframeTests.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using Xunit; + +using static SixLabors.ImageSharp.Tests.TestImages.Tiff; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public class TiffEncoderMultiframeTests : TiffEncoderBaseTester + { + [Theory] + [WithFile(MultiframeLzwPredictor, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); + + [Theory] + [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] + [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_NotSupport(TestImageProvider provider) + where TPixel : unmanaged, IPixel => Assert.Throws(() => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb)); + + [Theory] + [WithFile(MultiframeDeflateWithPreview, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_WithPreview(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb); + + [Theory] + [WithFile(TestImages.Gif.Receipt, PixelTypes.Rgb24)] + [WithFile(TestImages.Gif.Issues.BadDescriptorWidth, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_Convert(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit48, TiffPhotometricInterpretation.Rgb); + + [Theory] + [WithFile(MultiframeLzwPredictor, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_RemoveFrames(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Assert.True(image.Frames.Count > 1); + + image.Frames.RemoveFrame(0); + + TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Bit24; + var encoder = new TiffEncoder + { + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + BitsPerPixel = bitsPerPixel, + Compression = TiffCompression.Deflate + }; + + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + ImageComparer.Exact); + } + + [Theory] + [WithFile(TestImages.Png.Bike, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_AddFrames(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Assert.Equal(1, image.Frames.Count); + + using var image1 = new Image(image.Width, image.Height, Color.Green.ToRgba32()); + + using var image2 = new Image(image.Width, image.Height, Color.Yellow.ToRgba32()); + + image.Frames.AddFrame(image1.Frames.RootFrame); + image.Frames.AddFrame(image2.Frames.RootFrame); + + TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Bit24; + var encoder = new TiffEncoder + { + PhotometricInterpretation = TiffPhotometricInterpretation.Rgb, + BitsPerPixel = bitsPerPixel, + Compression = TiffCompression.Deflate + }; + + using (var ms = new System.IO.MemoryStream()) + { + image.Save(ms, encoder); + + ms.Position = 0; + using var output = Image.Load(ms); + + Assert.Equal(3, output.Frames.Count); + + ImageFrame frame1 = output.Frames[1]; + ImageFrame frame2 = output.Frames[2]; + + Assert.Equal(Color.Green.ToRgba32(), frame1[10, 10]); + Assert.Equal(Color.Yellow.ToRgba32(), frame2[10, 10]); + + Assert.Equal(TiffCompression.Deflate, frame1.Metadata.GetTiffMetadata().Compression); + Assert.Equal(TiffCompression.Deflate, frame1.Metadata.GetTiffMetadata().Compression); + + Assert.Equal(TiffPhotometricInterpretation.Rgb, frame1.Metadata.GetTiffMetadata().PhotometricInterpretation); + Assert.Equal(TiffPhotometricInterpretation.Rgb, frame2.Metadata.GetTiffMetadata().PhotometricInterpretation); + } + + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + ImageComparer.Exact); + } + + [Theory] + [WithBlankImages(100, 100, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeMultiframe_Create(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + using var image0 = new Image(image.Width, image.Height, Color.Red.ToRgba32()); + + using var image1 = new Image(image.Width, image.Height, Color.Green.ToRgba32()); + + using var image2 = new Image(image.Width, image.Height, Color.Yellow.ToRgba32()); + + image.Frames.AddFrame(image0.Frames.RootFrame); + image.Frames.AddFrame(image1.Frames.RootFrame); + image.Frames.AddFrame(image2.Frames.RootFrame); + image.Frames.RemoveFrame(0); + + TiffBitsPerPixel bitsPerPixel = TiffBitsPerPixel.Bit8; + var encoder = new TiffEncoder + { + PhotometricInterpretation = TiffPhotometricInterpretation.PaletteColor, + BitsPerPixel = bitsPerPixel, + Compression = TiffCompression.Lzw + }; + + using (var ms = new System.IO.MemoryStream()) + { + image.Save(ms, encoder); + + ms.Position = 0; + using var output = Image.Load(ms); + + Assert.Equal(3, output.Frames.Count); + + ImageFrame frame0 = output.Frames[0]; + ImageFrame frame1 = output.Frames[1]; + ImageFrame frame2 = output.Frames[2]; + + Assert.Equal(Color.Red.ToRgba32(), frame0[10, 10]); + Assert.Equal(Color.Green.ToRgba32(), frame1[10, 10]); + Assert.Equal(Color.Yellow.ToRgba32(), frame2[10, 10]); + + Assert.Equal(TiffCompression.Lzw, frame0.Metadata.GetTiffMetadata().Compression); + Assert.Equal(TiffCompression.Lzw, frame1.Metadata.GetTiffMetadata().Compression); + Assert.Equal(TiffCompression.Lzw, frame1.Metadata.GetTiffMetadata().Compression); + + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frame0.Metadata.GetTiffMetadata().PhotometricInterpretation); + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frame1.Metadata.GetTiffMetadata().PhotometricInterpretation); + Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frame2.Metadata.GetTiffMetadata().PhotometricInterpretation); + } + + image.VerifyEncoder( + provider, + "tiff", + bitsPerPixel, + encoder, + ImageComparer.Exact); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 0286671ae8..47b6fcf727 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -2,14 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System.IO; - -using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Constants; -using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit; @@ -19,10 +14,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { [Collection("RunSerial")] [Trait("Format", "Tiff")] - public class TiffEncoderTests + public class TiffEncoderTests : TiffEncoderBaseTester { - private static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(); - [Theory] [InlineData(null, TiffBitsPerPixel.Bit24)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffBitsPerPixel.Bit24)] @@ -451,96 +444,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff var encoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation }; image.DebugSave(provider, encoder); } - - private static void TestStripLength( - TestImageProvider provider, - TiffPhotometricInterpretation photometricInterpretation, - TiffCompression compression, - bool useExactComparer = true, - float compareTolerance = 0.01f) - where TPixel : unmanaged, IPixel - { - // arrange - var tiffEncoder = new TiffEncoder() { PhotometricInterpretation = photometricInterpretation, Compression = compression }; - using Image input = provider.GetImage(); - using var memStream = new MemoryStream(); - TiffFrameMetadata inputMeta = input.Frames.RootFrame.Metadata.GetTiffMetadata(); - TiffCompression inputCompression = inputMeta.Compression ?? TiffCompression.None; - - // act - input.Save(memStream, tiffEncoder); - - // assert - memStream.Position = 0; - using var output = Image.Load(memStream); - ExifProfile exifProfileOutput = output.Frames.RootFrame.Metadata.ExifProfile; - TiffFrameMetadata outputMeta = output.Frames.RootFrame.Metadata.GetTiffMetadata(); - ImageFrame rootFrame = output.Frames.RootFrame; - - Number rowsPerStrip = exifProfileOutput.GetValue(ExifTag.RowsPerStrip) != null ? exifProfileOutput.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; - Assert.True(output.Height > (int)rowsPerStrip); - Assert.True(exifProfileOutput.GetValue(ExifTag.StripOffsets)?.Value.Length > 1); - Number[] stripByteCounts = exifProfileOutput.GetValue(ExifTag.StripByteCounts)?.Value; - Assert.NotNull(stripByteCounts); - Assert.True(stripByteCounts.Length > 1); - Assert.NotNull(outputMeta.BitsPerPixel); - - foreach (Number sz in stripByteCounts) - { - Assert.True((uint)sz <= TiffConstants.DefaultStripSize); - } - - // For uncompressed more accurate test. - if (compression == TiffCompression.None) - { - for (int i = 0; i < stripByteCounts.Length - 1; i++) - { - // The difference must be less than one row. - int stripBytes = (int)stripByteCounts[i]; - int widthBytes = ((int)outputMeta.BitsPerPixel + 7) / 8 * rootFrame.Width; - - Assert.True((TiffConstants.DefaultStripSize - stripBytes) < widthBytes); - } - } - - // Compare with reference. - TestTiffEncoderCore( - provider, - inputMeta.BitsPerPixel, - photometricInterpretation, - inputCompression, - useExactComparer: useExactComparer, - compareTolerance: compareTolerance); - } - - private static void TestTiffEncoderCore( - TestImageProvider provider, - TiffBitsPerPixel? bitsPerPixel, - TiffPhotometricInterpretation photometricInterpretation, - TiffCompression compression = TiffCompression.None, - TiffPredictor predictor = TiffPredictor.None, - bool useExactComparer = true, - float compareTolerance = 0.001f, - IImageDecoder imageDecoder = null) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(); - var encoder = new TiffEncoder - { - PhotometricInterpretation = photometricInterpretation, - BitsPerPixel = bitsPerPixel, - Compression = compression, - HorizontalPredictor = predictor - }; - - // Does DebugSave & load reference CompareToReferenceInput(): - image.VerifyEncoder( - provider, - "tiff", - bitsPerPixel, - encoder, - useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), - referenceDecoder: imageDecoder ?? ReferenceDecoder); - } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index c80d9fc165..f9535607c6 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -278,8 +278,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff PixelResolutionUnit resolutionUnitInput = UnitConverter.ExifProfileToResolutionUnit(exifProfileInput); PixelResolutionUnit resolutionUnitEncoded = UnitConverter.ExifProfileToResolutionUnit(encodedImageExifProfile); Assert.Equal(resolutionUnitInput, resolutionUnitEncoded); - Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution), encodedImageExifProfile.GetValue(ExifTag.XResolution)); - Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution), encodedImageExifProfile.GetValue(ExifTag.YResolution)); + Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble()); + Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble()); Assert.Equal(xmpProfileInput, encodedImageXmpProfile);