2016 lines
68 KiB
C
2016 lines
68 KiB
C
|
/* 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 <lightningismyname@gmail.com>
|
||
|
*
|
||
|
* This program is free software: you can redistribute it and/or modify
|
||
|
* it under the terms of the GNU General Public License as published by
|
||
|
* the Free Software Foundation; either version 3 of the License, or
|
||
|
* (at your option) any later version.
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
|
||
|
/* The PDF export plugin has 3 main procedures:
|
||
|
* 1. file-pdf-save
|
||
|
* This is the main procedure. It has 3 options for optimizations of
|
||
|
* the pdf file, and it can show a gui. This procedure works on a single
|
||
|
* image.
|
||
|
* 2. file-pdf-save-defaults
|
||
|
* This procedures is the one that will be invoked by 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 <lightningismyname@gmail.com>
|
||
|
* First version of the plugin. This is only a proof of concept and not a full
|
||
|
* working plugin.
|
||
|
*
|
||
|
* May 6, 2009 Barak | Itkin <lightningismyname@gmail.com>
|
||
|
* Added new features and several bugfixes:
|
||
|
* - Added handling for image resolutions
|
||
|
* - fixed the 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 <lightningismyname@gmail.com>
|
||
|
* Most of the plugin was rewritten from scratch and it now has several new
|
||
|
* features:
|
||
|
* - Got rid of the optimization macros in the code. The gui now supports
|
||
|
* selecting which optimizations to apply.
|
||
|
* - Added a procedure to allow the creation of multiple paged PDF's
|
||
|
* - Registered the plugin on "<Image>/File/Create/PDF"
|
||
|
*
|
||
|
* August 21, 2009 | Barak Itkin <lightningismyname@gmail.com>
|
||
|
* Fixed a typo that prevented the plugin from compiling...
|
||
|
* A migration to the new 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 <lightningismyname@gmail.com>
|
||
|
* 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 <errno.h>
|
||
|
|
||
|
#include <glib/gstdio.h>
|
||
|
#include <cairo-pdf.h>
|
||
|
#include <pango/pangocairo.h>
|
||
|
|
||
|
#include <libpika/pika.h>
|
||
|
#include <libpika/pikaui.h>
|
||
|
|
||
|
#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,
|
||
|
const PikaValueArray *args,
|
||
|
gpointer run_data);
|
||
|
static PikaValueArray * pdf_save_multi (PikaProcedure *procedure,
|
||
|
const PikaValueArray *args,
|
||
|
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,
|
||
|
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, "<Image>/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,
|
||
|
const PikaValueArray *args,
|
||
|
gpointer run_data)
|
||
|
{
|
||
|
PikaProcedureConfig *config;
|
||
|
GError *error = NULL;
|
||
|
PikaPDBStatusType status = PIKA_PDB_SUCCESS;
|
||
|
gboolean had_saved_list = FALSE;
|
||
|
|
||
|
gegl_init (NULL, NULL);
|
||
|
|
||
|
/* Initializing all the settings */
|
||
|
multi_page.image_count = 0;
|
||
|
|
||
|
config = pika_procedure_create_config (procedure);
|
||
|
pika_procedure_config_begin_export (config, image, run_mode,
|
||
|
args, "application/pdf");
|
||
|
file_name = g_file_get_path (file);
|
||
|
|
||
|
switch (run_mode)
|
||
|
{
|
||
|
case PIKA_RUN_NONINTERACTIVE:
|
||
|
init_image_list_defaults (image);
|
||
|
break;
|
||
|
|
||
|
case PIKA_RUN_INTERACTIVE:
|
||
|
/* Possibly retrieve data */
|
||
|
had_saved_list = pika_get_data (DATA_IMAGE_LIST, &multi_page);
|
||
|
|
||
|
if (had_saved_list && (file_name == NULL || strlen (file_name) == 0))
|
||
|
{
|
||
|
file_name = multi_page.file_name;
|
||
|
}
|
||
|
|
||
|
init_image_list_defaults (image);
|
||
|
break;
|
||
|
|
||
|
case PIKA_RUN_WITH_LAST_VALS:
|
||
|
init_image_list_defaults (image);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
|
||
|
pika_procedure_config_end_export (config, image, file, status);
|
||
|
g_object_unref (config);
|
||
|
|
||
|
return pika_procedure_new_return_values (procedure, status, error);
|
||
|
}
|
||
|
|
||
|
static PikaValueArray *
|
||
|
pdf_save_multi (PikaProcedure *procedure,
|
||
|
const PikaValueArray *args,
|
||
|
gpointer run_data)
|
||
|
{
|
||
|
PikaProcedureConfig *config;
|
||
|
GError *error = NULL;
|
||
|
PikaPDBStatusType status = PIKA_PDB_SUCCESS;
|
||
|
PikaRunMode run_mode;
|
||
|
gchar *uri;
|
||
|
const gint32 *image_ids;
|
||
|
|
||
|
PikaImage *image = NULL;
|
||
|
GFile *file;
|
||
|
gboolean had_saved_list = FALSE;
|
||
|
|
||
|
gegl_init (NULL, NULL);
|
||
|
|
||
|
run_mode = PIKA_VALUES_GET_ENUM (args, 0);
|
||
|
config = pika_procedure_create_config (procedure);
|
||
|
pika_procedure_config_begin_run (config, NULL, run_mode, args);
|
||
|
g_object_get (config,
|
||
|
"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);
|
||
|
|
||
|
switch (run_mode)
|
||
|
{
|
||
|
gint i;
|
||
|
|
||
|
case PIKA_RUN_NONINTERACTIVE:
|
||
|
if (image_ids)
|
||
|
for (i = 0; i < multi_page.image_count; i++)
|
||
|
multi_page.images[i] = pika_image_get_by_id (image_ids[i]);
|
||
|
break;
|
||
|
|
||
|
case PIKA_RUN_INTERACTIVE:
|
||
|
/* Possibly retrieve data */
|
||
|
had_saved_list = pika_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 (! had_saved_list)
|
||
|
init_image_list_defaults (image);
|
||
|
break;
|
||
|
|
||
|
case PIKA_RUN_WITH_LAST_VALS:
|
||
|
/* Possibly retrieve data */
|
||
|
had_saved_list = pika_get_data (DATA_IMAGE_LIST, &multi_page);
|
||
|
if (had_saved_list)
|
||
|
file_name = multi_page.file_name;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
|
||
|
pika_procedure_config_end_run (config, status);
|
||
|
g_object_unref (config);
|
||
|
|
||
|
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)))
|
||
|
{
|
||
|
gchar *font_family;
|
||
|
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);
|
||
|
|
||
|
font_family = pika_text_layer_get_font (PIKA_TEXT_LAYER (layer));
|
||
|
font_description = pango_font_description_from_string (font_family);
|
||
|
|
||
|
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_free (font_family);
|
||
|
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);
|
||
|
|
||
|
if (! single_image)
|
||
|
{
|
||
|
g_strlcpy (multi_page.file_name, file_name, MAX_FILE_NAME_LENGTH);
|
||
|
pika_set_data (DATA_IMAGE_LIST, &multi_page, sizeof (multi_page));
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
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");
|
||
|
g_free (pika_image_get_layers (multi_page.images[0], &n_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);
|
||
|
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 *font_family;
|
||
|
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;
|
||
|
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);
|
||
|
|
||
|
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 */
|
||
|
font_family = pika_text_layer_get_font (PIKA_TEXT_LAYER (layer));
|
||
|
|
||
|
/* We need to find a way to convert PIKA's returned font name to a
|
||
|
* normal Pango name... Hopefully PIKA 2.8 with Pango will fix it.
|
||
|
*/
|
||
|
font_description = pango_font_description_from_string (font_family);
|
||
|
|
||
|
/* 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 (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 (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;
|
||
|
}
|