diff --git a/plug-ins/common/file-paa.c b/plug-ins/common/file-paa.c
index 090c6a1af9..5f0ab4c612 100644
--- a/plug-ins/common/file-paa.c
+++ b/plug-ins/common/file-paa.c
@@ -1,556 +1,556 @@
-/* GIMP - The GNU Image Manipulation Program
- * Copyright (C) 1995 Spencer Kimball and Peter Mattis
- *
- * Bohemia Interactive PAA graphics plug-in
- *
- * Copyright (C) 2025 Alex S.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-
-#include "config.h"
-
-#include
-#include
-
-#include
-
-#include
-#include
-
-
-#include "libgimp/stdplugins-intl.h"
-
-
-#define LOAD_PROC "file-paa-load"
-#define PLUG_IN_BINARY "file-paa"
-#define PLUG_IN_ROLE "gimp-file-paa"
-
-
-typedef enum
-{
- RGBA_4444 = 0x4444,
- RGBA_5551 = 0x1555,
- GRAY_ALPHA = 0x8080,
- RGBA_8888 = 0x8888,
- DXT1 = 0xFF01,
- DXT2 = 0xFF02,
- DXT3 = 0xFF03,
- DXT4 = 0xFF04,
- DXT5 = 0xFF05
-} PaaType;
-
-typedef struct _Paa Paa;
-typedef struct _PaaClass PaaClass;
-
-struct _Paa
-{
- GimpPlugIn parent_instance;
-};
-
-struct _PaaClass
-{
- GimpPlugInClass parent_class;
-};
-
-
-#define PAA_TYPE (paa_get_type ())
-#define PAA(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PAA_TYPE, Paa))
-
-GType paa_get_type (void) G_GNUC_CONST;
-
-
-static GList * paa_query_procedures (GimpPlugIn *plug_in);
-static GimpProcedure * paa_create_procedure (GimpPlugIn *plug_in,
- const gchar *name);
-
-static GimpValueArray * paa_load (GimpProcedure *procedure,
- GimpRunMode run_mode,
- GFile *file,
- GimpMetadata *metadata,
- GimpMetadataLoadFlags *flags,
- GimpProcedureConfig *config,
- gpointer run_data);
-
-static GimpImage * load_image (GFile *file,
- GimpProcedureConfig *config,
- GimpRunMode run_mode,
- FILE *fp,
- GError **error);
-
-static gboolean read_tag (FILE *fp,
- GError **error);
-static gboolean decode_lzss (guchar *raw_data,
- guchar *uncompressed_data,
- gint estimated_size);
-
-static void convert_from_a1r5g5b5 (gushort data,
- guint index,
- guchar *pixel);
-static void convert_from_a4r4g4b4 (gushort data,
- guint index,
- guchar *pixel);
-
-G_DEFINE_TYPE (Paa, paa, GIMP_TYPE_PLUG_IN)
-
-GIMP_MAIN (PAA_TYPE)
-DEFINE_STD_SET_I18N
-
-
-static void
-paa_class_init (PaaClass *klass)
-{
- GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);
-
- plug_in_class->query_procedures = paa_query_procedures;
- plug_in_class->create_procedure = paa_create_procedure;
- plug_in_class->set_i18n = STD_SET_I18N;
-}
-
-static void
-paa_init (Paa *paa)
-{
-}
-
-static GList *
-paa_query_procedures (GimpPlugIn *plug_in)
-{
- GList *list = NULL;
-
- list = g_list_append (list, g_strdup (LOAD_PROC));
-
- return list;
-}
-
-static GimpProcedure *
-paa_create_procedure (GimpPlugIn *plug_in,
- const gchar *name)
-{
- GimpProcedure *procedure = NULL;
-
- if (! strcmp (name, LOAD_PROC))
- {
- procedure = gimp_load_procedure_new (plug_in, name,
- GIMP_PDB_PROC_TYPE_PLUGIN,
- paa_load, NULL, NULL);
-
- gimp_procedure_set_menu_label (procedure,
- _("PAA Image"));
-
- gimp_procedure_set_documentation (procedure,
- _("Load file in the PAA file format"),
- _("Load file in the Bohemia Interactive "
- "PAA file format"),
- name);
- gimp_procedure_set_attribution (procedure,
- "Gruppe Adler",
- "Gruppe Adler",
- "2020");
-
- gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure),
- "paa");
- }
-
- return procedure;
-}
-
-static GimpValueArray *
-paa_load (GimpProcedure *procedure,
- GimpRunMode run_mode,
- GFile *file,
- GimpMetadata *metadata,
- GimpMetadataLoadFlags *flags,
- GimpProcedureConfig *config,
- gpointer run_data)
-{
- GimpValueArray *return_vals;
- GimpImage *image;
- FILE *fp;
- GError *error = NULL;
-
- gegl_init (NULL, NULL);
-
- fp = g_fopen (g_file_peek_path (file), "rb");
-
- if (! fp)
- {
- g_set_error (&error, G_FILE_ERROR, g_file_error_from_errno (errno),
- _("Could not open '%s' for reading: %s"),
- gimp_file_get_utf8_name (file), g_strerror (errno));
- return gimp_procedure_new_return_values (procedure,
- GIMP_PDB_EXECUTION_ERROR,
- error);
- }
-
- image = load_image (file, config, run_mode, fp, &error);
- fclose (fp);
-
- if (! image)
- return gimp_procedure_new_return_values (procedure,
- GIMP_PDB_EXECUTION_ERROR,
- error);
-
- return_vals = gimp_procedure_new_return_values (procedure,
- GIMP_PDB_SUCCESS,
- NULL);
- GIMP_VALUES_SET_IMAGE (return_vals, 1, image);
-
- return return_vals;
-}
-
-static GimpImage *
-load_image (GFile *file,
- GimpProcedureConfig *config,
- GimpRunMode run_mode,
- FILE *fp,
- GError **error)
-{
- GimpImage *image = NULL;
- GimpLayer *layer = NULL;
- GeglBuffer *buffer;
- GimpImageBaseType image_type;
- GimpImageType layer_type;
- gushort paa_type;
- gushort palette_index;
- gint num_mipmaps = 0;
-
- if (fread (&paa_type, 2, 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read header from '%s'"),
- gimp_file_get_utf8_name (file));
- return NULL;
- }
-
- paa_type = GUINT16_FROM_LE (paa_type);
-
- switch (paa_type)
- {
- case RGBA_4444:
- case RGBA_5551:
- case RGBA_8888:
- image_type = GIMP_RGB;
- layer_type = GIMP_RGBA_IMAGE;
- break;
-
- case GRAY_ALPHA:
- image_type = GIMP_GRAY;
- layer_type = GIMP_GRAYA_IMAGE;
- break;
-
- default:
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Currently unsupported PAA format: '%d'"),
- paa_type);
- return NULL;
- }
-
- /* Run through tags */
- while (read_tag (fp, error));
-
- if (error && *error)
- return NULL;
-
- if (fread (&palette_index, 2, 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read header from '%s'"),
- gimp_file_get_utf8_name (file));
- return NULL;
- }
-
- while (TRUE)
- {
- gushort width;
- gushort height;
- guchar block_size_array[3];
- guint32 block_size;
- guchar *raw_data;
-
- if (fread (&width, 2, 1, fp) == 0 ||
- fread (&height, 2, 1, fp) == 0 ||
- fread (block_size_array, 3, 1, fp) == 0)
- {
- /* If we have at least one layer and we get to the end of the file,
- * it's valid. Otherwise, we assume it's an error */
- if (! image)
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read header from '%s'"),
- gimp_file_get_utf8_name (file));
- return image;
- }
- width = GUINT16_FROM_LE (width);
- height = GUINT16_FROM_LE (height);
-
- block_size = ((guint32) block_size_array[2] << 16) +
- ((guint32) block_size_array[1] << 8) +
- block_size_array[0];
-
- raw_data = g_malloc0 (block_size);
- if (fread (raw_data, block_size, 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Couldn't read image data from '%s'"),
- gimp_file_get_utf8_name (file));
-
- return NULL;
- }
-
- if (image == NULL)
- image = gimp_image_new (width, height, image_type);
- layer = gimp_layer_new (image, _("Main surface"), width, height,
- layer_type, 100,
- gimp_image_get_default_new_layer_mode (image));
- gimp_image_insert_layer (image, layer, NULL, num_mipmaps);
-
- if (num_mipmaps > 0)
- {
- gchar *layer_name;
-
- layer_name = g_strdup_printf ("Mipmap: %dx%d", width, height);
-
- gimp_item_set_name (GIMP_ITEM (layer), layer_name);
- g_free (layer_name);
- }
- buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (layer));
-
- /* Non-DDS textures are compressed with LZSS */
- if (paa_type <= RGBA_8888)
- {
- guchar *uncompressed_data;
- gint estimated_size;
- guint dims = (guint32) width * height;
- guchar pixels[dims * 4];
-
- if (paa_type != RGBA_8888)
- estimated_size = dims * 2;
- else
- estimated_size = dims * 4;
-
- uncompressed_data = g_malloc0 (estimated_size);
- if (! decode_lzss (raw_data, uncompressed_data, estimated_size))
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Couldn't decompress image data from '%s'"),
- gimp_file_get_utf8_name (file));
-
- g_free (raw_data);
- g_free (uncompressed_data);
- g_object_unref (buffer);
- return NULL;
- }
-
- switch (paa_type)
- {
- case RGBA_4444:
- {
- for (gint j = 0; j < estimated_size; j += 2)
- {
- gushort condensed = ((guint16) uncompressed_data[j + 1] << 8) +
- uncompressed_data[j];
-
- convert_from_a4r4g4b4 (condensed, j / 2, pixels);
- }
-
- gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
- NULL, pixels, GEGL_AUTO_ROWSTRIDE);
- }
- break;
-
- case RGBA_5551:
- {
- for (gint j = 0; j < estimated_size; j += 2)
- {
- gushort condensed = ((guint16) uncompressed_data[j + 1] << 8) +
- uncompressed_data[j];
-
- convert_from_a1r5g5b5 (condensed, j / 2, pixels);
- }
-
- gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
- NULL, pixels, GEGL_AUTO_ROWSTRIDE);
- }
- break;
-
- case RGBA_8888:
- case GRAY_ALPHA:
- gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
- NULL, uncompressed_data, GEGL_AUTO_ROWSTRIDE);
- break;
-
- default:
- /* Shouldn't get here */
- break;
- }
- g_free (uncompressed_data);
-
- num_mipmaps++;
- }
-
- g_free (raw_data);
- g_object_unref (buffer);
- }
-
- return image;
-}
-
-static gboolean
-read_tag (FILE *fp,
- GError **error)
-{
- gchar tag[5];
- gchar tag_name[5];
- guint32 data_length;
- guchar *data;
-
- if (fread (tag, 4, 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read tag"));
-
- return FALSE;
- }
- tag[4] = '\0';
-
- if (g_strcmp0 (tag, "GGAT") != 0)
- {
- fseek (fp, -4, SEEK_CUR);
-
- return FALSE;
- }
-
- if (fread (tag_name, 4, 1, fp) == 0 ||
- fread (&data_length, 4, 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read tag"));
-
- return FALSE;
- }
- tag_name[4] = '\0';
- data_length = GUINT32_FROM_LE (data_length);
-
- data = g_malloc0 (data_length);
- if (fread (data, data_length, 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read tag"));
- g_free (data);
-
- return FALSE;
- }
- g_free (data);
-
- return TRUE;
-}
-
-/* Implementation referenced from ReadLZSS () in
- * https://github.com/PackJC/Paint.NET-PAA-PAC-Importer/
- blob/main/BIS/Core/Compression/LZSS.cs */
-static gboolean
-decode_lzss (guchar *raw_data,
- guchar *uncompressed_data,
- gint estimated_size)
-{
- gchar char_array[4113];
- gint index = 4078;
- gint flag = 0;
- gint raw_index = 0;
- gint data_index = 0;
- guchar pixel = 0;
-
- if (estimated_size <= 0)
- return FALSE;
-
- for (gint i = 0; i < index; i++)
- char_array[i] = ' ';
-
- while (estimated_size > 0)
- {
- if (((flag >>= 1) & 256) == 0)
- flag = raw_data[raw_index++] | 65280;
-
- if ((flag & 1) != 0)
- {
- guchar value = raw_data[raw_index++];
-
- pixel += (gchar) value;
-
- uncompressed_data[data_index++] = value;
- estimated_size--;
-
- char_array[index] = value;
-
- index = (index + 1) & 4095;
- }
- else
- {
- gint b1 = raw_data[raw_index++];
- gint b2 = raw_data[raw_index++];
- gint b3 = b1 | (b2 & 0xF0) << 4;
- gint b4 = (b2 & 0x0F) + 2;
-
- gint offset = index - b3;
- gint end_offset = b4 + offset;
-
- if ((b4 + 1) > (guint32) flag)
- return FALSE;
-
- for (; offset <=end_offset; offset++)
- {
- gint value = (gint) char_array[offset & 4095];
-
- pixel += (gchar) value;
-
- uncompressed_data[data_index++] = (guchar) value;
- estimated_size--;
-
- char_array[index] = (gchar) value;
- index = (index + 1) & 4095;
- }
- }
- }
- return TRUE;
-}
-
-static void
-convert_from_a1r5g5b5 (gushort data,
- guint index,
- guchar *pixel)
-{
- data = GUINT16_FROM_LE (data);
-
- pixel[index * 4] = (data & 0x1F) << 3;
- pixel[index * 4 + 1] = ((data >> 5) & 0x1F) << 3;
- pixel[index * 4 + 2] = (data >> 10) << 3;
-
- if (data & 0xF000)
- pixel[index * 4 + 3] = 255;
- else
- pixel[index * 4 + 3] = 0;
-}
-
-static void
-convert_from_a4r4g4b4 (gushort data,
- guint index,
- guchar *pixel)
-{
- data = GUINT16_FROM_LE (data);
-
- pixel[index * 4] = (data & 0x000F) << 4;
- pixel[index * 4 + 1] = (data & 0x00F0);
- pixel[index * 4 + 2] = (data & 0x0F00) >> 4;
- pixel[index * 4 + 3] = (data & 0xF000) >> 8;
-}
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * Bohemia Interactive PAA graphics plug-in
+ *
+ * Copyright (C) 2025 Alex S.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+
+#include "config.h"
+
+#include
+#include
+
+#include
+
+#include
+#include
+
+
+#include "libgimp/stdplugins-intl.h"
+
+
+#define LOAD_PROC "file-paa-load"
+#define PLUG_IN_BINARY "file-paa"
+#define PLUG_IN_ROLE "gimp-file-paa"
+
+
+typedef enum
+{
+ RGBA_4444 = 0x4444,
+ RGBA_5551 = 0x1555,
+ GRAY_ALPHA = 0x8080,
+ RGBA_8888 = 0x8888,
+ DXT1 = 0xFF01,
+ DXT2 = 0xFF02,
+ DXT3 = 0xFF03,
+ DXT4 = 0xFF04,
+ DXT5 = 0xFF05
+} PaaType;
+
+typedef struct _Paa Paa;
+typedef struct _PaaClass PaaClass;
+
+struct _Paa
+{
+ GimpPlugIn parent_instance;
+};
+
+struct _PaaClass
+{
+ GimpPlugInClass parent_class;
+};
+
+
+#define PAA_TYPE (paa_get_type ())
+#define PAA(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PAA_TYPE, Paa))
+
+GType paa_get_type (void) G_GNUC_CONST;
+
+
+static GList * paa_query_procedures (GimpPlugIn *plug_in);
+static GimpProcedure * paa_create_procedure (GimpPlugIn *plug_in,
+ const gchar *name);
+
+static GimpValueArray * paa_load (GimpProcedure *procedure,
+ GimpRunMode run_mode,
+ GFile *file,
+ GimpMetadata *metadata,
+ GimpMetadataLoadFlags *flags,
+ GimpProcedureConfig *config,
+ gpointer run_data);
+
+static GimpImage * load_image (GFile *file,
+ GimpProcedureConfig *config,
+ GimpRunMode run_mode,
+ FILE *fp,
+ GError **error);
+
+static gboolean read_tag (FILE *fp,
+ GError **error);
+static gboolean decode_lzss (guchar *raw_data,
+ guchar *uncompressed_data,
+ gint estimated_size);
+
+static void convert_from_a1r5g5b5 (gushort data,
+ guint index,
+ guchar *pixel);
+static void convert_from_a4r4g4b4 (gushort data,
+ guint index,
+ guchar *pixel);
+
+G_DEFINE_TYPE (Paa, paa, GIMP_TYPE_PLUG_IN)
+
+GIMP_MAIN (PAA_TYPE)
+DEFINE_STD_SET_I18N
+
+
+static void
+paa_class_init (PaaClass *klass)
+{
+ GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);
+
+ plug_in_class->query_procedures = paa_query_procedures;
+ plug_in_class->create_procedure = paa_create_procedure;
+ plug_in_class->set_i18n = STD_SET_I18N;
+}
+
+static void
+paa_init (Paa *paa)
+{
+}
+
+static GList *
+paa_query_procedures (GimpPlugIn *plug_in)
+{
+ GList *list = NULL;
+
+ list = g_list_append (list, g_strdup (LOAD_PROC));
+
+ return list;
+}
+
+static GimpProcedure *
+paa_create_procedure (GimpPlugIn *plug_in,
+ const gchar *name)
+{
+ GimpProcedure *procedure = NULL;
+
+ if (! strcmp (name, LOAD_PROC))
+ {
+ procedure = gimp_load_procedure_new (plug_in, name,
+ GIMP_PDB_PROC_TYPE_PLUGIN,
+ paa_load, NULL, NULL);
+
+ gimp_procedure_set_menu_label (procedure,
+ _("PAA Image"));
+
+ gimp_procedure_set_documentation (procedure,
+ _("Load file in the PAA file format"),
+ _("Load file in the Bohemia Interactive "
+ "PAA file format"),
+ name);
+ gimp_procedure_set_attribution (procedure,
+ "Gruppe Adler",
+ "Gruppe Adler",
+ "2020");
+
+ gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure),
+ "paa");
+ }
+
+ return procedure;
+}
+
+static GimpValueArray *
+paa_load (GimpProcedure *procedure,
+ GimpRunMode run_mode,
+ GFile *file,
+ GimpMetadata *metadata,
+ GimpMetadataLoadFlags *flags,
+ GimpProcedureConfig *config,
+ gpointer run_data)
+{
+ GimpValueArray *return_vals;
+ GimpImage *image;
+ FILE *fp;
+ GError *error = NULL;
+
+ gegl_init (NULL, NULL);
+
+ fp = g_fopen (g_file_peek_path (file), "rb");
+
+ if (! fp)
+ {
+ g_set_error (&error, G_FILE_ERROR, g_file_error_from_errno (errno),
+ _("Could not open '%s' for reading: %s"),
+ gimp_file_get_utf8_name (file), g_strerror (errno));
+ return gimp_procedure_new_return_values (procedure,
+ GIMP_PDB_EXECUTION_ERROR,
+ error);
+ }
+
+ image = load_image (file, config, run_mode, fp, &error);
+ fclose (fp);
+
+ if (! image)
+ return gimp_procedure_new_return_values (procedure,
+ GIMP_PDB_EXECUTION_ERROR,
+ error);
+
+ return_vals = gimp_procedure_new_return_values (procedure,
+ GIMP_PDB_SUCCESS,
+ NULL);
+ GIMP_VALUES_SET_IMAGE (return_vals, 1, image);
+
+ return return_vals;
+}
+
+static GimpImage *
+load_image (GFile *file,
+ GimpProcedureConfig *config,
+ GimpRunMode run_mode,
+ FILE *fp,
+ GError **error)
+{
+ GimpImage *image = NULL;
+ GimpLayer *layer = NULL;
+ GeglBuffer *buffer;
+ GimpImageBaseType image_type;
+ GimpImageType layer_type;
+ gushort paa_type;
+ gushort palette_index;
+ gint num_mipmaps = 0;
+
+ if (fread (&paa_type, 2, 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read header from '%s'"),
+ gimp_file_get_utf8_name (file));
+ return NULL;
+ }
+
+ paa_type = GUINT16_FROM_LE (paa_type);
+
+ switch (paa_type)
+ {
+ case RGBA_4444:
+ case RGBA_5551:
+ case RGBA_8888:
+ image_type = GIMP_RGB;
+ layer_type = GIMP_RGBA_IMAGE;
+ break;
+
+ case GRAY_ALPHA:
+ image_type = GIMP_GRAY;
+ layer_type = GIMP_GRAYA_IMAGE;
+ break;
+
+ default:
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Currently unsupported PAA format: '%d'"),
+ paa_type);
+ return NULL;
+ }
+
+ /* Run through tags */
+ while (read_tag (fp, error));
+
+ if (error && *error)
+ return NULL;
+
+ if (fread (&palette_index, 2, 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read header from '%s'"),
+ gimp_file_get_utf8_name (file));
+ return NULL;
+ }
+
+ while (TRUE)
+ {
+ gushort width;
+ gushort height;
+ guchar block_size_array[3];
+ guint32 block_size;
+ guchar *raw_data;
+
+ if (fread (&width, 2, 1, fp) == 0 ||
+ fread (&height, 2, 1, fp) == 0 ||
+ fread (block_size_array, 3, 1, fp) == 0)
+ {
+ /* If we have at least one layer and we get to the end of the file,
+ * it's valid. Otherwise, we assume it's an error */
+ if (! image)
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read header from '%s'"),
+ gimp_file_get_utf8_name (file));
+ return image;
+ }
+ width = GUINT16_FROM_LE (width);
+ height = GUINT16_FROM_LE (height);
+
+ block_size = ((guint32) block_size_array[2] << 16) +
+ ((guint32) block_size_array[1] << 8) +
+ block_size_array[0];
+
+ raw_data = g_malloc0 (block_size);
+ if (fread (raw_data, block_size, 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Couldn't read image data from '%s'"),
+ gimp_file_get_utf8_name (file));
+
+ return NULL;
+ }
+
+ if (image == NULL)
+ image = gimp_image_new (width, height, image_type);
+ layer = gimp_layer_new (image, _("Main surface"), width, height,
+ layer_type, 100,
+ gimp_image_get_default_new_layer_mode (image));
+ gimp_image_insert_layer (image, layer, NULL, num_mipmaps);
+
+ if (num_mipmaps > 0)
+ {
+ gchar *layer_name;
+
+ layer_name = g_strdup_printf ("Mipmap: %dx%d", width, height);
+
+ gimp_item_set_name (GIMP_ITEM (layer), layer_name);
+ g_free (layer_name);
+ }
+ buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (layer));
+
+ /* Non-DDS textures are compressed with LZSS */
+ if (paa_type <= RGBA_8888)
+ {
+ guchar *uncompressed_data;
+ gint estimated_size;
+ guint dims = (guint32) width * height;
+ guchar pixels[dims * 4];
+
+ if (paa_type != RGBA_8888)
+ estimated_size = dims * 2;
+ else
+ estimated_size = dims * 4;
+
+ uncompressed_data = g_malloc0 (estimated_size);
+ if (! decode_lzss (raw_data, uncompressed_data, estimated_size))
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Couldn't decompress image data from '%s'"),
+ gimp_file_get_utf8_name (file));
+
+ g_free (raw_data);
+ g_free (uncompressed_data);
+ g_object_unref (buffer);
+ return NULL;
+ }
+
+ switch (paa_type)
+ {
+ case RGBA_4444:
+ {
+ for (gint j = 0; j < estimated_size; j += 2)
+ {
+ gushort condensed = ((guint16) uncompressed_data[j + 1] << 8) +
+ uncompressed_data[j];
+
+ convert_from_a4r4g4b4 (condensed, j / 2, pixels);
+ }
+
+ gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
+ NULL, pixels, GEGL_AUTO_ROWSTRIDE);
+ }
+ break;
+
+ case RGBA_5551:
+ {
+ for (gint j = 0; j < estimated_size; j += 2)
+ {
+ gushort condensed = ((guint16) uncompressed_data[j + 1] << 8) +
+ uncompressed_data[j];
+
+ convert_from_a1r5g5b5 (condensed, j / 2, pixels);
+ }
+
+ gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
+ NULL, pixels, GEGL_AUTO_ROWSTRIDE);
+ }
+ break;
+
+ case RGBA_8888:
+ case GRAY_ALPHA:
+ gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0,
+ NULL, uncompressed_data, GEGL_AUTO_ROWSTRIDE);
+ break;
+
+ default:
+ /* Shouldn't get here */
+ break;
+ }
+ g_free (uncompressed_data);
+
+ num_mipmaps++;
+ }
+
+ g_free (raw_data);
+ g_object_unref (buffer);
+ }
+
+ return image;
+}
+
+static gboolean
+read_tag (FILE *fp,
+ GError **error)
+{
+ gchar tag[5];
+ gchar tag_name[5];
+ guint32 data_length;
+ guchar *data;
+
+ if (fread (tag, 4, 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read tag"));
+
+ return FALSE;
+ }
+ tag[4] = '\0';
+
+ if (g_strcmp0 (tag, "GGAT") != 0)
+ {
+ fseek (fp, -4, SEEK_CUR);
+
+ return FALSE;
+ }
+
+ if (fread (tag_name, 4, 1, fp) == 0 ||
+ fread (&data_length, 4, 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read tag"));
+
+ return FALSE;
+ }
+ tag_name[4] = '\0';
+ data_length = GUINT32_FROM_LE (data_length);
+
+ data = g_malloc0 (data_length);
+ if (fread (data, data_length, 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read tag"));
+ g_free (data);
+
+ return FALSE;
+ }
+ g_free (data);
+
+ return TRUE;
+}
+
+/* Implementation referenced from ReadLZSS () in
+ * https://github.com/PackJC/Paint.NET-PAA-PAC-Importer/
+ blob/main/BIS/Core/Compression/LZSS.cs */
+static gboolean
+decode_lzss (guchar *raw_data,
+ guchar *uncompressed_data,
+ gint estimated_size)
+{
+ gchar char_array[4113];
+ gint index = 4078;
+ gint flag = 0;
+ gint raw_index = 0;
+ gint data_index = 0;
+ guchar pixel = 0;
+
+ if (estimated_size <= 0)
+ return FALSE;
+
+ for (gint i = 0; i < index; i++)
+ char_array[i] = ' ';
+
+ while (estimated_size > 0)
+ {
+ if (((flag >>= 1) & 256) == 0)
+ flag = raw_data[raw_index++] | 65280;
+
+ if ((flag & 1) != 0)
+ {
+ guchar value = raw_data[raw_index++];
+
+ pixel += (gchar) value;
+
+ uncompressed_data[data_index++] = value;
+ estimated_size--;
+
+ char_array[index] = value;
+
+ index = (index + 1) & 4095;
+ }
+ else
+ {
+ gint b1 = raw_data[raw_index++];
+ gint b2 = raw_data[raw_index++];
+ gint b3 = b1 | (b2 & 0xF0) << 4;
+ gint b4 = (b2 & 0x0F) + 2;
+
+ gint offset = index - b3;
+ gint end_offset = b4 + offset;
+
+ if ((b4 + 1) > (guint32) flag)
+ return FALSE;
+
+ for (; offset <=end_offset; offset++)
+ {
+ gint value = (gint) char_array[offset & 4095];
+
+ pixel += (gchar) value;
+
+ uncompressed_data[data_index++] = (guchar) value;
+ estimated_size--;
+
+ char_array[index] = (gchar) value;
+ index = (index + 1) & 4095;
+ }
+ }
+ }
+ return TRUE;
+}
+
+static void
+convert_from_a1r5g5b5 (gushort data,
+ guint index,
+ guchar *pixel)
+{
+ data = GUINT16_FROM_LE (data);
+
+ pixel[index * 4] = (data & 0x1F) << 3;
+ pixel[index * 4 + 1] = ((data >> 5) & 0x1F) << 3;
+ pixel[index * 4 + 2] = (data >> 10) << 3;
+
+ if (data & 0xF000)
+ pixel[index * 4 + 3] = 255;
+ else
+ pixel[index * 4 + 3] = 0;
+}
+
+static void
+convert_from_a4r4g4b4 (gushort data,
+ guint index,
+ guchar *pixel)
+{
+ data = GUINT16_FROM_LE (data);
+
+ pixel[index * 4] = (data & 0x000F) << 4;
+ pixel[index * 4 + 1] = (data & 0x00F0);
+ pixel[index * 4 + 2] = (data & 0x0F00) >> 4;
+ pixel[index * 4 + 3] = (data & 0xF000) >> 8;
+}
diff --git a/plug-ins/common/file-tim.c b/plug-ins/common/file-tim.c
index d93a59092d..a0c52f0566 100644
--- a/plug-ins/common/file-tim.c
+++ b/plug-ins/common/file-tim.c
@@ -1,916 +1,916 @@
-/* GIMP - The GNU Image Manipulation Program
- * Copyright (C) 1995 Spencer Kimball and Peter Mattis
- *
- * Sony Playstation TIM graphics plug-in
- *
- * Copyright (C) 2025 Alex S.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-
-#include "config.h"
-
-#include
-#include
-
-#include
-
-#include
-#include
-
-
-#include "libgimp/stdplugins-intl.h"
-
-
-#define LOAD_PROC "file-tim-load"
-#define EXPORT_PROC "file-tim-export"
-#define PLUG_IN_BINARY "file-tim"
-#define PLUG_IN_ROLE "gimp-file-tim"
-
-
-typedef struct _TimSharedHeader
-{
- guchar magic[4];
- guchar type[4];
-
- guint32 data_offset;
- guchar x[2];
- guchar y[2];
-} TimSharedHeader;
-
-typedef enum
-{
- PSX_4BPP = 8,
- PSX_8BPP = 9,
- PSX_16BPP = 2,
- PSX_24BPP = 3
-} TimType;
-
-typedef struct _Tim Tim;
-typedef struct _TimClass TimClass;
-
-struct _Tim
-{
- GimpPlugIn parent_instance;
-};
-
-struct _TimClass
-{
- GimpPlugInClass parent_class;
-};
-
-
-#define TIM_TYPE (tim_get_type ())
-#define TIM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TIM_TYPE, Tim))
-
-GType tim_get_type (void) G_GNUC_CONST;
-
-
-static GList * tim_query_procedures (GimpPlugIn *plug_in);
-static GimpProcedure * tim_create_procedure (GimpPlugIn *plug_in,
- const gchar *name);
-
-static GimpValueArray * tim_load (GimpProcedure *procedure,
- GimpRunMode run_mode,
- GFile *file,
- GimpMetadata *metadata,
- GimpMetadataLoadFlags *flags,
- GimpProcedureConfig *config,
- gpointer run_data);
-static GimpValueArray * tim_export (GimpProcedure *procedure,
- GimpRunMode run_mode,
- GimpImage *image,
- GFile *file,
- GimpExportOptions *options,
- GimpMetadata *metadata,
- GimpProcedureConfig *config,
- gpointer run_data);
-
-static GimpImage * load_image (GFile *file,
- GObject *config,
- GimpRunMode run_mode,
- FILE *fp,
- GError **error);
-static gboolean export_image (GFile *file,
- GimpProcedureConfig *config,
- GimpImage *image,
- GimpDrawable *drawable,
- GError **error);
-
-static GimpExportCapabilities export_edit_options (GimpProcedure *procedure,
- GimpProcedureConfig *config,
- GimpExportOptions *options,
- gpointer create_data);
-
-static gboolean export_dialog (GimpImage *image,
- GimpProcedure *procedure,
- GObject *config);
-
-static void convert_from_a1r5g5b5 (gushort data,
- guint index,
- guchar *pixel);
-static void convert_to_a1r5g5b5 (guchar *pixel,
- guint index,
- guchar *data);
-
-G_DEFINE_TYPE (Tim, tim, GIMP_TYPE_PLUG_IN)
-
-GIMP_MAIN (TIM_TYPE)
-DEFINE_STD_SET_I18N
-
-
-static void
-tim_class_init (TimClass *klass)
-{
- GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);
-
- plug_in_class->query_procedures = tim_query_procedures;
- plug_in_class->create_procedure = tim_create_procedure;
- plug_in_class->set_i18n = STD_SET_I18N;
-}
-
-static void
-tim_init (Tim *tim)
-{
-}
-
-static GList *
-tim_query_procedures (GimpPlugIn *plug_in)
-{
- GList *list = NULL;
-
- list = g_list_append (list, g_strdup (LOAD_PROC));
- list = g_list_append (list, g_strdup (EXPORT_PROC));
-
- return list;
-}
-
-static GimpProcedure *
-tim_create_procedure (GimpPlugIn *plug_in,
- const gchar *name)
-{
- GimpProcedure *procedure = NULL;
-
- if (! strcmp (name, LOAD_PROC))
- {
- procedure = gimp_load_procedure_new (plug_in, name,
- GIMP_PDB_PROC_TYPE_PLUGIN,
- tim_load, NULL, NULL);
-
- gimp_procedure_set_menu_label (procedure,
- _("Playstation TIM image"));
-
- gimp_procedure_set_documentation (procedure,
- _("Load file in the TIM file format"),
- _("Load file in the Sony Playstation "
- "TIM file format"),
- name);
- gimp_procedure_set_attribution (procedure,
- "Andrew Kieschnick",
- "Andrew Kieschnick ",
- "1998");
-
- gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure),
- "tim");
- }
- else if (! strcmp (name, EXPORT_PROC))
- {
- procedure = gimp_export_procedure_new (plug_in, name,
- GIMP_PDB_PROC_TYPE_PLUGIN,
- FALSE, tim_export, NULL, NULL);
-
- gimp_procedure_set_image_types (procedure, "RGB*, GRAY*, INDEXED*");
-
- gimp_procedure_set_menu_label (procedure, _("Playstation TIM image"));
-
- gimp_procedure_set_documentation (procedure,
- _("Saves files in the TIM file format"),
- _("Saves files in the Playstation TIM "
- "file format"),
- name);
- gimp_procedure_set_attribution (procedure,
- "Andrew Kieschnick",
- "Andrew Kieschnick ",
- "1998");
-
- gimp_procedure_set_menu_label (procedure, _("Playstation TIM image"));
- gimp_file_procedure_set_format_name (GIMP_FILE_PROCEDURE (procedure),
- "Playstation TIM image");
- gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure),
- "tim");
-
- gimp_export_procedure_set_capabilities (GIMP_EXPORT_PROCEDURE (procedure),
- 0, export_edit_options, NULL, NULL);
-
- gimp_procedure_add_choice_argument (procedure, "type",
- _("_Type"),
- _("Type of TIM texture to export"),
- gimp_choice_new_with_values ("pal-4bpp", PSX_4BPP, _("4BPP (Indexed)"), NULL,
- "pal-8bpp", PSX_8BPP, _("8BPP (Indexed)"), NULL,
- "rgb-16bpp", PSX_16BPP, _("16BPP (RGB)"), NULL,
- "rgb-24bpp", PSX_24BPP, _("24BPP (RGB)"), NULL,
- NULL),
- "pal-8bpp",
- G_PARAM_READWRITE);
-
- gimp_procedure_add_int_argument (procedure, "origin-x",
- _("Image _X"),
- _("Origin X coordinate"),
- 0, G_MAXUSHORT, 0,
- G_PARAM_READWRITE);
-
- gimp_procedure_add_int_argument (procedure, "origin-y",
- _("Image _Y"),
- _("Origin Y coordinate"),
- 0, G_MAXUSHORT, 0,
- G_PARAM_READWRITE);
-
- /* Only used with indexed image exports */
- gimp_procedure_add_int_argument (procedure, "palette-x",
- _("_Palette X"),
- _("Palette X coordinate"),
- 0, G_MAXUSHORT, 0,
- G_PARAM_READWRITE);
-
- gimp_procedure_add_int_argument (procedure, "palette-y",
- _("P_alette Y"),
- _("Palette Y coordinate"),
- 0, 511, 0,
- G_PARAM_READWRITE);
- }
-
- return procedure;
-}
-
-static GimpValueArray *
-tim_load (GimpProcedure *procedure,
- GimpRunMode run_mode,
- GFile *file,
- GimpMetadata *metadata,
- GimpMetadataLoadFlags *flags,
- GimpProcedureConfig *config,
- gpointer run_data)
-{
- GimpValueArray *return_vals;
- GimpImage *image;
- FILE *fp;
- GError *error = NULL;
-
- gegl_init (NULL, NULL);
-
- fp = g_fopen (g_file_peek_path (file), "rb");
-
- if (! fp)
- {
- g_set_error (&error, G_FILE_ERROR, g_file_error_from_errno (errno),
- _("Could not open '%s' for reading: %s"),
- gimp_file_get_utf8_name (file), g_strerror (errno));
- return gimp_procedure_new_return_values (procedure,
- GIMP_PDB_EXECUTION_ERROR,
- error);
- }
-
- image = load_image (file, G_OBJECT (config), run_mode, fp, &error);
-
- fclose (fp);
-
- if (! image)
- return gimp_procedure_new_return_values (procedure,
- GIMP_PDB_EXECUTION_ERROR,
- error);
-
- return_vals = gimp_procedure_new_return_values (procedure,
- GIMP_PDB_SUCCESS,
- NULL);
-
- GIMP_VALUES_SET_IMAGE (return_vals, 1, image);
-
- return return_vals;
-}
-
-static GimpValueArray *
-tim_export (GimpProcedure *procedure,
- GimpRunMode run_mode,
- GimpImage *image,
- GFile *file,
- GimpExportOptions *options,
- GimpMetadata *metadata,
- GimpProcedureConfig *config,
- gpointer run_data)
-{
- GimpPDBStatusType status = GIMP_PDB_SUCCESS;
- GimpExportReturn export = GIMP_EXPORT_IGNORE;
- GList *drawables;
- GError *error = NULL;
-
- gegl_init (NULL, NULL);
-
- if (run_mode == GIMP_RUN_INTERACTIVE)
- {
- gimp_ui_init (PLUG_IN_BINARY);
-
- if (! export_dialog (image, procedure, G_OBJECT (config)))
- status = GIMP_PDB_CANCEL;
- }
-
- if (status == GIMP_PDB_SUCCESS)
- {
- export = gimp_export_options_get_image (options, &image);
- drawables = gimp_image_list_layers (image);
-
- if (! export_image (file, config, image, drawables->data, &error))
- status = GIMP_PDB_EXECUTION_ERROR;
-
- g_list_free (drawables);
- }
-
- if (export == GIMP_EXPORT_EXPORT)
- gimp_image_delete (image);
-
- return gimp_procedure_new_return_values (procedure, status, error);
-}
-
-static GimpImage *
-load_image (GFile *file,
- GObject *config,
- GimpRunMode run_mode,
- FILE *fp,
- GError **error)
-{
- GimpImage *image = NULL;
- GimpLayer *layer = NULL;
- GimpImageBaseType image_type;
- GimpImageType layer_type;
- GeglBuffer *buffer;
- TimSharedHeader tim_header;
- gushort width;
- gushort height;
-
- if (fread (&tim_header, sizeof (TimSharedHeader), 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read header from '%s'"),
- gimp_file_get_utf8_name (file));
- return NULL;
- }
-
- if (tim_header.type[0] != PSX_16BPP &&
- tim_header.type[0] != PSX_24BPP &&
- tim_header.type[0] != PSX_4BPP &&
- tim_header.type[0] != PSX_8BPP)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Unsupported TIM image type: %d"),
- tim_header.type[0]);
- return NULL;
- }
-
- image_type = GIMP_RGB;
- layer_type = GIMP_RGBA_IMAGE;
-
- if (tim_header.type[0] == PSX_16BPP ||
- tim_header.type[0] == PSX_24BPP)
- {
- if (fread (&width, 2, 1, fp) == 0 ||
- fread (&height, 2, 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read header from '%s'"),
- gimp_file_get_utf8_name (file));
- return NULL;
- }
-
- if (tim_header.type[0] == PSX_24BPP)
- {
- width = (gushort) ((gfloat) width / 1.5);
- layer_type = GIMP_RGB_IMAGE;
- }
-
- image = gimp_image_new (width, height, image_type);
- layer = gimp_layer_new (image, _("Background"),
- width, height, layer_type, 100,
- gimp_image_get_default_new_layer_mode (image));
- gimp_image_insert_layer (image, layer, NULL, 0);
-
- buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (layer));
-
- /* Pixels are in A1B5G5R5 format */
- if (tim_header.type[0] == PSX_16BPP)
- {
- gushort pixels[width * 2];
-
- for (gint i = 0; i < height; i++)
- {
- guchar row[width * 4];
-
- fread (&pixels, width * 2, 1, fp);
-
- for (gint j = 0; j < width; j++)
- convert_from_a1r5g5b5 (pixels[j], j, row);
-
- gegl_buffer_set (buffer, GEGL_RECTANGLE (0, i, width, 1), 0,
- NULL, row, GEGL_AUTO_ROWSTRIDE);
- }
- }
- else
- {
- guchar pixels[width * 3];
-
- for (gint i = 0; i < height; i++)
- {
- fread (&pixels, width * 3, 1, fp);
-
- gegl_buffer_set (buffer, GEGL_RECTANGLE (0, i, width, 1), 0,
- babl_format ("R'G'B' u8"), pixels,
- GEGL_AUTO_ROWSTRIDE);
- }
- }
-
- g_object_unref (buffer);
- }
- else if (tim_header.type[0] == PSX_4BPP ||
- tim_header.type[0] == PSX_8BPP)
- {
- gushort num_colors;
- gushort num_cluts;
-
- if (fread (&num_colors, 2, 1, fp) == 0 ||
- fread (&num_cluts, 2, 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read header from '%s'"),
- gimp_file_get_utf8_name (file));
- return NULL;
- }
-
- if (num_colors > 0)
- {
- guint clut_size = num_colors * num_cluts;
- gushort clut_data[clut_size];
- guchar color_map[clut_size * 4];
- gboolean promote_to_rgb = FALSE;
- guint32 image_offset;
- gushort x;
- gushort y;
-
- /* TODO: GIMP currently only supports 256 colors in an indexed image.
- * TIM textures can have multiple tables and exceeds that, so we'll
- * convert those to RGB instead*/
- if (clut_size > 256)
- promote_to_rgb = TRUE;
-
- if (! promote_to_rgb)
- {
- image_type = GIMP_INDEXED;
- layer_type = GIMP_INDEXED_IMAGE;
- }
-
- if (fread (&clut_data, (clut_size * 2), 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read palette from '%s'"),
- gimp_file_get_utf8_name (file));
- return NULL;
- }
-
- for (gint i = 0; i < clut_size; i++)
- convert_from_a1r5g5b5 (clut_data[i], i, color_map);
-
- if (fread (&image_offset, 4, 1, fp) == 0 ||
- fread (&x, 2, 1, fp) == 0 ||
- fread (&y, 2, 1, fp) == 0 ||
- fread (&width, 2, 1, fp) == 0 ||
- fread (&height, 2, 1, fp) == 0)
- {
- g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
- _("Could not read header from '%s'"),
- gimp_file_get_utf8_name (file));
- return NULL;
- }
-
- if (tim_header.type[0] == PSX_4BPP)
- width *= 4;
- else if (tim_header.type[0] == PSX_8BPP)
- width *= 2;
-
- image = gimp_image_new (width, height, image_type);
- layer = gimp_layer_new (image, _("Background"),
- width, height, layer_type, 100,
- gimp_image_get_default_new_layer_mode (image));
- gimp_image_insert_layer (image, layer, NULL, 0);
-
- gimp_palette_set_colormap (gimp_image_get_palette (image),
- babl_format ("R'G'B'A u8"),
- (guint8 *) color_map, clut_size * 4);
-
- buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (layer));
-
- /* Two pixels per byte */
- if (tim_header.type[0] == PSX_4BPP)
- {
- guchar pixels[width];
-
- for (gint i = 0; i < height; i++)
- {
- guchar row[width];
-
- fread (&pixels, width, 1, fp);
-
- for (gint j = 0; j < width; j++)
- {
- row[j * 2] = pixels[j] & 0x0F;
- row[j * 2 + 1] = pixels[j] >> 4;
- }
-
- gegl_buffer_set (buffer, GEGL_RECTANGLE (0, (i * 2), width, 2), 0,
- NULL, row, GEGL_AUTO_ROWSTRIDE);
-
- }
- }
- else if (tim_header.type[0] == PSX_8BPP)
- {
- guchar pixels[width];
- guchar rgb_pixels[width * 4];
-
- for (gint i = 0; i < height; i++)
- {
- fread (&pixels, width, 1, fp);
-
- if (! promote_to_rgb)
- {
- gegl_buffer_set (buffer, GEGL_RECTANGLE (0, i, width, 1), 0,
- NULL, pixels, GEGL_AUTO_ROWSTRIDE);
- }
- else
- {
- gint index = 0;
- for (gint i = 0; i < width; i++)
- {
- index = pixels[i];
- for (gint j = 0; j < 4; j++)
- rgb_pixels[(i * 4) + j] = color_map[index * 4 + j];
- }
-
- gegl_buffer_set (buffer, GEGL_RECTANGLE (0, i, width, 1), 0,
- NULL, rgb_pixels, GEGL_AUTO_ROWSTRIDE);
- }
- }
- }
- g_object_unref (buffer);
- }
- }
-
- return image;
-}
-
-static gboolean
-export_image (GFile *file,
- GimpProcedureConfig *config,
- GimpImage *image,
- GimpDrawable *drawable,
- GError **error)
-{
- FILE *fp;
- gsize file_size;
- GeglBuffer *buffer;
- guchar *pixels;
- guint16 width;
- guint16 height;
- guchar magic[4] = {0x10, 0, 0, 0};
- guchar type;
- guint16 origin_x;
- guint16 origin_y;
- guint16 palette_x;
- guint16 palette_y;
-
- g_object_get (config,
- "origin-x", &origin_x,
- "origin-y", &origin_y,
- "palette-x", &palette_x,
- "palette-y", &palette_y,
- NULL);
- type = (guchar) gimp_procedure_config_get_choice_id (config, "type");
-
- buffer = gimp_drawable_get_buffer (drawable);
- width = (guint16) gegl_buffer_get_width (buffer);
- height = (guint16) gegl_buffer_get_height (buffer);
-
- if (gegl_buffer_get_width (buffer) > G_MAXUSHORT ||
- gegl_buffer_get_height (buffer) > G_MAXUSHORT)
- {
- g_set_error (error, 0, 0,
- _("Unable to export '%s'. "
- "The TIM file format does not support images that are "
- "more than %d pixels wide or tall."),
- gimp_file_get_utf8_name (file), G_MAXUSHORT);
-
- return FALSE;
- }
-
- gimp_progress_init_printf (_("Exporting '%s'"),
- gimp_file_get_utf8_name (file));
-
- fp = g_fopen (g_file_peek_path (file), "wb");
-
- if (! fp)
- {
- g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
- _("Could not open '%s' for writing: %s"),
- gimp_file_get_utf8_name (file), g_strerror (errno));
- return FALSE;
- }
-
- /* Write header */
- fwrite (&magic, 4, 1, fp);
-
- fputc (type, fp);
- for (gint i = 0; i < 3; i++)
- fputc (0, fp);
-
- /* RGB images exported */
- if (type == PSX_16BPP ||
- type == PSX_24BPP)
- {
- const Babl *format = babl_format ("R'G'B' u8");
- guint16 export_width = width;
-
- /* Come back once we have the final file size */
- for (gint i = 0; i < 4; i++)
- fputc (0, fp);
-
- if (type == PSX_24BPP)
- export_width *= 1.5;
-
- fwrite (&origin_x, 2, 1, fp);
- fwrite (&origin_y, 2, 1, fp);
-
- fwrite (&export_width, 2, 1, fp);
- fwrite (&height, 2, 1, fp);
-
- /* Write pixel data */
- for (gint i = 0; i < height; i++)
- {
- pixels = g_malloc (width * 3);
-
- gegl_buffer_get (buffer, GEGL_RECTANGLE (0, i, width, 1),
- 1.0, format, pixels,
- GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
-
- if (type == PSX_24BPP)
- {
- for (gint j = 0; j < (width * 3); j++)
- fwrite (&pixels[j], 1, 1, fp);
- }
- else if (type == PSX_16BPP)
- {
- guchar converted[2];
-
- for (gint j = 0; j < width; j++)
- {
- convert_to_a1r5g5b5 (pixels, (j * 3), converted);
- fwrite (&converted, 2, 1, fp);
- }
- }
-
- g_free (pixels);
- gimp_progress_update (i / (gdouble) height);
- }
-
- /* Update file size */
- fseek (fp, 0, SEEK_END);
- file_size = ftell (fp) + 12;
- fseek (fp, 9, SEEK_SET);
-
- fwrite (&file_size, 4, 1, fp);
- }
- else
- {
- guchar *colormap;
- gint n_colors;
- guint32 colormap_size;
- guint16 palette_count;
- guint16 clut_count;
- guint32 image_size;
- guint16 export_width;
- guchar converted[2];
- guchar *indexes;
-
- colormap = gimp_palette_get_colormap (gimp_image_get_palette (image),
- babl_format ("R'G'B' u8"),
- &n_colors, NULL);
-
- /* Colormap size will be either a multiple of 16 or 256 */
- if (type == PSX_4BPP)
- colormap_size = ceil (n_colors / 16.0) * 16;
- else
- colormap_size = ceil (n_colors / 256.0) * 256;
-
- colormap_size = (colormap_size * 2) + 12;
-
- /* Palette Header */
- fwrite (&colormap_size, 4, 1, fp);
- fwrite (&palette_x, 2, 1, fp);
- fwrite (&palette_y, 2, 1, fp);
-
- if (type == PSX_4BPP)
- palette_count = 16;
- else
- palette_count = 256;
-
- clut_count = ceil ((gfloat) n_colors / palette_count);
-
- fwrite (&palette_count, 2, 1, fp);
- fwrite (&clut_count, 2, 1, fp);
-
- /* Writing actual palette */
- for (gint i = 0; i < n_colors; i++)
- {
- convert_to_a1r5g5b5 (colormap, (i * 3), converted);
- fwrite (&converted, 2, 1, fp);
- }
- /* Fill in remaining entries with padding */
- n_colors = (palette_count * clut_count) - n_colors;
- for (gint i = 0; i < n_colors; i++)
- fwrite (&converted, 2, 1, fp);
-
- /* Image header */
- image_size = width * height;
- if (type == PSX_4BPP)
- image_size /= 2;
-
- image_size += 12;
-
- fwrite (&image_size, 4, 1, fp);
- fwrite (&origin_x, 2, 1, fp);
- fwrite (&origin_y, 2, 1, fp);
-
- if (type == PSX_4BPP)
- export_width = width / 4;
- else
- export_width = width / 2;
-
- export_width = GUINT16_TO_LE (export_width);
- height = GUINT16_TO_LE (height);
-
- fwrite (&export_width, 2, 1, fp);
- fwrite (&height, 2, 1, fp);
-
- /* Writing actual image data */
- indexes = g_malloc ((guint32) width * height);
- gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, width, height),
- 1.0, NULL, indexes,
- GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
-
- if (type == PSX_8BPP)
- {
- for (gint j = 0; j < (width * height); j++)
- {
- /* Index can't exceed the palette size. In-game,
- * the palette can be swapped so we can access
- * the other colors */
- indexes[j] %= palette_count;
-
- fwrite (&indexes[j], 1, 1, fp);
- }
- }
- else
- {
- guchar compressed = 0;
-
- for (gint j = 0; j < (width * height); j += 2)
- {
- compressed = indexes[j] % palette_count;
-
- if ((j + 1) < (width * height))
- {
- indexes[j + 1] %= palette_count;
-
- compressed += (indexes[j + 1] << 4);
- }
-
- fwrite (&compressed, 1, 1, fp);
- }
- }
- }
-
- gimp_progress_update (1.0);
-
- fclose (fp);
-
- if (buffer)
- g_object_unref (buffer);
-
- return TRUE;
-}
-
-static GimpExportCapabilities
-export_edit_options (GimpProcedure *procedure,
- GimpProcedureConfig *config,
- GimpExportOptions *options,
- gpointer create_data)
-{
- GimpExportCapabilities capabilities;
- gint type;
-
- g_object_get (G_OBJECT (config),
- "type", &type,
- NULL);
-
- capabilities = (GIMP_EXPORT_CAN_HANDLE_INDEXED);
-
- if (type == PSX_16BPP || type == PSX_24BPP)
- capabilities |= GIMP_EXPORT_CAN_HANDLE_RGB;
-
- return capabilities;
-}
-
-
-static gboolean
-export_dialog (GimpImage *image,
- GimpProcedure *procedure,
- GObject *config)
-{
- GtkWidget *dialog;
- GtkWidget *box;
- gboolean run;
-
- dialog = gimp_export_procedure_dialog_new (GIMP_EXPORT_PROCEDURE (procedure),
- GIMP_PROCEDURE_CONFIG (config),
- image);
-
- gimp_procedure_dialog_get_label (GIMP_PROCEDURE_DIALOG (dialog),
- "image-origin-label", _("Image Origin"),
- FALSE, FALSE);
-
- box = gimp_procedure_dialog_fill_box (GIMP_PROCEDURE_DIALOG (dialog),
- "image-box", "origin-x", "origin-y",
- NULL);
- gtk_box_set_spacing (GTK_BOX (box), 12);
- gtk_orientable_set_orientation (GTK_ORIENTABLE (box),
- GTK_ORIENTATION_HORIZONTAL);
-
- gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog),
- "image-frame", "image-origin-label",
- FALSE, "image-box");
-
- gimp_procedure_dialog_get_label (GIMP_PROCEDURE_DIALOG (dialog),
- "palette-origin-label", _("Palette Origin"),
- FALSE, FALSE);
-
- box = gimp_procedure_dialog_fill_box (GIMP_PROCEDURE_DIALOG (dialog),
- "palette-box", "palette-x",
- "palette-y", NULL);
- gtk_box_set_spacing (GTK_BOX (box), 12);
- gtk_orientable_set_orientation (GTK_ORIENTABLE (box),
- GTK_ORIENTATION_HORIZONTAL);
-
- gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog),
- "palette-frame", "palette-origin-label",
- FALSE, "palette-box");
-
- gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), "type",
- "image-frame", "palette-frame", NULL);
-
- run = gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog));
-
- gtk_widget_destroy (dialog);
-
- return run;
-}
-
-static void
-convert_from_a1r5g5b5 (gushort data,
- guint index,
- guchar *pixel)
-{
- data = GUINT16_FROM_LE (data);
-
- pixel[index * 4] = (data & 0x1F) << 3;
- pixel[index * 4 + 1] = ((data >> 5) & 0x1F) << 3;
- pixel[index * 4 + 2] = (data >> 10) << 3;
- pixel[index * 4 + 3] = 255;
-}
-
-static void
-convert_to_a1r5g5b5 (guchar *pixel,
- guint index,
- guchar *data)
-{
- data[1] = ((pixel[index + 2] >> 3) << 2) & 0x7C;
- data[1] += (pixel[index + 1] >> 6) & 0x03;
-
- data[0] = ((pixel[index + 1] >> 3) << 5) & 0xE0;
- data[0] += (pixel[index] >> 3) & 0x1F;
-}
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * Sony Playstation TIM graphics plug-in
+ *
+ * Copyright (C) 2025 Alex S.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+
+#include "config.h"
+
+#include
+#include
+
+#include
+
+#include
+#include
+
+
+#include "libgimp/stdplugins-intl.h"
+
+
+#define LOAD_PROC "file-tim-load"
+#define EXPORT_PROC "file-tim-export"
+#define PLUG_IN_BINARY "file-tim"
+#define PLUG_IN_ROLE "gimp-file-tim"
+
+
+typedef struct _TimSharedHeader
+{
+ guchar magic[4];
+ guchar type[4];
+
+ guint32 data_offset;
+ guchar x[2];
+ guchar y[2];
+} TimSharedHeader;
+
+typedef enum
+{
+ PSX_4BPP = 8,
+ PSX_8BPP = 9,
+ PSX_16BPP = 2,
+ PSX_24BPP = 3
+} TimType;
+
+typedef struct _Tim Tim;
+typedef struct _TimClass TimClass;
+
+struct _Tim
+{
+ GimpPlugIn parent_instance;
+};
+
+struct _TimClass
+{
+ GimpPlugInClass parent_class;
+};
+
+
+#define TIM_TYPE (tim_get_type ())
+#define TIM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TIM_TYPE, Tim))
+
+GType tim_get_type (void) G_GNUC_CONST;
+
+
+static GList * tim_query_procedures (GimpPlugIn *plug_in);
+static GimpProcedure * tim_create_procedure (GimpPlugIn *plug_in,
+ const gchar *name);
+
+static GimpValueArray * tim_load (GimpProcedure *procedure,
+ GimpRunMode run_mode,
+ GFile *file,
+ GimpMetadata *metadata,
+ GimpMetadataLoadFlags *flags,
+ GimpProcedureConfig *config,
+ gpointer run_data);
+static GimpValueArray * tim_export (GimpProcedure *procedure,
+ GimpRunMode run_mode,
+ GimpImage *image,
+ GFile *file,
+ GimpExportOptions *options,
+ GimpMetadata *metadata,
+ GimpProcedureConfig *config,
+ gpointer run_data);
+
+static GimpImage * load_image (GFile *file,
+ GObject *config,
+ GimpRunMode run_mode,
+ FILE *fp,
+ GError **error);
+static gboolean export_image (GFile *file,
+ GimpProcedureConfig *config,
+ GimpImage *image,
+ GimpDrawable *drawable,
+ GError **error);
+
+static GimpExportCapabilities export_edit_options (GimpProcedure *procedure,
+ GimpProcedureConfig *config,
+ GimpExportOptions *options,
+ gpointer create_data);
+
+static gboolean export_dialog (GimpImage *image,
+ GimpProcedure *procedure,
+ GObject *config);
+
+static void convert_from_a1r5g5b5 (gushort data,
+ guint index,
+ guchar *pixel);
+static void convert_to_a1r5g5b5 (guchar *pixel,
+ guint index,
+ guchar *data);
+
+G_DEFINE_TYPE (Tim, tim, GIMP_TYPE_PLUG_IN)
+
+GIMP_MAIN (TIM_TYPE)
+DEFINE_STD_SET_I18N
+
+
+static void
+tim_class_init (TimClass *klass)
+{
+ GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);
+
+ plug_in_class->query_procedures = tim_query_procedures;
+ plug_in_class->create_procedure = tim_create_procedure;
+ plug_in_class->set_i18n = STD_SET_I18N;
+}
+
+static void
+tim_init (Tim *tim)
+{
+}
+
+static GList *
+tim_query_procedures (GimpPlugIn *plug_in)
+{
+ GList *list = NULL;
+
+ list = g_list_append (list, g_strdup (LOAD_PROC));
+ list = g_list_append (list, g_strdup (EXPORT_PROC));
+
+ return list;
+}
+
+static GimpProcedure *
+tim_create_procedure (GimpPlugIn *plug_in,
+ const gchar *name)
+{
+ GimpProcedure *procedure = NULL;
+
+ if (! strcmp (name, LOAD_PROC))
+ {
+ procedure = gimp_load_procedure_new (plug_in, name,
+ GIMP_PDB_PROC_TYPE_PLUGIN,
+ tim_load, NULL, NULL);
+
+ gimp_procedure_set_menu_label (procedure,
+ _("Playstation TIM image"));
+
+ gimp_procedure_set_documentation (procedure,
+ _("Load file in the TIM file format"),
+ _("Load file in the Sony Playstation "
+ "TIM file format"),
+ name);
+ gimp_procedure_set_attribution (procedure,
+ "Andrew Kieschnick",
+ "Andrew Kieschnick ",
+ "1998");
+
+ gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure),
+ "tim");
+ }
+ else if (! strcmp (name, EXPORT_PROC))
+ {
+ procedure = gimp_export_procedure_new (plug_in, name,
+ GIMP_PDB_PROC_TYPE_PLUGIN,
+ FALSE, tim_export, NULL, NULL);
+
+ gimp_procedure_set_image_types (procedure, "RGB*, GRAY*, INDEXED*");
+
+ gimp_procedure_set_menu_label (procedure, _("Playstation TIM image"));
+
+ gimp_procedure_set_documentation (procedure,
+ _("Saves files in the TIM file format"),
+ _("Saves files in the Playstation TIM "
+ "file format"),
+ name);
+ gimp_procedure_set_attribution (procedure,
+ "Andrew Kieschnick",
+ "Andrew Kieschnick ",
+ "1998");
+
+ gimp_procedure_set_menu_label (procedure, _("Playstation TIM image"));
+ gimp_file_procedure_set_format_name (GIMP_FILE_PROCEDURE (procedure),
+ "Playstation TIM image");
+ gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure),
+ "tim");
+
+ gimp_export_procedure_set_capabilities (GIMP_EXPORT_PROCEDURE (procedure),
+ 0, export_edit_options, NULL, NULL);
+
+ gimp_procedure_add_choice_argument (procedure, "type",
+ _("_Type"),
+ _("Type of TIM texture to export"),
+ gimp_choice_new_with_values ("pal-4bpp", PSX_4BPP, _("4BPP (Indexed)"), NULL,
+ "pal-8bpp", PSX_8BPP, _("8BPP (Indexed)"), NULL,
+ "rgb-16bpp", PSX_16BPP, _("16BPP (RGB)"), NULL,
+ "rgb-24bpp", PSX_24BPP, _("24BPP (RGB)"), NULL,
+ NULL),
+ "pal-8bpp",
+ G_PARAM_READWRITE);
+
+ gimp_procedure_add_int_argument (procedure, "origin-x",
+ _("Image _X"),
+ _("Origin X coordinate"),
+ 0, G_MAXUSHORT, 0,
+ G_PARAM_READWRITE);
+
+ gimp_procedure_add_int_argument (procedure, "origin-y",
+ _("Image _Y"),
+ _("Origin Y coordinate"),
+ 0, G_MAXUSHORT, 0,
+ G_PARAM_READWRITE);
+
+ /* Only used with indexed image exports */
+ gimp_procedure_add_int_argument (procedure, "palette-x",
+ _("_Palette X"),
+ _("Palette X coordinate"),
+ 0, G_MAXUSHORT, 0,
+ G_PARAM_READWRITE);
+
+ gimp_procedure_add_int_argument (procedure, "palette-y",
+ _("P_alette Y"),
+ _("Palette Y coordinate"),
+ 0, 511, 0,
+ G_PARAM_READWRITE);
+ }
+
+ return procedure;
+}
+
+static GimpValueArray *
+tim_load (GimpProcedure *procedure,
+ GimpRunMode run_mode,
+ GFile *file,
+ GimpMetadata *metadata,
+ GimpMetadataLoadFlags *flags,
+ GimpProcedureConfig *config,
+ gpointer run_data)
+{
+ GimpValueArray *return_vals;
+ GimpImage *image;
+ FILE *fp;
+ GError *error = NULL;
+
+ gegl_init (NULL, NULL);
+
+ fp = g_fopen (g_file_peek_path (file), "rb");
+
+ if (! fp)
+ {
+ g_set_error (&error, G_FILE_ERROR, g_file_error_from_errno (errno),
+ _("Could not open '%s' for reading: %s"),
+ gimp_file_get_utf8_name (file), g_strerror (errno));
+ return gimp_procedure_new_return_values (procedure,
+ GIMP_PDB_EXECUTION_ERROR,
+ error);
+ }
+
+ image = load_image (file, G_OBJECT (config), run_mode, fp, &error);
+
+ fclose (fp);
+
+ if (! image)
+ return gimp_procedure_new_return_values (procedure,
+ GIMP_PDB_EXECUTION_ERROR,
+ error);
+
+ return_vals = gimp_procedure_new_return_values (procedure,
+ GIMP_PDB_SUCCESS,
+ NULL);
+
+ GIMP_VALUES_SET_IMAGE (return_vals, 1, image);
+
+ return return_vals;
+}
+
+static GimpValueArray *
+tim_export (GimpProcedure *procedure,
+ GimpRunMode run_mode,
+ GimpImage *image,
+ GFile *file,
+ GimpExportOptions *options,
+ GimpMetadata *metadata,
+ GimpProcedureConfig *config,
+ gpointer run_data)
+{
+ GimpPDBStatusType status = GIMP_PDB_SUCCESS;
+ GimpExportReturn export = GIMP_EXPORT_IGNORE;
+ GList *drawables;
+ GError *error = NULL;
+
+ gegl_init (NULL, NULL);
+
+ if (run_mode == GIMP_RUN_INTERACTIVE)
+ {
+ gimp_ui_init (PLUG_IN_BINARY);
+
+ if (! export_dialog (image, procedure, G_OBJECT (config)))
+ status = GIMP_PDB_CANCEL;
+ }
+
+ if (status == GIMP_PDB_SUCCESS)
+ {
+ export = gimp_export_options_get_image (options, &image);
+ drawables = gimp_image_list_layers (image);
+
+ if (! export_image (file, config, image, drawables->data, &error))
+ status = GIMP_PDB_EXECUTION_ERROR;
+
+ g_list_free (drawables);
+ }
+
+ if (export == GIMP_EXPORT_EXPORT)
+ gimp_image_delete (image);
+
+ return gimp_procedure_new_return_values (procedure, status, error);
+}
+
+static GimpImage *
+load_image (GFile *file,
+ GObject *config,
+ GimpRunMode run_mode,
+ FILE *fp,
+ GError **error)
+{
+ GimpImage *image = NULL;
+ GimpLayer *layer = NULL;
+ GimpImageBaseType image_type;
+ GimpImageType layer_type;
+ GeglBuffer *buffer;
+ TimSharedHeader tim_header;
+ gushort width;
+ gushort height;
+
+ if (fread (&tim_header, sizeof (TimSharedHeader), 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read header from '%s'"),
+ gimp_file_get_utf8_name (file));
+ return NULL;
+ }
+
+ if (tim_header.type[0] != PSX_16BPP &&
+ tim_header.type[0] != PSX_24BPP &&
+ tim_header.type[0] != PSX_4BPP &&
+ tim_header.type[0] != PSX_8BPP)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Unsupported TIM image type: %d"),
+ tim_header.type[0]);
+ return NULL;
+ }
+
+ image_type = GIMP_RGB;
+ layer_type = GIMP_RGBA_IMAGE;
+
+ if (tim_header.type[0] == PSX_16BPP ||
+ tim_header.type[0] == PSX_24BPP)
+ {
+ if (fread (&width, 2, 1, fp) == 0 ||
+ fread (&height, 2, 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read header from '%s'"),
+ gimp_file_get_utf8_name (file));
+ return NULL;
+ }
+
+ if (tim_header.type[0] == PSX_24BPP)
+ {
+ width = (gushort) ((gfloat) width / 1.5);
+ layer_type = GIMP_RGB_IMAGE;
+ }
+
+ image = gimp_image_new (width, height, image_type);
+ layer = gimp_layer_new (image, _("Background"),
+ width, height, layer_type, 100,
+ gimp_image_get_default_new_layer_mode (image));
+ gimp_image_insert_layer (image, layer, NULL, 0);
+
+ buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (layer));
+
+ /* Pixels are in A1B5G5R5 format */
+ if (tim_header.type[0] == PSX_16BPP)
+ {
+ gushort pixels[width * 2];
+
+ for (gint i = 0; i < height; i++)
+ {
+ guchar row[width * 4];
+
+ fread (&pixels, width * 2, 1, fp);
+
+ for (gint j = 0; j < width; j++)
+ convert_from_a1r5g5b5 (pixels[j], j, row);
+
+ gegl_buffer_set (buffer, GEGL_RECTANGLE (0, i, width, 1), 0,
+ NULL, row, GEGL_AUTO_ROWSTRIDE);
+ }
+ }
+ else
+ {
+ guchar pixels[width * 3];
+
+ for (gint i = 0; i < height; i++)
+ {
+ fread (&pixels, width * 3, 1, fp);
+
+ gegl_buffer_set (buffer, GEGL_RECTANGLE (0, i, width, 1), 0,
+ babl_format ("R'G'B' u8"), pixels,
+ GEGL_AUTO_ROWSTRIDE);
+ }
+ }
+
+ g_object_unref (buffer);
+ }
+ else if (tim_header.type[0] == PSX_4BPP ||
+ tim_header.type[0] == PSX_8BPP)
+ {
+ gushort num_colors;
+ gushort num_cluts;
+
+ if (fread (&num_colors, 2, 1, fp) == 0 ||
+ fread (&num_cluts, 2, 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read header from '%s'"),
+ gimp_file_get_utf8_name (file));
+ return NULL;
+ }
+
+ if (num_colors > 0)
+ {
+ guint clut_size = num_colors * num_cluts;
+ gushort clut_data[clut_size];
+ guchar color_map[clut_size * 4];
+ gboolean promote_to_rgb = FALSE;
+ guint32 image_offset;
+ gushort x;
+ gushort y;
+
+ /* TODO: GIMP currently only supports 256 colors in an indexed image.
+ * TIM textures can have multiple tables and exceeds that, so we'll
+ * convert those to RGB instead*/
+ if (clut_size > 256)
+ promote_to_rgb = TRUE;
+
+ if (! promote_to_rgb)
+ {
+ image_type = GIMP_INDEXED;
+ layer_type = GIMP_INDEXED_IMAGE;
+ }
+
+ if (fread (&clut_data, (clut_size * 2), 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read palette from '%s'"),
+ gimp_file_get_utf8_name (file));
+ return NULL;
+ }
+
+ for (gint i = 0; i < clut_size; i++)
+ convert_from_a1r5g5b5 (clut_data[i], i, color_map);
+
+ if (fread (&image_offset, 4, 1, fp) == 0 ||
+ fread (&x, 2, 1, fp) == 0 ||
+ fread (&y, 2, 1, fp) == 0 ||
+ fread (&width, 2, 1, fp) == 0 ||
+ fread (&height, 2, 1, fp) == 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
+ _("Could not read header from '%s'"),
+ gimp_file_get_utf8_name (file));
+ return NULL;
+ }
+
+ if (tim_header.type[0] == PSX_4BPP)
+ width *= 4;
+ else if (tim_header.type[0] == PSX_8BPP)
+ width *= 2;
+
+ image = gimp_image_new (width, height, image_type);
+ layer = gimp_layer_new (image, _("Background"),
+ width, height, layer_type, 100,
+ gimp_image_get_default_new_layer_mode (image));
+ gimp_image_insert_layer (image, layer, NULL, 0);
+
+ gimp_palette_set_colormap (gimp_image_get_palette (image),
+ babl_format ("R'G'B'A u8"),
+ (guint8 *) color_map, clut_size * 4);
+
+ buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (layer));
+
+ /* Two pixels per byte */
+ if (tim_header.type[0] == PSX_4BPP)
+ {
+ guchar pixels[width];
+
+ for (gint i = 0; i < height; i++)
+ {
+ guchar row[width];
+
+ fread (&pixels, width, 1, fp);
+
+ for (gint j = 0; j < width; j++)
+ {
+ row[j * 2] = pixels[j] & 0x0F;
+ row[j * 2 + 1] = pixels[j] >> 4;
+ }
+
+ gegl_buffer_set (buffer, GEGL_RECTANGLE (0, (i * 2), width, 2), 0,
+ NULL, row, GEGL_AUTO_ROWSTRIDE);
+
+ }
+ }
+ else if (tim_header.type[0] == PSX_8BPP)
+ {
+ guchar pixels[width];
+ guchar rgb_pixels[width * 4];
+
+ for (gint i = 0; i < height; i++)
+ {
+ fread (&pixels, width, 1, fp);
+
+ if (! promote_to_rgb)
+ {
+ gegl_buffer_set (buffer, GEGL_RECTANGLE (0, i, width, 1), 0,
+ NULL, pixels, GEGL_AUTO_ROWSTRIDE);
+ }
+ else
+ {
+ gint index = 0;
+ for (gint i = 0; i < width; i++)
+ {
+ index = pixels[i];
+ for (gint j = 0; j < 4; j++)
+ rgb_pixels[(i * 4) + j] = color_map[index * 4 + j];
+ }
+
+ gegl_buffer_set (buffer, GEGL_RECTANGLE (0, i, width, 1), 0,
+ NULL, rgb_pixels, GEGL_AUTO_ROWSTRIDE);
+ }
+ }
+ }
+ g_object_unref (buffer);
+ }
+ }
+
+ return image;
+}
+
+static gboolean
+export_image (GFile *file,
+ GimpProcedureConfig *config,
+ GimpImage *image,
+ GimpDrawable *drawable,
+ GError **error)
+{
+ FILE *fp;
+ gsize file_size;
+ GeglBuffer *buffer;
+ guchar *pixels;
+ guint16 width;
+ guint16 height;
+ guchar magic[4] = {0x10, 0, 0, 0};
+ guchar type;
+ guint16 origin_x;
+ guint16 origin_y;
+ guint16 palette_x;
+ guint16 palette_y;
+
+ g_object_get (config,
+ "origin-x", &origin_x,
+ "origin-y", &origin_y,
+ "palette-x", &palette_x,
+ "palette-y", &palette_y,
+ NULL);
+ type = (guchar) gimp_procedure_config_get_choice_id (config, "type");
+
+ buffer = gimp_drawable_get_buffer (drawable);
+ width = (guint16) gegl_buffer_get_width (buffer);
+ height = (guint16) gegl_buffer_get_height (buffer);
+
+ if (gegl_buffer_get_width (buffer) > G_MAXUSHORT ||
+ gegl_buffer_get_height (buffer) > G_MAXUSHORT)
+ {
+ g_set_error (error, 0, 0,
+ _("Unable to export '%s'. "
+ "The TIM file format does not support images that are "
+ "more than %d pixels wide or tall."),
+ gimp_file_get_utf8_name (file), G_MAXUSHORT);
+
+ return FALSE;
+ }
+
+ gimp_progress_init_printf (_("Exporting '%s'"),
+ gimp_file_get_utf8_name (file));
+
+ fp = g_fopen (g_file_peek_path (file), "wb");
+
+ if (! fp)
+ {
+ g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
+ _("Could not open '%s' for writing: %s"),
+ gimp_file_get_utf8_name (file), g_strerror (errno));
+ return FALSE;
+ }
+
+ /* Write header */
+ fwrite (&magic, 4, 1, fp);
+
+ fputc (type, fp);
+ for (gint i = 0; i < 3; i++)
+ fputc (0, fp);
+
+ /* RGB images exported */
+ if (type == PSX_16BPP ||
+ type == PSX_24BPP)
+ {
+ const Babl *format = babl_format ("R'G'B' u8");
+ guint16 export_width = width;
+
+ /* Come back once we have the final file size */
+ for (gint i = 0; i < 4; i++)
+ fputc (0, fp);
+
+ if (type == PSX_24BPP)
+ export_width *= 1.5;
+
+ fwrite (&origin_x, 2, 1, fp);
+ fwrite (&origin_y, 2, 1, fp);
+
+ fwrite (&export_width, 2, 1, fp);
+ fwrite (&height, 2, 1, fp);
+
+ /* Write pixel data */
+ for (gint i = 0; i < height; i++)
+ {
+ pixels = g_malloc (width * 3);
+
+ gegl_buffer_get (buffer, GEGL_RECTANGLE (0, i, width, 1),
+ 1.0, format, pixels,
+ GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
+
+ if (type == PSX_24BPP)
+ {
+ for (gint j = 0; j < (width * 3); j++)
+ fwrite (&pixels[j], 1, 1, fp);
+ }
+ else if (type == PSX_16BPP)
+ {
+ guchar converted[2];
+
+ for (gint j = 0; j < width; j++)
+ {
+ convert_to_a1r5g5b5 (pixels, (j * 3), converted);
+ fwrite (&converted, 2, 1, fp);
+ }
+ }
+
+ g_free (pixels);
+ gimp_progress_update (i / (gdouble) height);
+ }
+
+ /* Update file size */
+ fseek (fp, 0, SEEK_END);
+ file_size = ftell (fp) + 12;
+ fseek (fp, 9, SEEK_SET);
+
+ fwrite (&file_size, 4, 1, fp);
+ }
+ else
+ {
+ guchar *colormap;
+ gint n_colors;
+ guint32 colormap_size;
+ guint16 palette_count;
+ guint16 clut_count;
+ guint32 image_size;
+ guint16 export_width;
+ guchar converted[2];
+ guchar *indexes;
+
+ colormap = gimp_palette_get_colormap (gimp_image_get_palette (image),
+ babl_format ("R'G'B' u8"),
+ &n_colors, NULL);
+
+ /* Colormap size will be either a multiple of 16 or 256 */
+ if (type == PSX_4BPP)
+ colormap_size = ceil (n_colors / 16.0) * 16;
+ else
+ colormap_size = ceil (n_colors / 256.0) * 256;
+
+ colormap_size = (colormap_size * 2) + 12;
+
+ /* Palette Header */
+ fwrite (&colormap_size, 4, 1, fp);
+ fwrite (&palette_x, 2, 1, fp);
+ fwrite (&palette_y, 2, 1, fp);
+
+ if (type == PSX_4BPP)
+ palette_count = 16;
+ else
+ palette_count = 256;
+
+ clut_count = ceil ((gfloat) n_colors / palette_count);
+
+ fwrite (&palette_count, 2, 1, fp);
+ fwrite (&clut_count, 2, 1, fp);
+
+ /* Writing actual palette */
+ for (gint i = 0; i < n_colors; i++)
+ {
+ convert_to_a1r5g5b5 (colormap, (i * 3), converted);
+ fwrite (&converted, 2, 1, fp);
+ }
+ /* Fill in remaining entries with padding */
+ n_colors = (palette_count * clut_count) - n_colors;
+ for (gint i = 0; i < n_colors; i++)
+ fwrite (&converted, 2, 1, fp);
+
+ /* Image header */
+ image_size = width * height;
+ if (type == PSX_4BPP)
+ image_size /= 2;
+
+ image_size += 12;
+
+ fwrite (&image_size, 4, 1, fp);
+ fwrite (&origin_x, 2, 1, fp);
+ fwrite (&origin_y, 2, 1, fp);
+
+ if (type == PSX_4BPP)
+ export_width = width / 4;
+ else
+ export_width = width / 2;
+
+ export_width = GUINT16_TO_LE (export_width);
+ height = GUINT16_TO_LE (height);
+
+ fwrite (&export_width, 2, 1, fp);
+ fwrite (&height, 2, 1, fp);
+
+ /* Writing actual image data */
+ indexes = g_malloc ((guint32) width * height);
+ gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, width, height),
+ 1.0, NULL, indexes,
+ GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
+
+ if (type == PSX_8BPP)
+ {
+ for (gint j = 0; j < (width * height); j++)
+ {
+ /* Index can't exceed the palette size. In-game,
+ * the palette can be swapped so we can access
+ * the other colors */
+ indexes[j] %= palette_count;
+
+ fwrite (&indexes[j], 1, 1, fp);
+ }
+ }
+ else
+ {
+ guchar compressed = 0;
+
+ for (gint j = 0; j < (width * height); j += 2)
+ {
+ compressed = indexes[j] % palette_count;
+
+ if ((j + 1) < (width * height))
+ {
+ indexes[j + 1] %= palette_count;
+
+ compressed += (indexes[j + 1] << 4);
+ }
+
+ fwrite (&compressed, 1, 1, fp);
+ }
+ }
+ }
+
+ gimp_progress_update (1.0);
+
+ fclose (fp);
+
+ if (buffer)
+ g_object_unref (buffer);
+
+ return TRUE;
+}
+
+static GimpExportCapabilities
+export_edit_options (GimpProcedure *procedure,
+ GimpProcedureConfig *config,
+ GimpExportOptions *options,
+ gpointer create_data)
+{
+ GimpExportCapabilities capabilities;
+ gint type;
+
+ g_object_get (G_OBJECT (config),
+ "type", &type,
+ NULL);
+
+ capabilities = (GIMP_EXPORT_CAN_HANDLE_INDEXED);
+
+ if (type == PSX_16BPP || type == PSX_24BPP)
+ capabilities |= GIMP_EXPORT_CAN_HANDLE_RGB;
+
+ return capabilities;
+}
+
+
+static gboolean
+export_dialog (GimpImage *image,
+ GimpProcedure *procedure,
+ GObject *config)
+{
+ GtkWidget *dialog;
+ GtkWidget *box;
+ gboolean run;
+
+ dialog = gimp_export_procedure_dialog_new (GIMP_EXPORT_PROCEDURE (procedure),
+ GIMP_PROCEDURE_CONFIG (config),
+ image);
+
+ gimp_procedure_dialog_get_label (GIMP_PROCEDURE_DIALOG (dialog),
+ "image-origin-label", _("Image Origin"),
+ FALSE, FALSE);
+
+ box = gimp_procedure_dialog_fill_box (GIMP_PROCEDURE_DIALOG (dialog),
+ "image-box", "origin-x", "origin-y",
+ NULL);
+ gtk_box_set_spacing (GTK_BOX (box), 12);
+ gtk_orientable_set_orientation (GTK_ORIENTABLE (box),
+ GTK_ORIENTATION_HORIZONTAL);
+
+ gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog),
+ "image-frame", "image-origin-label",
+ FALSE, "image-box");
+
+ gimp_procedure_dialog_get_label (GIMP_PROCEDURE_DIALOG (dialog),
+ "palette-origin-label", _("Palette Origin"),
+ FALSE, FALSE);
+
+ box = gimp_procedure_dialog_fill_box (GIMP_PROCEDURE_DIALOG (dialog),
+ "palette-box", "palette-x",
+ "palette-y", NULL);
+ gtk_box_set_spacing (GTK_BOX (box), 12);
+ gtk_orientable_set_orientation (GTK_ORIENTABLE (box),
+ GTK_ORIENTATION_HORIZONTAL);
+
+ gimp_procedure_dialog_fill_frame (GIMP_PROCEDURE_DIALOG (dialog),
+ "palette-frame", "palette-origin-label",
+ FALSE, "palette-box");
+
+ gimp_procedure_dialog_fill (GIMP_PROCEDURE_DIALOG (dialog), "type",
+ "image-frame", "palette-frame", NULL);
+
+ run = gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog));
+
+ gtk_widget_destroy (dialog);
+
+ return run;
+}
+
+static void
+convert_from_a1r5g5b5 (gushort data,
+ guint index,
+ guchar *pixel)
+{
+ data = GUINT16_FROM_LE (data);
+
+ pixel[index * 4] = (data & 0x1F) << 3;
+ pixel[index * 4 + 1] = ((data >> 5) & 0x1F) << 3;
+ pixel[index * 4 + 2] = (data >> 10) << 3;
+ pixel[index * 4 + 3] = 255;
+}
+
+static void
+convert_to_a1r5g5b5 (guchar *pixel,
+ guint index,
+ guchar *data)
+{
+ data[1] = ((pixel[index + 2] >> 3) << 2) & 0x7C;
+ data[1] += (pixel[index + 1] >> 6) & 0x03;
+
+ data[0] = ((pixel[index + 1] >> 3) << 5) & 0xE0;
+ data[0] += (pixel[index] >> 3) & 0x1F;
+}