/* webcollage-helper-cocoa --- scales and pastes one image into another
 * xscreensaver, Copyright (c) 2002-2018 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 Cocoa implementation.  See webcollage-helper.c for the
   GDK + JPEGlib implementation.
 */

#import <Cocoa/Cocoa.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>


#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4
 typedef int          NSInteger;
 typedef unsigned int NSUInteger;
#endif


char *progname;
static int verbose_p = 0;

static void write_image (NSImage *img, const char *file);


/* NSImage can't load PPMs by default...
 */
static NSImage *
load_ppm_image (const char *file)
{
  FILE *in = fopen (file, "r");
  if (! in) return 0;

  char buf[255];

  char *s = fgets (buf, sizeof(buf)-1, in);		/* P6 */
  if (!s || !!strcmp (s, "P6\n")) 
    return 0;

  s = fgets (buf, sizeof(buf)-1, in);			/* W H */
  if (!s)
    return 0;

  int w = 0, h = 0, d = 0;
  if (2 != sscanf (buf, " %d %d \n", &w, &h))
    return 0;
  if (w <= 0 || h <= 0)
    return 0;

  s = fgets (buf, sizeof(buf)-1, in);			/* 255 */
  if (!s)
    return 0;

  if (1 != sscanf (buf, " %d \n", &d))
    return 0;
  if (d != 255)
    return 0;

  int size = (w * (h+1) * 3);
  unsigned char *bits = malloc (size);
  if (!bits) return 0;

  int n = read (fileno (in), (void *) bits, size);	/* body */
  if (n < 20) return 0;

  fclose (in);
  
  NSBitmapImageRep *rep =
    [[NSBitmapImageRep alloc]
      initWithBitmapDataPlanes: &bits
                    pixelsWide: w
                    pixelsHigh: h
                 bitsPerSample: 8
               samplesPerPixel: 3
                      hasAlpha: NO
                      isPlanar: NO
                colorSpaceName: NSDeviceRGBColorSpace
                  bitmapFormat: NSAlphaFirstBitmapFormat
                   bytesPerRow: w * 3
                  bitsPerPixel: 8 * 3];

  NSImage *image = [[NSImage alloc] initWithSize: NSMakeSize (w, h)];
  [image addRepresentation: rep];
  [rep release];

  // #### 'bits' is leaked... the NSImageRep doesn't free it when freed.

  return image;
}


static NSImage *
load_image (const char *file)
{
  NSImage *image = [[NSImage alloc] 
                     initWithContentsOfFile:
                       [NSString stringWithCString: file
                                          encoding: NSUTF8StringEncoding]];
  if (! image)
    image = load_ppm_image (file);

  if (! image) {
    fprintf (stderr, "%s: unable to load %s\n", progname, file);
    exit (1);
  }

  
  // [NSImage size] defaults to the image size in points instead of pixels,
  // so if an image file specified "pixels per inch" we can end up with
  // absurdly sized images.  Set it back to 1:1 pixel:point.
  //
  NSImageRep *rep = [image.representations objectAtIndex:0];
  image.size = NSMakeSize (rep.pixelsWide, rep.pixelsHigh);

  return image;
}


static void
bevel_image (NSImage *img, int bevel_pct,
             int x, int y, int w, int h, double scale)
{
  int small_size = (w > h ? h : w);

  int bevel_size = small_size * (bevel_pct / 100.0);

  bevel_size /= scale;

  /* 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;


  NSBitmapImageRep *rep =
    [[NSBitmapImageRep alloc]
      initWithBitmapDataPlanes: NULL
                    pixelsWide: w
                    pixelsHigh: h
                 bitsPerSample: 8
               samplesPerPixel: 4
                      hasAlpha: YES
                      isPlanar: NO
                colorSpaceName: NSDeviceRGBColorSpace
                  bitmapFormat: NSAlphaFirstBitmapFormat
                   bytesPerRow: 0
                  bitsPerPixel: 0];

  NSInteger 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
    }

  memset ([rep bitmapData], 0xFFFFFFFF,
          [rep bytesPerRow] * h);

  for (yy = 0; yy < h; yy++)
    {
      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)
            {
              NSUInteger p[4];
              p[0] = 0xFF * r;
              p[1] = p[2] = p[3] = 0xFF;
              [rep setPixel:p atX:xx y:yy];
            }
        }
    }

  free (ramp);

  NSImage *bevel_img = [[NSImage alloc]
                         initWithData: [rep TIFFRepresentation]];

  [img lockFocus];
  y = [img size].height - (y + h);
  [bevel_img drawAtPoint: NSMakePoint (x, y)
                fromRect: NSMakeRect (0, 0, w, h)
               operation: NSCompositingOperationDestinationIn /* Destination image
                                                      wherever both images are
                                                      opaque, transparent
                                                      elsewhere. */
                fraction: 1.0];
  [img unlockFocus];

  [rep release];
  [bevel_img release];

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


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)
{
  NSImage *paste_img = load_image (paste_file);
  NSImage *base_img  = load_image (base_file);

  int paste_w = [paste_img size].width;
  int paste_h = [paste_img size].height;

  int base_w  = [base_img size].width;
  int base_h  = [base_img size].height;

  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 (bevel_pct > 0 && paste_w > 5 && paste_h > 5)
    bevel_image (paste_img, bevel_pct,
                 from_x, from_y, w, h, 
                 from_scale);

  int scaled_w = w * from_scale;
  int scaled_h = h * from_scale;

  from_y = paste_h - (from_y + h);  // Cocoa flipped coordinate system
  to_y   = base_h  - (to_y + scaled_h);

  [base_img lockFocus];
  [paste_img drawInRect: NSMakeRect (to_x, to_y, scaled_w, scaled_h)
               fromRect: NSMakeRect (from_x, from_y, w, h)
              operation: NSCompositingOperationSourceOver
               fraction: opacity];
  [base_img unlockFocus];

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

  [paste_img release];
  write_image (base_img, base_file);
  [base_img release];
}


static NSColor *
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 [NSColor colorWithRed: r / 255.0
                         green: g / 255.0
                          blue: b / 255.0
                         alpha: 1.0];
}


static void
create (const char *color,
        int w, int h,
        const char *file)
{
  NSColor *c = parse_color (color);
  NSImage *img = [[NSImage alloc] initWithSize:NSMakeSize(w, h)];
  [img lockFocus];
  [c drawSwatchInRect:NSMakeRect(0, 0, w, h)];
  [img unlockFocus];
  write_image (img, file);
  [img release];
}


static void
write_image (NSImage *img, const char *file)
{
  float jpeg_quality = .85;

  // Load the NSImage's contents into an NSBitmapImageRep.

#if 0
  // If the local display is Retina, this doubles the size of the output JPEG.
  NSBitmapImageRep *bit_rep = [NSBitmapImageRep
                                imageRepWithData:[img TIFFRepresentation]];
#else
  // Render the image into a rep using pixels instead of points.
  NSBitmapImageRep *bit_rep = [[NSBitmapImageRep alloc]
                                initWithBitmapDataPlanes:NULL
                                pixelsWide:[img size].width
                                pixelsHigh:[img size].height
                                bitsPerSample:8
                                samplesPerPixel:4
                                hasAlpha:YES
                                isPlanar:NO
                                colorSpaceName:NSCalibratedRGBColorSpace
                                bytesPerRow:0
                                bitsPerPixel:0];
    bit_rep.size = [img size];
    [NSGraphicsContext saveGraphicsState];
    [NSGraphicsContext setCurrentContext:
                         [NSGraphicsContext
                           graphicsContextWithBitmapImageRep:bit_rep]];
    [img drawInRect:NSMakeRect(0, 0, [img size].width, [img size].height)
           fromRect:NSZeroRect operation:NSCompositingOperationCopy fraction:1.0];
    [NSGraphicsContext restoreGraphicsState];
#endif

  // Write the bitmapImageRep to a JPEG file.
  if (bit_rep == nil)
    {
      fprintf (stderr, "%s: error converting image?\n", progname);
      exit (1);
    }

  if (verbose_p)
    fprintf (stderr, "%s: writing %s (q=%d%%) ", progname, file, 
             (int) (jpeg_quality * 100));

  NSData *jpeg_data = [bit_rep representationUsingType:NSJPEGFileType
                               properties:@{ NSImageCompressionFactor: 
                                             [NSNumber numberWithFloat:
                                                         jpeg_quality] }];
  [jpeg_data writeToFile:
               [NSString stringWithCString:file
                                  encoding:NSISOLatin1StringEncoding]
             atomically:YES];

  if (verbose_p)
    {
      struct stat st;
      if (stat (file, &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);
    }
}


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;

  // Much of Cocoa needs one of these to be available.
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  //Need an NSApp instance to make [NSImage TIFFRepresentation] work
  NSApp = [NSApplication sharedApplication];
  [NSApp autorelease];

  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 (w == 0 || h == 0 || 
          w > 10240 || h > 10240) {
        fprintf (stderr, "%s: absurd size: %d x %d\n", progname, w, h);
        exit (1);
      }

      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();
    }

  [pool release];

  exit (0);
}
