/* webcollage-helper --- scales and pastes one image into another
 * xscreensaver, Copyright © 2002-2022 Jamie Zawinski <jwz@jwz.org>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 */

/* This is the GDK + JPEGlib implementation.  See webcollage-helper-cocoa.m
   for the Cocoa implementation.

   It is annoying that this requires libjpeg, but you may be surprised to
   learn that, massive as the GdkPixbuf library is, it has no facility for
   writing image files, only reading.
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#if defined(HAVE_GDK_PIXBUF) && defined(HAVE_JPEGLIB)  /* whole file */

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>

# if (__GNUC__ >= 4)	/* Ignore useless warnings generated by GTK headers */
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wlong-long"
#  pragma GCC diagnostic ignored "-Wvariadic-macros"
#  pragma GCC diagnostic ignored "-Wpedantic"
# endif

#undef HAVE_STDLIB_H /* stupid jconfig.h! */
#include <jpeglib.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

# if (__GNUC__ >= 4)
#  pragma GCC diagnostic pop
# endif

char *progname;
static int verbose_p = 0;

static void add_jpeg_comment (struct jpeg_compress_struct *cinfo);
static void write_pixbuf (GdkPixbuf *pb, const char *file);

static GdkPixbuf *
load_pixbuf (const char *file)
{
  GError *err = NULL;
  GdkPixbuf *pb = gdk_pixbuf_new_from_file (file, &err);

  if (!pb)
    {
      fprintf (stderr, "%s: %s\n", progname, err->message);
      g_error_free (err);
      exit (1);
    }

  return pb;
}


static void
bevel_image (GdkPixbuf **pbP, int bevel_pct,
             int x, int y, int w, int h)
{
  GdkPixbuf *pb = *pbP;
  int small_size = (w > h ? h : w);

  int bevel_size = small_size * (bevel_pct / 100.0);

  /* Use a proportionally larger bevel size for especially small images. */
  if      (bevel_size < 20 && small_size > 40) bevel_size = 20;
  else if (bevel_size < 10 && small_size > 20) bevel_size = 10;
  else if (bevel_size < 5)    /* too small to bother bevelling */
    return;

  /* Ensure the pixbuf has an alpha channel. */
  if (! gdk_pixbuf_get_has_alpha (pb))
    {
      GdkPixbuf *pb2 = gdk_pixbuf_add_alpha (pb, FALSE, 0, 0, 0);
      g_object_unref (pb);
      pb = pb2;
    }

  {
    guchar *data = gdk_pixbuf_get_pixels (pb);
    guchar *line;
    int rs = gdk_pixbuf_get_rowstride (pb);
    int ch = gdk_pixbuf_get_n_channels (pb);
    int xx, yy;
    double *ramp = (double *) malloc (sizeof(*ramp) * (bevel_size + 1));

    if (!ramp)
      {
        fprintf (stderr, "%s: out of memory (%d)\n", progname, bevel_size);
        exit (1);
      }

    for (xx = 0; xx <= bevel_size; xx++)
      {

# if 0  /* linear */
        ramp[xx] = xx / (double) bevel_size;

# else /* sinusoidal */
        double p = (xx / (double) bevel_size);
        double s = sin (p * M_PI / 2);
        ramp[xx] = s;
# endif
      }

    line = data + (rs * y);
    for (yy = 0; yy < h; yy++)
      {
        guchar *p = line + (x * ch);
        for (xx = 0; xx < w; xx++)
          {
            double rx, ry, r;

            if (xx < bevel_size)           rx = ramp[xx];
            else if (xx >= w - bevel_size) rx = ramp[w - xx - 1];
            else rx = 1;

            if (yy < bevel_size)           ry = ramp[yy];
            else if (yy >= h - bevel_size) ry = ramp[h - yy - 1];
            else ry = 1;

            r = rx * ry;
            if (r != 1)
              p[ch-1] *= r;

            p += ch;
          }
        line += rs;
      }

#if 0  /* show the ramp */
    line = data + (rs * y);
    for (yy = 0; yy < h; yy++)
      {
        guchar *p = line + (x * ch);
        for (xx = 0; xx < w; xx++)
          {
            int cc = 0;
            for (cc = 0; cc < ch-1; cc++)
              p[cc] = 255;
            p += ch;
          }
        line += rs;
      }
#endif

    free (ramp);

    if (verbose_p)
      fprintf (stderr, "%s: added %d%% bevel (%d px)\n", progname,
               bevel_pct, bevel_size);
  }

  *pbP = pb;
}


static void
paste (const char *paste_file,
       const char *base_file,
       double from_scale,
       double opacity, int bevel_pct,
       int from_x, int from_y, int to_x, int to_y,
       int w, int h)
{
  GdkPixbuf *paste_pb;
  GdkPixbuf *base_pb;

  int paste_w, paste_h;
  int base_w, base_h;

  paste_pb = load_pixbuf (paste_file);
  base_pb  = load_pixbuf (base_file);

  paste_w = gdk_pixbuf_get_width (paste_pb);
  paste_h = gdk_pixbuf_get_height (paste_pb);

  base_w = gdk_pixbuf_get_width (base_pb);
  base_h = gdk_pixbuf_get_height (base_pb);

  if (verbose_p)
    {
      fprintf (stderr, "%s: loaded %s: %dx%d\n",
               progname, base_file, base_w, base_h);
      fprintf (stderr, "%s: loaded %s: %dx%d\n",
               progname, paste_file, paste_w, paste_h);
    }

  if (from_scale != 1.0)
    {
      int new_w = paste_w * from_scale;
      int new_h = paste_h * from_scale;
      GdkPixbuf *new_pb = gdk_pixbuf_scale_simple (paste_pb, new_w, new_h,
                                                   GDK_INTERP_HYPER);
      g_object_unref (paste_pb);
      paste_pb = new_pb;
      paste_w = gdk_pixbuf_get_width (paste_pb);
      paste_h = gdk_pixbuf_get_height (paste_pb);

      if (verbose_p)
        fprintf (stderr, "%s: %s: scaled to %dx%d (%.2f)\n",
                 progname, paste_file, paste_w, paste_h, from_scale);
    }

  if (w == 0) w = paste_w - from_x;
  if (h == 0) h = paste_h - from_y;

  {
    int ofx = from_x;
    int ofy = from_y;
    int otx = to_x;
    int oty = to_y;
    int ow = w;
    int oh = h;

    int clipped = 0;

    if (from_x < 0)		/* from left out of bounds */
      {
        w += from_x;
        from_x = 0;
        clipped = 1;
      }

    if (from_y < 0)		/* from top out of bounds */
      {
        h += from_y;
        from_y = 0;
        clipped = 1;
      }

    if (to_x < 0)		/* to left out of bounds */
      {
        w += to_x;
        from_x -= to_x;
        to_x = 0;
        clipped = 1;
      }

    if (to_y < 0)		/* to top out of bounds */
      {
        h += to_y;
        from_y -= to_y;
        to_y = 0;
        clipped = 1;
      }

    if (from_x + w > paste_w)	/* from right out of bounds */
      {
        w = paste_w - from_x;
        clipped = 1;
      }

    if (from_y + h > paste_h)	/* from bottom out of bounds */
      {
        h = paste_h - from_y;
        clipped = 1;
      }

    if (to_x + w > base_w)	/* to right out of bounds */
      {
        w = base_w - to_x;
        clipped = 1;
      }

    if (to_y + h > base_h)	/* to bottom out of bounds */
      {
        h = base_h - to_y;
        clipped = 1;
      }


    if (clipped && verbose_p)
      {
        fprintf (stderr, "clipped from: %dx%d %d,%d %d,%d\n",
                 ow, oh, ofx, ofy, otx, oty);
        fprintf (stderr, "clipped   to: %dx%d %d,%d %d,%d\n",
                 w, h, from_x, from_y, to_x, to_y);
      }
  }

  if (bevel_pct > 0)
    bevel_image (&paste_pb, bevel_pct,
                 from_x, from_y, w, h);

  if (opacity == 1.0 && bevel_pct == 0)
    gdk_pixbuf_copy_area (paste_pb,
                          from_x, from_y, w, h,
                          base_pb,
                          to_x, to_y);
  else
    {
      from_x++;  /* gdk_pixbuf_composite gets confused about the bevel: */
      from_y++;  /* it leaves a stripe on the top and left if we try to */
      to_x++;    /* start at 0,0, so pull it right and down by 1 pixel. */
      to_y++;    /* (problem seen in gtk2-2.4.14-2.fc3) */
      w--;
      h--;

      if (w > 0 && h > 0)
        gdk_pixbuf_composite (paste_pb, base_pb,
                              to_x, to_y, w, h,
                              to_x - from_x, to_y - from_y,
                              1.0, 1.0,
                              GDK_INTERP_HYPER,
                              opacity * 255);
    }

  if (verbose_p)
    fprintf (stderr, "%s: pasted %dx%d from %d,%d to %d,%d\n",
             progname, paste_w, paste_h, from_x, from_y, to_x, to_y);

  g_object_unref (paste_pb);
  write_pixbuf (base_pb, base_file);
  g_object_unref (base_pb);
}


static guint32
parse_color (const char *s)
{
  static const char hex[128] =
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0,
     0, 10,11,12,13,14,15,0, 0, 0, 0, 0, 0, 0, 0, 0,
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     0, 10,11,12,13,14,15,0, 0, 0, 0, 0, 0, 0, 0, 0,
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

  unsigned char r=0, g=0, b=0;

  if      (!strcasecmp (s, "black")) ;
  else if (!strcasecmp (s, "white")) r = g = b = 0xFF;
  else if (!strcasecmp (s, "red"))   r = 0xFF;
  else if (!strcasecmp (s, "green")) g = 0xFF;
  else if (!strcasecmp (s, "blue"))  b = 0xFF;
  else
    {
      if (*s != '#' || strlen(s) != 7)
        {
          fprintf (stderr, "%s: unparsable color: \"%s\"\n", progname, s);
          exit (1);
        }
      s++;
      r = (hex[(int) s[0]] << 4) | hex[(int) s[1]], s += 2;
      g = (hex[(int) s[0]] << 4) | hex[(int) s[1]], s += 2;
      b = (hex[(int) s[0]] << 4) | hex[(int) s[1]], s += 2;
    }

  return (r << 16) | (g << 8) | b;
}


static void
create (const char *color,
        int w, int h,
        const char *file)
{
  int i;
  GdkPixbuf *pb;
  guint32 pixel = parse_color (color);
  unsigned char *bytes = malloc (w * h * 3);
  if (!bytes) abort();
  for (i = 0; i < w * h * 3; i += 3)
    {
      bytes[i]   = 0xFF & (pixel >> 16);
      bytes[i+1] = 0xFF & (pixel >> 8);
      bytes[i+2] = 0xFF & (pixel);
    }

  pb = gdk_pixbuf_new_from_data (bytes, GDK_COLORSPACE_RGB,
                                 FALSE, 8, /* alpha, sample size */
                                 w, h,
                                 w * 3,   /* rowstride */
                                 NULL, 0);
  if (!pb) abort();
  write_pixbuf (pb, file);
  g_object_unref (pb);
  free (bytes);
}


static void
write_pixbuf (GdkPixbuf *pb, const char *file)
{
  int jpeg_quality = 85;

  int w = gdk_pixbuf_get_width (pb);
  int h = gdk_pixbuf_get_height (pb);
  guchar *data = gdk_pixbuf_get_pixels (pb);
  int ww = gdk_pixbuf_get_rowstride (pb);
  int channels = gdk_pixbuf_get_n_channels (pb);

  struct jpeg_compress_struct cinfo;
  struct jpeg_error_mgr jerr;
  FILE *out;

  if (channels != 3)
    {
      fprintf (stderr, "%s: %d channels?\n", progname, channels);
      exit (1);
    }

  cinfo.err = jpeg_std_error (&jerr);
  jpeg_create_compress (&cinfo);

  out = fopen (file, "wb");
  if (!out)
    {
      char buf[255];
      sprintf (buf, "%.100s: %.100s", progname, file);
      perror (buf);
      exit (1);
    }
  else if (verbose_p)
    fprintf (stderr, "%s: writing %s...", progname, file);

  jpeg_stdio_dest (&cinfo, out);

  cinfo.image_width = w;
  cinfo.image_height = h;
  cinfo.input_components = channels;
  cinfo.in_color_space = JCS_RGB;

  jpeg_set_defaults (&cinfo);
  jpeg_simple_progression (&cinfo);
  jpeg_set_quality (&cinfo, jpeg_quality, TRUE);

  jpeg_start_compress (&cinfo, TRUE);
  add_jpeg_comment (&cinfo);

  {
    guchar *d = data;
    guchar *end = d + (ww * h);
    while (d < end)
      {
        jpeg_write_scanlines (&cinfo, &d, 1);
        d += ww;
      }
  }

  jpeg_finish_compress (&cinfo);
  jpeg_destroy_compress (&cinfo);

  if (verbose_p)
    {
      struct stat st;
      fflush (out);
      if (fstat (fileno (out), &st))
        {
          char buf[255];
          sprintf (buf, "%.100s: %.100s", progname, file);
          perror (buf);
          exit (1);
        }
      fprintf (stderr, " %luK\n", ((unsigned long) st.st_size + 1023) / 1024);
    }

  fclose (out);
}


static void
add_jpeg_comment (struct jpeg_compress_struct *cinfo)
{
  time_t now = time ((time_t *) 0);
  struct tm *tm = localtime (&now);
  const char fmt[] =
    "\r\n"
    "    Generated by WebCollage: Exterminate All Rational Thought. \r\n"
    "    Copyright (c) 1999-%Y by Jamie Zawinski <jwz@jwz.org> \r\n"
    "\r\n"
    "        https://www.jwz.org/webcollage/ \r\n"
    "\r\n"
    "    This is what the web looked like on %d %b %Y at %I:%M:%S %p %Z. \r\n"
    "\r\n";
  char comment[sizeof(fmt) + 100];
  strftime (comment, sizeof(comment)-1, fmt, tm);
  jpeg_write_marker (cinfo, JPEG_COM,
                     (unsigned char *) comment,
                     strlen (comment));
}


static void
usage (void)
{
  fprintf (stderr,
           "\nusage: %s [-v] paste-file base-file\n"
           "\t from-scale opacity\n"
           "\t from-x from-y to-x to-y w h\n"
           "\n"
           "\t Pastes paste-file into base-file.\n"
           "\t base-file will be overwritten (with JPEG data.)\n"
           "\t scaling is applied first: coordinates apply to scaled image.\n"
           "\n"
           "usage: %s [-v] color width height output-file\n"
           "\t Creates a new image of a solid color.\n\n",
           progname, progname);
  exit (1);
}


int
main (int argc, char **argv)
{
  int i;
  char *paste_file, *base_file, *s, dummy;
  double from_scale, opacity;
  int from_x, from_y, to_x, to_y, w, h, bevel_pct;

  i = 0;
  progname = argv[i++];
  s = strrchr (progname, '/');
  if (s) progname = s+1;

  if (argc < 2) usage();

  if (!strcmp(argv[i], "-v"))
    verbose_p++, i++;

  if (argc == 11 || argc == 12)
    {
      paste_file = argv[i++];
      base_file = argv[i++];

      if (*paste_file == '-') usage();
      if (*base_file == '-') usage();

      s = argv[i++];
      if (1 != sscanf (s, " %lf %c", &from_scale, &dummy)) usage();
      if (from_scale <= 0 || from_scale > 100) usage();

      s = argv[i++];
      if (1 != sscanf (s, " %lf %c", &opacity, &dummy)) usage();
      if (opacity <= 0 || opacity > 1) usage();

      s = argv[i++]; if (1 != sscanf (s, " %d %c", &from_x, &dummy)) usage();
      s = argv[i++]; if (1 != sscanf (s, " %d %c", &from_y, &dummy)) usage();
      s = argv[i++]; if (1 != sscanf (s, " %d %c", &to_x, &dummy)) usage();
      s = argv[i++]; if (1 != sscanf (s, " %d %c", &to_y, &dummy)) usage();
      s = argv[i++]; if (1 != sscanf (s, " %d %c", &w, &dummy)) usage();
      s = argv[i];   if (1 != sscanf (s, " %d %c", &h, &dummy)) usage();

      bevel_pct = 10; /* #### */

      if (w < 0) usage();
      if (h < 0) usage();

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

      paste (paste_file, base_file,
             from_scale, opacity, bevel_pct,
             from_x, from_y, to_x, to_y,
             w, h);
    }
  else if (argc == 4 || argc == 5)
    {
      char *color = argv[i++];
      s = argv[i++]; if (1 != sscanf (s, " %d %c", &w, &dummy)) usage();
      s = argv[i++]; if (1 != sscanf (s, " %d %c", &h, &dummy)) usage();
      paste_file = argv[i++];
      if (*paste_file == '-') usage();

      create (color, w, h, paste_file);
    }
  else
    {
      usage();
    }

  exit (0);
}

#endif /* HAVE_GDK_PIXBUF && HAVE_JPEGLIB -- whole file */
