1
1
mirror of https://gitlab.gnome.org/GNOME/gimp.git synced 2025-10-06 01:12:40 +02:00
Files
gimp/plug-ins/file-ico/ico-export.c
Alx Sa f576531bec plug-ins: Fix palette being blacked out on indexed ICO export
Resolves #13099
For indexed ICO images, we get the palette from a
temporary image during export. Since the palette is
tied to the image, when we convert the temp image to
RGB and then delete it, the GimpPalette is lost and
the image is exported as pure black.

This patch resolves the issue by calling
gimp_palette_get_colors () to get the actual GeglColors,
then adding them to a new palette with
gimp_palette_new (). As this is separate from the
temporary image, it is retained after the call to
ico_image_get_reduced_buf ().
2025-03-13 03:25:26 +00:00

1740 lines
50 KiB
C

/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis
*
* GIMP Plug-in for Windows Icon files.
* Copyright (C) 2002 Christian Kreibich <christian@whoop.org>.
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <errno.h>
#include <string.h>
#include <glib/gstdio.h>
#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>
#include <png.h>
/* #define ICO_DBG */
#include "ico.h"
#include "ico-load.h"
#include "ico-export.h"
#include "ico-dialog.h"
#include "libgimp/stdplugins-intl.h"
static gint ico_write_int8 (FILE *fp,
guint8 *data,
gint count);
static gint ico_write_int16 (FILE *fp,
guint16 *data,
gint count);
static gint ico_write_int32 (FILE *fp,
guint32 *data,
gint count);
/* Helpers to set bits in a *cleared* data chunk */
static void ico_set_bit_in_data (guint8 *data,
gint line_width,
gint bit_num,
gint bit_val);
static void ico_set_nibble_in_data (guint8 *data,
gint line_width,
gint nibble_num,
gint nibble_val);
static void ico_set_byte_in_data (guint8 *data,
gint line_width,
gint byte_num,
gint byte_val);
static gint ico_get_layer_num_colors (GimpLayer *layer,
gboolean *uses_alpha_levels);
static void ico_image_get_reduced_buf (GimpDrawable *layer,
gint bpp,
GimpPalette **cmap_out,
guchar **buf_out);
static gboolean ico_save_init (GimpImage *image,
gint32 run_mode,
IcoSaveInfo *info,
gint n_hot_spot_x,
const gint32 *hot_spot_x,
gint n_hot_spot_y,
const gint32 *hot_spot_y,
GError **error);
static GimpPDBStatusType
shared_save_image (GFile *file,
FILE *fp_ani,
GimpImage *image,
GimpProcedure *procedure,
GimpProcedureConfig *config,
gint32 run_mode,
gsize *n_hot_spot_x,
const gint32 *hot_spot_x,
gint32 **new_hot_spot_x,
gsize *n_hot_spot_y,
const gint32 *hot_spot_y,
gint32 **new_hot_spot_y,
gint32 file_offset,
gint icon_index,
GError **error,
IcoSaveInfo *info);
static gint
ico_write_int32 (FILE *fp,
guint32 *data,
gint count)
{
gint total;
total = count;
if (count > 0)
{
#if (G_BYTE_ORDER == G_BIG_ENDIAN)
gint i;
for (i = 0; i < count; i++)
data[i] = GUINT32_FROM_LE (data[i]);
#endif
ico_write_int8 (fp, (guint8 *) data, count * 4);
#if (G_BYTE_ORDER == G_BIG_ENDIAN)
/* Put it back like we found it */
for (i = 0; i < count; i++)
data[i] = GUINT32_FROM_LE (data[i]);
#endif
}
return total * 4;
}
static gint
ico_write_int16 (FILE *fp,
guint16 *data,
gint count)
{
gint total;
total = count;
if (count > 0)
{
#if (G_BYTE_ORDER == G_BIG_ENDIAN)
gint i;
for (i = 0; i < count; i++)
data[i] = GUINT16_FROM_LE (data[i]);
#endif
ico_write_int8 (fp, (guint8 *) data, count * 2);
#if (G_BYTE_ORDER == G_BIG_ENDIAN)
/* Put it back like we found it */
for (i = 0; i < count; i++)
data[i] = GUINT16_FROM_LE (data[i]);
#endif
}
return total * 2;
}
static gint
ico_write_int8 (FILE *fp,
guint8 *data,
gint count)
{
gint total;
gint bytes;
total = count;
while (count > 0)
{
bytes = fwrite ((gchar *) data, sizeof (gchar), count, fp);
if (bytes <= 0) /* something bad happened */
break;
count -= bytes;
data += bytes;
}
return total;
}
static gboolean
ico_save_init (GimpImage *image,
gint32 run_mode,
IcoSaveInfo *info,
gint n_hot_spot_x,
const gint32 *hot_spot_x,
gint n_hot_spot_y,
const gint32 *hot_spot_y,
GError **error)
{
GList *iter;
gint num_colors;
gint i;
gboolean uses_alpha_values = FALSE;
info->layers = gimp_image_list_layers (image);
info->num_icons = g_list_length (info->layers);
info->depths = g_new (gint, info->num_icons);
info->default_depths = g_new (gint, info->num_icons);
info->compress = g_new (gboolean, info->num_icons);
info->hot_spot_x = g_new0 (gint, info->num_icons);
info->hot_spot_y = g_new0 (gint, info->num_icons);
if (run_mode == GIMP_RUN_NONINTERACTIVE &&
info->is_cursor &&
(n_hot_spot_x != info->num_icons ||
n_hot_spot_y != info->num_icons))
{
/* While it is acceptable for interactive and last values run (in
* such case, we just drop the previous values), we expect
* non-interactive calls to have exactly the right numbers of
* hot-spot values set.
*/
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
_("Called non-interactively with %d and %d hotspot "
"coordinates with an image of %d icons."),
n_hot_spot_x, n_hot_spot_y, info->num_icons);
return FALSE;
}
/* Only use existing values if we have exactly the same number of
* icons. Drop previous/saved values otherwise.
*/
if (hot_spot_x && n_hot_spot_x == info->num_icons)
{
/* XXX This is the limit of our array arguments not self-aware of
* their length (the separate args may be "lying" with a wrong
* call). We may end up with a segfault if the arg array was not
* big enough. There is not much we can really do here. I thought
* about g_renew() but it won't initialize to 0 if the arg is
* smaller. Also we don't want to free the original array.
*/
for (iter = info->layers, i = 0;
iter;
iter = g_list_next (iter), i++)
info->hot_spot_x[i] = hot_spot_x[i];
}
if (hot_spot_y && n_hot_spot_y == info->num_icons)
{
for (iter = info->layers, i = 0;
iter;
iter = g_list_next (iter), i++)
info->hot_spot_y[i] = hot_spot_y[i];
}
/* Limit the color depths to values that don't cause any color loss
* -- the user should pick these anyway, so we can save her some
* time. If the user wants to lose some colors, the settings can
* always be changed in the dialog:
*/
for (iter = info->layers, i = 0;
iter;
iter = g_list_next (iter), i++)
{
num_colors = ico_get_layer_num_colors (iter->data, &uses_alpha_values);
if (! uses_alpha_values)
{
if (num_colors <= 2)
{
/* Let's suggest monochrome */
info->default_depths [i] = 1;
}
else if (num_colors <= 16)
{
/* Let's suggest 4bpp */
info->default_depths [i] = 4;
}
else if (num_colors <= 256)
{
/* Let's suggest 8bpp */
info->default_depths [i] = 8;
}
else
{
/* Let's suggest 24bpp */
info->default_depths [i] = 24;
}
}
else
{
/* Otherwise, or if real alpha levels are used, stick with 32bpp */
info->default_depths [i] = 32;
}
/* vista icons */
if (gimp_drawable_get_width (iter->data) > 255 ||
gimp_drawable_get_height (iter->data) > 255)
{
info->compress[i] = TRUE;
}
else
{
info->compress[i] = FALSE;
}
}
/* set with default values */
memcpy (info->depths, info->default_depths,
sizeof (gint) * info->num_icons);
return TRUE;
}
static gboolean
ico_save_dialog (GimpImage *image,
GimpProcedure *procedure,
GimpProcedureConfig *config,
IcoSaveInfo *info,
AniFileHeader *ani_header,
AniSaveInfo *ani_info)
{
GtkWidget *dialog;
GList *iter;
gint i;
gboolean response;
gimp_ui_init (PLUG_IN_BINARY);
dialog = ico_dialog_new (image, procedure, config, info, ani_header,
ani_info);
for (iter = info->layers, i = 0;
iter;
iter = g_list_next (iter), i++)
{
if (info->is_cursor)
{
GimpParasite *parasite = NULL;
/* Loading hot spots for cursors if applicable */
parasite = gimp_item_get_parasite (GIMP_ITEM (iter->data), "cur-hot-spot");
if (parasite)
{
gchar *parasite_data;
guint32 parasite_size;
gint x, y;
parasite_data = (gchar *) gimp_parasite_get_data (parasite, &parasite_size);
parasite_data = g_strndup (parasite_data, parasite_size);
if (sscanf (parasite_data, "%i %i", &x, &y) == 2)
{
info->hot_spot_x[i] = x;
info->hot_spot_y[i] = y;
}
gimp_parasite_free (parasite);
g_free (parasite_data);
}
}
/* if (gimp_layer_get_visible(layers[i])) */
ico_dialog_add_icon (dialog, iter->data, i);
}
/* Scale the thing to approximately fit its content, but not too large ... */
gtk_window_set_default_size (GTK_WINDOW (dialog),
-1,
200 + (info->num_icons > 4 ?
500 : info->num_icons * 120));
gtk_widget_set_visible (dialog, TRUE);
response = gimp_procedure_dialog_run (GIMP_PROCEDURE_DIALOG (dialog));
gtk_widget_destroy (dialog);
return response;
}
static void
ico_set_bit_in_data (guint8 *data,
gint line_width,
gint bit_num,
gint bit_val)
{
gint line;
gint width32;
gint offset;
/* width per line in multiples of 32 bits */
width32 = (line_width % 32 == 0 ? line_width/32 : line_width/32 + 1);
line = bit_num / line_width;
offset = bit_num % line_width;
bit_val = bit_val & 0x00000001;
data[line * width32 * 4 + offset/8] |= (bit_val << (7 - (offset % 8)));
}
static void
ico_set_nibble_in_data (guint8 *data,
gint line_width,
gint nibble_num,
gint nibble_val)
{
gint line;
gint width8;
gint offset;
/* width per line in multiples of 32 bits */
width8 = (line_width % 8 == 0 ? line_width/8 : line_width/8 + 1);
line = nibble_num / line_width;
offset = nibble_num % line_width;
nibble_val = nibble_val & 0x0000000F;
data[line * width8 * 4 + offset/2] |=
(nibble_val << (4 * (1 - (offset % 2))));
}
static void
ico_set_byte_in_data (guint8 *data,
gint line_width,
gint byte_num,
gint byte_val)
{
gint line;
gint width4;
gint offset;
gint byte;
/* width per line in multiples of 32 bits */
width4 = (line_width % 4 == 0 ? line_width/4 : line_width/4 + 1);
line = byte_num / line_width;
offset = byte_num % line_width;
byte = byte_val & 0x000000FF;
data[line * width4 * 4 + offset] = byte;
}
/* Create a colormap from the given buffer data */
static guint32 *
ico_create_palette (GimpPalette *reduced_cmap,
gint num_colors,
gint *black_slot)
{
const Babl *format;
guchar *palette;
gint num_colors_used = 0;
gint i;
if (reduced_cmap)
num_colors_used = gimp_palette_get_color_count (reduced_cmap);
g_return_val_if_fail (num_colors_used <= num_colors, NULL);
palette = g_new0 (guchar, num_colors * 4);
*black_slot = -1;
format = babl_format ("R'G'B' u8");
for (i = 0; i < num_colors_used; i++)
{
GeglColor *color;
guchar rgb[3];
color = gimp_palette_get_entry_color (reduced_cmap, i);
gegl_color_get_pixel (color, format, rgb);
palette[i * 4 + 2] = rgb[0];
palette[i * 4 + 1] = rgb[1];
palette[i * 4] = rgb[2];
if ((rgb[0] == 0) &&
(rgb[1] == 0) &&
(rgb[2] == 0))
{
*black_slot = i;
}
}
if (*black_slot == -1)
{
if (num_colors_used == num_colors)
{
D(("WARNING -- no room for black, this shouldn't happen.\n"));
*black_slot = num_colors - 1;
palette[(num_colors-1) * 4] = 0;
palette[(num_colors-1) * 4 + 1] = 0;
palette[(num_colors-1) * 4 + 2] = 0;
}
else
{
*black_slot = num_colors_used;
}
}
return (guint32 *) palette;
}
static GHashTable *
ico_create_color_to_palette_map (const guint32 *palette,
gint num_colors)
{
GHashTable *hash;
gint i;
hash = g_hash_table_new_full (g_int_hash, g_int_equal,
(GDestroyNotify) g_free,
(GDestroyNotify) g_free);
for (i = 0; i < num_colors; i++)
{
const guint8 *pixel = (const guint8 *) &palette[i];
gint *color;
gint *slot;
color = g_new (gint, 1);
slot = g_new (gint, 1);
*color = (pixel[2] << 16 | pixel[1] << 8 | pixel[0]);
*slot = i;
g_hash_table_insert (hash, color, slot);
}
return hash;
}
static gint
ico_get_palette_index (GHashTable *hash,
gint red,
gint green,
gint blue)
{
gint color = 0;
gint *slot;
color = (red << 16 | green << 8 | blue);
slot = g_hash_table_lookup (hash, &color);
if (!slot)
{
return 0;
}
return *slot;
}
static gint
ico_get_layer_num_colors (GimpLayer *layer,
gboolean *uses_alpha_levels)
{
gint w, h;
gint bpp;
gint num_colors = 0;
guint num_pixels;
guchar *buf;
guchar *src;
guint32 *colors;
guint32 *c;
GHashTable *hash;
GeglBuffer *buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (layer));
const Babl *format;
w = gegl_buffer_get_width (buffer);
h = gegl_buffer_get_height (buffer);
num_pixels = w * h;
switch (gimp_drawable_type (GIMP_DRAWABLE (layer)))
{
case GIMP_RGB_IMAGE:
format = babl_format ("R'G'B' u8");
break;
case GIMP_RGBA_IMAGE:
format = babl_format ("R'G'B'A u8");
break;
case GIMP_GRAY_IMAGE:
format = babl_format ("Y' u8");
break;
case GIMP_GRAYA_IMAGE:
format = babl_format ("Y'A u8");
break;
case GIMP_INDEXED_IMAGE:
case GIMP_INDEXEDA_IMAGE:
format = gegl_buffer_get_format (buffer);
/* It is possible to count the colors of indexed image more easily
* with gimp_image_get_palette(), but counting only the colors
* actually used will allow more efficient bpp if possible. */
break;
default:
g_return_val_if_reached (0);
}
bpp = babl_format_get_bytes_per_pixel (format);
buf = src = g_new (guchar, num_pixels * bpp);
gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, w, h), 1.0,
format, buf,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
g_object_unref (buffer);
hash = g_hash_table_new (g_int_hash, g_int_equal);
*uses_alpha_levels = FALSE;
colors = c = g_new (guint32, num_pixels);
switch (bpp)
{
case 1:
while (num_pixels--)
{
*c = *src;
g_hash_table_insert (hash, c, c);
src++;
c++;
}
break;
case 2:
while (num_pixels--)
{
*c = (src[1] << 8) | src[0];
if (src[1] != 0 && src[1] != 255)
*uses_alpha_levels = TRUE;
g_hash_table_insert (hash, c, c);
src += 2;
c++;
}
break;
case 3:
while (num_pixels--)
{
*c = (src[2] << 16) | (src[1] << 8) | src[0];
g_hash_table_insert (hash, c, c);
src += 3;
c++;
}
break;
case 4:
while (num_pixels--)
{
*c = (src[3] << 24) | (src[2] << 16) | (src[1] << 8) | src[0];
if (src[3] != 0 && src[3] != 255)
*uses_alpha_levels = TRUE;
g_hash_table_insert (hash, c, c);
src += 4;
c++;
}
break;
}
num_colors = g_hash_table_size (hash);
/* Icons with 1 bit masks are converted to
* full transparency when loaded in GIMP.
* If the layer is not semi-transparent, we
* can subtract the "mask color" out to fix
* the default depth value for 1/4/8 bpp icons.
*/
if (! *uses_alpha_levels)
num_colors--;
g_hash_table_destroy (hash);
g_free (colors);
g_free (buf);
return num_colors;
}
gboolean
ico_cmap_contains_black (GimpPalette *cmap)
{
const Babl *format;
gint num_colors;
gint i;
gboolean success = FALSE;
format = babl_format ("R'G'B' u8");
num_colors = gimp_palette_get_color_count (cmap);
for (i = 0; i < num_colors; i++)
{
GeglColor *color = gimp_palette_get_entry_color (cmap, i);
guchar rgb[3];
gegl_color_get_pixel (color, format, rgb);
if (rgb[0] == 0 &&
rgb[1] == 0 &&
rgb[2] == 0)
{
g_object_unref (color);
success = TRUE;
break;
}
}
return success;
}
static void
ico_image_get_reduced_buf (GimpDrawable *layer,
gint bpp,
GimpPalette **cmap_out,
guchar **buf_out)
{
GimpImage *tmp_image;
GimpLayer *tmp_layer;
gint w, h;
guchar *buf;
GimpPalette *palette = NULL;
GeglBuffer *buffer = gimp_drawable_get_buffer (layer);
const Babl *format;
w = gegl_buffer_get_width (buffer);
h = gegl_buffer_get_height (buffer);
switch (gimp_drawable_type (layer))
{
case GIMP_RGB_IMAGE:
format = babl_format ("R'G'B' u8");
break;
case GIMP_RGBA_IMAGE:
format = babl_format ("R'G'B'A u8");
break;
case GIMP_GRAY_IMAGE:
format = babl_format ("Y' u8");
break;
case GIMP_GRAYA_IMAGE:
format = babl_format ("Y'A u8");
break;
case GIMP_INDEXED_IMAGE:
case GIMP_INDEXEDA_IMAGE:
format = gegl_buffer_get_format (buffer);
break;
default:
g_return_if_reached ();
}
buf = g_new (guchar, w * h * 4);
if (bpp <= 8 || bpp == 24 || babl_format_get_bytes_per_pixel (format) != 4)
{
GimpImage *image = gimp_item_get_image (GIMP_ITEM (layer));
GeglBuffer *tmp;
gint num_colors;
gint entry_num;
GeglColor **colors;
tmp_image = gimp_image_new (w, h, gimp_image_get_base_type (image));
gimp_image_undo_disable (tmp_image);
if (gimp_drawable_is_indexed (layer))
gimp_image_set_palette (tmp_image, gimp_image_get_palette (image));
tmp_layer = gimp_layer_new (tmp_image, "tmp", w, h,
gimp_drawable_type (layer),
100,
gimp_image_get_default_new_layer_mode (tmp_image));
gimp_image_insert_layer (tmp_image, tmp_layer, NULL, 0);
tmp = gimp_drawable_get_buffer (GIMP_DRAWABLE (tmp_layer));
gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, w, h), 1.0,
format, buf,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
gegl_buffer_copy (buffer, NULL, GEGL_ABYSS_NONE, tmp, NULL);
g_object_unref (tmp);
if (! gimp_drawable_is_rgb (GIMP_DRAWABLE (tmp_layer)))
gimp_image_convert_rgb (tmp_image);
if (bpp <= 8)
{
gimp_image_convert_indexed (tmp_image,
GIMP_CONVERT_DITHER_FS,
GIMP_CONVERT_PALETTE_GENERATE,
1 << bpp, TRUE, FALSE, "dummy");
palette = gimp_image_get_palette (tmp_image);
if (gimp_palette_get_color_count (palette) == (1 << bpp) &&
! ico_cmap_contains_black (palette))
{
/* Windows icons with color maps need the color black.
* We need to eliminate one more color to make room for black.
*/
if (gimp_drawable_is_indexed (layer))
{
gimp_image_set_palette (tmp_image, gimp_image_get_palette (image));
}
else if (gimp_drawable_is_gray (layer))
{
gimp_image_convert_grayscale (tmp_image);
}
else
{
gimp_image_convert_rgb (tmp_image);
}
tmp = gimp_drawable_get_buffer (GIMP_DRAWABLE (tmp_layer));
gegl_buffer_set (tmp, GEGL_RECTANGLE (0, 0, w, h), 0,
format, buf, GEGL_AUTO_ROWSTRIDE);
g_object_unref (tmp);
if (! gimp_drawable_is_rgb (layer))
gimp_image_convert_rgb (tmp_image);
gimp_image_convert_indexed (tmp_image,
GIMP_CONVERT_DITHER_FS,
GIMP_CONVERT_PALETTE_GENERATE,
(1<<bpp) - 1, TRUE, FALSE, "dummy");
palette = gimp_image_get_palette (tmp_image);
}
/* Copy the palette over now - otherwise, it goes away when we
* convert the image to RGB */
*cmap_out = gimp_palette_new ("ICO");
num_colors = gimp_palette_get_color_count (palette);
colors = gimp_palette_get_colors (palette);
for (gint i = 0; i < num_colors; i++)
gimp_palette_add_entry (*cmap_out, NULL, colors[i], &entry_num);
gimp_color_array_free (colors);
gimp_image_convert_rgb (tmp_image);
}
else if (bpp == 24)
{
gimp_drawable_merge_new_filter (GIMP_DRAWABLE (tmp_layer),
"gimp:threshold-alpha", NULL,
GIMP_LAYER_MODE_REPLACE, 1.0,
"value", ICO_ALPHA_THRESHOLD / 255.0,
NULL);
}
gimp_layer_add_alpha (tmp_layer);
tmp = gimp_drawable_get_buffer (GIMP_DRAWABLE (tmp_layer));
gegl_buffer_get (tmp, GEGL_RECTANGLE (0, 0, w, h), 1.0,
NULL, buf,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
g_object_unref (tmp);
gimp_image_delete (tmp_image);
}
else
{
gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, w, h), 1.0,
format, buf,
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
}
g_object_unref (buffer);
*buf_out = buf;
}
static gboolean
ico_write_png (FILE *fp,
GimpDrawable *layer,
gint32 depth)
{
png_structp png_ptr;
png_infop info_ptr;
png_byte **row_pointers;
gint i, rowstride;
gint width, height;
GimpPalette *palette;
guchar *buf;
row_pointers = NULL;
palette = NULL;
buf = NULL;
width = gimp_drawable_get_width (layer);
height = gimp_drawable_get_height (layer);
png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if ( !png_ptr )
return FALSE;
info_ptr = png_create_info_struct (png_ptr);
if ( !info_ptr )
{
png_destroy_write_struct (&png_ptr, NULL);
return FALSE;
}
if (setjmp (png_jmpbuf (png_ptr)))
{
png_destroy_write_struct (&png_ptr, &info_ptr);
if ( row_pointers )
g_free (row_pointers);
if (buf)
g_free (buf);
return FALSE;
}
ico_image_get_reduced_buf (layer, depth, &palette, &buf);
png_init_io (png_ptr, fp);
png_set_IHDR (png_ptr, info_ptr, width, height,
8,
PNG_COLOR_TYPE_RGBA,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
png_write_info (png_ptr, info_ptr);
rowstride = ico_rowstride (width, 32);
row_pointers = g_new (png_byte*, height);
for (i = 0; i < height; i++)
{
row_pointers[i] = buf + rowstride * i;
}
png_write_image (png_ptr, row_pointers);
row_pointers = NULL;
png_write_end (png_ptr, info_ptr);
png_destroy_write_struct (&png_ptr, &info_ptr);
g_free (row_pointers);
g_free (buf);
return TRUE;
}
static gboolean
ico_write_icon (FILE *fp,
GimpDrawable *layer,
gint32 depth)
{
IcoFileDataHeader header;
gint and_len, xor_len, palette_index, x, y;
gint num_colors = 0, black_index = 0;
gint width, height;
guchar *buf = NULL, *pixel;
guint32 *buf32;
GimpPalette *reduced_palette;
GHashTable *color_to_slot = NULL;
guchar *xor_map, *and_map;
guint32 *palette32 = NULL;
gint palette_len = 0;
guint8 alpha_threshold;
D(("Creating data structures for icon %i ------------------------\n",
num_icon));
width = gimp_drawable_get_width (layer);
height = gimp_drawable_get_height (layer);
header.header_size = 40;
header.width = width;
header.height = 2 * height;
header.planes = 1;
header.bpp = depth;
header.compression = 0;
header.image_size = 0;
header.x_res = 0;
header.y_res = 0;
header.used_clrs = 0;
header.important_clrs = 0;
num_colors = (1L << header.bpp);
D((" header size %i, w %i, h %i, planes %i, bpp %i\n",
header.header_size, header.width, header.height, header.planes,
header.bpp));
/* Reduce colors in copy of image */
ico_image_get_reduced_buf (layer, header.bpp, &reduced_palette, &buf);
buf32 = (guint32 *) buf;
/* Set up colormap and and_map when necessary: */
if (header.bpp <= 8)
{
gint num_colors_used = gimp_palette_get_color_count (reduced_palette);
/* Create a colormap */
palette32 = ico_create_palette (reduced_palette, num_colors, &black_index);
palette_len = num_colors * 4;
color_to_slot = ico_create_color_to_palette_map (palette32, num_colors_used);
D((" created %i-slot colormap with %i colors, black at slot %i\n",
num_colors, num_colors_used, black_index));
}
/* Create and_map. It's padded out to 32 bits per line: */
and_map = ico_alloc_map (width, height, 1, &and_len);
/* 32-bit bitmaps have an alpha channel as well as a mask. Any partially or
* fully opaque pixel should have an opaque mask (some ICO code in Windows
* draws pixels as black if they have a transparent mask but a non-transparent
* alpha value).
*
* For bitmaps without an alpha channel, we use the normal threshold to build
* the mask, so that the mask is as close as possible to the original alpha
* channel.
*/
alpha_threshold = header.bpp < 32 ? ICO_ALPHA_THRESHOLD : 0;
for (y = 0; y < height; y++)
for (x = 0; x < width; x++)
{
pixel = (guint8 *) &buf32[y * width + x];
ico_set_bit_in_data (and_map, width,
(height - y -1) * width + x,
(pixel[3] > alpha_threshold ? 0 : 1));
}
xor_map = ico_alloc_map (width, height, header.bpp, &xor_len);
/* Now fill in the xor map */
switch (header.bpp)
{
case 1:
for (y = 0; y < height; y++)
for (x = 0; x < width; x++)
{
pixel = (guint8 *) &buf32[y * width + x];
palette_index = ico_get_palette_index (color_to_slot, pixel[0],
pixel[1], pixel[2]);
if (ico_get_bit_from_data (and_map, width,
(height - y - 1) * width + x))
{
ico_set_bit_in_data (xor_map, width,
(height - y -1) * width + x,
black_index);
}
else
{
ico_set_bit_in_data (xor_map, width,
(height - y -1) * width + x,
palette_index);
}
}
break;
case 4:
for (y = 0; y < height; y++)
for (x = 0; x < width; x++)
{
pixel = (guint8 *) &buf32[y * width + x];
palette_index = ico_get_palette_index(color_to_slot, pixel[0],
pixel[1], pixel[2]);
if (ico_get_bit_from_data (and_map, width,
(height - y - 1) * width + x))
{
ico_set_nibble_in_data (xor_map, width,
(height - y -1) * width + x,
black_index);
}
else
{
ico_set_nibble_in_data (xor_map, width,
(height - y - 1) * width + x,
palette_index);
}
}
break;
case 8:
for (y = 0; y < height; y++)
for (x = 0; x < width; x++)
{
pixel = (guint8 *) &buf32[y * width + x];
palette_index = ico_get_palette_index (color_to_slot,
pixel[0],
pixel[1],
pixel[2]);
if (ico_get_bit_from_data (and_map, width,
(height - y - 1) * width + x))
{
ico_set_byte_in_data (xor_map, width,
(height - y - 1) * width + x,
black_index);
}
else
{
ico_set_byte_in_data (xor_map, width,
(height - y - 1) * width + x,
palette_index);
}
}
break;
case 24:
for (y = 0; y < height; y++)
{
guchar *row = xor_map + (xor_len * (height - y - 1) / height);
for (x = 0; x < width; x++)
{
pixel = (guint8 *) &buf32[y * width + x];
row[0] = pixel[2];
row[1] = pixel[1];
row[2] = pixel[0];
row += 3;
}
}
break;
default:
for (y = 0; y < height; y++)
for (x = 0; x < width; x++)
{
pixel = (guint8 *) &buf32[y * width + x];
((guint32 *) xor_map)[(height - y -1) * width + x] =
GUINT32_TO_LE ((pixel[0] << 16) |
(pixel[1] << 8) |
(pixel[2]) |
(pixel[3] << 24));
}
}
D((" filled and_map of length %i, xor_map of length %i\n",
and_len, xor_len));
if (color_to_slot)
g_hash_table_destroy (color_to_slot);
g_free (buf);
ico_write_int32 (fp, (guint32*) &header, 3);
ico_write_int16 (fp, &header.planes, 2);
ico_write_int32 (fp, &header.compression, 6);
if (palette_len)
ico_write_int8 (fp, (guint8 *) palette32, palette_len);
ico_write_int8 (fp, xor_map, xor_len);
ico_write_int8 (fp, and_map, and_len);
g_free (palette32);
g_free (xor_map);
g_free (and_map);
return TRUE;
}
static void
ico_save_info_free (IcoSaveInfo *info)
{
g_free (info->depths);
g_free (info->default_depths);
g_free (info->compress);
g_list_free (info->layers);
g_free (info->hot_spot_x);
g_free (info->hot_spot_y);
memset (info, 0, sizeof (IcoSaveInfo));
}
GimpPDBStatusType
ico_export_image (GFile *file,
GimpImage *image,
GimpProcedure *procedure,
GimpProcedureConfig *config,
gint32 run_mode,
GError **error)
{
IcoSaveInfo info;
D(("*** Exporting Microsoft icon file %s\n",
gimp_file_get_utf8_name (file)));
info.is_cursor = FALSE;
return shared_save_image (file, NULL, image, procedure,
config, run_mode,
0, NULL, NULL, 0, NULL, NULL,
0, 0, error, &info);
}
GimpPDBStatusType
cur_export_image (GFile *file,
GimpImage *image,
GimpProcedure *procedure,
GimpProcedureConfig *config,
gint32 run_mode,
gsize *n_hot_spot_x,
const gint32 *hot_spot_x,
gint32 **new_hot_spot_x,
gsize *n_hot_spot_y,
const gint32 *hot_spot_y,
gint32 **new_hot_spot_y,
GError **error)
{
IcoSaveInfo info;
D(("*** Exporting Microsoft cursor file %s\n",
gimp_file_get_utf8_name (file)));
info.is_cursor = TRUE;
return shared_save_image (file, NULL, image, procedure,
config, run_mode,
n_hot_spot_x, hot_spot_x, new_hot_spot_x,
n_hot_spot_y, hot_spot_y, new_hot_spot_y,
0, 0, error, &info);
}
/* Ported from James Huang's ani.c code, under the GPL v3 license */
GimpPDBStatusType
ani_export_image (GFile *file,
GimpImage *image,
GimpProcedure *procedure,
GimpProcedureConfig *config,
gint32 run_mode,
gsize *n_hot_spot_x,
const gint32 *hot_spot_x,
gint32 **new_hot_spot_x,
gsize *n_hot_spot_y,
const gint32 *hot_spot_y,
gint32 **new_hot_spot_y,
AniFileHeader *header,
AniSaveInfo *ani_info,
GError **error)
{
FILE *fp;
gint32 i;
gchar *str;
GimpParasite *parasite = NULL;
gchar id[5];
guint32 size;
guint8 padding = 0;
gint32 offset, ofs_size_riff, ofs_size_list, ofs_size_icon;
gint32 ofs_size_info = 0;
gint32 ofs_metadata = 0;
IcoSaveInfo info;
if (! ico_save_init (image, run_mode, &info,
*n_hot_spot_x, hot_spot_x,
*n_hot_spot_y, hot_spot_y,
error))
{
return GIMP_PDB_EXECUTION_ERROR;
}
/* Save individual frames as .cur so we can retain
* the hotspot information
*/
info.is_cursor = TRUE;
/* Default header values */
header->bSizeOf = sizeof (*header);
header->frames = info.num_icons;
header->steps = info.num_icons;
header->x = 0;
header->y = 0;
if (info.depths[0] == 24)
{
header->bpp = 4;
header->planes = 1;
}
else
{
header->bpp = 0;
header->planes = 0;
}
header->flags = 1;
/* Load metadata from parasite */
parasite = gimp_image_get_parasite (image, "ani-header");
if (parasite)
{
gchar *parasite_data;
guint32 parasite_size;
gint jif_rate;
parasite_data = (gchar *) gimp_parasite_get_data (parasite, &parasite_size);
parasite_data = g_strndup (parasite_data, parasite_size);
if (sscanf (parasite_data, "%i", &jif_rate) == 1)
{
header->jif_rate = jif_rate;
}
gimp_parasite_free (parasite);
g_free (parasite_data);
}
parasite = gimp_image_get_parasite (image, "ani-info-inam");
if (parasite)
{
guint32 parasite_size;
gchar *inam = NULL;
inam = (gchar *) gimp_parasite_get_data (parasite, &parasite_size);
ani_info->inam = g_strndup (inam, parasite_size);
gimp_parasite_free (parasite);
}
parasite = gimp_image_get_parasite (image, "ani-info-iart");
if (parasite)
{
guint32 parasite_size;
gchar *iart = NULL;
iart = (gchar *) gimp_parasite_get_data (parasite, &parasite_size);
ani_info->iart = g_strndup (iart, parasite_size);
gimp_parasite_free (parasite);
}
if (run_mode == GIMP_RUN_INTERACTIVE)
{
if (! ico_save_dialog (image, procedure, config, &info,
header, ani_info))
return GIMP_PDB_CANCEL;
for (i = 1; i < info.num_icons; i++)
{
info.depths[i] = info.depths[0];
info.default_depths[i] = info.default_depths[0];
info.compress[i] = info.compress[0];
}
}
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 GIMP_PDB_EXECUTION_ERROR;
}
/* Writing the .ani header data */
strcpy (id, "RIFF");
size = 0;
fwrite (id, 4, 1, fp);
ofs_size_riff = ftell (fp);
fwrite (&size, sizeof (size), 1, fp);
strcpy (id, "ACON");
fwrite (id, 4, 1, fp);
if ((ani_info->inam && strlen (ani_info->inam) > 0) ||
(ani_info->iart && strlen (ani_info->iart) > 0))
{
gint32 string_size;
strcpy (id, "LIST");
fwrite (id, 4, 1, fp);
ofs_size_info = ftell (fp);
fwrite (&size, sizeof (size), 1, fp);
strcpy (id, "INFO");
fwrite (id, 4, 1, fp);
if (ani_info->inam && strlen (ani_info->inam) > 0) /* Cursor name */
{
strcpy (id, "INAM");
fwrite (id, 4, 1, fp);
string_size = strlen (ani_info->inam) + 1;
fwrite (&string_size, 4, 1, fp);
fwrite (ani_info->inam, string_size, 1, fp);
ofs_metadata += 4;
/* Length of metadata must be even. */
if (string_size % 2 != 0)
fwrite (&padding, sizeof (padding), 1, fp);
}
if (ani_info->iart && strlen (ani_info->iart) > 0) /* Author name */
{
strcpy (id, "IART");
fwrite (id, 4, 1, fp);
string_size = strlen (ani_info->iart) + 1;
fwrite (&string_size, 4, 1, fp);
fwrite (ani_info->iart, string_size, 1, fp);
ofs_metadata += 4;
if (string_size % 2 != 0)
fwrite (&padding, sizeof (padding), 1, fp);
}
/* Go back and update info list size */
fseek (fp, 0L, SEEK_END);
size = ftell (fp) - ofs_size_info - 4;
fseek (fp, ofs_size_info, SEEK_SET);
fwrite (&size, sizeof (size), 1, fp);
fseek (fp, 0L, SEEK_END);
}
strcpy (id, "anih");
size = sizeof (*header);
fwrite (id, 4, 1, fp);
fwrite (&size, sizeof (size), 1, fp);
fwrite (header, sizeof (*header), 1, fp);
strcpy (id, "LIST");
fwrite (id, 4, 1, fp);
ofs_size_list = ftell (fp);
fwrite (&size, sizeof (size), 1, fp);
strcpy (id, "fram");
fwrite (id, 4, 1, fp);
strcpy (id, "icon");
for (i = 0; i < info.num_icons; i++ )
{
GimpPDBStatusType status;
fwrite (id, 4, 1, fp);
ofs_size_icon = ftell (fp);
fwrite (&size, sizeof (size), 1, fp);
offset = ftell (fp);
status = shared_save_image (file, fp, image, procedure,
config, run_mode,
n_hot_spot_x, hot_spot_x, new_hot_spot_x,
n_hot_spot_y, hot_spot_y, new_hot_spot_y,
offset, i, error, &info);
if (status != GIMP_PDB_SUCCESS)
{
ico_save_info_free (&info);
g_free (ani_info->inam);
g_free (ani_info->iart);
fclose (fp);
return GIMP_PDB_EXECUTION_ERROR;
}
fseek (fp, 0L, SEEK_END);
size = ftell (fp) - offset;
fseek (fp, ofs_size_icon, SEEK_SET);
fwrite (&size, sizeof (size), 1, fp);
fseek (fp, 0L, SEEK_END);
gimp_progress_update ((gdouble) i / (gdouble) info.num_icons);
}
ico_save_info_free (&info);
fseek (fp, 0L, SEEK_END);
size = ftell (fp);
size -= ofs_metadata;
fseek (fp, ofs_size_riff, SEEK_SET);
fwrite (&size, sizeof (size), 1, fp);
size -= ofs_size_list;
size += (ofs_metadata - 4);
fseek (fp, ofs_size_list, SEEK_SET);
fwrite (&size, sizeof (size), 1, fp);
fclose (fp);
/* Update metadata if needed */
str = g_strdup_printf ("%d", header->jif_rate);
parasite = gimp_parasite_new ("ani-header",
GIMP_PARASITE_PERSISTENT,
strlen (str) + 1, (gpointer) str);
g_free (str);
gimp_image_attach_parasite (image, parasite);
gimp_parasite_free (parasite);
if (ani_info->inam && strlen (ani_info->inam) > 0)
{
str = g_strdup_printf ("%s", ani_info->inam);
parasite = gimp_parasite_new ("ani-info-inam",
GIMP_PARASITE_PERSISTENT,
strlen (ani_info->inam) + 1, (gpointer) str);
g_free (str);
gimp_image_attach_parasite (image, parasite);
gimp_parasite_free (parasite);
}
if (ani_info->iart && strlen (ani_info->iart) > 0)
{
str = g_strdup_printf ("%s", ani_info->iart);
parasite = gimp_parasite_new ("ani-info-iart",
GIMP_PARASITE_PERSISTENT,
strlen (ani_info->iart) + 1, (gpointer) str);
g_free (str);
gimp_image_attach_parasite (image, parasite);
gimp_parasite_free (parasite);
}
gimp_progress_update (1.0);
return GIMP_PDB_SUCCESS;
}
GimpPDBStatusType
shared_save_image (GFile *file,
FILE *fp_ani,
GimpImage *image,
GimpProcedure *procedure,
GimpProcedureConfig *config,
gint32 run_mode,
gsize *n_hot_spot_x,
const gint32 *hot_spot_x,
gint32 **new_hot_spot_x,
gsize *n_hot_spot_y,
const gint32 *hot_spot_y,
gint32 **new_hot_spot_y,
gint32 file_offset,
gint icon_index,
GError **error,
IcoSaveInfo *info)
{
FILE *fp;
GList *iter;
gint width;
gint height;
IcoFileHeader header;
IcoFileEntry *entries;
gboolean saved;
gint i;
gint num_icons;
GimpParasite *parasite = NULL;
gchar *str;
if (! fp_ani &&
! ico_save_init (image, run_mode, info,
n_hot_spot_x ? *n_hot_spot_x : 0,
hot_spot_x,
n_hot_spot_y ? *n_hot_spot_y : 0,
hot_spot_y,
error))
{
return GIMP_PDB_EXECUTION_ERROR;
}
if (run_mode == GIMP_RUN_INTERACTIVE && ! fp_ani)
{
/* Allow user to override default values */
if (! ico_save_dialog (image, procedure, config, info,
NULL, NULL))
return GIMP_PDB_CANCEL;
}
num_icons = (fp_ani) ? 1 : info->num_icons;
if (! fp_ani)
gimp_progress_init_printf (_("Exporting '%s'"),
gimp_file_get_utf8_name (file));
/* If saving an .ani file, we append the next icon frame. */
if (! fp_ani)
{
fp = g_fopen (g_file_peek_path (file), "wb");
}
else
{
fp = fp_ani;
}
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 GIMP_PDB_EXECUTION_ERROR;
}
header.reserved = 0;
header.resource_type = 1;
if (info->is_cursor)
header.resource_type = 2;
header.icon_count = num_icons;
if (! ico_write_int16 (fp, &header.reserved, 1) ||
! ico_write_int16 (fp, &header.resource_type, 1) ||
! ico_write_int16 (fp, &header.icon_count, 1))
{
ico_save_info_free (info);
fclose (fp);
return GIMP_PDB_EXECUTION_ERROR;
}
entries = g_new0 (IcoFileEntry, num_icons);
if (fwrite (entries, sizeof (IcoFileEntry), num_icons, fp) <= 0)
{
ico_save_info_free (info);
g_free (entries);
fclose (fp);
return GIMP_PDB_EXECUTION_ERROR;
}
for (iter = info->layers, i = 0;
iter;
iter = g_list_next (iter), i++)
{
if (! fp_ani)
gimp_progress_update ((gdouble)i / (gdouble)info->num_icons);
/* If saving .ani file, jump to the correct frame */
if (fp_ani)
iter = g_list_nth (info->layers, icon_index);
width = gimp_drawable_get_width (iter->data);
height = gimp_drawable_get_height (iter->data);
if (width <= 255 && height <= 255)
{
entries[i].width = width;
entries[i].height = height;
}
else
{
entries[i].width = 0;
entries[i].height = 0;
}
if (info->depths[i] <= 8 )
entries[i].num_colors = 1 << info->depths[i];
else
entries[i].num_colors = 0;
entries[i].reserved = 0;
entries[i].planes = 1;
entries[i].bpp = info->depths[i];
/* .cur file reuses these fields for cursor offsets */
if (info->is_cursor)
{
gint hot_spot_index = icon_index ? icon_index : i;
entries[i].planes = info->hot_spot_x[hot_spot_index];
entries[i].bpp = info->hot_spot_y[hot_spot_index];
}
entries[i].offset = ftell (fp) - file_offset;
if (info->compress[i])
saved = ico_write_png (fp, iter->data, info->depths[i]);
else
saved = ico_write_icon (fp, iter->data, info->depths[i]);
if (!saved)
{
ico_save_info_free (info);
fclose (fp);
return GIMP_PDB_EXECUTION_ERROR;
}
entries[i].size = ftell (fp) - file_offset - entries[i].offset;
if (fp_ani)
break;
}
for (i = 0; i < num_icons; i++)
{
entries[i].planes = GUINT16_TO_LE (entries[i].planes);
entries[i].bpp = GUINT16_TO_LE (entries[i].bpp);
entries[i].size = GUINT32_TO_LE (entries[i].size);
entries[i].offset = GUINT32_TO_LE (entries[i].offset);
}
if (fseek (fp, sizeof (IcoFileHeader) + file_offset, SEEK_SET) < 0 ||
fwrite (entries, sizeof (IcoFileEntry), num_icons, fp) <= 0)
{
ico_save_info_free (info);
fclose (fp);
return GIMP_PDB_EXECUTION_ERROR;
}
if (! fp_ani)
gimp_progress_update (1.0);
/* Updating parasite hot spots if needed */
if (info->is_cursor)
{
for (iter = info->layers, i = 0;
iter;
iter = g_list_next (iter), i++)
{
str = g_strdup_printf ("%d %d", info->hot_spot_x[i], info->hot_spot_y[i]);
parasite = gimp_parasite_new ("cur-hot-spot",
GIMP_PARASITE_PERSISTENT,
strlen (str) + 1, (gpointer) str);
g_free (str);
gimp_item_attach_parasite (GIMP_ITEM (iter->data), parasite);
gimp_parasite_free (parasite);
}
}
if (! fp_ani)
{
if (new_hot_spot_x)
{
*new_hot_spot_x = info->hot_spot_x;
info->hot_spot_x = NULL;
*n_hot_spot_x = num_icons;
}
if (new_hot_spot_y)
{
*new_hot_spot_y = info->hot_spot_y;
info->hot_spot_y = NULL;
*n_hot_spot_y = num_icons;
}
}
/* If saving .ani file, don't clear until
* all icons are saved in ani_save_image ()
*/
if (! file_offset)
{
ico_save_info_free (info);
fclose (fp);
}
g_free (entries);
return GIMP_PDB_SUCCESS;
}