/*
 * PDF export Plugin for GIMP - The GNU Image Manipulation Program
 * This plugin uses the Cairo and PangoCairo to save pdf documents.
 *
 * For GIMP 2.8 or newer
 *
 * Copyright (C) 2009 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 <http://www.gnu.org/licenses/>.
 */
/*
 * REVISION HISTORY
 *
 * May 5th, 2009
 * 0.5 - Multiple changes and enhancements:
 *     * Added handling for image resolutions, and fixed the behaviour of
 *     getting font sizes (previously, getting the size of the font didn't
 *     check the unit).
 *     * Added optimization (solid rectangles instead of bitmaps wherever
 *     possible, layers with 0 opacity are not included) as a macro flag.
 *     Now, all we need is a GUI to replace this ugly macro.
 *     * Added checking for layer visibility. Hidden layers are not included.
 *     * Added handling for layer masks. As a part of this, drawables with one
 *     channel are now saved in CAIRO_FORMAT_A8 instead of CAIRO_FORMAT_RGB24.
 *     * Indexed layers are now handled correctly, without need to convert to
 *     RGB images.
 *     * Switched to GNU coding stlye.
 *
 * April 29th, 2009
 * 0.0 - A simple proof of concept. This is the first version
 *     of the plugin and right now It's considered an Alpha
 *     release. Nevertheless, this should be pretty much stable.
 *
 */
/*
 * TODO (listed by order of importance)
 *
 * - Add a GUI allowing to select multiple images as multiple pages.
 * - Ask the user if he wants optimization for the PDF
 *   Also, find some way to check an indexed drawable for optimization...
 *
 */

#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>

#include <cairo.h>
#include <cairo-pdf.h>
#include <pango/pangocairo.h>
#include <pango/pango.h>

#define SAVE_PROC      "file-pdf-save"
#define PLUG_IN_BINARY "file-pdf"
#define OPTIMIZE       TRUE

static void run (const gchar      *name,
                 gint              nparams,
                 const GimpParam  *param,
                 gint             *nreturn_vals,
                 GimpParam       **return_vals);

static void
query (void)
{
  static const GimpParamDef save_args[] =
    {
      { GIMP_PDB_INT32,    "run-mode",     "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" },
      { GIMP_PDB_IMAGE,    "image",        "Input image" },
      { GIMP_PDB_DRAWABLE, "drawable",     "Drawable to save" },
      { GIMP_PDB_STRING,   "filename",     "The name of the file to save the PDF in" },
      { GIMP_PDB_STRING,   "raw-filename", "The name entered" },
    };
  gimp_install_procedure (SAVE_PROC,
                          "Save file in PDF format.",
                          "Saves files in Portable Document Format (PDF) Format.",
                          "Barak Itkin",
                          "Barak Itkin <lightningismyname@gmail.com>",
                          "2009",
                          "Portable Document Format",
                          "RGB*, GRAY*, INDEXED*",
                          GIMP_PLUGIN,
                          G_N_ELEMENTS (save_args), 0,
                          save_args, NULL);

  gimp_register_file_handler_mime (SAVE_PROC, "application/pdf");
  gimp_register_save_handler (SAVE_PROC, "pdf", "");
}

GimpPlugInInfo PLUG_IN_INFO =
  {
    NULL,
    NULL,
    query,
    run
  };

MAIN()

  typedef struct RGBAcolor {
    double red;
    double green;
    double blue;
    double alpha;
  } RGBAcolor;

/* This function was taken from the GIMP print plugin
 * and modified to use GimpPixelRgn, in order to be able to handle
 * large images.
 * In addition, it now uses CAIRO_FORMAT_A8 where possible, and it
 * can also handle indexed layers.
 */

static cairo_surface_t *
get_drawable_image (GimpDrawable *drawable)
{
  cairo_surface_t *surface;
  cairo_format_t   format;
  guchar          *data;
  guchar          *dest;
  const guchar    *src;
  gint             dest_stride;
  gint             y;
  gint             bpp;

  gint32                         drawable_id;
  GimpPixelRgn         region;
  gint                         width;
  gint                         height;

  guchar*			 colors;
  gint			num_colors;
  gboolean indexed;

  drawable_id = drawable->drawable_id;
  width  = drawable->width;
  height = drawable->height;
  gimp_pixel_rgn_init (&region, drawable, 0, 0, width, height, FALSE, FALSE);
  bpp = region.bpp;
  data = g_new (guchar, width*height*bpp);
  gimp_pixel_rgn_get_rect (&region, data, 0, 0, width, height);

  indexed = gimp_drawable_is_indexed (drawable->drawable_id);
  if (indexed)
    colors = gimp_image_get_colormap (gimp_drawable_get_image (drawable->drawable_id), &num_colors);


  switch (bpp)
    {
    case 1: // GRAY or INDEXED
      if (!indexed)
        format = CAIRO_FORMAT_A8;
      else
        format = CAIRO_FORMAT_RGB24;
      break;
    case 3: // RGB
      format = CAIRO_FORMAT_RGB24;
      break;

    case 2: // GRAYA or INDEXEDA
    case 4: // RGBA
      format = CAIRO_FORMAT_ARGB32;
      break;

    default:
      g_assert_not_reached ();
      break;
    }

  surface = cairo_image_surface_create (format, width, height);

  src = data;

  dest        = cairo_image_surface_get_data (surface);
  dest_stride = cairo_image_surface_get_stride (surface);

  for (y = 0; y < height; y++)
    {
      const guchar *s = src;
      guchar       *d = dest;
      gint          w = width;

      switch (bpp)
        {
        case 1:
          if (!indexed) {
            while (w--)
              {
                d[0] = s[0];
                s += 1;
                d += 1;

              }
          }
          else {
            while (w--)
              {
                GIMP_CAIRO_RGB24_SET_PIXEL (d, colors[s[0]*3], colors[s[0]*3+1], colors[s[0]*3+2]);
                s += 1;
                d += 4;

              }
          }
          break;

        case 2:
          if (!indexed) {
            while (w--)
              {
                GIMP_CAIRO_ARGB32_SET_PIXEL (d, s[0], s[0], s[0], s[1]);
                s += 2;
                d += 4;
              }
          }
          else {
            while (w--)
              {
                GIMP_CAIRO_ARGB32_SET_PIXEL (d, colors[s[0]*3], colors[s[0]*3+1], colors[s[0]*3+2], s[1]);
                s += 2;
                d += 4;
              }
          }
          break;

        case 3:
          while (w--)
            {
              GIMP_CAIRO_RGB24_SET_PIXEL (d, s[0], s[1], s[2]);
              s += 3;
              d += 4;
            }
          break;

        case 4:
          while (w--)
            {
              GIMP_CAIRO_ARGB32_SET_PIXEL (d, s[0], s[1], s[2], s[3]);
              s += 4;
              d += 4;
            }
          break;
        }
      src  += width*bpp;
      dest += dest_stride;
    }
  if (indexed)
    g_free (colors);
  g_free (data);

  return surface;
}


static void query (void);

/* gimp_text_layer_get_font() may sometime return a font description which Pango doesn't like
 * however, I discovered that the font-name (in the way the user sees it) is always at the begining
 * of the returned string, and any additional information will be put after it, seperated by ','
 * so this function returns a new string which is terminated just before the first ','.
 * this string is freed later, using g_free
 */
void
fontFix (gchar* name)
{
  gchar* stop = g_strdup (name);
  gint i=0;
  for (i=0;stop[i] && stop[i] != ',';i++);
  if (stop[i])
    stop[i]=NULL;
  //	return stop;
}

RGBAcolor
getColor (GimpDrawable *layer, gboolean *single)
{
  RGBAcolor col;
  gdouble red, green, blue, alpha;
  gdouble dev, devSum;
  gdouble median, pixels, count, precentile;
  gint32 id;

  id = layer->drawable_id;
  devSum = 0;
  red = 0;
  green = 0;
  blue = 0;
  alpha = 0;
  dev = 0;

  if ( gimp_drawable_is_indexed (id)) {
    // FIXME: We can't do a propper histogram on indexed layers!
    *single = FALSE;
    col. red = col.green = col.blue = col.alpha = 0;
    return col;
  }

  if (layer->bpp >= 3) { // Are we in RGB mode?
    gimp_histogram (id, GIMP_HISTOGRAM_RED, 0, 256, &red, &dev, &median, &pixels, &count, &precentile);
    devSum += dev;
    gimp_histogram (id, GIMP_HISTOGRAM_GREEN, 0, 256, &green, &dev, &median, &pixels, &count, &precentile);
    devSum += dev;
    gimp_histogram (id, GIMP_HISTOGRAM_BLUE, 0, 256, &blue, &dev, &median, &pixels, &count, &precentile);
    devSum += dev;
  }
  else { // We are in Grayscale mode (or Indexed)
    gimp_histogram (id, GIMP_HISTOGRAM_VALUE, 0, 256, &red, &dev, &median, &pixels, &count, &precentile);
    devSum += dev;
    green = red;
    blue = red;
  }
  if (gimp_drawable_has_alpha (id))
    gimp_histogram (id, GIMP_HISTOGRAM_ALPHA, 0, 256, &alpha, &dev, &median, &pixels, &count, &precentile);
  else
    alpha = 255;

  devSum += dev;
  *single = devSum == 0;

  col.red = red/255;
  col.green = green/255;
  col.blue = blue/255;
  col.alpha = alpha/255;

  return col;
}

/* A function that uses Pango to render the text to our cairo surface, in the same way it was
 * the user saw it inside gimp.
 */
void
drawText (GimpDrawable* text_layer,
          gdouble opacity,
          cairo_t *cr,
          gdouble x_res,
          gdouble y_res)
{
  gint32 text_id = text_layer->drawable_id;
  GimpImageBaseType type = gimp_drawable_type (text_id);

  gchar* text = gimp_text_layer_get_text(text_id);;
  gchar* font_family;

  cairo_font_options_t* options;

  gint x;
  gint y;

  GimpRGB color;

  GimpUnit unit;
  gdouble size;

  GimpTextHintStyle hinting;
  cairo_hint_style_t cairo_hinting;

  GimpTextJustification j;
  gboolean justify;
  PangoAlignment align;
  GimpTextDirection dir;
  PangoDirection pango_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 ();
  
  cairo_save(cr);

  options = cairo_font_options_create ();
  attr_list = pango_attr_list_new ();
  cairo_get_font_options (cr, options);

  // Position
  gimp_drawable_offsets (text_id, &x, &y);
  cairo_move_to (cr, x, y);

  // Color
  // When dealing with a gray/indexed image, the viewed color of the text layer
  // can be different than the one kept in the memory
  if (type = GIMP_RGB)
    gimp_text_layer_get_color(text_id, &color);
  else
    gimp_image_pick_color ( gimp_drawable_get_image (text_id), text_id, x, y, FALSE, FALSE, 0, &color);
	
  cairo_set_source_rgba (cr, color.r, color.g, color.b, opacity);

  // Hinting
  hinting = gimp_text_layer_get_hint_style (text_id);
  switch (hinting)
    {
    case GIMP_TEXT_HINT_STYLE_NONE:
      cairo_hinting = CAIRO_HINT_STYLE_NONE;
      break;
    case GIMP_TEXT_HINT_STYLE_SLIGHT:
      cairo_hinting = CAIRO_HINT_STYLE_SLIGHT;
      break;
    case GIMP_TEXT_HINT_STYLE_MEDIUM:
      cairo_hinting = CAIRO_HINT_STYLE_MEDIUM;
      break;
    case GIMP_TEXT_HINT_STYLE_FULL:
      cairo_hinting = CAIRO_HINT_STYLE_FULL;
      break;
    }
  cairo_font_options_set_hint_style (options, cairo_hinting);

  // Antialias
  if (gimp_text_layer_get_antialias (text_id))
    cairo_font_options_set_antialias (options, CAIRO_ANTIALIAS_DEFAULT);
  else
    cairo_font_options_set_antialias (options, CAIRO_ANTIALIAS_NONE);

  // We are done with cairo's settings
  // Time to create the context
  cairo_set_font_options (cr, options);
  context = pango_cairo_create_context (cr);
  pango_cairo_context_set_font_options(context, options);

  // Text Direction
  dir = gimp_text_layer_get_base_direction (text_id);

  if (dir == GIMP_TEXT_DIRECTION_RTL)
    pango_dir = PANGO_DIRECTION_RTL;
  else
    pango_dir = PANGO_DIRECTION_LTR;

  pango_context_set_base_dir (context, pango_dir);

  // We are done with the context's settings
  // Time to create the layout
  layout = pango_layout_new (context);

  // Font
  font_family = gimp_text_layer_get_font (text_id);
  fontFix (font_family);
  font_description = pango_font_description_from_string (font_family);

  // Font Size
  size = gimp_text_layer_get_font_size (text_id, &unit);
  if (! g_strcmp0 (gimp_unit_get_abbreviation (unit), "px") == 0)
    size *= 1.0 / gimp_unit_get_factor(unit) * x_res / y_res;
  pango_font_description_set_absolute_size (font_description, size * PANGO_SCALE);

  pango_layout_set_font_description (layout, font_description);

  // Text
  pango_layout_set_text (layout, gimp_text_layer_get_text (text_id), -1);

  // Width and height
  pango_layout_set_width (layout, text_layer->width * PANGO_SCALE);
  pango_layout_set_height (layout, text_layer->height * PANGO_SCALE);

  // Justification,  and Aligning
  justify = FALSE;
  j = gimp_text_layer_get_justification (text_id);

  if (j == GIMP_TEXT_JUSTIFY_CENTER)
    align = PANGO_ALIGN_CENTER;
  else if (j == GIMP_TEXT_JUSTIFY_LEFT)
    align = PANGO_ALIGN_LEFT;
  else if (j == GIMP_TEXT_JUSTIFY_RIGHT)
    align = PANGO_ALIGN_RIGHT;
  else // GIMP_TEXT_JUSTIFY_FILL
    { 
      if (dir == GIMP_TEXT_DIRECTION_LTR)
        align = PANGO_ALIGN_LEFT;
      else
        align = PANGO_ALIGN_RIGHT;
      justify = TRUE;
    }

  pango_layout_set_justify (layout, justify);
  pango_layout_set_alignment(layout, align);

  // Indentation
  indent = gimp_text_layer_get_indent (text_id);
  pango_layout_set_indent (layout, (int)(PANGO_SCALE * indent));

  // Line Spacing
  line_spacing = gimp_text_layer_get_line_spacing (text_id);
  pango_layout_set_spacing (layout, (int)(PANGO_SCALE * line_spacing));

  // Letter Spacing
  letter_spacing = gimp_text_layer_get_letter_spacing (text_id);
  letter_spacing_at = pango_attr_letter_spacing_new ((int)(PANGO_SCALE * letter_spacing));
  pango_attr_list_insert (attr_list, letter_spacing_at);

  pango_layout_set_attributes (layout, attr_list);

  pango_cairo_show_layout (cr, layout);

  g_free (text);
  g_free (font_family);

  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 void
run (const gchar      *name,
     gint              nparams,
     const GimpParam  *param,
     gint             *nreturn_vals,
     GimpParam       **return_vals)
{
  static GimpParam  values[1];

  gint32 image_id;
  gint32 drawable_id = param[2].data.d_int32;
  gint32* layers;
  gint32 num_of_layers;
  GimpDrawable* layer;

  GimpImageBaseType type;

  cairo_t *cr;
  cairo_surface_t *pdf_file;
  cairo_surface_t *layer_image;

  gint x;
  gint y;
  gint width;
  gint height;
  gdouble opacity;
  gdouble x_res;
  gdouble y_res;
  gdouble x_scale;
  gdouble y_scale;

  gint i;

  gint mask_id;
  GimpDrawable* mask;

  cairo_surface_t* mask_image;


#if OPTIMIZE
  gboolean single;
  RGBAcolor myCol;
#endif

  GimpExportReturn  export = GIMP_EXPORT_CANCEL;
  GimpPDBStatusType status = GIMP_PDB_SUCCESS;

  /* Setting mandatory output values */
  *nreturn_vals = 1;
  *return_vals  = values;

  values[0].type = GIMP_PDB_STATUS;
  values[0].data.d_status = status;

  image_id = param[1].data.d_int32;

  gimp_ui_init (PLUG_IN_BINARY, FALSE);

  export = gimp_export_image (&image_id, &drawable_id, "PDF",
                              (GIMP_EXPORT_CAN_HANDLE_RGB | GIMP_EXPORT_CAN_HANDLE_ALPHA |
                               GIMP_EXPORT_CAN_HANDLE_GRAY | GIMP_EXPORT_CAN_HANDLE_LAYERS |
                               GIMP_EXPORT_CAN_HANDLE_INDEXED | GIMP_EXPORT_CAN_HANDLE_LAYER_MASKS));

  if (export == GIMP_EXPORT_CANCEL)
    {
      values[0].data.d_status = GIMP_PDB_CANCEL;
      return;
    }

	
  type = gimp_image_base_type (image_id);

  gimp_image_get_resolution (image_id, &x_res, &y_res);
  x_scale = 72.0 / x_res;
  y_scale = 72.0 / y_res;

  pdf_file = cairo_pdf_surface_create (param[3].data.d_string,
                                       gimp_image_width(image_id) * x_scale,
                                       gimp_image_height(image_id) * y_scale);

  // This way we set how many pixels are there in every inch - Important for PangoCairo
  cairo_surface_set_fallback_resolution (pdf_file, x_res, y_res);

  cr = cairo_create (pdf_file);
  // PDF is usually 72 points per inch. If we have a different resolution,
  // we will need this to fit our drawings
  cairo_scale (cr, x_scale, y_scale);

  // Do this for each image:
  layers = gimp_image_get_layers (image_id, &num_of_layers);

  for (i=0;i<num_of_layers;i++)
    {
      layer = gimp_drawable_get (layers[num_of_layers-i-1]);
      opacity = gimp_layer_get_opacity (layer->drawable_id)/100.0;

      // Gimp doesn't display indexed layers with opacity below 50%
      // And if it's above 50%, it will be rounded to 100%
      if (type == GIMP_INDEXED) {
        if (opacity <= 0.5)
          opacity = 0.0;
        else
          opacity = 1.0;
      }
	
      if (gimp_drawable_get_visible (layer->drawable_id)
#if OPTIMIZE
          && !(opacity == 0.0)
#endif
          )
        {
          mask_id = gimp_layer_get_mask (layer->drawable_id);

          if (mask_id != -1) {
            mask = gimp_drawable_get (mask_id);
            mask_image = get_drawable_image (mask);
          }

          width = gimp_drawable_width (layer->drawable_id);
          height = gimp_drawable_height (layer->drawable_id);
          gimp_drawable_offsets (layer->drawable_id, &x, &y);

          if (!gimp_drawable_is_text_layer (layer->drawable_id))
            {

              cairo_rectangle (cr, x, y, width, height);
				
#if OPTIMIZE
              myCol = getColor (layer, &single);
              if (single)
                {
                  cairo_set_source_rgba (cr, myCol.red, myCol.green, myCol.blue, myCol.alpha * opacity);
                  if (mask_id != -1)
                    cairo_mask_surface (cr, mask_image, x, y);
                  else
                    cairo_fill (cr);
                }

              else
                {
#endif
                  cairo_clip (cr);
                  layer_image = get_drawable_image (layer);
                  cairo_set_source_surface (cr, layer_image, x, y);
                  cairo_push_group (cr);
                  cairo_paint_with_alpha (cr, opacity);
                  cairo_pop_group_to_source(cr);
                  if (mask_id != -1)
                    cairo_mask_surface (cr, mask_image, x, y);
                  else
                    cairo_paint (cr);
                  cairo_surface_destroy (layer_image);
                  cairo_reset_clip (cr);
#if OPTIMIZE
                }
#endif
            }
          else
            {
              drawText (layer, opacity, cr, x_res, y_res);
            }

          if (mask_id != -1) {
            cairo_surface_destroy (mask_image);
            gimp_drawable_detach (mask);
          }
        }
      gimp_drawable_detach(layer);
    }
  cairo_show_page (cr);

  if (export == GIMP_EXPORT_EXPORT)
    gimp_image_delete (image_id);

  // End of the part to do for each image

  cairo_surface_destroy (pdf_file);
  cairo_destroy (cr);
}
