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
17package com.android.camera.util;
18
19import com.adobe.xmp.XMPException;
20import com.adobe.xmp.XMPMeta;
21import com.adobe.xmp.XMPMetaFactory;
22import com.adobe.xmp.options.SerializeOptions;
23import com.android.camera.debug.Log;
24
25import java.io.FileInputStream;
26import java.io.FileNotFoundException;
27import java.io.FileOutputStream;
28import java.io.IOException;
29import java.io.InputStream;
30import java.io.OutputStream;
31import java.io.UnsupportedEncodingException;
32import java.util.ArrayList;
33import 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 */
48public 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   */
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   */
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   */
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   */
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   */
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   */
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   */
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
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   */
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   */
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   */
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
403  private XmpUtil() {}
404}
405