| /* |
| * 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.commons.sanselan.formats.jpeg.exif; |
| |
| 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.commons.sanselan.ImageReadException; |
| import org.apache.commons.sanselan.ImageWriteException; |
| import org.apache.commons.sanselan.common.BinaryFileParser; |
| import org.apache.commons.sanselan.common.bytesource.ByteSource; |
| import org.apache.commons.sanselan.common.bytesource.ByteSourceArray; |
| import org.apache.commons.sanselan.common.bytesource.ByteSourceFile; |
| import org.apache.commons.sanselan.common.bytesource.ByteSourceInputStream; |
| import org.apache.commons.sanselan.formats.jpeg.JpegConstants; |
| import org.apache.commons.sanselan.formats.jpeg.JpegUtils; |
| import org.apache.commons.sanselan.formats.tiff.write.TiffImageWriterBase; |
| import org.apache.commons.sanselan.formats.tiff.write.TiffImageWriterLossless; |
| import org.apache.commons.sanselan.formats.tiff.write.TiffImageWriterLossy; |
| import org.apache.commons.sanselan.formats.tiff.write.TiffOutputSet; |
| import org.apache.commons.sanselan.util.Debug; |
| |
| /** |
| * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. |
| * <p> |
| * <p> |
| * See the source of the ExifMetadataUpdateExample class for example usage. |
| * |
| * @see org.apache.commons.sanselan.examples.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. |
| * <p> |
| * @param byteOrder byte order of EXIF segment. Optional. See BinaryConstants class. |
| * |
| * @see org.apache.commons.sanselan.common.BinaryConstants |
| */ |
| public ExifRewriter(int byteOrder) |
| { |
| setByteOrder(byteOrder); |
| } |
| |
| private static class JFIFPieces |
| { |
| public final List<JFIFPiece> pieces; |
| public final List<JFIFPiece> exifPieces; |
| |
| public JFIFPieces(final List<JFIFPiece> pieces, final List<JFIFPiece> 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 List<JFIFPiece> pieces = new ArrayList<JFIFPiece>(); |
| final List<JFIFPiece> exifPieces = new ArrayList<JFIFPiece>(); |
| |
| 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. |
| * <p> |
| * @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. |
| * <p> |
| * @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. |
| * <p> |
| * @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. |
| * <p> |
| * @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<JFIFPiece> 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. |
| * <p> |
| * 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. |
| * <p> |
| * @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. |
| * <p> |
| * 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. |
| * <p> |
| * @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. |
| * <p> |
| * 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. |
| * <p> |
| * @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. |
| * <p> |
| * 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. |
| * <p> |
| * @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<JFIFPiece> 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. |
| * <p> |
| * 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). |
| * <p> |
| * @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. |
| * <p> |
| * 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). |
| * <p> |
| * @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. |
| * <p> |
| * 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). |
| * <p> |
| * @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. |
| * <p> |
| * 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). |
| * <p> |
| * @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<JFIFPiece> 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<JFIFPiece> segments, |
| byte newBytes[]) throws ImageWriteException, IOException |
| { |
| int byteOrder = getByteOrder(); |
| |
| try |
| { |
| SOI.writeTo(os); |
| |
| boolean hasExif = false; |
| |
| for (int i = 0; i < segments.size(); i++) |
| { |
| JFIFPiece piece = 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 = 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) |
| { |
| EXIF_IDENTIFIER_CODE.writeTo(os); |
| os.write(0); |
| os.write(0); |
| } |
| |
| writer.write(os, outputSet); |
| |
| return os.toByteArray(); |
| } |
| |
| } |