/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * * 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 pika'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 (PIKA 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 pika_text_layer_get_font Returns a * font which is sometimes incompatible with pango_font_description_from_string * (pika_text_layer_get_font sometimes returns suffixes such as "semi-expanded" to * the font's name although the PIKA's font selection dialog shows the don'ts name * normally - This should be checked again in PIKA 2.7) * 3. Indexed layers can't be optimized yet (Since pika_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 PIKA. * 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 behavior 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 PIKA 2.8 api, which includes: * - Now using pika_export_dialog_new * - Using pika_text_layer_get_hint_style (2.8) instead of the deprecated * pika_text_layer_get_hinting (2.6). * * August 24, 2010 | Barak Itkin * More migrations to the new PIKA 2.8 api: * - Now using the PikaItem api * - Using pika_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 "libpika/stdplugins-intl.h" #define SAVE_PROC "file-pdf-save" #define SAVE_MULTI_PROC "file-pdf-save-multi" #define PLUG_IN_BINARY "file-pdf-save" #define PLUG_IN_ROLE "pika-file-pdf-save" #define DATA_IMAGE_LIST "file-pdf-data-multi-page" /* Pika 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 PIKA_PLUGIN_PDF_SAVE_ERROR pika_plugin_pdf_save_error_quark () typedef enum { PIKA_PLUGIN_PDF_SAVE_ERROR_FAILED } PikaPluginPDFError; GQuark pika_plugin_pdf_save_error_quark (void); typedef struct { PikaImage *images[MAX_PAGE_COUNT]; guint32 image_count; gchar file_name[MAX_FILE_NAME_LENGTH]; } PdfMultiPage; enum { THUMB, PAGE_NUMBER, IMAGE_NAME, IMAGE }; typedef struct { GdkPixbuf *thumb; gint32 page_number; gchar *image_name; } Page; typedef struct _Pdf Pdf; typedef struct _PdfClass PdfClass; struct _Pdf { PikaPlugIn parent_instance; }; struct _PdfClass { PikaPlugInClass parent_class; }; #define PDF_TYPE (pdf_get_type ()) #define PDF(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PDF_TYPE, Pdf)) GType pdf_get_type (void) G_GNUC_CONST; static GList * pdf_query_procedures (PikaPlugIn *plug_in); static PikaProcedure * pdf_create_procedure (PikaPlugIn *plug_in, const gchar *name); static PikaValueArray * pdf_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, PikaMetadata *metadata, PikaProcedureConfig *config, gpointer run_data); static PikaValueArray * pdf_save_multi (PikaProcedure *procedure, PikaProcedureConfig *config, gpointer run_data); static PikaPDBStatusType pdf_save_image (PikaProcedure *procedure, PikaProcedureConfig *config, gboolean single_image, gboolean show_progress, GError **error); static void init_image_list_defaults (PikaImage *image); static void validate_image_list (void); static gboolean gui_single (PikaProcedure *procedure, PikaProcedureConfig *config, PikaImage *image); static gboolean gui_multi (PikaProcedure *procedure, PikaProcedureConfig *config); 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 (PikaDrawable *drawable, gboolean as_mask, GError **error); static PikaRGB get_layer_color (PikaLayer *layer, gboolean *single); static void drawText (PikaLayer *layer, gdouble opacity, cairo_t *cr, gdouble x_res, gdouble y_res); static gboolean draw_layer (PikaLayer **layers, gint n_layers, PikaProcedureConfig *config, gint j, cairo_t *cr, gdouble x_res, gdouble y_res, const gchar *name, gboolean show_progress, gdouble progress_start, gdouble progress_end, gint layer_level, GError **error); G_DEFINE_TYPE (Pdf, pdf, PIKA_TYPE_PLUG_IN) PIKA_MAIN (PDF_TYPE) DEFINE_STD_SET_I18N static gboolean dnd_remove = TRUE; static PdfMultiPage multi_page; static GtkTreeModel *model; static GtkWidget *file_choose; static gchar *file_name; G_DEFINE_QUARK (pika-plugin-pdf-save-error-quark, pika_plugin_pdf_save_error) static void pdf_class_init (PdfClass *klass) { PikaPlugInClass *plug_in_class = PIKA_PLUG_IN_CLASS (klass); plug_in_class->query_procedures = pdf_query_procedures; plug_in_class->create_procedure = pdf_create_procedure; plug_in_class->set_i18n = STD_SET_I18N; } static void pdf_init (Pdf *pdf) { } static GList * pdf_query_procedures (PikaPlugIn *plug_in) { GList *list = NULL; list = g_list_append (list, g_strdup (SAVE_PROC)); list = g_list_append (list, g_strdup (SAVE_MULTI_PROC)); return list; } static PikaProcedure * pdf_create_procedure (PikaPlugIn *plug_in, const gchar *name) { PikaProcedure *procedure = NULL; if (! strcmp (name, SAVE_PROC)) { procedure = pika_save_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, TRUE, pdf_save, NULL, NULL); pika_procedure_set_image_types (procedure, "*"); pika_procedure_set_menu_label (procedure, _("Portable Document Format")); pika_procedure_set_documentation (procedure, "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.", name); pika_procedure_set_attribution (procedure, "Barak Itkin, Lionel N., Jehan", "Copyright Barak Itkin, Lionel N., Jehan", "August 2009, 2017"); pika_file_procedure_set_format_name (PIKA_FILE_PROCEDURE (procedure), _("PDF")); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "application/pdf"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "pdf"); PIKA_PROC_ARG_BOOLEAN (procedure, "vectorize", "Convert bitmaps to vector graphics where possible", "Convert bitmaps to vector graphics where possible", TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "ignore-hidden", "Omit hidden layers and layers with zero opacity", "Non-visible layers will not be exported", TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "apply-masks", "Apply layer masks", "Apply layer masks before saving (Keeping the mask " "will not change the output, only the PDF structure),", TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "layers-as-pages", "Layers as pages", "Layers as pages (bottom layers first).", FALSE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "reverse-order", "Reverse order", "Reverse the pages order (top layers first).", FALSE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "root-layers-only", "Root layers only", "Only the root layers are considered pages", TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "convert-text-layers", "Convert text layers to image", "Convert text layers to raster graphics", FALSE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "fill-background-color", _("_Fill transparent areas with background color"), _("Fill transparent areas with background color if " "layer has an alpha channel"), TRUE, G_PARAM_READWRITE); } else if (! strcmp (name, SAVE_MULTI_PROC)) { procedure = pika_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, pdf_save_multi, NULL, NULL); pika_procedure_set_image_types (procedure, "*"); pika_procedure_set_menu_label (procedure, _("_Create multipage PDF...")); #if 0 pika_procedure_add_menu_path (procedure, "/File/Create/PDF"); #endif pika_procedure_set_documentation (procedure, "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.", name); pika_procedure_set_attribution (procedure, "Barak Itkin", "Copyright Barak Itkin", "August 2009"); PIKA_PROC_ARG_ENUM (procedure, "run-mode", "Run mode", "The run mode", PIKA_TYPE_RUN_MODE, PIKA_RUN_INTERACTIVE, G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "count", "Count", "The number of images entered (This will be the " "number of pages).", 1, MAX_PAGE_COUNT, 1, G_PARAM_READWRITE); PIKA_PROC_ARG_INT32_ARRAY (procedure, "images", "Images", "Input image for each page (An image can " "appear more than once)", G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "vectorize", "Vectorize", "Convert bitmaps to vector graphics where possible.", TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "ignore-hidden", "Ignore hidden", "Omit hidden layers and layers with zero opacity.", TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "apply-masks", "Apply masks", "Apply layer masks before saving (Keeping them " "will not change the output),", TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "fill-background-color", _("_Fill transparent areas with background color"), _("Fill transparent areas with background color if " "layer has an alpha channel"), TRUE, G_PARAM_READWRITE); PIKA_PROC_ARG_STRING (procedure, "uri", "URI", "The URI of the file to save to", NULL, PIKA_PARAM_READWRITE); } return procedure; } static PikaValueArray * pdf_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, PikaMetadata *metadata, PikaProcedureConfig *config, gpointer run_data) { GError *error = NULL; PikaPDBStatusType status = PIKA_PDB_SUCCESS; gegl_init (NULL, NULL); /* Initializing all the settings */ multi_page.image_count = 0; file_name = g_file_get_path (file); init_image_list_defaults (image); validate_image_list (); if (run_mode == PIKA_RUN_INTERACTIVE) { if (! gui_single (procedure, config, image)) status = PIKA_PDB_CANCEL; } if (status == PIKA_PDB_SUCCESS) status = pdf_save_image (procedure, config, TRUE, (run_mode != PIKA_RUN_NONINTERACTIVE), &error); return pika_procedure_new_return_values (procedure, status, error); } static PikaValueArray * pdf_save_multi (PikaProcedure *procedure, PikaProcedureConfig *config, gpointer run_data) { GError *error = NULL; PikaPDBStatusType status = PIKA_PDB_SUCCESS; PikaRunMode run_mode; gchar *uri; const gint32 *image_ids = NULL; PikaImage *image = NULL; GFile *file; gegl_init (NULL, NULL); g_object_get (config, "run-mode", &run_mode, "uri", &uri, "count", &multi_page.image_count, "images", &image_ids, NULL); file = g_file_new_for_uri (uri); g_free (uri); file_name = g_file_get_path (file); if (image_ids) for (gint i = 0; i < multi_page.image_count; i++) multi_page.images[i] = pika_image_get_by_id (image_ids[i]); else init_image_list_defaults (image); validate_image_list (); /* Starting the executions */ if (run_mode == PIKA_RUN_INTERACTIVE) { if (! gui_multi (procedure, config)) status = PIKA_PDB_CANCEL; } if (status == PIKA_PDB_SUCCESS) status = pdf_save_image (procedure, config, FALSE, (run_mode != PIKA_RUN_NONINTERACTIVE), &error); return pika_procedure_new_return_values (procedure, status, error); } 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 GList * get_missing_fonts (GList *layers) { GList *missing_fonts = NULL; GList *iter; for (iter = layers; iter; iter = iter->next) { PikaLayer *layer = iter->data; if (pika_item_is_group (PIKA_ITEM (layer))) { GList *child_missing_fonts; GList *iter2; child_missing_fonts = get_missing_fonts (pika_item_list_children (PIKA_ITEM (layer))); for (iter2 = child_missing_fonts; iter2; iter2 = iter2->next) { gchar *missing = iter2->data; if (g_list_find_custom (missing_fonts, missing, (GCompareFunc) g_strcmp0) == NULL) missing_fonts = g_list_prepend (missing_fonts, missing); else g_free (missing); } g_list_free (child_missing_fonts); } else if (pika_item_is_text_layer (PIKA_ITEM (layer))) { PikaFont *pika_font; PangoFontDescription *font_description; PangoFontDescription *font_description2; PangoFontMap *fontmap; PangoFont *font; PangoContext *context; fontmap = pango_cairo_font_map_new_for_font_type (CAIRO_FONT_TYPE_FT); context = pango_font_map_create_context (fontmap); pika_font = pika_text_layer_get_font (PIKA_TEXT_LAYER (layer)); font_description = pika_font_get_pango_font_description (pika_font); font = pango_font_map_load_font (fontmap, context, font_description); font_description2 = pango_font_describe (font); if (g_strcmp0 (pango_font_description_get_family (font_description), pango_font_description_get_family (font_description2)) != 0) { const gchar *missing = pango_font_description_get_family (font_description); if (g_list_find_custom (missing_fonts, missing, (GCompareFunc) g_strcmp0) == NULL) missing_fonts = g_list_prepend (missing_fonts, g_strdup (missing)); } g_object_unref (font); pango_font_description_free (font_description); pango_font_description_free (font_description2); g_object_unref (context); g_object_unref (fontmap); } } g_list_free (layers); return missing_fonts; } static PikaPDBStatusType pdf_save_image (PikaProcedure *procedure, PikaProcedureConfig *config, gboolean single_image, gboolean show_progress, GError **error) { cairo_surface_t *pdf_file; cairo_t *cr; PikaExportCapabilities capabilities; FILE *fp; gint i; gboolean apply_masks; gboolean layers_as_pages; gboolean fill_background_color; g_object_get (config, "apply-masks", &apply_masks, "layers-as-pages", &layers_as_pages, "fill-background-color", &fill_background_color, NULL); fp = g_fopen (file_name, "wb"); if (! fp) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not open '%s' for writing: %s"), pika_filename_to_utf8 (file_name), g_strerror (errno)); return PIKA_PDB_EXECUTION_ERROR; } pdf_file = cairo_pdf_surface_create_for_stream (write_func, fp, 1, 1); if (cairo_surface_status (pdf_file) != CAIRO_STATUS_SUCCESS) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, _("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))); return PIKA_PDB_EXECUTION_ERROR; } cr = cairo_create (pdf_file); capabilities = (PIKA_EXPORT_CAN_HANDLE_RGB | PIKA_EXPORT_CAN_HANDLE_ALPHA | PIKA_EXPORT_CAN_HANDLE_GRAY | PIKA_EXPORT_CAN_HANDLE_LAYERS | PIKA_EXPORT_CAN_HANDLE_INDEXED); /* This seems counter-intuitive, but not setting the mask capability * will apply any layer mask upon pika_export_image(). */ if (! apply_masks) capabilities |= PIKA_EXPORT_CAN_HANDLE_LAYER_MASKS; for (i = 0; i < multi_page.image_count; i++) { PikaImage *image = multi_page.images[i]; PikaLayer **layers; gint32 n_layers; gdouble x_res, y_res; gdouble x_scale, y_scale; PikaDrawable **temp; PikaDrawable **temp_out; PikaItem **drawables; gint n_drawables; gint temp_size = 1; gint j; drawables = pika_image_get_selected_drawables (image, &n_drawables); if (n_drawables == 0) { g_free (drawables); continue; } temp = g_new (PikaDrawable *, 1); temp[0] = PIKA_DRAWABLE (drawables[0]); g_free (drawables); temp_out = temp; /* 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 (! (pika_export_image (&image, &temp_size, &temp, NULL, capabilities) == PIKA_EXPORT_EXPORT)) { /* pika_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 = pika_image_duplicate (image); } if (temp != temp_out) g_free (temp_out); g_free (temp); pika_selection_none (image); pika_image_get_resolution (image, &x_res, &y_res); x_scale = 72.0 / x_res; y_scale = 72.0 / y_res; cairo_pdf_surface_set_size (pdf_file, pika_image_get_width (image) * x_scale, pika_image_get_height (image) * 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 = pika_image_get_layers (image, &n_layers); /* Fill image with background color if transparent and * user chose that option. */ if (pika_drawable_has_alpha (PIKA_DRAWABLE (layers[n_layers - 1])) && fill_background_color) { PikaRGB color; cairo_rectangle (cr, 0.0, 0.0, pika_image_get_width (image), pika_image_get_height (image)); pika_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, config, j, cr, x_res, y_res, pika_procedure_get_name (procedure), show_progress, /* Progression is showed per image, and would restart at 0 * if you open several images. */ (gdouble) j / n_layers, (gdouble) (j + 1) / n_layers, 0, error)) { /* free the resources */ g_free (layers); cairo_surface_destroy (pdf_file); cairo_destroy (cr); fclose (fp); return PIKA_PDB_EXECUTION_ERROR; } } if (show_progress) pika_progress_update (1.0); 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 (! layers_as_pages) cairo_show_page (cr); cairo_restore (cr); pika_image_delete (image); } /* We are done with all the images - time to free the resources */ cairo_surface_destroy (pdf_file); cairo_destroy (cr); fclose (fp); return PIKA_PDB_SUCCESS; } /******************************************************/ /* Beginning of parameter handling functions */ /******************************************************/ /* A function that initializes the image list to default values */ static void init_image_list_defaults (PikaImage *image) { if (image) { 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 (pika_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 (PikaProcedure *procedure, PikaProcedureConfig *config, PikaImage *image) { GtkWidget *window; GtkWidget *widget; PikaLayer **layers; GList *missing_fonts; GList *dialog_props = NULL; gboolean run; gint32 n_layers; pika_ui_init (PLUG_IN_BINARY); window = pika_save_procedure_dialog_new (PIKA_SAVE_PROCEDURE (procedure), PIKA_PROCEDURE_CONFIG (config), image); pika_procedure_dialog_fill_box (PIKA_PROCEDURE_DIALOG (window), "pages-box", "reverse-order", "root-layers-only", NULL); /* XXX the "layers-as-pages" checkbox label used to be changing, * showing "top layers first" or "bottom layers first" depending on * the value of "reverse-order". Should we want this? Or do it * differently, i.e. maybe with an additional label showing the order? */ widget = pika_procedure_dialog_fill_frame (PIKA_PROCEDURE_DIALOG (window), "pages-frame", "layers-as-pages", FALSE, "pages-box"); /* Enable "layers-as-pages" if more than one layer, or there's a single * layer group has more than one layer */ layers = pika_image_get_layers (multi_page.images[0], &n_layers); if (n_layers == 1 && pika_item_is_group (PIKA_ITEM (layers[0]))) g_free (pika_item_get_children (PIKA_ITEM (layers[0]), &n_layers)); g_free (layers); gtk_widget_set_sensitive (widget, n_layers > 1); /* Warning for missing fonts (non-embeddable with rasterization * possible). */ missing_fonts = get_missing_fonts (pika_image_list_layers (multi_page.images[0])); if (missing_fonts != NULL) { GList *iter; gchar *font_list = NULL; gchar *text; for (iter = missing_fonts; iter; iter = iter->next) { gchar *fontname = iter->data; if (font_list == NULL) { font_list = g_strdup (fontname); } else { gchar *tmp = font_list; font_list = g_strjoin (", ", tmp, fontname, NULL); g_free (tmp); } } text = g_strdup_printf (_("The following fonts cannot be found: %s.\n" "It is recommended to convert your text layers to image " "or to install the missing fonts before exporting, " "otherwise your design may not look right."), font_list); /* TODO: we used to have a GtkImage showing a PIKA_ICON_MASCOT_EEK * icon in GTK_ICON_SIZE_BUTTON size, next to the label, to make * the warning more obvious. */ widget = pika_procedure_dialog_get_label (PIKA_PROCEDURE_DIALOG (window), "missing-fonts-label", text, FALSE, FALSE); gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); pika_procedure_dialog_fill_frame (PIKA_PROCEDURE_DIALOG (window), "convert-text-layers-frame", "convert-text-layers", TRUE, "missing-fonts-label"); dialog_props = g_list_prepend (dialog_props, "convert-text-layers-frame"); g_list_free_full (missing_fonts, g_free); g_free (text); g_free (font_list); } else { dialog_props = g_list_prepend (dialog_props, "convert-text-layers"); } dialog_props = g_list_prepend (dialog_props, "fill-background-color"); dialog_props = g_list_prepend (dialog_props, "ignore-hidden"); dialog_props = g_list_prepend (dialog_props, "vectorize"); dialog_props = g_list_prepend (dialog_props, "apply-masks"); dialog_props = g_list_prepend (dialog_props, "pages-frame"); pika_procedure_dialog_fill_list (PIKA_PROCEDURE_DIALOG (window), dialog_props); run = pika_procedure_dialog_run (PIKA_PROCEDURE_DIALOG (window)); gtk_widget_destroy (window); return run; } /* The main GUI function for saving multi-paged PDFs */ static gboolean gui_multi (PikaProcedure *procedure, PikaProcedureConfig *config) { 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; gboolean vectorize; gboolean ignore_hidden; gboolean fill_background_color; gboolean apply_masks; gboolean layers_as_pages; gboolean reverse_order; gboolean convert_text; g_object_get (config, "vectorize", &vectorize, "ignore-hidden", &ignore_hidden, "fill-background-color", &fill_background_color, "apply-masks", &apply_masks, "layers-as-pages", &layers_as_pages, "reverse-order", &reverse_order, "convert-text-layers", &convert_text, NULL); pika_ui_init (PLUG_IN_BINARY); window = pika_export_dialog_new ("PDF", PLUG_IN_ROLE, SAVE_MULTI_PROC); vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); gtk_box_pack_start (GTK_BOX (pika_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 = pika_image_combo_box_new (NULL, 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); 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), ignore_hidden); gtk_box_pack_end (GTK_BOX (vbox), ignore_hidden_c, 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), fill_background_color); gtk_box_pack_end (GTK_BOX (vbox), fill_background_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), 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), apply_masks); gtk_box_pack_end (GTK_BOX (vbox), apply_c, FALSE, FALSE, 0); pika_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); ignore_hidden = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ignore_hidden_c)); vectorize = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (vectorize_c)); apply_masks = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (apply_c)); gtk_widget_destroy (window); return run; } /* 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_peek_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 */ PIKA_TYPE_IMAGE); /* IMAGE */ for (i = 0 ; i < multi_page.image_count && i < MAX_PAGE_COUNT ; i++) { GtkTreeIter iter; PikaImage *image = multi_page.images[i]; GdkPixbuf *pixbuf; pixbuf = pika_image_get_thumbnail (image, THUMB_WIDTH, THUMB_HEIGHT, PIKA_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, pika_image_get_name (image), IMAGE, 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)) { PikaImage *image; gtk_tree_model_get (model, &iter, IMAGE, &image, -1); multi_page.images[multi_page.image_count] = image; multi_page.image_count++; g_object_unref (image); } 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_id; PikaImage *image; GdkPixbuf *pixbuf; dnd_remove = FALSE; pika_int_combo_box_get_active (img_combo, &image_id); image = pika_image_get_by_id (image_id); store = GTK_LIST_STORE (model); pixbuf = pika_image_get_thumbnail (image, THUMB_WIDTH, THUMB_HEIGHT, PIKA_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, pika_image_get_name (image), IMAGE, 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 (PikaDrawable *drawable, 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 = pika_drawable_get_buffer (drawable); width = gegl_buffer_get_width (src_buffer); height = gegl_buffer_get_height (src_buffer); if (as_mask) format = CAIRO_FORMAT_A8; else if (pika_drawable_has_alpha (drawable)) 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, PIKA_PLUGIN_PDF_SAVE_ERROR, PIKA_PLUGIN_PDF_SAVE_ERROR_FAILED, _("Cannot handle the size (either width or height) of the image.")); break; default: g_set_error (error, PIKA_PLUGIN_PDF_SAVE_ERROR, PIKA_PLUGIN_PDF_SAVE_ERROR_FAILED, "Cairo error: %s", cairo_status_to_string (status)); break; } return NULL; } dest_buffer = pika_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 PikaRGB get_layer_color (PikaLayer *layer, gboolean *single) { PikaRGB col; gdouble red, green, blue, alpha; gdouble dev, devSum; gdouble median, pixels, count, percentile; devSum = 0; red = 0; green = 0; blue = 0; alpha = 0; dev = 0; if (pika_drawable_is_indexed (PIKA_DRAWABLE (layer))) { /* 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 (pika_drawable_get_bpp (PIKA_DRAWABLE (layer)) >= 3) { /* Are we in RGB mode? */ pika_drawable_histogram (PIKA_DRAWABLE (layer), PIKA_HISTOGRAM_RED, 0.0, 1.0, &red, &dev, &median, &pixels, &count, &percentile); devSum += dev; pika_drawable_histogram (PIKA_DRAWABLE (layer), PIKA_HISTOGRAM_GREEN, 0.0, 1.0, &green, &dev, &median, &pixels, &count, &percentile); devSum += dev; pika_drawable_histogram (PIKA_DRAWABLE (layer), PIKA_HISTOGRAM_BLUE, 0.0, 1.0, &blue, &dev, &median, &pixels, &count, &percentile); devSum += dev; } else { /* We are in Grayscale mode (or Indexed) */ pika_drawable_histogram (PIKA_DRAWABLE (layer), PIKA_HISTOGRAM_VALUE, 0.0, 1.0, &red, &dev, &median, &pixels, &count, &percentile); devSum += dev; green = red; blue = red; } if (pika_drawable_has_alpha (PIKA_DRAWABLE (layer))) pika_drawable_histogram (PIKA_DRAWABLE (layer), PIKA_HISTOGRAM_ALPHA, 0.0, 1.0, &alpha, &dev, &median, &pixels, &count, &percentile); 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 pika. * Needs some work on choosing the font name better, and on hinting * (freetype and pango differences) */ static void drawText (PikaLayer *layer, gdouble opacity, cairo_t *cr, gdouble x_res, gdouble y_res) { PikaImageType type = pika_drawable_type (PIKA_DRAWABLE (layer)); gchar *text = pika_text_layer_get_text (PIKA_TEXT_LAYER (layer)); gchar *markup = pika_text_layer_get_markup (PIKA_TEXT_LAYER (layer)); gchar *language; cairo_font_options_t *options; gint x; gint y; PikaRGB color; PikaUnit unit; gdouble size; PikaTextHintStyle hinting; PikaTextJustification j; gboolean justify; PangoAlignment align; PikaTextDirection dir; PangoLayout *layout; PangoContext *context; PikaFont *font; 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 */ pika_drawable_get_offsets (PIKA_DRAWABLE (layer), &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 == PIKA_RGBA_IMAGE) pika_text_layer_get_color (PIKA_TEXT_LAYER (layer), &color); else pika_image_pick_color (pika_item_get_image (PIKA_ITEM (layer)), 1, (const PikaItem**) &layer, x, y, FALSE, FALSE, 0, &color); cairo_set_source_rgba (cr, color.r, color.g, color.b, opacity); /* Hinting */ hinting = pika_text_layer_get_hint_style (PIKA_TEXT_LAYER (layer)); switch (hinting) { case PIKA_TEXT_HINT_STYLE_NONE: cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_NONE); break; case PIKA_TEXT_HINT_STYLE_SLIGHT: cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_SLIGHT); break; case PIKA_TEXT_HINT_STYLE_MEDIUM: cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_MEDIUM); break; case PIKA_TEXT_HINT_STYLE_FULL: cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_FULL); break; } /* Antialiasing */ if (pika_text_layer_get_antialias (PIKA_TEXT_LAYER (layer))) 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); /* Font */ font = pika_text_layer_get_font (PIKA_TEXT_LAYER (layer)); font_description = pika_font_get_pango_font_description (font); /* This function breaks rendering with some fonts if it's called before * pika_font_get_pango_font_description(). I'm still unsure why yet it * probably means there is a bug somewhere we must fix. Until then, let's make * sure we keep this order. XXX */ 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 = pika_text_layer_get_language (PIKA_TEXT_LAYER (layer)); if (language) pango_context_set_language (context, pango_language_from_string (language)); /* Text Direction */ dir = pika_text_layer_get_base_direction (PIKA_TEXT_LAYER (layer)); switch (dir) { case PIKA_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 PIKA_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 PIKA_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 PIKA_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 PIKA_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 PIKA_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 Size */ size = pika_text_layer_get_font_size (PIKA_TEXT_LAYER (layer), &unit); size = pika_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, pika_drawable_get_width (PIKA_DRAWABLE (layer)) * PANGO_SCALE); else pango_layout_set_width (layout, pika_drawable_get_height (PIKA_DRAWABLE (layer)) * PANGO_SCALE); /* Justification, and Alignment */ justify = FALSE; j = pika_text_layer_get_justification (PIKA_TEXT_LAYER (layer)); align = PANGO_ALIGN_LEFT; switch (j) { case PIKA_TEXT_JUSTIFY_LEFT: align = PANGO_ALIGN_LEFT; break; case PIKA_TEXT_JUSTIFY_RIGHT: align = PANGO_ALIGN_RIGHT; break; case PIKA_TEXT_JUSTIFY_CENTER: align = PANGO_ALIGN_CENTER; break; case PIKA_TEXT_JUSTIFY_FILL: align = PANGO_ALIGN_LEFT; justify = TRUE; break; } pango_layout_set_alignment (layout, align); pango_layout_set_justify (layout, justify); /* Indentation */ indent = pika_text_layer_get_indent (PIKA_TEXT_LAYER (layer)); pango_layout_set_indent (layout, (int)(PANGO_SCALE * indent)); /* Line Spacing */ line_spacing = pika_text_layer_get_line_spacing (PIKA_TEXT_LAYER (layer)); pango_layout_set_spacing (layout, (int)(PANGO_SCALE * line_spacing)); /* Letter Spacing */ letter_spacing = pika_text_layer_get_letter_spacing (PIKA_TEXT_LAYER (layer)); 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 == PIKA_TEXT_DIRECTION_TTB_RTL || dir == PIKA_TEXT_DIRECTION_TTB_RTL_UPRIGHT) { cairo_translate (cr, pika_drawable_get_width (PIKA_DRAWABLE (layer)), 0); cairo_rotate (cr, G_PI_2); } if (dir == PIKA_TEXT_DIRECTION_TTB_LTR || dir == PIKA_TEXT_DIRECTION_TTB_LTR_UPRIGHT) { cairo_translate (cr, 0, pika_drawable_get_height (PIKA_DRAWABLE (layer))); cairo_rotate (cr, -G_PI_2); } pango_cairo_show_layout (cr, layout); g_free (text); 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 (PikaLayer **layers, gint n_layers, PikaProcedureConfig *config, gint j, cairo_t *cr, gdouble x_res, gdouble y_res, const gchar *name, gboolean show_progress, gdouble progress_start, gdouble progress_end, gint layer_level, GError **error) { PikaLayer *layer; gdouble opacity; gboolean vectorize; gboolean ignore_hidden; gboolean layers_as_pages; gboolean reverse_order; gboolean root_layers_only; gboolean convert_text; g_object_get (config, "vectorize", &vectorize, "ignore-hidden", &ignore_hidden, "layers-as-pages", &layers_as_pages, "reverse-order", &reverse_order, "root-layers-only", &root_layers_only, "convert-text-layers", &convert_text, NULL); if (reverse_order && layers_as_pages) layer = layers [j]; else layer = layers [n_layers - j - 1]; opacity = pika_layer_get_opacity (layer) / 100.0; if ((! pika_item_get_visible (PIKA_ITEM (layer)) || opacity == 0.0) && ignore_hidden) return TRUE; if (pika_item_is_group (PIKA_ITEM (layer))) { PikaItem **children; gint children_num; gint i; children = pika_item_get_children (PIKA_ITEM (layer), &children_num); for (i = 0; i < children_num; i++) { if (! draw_layer ((PikaLayer **) children, children_num, config, i, cr, x_res, y_res, name, show_progress, progress_start + i * (progress_end - progress_start) / children_num, progress_end, layer_level + 1, error)) { g_free (children); return FALSE; } } g_free (children); if (root_layers_only && layers_as_pages && children_num > 0 && layer_level == 0) cairo_show_page (cr); } else { cairo_surface_t *mask_image = NULL; PikaLayerMask *mask = NULL; gint x, y; if (show_progress) pika_progress_update (progress_start); mask = pika_layer_get_mask (layer); if (mask) { mask_image = get_cairo_surface (PIKA_DRAWABLE (mask), TRUE, error); if (*error) return FALSE; } pika_drawable_get_offsets (PIKA_DRAWABLE (layer), &x, &y); if (! pika_item_is_text_layer (PIKA_ITEM (layer)) || convert_text) { /* For raster layers */ PikaRGB layer_color; gboolean single_color = FALSE; layer_color = get_layer_color (layer, &single_color); cairo_rectangle (cr, x, y, pika_drawable_get_width (PIKA_DRAWABLE (layer)), pika_drawable_get_height (PIKA_DRAWABLE (layer))); if (vectorize && single_color) { cairo_set_source_rgba (cr, layer_color.r, layer_color.g, layer_color.b, layer_color.a * opacity); if (mask) cairo_mask_surface (cr, mask_image, x, y); else cairo_fill (cr); } else { cairo_surface_t *layer_image; layer_image = get_cairo_surface (PIKA_DRAWABLE (layer), 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) 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, opacity, cr, x_res, y_res); } /* draw new page if "layers as pages" option is checked */ if (layers_as_pages && (! root_layers_only || layer_level == 0)) cairo_show_page (cr); /* We are done with the layer - time to free some resources */ if (mask) cairo_surface_destroy (mask_image); } return TRUE; }