All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.sanselan.formats.jpeg.exifRewrite.ExifRewriter Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sanselan.formats.jpeg.exifRewrite;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import org.apache.sanselan.ImageReadException;
import org.apache.sanselan.ImageWriteException;
import org.apache.sanselan.common.BinaryFileParser;
import org.apache.sanselan.common.byteSources.ByteSource;
import org.apache.sanselan.common.byteSources.ByteSourceArray;
import org.apache.sanselan.common.byteSources.ByteSourceFile;
import org.apache.sanselan.common.byteSources.ByteSourceInputStream;
import org.apache.sanselan.formats.jpeg.JpegConstants;
import org.apache.sanselan.formats.jpeg.JpegUtils;
import org.apache.sanselan.formats.tiff.write.TiffImageWriterBase;
import org.apache.sanselan.formats.tiff.write.TiffImageWriterLossless;
import org.apache.sanselan.formats.tiff.write.TiffImageWriterLossy;
import org.apache.sanselan.formats.tiff.write.TiffOutputSet;
import org.apache.sanselan.util.Debug;

/**
 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images.
 * 

*

* See the source of the ExifMetadataUpdateExample class for example usage. * * @see org.apache.sanselan.sampleUsage.WriteExifMetadataExample */ public class ExifRewriter extends BinaryFileParser implements JpegConstants { /** * Constructor. to guess whether a file contains an image based on its file extension. */ public ExifRewriter() { setByteOrder(BYTE_ORDER_NETWORK); } /** * Constructor. *

* @param byteOrder byte order of EXIF segment. Optional. See BinaryConstants class. * * @see org.apache.sanselan.common.BinaryConstants */ public ExifRewriter(int byteOrder) { setByteOrder(byteOrder); } private static class JFIFPieces { public final List pieces; public final List exifPieces; public JFIFPieces(final List pieces, final List exifPieces) { this.pieces = pieces; this.exifPieces = exifPieces; } } private abstract static class JFIFPiece { protected abstract void write(OutputStream os) throws IOException; } private static class JFIFPieceSegment extends JFIFPiece { public final int marker; public final byte markerBytes[]; public final byte markerLengthBytes[]; public final byte segmentData[]; public JFIFPieceSegment(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) { this.marker = marker; this.markerBytes = markerBytes; this.markerLengthBytes = markerLengthBytes; this.segmentData = segmentData; } protected void write(OutputStream os) throws IOException { os.write(markerBytes); os.write(markerLengthBytes); os.write(segmentData); } } private static class JFIFPieceSegmentExif extends JFIFPieceSegment { public JFIFPieceSegmentExif(final int marker, final byte[] markerBytes, final byte[] markerLengthBytes, final byte[] segmentData) { super(marker, markerBytes, markerLengthBytes, segmentData); } } private static class JFIFPieceImageData extends JFIFPiece { public final byte markerBytes[]; public final byte imageData[]; public JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) { super(); this.markerBytes = markerBytes; this.imageData = imageData; } protected void write(OutputStream os) throws IOException { os.write(markerBytes); os.write(imageData); } } private JFIFPieces analyzeJFIF(ByteSource byteSource) throws ImageReadException, IOException // , ImageWriteException { final ArrayList pieces = new ArrayList(); final List exifPieces = new ArrayList(); JpegUtils.Visitor visitor = new JpegUtils.Visitor() { // return false to exit before reading image data. public boolean beginSOS() { return true; } public void visitSOS(int marker, byte markerBytes[], byte imageData[]) { pieces.add(new JFIFPieceImageData(markerBytes, imageData)); } // return false to exit traversal. public boolean visitSegment(int marker, byte markerBytes[], int markerLength, byte markerLengthBytes[], byte segmentData[]) throws // ImageWriteException, ImageReadException, IOException { if (marker != JPEG_APP1_Marker) { pieces.add(new JFIFPieceSegment(marker, markerBytes, markerLengthBytes, segmentData)); } else if (!byteArrayHasPrefix(segmentData, EXIF_IDENTIFIER_CODE)) { pieces.add(new JFIFPieceSegment(marker, markerBytes, markerLengthBytes, segmentData)); } // else if (exifSegmentArray[0] != null) // { // // TODO: add support for multiple segments // throw new ImageReadException( // "More than one APP1 EXIF segment."); // } else { JFIFPiece piece = new JFIFPieceSegmentExif(marker, markerBytes, markerLengthBytes, segmentData); pieces.add(piece); exifPieces.add(piece); } return true; } }; new JpegUtils().traverseJFIF(byteSource, visitor); // GenericSegment exifSegment = exifSegmentArray[0]; // if (exifSegments.size() < 1) // { // // TODO: add support for adding, not just replacing. // throw new ImageReadException("No APP1 EXIF segment found."); // } return new JFIFPieces(pieces, exifPieces); } /** * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 segment), * and writes the result to a stream. *

* @param src Image file. * @param os OutputStream to write the image to. * * @see java.io.File * @see java.io.OutputStream * @see java.io.File * @see java.io.OutputStream */ public void removeExifMetadata(File src, OutputStream os) throws ImageReadException, IOException, ImageWriteException { ByteSource byteSource = new ByteSourceFile(src); removeExifMetadata(byteSource, os); } /** * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 segment), * and writes the result to a stream. *

* @param src Byte array containing Jpeg image data. * @param os OutputStream to write the image to. */ public void removeExifMetadata(byte src[], OutputStream os) throws ImageReadException, IOException, ImageWriteException { ByteSource byteSource = new ByteSourceArray(src); removeExifMetadata(byteSource, os); } /** * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 segment), * and writes the result to a stream. *

* @param src InputStream containing Jpeg image data. * @param os OutputStream to write the image to. */ public void removeExifMetadata(InputStream src, OutputStream os) throws ImageReadException, IOException, ImageWriteException { ByteSource byteSource = new ByteSourceInputStream(src, null); removeExifMetadata(byteSource, os); } /** * Reads a Jpeg image, removes all EXIF metadata (by removing the APP1 segment), * and writes the result to a stream. *

* @param byteSource ByteSource containing Jpeg image data. * @param os OutputStream to write the image to. */ public void removeExifMetadata(ByteSource byteSource, OutputStream os) throws ImageReadException, IOException, ImageWriteException { JFIFPieces jfifPieces = analyzeJFIF(byteSource); List pieces = jfifPieces.pieces; // Debug.debug("pieces", pieces); // pieces.removeAll(jfifPieces.exifSegments); // Debug.debug("pieces", pieces); writeSegmentsReplacingExif(os, pieces, null); } /** * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. *

* Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF * segment that it can't parse (such as Maker Notes), this algorithm avoids overwriting * any part of the original segment that it couldn't parse. This can cause the EXIF segment to * grow with each update, which is a serious issue, since all EXIF data must fit in a single APP1 * segment of the Jpeg image. *

* @param src Image file. * @param os OutputStream to write the image to. * @param outputSet TiffOutputSet containing the EXIF data to write. */ public void updateExifMetadataLossless(File src, OutputStream os, TiffOutputSet outputSet) throws ImageReadException, IOException, ImageWriteException { ByteSource byteSource = new ByteSourceFile(src); updateExifMetadataLossless(byteSource, os, outputSet); } /** * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. *

* Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF * segment that it can't parse (such as Maker Notes), this algorithm avoids overwriting * any part of the original segment that it couldn't parse. This can cause the EXIF segment to * grow with each update, which is a serious issue, since all EXIF data must fit in a single APP1 * segment of the Jpeg image. *

* @param src Byte array containing Jpeg image data. * @param os OutputStream to write the image to. * @param outputSet TiffOutputSet containing the EXIF data to write. */ public void updateExifMetadataLossless(byte src[], OutputStream os, TiffOutputSet outputSet) throws ImageReadException, IOException, ImageWriteException { ByteSource byteSource = new ByteSourceArray(src); updateExifMetadataLossless(byteSource, os, outputSet); } /** * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. *

* Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF * segment that it can't parse (such as Maker Notes), this algorithm avoids overwriting * any part of the original segment that it couldn't parse. This can cause the EXIF segment to * grow with each update, which is a serious issue, since all EXIF data must fit in a single APP1 * segment of the Jpeg image. *

* @param src InputStream containing Jpeg image data. * @param os OutputStream to write the image to. * @param outputSet TiffOutputSet containing the EXIF data to write. */ public void updateExifMetadataLossless(InputStream src, OutputStream os, TiffOutputSet outputSet) throws ImageReadException, IOException, ImageWriteException { ByteSource byteSource = new ByteSourceInputStream(src, null); updateExifMetadataLossless(byteSource, os, outputSet); } /** * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. *

* Note that this uses the "Lossless" approach - in order to preserve data embedded in the EXIF * segment that it can't parse (such as Maker Notes), this algorithm avoids overwriting * any part of the original segment that it couldn't parse. This can cause the EXIF segment to * grow with each update, which is a serious issue, since all EXIF data must fit in a single APP1 * segment of the Jpeg image. *

* @param byteSource ByteSource containing Jpeg image data. * @param os OutputStream to write the image to. * @param outputSet TiffOutputSet containing the EXIF data to write. */ public void updateExifMetadataLossless(ByteSource byteSource, OutputStream os, TiffOutputSet outputSet) throws ImageReadException, IOException, ImageWriteException { // List outputDirectories = outputSet.getDirectories(); JFIFPieces jfifPieces = analyzeJFIF(byteSource); List pieces = jfifPieces.pieces; TiffImageWriterBase writer; // Just use first APP1 segment for now. // Multiple APP1 segments are rare and poorly supported. if (jfifPieces.exifPieces.size() > 0) { JFIFPieceSegment exifPiece = null; exifPiece = (JFIFPieceSegment) jfifPieces.exifPieces.get(0); byte exifBytes[] = exifPiece.segmentData; exifBytes = getByteArrayTail("trimmed exif bytes", exifBytes, 6); writer = new TiffImageWriterLossless(outputSet.byteOrder, exifBytes); } else writer = new TiffImageWriterLossy(outputSet.byteOrder); boolean includeEXIFPrefix = true; byte newBytes[] = writeExifSegment(writer, outputSet, includeEXIFPrefix); writeSegmentsReplacingExif(os, pieces, newBytes); } /** * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. *

* Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, * ignoring the possibility that it may be discarding data it couldn't parse (such as Maker Notes). *

* @param src Byte array containing Jpeg image data. * @param os OutputStream to write the image to. * @param outputSet TiffOutputSet containing the EXIF data to write. */ public void updateExifMetadataLossy(byte src[], OutputStream os, TiffOutputSet outputSet) throws ImageReadException, IOException, ImageWriteException { ByteSource byteSource = new ByteSourceArray(src); updateExifMetadataLossy(byteSource, os, outputSet); } /** * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. *

* Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, * ignoring the possibility that it may be discarding data it couldn't parse (such as Maker Notes). *

* @param src InputStream containing Jpeg image data. * @param os OutputStream to write the image to. * @param outputSet TiffOutputSet containing the EXIF data to write. */ public void updateExifMetadataLossy(InputStream src, OutputStream os, TiffOutputSet outputSet) throws ImageReadException, IOException, ImageWriteException { ByteSource byteSource = new ByteSourceInputStream(src, null); updateExifMetadataLossy(byteSource, os, outputSet); } /** * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. *

* Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, * ignoring the possibility that it may be discarding data it couldn't parse (such as Maker Notes). *

* @param src Image file. * @param os OutputStream to write the image to. * @param outputSet TiffOutputSet containing the EXIF data to write. */ public void updateExifMetadataLossy(File src, OutputStream os, TiffOutputSet outputSet) throws ImageReadException, IOException, ImageWriteException { ByteSource byteSource = new ByteSourceFile(src); updateExifMetadataLossy(byteSource, os, outputSet); } /** * Reads a Jpeg image, replaces the EXIF metadata and writes the result to a stream. *

* Note that this uses the "Lossy" approach - the algorithm overwrites the entire EXIF segment, * ignoring the possibility that it may be discarding data it couldn't parse (such as Maker Notes). *

* @param byteSource ByteSource containing Jpeg image data. * @param os OutputStream to write the image to. * @param outputSet TiffOutputSet containing the EXIF data to write. */ public void updateExifMetadataLossy(ByteSource byteSource, OutputStream os, TiffOutputSet outputSet) throws ImageReadException, IOException, ImageWriteException { JFIFPieces jfifPieces = analyzeJFIF(byteSource); List pieces = jfifPieces.pieces; TiffImageWriterBase writer = new TiffImageWriterLossy( outputSet.byteOrder); boolean includeEXIFPrefix = true; byte newBytes[] = writeExifSegment(writer, outputSet, includeEXIFPrefix); writeSegmentsReplacingExif(os, pieces, newBytes); } private void writeSegmentsReplacingExif(OutputStream os, List segments, byte newBytes[]) throws ImageWriteException, IOException { int byteOrder = getByteOrder(); try { os.write(SOI); boolean hasExif = false; for (int i = 0; i < segments.size(); i++) { JFIFPiece piece = (JFIFPiece) segments.get(i); if (piece instanceof JFIFPieceSegmentExif) hasExif = true; } if (!hasExif && newBytes != null) { byte markerBytes[] = convertShortToByteArray(JPEG_APP1_Marker, byteOrder); if (newBytes.length > 0xffff) throw new ExifOverflowException( "APP1 Segment is too long: " + newBytes.length); int markerLength = newBytes.length + 2; byte markerLengthBytes[] = convertShortToByteArray( markerLength, byteOrder); int index = 0; JFIFPieceSegment firstSegment = (JFIFPieceSegment) segments .get(index); if (firstSegment.marker == JFIFMarker) index = 1; segments.add(0, new JFIFPieceSegmentExif(JPEG_APP1_Marker, markerBytes, markerLengthBytes, newBytes)); } boolean APP1Written = false; for (int i = 0; i < segments.size(); i++) { JFIFPiece piece = (JFIFPiece) segments.get(i); if (piece instanceof JFIFPieceSegmentExif) { // only replace first APP1 segment; skips others. if (APP1Written) continue; APP1Written = true; if (newBytes == null) continue; byte markerBytes[] = convertShortToByteArray( JPEG_APP1_Marker, byteOrder); if (newBytes.length > 0xffff) throw new ExifOverflowException( "APP1 Segment is too long: " + newBytes.length); int markerLength = newBytes.length + 2; byte markerLengthBytes[] = convertShortToByteArray( markerLength, byteOrder); os.write(markerBytes); os.write(markerLengthBytes); os.write(newBytes); } else { piece.write(os); } } } finally { try { os.close(); } catch (Exception e) { Debug.debug(e); } } } public static class ExifOverflowException extends ImageWriteException { public ExifOverflowException(String s) { super(s); } } private byte[] writeExifSegment(TiffImageWriterBase writer, TiffOutputSet outputSet, boolean includeEXIFPrefix) throws IOException, ImageWriteException { ByteArrayOutputStream os = new ByteArrayOutputStream(); if (includeEXIFPrefix) { os.write(EXIF_IDENTIFIER_CODE); os.write(0); os.write(0); } writer.write(os, outputSet); return os.toByteArray(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy