Repository: SixLabors/ImageSharp

Issue:
created/overwritten/copied TIFF images are invalid for Adobe Premiere Pro
### Prerequisites

- [X] I have written a descriptive issue title
- [X] I have verified that I am running the latest version of ImageSharp
- [X] I have verified if the problem exist in both `DEBUG` and `RELEASE` mode
- [X] I have searched [open](https://github.com/SixLabors/ImageSharp/issues) and [closed](https://github.com/SixLabors/ImageSharp/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported

### ImageSharp version

2.1.3

### Other ImageSharp packages and versions

SixLabors.ImageSharp 2.1.3, SixLabors.ImageSharp.Drawing 1.0.0-beta14, SixLabors.Fonts 1.0.0-beta17

### Environment (Operating system, version and so on)

Windows Server 2019 Standard 1809

### .NET Framework version

.Net 6.0, .Net Framework 4.8

### Description

Tiff files created by ImageSharp are not recognized by Adobe Premiere Pro on any try of import.
If you try to import a tiff file an error will pop up: "The file cannot be opened because of a header error"
If you take a tiff file created by ImageSharp, open it with Paint or Adobe Photoshop and save it without any changes, choosing TIFF format, Importing files to Premiere Pro becomes possible.
It seems there is an issue, when encoding the tiff image by ImageSharp since other software can save the image in format properly. I could not find any differences in metadata that would indicate which parameter is invalid.

### Steps to Reproduce

`using Image<Rgba32> image = new(1920, 1080);`
`using MemoryStream stream = new();`
`image.SaveAsTiff(output);`

### Images

![Premiere Pro error](https://user-images.githubusercontent.com/77407760/202243562-ef7ccfb3-580e-426d-814c-be6606d5eddf.png)



Hints:
What is the memory stream used for in your repro? Seems unnecessary.

The following should be enough:
```
using Image<Rgba32> image = new(1920, 1080);
image.SaveAsTiff("output.tiff");
``` 

I kind of doubt that there is an issue with "the file header" of ImageSharp tiff's.
`tiffinfo` from `libtiff` does not report any errors. ImageMagick with `imagemagick identify -verbose` does not report any error.

Also I can open ImageSharp Tiff images with any viewer I have: 
- gimp
- pant.net
- xnview
- imagemagick
- irfanview
- paint

None of those complains. I do not have access to Adobe Premiere Pro.
Hi,

Thank you for a prompt reply.

The memory stream was my bad, I forgot to delete it after simplifying my
example.

I have to agree with you about the header, I took the stream of the file to
decode the header into hexdec string, and read about tiff format headers.
It all seems fine. I also read on various blogs that Premiere Pro "header
error" is an unhandled exception for any issues when importing files. I
have spent several hours with their support trying to get them to tell me
what exactly makes the file crash but no luck, "file is no good" is the
best I got out of them.

I conducted multiple tests, creating files in all possible ways using
ImageSharp. I don't have this problem with png or jpeg.

I created a file in Photoshop to ensure that it works fine in Premiere pro,
it's the same FullHD Black background (valid.tif attached). I thought if I
load it via ImageSharp and just Save a copy of it without any changes
(Copy.tif attached), the file should be identical, therefore be acceptable
for Premiere Pro.

using Image image = Image.Load(fileToCopy, out IImageFormat format);

using MemoryStream stream = new();

image.SaveAsTiff(stream);

File.WriteAllBytes(outputPath, stream.ToArray());


The output Copy.tiff file throws the same error when importing with
PremierePro. Again, if I open it in Photoshop or Paint, save as Tiff, it
will work just fine.


I tried comparing 2 files, MediaInfo shows identical metadata, and the
decoded header indicates Tiff properly. The only thing out of order is
different file sizes. If I copy the files using File Explorer, the created
copy has the same file size. From those two facts I presume there must be
some differences between those files, and whatever it is it causes Premiere
to crash.
[Attachments.zip](https://github.com/SixLabors/ImageSharp/files/10030359/Attachments.zip)

Maybe try a 1x1 pixel image. Save a 1x1 black pixel image with ImageSharp and again with another program you know is working with PremierePro. Then compare those binary and see what the difference is.

Maybe it's as simple as the file extension is not as expected? Tiff instead of Tif?
A quick google search shows that this indeed can cause this issue.

Other then that, I am not sure what we can do about it. If `libtiff` does not complain, I am pretty sure the tiff is valid.

I really think the PremierePro supports needs to tell why this error is happening.
I created 2 1px files:
-1pxPaint.tif
-1pxImageSharp.tif

1pxImageSharp was created like so:
```
using Image<Rgba32> image = new(1, 1);
TiffEncoder tiffEncoder = new()
{
	BitsPerPixel = TiffBitsPerPixel.Bit24,
	PhotometricInterpretation = SixLabors.ImageSharp.Formats.Tiff.Constants.TiffPhotometricInterpretation.Rgb,
	Compression = SixLabors.ImageSharp.Formats.Tiff.Constants.TiffCompression.Lzw
};
image.SaveAsTiff(@"\\plpns01\pl-sdi\TEMP\m_szulborski\Test\DubcardGenerator\1pxImageSharp.tif", tiffEncoder);
```
Encoder was needed because Paint creates Tiff with LZW Compression by default, and I wanted files to be identical.

Comparing 2 files I can see:
 - Different file size: ImageSharp 221KB, Paint 222KB
 - Difference in **header** and the entire byte array (I read that header is first 8 bytes)

1pxPaint:
**49492A000E000000**8000205010000F00FE00040001000000000000000001040001000000010000000101040001000000010000000201030003000000C80000000301030001000000050000000601030001000000020000001101040001000000080000001501030001000000030000001601040001000000010000001701040001000000050000001A01050001000000CE0000001B01050001000000D60000001C01030001000000010000002801030001000000020000003D01030001000000020000000000000008000800080000770100E803000000770100E8030000
1pxImageSharp:
**49492A000D000000**80002050100E000001040001000000010000000101040001000000010000000201030003000000BB0000000301030001000000050000000601030001000000020000001101040001000000080000001501040001000000030000001601040001000000010000001701040001000000050000001A01050001000000C10000001B01050001000000C90000001C0103000100000001000000280103000100000002000000310102000B000000D10000000000000008000800080060000000010000006000000001000000496D61676553686172700000

[1pxImages.zip](https://github.com/SixLabors/ImageSharp/files/10033099/1pxImages.zip)

> I created 2 1px files: -1pxPaint.tif -1pxImageSharp.tif
> 
> 1pxImageSharp was created like so:
> 

paint:
4949 byteorder
2A00 magick byte   
0E00 FirstIfdOffset

imagesharp:
4949 byteorder
2A00 magick byte
0D00 FirstIfdOffset

The difference is that imagesharp ifd offset starts at offset 13 and paint starts at 14. After double checking the spec, this could be indeed a violation of the spec: "The directory may be at any location in the file after the header but must begin on a word boundary."

Patch:
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
index 2013377ed6..94c2253d18 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
@@ -244,6 +244,12 @@ private long WriteFrame<TPixel>(
         entriesCollector.ProcessFrameInfo(frame, imageMetadata);
         entriesCollector.ProcessImageFormat(this);
 
+        if (writer.Position % 2 != 0)
+        {
+            // Write padding byte, because the tiff spec requires ifd offset to begin on a word boundary.
+            writer.Write(0);
+        }
+
         this.frameMarkers.Add((ifdOffset, (uint)writer.Position));
 
         return this.WriteIfd(writer, entriesCollector.Entries);
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
index 64b300cd60..a84b8aed52 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
@@ -178,7 +178,7 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
                     Value = imageMetadata.IptcProfile.Data
                 };
 
-                this.Collector.Add(iptc);
+                this.Collector.AddOrReplace(iptc);
             }
             else
             {
@@ -192,7 +192,7 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
                     Value = imageMetadata.IccProfile.ToByteArray()
                 };
 
-                this.Collector.Add(icc);
+                this.Collector.AddOrReplace(icc);
             }
             else
             {
@@ -206,7 +206,7 @@ private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, Exi
                     Value = xmpProfile.Data
                 };
 
-                this.Collector.Add(xmp);
+                this.Collector.AddOrReplace(xmp);
             }
             else
             {
@@ -278,7 +278,7 @@ public void Process(TiffEncoderCore encoder)
                 Value = (ushort)TiffPlanarConfiguration.Chunky
             };
 
-            ExifLong samplesPerPixel = new(ExifTagValue.SamplesPerPixel)
+            ExifShort samplesPerPixel = new(ExifTagValue.SamplesPerPixel)
             {
                 Value = GetSamplesPerPixel(encoder)
             };
@@ -317,7 +317,7 @@ TiffPhotometricInterpretation.PaletteColor or
             }
         }
 
-        private static uint GetSamplesPerPixel(TiffEncoderCore encoder)
+        private static ushort GetSamplesPerPixel(TiffEncoderCore encoder)
             => encoder.PhotometricInterpretation switch
             {
                 TiffPhotometricInterpretation.PaletteColor or


Test patch:
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
index 7b55c1c5d0..2c30cc3d05 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
@@ -246,6 +246,25 @@ public void TiffEncoder_PreservesPredictor<TPixel>(TestImageProvider<TPixel> pro
         Assert.Equal(expectedPredictor, frameMetadata.Predictor);
     }
 
+    // https://github.com/SixLabors/ImageSharp/issues/2297
+    [Fact]
+    public void TiffEncoder_WritesIfdOffsetAtWordBoundary()
+    {
+        // arrange
+        var tiffEncoder = new TiffEncoder();
+        using var memStream = new MemoryStream();
+        using Image<Rgba32> image = new(1, 1);
+        byte[] expectedIfdOffsetBytes = { 12, 0 };
+
+        // act
+        image.Save(memStream, tiffEncoder);
+
+        // assert
+        byte[] imageBytes = memStream.ToArray();
+        Assert.Equal(imageBytes[4], expectedIfdOffsetBytes[0]);
+        Assert.Equal(imageBytes[5], expectedIfdOffsetBytes[1]);
+    }
+
     [Theory]
     [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.CcittGroup3Fax, TiffCompression.CcittGroup3Fax)]
     [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.CcittGroup4Fax, TiffCompression.CcittGroup4Fax)]
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
index 8f4345b2e8..ffe4c573b3 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
@@ -71,7 +71,6 @@ private static void VerifyExpectedTiffFrameMetaDataIsPresent(TiffFrameMetadata f
         Assert.Equal(TiffBitsPerPixel.Bit4, frameMetaData.BitsPerPixel);
         Assert.Equal(TiffCompression.Lzw, frameMetaData.Compression);
         Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frameMetaData.PhotometricInterpretation);
-        Assert.Equal(TiffPredictor.None, frameMetaData.Predictor);
     }
 
     [Theory]
@@ -129,7 +128,7 @@ public void MetadataProfiles<TPixel>(TestImageProvider<TPixel> provider, bool ig
             Assert.NotNull(rootFrameMetaData.XmpProfile);
             Assert.NotNull(rootFrameMetaData.ExifProfile);
             Assert.Equal(2599, rootFrameMetaData.XmpProfile.Data.Length);
-            Assert.Equal(26, rootFrameMetaData.ExifProfile.Values.Count);
+            Assert.Equal(25, rootFrameMetaData.ExifProfile.Values.Count);
         }
     }
 
@@ -163,38 +162,33 @@ public void BaselineTags<TPixel>(TestImageProvider<TPixel> provider)
         TiffFrameMetadata tiffFrameMetadata = rootFrame.Metadata.GetTiffMetadata();
         Assert.NotNull(exifProfile);
 
-        // The original exifProfile has 30 values, but 4 of those values will be stored in the TiffFrameMetaData
-        // and removed from the profile on decode.
-        Assert.Equal(26, exifProfile.Values.Count);
+        Assert.Equal(25, exifProfile.Values.Count);
         Assert.Equal(TiffBitsPerPixel.Bit4, tiffFrameMetadata.BitsPerPixel);
         Assert.Equal(TiffCompression.Lzw, tiffFrameMetadata.Compression);
-        Assert.Equal("This is Название", exifProfile.GetValue(ExifTag.ImageDescription).Value);
-        Assert.Equal("This is Изготовитель камеры", exifProfile.GetValue(ExifTag.Make).Value);
-        Assert.Equal("This is Модель камеры", exifProfile.GetValue(ExifTag.Model).Value);
-        Assert.Equal("IrfanView", exifProfile.GetValue(ExifTag.Software).Value);
+        Assert.Equal("ImageDescription", exifProfile.GetValue(ExifTag.ImageDescription).Value);
+        Assert.Equal("Make", exifProfile.GetValue(ExifTag.Make).Value);
+        Assert.Equal("Model", exifProfile.GetValue(ExifTag.Model).Value);
+        Assert.Equal("ImageSharp", exifProfile.GetValue(ExifTag.Software).Value);
         Assert.Null(exifProfile.GetValue(ExifTag.DateTime)?.Value);
-        Assert.Equal("This is author1;Author2", exifProfile.GetValue(ExifTag.Artist).Value);
+        Assert.Equal("Artist", exifProfile.GetValue(ExifTag.Artist).Value);
         Assert.Null(exifProfile.GetValue(ExifTag.HostComputer)?.Value);
-        Assert.Equal("This is Авторские права", exifProfile.GetValue(ExifTag.Copyright).Value);
+        Assert.Equal("Copyright", exifProfile.GetValue(ExifTag.Copyright).Value);
         Assert.Equal(4, exifProfile.GetValue(ExifTag.Rating).Value);
         Assert.Equal(75, exifProfile.GetValue(ExifTag.RatingPercent).Value);
-        var expectedResolution = new Rational(10000, 1000, simplify: false);
+        var expectedResolution = new Rational(10, 1, simplify: false);
         Assert.Equal(expectedResolution, exifProfile.GetValue(ExifTag.XResolution).Value);
         Assert.Equal(expectedResolution, exifProfile.GetValue(ExifTag.YResolution).Value);
         Assert.Equal(new Number[] { 8u }, exifProfile.GetValue(ExifTag.StripOffsets)?.Value, new NumberComparer());
-        Assert.Equal(new Number[] { 297u }, exifProfile.GetValue(ExifTag.StripByteCounts)?.Value, new NumberComparer());
+        Assert.Equal(new Number[] { 285u }, exifProfile.GetValue(ExifTag.StripByteCounts)?.Value, new NumberComparer());
         Assert.Null(exifProfile.GetValue(ExifTag.ExtraSamples)?.Value);
         Assert.Equal(32u, exifProfile.GetValue(ExifTag.RowsPerStrip).Value);
         Assert.Null(exifProfile.GetValue(ExifTag.SampleFormat));
-        Assert.Equal(TiffPredictor.None, tiffFrameMetadata.Predictor);
         Assert.Equal(PixelResolutionUnit.PixelsPerInch, UnitConverter.ExifProfileToResolutionUnit(exifProfile));
         ushort[] colorMap = exifProfile.GetValue(ExifTag.ColorMap)?.Value;
         Assert.NotNull(colorMap);
         Assert.Equal(48, colorMap.Length);
-        Assert.Equal(10537, colorMap[0]);
-        Assert.Equal(14392, colorMap[1]);
-        Assert.Equal(58596, colorMap[46]);
-        Assert.Equal(3855, colorMap[47]);
+        Assert.Equal(4369, colorMap[0]);
+        Assert.Equal(8738, colorMap[1]);
         Assert.Equal(TiffPhotometricInterpretation.PaletteColor, tiffFrameMetadata.PhotometricInterpretation);
         Assert.Equal(1u, exifProfile.GetValue(ExifTag.SamplesPerPixel).Value);
 
@@ -238,7 +232,7 @@ public void Encode_PreservesMetadata<TPixel>(TestImageProvider<TPixel> provider)
     {
         // Load Tiff image
         DecoderOptions options = new() { SkipMetadata = false };
-        using Image<TPixel> image = provider.GetImage(new TiffDecoder(), options);
+        using Image<TPixel> image = provider.GetImage(TiffDecoder, options);
 
         ImageMetadata inputMetaData = image.Metadata;
         ImageFrame<TPixel> rootFrameInput = image.Frames.RootFrame;
@@ -284,17 +278,15 @@ public void Encode_PreservesMetadata<TPixel>(TestImageProvider<TPixel> provider)
         Assert.NotNull(encodedImageXmpProfile);
         Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data);
 
-        Assert.Equal("IrfanView", exifProfileInput.GetValue(ExifTag.Software).Value);
-        Assert.Equal("This is Название", exifProfileInput.GetValue(ExifTag.ImageDescription).Value);
-        Assert.Equal("This is Изготовитель камеры", exifProfileInput.GetValue(ExifTag.Make).Value);
-        Assert.Equal("This is Авторские права", exifProfileInput.GetValue(ExifTag.Copyright).Value);
-
+        Assert.Equal(exifProfileInput.GetValue(ExifTag.Software).Value, encodedImageExifProfile.GetValue(ExifTag.Software).Value);
         Assert.Equal(exifProfileInput.GetValue(ExifTag.ImageDescription).Value, encodedImageExifProfile.GetValue(ExifTag.ImageDescription).Value);
         Assert.Equal(exifProfileInput.GetValue(ExifTag.Make).Value, encodedImageExifProfile.GetValue(ExifTag.Make).Value);
         Assert.Equal(exifProfileInput.GetValue(ExifTag.Copyright).Value, encodedImageExifProfile.GetValue(ExifTag.Copyright).Value);
+        Assert.Equal(exifProfileInput.GetValue(ExifTag.Artist).Value, encodedImageExifProfile.GetValue(ExifTag.Artist).Value);
+        Assert.Equal(exifProfileInput.GetValue(ExifTag.Orientation).Value, encodedImageExifProfile.GetValue(ExifTag.Orientation).Value);
+        Assert.Equal(exifProfileInput.GetValue(ExifTag.Model).Value, encodedImageExifProfile.GetValue(ExifTag.Model).Value);
 
-        // Note that the encoded profile has PlanarConfiguration explicitly set, which is missing in the original image profile.
         Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value);
-        Assert.Equal(exifProfileInput.Values.Count + 1, encodedImageExifProfile.Values.Count);
+        Assert.Equal(exifProfileInput.Values.Count, encodedImageExifProfile.Values.Count);
     }
 }
diff --git a/tests/Images/Input/Tiff/metadata_sample.tiff b/tests/Images/Input/Tiff/metadata_sample.tiff
index d767352688..9c0671ee7c 100644
--- a/tests/Images/Input/Tiff/metadata_sample.tiff
+++ b/tests/Images/Input/Tiff/metadata_sample.tiff
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:72a1c8022d699e0e7248940f0734d01d6ab9bf4a71022e8b5626b64d66a5f39d
-size 8107
+oid sha256:c2f20e3ebb51b743b635377b93e78a6dc91c07ca97be38f500c4d6d3c465d9c1
+size 3472