Skip to content

Commit d6b3c67

Browse files
authored
add itext metadata support (#491)
1 parent 81482d5 commit d6b3c67

File tree

5 files changed

+108
-1
lines changed

5 files changed

+108
-1
lines changed

SDMeta/Metadata/PngMetadataExtractor.cs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.IO.Compression;
45
using System.Linq;
56
using System.Text;
67
using System.Threading.Tasks;
@@ -34,6 +35,11 @@ public class PngMetadataExtractor
3435
SkipBytes(fs, 4); // Move past the current chunk CRC
3536
yield return keywordValuePair;
3637
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;
3743
default:
3844
SkipBytes(fs, chunkLength + 4); // Move past the current chunk and its CRC
3945
break;
@@ -80,10 +86,71 @@ private async static Task<string> ReadChunkType(Stream stream)
8086
var nullIndex = dataString.IndexOf(NullTerminator);
8187
return new (
8288
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
8490
);
8591
}
8692

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+
87154
private static void SkipBytes(Stream stream, int bytesToSkip)
88155
{
89156
var originalPosition = stream.Position;

SDMetaTest/Metadata/PngMetadataExtractorTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,39 @@ public async Task PngMetadataExtractorTest()
2525
Assert.Contains("Sánchez", prompt);
2626
Assert.EndsWith("v1.8.0", prompt);
2727
}
28+
29+
[TestMethod]
30+
public async Task PngMetadataExtractorTest_iTXt_Uncompressed()
31+
{
32+
using var fs = new FileSystem().FileStream.New("./Metadata/itxt-uncompressed.png", FileMode.Open);
33+
34+
var items = PngMetadataExtractor.ExtractTextualInformation(fs);
35+
var metadata = await items.ToDictionaryAsync(p => p.Key, p => p.Value);
36+
37+
Assert.IsNotNull(metadata);
38+
Assert.HasCount(1, metadata);
39+
Assert.IsTrue(metadata.ContainsKey("parameters"));
40+
41+
var prompt = metadata["parameters"];
42+
Assert.IsNotNull(prompt);
43+
Assert.AreEqual("steps: 20, sampler: Euler a, cfg scale: 7, seed: 12345, size: 512x512, model: v1-5-pruned", prompt);
44+
}
45+
46+
[TestMethod]
47+
public async Task PngMetadataExtractorTest_iTXt_Compressed()
48+
{
49+
using var fs = new FileSystem().FileStream.New("./Metadata/itxt-compressed.png", FileMode.Open);
50+
51+
var items = PngMetadataExtractor.ExtractTextualInformation(fs);
52+
var metadata = await items.ToDictionaryAsync(p => p.Key, p => p.Value);
53+
54+
Assert.IsNotNull(metadata);
55+
Assert.HasCount(1, metadata);
56+
Assert.IsTrue(metadata.ContainsKey("parameters"));
57+
58+
var prompt = metadata["parameters"];
59+
Assert.IsNotNull(prompt);
60+
Assert.AreEqual("steps: 30, sampler: DPM++ 2M Karras, cfg scale: 8, seed: 67890, size: 768x768, model: v2-1", prompt);
61+
}
2862
}
2963
}
186 Bytes
Loading
183 Bytes
Loading

SDMetaTest/SDMetaTest.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
<None Update="Metadata\latin1-pngtext.png">
3030
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
3131
</None>
32+
<None Update="Metadata\itxt-uncompressed.png">
33+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
34+
</None>
35+
<None Update="Metadata\itxt-compressed.png">
36+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
37+
</None>
3238
</ItemGroup>
3339

3440
</Project>

0 commit comments

Comments
 (0)