长期以来相片管理都是困扰个人问题。git
现有的照片管理系统基本上都是基于数据库技术的,用好它的前提是,首先你得付出管理精力,好比给照片分类,分级,添加注释,等等。更专业的程序还包括编辑功能。这大约是专业摄影师才应该作的。我算不上摄影爱好者,个人相片大部分都是居家生活照,其中绝大部分是给孩子照的。在这种状况下,使用复杂的图片管理系统有些得不偿失。正则表达式
记得之前用 Ubuntu 的时候,使用过 Gnome 内置的一个相片管理程序,叫啥名已经忘记了。它导入照片时进能够识别 Exif 中的拍照时间,而且按照时间存放到文件系统里。好比一张照片拍照时间是 2000:01:01, 它就给它放到 'prefix/2000/01/01' 目录里去。我就以为这功能挺好用。这甚至意味着有序的管理方式,即使没有任何的照片管理系统,我也能本身有序地组织照片。以致于,不用 Gnome 的这些年,我一直坚持以这种方式手工管理照片。数据库
好吧,最近我忍受不了了。手机里的图照片越攒越多,由于我不知道哪一部分已经导入到电脑里,哪一些尚未导入。相机里的图片也越攒越多,除了面临和手机图片同样的困难,更大的困难是,相机图片在文件名里不包含拍照日期,不能经过文件名简单地判断该放在哪里,必须得用某个图片浏览器查看每一张照片的 Exif 来判断。这是使人恐惧的工做。最近看了一个 Python小脚本,并由此入了 Exif 的坑。浏览器
我决定本身用 Racket 写一个 Exif 库,除了供个人导入照片的小脚本用,之后也有可能会用到别的地方。app
Exif 是灵活的,也是复杂的,每一个 JPG 文件里有可能会有 Exif 段,也可能会有 JFIF 段,有可能二者都有。在 Exif 里,使用 TIFF 的格式来存储信息,而 TIFF 也很复杂,还分大小端。TIFF 经过 IFD 来存放不一样类别的信息,IFD 有多有少,IFD 里面还有可能有子 IFD,一样有多有少。不一样的 IFD 里有可能有相同的 TAG,而这些相同的 TAG 的值却不必定相同。查询某个 TAG 的值时应该到哪一个 IFD 里去查呢?IFD 的数目不固定,怎么给每一个 IFD 一个惟一的 ID 呢?用线性结构来组织彷佛不合适,用树形结构来组织也彷佛不太合适,用哈希表来存也不合适。我算是栽坑里了。代码只不过 300 多行,然却屡次推翻重写,没一次满意的。导入照片的小脚本早就写好能跑了,可是动不动就崩溃,缘由是它所使用的 Exif 库处处都是 BUG。其间什么奇葩问题都前见过了。好比,TIFF 里,有理数的分母可能会是 0!另外,Racket 的错误提示太弱了,简直连 gcc 都不如。ui
最后,我放弃了宏伟的蓝图,放弃了写出一个能正确读写 Exif 的库的企图。我不就想从照片里读个日期吗?费那劲干吗!直接正则表达式就搞定了嘛。code
#!/usr/bin/env racket #lang racket/base (require racket/path) (require racket/file) (require racket/string) (define err-log "impto-error.log") (define impto-log "impto.log") (define (error-log fmt) (display fmt) (display-to-file fmt err-log #:mode 'text #:exists 'append)) (define (add-log fmt) (display fmt) (display-to-file fmt impto-log #:mode 'text #:exists 'append)) (define (get-u16 in) (let ((h (read-byte in))) (+ (* h 256) (read-byte in)))) (define (date-from-exif re path) (define (find-exif in) (let ((marker (get-u16 in))) (let ((size (get-u16 in))) (if (not (<= #xffe0 marker #xffef)) #f (if (= marker #xffe1) (let ((tiff (read-bytes (- size 8) in))) (let ((m (regexp-match re tiff))) (if m (map bytes->string/latin-1 m) ;; 这里必要吗? #f))) (begin (read-bytes (- size 2) in) (find-exif in))))))) (call-with-input-file path (lambda (in) (let ((header (get-u16 in))) (if (not (= header #xffd8)) #f (find-exif in)))))) (define (date-from-file-name re path) (regexp-match re (file-name-from-path path))) ;; parsing date from Exif or file name, and convert it to path ;; "2017:02:15" -> "2017/02/15" ;; "20170215 -> "2017/02/15" (define (get-date path) (let ((re (pregexp "(19[89]\\d|20[012]\\d)\\D?(0[1-9]|1[0-2])\\D?(0[1-9]|[12]\\d|3[01])"))) (let ((ret (or (date-from-exif re path) (date-from-file-name re path)))) (if ret (string-join (cdr ret) "/") #f)))) (define (import-img img dest) (let ((date-string (get-date img))) (if date-string (let* ((sub-dir date-string) (dir-tree (string-append dest "/" sub-dir)) (new-file (string-append dir-tree "/" (path->string (file-name-from-path img))))) (if (file-exists? new-file) (add-log (format "[Warning]~s already exists\n" new-file)) (begin (make-directory* dir-tree) (copy-file img new-file) (add-log (format "~a -> ~a ok\n" img (path-only new-file)))))) (error-log (format "[Failed]unknow date: ~a\n" img))))) (define (jpeg? path) (let ((ext-name (path-get-extension path))) (member ext-name '(#".jpg" #".jpeg" #".JPG" #".JPEG")))) (define (worker path type dest) (when (and (eq? type 'file) (jpeg? path)) (import-img path dest)) dest) (define (go src dest) (fold-files worker dest src)) (define (usage) (printf "\n This program imports digital photos from 'source dir' to 'dest dir' and stores them in a directory tree organized in the form 'yyyy/mm/dd', the date-time information is read from the photo's buile-in Exif or file name. This program will recursively copy each JPEG file under the 'source dir' and all subdirectories. The 'source dir' parameter is optional, in which case program will look for JPEG images in the 'current working directory'\n If the attempt to get date info from Exif or file name fails, the file will be ignored. All failed log can be found at 'impto-error.log' in current directory. and All successful logs can be found at 'impto.log' too. Only files with '.jpg' '.jpeg' '.JPG' '.JPEG' extension will be copied. Usage: ~a [source dir] <dest dir>\n\n" (file-name-from-path (find-system-path 'run-file)))) (let ((paths (vector->list (current-command-line-arguments)))) (if (null? paths) (usage) (let ((src (if (= (length paths) 1) (current-directory) (car paths))) (dest (if (= (length paths) 1) (car paths) (cadr paths)))) (cond ((not (directory-exists? src)) (printf "directory ~a does not exists\n" src)) ((not (directory-exists? dest)) (printf "directory ~a does not exists\n" dest)) (else (when (file-exists? err-log) (delete-file err-log)) (when (file-exists? impto-log) (delete-file impto-log)) (go src dest))))))
小脚本的健壮性获得了革命性的改善。regexp
使用中发现,个人照片有太多的 Exif 是损坏的。缘由是我曾经使用过的某个照片管理程序,自觉得是地认为本身能处理每张照片的 Exif ,而且向用户提供了编辑的功能。它编辑的结果就是把 Exif 搞坏。等我发现时悔之晚矣。之前没注意这个问题,如今试图用上面的小脚本从新整理几十个 G 的照片库时发现 Exif 缺失的照片有点多。有些东西失去了,一生都找不回来了。要珍惜啊!慎用 Exif 编辑功能。那是元数据,就应该是只读的。甚至是能做为呈堂证供的东西,怎么能随便编辑呢?orm
问题终究形成了,幸亏我一直有序地存储照片,哪怕没有 Exif ,我也能知道每一张照片是哪一天拍的。有没有一个办法能重建缺失的 Exif 结构呢?虽然数据是找不回来了,但至少能保证它结构是完整的。由此我想到了移花接木法:找一张同一型号的相机拍摄的照片,把 Exif dump 出来,而后写到被损坏的照片的对应位置。在嫁接的同时,顺便把日期改一下,这样就没必要要面对精准地编辑每个条目的复杂性,一样能够正则表达式搞定。说干就干:图片
#!/usr/bin/env racket #lang racket/base (require racket/path) (require racket/port) (define SOS #xffda) (define EXIF #xffe1) (define (get-u16 in) (let ((h (read-byte in))) (+ (* h 256) (read-byte in)))) (define (find-range path mk) (define (find-marker in) (let ((marker (get-u16 in))) (let ((size (get-u16 in))) (cond ((= marker mk) (let* ((start (- (file-position in) 2)) (end (+ start size))) (cons start end))) (else (read-bytes (- size 2) in) (find-marker in)))))) (call-with-input-file path (lambda (in) (let ((header (get-u16 in))) (if (not (= header #xffd8)) #f (find-marker in)))))) (define (fix-exif src dest fixed date) (let ((exif-of-src (find-range src EXIF)) (exif-of-dest (find-range dest EXIF))) (call-with-output-file fixed (lambda (fo) (call-with-input-file dest (lambda (di) (call-with-input-file src (lambda (si) (let ((meta (update (read-bytes (cdr exif-of-src) si) date))) (read-bytes (cdr exif-of-dest) di) (let ((data (port->bytes di))) (write-bytes meta fo) (write-bytes data fo)))))))) #:exists 'replace))) (define (update bv date-str) (let ((re (pregexp "[12]\\d\\d\\d:[01]\\d:[0-3]\\d"))) (regexp-replace* re bv date-str))) (let ((args (vector->list (current-command-line-arguments)))) (if (< (length args) 4) (printf "Usage: ~a <template> <dest> <new-file> <date>\n" (file-name-from-path (find-system-path 'run-file))) (fix-exif (car args) (cadr args) (caddr args) (cadddr args))))
初步试了一下,能够正确地拼接。可是比较犹豫要不要用。缘由是,这让我老是想处处女膜修补。文件结构当然是修复了,但里面的数据除了日期之外几乎全是假的了。