1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.camera.util; 18 19 import android.util.Log; 20 21 import com.adobe.xmp.XMPException; 22 import com.adobe.xmp.XMPMeta; 23 import com.adobe.xmp.XMPMetaFactory; 24 import com.adobe.xmp.options.SerializeOptions; 25 26 import java.io.FileInputStream; 27 import java.io.FileNotFoundException; 28 import java.io.FileOutputStream; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.OutputStream; 32 import java.io.UnsupportedEncodingException; 33 import java.util.ArrayList; 34 import java.util.List; 35 36 /** 37 * Util class to read/write xmp from a jpeg image file. It only supports jpeg 38 * image format, and doesn't support extended xmp now. 39 * To use it: 40 * XMPMeta xmpMeta = XmpUtil.extractOrCreateXMPMeta(filename); 41 * xmpMeta.setProperty(PanoConstants.GOOGLE_PANO_NAMESPACE, "property_name", "value"); 42 * XmpUtil.writeXMPMeta(filename, xmpMeta); 43 * 44 * Or if you don't care the existing XMP meta data in image file: 45 * XMPMeta xmpMeta = XmpUtil.createXMPMeta(); 46 * xmpMeta.setPropertyBoolean(PanoConstants.GOOGLE_PANO_NAMESPACE, "bool_property_name", "true"); 47 * XmpUtil.writeXMPMeta(filename, xmpMeta); 48 */ 49 public class XmpUtil { 50 private static final String TAG = "XmpUtil"; 51 private static final int XMP_HEADER_SIZE = 29; 52 private static final String XMP_HEADER = "http://ns.adobe.com/xap/1.0/\0"; 53 private static final int MAX_XMP_BUFFER_SIZE = 65502; 54 55 private static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/"; 56 private static final String PANO_PREFIX = "GPano"; 57 58 private static final int M_SOI = 0xd8; // File start marker. 59 private static final int M_APP1 = 0xe1; // Marker for Exif or XMP. 60 private static final int M_SOS = 0xda; // Image data marker. 61 62 // Jpeg file is composed of many sections and image data. This class is used 63 // to hold the section data from image file. 64 private static class Section { 65 public int marker; 66 public int length; 67 public byte[] data; 68 } 69 70 static { 71 try { 72 XMPMetaFactory.getSchemaRegistry().registerNamespace( 73 GOOGLE_PANO_NAMESPACE, PANO_PREFIX); 74 } catch (XMPException e) { 75 e.printStackTrace(); 76 } 77 } 78 79 /** 80 * Extracts XMPMeta from JPEG image file. 81 * 82 * @param filename JPEG image file name. 83 * @return Extracted XMPMeta or null. 84 */ 85 public static XMPMeta extractXMPMeta(String filename) { 86 if (!filename.toLowerCase().endsWith(".jpg") 87 && !filename.toLowerCase().endsWith(".jpeg")) { 88 Log.d(TAG, "XMP parse: only jpeg file is supported"); 89 return null; 90 } 91 92 try { 93 return extractXMPMeta(new FileInputStream(filename)); 94 } catch (FileNotFoundException e) { 95 Log.e(TAG, "Could not read file: " + filename, e); 96 return null; 97 } 98 } 99 100 /** 101 * Extracts XMPMeta from a JPEG image file stream. 102 * 103 * @param is the input stream containing the JPEG image file. 104 * @return Extracted XMPMeta or null. 105 */ 106 public static XMPMeta extractXMPMeta(InputStream is) { 107 List<Section> sections = parse(is, true); 108 if (sections == null) { 109 return null; 110 } 111 // Now we don't support extended xmp. 112 for (Section section : sections) { 113 if (hasXMPHeader(section.data)) { 114 int end = getXMPContentEnd(section.data); 115 byte[] buffer = new byte[end - XMP_HEADER_SIZE]; 116 System.arraycopy( 117 section.data, XMP_HEADER_SIZE, buffer, 0, buffer.length); 118 try { 119 XMPMeta result = XMPMetaFactory.parseFromBuffer(buffer); 120 return result; 121 } catch (XMPException e) { 122 Log.d(TAG, "XMP parse error", e); 123 return null; 124 } 125 } 126 } 127 return null; 128 } 129 130 /** 131 * Creates a new XMPMeta. 132 */ 133 public static XMPMeta createXMPMeta() { 134 return XMPMetaFactory.create(); 135 } 136 137 /** 138 * Tries to extract XMP meta from image file first, if failed, create one. 139 */ 140 public static XMPMeta extractOrCreateXMPMeta(String filename) { 141 XMPMeta meta = extractXMPMeta(filename); 142 return meta == null ? createXMPMeta() : meta; 143 } 144 145 /** 146 * Writes the XMPMeta to the jpeg image file. 147 */ 148 public static boolean writeXMPMeta(String filename, XMPMeta meta) { 149 if (!filename.toLowerCase().endsWith(".jpg") 150 && !filename.toLowerCase().endsWith(".jpeg")) { 151 Log.d(TAG, "XMP parse: only jpeg file is supported"); 152 return false; 153 } 154 List<Section> sections = null; 155 try { 156 sections = parse(new FileInputStream(filename), false); 157 sections = insertXMPSection(sections, meta); 158 if (sections == null) { 159 return false; 160 } 161 } catch (FileNotFoundException e) { 162 Log.e(TAG, "Could not read file: " + filename, e); 163 return false; 164 } 165 FileOutputStream os = null; 166 try { 167 // Overwrite the image file with the new meta data. 168 os = new FileOutputStream(filename); 169 writeJpegFile(os, sections); 170 } catch (IOException e) { 171 Log.d(TAG, "Write file failed:" + filename, e); 172 return false; 173 } finally { 174 if (os != null) { 175 try { 176 os.close(); 177 } catch (IOException e) { 178 // Ignore. 179 } 180 } 181 } 182 return true; 183 } 184 185 /** 186 * Updates a jpeg file from inputStream with XMPMeta to outputStream. 187 */ 188 public static boolean writeXMPMeta(InputStream inputStream, OutputStream outputStream, 189 XMPMeta meta) { 190 List<Section> sections = parse(inputStream, false); 191 sections = insertXMPSection(sections, meta); 192 if (sections == null) { 193 return false; 194 } 195 try { 196 // Overwrite the image file with the new meta data. 197 writeJpegFile(outputStream, sections); 198 } catch (IOException e) { 199 Log.d(TAG, "Write to stream failed", e); 200 return false; 201 } finally { 202 if (outputStream != null) { 203 try { 204 outputStream.close(); 205 } catch (IOException e) { 206 // Ignore. 207 } 208 } 209 } 210 return true; 211 } 212 213 /** 214 * Write a list of sections to a Jpeg file. 215 */ 216 private static void writeJpegFile(OutputStream os, List<Section> sections) 217 throws IOException { 218 // Writes the jpeg file header. 219 os.write(0xff); 220 os.write(M_SOI); 221 for (Section section : sections) { 222 os.write(0xff); 223 os.write(section.marker); 224 if (section.length > 0) { 225 // It's not the image data. 226 int lh = section.length >> 8; 227 int ll = section.length & 0xff; 228 os.write(lh); 229 os.write(ll); 230 } 231 os.write(section.data); 232 } 233 } 234 235 private static List<Section> insertXMPSection( 236 List<Section> sections, XMPMeta meta) { 237 if (sections == null || sections.size() <= 1) { 238 return null; 239 } 240 byte[] buffer; 241 try { 242 SerializeOptions options = new SerializeOptions(); 243 options.setUseCompactFormat(true); 244 // We have to omit packet wrapper here because 245 // javax.xml.parsers.DocumentBuilder 246 // fails to parse the packet end <?xpacket end="w"?> in android. 247 options.setOmitPacketWrapper(true); 248 buffer = XMPMetaFactory.serializeToBuffer(meta, options); 249 } catch (XMPException e) { 250 Log.d(TAG, "Serialize xmp failed", e); 251 return null; 252 } 253 if (buffer.length > MAX_XMP_BUFFER_SIZE) { 254 // Do not support extended xmp now. 255 return null; 256 } 257 // The XMP section starts with XMP_HEADER and then the real xmp data. 258 byte[] xmpdata = new byte[buffer.length + XMP_HEADER_SIZE]; 259 System.arraycopy(XMP_HEADER.getBytes(), 0, xmpdata, 0, XMP_HEADER_SIZE); 260 System.arraycopy(buffer, 0, xmpdata, XMP_HEADER_SIZE, buffer.length); 261 Section xmpSection = new Section(); 262 xmpSection.marker = M_APP1; 263 // Adds the length place (2 bytes) to the section length. 264 xmpSection.length = xmpdata.length + 2; 265 xmpSection.data = xmpdata; 266 267 for (int i = 0; i < sections.size(); ++i) { 268 // If we can find the old xmp section, replace it with the new one. 269 if (sections.get(i).marker == M_APP1 270 && hasXMPHeader(sections.get(i).data)) { 271 // Replace with the new xmp data. 272 sections.set(i, xmpSection); 273 return sections; 274 } 275 } 276 // If the first section is Exif, insert XMP data before the second section, 277 // otherwise, make xmp data the first section. 278 List<Section> newSections = new ArrayList<Section>(); 279 int position = (sections.get(0).marker == M_APP1) ? 1 : 0; 280 newSections.addAll(sections.subList(0, position)); 281 newSections.add(xmpSection); 282 newSections.addAll(sections.subList(position, sections.size())); 283 return newSections; 284 } 285 286 /** 287 * Checks whether the byte array has XMP header. The XMP section contains 288 * a fixed length header XMP_HEADER. 289 * 290 * @param data Xmp metadata. 291 */ 292 private static boolean hasXMPHeader(byte[] data) { 293 if (data.length < XMP_HEADER_SIZE) { 294 return false; 295 } 296 try { 297 byte[] header = new byte[XMP_HEADER_SIZE]; 298 System.arraycopy(data, 0, header, 0, XMP_HEADER_SIZE); 299 if (new String(header, "UTF-8").equals(XMP_HEADER)) { 300 return true; 301 } 302 } catch (UnsupportedEncodingException e) { 303 return false; 304 } 305 return false; 306 } 307 308 /** 309 * Gets the end of the xmp meta content. If there is no packet wrapper, 310 * return data.length, otherwise return 1 + the position of last '>' 311 * without '?' before it. 312 * Usually the packet wrapper end is "<?xpacket end="w"?> but 313 * javax.xml.parsers.DocumentBuilder fails to parse it in android. 314 * 315 * @param data xmp metadata bytes. 316 * @return The end of the xmp metadata content. 317 */ 318 private static int getXMPContentEnd(byte[] data) { 319 for (int i = data.length - 1; i >= 1; --i) { 320 if (data[i] == '>') { 321 if (data[i - 1] != '?') { 322 return i + 1; 323 } 324 } 325 } 326 // It should not reach here for a valid xmp meta. 327 return data.length; 328 } 329 330 /** 331 * Parses the jpeg image file. If readMetaOnly is true, only keeps the Exif 332 * and XMP sections (with marker M_APP1) and ignore others; otherwise, keep 333 * all sections. The last section with image data will have -1 length. 334 * 335 * @param is Input image data stream. 336 * @param readMetaOnly Whether only reads the metadata in jpg. 337 * @return The parse result. 338 */ 339 private static List<Section> parse(InputStream is, boolean readMetaOnly) { 340 try { 341 if (is.read() != 0xff || is.read() != M_SOI) { 342 return null; 343 } 344 List<Section> sections = new ArrayList<Section>(); 345 int c; 346 while ((c = is.read()) != -1) { 347 if (c != 0xff) { 348 return null; 349 } 350 // Skip padding bytes. 351 while ((c = is.read()) == 0xff) { 352 } 353 if (c == -1) { 354 return null; 355 } 356 int marker = c; 357 if (marker == M_SOS) { 358 // M_SOS indicates the image data will follow and no metadata after 359 // that, so read all data at one time. 360 if (!readMetaOnly) { 361 Section section = new Section(); 362 section.marker = marker; 363 section.length = -1; 364 section.data = new byte[is.available()]; 365 is.read(section.data, 0, section.data.length); 366 sections.add(section); 367 } 368 return sections; 369 } 370 int lh = is.read(); 371 int ll = is.read(); 372 if (lh == -1 || ll == -1) { 373 return null; 374 } 375 int length = lh << 8 | ll; 376 if (!readMetaOnly || c == M_APP1) { 377 Section section = new Section(); 378 section.marker = marker; 379 section.length = length; 380 section.data = new byte[length - 2]; 381 is.read(section.data, 0, length - 2); 382 sections.add(section); 383 } else { 384 // Skip this section since all exif/xmp meta will be in M_APP1 385 // section. 386 is.skip(length - 2); 387 } 388 } 389 return sections; 390 } catch (IOException e) { 391 Log.d(TAG, "Could not parse file.", e); 392 return null; 393 } finally { 394 if (is != null) { 395 try { 396 is.close(); 397 } catch (IOException e) { 398 // Ignore. 399 } 400 } 401 } 402 } 403 404 private XmpUtil() {} 405 } 406