/*
* walker.c
*
* This is parser for DCF name system found on all modern digital cameras,
* and provide interpretation, you can use to tag photos.
*
* Author: Alexey Fisher
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* 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; see the file COPYING. If not, write to the Free Software
* Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
/*
* [wiki] "Design rule for Camera File system (DCF) is a JEITA specification
* (number CP-3461) which defines a file format and file system for digital
* cameras, including the directory structure, file naming method, character
* set, file format, and metadata format. It is currently the de facto
* industry standard for digital still cameras. The file format of DCF is
* based on the Exif 2.2 specification."
*
* Technical detiles:
* It use static file name lenth, so we do not need to care about how to parse
* dynamic names.
*
* independet of camera you use names have same lenth:
* DCIM/100NIKON/DSCN1234.JPG
* ^^^^ ^^^^^^^^ ^^^^^^^^^^^^
* 4 8 12 - chars
*/
#define _XOPEN_SOURCE 500
#include
#include
#include
#include
#include
#define D_UNCN 0x0f
#define D_DCIM 0x01
#define D_MISC 0x02
#define Q_LAST_4 0x4
#define Q_THIRST_3 0x3
#define JPG_IMG 0x01
#define RAW_IMG 0x02
#define AUDIO 0x03
#define VIDEO 0x04
#define PRINT_JOB 0x10
#define NICON 0x01
#define CANON 0x02
#define FUIJI 0x03
#define PANAS 0x04 /* panasonic */
#define GR_NONE 0x00 /* do not set any group */
#define GR_PANR 0x01 /* panorama group */
#define GR_INTV 0x02 /* interval group */
#define VEND_ID 0x03 /* we can match vendor here */
#define REL_MOD 0x04 /* related to some photo */
#define REL_COM 0x05
#define MATCH 1
#define NOT_MATCH 0
/**
* struct filebuffer - storage for filenames we can process. We put unsorted,
* sort it, check for dependencies and push out.
* @file - path to the file (DCIM/101NIKON/DSCN0001.JPG = 26 chars + \0)
* @number - image number (DSCN2561.JPG = 2561)
* @tag - file tags, (panorama, interval photo, audio comment)
* @file_type - type of produced file (jpg image, raw image, audio, video)
*/
struct filebuffer {
char file[27];
int number;
int tag;
int print;
int file_type;
} filebuffer[255];
/**
* compare_int - function needed for sorting filebuffer array.
* @c1,c2 - pointers to filebuffer struct.
*/
int compare_int(const void *c1, const void *c2)
{
struct filebuffer *info1 = (struct filebuffer *) c1;
struct filebuffer *info2 = (struct filebuffer *) c2;
if (info1->number > info2->number)
return 1;
else if (info1->number < info2->number)
return -1;
else {
/* file with same number is (mostly?) audio comment,
* just make sure audio file ordered after image file */
if (((info2->file_type == JPG_IMG) ||
(info2->file_type == RAW_IMG)) &&
((info1->file_type == AUDIO) ||
(info1->file_type == VIDEO)))
return 1;
else if (((info1->file_type == JPG_IMG) ||
(info1->file_type == RAW_IMG)) &&
((info2->file_type == AUDIO) ||
(info2->file_type == VIDEO)))
return -1;
else
return 0;
}
}
/**
* struct extinfo - structure for extension comparision. We convert here
* extension string to file_type integer. Every thing out of this list will
* not be processed!
* @ext - file extension we can handle, just include all image extensions.
* @file_type - we reduce different extensions to generic file type
* for example image, audio, video.
*/
struct extinfo {
const char *ext;
const int file_type;
} extinfo[] = {
{"JPG", JPG_IMG},
{"JPEG", JPG_IMG}, /* do not belong to DCIM */
{"NEF", RAW_IMG}, /* nikon */
{"CRW", RAW_IMG}, /* canon */
{"RAF", RAW_IMG}, /* fuji */
{"WAV", AUDIO},
{"AVI", VIDEO},
{"MOV", VIDEO}, /* multi type - video, or audio comment */
{"MRK", PRINT_JOB}
};
#define sizeof_extinfo (sizeof(extinfo))/sizeof(struct extinfo)
/**
* compare_ext - compare function needed to sort and search in extinfo array.
*/
int compare_ext(const void *c1, const void *c2)
{
struct extinfo *info1 = (struct extinfo *) c1;
struct extinfo *info2 = (struct extinfo *) c2;
return strcasecmp(info1->ext, info2->ext);
}
/**
* struct pattern_db - structure needed to match of file system,
* all digits converted to xxx.
* @pattern - pattern of DCIM fs.
* @vendor - vendor name of DCIM fs.
* @quirk - maske to read numerik part.
* @tag - tag provided directory name.
*/
struct pattern_db {
const char *pattern;
const int vendor;
const int quirk;
const int tag;
} pattern_db[] = {
/* nicon */
{"xxxNIKON", NICON, Q_THIRST_3, VEND_ID},
{"xxxP_xxx", NICON, Q_THIRST_3, GR_PANR},
{"xxxINTVL", NICON, Q_THIRST_3, GR_INTV},
/* WAV in sound folder - voice record */
{"xxxSOUND", NICON, Q_THIRST_3, GR_NONE},
/* JPG, AVI, WAV(comment)to in base */
{"DSCNxxxx", NICON, Q_LAST_4, GR_NONE},
/* same exif time to base. */
{"SSCNxxxx", NICON, Q_LAST_4, REL_MOD},
{"FSCNxxxx", NICON, Q_LAST_4, REL_MOD},
/* canon */
{"xxxCANON", CANON, Q_THIRST_3, VEND_ID},
{"CANONMSC", CANON, 0, GR_NONE},
{"IMG_xxxx", CANON, Q_LAST_4, GR_NONE}, /* JPG */
{"CRW_xxxx", CANON, Q_LAST_4, GR_NONE}, /* CRW */
/* JPG, b = A ertse pan grup, B, C */
{"STx_xxxx", CANON, Q_LAST_4, GR_PANR},
/* AVI or THM */
{"MVI_xxxx", CANON, Q_LAST_4, GR_NONE},
/* WAV , xxxx same as jpg to com */
{"SND_xxxx", CANON, Q_LAST_4, GR_NONE},
{"xxx_FUJI", FUIJI, Q_THIRST_3, VEND_ID},
/* JPG, RAF (raw), AVI, WAV (WAV with same xxxx as JPG is comment) */
{"DSCFxxxx", FUIJI, Q_LAST_4, GR_NONE},
{"xxx_PANA", PANAS, Q_THIRST_3, VEND_ID},
/* JPG, MOV (vid), MOV aud comment xxx same for JPG */
{"Pxxxxxxx", PANAS, Q_LAST_4, GR_NONE}
};
#define sizeof_pattern_db (sizeof(pattern_db))/sizeof(struct pattern_db)
/**
* compare_pattern - compare function needed to sort and search in pattern_db
*/
int compare_pattern (const void *c1, const void *c2)
{
struct pattern_db *info1 = (struct pattern_db *) c1;
struct pattern_db *info2 = (struct pattern_db *) c2;
return strcasecmp(info1->pattern, info2->pattern);
}
/**
* struct current - structure for temporary storage.
* @dcim_1 - flag we set if we found DCIM folder.
* @l2_tag - information provided on second level of DCIM system.
* @l3_tag - information provided on third level of DCIM system.
* @number - number we get from file name.
* @vendor - vendor name of fs.
* @level - current directory level we in.
* @bufnum - number of buffer array.
* @file_type - file type we currently process.
*/
struct current {
int dcim_1;
int l2_tag;
int l3_tag;
int number;
int vendor;
int level;
int bufnum;
int file_type;
} current;
/**
* match_pattern - function to match pattern of DCIM fs.
*/
static int match_pattern(const char *name, int tflag, struct FTW *ftwbuf)
{
char patt_tmp[8];
int i = 0;
char val[5];
struct pattern_db key, *res;
/* convert all digits to x, so we can match this pattern */
while (i < 9) {
if ((name[i] >= '0') && (name[i] <= '9')) {
patt_tmp[i] = 'x';
} else
patt_tmp[i] = name[i];
/* exception for canon, it use letter for panorama group */
if ((i == 2) && (ftwbuf->level == 3) &&
(current.vendor == CANON) &&
(name[0] == 'S' && name[1] == 'T'))
patt_tmp[i] = 'x';
/* if we parse filenames we have patt_tmp[8] == '.',
* so let us make sure it is '\0' */
if (i == 8)
patt_tmp[i] = '\0';
i += 1;
}
/* search for pattern */
key.pattern = patt_tmp;
res = bsearch(&key, pattern_db, sizeof_pattern_db,
sizeof(struct pattern_db), compare_pattern);
/* stop here if didn't found */
if (res == NULL)
return NOT_MATCH;
/* after we know what we have, we can use quirk for numerik part */
if (res->quirk == Q_THIRST_3)
sscanf(name, "%3s", val);
else if (res->quirk == Q_LAST_4)
sscanf(name, "%*4c%4s", val);
current.number = atoi(val);
/* not all pattern provide us vendor id, just test to be sure */
if (res->tag == VEND_ID)
current.vendor = res->vendor;
/* we can set l2_tag now, we get here only on level 2
* and we will use this info on level 3 too */
if (ftwbuf->level == 2)
current.l2_tag = res->tag;
else if (ftwbuf->level == 3) {
current.l3_tag = res->tag;
/* if panorama or interval photo,
* apply level 2 tag to level 3 */
if (((current.l2_tag == GR_PANR) ||
(current.l2_tag == GR_INTV)) &&
(current.l3_tag == GR_NONE))
current.l3_tag = current.l2_tag;
}
return MATCH;
}
/**
* check_file_ext - function to check file extention, and file type.
*/
static int check_file_ext(const char *file_name)
{
char *val;
struct extinfo key, *res;
/* first of all find point in file name */
val = strrchr(file_name, '.');
/* if there is no point, drop it */
if (val == NULL)
return NOT_MATCH;
/* map pinter to the next char after point */
++val;
key.ext = val;
/* search for file extension in extinfo database */
res = bsearch(&key, extinfo, sizeof_extinfo,
sizeof(struct extinfo), compare_ext);
if (res == NULL)
return NOT_MATCH;
/* we may need file type info later */
current.file_type = res->file_type;
return MATCH;
}
/**
* direct_out - push out directly files we do not care of but are images.
*/
static int direct_out(const char *fpath)
{
printf("direct: %s\n", fpath);
return 0;
}
/**
* buff_trigger - sort, parse and push out content of file buffer.
* in file buffer are only files we can match.
*/
static int buff_trigger(void)
{
if (current.bufnum != 0) {
int i = 0;
printf("drop old and start new\n");
/* sort filebuffer by file numbers */
qsort(filebuffer, current.bufnum,
sizeof(struct filebuffer), compare_int);
while (i < current.bufnum) {
/* push out sortet file */
printf("buf: %s, %i, %i\n",
filebuffer[i].file, filebuffer[i].tag,
filebuffer[i].number);
/* check this file and file before have same name.
* Only audio comments have same file names */
if ((filebuffer[i].number == filebuffer[i-1].number) &&
((filebuffer[i-1].file_type == JPG_IMG) ||
(filebuffer[i-1].file_type == RAW_IMG)) &&
((filebuffer[i].file_type == AUDIO) ||
(filebuffer[i].file_type == VIDEO))) {
printf("tadaaaaa thi is audio comment\n");
}
i += 1;
}
/* reset buffer counter */
current.bufnum = 0;
}
return 0;
}
/**
* add_to_buf - add matched files to buffer.
*/
static int add_to_buf(const char *fpath)
{
const char *ptr;
/* DCIM structure is static
* DCIM/101CANON/STA_1256.JPG = 26 char.
* we remove here prefix and leave only interesting part */
ptr = fpath + (strlen(fpath) - 26);
/* copy content to the buffer */
strcpy(filebuffer[current.bufnum].file, ptr);
filebuffer[current.bufnum].number = current.number;
filebuffer[current.bufnum].tag = current.l3_tag;
filebuffer[current.bufnum].file_type = current.file_type;
/* update buffer counter */
current.bufnum += 1;
return 0;
}
/**
* belong_to_dcim - function to check if processed names belong to
* DCIM name space.
*/
static int belong_to_dcim(const char *fpath, int tflag, struct FTW *ftwbuf)
{
/* check if there is a struction we know about */
if ((ftwbuf->level == 1)) {
/* we know about DCIM and MISC,
* if there are images outside of this,
* we still can copy them, but we do not need to
* tag it.
*/
if ((tflag == FTW_D) &&
(!strcmp(fpath + ftwbuf->base, "DCIM"))) {
current.dcim_1 = D_DCIM;
/* reset l2_tag we update dcim_1 */
current.l2_tag = D_UNCN;
return MATCH;
} else if ((tflag == FTW_D) &&
(!strcmp(fpath + ftwbuf->base, "MISC"))) {
current.dcim_1 = D_MISC;
current.l2_tag = D_UNCN;
return MATCH;
} else {
current.dcim_1 = D_UNCN;
current.l2_tag = D_UNCN;
return NOT_MATCH;
}
} else if (ftwbuf->level == 2) {
/* on this level should be only dirs,
* do not bother to match it */
if ((tflag == FTW_D) &&
(current.dcim_1 == D_DCIM) &&
(strlen(fpath + ftwbuf->base) == 8)) {
if (match_pattern(fpath + ftwbuf->base,
tflag, ftwbuf) == MATCH)
return MATCH;
else
current.l2_tag = D_UNCN;
return NOT_MATCH;
} else {
current.l2_tag = D_UNCN;
return NOT_MATCH;
}
} else if (ftwbuf->level == 3) {
if ((tflag == FTW_F) &&
/* extra check dir we currently in */
(current.l2_tag != D_UNCN) &&
(strlen(fpath + ftwbuf->base) == 12)) {
if (match_pattern(fpath + ftwbuf->base,
tflag, ftwbuf) == MATCH)
return MATCH;
}
}
return NOT_MATCH;
}
static int prezess_file(const char *fpath, const struct stat *sb,
int tflag, struct FTW *ftwbuf)
{
int err;
/* drop files which extensions we do not know */
if ((tflag == FTW_F) &&
(check_file_ext(fpath + ftwbuf->base) == NOT_MATCH))
return 0;
/* check if we are in DCIM structure */
err = belong_to_dcim(fpath, tflag, ftwbuf);
/* trigger buffer if we in a new directory */
if (current.level != ftwbuf->level)
buff_trigger();
current.level = ftwbuf->level;
/* now we need only files */
if (tflag != FTW_F)
return 0;
/* patterns we can match got to the buffer, other image files
* go direct out. We need buffer here to sort things before we
* can put it together */
if (err == MATCH)
add_to_buf(fpath);
else
direct_out(fpath);
return 0;
}
int main(int argc, char *argv[])
{
current.bufnum = 0;
/* create our tmp storage */
struct current *curr_pnt;
curr_pnt = ¤t;
/* sort databases */
qsort(extinfo, sizeof_extinfo,
sizeof(struct extinfo), compare_ext);
qsort(pattern_db, sizeof_pattern_db,
sizeof(struct pattern_db), compare_pattern);
/* read directory tree */
if (nftw((argc < 2) ? "." : argv[1], prezess_file, 20,
FTW_PHYS | FTW_MOUNT) == -1) {
perror("nftw");
exit(EXIT_FAILURE);
}
/* trigger the buffer if we forgot some thing in there */
if (current.bufnum != 0)
buff_trigger();
exit(EXIT_SUCCESS);
}