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