using IFSEngine.Model;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
namespace IFSRenderer.Shared;
/// <summary>
/// Writes rendered PNGs, optionally embedding the fractal parameters so an exported
/// image can be dropped back in and reopened as a document.
/// </summary>
/// <remarks>
/// The WPF app stores the serialized params in the PNG's <c>tEXt</c> chunk under the
/// keyword <c>Comment</c> (and an XMP copy in <c>iTXt</c> for Adobe tools). It writes them
/// through <c>BitmapMetadata</c>, which is Windows Imaging and not portable - but the chunks
/// themselves are standard PNG, so ImageSharp reads and writes the same bytes. Only the
/// <c>Comment</c> chunk is round-tripped here, because that is the one the app itself reads
/// back (<c>TryExtractParamsFromImage</c>); the XMP copy is Adobe interop only.
/// </remarks>
public static class PngParams
{
private const string CommentKeyword = "Comment";
/// <summary>
/// Saves BGRA8 pixels (bottom-up, as OpenGL reads them) as a top-down PNG.
/// </summary>
/// <param name="includeParams">
/// Embed the fractal params so the PNG can be reopened as a document. Settings >
/// Export options > Include params.
/// </param>
/// <param name="saveMetadata">
/// Write title, author, copyright and creation date. Settings > Export options >
/// Save metadata.
/// </param>
/// <param name="author">Author name from Settings > User settings, or null.</param>
/// <param name="createdUtc">Creation timestamp; supplied by the caller so this stays testable.</param>
public static void SavePng(string path, byte[] bgra, int width, int height, IFS ifs,
bool includeParams = true, bool saveMetadata = true, string author = null,
string createdUtc = null)
{
using var image = new Image<Rgba32>(width, height);
for (int y = 0; y < height; y++)
{
int srcRow = (height - 1 - y) * width * 4;
for (int x = 0; x < width; x++)
{
int i = srcRow + x * 4;
image[x, y] = new Rgba32(bgra[i + 2], bgra[i + 1], bgra[i + 0], bgra[i + 3]);
}
}
var text = image.Metadata.GetPngMetadata().TextData;
if (ifs is not null && includeParams)
{
string serialized = IfsNodesSerializer.SerializeJsonString(ifs);
text.Add(new PngTextData(CommentKeyword, serialized, null, null));
}
if (ifs is not null && saveMetadata)
{
if (!string.IsNullOrWhiteSpace(ifs.Title))
text.Add(new PngTextData("Title", ifs.Title, null, null));
if (!string.IsNullOrWhiteSpace(author))
{
text.Add(new PngTextData("Author", author, null, null));
text.Add(new PngTextData("Copyright", "Copyright " + author, null, null));
}
if (!string.IsNullOrWhiteSpace(createdUtc))
text.Add(new PngTextData("Creation Time", createdUtc, null, null));
text.Add(new PngTextData("Software", "IFSRenderer", null, null));
text.Add(new PngTextData("Keywords", "fractal;IFS;flame", null, null));
}
image.SaveAsPng(path);
}
/// <summary>Reads parameters back out of a PNG exported with them embedded.</summary>
public static bool TryExtractParams(string path, IEnumerable<TransformPlugin> transforms,
IEnumerable<EffectPlugin> postfxs, out IFS embeddedParams)
{
embeddedParams = null;
try
{
using Image image = Image.Load(path);
string serialized = image.Metadata.GetPngMetadata().TextData
.Where(t => t.Keyword == CommentKeyword)
.Select(t => t.Value)
.FirstOrDefault();
if (string.IsNullOrWhiteSpace(serialized))
return false;
embeddedParams = IfsNodesSerializer.DeserializeJsonString(serialized, transforms, postfxs, true);
return embeddedParams is not null;
}
catch
{
return false;
}
}
}