長期以來相片管理都是困擾個人問題。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))))
初步試了一下,能夠正確地拼接。可是比較猶豫要不要用。緣由是,這讓我老是想處處女膜修補。文件結構當然是修復了,但裏面的數據除了日期之外幾乎全是假的了。