Package net.sourceforge.gpstools.xmp

Source Code of net.sourceforge.gpstools.xmp.XMPJpeg

package net.sourceforge.gpstools.xmp;

/* gpsdings
* Copyright (C) 2007 Moritz Ringler
* $Id: XMPJpeg.java 441 2010-12-13 20:04:20Z ringler $
*
*  This program is free software: you can redistribute it and/or modify
*  it under the terms of the GNU General Public License as published by
*  the Free Software Foundation, either version 3 of the License, or
*  (at your option) any later version.
*
*  This program is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU General Public License for more details.
*
*  You should have received a copy of the GNU General Public License
*  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;
import java.util.List;
import net.sourceforge.gpstools.jpeg.*;
import static net.sourceforge.gpstools.jpeg.JpegStructure.Segment;

/**
* Handles extraction and insertion of XMP from/to APP1 marker segments in jpeg
* files.
* <p>
* The XMP Specification explicitly says <cite>"The XMP Packet cannot be split
* across the multiple APP1 sections, so the size of the XMP Packet can be at
* most 65502 bytes."</cite> However the C++ class JPEG_Handler in the Adobe XMP
* toolkit does exactly that: it splits XMP packets accross multiple APP1
* segments marking the extension segments with
* "http://ns.adobe.com/xmp/extension/".<br>
* This class does not support the unspecified extension mechanism. It will
* throw an XMPReadException when encountering an APP1 marker with a
* "http://ns.adobe.com/xmp/extension/" identifier and will throw an
* XMPWriteException when the client app tries to write XMP larger than 65502
* bytes.
*
* @see net.sourceforge.gpstools.jpeg.JpegStructure
**/
public class XMPJpeg {
    private static final String XMP_IDENTIFIER = "http://ns.adobe.com/xap/1.0/\u0000";
    private static final int XMP_IDENTIFIER_LENGTH = XMP_IDENTIFIER.length();
    private static final String XMP_EXT_IDENTIFIER = "http://ns.adobe.com/xmp/extension/\u0000";
    private static final int XMP_EXT_IDENTIFIER_LENGTH = XMP_EXT_IDENTIFIER
            .length();
    private final FileChannel src;
    /**
     * An existing XMP segment (length > 0) or the place where one should be
     * inserted (length = 0)
     **/
    private Segment xmpSegment;
    private final JpegStructure structure;
    private final boolean writable;

    /**
     * Constructs a new XMPJpeg. The input is immediately parsed. Clients should
     * lock the source channel using {@link FileChannel#tryLock} before passing
     * it to this constructor.
     *
     * @param jpeg
     *            an input file channel that is open for reading. This channel
     *            must not be in append mode.
     * @param writable
     *            whether or not <code>jpeg</code> is open for writing. If so
     *            the {@link #setXMP} and {@link #setXMPSegment} methods will
     *            edit the input file in place if possible
     * @throws XMPReadException
     *             if the Jpeg header cannot be parsed, in particular if an XMP
     *             Extension APP1 segment or multiple XMP APP1 segments are
     *             found
     */
    public XMPJpeg(final FileChannel jpeg, final boolean writable)
            throws IOException, XMPReadException {
        src = jpeg;
        this.writable = writable;
        try {
            structure = new JpegStructure(src);
        } catch (JpegException jx) {
            throw new XMPReadException(jx);
        }
        List<Segment> app1 = structure.getSegments(Marker.APP1);
        for (Segment sapp1 : app1) {
            handleAPP1Segment(sapp1);
        }
        // We have no XMP but other APP1, insert XMP behind other APP1
        if (xmpSegment == null && !app1.isEmpty()) {
            Segment lastAPP1 = app1.get(app1.size() - 1);
            xmpSegment = new Segment(Marker.APP1, lastAPP1.offset
                    + lastAPP1.length, 0);
        }
        // We have no APP1 but APP0, insert XMP behind APP0
        if (xmpSegment == null) {
            List<Segment> app0 = structure.getSegments(Marker.APP0);
            if (!app0.isEmpty()) {
                Segment lastAPP0 = app0.get(app0.size() - 1);
                xmpSegment = new Segment(Marker.APP1, lastAPP0.offset
                        + lastAPP0.length, 0);
            }
        }
        // We have neither XMP nor EXIF nor APP0, insert behind SOI
        if (xmpSegment == null) {
            List<Segment> soi = structure.getSegments(Marker.SOI);
            if (soi.isEmpty()) {
                throw new XMPReadException("No SOI marker found.");
            }
            xmpSegment = new Segment(Marker.APP1, soi.get(0).offset, 0);
        }
    }

    /** Returns the file Channel that this XMPJpeg was constructed from. */
    public FileChannel getFileChannel() {
        return src;
    }

    /** Returns the structure of the JPEG file. **/
    public JpegStructure getStructure() {
        return structure;
    }

    /**
     * Stores the XMP APP1 segment. This method should only be used during the
     * initial analysis of a jpeg file.
     **/
    private void addXMPSegment(Segment app1) throws XMPReadException {
        if (xmpSegment != null) {
            throw new XMPReadException(
                    "More than one XMP segment in source file. Giving up.");
        }
        xmpSegment = app1;
    }

    /** Tests APP1 segments found in the jpeg if they are XMP segments. */
    private void handleAPP1Segment(Segment app1) throws XMPReadException,
            IOException {
        ByteBuffer.allocate(Math.max(XMP_IDENTIFIER_LENGTH,
                XMP_EXT_IDENTIFIER_LENGTH));
        final int seglen = app1.length;
        synchronized (src) {
            if (seglen >= XMP_IDENTIFIER_LENGTH) {
                src.position(app1.offset);
                if (test(XMP_IDENTIFIER)) {
                    addXMPSegment(app1);
                    return;
                }
            }
            if (seglen >= XMP_EXT_IDENTIFIER_LENGTH) {
                src.position(app1.offset);
                if (test(XMP_EXT_IDENTIFIER)) {
                    throw new XMPReadException(
                            "Found extended XMP. This XMP reader currently does not read extended XMP.");
                }
            }
        }
    }

    /**
     * Returns pre-existing XMP data found in the JPEG file.
     *
     * @return the XMP data <em>without</em> 0xFF APP1 marker, length and XMP
     *         identifier or null if none exists.
     **/
    public ByteBuffer getXMP() throws IOException {
        return (xmpSegment.length == 0) ? null : getChunk(xmpSegment.offset
                + XMP_IDENTIFIER_LENGTH, xmpSegment.length
                - XMP_IDENTIFIER_LENGTH);
    }

    /**
     * Saves the JPEG file with new XMP data. If
     * <ul>
     * <li>the new XMP data is smaller or equal in size to the old XMP data and</li>
     * <li>the file channel that this XMPJpeg was constructed on is writable</li>
     * </ul>
     * this method will edit the input file in place. Otherwise a new file will
     * be created and returned.
     *
     * @param xmp
     *            a bare XMP xpacket <em>without</em> 0xFF APP1 marker, length,
     *            XMP identifier, but including xpacket container and padding
     * @return a new file where the XMP information has been changed or
     *         <code>null</code> if the file has been edited in place.
     * @throws XMPWriteException
     *             if xmp.remaining() is larger than 65502
     * @see #setXMPSegment
     * @see XMPTree#toXPacket
     **/
    public File setXMP(ByteBuffer xmp) throws IOException, XMPWriteException {
        int size = xmp.remaining();
        if (size > 65502) {
            throw new XMPWriteException(
                    "XMP xpacket is too large, size may not exceed 65502 bytes.");
        }
        size += 2 + XMP_IDENTIFIER_LENGTH;
        ByteBuffer sxmp = ByteBuffer.allocate(size + 2);
        sxmp.put((byte) 0xFF);
        sxmp.put((byte) Marker.APP1.intValue());
        sxmp.put((byte) (size >> 8));
        sxmp.put((byte) size);
        sxmp.put(XMP_IDENTIFIER.getBytes());
        sxmp.put(xmp);
        sxmp.flip();
        return setXMPSegment(sxmp);
    }

    /**
     * Returns the specified chunk of the input file.
     *
     * @param offset
     *            the offset of the first byte to be retrieved
     * @param len
     *            the number of bytes to retrieve
     * @return an array-backed byte buffer with position 0 and limit
     *         <code>len</code
     **/
    private ByteBuffer getChunk(long offset, int len) throws IOException {
        ByteBuffer buff = ByteBuffer.allocate(len);
        synchronized (src) {
            src.position(offset);
            while (buff.hasRemaining()) {
                if (src.read(buff) == -1) {
                    throw new IOException("Premature end of file.");
                }
            }
        }
        buff.flip();
        return buff;
    }

    /**
     * @return the XMP data <em>including</em> 0xFF APP1 marker, length and XMP
     *         identifier or null if none exists.
     **/
    public ByteBuffer getXMPSegment() throws IOException {
        return (xmpSegment.length == 0) ? null : getChunk(
                xmpSegment.offset - 4, xmpSegment.length + 4);
    }

    /**
     * Inserts the contents of the xmp byte buffer at the position of the XMP
     * segment.
     */
    private void insertXMPInPlace(ByteBuffer xmp) throws IOException {
        synchronized (src) {
            src.position(xmpSegment.offset);
            xmp.position(4); // do not overwrite APP1 marker and length bytes
            while (xmp.hasRemaining()) {
                src.write(xmp);
            }
        }
    }

    /**
     * @param xmp
     *            a full XMP APP1 segment including 0xFF APP1 marker, length,
     *            XMP identifier, xpacket container and padding. No validity
     *            check is performed.
     * @return a new file where the XMP information has been changed or
     *         <code>null</code> if the file has been edited in place.
     **/
    public File setXMPSegment(ByteBuffer xmp) throws IOException {
        int xmpSize = xmp.remaining();
        if (writable && xmpSize == xmpSegment.length + 4) {
            insertXMPInPlace(xmp);
            return null;
        }

        File tmpFile = File.createTempFile("xmptmp", ".jpg");
        long startXMP = (xmpSegment.length == 0) ? xmpSegment.offset
                : xmpSegment.offset - 4; // don't transfer app1 marker and
                                         // length
        long endXMP = xmpSegment.offset + xmpSegment.length;
        synchronized (src) {
            FileOutputStream fos = new FileOutputStream(tmpFile);
            try {
                FileChannel dst = fos.getChannel();
                src.transferTo(0, startXMP, dst);
                while (xmp.hasRemaining()) {
                    dst.write(xmp);
                }
                src.transferTo(endXMP, src.size() - endXMP, dst);
            } finally {
                fos.close();
            }
        }
        return tmpFile;
    }

    private boolean test(String sid) throws IOException, XMPReadException {
        byte[] id = new byte[sid.length()];
        ByteBuffer buff = ByteBuffer.wrap(id);
        while (buff.hasRemaining()) {
            if (src.read(buff) == -1) {
                return false;
            }
        }
        String bid = new String(id);
        return sid.equals(bid);
    }

}
TOP

Related Classes of net.sourceforge.gpstools.xmp.XMPJpeg

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.