/* Copyright (C) 2013, 2014  Andreas Politz
 *
 * Author: Andreas Politz <politza@fh-trier.de>
 *
 * 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/>. */

#include <config.h>

#include <assert.h>
#ifdef HAVE_ERR_H
#  include <err.h>
#endif
#ifdef HAVE_ERROR_H
#  include <error.h>
#endif
#include <glib.h>
#include <poppler.h>
#include <cairo.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <png.h>
#include <math.h>
#include <regex.h>
#include "synctex_parser.h"
#include "epdfinfo.h"


/* ================================================================== *
 * Helper Functions
 * ================================================================== */

#ifndef HAVE_ERR_H
/**
 * Print error message and quit.
 *
 * @param eval Return code
 * @param fmt Formatting string
 */
static void
err(int eval, const char *fmt, ...)
{
  va_list args;

  fprintf (stderr, "epdfinfo: ");
  if (fmt != NULL)
    {
      va_start (args, fmt);
      vfprintf (stderr, fmt, args);
      va_end (args);
      fprintf (stderr, ": %s\n", strerror(errno));
    }
  else
    {
      fprintf (stderr, "\n");
    }

  fflush (stderr);
  exit (eval);
}
#endif

#ifndef HAVE_GETLINE
/**
 * Read one line from a file.
 *
 * @param lineptr Pointer to malloc() allocated buffer
 * @param n Pointer to size of buffer
 * @param stream File pointer to read from
 */
static ssize_t
getline(char **lineptr, size_t *n, FILE *stream)
{
  size_t len = 0;
  int ch;

  if ((lineptr == NULL) || (n == NULL))
    {
      errno = EINVAL;
      return -1;
    }

  if (*lineptr == NULL)
    {
      *lineptr = malloc (128);
      *n = 128;
    }

  while ((ch = fgetc (stream)) != EOF)
    {
      (*lineptr)[len] = ch;

      if (++len >= *n)
        {
          *n += 128;
          *lineptr = realloc (*lineptr, *n);
        }

      if (ch == '\n')
        break;
    }
  (*lineptr)[len] = '\0';

  if (!len)
    {
      len = -1;
    }

  return len;
}
#endif

/**
 * Free a list of command arguments.
 *
 * @param args An array of command arguments.
 * @param n The length of the array.
 */
static void
free_command_args (command_arg_t *args, size_t n)
{
  if (! args)
    return;

  g_free (args);
}

/**
 * Free resources held by document.
 *
 * @param doc The document to be freed.
 */
static void
free_document (document_t *doc)
{
  if (! doc)
    return;

  g_free (doc->filename);
  g_free (doc->passwd);
  if (doc->annotations.pages)
    {
      int npages = poppler_document_get_n_pages (doc->pdf);
      int i;
      for (i = 0; i < npages; ++i)
        {
          GList *item;
          GList *annots  = doc->annotations.pages[i];
          for (item = annots; item; item = item->next)
            {
              annotation_t *a = (annotation_t*) item->data;
              poppler_annot_mapping_free(a->amap);
              g_free (a->key);
              g_free (a);
            }
          g_list_free (annots);
        }
      g_hash_table_destroy (doc->annotations.keys);
      g_free (doc->annotations.pages);
    }
  g_object_unref (doc->pdf);
  g_free (doc);
}

/**
 * Parse a list of whitespace separated double values.
 *
 * @param str The input string.
 * @param values[out] Values are put here.
 * @param nvalues How many values to parse.
 *
 * @return TRUE, if str contained exactly nvalues, else FALSE.
 */
static gboolean
parse_double_list (const char *str, gdouble *values, size_t nvalues)
{
  char *end;
  int i;

  if (! str)
    return FALSE;

  errno = 0;
  for (i = 0; i < nvalues; ++i)
    {
      gdouble n = g_ascii_strtod (str, &end);

      if (str == end || errno)
        return FALSE;

      values[i] = n;
      str = end;
    }

  if (*end)
    return FALSE;

  return TRUE;
}

static gboolean
parse_rectangle (const char *str, PopplerRectangle *r)
{
  gdouble values[4];

  if (! r)
    return FALSE;

  if (! parse_double_list (str, values, 4))
    return FALSE;

  r->x1 = values[0];
  r->y1 = values[1];
  r->x2 = values[2];
  r->y2 = values[3];

  return TRUE;
}

static gboolean
parse_edges_or_position (const char *str, PopplerRectangle *r)
{
  return (parse_rectangle (str, r)
          && r->x1 >= 0 && r->x1 <= 1
          && r->x2 <= 1
          && r->y1 >= 0 && r->y1 <= 1
          && r->y2 <= 1);
}

static gboolean
parse_edges (const char *str, PopplerRectangle *r)
{
  return (parse_rectangle (str, r)
          && r->x1 >= 0 && r->x1 <= 1
          && r->x2 >= 0 && r->x2 <= 1
          && r->y1 >= 0 && r->y1 <= 1
          && r->y2 >= 0 && r->y2 <= 1);
}

/**
 * Print a string properly escaped for a response.
 *
 * @param str The string to be printed.
 * @param suffix_char Append a newline if NEWLINE, a colon if COLON.
 */
static void
print_response_string (const char *str, enum suffix_char suffix)
{
  if (str)
    {
      while (*str)
        {
          switch (*str)
            {
            case '\n':
              printf ("\\n");
              break;
            case '\\':
              printf ("\\\\");
              break;
            case ':':
              printf ("\\:");
              break;
            default:
              putchar (*str);
            }
          ++str;
        }
    }

  switch (suffix)
    {
    case NEWLINE:
      putchar ('\n');
      break;
    case COLON:
      putchar (':');
      break;
    default: ;
    }
}


/**
 * Print a formatted error response.
 *
 * @param fmt The printf-like format string.
 */
static void
printf_error_response (const char *fmt, ...)
{
  va_list va;
  puts ("ERR");
  va_start (va, fmt);
  vprintf (fmt, va);
  va_end (va);
  puts ("\n.");
  fflush (stdout);
}

/**
 * Remove one trailing newline character.  Does nothing, if str does
 * not end with a newline.
 *
 * @param str The string.
 *
 * @return str with trailing newline removed.
 */
static char*
strchomp (char *str)
{
  size_t length;

  if (! str)
    return str;

  length = strlen (str);
  if (str[length - 1] == '\n')
    str[length - 1] = '\0';

  return str;
}

/**
 * Create a new, temporary file and returns its name.
 *
 * @return The filename.
 */
static char*
mktempfile()
{
#if defined (HAVE__TEMPNAM)
  char *filename = NULL;
  int tries = 3;
  while (! filename && tries-- > 0)
    {

      filename =  _tempnam(NULL, "epdfinfo");
      if (filename)
        {
          int fd = open(filename, O_CREAT | O_EXCL | O_RDONLY, S_IRUSR | S_IWUSR);
          if (fd > 0)
            close (fd);
          else
            {
              free (filename);
              filename = NULL;
            }
        }
    }
  if (! filename)
    fprintf (stderr, "Unable to create tempfile");
#else
  char template[] = P_tmpdir "/epdfinfoXXXXXX";
  char *filename = malloc(sizeof(template));
  memcpy(filename, template, sizeof(template));
  int fd = mkstemp(filename);
  if (fd == -1)
    {
      fprintf (stderr, "Unable to create tempfile");
      free(filename);
      filename = NULL;
    }
  else
    close(fd);
#endif
  return filename;
}

/* Holds RGB, HSL, HSV, Lab, or Lch... but note that the order in memory for HSL
 * and HSV are actually VSH and LSH. */
struct color
{
  union
  {
    double r, v, l;
  };
  union
  {
    double g, s, a;
  };
  union
  {
    double b, h;
  };
};

#define struct_color(x) (*((struct color *) x))
#define vec_color(x) ((double *) &x)

// Using values reported at https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
// instead of going through xyz. This ensures any whitepoint is ignored
static struct color
rgb2oklab(struct color rgb)
{
  struct color srgb;

  for (int i = 0; i < 3; i++)
    {
      double val = vec_color(rgb)[i];
      vec_color(srgb)[i] = ((val > 0.04045)
                            ? pow((val + 0.055) / 1.055, 2.4)
                            : (val / 12.92));
    }

  double l = 0.4121656120 * srgb.r + 0.5362752080 * srgb.g + 0.0514575653 * srgb.b;
  double m = 0.2118591070 * srgb.r + 0.6807189584 * srgb.g + 0.1074065790 * srgb.b;
  double s = 0.0883097947 * srgb.r + 0.2818474174 * srgb.g + 0.6302613616 * srgb.b;

  l = cbrt(l);
  m = cbrt(m);
  s = cbrt(s);

  return (struct color) {
    .l = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
    .a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
    .b = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s
  };
}

static double clamp(double x, double low, double high)
{
  return ((x < low)
          ? low
          : ((x > high) ? high : x));
}

static struct color
oklab2rgb(struct color lab)
{
  double l = lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
  double m = lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
  double s = lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b;

  l = l * l * l;
  m = m * m * m;
  s = s * s * s;

  struct color srgb = {
    .r =  4.0767245293 * l - 3.3072168827 * m + 0.2307590544 * s,
    .g = -1.2681437731 * l + 2.6093323231 * m - 0.3411344290 * s,
    .b = -0.0041119885 * l - 0.7034763098 * m + 1.7068625689 * s
  };

  struct color rgb;
  for (int i = 0; i < 3; i++)
    {
      double val = vec_color(srgb)[i];
      val = ((val > 0.0031308)
             ? (1.055 * pow(val, 1 / 2.4) - 0.055)
             : (12.92 * val));

      vec_color(rgb)[i] = clamp(val, 0.0, 1.0);
    }

  return rgb;
}

#undef struct_color
#undef vec_color

static inline gboolean color_equal(struct color a, struct color b)
{
  return (a.r == b.r && a.g == b.g && a.b == b.b);
}

static void
image_recolor (cairo_surface_t * surface, const PopplerColor * fg,
               const PopplerColor * bg, int usecolors,
               double gamma, int gammabeforeinvert)
{
  /* Performs one of two kinds of image recoloring depending on the value of usecolors:

     1 -> Bg-Fg Interpolation: maps source document colors to colors
          interpolated between the background and foreground values in
          pdf-view-midnight-colors via the lightness of the source color.  This
          discards hue information but allows you to fit your color theme
          perfectly.

     2 -> Hue-Preserving interpolation: same as above, similar to above, but
          attempts to preserve hue while still respecting the background and
          foreground colors.  This is done by matching source document white and
          black to the specified background and foreground as above, but mixes
          hue/saturation with the background color.  This preserves hue but is
          more expensive.
  */

  const unsigned int page_width = cairo_image_surface_get_width (surface);
  const unsigned int page_height = cairo_image_surface_get_height (surface);
  const int rowstride = cairo_image_surface_get_stride (surface);
  unsigned char *image = cairo_image_surface_get_data (surface);

  /* RGB weights for computing lightness. Must sum to one */
  static const double a[] = { 0.30, 0.59, 0.11 };

  const double f = 65535.;
  const struct color rgb_fg = {
    .r = fg->red / f,
    .g = fg->green / f,
    .b = fg->blue / f
  };
  const struct color rgb_bg = {
    .r = bg->red / f,
    .g = bg->green / f,
    .b = bg->blue / f
  };

  const struct color rgb_diff = {
    .r = rgb_bg.r - rgb_fg.r,
    .g = rgb_bg.g - rgb_fg.g,
    .b = rgb_bg.b - rgb_fg.b
  };

  switch (usecolors)
    {
    case 0:
      break;
    case 1:
      {
        unsigned int y;
        for (y = 0; y < page_height * rowstride; y += rowstride)
          {
            unsigned char *data = image + y;

            unsigned int x;
            for (x = 0; x < page_width; x++, data += 4)
              {
                /* Careful. data color components blue, green, red. */
                struct color rgb = {
                  .r = (double) data[2] / 255.,
                  .g = (double) data[1] / 255.,
                  .b = (double) data[0] / 255.
                };

                /* Linear interpolation between bg and fg based on the
                   perceptual lightness measure l */
                /* compute h, s, l data   */
                double l = a[0] * rgb.r + a[1] * rgb.g + a[2] * rgb.b;

                /* linear interpolation between dark and light with color
                   lightness as a parameter */
                data[2] =
                  (unsigned char) round (255. * (l * rgb_diff.r + rgb_fg.r));
                data[1] =
                  (unsigned char) round (255. * (l * rgb_diff.g + rgb_fg.g));
                data[0] =
                  (unsigned char) round (255. * (l * rgb_diff.b + rgb_fg.b));
              }
          }
      }
      break;
    case 2:
      {
        /* If using the Oklab transform, it is relatively expensive.  Precompute
           white->background and black->foreground and have a single entry cache to
           speed up computation */
        const struct color white = {.r = 1.0, .g = 1.0, .b = 1.0};
        const struct color black = {.r = 0.0, .g = 0.0, .b = 0.0};
        struct color precomputed_rgb = white;
        struct color precomputed_inv_rgb = rgb_bg;

        /* Must match the transformation of colors below. */
        struct color oklab_fg = rgb2oklab(rgb_fg);
        struct color oklab_bg = rgb2oklab(rgb_bg);

        unsigned int y;
        for (y = 0; y < page_height * rowstride; y += rowstride)
          {
            unsigned char *data = image + y;

            unsigned int x;
            for (x = 0; x < page_width; x++, data += 4)
              {
                /* Careful. data color components blue, green, red. */
                struct color rgb = {
                  .r = (double) data[2] / 255.,
                  .g = (double) data[1] / 255.,
                  .b = (double) data[0] / 255.
                };

                /* Convert to Oklab coordinates, invert perceived lightness,
                   convert back to RGB. */
                if (color_equal(white, rgb))
                  {
                    rgb = rgb_bg;
                  }
                else if (color_equal(black, rgb))
                  {
                    rgb = rgb_fg;
                  }
                else if (color_equal(precomputed_rgb, rgb))
                  {
                    rgb = precomputed_inv_rgb;
                  }
                else
                  {
                    struct color oklab = rgb2oklab(rgb);
                    precomputed_rgb = rgb;

                    /* Invert the perceived lightness, and scales it */
                    double l = oklab.l;
                    double inv_l = 1.0 - l;

                    /* Nonlinearly scale lightness */
                    if (gammabeforeinvert)
                      {
                        l = pow(l, gamma);
                        inv_l = 1.0 - l;
                      }
                    else
                      {
                        inv_l = pow(inv_l, gamma);
                        l = 1.0 - inv_l;
                      }

                    oklab.l = oklab_bg.l * l + oklab_fg.l * inv_l;

                    /* Have a and b parameters (which encode hue and saturation)
                       start at the background value and interpolate up to
                       foreground */
                    oklab.a = (oklab.a + oklab_bg.a * l + oklab_fg.a * inv_l);
                    oklab.b = (oklab.b + oklab_bg.b * l + oklab_fg.b * inv_l);

                    rgb = oklab2rgb(oklab);

                    precomputed_inv_rgb = rgb;
                  }

                data[2] = (unsigned char) round(255. * rgb.r);
                data[1] = (unsigned char) round(255. * rgb.g);
                data[0] = (unsigned char) round(255. * rgb.b);
              }
          }
      }
      break;
    default:
      internal_error ("image_recolor switch fell through");
    }
}

/**
 * Render a PDF page.
 *
 * @param pdf The PDF document.
 * @param page The page to be rendered.
 * @param width The desired width of the image.
 *
 * @return A cairo_t context encapsulating the rendered image, or
 *         NULL, if rendering failed for some reason.
 */
static cairo_surface_t*
image_render_page(PopplerDocument *pdf, PopplerPage *page,
                  int width, gboolean do_render_annotaions,
                  const render_options_t *options)
{
  cairo_t *cr = NULL;
  cairo_surface_t *surface = NULL;
  double pt_width, pt_height;
  int height;
  double scale = 1;

  if (! page || ! pdf)
    return NULL;

  if (width < 1)
    width = 1;

  poppler_page_get_size (page, &pt_width, &pt_height);
  scale = width / pt_width;
  height = (int) ((scale * pt_height) + 0.5);

  surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                        width, height);

  if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS)
    {
      fprintf (stderr, "Failed to create cairo surface\n");
      goto error;
    }

  cr = cairo_create (surface);
  if (cairo_status(cr) != CAIRO_STATUS_SUCCESS)
    {
      fprintf (stderr, "Failed to create cairo handle\n");
      goto error;
    }

  cairo_translate (cr, 0, 0);
  cairo_scale (cr, scale, scale);
  /* Render w/o annotations. */
  if (! do_render_annotaions || (options && options->printed))
    poppler_page_render_for_printing_with_options
      (page, cr, POPPLER_PRINT_DOCUMENT);
  else
    poppler_page_render (page, cr) ;
  if (cairo_status(cr) != CAIRO_STATUS_SUCCESS)
    {
      fprintf (stderr, "Failed to render page\n");
      goto error;
    }

  /* This makes the colors look right. */
  cairo_set_operator (cr, CAIRO_OPERATOR_DEST_OVER);
  cairo_set_source_rgb (cr, 1., 1., 1.);

  cairo_paint (cr);

  if (options && (options->usecolors))
    image_recolor (surface, &options->fg, &options->bg, options->usecolors,
                   options->gamma, options->gammabeforeinvert);

  cairo_destroy (cr);

  return surface;

 error:
  if (surface != NULL)
    cairo_surface_destroy (surface);
  if (cr != NULL)
    cairo_destroy (cr);
  return NULL;
}

/**
 * Write an image to a filename.
 *
 * @param cr The cairo context encapsulating the image.
 * @param filename The filename to be written to.
 * @param type The desired image type.
 *
 * @return 1 if the image was written successfully, else 0.
 */
static gboolean
image_write (cairo_surface_t *surface, const char *filename, enum image_type type)
{

  int i, j;
  unsigned char *data;
  int width, height;
  FILE *file = NULL;
  gboolean success = 0;

  if (! surface ||
      cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS)
    {
      fprintf (stderr, "Invalid cairo surface\n");
      return 0;
    }

  if (! (file = fopen (filename, "wb")))
    {
      fprintf (stderr, "Can not open file: %s\n", filename);
      return 0;
    }

  cairo_surface_flush (surface);
  width = cairo_image_surface_get_width (surface);
  height = cairo_image_surface_get_height (surface);
  data = cairo_image_surface_get_data (surface);

  switch (type)
    {
    case PPM:
      {
        unsigned char *buffer = g_malloc (width * height * 3);
        unsigned char *buffer_p = buffer;

        fprintf (file, "P6\n%d %d\n255\n", width, height);
        for (i = 0; i < width * height; ++i, data += 4, buffer_p += 3)
          ARGB_TO_RGB (buffer_p, data);
        fwrite (buffer, 1, width * height * 3, file);
        g_free (buffer);
        success = 1;
      }
      break;
    case PNG:
      {
        png_infop info_ptr = NULL;
        png_structp png_ptr = NULL;
        unsigned char *row = NULL;

        png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
        if (!png_ptr)
          goto finalize;

        info_ptr = png_create_info_struct(png_ptr);
        if (!info_ptr)
          goto finalize;

        if (setjmp(png_jmpbuf(png_ptr)))
          goto finalize;

        png_init_io (png_ptr, file);
        png_set_compression_level (png_ptr, 1);
        png_set_IHDR (png_ptr, info_ptr, width, height,
                      8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
                      PNG_COMPRESSION_TYPE_BASE,
                      PNG_FILTER_TYPE_DEFAULT);

        png_set_filter (png_ptr, PNG_FILTER_TYPE_BASE,
                        PNG_FILTER_NONE);
        png_write_info (png_ptr, info_ptr);
        row = g_malloc (3 * width);
        for (i = 0; i < height; ++i)
          {
            unsigned char *row_p = row;
            for (j = 0; j < width; ++j, data += 4, row_p += 3)
              {
                ARGB_TO_RGB (row_p, data);
              }
            png_write_row (png_ptr, row);
          }
        png_write_end (png_ptr, NULL);
        success = 1;
      finalize:
        if (png_ptr)
          png_destroy_write_struct (&png_ptr, &info_ptr);
        if (row)
          g_free (row);
        if (! success)
          fprintf (stderr, "Error writing png data\n");
      }
      break;
    default:
      internal_error ("switch fell through");
    }

  fclose (file);
  return success;
}

static void
image_write_print_response(cairo_surface_t *surface, enum image_type type)
{
  char *filename = mktempfile ();

  perror_if_not (filename, "Unable to create temporary file");
  if (image_write (surface, filename, type))
    {
      OK_BEGIN ();
      print_response_string (filename, NEWLINE);
      OK_END ();
    }
  else
    {
      printf_error_response ("Unable to write image");
    }
  free (filename);
 error:
  return;
}

static void
region_print (cairo_region_t *region, double width, double height)
{
  int i;

  for (i = 0; i < cairo_region_num_rectangles (region); ++i)
    {
      cairo_rectangle_int_t r;

      cairo_region_get_rectangle (region, i, &r);
      printf ("%f %f %f %f",
              r.x / width,
              r.y / height,
              (r.x + r.width) / width,
              (r.y + r.height) / height);
      if (i < cairo_region_num_rectangles (region) - 1)
        putchar (':');
    }
  if (0 == cairo_region_num_rectangles (region))
    printf ("0.0 0.0 0.0 0.0");
}

/**
 * Return a string representation of a PopplerActionType.
 *
 * @param type The PopplerActionType.
 *
 * @return Its string representation.
 */
static const char *
xpoppler_action_type_string(PopplerActionType type)
{
  switch (type)
    {
    case POPPLER_ACTION_UNKNOWN: return "unknown";
    case POPPLER_ACTION_NONE: return "none";
    case POPPLER_ACTION_GOTO_DEST: return "goto-dest";
    case POPPLER_ACTION_GOTO_REMOTE: return "goto-remote";
    case POPPLER_ACTION_LAUNCH: return "launch";
    case POPPLER_ACTION_URI: return "uri";
    case POPPLER_ACTION_NAMED: return "goto-dest"; /* actually "named" */
    case POPPLER_ACTION_MOVIE: return "movie";
    case POPPLER_ACTION_RENDITION: return "rendition";
    case POPPLER_ACTION_OCG_STATE: return "ocg-state";
    case POPPLER_ACTION_JAVASCRIPT: return "javascript";
    default: return "invalid";
    }
}

/**
 * Return a string representation of a PopplerAnnotType.
 *
 * @param type The PopplerAnnotType.
 *
 * @return Its string representation.
 */
static const char *
xpoppler_annot_type_string (PopplerAnnotType type)
{
  switch (type)
    {
    case POPPLER_ANNOT_UNKNOWN: return "unknown";
    case POPPLER_ANNOT_TEXT: return "text";
    case POPPLER_ANNOT_LINK: return "link";
    case POPPLER_ANNOT_FREE_TEXT: return "free-text";
    case POPPLER_ANNOT_LINE: return "line";
    case POPPLER_ANNOT_SQUARE: return "square";
    case POPPLER_ANNOT_CIRCLE: return "circle";
    case POPPLER_ANNOT_POLYGON: return "polygon";
    case POPPLER_ANNOT_POLY_LINE: return "poly-line";
    case POPPLER_ANNOT_HIGHLIGHT: return "highlight";
    case POPPLER_ANNOT_UNDERLINE: return "underline";
    case POPPLER_ANNOT_SQUIGGLY: return "squiggly";
    case POPPLER_ANNOT_STRIKE_OUT: return "strike-out";
    case POPPLER_ANNOT_STAMP: return "stamp";
    case POPPLER_ANNOT_CARET: return "caret";
    case POPPLER_ANNOT_INK: return "ink";
    case POPPLER_ANNOT_POPUP: return "popup";
    case POPPLER_ANNOT_FILE_ATTACHMENT: return "file";
    case POPPLER_ANNOT_SOUND: return "sound";
    case POPPLER_ANNOT_MOVIE: return "movie";
    case POPPLER_ANNOT_WIDGET: return "widget";
    case POPPLER_ANNOT_SCREEN: return "screen";
    case POPPLER_ANNOT_PRINTER_MARK: return "printer-mark";
    case POPPLER_ANNOT_TRAP_NET: return "trap-net";
    case POPPLER_ANNOT_WATERMARK: return "watermark";
    case POPPLER_ANNOT_3D: return "3d";
    default: return "invalid";
    }
}

/**
 * Return a string representation of a PopplerAnnotTextState.
 *
 * @param type The PopplerAnnotTextState.
 *
 * @return Its string representation.
 */
static const char *
xpoppler_annot_text_state_string (PopplerAnnotTextState state)
{
  switch (state)
    {
    case POPPLER_ANNOT_TEXT_STATE_MARKED: return "marked";
    case POPPLER_ANNOT_TEXT_STATE_UNMARKED: return "unmarked";
    case POPPLER_ANNOT_TEXT_STATE_ACCEPTED: return "accepted";
    case POPPLER_ANNOT_TEXT_STATE_REJECTED: return "rejected";
    case POPPLER_ANNOT_TEXT_STATE_CANCELLED: return "cancelled";
    case POPPLER_ANNOT_TEXT_STATE_COMPLETED: return "completed";
    case POPPLER_ANNOT_TEXT_STATE_NONE: return "none";
    case POPPLER_ANNOT_TEXT_STATE_UNKNOWN:
    default: return "unknown";
    }
};

/**
 * Validate a PopplerSelectionStyle by replacing invalid styles
 * with a default of POPPLER_SELECTION_GLYPH.
 *
 * @param selection_style The selection style.
 *
 * @return selection_style for valid styles, otherwise POPPLER_SELECTION_GLYPH.
 */
static PopplerSelectionStyle
xpoppler_validate_selection_style (int selection_style)
{
  switch (selection_style) {
    case POPPLER_SELECTION_GLYPH:
    case POPPLER_SELECTION_WORD:
    case POPPLER_SELECTION_LINE:
      return selection_style;
  }
  return POPPLER_SELECTION_GLYPH;
}

static document_t*
document_open (const epdfinfo_t *ctx, const char *filename,
               const char *passwd, GError **gerror)
{
  char *uri;
  document_t *doc = g_hash_table_lookup (ctx->documents, filename);

  if (NULL != doc)
    return doc;

  doc = g_malloc0(sizeof (document_t));
  uri = g_filename_to_uri (filename, NULL, gerror);
  if (uri != NULL)
    doc->pdf = poppler_document_new_from_file(uri, passwd, gerror);

  if (NULL == doc->pdf)
    {
      g_free (doc);
      doc = NULL;
    }
  else
    {
      doc->filename = g_strdup (filename);
      doc->passwd = g_strdup (passwd);
      g_hash_table_insert (ctx->documents, doc->filename, doc);
    }
  g_free (uri);
  return doc;
}

/**
 * Split command args into a list of strings.
 *
 * @param args The colon separated list of arguments.
 * @param nargs[out] The number of returned arguments.
 *
 * @return The list of arguments, which should be freed by the caller.
 */
static char **
command_arg_split (const char *args, int *nargs)
{
  char **list = g_malloc (sizeof (char*) * 16);
  int i = 0;
  size_t allocated = 16;
  char *buffer = NULL;
  gboolean last = FALSE;

  if (! args)
    goto theend;

  buffer = g_malloc (strlen (args) + 1);

  while (*args || last)
    {
      gboolean esc = FALSE;
      char *buffer_p = buffer;

      while (*args && (*args != ':' || esc))
        {
          if (esc)
            {
              if (*args == 'n')
                {
                  ++args;
                  *buffer_p++ = '\n';
                }
              else
                {
                  *buffer_p++ = *args++;
                }
              esc = FALSE;
            }
          else if (*args == '\\')
            {
              ++args;
              esc = TRUE;
            }
          else
            {
              *buffer_p++ = *args++;
            }
        }

      *buffer_p = '\0';

      if (i >= allocated)
        {
          allocated = 2 * allocated + 1;
          list = g_realloc (list, sizeof (char*) * allocated);
        }
      list[i++] = g_strdup (buffer);

      last = FALSE;
      if (*args)
        {
          ++args;
          if (! *args)
            last = TRUE;
        }
    }

 theend:
  g_free (buffer);
  *nargs = i;

  return list;
}

static gboolean
command_arg_parse_arg (const epdfinfo_t *ctx, const char *arg,
                       command_arg_t *cmd_arg, command_arg_type_t type,
                       gchar **error_msg)
{
  GError *gerror = NULL;

  if (! arg || !cmd_arg)
    return FALSE;

  switch (type)
    {
    case ARG_DOC:
      {
        document_t *doc = document_open (ctx, arg, NULL, &gerror);
        cerror_if_not (doc, error_msg,
                       "Error opening %s:%s", arg,
                       gerror ? gerror->message : "Unknown reason");

        cmd_arg->value.doc = doc;
        break;
      }
    case ARG_BOOL:
      cerror_if_not (! strcmp (arg, "0") || ! strcmp (arg, "1"),
                     error_msg, "Expected 0 or 1:%s", arg);
      cmd_arg->value.flag = *arg == '1';
      break;
    case ARG_DOUBLE:
      {
        char *endptr;
        double n = strtod (arg, &endptr);
        cerror_if_not (! (*endptr),
                       error_msg, "Expected double (floating point): %s", arg);
        cmd_arg->value.scalar = n;
      }
      break;
    case ARG_NONEMPTY_STRING:
      cerror_if_not (*arg, error_msg, "Non-empty string expected");
      /* fall through */
    case ARG_STRING:
      cmd_arg->value.string = arg;
      break;
    case ARG_NATNUM:
      {
        char *endptr;
        long n = strtol (arg, &endptr, 0);
        cerror_if_not (! (*endptr || (n < 0)), error_msg,
                       "Expected natural number:%s", arg);
        cmd_arg->value.natnum = n;
      }
      break;
    case ARG_EDGES_OR_POSITION:
      {
        PopplerRectangle *r = &cmd_arg->value.rectangle;
        cerror_if_not (parse_edges_or_position (arg, r),
                       error_msg,
                       "Expected a relative position or rectangle: %s", arg);
      }
      break;
    case ARG_EDGES:
      {
        PopplerRectangle *r = &cmd_arg->value.rectangle;
        cerror_if_not (parse_edges (arg, r),
                       error_msg,
                       "Expected a relative rectangle: %s", arg);
      }
      break;
    case ARG_EDGE_OR_NEGATIVE:
    case ARG_EDGE:
      {
        char *endptr;
        double n = strtod (arg, &endptr);
        cerror_if_not (! (*endptr || (type != ARG_EDGE_OR_NEGATIVE && n < 0.0) || n > 1.0),
                       error_msg, "Expected a relative edge: %s", arg);
        cmd_arg->value.edge = n;
      }
      break;
    case ARG_COLOR:
      {
        guint r,g,b;
        cerror_if_not ((strlen (arg) == 7
                        && 3 == sscanf (arg, "#%2x%2x%2x", &r, &g, &b)),
                       error_msg, "Invalid color: %s", arg);
        cmd_arg->value.color.red = r << 8;
        cmd_arg->value.color.green = g << 8;
        cmd_arg->value.color.blue = b << 8;
      }
      break;
    case ARG_INVALID:
    default:
      internal_error ("switch fell through");
    }

  cmd_arg->type = type;

  return TRUE;
 error:
  if (gerror)
    {
      g_error_free (gerror);
      gerror = NULL;
    }
  return FALSE;
}

/**
 * Parse arguments for a command.
 *
 * @param ctx The epdfinfo context.
 * @param args A string holding the arguments.  This is either empty
 *             or the suffix of the command starting at the first
 *             colon after the command name.
 * @param len The length of args.
 * @param cmd The command for which the arguments should be parsed.
 *
 * @return
 */
static command_arg_t*
command_arg_parse(epdfinfo_t *ctx, char **args, int nargs,
                  const command_t *cmd, gchar **error_msg)
{
  command_arg_t *cmd_args = g_malloc0 (cmd->nargs * sizeof (command_arg_t));
  int i;

  if (nargs < cmd->nargs - 1
      || (nargs == cmd->nargs - 1
          &&  cmd->args_spec[cmd->nargs - 1] != ARG_REST)
      || (nargs > cmd->nargs
          && (cmd->nargs == 0
              || cmd->args_spec[cmd->nargs - 1] != ARG_REST)))
    {
      if (error_msg)
        {
          *error_msg =
            g_strdup_printf ("Command `%s' expects %d argument(s), %d given",
                             cmd->name, cmd->nargs, nargs);
        }
      goto failure;
    }

  for (i = 0; i < cmd->nargs; ++i)
    {
      if (i == cmd->nargs - 1 && cmd->args_spec[i] == ARG_REST)
        {
          cmd_args[i].value.rest.args = args + i;
          cmd_args[i].value.rest.nargs = nargs - i;
          cmd_args[i].type = ARG_REST;
        }
      else if (i >= nargs
               || ! command_arg_parse_arg (ctx, args[i], cmd_args + i,
                                           cmd->args_spec[i], error_msg))
        {
          goto failure;
        }
    }

  return cmd_args;

 failure:
  free_command_args (cmd_args, cmd->nargs);
  return NULL;
}

static void
command_arg_print(const command_arg_t *arg)
{
  switch (arg->type)
    {
    case ARG_INVALID:
      printf ("[invalid]");
      break;
    case ARG_DOC:
      print_response_string (arg->value.doc->filename, NONE);
      break;
    case ARG_BOOL:
      printf ("%d", arg->value.flag ? 1 : 0);
      break;
    case ARG_DOUBLE:
      printf ("%f", arg->value.scalar);
      break;
    case ARG_NONEMPTY_STRING:   /* fall */
    case ARG_STRING:
      print_response_string (arg->value.string, NONE);
      break;
    case ARG_NATNUM:
      printf ("%ld", arg->value.natnum);
      break;
    case ARG_EDGE_OR_NEGATIVE:  /* fall */
    case ARG_EDGE:
      printf ("%f", arg->value.edge);
      break;
    case ARG_EDGES_OR_POSITION: /* fall */
    case ARG_EDGES:
      {
        const PopplerRectangle *r = &arg->value.rectangle;
        if (r->x2 < 0 && r->y2 < 0)
          printf ("%f %f", r->x1, r->y1);
        else
          printf ("%f %f %f %f", r->x1, r->y1, r->x2, r->y2);
        break;
      }
    case ARG_COLOR:
      {
        const PopplerColor *c = &arg->value.color;
        printf ("#%.2x%.2x%.2x", c->red >> 8,
                c->green >> 8, c->blue >> 8);
        break;
      }
    case ARG_REST:
      {
        int i;
        for (i = 0; i < arg->value.rest.nargs; ++i)
          print_response_string (arg->value.rest.args[i], COLON);
        if (arg->value.rest.nargs > 0)
          print_response_string (arg->value.rest.args[i], NONE);
        break;
      }
    default:
      internal_error ("switch fell through");
    }
}

static size_t
command_arg_type_size(command_arg_type_t type)
{
  command_arg_t arg;
  switch (type)
    {
    case ARG_INVALID: return 0;
    case ARG_DOC: return sizeof (arg.value.doc);
    case ARG_BOOL: return sizeof (arg.value.flag);
    case ARG_DOUBLE: return sizeof (arg.value.scalar);
    case ARG_NONEMPTY_STRING:   /* fall */
    case ARG_STRING: return sizeof (arg.value.string);
    case ARG_NATNUM: return sizeof (arg.value.natnum);
    case ARG_EDGE_OR_NEGATIVE:  /* fall */
    case ARG_EDGE: return sizeof (arg.value.edge);
    case ARG_EDGES_OR_POSITION: /* fall */
    case ARG_EDGES: return sizeof (arg.value.rectangle);
    case ARG_COLOR: return sizeof (arg.value.color);
    case ARG_REST: return sizeof (arg.value.rest);
    default:
      internal_error ("switch fell through");
      return 0;
    }
}


/* ------------------------------------------------------------------ *
 * PDF Actions
 * ------------------------------------------------------------------ */

static gboolean
action_is_handled (PopplerAction *action)
{
  if (! action)
    return FALSE;

  switch (action->any.type)
    {
    case POPPLER_ACTION_GOTO_REMOTE:
    case POPPLER_ACTION_GOTO_DEST:
    case POPPLER_ACTION_NAMED:
      /* case POPPLER_ACTION_LAUNCH: */
    case POPPLER_ACTION_URI:
      return TRUE;
    default: ;
    }
  return FALSE;
}

static void
action_print_destination (PopplerDocument *doc, PopplerAction *action)
{
  PopplerDest *dest = NULL;
  gboolean free_dest = FALSE;
  double width, height, top;
  PopplerPage *page;
  int saved_stdin;

  if (action->any.type == POPPLER_ACTION_GOTO_DEST
      && action->goto_dest.dest->type == POPPLER_DEST_NAMED)
    {
      DISCARD_STDOUT (saved_stdin);
      /* poppler_document_find_dest reports errors to stdout, so
         discard them. */
      dest = poppler_document_find_dest
        (doc, action->goto_dest.dest->named_dest);
      UNDISCARD_STDOUT (saved_stdin);
      free_dest = TRUE;
    }
  else if (action->any.type == POPPLER_ACTION_NAMED)

    {
      DISCARD_STDOUT (saved_stdin);
      dest = poppler_document_find_dest (doc, action->named.named_dest);
      UNDISCARD_STDOUT (saved_stdin);
      free_dest = TRUE;
    }

  else if (action->any.type == POPPLER_ACTION_GOTO_REMOTE)
    {
      print_response_string (action->goto_remote.file_name, COLON);
      dest = action->goto_remote.dest;
    }
  else if (action->any.type == POPPLER_ACTION_GOTO_DEST)
    dest = action->goto_dest.dest;

  if (!dest
      || dest->type == POPPLER_DEST_UNKNOWN
      || dest->page_num < 1
      || dest->page_num > poppler_document_get_n_pages (doc))
    {
      printf (":");
      goto theend;
    }

  printf ("%d:", dest->page_num);

  if (action->type == POPPLER_ACTION_GOTO_REMOTE
      || NULL == (page = poppler_document_get_page (doc, dest->page_num - 1)))
    {
      goto theend;
    }

  poppler_page_get_size (page, &width, &height);
  g_object_unref (page);
  top = (height - dest->top) / height;

  /* adapted from xpdf */
  switch (dest->type)
    {
    case POPPLER_DEST_XYZ:
      if (dest->change_top)
        printf ("%f", top);
      break;
    case POPPLER_DEST_FIT:
    case POPPLER_DEST_FITB:
    case POPPLER_DEST_FITH:
    case POPPLER_DEST_FITBH:
      putchar ('0');
      break;
    case POPPLER_DEST_FITV:
    case POPPLER_DEST_FITBV:
    case POPPLER_DEST_FITR:
      printf ("%f", top);
      break;
    default: ;
    }

 theend:
  if (free_dest)
    poppler_dest_free (dest);
}

static void
action_print (PopplerDocument *doc, PopplerAction *action)
{
  if (! action_is_handled (action))
    return;

  print_response_string (xpoppler_action_type_string (action->any.type), COLON);
  print_response_string (action->any.title, COLON);
  switch (action->any.type)
    {
    case POPPLER_ACTION_GOTO_REMOTE:
    case POPPLER_ACTION_GOTO_DEST:
    case POPPLER_ACTION_NAMED:
      action_print_destination (doc, action);
      putchar ('\n');
      break;
    case POPPLER_ACTION_LAUNCH:
      print_response_string (action->launch.file_name, COLON);
      print_response_string (action->launch.params, NEWLINE);
      break;
    case POPPLER_ACTION_URI:
      print_response_string (action->uri.uri, NEWLINE);
      break;
    default:
      ;
    }
}


/* ------------------------------------------------------------------ *
 * PDF Annotations and Attachments
 * ------------------------------------------------------------------ */

/* static gint
 * annotation_cmp_edges (const annotation_t *a1, const annotation_t *a2)
 * {
 *   PopplerRectangle *e1 = &a1->amap->area;
 *   PopplerRectangle *e2 = &a2->amap->area;
 *
 *   return (e1->y1 > e2->y1 ? -1
 *           : e1->y1 < e2->y1 ? 1
 *           : e1->x1 < e2->x1 ? -1
 *           : e1->x1 != e2->x1);
 * } */

static GList*
annoation_get_for_page (document_t *doc, gint pn)
{

  GList *annot_list, *item;
  PopplerPage *page;
  gint i = 0;
  gint npages = poppler_document_get_n_pages (doc->pdf);

  if (pn < 1 || pn > npages)
    return NULL;

  if (! doc->annotations.pages)
    doc->annotations.pages = g_malloc0 (npages * sizeof(GList*));

  if (doc->annotations.pages[pn - 1])
    return doc->annotations.pages[pn - 1];

  if (! doc->annotations.keys)
    doc->annotations.keys = g_hash_table_new (g_str_hash, g_str_equal);

  page = poppler_document_get_page (doc->pdf, pn - 1);
  if (NULL == page)
    return NULL;

  annot_list = poppler_page_get_annot_mapping (page);
  for (item = annot_list; item; item = item->next)
    {
      PopplerAnnotMapping *map = (PopplerAnnotMapping *)item->data;
      gchar *key = g_strdup_printf ("annot-%d-%d", pn, i);
      annotation_t *a = g_malloc (sizeof (annotation_t));
      a->amap = map;
      a->key = key;
      doc->annotations.pages[pn - 1] =
        g_list_prepend (doc->annotations.pages[pn - 1], a);
      assert (NULL == g_hash_table_lookup (doc->annotations.keys, key));
      g_hash_table_insert (doc->annotations.keys, key, a);
      ++i;
    }
  g_list_free (annot_list);
  g_object_unref (page);
  return doc->annotations.pages[pn - 1];
}

static annotation_t*
annotation_get_by_key (document_t *doc, const gchar *key)
{
  if (! doc->annotations.keys)
    return NULL;

  return g_hash_table_lookup (doc->annotations.keys, key);
}

#ifdef HAVE_POPPLER_ANNOT_MARKUP
static cairo_region_t*
annotation_markup_get_text_regions (PopplerPage *page, PopplerAnnotTextMarkup *a)
{
  GArray *quads = poppler_annot_text_markup_get_quadrilaterals (a);
  int i;
  cairo_region_t *region = cairo_region_create ();
  gdouble height;

  poppler_page_get_size (page, NULL, &height);

  for (i = 0; i < quads->len; ++i)
    {
      PopplerQuadrilateral *q = &g_array_index (quads, PopplerQuadrilateral, i);
      cairo_rectangle_int_t r;

      q->p1.y = height - q->p1.y;
      q->p2.y = height - q->p2.y;
      q->p3.y = height - q->p3.y;
      q->p4.y = height - q->p4.y;

      r.x = (int) (MIN (q->p1.x, MIN (q->p2.x, MIN (q->p3.x, q->p4.x))) + 0.5);
      r.y = (int) (MIN (q->p1.y, MIN (q->p2.y, MIN (q->p3.y, q->p4.y))) + 0.5);
      r.width = (int) (MAX (q->p1.x, MAX (q->p2.x, MAX (q->p3.x, q->p4.x))) + 0.5)
                - r.x;
      r.height = (int) (MAX (q->p1.y, MAX (q->p2.y, MAX (q->p3.y, q->p4.y))) + 0.5)
                 - r.y;

      cairo_region_union_rectangle (region, &r);
    }
  g_array_unref (quads);
  return region;
}

/**
 * Append quadrilaterals equivalent to region to an array.
 *
 * @param page The page of the annotation.  This is used to get the
 *             text regions and pagesize.
 * @param selection_style The selection style.
 * @param region The region to add.
 * @param garray[in,out] An array of PopplerQuadrilateral, where the
 *              new quadrilaterals will be appended.
 */
static void
annotation_markup_append_text_region (PopplerPage *page,
				      PopplerSelectionStyle selection_style,
				      PopplerRectangle *region,
                                      GArray *garray)
{
  gdouble height;
  /* poppler_page_get_selection_region is deprecated w/o a
     replacement.  (poppler_page_get_selected_region returns a union
     of rectangles.) */
  GList *regions =
    poppler_page_get_selection_region (page, 1.0, selection_style, region);
  GList *item;

  poppler_page_get_size (page, NULL, &height);
  for (item = regions; item; item = item->next)
    {
      PopplerRectangle *r = item->data;
      PopplerQuadrilateral q;

      q.p1.x = r->x1;
      q.p1.y = height - r->y1;
      q.p2.x = r->x2;
      q.p2.y = height - r->y1;
      q.p4.x = r->x2;
      q.p4.y = height - r->y2;
      q.p3.x = r->x1;
      q.p3.y = height - r->y2;

      g_array_append_val (garray, q);
    }
  g_list_free (regions);
}

#endif
/**
 * Create a new annotation.
 *
 * @param doc The document for which to create it.
 * @param type The type of the annotation.
 * @param selection_style The selection style.
 * @param r The rectangle where annotation will end up on the page.
 *
 * @return The new annotation, or NULL, if the annotation type is
 *         not available.
 */
static PopplerAnnot*
annotation_new (const epdfinfo_t *ctx, document_t *doc, PopplerPage *page,
                const char *type, PopplerSelectionStyle selection_style,
                PopplerRectangle *r, const command_arg_t *rest,
                char **error_msg)
{

  PopplerAnnot *a = NULL;
  int nargs = rest->value.rest.nargs;
#ifdef HAVE_POPPLER_ANNOT_MARKUP
  char * const *args = rest->value.rest.args;
  int i;
  GArray *garray = NULL;
  command_arg_t carg;
  double width, height;
  cairo_region_t *region = NULL;
#endif

  if (! strcmp (type, "text"))
    {
      cerror_if_not (nargs == 0, error_msg, "%s", "Too many arguments");
      return poppler_annot_text_new (doc->pdf, r);
    }

#ifdef HAVE_POPPLER_ANNOT_MARKUP
  garray = g_array_new (FALSE, FALSE, sizeof (PopplerQuadrilateral));
  poppler_page_get_size (page, &width, &height);
  if (nargs == 0)
    {
      PopplerQuadrilateral q;

      q.p1.x = r->x1;
      q.p1.y = r->y1;
      q.p2.x = r->x2;
      q.p2.y = r->y1;
      q.p4.x = r->x2;
      q.p4.y = r->y2;
      q.p3.x = r->x1;
      q.p3.y = r->y2;

      g_array_append_val (garray, q);
    }
  for (i = 0; i < nargs; ++i)
    {
      PopplerRectangle *rr = &carg.value.rectangle;

      error_if_not (command_arg_parse_arg (ctx, args[i], &carg,
                                           ARG_EDGES, error_msg));
      rr->x1 *= width; rr->x2 *= width;
      rr->y1 *= height; rr->y2 *= height;
      annotation_markup_append_text_region (page, selection_style, rr, garray);
    }
  cerror_if_not (garray->len > 0, error_msg, "%s",
                 "Unable to create empty markup annotation");

  if (! strcmp (type, "highlight"))
    a = poppler_annot_text_markup_new_highlight (doc->pdf, r, garray);
  else if (! strcmp (type, "squiggly"))
    a = poppler_annot_text_markup_new_squiggly (doc->pdf, r, garray);
  else if (! strcmp (type, "strike-out"))
    a = poppler_annot_text_markup_new_strikeout (doc->pdf, r, garray);
  else if (! strcmp (type, "underline"))
    a = poppler_annot_text_markup_new_underline (doc->pdf, r, garray);
  else
    cerror_if_not (0, error_msg, "Unknown annotation type: %s", type);

#endif
 error:
#ifdef HAVE_POPPLER_ANNOT_MARKUP
  if (garray) g_array_unref (garray);
  if (region) cairo_region_destroy (region);
#endif
  return a;
}

static gboolean
annotation_edit_validate (const epdfinfo_t *ctx, const command_arg_t *rest,
                          PopplerAnnot *annotation, char **error_msg)
{
  int nargs = rest->value.rest.nargs;
  char * const *args = rest->value.rest.args;
  int i = 0;
  command_arg_t carg;

  const char* error_fmt =
    "Can modify `%s' property only for %s annotations";

  while (i < nargs)
    {
      command_arg_type_t atype = ARG_INVALID;
      const char *key = args[i++];

      cerror_if_not (i < nargs, error_msg, "Missing a value argument");

      if (! strcmp (key, "flags"))
        atype = ARG_NATNUM;
      else if (! strcmp (key, "color"))
        atype = ARG_COLOR;
      else if (! strcmp (key, "contents"))
        atype = ARG_STRING;
      else if (! strcmp (key, "edges"))
        atype = ARG_EDGES_OR_POSITION;
      else if (! strcmp (key, "label"))
        {
          cerror_if_not (POPPLER_IS_ANNOT_MARKUP (annotation), error_msg,
                         error_fmt, key, "markup");
          atype = ARG_STRING;
        }
      else if (! strcmp (key, "opacity"))
        {
          cerror_if_not (POPPLER_IS_ANNOT_MARKUP (annotation), error_msg,
                         error_fmt, key, "markup");
          atype = ARG_EDGE;
        }
      else if (! strcmp (key, "popup"))
        {
          cerror_if_not (POPPLER_IS_ANNOT_MARKUP (annotation), error_msg,
                         error_fmt, key, "markup");
          atype = ARG_EDGES;
        }
      else if (! strcmp (key, "popup-is-open"))
        {
          cerror_if_not (POPPLER_IS_ANNOT_MARKUP (annotation), error_msg,
                         error_fmt, key, "markup");
          atype = ARG_BOOL;
        }
      else if (! strcmp (key, "icon"))
        {
          cerror_if_not (POPPLER_IS_ANNOT_TEXT (annotation), error_msg,
                         error_fmt, key, "text");
          atype = ARG_STRING;
        }
      else if (! strcmp (key, "is-open"))
        {
          cerror_if_not (POPPLER_IS_ANNOT_TEXT (annotation), error_msg,
                         error_fmt, key, "text");
          atype = ARG_BOOL;
        }
      else
        {
          cerror_if_not (0, error_msg,
                         "Unable to modify property `%s'", key);
        }

      if (! command_arg_parse_arg (ctx, args[i++], &carg, atype, error_msg))
        return FALSE;
    }

  return TRUE;

 error:
  return FALSE;
}

static void
annotation_print (const annotation_t *annot, /* const */ PopplerPage *page)
{
  double width, height;
  PopplerAnnotMapping *m;
  const gchar *key;
  PopplerAnnot *a;
  PopplerAnnotMarkup *ma;
  PopplerAnnotText *ta;
  PopplerRectangle r;
  PopplerColor *color;
  gchar *text;
  gdouble opacity;
  cairo_region_t *region = NULL;
  GDate *date;

  if (! annot || ! page)
    return;

  m = annot->amap;
  key = annot->key;
  a = m->annot;
  poppler_page_get_size (page, &width, &height);

  r.x1 = m->area.x1;
  r.x2 = m->area.x2;
  r.y1 = height - m->area.y2;
  r.y2 = height - m->area.y1;

#ifdef HAVE_POPPLER_ANNOT_MARKUP
  if (POPPLER_IS_ANNOT_TEXT_MARKUP (a))
    {
      region = annotation_markup_get_text_regions (page, POPPLER_ANNOT_TEXT_MARKUP (a));
      perror_if_not (region, "%s", "Unable to extract annotation's text regions");
    }
#endif

  /* >>> Any Annotation >>> */
  /* Page */
  printf ("%d:", poppler_page_get_index (page) + 1);
  /* Area */
  printf ("%f %f %f %f:", r.x1 / width, r.y1 / height
          , r.x2 / width, r.y2 / height);

  /* Type */
  printf ("%s:", xpoppler_annot_type_string (poppler_annot_get_annot_type (a)));
  /* Internal Key */
  print_response_string (key, COLON);

  /* Flags */
  printf ("%d:", poppler_annot_get_flags (a));

  /* Color */
  color = poppler_annot_get_color (a);
  if (color)
    {
      /* Reduce 2 Byte to 1 Byte color space  */
      printf ("#%.2x%.2x%.2x", (color->red >> 8)
              , (color->green >> 8)
              , (color->blue >> 8));
      g_free (color);
    }

  putchar (':');

  /* Text Contents */
  text = poppler_annot_get_contents (a);
  print_response_string (text, COLON);
  g_free (text);

  /* Modified Date */
  text = poppler_annot_get_modified (a);
  print_response_string (text, NONE);
  g_free (text);

  /* <<< Any Annotation <<< */

  /* >>> Markup Annotation >>> */
  if (! POPPLER_IS_ANNOT_MARKUP (a))
    {
      putchar ('\n');
      goto theend;
    }

  putchar (':');
  ma = POPPLER_ANNOT_MARKUP (a);
  /* Label */
  text = poppler_annot_markup_get_label (ma);
  print_response_string (text, COLON);
  g_free (text);

  /* Subject */
  text = poppler_annot_markup_get_subject (ma);
  print_response_string (text, COLON);
  g_free (text);

  /* Opacity */
  opacity = poppler_annot_markup_get_opacity (ma);
  printf ("%f:", opacity);

  /* Popup (Area + isOpen) */
  if (poppler_annot_markup_has_popup (ma)
      && poppler_annot_markup_get_popup_rectangle (ma, &r))
    {
      gdouble tmp = r.y1;
      r.y1 = height - r.y2;
      r.y2 = height - tmp;
      printf ("%f %f %f %f:%d:", r.x1 / width, r.y1 / height
              , r.x2 / width, r.y2 / height
              , poppler_annot_markup_get_popup_is_open (ma) ? 1 : 0);

    }
  else
    printf ("::");

  /* Creation Date */
  date = poppler_annot_markup_get_date (ma);
  if (date != NULL && g_date_valid(date))
    {
      gchar datebuf[128];
      g_date_strftime (datebuf, 127, "%x", date);
      print_response_string (datebuf, NONE);
      g_date_free (date);
    }

  /* <<< Markup Annotation <<< */

  /* >>>  Text Annotation >>> */
  if (POPPLER_IS_ANNOT_TEXT (a))
    {
      putchar (':');
      ta = POPPLER_ANNOT_TEXT (a);
      /* Text Icon */
      text = poppler_annot_text_get_icon (ta);
      print_response_string (text, COLON);
      g_free (text);
      /* Text State */
      printf ("%s:%d",
              xpoppler_annot_text_state_string (poppler_annot_text_get_state (ta)),
              poppler_annot_text_get_is_open (ta));
    }
#ifdef HAVE_POPPLER_ANNOT_MARKUP
  /* <<< Text Annotation <<< */
  else if (POPPLER_IS_ANNOT_TEXT_MARKUP (a))
    {
      /* >>> Markup Text Annotation >>> */
      putchar (':');
      region_print (region, width, height);
      /* <<< Markup Text Annotation <<< */
    }
#endif
  putchar ('\n');
 theend:
#ifdef HAVE_POPPLER_ANNOT_MARKUP
 error:
#endif
  if (region) cairo_region_destroy (region);
}

static void
attachment_print (PopplerAttachment *att, const char *id, gboolean do_save)
{
  time_t time;

  print_response_string (id, COLON);
  print_response_string (att->name, COLON);
  print_response_string (att->description, COLON);
  if (att->size + 1 != 0)
    printf ("%" G_GSIZE_FORMAT ":", att->size);
  else
    printf ("-1:");
  time = (time_t) att->mtime;
  print_response_string (time > 0 ? strchomp (ctime (&time)) : "", COLON);
  time = (time_t) att->ctime;
  print_response_string (time > 0 ? strchomp (ctime (&time)) : "", COLON);
  print_response_string (att->checksum ? att->checksum->str : "" , COLON);
  if (do_save)
    {
      char *filename = mktempfile ();
      GError *error = NULL;
      if (filename)
        {
          if (! poppler_attachment_save (att, filename, &error))
            {
              fprintf (stderr, "Writing attachment failed: %s"
                       , error ? error->message : "reason unknown");
              if (error)
                g_free (error);
            }
          else
            {
              print_response_string (filename, NONE);
            }
          free (filename);
        }
    }
  putchar ('\n');
}



/* ================================================================== *
 * Server command implementations
 * ================================================================== */

/* Name: features
   Args: None
   Returns: A list of compile-time features.
   Errors: None
*/

const command_arg_type_t cmd_features_spec[] = {};

static void
cmd_features (const epdfinfo_t *ctx, const command_arg_t *args)
{
  const char *features[] = {
#ifdef HAVE_POPPLER_FIND_OPTS
    "case-sensitive-search",
#else
    "no-case-sensitive-search",
#endif
#ifdef HAVE_POPPLER_ANNOT_WRITE
    "writable-annotations",
#else
    "no-writable-annotations",
#endif
#ifdef HAVE_POPPLER_ANNOT_MARKUP
    "markup-annotations"
#else
    "no-markup-annotations"
#endif
  };
  int i;
  OK_BEGIN ();
  for (i = 0; i < G_N_ELEMENTS (features); ++i)
    {
      printf ("%s", features[i]);
      if (i < G_N_ELEMENTS (features) - 1)
        putchar (':');
    }
  putchar ('\n');
  OK_END ();
}


/* Name: open
   Args: filename password
   Returns: Nothing
   Errors: If file can't be opened or is not a PDF document.
*/

const command_arg_type_t cmd_open_spec[] =
  {
    ARG_NONEMPTY_STRING,        /* filename */
    ARG_STRING,                 /* password */
  };

static void
cmd_open (const epdfinfo_t *ctx, const command_arg_t *args)
{
  const char *filename = args[0].value.string;
  const char *passwd = args[1].value.string;
  GError *gerror = NULL;
  document_t *doc;

  if (! *passwd)
    passwd = NULL;

  doc = document_open(ctx, filename, passwd, &gerror);
  perror_if_not (doc, "Error opening %s:%s", filename,
                gerror ? gerror->message : "unknown error");
  OK ();

 error:
  if (gerror)
    {
      g_error_free (gerror);
      gerror = NULL;
    }
}

/* Name: close
   Args: filename
   Returns: 1 if file was open, otherwise 0.
   Errors: None
*/

const command_arg_type_t cmd_close_spec[] =
  {
    ARG_NONEMPTY_STRING         /* filename */
  };

static void
cmd_close (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = g_hash_table_lookup(ctx->documents, args->value.string);

  g_hash_table_remove (ctx->documents, args->value.string);
  free_document (doc);
  OK_BEGIN ();
  puts (doc ? "1" : "0");
  OK_END ();
}

/* Name: closeall
   Args: None
   Returns: Nothing
   Errors: None
*/
static void
cmd_closeall (const epdfinfo_t *ctx, const command_arg_t *args)
{
  GHashTableIter iter;
  gpointer key, value;

  g_hash_table_iter_init (&iter, ctx->documents);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      document_t *doc = (document_t*) value;
      free_document (doc);
      g_hash_table_iter_remove (&iter);
    }
  OK ();
}


const command_arg_type_t cmd_search_regexp_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM,                 /* first page */
    ARG_NATNUM,                 /* last page */
    ARG_NONEMPTY_STRING,        /* regexp */
    ARG_NATNUM,                 /* compile flags */
    ARG_NATNUM                  /* match flags */
  };

static void
cmd_search_regexp(const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerDocument *doc = args[0].value.doc->pdf;
  int first = args[1].value.natnum;
  int last = args[2].value.natnum;
  const gchar *regexp = args[3].value.string;
  GRegexCompileFlags cflags = args[4].value.natnum;
  GRegexMatchFlags mflags = args[5].value.natnum;
  double width, height;
  int pn;
  GError *gerror = NULL;
  GRegex *re = NULL;

  NORMALIZE_PAGE_ARG (doc, &first, &last);

  re = g_regex_new (regexp, cflags, mflags, &gerror);
  perror_if_not (NULL == gerror, "Invalid regexp: %s", gerror->message);

  OK_BEGIN ();
  for (pn = first; pn <= last; ++pn)
    {
      PopplerPage *page = poppler_document_get_page(doc, pn - 1);
      char *text;
      PopplerRectangle *rectangles = NULL;
      guint nrectangles;
      GMatchInfo *match = NULL;

      if (! page)
        continue;

      text = poppler_page_get_text (page);
      poppler_page_get_text_layout (page, &rectangles, &nrectangles);
      poppler_page_get_size (page, &width, &height);
      g_regex_match (re, text, 0, &match);

      while (g_match_info_matches (match))
        {
          const double scale = 100.0;
          gint start, end, ustart, ulen;
          gchar *string = NULL;
          gchar *line = NULL;
          int i;

          /* Does this ever happen ? */
          if (! g_match_info_fetch_pos (match, 0, &start, &end))
            continue;

          string = g_match_info_fetch (match, 0);
          ustart = g_utf8_strlen (text, start);
          ulen = g_utf8_strlen (string, -1);

          cairo_region_t *region = cairo_region_create ();
          /* Merge matched glyph rectangles. Scale them so we're able
             to use cairo . */
          if (ulen > 0)
            {
              assert (ustart < nrectangles
                      && ustart + ulen <= nrectangles);
              line = poppler_page_get_selected_text
                     (page, POPPLER_SELECTION_LINE, rectangles + ustart);

              for (i = ustart; i < ustart + ulen; ++i)
                {
                  PopplerRectangle *r = rectangles + i;
                  cairo_rectangle_int_t c;

                  c.x = (int) (scale * r->x1 + 0.5);
                  c.y = (int) (scale * r->y1 + 0.5);
                  c.width = (int) (scale * (r->x2 - r->x1) + 0.5);
                  c.height = (int) (scale * (r->y2 - r->y1) + 0.5);

                  cairo_region_union_rectangle (region, &c);
                }

            }

          printf ("%d:", pn);
          print_response_string (string, COLON);
          print_response_string (strchomp (line), COLON);
          region_print (region, width * scale, height * scale);
          putchar ('\n');
          cairo_region_destroy (region);
          g_free (string);
          g_free (line);
          g_match_info_next (match, NULL);
        }
      g_free (rectangles);
      g_object_unref (page);
      g_free (text);
      g_match_info_free (match);
    }
  OK_END ();

 error:
  if (re) g_regex_unref (re);
  if (gerror) g_error_free (gerror);
}

const command_arg_type_t cmd_regexp_flags_spec[] =
  {
  };

static void
cmd_regexp_flags (const epdfinfo_t *ctx, const command_arg_t *args)
{
  OK_BEGIN ();
  printf ("caseless:%d\n", G_REGEX_CASELESS);
  printf ("multiline:%d\n", G_REGEX_MULTILINE);
  printf ("dotall:%d\n", G_REGEX_DOTALL);
  printf ("extended:%d\n", G_REGEX_EXTENDED);
  printf ("anchored:%d\n", G_REGEX_ANCHORED);
  printf ("dollar-endonly:%d\n", G_REGEX_DOLLAR_ENDONLY);
  printf ("ungreedy:%d\n", G_REGEX_UNGREEDY);
  printf ("raw:%d\n", G_REGEX_RAW);
  printf ("no-auto-capture:%d\n", G_REGEX_NO_AUTO_CAPTURE);
  printf ("optimize:%d\n", G_REGEX_OPTIMIZE);
  printf ("dupnames:%d\n", G_REGEX_DUPNAMES);
  printf ("newline-cr:%d\n", G_REGEX_NEWLINE_CR);
  printf ("newline-lf:%d\n", G_REGEX_NEWLINE_LF);
  printf ("newline-crlf:%d\n", G_REGEX_NEWLINE_CRLF);

  printf ("match-anchored:%d\n", G_REGEX_MATCH_ANCHORED);
  printf ("match-notbol:%d\n", G_REGEX_MATCH_NOTBOL);
  printf ("match-noteol:%d\n", G_REGEX_MATCH_NOTEOL);
  printf ("match-notempty:%d\n", G_REGEX_MATCH_NOTEMPTY);
  printf ("match-partial:%d\n", G_REGEX_MATCH_PARTIAL);
  printf ("match-newline-cr:%d\n", G_REGEX_MATCH_NEWLINE_CR);
  printf ("match-newline-lf:%d\n", G_REGEX_MATCH_NEWLINE_LF);
  printf ("match-newline-crlf:%d\n", G_REGEX_MATCH_NEWLINE_CRLF);
  printf ("match-newline-any:%d\n", G_REGEX_MATCH_NEWLINE_ANY);

  OK_END ();
}


const command_arg_type_t cmd_search_string_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM,                 /* first page */
    ARG_NATNUM,                 /* last page */
    ARG_NONEMPTY_STRING,        /* search string */
    ARG_BOOL,                   /* ignore-case */
  };

static void
cmd_search_string(const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerDocument *doc = args[0].value.doc->pdf;
  int first = args[1].value.natnum;
  int last = args[2].value.natnum;
  const char *string = args[3].value.string;
  gboolean ignore_case = args[4].value.flag;
  GList *list, *item;
  double width, height;
  int pn;
#ifdef HAVE_POPPLER_FIND_OPTS
  PopplerFindFlags flags = ignore_case ? 0 : POPPLER_FIND_CASE_SENSITIVE;
#endif

  NORMALIZE_PAGE_ARG (doc, &first, &last);
  OK_BEGIN ();
  for (pn = first; pn <= last; ++pn)
    {
      PopplerPage *page = poppler_document_get_page(doc, pn - 1);

      if (! page)
        continue;

#ifdef HAVE_POPPLER_FIND_OPTS
      list = poppler_page_find_text_with_options(page, string, flags);
#else
      list = poppler_page_find_text(page, string);
#endif

      poppler_page_get_size (page, &width, &height);

      for (item = list; item; item = item->next)
        {
          gchar *line, *match;
          PopplerRectangle *r = item->data;
          gdouble y1 =  r->y1;

          r->y1 = height - r->y2;
          r->y2 = height - y1;

          printf ("%d:", pn);
          line = strchomp (poppler_page_get_selected_text
                           (page, POPPLER_SELECTION_LINE, r));
          match = strchomp (poppler_page_get_selected_text
                           (page, POPPLER_SELECTION_GLYPH, r));
          print_response_string (match, COLON);
          print_response_string (line, COLON);
          printf ("%f %f %f %f\n",
                  r->x1 / width, r->y1 / height,
                  r->x2 / width, r->y2 / height);
          g_free (line);
          g_free (match);
          poppler_rectangle_free (r);
        }
      g_list_free (list);
      g_object_unref (page);
    }
  OK_END ();
}

/* Name: metadata
   Args: filename
   Returns: PDF's metadata
   Errors: None

   title author subject keywords creator producer pdf-version create-date mod-date

   Dates are in seconds since the epoche.

*/

const command_arg_type_t cmd_metadata_spec[] =
  {
    ARG_DOC,
  };

static void
cmd_metadata (const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerDocument *doc = args[0].value.doc->pdf;
  time_t date;
  gchar *md[6];
  gchar *title;
  int i;
  char *time_str;

  OK_BEGIN ();

  title = poppler_document_get_title (doc);
  print_response_string (title, COLON);
  g_free (title);

  md[0] = poppler_document_get_author (doc);
  md[1] = poppler_document_get_subject (doc);
  md[2] = poppler_document_get_keywords (doc);
  md[3] = poppler_document_get_creator (doc);
  md[4] = poppler_document_get_producer (doc);
  md[5] = poppler_document_get_pdf_version_string (doc);

  for (i = 0; i < 6; ++i)
    {
      print_response_string (md[i], COLON);
      g_free (md[i]);
    }

  date = poppler_document_get_creation_date (doc);
  time_str = strchomp (ctime (&date));
  print_response_string (time_str ? time_str : "", COLON);
  date = poppler_document_get_modification_date (doc);
  time_str = strchomp (ctime (&date));
  print_response_string (time_str ? time_str : "", NEWLINE);
  OK_END ();
}

/* Name: outline
   Args: filename

   Returns: The documents outline (or index) as a, possibly empty,
   list of records:

   tree-level ACTION

   See cmd_pagelinks for how ACTION is constructed.

   Errors: None
*/

static void
cmd_outline_walk (PopplerDocument *doc, PopplerIndexIter *iter, int depth)
{
  do
    {
      PopplerIndexIter *child;
      PopplerAction *action = poppler_index_iter_get_action (iter);

      if (! action)
        continue;

      if (action_is_handled (action))
        {
          printf ("%d:", depth);
          action_print (doc, action);
        }

      child = poppler_index_iter_get_child (iter);
      if (child)
        {
          cmd_outline_walk (doc, child, depth + 1);
        }
      poppler_action_free (action);
      poppler_index_iter_free (child);
    } while (poppler_index_iter_next (iter));
}

const command_arg_type_t cmd_outline_spec[] =
  {
    ARG_DOC,
  };

static void
cmd_outline (const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerIndexIter *iter = poppler_index_iter_new (args->value.doc->pdf);
  OK_BEGIN ();
  if (iter)
    {
      cmd_outline_walk (args->value.doc->pdf, iter, 1);
      poppler_index_iter_free (iter);
    }
  OK_END ();
}

/* Name: quit
   Args: None
   Returns: Nothing
   Errors: None

   Close all documents and exit.
*/


const command_arg_type_t cmd_quit_spec[] = {};

static void
cmd_quit (const epdfinfo_t *ctx, const command_arg_t *args)
{
  cmd_closeall (ctx, args);
  exit (EXIT_SUCCESS);
}

/* Name: number-of-pages
   Args: filename
   Returns: The number of pages.
   Errors: None
*/


const command_arg_type_t cmd_number_of_pages_spec[] =
  {
    ARG_DOC
  };

static void
cmd_number_of_pages (const epdfinfo_t *ctx, const command_arg_t *args)
{
  int npages = poppler_document_get_n_pages (args->value.doc->pdf);
  OK_BEGIN ();
  printf ("%d\n", npages);
  OK_END ();
}

/* Name: pagelinks
   Args: filename page
   Returns: A list of linkmaps:

   edges ACTION ,

   where ACTION is one of

   'goto-dest' title page top
   'goto-remote' title filename page top
   'uri' title URI
   'launch' title program arguments

   top is desired vertical position, filename is the target PDF of the
   `goto-remote' link.

   Errors: None
*/


const command_arg_type_t cmd_pagelinks_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM                  /* page number */
  };

static void
cmd_pagelinks(const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerDocument *doc = args[0].value.doc->pdf;
  PopplerPage *page = NULL;
  int pn = args[1].value.natnum;
  double width, height;
  GList *link_map = NULL, *item;

  page = poppler_document_get_page (doc, pn - 1);
  perror_if_not (page, "No such page %d", pn);
  poppler_page_get_size (page, &width, &height);
  link_map = poppler_page_get_link_mapping (page);

  OK_BEGIN ();
  for (item = g_list_last (link_map); item; item = item->prev)
    {

      PopplerLinkMapping *link = item->data;
      PopplerRectangle *r = &link->area;
      gdouble y1 = r->y1;
      /* LinkMappings have a different gravity. */
      r->y1 = height - r->y2;
      r->y2 = height - y1;

      if (! action_is_handled (link->action))
        continue;

      printf ("%f %f %f %f:",
              r->x1 / width, r->y1 / height,
              r->x2 / width, r->y2 / height);
      action_print (doc, link->action);
    }
  OK_END ();
 error:
  if (page) g_object_unref (page);
  if (link_map) poppler_page_free_link_mapping (link_map);
}

/* Name: gettext
   Args: filename page edges selection-style
   Returns: The selection's text.
   Errors: If page is out of range.

   For the selection-style argument see getselection command.
*/


const command_arg_type_t cmd_gettext_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM,                 /* page number */
    ARG_EDGES,                  /* selection */
    ARG_NATNUM                  /* selection-style */
  };

static void
cmd_gettext(const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerDocument *doc = args[0].value.doc->pdf;
  int pn = args[1].value.natnum;
  PopplerRectangle r = args[2].value.rectangle;
  int selection_style = args[3].value.natnum;
  PopplerPage *page = NULL;
  double width, height;
  gchar *text = NULL;

  selection_style = xpoppler_validate_selection_style (selection_style);
  page = poppler_document_get_page (doc, pn - 1);
  perror_if_not (page, "No such page %d", pn);
  poppler_page_get_size (page, &width, &height);
  r.x1 = r.x1 * width;
  r.x2 = r.x2 * width;
  r.y1 = r.y1 * height;
  r.y2 = r.y2 * height;
  /* printf ("%f %f %f %f , %f %f\n", r.x1, r.y1, r.x2, r.y2, width, height); */
  text = poppler_page_get_selected_text (page, selection_style, &r);

  OK_BEGIN ();
  print_response_string (text, NEWLINE);
  OK_END ();

 error:
  g_free (text);
  if (page) g_object_unref (page);
}

/* Name: getselection
   Args: filename page edges selection-selection_style
   Returns: The selection's text.
   Errors: If page is out of range.

   selection-selection_style should be as follows.

   0 (POPPLER_SELECTION_GLYPH)
	glyph is the minimum unit for selection

   1 (POPPLER_SELECTION_WORD)
	word is the minimum unit for selection

   2 (POPPLER_SELECTION_LINE)
	line is the minimum unit for selection
*/


const command_arg_type_t cmd_getselection_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM,                 /* page number */
    ARG_EDGES,                  /* selection */
    ARG_NATNUM                  /* selection-style */
  };

static void
cmd_getselection (const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerDocument *doc = args[0].value.doc->pdf;
  int pn = args[1].value.natnum;
  PopplerRectangle r = args[2].value.rectangle;
  int selection_style = args[3].value.natnum;
  gdouble width, height;
  cairo_region_t *region = NULL;
  PopplerPage *page = NULL;
  int i;

  selection_style = xpoppler_validate_selection_style (selection_style);
  page = poppler_document_get_page (doc, pn - 1);
  perror_if_not (page, "No such page %d", pn);
  poppler_page_get_size (page, &width, &height);

  r.x1 = r.x1 * width;
  r.x2 = r.x2 * width;
  r.y1 = r.y1 * height;
  r.y2 = r.y2 * height;

  region = poppler_page_get_selected_region (page, 1.0, selection_style, &r);

  OK_BEGIN ();
  for (i = 0; i < cairo_region_num_rectangles (region); ++i)
    {
      cairo_rectangle_int_t r;

      cairo_region_get_rectangle (region, i, &r);
      printf ("%f %f %f %f\n",
              r.x / width,
              r.y / height,
              (r.x + r.width) / width,
              (r.y + r.height) / height);
    }
  OK_END ();

 error:
  if (region) cairo_region_destroy (region);
  if (page) g_object_unref (page);
}

/* Name: pagesize
   Args: filename page
   Returns: width height
   Errors: If page is out of range.
*/


const command_arg_type_t cmd_pagesize_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM                  /* page number */
  };

static void
cmd_pagesize(const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerDocument *doc = args[0].value.doc->pdf;
  int pn = args[1].value.natnum;
  PopplerPage *page = NULL;
  double width, height;


  page = poppler_document_get_page (doc, pn - 1);
  perror_if_not (page, "No such page %d", pn);
  poppler_page_get_size (page, &width, &height);

  OK_BEGIN ();
  printf ("%f:%f\n", width, height);
  OK_END ();

 error:
  if (page) g_object_unref (page);
}

/* Annotations */

/* Name: getannots
   Args: filename firstpage lastpage
   Returns: The list of annotations of this page.

   For all annotations

   page edges type key flags color contents mod-date

   ,where

   name is a document-unique name,
   flags is PopplerAnnotFlag bitmask,
   color is 3-byte RGB hex number and

   Then

   label subject opacity popup-edges popup-is-open create-date

   if this is a markup annotation and additionally

   text-icon text-state

   for markup text annotations.

   Errors: If page is out of range.
*/


const command_arg_type_t cmd_getannots_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM,                 /* first page */
    ARG_NATNUM                  /* last page */
  };

static void
cmd_getannots(const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerDocument *doc = args[0].value.doc->pdf;
  gint first = args[1].value.natnum;
  gint last = args[2].value.natnum;
  GList *list;
  gint pn;

  first = MAX(1, first);
  if (last <= 0)
    last = poppler_document_get_n_pages (doc);
  else
    last = MIN(last, poppler_document_get_n_pages (doc));

  OK_BEGIN ();
  for (pn = first; pn <= last; ++pn)
    {
      GList *annots = annoation_get_for_page (args->value.doc, pn);
      PopplerPage *page = poppler_document_get_page (doc, pn - 1);

      if (! page)
        continue;

      for (list = annots; list; list = list->next)
        {
          annotation_t *annot = (annotation_t *)list->data;
          annotation_print (annot, page);
        }
      g_object_unref (page);
    }
  OK_END ();
}

/* Name: getannot
   Args: filename name
   Returns: The annotation for name, see cmd_getannots.
   Errors: If no annotation named ,name' exists.
*/


const command_arg_type_t cmd_getannot_spec[] =
  {
    ARG_DOC,
    ARG_NONEMPTY_STRING,        /* annotation's key */
  };

static void
cmd_getannot (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args->value.doc;
  const gchar *key = args[1].value.string;
  PopplerPage *page = NULL;
  annotation_t *a = annotation_get_by_key (doc, key);
  gint index;

  perror_if_not (a, "No such annotation: %s", key);
  index = poppler_annot_get_page_index (a->amap->annot);
  if (index >= 0)
    page = poppler_document_get_page (doc->pdf, index);
  perror_if_not (page, "Unable to get page %d", index + 1);

  OK_BEGIN ();
  annotation_print (a, page);
  OK_END ();

 error:
  if (page) g_object_unref (page);
}

/* Name: getannot_attachment
   Args: filename name [output-filename]
   Returns: name description size mtime ctime output-filename
   Errors: If no annotation named ,name' exists or output-filename is
   not writable.
*/


const command_arg_type_t cmd_getattachment_from_annot_spec[] =
  {
    ARG_DOC,
    ARG_NONEMPTY_STRING,        /* annotation's name */
    ARG_BOOL                    /* save attachment */
  };

static void
cmd_getattachment_from_annot (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args->value.doc;
  const gchar *key = args[1].value.string;
  gboolean do_save = args[2].value.flag;
  PopplerAttachment *att = NULL;
  annotation_t *a = annotation_get_by_key (doc, key);
  gchar *id = NULL;

  perror_if_not (a, "No such annotation: %s", key);
  perror_if_not (POPPLER_IS_ANNOT_FILE_ATTACHMENT (a->amap->annot),
                "Not a file annotation: %s", key);
  att = poppler_annot_file_attachment_get_attachment
        (POPPLER_ANNOT_FILE_ATTACHMENT (a->amap->annot));
  perror_if_not (att, "Unable to get attachment: %s", key);
  id = g_strdup_printf ("attachment-%s", key);

  OK_BEGIN ();
  attachment_print (att, id, do_save);
  OK_END ();

 error:
  if (att) g_object_unref (att);
  if (id) g_free (id);
}


/* document-level attachments */
const command_arg_type_t cmd_getattachments_spec[] =
  {
    ARG_DOC,
    ARG_BOOL,        /* save attachments */
  };

static void
cmd_getattachments (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args->value.doc;
  gboolean do_save = args[1].value.flag;
  GList *item;
  GList *attmnts = poppler_document_get_attachments (doc->pdf);
  int i;

  OK_BEGIN ();
  for (item = attmnts, i = 0; item; item = item->next, ++i)
    {
      PopplerAttachment *att = (PopplerAttachment*) item->data;
      gchar *id = g_strdup_printf ("attachment-document-%d", i);

      attachment_print (att, id, do_save);
      g_object_unref (att);
      g_free (id);
    }
  g_list_free (attmnts);

  OK_END ();
}

#ifdef HAVE_POPPLER_ANNOT_WRITE

const command_arg_type_t cmd_addannot_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM,                 /* page number */
    ARG_STRING,                 /* type */
    ARG_NATNUM,                 /* selection-style */
    ARG_EDGES_OR_POSITION,      /* edges or position (uses default size) */
    ARG_REST,                  /* markup regions */
  };

static void
cmd_addannot (const epdfinfo_t *ctx, const command_arg_t *args)
{

  document_t *doc = args->value.doc;
  gint pn = args[1].value.natnum;
  const char *type_string = args[2].value.string;
  int selection_style = args[3].value.natnum;
  PopplerRectangle r = args[4].value.rectangle;
  int i;
  PopplerPage *page = NULL;
  double width, height;
  PopplerAnnot *pa;
  PopplerAnnotMapping *amap;
  annotation_t *a;
  gchar *key;
  GList *annotations;
  gdouble y2;
  char *error_msg = NULL;

  selection_style = xpoppler_validate_selection_style (selection_style);
  page = poppler_document_get_page (doc->pdf, pn - 1);
  perror_if_not (page, "Unable to get page %d", pn);
  poppler_page_get_size (page, &width, &height);
  r.x1 *= width; r.x2 *= width;
  r.y1 *= height; r.y2 *= height;
  if (r.y2 < 0)
    r.y2 = r.y1 + 24;
  if (r.x2 < 0)
    r.x2 = r.x1 + 24;
  y2 = r.y2;
  r.y2 = height - r.y1;
  r.y1 = height - y2;

  pa = annotation_new (ctx, doc, page, type_string, selection_style, &r, &args[5],
                       &error_msg);
  perror_if_not (pa, "Creating annotation failed: %s",
                 error_msg ? error_msg : "Reason unknown");
  amap = poppler_annot_mapping_new ();
  amap->area = r;
  amap->annot = pa;
  annotations = annoation_get_for_page (doc, pn);

  i = g_list_length (annotations);
  key = g_strdup_printf ("annot-%d-%d", pn, i);
  while (g_hash_table_lookup (doc->annotations.keys, key))
    {
      g_free (key);
      key = g_strdup_printf ("annot-%d-%d", pn, ++i);
    }
  a = g_malloc (sizeof (annotation_t));
  a->amap = amap;
  a->key = key;
  doc->annotations.pages[pn - 1] =
    g_list_prepend (annotations, a);
  g_hash_table_insert (doc->annotations.keys, key, a);
  poppler_page_add_annot (page, pa);
  OK_BEGIN ();
  annotation_print (a, page);
  OK_END ();

 error:
  if (page) g_object_unref (page);
  if (error_msg) g_free (error_msg);
}


const command_arg_type_t cmd_delannot_spec[] =
  {
    ARG_DOC,
    ARG_NONEMPTY_STRING         /* Annotation's key */
  };

static void
cmd_delannot (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args->value.doc;
  const gchar *key = args[1].value.string;
  PopplerPage *page = NULL;
  annotation_t *a = annotation_get_by_key (doc, key);
  gint pn;

  perror_if_not (a, "No such annotation: %s", key);
  pn = poppler_annot_get_page_index (a->amap->annot) + 1;
  if (pn >= 1)
    page = poppler_document_get_page (doc->pdf, pn - 1);
  perror_if_not (page, "Unable to get page %d", pn);
  poppler_page_remove_annot (page, a->amap->annot);
  doc->annotations.pages[pn - 1] =
    g_list_remove (doc->annotations.pages[pn - 1], a);
  g_hash_table_remove (doc->annotations.keys, a->key);
  poppler_annot_mapping_free(a->amap);
  OK ();

 error:
  if (a)
    {
      g_free (a->key);
      g_free (a);
    }
  if (page) g_object_unref (page);
}

const command_arg_type_t cmd_editannot_spec[] =
  {
    ARG_DOC,
    ARG_NONEMPTY_STRING,        /* annotation key */
    ARG_REST                    /* (KEY VALUE ...) */
  };

static void
cmd_editannot (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args->value.doc;
  const char *key = args[1].value.string;
  int nrest_args = args[2].value.rest.nargs;
  char * const *rest_args = args[2].value.rest.args;
  annotation_t *a = annotation_get_by_key (doc, key);
  PopplerAnnot *pa;
  PopplerPage *page = NULL;
  int i = 0;
  gint index;
  char *error_msg = NULL;
  command_arg_t carg;
  const char *unexpected_parse_error = "Internal error while parsing arg `%s'";

  perror_if_not (a, "No such annotation: %s", key);
  pa = a->amap->annot;
  perror_if_not (annotation_edit_validate (ctx, &args[2], pa, &error_msg),
                 "%s", error_msg);
  index = poppler_annot_get_page_index (pa);
  page = poppler_document_get_page (doc->pdf, index);
  perror_if_not (page, "Unable to get page %d for annotation", index);

  for (i = 0; i < nrest_args; ++i)
    {
      const char *key = rest_args[i++];

      if (! strcmp (key, "flags"))
        {
          perror_if_not (command_arg_parse_arg  (ctx, rest_args[i], &carg,
                                                 ARG_NATNUM, NULL),
                         unexpected_parse_error, rest_args[i]);
          poppler_annot_set_flags (pa, carg.value.natnum);
        }
      else if (! strcmp (key, "color"))
        {
          perror_if_not (command_arg_parse_arg  (ctx, rest_args[i], &carg,
                                                 ARG_COLOR, NULL),
                         unexpected_parse_error, rest_args[i]);
          poppler_annot_set_color (pa, &carg.value.color);
        }
      else if (! strcmp (key, "contents"))
        {
          perror_if_not (command_arg_parse_arg  (ctx, rest_args[i], &carg,
                                                 ARG_STRING, NULL),
                         unexpected_parse_error, rest_args[i]);
          poppler_annot_set_contents (pa, carg.value.string);
        }
      else if (! strcmp (key, "edges"))
        {
          PopplerRectangle *area = &a->amap->area;
          gdouble width, height;
          PopplerRectangle r;

          perror_if_not (command_arg_parse_arg  (ctx, rest_args[i], &carg,
                                                 ARG_EDGES_OR_POSITION, NULL),
                         unexpected_parse_error, rest_args[i]);
          r = carg.value.rectangle;
          poppler_page_get_size (page, &width, &height);

          /* Translate Gravity and maybe keep the width and height. */
          if (r.x2 < 0)
            area->x2 +=  (r.x1 * width) - area->x1;
          else
            area->x2 = r.x2 * width;

          if (r.y2 < 0)
            area->y1 -=  (r.y1 * height) - (height - area->y2);
          else
            area->y1 = height - (r.y2 * height);

          area->x1 = r.x1 * width;
          area->y2 = height - (r.y1 * height);

          poppler_annot_set_rectangle (pa, area);
        }
      else if (! strcmp (key, "label"))
        {
          PopplerAnnotMarkup *ma = POPPLER_ANNOT_MARKUP (pa);
          perror_if_not (command_arg_parse_arg  (ctx, rest_args[i], &carg,
                                                 ARG_STRING, NULL),
                         unexpected_parse_error, rest_args[i]);
          poppler_annot_markup_set_label (ma, carg.value.string);
        }
      else if (! strcmp (key, "opacity"))
        {
          PopplerAnnotMarkup *ma = POPPLER_ANNOT_MARKUP (pa);
          perror_if_not (command_arg_parse_arg  (ctx, rest_args[i], &carg,
                                                 ARG_EDGE, NULL),
                         unexpected_parse_error, rest_args[i]);
          poppler_annot_markup_set_opacity (ma, carg.value.edge);
        }
      else if (! strcmp (key, "popup"))
        {
          PopplerAnnotMarkup *ma = POPPLER_ANNOT_MARKUP (pa);
          perror_if_not (command_arg_parse_arg  (ctx, rest_args[i], &carg,
                                                 ARG_EDGES, NULL),
                         unexpected_parse_error, rest_args[i]);
          poppler_annot_markup_set_popup (ma, &carg.value.rectangle);
        }
      else if (! strcmp (key, "popup-is-open"))
        {
          PopplerAnnotMarkup *ma = POPPLER_ANNOT_MARKUP (pa);
          perror_if_not (command_arg_parse_arg  (ctx, rest_args[i], &carg,
                                                 ARG_BOOL, NULL),
                         unexpected_parse_error, rest_args[i]);
          poppler_annot_markup_set_popup_is_open (ma, carg.value.flag);
        }
      else if (! strcmp (key, "icon"))
        {
          PopplerAnnotText *ta = POPPLER_ANNOT_TEXT (pa);
          perror_if_not (command_arg_parse_arg  (ctx, rest_args[i], &carg,
                                                 ARG_STRING, NULL),
                         unexpected_parse_error, rest_args[i]);
          poppler_annot_text_set_icon (ta, carg.value.string);
        }
      else if (! strcmp (key, "is-open"))
        {
          PopplerAnnotText *ta = POPPLER_ANNOT_TEXT (pa);
          perror_if_not (command_arg_parse_arg  (ctx, rest_args[i], &carg,
                                                 ARG_BOOL, NULL),
                         unexpected_parse_error, rest_args[i]);
          poppler_annot_text_set_is_open (ta, carg.value.flag);
        }
      else
        {
          perror_if_not (0, "internal error: annotation property validation failed");
        }
    }

  OK_BEGIN ();
  annotation_print (a, page);
  OK_END ();

 error:
  if (error_msg) g_free (error_msg);
  if (page) g_object_unref (page);
}

const command_arg_type_t cmd_save_spec[] =
  {
    ARG_DOC,
  };

static void
cmd_save (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args->value.doc;
  char *filename = mktempfile ();
  GError *gerror = NULL;
  gchar *uri;
  gboolean success = FALSE;

  if (!filename)
    {
      printf_error_response ("Unable to create temporary file");
      return;
    }

  uri = g_filename_to_uri (filename, NULL, &gerror);

  if (uri)
    {
      success = poppler_document_save (doc->pdf, uri, &gerror);
      g_free (uri);
    }
  if (! success)
    {
      printf_error_response ("Error while saving %s:%s"
                    , filename, gerror ? gerror->message : "?");
      if (gerror)
        g_error_free (gerror);
      return;
    }
  OK_BEGIN ();
  print_response_string (filename, NEWLINE);
  OK_END ();
}

#endif  /* HAVE_POPPLER_ANNOT_WRITE */


const command_arg_type_t cmd_synctex_forward_search_spec[] =
  {
    ARG_DOC,
    ARG_NONEMPTY_STRING,        /* source file */
    ARG_NATNUM,                 /* line number */
    ARG_NATNUM                  /* column number */
  };

static void
cmd_synctex_forward_search (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args[0].value.doc;
  const char *source = args[1].value.string;
  int line = args[2].value.natnum;
  int column = args[3].value.natnum;
  synctex_scanner_p scanner = NULL;
  synctex_node_p node;
  float x1, y1, x2, y2;
  PopplerPage *page = NULL;
  double width, height;
  int pn;

  scanner = synctex_scanner_new_with_output_file (doc->filename, NULL, 1);
  perror_if_not (scanner, "Unable to create synctex scanner,\
 did you run latex with `--synctex=1' ?");

  perror_if_not (synctex_display_query (scanner, source, line, column, 0)
                && (node = synctex_scanner_next_result (scanner)),
                "Destination not found");

  pn = synctex_node_page (node);
  page = poppler_document_get_page(doc->pdf, pn - 1);
  perror_if_not (page, "Page not found");
  x1 =  synctex_node_box_visible_h (node);
  y1 =  synctex_node_box_visible_v (node)
        - synctex_node_box_visible_height (node);
  x2 = synctex_node_box_visible_width (node) + x1;
  y2 = synctex_node_box_visible_depth (node)
       + synctex_node_box_visible_height (node) + y1;
  poppler_page_get_size (page, &width, &height);
  x1 /= width;
  y1 /= height;
  x2 /= width;
  y2 /= height;

  OK_BEGIN ();
  printf("%d:%f:%f:%f:%f\n", pn, x1, y1, x2, y2);
  OK_END ();

 error:
  if (page) g_object_unref (page);
  if (scanner) synctex_scanner_free (scanner);
}


const command_arg_type_t cmd_synctex_backward_search_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM,                 /* page number */
    ARG_EDGE,                   /* x */
    ARG_EDGE                    /* y */
  };

static void
cmd_synctex_backward_search (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args[0].value.doc;
  int pn = args[1].value.natnum;
  double x = args[2].value.edge;
  double y = args[3].value.edge;
  synctex_scanner_p scanner = NULL;
  const char *filename;
  PopplerPage *page = NULL;
  synctex_node_p node;
  double width, height;
  int line, column;

  scanner = synctex_scanner_new_with_output_file (doc->filename, NULL, 1);
  perror_if_not (scanner, "Unable to create synctex scanner,\
 did you run latex with `--synctex=1' ?");

  page = poppler_document_get_page(doc->pdf, pn - 1);
  perror_if_not (page, "Page not found");
  poppler_page_get_size (page, &width, &height);
  x = x * width;
  y = y * height;

  if (! synctex_edit_query (scanner, pn, x, y)
      || ! (node = synctex_scanner_next_result (scanner))
      || ! (filename =
            synctex_scanner_get_name (scanner, synctex_node_tag (node))))
    {
      printf_error_response ("Destination not found");
      goto error;
    }

  line = synctex_node_line (node);
  column = synctex_node_column (node);

  OK_BEGIN ();
  print_response_string (filename, COLON);
  printf("%d:%d\n", line, column);
  OK_END ();

 error:
  if (page) g_object_unref (page);
  if (scanner) synctex_scanner_free (scanner);
}


const command_arg_type_t cmd_renderpage_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM,                 /* page number */
    ARG_NATNUM,                 /* width */
    ARG_REST,                   /* commands */
  };

static void
cmd_renderpage (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args[0].value.doc;
  int pn = args[1].value.natnum;
  int width = args[2].value.natnum;
  int nrest_args = args[3].value.rest.nargs;
  char * const *rest_args = args[3].value.rest.args;
  PopplerPage *page = poppler_document_get_page(doc->pdf, pn - 1);
  cairo_surface_t *surface = NULL;
  cairo_t *cr = NULL;
  command_arg_t rest_arg;
  gchar *error_msg = NULL;
  double pt_width, pt_height;
  PopplerColor fg = { 0, 0, 0 };
  PopplerColor bg = { 65535, 0, 0 };
  double alpha = 1.0;
  double line_width = 1.5;
  PopplerSelectionStyle selection_style = POPPLER_SELECTION_GLYPH;
  PopplerRectangle cb = {0.0, 0.0, 1.0, 1.0};
  int i = 0;

  perror_if_not (page, "No such page %d", pn);
  poppler_page_get_size (page, &pt_width, &pt_height);
  surface = image_render_page (doc->pdf, page, width, 1,
                               &doc->options.render);
  perror_if_not (surface, "Failed to render page %d", pn);

  if (! nrest_args)
    goto theend;

  cr = cairo_create (surface);
  cairo_scale (cr, width / pt_width, width / pt_width);

  while (i < nrest_args)
    {
      const char* keyword;

      perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &rest_arg,
                                            ARG_STRING, &error_msg),
                     "%s", error_msg);
      keyword = rest_arg.value.string;
      ++i;

      perror_if_not (i < nrest_args, "Keyword is `%s' missing an argument",
                     keyword);

      if (! strcmp (keyword, ":foreground")
          || ! strcmp (keyword, ":background"))
        {
          perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &rest_arg,
                                                ARG_COLOR, &error_msg),
                         "%s", error_msg);
          ++i;
          if (! strcmp (keyword, ":foreground"))
            fg = rest_arg.value.color;
          else
            bg = rest_arg.value.color;

        }
      else if (! strcmp (keyword, ":alpha"))
        {
          perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &rest_arg,
                                                ARG_EDGE, &error_msg),
                         "%s", error_msg);
          ++i;
          alpha = rest_arg.value.edge;
        }
      else if (! strcmp (keyword, ":crop-to")
               || ! strcmp (keyword, ":highlight-region")
               || ! strcmp (keyword, ":highlight-text")
               || ! strcmp (keyword, ":highlight-line"))
        {
          PopplerRectangle *r;
          perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &rest_arg,
                                                ARG_EDGES, &error_msg),
                         "%s", error_msg);

          ++i;
          r = &rest_arg.value.rectangle;

          if (! strcmp (keyword, ":crop-to"))
            {
              gdouble w = (cb.x2 - cb.x1);
              gdouble h = (cb.y2 - cb.y1);
              gdouble x1 = cb.x1;
              gdouble y1 = cb.y1;

              cb.x1 = r->x1 * w + x1;
              cb.x2 = r->x2 * w + x1;
              cb.y1 = r->y1 * h + y1;
              cb.y2 = r->y2 * h + y1;

            }
          else
            {
              r->x1 = pt_width * r->x1 * (cb.x2 - cb.x1) + pt_width * cb.x1;
              r->x2 = pt_width * r->x2 * (cb.x2 - cb.x1) + pt_width * cb.x1;
              r->y1 = pt_height * r->y1 * (cb.y2 - cb.y1) + pt_height * cb.y1;
              r->y2 = pt_height * r->y2 * (cb.y2 - cb.y1) + pt_height * cb.y1;

              if (! strcmp (keyword, ":highlight-region"))
                {
                  const double deg = M_PI / 180.0;
                  double rad;

                  r->x1 += line_width / 2;
                  r->x2 -= line_width / 2;
                  r->y1 += line_width / 2;
                  r->y2 -= line_width / 2;

                  rad = MIN (5, MIN (r->x2 - r->x1, r->y2 - r->y1) / 2.0);

                  cairo_move_to (cr, r->x1 , r->y1 + rad);
                  cairo_arc (cr, r->x1 + rad, r->y1 + rad, rad, 180 * deg, 270 * deg);
                  cairo_arc (cr, r->x2 - rad, r->y1 + rad, rad, 270 * deg, 360 * deg);
                  cairo_arc (cr, r->x2 - rad, r->y2 - rad, rad, 0 * deg, 90 * deg);
                  cairo_arc (cr, r->x1 + rad, r->y2 - rad, rad, 90 * deg, 180 * deg);
                  cairo_close_path (cr);

                  cairo_set_source_rgba (cr,
                                         bg.red / 65535.0,
                                         bg.green / 65535.0,
                                         bg.blue / 65535.0, alpha);
                  cairo_fill_preserve (cr);
                  cairo_set_source_rgba (cr,
                                         fg.red / 65535.0,
                                         fg.green / 65535.0,
                                         fg.blue / 65535.0, 1.0);
                  cairo_set_line_width (cr, line_width);
                  cairo_stroke (cr);
                }
              else
                {
                  gboolean is_single_line = ! strcmp (keyword, ":highlight-line");

                  if (is_single_line)
                    {
                      gdouble m = r->y1 + (r->y2 - r->y1) / 2;

                      /* Make the rectangle flat, otherwise poppler frequently
                         renders neighboring lines.*/
                      r->y1 = m;
                      r->y2 = m;
                    }

                  poppler_page_render_selection (page, cr, r, NULL,
                                                 selection_style, &fg, &bg);
                }
            }
        }
      else if (! strcmp (keyword, ":selection-style"))
        {
          perror_if_not (command_arg_parse_arg (ctx, rest_args[i], &rest_arg,
                                                ARG_NATNUM, &error_msg),
                         "%s", error_msg);
          ++i;
	  selection_style = xpoppler_validate_selection_style (rest_arg.value.natnum);
        }
      else
        perror_if_not (0, "Unknown render command: %s", keyword);
    }
  if (cb.x1 != 0 || cb.y1 != 0 || cb.x2 != 1 || cb.y2 != 1)
    {
      int height = cairo_image_surface_get_height (surface);
      cairo_rectangle_int_t r = {(int) (width * cb.x1 + 0.5),
                                 (int) (height * cb.y1 + 0.5),
                                 (int) (width * (cb.x2 - cb.x1) + 0.5),
                                 (int) (height * (cb.y2 - cb.y1) + 0.5)};
      cairo_surface_t *nsurface =
        cairo_image_surface_create (CAIRO_FORMAT_ARGB32, r.width, r.height);
      perror_if_not (cairo_surface_status (surface) == CAIRO_STATUS_SUCCESS,
                     "%s", "Failed to create cairo surface");
      cairo_destroy (cr);
      cr = cairo_create (nsurface);
      perror_if_not (cairo_status (cr) == CAIRO_STATUS_SUCCESS,
                     "%s", "Failed to create cairo context");
      cairo_set_source_surface (cr, surface, -r.x, -r.y);
      cairo_paint (cr);
      cairo_surface_destroy (surface);
      surface = nsurface;
    }

 theend:
  image_write_print_response (surface, PNG);

 error:
  if (error_msg) g_free (error_msg);
  if (cr) cairo_destroy (cr);
  if (surface) cairo_surface_destroy (surface);
  if (page) g_object_unref (page);
}

const command_arg_type_t cmd_boundingbox_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM,                 /* page number */
    /* region */
  };

static void
cmd_boundingbox (const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args[0].value.doc;
  int pn = args[1].value.natnum;
  PopplerPage *page = poppler_document_get_page(doc->pdf, pn - 1);
  cairo_surface_t *surface = NULL;
  int width, height;
  double pt_width, pt_height;
  unsigned char *data, *data_p;
  PopplerRectangle bbox;
  int i, j;

  perror_if_not (page, "No such page %d", pn);
  poppler_page_get_size (page, &pt_width, &pt_height);
  surface = image_render_page (doc->pdf, page, (int) pt_width, 1,
                               &doc->options.render);

  perror_if_not (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS,
                "Failed to render page");

  width = cairo_image_surface_get_width (surface);
  height = cairo_image_surface_get_height (surface);
  data = cairo_image_surface_get_data (surface);

  /* Determine the bbox by comparing each pixel in the 4 corner
     stripes with the origin. */
  for (i = 0; i < width; ++i)
    {
      data_p = data + 4 * i;
      for (j = 0; j < height; ++j, data_p += 4 * width)
        {
          if (! ARGB_EQUAL (data, data_p))
            break;
        }
      if (j < height)
        break;
    }
  bbox.x1 = i;

  for (i = width - 1; i > -1; --i)
    {
      data_p = data + 4 * i;
      for (j = 0; j < height; ++j, data_p += 4 * width)
        {
          if (! ARGB_EQUAL (data, data_p))
            break;
        }
      if (j < height)
        break;
    }
  bbox.x2 = i + 1;

  for (i = 0; i < height; ++i)
    {
      data_p = data + 4 * i * width;
      for (j = 0; j < width; ++j, data_p += 4)
        {
          if (! ARGB_EQUAL (data, data_p))
            break;
        }
      if (j < width)
        break;
    }
  bbox.y1 = i;

  for (i = height - 1; i > -1; --i)
    {
      data_p = data + 4 * i * width;
      for (j = 0; j < width; ++j, data_p += 4)
        {
          if (! ARGB_EQUAL (data, data_p))
            break;
        }
      if (j < width)
        break;
    }
  bbox.y2 = i + 1;

  OK_BEGIN ();
  if (bbox.x1 >= bbox.x2 || bbox.y1 >= bbox.y2)
    {
      /* empty page */
      puts ("0:0:1:1");
    }
  else
    {
      printf ("%f:%f:%f:%f\n",
              bbox.x1 / width,
              bbox.y1 / height,
              bbox.x2 / width,
              bbox.y2 / height);
    }
  OK_END ();

 error:
  if (surface) cairo_surface_destroy (surface);
  if (page) g_object_unref (page);
}

const command_arg_type_t cmd_charlayout_spec[] =
  {
    ARG_DOC,
    ARG_NATNUM,                 /* page number */
    ARG_EDGES_OR_POSITION,      /* region or position */
  };

static void
cmd_charlayout(const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerDocument *doc = args[0].value.doc->pdf;
  int pn = args[1].value.natnum;
  PopplerRectangle region = args[2].value.rectangle;
  double width, height;
  PopplerPage *page = poppler_document_get_page(doc, pn - 1);
  char *text = NULL;
  char *text_p;
  PopplerRectangle *rectangles = NULL;
  guint nrectangles;
  int i;
  gboolean have_position = region.y2 < 0;

  perror_if_not (page, "No such page %d", pn);

  text = poppler_page_get_text (page);
  text_p = text;
  poppler_page_get_text_layout (page, &rectangles, &nrectangles);
  poppler_page_get_size (page, &width, &height);
  region.x1 *= width;
  region.x2 *= width;
  region.y1 *= height;
  region.y2 *= height;

  OK_BEGIN ();
  for (i = 0; i < nrectangles && *text_p; ++i)
    {
      PopplerRectangle *r = &rectangles[i];
      char *nextc = g_utf8_offset_to_pointer (text_p, 1);

      if ((have_position
           && region.x1 >= r->x1
           && region.x1 <= r->x2
           && region.y1 >= r->y1
           && region.y1 <= r->y2)
          || (! have_position
              && r->x1 >= region.x1
              && r->y1 >= region.y1
              && r->x2 <= region.x2
              && r->y2 <= region.y2))
        {
          char endc = *nextc;

          printf ("%f %f %f %f:",
                  r->x1 / width, r->y1 / height,
                  r->x2 / width, r->y2 / height);
          *nextc = '\0';
          print_response_string (text_p, NEWLINE);
          *nextc = endc;
        }
      text_p = nextc;
    }
  OK_END ();

  g_free (rectangles);
  g_object_unref (page);
  g_free (text);

 error:
  return;
}

const document_option_t document_options [] =
  {
    DEC_DOPT (":render/usecolors", ARG_NATNUM, render.usecolors),
    DEC_DOPT (":render/printed", ARG_BOOL, render.printed),
    DEC_DOPT (":render/foreground", ARG_COLOR, render.fg),
    DEC_DOPT (":render/background", ARG_COLOR, render.bg),
    DEC_DOPT (":render/gamma", ARG_DOUBLE, render.gamma),
    DEC_DOPT (":render/gammabeforeinvert", ARG_BOOL, render.gammabeforeinvert),
  };

const command_arg_type_t cmd_getoptions_spec[] =
  {
    ARG_DOC,
  };

static void
cmd_getoptions(const epdfinfo_t *ctx, const command_arg_t *args)
{
  document_t *doc = args[0].value.doc;
  int i;
  OK_BEGIN ();
  for (i = 0; i < G_N_ELEMENTS (document_options); ++i)
    {
      command_arg_t arg;

      arg.type = document_options[i].type;
      memcpy (&arg.value,
              ((char*) &doc->options) + document_options[i].offset,
              command_arg_type_size (arg.type));
      print_response_string (document_options[i].name, COLON);
      command_arg_print (&arg);
      puts("");
    }
  OK_END ();
}

const command_arg_type_t cmd_setoptions_spec[] =
  {
    ARG_DOC,
    ARG_REST                    /* key value pairs */
  };

static void
cmd_setoptions(const epdfinfo_t *ctx, const command_arg_t *args)
{
  int i = 0;
  document_t *doc = args[0].value.doc;
  int nrest = args[1].value.rest.nargs;
  char * const *rest = args[1].value.rest.args;
  gchar *error_msg = NULL;
  document_options_t opts = doc->options;
  const size_t nopts = G_N_ELEMENTS (document_options);

  perror_if_not (nrest % 2 == 0, "Even number of key/value pairs expected");

  while (i < nrest)
    {
      int j;
      command_arg_t key, value;

      perror_if_not (command_arg_parse_arg
                     (ctx, rest[i], &key, ARG_NONEMPTY_STRING, &error_msg),
                     "%s", error_msg);

      ++i;
      for (j = 0; j < nopts; ++j)
        {
          const document_option_t *dopt = &document_options[j];
          if (! strcmp (key.value.string, dopt->name))
            {
              perror_if_not (command_arg_parse_arg
                             (ctx, rest[i], &value, dopt->type, &error_msg),
                             "%s", error_msg);
              memcpy (((char*) &opts) + dopt->offset,
                      &value.value, command_arg_type_size (value.type));
              break;
            }
        }
      perror_if_not (j < nopts, "Unknown option: %s", key.value.string);
      ++i;
    }
  doc->options = opts;
  cmd_getoptions (ctx, args);

 error:
  if (error_msg) g_free (error_msg);
}

const command_arg_type_t cmd_pagelabels_spec[] =
  {
    ARG_DOC,
  };

static void
cmd_pagelabels(const epdfinfo_t *ctx, const command_arg_t *args)
{
  PopplerDocument *doc = args[0].value.doc->pdf;
  int i;

  OK_BEGIN ();
  for (i = 0; i < poppler_document_get_n_pages (doc); ++i)
    {
      PopplerPage *page = poppler_document_get_page(doc, i);
      gchar *label = poppler_page_get_label (page);

      print_response_string (label ? label : "", NEWLINE);
      g_object_unref (page);
      g_free (label);
    }
  OK_END ();
}

const command_arg_type_t cmd_ping_spec[] =
  {
    ARG_STRING                  /* any message */
  };

static void
cmd_ping (const epdfinfo_t *ctx, const command_arg_t *args)
{
  const gchar *msg = args[0].value.string;
  OK_BEGIN ();
  print_response_string (msg, NEWLINE);
  OK_END ();
}


/* ================================================================== *
 * Main
 * ================================================================== */

static const command_t commands [] =
  {
    /* Basic */
    DEC_CMD (ping),
    DEC_CMD (features),
    DEC_CMD (open),
    DEC_CMD (close),
    DEC_CMD (quit),
    DEC_CMD (getoptions),
    DEC_CMD (setoptions),

    /* Searching */
    DEC_CMD2 (search_string, "search-string"),
    DEC_CMD2 (search_regexp, "search-regexp"),
    DEC_CMD2 (regexp_flags, "regexp-flags"),

    /* General Information */
    DEC_CMD (metadata),
    DEC_CMD (outline),
    DEC_CMD2 (number_of_pages, "number-of-pages"),
    DEC_CMD (pagelinks),
    DEC_CMD (gettext),
    DEC_CMD (getselection),
    DEC_CMD (pagesize),
    DEC_CMD (boundingbox),
    DEC_CMD (charlayout),

    /* General Information */
    DEC_CMD (metadata),
    DEC_CMD (outline),
    DEC_CMD2 (number_of_pages, "number-of-pages"),
    DEC_CMD (pagelinks),
    DEC_CMD (gettext),
    DEC_CMD (getselection),
    DEC_CMD (pagesize),
    DEC_CMD (boundingbox),
    DEC_CMD (charlayout),
    DEC_CMD (pagelabels),

    /* Annotations */
    DEC_CMD (getannots),
    DEC_CMD (getannot),
#ifdef HAVE_POPPLER_ANNOT_WRITE
    DEC_CMD (addannot),
    DEC_CMD (delannot),
    DEC_CMD (editannot),
    DEC_CMD (save),
#endif

    /* Attachments */
    DEC_CMD2 (getattachment_from_annot, "getattachment-from-annot"),
    DEC_CMD (getattachments),

    /* Synctex */
    DEC_CMD2 (synctex_forward_search, "synctex-forward-search"),
    DEC_CMD2 (synctex_backward_search, "synctex-backward-search"),

    /* Rendering */
    DEC_CMD (renderpage),
  };

int main(int argc, char **argv)
{
  epdfinfo_t ctx = {0};
  char *line = NULL;
  ssize_t read;
  size_t line_size;
  const char *error_log = "/dev/null";

#ifdef __MINGW32__
  error_log = "NUL";
  _setmode(_fileno(stdin), _O_BINARY);
  _setmode(_fileno(stdout), _O_BINARY);
#endif

  if (argc > 2)
    {
      fprintf(stderr, "usage: epdfinfo [ERROR-LOGFILE]\n");
      exit (EXIT_FAILURE);
    }
  if (argc == 2)
    error_log = argv[1];

  if (! freopen (error_log, "a", stderr))
    err (2, "Unable to redirect stderr");

#if ! GLIB_CHECK_VERSION(2,36,0)
  g_type_init ();
#endif

  ctx.documents = g_hash_table_new (g_str_hash, g_str_equal);

  setvbuf (stdout, NULL, _IOFBF, BUFSIZ);

  while ((read = getline (&line, &line_size, stdin)) != -1)
    {
      int nargs = 0;
      command_arg_t *cmd_args = NULL;
      char **args = NULL;
      gchar *error_msg = NULL;
      int i;

      if (read <= 1 || line[read - 1] != '\n')
        {
          fprintf (stderr, "Skipped parts of a line: `%s'\n", line);
          goto next_line;
        }

      line[read - 1] = '\0';
      args = command_arg_split (line, &nargs);
      if (nargs == 0)
        continue;

      for (i = 0; i < G_N_ELEMENTS (commands);  i++)
        {
          if (! strcmp (commands[i].name, args[0]))
            {
              if (commands[i].nargs == 0
                  || (cmd_args = command_arg_parse (&ctx, args + 1, nargs - 1,
                                                    commands + i, &error_msg)))
                {
                  commands[i].execute (&ctx, cmd_args);
                  if (commands[i].nargs > 0)
                    free_command_args (cmd_args, commands[i].nargs);
                }
              else
                {
                  printf_error_response ("%s", error_msg ? error_msg :
                                         "Unknown error (this is a bug)");
                }
              if (error_msg)
                g_free (error_msg);
              break;
            }
        }
      if (G_N_ELEMENTS (commands) == i)
        {
          printf_error_response ("Unknown command: %s", args[0]);
        }
      for (i = 0; i < nargs; ++i)
        g_free (args[i]);
      g_free (args);
    next_line:
      free (line);
      line = NULL;
    }

  if (ferror (stdin))
    err (2, NULL);
  exit (EXIT_SUCCESS);
}
