/* GIMP - The GNU Image Manipulation Program * * file-pdf-save.c - PDF file exporter, based on the cairo PDF surface * * Copyright (C) 2010 Barak Itkin * * 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 . */ /* 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 * First version of the plugin. This is only a proof of concept and not a full * working plugin. * * May 6, 2009 Barak | Itkin * 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 * 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 "/File/Create/PDF" * * August 21, 2009 | Barak Itkin * 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 * 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 #include #include #include #include #include #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, "/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; }