From e002c0b1be44793ccdd84a1798e7e509351c065f Mon Sep 17 00:00:00 2001 From: Angel Carpintero Date: Mon, 21 Feb 2011 13:23:55 +0100 Subject: [PATCH] Added Exif feature http://www.lavrsen.dk/foswiki/bin/view/Motion/ExifTaggingPatch --- CHANGELOG | 1 + conf.c | 11 ++ conf.h | 1 + motion-dist.conf.in | 5 + motion.1 | 5 + motion.c | 2 +- motion.h | 2 +- picture.c | 338 +++++++++++++++++++++++++++++++++++++++++++- video2.c | 3 +- 9 files changed, 361 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 07e67ea..8d79ec0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,6 +38,7 @@ Features * Implemented new logging system http://www.lavrsen.dk/foswiki/bin/view/Motion/MotionLog (Angel Carpintero) * Added a macro MOTION_LOG , no need to add __FUNCTION__ anymore. (Angel Carpintero) + * Added EXTIF feature for jpeg images , http://www.lavrsen.dk/foswiki/bin/view/Motion/ExifTaggingPatch (Wim Lewis) Bugfixes * Avoid segfault detecting strerror_r() version GNU or SUSv3. (Angel Carpintero) diff --git a/conf.c b/conf.c index 75d9993..c3565e0 100644 --- a/conf.c +++ b/conf.c @@ -154,6 +154,7 @@ struct config conf_template = { despeckle_filter: NULL, area_detect: NULL, minimum_motion_frames: 1, + exif_text: NULL, pid_file: NULL, log_file: NULL, log_level: LEVEL_DEFAULT+10, @@ -894,6 +895,16 @@ config_param config_params[] = { print_bool }, { + "exif_text", + "# Text to include in a JPEG EXIF comment\n" + "# May be any text, including conversion specifiers.\n" + "# The EXIF timestamp is included independent of this text.", + 0, + CONF_OFFSET(exif_text), + copy_string, + print_string + }, + { "target_dir", "\n############################################################\n" "# Target Directories and filenames For Images And Films\n" diff --git a/conf.h b/conf.h index afadce6..b03397e 100644 --- a/conf.h +++ b/conf.h @@ -131,6 +131,7 @@ struct config { const char *despeckle_filter; const char *area_detect; int minimum_motion_frames; + const char *exif_text; char *pid_file; int argc; char **argv; diff --git a/motion-dist.conf.in b/motion-dist.conf.in index 2946805..4d688b1 100644 --- a/motion-dist.conf.in +++ b/motion-dist.conf.in @@ -392,6 +392,11 @@ text_event %Y%m%d%H%M%S text_double off +# Text to include in a JPEG EXIF comment +# May be any text, including conversion specifiers. +# The EXIF timestamp is included independent of this text. +;exif_text %i%J/%K%L + ############################################################ # Target Directories and filenames For Images And Films # For the options snapshot_, picture_, movie_ and timelapse_filename diff --git a/motion.1 b/motion.1 index 26f09af..1fe6fc9 100644 --- a/motion.1 +++ b/motion.1 @@ -111,6 +111,11 @@ Values: 0 - 2147483647 / Default: 60 .br Event Gap is the seconds of no motion detection that triggers the end of an event. An event is defined as a series of motion images taken within a short timeframe. .TP +.B exif_text string +Values: Max 4095 characters / Default: Not defined +.br +Text to include in a JPEG EXIF comment , may be any text, including conversion specifiers. The EXIF timestamp is included independent of this text. +.TP .B extpipe string Values: Max 4095 characters / Default: Not defined .br diff --git a/motion.c b/motion.c index c10d358..f996539 100644 --- a/motion.c +++ b/motion.c @@ -3110,7 +3110,7 @@ int myfclose(FILE* fh) * * Returns: number of bytes written to the string s */ -size_t mystrftime(struct context *cnt, char *s, size_t max, const char *userformat, +size_t mystrftime(const struct context *cnt, char *s, size_t max, const char *userformat, const struct tm *tm, const char *filename, int sqltype) { char formatstring[PATH_MAX] = ""; diff --git a/motion.h b/motion.h index 09d4bbc..24395d3 100644 --- a/motion.h +++ b/motion.h @@ -454,6 +454,6 @@ void * mymalloc(size_t); void * myrealloc(void *, size_t, const char *); FILE * myfopen(const char *, const char *, size_t); int myfclose(FILE *); -size_t mystrftime(struct context *, char *, size_t, const char *, const struct tm *, const char *, int); +size_t mystrftime(const struct context *, char *, size_t, const char *, const struct tm *, const char *, int); int create_path(const char *); #endif /* _INCLUDE_MOTION_H */ diff --git a/picture.c b/picture.c index f946217..61c10ed 100644 --- a/picture.c +++ b/picture.c @@ -3,6 +3,7 @@ * Various funtions for saving/loading pictures. * Copyright 2002 by Jeroen Vreeken (pe1rxq@amsat.org) * Portions of this file are Copyright by Lionnel Maugis + * Portions of this file are Copyright 2010 by Wim Lewis (wiml@hhhh.org) * This software is distributed under the GNU public license version 2 * See also the file 'COPYING'. * @@ -11,6 +12,8 @@ #include "picture.h" #include "event.h" +#include + #undef HAVE_STDLIB_H #include #include @@ -80,6 +83,320 @@ static GLOBAL(int) _jpeg_mem_size(j_compress_ptr cinfo) return dest->jpegsize; } +/* EXIF image data is always in TIFF format, even if embedded in another + * file type. This consists of a constant header (TIFF file header, + * IFD header) followed by the tags in the IFD and then the data + * from any tags which do not fit inline in the IFD. + * + * The tags we write in the main IFD are: + * 0x010E Image description + * 0x8769 Exif sub-IFD + * 0x882A Time zone of time stamps + * and in the Exif sub-IFD: + * 0x9000 Exif version + * 0x9003 File date and time + * 0x9291 File date and time subsecond info + * But we omit any empty IFDs. + */ + +#define TIFF_TAG_IMAGE_DESCRIPTION 0x010E +#define TIFF_TAG_DATETIME 0x0132 +#define TIFF_TAG_EXIF_IFD 0x8769 +#define TIFF_TAG_TZ_OFFSET 0x882A + +#define EXIF_TAG_EXIF_VERSION 0x9000 +#define EXIF_TAG_ORIGINAL_DATETIME 0x9003 +#define EXIF_TAG_SUBJECT_AREA 0x9214 +#define EXIF_TAG_TIFF_DATETIME_SS 0x9290 +#define EXIF_TAG_ORIGINAL_DATETIME_SS 0x9291 + +#define TIFF_TYPE_ASCII 2 /* ASCII text */ +#define TIFF_TYPE_USHORT 3 /* Unsigned 16-bit int */ +#define TIFF_TYPE_LONG 4 /* Unsigned 32-bit int */ +#define TIFF_TYPE_UNDEF 7 /* Byte blob */ +#define TIFF_TYPE_SSHORT 8 /* Signed 16-bit int */ + +static const char exif_marker_start[14] = { + 'E', 'x', 'i', 'f', 0, 0, /* EXIF marker signature */ + 'M', 'M', 0, 42, /* TIFF file header (big-endian) */ + 0, 0, 0, 8, /* Offset to first toplevel IFD */ +}; + +static const char exif_version_tag[12] = { + 0x90, 0x00, /* EXIF version tag, 0x9000 */ + 0x00, 0x07, /* Data type 7 = "unknown" (raw byte blob) */ + 0x00, 0x00, 0x00, 0x04, /* Data length */ + 0x30, 0x32, 0x32, 0x30 /* Inline data, EXIF version 2.2 */ +}; + +static const char exif_subifd_tag[8] = { + 0x87, 0x69, /* EXIF Sub-IFD tag */ + 0x00, 0x04, /* Data type 4 = uint32 */ + 0x00, 0x00, 0x00, 0x01, /* Number of values */ +}; + +static const char exif_tzoffset_tag[12] = { + 0x88, 0x2A, /* TIFF/EP time zone offset tag */ + 0x00, 0x08, /* Data type 8 = sint16 */ + 0x00, 0x00, 0x00, 0x01, /* Number of values */ + 0, 0, 0, 0 /* Dummy data */ +}; + +static void put_uint16(JOCTET *buf, unsigned value) +{ + buf[0] = ( value & 0xFF00 ) >> 8; + buf[1] = ( value & 0x00FF ); +} + +static void put_sint16(JOCTET *buf, int value) +{ + buf[0] = ( value & 0xFF00 ) >> 8; + buf[1] = ( value & 0x00FF ); +} + +static void put_uint32(JOCTET *buf, unsigned value) +{ + buf[0] = ( value & 0xFF000000 ) >> 24; + buf[1] = ( value & 0x00FF0000 ) >> 16; + buf[2] = ( value & 0x0000FF00 ) >> 8; + buf[3] = ( value & 0x000000FF ); +} + +struct tiff_writing { + JOCTET * const base; + JOCTET *buf; + unsigned data_offset; +}; + +static void put_direntry(struct tiff_writing *into, const char *data, unsigned length) +{ + if (length <= 4) { + /* Entries that fit in the directory entry are stored there */ + memset(into->buf, 0, 4); + memcpy(into->buf, data, length); + } else { + /* Longer entries are stored out-of-line */ + unsigned offset = into->data_offset; + + while ((offset & 0x03) != 0) { /* Alignment */ + into->base[offset] = 0; + offset ++; + } + + put_uint32(into->buf, offset); + memcpy(into->base + offset, data, length); + into->data_offset = offset + length; + } +} + +static void put_stringentry(struct tiff_writing *into, unsigned tag, const char *str, int with_nul) +{ + unsigned stringlength = strlen(str) + (with_nul?1:0); + + put_uint16(into->buf, tag); + put_uint16(into->buf + 2, TIFF_TYPE_ASCII); + put_uint32(into->buf + 4, stringlength); + into->buf += 8; + put_direntry(into, str, stringlength); + into->buf += 4; +} + +static void put_subjectarea(struct tiff_writing *into, const struct coord *box) +{ + put_uint16(into->buf , EXIF_TAG_SUBJECT_AREA); + put_uint16(into->buf + 2, TIFF_TYPE_USHORT); + put_uint32(into->buf + 4, 4 /* Four USHORTs */); + put_uint32(into->buf + 8, into->data_offset); + into->buf += 12; + JOCTET *ool = into->base + into->data_offset; + put_uint16(ool , box->x); /* Center.x */ + put_uint16(ool+2, box->y); /* Center.y */ + put_uint16(ool+4, box->width); + put_uint16(ool+6, box->height); + into->data_offset += 8; +} + +/* + * put_jpeg_exif writes the EXIF APP1 chunk to the jpeg file. + * It must be called after jpeg_start_compress() but before + * any image data is written by jpeg_write_scanlines(). + */ +static void put_jpeg_exif(j_compress_ptr cinfo, + const struct context *cnt, + const struct tm *timestamp, + const struct coord *box) +{ + /* description, datetime, and subtime are the values that are actually + * put into the EXIF data + */ + char *description, *datetime, *subtime; + char datetime_buf[22]; + + if (timestamp) { + /* Exif requires this exact format */ + snprintf(datetime_buf, 21, "%04d:%02d:%02d %02d:%02d:%02d", + timestamp->tm_year + 1900, + timestamp->tm_mon + 1, + timestamp->tm_mday, + timestamp->tm_hour, + timestamp->tm_min, + timestamp->tm_sec); + datetime = datetime_buf; + } else { + datetime = NULL; + } + + // TODO: Extract subsecond timestamp from somewhere, but only + // use as much of it as is indicated by conf->frame_limit + subtime = NULL; + + if (cnt->conf.exif_text) { + description = malloc(PATH_MAX); + mystrftime(cnt, description, PATH_MAX-1, + cnt->conf.exif_text, + timestamp, NULL, 0); + } else { + description = NULL; + } + + /* Calculate an upper bound on the size of the APP1 marker so + * we can allocate a buffer for it. + */ + + /* Count up the number of tags and max amount of OOL data */ + int ifd0_tagcount = 0; + int ifd1_tagcount = 0; + unsigned datasize = 0; + + if (description) { + ifd0_tagcount ++; + datasize += 5 + strlen(description); /* Add 5 for NUL and alignment */ + } + + if (datetime) { + /* We write this to both the TIFF datetime tag (which most programs + * treat as "last-modified-date") and the EXIF "time of creation of + * original image" tag (which many programs ignore). This is + * redundant but seems to be the thing to do. + */ + ifd0_tagcount++; + ifd1_tagcount++; + /* We also write the timezone-offset tag in IFD0 */ + ifd0_tagcount++; + /* It would be nice to use the same offset for both tags' values, + * but I don't want to write the bookkeeping for that right now */ + datasize += 2 * (5 + strlen(datetime)); + } + + if (subtime) { + ifd1_tagcount++; + datasize += 5 + strlen(subtime); + } + + if (box) { + ifd1_tagcount++; + datasize += 2 * 4; /* Four 16-bit ints */ + } + + if (ifd1_tagcount > 0) { + /* If we're writing the Exif sub-IFD, account for the + * two tags that requires */ + ifd0_tagcount ++; /* The tag in IFD0 that points to IFD1 */ + ifd1_tagcount ++; /* The EXIF version tag */ + } + + /* Each IFD takes 12 bytes per tag, plus six more (the tag count and the + * pointer to the next IFD, always zero in our case) + */ + unsigned int ifds_size = + ( ifd1_tagcount > 0 ? ( 12 * ifd1_tagcount + 6 ) : 0 ) + + ( ifd0_tagcount > 0 ? ( 12 * ifd0_tagcount + 6 ) : 0 ); + + if (ifds_size == 0) { + /* We're not actually going to write any information. */ + return; + } + + unsigned int buffer_size = 6 /* EXIF marker signature */ + + 8 /* TIFF file header */ + + ifds_size /* the tag directories */ + + datasize; + + JOCTET *marker = malloc(buffer_size); + memcpy(marker, exif_marker_start, 14); /* EXIF and TIFF headers */ + + struct tiff_writing writing = (struct tiff_writing) { + .base = marker + 6, /* base address for intra-TIFF offsets */ + .buf = marker + 14, /* current write position */ + .data_offset = 8 + ifds_size, /* where to start storing data */ + }; + + /* Write IFD 0 */ + /* Note that tags are stored in numerical order */ + put_uint16(writing.buf, ifd0_tagcount); + writing.buf += 2; + + if (description) + put_stringentry(&writing, TIFF_TAG_IMAGE_DESCRIPTION, description, 0); + + if (datetime) + put_stringentry(&writing, TIFF_TAG_DATETIME, datetime, 1); + + if (ifd1_tagcount > 0) { + /* Offset of IFD1 - TIFF header + IFD0 size. */ + unsigned ifd1_offset = 8 + 6 + ( 12 * ifd0_tagcount ); + memcpy(writing.buf, exif_subifd_tag, 8); + put_uint32(writing.buf + 8, ifd1_offset); + writing.buf += 12; + } + + if (datetime) { + memcpy(writing.buf, exif_tzoffset_tag, 12); + put_sint16(writing.buf+8, timestamp->tm_gmtoff / 3600); + writing.buf += 12; + } + + put_uint32(writing.buf, 0); /* Next IFD offset = 0 (no next IFD) */ + writing.buf += 4; + + /* Write IFD 1 */ + if (ifd1_tagcount > 0) { + /* (remember that the tags in any IFD must be in numerical order + * by tag) */ + put_uint16(writing.buf, ifd1_tagcount); + memcpy(writing.buf + 2, exif_version_tag, 12); /* tag 0x9000 */ + writing.buf += 14; + + if (datetime) + put_stringentry(&writing, EXIF_TAG_ORIGINAL_DATETIME, datetime, 1); + + if (box) + put_subjectarea(&writing, box); + + if (subtime) + put_stringentry(&writing, EXIF_TAG_ORIGINAL_DATETIME_SS, subtime, 0); + + put_uint32(writing.buf, 0); /* Next IFD = 0 (no next IFD) */ + writing.buf += 4; + } + + /* We should have met up with the OOL data */ + assert( (writing.buf - writing.base) == 8 + ifds_size ); + + /* The buffer is complete; write it out */ + unsigned marker_len = 6 + writing.data_offset; + + /* assert we didn't underestimate the original buffer size */ + assert(marker_len <= buffer_size); + + /* EXIF data lives in a JPEG APP1 marker */ + jpeg_write_marker(cinfo, JPEG_APP0 + 1, marker, marker_len); + + if (description) + free(description); + + free(marker); +} /** * put_jpeg_yuv420p_memory @@ -97,7 +414,9 @@ static GLOBAL(int) _jpeg_mem_size(j_compress_ptr cinfo) * Returns buffer size of jpeg image */ static int put_jpeg_yuv420p_memory(unsigned char *dest_image, int image_size, - unsigned char *input_image, int width, int height, int quality) + unsigned char *input_image, int width, int height, int quality, + struct context *cnt, struct tm *tm, struct coord *box) + { int i, j, jpeg_image_size; @@ -140,6 +459,8 @@ static int put_jpeg_yuv420p_memory(unsigned char *dest_image, int image_size, jpeg_start_compress (&cinfo, TRUE); + put_jpeg_exif(&cinfo, cnt, tm, box); + for (j = 0; j < height; j += 16) { for (i = 0; i < 16; i++) { y[i] = input_image + width * (i + j); @@ -196,6 +517,8 @@ static int put_jpeg_grey_memory(unsigned char *dest_image, int image_size, unsig jpeg_start_compress (&cjpeg, TRUE); + put_jpeg_exif(&cjpeg, NULL, NULL, NULL); + row_ptr[0] = input_image; for (y = 0; y < height; y++) { @@ -225,7 +548,10 @@ static int put_jpeg_grey_memory(unsigned char *dest_image, int image_size, unsig * * Returns nothing */ -static void put_jpeg_yuv420p_file(FILE *fp, unsigned char *image, int width, int height, int quality) +static void put_jpeg_yuv420p_file(FILE *fp, + unsigned char *image, int width, int height, + int quality, + struct context *cnt, struct tm *tm, struct coord *box) { int i, j; @@ -267,6 +593,8 @@ static void put_jpeg_yuv420p_file(FILE *fp, unsigned char *image, int width, int jpeg_stdio_dest(&cinfo, fp); // Data written to file jpeg_start_compress(&cinfo, TRUE); + put_jpeg_exif(&cinfo, cnt, tm, box); + for (j = 0; j < height; j += 16) { for (i = 0; i < 16; i++) { y[i] = image + width * (i + j); @@ -319,6 +647,8 @@ static void put_jpeg_grey_file(FILE *picture, unsigned char *image, int width, i jpeg_start_compress(&cjpeg, TRUE); + put_jpeg_exif(&cjpeg, NULL, NULL, NULL); + row_ptr[0] = image; for (y = 0; y < height; y++) { @@ -557,7 +887,7 @@ int put_picture_memory(struct context *cnt, unsigned char* dest_image, int image switch (cnt->imgs.type) { case VIDEO_PALETTE_YUV420P: return put_jpeg_yuv420p_memory(dest_image, image_size, image, - cnt->imgs.width, cnt->imgs.height, quality); + cnt->imgs.width, cnt->imgs.height, quality, cnt, &(cnt->current_image->timestamp_tm), &(cnt->current_image->location)); case VIDEO_PALETTE_GREY: return put_jpeg_grey_memory(dest_image, image_size, image, cnt->imgs.width, cnt->imgs.height, quality); @@ -576,7 +906,7 @@ void put_picture_fd(struct context *cnt, FILE *picture, unsigned char *image, in } else { switch (cnt->imgs.type) { case VIDEO_PALETTE_YUV420P: - put_jpeg_yuv420p_file(picture, image, cnt->imgs.width, cnt->imgs.height, quality); + put_jpeg_yuv420p_file(picture, image, cnt->imgs.width, cnt->imgs.height, quality, cnt, &(cnt->current_image->timestamp_tm), &(cnt->current_image->location)); break; case VIDEO_PALETTE_GREY: put_jpeg_grey_file(picture, image, cnt->imgs.width, cnt->imgs.height, quality); diff --git a/video2.c b/video2.c index f413b14..9896edb 100644 --- a/video2.c +++ b/video2.c @@ -260,7 +260,8 @@ static int v4l2_select_input(struct config *conf, struct video_dev *viddev, if (xioctl(vid_source->fd, VIDIOC_ENUMINPUT, &input) == -1) { MOTION_LOG(ERR, TYPE_VIDEO, SHOW_ERRNO, "%s: Unable to query input %d." - " VIDIOC_ENUMINPUT", input.index); + " VIDIOC_ENUMINPUT, if you use a WEBCAM change input value in conf by -1", + input.index); return -1; }