|
1 | 1 | using System; |
2 | 2 | using System.Collections.Generic; |
3 | 3 | using System.IO; |
| 4 | +using System.IO.Compression; |
4 | 5 | using System.Linq; |
5 | 6 | using System.Text; |
6 | 7 | using System.Threading.Tasks; |
@@ -34,6 +35,11 @@ public class PngMetadataExtractor |
34 | 35 | SkipBytes(fs, 4); // Move past the current chunk CRC |
35 | 36 | yield return keywordValuePair; |
36 | 37 | break; |
| 38 | + case "iTXt": |
| 39 | + var itxtKeywordValuePair = await ReadInternationalTextualData(fs, chunkLength); |
| 40 | + SkipBytes(fs, 4); // Move past the current chunk CRC |
| 41 | + yield return itxtKeywordValuePair; |
| 42 | + break; |
37 | 43 | default: |
38 | 44 | SkipBytes(fs, chunkLength + 4); // Move past the current chunk and its CRC |
39 | 45 | break; |
@@ -80,10 +86,71 @@ private async static Task<string> ReadChunkType(Stream stream) |
80 | 86 | var nullIndex = dataString.IndexOf(NullTerminator); |
81 | 87 | return new ( |
82 | 88 | nullIndex > -1 ? dataString.Substring(0, nullIndex) : string.Empty, |
83 | | - nullIndex > -1 && nullIndex + 1 < length ? dataString.Substring(nullIndex + 1).TrimEnd(NullTerminator) : string.Empty |
| 89 | + nullIndex > -1 && nullIndex + 1 < length ? dataString.Substring(nullIndex + 1).Trim(NullTerminator) : string.Empty |
84 | 90 | ); |
85 | 91 | } |
86 | 92 |
|
| 93 | + private async static Task<(string Key, string Value)> ReadInternationalTextualData(Stream stream, int length) |
| 94 | + { |
| 95 | + var buffer = new byte[length]; |
| 96 | + if (await stream.ReadAsync(buffer) != length) |
| 97 | + { |
| 98 | + throw new EndOfStreamException("Unexpected end of file while reading iTXt data."); |
| 99 | + } |
| 100 | + |
| 101 | + // iTXt layout (all indices into buffer): |
| 102 | + // [keyword]\0 [compression_flag:1] [compression_method:1] |
| 103 | + // [language_tag]\0 [translated_keyword]\0 [text...] |
| 104 | + var keywordEnd = Array.IndexOf(buffer, (byte)0); |
| 105 | + if (keywordEnd < 0 || keywordEnd + 4 > buffer.Length) |
| 106 | + { |
| 107 | + return (string.Empty, string.Empty); |
| 108 | + } |
| 109 | + |
| 110 | + var keyword = Encoding.UTF8.GetString(buffer, 0, keywordEnd); |
| 111 | + var compressionFlag = buffer[keywordEnd + 1]; |
| 112 | + var compressionMethod = buffer[keywordEnd + 2]; |
| 113 | + // compression_method is at keywordEnd + 2; only method 0 (zlib) is defined |
| 114 | + var afterFlags = keywordEnd + 3; |
| 115 | + |
| 116 | + // Skip language tag (null-terminated) |
| 117 | + var languageEnd = Array.IndexOf(buffer, (byte)0, afterFlags); |
| 118 | + if (languageEnd < 0 || languageEnd + 1 > buffer.Length) |
| 119 | + { |
| 120 | + return (keyword, string.Empty); |
| 121 | + } |
| 122 | + |
| 123 | + // Skip translated keyword (null-terminated) |
| 124 | + var translatedKeywordEnd = Array.IndexOf(buffer, (byte)0, languageEnd + 1); |
| 125 | + if (translatedKeywordEnd < 0 || translatedKeywordEnd + 1 > buffer.Length) |
| 126 | + { |
| 127 | + return (keyword, string.Empty); |
| 128 | + } |
| 129 | + |
| 130 | + var textStart = translatedKeywordEnd + 1; |
| 131 | + var textBytes = buffer.AsSpan(textStart).ToArray(); |
| 132 | + |
| 133 | + string text; |
| 134 | + if (compressionFlag == 1) |
| 135 | + { |
| 136 | + if (compressionMethod != 0) |
| 137 | + { |
| 138 | + throw new InvalidDataException($"Unsupported iTXt compression method: {compressionMethod}. Only zlib deflate (method 0) is supported."); |
| 139 | + } |
| 140 | + using var compressed = new MemoryStream(textBytes); |
| 141 | + using var zlib = new ZLibStream(compressed, CompressionMode.Decompress); |
| 142 | + using var decompressed = new MemoryStream(); |
| 143 | + await zlib.CopyToAsync(decompressed); |
| 144 | + text = Encoding.UTF8.GetString(decompressed.ToArray()); |
| 145 | + } |
| 146 | + else |
| 147 | + { |
| 148 | + text = Encoding.UTF8.GetString(textBytes); |
| 149 | + } |
| 150 | + |
| 151 | + return (keyword, text); |
| 152 | + } |
| 153 | + |
87 | 154 | private static void SkipBytes(Stream stream, int bytesToSkip) |
88 | 155 | { |
89 | 156 | var originalPosition = stream.Position; |
|
0 commit comments