diff --git a/Dotnet/ScreenshotMetadata/PNGChunk.cs b/Dotnet/ScreenshotMetadata/PNGChunk.cs index f0b364f5..bcfd01e0 100644 --- a/Dotnet/ScreenshotMetadata/PNGChunk.cs +++ b/Dotnet/ScreenshotMetadata/PNGChunk.cs @@ -1,6 +1,7 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -117,6 +118,35 @@ public struct PNGChunk return new Tuple(width, height); } + /// + /// Validates this chunk against the chunk at the same index in a given file stream, checking the chunk length and CRC. + /// + /// The file stream from which the chunk data is read for validation. + /// True if chunk exists at index and is valid, false otherwise. + public bool ExistsInFile(FileStream fileStream) + { + fileStream.Seek(Index, SeekOrigin.Begin); + byte[] buffer = new byte[Length]; + fileStream.ReadExactly(buffer, 0, Length); + + if (BitConverter.IsLittleEndian) + Array.Reverse(buffer, 0, 4); + + int chunkLength = BitConverter.ToInt32(buffer, 0); + if (chunkLength != Length) + return false; + + fileStream.Seek(4 + chunkLength, SeekOrigin.Current); + fileStream.ReadExactly(buffer, 0, Length); + + if (BitConverter.IsLittleEndian) + Array.Reverse(buffer, 0, 4); + + uint crc = BitConverter.ToUInt32(buffer, 0); + + return crc == CalculateCRC(); + } + /// /// Constructs and returns a byte array representation of the PNG chunk. Generates a CRC. /// This data can be added to a PNG file as-is. @@ -146,7 +176,7 @@ public struct PNGChunk Buffer.BlockCopy(Data, 0, result, 8, Data.Length); // Calculate and copy CRC - uint crc = Crc32(Data, 0, Data.Length, Crc32(chunkTypeBytes, 0, chunkTypeBytes.Length, 0)); + uint crc = CalculateCRC(); uint reversedCrc = BinaryPrimitives.ReverseEndianness(crc); Buffer.BlockCopy(BitConverter.GetBytes(reversedCrc), 0, result, totalLength - 4, 4); @@ -154,6 +184,12 @@ public struct PNGChunk return result; } + public uint CalculateCRC() + { + var chunkTypeBytes = Encoding.ASCII.GetBytes(ChunkType); + return Crc32(Data, 0, Data.Length, Crc32(chunkTypeBytes, 0, chunkTypeBytes.Length, 0)); + } + // Crc32 implementation from // https://web.archive.org/web/20150825201508/http://upokecenter.dreamhosters.com/articles/png-image-encoder-in-c/ private static uint Crc32(byte[] stream, int offset, int length, uint crc) diff --git a/Dotnet/ScreenshotMetadata/PNGFile.cs b/Dotnet/ScreenshotMetadata/PNGFile.cs index 82e124cc..bf6c51b4 100644 --- a/Dotnet/ScreenshotMetadata/PNGFile.cs +++ b/Dotnet/ScreenshotMetadata/PNGFile.cs @@ -27,6 +27,11 @@ public class PNGFile : IDisposable { fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, 4096); } + + public PNGFile(string filePath, int bufferSize) + { + fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, bufferSize); + } /// /// Retrieves the first PNG chunk of the specified type from the file, or null if none were found. @@ -106,7 +111,57 @@ public class PNGFile : IDisposable return true; } + + /// + /// Deletes a PNG chunk from the file. + /// + /// The PNG chunk to delete. Needs a valid index set. + /// True if the chunk was successfully deleted, otherwise false. + public bool DeleteChunk(PNGChunk chunk) + { + if (!chunk.ExistsInFile(fileStream)) + return false; + + int bufferSize = 128 * 1024; + int deleteStart = chunk.Index; + int deleteLength = chunk.Length + CHUNK_NONDATA_SIZE; + + long sourcePos = deleteStart + deleteLength; + long destPos = deleteStart; + byte[] buffer = new byte[bufferSize]; + + // Copy everything after the deleted section forward + while (sourcePos < fileStream.Length) + { + fileStream.Seek(sourcePos, SeekOrigin.Begin); + int bytesRead = fileStream.Read(buffer, 0, Math.Min(buffer.Length, (int)(fileStream.Length - sourcePos))); + + if (bytesRead == 0) + break; + + fileStream.Seek(destPos, SeekOrigin.Begin); + fileStream.Write(buffer, 0, bytesRead); + + sourcePos += bytesRead; + destPos += bytesRead; + } + + fileStream.SetLength(fileStream.Length - deleteLength); + + metadataChunkCache.Remove(chunk); + + // update the index of all cached chunks + for (int i = 0; i < metadataChunkCache.Count; i++) + { + var cachedChunk = metadataChunkCache[i]; + if (cachedChunk.Index > deleteStart) + cachedChunk.Index -= deleteLength; + } + + return true; + } + /// /// Retrieves all PNG metadata chunks of a specified type /// diff --git a/Dotnet/ScreenshotMetadata/PNGHelper.cs b/Dotnet/ScreenshotMetadata/PNGHelper.cs index bf21f32d..941c9465 100644 --- a/Dotnet/ScreenshotMetadata/PNGHelper.cs +++ b/Dotnet/ScreenshotMetadata/PNGHelper.cs @@ -66,6 +66,22 @@ namespace VRCX return null; } + public static bool DeleteTextChunk(string keyword, PNGFile pngFile) + { + var iTXtChunk = pngFile.GetChunksOfType(PNGChunkTypeFilter.iTXt); + if (iTXtChunk.Count == 0) + return false; + + for (int i = 0; i < iTXtChunk.Count; i++) + { + var data = iTXtChunk[i].ReadITXtChunk(); + if (data.Item1 == keyword) + return pngFile.DeleteChunk(iTXtChunk[i]); + } + + return false; + } + /// /// Generates a PNG text chunk ready for writing. /// diff --git a/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs b/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs index 8d4514c9..1b0af4d1 100644 --- a/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs +++ b/Dotnet/ScreenshotMetadata/ScreenshotHelper.cs @@ -219,6 +219,15 @@ namespace VRCX return result; } + + public static void DeleteTextMetadata(string path, bool deleteVRChatMetadata = false) + { + using var pngFile = new PNGFile(path, 128 * 1024); + if (deleteVRChatMetadata) + PNGHelper.DeleteTextChunk("XML:com.adobe.xmp", pngFile); + + PNGHelper.DeleteTextChunk("Description", pngFile); + } public static bool WriteVRCXMetadata(string text, string path) { @@ -226,7 +235,7 @@ namespace VRCX var chunk = PNGHelper.GenerateTextChunk("Description", text); return pngFile.WriteChunk(chunk); } - + public static ScreenshotMetadata ParseVRCImage(string xmlString) { var index = xmlString.IndexOf("