/* * pcx.c PIKA plug-in for loading & exporting PCX files * * 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 . */ /* This code is based in parts on code by Francisco Bustamante, but the largest portion of the code has been rewritten and is now maintained occasionally by Nick Lamb njl195@zepler.org.uk */ #include "config.h" #include #include #include #include #include #include "libpika/stdplugins-intl.h" #define LOAD_PROC "file-pcx-load" #define LOAD_PROC_DCX "file-dcx-load" #define SAVE_PROC "file-pcx-save" #define PLUG_IN_BINARY "file-pcx" #define PLUG_IN_ROLE "pika-file-pcx" typedef struct _Pcx Pcx; typedef struct _PcxClass PcxClass; struct _Pcx { PikaPlugIn parent_instance; }; struct _PcxClass { PikaPlugInClass parent_class; }; #define PCX_TYPE (pcx_get_type ()) #define PCX (obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PCX_TYPE, Pcx)) GType pcx_get_type (void) G_GNUC_CONST; static GList * pcx_query_procedures (PikaPlugIn *plug_in); static PikaProcedure * pcx_create_procedure (PikaPlugIn *plug_in, const gchar *name); static PikaValueArray * pcx_load (PikaProcedure *procedure, PikaRunMode run_mode, GFile *file, const PikaValueArray *args, gpointer run_data); static PikaValueArray * dcx_load (PikaProcedure *procedure, PikaRunMode run_mode, GFile *file, const PikaValueArray *args, gpointer run_data); static PikaValueArray * pcx_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, const PikaValueArray *args, gpointer run_data); static PikaImage * load_single (PikaProcedure *procedure, GFile *file, GObject *config, gint run_mode, GError **error); static PikaImage * load_multi (PikaProcedure *procedure, GFile *file, GObject *config, gint run_mode, GError **error); static PikaImage * load_image (PikaProcedure *procedure, FILE *fd, const gchar *filename, GObject *config, gint run_mode, gint image_num, GError **error); static gboolean pcx_load_dialog (PikaProcedure *procedure, GObject *config); static void load_1 (FILE *fp, gint width, gint height, guchar *buf, guint16 bytes); static void load_4 (FILE *fp, gint width, gint height, guchar *buf, guint16 bytes); static void load_sub_8 (FILE *fp, gint width, gint height, gint bpp, gint plane, guchar *buf, guint16 bytes); static void load_8 (FILE *fp, gint width, gint height, guchar *buf, guint16 bytes); static void load_24 (FILE *fp, gint width, gint height, guchar *buf, guint16 bytes, guint8 planes); static void readline (FILE *fp, guchar *buf, gint bytes); static gboolean save_image (GFile *file, PikaImage *image, PikaDrawable *drawable, GError **error); static void save_less_than_8 (FILE *fp, gint width, gint height, const gint bpp, const guchar *buf, gboolean padding); static void save_8 (FILE *fp, gint width, gint height, const guchar *buf, gboolean padding); static void save_24 (FILE *fp, gint width, gint height, const guchar *buf, gboolean padding); static void writeline (FILE *fp, const guchar *buf, gint bytes); G_DEFINE_TYPE (Pcx, pcx, PIKA_TYPE_PLUG_IN) PIKA_MAIN (PCX_TYPE) DEFINE_STD_SET_I18N static void pcx_class_init (PcxClass *klass) { PikaPlugInClass *plug_in_class = PIKA_PLUG_IN_CLASS (klass); plug_in_class->query_procedures = pcx_query_procedures; plug_in_class->create_procedure = pcx_create_procedure; plug_in_class->set_i18n = STD_SET_I18N; } static void pcx_init (Pcx *pcx) { } static GList * pcx_query_procedures (PikaPlugIn *plug_in) { GList *list = NULL; list = g_list_append (list, g_strdup (LOAD_PROC)); list = g_list_append (list, g_strdup (LOAD_PROC_DCX)); list = g_list_append (list, g_strdup (SAVE_PROC)); return list; } static PikaProcedure * pcx_create_procedure (PikaPlugIn *plug_in, const gchar *name) { PikaProcedure *procedure = NULL; if (! strcmp (name, LOAD_PROC)) { procedure = pika_load_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, pcx_load, NULL, NULL); pika_procedure_set_menu_label (procedure, _("ZSoft PCX image")); pika_procedure_set_documentation (procedure, _("Loads files in Zsoft PCX file format"), "FIXME: write help for pcx_load", name); pika_procedure_set_attribution (procedure, "Francisco Bustamante & Nick Lamb", "Nick Lamb ", "January 1997"); PIKA_PROC_ARG_INT (procedure, "override-palette", _("Palette Options"), _("Use built-in palette (0) or override with " "black/white (1)"), 0, 1, 0, G_PARAM_READWRITE); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "image/x-pcx"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "pcx,pcc"); pika_file_procedure_set_magics (PIKA_FILE_PROCEDURE (procedure), "0&,byte,10,2&,byte,1,3&,byte,>0,3,byte,<9"); } else if (! strcmp (name, LOAD_PROC_DCX)) { procedure = pika_load_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, dcx_load, NULL, NULL); pika_procedure_set_menu_label (procedure, _("ZSoft DCX image")); pika_procedure_set_documentation (procedure, _("Loads files in Zsoft DCX file format"), "FIXME: write help for dcx_load", name); pika_procedure_set_attribution (procedure, "Francisco Bustamante, Nick Lamb, Alex S.", "Alex S.", "2023"); PIKA_PROC_ARG_INT (procedure, "override-palette", _("Palette Options"), _("Use built-in palette (0) or override with " "black/white (1)"), 0, 1, 0, G_PARAM_READWRITE); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "image/x-dcx"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "dcx"); pika_file_procedure_set_magics (PIKA_FILE_PROCEDURE (procedure), "0,string,\xB1\x68\xDE\x3A"); } else if (! strcmp (name, SAVE_PROC)) { procedure = pika_save_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, pcx_save, NULL, NULL); pika_procedure_set_image_types (procedure, "INDEXED, RGB, GRAY"); pika_procedure_set_menu_label (procedure, _("ZSoft PCX image")); pika_procedure_set_documentation (procedure, "Exports files in ZSoft PCX file format", "FIXME: write help for pcx_save", name); pika_procedure_set_attribution (procedure, "Francisco Bustamante & Nick Lamb", "Nick Lamb ", "January 1997"); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "image/x-pcx"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "pcx,pcc"); } return procedure; } static PikaValueArray * pcx_load (PikaProcedure *procedure, PikaRunMode run_mode, GFile *file, const PikaValueArray *args, gpointer run_data) { PikaProcedureConfig *config; PikaValueArray *return_vals; PikaImage *image; GError *error = NULL; gegl_init (NULL, NULL); config = pika_procedure_create_config (procedure); pika_procedure_config_begin_run (config, NULL, run_mode, args); image = load_single (procedure, file, G_OBJECT (config), run_mode, &error); if (! image) { pika_procedure_config_end_run (config, PIKA_PDB_CANCEL); g_object_unref (config); return pika_procedure_new_return_values (procedure, PIKA_PDB_EXECUTION_ERROR, error); } pika_procedure_config_end_run (config, PIKA_PDB_SUCCESS); g_object_unref (config); return_vals = pika_procedure_new_return_values (procedure, PIKA_PDB_SUCCESS, NULL); PIKA_VALUES_SET_IMAGE (return_vals, 1, image); return return_vals; } static PikaValueArray * dcx_load (PikaProcedure *procedure, PikaRunMode run_mode, GFile *file, const PikaValueArray *args, gpointer run_data) { PikaProcedureConfig *config; PikaValueArray *return_vals; PikaImage *image; GError *error = NULL; gegl_init (NULL, NULL); config = pika_procedure_create_config (procedure); pika_procedure_config_begin_run (config, NULL, run_mode, args); image = load_multi (procedure, file, G_OBJECT (config), run_mode, &error); if (! image) { pika_procedure_config_end_run (config, PIKA_PDB_CANCEL); g_object_unref (config); return pika_procedure_new_return_values (procedure, PIKA_PDB_EXECUTION_ERROR, error); } pika_procedure_config_end_run (config, PIKA_PDB_SUCCESS); g_object_unref (config); return_vals = pika_procedure_new_return_values (procedure, PIKA_PDB_SUCCESS, NULL); PIKA_VALUES_SET_IMAGE (return_vals, 1, image); return return_vals; } static PikaValueArray * pcx_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, const PikaValueArray *args, gpointer run_data) { PikaPDBStatusType status = PIKA_PDB_SUCCESS; PikaExportReturn export = PIKA_EXPORT_CANCEL; GError *error = NULL; gegl_init (NULL, NULL); switch (run_mode) { case PIKA_RUN_INTERACTIVE: case PIKA_RUN_WITH_LAST_VALS: pika_ui_init (PLUG_IN_BINARY); export = pika_export_image (&image, &n_drawables, &drawables, "PCX", PIKA_EXPORT_CAN_HANDLE_RGB | PIKA_EXPORT_CAN_HANDLE_GRAY | PIKA_EXPORT_CAN_HANDLE_INDEXED); if (export == PIKA_EXPORT_CANCEL) return pika_procedure_new_return_values (procedure, PIKA_PDB_CANCEL, NULL); break; default: break; } if (n_drawables != 1) { g_set_error (&error, G_FILE_ERROR, 0, _("PCX format does not support multiple layers.")); return pika_procedure_new_return_values (procedure, PIKA_PDB_CALLING_ERROR, error); } if (! save_image (file, image, drawables[0], &error)) { status = PIKA_PDB_EXECUTION_ERROR; } if (export == PIKA_EXPORT_EXPORT) { pika_image_delete (image); g_free (drawables); } return pika_procedure_new_return_values (procedure, status, error); } static struct { guint8 manufacturer; guint8 version; guint8 compression; guint8 bpp; guint16 x1, y1; guint16 x2, y2; guint16 hdpi; guint16 vdpi; guint8 colormap[48]; guint8 reserved; guint8 planes; guint16 bytesperline; guint16 color; guint8 filler[58]; } pcx_header; static struct { size_t size; gpointer address; } const pcx_header_buf_xlate[] = { { 1, &pcx_header.manufacturer }, { 1, &pcx_header.version }, { 1, &pcx_header.compression }, { 1, &pcx_header.bpp }, { 2, &pcx_header.x1 }, { 2, &pcx_header.y1 }, { 2, &pcx_header.x2 }, { 2, &pcx_header.y2 }, { 2, &pcx_header.hdpi }, { 2, &pcx_header.vdpi }, { 48, &pcx_header.colormap }, { 1, &pcx_header.reserved }, { 1, &pcx_header.planes }, { 2, &pcx_header.bytesperline }, { 2, &pcx_header.color }, { 58, &pcx_header.filler }, { 0, NULL } }; static void pcx_header_from_buffer (guint8 *buf) { gint i; gint buf_offset = 0; for (i = 0; pcx_header_buf_xlate[i].size != 0; i++) { memmove (pcx_header_buf_xlate[i].address, buf + buf_offset, pcx_header_buf_xlate[i].size); buf_offset += pcx_header_buf_xlate[i].size; } } static void pcx_header_to_buffer (guint8 *buf) { gint i; gint buf_offset = 0; for (i = 0; pcx_header_buf_xlate[i].size != 0; i++) { memmove (buf + buf_offset, pcx_header_buf_xlate[i].address, pcx_header_buf_xlate[i].size); buf_offset += pcx_header_buf_xlate[i].size; } } static PikaImage * load_single (PikaProcedure *procedure, GFile *file, GObject *config, gint run_mode, GError **error) { FILE *fd; PikaImage *image; pika_progress_init_printf (_("Opening '%s'"), pika_file_get_utf8_name (file)); fd = g_fopen (g_file_peek_path (file), "rb"); if (! fd) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not open '%s' for reading: %s"), pika_file_get_utf8_name (file), g_strerror (errno)); return NULL; } image = load_image (procedure, fd, pika_file_get_utf8_name (file), G_OBJECT (config), run_mode, 0, error); fclose (fd); if (! image) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not load PCX image")); return NULL; } pika_progress_update (1.0); return image; } static PikaImage * load_multi (PikaProcedure *procedure, GFile *file, GObject *config, gint run_mode, GError **error) { FILE *fd; PikaImage *image = NULL; PikaImage *temp_image = NULL; gint offset; gint next_id = 8; gint valid_offset; pika_progress_init_printf (_("Opening '%s'"), pika_file_get_utf8_name (file)); fd = g_fopen (g_file_peek_path (file), "rb"); if (! fd) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not open '%s' for reading: %s"), pika_file_get_utf8_name (file), g_strerror (errno)); return NULL; } /* Skip header */ fread (&offset, 1, 4, fd); /* Read the first offset */ fread (&offset, 1, 4, fd); valid_offset = fseek (fd, offset, SEEK_SET); if (valid_offset != 0) { fclose (fd); g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("DCX image offset exceeds the file size")); return NULL; } image = load_image (procedure, fd, pika_file_get_utf8_name (file), G_OBJECT (config), run_mode, 0, error); if (! image) { fclose (fd); g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not load DCX image")); return NULL; } fseek (fd, next_id, SEEK_SET); /* DCX can hold a maximum of 1023 images, * plus a terminal value of 0 */ for (gint i = 1; i < 1023; i++) { PikaLayer **layers; PikaLayer *new_layer; gint n_layers; fread (&offset, 1, 4, fd); if (offset == 0) break; valid_offset = fseek (fd, offset, SEEK_SET); if (valid_offset != 0) { fclose (fd); g_message (_("%s: DCX image offset exceeds the file size: %s\n"), G_STRFUNC, (*error)->message); g_clear_error (error); return image; } temp_image = load_image (procedure, fd, pika_file_get_utf8_name (file), G_OBJECT (config), run_mode, i, error); if (temp_image) { layers = pika_image_get_layers (temp_image, &n_layers); new_layer = pika_layer_new_from_drawable (PIKA_DRAWABLE (layers[0]), image); pika_item_set_name (PIKA_ITEM (new_layer), g_file_get_basename (file)); if (! pika_image_insert_layer (image, new_layer, NULL, 0)) g_message (_("Mixed-mode DCX image not loaded")); g_free (layers); } else { fclose (fd); g_message (_("%s: Could not load all DCX images: %s\n"), G_STRFUNC, (*error)->message); g_clear_error (error); return image; } next_id += 4; fseek (fd, next_id, SEEK_SET); } if (image) fclose (fd); pika_progress_update (1.0); return image; } static PikaImage * load_image (PikaProcedure *procedure, FILE *fd, const gchar *filename, GObject *config, gint run_mode, gint image_num, GError **error) { GeglBuffer *buffer; guint16 offset_x, offset_y, bytesperline; gint32 width, height; guint16 resolution_x, resolution_y; PikaImage *image; PikaLayer *layer; guchar *dest, cmap[768]; guint8 header_buf[128]; gboolean override_palette = FALSE; if (fread (header_buf, 128, 1, fd) == 0) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, _("Could not read header from '%s'"), filename); fclose (fd); return NULL; } pcx_header_from_buffer (header_buf); if (pcx_header.manufacturer != 10) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, _("'%s' is not a PCX file"), filename); fclose (fd); return NULL; } offset_x = GUINT16_FROM_LE (pcx_header.x1); offset_y = GUINT16_FROM_LE (pcx_header.y1); width = GUINT16_FROM_LE (pcx_header.x2) - offset_x + 1; height = GUINT16_FROM_LE (pcx_header.y2) - offset_y + 1; bytesperline = GUINT16_FROM_LE (pcx_header.bytesperline); resolution_x = GUINT16_FROM_LE (pcx_header.hdpi); resolution_y = GUINT16_FROM_LE (pcx_header.vdpi); if ((width <= 0) || (width > PIKA_MAX_IMAGE_SIZE)) { g_message (_("Unsupported or invalid image width: %d"), width); fclose (fd); return NULL; } if ((height <= 0) || (height > PIKA_MAX_IMAGE_SIZE)) { g_message (_("Unsupported or invalid image height: %d"), height); fclose (fd); return NULL; } if ((bytesperline + 1) < ((width * pcx_header.bpp + 7) / 8)) { g_message (_("Invalid number of bytes per line in PCX header")); fclose (fd); return NULL; } if ((resolution_x < 1) || (resolution_x > PIKA_MAX_RESOLUTION) || (resolution_y < 1) || (resolution_y > PIKA_MAX_RESOLUTION)) { g_message (_("Resolution out of bounds in XCX header, using 72x72")); resolution_x = 72; resolution_y = 72; } /* Shield against potential buffer overflows in load_*() functions. */ if (G_MAXSIZE / width / height < 3) { g_message (_("Image dimensions too large: width %d x height %d"), width, height); fclose (fd); return NULL; } if (pcx_header.planes == 3 && pcx_header.bpp == 8) { image = pika_image_new (width, height, PIKA_RGB); layer = pika_layer_new (image, _("Background"), width, height, PIKA_RGB_IMAGE, 100, pika_image_get_default_new_layer_mode (image)); } else if (pcx_header.planes == 4 && pcx_header.bpp == 8) { image = pika_image_new (width, height, PIKA_RGB); layer = pika_layer_new (image, _("Background"), width, height, PIKA_RGBA_IMAGE, 100, pika_image_get_default_new_layer_mode (image)); } else { image = pika_image_new (width, height, PIKA_INDEXED); layer = pika_layer_new (image, _("Background"), width, height, PIKA_INDEXED_IMAGE, 100, pika_image_get_default_new_layer_mode (image)); } pika_image_set_resolution (image, resolution_x, resolution_y); pika_image_insert_layer (image, layer, NULL, 0); pika_layer_set_offsets (layer, offset_x, offset_y); buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (layer)); if (pcx_header.planes == 1 && pcx_header.bpp == 1) { const guint8 *colormap = pcx_header.colormap; dest = g_new (guchar, ((gsize) width) * height); load_1 (fd, width, height, dest, bytesperline); if (run_mode == PIKA_RUN_INTERACTIVE) { g_object_get (config, "override-palette", &override_palette, NULL); /* Only show dialogue once for DCX import */ if (image_num == 0 && pcx_load_dialog (procedure, config)) { g_object_get (config, "override-palette", &override_palette, NULL); } } /* Monochrome does not mean necessarily B&W. Therefore we still * want to check the header palette, even for just 2 colors. * Hopefully the header palette will always be filled with * meaningful colors and the creator software did not just assume * B&W by being monochrome. * Until now test samples showed that even when B&W the header * palette was correctly filled with these 2 colors and we didn't * find counter-examples. * See bug 159947, comment 21 and 23. */ /* ... Actually, there *are* files out there with a zeroed 1-bit palette, * which are supposed to be displayed as B&W (see issue #2997.) These * files *might* be in the wrong (who knows...) but the fact is that * other software, including older versions of PIKA, do display them * "correctly", so let's follow suit: if the two palette colors are * equal (or if the user chooses the option in the load dialog), * use a B&W palette instead. */ if (! memcmp (colormap, colormap + 3, 3) || override_palette) { static const guint8 bw_colormap[6] = { 0, 0, 0, 255, 255, 255}; colormap = bw_colormap; } pika_image_set_colormap (image, colormap, 2); } else if (pcx_header.bpp == 1 && pcx_header.planes == 2) { dest = g_new (guchar, ((gsize) width) * height); load_sub_8 (fd, width, height, 1, 2, dest, bytesperline); pika_image_set_colormap (image, pcx_header.colormap, 4); } else if (pcx_header.bpp == 2 && pcx_header.planes == 1) { dest = g_new (guchar, ((gsize) width) * height); load_sub_8 (fd, width, height, 2, 1, dest, bytesperline); pika_image_set_colormap (image, pcx_header.colormap, 4); } else if (pcx_header.bpp == 1 && pcx_header.planes == 3) { dest = g_new (guchar, ((gsize) width) * height); load_sub_8 (fd, width, height, 1, 3, dest, bytesperline); pika_image_set_colormap (image, pcx_header.colormap, 8); } else if (pcx_header.bpp == 1 && pcx_header.planes == 4) { dest = g_new (guchar, ((gsize) width) * height); load_4 (fd, width, height, dest, bytesperline); pika_image_set_colormap (image, pcx_header.colormap, 16); } else if (pcx_header.bpp == 4 && pcx_header.planes == 1) { dest = g_new (guchar, ((gsize) width) * height); load_sub_8 (fd, width, height, 4, 1, dest, bytesperline); pika_image_set_colormap (image, pcx_header.colormap, 16); } else if (pcx_header.bpp == 8 && pcx_header.planes == 1) { dest = g_new (guchar, ((gsize) width) * height); load_8 (fd, width, height, dest, bytesperline); fseek (fd, -768L, SEEK_END); fread (cmap, 768, 1, fd); pika_image_set_colormap (image, cmap, 256); } else if (pcx_header.bpp == 8 && (pcx_header.planes == 3 || pcx_header.planes == 4)) { dest = g_new (guchar, ((gsize) width) * height * pcx_header.planes); load_24 (fd, width, height, dest, bytesperline, pcx_header.planes); } else { g_message (_("Unusual PCX flavour, giving up")); fclose (fd); return NULL; } gegl_buffer_set (buffer, GEGL_RECTANGLE (0, 0, width, height), 0, NULL, dest, GEGL_AUTO_ROWSTRIDE); g_free (dest); g_object_unref (buffer); return image; } static gboolean pcx_load_dialog (PikaProcedure *procedure, GObject *config) { GtkWidget *dialog; GtkListStore *store; gboolean run; pika_ui_init (PLUG_IN_BINARY); dialog = pika_procedure_dialog_new (procedure, PIKA_PROCEDURE_CONFIG (config), _("Import from PCX")); pika_dialog_set_alternative_button_order (GTK_DIALOG (dialog), GTK_RESPONSE_OK, GTK_RESPONSE_CANCEL, -1); pika_window_set_transient (GTK_WINDOW (dialog)); store = pika_int_store_new (_("Use PCX image's built-in palette"), 0, _("Use black and white palette"), 1, NULL); pika_procedure_dialog_get_int_radio (PIKA_PROCEDURE_DIALOG (dialog), "override-palette", PIKA_INT_STORE (store)); pika_procedure_dialog_fill (PIKA_PROCEDURE_DIALOG (dialog), NULL); gtk_widget_show (dialog); run = pika_procedure_dialog_run (PIKA_PROCEDURE_DIALOG (dialog)); gtk_widget_destroy (dialog); return run; } static void load_8 (FILE *fp, gint width, gint height, guchar *buf, guint16 bytes) { gint row; guchar *line = g_new (guchar, bytes); for (row = 0; row < height; buf += width, ++row) { readline (fp, line, bytes); memcpy (buf, line, width); pika_progress_update ((double) row / (double) height); } g_free (line); } static void load_24 (FILE *fp, gint width, gint height, guchar *buf, guint16 bytes, guint8 planes) { gint x, y, c; guchar *line = g_new (guchar, bytes); for (y = 0; y < height; buf += width * planes, ++y) { for (c = 0; c < planes; ++c) { readline (fp, line, bytes); for (x = 0; x < width; ++x) { buf[x * planes + c] = line[x]; } } pika_progress_update ((double) y / (double) height); } g_free (line); } static void load_1 (FILE *fp, gint width, gint height, guchar *buf, guint16 bytes) { gint x, y; guchar *line = g_new (guchar, bytes); for (y = 0; y < height; buf += width, ++y) { readline (fp, line, bytes); for (x = 0; x < width; ++x) { if (line[x / 8] & (128 >> (x % 8))) buf[x] = 1; else buf[x] = 0; } pika_progress_update ((double) y / (double) height); } g_free (line); } static void load_4 (FILE *fp, gint width, gint height, guchar *buf, guint16 bytes) { gint x, y, c; guchar *line = g_new (guchar, bytes); for (y = 0; y < height; buf += width, ++y) { for (x = 0; x < width; ++x) buf[x] = 0; for (c = 0; c < 4; ++c) { readline(fp, line, bytes); for (x = 0; x < width; ++x) { if (line[x / 8] & (128 >> (x % 8))) buf[x] += (1 << c); } } pika_progress_update ((double) y / (double) height); } g_free (line); } static void load_sub_8 (FILE *fp, gint width, gint height, gint bpp, gint plane, guchar *buf, guint16 bytes) { gint x, y, c, b; guchar *line = g_new (guchar, bytes); gint real_bpp = bpp - 1; gint current_bit = 0; for (y = 0; y < height; buf += width, ++y) { for (x = 0; x < width; ++x) buf[x] = 0; for (c = 0; c < plane; ++c) { readline (fp, line, bytes); for (x = 0; x < width; ++x) { for (b = 0; b < bpp; b++) { current_bit = bpp * x + b; if (line[current_bit / 8] & (128 >> (current_bit % 8))) buf[x] += (1 << (real_bpp - b + c)); } } } pika_progress_update ((double) y / (double) height); } g_free (line); } static void readline (FILE *fp, guchar *buf, gint bytes) { static guchar count = 0, value = 0; if (pcx_header.compression) { while (bytes--) { if (count == 0) { value = fgetc (fp); if (value < 0xc0) { count = 1; } else { count = value - 0xc0; value = fgetc (fp); } } count--; *(buf++) = value; } } else { fread (buf, bytes, 1, fp); } } static gboolean save_image (GFile *file, PikaImage *image, PikaDrawable *drawable, GError **error) { FILE *fp; GeglBuffer *buffer; const Babl *format; PikaImageType drawable_type; guchar *cmap= NULL; guchar *pixels; gint offset_x, offset_y; guint width, height; gdouble resolution_x, resolution_y; gint colors, i; guint8 header_buf[128]; gboolean padding = FALSE; drawable_type = pika_drawable_type (drawable); pika_drawable_get_offsets (drawable, &offset_x, &offset_y); buffer = pika_drawable_get_buffer (drawable); width = gegl_buffer_get_width (buffer); height = gegl_buffer_get_height (buffer); pika_progress_init_printf (_("Exporting '%s'"), pika_file_get_utf8_name (file)); pcx_header.manufacturer = 0x0a; pcx_header.version = 5; pcx_header.compression = 1; switch (drawable_type) { case PIKA_INDEXED_IMAGE: cmap = pika_image_get_colormap (image, NULL, &colors); if (colors > 16) { pcx_header.bpp = 8; pcx_header.planes = 1; pcx_header.bytesperline = width; } else if (colors > 2) { pcx_header.bpp = 4; pcx_header.planes = 1; pcx_header.bytesperline = (width + 1) / 2; } else { pcx_header.bpp = 1; pcx_header.planes = 1; pcx_header.bytesperline = (width + 7) / 8; } pcx_header.color = GUINT16_TO_LE (1); format = NULL; /* Some references explain that 2bpp/1plane and 4bpp/1plane files * would use the palette at EOF (not the one from the header) if * we are in version 5 of PCX. Other sources affirm that even in * version 5, EOF palette must be used only when there are more * than 16 colors. We go with this second assumption. * See bug 159947, comment 21 and 23. */ if (colors <= 16) { for (i = 0; i < (colors * 3); i++) { pcx_header.colormap[i] = cmap[i]; } } break; case PIKA_RGB_IMAGE: pcx_header.bpp = 8; pcx_header.planes = 3; pcx_header.color = GUINT16_TO_LE (1); pcx_header.bytesperline = width; format = babl_format ("R'G'B' u8"); break; case PIKA_GRAY_IMAGE: pcx_header.bpp = 8; pcx_header.planes = 1; pcx_header.color = GUINT16_TO_LE (2); pcx_header.bytesperline = width; format = babl_format ("Y' u8"); break; default: g_message (_("Cannot export images with alpha channel.")); return FALSE; } /* Bytes per Line must be an even number, according to spec */ if (pcx_header.bytesperline % 2 != 0) { pcx_header.bytesperline++; padding = TRUE; } pcx_header.bytesperline = GUINT16_TO_LE (pcx_header.bytesperline); pixels = (guchar *) g_malloc (width * height * pcx_header.planes); gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, width, height), 1.0, format, pixels, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); if ((offset_x < 0) || (offset_x > (1<<16))) { g_message (_("Invalid X offset: %d"), offset_x); return FALSE; } if ((offset_y < 0) || (offset_y > (1<<16))) { g_message (_("Invalid Y offset: %d"), offset_y); return FALSE; } if (offset_x + width - 1 > (1<<16)) { g_message (_("Right border out of bounds (must be < %d): %d"), (1<<16), offset_x + width - 1); return FALSE; } if (offset_y + height - 1 > (1<<16)) { g_message (_("Bottom border out of bounds (must be < %d): %d"), (1<<16), offset_y + height - 1); return FALSE; } 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"), pika_file_get_utf8_name (file), g_strerror (errno)); return FALSE; } pcx_header.x1 = GUINT16_TO_LE ((guint16)offset_x); pcx_header.y1 = GUINT16_TO_LE ((guint16)offset_y); pcx_header.x2 = GUINT16_TO_LE ((guint16)(offset_x + width - 1)); pcx_header.y2 = GUINT16_TO_LE ((guint16)(offset_y + height - 1)); pika_image_get_resolution (image, &resolution_x, &resolution_y); pcx_header.hdpi = GUINT16_TO_LE (RINT (MAX (resolution_x, 1.0))); pcx_header.vdpi = GUINT16_TO_LE (RINT (MAX (resolution_y, 1.0))); pcx_header.reserved = 0; pcx_header_to_buffer (header_buf); fwrite (header_buf, 128, 1, fp); switch (drawable_type) { case PIKA_INDEXED_IMAGE: if (colors > 16) { save_8 (fp, width, height, pixels, padding); fputc (0x0c, fp); fwrite (cmap, colors, 3, fp); for (i = colors; i < 256; i++) { fputc (0, fp); fputc (0, fp); fputc (0, fp); } } else /* Covers 1 and 4 bpp */ { save_less_than_8 (fp, width, height, pcx_header.bpp, pixels, padding); } break; case PIKA_RGB_IMAGE: save_24 (fp, width, height, pixels, padding); break; case PIKA_GRAY_IMAGE: save_8 (fp, width, height, pixels, padding); fputc (0x0c, fp); for (i = 0; i < 256; i++) { fputc ((guchar) i, fp); fputc ((guchar) i, fp); fputc ((guchar) i, fp); } break; default: return FALSE; } g_object_unref (buffer); g_free (pixels); if (fclose (fp) != 0) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Writing to file '%s' failed: %s"), pika_file_get_utf8_name (file), g_strerror (errno)); return FALSE; } return TRUE; } static void save_less_than_8 (FILE *fp, gint width, gint height, const gint bpp, const guchar *buf, gboolean padding) { const gint bit_limit = (8 - bpp); const gint buf_size = width * height; const gint line_end = width - 1; gint j = bit_limit; gint count = 0; guchar byte_to_write = 0x00; guchar *line; gint x; line = (guchar *) g_malloc (((width + 7) / 8) * bpp); for (x = 0; x < buf_size; x++) { byte_to_write |= (buf[x] << j); j -= bpp; if (j < 0 || (x % width == line_end)) { line[count] = byte_to_write; count++; byte_to_write = 0x00; j = bit_limit; if ((x % width == line_end)) { writeline (fp, line, count); count = 0; if (padding) fputc ('\0', fp); pika_progress_update ((double) x / (double) buf_size); } } } g_free (line); } static void save_8 (FILE *fp, gint width, gint height, const guchar *buf, gboolean padding) { int row; for (row = 0; row < height; ++row) { writeline (fp, buf, width); buf += width; if (padding) fputc ('\0', fp); pika_progress_update ((double) row / (double) height); } } static void save_24 (FILE *fp, gint width, gint height, const guchar *buf, gboolean padding) { int x, y, c; guchar *line; line = (guchar *) g_malloc (width); for (y = 0; y < height; ++y) { for (c = 0; c < 3; ++c) { for (x = 0; x < width; ++x) { line[x] = buf[(3*x) + c]; } writeline (fp, line, width); if (padding) fputc ('\0', fp); } buf += width * 3; pika_progress_update ((double) y / (double) height); } g_free (line); } static void writeline (FILE *fp, const guchar *buf, gint bytes) { const guchar *finish = buf + bytes; guchar value; guchar count; while (buf < finish) { value = *(buf++); count = 1; while (buf < finish && count < 63 && *buf == value) { count++; buf++; } if (value < 0xc0 && count == 1) { fputc (value, fp); } else { fputc (0xc0 + count, fp); fputc (value, fp); } } }