1package com.android.rs.refocus;
2
3import android.graphics.Bitmap;
4import android.graphics.BitmapFactory;
5import android.util.Log;
6
7import com.adobe.xmp.XMPConst;
8import com.adobe.xmp.XMPException;
9import com.adobe.xmp.XMPIterator;
10import com.adobe.xmp.XMPMeta;
11import com.adobe.xmp.XMPMetaFactory;
12import com.adobe.xmp.properties.XMPPropertyInfo;
13
14import java.io.ByteArrayInputStream;
15import java.io.IOException;
16import java.io.InputStream;
17import java.io.UnsupportedEncodingException;
18import java.util.ArrayList;
19import java.util.List;
20
21import com.android.rs.refocus.image.RangeInverseDepthTransform;
22import com.android.rs.refocus.image.RangeLinearDepthTransform;
23
24
25/**
26 * Created by hoford on 5/15/15.
27 */
28public class XmpDepthDecode {
29    private static final String TAG = "XmpUtil";
30    private static final String XMP_DEPTHMAP = "http://ns.google.com/photos/1.0/depthmap/";
31    private static final String XMP_FOCUS = "http://ns.google.com/photos/1.0/focus/";
32    private static final String XMP_HEADER = "http://ns.adobe.com/xap/1.0/\0";
33    private static final String XMP_EXTENSION_HEADER =
34            "http://ns.adobe.com/xmp/extension/\0";
35    private static final String XMP_HAS_EXTENSION = "HasExtendedXMP";
36    private static final int XMP_EXTENSION_HEADER_GUID_SIZE =
37            XMP_EXTENSION_HEADER.length() + 32 + 1; // 32 byte GUID + 1 byte null termination.
38    private static final int XMP_EXTENSION_HEADER_OFFSET = 7;
39
40    private static final int M_SOI = 0xd8; // File start marker.
41    private static final int M_APP1 = 0xe1; // Marker for EXIF or XMP.
42    private static final int M_SOS = 0xda; // Image data marker.
43
44    private final String mFormat;
45    private final double mFar;
46    private final double mNear;
47    private final Bitmap mDepthBitmap;
48    private final double mBlurAtInfinity;
49    private final double mFocalDistance;
50    private final double mDepthOfFiled;
51    private final double mFocalPointX;
52    private final double mFocalPointY;
53    private final DepthTransform mDepthTransform;
54
55    public XmpDepthDecode(InputStream is) throws IOException {
56        XMPMeta meta = read(is, false);
57        try {
58            mFormat = meta.getPropertyString(XMP_DEPTHMAP, "GDepth:Format");
59
60            mFar = Double.parseDouble(meta.getPropertyString(XMP_DEPTHMAP, "GDepth:Far"));
61            mNear = Double.parseDouble(meta.getPropertyString(XMP_DEPTHMAP, "GDepth:Near"));
62
63            DepthTransform tDepthTransform = null;
64            String format = meta.getPropertyString(
65                    XMP_DEPTHMAP, "GDepth:Format");
66            if (RangeInverseDepthTransform.FORMAT.equals(format)) {
67              tDepthTransform = new RangeInverseDepthTransform((float)mNear, (float)mFar);
68            } else if (RangeLinearDepthTransform.FORMAT.equals(format)) {
69              tDepthTransform = new RangeLinearDepthTransform((float)mNear, (float)mFar);
70            } else {
71              Log.e(TAG, "Unknown GDepth format: " + format);
72            }
73            mDepthTransform = tDepthTransform;
74
75            byte[] data = meta.getPropertyBase64(XMP_DEPTHMAP, "GDepth:Data");
76            mDepthBitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(data));
77
78            mBlurAtInfinity = Double.parseDouble(meta.getPropertyString(XMP_FOCUS, "GFocus:BlurAtInfinity"));
79            mFocalDistance = Double.parseDouble(meta.getPropertyString(XMP_FOCUS, "GFocus:FocalDistance"));
80            mDepthOfFiled = Double.parseDouble(meta.getPropertyString(XMP_FOCUS, "GFocus:DepthOfField"));
81            mFocalPointX = Double.parseDouble(meta.getPropertyString(XMP_FOCUS, "GFocus:FocalPointX"));
82            mFocalPointY = Double.parseDouble(meta.getPropertyString(XMP_FOCUS, "GFocus:FocalPointY"));
83        } catch (XMPException e) {
84            throw new IOException("XMP data missing");
85        }
86    }
87
88    public Bitmap getDepthBitmap() {
89        return mDepthBitmap;
90    }
91
92    public DepthTransform getDepthTransform() { return  mDepthTransform; }
93
94    public String getFormat() {
95        return mFormat;
96    }
97
98    public double getFar() {
99        return mFar;
100    }
101
102    public double getNear() {
103        return mNear;
104    }
105
106    public double getBlurAtInfinity() {
107        return mBlurAtInfinity;
108    }
109
110    public double getFocalDistance() {
111        return mFocalDistance;
112    }
113
114    public double getDepthOfField() { return mDepthOfFiled; }
115
116    public double getFocalPointX() {
117        return mFocalPointX;
118    }
119
120    public double getFocalPointY() {
121        return mFocalPointY;
122    }
123
124
125    // JPEG file is composed of many sections and image data. This class is used
126    // to hold the section data from image file.
127    private static class Section {
128        public int marker;
129        public int length;
130        public byte[] data;
131    }
132
133    static XMPMeta read(InputStream is, boolean skipExtendedContent) {
134        List<Section> sections = parse(is, true, skipExtendedContent);
135        if (sections == null) {
136            return null;
137        }
138
139        XMPMeta xmpMeta = parseFirstValidXMPSection(sections);
140        if (xmpMeta == null ||
141                !xmpMeta.doesPropertyExist(XMPConst.NS_XMP_NOTE, XMP_HAS_EXTENSION)) {
142            return xmpMeta;
143        }
144
145        String extensionName = null;
146        try {
147            extensionName = (String) xmpMeta.getProperty(
148                    XMPConst.NS_XMP_NOTE, XMP_HAS_EXTENSION).getValue();
149        } catch (XMPException e) {
150            e.printStackTrace();
151            return null;
152        }
153
154        if (skipExtendedContent) {
155            if (!checkExtendedSectionExists(sections, extensionName)) {
156                // The main XMP section referenced an extended section that is not present.
157                // This is an error.
158                return null;
159            }
160            return xmpMeta;
161        }
162
163        XMPMeta xmpExtended = parseExtendedXMPSections(sections, extensionName);
164        if (xmpExtended == null) {
165            // The main XMP section referenced an extended section that is not present.
166            // This is an error.
167            return null;
168        }
169
170        // Merge the extended properties into the main one.
171        try {
172            XMPIterator iterator = xmpExtended.iterator();
173            while (true) {
174                XMPPropertyInfo info = (XMPPropertyInfo) iterator.next();
175                if (info.getPath() != null) {
176                    xmpMeta.setProperty(info.getNamespace(), info.getPath(),
177                            info.getValue(), info.getOptions());
178                }
179            }
180        } catch (Exception e) {
181            // Catch XMPException and NoSuchElementException.
182        }
183        return xmpMeta;
184    }
185
186    /**
187     * Parses the JPEG image file. If readMetaOnly is true, only keeps the EXIF
188     * and XMP sections (with marker M_APP1) and ignore others; otherwise, keep
189     * all sections. The last section with image data will have -1 length.
190     *
191     * @param is                  Input image data stream
192     * @param readMetaOnly        Whether only reads the metadata in jpg
193     * @param skipExtendedContent Whether to skip the content of extended sections
194     * @return The parse result
195     */
196    private static List<Section> parse(InputStream is, boolean readMetaOnly,
197                                       boolean skipExtendedContent) {
198        List<Section> sections = new ArrayList<Section>();
199        if (is == null) {
200            return sections;
201        }
202
203        try {
204            if (is.read() != 0xff || is.read() != M_SOI) {
205                return sections;
206            }
207            int c;
208            while ((c = is.read()) != -1) {
209                if (c != 0xff) {
210                    return sections;
211                }
212                // Skip padding bytes.
213                while ((c = is.read()) == 0xff) {
214                }
215                if (c == -1) {
216                    return sections;
217                }
218                int marker = c;
219                if (marker == M_SOS) {
220                    // M_SOS indicates the image data will follow and no metadata after
221                    // that, so read all data at one time.
222                    if (!readMetaOnly) {
223                        Section section = new Section();
224                        section.marker = marker;
225                        section.length = -1;
226                        section.data = new byte[is.available()];
227                        is.read(section.data, 0, section.data.length);
228                        sections.add(section);
229                    }
230                    return sections;
231                }
232                int lh = is.read();
233                int ll = is.read();
234                if (lh == -1 || ll == -1) {
235                    return sections;
236                }
237                int length = lh << 8 | ll;
238                if (!readMetaOnly || marker == M_APP1) {
239                    sections.add(readSection(is, length, marker, skipExtendedContent));
240                } else {
241                    // Skip this section since all EXIF/XMP meta will be in M_APP1
242                    // section.
243                    is.skip(length - 2);
244                }
245            }
246            return sections;
247        } catch (IOException e) {
248            System.out.println("Could not parse file." + e);
249            return sections;
250        } finally {
251            if (is != null) {
252                try {
253                    is.close();
254                } catch (IOException e) {
255                    // Ignore.
256                }
257            }
258        }
259    }
260
261    /**
262     * Checks whether the byte array has XMP header. The XMP section contains
263     * a fixed length header XMP_HEADER.
264     *
265     * @param data   XMP metadata
266     * @param header The header to look for
267     */
268    private static boolean hasHeader(byte[] data, String header) {
269        if (data.length < header.length()) {
270            return false;
271        }
272        try {
273            byte[] buffer = new byte[header.length()];
274            System.arraycopy(data, 0, buffer, 0, header.length());
275            if (new String(buffer, "UTF-8").equals(header)) {
276                return true;
277            }
278        } catch (UnsupportedEncodingException e) {
279            return false;
280        }
281        return false;
282    }
283
284    private static Section readSection(InputStream is, int length,
285                                       int marker, boolean skipExtendedContent) throws IOException {
286        if (length - 2 < XMP_EXTENSION_HEADER_GUID_SIZE || !skipExtendedContent) {
287            Section section = new Section();
288            section.marker = marker;
289            section.length = length;
290            section.data = new byte[length - 2];
291            is.read(section.data, 0, length - 2);
292            return section;
293        }
294
295        byte[] header = new byte[XMP_EXTENSION_HEADER_GUID_SIZE];
296        is.read(header, 0, header.length);
297
298        if (hasHeader(header, XMP_EXTENSION_HEADER) && skipExtendedContent) {
299            Section section = new Section();
300            section.marker = marker;
301            section.length = header.length + 2;
302            section.data = header;
303            is.skip(length - 2 - header.length);
304            return section;
305        }
306
307        Section section = new Section();
308        section.marker = marker;
309        section.length = length;
310        section.data = new byte[length - 2];
311        System.arraycopy(header, 0, section.data, 0, header.length);
312        is.read(section.data, header.length, length - 2 - header.length);
313        return section;
314    }
315
316    /**
317     * Gets the end of the XMP meta content. If there is no packet wrapper,
318     * return data.length, otherwise return 1 + the position of last '>'
319     * without '?' before it.
320     * Usually the packet wrapper end is "<?xpacket end="w"?> but
321     * javax.xml.parsers.DocumentBuilder fails to parse it in android.
322     *
323     * @param data XMP metadata bytes
324     * @return The end of the XMP metadata content
325     */
326    private static int getXMPContentEnd(byte[] data) {
327        for (int i = data.length - 1; i >= 1; --i) {
328            if (data[i] == '>') {
329                if (data[i - 1] != '?') {
330                    return i + 1;
331                }
332            }
333        }
334        // It should not reach here for a valid XMP meta.
335        return data.length;
336    }
337
338    /**
339     * Parses the first valid XMP section. Any other valid XMP section will be
340     * ignored.
341     *
342     * @param sections The list of sections parse
343     * @return The parsed XMPMeta object
344     */
345    private static XMPMeta parseFirstValidXMPSection(List<Section> sections) {
346        for (Section section : sections) {
347            if (hasHeader(section.data, XMP_HEADER)) {
348                int end = getXMPContentEnd(section.data);
349                byte[] buffer = new byte[end - XMP_HEADER.length()];
350                System.arraycopy(
351                        section.data, XMP_HEADER.length(), buffer, 0, buffer.length);
352                try {
353                    XMPMeta result = XMPMetaFactory.parseFromBuffer(buffer);
354                    return result;
355                } catch (XMPException e) {
356                    System.out.println("XMP parse error " + e);
357                    return null;
358                }
359            }
360        }
361        return null;
362    }
363
364    /**
365     * Checks there is an extended section with the given name.
366     *
367     * @param sections    The list of sections to parse
368     * @param sectionName The name of the extended sections
369     * @return Whether there is an extended section with the given name
370     */
371    private static boolean checkExtendedSectionExists(List<Section> sections,
372                                                      String sectionName) {
373        String extendedHeader = XMP_EXTENSION_HEADER + sectionName + "\0";
374        for (Section section : sections) {
375            if (hasHeader(section.data, extendedHeader)) {
376                return true;
377            }
378        }
379        return false;
380    }
381
382    /**
383     * Parses the extended XMP sections with the given name. All other sections
384     * will be ignored.
385     *
386     * @param sections    The list of sections to parse
387     * @param sectionName The name of the extended sections
388     * @return The parsed XMPMeta object
389     */
390    private static XMPMeta parseExtendedXMPSections(List<Section> sections,
391                                                    String sectionName) {
392        String extendedHeader = XMP_EXTENSION_HEADER + sectionName + "\0";
393
394        // Compute the size of the buffer to parse the extended sections.
395        List<Section> xmpSections = new ArrayList<Section>();
396        List<Integer> xmpStartOffset = new ArrayList<Integer>();
397        List<Integer> xmpEndOffset = new ArrayList<Integer>();
398        int bufferSize = 0;
399        for (Section section : sections) {
400            if (hasHeader(section.data, extendedHeader)) {
401                int startOffset = extendedHeader.length() + XMP_EXTENSION_HEADER_OFFSET;
402                int endOffset = section.data.length;
403                bufferSize += Math.max(0, section.data.length - startOffset);
404                xmpSections.add(section);
405                xmpStartOffset.add(startOffset);
406                xmpEndOffset.add(endOffset);
407            }
408        }
409        if (bufferSize == 0) {
410            return null;
411        }
412
413        // Copy all the relevant sections' data into a buffer.
414        byte buffer[] = new byte[bufferSize];
415        int offset = 0;
416        for (int i = 0; i < xmpSections.size(); ++i) {
417            Section section = xmpSections.get(i);
418            int startOffset = xmpStartOffset.get(i);
419            int endOffset = xmpEndOffset.get(i);
420            int length = endOffset - startOffset;
421            System.arraycopy(
422                    section.data, startOffset, buffer, offset, length);
423            offset += length;
424        }
425
426        XMPMeta xmpExtended = null;
427        try {
428            xmpExtended = XMPMetaFactory.parseFromBuffer(buffer);
429        } catch (XMPException e) {
430            System.out.println("Extended XMP parse error " + e);
431            return null;
432        }
433        return xmpExtended;
434    }
435}
436