Files
VRCX/Dotnet/ScreenshotMetadata/PNGHelper.cs
Teacup 4e64177722 feat: Rewrite png metadata handling, new VRC metadata (#1311)
* refactor: Move ScreenshotHelper png parsing to PNGHelper, simplify interface

* refactor: Fix references to screenshotmanager

* fix: Read resolution, not description

* refactor: Rewrite/move all png reading logic into new class

* refactor: Integrate new metadata helper functions

* refactor: Add docs, re-add legacy mods support, change error handling

There are no longer specific errors for each metadata type as it was
just super unnecessary; A verbose log including the exception/string is
now logged to file instead and a generic error is given in the UI.

* fix: Show old vrc beta format images

They were being treated as a non-image
2025-08-04 18:05:40 +12:00

99 lines
3.8 KiB
C#

using SixLabors.ImageSharp.ColorSpaces;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading.Tasks;
namespace VRCX
{
public static class PNGHelper
{
public static string ReadResolution(PNGFile pngFile)
{
var ihdrChunk = pngFile.GetChunk(PNGChunkTypeFilter.IHDR);
if (ihdrChunk.HasValue)
{
var resolution = ihdrChunk.Value.ReadIHDRChunkResolution();
return resolution.Item1 + "x" + resolution.Item2;
}
return "0x0";
}
/// <summary>
/// Reads the metadata associated with a specified keyword from text chunks within a PNG file.
/// </summary>
/// <param name="keyword">The unique keyword for a speicifc text chunk to search for.</param>
/// <param name="pngFile">The PNG file containing the chunks to be searched.</param>
/// <param name="legacySearch">
/// Specifies whether to search for legacy text chunks created by older VRChat mods.
/// If true, the function searches from the end of the file using a reverse search bruteforce method.
/// </param>
/// <returns>The text associated with the specified keyword, or null if not found.</returns>
public static string ReadTextChunk(string keyword, PNGFile pngFile, bool legacySearch = false)
{
// Search for legacy text chunks created by old vrchat mods
if (legacySearch)
{
var legacyTextChunk = pngFile.GetChunkReverse(PNGChunkTypeFilter.iTXt);
if (legacyTextChunk.HasValue)
{
var data = legacyTextChunk.Value.ReadITXtChunk();
if (data.Item1 == keyword)
return data.Item2;
}
return null;
}
var iTXtChunk = pngFile.GetChunksOfType(PNGChunkTypeFilter.iTXt);
if (iTXtChunk.Count == 0)
return null;
for (int i = 0; i < iTXtChunk.Count; i++)
{
var data = iTXtChunk[i].ReadITXtChunk();
if (data.Item1 == keyword)
return data.Item2;
}
return null;
}
/// <summary>
/// Generates a PNG text chunk ready for writing.
/// </summary>
/// <param name="keyword">The keyword to write to the text chunk.</param>
/// <param name="text">The text to write to the text chunk.</param>
/// <returns>The binary data for the text chunk.</returns>
public static PNGChunk GenerateTextChunk(string keyword, string text)
{
byte[] textBytes = Encoding.UTF8.GetBytes(text);
byte[] keywordBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(keyword);
List<byte> constructedTextChunk = new List<byte>();
constructedTextChunk.AddRange(keywordBytes);
constructedTextChunk.Add(0x0); // Null separator
constructedTextChunk.Add(0x0); // Compression flag
constructedTextChunk.Add(0x0); // Compression method
constructedTextChunk.Add(0x0); // Null separator (skipping over language tag byte)
constructedTextChunk.Add(0x0); // Null separator (skipping over translated keyword byte)
constructedTextChunk.AddRange(textBytes);
return new PNGChunk
{
ChunkType = "iTXt",
ChunkTypeEnum = PNGChunkTypeFilter.iTXt,
Data = constructedTextChunk.ToArray(),
Length = constructedTextChunk.Count
};
}
}
}