/* * 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); }