From 535b65875e7e47e1fd6bec1753f687592ae600b8 Mon Sep 17 00:00:00 2001 From: Lars Ingebrigtsen Date: Sat, 21 Sep 2019 18:13:05 +0200 Subject: [PATCH] Add an Exif parsing library * lisp/image/exif.el: New file (bug#23070). * test/lisp/image/exif-tests.el: Add some basic tests. --- etc/NEWS | 4 + lisp/image/exif.el | 224 ++++++++++++++++++++++++++++++++++ test/data/image/black.jpg | Bin 0 -> 52456 bytes test/lisp/image/exif-tests.el | 44 +++++++ 4 files changed, 272 insertions(+) create mode 100644 lisp/image/exif.el create mode 100644 test/data/image/black.jpg create mode 100644 test/lisp/image/exif-tests.el diff --git a/etc/NEWS b/etc/NEWS index 238ea840dde..b120b5a817c 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2567,6 +2567,10 @@ left to higher-level functions. ** Image mode +*** An Exif library has been added that can parse JPEG files and +output data about creation times and orientation and the like. +'exif-parse' is the main interface function. + *** 'image-mode' started using ImageMagick by default for all images some years back. It now respects 'imagemagick-types-inhibit' as a way to disable that. diff --git a/lisp/image/exif.el b/lisp/image/exif.el new file mode 100644 index 00000000000..2ec256bb2ee --- /dev/null +++ b/lisp/image/exif.el @@ -0,0 +1,224 @@ +;;; exif.el --- parsing Exif data in JPEG images -*- lexical-binding: t -*- + +;; Copyright (C) 2019 Free Software Foundation, Inc. + +;; Author: Lars Magne Ingebrigtsen +;; Keywords: images + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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. + +;; GNU Emacs 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 GNU Emacs. If not, see . + +;;; Commentary: + +;; Specification at: + +;; https://www.media.mit.edu/pia/Research/deepview/exif.html +;; but it's kinda er not very easy to read. + +;; The JPEG format is: +;; +;; FFD8 and then any number of chunks on the format: FFxx SSSS ..., +;; where FFxx is the ID, and SSSS is the length of the chunk plus 2. +;; When you get to ID FFDA, the image itself is over and you can stop +;; parsing. +;; +;; The Exif data is in the TIFF format. It starts off with the six +;; bytes "Exif^0^0". +;; +;; Then either "II" or "MM", where "II" means little-endian and "MM" +;; means big-endian. All subsequent numbers should be read in +;; according to this. +;; +;; Next follows two bytes that should always represent 0x2a, and then +;; four bytes that's the offset to where the IFD "image file +;; directory" starts. (It's an offset from the start of this chunk; +;; i.e., where "II"/"MM" is; all offsets in the TIFF format are from +;; this point.) +;; +;; The IFD starts with two bytes that says how many entries there are +;; in the directory, and then that number of entries follows, and then +;; an offset to the next IFD. + +;; Usage: (exif-parse "test.jpg") => +;; ((:tag 274 :tag-name orientation :format 3 :format-type short :value 1) +;; (:tag 282 :tag-name x-resolution :format 5 :format-type rational :value +;; (180 . 1)) +;; (:tag 306 :tag-name date-time :format 2 :format-type ascii +;; :value "2019:09:21 16:22:13") +;; ...) + +;;; Code: + +(require 'cl-lib) + +(defvar exif-tag-alist + '((11 processing-software) + (271 make) + (272 model) + (274 orientation) + (282 x-resolution) + (283 y-resolution) + (296 resolution-unit) + (305 software) + (306 date-time)) + "Alist of tag values and their names.") + +(defun exif-parse (file) + "Parse FILE (a JPEG file) and return the Exif data, if any. +The return value is a list of Exif items." + (when-let ((app1 (cdr (assq #xffe1 (exif--parse-jpeg file))))) + (exif--parse-exif-chunk app1))) + +(defun exif--parse-jpeg (file) + (with-temp-buffer + (set-buffer-multibyte nil) + (insert-file-contents-literally file) + (unless (= (exif--read-number-be 2) #xffd8) ; SOI (start of image) + (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)) + collect (cons segment (exif--read-chunk (- size 2)))))) + +(defun 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)) + (error "Not a valid Exif chunk")) + (delete-region (point-min) (point)) + (let* ((endian-marker (exif--read-chunk 2)) + (le (cond + ;; "Morotola" is big-endian. + ((equal endian-marker "MM") + nil) + ;; "Intel" is little-endian. + ((equal endian-marker "II") + t) + (t + (error "Invalid endian-ness %s" endian-marker))))) + ;; Another magical number. + (unless (= (exif--read-number 2 le) #x002a) + (error "Invalid TIFF header length")) + (let ((offset (exif--read-number 2 le))) + ;; Jump to where the IFD (directory) starts and parse it. + (goto-char (1+ offset)) + (exif--parse-directory le))))) + +(defun 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)) + (otherwise (cons 'unknown 1)))) + +(defun exif--parse-directory (le) + (let ((dir + (cl-loop repeat (exif--read-number 2 le) + for tag = (exif--read-number 2 le) + for format = (exif--read-number 2 le) + for field-format = (exif--field-format format) + ;; The actual length is the number in this field + ;; times the "inherent" length of the field format + ;; (i.e., "long integer" (4 bytes) or "ascii" (1 + ;; byte). + for length = (* (exif--read-number 4 le) + (cdr field-format)) + for value = (exif--read-number 4 le) + collect (list :tag tag + :tag-name (cadr (assq tag exif-tag-alist)) + :format format + :format-type (car field-format) + :value (exif--process-value + (if (> length 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. + (buffer-substring + (1+ value) (+ (1+ value) length)) + ;; The value is stored + ;; directly in the directory. + value) + (car field-format) + 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 + (goto-char (1+ next)) + (append dir (exif--parse-directory le))) + ;; We've reached the end of the directories. + dir)))) + +(defun exif--process-value (value type le) + "Do type-based post-processing of the value." + (cl-case type + ;; Chop off trailing zero byte. + ('ascii (substring value 0 (1- (length value)))) + ('rational (with-temp-buffer + (set-buffer-multibyte nil) + (insert value) + (goto-char (point-min)) + (cons (exif--read-number 4 le) + (exif--read-number 4 le)))) + (otherwise value))) + +(defun exif--read-chunk (bytes) + "Return BYTES octets from the buffer and advance point that much." + (prog1 + (buffer-substring (point) (+ (point) bytes)) + (forward-char bytes))) + +(defun exif--read-number-be (bytes) + "Read BYTES octets from the buffer as a chunk of big-endian bytes. +Advance point to after the read bytes." + (let ((sum 0)) + (dotimes (_ bytes) + (setq sum (+ (* sum 256) (following-char))) + (forward-char 1)) + sum)) + +(defun exif--read-number-le (bytes) + "Read BYTES octets from the buffer as a chunk of low-endian bytes. +Advance point to after the read bytes." + (let ((sum 0)) + (dotimes (i bytes) + (setq sum (+ (* (following-char) (expt 256 i)) sum)) + (forward-char 1)) + sum)) + +(defun exif--read-number (bytes lower-endian) + "Read BYTES octets from the buffer with endianness determined by LOWER-ENDIAN. +Advance point to after the read bytes." + (if lower-endian + (exif--read-number-le bytes) + (exif--read-number-be bytes))) + +(provide 'exif) + +;;; exif.el ends here diff --git a/test/data/image/black.jpg b/test/data/image/black.jpg new file mode 100644 index 0000000000000000000000000000000000000000..be9af2a9a0537c492eff5cfb6f54477839eaa6e2 GIT binary patch literal 52456 zcmeHw4R}=5x$d{t?Aci}8IqmJFbt96n#m+gBH$1a1GRQf65<3w!;gs6>I4H$5E1YX zsnwNXh#{qPh-si4v@s&3)}u(NTC^Gv6)jb;XsPY-V1GQ=p4&@X`{z}1-<_GrV0n7a zJ@+}!bDw+n-g)=>X6^O+u5bP9wf9;XJT`coj2Vqhjl>wEJveFb-B-3vU){EZsHv%% z1QCs(V#di0=@GzyY}vvod6|P4ObcbqIb=Qy`J9}50CFVfhC`l|3+E8z8puAFZvmw| z8;<3s?OtstoHh9I@PPt447=X|Z@g4MCd|Ev?8lH5@=)rmR9UIC%=wE~O{|$1m6B?rlcr9KPMuaWNluzFt)^z$qzkBdRon7gn`Tk8 z=E5jZ5q$V@TB#v&P%EwlL$Z}`BG=H>;)AG=9>vu&B##)n7Lk=}0t2{!REsf%tE&Xk zIQS^enw#Ok`n?N*S|%HlHLpdq*6*)aK4JdQku`sPPS%9=|MN#BZB1phDXtOSpOc#* zhdNJd+_;>qKz}sPo-zdlrzp(}qK-^f%ND>w%gM_ivrSny44TI`W#dwon{M}zzH~x% zc4zG~kPEuA@mi%SaFakzj83O*#Ctp=XW4%7|h#R{X< zx(sECXf;Hu_MNWvM4RC9Gz%Q{)$(msVMos1%56cOn_~DFs^OE+cIE7?2*%{OIX5R4 z!d~Xt2N9A;K*L5tBXjm}Lg75Q0&Xm=SOd?Mx%5OQM{CFBxryXtD{WOsv1Jlk-s(Iz z=MCHA*7!X46Y|_l%(Fi~Pmbovlk()6oNPsR0a&ow;Vz_ea-N$D^W-Txd2o>EqG5X^ zwl*hQJWkEYR+!U<-9Y|qo_ujmwrcF>^6W3klRux6t#HgdIhH5a=VUA1hMc@&6w&lN zH;sAnrFn8ZPj1S|RyddC@jNq6o<+_fJ$V59T#j_F^dx8AU%h^{$zB1?6D(?k3-N0?Sc36huUw2xj#ITG$vjuVw{M90y>xb>}dog4a z<{{WG$;lpcNjK!=nUIsvupu3TyEfRzVQzr^(meYcbM{AJzbw!Grkwp4WNvw${mrmf z2&WbHEAs4D=Inn3`&D^d-I6EYnkV0uC*PhY-;pP;&MTKY^X%8;$#><+_vEEV<X1>9l&6OwRr;wDOyB@)|Tu-FbQG&679h+`ub+BTwF%ldZJ<6Li=z zXJQO;e@>nU`GGw5-^`QykY5S+yWr-*ocn7a@5poWP|m&;_TPg01k!jI^3FUr-_Dc2 zlP5o#C;u~KiyP~B9Fo-*EkiiFa%J880?`wB?sw{Zt(M+8EctA0B6=E)w5MFdrj!4LonnQ2*tGoFhs}V^z<&(`4f7eF-b{3Iu%RyO zJ~?#|On%(;tdkM6eU4y*>&0Cxd*0}4n1X`ln>1iF9?z(ycbJUG}5^Z>oUpNlEf~6elWQ5wYGejoXOF@Rrx z??W$8rhmsdR;N?da969puZ49gzC`cQChB-5LL4Fe1dr<9j7-xVd1@0vf z0>VILfsd*SA~dmJ8bt~+G#-coHMn;%Fd1kB<^v0X1waE(XPG)V@qAf)H!uTo8SEM% z$Mt{JE(cG|x<=O`?gfB}JlE;pDOe424NO1WUZY=F+KPLN01dyNhPg!NG!y1L;BC13 zIl_Gh?%V~5A|K!_xP+~dmoO^ul7JthKqFot#L2;{>MFTkJmo4Aj)H(7ZHySl#)@;; z63mPzfL36!(8v^zP@VWLHHfdVMiFCu;$QWC@%h5-;(XT*vAkdl+-(K60S^Efpd07` zdVwUc6d+*|7mRj7egrrmJ{UYC#?VW`px1x_k*0&9hxUsd^s3lKVG&}L!pkJm6+*fm zh4~oF$?zKis&Rij?oWgqgD;CFx<@Bl@?tFCeEkn0@w8qC8m-#~nLJF#T9Q0k~_aPTeQ81Odm zbKp35{{_nHSHOGV|AZj^p4(k;!mVk)0sh@R;QF2WFKmh+dD2}(f54>e2krv;v)f01 z#qSUC`y>25h2OghHElPr2Y3>A8rTaw3p@v$9BgcOjNXCXB+wEp0<1a26p#iwfKH$b z*Z^zVO8I5r_jbfSJJMKr=80m)~-2j|YKVb+TxYu-KtECj+p1gHj9!1Z{T6M-mD155^{0JXq0 z;9}qszyvsOInWHu0p_2d}P2tAg29 zUdTu*G)XUvkSGk0yU-v{p@)ia&j7rD5AXv4pbQvOC}}L*Nw^E)cNnNF9Q=F)xE5hl z7n+O57cQAUvGB@7sFKu2m_Ts z1gHka0~3KLPy<0D#PXbQ^ zdx2+x=KzJ$O#x}31Ly?0fDF(LYydU_JwPw81=tE~10Db{=9=3`gptOW;vaJkGXRVQ zG(ZOg;08QE5l{vgfEVxqegJuy>!!PlIKrD#e>vy`^MD5Efa!mb>in0n2LCQ^qyO!q zxPQ?t680f~F@^nJssti_KUMpyX*@8|e*s1Ptq84tmKTMFVTCIkUE#(nb^EXxuo=sjs{7}6>irP-BlsycC6~- z2SuA`+eK5zOLeh^rn&P4)qHYX{K>Hji?{-n0bI+*8MuxaHx{d^&K>^_y-9N7a(at} zCjRvlIm#!3a!18PgD*Md4}Zwhr$$wH`4XzVvx`--+B+X%vuN7ttt>To@#^FB9$kEA z67D~DXa68M%-c3lhR1H@yx`R2)jodzVD0T?i0iXg9z$Hy=9ql{;MC@8*@3~@S&IFQ zrp&bBnsV78#5=9&;Hhpttw}n2^7e*SwW{Mpi zti06Cu4j>%ZmbwM?}{n#b^aAr8ZNk8^5Y+mo_seyeyVKB625^hT3g8_yJ)S4?|{4p z@lI{`vo~>_2KS$B-^LzhvAZ`wzI08P4}5%4dI!JiRPEXgeD#3=J)&IDfUs@OckR0(ki}QH>wfyRj7tP$oPoDZ>(+J@yDRhfM!zDCP z=qmWqGQHq)Q$9a&`Xy!IDN#>hpzNF?&Fw9$C~=M`F4DcC$XQ${ykEY2WUzQtRF>CN zy3akQR#cY+e4c3O=y9J3MlPxyUo@s{r0x*se5NRIV*x+^{GiJ>@iV34%e}6EQCe7_ ziBe;PM=vhW3N(+)!MV76jZ@P-|zwP{jNfKXmS zlq*v?6vMYraTVh+uM}x*rThgY6^qs@2V=U&A1$fwUg}QHB)N{ac*xMWsi5@~NCeG? zQ>Ncck=IS8H`eh5qVr4@z+&HJM*Jvh3(ZliBc@DiV~PrY#49omql!wn-=W>uT7 zNoN_M%)mfhFcqhS!ODU@^2Ho6ZDuOi*ETXi%p2)RSM|EQF_qxv2A0NWbOt$uw$y0V zJt@-@^u~-%ql632x@GN+yMml`1h*w9*iXKYF1yXN$%&gXsHSUiFJa+Jz)Rst)h;8R zO898E+%XV_eY#3_rH&)-O;3h;8d6QECL`g+T3u?`E?qitsbNL5!snG^klyaN5-HLQ zO-S;V=%T~yHF+@8qVUCCQJBn>E5*3*nlw_Ujzs2NpLFj$)a;G*2gQ*^h6+4VwISY> zNbOTq2ZUlhW=GIxsNn7xH+-_g3@0tsrTD2m4Pxq2!dJVX{M?gOo|(#!#O57$&g!P2jWmNXh=#b!9ARM zTSBcWnUN_{)ZLabFE?qC4C8aWn4_8FTYE0jQ3Rdf1H7q+bR_nr24emuy$PSmnH^0H zEH%Q%{Bf`pn_f4Ia15_GM|lm&+1)TK`|MrI-nzrdk#k|$Tdu6%)*Z{tuEfO`p5*-G zj=@#utI$yrRW~I3HD#dy6(q@ z&ii4XhW2)0W&XX?iS^sNaqgr(+6>2}<0htvt*n}PeW^UNJ|HTEb-(wFH*#NSSLA}w9mF<=JGqS;8( zYPh%!tNOnT`7783B7}CU9y=l|LU@aDZN+sd_${E%z`hZC99%VoFJO0s>xP){VK0l% zQ;eG7>r#Zd0J6o8hsM%{REM1w=HrZG2L%&xe+{W$kCg33s=ot{dXT2&U|==UcL(hI z!P=9^^*M*cstgS_gNJ!`908dhqrwx(|00QrwSFEZ!cYhrsoBu+Bb>d-r26 zpFV{C3~YD7=L6v67R1qs5WWWH9>CphxL%3;eGzHC0dcQFI?~|iHpF!u_MeCWEy(+g zD52Z255o$iW*VJ~Jk13YsNC4&A%WB-;kN**)GNS3J@$sU3VhGPz3Z&)KcIypu@}d5 zYQmibz!fMDYhMW+`~>M@*na`Gb8&A0{Qsv8vy`nMNnYARhow(^-%I%jrH~O zB?7$}t;pGrFIJB(XS{_+s81$LnQG!KQW!J2gMK~kD~{~>G`&g(=?C;9?BH~W-eB3Y z70k4Oznj3~N}7N!=*$?tHtrrr_I#0EK?n!vW%&OIMq%0W2Kc-Y94&|MRcG-(ZQNQ% z_I!?BgWqS7hPRN0AG7Sa82)d7|7Gxf6a3?I?lX>GR}RO&A3hJ#b997$OmEX+mOb0x z(+c5c_&;0x8!JCiD{#Qb`{yzUxD~Hqn0(`zi17P6| zq+mGyByzqCVJwGl%RfFXKjZjS#g`n}^F_q{L)wS*AIXcqZAddM7FHn-6R7A@{x?<( z$Nvo2e+}jL5*Bye@k^Uc|Ejfe~9HG})_H0AWE&aVYr@x<$|EuN0^=AOuJAf3tZfP&Q$+D;A z{|2G>h{Upu4zwGHu?<^R*=f1teEkv*SB z{69kaUk8h?f0BPo^OvFoEdI_`{$+Xk^D=a2A3}NS4E}C}{~Hj>s~IY1N;CPwCHy zNV_9@?nC}929BWpd;Jsrwe-&_KTH3u_&=>bdn5Vv?`81+L+J01KWRU#lv_3UrlA!4 zeg3bn>T+byXOZ_8QGZ{>2@Uw9|5%FjFGeUU&ZQN?K>A-p4shn$pKSlt zhS+ZzD!)(nf7ewH*T1I_?~Cw%6uE!x6aCNfcLUU@_YF!{0M)HKgYM|*|V43 z$CB);Y&%=ew!!~BpYp$b+=-&>`A2?^W1{0c$Ni2n$BmBnME2YZ9^Ph>PvD2xNAUlS z^UU~Dies@uM&I z+(2qH!ox0EyQ;`3*?y_ajOxMUj!OD6#(G_0jb$B^4b5X>d~JKIon(6`62XEeA~T)# zB_17}VR~CbB5f)yWypk24iGPiVV^&ZFv7vZvk~C~SZ+0*OVj9++CW;@7^O9F0Bf<7 z2M@7Q7kEGynu#ZQ!o7%|C}E-`g{-KL&UL5aBcRrn&xrR_Hi)k%gDrqKDinFE?8=offIdT`DzXQ3G> z3TOJYAeYMJrS=*vLggeL6h@|QrjvP^Npw&TH@6%#iJK@y!RrWP!G(BMU_s<3uB@>q zQY0kjbq;M`m|@<6(xx9y@$`?)NlQ*}N2s%_1p{os8%W7|W4l*rqaeLG8Bc*KZ`ps7fd9F#`+`Uq9L#OtI%Q4LN*U2aVdBvjZM38zh67$)R% zVkD_%C&`e>ug5NP=75yghs}qDnYvI&BMBt8&@Z_|MT~ZC@E|WyuZTrM6Th0QTKsK|^{ zfXh^z=5Wckca-%?CpLEtbtATjFXF39BWw^n1Ixf8VHZWpxRWwW!=yp;s#T%77DMRL zrzi-87Mj$sKoAmq_??qOjqcg5i1f#_xWQ$hAm$Gq9WVqX=N<3E>;}|gr$IXYF7+;^b@!3nF-cQK1do4A8@+*y8bdPVSxBKUGZ#0o4xDS$pg9gFl4h6) zi3BZ5rr&kS<_KDjU8Kid4%9+pJke&iX1;M(Hw zP0JE~SZY=)f(?4jfX2GnZqqx?6ux9k%tCRm6KP4cy{L;CHH&V{3lT@Fg}P)wCn+9? z7|Cts!S;T4>3W{Xba2COXlhi1%UUtUft8&SYgNs@kZ(LCR2_?{gE~slB4AD*H7L|E zOvxQJd3_j-p3kI!L0(d%#S8!Ep)Ok#bV8r55*lOZ*95heb$^XQsjDL3_j@y0KA z3$H{m4=ALerDhOb5)hCoua+p*Q`lKh(5MIB?)h<(L#S?y{h6nYaPi{Jo#%pojtcf)G z3r9P0Lt3(@>99hQ*q8)4D6)YW4nr9RH)i!^;>`|{3c~{}q86FbkP^w(u#jG>^Ah|Z z$69DKGlCgV=5?ewnI>uuDzrNd4MQ36g^ksdlVrFt9c)s;ynE#eT8C~)cA|PXbdiz@ zQ*gd&L-bmHB~@#*USg_e^cHTo4Miv-ye|&&p4dvtw zR!-U)Bq}KJurCH?!zRsEQHjDryV+S1(G4^f@yQ`CacsSi!L>wbv<0_))W}pIoG`c_ z&5@TnF49J0##S0Jf5<2MP;$NTIhzJqv3ot=Wiqs6C85 z0y_!K0tFk86qgeO#%VWIV+8}`Qh{=r62@GXKR6_$k8?OY^`gA0d#EY_agh@SkSev&3ZArTj zYI;B&HzFg&laW!}A$nAYm=`fv!8I95_w$0ffTX$^Aj^S5NztMKDZXu(nrtRsjjARU z3IlswV6R+5cDAJJga|~W?3HMQQL&%`lx{IYmRyE+8b(dsSIN~rK9)?OAzS;d#+vI; zXCe{9?X{jiV_qzbHcfPIm#Ks(=b8^SH5D@x=r8+JB;KcFfpnRiyb-jPs0nCAcUsr> zP9L!Zdd$9pDkGT@2QQmVED|MXxoce&PZ%zSp2GWU&JlhM`i)MgU!RZk2pPU`ol&21 zp)9Ktk#?gY-OUDIof(b11sgRXkQYJQN!WqY(83%BM+FiVSL%2yq|7b|3<8NGOP=7hz&*-WAhaT1;vK$^@NtX&Q*3 z)4-6PVhr^d6$@o*RDkkoL?!jUo_35h+7E~_7nhl!<F*QNPJOsB+jtn&Oe2lnVB{sLgwe zif7^)^`Z5Wmm8M4HgU6o(lav0RmBU`9gDVj-scz+qpS~UNfk;q5(Is~L*b?WW;Q0QamP}br zT|BwY$u1pj7!Al{n{XD@@L|SFRYt4(@M&E$q5 zUuu1V(o&hegS`#=6JMxdmGKl_bZQMyBxqx|i4Mi&ZAOz8%mg<^l*XHlmFP05xhf<5 z5zV9o@*K>xQ=!8gLm@QZnQC;=F=nc&09yBS3`692x(?4LXncO2@S@XJC2B<{OEDwC zW5(4gW;A9fkp#zh{}$|xE2d20*yE%+)uf2aXslU*y{Z(Rl0o$wsaiw}&B!p49Vnpt zoCQa;#FG&{MlGs}ujq92Rhd=7G(uhE{WcmXX`p6gpjkTGQD~U+n1gYnns=kv(v*pW zBAMwOBHfk_cPUqxD4H_2G+7!+fI&2$nmAJZ1d2Q%TZ4hr>3^w|lAiGfSJ$4s{nnW*(O-!?F;@w#-Y>yb*R z$NUk7rW-Fh@ouru+G7LVV&A@fXq78W5pnGj-G#(5@m~3Az#q%RI7I_5xiXf@$?lnW z2Z>t4sd9!JV@Q?gB+Z5WuU$p$Y0-H-Mv|kqxeD5vmh=q);! zek>!c)}znQx|8lUzYnxWN`tAOCb&eWF$2$~Eh#b*9j4Xryk+`hc-XNT&xkq*y&ItE zm10I@Jshhh4KGGDBNLmKNGC8emBdgak-|_lgR0IntwoWJhZa`ujg|RAl45CQN*?oz zl<2^ozwH#1P%35UNxHrRRG}FQEQjhiD8;CcCyiHAc>j)}RfRnMT2)5^^u0)(82g%V z>(Vu&!(e_=lr|mcEOghd(J7NdVaqppc~MLzJ7niLxaAbmTG`T64(br8-WxU&_h(pXvnaZ<4 z<4nyoA{piQt0Bf4-bo}+d@4bSR8ViHekJa2pa2qt`jNJF zIcjI7F~F;^e{n>Vydbp(zoBAjkv8vZBRs2jkjn*ECacX{?~Hd!TG53$oUV{W^^sh| zQy{r9%1Fs>3~qfOADu+7(#6EqNGbE0)YnO74E>)-m<Sw-Z*VSAs2p(dwW%pOeb{N(Z)>-M-nJtw3MM>h8mlkZ9%wiOM6n}kx0Jg zGmXMbbHhBC2^IInyJ_B?WOmG^h$;Lg_PsiYVV=i0zq>jkjl`q)PNZwJN;D!%_~Zd; zO;I1En{;|9s7#EhI0~{UWriXnAB@h@iGmr4mbA|&qX8cDQb8iMi+tsE$W62jgbQMw zQg1Bc*4w7aqW^Be+XjZxp;|5Mt4Zv%JDtU0u}qGrnL07s6Yzi zU?d`uTGZZB$1u6Yn&TxSRZB+|!^8$Ya4>?&j#$R@)#0NNPji(ofuSk}@B($ZgCun% zwTjSI#ya8|Q^v^I;L=+NV*!#qLXTz0FVP$X57!yv(RX1OM^1B>K<_Yv^TK(ExY7KM{`@u@rMB(aB0D zp{HbAdXpWK@jMz=ap+1UJ-}laltgH{_e22;S5XURsl$S!RcOZ(?s3)~zpYOxC3uMu z*@_nW-Wb{`eAl2N=H6IwcS*3*Spgl-D| z&!B@Tlv!G0aF%p1XoMM#Qv!wN3N)med=d#VH=>4NE|Y?Mdl7TgW#ZkjaBSVXT7=0C zY-iM-^iIequDU}8ZIIUYC_Wb@cv1`SV^MUy)<-@x8{gy9Bc~K-QEgmypywWAdR=MM zLnHB29T&n3Oa<}$<379fD$fgA(wtg%t%Si>|>b{EtH7idn*jz@a7`! zOKvaR=IV6>n&$8GMksP{mVq(xKty45Gr>!Gf+nRR?w2^MILu?K$f2A(pMtKl=*$7Lk`_=50~EJ5rrkArI%rqk87Sq5awI( zc!yWbq(Pd4|NJ=oKMAAw5091$%+0dwx9d87o$WuV9=PrFe^NaJIsZSYGOi(X?5BMv z;>pSH<4G$2KdD-=>;Wuy!)EqBh}c(~0h{e&|x)h)`5TEt2y-o;j(sn$J~p9YYwMVL#xZaZ2fazt;FdK zPKxQJvs(&6D}6QXn;sP9bx-UYIp@g9@CA!jrCe_RsKDs5ipp`3s_MxX;@{p)omSs4 zz46j`(~Pf~yx^-@16o4av5px7>Q$?RTucbIscHx{iB0*LQW_*Yow>&0D^) zwf~{*5AXQa&Tl`m`@4I-_vBMge}C`(7Y@Am(#r>5dG(DSAAR$!pB($?+wcD3m%sY; zd;fOg{onll4}bjNPk;W)U;l>xUyap#x#REhQ-b_(O><}kR(=@2!^&)l<_u2Ky|LK^ zEh|eyHSNMzx9Pzr_PNS0m~(REqE$!S{_x~?D}HZfX(&hku?(gDQ#tx1L!abl@NM!q z(Cd{rO0Y2Hqd|IjJDX0vX8uMqKVmw4cx{9C2aZb@5`>5QS}y%l-^@rK@1p#R*00

4xE)GF_HvnNg_36z z?Qb87rf|r*>67U8^bE!1K!9@&!*2!B@(zNhkCI5S?pp$HaKU@qofT&UXjxi0a`v~c ztKdbJYx;aygto&*xi3HjR=|x(`jKu02Uk8}dC`%o-%jV2)sTJeGJ_UI&iTYL8)~H_dbQ*KSOj*8z$v$vbywD23+eKB$KsMp{On*4P9QHFJj=`UvrRv*OHUl;T zHUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;T zHUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;T zHUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;THUl;TpJL!=_^+tLJI4JZ zlXJ%K$N2DZTH@H^uRhQ38P0~C|1Zt#4t_&;J+z}cYzzE8sY%6D6^ zrQg0Izvtf26E$byvhcj0{{>0uT zJ^xyNnKpaF?H^{|JnsD7@hcBG+$X-Unq50VyZtMB+ZSI|^5PG=%BOCmZ@CtH@#n`H zZ)$)3r*l7i-cfwrNzaYndFBrbrhQgb?LRhddVCKnzxp{v_xsJIcis1D@^^=Q&tLon z*ZR%ZJ>GobN54JsT){KH9|-*X#+{yTpZMjm=MNXJxO3J`RCK&%|9Sn_ushuER^Q!M z@VD1)*!B1;;fIg9t}i<|^{wB1*q;7V^4OvGnSD%*&39e*DJg=$VKA?f5-c zt~^<8eCE5y&4VwlRDoCjAdVK^S~B*5fiGTIz5lV}ZRcNm$z>N$-2DBj(k+j#Zh!p! z#LnbRH_qL>{-+CW?RfBQS@W64RK+`^-uw$i_J8n$fg8-u*6||f zyzlyW{l;y7YTPvC+279p>21qD7. + +;;; Code: + +(require 'ert) +(require 'exif) +(require 'seq) + +(defun test-image-file (name) + (expand-file-name + name (expand-file-name "data/image" + (or (getenv "EMACS_TEST_DIRECTORY") + "../../")))) + +(defun exif-elem (exif elem) + (plist-get (seq-find (lambda (e) + (eq elem (plist-get e :tag-name))) + exif) + :value)) + +(ert-deftest test-exif-parse () + (let ((exif (exif-parse (test-image-file "black.jpg")))) + (should (equal (exif-elem exif 'make) "Panasonic")) + (should (equal (exif-elem exif 'orientation) 1)) + (should (equal (exif-elem exif 'x-resolution) '(180 . 1))))) + +;;; exif-tests.el ends here -- 2.39.5