1package com.android.rs.refocus; 2 3import android.graphics.Bitmap; 4import android.graphics.BitmapFactory; 5import android.util.Log; 6 7import com.adobe.xmp.XMPConst; 8import com.adobe.xmp.XMPException; 9import com.adobe.xmp.XMPIterator; 10import com.adobe.xmp.XMPMeta; 11import com.adobe.xmp.XMPMetaFactory; 12import com.adobe.xmp.properties.XMPPropertyInfo; 13 14import java.io.ByteArrayInputStream; 15import java.io.IOException; 16import java.io.InputStream; 17import java.io.UnsupportedEncodingException; 18import java.util.ArrayList; 19import java.util.List; 20 21import com.android.rs.refocus.image.RangeInverseDepthTransform; 22import com.android.rs.refocus.image.RangeLinearDepthTransform; 23 24 25/** 26 * Created by hoford on 5/15/15. 27 */ 28public class XmpDepthDecode { 29 private static final String TAG = "XmpUtil"; 30 private static final String XMP_DEPTHMAP = "http://ns.google.com/photos/1.0/depthmap/"; 31 private static final String XMP_FOCUS = "http://ns.google.com/photos/1.0/focus/"; 32 private static final String XMP_HEADER = "http://ns.adobe.com/xap/1.0/\0"; 33 private static final String XMP_EXTENSION_HEADER = 34 "http://ns.adobe.com/xmp/extension/\0"; 35 private static final String XMP_HAS_EXTENSION = "HasExtendedXMP"; 36 private static final int XMP_EXTENSION_HEADER_GUID_SIZE = 37 XMP_EXTENSION_HEADER.length() + 32 + 1; // 32 byte GUID + 1 byte null termination. 38 private static final int XMP_EXTENSION_HEADER_OFFSET = 7; 39 40 private static final int M_SOI = 0xd8; // File start marker. 41 private static final int M_APP1 = 0xe1; // Marker for EXIF or XMP. 42 private static final int M_SOS = 0xda; // Image data marker. 43 44 private final String mFormat; 45 private final double mFar; 46 private final double mNear; 47 private final Bitmap mDepthBitmap; 48 private final double mBlurAtInfinity; 49 private final double mFocalDistance; 50 private final double mDepthOfFiled; 51 private final double mFocalPointX; 52 private final double mFocalPointY; 53 private final DepthTransform mDepthTransform; 54 55 public XmpDepthDecode(InputStream is) throws IOException { 56 XMPMeta meta = read(is, false); 57 try { 58 mFormat = meta.getPropertyString(XMP_DEPTHMAP, "GDepth:Format"); 59 60 mFar = Double.parseDouble(meta.getPropertyString(XMP_DEPTHMAP, "GDepth:Far")); 61 mNear = Double.parseDouble(meta.getPropertyString(XMP_DEPTHMAP, "GDepth:Near")); 62 63 DepthTransform tDepthTransform = null; 64 String format = meta.getPropertyString( 65 XMP_DEPTHMAP, "GDepth:Format"); 66 if (RangeInverseDepthTransform.FORMAT.equals(format)) { 67 tDepthTransform = new RangeInverseDepthTransform((float)mNear, (float)mFar); 68 } else if (RangeLinearDepthTransform.FORMAT.equals(format)) { 69 tDepthTransform = new RangeLinearDepthTransform((float)mNear, (float)mFar); 70 } else { 71 Log.e(TAG, "Unknown GDepth format: " + format); 72 } 73 mDepthTransform = tDepthTransform; 74 75 byte[] data = meta.getPropertyBase64(XMP_DEPTHMAP, "GDepth:Data"); 76 mDepthBitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(data)); 77 78 mBlurAtInfinity = Double.parseDouble(meta.getPropertyString(XMP_FOCUS, "GFocus:BlurAtInfinity")); 79 mFocalDistance = Double.parseDouble(meta.getPropertyString(XMP_FOCUS, "GFocus:FocalDistance")); 80 mDepthOfFiled = Double.parseDouble(meta.getPropertyString(XMP_FOCUS, "GFocus:DepthOfField")); 81 mFocalPointX = Double.parseDouble(meta.getPropertyString(XMP_FOCUS, "GFocus:FocalPointX")); 82 mFocalPointY = Double.parseDouble(meta.getPropertyString(XMP_FOCUS, "GFocus:FocalPointY")); 83 } catch (XMPException e) { 84 throw new IOException("XMP data missing"); 85 } 86 } 87 88 public Bitmap getDepthBitmap() { 89 return mDepthBitmap; 90 } 91 92 public DepthTransform getDepthTransform() { return mDepthTransform; } 93 94 public String getFormat() { 95 return mFormat; 96 } 97 98 public double getFar() { 99 return mFar; 100 } 101 102 public double getNear() { 103 return mNear; 104 } 105 106 public double getBlurAtInfinity() { 107 return mBlurAtInfinity; 108 } 109 110 public double getFocalDistance() { 111 return mFocalDistance; 112 } 113 114 public double getDepthOfField() { return mDepthOfFiled; } 115 116 public double getFocalPointX() { 117 return mFocalPointX; 118 } 119 120 public double getFocalPointY() { 121 return mFocalPointY; 122 } 123 124 125 // JPEG file is composed of many sections and image data. This class is used 126 // to hold the section data from image file. 127 private static class Section { 128 public int marker; 129 public int length; 130 public byte[] data; 131 } 132 133 static XMPMeta read(InputStream is, boolean skipExtendedContent) { 134 List<Section> sections = parse(is, true, skipExtendedContent); 135 if (sections == null) { 136 return null; 137 } 138 139 XMPMeta xmpMeta = parseFirstValidXMPSection(sections); 140 if (xmpMeta == null || 141 !xmpMeta.doesPropertyExist(XMPConst.NS_XMP_NOTE, XMP_HAS_EXTENSION)) { 142 return xmpMeta; 143 } 144 145 String extensionName = null; 146 try { 147 extensionName = (String) xmpMeta.getProperty( 148 XMPConst.NS_XMP_NOTE, XMP_HAS_EXTENSION).getValue(); 149 } catch (XMPException e) { 150 e.printStackTrace(); 151 return null; 152 } 153 154 if (skipExtendedContent) { 155 if (!checkExtendedSectionExists(sections, extensionName)) { 156 // The main XMP section referenced an extended section that is not present. 157 // This is an error. 158 return null; 159 } 160 return xmpMeta; 161 } 162 163 XMPMeta xmpExtended = parseExtendedXMPSections(sections, extensionName); 164 if (xmpExtended == null) { 165 // The main XMP section referenced an extended section that is not present. 166 // This is an error. 167 return null; 168 } 169 170 // Merge the extended properties into the main one. 171 try { 172 XMPIterator iterator = xmpExtended.iterator(); 173 while (true) { 174 XMPPropertyInfo info = (XMPPropertyInfo) iterator.next(); 175 if (info.getPath() != null) { 176 xmpMeta.setProperty(info.getNamespace(), info.getPath(), 177 info.getValue(), info.getOptions()); 178 } 179 } 180 } catch (Exception e) { 181 // Catch XMPException and NoSuchElementException. 182 } 183 return xmpMeta; 184 } 185 186 /** 187 * Parses the JPEG image file. If readMetaOnly is true, only keeps the EXIF 188 * and XMP sections (with marker M_APP1) and ignore others; otherwise, keep 189 * all sections. The last section with image data will have -1 length. 190 * 191 * @param is Input image data stream 192 * @param readMetaOnly Whether only reads the metadata in jpg 193 * @param skipExtendedContent Whether to skip the content of extended sections 194 * @return The parse result 195 */ 196 private static List<Section> parse(InputStream is, boolean readMetaOnly, 197 boolean skipExtendedContent) { 198 List<Section> sections = new ArrayList<Section>(); 199 if (is == null) { 200 return sections; 201 } 202 203 try { 204 if (is.read() != 0xff || is.read() != M_SOI) { 205 return sections; 206 } 207 int c; 208 while ((c = is.read()) != -1) { 209 if (c != 0xff) { 210 return sections; 211 } 212 // Skip padding bytes. 213 while ((c = is.read()) == 0xff) { 214 } 215 if (c == -1) { 216 return sections; 217 } 218 int marker = c; 219 if (marker == M_SOS) { 220 // M_SOS indicates the image data will follow and no metadata after 221 // that, so read all data at one time. 222 if (!readMetaOnly) { 223 Section section = new Section(); 224 section.marker = marker; 225 section.length = -1; 226 section.data = new byte[is.available()]; 227 is.read(section.data, 0, section.data.length); 228 sections.add(section); 229 } 230 return sections; 231 } 232 int lh = is.read(); 233 int ll = is.read(); 234 if (lh == -1 || ll == -1) { 235 return sections; 236 } 237 int length = lh << 8 | ll; 238 if (!readMetaOnly || marker == M_APP1) { 239 sections.add(readSection(is, length, marker, skipExtendedContent)); 240 } else { 241 // Skip this section since all EXIF/XMP meta will be in M_APP1 242 // section. 243 is.skip(length - 2); 244 } 245 } 246 return sections; 247 } catch (IOException e) { 248 System.out.println("Could not parse file." + e); 249 return sections; 250 } finally { 251 if (is != null) { 252 try { 253 is.close(); 254 } catch (IOException e) { 255 // Ignore. 256 } 257 } 258 } 259 } 260 261 /** 262 * Checks whether the byte array has XMP header. The XMP section contains 263 * a fixed length header XMP_HEADER. 264 * 265 * @param data XMP metadata 266 * @param header The header to look for 267 */ 268 private static boolean hasHeader(byte[] data, String header) { 269 if (data.length < header.length()) { 270 return false; 271 } 272 try { 273 byte[] buffer = new byte[header.length()]; 274 System.arraycopy(data, 0, buffer, 0, header.length()); 275 if (new String(buffer, "UTF-8").equals(header)) { 276 return true; 277 } 278 } catch (UnsupportedEncodingException e) { 279 return false; 280 } 281 return false; 282 } 283 284 private static Section readSection(InputStream is, int length, 285 int marker, boolean skipExtendedContent) throws IOException { 286 if (length - 2 < XMP_EXTENSION_HEADER_GUID_SIZE || !skipExtendedContent) { 287 Section section = new Section(); 288 section.marker = marker; 289 section.length = length; 290 section.data = new byte[length - 2]; 291 is.read(section.data, 0, length - 2); 292 return section; 293 } 294 295 byte[] header = new byte[XMP_EXTENSION_HEADER_GUID_SIZE]; 296 is.read(header, 0, header.length); 297 298 if (hasHeader(header, XMP_EXTENSION_HEADER) && skipExtendedContent) { 299 Section section = new Section(); 300 section.marker = marker; 301 section.length = header.length + 2; 302 section.data = header; 303 is.skip(length - 2 - header.length); 304 return section; 305 } 306 307 Section section = new Section(); 308 section.marker = marker; 309 section.length = length; 310 section.data = new byte[length - 2]; 311 System.arraycopy(header, 0, section.data, 0, header.length); 312 is.read(section.data, header.length, length - 2 - header.length); 313 return section; 314 } 315 316 /** 317 * Gets the end of the XMP meta content. If there is no packet wrapper, 318 * return data.length, otherwise return 1 + the position of last '>' 319 * without '?' before it. 320 * Usually the packet wrapper end is "<?xpacket end="w"?> but 321 * javax.xml.parsers.DocumentBuilder fails to parse it in android. 322 * 323 * @param data XMP metadata bytes 324 * @return The end of the XMP metadata content 325 */ 326 private static int getXMPContentEnd(byte[] data) { 327 for (int i = data.length - 1; i >= 1; --i) { 328 if (data[i] == '>') { 329 if (data[i - 1] != '?') { 330 return i + 1; 331 } 332 } 333 } 334 // It should not reach here for a valid XMP meta. 335 return data.length; 336 } 337 338 /** 339 * Parses the first valid XMP section. Any other valid XMP section will be 340 * ignored. 341 * 342 * @param sections The list of sections parse 343 * @return The parsed XMPMeta object 344 */ 345 private static XMPMeta parseFirstValidXMPSection(List<Section> sections) { 346 for (Section section : sections) { 347 if (hasHeader(section.data, XMP_HEADER)) { 348 int end = getXMPContentEnd(section.data); 349 byte[] buffer = new byte[end - XMP_HEADER.length()]; 350 System.arraycopy( 351 section.data, XMP_HEADER.length(), buffer, 0, buffer.length); 352 try { 353 XMPMeta result = XMPMetaFactory.parseFromBuffer(buffer); 354 return result; 355 } catch (XMPException e) { 356 System.out.println("XMP parse error " + e); 357 return null; 358 } 359 } 360 } 361 return null; 362 } 363 364 /** 365 * Checks there is an extended section with the given name. 366 * 367 * @param sections The list of sections to parse 368 * @param sectionName The name of the extended sections 369 * @return Whether there is an extended section with the given name 370 */ 371 private static boolean checkExtendedSectionExists(List<Section> sections, 372 String sectionName) { 373 String extendedHeader = XMP_EXTENSION_HEADER + sectionName + "\0"; 374 for (Section section : sections) { 375 if (hasHeader(section.data, extendedHeader)) { 376 return true; 377 } 378 } 379 return false; 380 } 381 382 /** 383 * Parses the extended XMP sections with the given name. All other sections 384 * will be ignored. 385 * 386 * @param sections The list of sections to parse 387 * @param sectionName The name of the extended sections 388 * @return The parsed XMPMeta object 389 */ 390 private static XMPMeta parseExtendedXMPSections(List<Section> sections, 391 String sectionName) { 392 String extendedHeader = XMP_EXTENSION_HEADER + sectionName + "\0"; 393 394 // Compute the size of the buffer to parse the extended sections. 395 List<Section> xmpSections = new ArrayList<Section>(); 396 List<Integer> xmpStartOffset = new ArrayList<Integer>(); 397 List<Integer> xmpEndOffset = new ArrayList<Integer>(); 398 int bufferSize = 0; 399 for (Section section : sections) { 400 if (hasHeader(section.data, extendedHeader)) { 401 int startOffset = extendedHeader.length() + XMP_EXTENSION_HEADER_OFFSET; 402 int endOffset = section.data.length; 403 bufferSize += Math.max(0, section.data.length - startOffset); 404 xmpSections.add(section); 405 xmpStartOffset.add(startOffset); 406 xmpEndOffset.add(endOffset); 407 } 408 } 409 if (bufferSize == 0) { 410 return null; 411 } 412 413 // Copy all the relevant sections' data into a buffer. 414 byte buffer[] = new byte[bufferSize]; 415 int offset = 0; 416 for (int i = 0; i < xmpSections.size(); ++i) { 417 Section section = xmpSections.get(i); 418 int startOffset = xmpStartOffset.get(i); 419 int endOffset = xmpEndOffset.get(i); 420 int length = endOffset - startOffset; 421 System.arraycopy( 422 section.data, startOffset, buffer, offset, length); 423 offset += length; 424 } 425 426 XMPMeta xmpExtended = null; 427 try { 428 xmpExtended = XMPMetaFactory.parseFromBuffer(buffer); 429 } catch (XMPException e) { 430 System.out.println("Extended XMP parse error " + e); 431 return null; 432 } 433 return xmpExtended; 434 } 435} 436