//
//  Lynkeos
//  $Id: DcrawReader.m 575 2014-09-07 16:33:25Z j-etienne $
//
//  Created by Jean-Etienne LAMIAUD on Wed Apr 27 2005.
//  Copyright (c) 2005-2014. Jean-Etienne LAMIAUD
//
// This program 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 of the License, or
// (at your option) any later version.
//
// This program 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.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
// 

#include <AppKit/NSGraphics.h>

#include <LynkeosCore/LynkeosStandardImageBuffer.h>
#include <LynkeosCore/LynkeosMetadata.h>
#include "processing_core.h"
#include "DcrawReaderPrefs.h"
#include "DcrawReader.h"

#define K_COUNTER_KEY @"ImageCount"

static NSCursor *watchCursor = nil;
static NSMutableArray *rawFilesTypes = nil;

/*!
 * @abstract The RAW custom image class
 * @ingroup FileAccess
 */
@interface DcrawCustomImage : LynkeosStandardImageBuffer
{
   NSString *_pgmFilePath;
}

- (NSString*) fileName;
@end

@interface DcrawCustomImage(Private)
- (void) saveAsPgm;
@end

/*!
 * @abstract Private part of the DCRAW readder class
 * @ingroup FileAccess
 */
@interface DcrawReader(Private)
/*!
 * @abstract Extract image informations
 */
- (void) getImageInfo ;

/*!
 * @abstract Launch the conversion
 */
- (void) launchConversion ;

/*!
 * @abstract Wait for the dcraw task to complete the conversion.
 * @result None
 */
- (void) waitForConversion ;

/*!
 * @abstract Extract a data sample in the converted PPM file
 * @param data The data buffer to fill
 * @param x X coordinate of the sample
 * @param y Y coordinate of the sample
 * @param w Width of the sample
 * @param h Height of the sample
 */
- (void) getPPMsample:(u_short*)data 
                  atX:(u_short)x Y:(u_short)y 
                    W:(u_short)w H:(u_short)h;
@end

@implementation DcrawCustomImage(Private)

- (void) saveAsPgm
{
   NSUserDefaults *pref = [NSUserDefaults standardUserDefaults];
   int cnt;
   const char *tmpdir = NULL;
   FILE *pgmFile;
   u_short x, y;

   // Create a temporary filename for the PGM file
   NSString *tmpdirStr = [pref stringForKey:K_TMPDIR_KEY];
   if ( tmpdirStr != nil )
      tmpdir = [[tmpdirStr stringByExpandingTildeInPath] UTF8String];
   if ( tmpdir == NULL || *tmpdir == '\0' )
      tmpdir = getenv("TMPDIR");
   if ( tmpdir == NULL || *tmpdir == '\0' )
      tmpdir = "/tmp";
   // with a count suffix shared by all documents
   cnt = [pref integerForKey:K_COUNTER_KEY] % 10000;
   _pgmFilePath = [[NSString stringWithFormat:@"%s/dark%04d.pgm",
                    tmpdir, cnt] retain];
   cnt++;
   [pref setInteger:cnt forKey:K_COUNTER_KEY];

   pgmFile = fopen([_pgmFilePath fileSystemRepresentation], "wb");

   fprintf( pgmFile, "P5\n" );

   fprintf( pgmFile, "%d %d\n", _w, _h );
   fprintf( pgmFile, "%d\n", 65535 );

   for( y = 0; y < _h; y++ )
   {
      for( x = 0; x < _w; x++ )
      {
         REAL v = GET_SAMPLE(_data,PROCESSING_PRECISION,x,y,_padw);
         u_short outValue;

         if ( v < 0.0 )
            v = 0.0;
         else if ( v > 65535.0 )
            v = 65535.0;

         outValue = NSSwapHostShortToBig((u_short)(v+0.5));

         fwrite( &outValue, 1, 2, pgmFile );
      }
   }
   
   fclose( pgmFile );
}

@end

@implementation DcrawCustomImage

- (id) init
{
   self = [super init];
   if ( self != nil )
      _pgmFilePath = nil;

   return( self );
}

- (void) dealloc
{
   NSError *err;

   if ( _pgmFilePath != nil )
   {
      if ( ![[NSFileManager defaultManager] removeItemAtURL:
                                            [NSURL fileURLWithPath:_pgmFilePath]
                                                  error:&err] )
      NSLog( @"Could not remove temporary raw saved dark image %@. %@",
            _pgmFilePath, [err description]);
   }

   [super dealloc];
}

- (NSString*) fileName
{
   if ( _pgmFilePath == nil )
      [self saveAsPgm];

   return( _pgmFilePath );
}

- (void) calibrateWithDarkFrame:(id<LynkeosImageBuffer>)darkFrame
                      flatField:(id<LynkeosImageBuffer>)flatField
                            atX:(u_short)ox Y:(u_short)oy
{
   // Dark frame is already applied
   if ( flatField != nil )
      [super calibrateWithDarkFrame:nil flatField:flatField atX:ox Y:oy];
}

@end

@implementation DcrawReader(Private)

- (void) getImageInfo
{
   NSAssert(_mode != UnsetListMode,
            @"Attempt to get image info without mode set");

   // Prepare a task
   NSTask *infoTask = [[NSTask alloc] init];

   NSAssert( infoTask != nil, @"Failed to create a task to get RAW info" );

   // Set its executable path to dcraw in our ressources
   NSString *dcrawPath
      = [[NSBundle bundleForClass:[self class]] pathForResource:@"dcraw"
                                                         ofType:nil];
   [infoTask setLaunchPath:dcrawPath];

   // Connect its output to a pipe
   NSPipe *infoPipe = [NSPipe pipe];
   [infoTask setStandardOutput:infoPipe];

   // Options : "verbose informations"
   NSMutableArray *args = [NSMutableArray arrayWithObjects:
                              @"-i", @"-v", nil];
   // And maybe no image rotation
   if ( _mode == DarkFrameMode ||
        ![[NSUserDefaults standardUserDefaults] boolForKey:K_ROTATION_KEY] )
   {
      [args addObject:@"-t"];
      [args addObject:@"0"];
   }

   // The last arg is the file to convert
   [args addObject:[_url path]];

   [infoTask setArguments:args];

   // Run to get the information
   [infoTask launch];

   // Read the output
   NSData *info = [[infoPipe fileHandleForReading] readDataToEndOfFile];
   [infoTask waitUntilExit];

   NSAssert( [infoTask terminationStatus] == 0,
             @"Could not get information on %@", _url );
   [infoTask release];

   // Scan the output to find the image information
   NSScanner *scan = [NSScanner scannerWithString:
                         [[[NSString alloc] initWithData:info
                                                encoding:NSUTF8StringEncoding]
                                                                  autorelease]];
   [scan setCharactersToBeSkipped:[NSCharacterSet whitespaceCharacterSet]];
   BOOL success = YES;
   int w = 0, h = 0;
   while ( success && ![scan isAtEnd] )
   {
      NSString *token, *marker = @":";

      // Get to the start of the next token
      [scan scanUpToCharactersFromSet:[NSCharacterSet alphanumericCharacterSet]
                           intoString:NULL];

      success = [scan scanUpToString:marker intoString:&token];
      if ( success )
         success = [scan scanString:marker intoString:NULL];

      if ( success )
      {
         if ( [token isEqualToString:@"Output size"] )
         {
            if ( [scan scanInt:&w]
                 && [scan scanString:@"x" intoString:NULL]
                 && [scan scanInt:&h] )
            {
               _width = w;
               _height = h;
            }
         }
         else if ( [token isEqualToString:@"Image size"] )
         {
            if ( [scan scanInt:&w]
                 && [scan scanString:@"x" intoString:NULL]
                 && [scan scanInt:&h] )
            {
               _baseWidth = w;
               _baseHeight = h;
            }
         }
         else if ( [token isEqualToString:@"Camera"] )
         {
            NSString *camera;
            [scan scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet]
                                 intoString:&camera];
            [_metadata setObject:camera forKey:LynkeosMD_CameraModel()];
         }
         // TODO : get the remaining info and put it in a metadata dictionary
         else
            [scan scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet]
                                 intoString:NULL];
      }
   }
}

- (void) launchConversion
{
   NSAssert(_mode != UnsetListMode, @"Cannot convert without the mode");

   // Abort ongoing conversion, if any
   if (_dcrawTask != nil && [_dcrawTask isRunning] )
   {
      [_dcrawTask terminate];
      [_dcrawTask waitUntilExit];
      [_dcrawTask release];
   }

   // Prepare a task for the conversion
   _dcrawTask = [[NSTask alloc] init];

   if ( _dcrawTask != nil )
   {
      NSFileManager *fileMgr = [NSFileManager defaultManager];
      NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

      // Set its executable path to dcraw in our ressources
      NSString *dcrawPath
         = [[NSBundle bundleForClass:[self class]] pathForResource:@"dcraw"
                                                            ofType:nil];
      [_dcrawTask setLaunchPath:dcrawPath];

      // Create an empty file by this name
      [fileMgr createFileAtPath:_ppmFilePath contents:nil attributes:nil];
      [_dcrawTask setStandardOutput:
                     [NSFileHandle fileHandleForWritingAtPath:_ppmFilePath]];

      // Common option to DCRAW is : "output on stdout, 16 bits" PPM"
      NSMutableArray *args = [NSMutableArray arrayWithObjects:
                              @"-c", @"-4", nil];

      switch ( _mode )
      {
         case DarkFrameMode:
            // Dark frames are extracted without rotation,
            [args addObject:@"-t"];
            [args addObject:@"0"];
            // in "raw document mode"
            [args addObject:@"-D"];
            break;

         case ImageMode:
            if ( _dark != nil )
            {
               // Dark frame option
               [args addObject:@"-K"];
               [args addObject:[(DcrawCustomImage*)_dark fileName]];
            }

            // Fall through to options common with flat field
            
         case FlatFieldMode:
            // User preferences options

            // Image rotation
            if ( ![prefs boolForKey:K_ROTATION_KEY] )
            {
               // Or not
               [args addObject:@"-t"];
               [args addObject:@"0"];
            }

            if ( [prefs boolForKey:K_MANUALWB_KEY] )
            {
               // Custom white balance
               [args addObject:@"-r"];
               [args addObject:[prefs stringForKey:K_RED_KEY]];
               [args addObject:[prefs stringForKey:K_GREEN1_KEY]];
               [args addObject:[prefs stringForKey:K_BLUE_KEY]];
               [args addObject:[prefs stringForKey:K_GREEN2_KEY]];
            }
            else
               [args addObject:@"-w"];    // Camera white balance

            if ( [prefs boolForKey:K_LEVELS_KEY] )
            {
               // Custom dark and saturation levels
               [args addObject:@"-k"];
               [args addObject:[prefs stringForKey:K_DARK_KEY]];
               [args addObject:@"-S"];
               [args addObject:[prefs stringForKey:K_SATURATION_KEY]];
            }
            break;
         default:
            NSAssert(NO, @"Invalid image mode %d", _mode );
            break;
      }

      // The last arg is the file to convert
      [args addObject:[_url path]];

      [_dcrawTask setArguments:args];

      // Start the conversion
      [_dcrawTask launch];
   }

   NSAssert( _dcrawTask != nil && ([_dcrawTask isRunning]
                                   || [_dcrawTask terminationStatus] != 0),
             @"Could not start the conversion of %@", _url );
}

- (void) waitForConversion
{
   // If the conversion is not finished, wait for its completion
   if (  _dcrawTask != nil )
   {
      FILE * ppmFile;

      if ( [_dcrawTask isRunning] )
      {
         // For a reason not fully understood, we shall wait a bit before
         // setting the cursor to a watch icon
         [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
         [watchCursor push];

         [_dcrawTask waitUntilExit];

         [NSCursor pop];
      }

      // When succesful, initialize the PPM file info
      if ( [_dcrawTask terminationStatus] == 0 )
      {
         char line[40];
         u_short w, h;

         ppmFile = fopen( [_ppmFilePath fileSystemRepresentation], "rb" );

         fgets( line, 40, ppmFile ); // First line of input "P5" or "P6"
         if ( strcmp( "P5\n", line ) == 0 )
            NSAssert( _numberOfPlanes == 1,
                      @"Number of planes inconsistent after conversion" );
         else if ( strcmp( "P6\n", line ) == 0 )
            NSAssert( _numberOfPlanes == 3,
                      @"Number of planes inconsistent after conversion" );

         fgets( line, 40, ppmFile ); // Second line "w h"
         NSAssert( sscanf( line, "%hu %hu", &w, &h ) == 2,
                   @"Cannot read image size after conversion" );
         NSAssert( w == _width && h == _height,
                   @"Image size inconsistent after conversion" );
         fgets( line, 40, ppmFile ); // Third line "max"
         if ( sscanf( line, "%hu", &_dataMax ) != 1 )
            _dataMax = 0;

         _ppmDataOffset = ftell( ppmFile );

         // Don(t keep the file open, it may overflow the maximum number of open
         // file descriptors
         fclose( ppmFile );
      }

      // Anyway, the task has ended
      [_dcrawTask release];
      _dcrawTask = nil;
   }
}

- (void) getPPMsample:(u_short*)data 
                  atX:(u_short)x Y:(u_short)y 
                    W:(u_short)w H:(u_short)h
{
   FILE * ppmFile;

   [self waitForConversion];

   // Open and close the file at each read, to keep open file descriptors number
   // low
   ppmFile = fopen( [_ppmFilePath fileSystemRepresentation], "rb");
   if( ppmFile != NULL )
   {
      u_short ys;

      // Transfer "w" pixels from "h" lines
      for( ys = 0; ys < h; ys++ )
      {
         // Jump to the line start
         fseek( ppmFile,
                _ppmDataOffset + sizeof(u_short)*_numberOfPlanes*((y+ys)*_width+x),
                SEEK_SET );
         fread( &data[ys*w*_numberOfPlanes], _numberOfPlanes*sizeof(u_short), w, ppmFile );
      }

      fclose( ppmFile );
   }
}
@end

@implementation DcrawReader

+ (void) load
{
   // Nothing to do, this is just to force the runtime to load this class
}

+ (void) initialize
{
   if ( watchCursor == nil )
      watchCursor = [[NSCursor alloc] initWithImage:
                       [NSImage imageNamed:@"watch"]
                                              hotSpot:NSMakePoint(8,8)];
}

+ (void) lynkeosFileTypes:(NSArray**)fileTypes
{
   // Read the file extensions in the configuration file
   if ( rawFilesTypes == nil )
   {
      NSNumber *pri = [NSNumber numberWithInt:1];
      NSString *cfgFile;
      NSArray *cfgFileTypes;
      cfgFile = [[NSBundle bundleForClass:[self class]] pathForResource:
                                                        @"dcraw_file_extensions"
                                                               ofType:@"plist"];
      NSData *plistData;
      NSString *error;
      NSPropertyListFormat format;
      NSMutableDictionary *dict;
      plistData = [NSData dataWithContentsOfFile:cfgFile];
      dict = [NSPropertyListSerialization propertyListFromData:plistData
                                mutabilityOption:NSPropertyListMutableContainers
                                                        format:&format
                                              errorDescription:&error];
      NSAssert( dict != nil, @"Failed to read RAW files configuration" );
      cfgFileTypes = [dict objectForKey:@"extensions"];
      NSAssert( cfgFileTypes != nil,
               @"Failed to access to RAW files extensions" );

      rawFilesTypes =
             [[NSMutableArray arrayWithCapacity:[cfgFileTypes count]*2] retain];

      NSEnumerator *list;
      NSString *fileType;
      for( list = [cfgFileTypes objectEnumerator];
           (fileType = [list nextObject]) != nil ; )
      {
         [rawFilesTypes addObject:pri];
         [rawFilesTypes addObject:fileType];
      }

   }
   *fileTypes = rawFilesTypes;
}

- (id) init
{
   self = [super init];
   if ( self != nil )
   {
      _url = nil;
      _dcrawTask = nil;
      _ppmFilePath = nil;
      _mode = UnsetListMode;
      _dark = nil;
      _metadata = [[NSMutableDictionary dictionary] retain];
   }
   return( self );
}

- (id) initWithURL:(NSURL*)url
{
   NSFileManager *fileMgr = [NSFileManager defaultManager];

   if ( [fileMgr isReadableFileAtPath:[url path]] )
      self = [self init];

   else
   {
      [self release];
      self = nil;
   }

   if ( self != nil )
   {
      _url = [url retain];
   }

   return( self );
}

- (void) dealloc
{
   NSError *err;

   if ( _dcrawTask != nil )
   {
      if ( [_dcrawTask isRunning] )
         [_dcrawTask terminate];
      [_dcrawTask release];
   }

   if ( _ppmFilePath != nil )
   {
      if ( ![[NSFileManager defaultManager] removeItemAtURL:
                                            [NSURL fileURLWithPath:_ppmFilePath]
                                                      error:&err] )
         NSLog( @"Could not remove temporary raw converted image %@. %@",
                _ppmFilePath, [err description]);
   }

   [_metadata release];
   if ( _url != nil )
      [_url release];

   [super dealloc];
}

- (void) setMode:(ListMode_t)mode
{
   NSFileManager *fileMgr = [NSFileManager defaultManager];
   NSUserDefaults *pref = [NSUserDefaults standardUserDefaults];
   int cnt;
   const char *tmpdir = NULL;

   _mode = mode;

   // Create a temporary filename for the converted PPM
   NSString *tmpdirStr = [pref stringForKey:K_TMPDIR_KEY];
   if ( tmpdirStr != nil )
      tmpdir = [[tmpdirStr stringByExpandingTildeInPath] UTF8String];
   if ( tmpdir == NULL || *tmpdir == '\0' )
      tmpdir = getenv("TMPDIR");
   if ( tmpdir == NULL || *tmpdir == '\0' )
      tmpdir = "/tmp";
   // with a count suffix shared by all documents
   cnt = [pref integerForKey:K_COUNTER_KEY] % 10000;
   _ppmFilePath = [[NSString stringWithFormat:@"%s/%@%04d.%s",
                                        tmpdir,
                                        [fileMgr displayNameAtPath:[_url path]],
                                        cnt,
                                        (mode == DarkFrameMode ? "pgm" : "ppm")]
                   retain];
   cnt++;
   [pref setInteger:cnt forKey:K_COUNTER_KEY];

   // Retrieve image information
   [self getImageInfo];
   _numberOfPlanes = (mode == DarkFrameMode ? 1 : 3);
   _dataMax = 65535;

   // Schedule the conversion, letting time for the dark frame if not aleady set
   if ( _dark == nil )
      [self performSelector:@selector(launchConversion)
                 withObject:nil afterDelay:0.1];
   else
      [self launchConversion];
}

- (void) setDarkFrame:(id<LynkeosImageBuffer>)dark
{
   NSAssert( dark == nil || _mode == ImageMode || _mode == UnsetListMode,
             @"Inconsistent combination of image mode and calibration frames" );

   _dark = dark;

   if ( _mode != UnsetListMode )
   {
      // We got the dark frame, and the mode is set : convert
      // But don't forget to cancel the scheduled conversion, if any
      [[NSRunLoop currentRunLoop] cancelPerformSelectorsWithTarget:self];
      [self launchConversion];
   }
}

- (void) setFlatField:(id<LynkeosImageBuffer>)flat
{
   // We don't care ;o)
}

- (void) imageWidth:(u_short*)w height:(u_short*)h
{
   [self waitForConversion];

   *w = _width;
   *h = _height;
}

- (u_short) numberOfPlanes
{
   return( _numberOfPlanes );
}

- (void) getMinLevel:(double*)vmin maxLevel:(double*)vmax
{
   *vmin = 0.0;
   if ( _numberOfPlanes == 1 )
      *vmax = 65535.0;    // Dark frames are not scaled
   else
      *vmax = 255.0;
}

- (NSImage*) getNSImage
{
   NSImage *image = nil;
   NSBitmapImageRep* bitmap;

   // Create a RGB bitmap
   bitmap =
      [[[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil
                                               pixelsWide:_width
                                               pixelsHigh:_height
                                            bitsPerSample:8
                                          samplesPerPixel:_numberOfPlanes
                                                 hasAlpha:NO
                                                 isPlanar:NO
                                           colorSpaceName:
                                              (_numberOfPlanes == 1 ?
                                               NSCalibratedWhiteColorSpace :
                                               NSCalibratedRGBColorSpace)
                                             bitmapFormat:0
                                              bytesPerRow:0
                                             bitsPerPixel:8*_numberOfPlanes]
          autorelease];

   if ( bitmap != nil )
   {
      u_char *pixels = (u_char*)[bitmap bitmapData];
      u_short *sample = (u_short*)malloc( sizeof(u_short)*_numberOfPlanes*_width*_height );
      u_long x, y, p;
      double scale =  256.0 / ((double)_dataMax + 1.0);
      int bpp = [bitmap bitsPerPixel];
      int bpr = [bitmap bytesPerRow];

      NSAssert( (bpp%8) == 0,
                @"Hey, I do not intend to work on non byte boudaries" );
      bpp /= 8;

      [self getPPMsample:sample atX:0 Y:0 W:_width H:_height];

      for( y = 0; y < _height; y++ )
      {
         for( x = 0; x < _width; x++ )
         {
            u_short *v = &sample[(y*_width+x)*_numberOfPlanes];

            for ( p = 0 ; p < _numberOfPlanes; p++ )
               pixels[y*bpr+x*bpp+p] = CFSwapInt16BigToHost(v[p])*scale;
         }
      }

      free( sample );

      image = [[[NSImage alloc] initWithSize:NSMakeSize(_width,_height)]
                                                                   autorelease];

      if ( image != nil )
         [image addRepresentation:bitmap];
   }

   return( image );
}

/*! Pixels values are scaled to remain with a 256 maximum while retaining
 * 16 bits precision (because they are floating precision numbers)
 * But not for dark frames (mono) which are not scaled
 */
- (void) getImageSample:(void * const * const)sample 
          withPrecision:(floating_precision_t)precision
             withPlanes:(u_short)nPlanes
                    atX:(u_short)x Y:(u_short)y W:(u_short)w H:(u_short)h
              lineWidth:(u_short)lineW
{
   const double scale = (_numberOfPlanes == 1 ? 1.0 : 1.0/256.0);
   u_short xs, ys, cs;
   u_short *ppmData;

   NSAssert( x+w <= _width && y+h <= _height, 
             @"Sample at least partly outside the image" );

   ppmData = (u_short*)malloc( sizeof(u_short)*_numberOfPlanes*w*h );

   [self getPPMsample:ppmData atX:x Y:y W:w H:h];

   for ( ys = 0; ys < h; ys++ )
   {
      for( xs = 0; xs < w; xs++ )
      {
         if ( nPlanes == 1 && _numberOfPlanes != 1 )
         {
            u_short *v = &ppmData[(ys*w+xs)*_numberOfPlanes];

            // Convert to monochrome
            SET_SAMPLE( sample[0],precision,xs,ys,lineW, 
                        (CFSwapInt16BigToHost(v[0])
                         +CFSwapInt16BigToHost(v[1])
                         +CFSwapInt16BigToHost(v[2]))/3.0*scale );
         }
         else
         {
            for( cs = 0; cs < nPlanes; cs++ )
               SET_SAMPLE( sample[cs],precision,xs,ys,lineW, 
                           CFSwapInt16BigToHost(
                              ppmData[(ys*w+xs)*_numberOfPlanes+cs])*scale );
         }
      }
   }

   free( ppmData );
}

- (NSDictionary*) getMetaData 
{
   return( (NSDictionary*)_metadata );
}

- (id <LynkeosImageBuffer>) getCustomImageSampleAtX:(u_short)x Y:(u_short)y 
                                                  W:(u_short)w H:(u_short)h
{
   DcrawCustomImage *img
      = [[[DcrawCustomImage alloc] initWithNumberOfPlanes:_numberOfPlanes width:w height:h]
           retain];

   [self getImageSample:[img colorPlanes]
          withPrecision:PROCESSING_PRECISION
             withPlanes:_numberOfPlanes atX:x Y:y W:w H:h lineWidth:img->_padw];

   return( img );
}

- (BOOL) canBeCalibratedBy:(id <LynkeosFileReader>)reader
                    asMode:(ListMode_t)mode
{
   u_short w, h, n, nref;
   NSString *camera
      = [[reader getMetaData] objectForKey:LynkeosMD_CameraModel()];

   if ( mode == DarkFrameMode )
      nref = 1;
   else
      nref = _numberOfPlanes;

   n = [reader numberOfPlanes];
   [reader imageWidth:&w height:&h];

   return( [reader isKindOfClass:[self class]]
           && [[_metadata objectForKey:LynkeosMD_CameraModel()]
                                                         isEqualToString:camera]
           && n == nref && w == _baseWidth && h == _baseHeight );
}

@end
