/** @file scale.c
 * JPEG image scaling
 */

/* Copyright © 2003 Marko Mäkelä.

   This file is part of PHOTOMOLO, a program for generating
   thumbnail images and HTML files for browsing digital photographs.

   PHOTOMOLO 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 2, or (at your option)
   any later version.

   PHOTOMOLO 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.

   The GNU General Public License is often shipped with GNU software, and
   is generally kept in a file called COPYING or LICENSE.  If you do not
   have a copy of the license, write to the Free Software Foundation,
   59 Temple Place, Suite 330, Boston, MA 02111 USA. */

#include <stdio.h>
#include <setjmp.h>
#include "scale.h"

/** libjpeg error manager */
struct my_error_mgr
{
  /** "public" fields */
  struct jpeg_error_mgr pub;
  /** for returning to caller */
  jmp_buf setjmp_buffer;
};

/**
 * Custom error exit handler
 * @param cinfo		the libjpeg context
 */
METHODDEF(void)
my_error_exit (struct jpeg_common_struct* cinfo)
{
  /* cinfo->err really points to a my_error_mgr struct, so coerce pointer */
  struct my_error_mgr* myerr = (struct my_error_mgr*) cinfo->err;

  /* Always display the message. */
  /* We could postpone this until after returning, if we chose. */
  (*cinfo->err->output_message) (cinfo);

  /* Return control to the setjmp point */
  longjmp (myerr->setjmp_buffer, 1);
}

/**
 * Recompress a JPEG image
 * @param cin	input context (image being decompressed)
 * @param cout	output context (image being compressed)
 */
static void
copy_jpeg (struct jpeg_decompress_struct* cin,
	   struct jpeg_compress_struct* cout)
{
  /** input/output row buffer */
  JSAMPARRAY buffer;
  /* Make a one-row-high sample array that will go away when done with image */
  buffer = (*cin->mem->alloc_sarray)
    ((j_common_ptr) cin, JPOOL_IMAGE,
     cin->output_width * cin->output_components, 1);
  while (cin->output_scanline < cin->output_height) {
    (void) jpeg_read_scanlines (cin, buffer, 1);
    (void) jpeg_write_scanlines (cout, buffer, 1);
  }
}

/**
 * Recompress and rescale a JPEG image.
 * The algorithm has been adapted from pnmscale.c by Jef Poskanzer.
 * @param cin	input context (image being decompressed)
 * @param cout	output context (image being compressed)
 */
static void
rescale_jpeg (struct jpeg_decompress_struct* cin,
	      struct jpeg_compress_struct* cout)
{
  /** horizontal and vertical scaling factor */
  float xscale, yscale;
  /** number of fractional rows read */
  float yfrac;
  /** number of fractional rows to read */
  float yleft;
  /** array of pixels, indexed by colour component and column */
  float* pix;
  /** new pixel, indexed by colour component */
  float* newpix;
  /** pixel counter */
  unsigned x;
  /** number of colour components */
  unsigned comps;
  /** flag: need to read a row? */
  int need_row;

  /** input row buffer */
  JSAMPARRAY inrow1;
  /** vertically scaled inrow1 */
  JSAMPROW inrow2;
  /** output row buffer (horizontally scaled inrow2) */
  JSAMPARRAY outrow;

  xscale = (float) cout->image_width / cin->output_width;
  yscale = (float) cout->image_height / cin->output_height;
  comps = cin->output_components;
  x = cin->output_width * comps;

  inrow1 = (*cin->mem->alloc_sarray)
    ((j_common_ptr) cin, JPOOL_IMAGE, x, 1);
  inrow2 = cin->output_height == cout->image_height
    ? *inrow1
    : (*cin->mem->alloc_small) ((j_common_ptr) cin, JPOOL_IMAGE, x);
  outrow = (*cout->mem->alloc_sarray)
    ((j_common_ptr) cout, JPOOL_IMAGE, cout->image_width * comps, 1);
  pix = (float*) (*cout->mem->alloc_small)
    ((j_common_ptr) cout, JPOOL_IMAGE, x * sizeof *pix);
  newpix = (float*) (*cout->mem->alloc_small)
    ((j_common_ptr) cout, JPOOL_IMAGE, comps * sizeof *newpix);

  while (x--)
    pix[x] = 0.0;
  yleft = yscale;
  need_row = 1;
  yfrac = 1.0;

  while (cout->next_scanline < cout->image_height) {
    if (cin->output_height == cout->image_height)
      jpeg_read_scanlines (cin, inrow1, 1);
    else {
      while (yleft < yfrac) {
	if (need_row && cin->output_scanline < cin->output_height)
	  jpeg_read_scanlines (cin, inrow1, 1);
	for (x = cin->output_width * comps; x--; )
	  pix[x] += yleft * GETJSAMPLE ((*inrow1)[x]);
	yfrac -= yleft;
	yleft = yscale;
	need_row = 1;
      }
      if (need_row && cin->output_scanline < cin->output_height) {
	jpeg_read_scanlines (cin, inrow1, 1);
	need_row = 0;
      }
      for (x = cin->output_width * comps; x--; ) {
	inrow2[x] = (JSAMPLE)
	  (pix[x] + yfrac * GETJSAMPLE ((*inrow1)[x]) + .5);
	pix[x] = 0.0;
      }
      yleft -= yfrac;
      if (yleft <= 0.0) {
	yleft = yscale;
	need_row = 1;
      }
      yfrac = 1.0;
    }
    if (cin->output_width == cout->image_width)
      (void) jpeg_write_scanlines (cout, &inrow2, 1);
    else {
      /* Scale inrow2 horizontally into outrow */
      unsigned newx = 0;
      /** number of fractional columns read */
      float xfrac = 1.0;
      /** number of fractional rows to read */
      float xleft;
      /** colour component counter */
      unsigned c;
      /* clear the colour components */
      for (c = comps; c--; )
	newpix[c] = 0.0;
      for (x = 0; x < cin->output_width; x++) {
	xleft = xscale;
	while (xleft >= xfrac) {
	  for (c = comps; c--; )
	    (*outrow)[newx * comps + c] = (JSAMPLE)
	      ((newpix[c] += xfrac * GETJSAMPLE (inrow2[x * comps + c])) + .5);
	  xleft -= xfrac;
	  newx++;
	  xfrac = 1.0;
	  for (c = comps; c--; )
	    newpix[c] = 0.0;
	}
	if (xleft > 0.0) {
	  for (c = comps; c--; )
	    newpix[c] += xleft * GETJSAMPLE (inrow2[x * comps + c]);
	  xfrac -= xleft;
	}
      }

      if (newx < cout->image_width) {
	/* fill last column if needed */
	for (c = comps; c--; )
	  (*outrow)[newx * comps + c] = (JSAMPLE)
	    ((newpix[c] += xfrac * GETJSAMPLE (inrow2[(x - 1) * comps + c]))
	     + .5);
      }

      /* write the output row */
      (void) jpeg_write_scanlines (cout, outrow, 1);
    }
  }
}

/** Scale a JPEG image
 * @param cin		the image to be scaled
 * @param cout		the output image
 *			(image_width and image_width set to the maximum,
 *			0=unspecified)
 * @param shrink	shrink factor (0=unspecified, 1, 2, 4 or 8)
 * @return		0 on success, nonzero on failure
 */
int
scale_image (struct jpeg_decompress_struct* cin,
	     struct jpeg_compress_struct* cout,
	     unsigned shrink)
{
  struct my_error_mgr jerr;
  cin->err = cout->err = jpeg_std_error (&jerr.pub);
  jerr.pub.error_exit = my_error_exit;

  /* Establish the setjmp return context for my_error_exit to use. */
  if (setjmp (jerr.setjmp_buffer))
    return 1; /* failure in libjpeg */

  (void) jpeg_read_header (cin, TRUE);
  if (cout->image_width || cout->image_height) {
    if (!cout->image_width) {
    set_width:
      cout->image_width = cin->image_width *
	((float) cout->image_height / cin->image_height);
    }
    else if (!cout->image_height) {
    set_height:
      cout->image_height = cin->image_height *
	((float) cout->image_width / cin->image_width);
    }
    else {
      /* stick to the bounding box, maintaining the aspect ratio */
      if ((float) cout->image_width / cout->image_height >
	  (float) cin->image_width / cin->image_height)
	goto set_width;
      else
	goto set_height;
    }
    /* prescale the JPEG image to speed up processing */
    for (shrink = 0; shrink < 4; shrink++)
      if (cin->image_width <= cout->image_width << shrink ||
	  cin->image_height <= cout->image_height << shrink)
	break;
    if (shrink) shrink--;
    cin->scale_denom = 1 << shrink;
  }
  else
    cin->scale_denom = shrink;

  (void) jpeg_start_decompress (cin);

  if (!cout->image_width && !cout->image_height) {
    /* scale by the shrink factor */
    cout->image_width = cin->output_width;
    cout->image_height = cin->output_height;
  }

  cout->input_components = cin->output_components;
  cout->in_color_space = cin->out_color_space;
  jpeg_set_defaults (cout);
  cout->optimize_coding = TRUE;
  jpeg_simple_progression (cout);
  jpeg_start_compress (cout, TRUE);

  if (cout->image_width == cin->output_width &&
      cout->image_height == cin->output_height)
    copy_jpeg (cin, cout);
  else
    rescale_jpeg (cin, cout);

  (void) jpeg_finish_compress (cout);
  (void) jpeg_finish_decompress (cin);
  return 0;
}
