void KakaduImage::process( unsigned int res, int layers, int xoffset, int yoffset, unsigned int tw, unsigned int th, void *d )
{

  // Scale up our output bit depth to the nearest factor of 8
  unsigned int obpc = bpc;
  if( bpc <= 16 && bpc > 8 ) obpc = 16;
  else if( bpc <= 8 ) obpc = 8;

  int vipsres = ( numResolutions - 1 ) - res;

  // Handle virtual resolutions
  if( res < virtual_levels ){
    unsigned int factor = 1 << (virtual_levels-res);
    xoffset *= factor;
    yoffset *= factor;
    tw *= factor;
    th *= factor;
    vipsres = numResolutions - 1 - virtual_levels;
#ifdef DEBUG
  logfile << "Kakadu :: using smallest existing resolution " << virtual_levels << endl;
#endif
  }

  // Set the number of layers to half of the number of detected layers if we have not set the
  // layers parameter manually. If layers is set to less than 0, use all layers.
  if( layers < 0 ) layers = quality_layers;
  else if( layers == 0 ) layers = ceil( quality_layers/2.0 );

  // Also make sure we have at least 1 layer
  if( layers < 1 ) layers = 1;


  // Set up the bounding box for our tile
  kdu_dims image_dims, canvas_dims;
  canvas_dims.pos = kdu_coords( xoffset, yoffset );
  canvas_dims.size = kdu_coords( tw, th );

  // Check our codestream status - throw exception for malformed codestreams
  if( !codestream.exists() ) throw file_error( "Kakadu :: Malformed JPEG2000 - unable to access codestream");

  // Apply our resolution restrictions to calculate the rendering zone on the highest resolution
  // canvas
  codestream.apply_input_restrictions( 0,0,vipsres,layers,&canvas_dims,KDU_WANT_OUTPUT_COMPONENTS );
  codestream.map_region( 0, canvas_dims, image_dims, true );


  // Create some worker threads
#ifdef NPROCS
  int num_threads = get_nprocs_conf();
#else
  int num_threads = 0;
#endif


  kdu_thread_env env, *env_ref = NULL;
  if( num_threads > 0 ){
    env.create();
    for (int nt=0; nt < num_threads; nt++){
      // Unable to create all the threads requested
      if( !env.add_thread() ) num_threads = nt;
    }
    env_ref = &env;
  }



#ifdef DEBUG
  logfile << "Kakadu :: decompressor init with " << num_threads << " threads" << endl;
  logfile << "Kakadu :: decoding " << layers << " quality layers" << endl;
#endif


  // Setup tile and stripe buffers
  void *buffer = NULL;
  void *stripe_buffer = NULL;
  int *stripe_heights = NULL;

  try{

    // Note that we set max channels rather than leave the default to strip off alpha channels
    codestream.apply_input_restrictions( 0, channels, vipsres, layers, &image_dims, KDU_WANT_OUTPUT_COMPONENTS );

    decompressor.start( codestream, false, true, env_ref, NULL );

    stripe_heights = new int[channels];
    codestream.get_dims(0,comp_dims,true);

#ifdef DEBUG
    logfile << "Kakadu :: decompressor starting" << endl;

    logfile << "Kakadu :: requested region on high resolution canvas: position: "
	    << image_dims.pos.x << "x" << image_dims.pos.y
	    << ". size: " << image_dims.size.x << "x" << image_dims.size.y << endl;

    logfile << "Kakadu :: mapped resolution region size: " << comp_dims.size.x << "x" << comp_dims.size.y << endl;
    logfile << "Kakadu :: About to pull stripes" << endl;
#endif

    // Make sure we don't have zero or negative sized images
    if( comp_dims.size.x <= 0 || comp_dims.size.y <= 0 ){
#ifdef DEBUG
      logfile << "Kakadu :: Error: region of zero size requested" << endl;
#endif
      throw 1;
    }

    int index = 0;
    bool continues = true;

    // Get our stripe heights so that we can allocate our stripe buffer
    // Assume that first stripe height is largest
    decompressor.get_recommended_stripe_heights( comp_dims.size.y,
						 1024, stripe_heights, NULL );

#ifdef DEBUG
    logfile << "Kakadu :: Allocating memory for stripe height " << stripe_heights[0] << endl;
#endif

    // Create our buffers

    if( obpc == 16 ){
      stripe_buffer = new kdu_uint16[tw*stripe_heights[0]*channels];
      buffer = new unsigned short[tw*th*channels];
    }
    else if( obpc == 8 ){
      stripe_buffer = new kdu_byte[tw*stripe_heights[0]*channels];
      buffer = new unsigned char[tw*th*channels];
    }

    // Keep track of changes in stripe heights
    int previous_stripe_heights = stripe_heights[0];


    while( continues ){


      decompressor.get_recommended_stripe_heights( comp_dims.size.y,
						   1024, stripe_heights, NULL );


      // If we have a larger stripe height, allocate new memory for this
      if( stripe_heights[0] > previous_stripe_heights ){

	// First delete then re-allocate our buffers
	delete_buffer( stripe_buffer );
	if( obpc == 16 ){
	  stripe_buffer = new kdu_uint16[tw*stripe_heights[0]*channels];
	}
	else if( obpc == 8 ){
	  stripe_buffer = new kdu_byte[tw*stripe_heights[0]*channels];
	}

#ifdef DEBUG
	logfile << "Kakadu :: Stripe height increase: re-allocating memory for height " << stripe_heights[0] << endl;
#endif
      }

      // Check for zero height, which can occur with incorrect position or size parameters
      if( stripe_heights[0] == 0 ){
#ifdef DEBUG
	logfile << "Kakadu :: Error: Zero stripe height" << endl;
#endif
	throw 1;
      }


      if( obpc == 16 ){
	// Set these to false to get unsigned 16 bit values
	bool s[3] = {false,false,false};
	continues = decompressor.pull_stripe( (kdu_int16*) stripe_buffer, stripe_heights, NULL, NULL, NULL, NULL, s );
      }
      else if( obpc == 8 ){
	continues = decompressor.pull_stripe( (kdu_byte*) stripe_buffer, stripe_heights, NULL, NULL, NULL );
      }


#ifdef DEBUG
      logfile << "Kakadu :: stripe pulled" << endl;
#endif

      // Copy the data into the supplied buffer
      void *b1, *b2;
      if( obpc == 16 ){
	b1 = &( ((kdu_uint16*)stripe_buffer)[0] );
	b2 = &( ((unsigned short*)buffer)[index] );
      }
      else{ // if( obpc == 8 ){
	b1 = &( ((kdu_byte*)stripe_buffer)[0] );
	b2 = &( ((unsigned char*)buffer)[index] );

	/* Handle 1 bit bilevel images, which we output scaled to 8 bits
	   - ideally we would do this in the Kakadu pull_stripe function,
	   but the precisions parameter seems not to work as expected.
	   When requesting OUTPUT_COMPONENTS, data is provided as 0 or 128,
	   so simply scale this up to [0,255]
	*/
	if( bpc == 1 ){

	  unsigned int k = tw * stripe_heights[0] * channels;

	  // Deal with inverted LUTs - we should really handle LUTs more generally, however
	  if( !lut.empty() && lut[0]>lut[1] ){
	    for( unsigned int n=0; n<k; n++ ){
	      ((kdu_byte*)stripe_buffer)[n] =  ~(-((kdu_byte*)stripe_buffer)[n] >> 8);
	    }
	  }
	  else{
	    for( unsigned int n=0; n<k; n++ ){
	      ((kdu_byte*)stripe_buffer)[n] =  (-((kdu_byte*)stripe_buffer)[n] >> 8);
	    }
	  }
	}
      }

      memcpy( b2, b1, tw * stripe_heights[0] * channels * (obpc/8) );

      // Advance our output buffer pointer
      index += tw * stripe_heights[0] * channels;

#ifdef DEBUG
      logfile << "Kakadu :: stripe complete with height " << stripe_heights[0] << endl;
#endif

    }


    if( !decompressor.finish() ){
      throw file_error( "Kakadu :: Error indicated by finish()" );
    }


    // Shrink virtual resolution tiles
    if( res < virtual_levels ){

#ifdef DEBUG
      logfile << "Kakadu :: resizing tile to virtual resolution with factor " << (1 << (virtual_levels-res)) << endl;
#endif

      unsigned int n = 0;
      unsigned int factor = 1 << (virtual_levels-res);
      for( unsigned int j=0; j<th; j+=factor ){
	for( unsigned int i=0; i<tw; i+=factor ){
	  for( unsigned int k=0; k<channels; k++ ){
	    // Handle 16 and 8 bit data
	    if( obpc==16 ){
	      ((unsigned short*)d)[n++] = ((unsigned short*)buffer)[j*tw*channels + i*channels + k];
	    }
	    else if( obpc==8 ){
	      ((unsigned char*)d)[n++] = ((unsigned char*)buffer)[j*tw*channels + i*channels + k];
	    }
	  }
	}
      }
    }
    else memcpy( d, buffer, tw*th*channels * (obpc/8) );

    // Delete our local buffer
    delete_buffer( buffer );

#ifdef DEBUG
    logfile << "Kakadu :: decompressor completed" << endl;
#endif


  }
  catch (...){
    // Shut down our decompressor, delete our buffers, destroy our threads and codestream before rethrowing the exception
    decompressor.finish();
    if( env.exists() ) env.destroy();
    delete_buffer( stripe_buffer );
    delete_buffer( buffer );
    if( stripe_heights ) delete[] stripe_heights;
    throw file_error( "Kakadu :: Core Exception Caught"); // Rethrow the exception
  }


  // Destroy our threads
  if( env.exists() ) env.destroy();

  // Delete our stripe buffer
  delete_buffer( stripe_buffer );
  if( stripe_heights ){
    delete[] stripe_heights;
    stripe_heights = NULL;
  }

}