OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / Shared / PngParams.cs

104 lines · csharp ref:
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;
        }
    }
}