001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.imaging.formats.jpeg.xmp; 018 019import static org.apache.commons.imaging.common.BinaryFunctions.startsWith; 020 021import java.io.DataOutputStream; 022import java.io.IOException; 023import java.io.OutputStream; 024import java.nio.ByteOrder; 025import java.util.ArrayList; 026import java.util.List; 027 028import org.apache.commons.imaging.ImageReadException; 029import org.apache.commons.imaging.ImageWriteException; 030import org.apache.commons.imaging.common.BinaryFileParser; 031import org.apache.commons.imaging.common.ByteConversions; 032import org.apache.commons.imaging.common.bytesource.ByteSource; 033import org.apache.commons.imaging.formats.jpeg.JpegConstants; 034import org.apache.commons.imaging.formats.jpeg.JpegUtils; 035import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser; 036 037/** 038 * Interface for Exif write/update/remove functionality for Jpeg/JFIF images. 039 */ 040public class JpegRewriter extends BinaryFileParser { 041 private static final ByteOrder JPEG_BYTE_ORDER = ByteOrder.BIG_ENDIAN; 042 private static final SegmentFilter EXIF_SEGMENT_FILTER = new SegmentFilter() { 043 @Override 044 public boolean filter(final JFIFPieceSegment segment) { 045 return segment.isExifSegment(); 046 } 047 }; 048 private static final SegmentFilter XMP_SEGMENT_FILTER = new SegmentFilter() { 049 @Override 050 public boolean filter(final JFIFPieceSegment segment) { 051 return segment.isXmpSegment(); 052 } 053 }; 054 private static final SegmentFilter PHOTOSHOP_APP13_SEGMENT_FILTER = new SegmentFilter() { 055 @Override 056 public boolean filter(final JFIFPieceSegment segment) { 057 return segment.isPhotoshopApp13Segment(); 058 } 059 }; 060 061 /** 062 * Constructor. to guess whether a file contains an image based on its file 063 * extension. 064 */ 065 public JpegRewriter() { 066 setByteOrder(JPEG_BYTE_ORDER); 067 } 068 069 protected static class JFIFPieces { 070 public final List<JFIFPiece> pieces; 071 public final List<JFIFPiece> segmentPieces; 072 073 public JFIFPieces(final List<JFIFPiece> pieces, 074 final List<JFIFPiece> segmentPieces) { 075 this.pieces = pieces; 076 this.segmentPieces = segmentPieces; 077 } 078 079 } 080 081 protected abstract static class JFIFPiece { 082 protected abstract void write(OutputStream os) throws IOException; 083 084 @Override 085 public String toString() { 086 return "[" + this.getClass().getName() + "]"; 087 } 088 } 089 090 protected static class JFIFPieceSegment extends JFIFPiece { 091 public final int marker; 092 private final byte[] markerBytes; 093 private final byte[] segmentLengthBytes; 094 private final byte[] segmentData; 095 096 public JFIFPieceSegment(final int marker, final byte[] segmentData) { 097 this(marker, 098 ByteConversions.toBytes((short) marker, JPEG_BYTE_ORDER), 099 ByteConversions.toBytes((short) (segmentData.length + 2), JPEG_BYTE_ORDER), 100 segmentData); 101 } 102 103 JFIFPieceSegment(final int marker, final byte[] markerBytes, 104 final byte[] segmentLengthBytes, final byte[] segmentData) { 105 this.marker = marker; 106 this.markerBytes = markerBytes; 107 this.segmentLengthBytes = segmentLengthBytes; 108 this.segmentData = segmentData; // TODO clone? 109 } 110 111 @Override 112 public String toString() { 113 return "[" + this.getClass().getName() + " (0x" 114 + Integer.toHexString(marker) + ")]"; 115 } 116 117 @Override 118 protected void write(final OutputStream os) throws IOException { 119 os.write(markerBytes); 120 os.write(segmentLengthBytes); 121 os.write(segmentData); 122 } 123 124 public boolean isApp1Segment() { 125 return marker == JpegConstants.JPEG_APP1_MARKER; 126 } 127 128 public boolean isAppSegment() { 129 return marker >= JpegConstants.JPEG_APP0_MARKER && marker <= JpegConstants.JPEG_APP15_MARKER; 130 } 131 132 public boolean isExifSegment() { 133 if (marker != JpegConstants.JPEG_APP1_MARKER) { 134 return false; 135 } 136 if (!startsWith(segmentData, JpegConstants.EXIF_IDENTIFIER_CODE)) { 137 return false; 138 } 139 return true; 140 } 141 142 public boolean isPhotoshopApp13Segment() { 143 if (marker != JpegConstants.JPEG_APP13_MARKER) { 144 return false; 145 } 146 if (!new IptcParser().isPhotoshopJpegSegment(segmentData)) { 147 return false; 148 } 149 return true; 150 } 151 152 public boolean isXmpSegment() { 153 if (marker != JpegConstants.JPEG_APP1_MARKER) { 154 return false; 155 } 156 if (!startsWith(segmentData, JpegConstants.XMP_IDENTIFIER)) { 157 return false; 158 } 159 return true; 160 } 161 162 public byte[] getSegmentData() { 163 return segmentData; // TODO clone? 164 } 165 166 } 167 168 static class JFIFPieceImageData extends JFIFPiece { 169 private final byte[] markerBytes; 170 private final byte[] imageData; 171 172 JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) { 173 super(); 174 this.markerBytes = markerBytes; 175 this.imageData = imageData; 176 } 177 178 @Override 179 protected void write(final OutputStream os) throws IOException { 180 os.write(markerBytes); 181 os.write(imageData); 182 } 183 } 184 185 protected JFIFPieces analyzeJFIF(final ByteSource byteSource) throws ImageReadException, IOException { 186 final List<JFIFPiece> pieces = new ArrayList<>(); 187 final List<JFIFPiece> segmentPieces = new ArrayList<>(); 188 189 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { 190 // return false to exit before reading image data. 191 @Override 192 public boolean beginSOS() { 193 return true; 194 } 195 196 @Override 197 public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) { 198 pieces.add(new JFIFPieceImageData(markerBytes, imageData)); 199 } 200 201 // return false to exit traversal. 202 @Override 203 public boolean visitSegment(final int marker, final byte[] markerBytes, 204 final int segmentLength, final byte[] segmentLengthBytes, 205 final byte[] segmentData) throws ImageReadException, IOException { 206 final JFIFPiece piece = new JFIFPieceSegment(marker, markerBytes, 207 segmentLengthBytes, segmentData); 208 pieces.add(piece); 209 segmentPieces.add(piece); 210 211 return true; 212 } 213 }; 214 215 new JpegUtils().traverseJFIF(byteSource, visitor); 216 217 return new JFIFPieces(pieces, segmentPieces); 218 } 219 220 private interface SegmentFilter { 221 boolean filter(JFIFPieceSegment segment); 222 } 223 224 protected <T extends JFIFPiece> List<T> removeXmpSegments(final List<T> segments) { 225 return filterSegments(segments, XMP_SEGMENT_FILTER); 226 } 227 228 protected <T extends JFIFPiece> List<T> removePhotoshopApp13Segments( 229 final List<T> segments) { 230 return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER); 231 } 232 233 protected <T extends JFIFPiece> List<T> findPhotoshopApp13Segments( 234 final List<T> segments) { 235 return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER, true); 236 } 237 238 protected <T extends JFIFPiece> List<T> removeExifSegments(final List<T> segments) { 239 return filterSegments(segments, EXIF_SEGMENT_FILTER); 240 } 241 242 protected <T extends JFIFPiece> List<T> filterSegments(final List<T> segments, 243 final SegmentFilter filter) { 244 return filterSegments(segments, filter, false); 245 } 246 247 protected <T extends JFIFPiece> List<T> filterSegments(final List<T> segments, 248 final SegmentFilter filter, final boolean reverse) { 249 final List<T> result = new ArrayList<>(); 250 251 for (final T piece : segments) { 252 if (piece instanceof JFIFPieceSegment) { 253 if (filter.filter((JFIFPieceSegment) piece) ^ !reverse) { 254 result.add(piece); 255 } 256 } else if (!reverse) { 257 result.add(piece); 258 } 259 } 260 261 return result; 262 } 263 264 protected <T extends JFIFPiece, U extends JFIFPiece> List<JFIFPiece> insertBeforeFirstAppSegments( 265 final List<T> segments, final List<U> newSegments) throws ImageWriteException { 266 int firstAppIndex = -1; 267 for (int i = 0; i < segments.size(); i++) { 268 final JFIFPiece piece = segments.get(i); 269 if (!(piece instanceof JFIFPieceSegment)) { 270 continue; 271 } 272 273 final JFIFPieceSegment segment = (JFIFPieceSegment) piece; 274 if (segment.isAppSegment()) { 275 if (firstAppIndex == -1) { 276 firstAppIndex = i; 277 } 278 } 279 } 280 281 final List<JFIFPiece> result = new ArrayList<>(segments); 282 if (firstAppIndex == -1) { 283 throw new ImageWriteException("JPEG file has no APP segments."); 284 } 285 result.addAll(firstAppIndex, newSegments); 286 return result; 287 } 288 289 protected <T extends JFIFPiece, U extends JFIFPiece> List<JFIFPiece> insertAfterLastAppSegments( 290 final List<T> segments, final List<U> newSegments) throws ImageWriteException { 291 int lastAppIndex = -1; 292 for (int i = 0; i < segments.size(); i++) { 293 final JFIFPiece piece = segments.get(i); 294 if (!(piece instanceof JFIFPieceSegment)) { 295 continue; 296 } 297 298 final JFIFPieceSegment segment = (JFIFPieceSegment) piece; 299 if (segment.isAppSegment()) { 300 lastAppIndex = i; 301 } 302 } 303 304 final List<JFIFPiece> result = new ArrayList<>(segments); 305 if (lastAppIndex == -1) { 306 if (segments.size() < 1) { 307 throw new ImageWriteException("JPEG file has no APP segments."); 308 } 309 result.addAll(1, newSegments); 310 } else { 311 result.addAll(lastAppIndex + 1, newSegments); 312 } 313 314 return result; 315 } 316 317 protected void writeSegments(final OutputStream outputStream, 318 final List<? extends JFIFPiece> segments) throws IOException { 319 try (DataOutputStream os = new DataOutputStream(outputStream)) { 320 JpegConstants.SOI.writeTo(os); 321 322 for (final JFIFPiece piece : segments) { 323 piece.write(os); 324 } 325 } 326 } 327 328 // private void writeSegment(OutputStream os, JFIFPieceSegment piece) 329 // throws ImageWriteException, IOException 330 // { 331 // byte markerBytes[] = convertShortToByteArray(JPEG_APP1_MARKER, 332 // JPEG_BYTE_ORDER); 333 // if (piece.segmentData.length > 0xffff) 334 // throw new JpegSegmentOverflowException("Jpeg segment is too long: " 335 // + piece.segmentData.length); 336 // int segmentLength = piece.segmentData.length + 2; 337 // byte segmentLengthBytes[] = convertShortToByteArray(segmentLength, 338 // JPEG_BYTE_ORDER); 339 // 340 // os.write(markerBytes); 341 // os.write(segmentLengthBytes); 342 // os.write(piece.segmentData); 343 // } 344 345 public static class JpegSegmentOverflowException extends ImageWriteException { 346 private static final long serialVersionUID = -1062145751550646846L; 347 348 public JpegSegmentOverflowException(final String message) { 349 super(message); 350 } 351 } 352 353}