core, dialog: Add Procreate swatches import support

Procreate swatches are zipped JSON files.
They contain an object array of HSB
color palette values. Newer versions of the
format also support different color models,
spaces, and color profiles.

This patch adds support for importing the
palette name, colors, and associated color
profile in the original HSB format.
This commit is contained in:
Alx Sa 2026-02-12 01:49:47 +00:00
parent 9e277c3944
commit dbc1c55de4
4 changed files with 282 additions and 5 deletions

View file

@ -567,6 +567,10 @@ gimp_palette_import_from_file (GimpContext *context,
palette_list = gimp_palette_load_sbz (context, file, input, error);
break;
case GIMP_PALETTE_FILE_FORMAT_PROCREATE:
palette_list = gimp_palette_load_procreate (context, file, input, error);
break;
default:
g_set_error (error,
GIMP_DATA_ERROR, GIMP_DATA_ERROR_READ,

View file

@ -17,6 +17,8 @@
#include "config.h"
#include <json-glib/json-glib.h>
#include <stdlib.h>
#include <archive.h>
@ -38,11 +40,12 @@
#include "gimp-intl.h"
/* Used by SwatchBooker and Procreate color profiles */
typedef struct
{
GimpColorProfile *profile;
gchar *id;
} SwatchBookerColorProfile;
} PaletteColorProfile;
typedef struct
{
@ -1424,10 +1427,10 @@ gimp_palette_load_sbz (GimpContext *context,
if (profile)
{
SwatchBookerColorProfile sbz_profile;
PaletteColorProfile sbz_profile;
sbz_profile.profile = profile;
sbz_profile.id = g_strdup (archive_entry_pathname (entry));
sbz_profile.id = g_strdup (archive_entry_pathname (entry));
sbz_data.embedded_profiles =
g_list_append (sbz_data.embedded_profiles, &sbz_profile);
@ -1466,6 +1469,261 @@ gimp_palette_load_sbz (GimpContext *context,
return g_list_prepend (NULL, sbz_data.palette);
}
GList *
gimp_palette_load_procreate (GimpContext *context,
GFile *file,
GInputStream *input,
GError **error)
{
GimpPalette *palette = NULL;
gchar *json_data = NULL;
struct archive *a;
struct archive_entry *entry;
size_t entry_size;
int r;
g_return_val_if_fail (G_IS_FILE (file), NULL);
g_return_val_if_fail (G_IS_INPUT_STREAM (input), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
if ((a = archive_read_new ()))
{
const gchar *name = gimp_file_get_utf8_name (file);
archive_read_support_format_all (a);
r = archive_read_open_filename (a, name, 10240);
if (r != ARCHIVE_OK)
{
archive_read_free (a);
/* TODO: Translate after string freeze */
g_set_error (error, GIMP_DATA_ERROR, GIMP_DATA_ERROR_READ,
"Unable to read Procreate swatches file");
return NULL;
}
while (archive_read_next_header (a, &entry) == ARCHIVE_OK)
{
const gchar *lower = g_ascii_strdown (archive_entry_pathname (entry), -1);
if (g_str_has_suffix (lower, ".json"))
{
entry_size = archive_entry_size (entry);
json_data = (gchar *) g_malloc (entry_size);
r = archive_read_data (a, json_data, entry_size);
}
}
if (json_data)
{
JsonParser *parser;
JsonReader *reader;
GList *profiles = NULL;
parser = json_parser_new ();
if (! json_parser_load_from_data (parser, json_data, entry_size,
error))
{
g_set_error (error, GIMP_DATA_ERROR, GIMP_DATA_ERROR_READ,
_("Could not read header from palette file '%s': "),
gimp_file_get_utf8_name (file));
g_free (json_data);
g_clear_object (&parser);
return NULL;
}
reader = json_reader_new (json_parser_get_root (parser));
if (json_reader_read_member (reader, "name") &&
json_reader_is_value (reader))
{
const gchar *name = json_reader_get_string_value (reader);
palette = GIMP_PALETTE (gimp_palette_new (context, name));
json_reader_end_member (reader);
}
else
{
g_set_error (error, GIMP_DATA_ERROR, GIMP_DATA_ERROR_READ,
_("Could not read header from palette file '%s': "),
gimp_file_get_utf8_name (file));
g_free (json_data);
g_clear_object (&reader);
g_clear_object (&parser);
return NULL;
}
/* Procreate shows columns in rows of 10 */
gimp_palette_set_columns (palette, 10);
if (json_reader_read_member (reader, "colorProfiles") &&
json_reader_is_array (reader))
{
guint array_length = json_reader_count_elements (reader);
for (gint i = 0; i < array_length; i++)
{
PaletteColorProfile *data;
GError *test = NULL;
data = g_malloc0 (sizeof (PaletteColorProfile));
data->id = NULL;
data->profile = NULL;
json_reader_read_element (reader, i);
if (json_reader_read_member (reader, "hash") &&
json_reader_is_value (reader))
{
const gchar *hash =
json_reader_get_string_value (reader);
data->id = g_strdup (hash);
}
json_reader_end_member (reader);
if (json_reader_read_member (reader, "iccData") &&
json_reader_is_value (reader))
{
GimpColorProfile *profile = NULL;
const gchar *icc_data = NULL;
guchar *decoded = NULL;
gsize icc_size;
icc_data = json_reader_get_string_value (reader);
decoded = g_base64_decode (icc_data, &icc_size);
if (decoded)
{
profile =
gimp_color_profile_new_from_icc_profile ((const guint8 *) decoded,
icc_size,
&test);
g_free (decoded);
}
if (profile)
data->profile = profile;
}
json_reader_end_member (reader);
if (data->id && data->profile)
profiles = g_list_append (profiles, data);
json_reader_end_element (reader);
}
}
json_reader_end_member (reader);
if (json_reader_read_member (reader, "swatches") &&
json_reader_is_array (reader))
{
guint array_length = json_reader_count_elements (reader);
const gchar *labels[3] = {"hue", "saturation", "brightness"};
guint position = 0;
for (gint i = 0; i < array_length; i++)
{
gfloat hsva[4] = { 0, 0, 0, 1 };
gint valid = 0;
json_reader_read_element (reader, i);
for (gint j = 0; j < 3; j++)
{
if (json_reader_read_member (reader, labels[j]) &&
json_reader_is_value (reader))
{
hsva[j] =
(gfloat) json_reader_get_double_value (reader);
valid++;
}
json_reader_end_member (reader);
}
if (json_reader_read_member (reader, "alpha") &&
json_reader_is_value (reader))
hsva[3] = (gfloat) json_reader_get_double_value (reader);
json_reader_end_member (reader);
if (valid >= 3)
{
GeglColor *color = gegl_color_new (NULL);
const Babl *space = NULL;
if (profiles &&
json_reader_read_member (reader, "colorProfile") &&
json_reader_is_value (reader))
{
GList *profile_list;
const gchar *hash =
json_reader_get_string_value (reader);
for (profile_list = profiles; profile_list;
profile_list = g_list_next (profile_list))
{
PaletteColorProfile *icc = profile_list->data;
if (! strcmp (icc->id, hash))
{
space = gimp_color_profile_get_space (icc->profile,
GIMP_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC,
NULL);
break;
}
}
}
json_reader_end_member (reader);
gegl_color_set_pixel (color,
babl_format_with_space ("HSV float", space),
hsva);
if (! gimp_palette_find_entry (palette, color, NULL))
gimp_palette_add_entry (palette, position, NULL, color);
if (json_reader_read_member (reader, "name") &&
json_reader_is_value (reader))
{
const gchar *name =
json_reader_get_string_value (reader);
gimp_palette_set_entry_name (palette, position, name);
}
json_reader_end_member (reader);
position++;
g_object_unref (color);
}
json_reader_end_element (reader);
}
}
json_reader_end_member (reader);
if (profiles)
g_list_free (profiles);
g_free (json_data);
g_clear_object (&reader);
g_clear_object (&parser);
}
r = archive_read_free (a);
}
else
{
/* TODO: Translate after string freeze */
g_set_error (error, GIMP_DATA_ERROR, GIMP_DATA_ERROR_READ,
"Unable to read Procreate swatches file");
return NULL;
}
return g_list_prepend (NULL, palette);
}
static void
swatchbooker_load_start_element (GMarkupParseContext *context,
const gchar *element_name,
@ -1624,7 +1882,7 @@ swatchbooker_load_text (GMarkupParseContext *context,
profile_list;
profile_list = g_list_next (profile_list))
{
SwatchBookerColorProfile *icc = profile_list->data;
PaletteColorProfile *icc = profile_list->data;
if (! strcmp (sbz_data->color_space, icc->id))
{
@ -1753,6 +2011,10 @@ gimp_palette_load_detect_format (GFile *file,
{
format = GIMP_PALETTE_FILE_FORMAT_SBZ;
}
else if (g_str_has_suffix (lower, ".swatches"))
{
format = GIMP_PALETTE_FILE_FORMAT_PROCREATE;
}
g_free (lower);
}

View file

@ -32,7 +32,8 @@ typedef enum
GIMP_PALETTE_FILE_FORMAT_ACB, /* Photoshop ACB color book */
GIMP_PALETTE_FILE_FORMAT_ASE, /* Photoshop ASE color palette */
GIMP_PALETTE_FILE_FORMAT_CSS, /* Cascaded Stylesheet file (CSS) */
GIMP_PALETTE_FILE_FORMAT_SBZ /* Swatchbooker SBZ file */
GIMP_PALETTE_FILE_FORMAT_SBZ, /* Swatchbooker SBZ file */
GIMP_PALETTE_FILE_FORMAT_PROCREATE /* Procreate .swatches file */
} GimpPaletteFileFormat;
@ -72,6 +73,10 @@ GList * gimp_palette_load_sbz (GimpContext *context,
GFile *file,
GInputStream *input,
GError **error);
GList * gimp_palette_load_procreate (GimpContext *context,
GFile *file,
GInputStream *input,
GError **error);
GimpPaletteFileFormat gimp_palette_load_detect_format (GFile *file,
GInputStream *input);

View file

@ -948,6 +948,12 @@ palette_import_file_set_filters (GtkFileChooser *file_chooser)
gtk_file_filter_add_pattern (filter, "*.PAL");
gtk_file_chooser_add_filter (file_chooser, filter);
filter = gtk_file_filter_new ();
gtk_file_filter_set_name (filter, "Procreate (*.swatches)");
gtk_file_filter_add_pattern (filter, "*.swatches");
gtk_file_filter_add_pattern (filter, "*.SWATCHES");
gtk_file_chooser_add_filter (file_chooser, filter);
filter = gtk_file_filter_new ();
gtk_file_filter_set_name (filter, _("SwatchBooker (*.sbz)"));
gtk_file_filter_add_pattern (filter, "*.sbz");