test_exif.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. # GNU MediaGoblin -- federated, autonomous media hosting
  2. # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import os
  17. try:
  18. from PIL import Image
  19. except ImportError:
  20. import Image
  21. from collections import OrderedDict
  22. from mediagoblin.tools.exif import exif_fix_image_orientation, \
  23. extract_exif, clean_exif, get_gps_data, get_useful
  24. from .resources import GOOD_JPG, EMPTY_JPG, BAD_JPG, GPS_JPG
  25. def assert_in(a, b):
  26. assert a in b, "%r not in %r" % (a, b)
  27. def test_exif_extraction():
  28. '''
  29. Test EXIF extraction from a good image
  30. '''
  31. result = extract_exif(GOOD_JPG)
  32. clean = clean_exif(result)
  33. useful = get_useful(clean)
  34. gps = get_gps_data(result)
  35. # Do we have the result?
  36. assert len(result) >= 50
  37. # Do we have clean data?
  38. assert len(clean) >= 50
  39. # GPS data?
  40. assert gps == {}
  41. # Do we have the "useful" tags?
  42. expected = OrderedDict({'EXIF CVAPattern': {'field_length': 8,
  43. 'field_offset': 26224,
  44. 'field_type': 7,
  45. 'printable': '[0, 2, 0, 2, 1, 2, 0, 1]',
  46. 'tag': 41730,
  47. 'values': [0, 2, 0, 2, 1, 2, 0, 1]},
  48. 'EXIF ColorSpace': {'field_length': 2,
  49. 'field_offset': 476,
  50. 'field_type': 3,
  51. 'printable': 'sRGB',
  52. 'tag': 40961,
  53. 'values': [1]},
  54. 'EXIF ComponentsConfiguration': {'field_length': 4,
  55. 'field_offset': 308,
  56. 'field_type': 7,
  57. 'printable': 'YCbCr',
  58. 'tag': 37121,
  59. 'values': [1, 2, 3, 0]},
  60. 'EXIF CompressedBitsPerPixel': {'field_length': 8,
  61. 'field_offset': 756,
  62. 'field_type': 5,
  63. 'printable': u'4',
  64. 'tag': 37122,
  65. 'values': [[4, 1]]},
  66. 'EXIF Contrast': {'field_length': 2,
  67. 'field_offset': 656,
  68. 'field_type': 3,
  69. 'printable': u'Soft',
  70. 'tag': 41992,
  71. 'values': [1]},
  72. 'EXIF CustomRendered': {'field_length': 2,
  73. 'field_offset': 572,
  74. 'field_type': 3,
  75. 'printable': u'Normal',
  76. 'tag': 41985,
  77. 'values': [0]},
  78. 'EXIF DateTimeDigitized': {'field_length': 20,
  79. 'field_offset': 736,
  80. 'field_type': 2,
  81. 'printable': u'2011:06:22 12:20:33',
  82. 'tag': 36868,
  83. 'values': u'2011:06:22 12:20:33'},
  84. 'EXIF DateTimeOriginal': {'field_length': 20,
  85. 'field_offset': 716,
  86. 'field_type': 2,
  87. 'printable': u'2011:06:22 12:20:33',
  88. 'tag': 36867,
  89. 'values': u'2011:06:22 12:20:33'},
  90. 'EXIF DigitalZoomRatio': {'field_length': 8,
  91. 'field_offset': 26232,
  92. 'field_type': 5,
  93. 'printable': u'1',
  94. 'tag': 41988,
  95. 'values': [[1, 1]]},
  96. 'EXIF ExifImageLength': {'field_length': 2,
  97. 'field_offset': 500,
  98. 'field_type': 3,
  99. 'printable': u'2592',
  100. 'tag': 40963,
  101. 'values': [2592]},
  102. 'EXIF ExifImageWidth': {'field_length': 2,
  103. 'field_offset': 488,
  104. 'field_type': 3,
  105. 'printable': u'3872',
  106. 'tag': 40962,
  107. 'values': [3872]},
  108. 'EXIF ExifVersion': {'field_length': 4,
  109. 'field_offset': 272,
  110. 'field_type': 7,
  111. 'printable': u'0221',
  112. 'tag': 36864,
  113. 'values': [48, 50, 50, 49]},
  114. 'EXIF ExposureBiasValue': {'field_length': 8,
  115. 'field_offset': 764,
  116. 'field_type': 10,
  117. 'printable': u'0',
  118. 'tag': 37380,
  119. 'values': [[0, 1]]},
  120. 'EXIF ExposureMode': {'field_length': 2,
  121. 'field_offset': 584,
  122. 'field_type': 3,
  123. 'printable': u'Manual Exposure',
  124. 'tag': 41986,
  125. 'values': [1]},
  126. 'EXIF ExposureProgram': {'field_length': 2,
  127. 'field_offset': 248,
  128. 'field_type': 3,
  129. 'printable': u'Manual',
  130. 'tag': 34850,
  131. 'values': [1]},
  132. 'EXIF ExposureTime': {'field_length': 8,
  133. 'field_offset': 700,
  134. 'field_type': 5,
  135. 'printable': u'1/125',
  136. 'tag': 33434,
  137. 'values': [[1, 125]]},
  138. 'EXIF FNumber': {'field_length': 8,
  139. 'field_offset': 708,
  140. 'field_type': 5,
  141. 'printable': u'10',
  142. 'tag': 33437,
  143. 'values': [[10, 1]]},
  144. 'EXIF FileSource': {'field_length': 1,
  145. 'field_offset': 536,
  146. 'field_type': 7,
  147. 'printable': u'Digital Camera',
  148. 'tag': 41728,
  149. 'values': [3]},
  150. 'EXIF Flash': {'field_length': 2,
  151. 'field_offset': 380,
  152. 'field_type': 3,
  153. 'printable': u'Flash did not fire',
  154. 'tag': 37385,
  155. 'values': [0]},
  156. 'EXIF FlashPixVersion': {'field_length': 4,
  157. 'field_offset': 464,
  158. 'field_type': 7,
  159. 'printable': u'0100',
  160. 'tag': 40960,
  161. 'values': [48, 49, 48, 48]},
  162. 'EXIF FocalLength': {'field_length': 8,
  163. 'field_offset': 780,
  164. 'field_type': 5,
  165. 'printable': u'18',
  166. 'tag': 37386,
  167. 'values': [[18, 1]]},
  168. 'EXIF FocalLengthIn35mmFilm': {'field_length': 2,
  169. 'field_offset': 620,
  170. 'field_type': 3,
  171. 'printable': u'27',
  172. 'tag': 41989,
  173. 'values': [27]},
  174. 'EXIF GainControl': {'field_length': 2,
  175. 'field_offset': 644,
  176. 'field_type': 3,
  177. 'printable': u'None',
  178. 'tag': 41991,
  179. 'values': [0]},
  180. 'EXIF ISOSpeedRatings': {'field_length': 2,
  181. 'field_offset': 260,
  182. 'field_type': 3,
  183. 'printable': u'100',
  184. 'tag': 34855,
  185. 'values': [100]},
  186. 'EXIF InteroperabilityOffset': {'field_length': 4,
  187. 'field_offset': 512,
  188. 'field_type': 4,
  189. 'printable': u'26240',
  190. 'tag': 40965,
  191. 'values': [26240]},
  192. 'EXIF LightSource': {'field_length': 2,
  193. 'field_offset': 368,
  194. 'field_type': 3,
  195. 'printable': u'Unknown',
  196. 'tag': 37384,
  197. 'values': [0]},
  198. 'EXIF MaxApertureValue': {'field_length': 8,
  199. 'field_offset': 772,
  200. 'field_type': 5,
  201. 'printable': u'18/5',
  202. 'tag': 37381,
  203. 'values': [[18, 5]]},
  204. 'EXIF MeteringMode': {'field_length': 2,
  205. 'field_offset': 356,
  206. 'field_type': 3,
  207. 'printable': u'Pattern',
  208. 'tag': 37383,
  209. 'values': [5]},
  210. 'EXIF Saturation': {'field_length': 2,
  211. 'field_offset': 668,
  212. 'field_type': 3,
  213. 'printable': u'Normal',
  214. 'tag': 41993,
  215. 'values': [0]},
  216. 'EXIF SceneCaptureType': {'field_length': 2,
  217. 'field_offset': 632,
  218. 'field_type': 3,
  219. 'printable': u'Standard',
  220. 'tag': 41990,
  221. 'values': [0]},
  222. 'EXIF SceneType': {'field_length': 1,
  223. 'field_offset': 548,
  224. 'field_type': 7,
  225. 'printable': u'Directly Photographed',
  226. 'tag': 41729,
  227. 'values': [1]},
  228. 'EXIF SensingMethod': {'field_length': 2,
  229. 'field_offset': 524,
  230. 'field_type': 3,
  231. 'printable': u'One-chip color area',
  232. 'tag': 41495,
  233. 'values': [2]},
  234. 'EXIF Sharpness': {'field_length': 2,
  235. 'field_offset': 680,
  236. 'field_type': 3,
  237. 'printable': u'Normal',
  238. 'tag': 41994,
  239. 'values': [0]},
  240. 'EXIF SubSecTime': {'field_length': 3,
  241. 'field_offset': 428,
  242. 'field_type': 2,
  243. 'printable': u'10',
  244. 'tag': 37520,
  245. 'values': u'10'},
  246. 'EXIF SubSecTimeDigitized': {'field_length': 3,
  247. 'field_offset': 452,
  248. 'field_type': 2,
  249. 'printable': u'10',
  250. 'tag': 37522,
  251. 'values': u'10'},
  252. 'EXIF SubSecTimeOriginal': {'field_length': 3,
  253. 'field_offset': 440,
  254. 'field_type': 2,
  255. 'printable': u'10',
  256. 'tag': 37521,
  257. 'values': u'10'},
  258. 'EXIF SubjectDistanceRange': {'field_length': 2,
  259. 'field_offset': 692,
  260. 'field_type': 3,
  261. 'printable': u'0',
  262. 'tag': 41996,
  263. 'values': [0]},
  264. 'EXIF WhiteBalance': {'field_length': 2,
  265. 'field_offset': 596,
  266. 'field_type': 3,
  267. 'printable': u'Auto',
  268. 'tag': 41987,
  269. 'values': [0]},
  270. 'Image DateTime': {'field_length': 20,
  271. 'field_offset': 194,
  272. 'field_type': 2,
  273. 'printable': u'2011:06:22 12:20:33',
  274. 'tag': 306,
  275. 'values': u'2011:06:22 12:20:33'},
  276. 'Image ExifOffset': {'field_length': 4,
  277. 'field_offset': 126,
  278. 'field_type': 4,
  279. 'printable': u'214',
  280. 'tag': 34665,
  281. 'values': [214]},
  282. 'Image Make': {'field_length': 18,
  283. 'field_offset': 134,
  284. 'field_type': 2,
  285. 'printable': u'NIKON CORPORATION',
  286. 'tag': 271,
  287. 'values': u'NIKON CORPORATION'},
  288. 'Image Model': {'field_length': 10,
  289. 'field_offset': 152,
  290. 'field_type': 2,
  291. 'printable': u'NIKON D80',
  292. 'tag': 272,
  293. 'values': u'NIKON D80'},
  294. 'Image Orientation': {'field_length': 2,
  295. 'field_offset': 42,
  296. 'field_type': 3,
  297. 'printable': u'Rotated 90 CCW',
  298. 'tag': 274,
  299. 'values': [6]},
  300. 'Image ResolutionUnit': {'field_length': 2,
  301. 'field_offset': 78,
  302. 'field_type': 3,
  303. 'printable': u'Pixels/Inch',
  304. 'tag': 296,
  305. 'values': [2]},
  306. 'Image Software': {'field_length': 15,
  307. 'field_offset': 178,
  308. 'field_type': 2,
  309. 'printable': u'Shotwell 0.9.3',
  310. 'tag': 305,
  311. 'values': u'Shotwell 0.9.3'},
  312. 'Image XResolution': {'field_length': 8,
  313. 'field_offset': 162,
  314. 'field_type': 5,
  315. 'printable': u'300',
  316. 'tag': 282,
  317. 'values': [[300, 1]]},
  318. 'Image YCbCrPositioning': {'field_length': 2,
  319. 'field_offset': 114,
  320. 'field_type': 3,
  321. 'printable': u'Co-sited',
  322. 'tag': 531,
  323. 'values': [2]},
  324. 'Image YResolution': {'field_length': 8,
  325. 'field_offset': 170,
  326. 'field_type': 5,
  327. 'printable': u'300',
  328. 'tag': 283,
  329. 'values': [[300, 1]]},
  330. 'Thumbnail Compression': {'field_length': 2,
  331. 'field_offset': 26280,
  332. 'field_type': 3,
  333. 'printable': u'JPEG (old-style)',
  334. 'tag': 259,
  335. 'values': [6]},
  336. 'Thumbnail ResolutionUnit': {'field_length': 2,
  337. 'field_offset': 26316,
  338. 'field_type': 3,
  339. 'printable': u'Pixels/Inch',
  340. 'tag': 296,
  341. 'values': [2]},
  342. 'Thumbnail XResolution': {'field_length': 8,
  343. 'field_offset': 26360,
  344. 'field_type': 5,
  345. 'printable': u'300',
  346. 'tag': 282,
  347. 'values': [[300, 1]]},
  348. 'Thumbnail YCbCrPositioning': {'field_length': 2,
  349. 'field_offset': 26352,
  350. 'field_type': 3,
  351. 'printable': u'Co-sited',
  352. 'tag': 531,
  353. 'values': [2]},
  354. 'Thumbnail YResolution': {'field_length': 8,
  355. 'field_offset': 26368,
  356. 'field_type': 5,
  357. 'printable': u'300',
  358. 'tag': 283,
  359. 'values': [[300, 1]]}})
  360. for key in expected.keys():
  361. assert useful[key] == expected[key]
  362. def test_exif_image_orientation():
  363. '''
  364. Test image reorientation based on EXIF data
  365. '''
  366. result = extract_exif(GOOD_JPG)
  367. image = exif_fix_image_orientation(
  368. Image.open(GOOD_JPG),
  369. result)
  370. # Are the dimensions correct?
  371. assert image.size in ((428, 640), (640, 428))
  372. # If this pixel looks right, the rest of the image probably will too.
  373. assert_in(image.getdata()[10000],
  374. ((41, 28, 11), (43, 27, 11))
  375. )
  376. def test_exif_no_exif():
  377. '''
  378. Test an image without exif
  379. '''
  380. result = extract_exif(EMPTY_JPG)
  381. clean = clean_exif(result)
  382. useful = get_useful(clean)
  383. gps = get_gps_data(result)
  384. assert result == {}
  385. assert clean == {}
  386. assert gps == {}
  387. assert useful == {}
  388. def test_exif_bad_image():
  389. '''
  390. Test EXIF extraction from a faithful, but bad image
  391. '''
  392. result = extract_exif(BAD_JPG)
  393. clean = clean_exif(result)
  394. useful = get_useful(clean)
  395. gps = get_gps_data(result)
  396. assert result == {}
  397. assert clean == {}
  398. assert gps == {}
  399. assert useful == {}
  400. def test_exif_gps_data():
  401. '''
  402. Test extractiion of GPS data
  403. '''
  404. result = extract_exif(GPS_JPG)
  405. gps = get_gps_data(result)
  406. assert gps == {
  407. 'latitude': 59.336666666666666,
  408. 'direction': 25.674046740467404,
  409. 'altitude': 37.64365671641791,
  410. 'longitude': 18.016166666666667}