public void TestReadJpegSegments_InvalidData()
 {
     var app2Bytes = File.ReadAllBytes("Tests/Data/iccDataInvalid1.jpg.app2");
     var directory = new IccReader().ReadJpegSegments(new[] { app2Bytes }, JpegSegmentType.App2);
     Assert.NotNull(directory);
     Assert.True(directory.Single().HasError);
 }
        public void ReadJpegSegments_InvalidData()
        {
            var app2      = new JpegSegment(JpegSegmentType.App2, TestDataUtil.GetBytes("Data/iccDataInvalid1.jpg.app2"), offset: 0);
            var directory = new IccReader().ReadJpegSegments(new[] { app2 });

            Assert.NotNull(directory);
            Assert.True(directory.Single().HasError);
        }
        public void TestReadJpegSegments_InvalidData()
        {
            var app2Bytes = File.ReadAllBytes("Tests/Data/iccDataInvalid1.jpg.app2");
            var directory = new IccReader().ReadJpegSegments(new[] { app2Bytes }, JpegSegmentType.App2);

            Assert.NotNull(directory);
            Assert.True(directory.Single().HasError);
        }
Example #4
0
        public void ReadProfile_DuplicateEntry()
        {
            IccReader reader = this.CreateReader();

            IccProfile output = reader.Read(IccTestDataProfiles.Profile_Random_Array);

            Assert.Equal(2, output.Entries.Length);
            Assert.True(ReferenceEquals(output.Entries[0], output.Entries[1]));
        }
        public void TestExtract_InvalidData()
        {
            var app2Bytes = File.ReadAllBytes("Tests/Data/iccDataInvalid1.jpg.app2");

            // When in an APP2 segment, ICC data starts after a 14-byte preamble
            var icc = TestHelper.SkipBytes(app2Bytes, 14);
            var directory = new IccReader().Extract(new ByteArrayReader(icc));
            Assert.NotNull(directory);
            Assert.True(directory.HasError);
        }
        public void Extract_InvalidData()
        {
            var app2Bytes = File.ReadAllBytes("Data/iccDataInvalid1.jpg.app2");

            // When in an APP2 segment, ICC data starts after a 14-byte preamble
            var icc       = TestHelper.SkipBytes(app2Bytes, 14);
            var directory = new IccReader().Extract(new ByteArrayReader(icc));

            Assert.NotNull(directory);
            Assert.True(directory.HasError);
        }
        public void Extract_ProfileDateTime()
        {
            var app2 = new JpegSegment(JpegSegmentType.App2, File.ReadAllBytes("Data/withExifAndIptc.jpg.app2"), offset: 0);

            var directory = new IccReader()
                            .ReadJpegSegments(new[] { app2 })
                            .OfType <IccDirectory>()
                            .Single();

            Assert.NotNull(directory);
//            Assert.Equal("1998:02:09 06:49:00", directory.GetString(IccDirectory.TagProfileDateTime));
            Assert.Equal(new DateTime(1998, 2, 9, 6, 49, 0), directory.GetDateTime(IccDirectory.TagProfileDateTime));
        }
        public void TestExtract_ProfileDateTime()
        {
            var app2Bytes = File.ReadAllBytes("Tests/Data/withExifAndIptc.jpg.app2");

            var directory = new IccReader()
                .ReadJpegSegments(new[] { app2Bytes }, JpegSegmentType.App2)
                .OfType<IccDirectory>()
                .Single();

            Assert.NotNull(directory);
            //            Assert.Equal("1998:02:09 06:49:00", directory.GetString(IccDirectory.TagProfileDateTime));
            Assert.Equal(new DateTime(1998, 2, 9, 6, 49, 0), directory.GetDateTime(IccDirectory.TagProfileDateTime));
        }
        public DirectoryList Extract(SequentialReader reader, int length)
        {
            var directory = new PhotoshopDirectory();

            var directories = new List <Directory> {
                directory
            };

            // Data contains a sequence of Image Resource Blocks (IRBs):
            //
            // 4 bytes - Signature; mostly "8BIM" but "PHUT", "AgHg" and "DCSR" are also found
            // 2 bytes - Resource identifier
            // String  - Pascal string, padded to make length even
            // 4 bytes - Size of resource data which follows
            // Data    - The resource data, padded to make size even
            //
            // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504
            var pos = 0;

            while (pos < length)
            {
                try
                {
                    // 4 bytes for the signature ("8BIM", "PHUT", etc.)
                    var signature = reader.GetString(4, Encoding.UTF8);
                    pos += 4;

                    // 2 bytes for the resource identifier (tag type).
                    var tagType = reader.GetUInt16();
                    pos += 2;

                    // A variable number of bytes holding a pascal string (two leading bytes for length).
                    var descriptionLength = reader.GetByte();
                    pos += 1;

                    // Some basic bounds checking
                    if (descriptionLength + pos > length)
                    {
                        throw new ImageProcessingException("Invalid string length");
                    }

                    // We don't use the string value here
                    reader.Skip(descriptionLength);
                    pos += descriptionLength;

                    // The number of bytes is padded with a trailing zero, if needed, to make the size even.
                    if (pos % 2 != 0)
                    {
                        reader.Skip(1);
                        pos++;
                    }

                    // 4 bytes for the size of the resource data that follows.
                    var byteCount = reader.GetInt32();
                    pos += 4;

                    // The resource data.
                    var tagBytes = reader.GetBytes(byteCount);
                    pos += byteCount;

                    // The number of bytes is padded with a trailing zero, if needed, to make the size even.
                    if (pos % 2 != 0)
                    {
                        reader.Skip(1);
                        pos++;
                    }

                    // Skip any unsupported IRBs
                    if (signature != "8BIM")
                    {
                        continue;
                    }

                    switch (tagType)
                    {
                    case PhotoshopDirectory.TagIptc:
                        var iptcDirectory = new IptcReader().Extract(new SequentialByteArrayReader(tagBytes), tagBytes.Length);
                        iptcDirectory.Parent = directory;
                        directories.Add(iptcDirectory);
                        break;

                    case PhotoshopDirectory.TagIccProfileBytes:
                        var iccDirectory = new IccReader().Extract(new ByteArrayReader(tagBytes));
                        iccDirectory.Parent = directory;
                        directories.Add(iccDirectory);
                        break;

                    case PhotoshopDirectory.TagExifData1:
                    case PhotoshopDirectory.TagExifData3:
                        var exifDirectories = new ExifReader().Extract(new ByteArrayReader(tagBytes));
                        foreach (var exifDirectory in exifDirectories.Where(d => d.Parent == null))
                        {
                            exifDirectory.Parent = directory;
                        }
                        directories.AddRange(exifDirectories);
                        break;

                    case PhotoshopDirectory.TagXmpData:
                        var xmpDirectory = new XmpReader().Extract(tagBytes);
                        xmpDirectory.Parent = directory;
                        directories.Add(xmpDirectory);
                        break;

                    default:
                        directory.Set(tagType, tagBytes);
                        break;
                    }

                    if (tagType >= 0x0fa0 && tagType <= 0x1387)
                    {
                        PhotoshopDirectory.TagNameMap[tagType] = $"Plug-in {tagType - 0x0fa0 + 1} Data";
                    }
                }
                catch (Exception ex)
                {
                    directory.AddError(ex.Message);
                    break;
                }
            }

            return(directories);
        }
 public void GetStringFromUInt32()
 {
     Assert.Equal("ABCD", IccReader.GetStringFromUInt32(0x41424344u));
 }
Example #11
0
        /// <exception cref="PngProcessingException"/>
        /// <exception cref="System.IO.IOException"/>
        private static IEnumerable <Directory> ProcessChunk([NotNull] PngChunk chunk)
        {
            var chunkType = chunk.ChunkType;
            var bytes     = chunk.Bytes;

            if (chunkType == PngChunkType.IHDR)
            {
                var header    = new PngHeader(bytes);
                var directory = new PngDirectory(PngChunkType.IHDR);
                directory.Set(PngDirectory.TagImageWidth, header.ImageWidth);
                directory.Set(PngDirectory.TagImageHeight, header.ImageHeight);
                directory.Set(PngDirectory.TagBitsPerSample, header.BitsPerSample);
                directory.Set(PngDirectory.TagColorType, header.ColorType.NumericValue);
                directory.Set(PngDirectory.TagCompressionType, header.CompressionType);
                directory.Set(PngDirectory.TagFilterMethod, header.FilterMethod);
                directory.Set(PngDirectory.TagInterlaceMethod, header.InterlaceMethod);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.PLTE)
            {
                var directory = new PngDirectory(PngChunkType.PLTE);
                directory.Set(PngDirectory.TagPaletteSize, bytes.Length / 3);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.tRNS)
            {
                var directory = new PngDirectory(PngChunkType.tRNS);
                directory.Set(PngDirectory.TagPaletteHasTransparency, 1);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.sRGB)
            {
                int srgbRenderingIntent = unchecked ((sbyte)bytes[0]);
                var directory           = new PngDirectory(PngChunkType.sRGB);
                directory.Set(PngDirectory.TagSrgbRenderingIntent, srgbRenderingIntent);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.cHRM)
            {
                var chromaticities = new PngChromaticities(bytes);
                var directory      = new PngChromaticitiesDirectory();
                directory.Set(PngChromaticitiesDirectory.TagWhitePointX, chromaticities.WhitePointX);
                directory.Set(PngChromaticitiesDirectory.TagWhitePointY, chromaticities.WhitePointY);
                directory.Set(PngChromaticitiesDirectory.TagRedX, chromaticities.RedX);
                directory.Set(PngChromaticitiesDirectory.TagRedY, chromaticities.RedY);
                directory.Set(PngChromaticitiesDirectory.TagGreenX, chromaticities.GreenX);
                directory.Set(PngChromaticitiesDirectory.TagGreenY, chromaticities.GreenY);
                directory.Set(PngChromaticitiesDirectory.TagBlueX, chromaticities.BlueX);
                directory.Set(PngChromaticitiesDirectory.TagBlueY, chromaticities.BlueY);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.gAMA)
            {
                var gammaInt  = ByteConvert.ToInt32BigEndian(bytes);
                var directory = new PngDirectory(PngChunkType.gAMA);
                directory.Set(PngDirectory.TagGamma, gammaInt / 100000.0);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.iCCP)
            {
                var reader      = new SequentialByteArrayReader(bytes);
                var profileName = reader.GetNullTerminatedStringValue(maxLengthBytes: 79);
                var directory   = new PngDirectory(PngChunkType.iCCP);
                directory.Set(PngDirectory.TagIccProfileName, profileName);
                var compressionMethod = reader.GetSByte();
                if (compressionMethod == 0)
                {
                    // Only compression method allowed by the spec is zero: deflate
                    // This assumes 1-byte-per-char, which it is by spec.
                    var bytesLeft         = bytes.Length - profileName.Bytes.Length - 2;
                    var compressedProfile = reader.GetBytes(bytesLeft);
                    using (var inflaterStream = new DeflateStream(new MemoryStream(compressedProfile), CompressionMode.Decompress))
//                    using (var inflaterStream = new InflaterInputStream(new MemoryStream(compressedProfile)))
                    {
                        var iccDirectory = new IccReader().Extract(new IndexedCapturingReader(inflaterStream));
                        iccDirectory.Parent = directory;
                        yield return(iccDirectory);
                    }
                }
                else
                {
                    directory.AddError("Invalid compression method value");
                }
                yield return(directory);
            }
            else if (chunkType == PngChunkType.bKGD)
            {
                var directory = new PngDirectory(PngChunkType.bKGD);
                directory.Set(PngDirectory.TagBackgroundColor, bytes);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.tEXt)
            {
                var reader    = new SequentialByteArrayReader(bytes);
                var keyword   = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding);
                var bytesLeft = bytes.Length - keyword.Length - 1;
                var value     = reader.GetNullTerminatedStringValue(bytesLeft, _latin1Encoding);

                var textPairs = new List <KeyValuePair> {
                    new KeyValuePair(keyword, value)
                };
                var directory = new PngDirectory(PngChunkType.iTXt);
                directory.Set(PngDirectory.TagTextualData, textPairs);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.zTXt)
            {
                var reader            = new SequentialByteArrayReader(bytes);
                var keyword           = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding);
                var compressionMethod = reader.GetSByte();

                var    bytesLeft = bytes.Length - keyword.Length - 1 - 1 - 1 - 1;
                byte[] textBytes = null;
                if (compressionMethod == 0)
                {
                    using (var inflaterStream = new DeflateStream(new MemoryStream(bytes, bytes.Length - bytesLeft, bytesLeft), CompressionMode.Decompress))
                        using (var decompStream = new MemoryStream())
                        {
#if !NET35
                            inflaterStream.CopyTo(decompStream);
#else
                            byte[] buffer = new byte[256];
                            int    count;
                            int    totalBytes = 0;
                            while ((count = inflaterStream.Read(buffer, 0, 256)) > 0)
                            {
                                decompStream.Write(buffer, 0, count);
                                totalBytes += count;
                            }
#endif
                            textBytes = decompStream.ToArray();
                        }
                }
                else
                {
                    var directory = new PngDirectory(PngChunkType.zTXt);
                    directory.AddError("Invalid compression method value");
                    yield return(directory);
                }
                if (textBytes != null)
                {
                    if (keyword == "XML:com.adobe.xmp")
                    {
                        // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary
                        yield return(new XmpReader().Extract(textBytes));
                    }
                    else
                    {
                        var textPairs = new List <KeyValuePair> {
                            new KeyValuePair(keyword, new StringValue(textBytes, _latin1Encoding))
                        };
                        var directory = new PngDirectory(PngChunkType.zTXt);
                        directory.Set(PngDirectory.TagTextualData, textPairs);
                        yield return(directory);
                    }
                }
            }
            else if (chunkType == PngChunkType.iTXt)
            {
                var reader            = new SequentialByteArrayReader(bytes);
                var keyword           = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding);
                var compressionFlag   = reader.GetSByte();
                var compressionMethod = reader.GetSByte();

                // TODO we currently ignore languageTagBytes and translatedKeywordBytes
                var languageTagBytes       = reader.GetNullTerminatedBytes(bytes.Length);
                var translatedKeywordBytes = reader.GetNullTerminatedBytes(bytes.Length);

                var    bytesLeft = bytes.Length - keyword.Length - 1 - 1 - 1 - languageTagBytes.Length - 1 - translatedKeywordBytes.Length - 1;
                byte[] textBytes = null;
                if (compressionFlag == 0)
                {
                    textBytes = reader.GetNullTerminatedBytes(bytesLeft);
                }
                else if (compressionFlag == 1)
                {
                    if (compressionMethod == 0)
                    {
                        using (var inflaterStream = new DeflateStream(new MemoryStream(bytes, bytes.Length - bytesLeft, bytesLeft), CompressionMode.Decompress))
                            using (var decompStream = new MemoryStream())
                            {
#if !NET35
                                inflaterStream.CopyTo(decompStream);
#else
                                byte[] buffer = new byte[256];
                                int    count;
                                int    totalBytes = 0;
                                while ((count = inflaterStream.Read(buffer, 0, 256)) > 0)
                                {
                                    decompStream.Write(buffer, 0, count);
                                    totalBytes += count;
                                }
#endif
                                textBytes = decompStream.ToArray();
                            }
                    }
                    else
                    {
                        var directory = new PngDirectory(PngChunkType.iTXt);
                        directory.AddError("Invalid compression method value");
                        yield return(directory);
                    }
                }
                else
                {
                    var directory = new PngDirectory(PngChunkType.iTXt);
                    directory.AddError("Invalid compression flag value");
                    yield return(directory);
                }

                if (textBytes != null)
                {
                    if (keyword == "XML:com.adobe.xmp")
                    {
                        // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary
                        yield return(new XmpReader().Extract(textBytes));
                    }
                    else
                    {
                        var textPairs = new List <KeyValuePair> {
                            new KeyValuePair(keyword, new StringValue(textBytes, _latin1Encoding))
                        };
                        var directory = new PngDirectory(PngChunkType.iTXt);
                        directory.Set(PngDirectory.TagTextualData, textPairs);
                        yield return(directory);
                    }
                }
            }
            else if (chunkType == PngChunkType.tIME)
            {
                var reader    = new SequentialByteArrayReader(bytes);
                var year      = reader.GetUInt16();
                var month     = reader.GetByte();
                int day       = reader.GetByte();
                int hour      = reader.GetByte();
                int minute    = reader.GetByte();
                int second    = reader.GetByte();
                var directory = new PngDirectory(PngChunkType.tIME);
                try
                {
                    var time = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified);
                    directory.Set(PngDirectory.TagLastModificationTime, time);
                }
                catch (ArgumentOutOfRangeException e)
                {
                    directory.AddError("Error constructing DateTime: " + e.Message);
                }
                yield return(directory);
            }
            else if (chunkType == PngChunkType.pHYs)
            {
                var reader         = new SequentialByteArrayReader(bytes);
                var pixelsPerUnitX = reader.GetInt32();
                var pixelsPerUnitY = reader.GetInt32();
                var unitSpecifier  = reader.GetSByte();
                var directory      = new PngDirectory(PngChunkType.pHYs);
                directory.Set(PngDirectory.TagPixelsPerUnitX, pixelsPerUnitX);
                directory.Set(PngDirectory.TagPixelsPerUnitY, pixelsPerUnitY);
                directory.Set(PngDirectory.TagUnitSpecifier, unitSpecifier);
                yield return(directory);
            }
            else if (chunkType.Equals(PngChunkType.sBIT))
            {
                var directory = new PngDirectory(PngChunkType.sBIT);
                directory.Set(PngDirectory.TagSignificantBits, bytes);
                yield return(directory);
            }
        }
        /// <exception cref="PngProcessingException"/>
        /// <exception cref="System.IO.IOException"/>
        private static IEnumerable<Directory> ProcessChunk([NotNull] PngChunk chunk)
        {
            // For more guidance:
            // http://www.w3.org/TR/PNG-Decoders.html#D.Text-chunk-processing
            // http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.iCCP
            // by spec, PNG is generally supposed to use this encoding
            const string defaultEncodingName = "ISO-8859-1";
            var defaultEncoding = Encoding.GetEncoding(defaultEncodingName);

            var chunkType = chunk.ChunkType;
            var bytes = chunk.Bytes;

            if (chunkType == PngChunkType.IHDR)
            {
                var header = new PngHeader(bytes);
                var directory = new PngDirectory(PngChunkType.IHDR);
                directory.Set(PngDirectory.TagImageWidth, header.ImageWidth);
                directory.Set(PngDirectory.TagImageHeight, header.ImageHeight);
                directory.Set(PngDirectory.TagBitsPerSample, header.BitsPerSample);
                directory.Set(PngDirectory.TagColorType, header.ColorType.NumericValue);
                directory.Set(PngDirectory.TagCompressionType, header.CompressionType);
                directory.Set(PngDirectory.TagFilterMethod, header.FilterMethod);
                directory.Set(PngDirectory.TagInterlaceMethod, header.InterlaceMethod);
                yield return directory;
            }
            else if (chunkType == PngChunkType.PLTE)
            {
                var directory = new PngDirectory(PngChunkType.PLTE);
                directory.Set(PngDirectory.TagPaletteSize, bytes.Length / 3);
                yield return directory;
            }
            else if (chunkType == PngChunkType.tRNS)
            {
                var directory = new PngDirectory(PngChunkType.tRNS);
                directory.Set(PngDirectory.TagPaletteHasTransparency, 1);
                yield return directory;
            }
            else if (chunkType == PngChunkType.sRGB)
            {
                int srgbRenderingIntent = unchecked((sbyte)bytes[0]);
                var directory = new PngDirectory(PngChunkType.sRGB);
                directory.Set(PngDirectory.TagSrgbRenderingIntent, srgbRenderingIntent);
                yield return directory;
            }
            else if (chunkType == PngChunkType.cHRM)
            {
                var chromaticities = new PngChromaticities(bytes);
                var directory = new PngChromaticitiesDirectory();
                directory.Set(PngChromaticitiesDirectory.TagWhitePointX, chromaticities.WhitePointX);
                directory.Set(PngChromaticitiesDirectory.TagWhitePointY, chromaticities.WhitePointY);
                directory.Set(PngChromaticitiesDirectory.TagRedX, chromaticities.RedX);
                directory.Set(PngChromaticitiesDirectory.TagRedY, chromaticities.RedY);
                directory.Set(PngChromaticitiesDirectory.TagGreenX, chromaticities.GreenX);
                directory.Set(PngChromaticitiesDirectory.TagGreenY, chromaticities.GreenY);
                directory.Set(PngChromaticitiesDirectory.TagBlueX, chromaticities.BlueX);
                directory.Set(PngChromaticitiesDirectory.TagBlueY, chromaticities.BlueY);
                yield return directory;
            }
            else if (chunkType == PngChunkType.gAMA)
            {
                var gammaInt = ByteConvert.ToInt32BigEndian(bytes);
                var directory = new PngDirectory(PngChunkType.gAMA);
                directory.Set(PngDirectory.TagGamma, gammaInt / 100000.0);
                yield return directory;
            }
            else if (chunkType == PngChunkType.iCCP)
            {
                var reader = new SequentialByteArrayReader(bytes);
                var profileName = reader.GetNullTerminatedStringValue(maxLengthBytes: 79);
                var directory = new PngDirectory(PngChunkType.iCCP);
                directory.Set(PngDirectory.TagIccProfileName, profileName);
                var compressionMethod = reader.GetSByte();
                if (compressionMethod == 0)
                {
                    // Only compression method allowed by the spec is zero: deflate
                    // This assumes 1-byte-per-char, which it is by spec.
                    var bytesLeft = bytes.Length - profileName.Bytes.Length - 2;
                    var compressedProfile = reader.GetBytes(bytesLeft);
                    using (var inflaterStream = new InflaterInputStream(new MemoryStream(compressedProfile)))
                    {
                        var iccDirectory = new IccReader().Extract(new IndexedCapturingReader(inflaterStream));
                        iccDirectory.Parent = directory;
                        yield return iccDirectory;
                    }
                }
                else
                {
                    directory.AddError("Invalid compression method value");
                }
                yield return directory;
            }
            else if (chunkType == PngChunkType.bKGD)
            {
                var directory = new PngDirectory(PngChunkType.bKGD);
                directory.Set(PngDirectory.TagBackgroundColor, bytes);
                yield return directory;
            }
            else if (chunkType == PngChunkType.tEXt)
            {
                var reader = new SequentialByteArrayReader(bytes);
                var keyword = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(defaultEncoding);
                var bytesLeft = bytes.Length - keyword.Length - 1;
                var value = reader.GetNullTerminatedStringValue(bytesLeft, defaultEncoding);

                var textPairs = new List<KeyValuePair> { new KeyValuePair(keyword, value) };
                var directory = new PngDirectory(PngChunkType.iTXt);
                directory.Set(PngDirectory.TagTextualData, textPairs);
                yield return directory;
            }
            else if (chunkType == PngChunkType.iTXt)
            {
                var reader = new SequentialByteArrayReader(bytes);
                var keyword = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(defaultEncoding);
                var compressionFlag = reader.GetSByte();
                var compressionMethod = reader.GetSByte();
                var languageTag = reader.GetNullTerminatedStringValue(bytes.Length, defaultEncoding);

                var translatedKeyword = reader.GetNullTerminatedStringValue(bytes.Length, defaultEncoding);

                var bytesLeft = bytes.Length - keyword.Length - 1 - 1 - 1 - languageTag.Bytes.Length - 1 - translatedKeyword.Bytes.Length - 1;
                byte[] textBytes = null;
                if (compressionFlag == 0)
                {
                    textBytes = reader.GetNullTerminatedBytes(bytesLeft);
                }
                else if (compressionFlag == 1)
                {
                    if (compressionMethod == 0)
                    {
                        using (var inflaterStream = new DeflateStream(new MemoryStream(bytes, bytes.Length - bytesLeft, bytesLeft), CompressionMode.Decompress))
                        using (var decompStream = new MemoryStream())
                        {
#if !NET35
                            inflaterStream.CopyTo(decompStream);
#else
                            byte[] buffer = new byte[256];
                            int count;
                            int totalBytes = 0;
                            while ((count = inflaterStream.Read(buffer, 0, 256)) > 0)
                            {
                                decompStream.Write(buffer, 0, count);
                                totalBytes += count;
                            }
#endif
                            textBytes = decompStream.ToArray();
                        }
                    }
                    else
                    {
                        var directory = new PngDirectory(PngChunkType.iTXt);
                        directory.AddError("Invalid compression method value");
                        yield return directory;
                    }
                }
                else
                {
                    var directory = new PngDirectory(PngChunkType.iTXt);
                    directory.AddError("Invalid compression flag value");
                    yield return directory;
                }

                if (textBytes != null)
                {
                    if (keyword == "XML:com.adobe.xmp")
                    {
                        // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary
                        yield return new XmpReader().Extract(textBytes);
                    }
                    else
                    {
                        var textPairs = new List<KeyValuePair> { new KeyValuePair(keyword, new StringValue(textBytes, defaultEncoding)) };
                        var directory = new PngDirectory(PngChunkType.iTXt);
                        directory.Set(PngDirectory.TagTextualData, textPairs);
                        yield return directory;
                    }
                }
            }
            else if (chunkType == PngChunkType.tIME)
            {
                var reader = new SequentialByteArrayReader(bytes);
                var year = reader.GetUInt16();
                var month = reader.GetByte();
                int day = reader.GetByte();
                int hour = reader.GetByte();
                int minute = reader.GetByte();
                int second = reader.GetByte();
                var directory = new PngDirectory(PngChunkType.tIME);
                try
                {
                    var time = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified);
                    directory.Set(PngDirectory.TagLastModificationTime, time);
                }
                catch (ArgumentOutOfRangeException e)
                {
                    directory.AddError("Error constructing DateTime: " + e.Message);
                }
                yield return directory;
            }
            else if (chunkType == PngChunkType.pHYs)
            {
                var reader = new SequentialByteArrayReader(bytes);
                var pixelsPerUnitX = reader.GetInt32();
                var pixelsPerUnitY = reader.GetInt32();
                var unitSpecifier = reader.GetSByte();
                var directory = new PngDirectory(PngChunkType.pHYs);
                directory.Set(PngDirectory.TagPixelsPerUnitX, pixelsPerUnitX);
                directory.Set(PngDirectory.TagPixelsPerUnitY, pixelsPerUnitY);
                directory.Set(PngDirectory.TagUnitSpecifier, unitSpecifier);
                yield return directory;
            }
            else if (chunkType.Equals(PngChunkType.sBIT))
            {
                var directory = new PngDirectory(PngChunkType.sBIT);
                directory.Set(PngDirectory.TagSignificantBits, bytes);
                yield return directory;
            }
        }
Example #13
0
        /// <exception cref="PngProcessingException"/>
        /// <exception cref="System.IO.IOException"/>
        private static IEnumerable <Directory> ProcessChunk(PngChunk chunk)
        {
            var chunkType = chunk.ChunkType;
            var bytes     = chunk.Bytes;

            if (chunkType == PngChunkType.IHDR)
            {
                var header    = new PngHeader(bytes);
                var directory = new PngDirectory(PngChunkType.IHDR);
                directory.Set(PngDirectory.TagImageWidth, header.ImageWidth);
                directory.Set(PngDirectory.TagImageHeight, header.ImageHeight);
                directory.Set(PngDirectory.TagBitsPerSample, header.BitsPerSample);
                directory.Set(PngDirectory.TagColorType, header.ColorType.NumericValue);
                directory.Set(PngDirectory.TagCompressionType, header.CompressionType);
                directory.Set(PngDirectory.TagFilterMethod, header.FilterMethod);
                directory.Set(PngDirectory.TagInterlaceMethod, header.InterlaceMethod);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.PLTE)
            {
                var directory = new PngDirectory(PngChunkType.PLTE);
                directory.Set(PngDirectory.TagPaletteSize, bytes.Length / 3);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.tRNS)
            {
                var directory = new PngDirectory(PngChunkType.tRNS);
                directory.Set(PngDirectory.TagPaletteHasTransparency, 1);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.sRGB)
            {
                int srgbRenderingIntent = unchecked ((sbyte)bytes[0]);
                var directory           = new PngDirectory(PngChunkType.sRGB);
                directory.Set(PngDirectory.TagSrgbRenderingIntent, srgbRenderingIntent);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.cHRM)
            {
                var chromaticities = new PngChromaticities(bytes);
                var directory      = new PngChromaticitiesDirectory();
                directory.Set(PngChromaticitiesDirectory.TagWhitePointX, chromaticities.WhitePointX);
                directory.Set(PngChromaticitiesDirectory.TagWhitePointY, chromaticities.WhitePointY);
                directory.Set(PngChromaticitiesDirectory.TagRedX, chromaticities.RedX);
                directory.Set(PngChromaticitiesDirectory.TagRedY, chromaticities.RedY);
                directory.Set(PngChromaticitiesDirectory.TagGreenX, chromaticities.GreenX);
                directory.Set(PngChromaticitiesDirectory.TagGreenY, chromaticities.GreenY);
                directory.Set(PngChromaticitiesDirectory.TagBlueX, chromaticities.BlueX);
                directory.Set(PngChromaticitiesDirectory.TagBlueY, chromaticities.BlueY);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.gAMA)
            {
                var gammaInt  = ByteConvert.ToInt32BigEndian(bytes);
                var directory = new PngDirectory(PngChunkType.gAMA);
                directory.Set(PngDirectory.TagGamma, gammaInt / 100000.0);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.iCCP)
            {
                var reader      = new SequentialByteArrayReader(bytes);
                var profileName = reader.GetNullTerminatedStringValue(maxLengthBytes: 79);
                var directory   = new PngDirectory(PngChunkType.iCCP);
                directory.Set(PngDirectory.TagIccProfileName, profileName);
                var compressionMethod = reader.GetSByte();
                if (compressionMethod == 0)
                {
                    // Only compression method allowed by the spec is zero: deflate
                    // This assumes 1-byte-per-char, which it is by spec.
                    var bytesLeft = bytes.Length - profileName.Bytes.Length - 2;

                    // http://george.chiramattel.com/blog/2007/09/deflatestream-block-length-does-not-match.html
                    // First two bytes are part of the zlib specification (RFC 1950), not the deflate specification (RFC 1951).
                    reader.Skip(2);
                    bytesLeft -= 2;

                    var compressedProfile = reader.GetBytes(bytesLeft);

                    IccDirectory?iccDirectory = null;
                    Exception?   ex           = null;
                    try
                    {
                        using var inflaterStream = new DeflateStream(new MemoryStream(compressedProfile), CompressionMode.Decompress);
                        iccDirectory             = new IccReader().Extract(new IndexedCapturingReader(inflaterStream));
                        iccDirectory.Parent      = directory;
                    }
                    catch (Exception e)
                    {
                        ex = e;
                    }

                    if (iccDirectory != null)
                    {
                        yield return(iccDirectory);
                    }
                    else if (ex != null)
                    {
                        directory.AddError($"Exception decompressing {nameof(PngChunkType.iCCP)} chunk: {ex.Message}");
                    }
                }
                else
                {
                    directory.AddError("Invalid compression method value");
                }
                yield return(directory);
            }
            else if (chunkType == PngChunkType.bKGD)
            {
                var directory = new PngDirectory(PngChunkType.bKGD);
                directory.Set(PngDirectory.TagBackgroundColor, bytes);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.tEXt)
            {
                var reader    = new SequentialByteArrayReader(bytes);
                var keyword   = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding);
                var bytesLeft = bytes.Length - keyword.Length - 1;
                var value     = reader.GetNullTerminatedStringValue(bytesLeft, _latin1Encoding);

                var textPairs = new List <KeyValuePair> {
                    new KeyValuePair(keyword, value)
                };
                var directory = new PngDirectory(PngChunkType.tEXt);
                directory.Set(PngDirectory.TagTextualData, textPairs);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.zTXt)
            {
                var reader            = new SequentialByteArrayReader(bytes);
                var keyword           = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding);
                var compressionMethod = reader.GetSByte();

                var bytesLeft = bytes.Length - keyword.Length - 1 - 1 - 1 - 1;
                byte[]? textBytes = null;
                if (compressionMethod == 0)
                {
                    if (!TryDeflate(bytes, bytesLeft, out textBytes, out string?errorMessage))
                    {
                        var directory = new PngDirectory(PngChunkType.zTXt);
                        directory.AddError($"Exception decompressing {nameof(PngChunkType.zTXt)} chunk with keyword \"{keyword}\": {errorMessage}");
                        yield return(directory);
                    }
                }
                else
                {
                    var directory = new PngDirectory(PngChunkType.zTXt);
                    directory.AddError("Invalid compression method value");
                    yield return(directory);
                }

                if (textBytes != null)
                {
                    foreach (var directory in ProcessTextChunk(keyword, textBytes))
                    {
                        yield return(directory);
                    }
                }
            }
            else if (chunkType == PngChunkType.iTXt)
            {
                var reader            = new SequentialByteArrayReader(bytes);
                var keyword           = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding);
                var compressionFlag   = reader.GetSByte();
                var compressionMethod = reader.GetSByte();

                // TODO we currently ignore languageTagBytes and translatedKeywordBytes
                var languageTagBytes       = reader.GetNullTerminatedBytes(bytes.Length);
                var translatedKeywordBytes = reader.GetNullTerminatedBytes(bytes.Length);

                var bytesLeft = bytes.Length - keyword.Length - 1 - 1 - 1 - languageTagBytes.Length - 1 - translatedKeywordBytes.Length - 1;
                byte[]? textBytes = null;
                if (compressionFlag == 0)
                {
                    textBytes = reader.GetNullTerminatedBytes(bytesLeft);
                }
                else if (compressionFlag == 1)
                {
                    if (compressionMethod == 0)
                    {
                        if (!TryDeflate(bytes, bytesLeft, out textBytes, out string?errorMessage))
                        {
                            var directory = new PngDirectory(PngChunkType.iTXt);
                            directory.AddError($"Exception decompressing {nameof(PngChunkType.iTXt)} chunk with keyword \"{keyword}\": {errorMessage}");
                            yield return(directory);
                        }
                    }
                    else
                    {
                        var directory = new PngDirectory(PngChunkType.iTXt);
                        directory.AddError("Invalid compression method value");
                        yield return(directory);
                    }
                }
                else
                {
                    var directory = new PngDirectory(PngChunkType.iTXt);
                    directory.AddError("Invalid compression flag value");
                    yield return(directory);
                }

                if (textBytes != null)
                {
                    foreach (var directory in ProcessTextChunk(keyword, textBytes))
                    {
                        yield return(directory);
                    }
                }
            }
            else if (chunkType == PngChunkType.tIME)
            {
                var reader    = new SequentialByteArrayReader(bytes);
                var year      = reader.GetUInt16();
                var month     = reader.GetByte();
                int day       = reader.GetByte();
                int hour      = reader.GetByte();
                int minute    = reader.GetByte();
                int second    = reader.GetByte();
                var directory = new PngDirectory(PngChunkType.tIME);
                if (DateUtil.IsValidDate(year, month, day) && DateUtil.IsValidTime(hour, minute, second))
                {
                    var time = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified);
                    directory.Set(PngDirectory.TagLastModificationTime, time);
                }
                else
                {
                    directory.AddError($"PNG tIME data describes an invalid date/time: year={year} month={month} day={day} hour={hour} minute={minute} second={second}");
                }
                yield return(directory);
            }
            else if (chunkType == PngChunkType.pHYs)
            {
                var reader         = new SequentialByteArrayReader(bytes);
                var pixelsPerUnitX = reader.GetInt32();
                var pixelsPerUnitY = reader.GetInt32();
                var unitSpecifier  = reader.GetSByte();
                var directory      = new PngDirectory(PngChunkType.pHYs);
                directory.Set(PngDirectory.TagPixelsPerUnitX, pixelsPerUnitX);
                directory.Set(PngDirectory.TagPixelsPerUnitY, pixelsPerUnitY);
                directory.Set(PngDirectory.TagUnitSpecifier, unitSpecifier);
                yield return(directory);
            }
            else if (chunkType.Equals(PngChunkType.sBIT))
            {
                var directory = new PngDirectory(PngChunkType.sBIT);
                directory.Set(PngDirectory.TagSignificantBits, bytes);
                yield return(directory);
            }

            yield break;

            IEnumerable <Directory> ProcessTextChunk(string keyword, byte[] textBytes)
            {
                if (keyword == "XML:com.adobe.xmp")
                {
                    yield return(new XmpReader().Extract(textBytes));
                }
                else if (keyword == "Raw profile type xmp")
                {
                    if (TryProcessRawProfile(out int byteCount))
                    {
                        yield return(new XmpReader().Extract(textBytes, 0, byteCount));
                    }
                    else
                    {
                        yield return(ReadTextDirectory(keyword, textBytes, chunkType));
                    }
                }
                else if (keyword == "Raw profile type exif" || keyword == "Raw profile type APP1")
                {
                    if (TryProcessRawProfile(out _))
                    {
                        foreach (var exifDirectory in new ExifReader().Extract(new ByteArrayReader(textBytes)))
                        {
                            yield return(exifDirectory);
                        }
                    }
                    else
                    {
                        yield return(ReadTextDirectory(keyword, textBytes, chunkType));
                    }
                }
                else if (keyword == "Raw profile type icc" || keyword == "Raw profile type icm")
                {
                    if (TryProcessRawProfile(out _))
                    {
                        yield return(new IccReader().Extract(new ByteArrayReader(textBytes)));
                    }
                    else
                    {
                        yield return(ReadTextDirectory(keyword, textBytes, chunkType));
                    }
                }
                else if (keyword == "Raw profile type iptc")
                {
                    if (TryProcessRawProfile(out int byteCount))
                    {
                        yield return(new IptcReader().Extract(new SequentialByteArrayReader(textBytes), byteCount));
                    }
                    else
                    {
                        yield return(ReadTextDirectory(keyword, textBytes, chunkType));
                    }
                }
                else
                {
                    yield return(ReadTextDirectory(keyword, textBytes, chunkType));
                }

                PngDirectory ReadTextDirectory(string keyword, byte[] textBytes, PngChunkType pngChunkType)
                {
                    var textPairs = new[] { new KeyValuePair(keyword, new StringValue(textBytes, _latin1Encoding)) };
                    var directory = new PngDirectory(pngChunkType);

                    directory.Set(PngDirectory.TagTextualData, textPairs);
                    return(directory);
                }

                bool TryProcessRawProfile(out int byteCount)
                {
                    // Raw profiles have form "\n<name>\n<length>\n<hex>\n"

                    if (textBytes.Length == 0 || textBytes[0] != '\n')
                    {
                        byteCount = default;
                        return(false);
                    }

                    var i = 1;

                    // Skip name
                    while (i < textBytes.Length && textBytes[i] != '\n')
                    {
                        i++;
                    }

                    if (i == textBytes.Length)
                    {
                        byteCount = default;
                        return(false);
                    }

                    // Read length
                    int length = 0;

                    while (true)
                    {
                        i++;
                        var c = (char)textBytes[i];

                        if (c == ' ')
                        {
                            continue;
                        }
                        if (c == '\n')
                        {
                            break;
                        }

                        if (c >= '0' && c <= '9')
                        {
                            length *= 10;
                            length += c - '0';
                        }
                        else
                        {
                            byteCount = default;
                            return(false);
                        }
                    }

                    i++;

                    // We should be at the ASCII-encoded hex data. Walk through the remaining bytes, re-writing as raw bytes
                    // starting at offset zero in the array. We have to skip \n characters.

                    // Validate the data can be correctly parsed before modifying it in-place, because if parsing fails later
                    // consumers may want the unmodified data.

                    // Each row must have 72 characters (36 bytes once decoded) separated by \n
                    const int rowCharCount = 72;
                    int       charsInRow   = rowCharCount;

                    for (int j = i; j < length + i; j++)
                    {
                        byte c = textBytes[j];

                        if (charsInRow-- == 0)
                        {
                            if (c != '\n')
                            {
                                byteCount = default;
                                return(false);
                            }

                            charsInRow = rowCharCount;
                            continue;
                        }

                        if ((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F'))
                        {
                            byteCount = default;
                            return(false);
                        }
                    }

                    byteCount = length;
                    var writeIndex = 0;

                    charsInRow = rowCharCount;
                    while (length > 0)
                    {
                        var c1 = textBytes[i++];

                        if (charsInRow-- == 0)
                        {
                            Debug.Assert(c1 == '\n');
                            charsInRow = rowCharCount;
                            continue;
                        }

                        var c2 = textBytes[i++];

                        charsInRow--;

                        var n1 = ParseHexNibble(c1);
                        var n2 = ParseHexNibble(c2);

                        length--;
                        textBytes[writeIndex++] = (byte)((n1 << 4) | n2);
                    }

                    return(writeIndex == byteCount);

                    static int ParseHexNibble(int h)
                    {
                        if (h >= '0' && h <= '9')
                        {
                            return(h - '0');
                        }

                        if (h >= 'a' && h <= 'f')
                        {
                            return(10 + (h - 'a'));
                        }

                        if (h >= 'A' && h <= 'F')
                        {
                            return(10 + (h - 'A'));
                        }

                        Debug.Fail("Should not reach here");
                        throw new InvalidOperationException();
                    }
                }
Example #14
0
        private static void ReadBitmapHeader(SequentialReader reader, BmpHeaderDirectory directory, List <Directory> directories)
        {
            /*
             * BITMAPCOREHEADER (12 bytes):
             *
             *    DWORD Size              - Size of this header in bytes
             *    SHORT Width             - Image width in pixels
             *    SHORT Height            - Image height in pixels
             *    WORD  Planes            - Number of color planes
             *    WORD  BitsPerPixel      - Number of bits per pixel
             *
             * OS21XBITMAPHEADER (12 bytes):
             *
             *    DWORD  Size             - Size of this structure in bytes
             *    WORD   Width            - Bitmap width in pixels
             *    WORD   Height           - Bitmap height in pixel
             *    WORD   NumPlanes        - Number of bit planes (color depth)
             *    WORD   BitsPerPixel     - Number of bits per pixel per plane
             *
             * OS22XBITMAPHEADER (16/64 bytes):
             *
             *    DWORD  Size             - Size of this structure in bytes
             *    DWORD  Width            - Bitmap width in pixels
             *    DWORD  Height           - Bitmap height in pixel
             *    WORD   NumPlanes        - Number of bit planes (color depth)
             *    WORD   BitsPerPixel     - Number of bits per pixel per plane
             *
             *    - Short version ends here -
             *
             *    DWORD  Compression      - Bitmap compression scheme
             *    DWORD  ImageDataSize    - Size of bitmap data in bytes
             *    DWORD  XResolution      - X resolution of display device
             *    DWORD  YResolution      - Y resolution of display device
             *    DWORD  ColorsUsed       - Number of color table indices used
             *    DWORD  ColorsImportant  - Number of important color indices
             *    WORD   Units            - Type of units used to measure resolution
             *    WORD   Reserved         - Pad structure to 4-byte boundary
             *    WORD   Recording        - Recording algorithm
             *    WORD   Rendering        - Halftoning algorithm used
             *    DWORD  Size1            - Reserved for halftoning algorithm use
             *    DWORD  Size2            - Reserved for halftoning algorithm use
             *    DWORD  ColorEncoding    - Color model used in bitmap
             *    DWORD  Identifier       - Reserved for application use
             *
             * BITMAPINFOHEADER (40 bytes), BITMAPV2INFOHEADER (52 bytes), BITMAPV3INFOHEADER (56 bytes),
             * BITMAPV4HEADER (108 bytes) and BITMAPV5HEADER (124 bytes):
             *
             *    DWORD Size              - Size of this header in bytes
             *    LONG  Width             - Image width in pixels
             *    LONG  Height            - Image height in pixels
             *    WORD  Planes            - Number of color planes
             *    WORD  BitsPerPixel      - Number of bits per pixel
             *    DWORD Compression       - Compression methods used
             *    DWORD SizeOfBitmap      - Size of bitmap in bytes
             *    LONG  HorzResolution    - Horizontal resolution in pixels per meter
             *    LONG  VertResolution    - Vertical resolution in pixels per meter
             *    DWORD ColorsUsed        - Number of colors in the image
             *    DWORD ColorsImportant   - Minimum number of important colors
             *
             *    - BITMAPINFOHEADER ends here -
             *
             *    DWORD RedMask           - Mask identifying bits of red component
             *    DWORD GreenMask         - Mask identifying bits of green component
             *    DWORD BlueMask          - Mask identifying bits of blue component
             *
             *    - BITMAPV2INFOHEADER ends here -
             *
             *    DWORD AlphaMask         - Mask identifying bits of alpha component
             *
             *    - BITMAPV3INFOHEADER ends here -
             *
             *    DWORD CSType            - Color space type
             *    LONG  RedX              - X coordinate of red endpoint
             *    LONG  RedY              - Y coordinate of red endpoint
             *    LONG  RedZ              - Z coordinate of red endpoint
             *    LONG  GreenX            - X coordinate of green endpoint
             *    LONG  GreenY            - Y coordinate of green endpoint
             *    LONG  GreenZ            - Z coordinate of green endpoint
             *    LONG  BlueX             - X coordinate of blue endpoint
             *    LONG  BlueY             - Y coordinate of blue endpoint
             *    LONG  BlueZ             - Z coordinate of blue endpoint
             *    DWORD GammaRed          - Gamma red coordinate scale value
             *    DWORD GammaGreen        - Gamma green coordinate scale value
             *    DWORD GammaBlue         - Gamma blue coordinate scale value
             *
             *    - BITMAPV4HEADER ends here -
             *
             *    DWORD Intent            - Rendering intent for bitmap
             *    DWORD ProfileData       - Offset of the profile data relative to BITMAPV5HEADER
             *    DWORD ProfileSize       - Size, in bytes, of embedded profile data
             *    DWORD Reserved          - Shall be zero
             *
             */

            try
            {
                int  bitmapType   = directory.GetInt32(BmpHeaderDirectory.TagBitmapType);
                long headerOffset = reader.Position;
                int  headerSize   = reader.GetInt32();

                directory.Set(BmpHeaderDirectory.TagHeaderSize, headerSize);

                /*
                 * Known header type sizes:
                 *
                 *  12 - BITMAPCOREHEADER or OS21XBITMAPHEADER
                 *  16 - OS22XBITMAPHEADER (short)
                 *  40 - BITMAPINFOHEADER
                 *  52 - BITMAPV2INFOHEADER
                 *  56 - BITMAPV3INFOHEADER
                 *  64 - OS22XBITMAPHEADER (full)
                 * 108 - BITMAPV4HEADER
                 * 124 - BITMAPV5HEADER
                 *
                 */

                if (headerSize == 12 && bitmapType == (int)BmpHeaderDirectory.BitmapType.Bitmap)
                {
                    //BITMAPCOREHEADER

                    /*
                     * There's no way to tell BITMAPCOREHEADER and OS21XBITMAPHEADER
                     * apart for the "standard" bitmap type. The difference is only
                     * that BITMAPCOREHEADER has signed width and height while
                     * in OS21XBITMAPHEADER they are unsigned. Since BITMAPCOREHEADER,
                     * the Windows version, is most common, read them as signed.
                     */
                    directory.Set(BmpHeaderDirectory.TagImageWidth, reader.GetInt16());
                    directory.Set(BmpHeaderDirectory.TagImageHeight, reader.GetInt16());
                    directory.Set(BmpHeaderDirectory.TagColourPlanes, reader.GetUInt16());
                    directory.Set(BmpHeaderDirectory.TagBitsPerPixel, reader.GetUInt16());
                }
                else if (headerSize == 12)
                {
                    // OS21XBITMAPHEADER
                    directory.Set(BmpHeaderDirectory.TagImageWidth, reader.GetUInt16());
                    directory.Set(BmpHeaderDirectory.TagImageHeight, reader.GetUInt16());
                    directory.Set(BmpHeaderDirectory.TagColourPlanes, reader.GetUInt16());
                    directory.Set(BmpHeaderDirectory.TagBitsPerPixel, reader.GetUInt16());
                }
                else if (headerSize == 16 || headerSize == 64)
                {
                    // OS22XBITMAPHEADER
                    directory.Set(BmpHeaderDirectory.TagImageWidth, reader.GetInt32());
                    directory.Set(BmpHeaderDirectory.TagImageHeight, reader.GetInt32());
                    directory.Set(BmpHeaderDirectory.TagColourPlanes, reader.GetUInt16());
                    directory.Set(BmpHeaderDirectory.TagBitsPerPixel, reader.GetUInt16());
                    if (headerSize > 16)
                    {
                        directory.Set(BmpHeaderDirectory.TagCompression, reader.GetInt32());
                        reader.Skip(4); // skip the pixel data length
                        directory.Set(BmpHeaderDirectory.TagXPixelsPerMeter, reader.GetInt32());
                        directory.Set(BmpHeaderDirectory.TagYPixelsPerMeter, reader.GetInt32());
                        directory.Set(BmpHeaderDirectory.TagPaletteColourCount, reader.GetInt32());
                        directory.Set(BmpHeaderDirectory.TagImportantColourCount, reader.GetInt32());
                        reader.Skip(
                            2 + // Skip Units, can only be 0 (pixels per meter)
                            2 + // Skip padding
                            2   // Skip Recording, can only be 0 (left to right, bottom to top)
                            );
                        directory.Set(BmpHeaderDirectory.TagRendering, reader.GetUInt16());
                        reader.Skip(4 + 4); // Skip Size1 and Size2
                        directory.Set(BmpHeaderDirectory.TagColorEncoding, reader.GetInt32());
                        reader.Skip(4);     // Skip Identifier
                    }
                }
                else if (
                    headerSize == 40 || headerSize == 52 || headerSize == 56 ||
                    headerSize == 108 || headerSize == 124)
                {
                    // BITMAPINFOHEADER V1-5
                    directory.Set(BmpHeaderDirectory.TagImageWidth, reader.GetInt32());
                    directory.Set(BmpHeaderDirectory.TagImageHeight, reader.GetInt32());
                    directory.Set(BmpHeaderDirectory.TagColourPlanes, reader.GetUInt16());
                    directory.Set(BmpHeaderDirectory.TagBitsPerPixel, reader.GetUInt16());
                    directory.Set(BmpHeaderDirectory.TagCompression, reader.GetInt32());
                    // skip the pixel data length
                    reader.Skip(4);
                    directory.Set(BmpHeaderDirectory.TagXPixelsPerMeter, reader.GetInt32());
                    directory.Set(BmpHeaderDirectory.TagYPixelsPerMeter, reader.GetInt32());
                    directory.Set(BmpHeaderDirectory.TagPaletteColourCount, reader.GetInt32());
                    directory.Set(BmpHeaderDirectory.TagImportantColourCount, reader.GetInt32());
                    if (headerSize == 40)
                    {
                        // BITMAPINFOHEADER end
                        return;
                    }
                    directory.Set(BmpHeaderDirectory.TagRedMask, reader.GetUInt32());
                    directory.Set(BmpHeaderDirectory.TagGreenMask, reader.GetUInt32());
                    directory.Set(BmpHeaderDirectory.TagBlueMask, reader.GetUInt32());
                    if (headerSize == 52)
                    {
                        // BITMAPV2INFOHEADER end
                        return;
                    }
                    directory.Set(BmpHeaderDirectory.TagAlphaMask, reader.GetUInt32());
                    if (headerSize == 56)
                    {
                        // BITMAPV3INFOHEADER end
                        return;
                    }
                    long csType = reader.GetUInt32();
                    directory.Set(BmpHeaderDirectory.TagColorSpaceType, csType);
                    reader.Skip(36); // Skip color endpoint coordinates
                    directory.Set(BmpHeaderDirectory.TagGammaRed, reader.GetUInt32());
                    directory.Set(BmpHeaderDirectory.TagGammaGreen, reader.GetUInt32());
                    directory.Set(BmpHeaderDirectory.TagGammaBlue, reader.GetUInt32());
                    if (headerSize == 108)
                    {
                        // BITMAPV4HEADER end
                        return;
                    }
                    directory.Set(BmpHeaderDirectory.TagIntent, reader.GetInt32());
                    if (csType == (long)BmpHeaderDirectory.ColorSpaceType.ProfileEmbedded || csType == (long)BmpHeaderDirectory.ColorSpaceType.ProfileLinked)
                    {
                        long profileOffset = reader.GetUInt32();
                        int  profileSize   = reader.GetInt32();
                        if (reader.Position > headerOffset + profileOffset)
                        {
                            directory.AddError("Invalid profile data offset 0x" + (headerOffset + profileOffset).ToString("X8"));
                            return;
                        }
                        reader.Skip(headerOffset + profileOffset - reader.Position);
                        if (csType == (long)BmpHeaderDirectory.ColorSpaceType.ProfileLinked)
                        {
                            directory.Set(BmpHeaderDirectory.TagLinkedProfile, reader.GetNullTerminatedString(profileSize, Encoding.GetEncoding(1252)));
                        }
                        else
                        {
                            var iccReader    = new ByteArrayReader(reader.GetBytes(profileSize));
                            var iccDirectory = new IccReader().Extract(iccReader);
                            iccDirectory.Parent = directory;
                            directories.Add(iccDirectory);
                        }
                    }
                    else
                    {
                        reader.Skip(
                            4 + // Skip ProfileData offset
                            4 + // Skip ProfileSize
                            4   // Skip Reserved
                            );
                    }
                }
                else
                {
                    directory.AddError("Unexpected DIB header size: " + headerSize);
                }
            }
            catch (IOException)
            {
                directory.AddError("Unable to read BMP header");
            }
            catch (MetadataException)
            {
                directory.AddError("Internal error");
            }
        }
        public DirectoryList Extract(SequentialReader reader, int length)
        {
            var directory = new PhotoshopDirectory();

            var directories = new List <Directory> {
                directory
            };

            // Data contains a sequence of Image Resource Blocks (IRBs):
            //
            // 4 bytes - Signature; mostly "8BIM" but "PHUT", "AgHg" and "DCSR" are also found
            // 2 bytes - Resource identifier
            // String  - Pascal string, padded to make length even
            // 4 bytes - Size of resource data which follows
            // Data    - The resource data, padded to make size even
            //
            // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504
            var pos = 0;
            int clippingPathCount = 0;

            while (pos < length)
            {
                try
                {
                    // 4 bytes for the signature ("8BIM", "PHUT", etc.)
                    var signature = reader.GetString(4, Encoding.UTF8);
                    pos += 4;

                    // 2 bytes for the resource identifier (tag type).
                    var tagType = reader.GetUInt16();
                    pos += 2;

                    // A variable number of bytes holding a pascal string (two leading bytes for length).
                    var descriptionLength = reader.GetByte();
                    pos += 1;

                    // Some basic bounds checking
                    if (descriptionLength + pos > length)
                    {
                        throw new ImageProcessingException("Invalid string length");
                    }

                    // Get name (important for paths)
                    var description = new StringBuilder();
                    // Loop through each byte and append to string
                    while (descriptionLength > 0)
                    {
                        description.Append((char)reader.GetByte());
                        pos++;
                        descriptionLength--;
                    }

                    // The number of bytes is padded with a trailing zero, if needed, to make the size even.
                    if (pos % 2 != 0)
                    {
                        reader.Skip(1);
                        pos++;
                    }

                    // 4 bytes for the size of the resource data that follows.
                    var byteCount = reader.GetInt32();
                    pos += 4;

                    // The resource data.
                    var tagBytes = reader.GetBytes(byteCount);
                    pos += byteCount;

                    // The number of bytes is padded with a trailing zero, if needed, to make the size even.
                    if (pos % 2 != 0)
                    {
                        reader.Skip(1);
                        pos++;
                    }

                    // Skip any unsupported IRBs
                    if (signature != "8BIM")
                    {
                        continue;
                    }

                    switch (tagType)
                    {
                    case PhotoshopDirectory.TagIptc:
                        var iptcDirectory = new IptcReader().Extract(new SequentialByteArrayReader(tagBytes), tagBytes.Length);
                        iptcDirectory.Parent = directory;
                        directories.Add(iptcDirectory);
                        break;

                    case PhotoshopDirectory.TagIccProfileBytes:
                        var iccDirectory = new IccReader().Extract(new ByteArrayReader(tagBytes));
                        iccDirectory.Parent = directory;
                        directories.Add(iccDirectory);
                        break;

                    case PhotoshopDirectory.TagExifData1:
                    case PhotoshopDirectory.TagExifData3:
                        var exifDirectories = new ExifReader().Extract(new ByteArrayReader(tagBytes));
                        foreach (var exifDirectory in exifDirectories.Where(d => d.Parent == null))
                        {
                            exifDirectory.Parent = directory;
                        }
                        directories.AddRange(exifDirectories);
                        break;

                    case PhotoshopDirectory.TagXmpData:
                        var xmpDirectory = new XmpReader().Extract(tagBytes);
                        xmpDirectory.Parent = directory;
                        directories.Add(xmpDirectory);
                        break;

                    default:
                        if (tagType >= PhotoshopDirectory.TagClippingPathBlockStart && tagType <= PhotoshopDirectory.TagClippingPathBlockEnd)
                        {
                            clippingPathCount++;
                            Array.Resize(ref tagBytes, tagBytes.Length + description.Length + 1);
                            // Append description(name) to end of byte array with 1 byte before the description representing the length
                            for (int i = tagBytes.Length - description.Length - 1; i < tagBytes.Length; i++)
                            {
                                if (i % (tagBytes.Length - description.Length - 1 + description.Length) == 0)
                                {
                                    tagBytes[i] = (byte)description.Length;
                                }
                                else
                                {
                                    tagBytes[i] = (byte)description[i - (tagBytes.Length - description.Length - 1)];
                                }
                            }
                            PhotoshopDirectory.TagNameMap[PhotoshopDirectory.TagClippingPathBlockStart + clippingPathCount - 1] = "Path Info " + clippingPathCount;
                            directory.Set(PhotoshopDirectory.TagClippingPathBlockStart + clippingPathCount - 1, tagBytes);
                        }
                        else
                        {
                            directory.Set(tagType, tagBytes);
                        }
                        break;
                    }

                    if (tagType >= 0x0fa0 && tagType <= 0x1387)
                    {
                        PhotoshopDirectory.TagNameMap[tagType] = $"Plug-in {tagType - 0x0fa0 + 1} Data";
                    }
                }
                catch (Exception ex)
                {
                    directory.AddError(ex.Message);
                    break;
                }
            }

            return(directories);
        }
        /// <exception cref="PngProcessingException"/>
        /// <exception cref="System.IO.IOException"/>
        private static IEnumerable <Directory> ProcessChunk([NotNull] PngChunk chunk)
        {
            var chunkType = chunk.ChunkType;
            var bytes     = chunk.Bytes;

            if (chunkType == PngChunkType.IHDR)
            {
                var header    = new PngHeader(bytes);
                var directory = new PngDirectory(PngChunkType.IHDR);
                directory.Set(PngDirectory.TagImageWidth, header.ImageWidth);
                directory.Set(PngDirectory.TagImageHeight, header.ImageHeight);
                directory.Set(PngDirectory.TagBitsPerSample, header.BitsPerSample);
                directory.Set(PngDirectory.TagColorType, header.ColorType.NumericValue);
                directory.Set(PngDirectory.TagCompressionType, header.CompressionType);
                directory.Set(PngDirectory.TagFilterMethod, header.FilterMethod);
                directory.Set(PngDirectory.TagInterlaceMethod, header.InterlaceMethod);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.PLTE)
            {
                var directory = new PngDirectory(PngChunkType.PLTE);
                directory.Set(PngDirectory.TagPaletteSize, bytes.Length / 3);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.tRNS)
            {
                var directory = new PngDirectory(PngChunkType.tRNS);
                directory.Set(PngDirectory.TagPaletteHasTransparency, 1);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.sRGB)
            {
                int srgbRenderingIntent = unchecked ((sbyte)bytes[0]);
                var directory           = new PngDirectory(PngChunkType.sRGB);
                directory.Set(PngDirectory.TagSrgbRenderingIntent, srgbRenderingIntent);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.cHRM)
            {
                var chromaticities = new PngChromaticities(bytes);
                var directory      = new PngChromaticitiesDirectory();
                directory.Set(PngChromaticitiesDirectory.TagWhitePointX, chromaticities.WhitePointX);
                directory.Set(PngChromaticitiesDirectory.TagWhitePointY, chromaticities.WhitePointY);
                directory.Set(PngChromaticitiesDirectory.TagRedX, chromaticities.RedX);
                directory.Set(PngChromaticitiesDirectory.TagRedY, chromaticities.RedY);
                directory.Set(PngChromaticitiesDirectory.TagGreenX, chromaticities.GreenX);
                directory.Set(PngChromaticitiesDirectory.TagGreenY, chromaticities.GreenY);
                directory.Set(PngChromaticitiesDirectory.TagBlueX, chromaticities.BlueX);
                directory.Set(PngChromaticitiesDirectory.TagBlueY, chromaticities.BlueY);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.gAMA)
            {
                var gammaInt  = ByteConvert.ToInt32BigEndian(bytes);
                var directory = new PngDirectory(PngChunkType.gAMA);
                directory.Set(PngDirectory.TagGamma, gammaInt / 100000.0);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.iCCP)
            {
                var reader      = new SequentialByteArrayReader(bytes);
                var profileName = reader.GetNullTerminatedStringValue(maxLengthBytes: 79);
                var directory   = new PngDirectory(PngChunkType.iCCP);
                directory.Set(PngDirectory.TagIccProfileName, profileName);
                var compressionMethod = reader.GetSByte();
                if (compressionMethod == 0)
                {
                    // Only compression method allowed by the spec is zero: deflate
                    // This assumes 1-byte-per-char, which it is by spec.
                    var bytesLeft = bytes.Length - profileName.Bytes.Length - 2;

                    // http://george.chiramattel.com/blog/2007/09/deflatestream-block-length-does-not-match.html
                    // First two bytes are part of the zlib specification (RFC 1950), not the deflate specification (RFC 1951).
                    reader.GetByte(); reader.GetByte();
                    bytesLeft -= 2;

                    var compressedProfile = reader.GetBytes(bytesLeft);

                    IccDirectory iccDirectory = null;
                    Exception    ex           = null;
                    try
                    {
                        using (var inflaterStream = new DeflateStream(new MemoryStream(compressedProfile), CompressionMode.Decompress))
                        {
                            iccDirectory        = new IccReader().Extract(new IndexedCapturingReader(inflaterStream));
                            iccDirectory.Parent = directory;
                        }
                    }
                    catch (Exception e)
                    {
                        ex = e;
                    }

                    if (ex == null)
                    {
                        yield return(iccDirectory);
                    }
                    else
                    {
                        directory.AddError($"Exception decompressing {nameof(PngChunkType.iCCP)} chunk: {ex.Message}");
                    }
                }
                else
                {
                    directory.AddError("Invalid compression method value");
                }
                yield return(directory);
            }
            else if (chunkType == PngChunkType.bKGD)
            {
                var directory = new PngDirectory(PngChunkType.bKGD);
                directory.Set(PngDirectory.TagBackgroundColor, bytes);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.tEXt)
            {
                var reader    = new SequentialByteArrayReader(bytes);
                var keyword   = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding);
                var bytesLeft = bytes.Length - keyword.Length - 1;
                var value     = reader.GetNullTerminatedStringValue(bytesLeft, _latin1Encoding);

                var textPairs = new List <KeyValuePair> {
                    new KeyValuePair(keyword, value)
                };
                var directory = new PngDirectory(PngChunkType.tEXt);
                directory.Set(PngDirectory.TagTextualData, textPairs);
                yield return(directory);
            }
            else if (chunkType == PngChunkType.zTXt)
            {
                var reader            = new SequentialByteArrayReader(bytes);
                var keyword           = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding);
                var compressionMethod = reader.GetSByte();

                var    bytesLeft = bytes.Length - keyword.Length - 1 - 1 - 1 - 1;
                byte[] textBytes = null;
                if (compressionMethod == 0)
                {
                    using (var inflaterStream = new DeflateStream(new MemoryStream(bytes, bytes.Length - bytesLeft, bytesLeft), CompressionMode.Decompress))
                    {
                        Exception ex = null;
                        try
                        {
                            textBytes = ReadStreamToBytes(inflaterStream);
                        }
                        catch (Exception e)
                        {
                            ex = e;
                        }

                        // Work-around no yield-return from catch blocks
                        if (ex != null)
                        {
                            var directory = new PngDirectory(PngChunkType.zTXt);
                            directory.AddError($"Exception decompressing {nameof(PngChunkType.zTXt)} chunk with keyword \"{keyword}\": {ex.Message}");
                            yield return(directory);
                        }
                    }
                }
                else
                {
                    var directory = new PngDirectory(PngChunkType.zTXt);
                    directory.AddError("Invalid compression method value");
                    yield return(directory);
                }
                if (textBytes != null)
                {
                    if (keyword == "XML:com.adobe.xmp")
                    {
                        // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary
                        yield return(new XmpReader().Extract(textBytes));
                    }
                    else
                    {
                        var textPairs = new List <KeyValuePair> {
                            new KeyValuePair(keyword, new StringValue(textBytes, _latin1Encoding))
                        };
                        var directory = new PngDirectory(PngChunkType.zTXt);
                        directory.Set(PngDirectory.TagTextualData, textPairs);
                        yield return(directory);
                    }
                }
            }
            else if (chunkType == PngChunkType.iTXt)
            {
                var reader            = new SequentialByteArrayReader(bytes);
                var keyword           = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding);
                var compressionFlag   = reader.GetSByte();
                var compressionMethod = reader.GetSByte();

                // TODO we currently ignore languageTagBytes and translatedKeywordBytes
                var languageTagBytes       = reader.GetNullTerminatedBytes(bytes.Length);
                var translatedKeywordBytes = reader.GetNullTerminatedBytes(bytes.Length);

                var    bytesLeft = bytes.Length - keyword.Length - 1 - 1 - 1 - languageTagBytes.Length - 1 - translatedKeywordBytes.Length - 1;
                byte[] textBytes = null;
                if (compressionFlag == 0)
                {
                    textBytes = reader.GetNullTerminatedBytes(bytesLeft);
                }
                else if (compressionFlag == 1)
                {
                    if (compressionMethod == 0)
                    {
                        using (var inflaterStream = new DeflateStream(new MemoryStream(bytes, bytes.Length - bytesLeft, bytesLeft), CompressionMode.Decompress))
                        {
                            Exception ex = null;
                            try
                            {
                                textBytes = ReadStreamToBytes(inflaterStream);
                            }
                            catch (Exception e)
                            {
                                ex = e;
                            }

                            // Work-around no yield-return from catch blocks
                            if (ex != null)
                            {
                                var directory = new PngDirectory(PngChunkType.iTXt);
                                directory.AddError($"Exception decompressing {nameof(PngChunkType.iTXt)} chunk with keyword \"{keyword}\": {ex.Message}");
                                yield return(directory);
                            }
                        }
                    }
                    else
                    {
                        var directory = new PngDirectory(PngChunkType.iTXt);
                        directory.AddError("Invalid compression method value");
                        yield return(directory);
                    }
                }
                else
                {
                    var directory = new PngDirectory(PngChunkType.iTXt);
                    directory.AddError("Invalid compression flag value");
                    yield return(directory);
                }

                if (textBytes != null)
                {
                    if (keyword == "XML:com.adobe.xmp")
                    {
                        // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary
                        yield return(new XmpReader().Extract(textBytes));
                    }
                    else
                    {
                        var textPairs = new List <KeyValuePair> {
                            new KeyValuePair(keyword, new StringValue(textBytes, _latin1Encoding))
                        };
                        var directory = new PngDirectory(PngChunkType.iTXt);
                        directory.Set(PngDirectory.TagTextualData, textPairs);
                        yield return(directory);
                    }
                }
            }
            else if (chunkType == PngChunkType.tIME)
            {
                var reader    = new SequentialByteArrayReader(bytes);
                var year      = reader.GetUInt16();
                var month     = reader.GetByte();
                int day       = reader.GetByte();
                int hour      = reader.GetByte();
                int minute    = reader.GetByte();
                int second    = reader.GetByte();
                var directory = new PngDirectory(PngChunkType.tIME);
                if (DateUtil.IsValidDate(year, month, day) && DateUtil.IsValidTime(hour, minute, second))
                {
                    var time = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified);
                    directory.Set(PngDirectory.TagLastModificationTime, time);
                }
                else
                {
                    directory.AddError($"PNG tIME data describes an invalid date/time: year={year} month={month} day={day} hour={hour} minute={minute} second={second}");
                }
                yield return(directory);
            }
            else if (chunkType == PngChunkType.pHYs)
            {
                var reader         = new SequentialByteArrayReader(bytes);
                var pixelsPerUnitX = reader.GetInt32();
                var pixelsPerUnitY = reader.GetInt32();
                var unitSpecifier  = reader.GetSByte();
                var directory      = new PngDirectory(PngChunkType.pHYs);
                directory.Set(PngDirectory.TagPixelsPerUnitX, pixelsPerUnitX);
                directory.Set(PngDirectory.TagPixelsPerUnitY, pixelsPerUnitY);
                directory.Set(PngDirectory.TagUnitSpecifier, unitSpecifier);
                yield return(directory);
            }
            else if (chunkType.Equals(PngChunkType.sBIT))
            {
                var directory = new PngDirectory(PngChunkType.sBIT);
                directory.Set(PngDirectory.TagSignificantBits, bytes);
                yield return(directory);
            }
        }
            Extract([NotNull] SequentialReader reader, int length)
        {
            var directory = new PhotoshopDirectory();

            var directories = new List<Directory> { directory };

            // Data contains a sequence of Image Resource Blocks (IRBs):
            //
            // 4 bytes - Signature; mostly "8BIM" but "PHUT", "AgHg" and "DCSR" are also found
            // 2 bytes - Resource identifier
            // String  - Pascal string, padded to make length even
            // 4 bytes - Size of resource data which follows
            // Data    - The resource data, padded to make size even
            //
            // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504
            var pos = 0;
            while (pos < length)
            {
                try
                {
                    // 4 bytes for the signature ("8BIM", "PHUT", etc.)
                    var signature = reader.GetString(4);
                    pos += 4;

                    // 2 bytes for the resource identifier (tag type).
                    var tagType = reader.GetUInt16();
                    pos += 2;

                    // A variable number of bytes holding a pascal string (two leading bytes for length).
                    var descriptionLength = reader.GetByte();
                    pos += 1;

                    // Some basic bounds checking
                    if (descriptionLength + pos > length)
                        throw new ImageProcessingException("Invalid string length");

                    // We don't use the string value here
                    reader.Skip(descriptionLength);
                    pos += descriptionLength;

                    // The number of bytes is padded with a trailing zero, if needed, to make the size even.
                    if (pos % 2 != 0)
                    {
                        reader.Skip(1);
                        pos++;
                    }

                    // 4 bytes for the size of the resource data that follows.
                    var byteCount = reader.GetInt32();
                    pos += 4;

                    // The resource data.
                    var tagBytes = reader.GetBytes(byteCount);
                    pos += byteCount;

                    // The number of bytes is padded with a trailing zero, if needed, to make the size even.
                    if (pos % 2 != 0)
                    {
                        reader.Skip(1);
                        pos++;
                    }

                    // Skip any unsupported IRBs
                    if (signature != "8BIM")
                        continue;

                    switch (tagType)
                    {
                        case PhotoshopDirectory.TagIptc:
                            var iptcDirectory = new IptcReader().Extract(new SequentialByteArrayReader(tagBytes), tagBytes.Length);
                            iptcDirectory.Parent = directory;
                            directories.Add(iptcDirectory);
                            break;
                        case PhotoshopDirectory.TagIccProfileBytes:
                            var iccDirectory = new IccReader().Extract(new ByteArrayReader(tagBytes));
                            iccDirectory.Parent = directory;
                            directories.Add(iccDirectory);
                            break;
                        case PhotoshopDirectory.TagExifData1:
                        case PhotoshopDirectory.TagExifData3:
                            var exifDirectories = new ExifReader().Extract(new ByteArrayReader(tagBytes));
                            foreach (var exifDirectory in exifDirectories.Where(d => d.Parent == null))
                                exifDirectory.Parent = directory;
                            directories.AddRange(exifDirectories);
                            break;
                        case PhotoshopDirectory.TagXmpData:
                            var xmpDirectory = new XmpReader().Extract(tagBytes);
                            xmpDirectory.Parent = directory;
                            directories.Add(xmpDirectory);
                            break;
                        default:
                            directory.Set(tagType, tagBytes);
                            break;
                    }

                    if (tagType >= 0x0fa0 && tagType <= 0x1387)
                        PhotoshopDirectory.TagNameMap[tagType] = $"Plug-in {tagType - 0x0fa0 + 1} Data";
                }
                catch (Exception ex)
                {
                    directory.AddError(ex.Message);
                    break;
                }
            }

            return directories;
        }