19323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook/* 2c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei * Copyright (C) 2013 The Android Open Source Project 39323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * 49323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * Licensed under the Apache License, Version 2.0 (the "License"); 59323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * you may not use this file except in compliance with the License. 69323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * You may obtain a copy of the License at 79323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * 89323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * http://www.apache.org/licenses/LICENSE-2.0 99323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * 109323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * Unless required by applicable law or agreed to in writing, software 119323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * distributed under the License is distributed on an "AS IS" BASIS, 129323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 139323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * See the License for the specific language governing permissions and 149323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook * limitations under the License. 159323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook */ 169323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 179323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrookpackage com.android.ex.photo.util; 189323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 199323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrookimport android.util.Log; 209323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 21c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Weiimport java.io.ByteArrayInputStream; 22c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Weiimport java.io.InputStream; 23c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 249323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrookpublic class Exif { 259323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook private static final String TAG = "CameraExif"; 269323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 27c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei /** 28c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei * Returns the degrees in clockwise. Values are 0, 90, 180, or 270. 29c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei * @param inputStream The input stream will not be closed for you. 30c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei * @param byteSize Recommended parameter declaring the length of the input stream. If you 31c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei * pass in -1, we will have to read more from the input stream. 32c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei * @return 0, 90, 180, or 270. 33c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei */ 34c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei public static int getOrientation(final InputStream inputStream, final long byteSize) { 35c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei if (inputStream == null) { 369323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return 0; 379323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 389323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 39c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei /* 40c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei Looking at this algorithm, we never look ahead more than 8 bytes. As long as we call 41c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei advanceTo() at the end of every loop, we should never have to reallocate a larger buffer. 42c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 43c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei Also, the most we ever read backwards is 4 bytes. pack() reads backwards if the encoding 44c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei is in little endian format. These following two lines potentially reads 4 bytes backwards: 45c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 46c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei int tag = pack(jpeg, offset, 4, false); 47c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei count = pack(jpeg, offset - 2, 2, littleEndian); 48c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 49c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei To be safe, we will always advance to some index-4, so we'll need 4 more for the +8 50c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei look ahead, which makes it a +12 look ahead total. Use 16 just in case my analysis is off. 51c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 52c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei This means we only need to allocate a single 16 byte buffer. 53c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 54c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei Note: If you do not pass in byteSize parameter, a single large allocation will occur. 55c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei For a 1MB image, I see one 30KB allocation. This is due to the line containing: 56c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 57c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei has(jpeg, byteSize, offset + length - 1) 58c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 59c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei where length is a variable int (around 30KB above) read from the EXIF headers. 60c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 61c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei This is still much better than allocating a 1MB byte[] which we were doing before. 62c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei */ 63c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 64c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei final int lookAhead = 16; 65c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei final int readBackwards = 4; 66c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei final InputStreamBuffer jpeg = new InputStreamBuffer(inputStream, lookAhead, false); 67c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 689323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook int offset = 0; 699323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook int length = 0; 709323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 71b0cade17e70016ce562fb2033ea9e4a044137100Mark Wei if (has(jpeg, byteSize, 1)) { 72b0cade17e70016ce562fb2033ea9e4a044137100Mark Wei // JPEG image files begin with FF D8. Only JPEG images have EXIF data. 73b0cade17e70016ce562fb2033ea9e4a044137100Mark Wei final boolean possibleJpegFormat = jpeg.get(0) == (byte) 0xFF 74b0cade17e70016ce562fb2033ea9e4a044137100Mark Wei && jpeg.get(1) == (byte) 0xD8; 75b0cade17e70016ce562fb2033ea9e4a044137100Mark Wei if (!possibleJpegFormat) { 76b0cade17e70016ce562fb2033ea9e4a044137100Mark Wei return 0; 77b0cade17e70016ce562fb2033ea9e4a044137100Mark Wei } 78b0cade17e70016ce562fb2033ea9e4a044137100Mark Wei } 79b0cade17e70016ce562fb2033ea9e4a044137100Mark Wei 809323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // ISO/IEC 10918-1:1993(E) 81c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei while (has(jpeg, byteSize, offset + 3) && (jpeg.get(offset++) & 0xFF) == 0xFF) { 82c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei final int marker = jpeg.get(offset) & 0xFF; 839323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 849323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // Check if the marker is a padding. 859323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook if (marker == 0xFF) { 869323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook continue; 879323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 889323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook offset++; 899323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 909323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // Check if the marker is SOI or TEM. 919323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook if (marker == 0xD8 || marker == 0x01) { 929323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook continue; 939323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 949323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // Check if the marker is EOI or SOS. 959323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook if (marker == 0xD9 || marker == 0xDA) { 96c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei // Loop ends. 97c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei jpeg.advanceTo(offset - readBackwards); 989323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook break; 999323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1009323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 1019323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // Get the length and check if it is reasonable. 1029323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook length = pack(jpeg, offset, 2, false); 103c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei if (length < 2 || !has(jpeg, byteSize, offset + length - 1)) { 1049323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook Log.e(TAG, "Invalid length"); 1059323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return 0; 1069323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1079323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 1089323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // Break if the marker is EXIF in APP1. 1099323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook if (marker == 0xE1 && length >= 8 && 1109323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook pack(jpeg, offset + 2, 4, false) == 0x45786966 && 1119323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook pack(jpeg, offset + 6, 2, false) == 0) { 1129323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook offset += 8; 1139323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook length -= 8; 114c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei // Loop ends. 115c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei jpeg.advanceTo(offset - readBackwards); 1169323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook break; 1179323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1189323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 1199323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // Skip other markers. 1209323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook offset += length; 1219323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook length = 0; 122c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 123c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei // Loop ends. 124c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei jpeg.advanceTo(offset - readBackwards); 1259323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1269323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 1279323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // JEITA CP-3451 Exif Version 2.2 1289323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook if (length > 8) { 1299323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // Identify the byte order. 1309323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook int tag = pack(jpeg, offset, 4, false); 1319323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook if (tag != 0x49492A00 && tag != 0x4D4D002A) { 1329323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook Log.e(TAG, "Invalid byte order"); 1339323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return 0; 1349323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 135c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei final boolean littleEndian = (tag == 0x49492A00); 1369323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 1379323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // Get the offset and check if it is reasonable. 1389323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook int count = pack(jpeg, offset + 4, 4, littleEndian) + 2; 1399323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook if (count < 10 || count > length) { 1409323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook Log.e(TAG, "Invalid offset"); 1419323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return 0; 1429323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1439323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook offset += count; 1449323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook length -= count; 1459323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 146c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei // Offset has changed significantly. 147c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei jpeg.advanceTo(offset - readBackwards); 148c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 1499323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // Get the count and go through all the elements. 1509323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook count = pack(jpeg, offset - 2, 2, littleEndian); 151c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 1529323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook while (count-- > 0 && length >= 12) { 1539323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // Get the tag and check if it is orientation. 1549323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook tag = pack(jpeg, offset, 2, littleEndian); 1559323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook if (tag == 0x0112) { 1569323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook // We do not really care about type and count, do we? 157c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei final int orientation = pack(jpeg, offset + 8, 2, littleEndian); 1589323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook switch (orientation) { 1599323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook case 1: 1609323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return 0; 1619323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook case 3: 1629323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return 180; 1639323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook case 6: 1649323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return 90; 1659323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook case 8: 1669323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return 270; 1679323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1689323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook Log.i(TAG, "Unsupported orientation"); 1699323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return 0; 1709323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1719323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook offset += 12; 1729323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook length -= 12; 173c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 174c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei // Loop ends. 175c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei jpeg.advanceTo(offset - readBackwards); 1769323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1779323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1789323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 1799323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return 0; 1809323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1819323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 182c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei private static int pack(final InputStreamBuffer bytes, int offset, int length, 183c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei final boolean littleEndian) { 1849323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook int step = 1; 1859323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook if (littleEndian) { 1869323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook offset += length - 1; 1879323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook step = -1; 1889323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1899323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook 1909323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook int value = 0; 1919323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook while (length-- > 0) { 192c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei value = (value << 8) | (bytes.get(offset) & 0xFF); 1939323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook offset += step; 1949323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 1959323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook return value; 1969323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook } 197c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 198c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei private static boolean has(final InputStreamBuffer jpeg, final long byteSize, final int index) { 199c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei if (byteSize >= 0) { 200c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei return index < byteSize; 201c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei } else { 202c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei // For large values of index, this will cause the internal buffer to resize. 203c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei return jpeg.has(index); 204c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei } 205c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei } 206c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei 207c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei @Deprecated 208c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei public static int getOrientation(final byte[] jpeg) { 209c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei return getOrientation(new ByteArrayInputStream(jpeg), jpeg.length); 210c631b5a4b1f19f84a70b772bc879fae7c92fd4a8Mark Wei } 2119323b13fc9bc79ce38ce7c851a2aa894ab988ed0Paul Westbrook} 212