From a02f0dc5959119f5021614947bc14b45e5d92127 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:01:52 +0000 Subject: [PATCH 1/2] Initial plan From 1a5bd10049770a69b80071fcf8c0696946c5e45a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:05:35 +0000 Subject: [PATCH 2/2] Add proper iTXt chunk parsing with zlib decompression and test fixtures Co-authored-by: jamesmoore <6506748+jamesmoore@users.noreply.github.com> --- SDMeta/Metadata/PngMetadataExtractor.cs | 68 +++++++++++++++++- .../Metadata/PngMetadataExtractorTests.cs | 34 +++++++++ SDMetaTest/Metadata/itxt-compressed.png | Bin 0 -> 186 bytes SDMetaTest/Metadata/itxt-uncompressed.png | Bin 0 -> 183 bytes SDMetaTest/SDMetaTest.csproj | 6 ++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 SDMetaTest/Metadata/itxt-compressed.png create mode 100644 SDMetaTest/Metadata/itxt-uncompressed.png diff --git a/SDMeta/Metadata/PngMetadataExtractor.cs b/SDMeta/Metadata/PngMetadataExtractor.cs index 386e42f..87bd680 100644 --- a/SDMeta/Metadata/PngMetadataExtractor.cs +++ b/SDMeta/Metadata/PngMetadataExtractor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -30,11 +31,15 @@ public class PngMetadataExtractor SkipBytes(fs, 4); break; case "tEXt": - case "iTXt": var keywordValuePair = await ReadTextualData(fs, chunkLength); SkipBytes(fs, 4); // Move past the current chunk CRC yield return keywordValuePair; break; + case "iTXt": + var itxtKeywordValuePair = await ReadInternationalTextualData(fs, chunkLength); + SkipBytes(fs, 4); // Move past the current chunk CRC + yield return itxtKeywordValuePair; + break; default: SkipBytes(fs, chunkLength + 4); // Move past the current chunk and its CRC break; @@ -85,6 +90,67 @@ private async static Task ReadChunkType(Stream stream) ); } + private async static Task<(string Key, string Value)> ReadInternationalTextualData(Stream stream, int length) + { + var buffer = new byte[length]; + if (await stream.ReadAsync(buffer) != length) + { + throw new EndOfStreamException("Unexpected end of file while reading iTXt data."); + } + + // iTXt layout (all indices into buffer): + // [keyword]\0 [compression_flag:1] [compression_method:1] + // [language_tag]\0 [translated_keyword]\0 [text...] + var keywordEnd = Array.IndexOf(buffer, (byte)0); + if (keywordEnd < 0 || keywordEnd + 4 > buffer.Length) + { + return (string.Empty, string.Empty); + } + + var keyword = Encoding.UTF8.GetString(buffer, 0, keywordEnd); + var compressionFlag = buffer[keywordEnd + 1]; + var compressionMethod = buffer[keywordEnd + 2]; + // compression_method is at keywordEnd + 2; only method 0 (zlib) is defined + var afterFlags = keywordEnd + 3; + + // Skip language tag (null-terminated) + var languageEnd = Array.IndexOf(buffer, (byte)0, afterFlags); + if (languageEnd < 0 || languageEnd + 1 > buffer.Length) + { + return (keyword, string.Empty); + } + + // Skip translated keyword (null-terminated) + var translatedKeywordEnd = Array.IndexOf(buffer, (byte)0, languageEnd + 1); + if (translatedKeywordEnd < 0 || translatedKeywordEnd + 1 > buffer.Length) + { + return (keyword, string.Empty); + } + + var textStart = translatedKeywordEnd + 1; + var textBytes = buffer.AsSpan(textStart).ToArray(); + + string text; + if (compressionFlag == 1) + { + if (compressionMethod != 0) + { + throw new InvalidDataException($"Unsupported iTXt compression method: {compressionMethod}. Only zlib deflate (method 0) is supported."); + } + using var compressed = new MemoryStream(textBytes); + using var zlib = new ZLibStream(compressed, CompressionMode.Decompress); + using var decompressed = new MemoryStream(); + await zlib.CopyToAsync(decompressed); + text = Encoding.UTF8.GetString(decompressed.ToArray()); + } + else + { + text = Encoding.UTF8.GetString(textBytes); + } + + return (keyword, text); + } + private static void SkipBytes(Stream stream, int bytesToSkip) { var originalPosition = stream.Position; diff --git a/SDMetaTest/Metadata/PngMetadataExtractorTests.cs b/SDMetaTest/Metadata/PngMetadataExtractorTests.cs index 26eed3b..844e684 100644 --- a/SDMetaTest/Metadata/PngMetadataExtractorTests.cs +++ b/SDMetaTest/Metadata/PngMetadataExtractorTests.cs @@ -25,5 +25,39 @@ public async Task PngMetadataExtractorTest() Assert.Contains("Sánchez", prompt); Assert.EndsWith("v1.8.0", prompt); } + + [TestMethod] + public async Task PngMetadataExtractorTest_iTXt_Uncompressed() + { + using var fs = new FileSystem().FileStream.New("./Metadata/itxt-uncompressed.png", FileMode.Open); + + var items = PngMetadataExtractor.ExtractTextualInformation(fs); + var metadata = await items.ToDictionaryAsync(p => p.Key, p => p.Value); + + Assert.IsNotNull(metadata); + Assert.HasCount(1, metadata); + Assert.IsTrue(metadata.ContainsKey("parameters")); + + var prompt = metadata["parameters"]; + Assert.IsNotNull(prompt); + Assert.AreEqual("steps: 20, sampler: Euler a, cfg scale: 7, seed: 12345, size: 512x512, model: v1-5-pruned", prompt); + } + + [TestMethod] + public async Task PngMetadataExtractorTest_iTXt_Compressed() + { + using var fs = new FileSystem().FileStream.New("./Metadata/itxt-compressed.png", FileMode.Open); + + var items = PngMetadataExtractor.ExtractTextualInformation(fs); + var metadata = await items.ToDictionaryAsync(p => p.Key, p => p.Value); + + Assert.IsNotNull(metadata); + Assert.HasCount(1, metadata); + Assert.IsTrue(metadata.ContainsKey("parameters")); + + var prompt = metadata["parameters"]; + Assert.IsNotNull(prompt); + Assert.AreEqual("steps: 30, sampler: DPM++ 2M Karras, cfg scale: 8, seed: 67890, size: 768x768, model: v2-1", prompt); + } } } diff --git a/SDMetaTest/Metadata/itxt-compressed.png b/SDMetaTest/Metadata/itxt-compressed.png new file mode 100644 index 0000000000000000000000000000000000000000..3389da4abeb80c59cd9e78647f56916acd6fe59b GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`_c)?97mel7hsd#N5=9)S_arf{HoX zdY-x(+kyGjHpOY@oGVo-U3d c6?2jkfNUlPMvwJN3V|#JPgg&ebxsLQ0L;caR{#J2 literal 0 HcmV?d00001 diff --git a/SDMetaTest/Metadata/itxt-uncompressed.png b/SDMetaTest/Metadata/itxt-uncompressed.png new file mode 100644 index 0000000000000000000000000000000000000000..f3c730dd6184615acbf870f1ea40c12530c57002 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`_c)jLeXTl7hsd#N5=9)S_aL3`22A zYC*A;f{}raLUCelK~8Fsm4a(2kWxt0QAkcpS13+S%t^IUFbB$~rlwda7#bOym;yPO zRX|ZwL!$~H)=|jKPf5+OQYbUjHPtOBD$PqxVQAa*8E8J2r;B4q#hl~>Ae)JS(PRCR QLLiI5)78&qol`;+0QEU9<^TWy literal 0 HcmV?d00001 diff --git a/SDMetaTest/SDMetaTest.csproj b/SDMetaTest/SDMetaTest.csproj index 680d459..b6ab0bc 100644 --- a/SDMetaTest/SDMetaTest.csproj +++ b/SDMetaTest/SDMetaTest.csproj @@ -29,6 +29,12 @@ Always + + Always + + + Always +