Home | History | Annotate | Download | only in util
      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    */
     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