Saturday, October 6, 2012

iOS photos EXIF data

I ran into an issue that looking at a photo made on my iPhone had the Exif data with the GPS (geolocation) stored in it and I wanted to post it online without revealing any personal details there. What I usually do then is just to click in Windows on Properties, Details and click "Remove Properties and Personal Information" as you can see in these two screenshots here.
Image Properties
Image Properties

Remove Properties and Personal Information
Remove Properties and Personal Information
I don't know why I can't select F-stop, Exposure time and all that, but maybe that's not considered personal enough. Anyway, the problem now is that this removal process fails, although I remember this worked well at some time in the past. This is what happens:
Apply Property Error
Apply Property Error
An error occurred when writing the property 'Altitude' to the file 'IMG_0734.JPG'.
Not all personal properties were cleared
Not all personal properties were cleared
Windows was unable to remove properties from the
selected files. Before sharing these files, you should
review them for unwanted personal information.
After some googling, I found that other people had similar problems. Someone said that this is because iOS now writes some information about the fact that the photo was taken from the lockscreen as property into the image itself and Windows cannot handle that information. That sounded reasonable, but turned out to be wrong, as we will see later. But this got me interested in finding out what information is exactly stored in the Exif data of my photos. Another thing that I wanted to find out was how iOS knows if it is an HDR photo or not. It has to know it from somewhere, because HDR photos show this special symbol.

HDR
HDR symbol
So this got me enough interested now to start a full examination of the Exif headers. We will examine the following photo, so if you want to look at it as well, download it from Picasa and verify the checksum first, to make sure we're looking at the same file.


Apple Store Zurich
MD5: ddbe1f1ea166c7793328db78896f18dd
SHA1: ba2e522cdd8ffa1644cb4f09e38a8bee68359956

Ok, to start, I opened this jpg file in a hex editor. To find the Exif area, you first have to understand the jpg image format. Every jpg image starts with the "marker" ff d8. Then there follow blocks of data. Each block begins with a two-byte marker and a two-byte length. So for our file we have the following blocks (simplified):
000000: ff d8                   ; marker SOI, Start Of Image

000002: ff e1                   ; marker APP1, Application-specific
000004: 3f fe                   ; length of this block (next block at 004002)

000006: 45 78 69 66 00 00 4d... ; content of this block

004002: ff db                   ; marker DQT, Define Quantization Table(s)
004004: 00 84                   ; length of this block (next block at 004088)

004006: 00 01 01 01 01 01 01... ; content of this block

004088: ff c0                   ; marker SOF0, Start Of Frame (Baseline DCT)
00408a: 00 11                   ; length of this block (next block at 00409b)

00408c: 08 09 90 0c c0 03 01... ; content of this block

00409b: ff c4                   ; marker DHT, Huffman table
00409d: 01 a2                   ; length of this block (next block at 00423f)

00409f: 00 00 01 05 01 01 01... ; content of this block

00423f: ff da                   ; marker SOS, Start Of Scan
004241: 00 0c 03 01 00 02 11... ; 12 bytes general data, then the image data

377f39: ff d9                   ; marker EOI, End Of Image
One thing that is difficult here is that the image data (in the 'ff da' tag) cannot be read easily manually, so there is no easy way to find the next block, so if there is a block after that one, we would probably miss it. If you want to know more details and want to investigate deeper, there are some links at the end of this post. But actually this is not important for now, as we only need to look at the APP1 block, which contains the Exif data. We will examine this APP1 block now.

In this APP1 block, this starts with an Exif marker to indicate the Exif information.
0000: 45 78 69 66 00 ; "Exif\0"
0005: 00             ; padding
0006:

After this 6-byte introduction the structure of the rest of the Exif-block is similar to a TIFF version 6 file. I will start showing the addresses now starting with zero again, because all further pointers are starting here as well. If you want to calculate this back to the offset in the entire file, you have to add 6 for the Exif introduction and 6 more bytes for the jpg start (SOI+APP1 markers and length). So just add 0xC or 12d to the offset to get the real position within the file.

Ok, after the Exif introduction above, the TIFF header starts, now as mentioned, starting with offset 0 here.
0000: 4d 4d       ; big-endian (Motorola)
0002: 00 2a       ; 42d magic number
0004: 00 00 00 08 ; Offset to first IFD (just following)
First we have an identification that all the following values are stored in big-endian (Motorola-style), meaning you can read the bytes just from left to right. I like this a lot more than little-endian, where you have to read from right to left (even worse if you store nibbles in bytes in words and such stuff; get almost impossible to read manually). So this identification says that the rest of the Exif block has to be read in big-endian style. Please note that there is no alignment, so a SHORT value (two bytes) can start either at an even or at an odd address.

What the rest of this Exif information contains, is actually just a list of IFDs (IFD stands for TIFF image file directory). The first IFD (IFD 0) usually just follows the TIFF header, so that's why the above offset is set to 00000008, meaning we can read it at the next address.

How is an IFD constructed? First we have a length (number of entries in the directory), then the entries themself (each 0xc/12d  bytes long), then a LONG value with a pointer to the next IFD (zero if no other follows) and then usually the data that is referenced by the entries.

Each entry looks like this:
  1. two bytes tag identification
  2. two bytes data type
  3. four bytes number of values (not bytes!)
  4. four bytes pointer to the data, or, if the data fits into four bytes, the data itself
The data types can be:
  • 1=BYTE
  • 2=ASCII (bytes, no unicode)
  • 3=SHORT
  • 4=LONG
  • 5=RATIONAL (2 LONGs, first is numerator, then denominator)
  • 7=UNDEFINED
  • 9=SIGNED LONG
  • 0xa=SIGNED RATIONAL
So in our example image we can find the following in IFD 0:
0008: 00 0b       ; 11d fields follow, each 0xc/12d bytes size

000a: 01 0f 00 02 00 00 00 06 00 00 00 92 ; 010f Make             06 ASC:92->
0016: 01 10 00 02 00 00 00 0a 00 00 00 98 ; 0110 Model            0a ASC:98->
0022: 01 12 00 03 00 00 00 01 00 01 00 00 ; 0112 Orientation      01 SRT:0001
002e: 01 1a 00 05 00 00 00 01 00 00 00 a2 ; 011a XResolution      01 RAT:a2->
003a: 01 1b 00 05 00 00 00 01 00 00 00 aa ; 011b YResolution      01 RAT:aa->
0046: 01 28 00 03 00 00 00 01 00 02 00 00 ; 0128 ResolutionUnit   01 SRT:0002
0052: 01 31 00 02 00 00 00 06 00 00 00 b2 ; 0131 Software         06 ASC:b2->
005e: 01 32 00 02 00 00 00 14 00 00 00 b8 ; 0132 DateTime         14 ASC:b8->
006a: 02 13 00 03 00 00 00 01 00 01 00 00 ; 0213 YCbCrPositioning 01 SRT:0001
0076: 87 69 00 04 00 00 00 01 00 00 00 cc ; 8769 ExifIFD          01 LNG:000000cc
0082: 88 25 00 04 00 00 00 01 00 00 02 4a ; 8825 GPS IFD          01 LNG:0000024a


008e: 00 00 03 14 ; pointer to next IFD (314->)

0092: 41 70 70 6c 65 00             ; "Apple\0"
0098: 69 50 68 6f 6e 65 20 34 53 00 ; "iPhone 4S\0"
00a2: 00 00 00 48 00 00 00 01       ; 72d/1=72d
00aa: 00 00 00 48 00 00 00 01       ; 72d/1=72d
00b2: 35 2e 31 2e 31 00             ; "5.1.1\0"
00b8: 32 30 31 32 3a 31 30 3a 30 34 ; "2012:10:04 13:39:02\0"

00c2: 20 31 33 3a 33 39 3a 30 32 00
So this IFD 0 gives us these properties:
  • Make: Apple
  • Model: iPhone 4S
  • Orientation: 1 (normal, the image doesn't need to be turned, it's horizontal already)
  • XResolution: 72d
  • YResolution: 72d
  • ResolutionUnit: 2 (Inch)
  • Software: 5.1.1 (iOS software version I used)
  • DateTime: 2012:10:04 13:39:02
  • YCbCrPositioning: 1 (centered)
But there are three more things in there. We have the properties ExifIFD and GPS IFD, both point to a Sub-IFD with more information, actually another IFD block with the same structure. Also, at the end there was a pointer to the next IFD (IFD 1). So we have three more IFDs to read; at addresses 00cc (Exif Sub-IFD), 024a (GPS Sub-IFD) and 0314 (IFD 1). Here they are. You can skip reading this block if you're not interested in these details.
First is the Exif Sub-IFD:
00cc: 00 18       ; 11d fields follow, each 12d bytes size, same as above

00ce: 82 9a 00 05 00 00 00 01 00 00 01 f2 ; 829a ExposureTime            | 5=RATNL | 01 | 1f2->
00da: 82 9d 00 05 00 00 00 01 00 00 01 fa ; 829d FNumber                 | 5=RATNL | 01 | 1fa->
00e6: 88 22 00 03 00 00 00 01 00 02 00 00 ; 8822 ExposureProgram         | 3=SHORT | 01 | 0002 normal prog
00f2: 88 27 00 03 00 00 00 01 00 50 00 00 ; 8827 ISOSpeedRatings         | 3=SHORT | 01 | 0050 80d, ISO 80
00fe: 90 00 00 07 00 00 00 04 30 32 32 31 ; 9000 ExifVersion             | 7=UNDEF | 04 | "0221" V2.21
010a: 90 03 00 02 00 00 00 14 00 00 02 02 ; 9003 DateTimeOriginal        | 2=ASCII | 14 | 202->
0116: 90 04 00 02 00 00 00 14 00 00 02 16 ; 9004 DateTimeDigitized       | 2=ASCII | 14 | 216->
0122: 91 01 00 07 00 00 00 04 01 02 03 00 ; 9101 ComponentsConfiguration | 7=UNDEF | 04 | 1230 -> Y,Cb,Cr
012e: 92 01 00 0a 00 00 00 01 00 00 02 2a ; 9201 ShutterSpeedValue       | a=S-RAT | 01 | 22a->
013a: 92 02 00 05 00 00 00 01 00 00 02 32 ; 9202 ApertureValue           | 5=RATNL | 01 | 232->
0146: 92 03 00 0a 00 00 00 01 00 00 02 3a ; 9203 BrightnessValue         | a=S-RAT | 01 | 23a->
0152: 92 07 00 03 00 00 00 01 00 05 00 00 ; 9207 MeteringMode            | 3=SHORT | 01 | 0005 (Pattern)
015e: 92 09 00 03 00 00 00 01 00 00 00 00 ; 9209 Flash                   | 3=SHORT | 01 | 0000 (not fired)
016a: 92 0a 00 05 00 00 00 01 00 00 02 42 ; 920a FocalLength             | 5=RATNL | 01 | 242->
0176: a0 00 00 07 00 00 00 04 30 31 30 30 ; a000 FlashpixVersion         | 7=UNDEF | 04 | "0100" V1.0
0182: a0 01 00 03 00 00 00 01 00 01 00 00 ; a001 ColorSpace              | 3=SHORT | 01 | 0001 (sRGB)
018e: a0 02 00 04 00 00 00 01 00 00 0c c0 ; a002 PixelXDimension         | 4=LONG  | 01 | 00000cc0 (3264d)
019a: a0 03 00 04 00 00 00 01 00 00 09 90 ; a003 PixelYDimension         | 4=LONG  | 01 | 00000990 (2448d)
01a6: a2 17 00 03 00 00 00 01 00 02 00 00 ; a217 SensingMethod           | 3=SHORT | 01 | 0002 colarea sens
01b2: a4 01 00 03 00 00 00 01 00 03 00 00 ; a401 CustomRendered          | 3=SHORT | 01 | 0003 0=Norm,1=Cst

01be: a4 02 00 03 00 00 00 01 00 00 00 00 ; a402 ExposureMode            | 3=SHORT | 01 | 0000 auto exp
01ca: a4 03 00 03 00 00 00 01 00 00 00 00 ; a403 WhiteBalance            | 3=SHORT | 01 | 0000 autowhitebal
01d6: a4 05 00 03 00 00 00 01 00 23 00 00 ; a405 FocalLengthIn35mmFilm   | 3=SHORT | 01 | 0023 35d
01e2: a4 06 00 03 00 00 00 01 00 00 00 00 ; a406 SceneCaptureType        | 3=SHORT | 01 | 0000 Standard


01ee: 00 00 00 00 ; offset to next IFD (none)

01f2: 00 00 00 01 00 00 00 78       ; Exposure time in seconds: 1/120d
01fa: 00 00 00 0c 00 00 00 05       ; F Number: 12d/5d=2.4d
0202: 32 30 31 32 3a 31 30 3a 30 34 ; "2012:10:04 13:39:02\0"

020c: 20 31 33 3a 33 39 3a 30 32 00
0216: 32 30 31 32 3a 31 30 3a 30 34 ; "2012:10:04 13:39:02\0"

0220: 20 31 33 3a 33 39 3a 30 32 00
022a: 00 00 15 bf 00 00 03 26       ; Shutter speed, APEX setting 15bf/326=5567d/806d=6->(1/2^x)->1/64d
0232: 00 00 12 ed 00 00 07 7e       ; lens aperture, APEX unit 12ed/77e=4845d/1918d=2->(sqrt(2)^x)->2
023a: 00 00 22 7e 00 00 06 1b       ; value of brightness: 227e/61b=8830d/1563d=~5.6d EV
0242: 00 00 00 6b 00 00 00 19       ; actual focal length of the lens in mm: 6b/19=107d/25d=4.28d

This is the GPS Sub-IFD:
024a: 00 09       ; 9 fields follow, each 12d bytes size, same as above

024c: 00 01 00 02 00 00 00 02 4e 00 00 00 ; 0001 GPSLatitudeRef     | 2=ASCII | 02 | "N\0" (north latitude)
0258: 00 02 00 05 00 00 00 03 00 00 02 bc ; 0002 GPSLatitude        | 5=RATNL | 03 | 2bc->
0264: 00 03 00 02 00 00 00 02 45 00 00 00 ; 0003 GPSLongitudeRef    | 2=ASCII | 02 | "E\0" (east longitude)
0270: 00 04 00 05 00 00 00 03 00 00 02 d4 ; 0004 GPSLongitude       | 5=RATNL | 03 | 2d4->
027c: 00 05 00 01 00 00 00 01 00 00 00 00 ; 0005 GPSAltitudeRef     | 1=BYTE  | 01 | 0 (above sea level)
0288: 00 06 00 05 00 00 00 01 00 00 02 ec ; 0006 GPSAltitude        | 5=RATNL | 01 | 2ec->
0294: 00 07 00 05 00 00 00 03 00 00 02 f4 ; 0007 GPSTimeStamp       | 5=RATNL | 03 | 2f4->
02a0: 00 10 00 02 00 00 00 02 54 00 00 00 ; 0010 GPSImgDirectionRef | 2=ASCII | 02 | "T\0" (True North dir)
02ac: 00 11 00 05 00 00 00 01 00 00 03 0c ; 0011 GPSImgDirection    | 5=RATNL | 01 | 30c->


02b8: 00 00 00 00 ; offset to next IFD (none)

02bc: 00 00 00 2f 00 00 00 01 ; latitude: 47d/1, 2248d/100d, 0/1 -> 47° 22.48'
02c4: 00 00 08 c8 00 00 00 64
02cc: 00 00 00 00 00 00 00 01
02d4: 00 00 00 08 00 00 00 01 ; longitude: 8/1, 3233d/100d, 0/1 -> 8° 32.33'

02dc: 00 00 0c a1 00 00 00 64
02e4: 00 00 00 00 00 00 00 01
02ec: 00 00 01 b6 00 00 00 01 ; Altitude in meters: 1b6/1=438d/1
02f4: 00 00 00 0b 00 00 00 01 ; timestamp: 11d/1,39/1,206/100 -> "11:39:2.06 GMT"

02fc: 00 00 00 27 00 00 00 01
0304: 00 00 00 ce 00 00 00 64
030c: 00 00 b1 76 00 00 00 97 ; direction of image: b176/97=45430/151=~300.86°

And this is IFD 1:
0314: 00 06       ; 6 fields follow, each 12d bytes size, same as above

0316: 01 03 00 03 00 00 00 01 00 06 00 00 ; 0103 Compression                 | 01 SRT:0006 (JPEG old-style)
0322: 01 1a 00 05 00 00 00 01 00 00 03 62 ; 011a XResolution                 | 01 RAT:362->
032e: 01 1b 00 05 00 00 00 01 00 00 03 6a ; 011b YResolution                 | 01 RAT:36a->
0346: 02 01 00 04 00 00 00 01 00 00 03 72 ; 0201 JPEGInterchangeFormat       | 01 LNG:372->
0352: 02 02 00 04 00 00 00 01 00 00 30 ed ; 0202 JPEGInterchangeFormatLength | 01 LNG:000030ed


035e: 00 00 00 00 ; offset to next IFD (none)

0362: 00 00 00 48 00 00 00 01 ; number of pixels per resolution unit in image width direction: 48/1=72d
036a: 00 00 00 48 00 00 00 01 ; number of pixels per resolution unit in image length direction: 48/1=72d


Here starts the thumbnail image, also part of this IFD 1:

0372: ff d8 ff db 00 43 00 02 ; JPEG image (thumbnail 160x120)
...
3457: 8c f4 a2 8a 82 d1 ff d9 ; end of JPEG image thumbnail, JPEG length is 000030ed
345f:


00346b: 00 00 00 00 00 00 00 00 ; null bytes at address (file offset now) 0372+000030ed+C=00346b
...
003ffa: 00 00 00 00 00 00 00 00 ; number of null bytes:

                                ; APP1_size-length_size-Exif_start-Exif_content_size=3ffe-2-6-345f=b97,
                                ; also 004002-00346b=b97 (2967d)

004002: ff db                   ; marker DQT, Define Quantization Table(s)
004004: 00 84                   ; length of this block (next block at 004004+0084=004088)
004006: 00 01 01 01 01 01 01... ; content of this block


So where is the HDR marker (this is an HDR image)?

The HDR marker is the a401 CustomRendered=3 tag in the Exif IFD. In non-HDR photos, this tag is completely missing. In non-HDR photos there are two other tags, both in the Exif Sub-IFD instead:
  • 9214 SubjectArea, four SHORTs, X-Y-W-H for the area you clicked to be the subject, located between tags 920a and a000
  • a40a Sharpness, one SHORT, value 0 (=Normal), located at the end
In the non-HDR version of this image, I had the SubjectArea coordinates X=065F, Y=04C7, W=H=0371.

Ok, nothing special so far. But the bugs? Did you spot anything that could've caused Windows to react in such a way? Well, yes, there are many things that are bad here:
  • In IFD 1 the counter says there are 6 fields, but there are actually only 5.
  • In the Exif IFD, a405 FocalLengthIn35mmFilm has a value of 35. I assume this is wrong and not accidentally just exactly 35. Probably they don't care and just wanted to fill out this field with "something". The focal length is 4.28mm, but calculated into 35mm film?
  • old-style thumbnail: The usage of the tags 0201 and 0202 has been disallowed since 17-Mar-1995, because there are problems and incompatibilities. See TIFF TechNote 2 in the links for details.
  • After the thumbnail image there are a lot of zeroes. While not disallowed, this is still a waste of space in the file.
  • The Exif IFD tag a401 CustomRenderer, used as the HDR marker, allows only the values 0 (Normal) and 1 (Custom). The value of 3 (HDR) is not defined in the standard. The standard says that custom tags can be obtained and I don't see the reason why Apple is not using any newly requested standard tag for that.
  • In the GPS IFD the mandatory version tag is missing.
  • The GPS IFD has a GPSTimeStamp tag, but not a GPSDateStamp.
From all these problems in the image, I think what could cause the bad Windows behaviour would be the wrong count of elements and the missing version tag in the GPS IFD. Maybe also the old-style thumbnail. I fixed all these problems in the file and still had the same problem in Windows.

Finally I found out that having the GPS IFD in general is causing this. It is not iOS related. I found several images with geolocation information on the Internet (from four different Nikon cameras: Coolpix P6000, D90, D300, D200) and the problem existed there as well. So Windows cannot remove GPS location properties. I think geolocation data is the most personal identifying information that a photo can have, so if Windows cannot remove that, then this feature is totally broken. I tried only with Windows 7, 64-bit, Ultimate, english. Maybe it's fixed with Windows 8. And from the above iOS image problems, maybe some are fixed with the release of iOS6, but I didn't test yet.

Here some links for more information about the file structure that were helpful for me if you want to dig into this yourself: