TODO: figure out how to autodetect lib vs lib64. These changes were made some time ago (can't remember when) but didn't get committed until now. (Yes, naughty me, but perhaps I was in a rush.)
551 lines
16 KiB
C
551 lines
16 KiB
C
#include <photo.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <util.h>
|
|
#include <wand/MagickWand.h>
|
|
#include <libgen.h>
|
|
#include <dprintf.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <jpeg.h>
|
|
#include <settings.h>
|
|
|
|
/* Read photo information in. It takes a gallery and photo filename as
|
|
* arguments, and can optionally take a pre-allocated photo_info struct.
|
|
*
|
|
* Returns a struct on success, or NULL on failure.
|
|
*/
|
|
struct photo_info* read_photo_info( struct gallery_contents* gallery,
|
|
char* filename,
|
|
struct photo_info* dest ) {
|
|
int free_struct = 1;
|
|
|
|
if ( gallery == NULL ) return NULL;
|
|
if ( filename == NULL ) return NULL;
|
|
if ( dest == NULL ) {
|
|
dest = (struct photo_info*)
|
|
gccalloc( 1, sizeof(struct photo_info) );
|
|
dprintf( "read_photo_info: allocated struct at %p\n", dest );
|
|
if ( dest == NULL ) return NULL;
|
|
} else {
|
|
free_struct = 0;
|
|
}
|
|
dprintf( "read_photo_info: loading photo %s into %p\n",
|
|
filename, dest );
|
|
|
|
/* Determine the full file path */
|
|
char* filepath = construct_path( "s/s",
|
|
gallery->info->gallery_dir, filename );
|
|
if ( filepath == NULL ) {
|
|
if ( free_struct ) gcfree( dest );
|
|
return NULL;
|
|
}
|
|
dprintf( "read_photo_info: full path at %s\n", filepath );
|
|
|
|
/* Read in basic photo metadata */
|
|
struct photo_meta* meta =
|
|
read_photo_meta( filepath, &dest->meta );
|
|
if ( meta == NULL ) {
|
|
gcfree( filepath );
|
|
if ( free_struct ) gcfree( dest );
|
|
return NULL;
|
|
}
|
|
dprintf( "read_photo_info: metadata loaded to %p\n", meta );
|
|
|
|
/* Partially initialise the struct */
|
|
dest->gc = gallery;
|
|
dest->index = (unsigned long)(-1);
|
|
dest->previous = NULL;
|
|
dest->next = NULL;
|
|
dest->annotation = NULL;
|
|
dest->description = NULL;
|
|
|
|
/* Default settings should be set according to system defaults */
|
|
dest->default_rotation = DEFAULT_ROTATION;
|
|
dest->default_quality = DEFAULT_QUALITY;
|
|
if ( meta->aspect_ratio >= PANORAMA_ASPECT_RATIO ) {
|
|
dest->default_dims.width = PANORAMA_DEFAULT_WIDTH;
|
|
dest->default_dims.height = PANORAMA_DEFAULT_HEIGHT;
|
|
} else {
|
|
dest->default_dims.width = DEFAULT_WIDTH;
|
|
dest->default_dims.height = DEFAULT_HEIGHT;
|
|
}
|
|
|
|
/* Attempt to locate the photo and its neighbours */
|
|
size_t i;
|
|
for( i = 0; i < gallery->photos->length; i++ ) {
|
|
if ( strcmp( filename, gallery->photos->string[i] ) == 0 ) {
|
|
dest->index = i;
|
|
if ( i > 0 )
|
|
dest->previous = strdup(
|
|
gallery->photos->string[i-1] );
|
|
if ( i < gallery->photos->length-1 )
|
|
dest->next = strdup(
|
|
gallery->photos->string[i+1] );
|
|
break;
|
|
}
|
|
}
|
|
dprintf( "read_photo_info: neighbours found: %s (%p) and %s (%p)\n",
|
|
dest->previous, dest->previous, dest->next, dest->next );
|
|
|
|
/* Attempt to locate the thumbnail image */
|
|
dest->thumbnail = resize_photo( meta, 100, 100, 25, 0, &dest->thumbdims );
|
|
/* char* thumbnail = construct_path( "s/s/s", gallery->info->gallery_name,
|
|
"thumbnails",
|
|
filename );
|
|
if ( thumbnail != NULL ) {
|
|
dest->thumbnail = substitute_ext( thumbnail, ".jpg" );
|
|
gcfree( thumbnail );
|
|
}*/
|
|
dprintf( "read_photo_info: thumbnail at %s (%p)\n",
|
|
dest->thumbnail, dest->thumbnail );
|
|
|
|
/* Attempt to load the annotation. Two possible formats are available:
|
|
* (1) "Legacy" annotations.txt database
|
|
* (2) New per-file annotations (in <filename>.txt)
|
|
*
|
|
* A load will be attempted in this order.
|
|
*/
|
|
char* ann_file = construct_path( "s/s", gallery->info->gallery_dir,
|
|
"annotations.txt" );
|
|
if ( ann_file != NULL ) {
|
|
FILE* annotations = fopen( ann_file, "r" );
|
|
if ( annotations != NULL ) {
|
|
char buffer[1024];
|
|
char* strtok_ptr;
|
|
dprintf( "read_photo_info: parsing legacy annotations "
|
|
"database in %s\n", ann_file );
|
|
|
|
/* Read each line in... and split according to tabs */
|
|
while( fgets( buffer, 1024, annotations ) ) {
|
|
char* file = strtok_r( buffer, "\t", &strtok_ptr );
|
|
|
|
/* Does it match? */
|
|
if ( strcmp( file, filename ) == 0 ) {
|
|
/* Yes... grab the next token */
|
|
char* ann = strtok_r( NULL, "\t",
|
|
&strtok_ptr );
|
|
/* If not null, copy it */
|
|
if ( ann != NULL )
|
|
dest->annotation = strdup(ann);
|
|
|
|
/* Stop here */
|
|
break;
|
|
}
|
|
}
|
|
fclose( annotations );
|
|
}
|
|
gcfree( ann_file );
|
|
}
|
|
|
|
/* If we still haven't located the annotation yet... try method (2). */
|
|
if ( dest->annotation == NULL ) {
|
|
/* The newer format uses a file with the same format as
|
|
* info.txt, and thus can handle more information and longer
|
|
* strings. It'll soon store more than just the annotation,
|
|
* but for now, that's it.
|
|
*
|
|
* The file is the name of the image, with the extension
|
|
* substituted with .txt.
|
|
*/
|
|
ann_file = substitute_ext( filepath, ".txt" );
|
|
if ( ann_file != NULL ) {
|
|
dprintf( "read_photo_info: legacy read failed, "
|
|
"attempting to load annotations from %s\n",
|
|
ann_file );
|
|
/* Read the file in */
|
|
struct txtinfo_chain* annotations =
|
|
read_txtinfo_chain( ann_file );
|
|
gcfree( ann_file );
|
|
|
|
if ( annotations != NULL ) {
|
|
dest->annotation =
|
|
get_txtinfo_chain_val( annotations,
|
|
".annotation" );
|
|
|
|
/* This method can also specify default width,
|
|
* height, quality and dimension info
|
|
*/
|
|
char* str;
|
|
str = get_txtinfo_chain_val(
|
|
annotations, ".width" );
|
|
if ( str != NULL )
|
|
dest->default_dims.width = atoi(str);
|
|
str = get_txtinfo_chain_val(
|
|
annotations, ".height" );
|
|
if ( str != NULL )
|
|
dest->default_dims.height = atoi(str);
|
|
str = get_txtinfo_chain_val(
|
|
annotations, ".quality" );
|
|
if ( str != NULL )
|
|
dest->default_quality = (char)atoi(str);
|
|
str = get_txtinfo_chain_val(
|
|
annotations, ".rotation" );
|
|
if ( str != NULL )
|
|
dest->default_rotation = atof(str);
|
|
|
|
/* Description is a block of HTML
|
|
* displayed below the photo */
|
|
dest->description =
|
|
get_txtinfo_chain_val( annotations,
|
|
".description" );
|
|
|
|
destroy_txtinfo_chain( annotations );
|
|
}
|
|
}
|
|
}
|
|
dprintf( "read_photo_info: annotation= %s (%p)\n",
|
|
dest->annotation, dest->annotation );
|
|
|
|
return dest;
|
|
}
|
|
|
|
/* Copy photo information to a new struct.
|
|
*
|
|
* This can take a pre-allocated struct for destination storage.
|
|
*
|
|
* Returns the copy on success, NULL on failure.
|
|
*/
|
|
struct photo_info* copy_photo_info( struct photo_info* src,
|
|
struct photo_info* dest ) {
|
|
|
|
int free_struct = 1;
|
|
|
|
if ( src == NULL ) return NULL;
|
|
if ( dest == NULL ) {
|
|
dest = (struct photo_info*)
|
|
gcmalloc( sizeof(struct photo_info) );
|
|
if ( dest == NULL ) return NULL;
|
|
} else {
|
|
free_struct = 0;
|
|
}
|
|
|
|
/* Copy gallery pointer */
|
|
dest->gc = src->gc;
|
|
|
|
/* Copy the strings */
|
|
if ( src->previous != NULL )
|
|
dest->previous = strdup( src->previous );
|
|
if ( src->next != NULL )
|
|
dest->next = strdup( src->next );
|
|
if ( src->thumbnail != NULL )
|
|
dest->thumbnail = strdup( src->thumbnail );
|
|
if ( src->annotation != NULL )
|
|
dest->annotation = strdup( src->annotation );
|
|
if ( src->description != NULL )
|
|
dest->description = strdup( src->description );
|
|
|
|
/* Copy the metadata */
|
|
copy_photo_meta( &src->meta, &dest->meta );
|
|
|
|
return dest;
|
|
}
|
|
|
|
/* Destroy photo information struct. */
|
|
void destroy_photo_info( struct photo_info* photo, int fields_only ) {
|
|
destroy_photo_meta( &photo->meta, 1 );
|
|
if ( photo->previous != NULL ) gcfree( photo->previous );
|
|
if ( photo->next != NULL ) gcfree( photo->next );
|
|
if ( photo->thumbnail != NULL ) gcfree( photo->thumbnail );
|
|
if ( photo->annotation != NULL ) gcfree( photo->annotation );
|
|
if ( photo->description != NULL ) gcfree( photo->description );
|
|
if ( !fields_only ) gcfree( photo );
|
|
}
|
|
|
|
/* Read photo metadata. This takes a full path to an image or movie, and
|
|
* returns the dimensions and other critical metadata.
|
|
*
|
|
* It may optionally take a pre-allocated struct.
|
|
*
|
|
* Returns the photo metadata obtained, or NULL if it fails.
|
|
*/
|
|
struct photo_meta* read_photo_meta( const char* filename,
|
|
struct photo_meta* dest ) {
|
|
int free_struct = 1;
|
|
|
|
if ( filename == NULL ) return NULL;
|
|
if ( dest == NULL ) {
|
|
dest = (struct photo_meta*)
|
|
gcmalloc( sizeof(struct photo_meta) );
|
|
dprintf("read_photo_meta: allocated struct at %p\n", dest);
|
|
if ( dest == NULL ) return NULL;
|
|
} else {
|
|
free_struct = 0;
|
|
}
|
|
|
|
dprintf( "read_photo_meta: reading photo metadata from %s into %p\n",
|
|
filename, dest );
|
|
|
|
/* Gallery name should be the directory containing the photo */
|
|
char* dirname = get_dirname( filename );
|
|
if ( dirname == NULL ) {
|
|
if ( free_struct ) gcfree( dest );
|
|
return NULL;
|
|
}
|
|
char* gallery = get_basename( dirname );
|
|
gcfree( dirname );
|
|
if ( gallery == NULL ) {
|
|
if ( free_struct ) gcfree( dest );
|
|
return NULL;
|
|
}
|
|
dest->gallery_name = gallery;
|
|
dest->filepath = strdup( filename );
|
|
dest->filename = basename( dest->filepath );
|
|
dprintf( "read_photo_meta: gallery: %s, path: %s, name: %s\n",
|
|
dest->gallery_name, dest->filepath, dest->filename );
|
|
|
|
char* extn = rindex( filename, '.' );
|
|
/* If the extension is one of these, it's a movie */
|
|
if ( (strcasecmp( extn, ".mpg" ) == 0) ||
|
|
(strcasecmp( extn, ".mov" ) == 0) ||
|
|
(strcasecmp( extn, ".mp4" ) == 0) ||
|
|
(strcasecmp( extn, ".avi" ) == 0) ||
|
|
(strcasecmp( extn, ".mkv" ) == 0) ||
|
|
(strcasecmp( extn, ".ogm" ) == 0) ||
|
|
(strcasecmp( extn, ".wmv" ) == 0) ||
|
|
(strcasecmp( extn, ".ogg" ) == 0) ) {
|
|
/* TODO: Width and height of video! */
|
|
dest->is_video = 1;
|
|
} else {
|
|
dest->is_video = 0;
|
|
if ( ( strcasecmp( extn, ".jpg" ) == 0 ) ||
|
|
( strcasecmp( extn, ".jpeg" ) == 0 ) ) {
|
|
|
|
/* We have a JPEG image, use the simpler routine to
|
|
* decode it.
|
|
*/
|
|
read_jpeg_dims( filename, &dest->size );
|
|
} else {
|
|
MagickWand* wand = NewMagickWand();
|
|
if (MagickReadImage( wand, filename ) == MagickFalse) {
|
|
/* Failure. Free everything and return */
|
|
DestroyMagickWand( wand );
|
|
gcfree( gallery );
|
|
if ( free_struct ) gcfree( dest );
|
|
return NULL;
|
|
}
|
|
/* Okay, image read in... get the dimensions */
|
|
dest->size.width = MagickGetImageWidth( wand );
|
|
dest->size.height = MagickGetImageHeight( wand );
|
|
DestroyMagickWand( wand );
|
|
}
|
|
dest->aspect_ratio = (double)dest->size.width /
|
|
(double)dest->size.height;
|
|
}
|
|
dprintf( "read_photo_meta: video: %d, width: %d, height: %d\n",
|
|
dest->is_video, dest->size.width, dest->size.height );
|
|
|
|
return dest;
|
|
}
|
|
|
|
/* Copy photo metadata. Takes an existing photo_meta struct, and may optionally
|
|
* take a second as the destination.
|
|
*
|
|
* Returns the copy on success, NULL on failure.
|
|
*/
|
|
struct photo_meta* copy_photo_meta( struct photo_meta* src,
|
|
struct photo_meta* dest ) {
|
|
int free_struct = 1;
|
|
|
|
if ( src == NULL ) return NULL;
|
|
if ( dest == NULL ) {
|
|
dest = (struct photo_meta*)
|
|
memdup( (void*)src, sizeof(struct photo_meta) );
|
|
if ( dest == NULL ) return NULL;
|
|
} else {
|
|
memcpy( (void*)dest, (void*)src, sizeof( struct photo_meta ) );
|
|
free_struct = 0;
|
|
}
|
|
|
|
dest->gallery_name = strdup( src->gallery_name );
|
|
dest->filepath = strdup( src->filepath );
|
|
dest->filename = basename( src->filepath );
|
|
return dest;
|
|
}
|
|
|
|
/* Free photo metadata.
|
|
*
|
|
* Frees the metadata contained in the specified struct and (if fields_only is
|
|
* zero) the struct itself.
|
|
*/
|
|
void destroy_photo_meta( struct photo_meta* meta, int fields_only ) {
|
|
if ( meta->gallery_name != NULL ) gcfree( meta->gallery_name );
|
|
if ( meta->filepath != NULL ) gcfree( meta->filepath );
|
|
if ( !fields_only ) gcfree( meta );
|
|
}
|
|
|
|
/* Helper function... calculate size of resized photo */
|
|
inline void calculate_dims( struct photo_meta* meta,
|
|
unsigned int width, unsigned int height,
|
|
struct dimensions* dims ) {
|
|
if ( (width == 0) && (height == 0) ) {
|
|
width = meta->size.width;
|
|
height = meta->size.height;
|
|
} else if ( width == 0 ) {
|
|
width = (unsigned int)((double)height * meta->aspect_ratio);
|
|
} else if ( height == 0 ) {
|
|
height = (unsigned int)((double)width / meta->aspect_ratio);
|
|
} else {
|
|
/* We're given both. Scale the photo to fit within this area */
|
|
unsigned int s_width = (unsigned int)
|
|
((double)height * meta->aspect_ratio);
|
|
if ( s_width > width )
|
|
height = (unsigned int)
|
|
((double)width / meta->aspect_ratio);
|
|
else
|
|
width = s_width;
|
|
}
|
|
|
|
dims->width = width;
|
|
dims->height = height;
|
|
/* Check limits...
|
|
* These will overwrite the above values if out-of-bounds */
|
|
if ( width > MAX_WIDTH )
|
|
calculate_dims( meta, MAX_WIDTH, height, dims );
|
|
else if ( height > MAX_HEIGHT )
|
|
calculate_dims( meta, width, MAX_HEIGHT, dims );
|
|
else if ( ( width > MAX_WIDTH ) && ( height > MAX_HEIGHT ) )
|
|
calculate_dims( meta, MAX_WIDTH, MAX_HEIGHT, dims );
|
|
}
|
|
|
|
/* Resize a photo. This sizes and scales a photo to the specified dimensions
|
|
* and settings. The image is written to disk cache, the filename being
|
|
* returned for display in the browser (when generating HTML or JSON).
|
|
*
|
|
* The width or height may be set to zero, in which case, it sets it to preserve
|
|
* the aspect ratio. If both are set to zero, then the image will not be
|
|
* resized.
|
|
*
|
|
* Returns the filename of the resized image in cache, or NULL if this fails.
|
|
*/
|
|
char* resize_photo( struct photo_meta* dest,
|
|
unsigned int width, unsigned int height,
|
|
unsigned char quality, double rotation,
|
|
struct dimensions* resized_dims ) {
|
|
if ( dest == NULL ) return NULL;
|
|
|
|
dprintf( "resize_photo: fitting %s (from %s) in %dx%d area\n",
|
|
dest->filename, dest->gallery_name,
|
|
width, height );
|
|
struct dimensions my_dims;
|
|
calculate_dims( dest, width, height, &my_dims );
|
|
width = my_dims.width;
|
|
height= my_dims.height;
|
|
dprintf( "resize_photo: dimensions %dx%d\n", width, height );
|
|
|
|
if ( resized_dims != NULL )
|
|
memcpy( resized_dims, &my_dims, sizeof(my_dims) );
|
|
|
|
/* Determine the name of the cache item */
|
|
char* cache_file = construct_path( "s/scscicicicfs", CACHEDIR,
|
|
dest->gallery_name, '-', dest->filename, '-',
|
|
width, 'x', height, '-', quality,
|
|
'-', rotation, (quality == 100) ? ".png" : ".jpg" );
|
|
dprintf( "resize_photo: cache at %s (%p)\n", cache_file, cache_file );
|
|
if ( cache_file == NULL ) return NULL;
|
|
|
|
char* dir = get_cgidir();
|
|
char* cache_path = construct_path( "s/s", dir, cache_file );
|
|
//gcfree( dir );
|
|
|
|
/* Check if the file already exists... if it does, return now */
|
|
struct stat cache_stat;
|
|
if ( stat( cache_path, &cache_stat ) == 0 ) {
|
|
struct stat orig_stat;
|
|
if ( stat( dest->filepath, &orig_stat ) == 0 ) {
|
|
if ( orig_stat.st_mtime <= cache_stat.st_mtime ) {
|
|
/* Recent cache already exists */
|
|
gcfree( cache_path );
|
|
return cache_file;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifndef NO_SEMAPHORE
|
|
/*
|
|
* Check for semaphore... if it exists, wait for it to disappear.
|
|
* For this, we use the open() syscall with O_CREAT|O_EXCL, which
|
|
* will fail if the file already exists.
|
|
*/
|
|
char* sem_file = construct_path( "s/ss", get_cgidir(),
|
|
cache_file, ".sem" );
|
|
int fd = open( sem_file, O_CREAT|O_EXCL, S_IRUSR|S_IWUSR );
|
|
while( fd == -1 ) {
|
|
sleep(1);
|
|
fd = open( sem_file, O_CREAT|O_EXCL, S_IRUSR|S_IWUSR );
|
|
fprintf( stderr, "resize_photo: waiting for semaphore %s (fd: %d)\n",
|
|
sem_file, fd );
|
|
dprintf( "resize_photo: waiting for semaphore %s (fd: %d)\n",
|
|
sem_file, fd );
|
|
}
|
|
close(fd);
|
|
#endif
|
|
|
|
MagickWand* wand = NewMagickWand();
|
|
dprintf( "resize_photo: wand created at %p\n", wand);
|
|
if (MagickReadImage( wand, dest->filepath ) == MagickFalse) {
|
|
ExceptionType severity;
|
|
char* description=MagickGetException(wand,&severity);
|
|
dprintf("%s %s %ld %s\n",GetMagickModule(),description);
|
|
gcfree( cache_file );
|
|
|
|
#ifndef NO_SEMAPHORE
|
|
/* Get rid of the semaphore */
|
|
unlink( sem_file );
|
|
gcfree( sem_file );
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
/* Background colour */
|
|
PixelWand* background = NewPixelWand();
|
|
PixelSetColor( background, "#ffffff" );
|
|
|
|
MagickResizeImage( wand, width, height, LanczosFilter, 1.0 );
|
|
MagickRotateImage( wand, background, rotation );
|
|
|
|
/* Since rotating the image changes the aspect ratio, we'll
|
|
* update the resized image dimensions here.
|
|
*/
|
|
if ( ( rotation != 0.0) && ( resized_dims != NULL ) ) {
|
|
resized_dims->width = MagickGetImageWidth( wand );
|
|
resized_dims->height = MagickGetImageHeight( wand );
|
|
}
|
|
|
|
/* Image quality */
|
|
if ( quality < 100 )
|
|
MagickSetImageCompressionQuality( wand,
|
|
(unsigned long)quality );
|
|
|
|
MagickSetImageInterlaceScheme( wand, LineInterlace );
|
|
|
|
if (MagickWriteImage( wand, cache_path ) == MagickFalse) {
|
|
ExceptionType severity;
|
|
char* description=MagickGetException(wand,&severity);
|
|
dprintf("%s %s %ld %s\n",GetMagickModule(),description);
|
|
|
|
#ifndef NO_SEMAPHORE
|
|
/* Get rid of the semaphore */
|
|
unlink( sem_file );
|
|
gcfree( sem_file );
|
|
#endif
|
|
|
|
gcfree( cache_file );
|
|
cache_file = NULL;
|
|
}
|
|
|
|
DestroyMagickWand( wand );
|
|
|
|
#ifndef NO_SEMAPHORE
|
|
/* Get rid of the semaphore */
|
|
unlink( sem_file );
|
|
gcfree( sem_file );
|
|
#endif
|
|
|
|
gcfree( cache_path );
|
|
dprintf( "resize_photo: resized photo at %s (%p)\n", cache_file, cache_file );
|
|
return cache_file;
|
|
}
|