OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / IFSEngine / Serialization / FlameFormatImporter.cs

129 lines · csharp ref:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

using IFSEngine.Model;

namespace IFSEngine.Serialization;

/// <summary>
/// Imports palette collections from flame format files (*.gradient / *.ugr).
/// Extracted from the original FlamePalette parsing logic.
/// </summary>
public static partial class FlameFormatImporter
{
    [GeneratedRegex(@"\{[^{]+\}")]
    private static partial Regex PaletteMatcher();

    [GeneratedRegex(@"title=""([^""]*)""\s")]
    private static partial Regex TitleMatcher();

    [GeneratedRegex(@"rotation=(-?\d*)\s")]
    private static partial Regex RotationMatcher();

    [GeneratedRegex(@"index=(-?\d+)\s+color=(\d+)\s")]
    private static partial Regex ColorIndexMatcher();

    /// <summary>
    /// Parse *.gradient / *.ugr files. Compatible with Flam3, ChaosPro, UltraFractal.
    /// </summary>
    /// <param name="filePath">Path to the flame palette file.</param>
    /// <returns>A new PaletteCollection containing all parsed palettes.</returns>
    /// <exception cref="InvalidOperationException">Thrown when the file contains no parseable palettes.</exception>
    public static async Task<PaletteCollection> ImportFromFileAsync(string filePath)
    {
        string content = await File.ReadAllTextAsync(filePath);
        var palettes = ParsePalettesFromContent(content);

        if (palettes.Count == 0)
            throw new InvalidOperationException("The file contains no parseable palettes.");

        string collectionName = System.IO.Path.GetFileNameWithoutExtension(filePath);
        if (string.IsNullOrWhiteSpace(collectionName))
            collectionName = "Imported collection";

        return new PaletteCollection
        {
            Name = collectionName,
            Description = "",
            Author = Author.Unknown,
            Palettes = palettes
        };
    }

    /// <summary>
    /// Parse palettes from raw flame format content string.
    /// </summary>
    public static List<ColorPalette> ParsePalettesFromContent(string content)
    {
        const int defaultMaxIndices = 400;
        var palettes = new List<ColorPalette>();
        var paletteTexts = PaletteMatcher().Matches(content).Select(m => m.Value).ToList();

        foreach (var paletteText in paletteTexts)
        {
            var title = TitleMatcher().Match(paletteText).Groups[1].Value;

            int rotation = 0;
            var rotationMatch = RotationMatcher().Match(paletteText);
            if (rotationMatch.Success)
                rotation = int.Parse(rotationMatch.Groups[1].Value);

            var indexedColors = ColorIndexMatcher().Matches(paletteText)
                .Select(m =>
                {
                    var bytes = BitConverter.GetBytes(int.Parse(m.Groups[2].Value));
                    return (
                        index: int.Parse(m.Groups[1].Value),
                        color: new Vector4(bytes[0] / 255.0f, bytes[1] / 255.0f, bytes[2] / 255.0f, 1.0f));
                }).ToList();

            if (indexedColors.Count == 0)
                continue;

            int maxIndices = defaultMaxIndices;
            if (rotation == 0 && indexedColors.Any(ic => ic.index > 400))
                maxIndices = 512; // Some palette editors export with 512 indices

            indexedColors = indexedColors
                .ConvertAll(ic => (index: (ic.index + maxIndices) % maxIndices, ic.color))
                .OrderBy(ic => ic.index)
                .ToList();

            // Use explicitly defined color indices as gradient keys
            var palette = new ColorPalette
            {
                Name = string.IsNullOrWhiteSpace(title) ? "Imported palette" : title,
                InterpolationMode = InterpolationMode.LinearRGB,
                BackgroundColor = null,
                KeyColors = []
            };

            foreach (var (index, color) in indexedColors)
            {
                double position = (double)index / (maxIndices - 1);
                // Round to 2 decimal places to avoid floating point issues
                position = Math.Round(position, 2, MidpointRounding.AwayFromZero);
                palette.KeyColors[position] = color;
            }

            // Ensure we have at least 2 keys
            if (palette.KeyColors.Count < 2)
            {
                if (!palette.KeyColors.ContainsKey(0.0))
                    palette.KeyColors[0.0] = indexedColors[0].color;
                if (!palette.KeyColors.ContainsKey(1.0))
                    palette.KeyColors[1.0] = indexedColors[^1].color;
            }

            palettes.Add(palette);
        }

        return palettes;
    }
}