summaryrefslogtreecommitdiffstats
path: root/plug-ins/common/file-pdf-save.c
diff options
context:
space:
mode:
Diffstat (limited to 'plug-ins/common/file-pdf-save.c')
-rw-r--r--plug-ins/common/file-pdf-save.c1882
1 files changed, 1882 insertions, 0 deletions
diff --git a/plug-ins/common/file-pdf-save.c b/plug-ins/common/file-pdf-save.c
new file mode 100644
index 0000000..ce7fa8d
--- /dev/null
+++ b/plug-ins/common/file-pdf-save.c
@@ -0,0 +1,1882 @@
+/* GIMP - The GNU Image Manipulation Program
+ *
+ * file-pdf-save.c - PDF file exporter, based on the cairo PDF surface
+ *
+ * Copyright (C) 2010 Barak Itkin <lightningismyname@gmail.com>
+ *
+ * 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/>.
+ */
+
+/* The PDF export plugin has 3 main procedures:
+ * 1. file-pdf-save
+ * This is the main procedure. It has 3 options for optimizations of
+ * the pdf file, and it can show a gui. This procedure works on a single
+ * image.
+ * 2. file-pdf-save-defaults
+ * This procedures is the one that will be invoked by gimp's file-save,
+ * when the pdf extension is chosen. If it's in RUN_INTERACTIVE, it will
+ * pop a user interface with more options, like file-pdf-save. If it's in
+ * RUN_NONINTERACTIVE, it will simply use the default values. Note that on
+ * RUN_WITH_LAST_VALS there will be no gui, however the values will be the
+ * ones that were used in the last interactive run (or the defaults if none
+ * are available.
+ * 3. file-pdf-save-multi
+ * This procedures is more advanced, and it allows the creation of multiple
+ * paged pdf files. It will be located in File/Create/Multiple page PDF...
+ *
+ * It was suggested that file-pdf-save-multi will be removed from the UI as it
+ * does not match the product vision (GIMP isn't a program for editing multiple
+ * paged documents).
+ */
+
+/* Known Issues (except for the coding style issues):
+ * 1. Grayscale layers are inverted (although layer masks which are not grayscale,
+ * are not inverted)
+ * 2. Exporting some fonts doesn't work since gimp_text_layer_get_font Returns a
+ * font which is sometimes incompatiable with pango_font_description_from_string
+ * (gimp_text_layer_get_font sometimes returns suffixes such as "semi-expanded" to
+ * the font's name although the GIMP's font selection dialog shows the don'ts name
+ * normally - This should be checked again in GIMP 2.7)
+ * 3. Indexed layers can't be optimized yet (Since gimp_histogram won't work on
+ * indexed layers)
+ * 4. Rendering the pango layout requires multiplying the size in PANGO_SCALE. This
+ * means I'll need to do some hacking on the markup returned from GIMP.
+ * 5. When accessing the contents of layer groups is supported, we should do use it
+ * (since this plugin should preserve layers).
+ *
+ * Also, there are 2 things which we should warn the user about:
+ * 1. Cairo does not support bitmap masks for text.
+ * 2. Currently layer modes are ignored. We do support layers, including
+ * transparency and opacity, but layer modes are not supported.
+ */
+
+/* Changelog
+ *
+ * April 29, 2009 | Barak Itkin <lightningismyname@gmail.com>
+ * First version of the plugin. This is only a proof of concept and not a full
+ * working plugin.
+ *
+ * May 6, 2009 Barak | Itkin <lightningismyname@gmail.com>
+ * Added new features and several bugfixes:
+ * - Added handling for image resolutions
+ * - fixed the behaviour of getting font sizes
+ * - Added various optimizations (solid rectangles instead of bitmaps, ignoring
+ * invisible layers, etc.) as a macro flag.
+ * - Added handling for layer masks, use CAIRO_FORMAT_A8 for grayscale drawables.
+ * - Indexed layers are now supported
+ *
+ * August 17, 2009 | Barak Itkin <lightningismyname@gmail.com>
+ * Most of the plugin was rewritten from scratch and it now has several new
+ * features:
+ * - Got rid of the optimization macros in the code. The gui now supports
+ * selecting which optimizations to apply.
+ * - Added a procedure to allow the creation of multiple paged PDF's
+ * - Registered the plugin on "<Image>/File/Create/PDF"
+ *
+ * August 21, 2009 | Barak Itkin <lightningismyname@gmail.com>
+ * Fixed a typo that prevented the plugin from compiling...
+ * A migration to the new GIMP 2.8 api, which includes:
+ * - Now using gimp_export_dialog_new
+ * - Using gimp_text_layer_get_hint_style (2.8) instead of the depreceated
+ * gimp_text_layer_get_hinting (2.6).
+ *
+ * August 24, 2010 | Barak Itkin <lightningismyname@gmail.com>
+ * More migrations to the new GIMP 2.8 api:
+ * - Now using the GimpItem api
+ * - Using gimp_text_layer_get_markup where possible
+ * - Fixed some compiler warnings
+ * Also merged the header and c file into one file, Updated some of the comments
+ * and documentation, and moved this into the main source repository.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+
+#include <glib/gstdio.h>
+#include <cairo-pdf.h>
+#include <pango/pangocairo.h>
+
+#include <libgimp/gimp.h>
+#include <libgimp/gimpui.h>
+
+#include "libgimp/stdplugins-intl.h"
+
+
+#define SAVE_PROC "file-pdf-save"
+#define SAVE2_PROC "file-pdf-save2"
+#define SAVE_MULTI_PROC "file-pdf-save-multi"
+#define SAVE_TRANSPARENT_PROC "file-pdf-save-transparent"
+#define SAVE_MULTI_TRANSPARENT_PROC "file-pdf-save-multi-transparent"
+#define PLUG_IN_BINARY "file-pdf-save"
+#define PLUG_IN_ROLE "gimp-file-pdf-save"
+
+#define DATA_OPTIMIZE "file-pdf-data-optimize"
+#define DATA_IMAGE_LIST "file-pdf-data-multi-page"
+
+/* Gimp will crash before you reach this limitation :D */
+#define MAX_PAGE_COUNT 350
+#define MAX_FILE_NAME_LENGTH 350
+
+#define THUMB_WIDTH 90
+#define THUMB_HEIGHT 120
+
+#define GIMP_PLUGIN_PDF_SAVE_ERROR gimp_plugin_pdf_save_error_quark ()
+
+typedef enum
+{
+ GIMP_PLUGIN_PDF_SAVE_ERROR_FAILED
+} GimpPluginPDFError;
+
+GQuark gimp_plugin_pdf_save_error_quark (void);
+
+typedef enum
+{
+ SA_RUN_MODE,
+ SA_IMAGE,
+ SA_DRAWABLE,
+ SA_FILENAME,
+ SA_RAW_FILENAME,
+ SA_VECTORIZE,
+ SA_IGNORE_HIDDEN,
+ SA_APPLY_MASKS,
+ SA_LAYERS_AS_PAGES,
+ SA_REVERSE_ORDER,
+ SA_TRANSPARENT_BACKGROUND,
+ SA_ARG_COUNT
+} SaveArgs;
+
+typedef enum
+{
+ SMA_RUN_MODE,
+ SMA_COUNT,
+ SMA_IMAGES,
+ SMA_VECTORIZE,
+ SMA_IGNORE_HIDDEN,
+ SMA_APPLY_MASKS,
+ SMA_FILENAME,
+ SMA_RAWFILENAME,
+ SMA_TRANSPARENT_BACKGROUND,
+ SMA_ARG_COUNT
+} SaveMultiArgs;
+
+typedef struct
+{
+ gboolean vectorize;
+ gboolean ignore_hidden;
+ gboolean apply_masks;
+ gboolean layers_as_pages;
+ gboolean reverse_order;
+ gboolean transparent_background;
+} PdfOptimize;
+
+typedef struct
+{
+ gint32 images[MAX_PAGE_COUNT];
+ guint32 image_count;
+ gchar file_name[MAX_FILE_NAME_LENGTH];
+} PdfMultiPage;
+
+typedef struct
+{
+ PdfOptimize optimize;
+ GArray *images;
+} PdfMultiVals;
+
+enum
+{
+ THUMB,
+ PAGE_NUMBER,
+ IMAGE_NAME,
+ IMAGE_ID
+};
+
+typedef struct
+{
+ GdkPixbuf *thumb;
+ gint32 page_number;
+ gchar *image_name;
+} Page;
+
+
+static void query (void);
+
+static void run (const gchar *name,
+ gint nparams,
+ const GimpParam *param,
+ gint *nreturn_vals,
+ GimpParam **return_vals);
+
+static gboolean init_vals (const gchar *name,
+ gint nparams,
+ const GimpParam *param,
+ gboolean *single,
+ gboolean *defaults,
+ GimpRunMode *run_mode);
+
+static void init_image_list_defaults (gint32 image);
+
+static void validate_image_list (void);
+
+static gboolean gui_single (void);
+
+static gboolean gui_multi (void);
+
+static void reverse_order_toggled (GtkToggleButton *reverse_order,
+ GtkButton *layers_as_pages);
+
+static void choose_file_call (GtkWidget *browse_button,
+ gpointer file_entry);
+
+static gboolean get_image_list (void);
+
+static GtkTreeModel * create_model (void);
+
+static void add_image_call (GtkWidget *widget,
+ gpointer img_combo);
+static void del_image_call (GtkWidget *widget,
+ gpointer icon_view);
+static void remove_call (GtkTreeModel *tree_model,
+ GtkTreePath *path,
+ gpointer user_data);
+static void recount_pages (void);
+
+static cairo_surface_t * get_cairo_surface (gint32 drawable_ID,
+ gboolean as_mask,
+ GError **error);
+
+static GimpRGB get_layer_color (gint32 layer_ID,
+ gboolean *single);
+
+static void drawText (gint32 text_id,
+ gdouble opacity,
+ cairo_t *cr,
+ gdouble x_res,
+ gdouble y_res);
+
+static gboolean draw_layer (gint32 *layers,
+ gint n_layers,
+ gint j,
+ cairo_t *cr,
+ gdouble x_res,
+ gdouble y_res,
+ const gchar *name,
+ GError **error);
+
+static gboolean dnd_remove = TRUE;
+static PdfMultiPage multi_page;
+
+static PdfOptimize optimize =
+{
+ TRUE, /* vectorize */
+ TRUE, /* ignore_hidden */
+ TRUE, /* apply_masks */
+ FALSE, /* layers_as_pages */
+ FALSE, /* reverse_order */
+ TRUE /* transparent_background */
+};
+
+static GtkTreeModel *model;
+static GtkWidget *file_choose;
+static gchar *file_name;
+
+GimpPlugInInfo PLUG_IN_INFO =
+{
+ NULL,
+ NULL,
+ query,
+ run
+};
+
+G_DEFINE_QUARK (gimp-plugin-pdf-save-error-quark, gimp_plugin_pdf_save_error)
+
+MAIN()
+
+static void
+query (void)
+{
+ static GimpParamDef save_args[] =
+ {
+ { GIMP_PDB_INT32, "run-mode", "Run mode" },
+ { GIMP_PDB_IMAGE, "image", "Input image" },
+ { GIMP_PDB_DRAWABLE, "drawable", "Input drawable" },
+ { GIMP_PDB_STRING, "filename", "The name of the file to save the image in" },
+ { GIMP_PDB_STRING, "raw-filename", "The name of the file to save the image in" },
+ { GIMP_PDB_INT32, "vectorize", "Convert bitmaps to vector graphics where possible. TRUE or FALSE" },
+ { GIMP_PDB_INT32, "ignore-hidden", "Omit hidden layers and layers with zero opacity. TRUE or FALSE" },
+ { GIMP_PDB_INT32, "apply-masks", "Apply layer masks before saving. TRUE or FALSE (Keeping them will not change the output)" }
+ };
+
+ static GimpParamDef save2_args[] =
+ {
+ { GIMP_PDB_INT32, "run-mode", "Run mode" },
+ { GIMP_PDB_IMAGE, "image", "Input image" },
+ { GIMP_PDB_DRAWABLE, "drawable", "Input drawable" },
+ { GIMP_PDB_STRING, "filename", "The name of the file to save the image in" },
+ { GIMP_PDB_STRING, "raw-filename", "The name of the file to save the image in" },
+ { GIMP_PDB_INT32, "vectorize", "Convert bitmaps to vector graphics where possible. TRUE or FALSE" },
+ { GIMP_PDB_INT32, "ignore-hidden", "Omit hidden layers and layers with zero opacity. TRUE or FALSE" },
+ { GIMP_PDB_INT32, "apply-masks", "Apply layer masks before saving. TRUE or FALSE (Keeping them will not change the output)" },
+ { GIMP_PDB_INT32, "layers-as-pages", "Layers as pages (bottom layers first). TRUE or FALSE" },
+ { GIMP_PDB_INT32, "reverse-order", "Reverse the pages order (top layers first). TRUE or FALSE" }
+ };
+
+ static GimpParamDef save_transparent_args[] =
+ {
+ { GIMP_PDB_INT32, "run-mode", "Run mode" },
+ { GIMP_PDB_IMAGE, "image", "Input image" },
+ { GIMP_PDB_DRAWABLE, "drawable", "Input drawable" },
+ { GIMP_PDB_STRING, "filename", "The name of the file to save the image in" },
+ { GIMP_PDB_STRING, "raw-filename", "The name of the file to save the image in" },
+ { GIMP_PDB_INT32, "vectorize", "Convert bitmaps to vector graphics where possible. TRUE or FALSE" },
+ { GIMP_PDB_INT32, "ignore-hidden", "Omit hidden layers and layers with zero opacity. TRUE or FALSE" },
+ { GIMP_PDB_INT32, "apply-masks", "Apply layer masks before saving. TRUE or FALSE (Keeping them will not change the output)" },
+ { GIMP_PDB_INT32, "layers-as-pages", "Layers as pages (bottom layers first). TRUE or FALSE" },
+ { GIMP_PDB_INT32, "fill-background-color", "Fill transparent areas with background color if layer has an alpha channel. TRUE or FALSE" }
+ };
+
+ static GimpParamDef save_multi_args[] =
+ {
+ { GIMP_PDB_INT32, "run-mode", "Run mode" },
+ { GIMP_PDB_INT32, "count", "The amount of images entered (This will be the amount of pages). 1 <= count <= MAX_PAGE_COUNT" },
+ { GIMP_PDB_INT32ARRAY, "images", "Input image for each page (An image can appear more than once)" },
+ { GIMP_PDB_INT32, "vectorize", "Convert bitmaps to vector graphics where possible. TRUE or FALSE" },
+ { GIMP_PDB_INT32, "ignore-hidden", "Omit hidden layers and layers with zero opacity. TRUE or FALSE" },
+ { GIMP_PDB_INT32, "apply-masks", "Apply layer masks before saving. TRUE or FALSE (Keeping them will not change the output)" },
+ { GIMP_PDB_STRING, "filename", "The name of the file to save the image in" },
+ { GIMP_PDB_STRING, "raw-filename", "The name of the file to save the image in" }
+ };
+
+ static GimpParamDef save_multi_transparent_args[] =
+ {
+ { GIMP_PDB_INT32, "run-mode", "Run mode" },
+ { GIMP_PDB_INT32, "count", "The amount of images entered (This will be the amount of pages). 1 <= count <= MAX_PAGE_COUNT" },
+ { GIMP_PDB_INT32ARRAY, "images", "Input image for each page (An image can appear more than once)" },
+ { GIMP_PDB_INT32, "vectorize", "Convert bitmaps to vector graphics where possible. TRUE or FALSE" },
+ { GIMP_PDB_INT32, "ignore-hidden", "Omit hidden layers and layers with zero opacity. TRUE or FALSE" },
+ { GIMP_PDB_INT32, "apply-masks", "Apply layer masks before saving. TRUE or FALSE (Keeping them will not change the output)" },
+ { GIMP_PDB_STRING, "filename", "The name of the file to save the image in" },
+ { GIMP_PDB_STRING, "raw-filename", "The name of the file to save the image in" },
+ { GIMP_PDB_INT32, "fill-background-color", "Fill transparent areas with background color if layer has an alpha channel. TRUE or FALSE" }
+ };
+
+ gimp_install_procedure (SAVE_PROC,
+ "Save files in PDF format",
+ "Saves files in Adobe's Portable Document Format. "
+ "PDF is designed to be easily processed by a variety "
+ "of different platforms, and is a distant cousin of "
+ "PostScript.",
+ "Barak Itkin",
+ "Copyright Barak Itkin",
+ "August 2009",
+ N_("Portable Document Format"),
+ "RGB*, GRAY*, INDEXED*",
+ GIMP_PLUGIN,
+ G_N_ELEMENTS (save_args), 0,
+ save_args, NULL);
+
+ gimp_install_procedure (SAVE2_PROC,
+ "Save files in PDF format",
+ "Saves files in Adobe's Portable Document Format. "
+ "PDF is designed to be easily processed by a variety "
+ "of different platforms, and is a distant cousin of "
+ "PostScript.\n"
+ "This procedure adds an extra parameter to "
+ "file-pdf-save to save layers as pages.",
+ "Barak Itkin, Lionel N., Jehan",
+ "Copyright Barak Itkin, Lionel N., Jehan",
+ "August 2009, 2017",
+ N_("Portable Document Format"),
+ "RGB*, GRAY*, INDEXED*",
+ GIMP_PLUGIN,
+ G_N_ELEMENTS (save2_args), 0,
+ save2_args, NULL);
+
+ gimp_install_procedure (SAVE_TRANSPARENT_PROC,
+ "Save files in PDF format",
+ "Saves files in Adobe's Portable Document Format. "
+ "PDF is designed to be easily processed by a variety "
+ "of different platforms, and is a distant cousin of "
+ "PostScript.\n"
+ "This procedure adds an extra parameter to "
+ "file-pdf-save2 to optionally fill transparent "
+ "areas with the background color",
+ "Barak Itkin, Lionel N., Jehan",
+ "Copyright Barak Itkin, Lionel N., Jehan",
+ "August 2009, 2017",
+ N_("Portable Document Format"),
+ "RGB*, GRAY*, INDEXED*",
+ GIMP_PLUGIN,
+ G_N_ELEMENTS (save_transparent_args), 0,
+ save_transparent_args, NULL);
+
+ gimp_install_procedure (SAVE_MULTI_PROC,
+ "Save files in PDF format",
+ "Saves files in Adobe's Portable Document Format. "
+ "PDF is designed to be easily processed by a variety "
+ "of different platforms, and is a distant cousin of "
+ "PostScript.",
+ "Barak Itkin",
+ "Copyright Barak Itkin",
+ "August 2009",
+ N_("_Create multipage PDF..."),
+ "RGB*, GRAY*, INDEXED*",
+ GIMP_PLUGIN,
+ G_N_ELEMENTS (save_multi_args), 0,
+ save_multi_args, NULL);
+
+ gimp_install_procedure (SAVE_MULTI_TRANSPARENT_PROC,
+ "Save files in PDF format",
+ "Saves files in Adobe's Portable Document Format. "
+ "PDF is designed to be easily processed by a variety "
+ "of different platforms, and is a distant cousin of "
+ "PostScript.\n"
+ "This procedure adds an extra parameter to "
+ "file-pdf-multi to optionally fill transparent "
+ "areas with the background color",
+ "Barak Itkin",
+ "Copyright Barak Itkin",
+ "August 2009",
+ N_("_Create multipage PDF..."),
+ "RGB*, GRAY*, INDEXED*",
+ GIMP_PLUGIN,
+ G_N_ELEMENTS (save_multi_transparent_args), 0,
+ save_multi_transparent_args, NULL);
+
+
+#if 0
+ gimp_plugin_menu_register (SAVE_MULTI_PROC,
+ "<Image>/File/Create/PDF");
+#endif
+
+ gimp_register_file_handler_mime (SAVE2_PROC, "application/pdf");
+ gimp_register_save_handler (SAVE2_PROC, "pdf", "");
+}
+
+static cairo_status_t
+write_func (void *fp,
+ const unsigned char *data,
+ unsigned int size)
+{
+ return fwrite (data, 1, size, fp) == size ? CAIRO_STATUS_SUCCESS
+ : CAIRO_STATUS_WRITE_ERROR;
+}
+
+static void
+run (const gchar *name,
+ gint nparams,
+ const GimpParam *param,
+ gint *nreturn_vals,
+ GimpParam **return_vals)
+{
+ static GimpParam values[2];
+ GimpPDBStatusType status = GIMP_PDB_SUCCESS;
+ GimpRunMode run_mode;
+ gboolean single_image;
+ gboolean defaults_proc;
+ cairo_surface_t *pdf_file;
+ cairo_t *cr;
+ GimpExportCapabilities capabilities;
+ FILE *fp;
+ gint i;
+ GError *error = NULL;
+
+ INIT_I18N ();
+ gegl_init (NULL, NULL);
+
+ /* Setting mandatory output values */
+ *nreturn_vals = 1;
+ *return_vals = values;
+
+ values[0].type = GIMP_PDB_STATUS;
+ values[0].data.d_status = status;
+
+ /* Initializing all the settings */
+ multi_page.image_count = 0;
+
+ if (! init_vals (name, nparams, param, &single_image,
+ &defaults_proc, &run_mode))
+ {
+ values[0].data.d_status = GIMP_PDB_CALLING_ERROR;
+ return;
+ }
+
+ /* Starting the executions */
+ if (run_mode == GIMP_RUN_INTERACTIVE)
+ {
+ if (single_image)
+ {
+ if (! gui_single ())
+ {
+ values[0].data.d_status = GIMP_PDB_CANCEL;
+ return;
+ }
+ }
+ else if (! gui_multi ())
+ {
+ values[0].data.d_status = GIMP_PDB_CANCEL;
+ return;
+ }
+
+ if (file_name == NULL)
+ {
+ values[0].data.d_status = GIMP_PDB_CALLING_ERROR;
+ gimp_message (_("You must select a file to save!"));
+ return;
+ }
+ }
+
+ fp = g_fopen (file_name, "wb");
+ if (fp == NULL)
+ {
+ *nreturn_vals = 2;
+
+ values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR;
+ values[1].type = GIMP_PDB_STRING;
+ if (error == NULL)
+ {
+ g_set_error (&error, G_FILE_ERROR, g_file_error_from_errno (errno),
+ _("Could not open '%s' for writing: %s"),
+ gimp_filename_to_utf8 (file_name), g_strerror (errno));
+ }
+ values[1].data.d_string = error->message;
+ return;
+ }
+
+ pdf_file = cairo_pdf_surface_create_for_stream (write_func, fp, 1, 1);
+
+ if (cairo_surface_status (pdf_file) != CAIRO_STATUS_SUCCESS)
+ {
+ g_message (_("An error occurred while creating the PDF file:\n"
+ "%s\n"
+ "Make sure you entered a valid filename and that the "
+ "selected location isn't read only!"),
+ cairo_status_to_string (cairo_surface_status (pdf_file)));
+
+ values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR;
+ return;
+ }
+
+ cr = cairo_create (pdf_file);
+
+ capabilities = (GIMP_EXPORT_CAN_HANDLE_RGB |
+ GIMP_EXPORT_CAN_HANDLE_ALPHA |
+ GIMP_EXPORT_CAN_HANDLE_GRAY |
+ GIMP_EXPORT_CAN_HANDLE_LAYERS |
+ GIMP_EXPORT_CAN_HANDLE_INDEXED);
+ /* This seems counter-intuitive, but not setting the mask capability
+ * will apply any layer mask upon gimp_export_image().
+ */
+ if (! optimize.apply_masks)
+ capabilities |= GIMP_EXPORT_CAN_HANDLE_LAYER_MASKS;
+
+ for (i = 0; i < multi_page.image_count; i++)
+ {
+ gint32 image_ID = multi_page.images[i];
+ gint32 *layers;
+ gint32 n_layers;
+ gdouble x_res, y_res;
+ gdouble x_scale, y_scale;
+ gint32 temp;
+ gint j;
+
+ temp = gimp_image_get_active_drawable (image_ID);
+ if (temp < 1)
+ continue;
+
+ /* Save the state of the surface before any changes, so that
+ * settings from one page won't affect all the others
+ */
+ cairo_save (cr);
+
+ if (! (gimp_export_image (&image_ID, &temp, NULL,
+ capabilities) == GIMP_EXPORT_EXPORT))
+ {
+ /* gimp_drawable_histogram() only works within the bounds of
+ * the selection, which is a problem (see issue #2431).
+ * Instead of saving the selection, unselecting to later
+ * reselect, let's just always work on a duplicate of the
+ * image.
+ */
+ image_ID = gimp_image_duplicate (image_ID);
+ }
+ gimp_selection_none (image_ID);
+
+ gimp_image_get_resolution (image_ID, &x_res, &y_res);
+ x_scale = 72.0 / x_res;
+ y_scale = 72.0 / y_res;
+
+ cairo_pdf_surface_set_size (pdf_file,
+ gimp_image_width (image_ID) * x_scale,
+ gimp_image_height (image_ID) * y_scale);
+
+ /* This way we set how many pixels are there in every inch.
+ * It's very important for PangoCairo
+ */
+ cairo_surface_set_fallback_resolution (pdf_file, x_res, y_res);
+
+ /* Cairo has a concept of user-space vs device-space units.
+ * From what I understand, by default the user-space unit is the
+ * typographical "point". Since we work mostly with pixels, not
+ * points, the following call simply scales the transformation
+ * matrix from points to pixels, relatively to the image
+ * resolution, knowing that 1 typographical point == 1/72 inch.
+ */
+ cairo_scale (cr, x_scale, y_scale);
+
+ layers = gimp_image_get_layers (image_ID, &n_layers);
+
+ /* Fill image with background color if transparent and
+ * user chose that option.
+ */
+ if (gimp_drawable_has_alpha (layers[n_layers - 1]) &&
+ optimize.transparent_background)
+ {
+ GimpRGB color;
+
+ cairo_rectangle (cr, 0.0, 0.0,
+ gimp_image_width (image_ID),
+ gimp_image_height (image_ID));
+ gimp_context_get_background (&color);
+ cairo_set_source_rgb (cr,
+ color.r,
+ color.g,
+ color.b);
+ cairo_fill (cr);
+ }
+
+ /* Now, we should loop over the layers of each image */
+ for (j = 0; j < n_layers; j++)
+ {
+ if (! draw_layer (layers, n_layers, j, cr, x_res, y_res, name, &error))
+ {
+ *nreturn_vals = 2;
+
+ /* free the resources */
+ g_free (layers);
+ cairo_surface_destroy (pdf_file);
+ cairo_destroy (cr);
+ fclose (fp);
+
+ values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR;
+
+ values[1].type = GIMP_PDB_STRING;
+ values[1].data.d_string = error->message;
+ return;
+ }
+ }
+ g_free (layers);
+
+ /* We are done with this image - Show it!
+ * Unless that's a multi-page to avoid blank page at the end
+ */
+ if (g_strcmp0 (name, SAVE2_PROC) != 0 ||
+ ! optimize.layers_as_pages)
+ cairo_show_page (cr);
+ cairo_restore (cr);
+
+ gimp_image_delete (image_ID);
+ }
+
+ /* We are done with all the images - time to free the resources */
+ cairo_surface_destroy (pdf_file);
+ cairo_destroy (cr);
+
+ fclose (fp);
+
+ /* Finally done, let's save the parameters */
+ gimp_set_data (DATA_OPTIMIZE, &optimize, sizeof (optimize));
+
+ if (! single_image)
+ {
+ g_strlcpy (multi_page.file_name, file_name, MAX_FILE_NAME_LENGTH);
+ gimp_set_data (DATA_IMAGE_LIST, &multi_page, sizeof (multi_page));
+ }
+}
+
+/******************************************************/
+/* Beginning of parameter handling functions */
+/******************************************************/
+
+/* A function that takes care of loading the basic parameters
+ */
+static gboolean
+init_vals (const gchar *name,
+ gint nparams,
+ const GimpParam *param,
+ gboolean *single_image,
+ gboolean *defaults_proc,
+ GimpRunMode *run_mode)
+{
+ gboolean had_saved_list = FALSE;
+ gboolean single;
+ gboolean defaults = FALSE;
+ gint32 i;
+ gint32 image;
+
+ if ((g_str_equal (name, SAVE_PROC) && nparams == SA_ARG_COUNT - 3) ||
+ (g_str_equal (name, SAVE2_PROC) && nparams == SA_ARG_COUNT - 1) ||
+ (g_str_equal (name, SAVE_TRANSPARENT_PROC) && nparams == SA_ARG_COUNT))
+ {
+ single = TRUE;
+ *run_mode = param[SA_RUN_MODE].data.d_int32;
+ image = param[SA_IMAGE].data.d_int32;
+ file_name = param[SA_FILENAME].data.d_string;
+
+ if (*run_mode == GIMP_RUN_NONINTERACTIVE)
+ {
+ optimize.apply_masks = param[SA_APPLY_MASKS].data.d_int32;
+ optimize.vectorize = param[SA_VECTORIZE].data.d_int32;
+ optimize.ignore_hidden = param[SA_IGNORE_HIDDEN].data.d_int32;
+ if (nparams >= SA_ARG_COUNT - 1)
+ {
+ optimize.layers_as_pages = param[SA_LAYERS_AS_PAGES].data.d_int32;
+ optimize.reverse_order = param[SA_REVERSE_ORDER].data.d_int32;
+ }
+ if (nparams == SA_ARG_COUNT)
+ optimize.transparent_background = param[SA_TRANSPARENT_BACKGROUND].data.d_int32;
+ }
+ else
+ defaults = TRUE;
+ }
+ else if ((g_str_equal (name, SAVE_MULTI_PROC) && nparams == SMA_ARG_COUNT - 1) ||
+ (g_str_equal (name, SAVE_MULTI_TRANSPARENT_PROC) && nparams == SMA_ARG_COUNT))
+ {
+ single = FALSE;
+
+ *run_mode = param[SMA_RUN_MODE].data.d_int32;
+ image = -1;
+ file_name = param[SMA_FILENAME].data.d_string;
+
+ optimize.apply_masks = param[SMA_APPLY_MASKS].data.d_int32;
+ optimize.vectorize = param[SMA_VECTORIZE].data.d_int32;
+ optimize.ignore_hidden = param[SMA_IGNORE_HIDDEN].data.d_int32;
+
+ if (nparams == SA_ARG_COUNT)
+ optimize.transparent_background = param[SA_TRANSPARENT_BACKGROUND].data.d_int32;
+ }
+ else
+ {
+ return FALSE;
+ }
+
+ switch (*run_mode)
+ {
+ case GIMP_RUN_NONINTERACTIVE:
+ if (single)
+ {
+ init_image_list_defaults (image);
+ }
+ else
+ {
+ multi_page.image_count = param[SMA_COUNT].data.d_int32;
+ if (param[SMA_IMAGES].data.d_int32array != NULL)
+ for (i = 0; i < param[SMA_COUNT].data.d_int32; i++)
+ multi_page.images[i] = param[SMA_IMAGES].data.d_int32array[i];
+ }
+ break;
+
+ case GIMP_RUN_INTERACTIVE:
+ /* Possibly retrieve data */
+ gimp_get_data (DATA_OPTIMIZE, &optimize);
+ had_saved_list = gimp_get_data (DATA_IMAGE_LIST, &multi_page);
+
+ if (had_saved_list && (file_name == NULL || strlen (file_name) == 0))
+ {
+ file_name = multi_page.file_name;
+ }
+
+ if (single || ! had_saved_list )
+ init_image_list_defaults (image);
+ break;
+
+ case GIMP_RUN_WITH_LAST_VALS:
+ /* Possibly retrieve data */
+ if (! single)
+ {
+ had_saved_list = gimp_get_data (DATA_IMAGE_LIST, &multi_page);
+ if (had_saved_list)
+ {
+ file_name = multi_page.file_name;
+ }
+ }
+ else
+ {
+ init_image_list_defaults (image);
+ }
+ gimp_get_data (DATA_OPTIMIZE, &optimize);
+ break;
+ }
+
+ *defaults_proc = defaults;
+ *single_image = single;
+
+ validate_image_list ();
+
+ return TRUE;
+}
+
+/* A function that initializes the image list to default values */
+static void
+init_image_list_defaults (gint32 image)
+{
+ if (image != -1)
+ {
+ multi_page.images[0] = image;
+ multi_page.image_count = 1;
+ }
+ else
+ {
+ multi_page.image_count = 0;
+ }
+}
+
+/* A function that removes images that are no longer valid from the
+ * image list
+ */
+static void
+validate_image_list (void)
+{
+ gint32 valid = 0;
+ guint32 i = 0;
+
+ for (i = 0 ; i < MAX_PAGE_COUNT && i < multi_page.image_count ; i++)
+ {
+ if (gimp_image_is_valid (multi_page.images[i]))
+ {
+ multi_page.images[valid] = multi_page.images[i];
+ valid++;
+ }
+ }
+
+ multi_page.image_count = valid;
+}
+
+
+/******************************************************/
+/* Beginning of GUI functions */
+/******************************************************/
+
+/* The main GUI function for saving single-paged PDFs */
+
+static gboolean
+gui_single (void)
+{
+ GtkWidget *window;
+ GtkWidget *vbox;
+ GtkWidget *vectorize_c;
+ GtkWidget *ignore_hidden_c;
+ GtkWidget *apply_c;
+ GtkWidget *layers_as_pages_c;
+ GtkWidget *reverse_order_c;
+ GtkWidget *fill_background_c;
+ GtkWidget *frame;
+ gchar *text;
+ gboolean run;
+ gint32 n_layers;
+
+ gimp_ui_init (PLUG_IN_BINARY, FALSE);
+
+ window = gimp_export_dialog_new ("PDF", PLUG_IN_ROLE, SAVE_PROC);
+
+ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
+ gtk_box_pack_start (GTK_BOX (gimp_export_dialog_get_content_area (window)),
+ vbox, TRUE, TRUE, 0);
+
+ gtk_container_set_border_width (GTK_CONTAINER (window), 12);
+
+ fill_background_c = gtk_check_button_new_with_mnemonic (_("_Fill transparent areas with background color"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (fill_background_c),
+ optimize.transparent_background);
+ gtk_box_pack_end (GTK_BOX (vbox), fill_background_c, TRUE, TRUE, 0);
+
+ ignore_hidden_c = gtk_check_button_new_with_mnemonic (_("_Omit hidden layers and layers with zero opacity"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ignore_hidden_c),
+ optimize.ignore_hidden);
+ gtk_box_pack_end (GTK_BOX (vbox), ignore_hidden_c, TRUE, TRUE, 0);
+
+ vectorize_c = gtk_check_button_new_with_mnemonic (_("Convert _bitmaps to vector graphics where possible"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (vectorize_c),
+ optimize.vectorize);
+ gtk_box_pack_end (GTK_BOX (vbox), vectorize_c, TRUE, TRUE, 0);
+
+ apply_c = gtk_check_button_new_with_mnemonic (_("_Apply layer masks before saving"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (apply_c),
+ optimize.apply_masks);
+ gtk_box_pack_end (GTK_BOX (vbox), apply_c, TRUE, TRUE, 0);
+ gimp_help_set_help_data (apply_c, _("Keeping the masks will not change the output"), NULL);
+
+ /* Frame for multi-page from layers. */
+ frame = gtk_frame_new (NULL);
+ gtk_box_pack_end (GTK_BOX (vbox), frame, TRUE, TRUE, 0);
+
+ text = g_strdup_printf (_("_Layers as pages (%s)"),
+ optimize.reverse_order ?
+ _("top layers first") : _("bottom layers first"));
+ layers_as_pages_c = gtk_check_button_new_with_mnemonic (text);
+ g_free (text);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (layers_as_pages_c),
+ optimize.layers_as_pages);
+ gtk_frame_set_label_widget (GTK_FRAME (frame), layers_as_pages_c);
+ g_free (gimp_image_get_layers (multi_page.images[0], &n_layers));
+
+ reverse_order_c = gtk_check_button_new_with_mnemonic (_("_Reverse the pages order"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (reverse_order_c),
+ optimize.reverse_order);
+ gtk_container_add (GTK_CONTAINER (frame), reverse_order_c);
+
+ if (n_layers <= 1)
+ {
+ gtk_widget_set_sensitive (layers_as_pages_c, FALSE);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (layers_as_pages_c),
+ FALSE);
+ }
+
+ g_object_bind_property (layers_as_pages_c, "active",
+ reverse_order_c, "sensitive",
+ G_BINDING_SYNC_CREATE);
+ g_signal_connect (G_OBJECT (reverse_order_c), "toggled",
+ G_CALLBACK (reverse_order_toggled),
+ layers_as_pages_c);
+
+ gtk_widget_show_all (window);
+
+ run = gtk_dialog_run (GTK_DIALOG (window)) == GTK_RESPONSE_OK;
+
+ optimize.transparent_background =
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (fill_background_c));
+ optimize.ignore_hidden =
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ignore_hidden_c));
+ optimize.vectorize =
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (vectorize_c));
+ optimize.apply_masks =
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (apply_c));
+ optimize.layers_as_pages =
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (layers_as_pages_c));
+ optimize.reverse_order =
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (reverse_order_c));
+
+ gtk_widget_destroy (window);
+
+ return run;
+}
+
+/* The main GUI function for saving multi-paged PDFs */
+
+static gboolean
+gui_multi (void)
+{
+ GtkWidget *window;
+ GtkWidget *vbox;
+ GtkWidget *file_label;
+ GtkWidget *file_entry;
+ GtkWidget *file_browse;
+ GtkWidget *file_hbox;
+ GtkWidget *vectorize_c;
+ GtkWidget *ignore_hidden_c;
+ GtkWidget *fill_background_c;
+ GtkWidget *apply_c;
+ GtkWidget *scroll;
+ GtkWidget *page_view;
+ GtkWidget *h_but_box;
+ GtkWidget *del;
+ GtkWidget *h_box;
+ GtkWidget *img_combo;
+ GtkWidget *add_image;
+ gboolean run;
+ const gchar *temp;
+
+ gimp_ui_init (PLUG_IN_BINARY, FALSE);
+
+ window = gimp_export_dialog_new ("PDF", PLUG_IN_ROLE, SAVE_MULTI_PROC);
+
+ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
+ gtk_box_pack_start (GTK_BOX (gimp_export_dialog_get_content_area (window)),
+ vbox, TRUE, TRUE, 0);
+
+ gtk_container_set_border_width (GTK_CONTAINER (window), 12);
+
+ file_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
+ file_label = gtk_label_new (_("Save to:"));
+ file_entry = gtk_entry_new ();
+ if (file_name != NULL)
+ gtk_entry_set_text (GTK_ENTRY (file_entry), file_name);
+ file_browse = gtk_button_new_with_label (_("Browse..."));
+ file_choose = gtk_file_chooser_dialog_new (_("Multipage PDF export"),
+ GTK_WINDOW (window),
+ GTK_FILE_CHOOSER_ACTION_SAVE,
+ _("_Save"), GTK_RESPONSE_OK,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ NULL);
+
+ gtk_box_pack_start (GTK_BOX (file_hbox), file_label, FALSE, FALSE, 0);
+ gtk_box_pack_start (GTK_BOX (file_hbox), file_entry, TRUE, TRUE, 0);
+ gtk_box_pack_start (GTK_BOX (file_hbox), file_browse, FALSE, FALSE, 0);
+
+ gtk_box_pack_start (GTK_BOX (vbox), file_hbox, TRUE, TRUE, 0);
+
+ page_view = gtk_icon_view_new ();
+ model = create_model ();
+ gtk_icon_view_set_model (GTK_ICON_VIEW (page_view), model);
+ gtk_icon_view_set_reorderable (GTK_ICON_VIEW (page_view), TRUE);
+ gtk_icon_view_set_selection_mode (GTK_ICON_VIEW (page_view),
+ GTK_SELECTION_MULTIPLE);
+
+ gtk_icon_view_set_pixbuf_column (GTK_ICON_VIEW (page_view), THUMB);
+ gtk_icon_view_set_text_column (GTK_ICON_VIEW (page_view), PAGE_NUMBER);
+ gtk_icon_view_set_tooltip_column (GTK_ICON_VIEW (page_view), IMAGE_NAME);
+
+ scroll = gtk_scrolled_window_new (NULL, NULL);
+ gtk_widget_set_size_request (scroll, -1, 300);
+
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
+ gtk_container_add (GTK_CONTAINER (scroll), page_view);
+
+ gtk_box_pack_start (GTK_BOX (vbox), scroll, TRUE, TRUE, 0);
+
+ h_but_box = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
+ gtk_button_box_set_layout (GTK_BUTTON_BOX (h_but_box), GTK_BUTTONBOX_START);
+
+ del = gtk_button_new_with_label (_("Remove the selected pages"));
+ gtk_box_pack_start (GTK_BOX (h_but_box), del, TRUE, TRUE, 0);
+
+ gtk_box_pack_start (GTK_BOX (vbox), h_but_box, FALSE, FALSE, 0);
+
+ h_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
+
+ img_combo = gimp_image_combo_box_new (NULL, NULL);
+ gtk_box_pack_start (GTK_BOX (h_box), img_combo, FALSE, FALSE, 0);
+
+ add_image = gtk_button_new_with_label (_("Add this image"));
+ gtk_box_pack_start (GTK_BOX (h_box), add_image, FALSE, FALSE, 0);
+
+ gtk_box_pack_start (GTK_BOX (vbox), h_box, FALSE, FALSE, 0);
+
+ fill_background_c = gtk_check_button_new_with_mnemonic (_("_Fill transparent areas with background color"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (fill_background_c),
+ optimize.transparent_background);
+ gtk_box_pack_end (GTK_BOX (vbox), fill_background_c, TRUE, TRUE, 0);
+
+ ignore_hidden_c = gtk_check_button_new_with_mnemonic (_("_Omit hidden layers and layers with zero opacity"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ignore_hidden_c),
+ optimize.ignore_hidden);
+ gtk_box_pack_end (GTK_BOX (vbox), ignore_hidden_c, FALSE, FALSE, 0);
+
+ vectorize_c = gtk_check_button_new_with_mnemonic (_("Convert _bitmaps to vector graphics where possible"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (vectorize_c),
+ optimize.vectorize);
+ gtk_box_pack_end (GTK_BOX (vbox), vectorize_c, FALSE, FALSE, 0);
+
+ apply_c = gtk_check_button_new_with_mnemonic (_("_Apply layer masks before saving"));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (apply_c),
+ optimize.apply_masks);
+ gtk_box_pack_end (GTK_BOX (vbox), apply_c, FALSE, FALSE, 0);
+ gimp_help_set_help_data (apply_c, _("Keeping the masks will not change the output"), NULL);
+
+ gtk_widget_show_all (window);
+
+ g_signal_connect (G_OBJECT (file_browse), "clicked",
+ G_CALLBACK (choose_file_call),
+ file_entry);
+
+ g_signal_connect (G_OBJECT (add_image), "clicked",
+ G_CALLBACK (add_image_call),
+ img_combo);
+
+ g_signal_connect (G_OBJECT (del), "clicked",
+ G_CALLBACK (del_image_call),
+ page_view);
+
+ g_signal_connect (G_OBJECT (model), "row-deleted",
+ G_CALLBACK (remove_call),
+ NULL);
+
+ run = gtk_dialog_run (GTK_DIALOG (window)) == GTK_RESPONSE_OK;
+
+ run &= get_image_list ();
+
+ temp = gtk_entry_get_text (GTK_ENTRY (file_entry));
+ g_stpcpy (file_name, temp);
+
+ optimize.transparent_background =
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (fill_background_c));
+ optimize.ignore_hidden =
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ignore_hidden_c));
+ optimize.vectorize =
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (vectorize_c));
+ optimize.apply_masks =
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (apply_c));
+
+ gtk_widget_destroy (window);
+
+ return run;
+}
+
+static void
+reverse_order_toggled (GtkToggleButton *reverse_order,
+ GtkButton *layers_as_pages)
+{
+ gchar *text;
+
+ text = g_strdup_printf (_("Layers as pages (%s)"),
+ gtk_toggle_button_get_active (reverse_order) ?
+ _("top layers first") : _("bottom layers first"));
+ gtk_button_set_label (layers_as_pages, text);
+ g_free (text);
+}
+
+/* A function that is called when the button for browsing for file
+ * locations was clicked
+ */
+static void
+choose_file_call (GtkWidget *browse_button,
+ gpointer file_entry)
+{
+ GFile *file = g_file_new_for_path (gtk_entry_get_text (GTK_ENTRY (file_entry)));
+
+ gtk_file_chooser_set_uri (GTK_FILE_CHOOSER (file_choose),
+ g_file_get_uri (file));
+
+ if (gtk_dialog_run (GTK_DIALOG (file_choose)) == GTK_RESPONSE_OK)
+ {
+ file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (file_choose));
+ gtk_entry_set_text (GTK_ENTRY (file_entry), g_file_get_path (file));
+ }
+
+ file_name = g_file_get_path (file);
+ gtk_widget_hide (file_choose);
+}
+
+/* A function to create the basic GtkTreeModel for the icon view */
+static GtkTreeModel*
+create_model (void)
+{
+ GtkListStore *model;
+ guint32 i;
+
+ /* validate_image_list was called earlier, so all the images
+ * up to multi_page.image_count are valid
+ */
+ model = gtk_list_store_new (4,
+ GDK_TYPE_PIXBUF, /* THUMB */
+ G_TYPE_STRING, /* PAGE_NUMBER */
+ G_TYPE_STRING, /* IMAGE_NAME */
+ G_TYPE_INT); /* IMAGE_ID */
+
+ for (i = 0 ; i < multi_page.image_count && i < MAX_PAGE_COUNT ; i++)
+ {
+ GtkTreeIter iter;
+ gint32 image = multi_page.images[i];
+ GdkPixbuf *pixbuf;
+
+ pixbuf = gimp_image_get_thumbnail (image, THUMB_WIDTH, THUMB_HEIGHT,
+ GIMP_PIXBUF_SMALL_CHECKS);
+
+ gtk_list_store_append (model, &iter);
+ gtk_list_store_set (model, &iter,
+ THUMB, pixbuf,
+ PAGE_NUMBER, g_strdup_printf (_("Page %d"), i + 1),
+ IMAGE_NAME, gimp_image_get_name (image),
+ IMAGE_ID, image,
+ -1);
+
+ g_object_unref (pixbuf);
+ }
+
+ return GTK_TREE_MODEL (model);
+}
+
+/* A function that puts the images from the model inside the images
+ * (pages) array
+ */
+static gboolean
+get_image_list (void)
+{
+ GtkTreeIter iter;
+ gboolean valid;
+
+ multi_page.image_count = 0;
+
+ for (valid = gtk_tree_model_get_iter_first (model, &iter);
+ valid;
+ valid = gtk_tree_model_iter_next (model, &iter))
+ {
+ gint32 image;
+
+ gtk_tree_model_get (model, &iter,
+ IMAGE_ID, &image,
+ -1);
+ multi_page.images[multi_page.image_count] = image;
+ multi_page.image_count++;
+ }
+
+ if (multi_page.image_count == 0)
+ {
+ g_message (_("Error! In order to save the file, at least one image "
+ "should be added!"));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/* A function that is called when the button for adding an image was
+ * clicked
+ */
+static void
+add_image_call (GtkWidget *widget,
+ gpointer img_combo)
+{
+ GtkListStore *store;
+ GtkTreeIter iter;
+ gint32 image;
+ GdkPixbuf *pixbuf;
+
+ dnd_remove = FALSE;
+
+ gimp_int_combo_box_get_active (img_combo, &image);
+
+ store = GTK_LIST_STORE (model);
+
+ pixbuf = gimp_image_get_thumbnail (image, THUMB_WIDTH, THUMB_HEIGHT,
+ GIMP_PIXBUF_SMALL_CHECKS);
+
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter,
+ PAGE_NUMBER, g_strdup_printf (_("Page %d"),
+ multi_page.image_count+1),
+ THUMB, pixbuf,
+ IMAGE_NAME, gimp_image_get_name (image),
+ IMAGE_ID, image,
+ -1);
+
+ g_object_unref (pixbuf);
+
+ multi_page.image_count++;
+
+ dnd_remove = TRUE;
+}
+
+/* A function that is called when the button for deleting the selected
+ * images was clicked
+ */
+static void
+del_image_call (GtkWidget *widget,
+ gpointer icon_view)
+{
+ GList *list;
+ GtkTreeRowReference **items;
+ GtkTreePath *item_path;
+ GtkTreeIter item;
+ gpointer temp;
+ guint32 len;
+
+ dnd_remove = FALSE;
+
+ list = gtk_icon_view_get_selected_items (GTK_ICON_VIEW (icon_view));
+
+ len = g_list_length (list);
+ if (len > 0)
+ {
+ gint i;
+
+ items = g_newa (GtkTreeRowReference*, len);
+
+ for (i = 0; i < len; i++)
+ {
+ temp = g_list_nth_data (list, i);
+ items[i] = gtk_tree_row_reference_new (model, temp);
+ gtk_tree_path_free (temp);
+ }
+ g_list_free (list);
+
+ for (i = 0; i < len; i++)
+ {
+ item_path = gtk_tree_row_reference_get_path (items[i]);
+
+ gtk_tree_model_get_iter (model, &item, item_path);
+ gtk_list_store_remove (GTK_LIST_STORE (model), &item);
+
+ gtk_tree_path_free (item_path);
+
+ gtk_tree_row_reference_free (items[i]);
+ multi_page.image_count--;
+ }
+ }
+
+ dnd_remove = TRUE;
+
+ recount_pages ();
+}
+
+/* A function that is called on rows-deleted signal. It will call the
+ * function to relabel the pages
+ */
+static void
+remove_call (GtkTreeModel *tree_model,
+ GtkTreePath *path,
+ gpointer user_data)
+{
+
+ if (dnd_remove)
+ /* The gtk documentation says that we should not free the indices array */
+ recount_pages ();
+}
+
+/* A function to relabel the pages in the icon view, when their order
+ * was changed
+ */
+static void
+recount_pages (void)
+{
+ GtkListStore *store;
+ GtkTreeIter iter;
+ gboolean valid;
+ gint32 i = 0;
+
+ store = GTK_LIST_STORE (model);
+
+ for (valid = gtk_tree_model_get_iter_first (model, &iter);
+ valid;
+ valid = gtk_tree_model_iter_next (model, &iter))
+ {
+ gtk_list_store_set (store, &iter,
+ PAGE_NUMBER, g_strdup_printf (_("Page %d"), i + 1),
+ -1);
+ i++;
+ }
+}
+
+
+/******************************************************/
+/* Beginning of the actual PDF functions */
+/******************************************************/
+
+static cairo_surface_t *
+get_cairo_surface (gint32 drawable_ID,
+ gboolean as_mask,
+ GError **error)
+{
+ GeglBuffer *src_buffer;
+ GeglBuffer *dest_buffer;
+ cairo_surface_t *surface;
+ cairo_status_t status;
+ cairo_format_t format;
+ gint width;
+ gint height;
+
+ src_buffer = gimp_drawable_get_buffer (drawable_ID);
+
+ width = gegl_buffer_get_width (src_buffer);
+ height = gegl_buffer_get_height (src_buffer);
+
+ if (as_mask)
+ format = CAIRO_FORMAT_A8;
+ else if (gimp_drawable_has_alpha (drawable_ID))
+ format = CAIRO_FORMAT_ARGB32;
+ else
+ format = CAIRO_FORMAT_RGB24;
+
+ surface = cairo_image_surface_create (format, width, height);
+
+ status = cairo_surface_status (surface);
+ if (status != CAIRO_STATUS_SUCCESS)
+ {
+ switch (status)
+ {
+ case CAIRO_STATUS_INVALID_SIZE:
+ g_set_error_literal (error,
+ GIMP_PLUGIN_PDF_SAVE_ERROR,
+ GIMP_PLUGIN_PDF_SAVE_ERROR_FAILED,
+ _("Cannot handle the size (either width or height) of the image."));
+ break;
+ default:
+ g_set_error (error,
+ GIMP_PLUGIN_PDF_SAVE_ERROR,
+ GIMP_PLUGIN_PDF_SAVE_ERROR_FAILED,
+ "Cairo error: %s",
+ cairo_status_to_string (status));
+ break;
+ }
+
+ return NULL;
+ }
+
+ dest_buffer = gimp_cairo_surface_create_buffer (surface);
+ if (as_mask)
+ {
+ /* src_buffer represents a mask in "Y u8", "Y u16", etc. formats.
+ * Yet cairo_mask*() functions only care about the alpha channel of
+ * the surface. Hence I change the format of dest_buffer so that the
+ * Y channel of src_buffer actually refers to the A channel of
+ * dest_buffer/surface in Cairo.
+ */
+ gegl_buffer_set_format (dest_buffer, babl_format ("Y u8"));
+ }
+
+ gegl_buffer_copy (src_buffer, NULL, GEGL_ABYSS_NONE, dest_buffer, NULL);
+
+ cairo_surface_mark_dirty (surface);
+
+ g_object_unref (src_buffer);
+ g_object_unref (dest_buffer);
+
+ return surface;
+}
+
+/* A function to check if a drawable is single colored This allows to
+ * convert bitmaps to vector where possible
+ */
+static GimpRGB
+get_layer_color (gint32 layer_ID,
+ gboolean *single)
+{
+ GimpRGB col;
+ gdouble red, green, blue, alpha;
+ gdouble dev, devSum;
+ gdouble median, pixels, count, precentile;
+
+ devSum = 0;
+ red = 0;
+ green = 0;
+ blue = 0;
+ alpha = 0;
+ dev = 0;
+
+ if (gimp_drawable_is_indexed (layer_ID))
+ {
+ /* FIXME: We can't do a proper histogram on indexed layers! */
+ *single = FALSE;
+ col. r = col.g = col.b = col.a = 0;
+ return col;
+ }
+
+ if (gimp_drawable_bpp (layer_ID) >= 3)
+ {
+ /* Are we in RGB mode? */
+
+ gimp_drawable_histogram (layer_ID, GIMP_HISTOGRAM_RED, 0.0, 1.0,
+ &red, &dev, &median, &pixels, &count, &precentile);
+ devSum += dev;
+ gimp_drawable_histogram (layer_ID, GIMP_HISTOGRAM_GREEN, 0.0, 1.0,
+ &green, &dev, &median, &pixels, &count, &precentile);
+ devSum += dev;
+ gimp_drawable_histogram (layer_ID, GIMP_HISTOGRAM_BLUE, 0.0, 1.0,
+ &blue, &dev, &median, &pixels, &count, &precentile);
+ devSum += dev;
+ }
+ else
+ {
+ /* We are in Grayscale mode (or Indexed) */
+
+ gimp_drawable_histogram (layer_ID, GIMP_HISTOGRAM_VALUE, 0.0, 1.0,
+ &red, &dev, &median, &pixels, &count, &precentile);
+ devSum += dev;
+ green = red;
+ blue = red;
+ }
+
+ if (gimp_drawable_has_alpha (layer_ID))
+ gimp_drawable_histogram (layer_ID, GIMP_HISTOGRAM_ALPHA, 0.0, 1.0,
+ &alpha, &dev, &median, &pixels, &count, &precentile);
+ else
+ alpha = 255;
+
+ devSum += dev;
+ *single = devSum == 0;
+
+ col.r = red/255;
+ col.g = green/255;
+ col.b = blue/255;
+ col.a = alpha/255;
+
+ return col;
+}
+
+/* A function that uses Pango to render the text to our cairo surface,
+ * in the same way it was the user saw it inside gimp.
+ * Needs some work on choosing the font name better, and on hinting
+ * (freetype and pango differences)
+ */
+static void
+drawText (gint32 text_id,
+ gdouble opacity,
+ cairo_t *cr,
+ gdouble x_res,
+ gdouble y_res)
+{
+ GimpImageType type = gimp_drawable_type (text_id);
+ gchar *text = gimp_text_layer_get_text (text_id);
+ gchar *markup = gimp_text_layer_get_markup (text_id);
+ gchar *font_family;
+ gchar *language;
+ cairo_font_options_t *options;
+ gint x;
+ gint y;
+ GimpRGB color;
+ GimpUnit unit;
+ gdouble size;
+ GimpTextHintStyle hinting;
+ GimpTextJustification j;
+ gboolean justify;
+ PangoAlignment align;
+ GimpTextDirection dir;
+ PangoLayout *layout;
+ PangoContext *context;
+ PangoFontDescription *font_description;
+ gdouble indent;
+ gdouble line_spacing;
+ gdouble letter_spacing;
+ PangoAttribute *letter_spacing_at;
+ PangoAttrList *attr_list = pango_attr_list_new ();
+ PangoFontMap *fontmap;
+
+ cairo_save (cr);
+
+ options = cairo_font_options_create ();
+ attr_list = pango_attr_list_new ();
+ cairo_get_font_options (cr, options);
+
+ /* Position */
+ gimp_drawable_offsets (text_id, &x, &y);
+ cairo_translate (cr, x, y);
+
+ /* Color */
+ /* When dealing with a gray/indexed image, the viewed color of the text layer
+ * can be different than the one kept in the memory */
+ if (type == GIMP_RGBA_IMAGE)
+ gimp_text_layer_get_color (text_id, &color);
+ else
+ gimp_image_pick_color (gimp_item_get_image (text_id),
+ text_id, x, y, FALSE, FALSE, 0, &color);
+
+ cairo_set_source_rgba (cr, color.r, color.g, color.b, opacity);
+
+ /* Hinting */
+ hinting = gimp_text_layer_get_hint_style (text_id);
+ switch (hinting)
+ {
+ case GIMP_TEXT_HINT_STYLE_NONE:
+ cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_NONE);
+ break;
+
+ case GIMP_TEXT_HINT_STYLE_SLIGHT:
+ cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_SLIGHT);
+ break;
+
+ case GIMP_TEXT_HINT_STYLE_MEDIUM:
+ cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_MEDIUM);
+ break;
+
+ case GIMP_TEXT_HINT_STYLE_FULL:
+ cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_FULL);
+ break;
+ }
+
+ /* Antialiasing */
+ if (gimp_text_layer_get_antialias (text_id))
+ cairo_font_options_set_antialias (options, CAIRO_ANTIALIAS_DEFAULT);
+ else
+ cairo_font_options_set_antialias (options, CAIRO_ANTIALIAS_NONE);
+
+ /* We are done with cairo's settings. It's time to create the
+ * context
+ */
+ fontmap = pango_cairo_font_map_new_for_font_type (CAIRO_FONT_TYPE_FT);
+
+ pango_cairo_font_map_set_resolution (PANGO_CAIRO_FONT_MAP (fontmap), y_res);
+
+ context = pango_font_map_create_context (fontmap);
+ g_object_unref (fontmap);
+
+ pango_cairo_context_set_font_options (context, options);
+
+ /* Language */
+ language = gimp_text_layer_get_language (text_id);
+ if (language)
+ pango_context_set_language (context,
+ pango_language_from_string(language));
+
+ /* Text Direction */
+ dir = gimp_text_layer_get_base_direction (text_id);
+
+ switch (dir)
+ {
+ case GIMP_TEXT_DIRECTION_LTR:
+ pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);
+ pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_NATURAL);
+ pango_context_set_base_gravity (context, PANGO_GRAVITY_SOUTH);
+ break;
+
+ case GIMP_TEXT_DIRECTION_RTL:
+ pango_context_set_base_dir (context, PANGO_DIRECTION_RTL);
+ pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_NATURAL);
+ pango_context_set_base_gravity (context, PANGO_GRAVITY_SOUTH);
+ break;
+
+ case GIMP_TEXT_DIRECTION_TTB_RTL:
+ pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);
+ pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_LINE);
+ pango_context_set_base_gravity (context, PANGO_GRAVITY_EAST);
+ break;
+
+ case GIMP_TEXT_DIRECTION_TTB_RTL_UPRIGHT:
+ pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);
+ pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_STRONG);
+ pango_context_set_base_gravity (context, PANGO_GRAVITY_EAST);
+ break;
+
+ case GIMP_TEXT_DIRECTION_TTB_LTR:
+ pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);
+ pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_LINE);
+ pango_context_set_base_gravity (context, PANGO_GRAVITY_WEST);
+ break;
+
+ case GIMP_TEXT_DIRECTION_TTB_LTR_UPRIGHT:
+ pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);
+ pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_STRONG);
+ pango_context_set_base_gravity (context, PANGO_GRAVITY_WEST);
+ break;
+ }
+
+ /* We are done with the context's settings. It's time to create the
+ * layout
+ */
+ layout = pango_layout_new (context);
+ pango_layout_set_wrap (layout, PANGO_WRAP_WORD_CHAR);
+
+ /* Font */
+ font_family = gimp_text_layer_get_font (text_id);
+
+ /* We need to find a way to convert GIMP's returned font name to a
+ * normal Pango name... Hopefully GIMP 2.8 with Pango will fix it.
+ */
+ font_description = pango_font_description_from_string (font_family);
+
+ /* Font Size */
+ size = gimp_text_layer_get_font_size (text_id, &unit);
+ size = gimp_units_to_pixels (size, unit, y_res);
+ pango_font_description_set_absolute_size (font_description, size * PANGO_SCALE);
+
+ pango_layout_set_font_description (layout, font_description);
+
+ /* Width */
+ if (! PANGO_GRAVITY_IS_VERTICAL (pango_context_get_base_gravity (context)))
+ pango_layout_set_width (layout, gimp_drawable_width (text_id) * PANGO_SCALE);
+ else
+ pango_layout_set_width (layout, gimp_drawable_height (text_id) * PANGO_SCALE);
+
+ /* Justification, and Alignment */
+ justify = FALSE;
+ j = gimp_text_layer_get_justification (text_id);
+ align = PANGO_ALIGN_LEFT;
+ switch (j)
+ {
+ case GIMP_TEXT_JUSTIFY_LEFT:
+ align = PANGO_ALIGN_LEFT;
+ break;
+ case GIMP_TEXT_JUSTIFY_RIGHT:
+ align = PANGO_ALIGN_RIGHT;
+ break;
+ case GIMP_TEXT_JUSTIFY_CENTER:
+ align = PANGO_ALIGN_CENTER;
+ break;
+ case GIMP_TEXT_JUSTIFY_FILL:
+ align = PANGO_ALIGN_LEFT;
+ justify = TRUE;
+ break;
+ }
+ pango_layout_set_alignment (layout, align);
+ pango_layout_set_justify (layout, justify);
+
+ /* Indentation */
+ indent = gimp_text_layer_get_indent (text_id);
+ pango_layout_set_indent (layout, (int)(PANGO_SCALE * indent));
+
+ /* Line Spacing */
+ line_spacing = gimp_text_layer_get_line_spacing (text_id);
+ pango_layout_set_spacing (layout, (int)(PANGO_SCALE * line_spacing));
+
+ /* Letter Spacing */
+ letter_spacing = gimp_text_layer_get_letter_spacing (text_id);
+ letter_spacing_at = pango_attr_letter_spacing_new ((int)(PANGO_SCALE * letter_spacing));
+ pango_attr_list_insert (attr_list, letter_spacing_at);
+
+
+ pango_layout_set_attributes (layout, attr_list);
+
+ /* Use the pango markup of the text layer */
+
+ if (markup != NULL && markup[0] != '\0')
+ pango_layout_set_markup (layout, markup, -1);
+ else /* If we can't find a markup, then it has just text */
+ pango_layout_set_text (layout, text, -1);
+
+ if (dir == GIMP_TEXT_DIRECTION_TTB_RTL ||
+ dir == GIMP_TEXT_DIRECTION_TTB_RTL_UPRIGHT)
+ {
+ cairo_translate (cr, gimp_drawable_width (text_id), 0);
+ cairo_rotate (cr, G_PI_2);
+ }
+
+ if (dir == GIMP_TEXT_DIRECTION_TTB_LTR ||
+ dir == GIMP_TEXT_DIRECTION_TTB_LTR_UPRIGHT)
+ {
+ cairo_translate (cr, 0, gimp_drawable_height (text_id));
+ cairo_rotate (cr, -G_PI_2);
+ }
+
+ pango_cairo_show_layout (cr, layout);
+
+ g_free (text);
+ g_free (font_family);
+ g_free (language);
+
+ g_object_unref (layout);
+ pango_font_description_free (font_description);
+ g_object_unref (context);
+ pango_attr_list_unref (attr_list);
+
+ cairo_font_options_destroy (options);
+
+ cairo_restore (cr);
+}
+
+static gboolean
+draw_layer (gint32 *layers,
+ gint n_layers,
+ gint j,
+ cairo_t *cr,
+ gdouble x_res,
+ gdouble y_res,
+ const gchar *name,
+ GError **error)
+{
+ gint32 layer_ID;
+ gdouble opacity;
+
+ if (optimize.reverse_order && optimize.layers_as_pages)
+ layer_ID = layers [j];
+ else
+ layer_ID = layers [n_layers - j - 1];
+
+ opacity = gimp_layer_get_opacity (layer_ID) / 100.0;
+ if ((! gimp_item_get_visible (layer_ID) || opacity == 0.0) &&
+ optimize.ignore_hidden)
+ return TRUE;
+
+ if (gimp_item_is_group (layer_ID))
+ {
+ gint *children;
+ gint children_num;
+ gint i;
+
+ children = gimp_item_get_children (layer_ID, &children_num);
+ for (i = 0; i < children_num; i++)
+ {
+ if (! draw_layer (children, children_num, i,
+ cr, x_res, y_res, name, error))
+ {
+ g_free (children);
+ return FALSE;
+ }
+ }
+ g_free (children);
+ }
+ else
+ {
+ cairo_surface_t *mask_image = NULL;
+ gint32 mask_ID = -1;
+ gint x, y;
+
+ mask_ID = gimp_layer_get_mask (layer_ID);
+ if (mask_ID != -1)
+ {
+ mask_image = get_cairo_surface (mask_ID, TRUE, error);
+
+ if (*error)
+ return FALSE;
+ }
+
+ gimp_drawable_offsets (layer_ID, &x, &y);
+
+ if (! gimp_item_is_text_layer (layer_ID))
+ {
+ /* For raster layers */
+ GimpRGB layer_color;
+ gboolean single_color = FALSE;
+
+ layer_color = get_layer_color (layer_ID, &single_color);
+
+ cairo_rectangle (cr, x, y,
+ gimp_drawable_width (layer_ID),
+ gimp_drawable_height (layer_ID));
+
+ if (optimize.vectorize && single_color)
+ {
+ cairo_set_source_rgba (cr,
+ layer_color.r,
+ layer_color.g,
+ layer_color.b,
+ layer_color.a * opacity);
+ if (mask_ID != -1)
+ cairo_mask_surface (cr, mask_image, x, y);
+ else
+ cairo_fill (cr);
+ }
+ else
+ {
+ cairo_surface_t *layer_image;
+
+ layer_image = get_cairo_surface (layer_ID, FALSE, error);
+
+ if (*error)
+ return FALSE;
+
+ cairo_clip (cr);
+
+ cairo_set_source_surface (cr, layer_image, x, y);
+ cairo_push_group (cr);
+ cairo_paint_with_alpha (cr, opacity);
+ cairo_pop_group_to_source (cr);
+
+ if (mask_ID != -1)
+ cairo_mask_surface (cr, mask_image, x, y);
+ else
+ cairo_paint (cr);
+
+ cairo_reset_clip (cr);
+
+ cairo_surface_destroy (layer_image);
+ }
+ }
+ else
+ {
+ /* For text layers */
+ drawText (layer_ID, opacity, cr, x_res, y_res);
+ }
+
+ /* draw new page if "layers as pages" option is checked */
+ if (optimize.layers_as_pages &&
+ g_strcmp0 (name, SAVE2_PROC) == 0)
+ cairo_show_page (cr);
+
+ /* We are done with the layer - time to free some resources */
+ if (mask_ID != -1)
+ cairo_surface_destroy (mask_image);
+ }
+
+ return TRUE;
+}