diff --git a/app/core/gimppalette-import.c b/app/core/gimppalette-import.c index 5940251020..87fac5abe8 100644 --- a/app/core/gimppalette-import.c +++ b/app/core/gimppalette-import.c @@ -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, diff --git a/app/core/gimppalette-load.c b/app/core/gimppalette-load.c index f7a5be85ce..9870822dcf 100644 --- a/app/core/gimppalette-load.c +++ b/app/core/gimppalette-load.c @@ -17,6 +17,8 @@ #include "config.h" +#include + #include #include @@ -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); } diff --git a/app/core/gimppalette-load.h b/app/core/gimppalette-load.h index e00aefef97..0ef3ec5b01 100644 --- a/app/core/gimppalette-load.h +++ b/app/core/gimppalette-load.h @@ -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); diff --git a/app/dialogs/palette-import-dialog.c b/app/dialogs/palette-import-dialog.c index d192c96e39..488f00be51 100644 --- a/app/dialogs/palette-import-dialog.c +++ b/app/dialogs/palette-import-dialog.c @@ -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");