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