/* ximage-loader.c --- converts image files or data to XImages or Pixmap.
 * xscreensaver, Copyright © 1998-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.
 */

#include "screenhackI.h"
#include "ximage-loader.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#if defined(HAVE_GDK_PIXBUF) || defined(HAVE_COCOA) || defined(HAVE_ANDROID)
# undef HAVE_LIBPNG
#endif

#ifdef HAVE_COCOA
# include "grabscreen.h"  /* for osx_load_image_file() */
#endif

#ifdef HAVE_GDK_PIXBUF

# 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

# include <gdk-pixbuf/gdk-pixbuf.h>
# include <gdk-pixbuf-xlib/gdk-pixbuf-xlib.h>

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

#endif /* HAVE_GDK_PIXBUF */

#ifdef HAVE_LIBPNG
# include <png.h>
#endif

#ifdef HAVE_ANDROID
 /* So that debug output shows up in logcat... */
extern void Log(const char *format, ...);
# undef  fprintf
# define fprintf(S, ...) Log(__VA_ARGS__)
#endif

static Bool
bigendian (void)
{
  union { int i; char c[sizeof(int)]; } u;
  u.i = 1;
  return !u.c[0];
}


#ifdef HAVE_GDK_PIXBUF

/* Loads the image to an XImage, RGBA -- GDK Pixbuf version.
 */
static XImage *
make_ximage (Display *dpy, Visual *visual, const char *filename,
             const unsigned char *image_data, unsigned long data_size)
{
  GdkPixbuf *pb;
  static int initted = 0;
  GError *gerr = NULL;

  if (!initted)
    {
# if !GLIB_CHECK_VERSION(2, 36 ,0)
      g_type_init ();
# endif
      if (dpy)
        {
          /* Turns out gdk-pixbuf works even if you don't have display
             connection, which is good news for analogtv-cli. */
          gdk_pixbuf_xlib_init (dpy, DefaultScreen (dpy));
          xlib_rgb_init (dpy, DefaultScreenOfDisplay (dpy));
        }
      initted = 1;
    }

  if (filename)
    {
      pb = gdk_pixbuf_new_from_file (filename, &gerr);
      if (!pb)
        {
          fprintf (stderr, "%s: %s\n", progname, gerr->message);
          return 0;
        }
    }
  else
    {
      GInputStream *s =
        g_memory_input_stream_new_from_data (image_data, data_size, 0);
      pb = gdk_pixbuf_new_from_stream (s, 0, &gerr);

      g_input_stream_close (s, NULL, NULL);
      /* #### valgrind on xflame says there's a small leak in s? */
      g_object_unref (s);

      if (! pb)
        {
          /* fprintf (stderr, "%s: GDK unable to parse image data: %s\n",
                   progname, (gerr ? gerr->message : "?")); */
          return 0;
        }
    }

  if (!pb) abort();

  {
    XImage *image;
    int w = gdk_pixbuf_get_width (pb);
    int h = gdk_pixbuf_get_height (pb);
    guchar *row = gdk_pixbuf_get_pixels (pb);
    int stride = gdk_pixbuf_get_rowstride (pb);
    int chan = gdk_pixbuf_get_n_channels (pb);
    int x, y;

    image = XCreateImage (dpy, visual, 32, ZPixmap, 0, 0, w, h, 32, 0);
    image->data = (char *) malloc(h * image->bytes_per_line);

    /* Set the bit order in the XImage structure to whatever the
       local host's native bit order is.
    */
    image->bitmap_bit_order =
      image->byte_order =
      (bigendian() ? MSBFirst : LSBFirst);

    if (!image->data)
      {
        fprintf (stderr, "%s: out of memory (%d x %d)\n", progname, w, h);
        return 0;
      }

    for (y = 0; y < h; y++)
      {
        guchar *i = row;
        for (x = 0; x < w; x++)
          {
            unsigned long rgba = 0;
            switch (chan) {
            case 1:
              rgba = ((0xFF << 24) |
                      (*i   << 16) |
                      (*i   <<  8) |
                       *i);
              i++;
              break;
            case 3:
              rgba = ((0xFF << 24) |
                      (i[2] << 16) |
                      (i[1] <<  8) |
                      i[0]);
              i += 3;
              break;
            case 4:
              rgba = ((i[3] << 24) |
                      (i[2] << 16) |
                      (i[1] <<  8) |
                      i[0]);
              i += 4;
              break;
            default:
              abort();
              break;
            }
            XPutPixel (image, x, y, rgba);
          }
        row += stride;
      }

    /* #### valgrind on xflame says there's a small leak in pb? */
    g_object_unref (pb);
    return image;
  }
}

#elif defined(HAVE_JWXYZ) /* MacOS, iOS or Android */

/* Loads the image to an XImage, RGBA -- MacOS, iOS or Android version.
 */
static XImage *
make_ximage (Display *dpy, Visual *visual, const char *filename,
             const unsigned char *image_data, unsigned long data_size)
{
  XImage *ximage = 0;

  if (filename)
    {
# ifdef HAVE_COCOA  /* MacOS */
      XRectangle geom;
      Screen *screen = DefaultScreenOfDisplay (dpy);
      Window window = RootWindowOfScreen (screen);
      XWindowAttributes xgwa;
      XGetWindowAttributes (dpy, window, &xgwa);
      Pixmap pixmap =
        XCreatePixmap (dpy, window, xgwa.width, xgwa.height, xgwa.depth);
      int x, y;

      if (! osx_load_image_file (screen, window, pixmap, filename, &geom))
        {
          fprintf (stderr, "%s: %s failed\n", progname, filename);
          return 0;
        }

      ximage = XGetImage (dpy, pixmap, geom.x, geom.y,
                          geom.width, geom.height,
                          ~0L, ZPixmap);
      if (!ximage) abort();

      /* Have to convert ABGR to RGBA */
      for (y = 0; y < ximage->height; y++)
        for (x = 0; x < ximage->width; x++)
          {
            unsigned long p = XGetPixel (ximage, x, y);
            unsigned long a = (p >> 24) & 0xFF;
            unsigned long b = (p >> 16) & 0xFF;
            unsigned long g = (p >>  8) & 0xFF;
            unsigned long r = (p >>  0) & 0xFF;
            p = (r << 24) | (g << 16) | (b << 8) | (a << 0);
            XPutPixel (ximage, x, y, p);
          }

      XFreePixmap (dpy, pixmap);

# else   /* !HAVE_COCOA -- iOS or Android. */
      fprintf (stderr, "%s: image file loading not supported\n", progname);
      return 0;
# endif  /* !HAVE_COCOA */
    }
  else
    {
      ximage = jwxyz_png_to_ximage (dpy, visual, image_data, data_size);
    }

  return ximage;
}

#elif defined(HAVE_LIBPNG)

typedef struct {
  const unsigned char *buf;
  png_size_t siz, ptr;
} png_read_closure;

static void
png_reader_fn (png_structp png_ptr, png_bytep buf, png_size_t siz)
{
  png_read_closure *r = png_get_io_ptr (png_ptr);
  if (siz > r->siz - r->ptr)
    png_error (png_ptr, "PNG internal read error");
  memcpy (buf, r->buf + r->ptr, siz);
  r->ptr += siz;
}


/* Loads the image to an XImage, RGBA -- libpng version.
 */
static XImage *
make_ximage (Display *dpy, Visual *visual,
             const char *filename, const unsigned char *image_data,
             unsigned long data_size)
{
  XImage *image = 0;
  png_structp png_ptr;
  png_infop info_ptr;
  png_infop end_info;
  png_uint_32 width, height, channels;
  int bit_depth, color_type, interlace_type;
  FILE *fp = 0;
  /* Must be at top or it goes out of scope in the setjmp! */
  png_read_closure closure;

  png_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING, 0, 0, 0);
  if (!png_ptr) return 0;

  info_ptr = png_create_info_struct (png_ptr);
  if (!info_ptr)
    {
      png_destroy_read_struct (&png_ptr, 0, 0);
      return 0;
    }

  end_info = png_create_info_struct (png_ptr);
  if (!end_info)
    {
      png_destroy_read_struct (&png_ptr, &info_ptr, 0);
      return 0;
    }

  if (setjmp (png_jmpbuf(png_ptr)))
    {
      png_destroy_read_struct (&png_ptr, &info_ptr, &end_info);
      return 0;
    }

  if (filename)
    {
      fp = fopen (filename, "r");
      if (! fp)
        {
          fprintf (stderr, "%s: unable to read %s\n", progname, filename);
          return 0;
        }
      png_init_io (png_ptr, fp);
    }
  else
    {
      closure.buf = image_data;
      closure.siz = data_size;
      closure.ptr = 0;
      png_set_read_fn (png_ptr, (void *) &closure, png_reader_fn);
    }

  png_read_info (png_ptr, info_ptr);
  png_get_IHDR (png_ptr, info_ptr,
                &width, &height, &bit_depth, &color_type,
                &interlace_type, 0, 0);

  png_set_strip_16 (png_ptr);  /* Truncate 16 bits per component to 8 */
  png_set_packing (png_ptr);   /* Unpack to 1 pixel per byte */

# if 0
  if (color_type == PNG_COLOR_TYPE_PALETTE)  /* Colormap to RGB */
    png_set_palette_rgb (png_ptr);

  if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)  /* Mono to 8bit */
    png_set_gray_1_2_4_to_8 (png_ptr);
# endif

  if (png_get_valid (png_ptr, info_ptr, PNG_INFO_tRNS)) /* Fix weird alpha */
    png_set_tRNS_to_alpha (png_ptr);

  /* At least 8 bits deep */
  if (color_type == PNG_COLOR_TYPE_PALETTE && bit_depth <= 8)
    png_set_expand (png_ptr);

   if (bit_depth == 8 &&          /* Convert RGB to RGBA */
           (color_type == PNG_COLOR_TYPE_RGB ||
            color_type == PNG_COLOR_TYPE_PALETTE))
     png_set_filler (png_ptr, 0xFF, PNG_FILLER_AFTER);

  /* Grayscale to color */
  if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
    png_set_expand (png_ptr);


  /* Convert graysale to color */
  if (color_type == PNG_COLOR_TYPE_GRAY ||
      color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
    png_set_gray_to_rgb (png_ptr);

# if 0
  {
    png_color_16 *bg;
    if (png_get_bKGD (png_ptr, info_ptr, &bg))
      png_set_background (png_ptr, bg, PNG_BACKGROUND_GAMMA_FILE, 1, 1.0);
  }
# endif

  /* Commit */
  png_read_update_info (png_ptr, info_ptr);

  channels = png_get_channels (png_ptr, info_ptr);

  {
    png_bytep *rows = png_malloc (png_ptr, height * sizeof(*rows));
    int x, y;
    for (y = 0; y < height; y++)
      rows[y] = png_malloc (png_ptr, png_get_rowbytes (png_ptr, info_ptr));
    png_read_image (png_ptr, rows);
    png_read_end (png_ptr, info_ptr);

    image = XCreateImage (dpy, visual, 32, ZPixmap, 0, 0,
                          width, height, 32, 0);
    image->data = (char *) malloc (height * image->bytes_per_line);

    /* Set the bit order in the XImage structure to whatever the
       local host's native bit order is.
     */
    image->bitmap_bit_order =
      image->byte_order =
        (bigendian() ? MSBFirst : LSBFirst);

    if (!image->data)
      {
        fprintf (stderr, "%s: out of memory (%lu x %lu)\n",
                 progname, (unsigned long)width, (unsigned long)height);
        return 0;
      }

    for (y = 0; y < height; y++)
      {
        png_bytep i = rows[y];
        for (x = 0; x < width; x++)
          {
            unsigned long rgba;
            switch (channels) {
            case 4:
              rgba = ((i[3] << 24) |
                      (i[2] << 16) |
                      (i[1] << 8)  |
                       i[0]);
              break;
            case 3:
              rgba = ((0xFF << 24) |
                      (i[2] << 16) |
                      (i[1] << 8)  |
                       i[0]);
              break;
            case 2:
              rgba = ((i[1] << 24) |
                      (i[0] << 16) |
                      (i[0] << 8)  |
                       i[0]);
              break;
            case 1:
              rgba = ((0xFF << 24) |
                      (i[0] << 16) |
                      (i[0] << 8)  |
                       i[0]);
              break;
            default:
              abort();
            }
            XPutPixel (image, x, y, rgba);
            i += channels;
          }
        png_free (png_ptr, rows[y]);
      }

    png_free (png_ptr, rows);
  }

  png_destroy_read_struct (&png_ptr, &info_ptr, &end_info);  
  if (fp) fclose (fp);

  return image;
}


#else /* No image loaders! */

static XImage *
make_ximage (Display *dpy, Visual *visual,
             const char *filename, const unsigned char *image_data,
             unsigned long data_size)
{
  fprintf (stderr, "%s: no image loading support!\n", progname);
  return 0;
}

#endif /* no loaders */


/* Given a bitmask, returns the position and width of the field.
 */
static void
decode_mask (unsigned long mask, unsigned long *pos_ret,
             unsigned long *size_ret)
{
  int i;
  for (i = 0; i < 32; i++)
    if (mask & (1L << i))
      {
        int j = 0;
        *pos_ret = i;
        for (; i < 32; i++, j++)
          if (! (mask & (1L << i)))
            break;
        *size_ret = j;
        return;
      }
}


/* Loads the image to a Pixmap and optional 1-bit mask.
 */
static Pixmap
make_pixmap (Display *dpy, Window window,
             const char *filename,
             const unsigned char *image_data, unsigned long data_size,
             int *width_ret, int *height_ret, Pixmap *mask_ret)
{
  XWindowAttributes xgwa;
  XImage *in, *out, *mask = 0;
  Pixmap pixmap;
  XGCValues gcv;
  GC gc;
  int x, y;

  unsigned long crpos=0, cgpos=0, cbpos=0, capos=0; /* bitfield positions */
  unsigned long srpos=0, sgpos=0, sbpos=0;
  unsigned long srmsk=0, sgmsk=0, sbmsk=0;
  unsigned long srsiz=0, sgsiz=0, sbsiz=0;

# ifdef HAVE_JWXYZ
  /* BlackPixel has alpha: 0xFF000000. */
  unsigned long black = BlackPixelOfScreen (DefaultScreenOfDisplay (dpy));
#else
  unsigned long black = 0;
# endif

  XGetWindowAttributes (dpy, window, &xgwa);

  in = make_ximage (dpy, xgwa.visual, filename, image_data, data_size);
  if (!in) return 0;

  /* Create a new image in the depth and bit-order of the server. */
  out = XCreateImage (dpy, xgwa.visual, xgwa.depth, ZPixmap, 0, 0,
                      in->width, in->height, 8, 0);

  out->bitmap_bit_order = in->bitmap_bit_order;
  out->byte_order = in->byte_order;

  out->bitmap_bit_order = BitmapBitOrder (dpy);
  out->byte_order = ImageByteOrder (dpy);

  out->data = (char *) malloc (out->height * out->bytes_per_line);
  if (!out->data) abort();

  if (mask_ret)
    {
      mask = XCreateImage (dpy, xgwa.visual, 1, XYPixmap, 0, 0,
                           in->width, in->height, 8, 0);
      mask->byte_order = in->byte_order;
      mask->data = (char *) malloc (mask->height * mask->bytes_per_line);
    }

  /* Find the server's color masks.
   */
  srmsk = out->red_mask;
  sgmsk = out->green_mask;
  sbmsk = out->blue_mask;

  if (!(srmsk && sgmsk && sbmsk)) abort();  /* No server color masks? */

  decode_mask (srmsk, &srpos, &srsiz);
  decode_mask (sgmsk, &sgpos, &sgsiz);
  decode_mask (sbmsk, &sbpos, &sbsiz);

  /* 'in' is RGBA in client endianness.  Convert to what the server wants. */
  if (bigendian())
    crpos = 24, cgpos = 16, cbpos =  8, capos =  0;
  else
    crpos =  0, cgpos =  8, cbpos = 16, capos = 24;

  for (y = 0; y < in->height; y++)
    for (x = 0; x < in->width; x++)
      {
        unsigned long p = XGetPixel (in, x, y);
        unsigned char a = (p >> capos) & 0xFF;
        unsigned char b = (p >> cbpos) & 0xFF;
        unsigned char g = (p >> cgpos) & 0xFF;
        unsigned char r = (p >> crpos) & 0xFF;
        XPutPixel (out, x, y, ((r << srpos) |
                               (g << sgpos) |
                               (b << sbpos) |
                               black));
        if (mask)
          XPutPixel (mask, x, y, (a ? 1 : 0));
      }

  XDestroyImage (in);
  in = 0;

  pixmap = XCreatePixmap (dpy, window, out->width, out->height, xgwa.depth);
  gc = XCreateGC (dpy, pixmap, 0, &gcv);
  XPutImage (dpy, pixmap, gc, out, 0, 0, 0, 0, out->width, out->height);
  XFreeGC (dpy, gc);

  if (mask)
    {
      Pixmap p2 = XCreatePixmap (dpy, window, mask->width, mask->height, 1);
      gcv.foreground = 1;
      gcv.background = 0;
      gc = XCreateGC (dpy, p2, GCForeground|GCBackground, &gcv);
      XPutImage (dpy, p2, gc, mask, 0, 0, 0, 0, mask->width, mask->height);
      XFreeGC (dpy, gc);
      XDestroyImage (mask);
      mask = 0;
      *mask_ret = p2;
    }

  if (width_ret)  *width_ret  = out->width;
  if (height_ret) *height_ret = out->height;

  XDestroyImage (out);

  return pixmap;
}


/* Textures are upside down, so invert XImages before returning them.
 */
static void
flip_ximage (XImage *ximage)
{
  char *data2, *in, *out;
  int y;

  if (!ximage) return;
  data2 = malloc (ximage->bytes_per_line * ximage->height);
  if (!data2) abort();
  in = ximage->data;
  out = data2 + ximage->bytes_per_line * (ximage->height - 1);
  for (y = 0; y < ximage->height; y++)
    {
      memcpy (out, in, ximage->bytes_per_line);
      in  += ximage->bytes_per_line;
      out -= ximage->bytes_per_line;
    }
  free (ximage->data);
  ximage->data = data2;
}


Pixmap
image_data_to_pixmap (Display *dpy, Window window, 
                      const unsigned char *image_data, unsigned long data_size,
                      int *width_ret, int *height_ret,
                      Pixmap *mask_ret)
{
  return make_pixmap (dpy, window, 0, image_data, data_size,
                      width_ret, height_ret, mask_ret);
}

Pixmap
file_to_pixmap (Display *dpy, Window window, const char *filename,
                int *width_ret, int *height_ret,
                Pixmap *mask_ret)
{
  return make_pixmap (dpy, window, filename, 0, 0,
                      width_ret, height_ret, mask_ret);
}


/* This XImage has RGBA data, which is what OpenGL code typically expects.
   Also it is upside down: the origin is at the bottom left of the image.
   X11 typically expects 0RGB as it has no notion of alpha, only 1-bit masks.
   With X11 code, you should probably use the _pixmap routines instead.
 */
XImage *
image_data_to_ximage (Display *dpy, Visual *visual,
                      const unsigned char *image_data,
                      unsigned long data_size)
{
  XImage *ximage = make_ximage (dpy, visual, 0, image_data, data_size);
  flip_ximage (ximage);
  return ximage;
}

XImage *
file_to_ximage (Display *dpy, Visual *visual, const char *filename)
{
  XImage *ximage = make_ximage (dpy, visual, filename, 0, 0);
  flip_ximage (ximage);
  return ximage;
}
