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