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 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