;;; my-exif.el --- Private fix to exif.el -*- lexical-binding: t; -*- ;; Copyright (C) 2019-2021 Free Software Foundation, Inc. ;; Copyright (C) 2021 AKIYAMA Kouhei ;; Original Author: Lars Magne Ingebrigtsen ;; Author: AKIYAMA Kouhei ;; Keywords: images ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; 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. If not, see . ;;; Commentary: ;; I can't write a fix request in English, so I am making fix privately. ;; Changes: ;; - [Bug] Skip APP1 segments that are not Exif (Such as XMP Meta Info) ;; - [Bug] Fix reading big-endian offset ;; => already fixed https://git.savannah.gnu.org/cgit/emacs.git/commit/lisp/image/exif.el?id=72bcc6f988350329f3c0eb2f86af17f3ed97cf40 ;; - [Bug] Fix reading small tag value (< 4-bytes) in big-endian ;; - [Add] Add support for reading sub Exif info and GPS info ;; - [Add] Add support for reading multiple components in a tag (Such as latitude, longitude) ;; - [Add] Add tag names I often see ;;; Code: (require 'exif) (defun my-exif-prop-find (exif tag) (seq-find (lambda (e) (equal (plist-get e :tag) tag)) exif)) (defun my-exif-prop-value (exif tag &optional key) (plist-get (my-exif-prop-find exif tag) (or key :value))) (defun my-exif-3rationals-to-float (v neg) (let ((value (+ (/ (caar v) (* 1.0 (cdar v))) (/ (caadr v) (* 60.0 (cdadr v))) (/ (caaddr v) (* 3600.0 (cdaddr v)))))) (if neg (- value) value))) ;; (defun my-exif-parse-file (file) (cl-letf (((symbol-function 'exif--parse-jpeg) #'my-exif--find-app1-exif-in-jpeg) ;; [BugFix] Support APP1 segments that are not Exif (Such as XMP) ((symbol-function 'exif--parse-exif-chunk) #'my-exif--parse-exif-chunk)) (exif-parse-file file))) (defun my-exif--find-app1-exif-in-jpeg () (unless (= (exif--read-number-be 2) #xffd8) ; SOI (start of image) (signal 'exif-error "Not a valid JPEG file")) (cl-loop for segment = (exif--read-number-be 2) for size = (exif--read-number-be 2) ;; Stop parsing when we get to SOS (start of stream); ;; this is when the image itself starts, and there will ;; be no more chunks of interest after that. while (not (= segment #xffda)) ;;FFDA=SOS do (if (and (= segment #xffe1) ;;FFE1=APP1 (equal (save-excursion (exif--read-chunk 6)) (string ?E ?x ?i ?f ?\0 ?\0))) ;; Return APP1 Exif ;; (Matched to return value of exif--parse-jpeg) (cl-return (list (cons segment (exif--read-chunk (- size 2))))) ;; Skip segment (forward-char (- size 2))))) (defun my-exif--parse-exif-chunk (data) (with-temp-buffer (set-buffer-multibyte nil) (insert data) (goto-char (point-min)) ;; The Exif data is in the APP1 JPEG chunk and starts with ;; "Exif\0\0". (unless (equal (exif--read-chunk 6) (string ?E ?x ?i ?f ?\0 ?\0)) (signal 'exif-error "Not a valid Exif chunk")) (delete-region (point-min) (point)) (let* ((endian-marker (exif--read-chunk 2)) (le (cond ;; "Motorola" is big-endian. ((equal endian-marker "MM") nil) ;; "Intel" is little-endian. ((equal endian-marker "II") t) (t (signal 'exif-error (format "Invalid endian-ness %s" endian-marker)))))) ;; Another magical number. (unless (= (exif--read-number 2 le) #x002a) (signal 'exif-error "Invalid TIFF header length")) (let ((offset (exif--read-number 4 le))) ;; <==== [BugFix] 2 => 4 ==== ;; Jump to where the IFD (directory) starts and parse it. (when (> (1+ offset) (point-max)) (signal 'exif-error "Invalid IFD (directory) offset")) (goto-char (1+ offset)) (my-exif--parse-directory le))))) (defun my-exif--parse-directory (le) (let ((dir (cl-loop repeat (exif--read-number 2 le) collect (my-exif--read-value le)))) (let ((next (exif--read-number 4 le))) (if (> next 0) ;; There's more than one directory; if so, jump to it and ;; keep parsing. (progn (when (> (1+ next) (point-max)) (signal 'exif-error "Invalid IFD (directory) next-offset")) (goto-char (1+ next)) (nconc dir (my-exif--parse-directory le))) ;; We've reached the end of the directories. dir)))) (defvar my-exif-pointer-tags '(34665 ;;Exif IFD Pointer 34853)) ;;GPS Info IFD Pointer (defun my-exif--read-value (le) ;; [Add] Support multiple components ;; [BugFix] Reading small value length < 4 and big-endian (let* ((tag (exif--read-number 2 le)) (format (exif--read-number 2 le)) (field-format (my-exif--field-format format)) (type (car field-format)) (bytes/component (cdr field-format)) (num-components (exif--read-number 4 le)) (num-bytes (* num-components bytes/component)) (components (save-excursion (when (> num-bytes 4) ;; If the length of the data is ;; more than 4 bytes, then it's ;; actually stored after this ;; directory, and the value ;; here is just the offset to ;; use to find the data. (let ((offset (exif--read-number 4 le))) (when (> (+ (1+ offset) num-bytes) (point-max)) (signal 'exif-error "Premature end of file")) (goto-char (1+ offset)))) (pcase type ;;@todo support signed ;;@todo support float and double ((or 'byte 'short 'long 's-byte 's-short 's-long) (cl-loop repeat num-components collect (exif--read-number bytes/component le))) ((or 'rational 's-rational) (cl-loop repeat num-components collect (cons (exif--read-number 4 le) (exif--read-number 4 le)))) ('ascii (buffer-substring (point) (+ (point) num-bytes -1)));;Chop off trailing zero byte. ('undefined (buffer-substring (point) (+ (point) num-bytes))) (_ nil)))) (value (cond ((eq type 'ascii) components) ((eq type 'undefined) components) ((= num-components 1) (car components)) (t components))) (pointee (when (and (memq tag my-exif-pointer-tags) (integerp value)) (save-excursion (goto-char (1+ value)) (my-exif--parse-directory le))))) ;; Skip Value (4 bytes) (goto-char (+ (point) 4)) (nconc (list :tag tag :tag-name (cadr (assq tag my-exif-tag-alist)) :format format :format-type (car field-format) :value value) (when pointee (list :pointee pointee))))) (defun my-exif--field-format (number) (cl-case number (1 (cons 'byte 1)) (2 (cons 'ascii 1)) (3 (cons 'short 2)) (4 (cons 'long 4)) (5 (cons 'rational 8)) (6 (cons 's-byte 1)) (7 (cons 'undefined 1)) (8 (cons 's-short 2)) (9 (cons 's-long 4)) (10 (cons 's-rational 8)) (11 (cons 'float 4)) (12 (cons 'double 8)) (otherwise (cons 'unknown 1)))) (defvar my-exif-tag-alist '((11 processing-software) (256 image-width) (257 image-length) (258 bits-per-sample) (259 compression) (262 photometric-interpretation) (270 image-description) (271 make) (272 model) (273 strip-offsets) (274 orientation) (277 samples-per-pixel) (282 x-resolution) (283 y-resolution) (296 resolution-unit) (305 software) (306 date-time) (315 artist) (513 jpeg-interchange-format) (514 jpeg-interchange-format-length) (531 ycbcr-positioning) (33434 exposure-time) (33437 f-number) (34665 exif-ifd-pointer) (34850 exposure-program) (34853 gps-info-ifd-pointer) (34855 iso-speed-ratings) (36864 exif-version) (36867 date-time-original) (36868 date-time-digitized) (37121 components-configuration) (37377 shutter-speed-value) (37378 aperture-value) (37379 brightness-value) (37380 exposure-bias-value) (37381 max-aperture-value) (37382 subject-distance) (37383 metering-mode) (37384 light-source) (37385 flash) (37386 focal-length) (37520 sub-sec-time) (37521 sub-sec-time-original) (37522 sub-sec-time-digitized) (40960 flashpix-version) (40961 color-space) (40962 pixel-x-dimension) (40963 pixel-y-dimension) (40964 related-sound-file) (40965 interoperability-tag) (41495 sensing-method) (41729 scene-type) (41985 custom-rendered) (41986 exposure-mode) (41987 white-balance) (41988 digital-zoom-ratio) (41989 focal-length-in-35mm-film) (41990 scene-capture-type) (41991 gain-control) (41992 contrast) (41993 saturation) (41994 sharpness) (41995 device-setting-description) (41996 subject-distance-range)) "Alist of tag values and their names.") ;; (defun my-exif-encode-date-time (exif-date-time-str) (when exif-date-time-str (encode-time (nconc (nreverse (mapcar #'string-to-number (split-string exif-date-time-str "[ :]"))) (list nil nil nil))))) (defun my-exif-date-time-original (exif) (when-let ((exif-ifd (my-exif-prop-value exif 34665 :pointee)) (date-time-original (my-exif-prop-value exif-ifd 36867))) (my-exif-encode-date-time date-time-original))) (defun my-exif-latlng (exif) (when-let ((gps (my-exif-prop-value exif 34853 :pointee)) (lat-ref (my-exif-prop-value gps 1)) (lat (my-exif-prop-value gps 2)) (lng-ref (my-exif-prop-value gps 3)) (lng (my-exif-prop-value gps 4))) (cons (my-exif-3rationals-to-float lat (string= lat-ref "S")) (my-exif-3rationals-to-float lng (string= lng-ref "W"))))) (provide 'my-exif)