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 = JFIFPieceSegment::isExifSegment; 043 private static final SegmentFilter XMP_SEGMENT_FILTER = JFIFPieceSegment::isXmpSegment; 044 private static final SegmentFilter PHOTOSHOP_APP13_SEGMENT_FILTER = JFIFPieceSegment::isPhotoshopApp13Segment; 045 046 /** 047 * Constructor. to guess whether a file contains an image based on its file 048 * extension. 049 */ 050 public JpegRewriter() { 051 setByteOrder(JPEG_BYTE_ORDER); 052 } 053 054 protected static class JFIFPieces { 055 public final List<JFIFPiece> pieces; 056 public final List<JFIFPiece> segmentPieces; 057 058 public JFIFPieces(final List<JFIFPiece> pieces, 059 final List<JFIFPiece> segmentPieces) { 060 this.pieces = pieces; 061 this.segmentPieces = segmentPieces; 062 } 063 064 } 065 066 protected abstract static class JFIFPiece { 067 protected abstract void write(OutputStream os) throws IOException; 068 069 @Override 070 public String toString() { 071 return "[" + this.getClass().getName() + "]"; 072 } 073 } 074 075 protected static class JFIFPieceSegment extends JFIFPiece { 076 public final int marker; 077 private final byte[] markerBytes; 078 private final byte[] segmentLengthBytes; 079 private final byte[] segmentData; 080 081 public JFIFPieceSegment(final int marker, final byte[] segmentData) { 082 this(marker, 083 ByteConversions.toBytes((short) marker, JPEG_BYTE_ORDER), 084 ByteConversions.toBytes((short) (segmentData.length + 2), JPEG_BYTE_ORDER), 085 segmentData); 086 } 087 088 JFIFPieceSegment(final int marker, final byte[] markerBytes, 089 final byte[] segmentLengthBytes, final byte[] segmentData) { 090 this.marker = marker; 091 this.markerBytes = markerBytes; 092 this.segmentLengthBytes = segmentLengthBytes; 093 this.segmentData = segmentData.clone(); 094 } 095 096 @Override 097 public String toString() { 098 return "[" + this.getClass().getName() + " (0x" 099 + Integer.toHexString(marker) + ")]"; 100 } 101 102 @Override 103 protected void write(final OutputStream os) throws IOException { 104 os.write(markerBytes); 105 os.write(segmentLengthBytes); 106 os.write(segmentData); 107 } 108 109 public boolean isApp1Segment() { 110 return marker == JpegConstants.JPEG_APP1_MARKER; 111 } 112 113 public boolean isAppSegment() { 114 return marker >= JpegConstants.JPEG_APP0_MARKER && marker <= JpegConstants.JPEG_APP15_MARKER; 115 } 116 117 public boolean isExifSegment() { 118 if (marker != JpegConstants.JPEG_APP1_MARKER) { 119 return false; 120 } 121 if (!startsWith(segmentData, JpegConstants.EXIF_IDENTIFIER_CODE)) { 122 return false; 123 } 124 return true; 125 } 126 127 public boolean isPhotoshopApp13Segment() { 128 if (marker != JpegConstants.JPEG_APP13_MARKER) { 129 return false; 130 } 131 if (!new IptcParser().isPhotoshopJpegSegment(segmentData)) { 132 return false; 133 } 134 return true; 135 } 136 137 public boolean isXmpSegment() { 138 if (marker != JpegConstants.JPEG_APP1_MARKER) { 139 return false; 140 } 141 if (!startsWith(segmentData, JpegConstants.XMP_IDENTIFIER)) { 142 return false; 143 } 144 return true; 145 } 146 147 public byte[] getSegmentData() { 148 return segmentData.clone(); 149 } 150 151 } 152 153 static class JFIFPieceImageData extends JFIFPiece { 154 private final byte[] markerBytes; 155 private final byte[] imageData; 156 157 JFIFPieceImageData(final byte[] markerBytes, final byte[] imageData) { 158 this.markerBytes = markerBytes; 159 this.imageData = imageData; 160 } 161 162 @Override 163 protected void write(final OutputStream os) throws IOException { 164 os.write(markerBytes); 165 os.write(imageData); 166 } 167 } 168 169 protected JFIFPieces analyzeJFIF(final ByteSource byteSource) throws ImageReadException, IOException { 170 final List<JFIFPiece> pieces = new ArrayList<>(); 171 final List<JFIFPiece> segmentPieces = new ArrayList<>(); 172 173 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { 174 // return false to exit before reading image data. 175 @Override 176 public boolean beginSOS() { 177 return true; 178 } 179 180 @Override 181 public void visitSOS(final int marker, final byte[] markerBytes, final byte[] imageData) { 182 pieces.add(new JFIFPieceImageData(markerBytes, imageData)); 183 } 184 185 // return false to exit traversal. 186 @Override 187 public boolean visitSegment(final int marker, final byte[] markerBytes, 188 final int segmentLength, final byte[] segmentLengthBytes, 189 final byte[] segmentData) throws ImageReadException, IOException { 190 final JFIFPiece piece = new JFIFPieceSegment(marker, markerBytes, 191 segmentLengthBytes, segmentData); 192 pieces.add(piece); 193 segmentPieces.add(piece); 194 195 return true; 196 } 197 }; 198 199 new JpegUtils().traverseJFIF(byteSource, visitor); 200 201 return new JFIFPieces(pieces, segmentPieces); 202 } 203 204 private interface SegmentFilter { 205 boolean filter(JFIFPieceSegment segment); 206 } 207 208 protected <T extends JFIFPiece> List<T> removeXmpSegments(final List<T> segments) { 209 return filterSegments(segments, XMP_SEGMENT_FILTER); 210 } 211 212 protected <T extends JFIFPiece> List<T> removePhotoshopApp13Segments( 213 final List<T> segments) { 214 return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER); 215 } 216 217 protected <T extends JFIFPiece> List<T> findPhotoshopApp13Segments( 218 final List<T> segments) { 219 return filterSegments(segments, PHOTOSHOP_APP13_SEGMENT_FILTER, true); 220 } 221 222 protected <T extends JFIFPiece> List<T> removeExifSegments(final List<T> segments) { 223 return filterSegments(segments, EXIF_SEGMENT_FILTER); 224 } 225 226 protected <T extends JFIFPiece> List<T> filterSegments(final List<T> segments, 227 final SegmentFilter filter) { 228 return filterSegments(segments, filter, false); 229 } 230 231 protected <T extends JFIFPiece> List<T> filterSegments(final List<T> segments, 232 final SegmentFilter filter, final boolean reverse) { 233 final List<T> result = new ArrayList<>(); 234 235 for (final T piece : segments) { 236 if (piece instanceof JFIFPieceSegment) { 237 if (filter.filter((JFIFPieceSegment) piece) == reverse) { 238 result.add(piece); 239 } 240 } else if (!reverse) { 241 result.add(piece); 242 } 243 } 244 245 return result; 246 } 247 248 protected <T extends JFIFPiece, U extends JFIFPiece> List<JFIFPiece> insertBeforeFirstAppSegments( 249 final List<T> segments, final List<U> newSegments) throws ImageWriteException { 250 int firstAppIndex = -1; 251 for (int i = 0; i < segments.size(); i++) { 252 final JFIFPiece piece = segments.get(i); 253 if (!(piece instanceof JFIFPieceSegment)) { 254 continue; 255 } 256 257 final JFIFPieceSegment segment = (JFIFPieceSegment) piece; 258 if (segment.isAppSegment()) { 259 if (firstAppIndex == -1) { 260 firstAppIndex = i; 261 } 262 } 263 } 264 265 final List<JFIFPiece> result = new ArrayList<>(segments); 266 if (firstAppIndex == -1) { 267 throw new ImageWriteException("JPEG file has no APP segments."); 268 } 269 result.addAll(firstAppIndex, newSegments); 270 return result; 271 } 272 273 protected <T extends JFIFPiece, U extends JFIFPiece> List<JFIFPiece> insertAfterLastAppSegments( 274 final List<T> segments, final List<U> newSegments) throws ImageWriteException { 275 int lastAppIndex = -1; 276 for (int i = 0; i < segments.size(); i++) { 277 final JFIFPiece piece = segments.get(i); 278 if (!(piece instanceof JFIFPieceSegment)) { 279 continue; 280 } 281 282 final JFIFPieceSegment segment = (JFIFPieceSegment) piece; 283 if (segment.isAppSegment()) { 284 lastAppIndex = i; 285 } 286 } 287 288 final List<JFIFPiece> result = new ArrayList<>(segments); 289 if (lastAppIndex == -1) { 290 if (segments.isEmpty()) { 291 throw new ImageWriteException("JPEG file has no APP segments."); 292 } 293 result.addAll(1, newSegments); 294 } else { 295 result.addAll(lastAppIndex + 1, newSegments); 296 } 297 298 return result; 299 } 300 301 protected void writeSegments(final OutputStream outputStream, 302 final List<? extends JFIFPiece> segments) throws IOException { 303 try (DataOutputStream os = new DataOutputStream(outputStream)) { 304 JpegConstants.SOI.writeTo(os); 305 306 for (final JFIFPiece piece : segments) { 307 piece.write(os); 308 } 309 } 310 } 311 312 // private void writeSegment(OutputStream os, JFIFPieceSegment piece) 313 // throws ImageWriteException, IOException 314 // { 315 // byte markerBytes[] = convertShortToByteArray(JPEG_APP1_MARKER, 316 // JPEG_BYTE_ORDER); 317 // if (piece.segmentData.length > 0xffff) 318 // throw new JpegSegmentOverflowException("Jpeg segment is too long: " 319 // + piece.segmentData.length); 320 // int segmentLength = piece.segmentData.length + 2; 321 // byte segmentLengthBytes[] = convertShortToByteArray(segmentLength, 322 // JPEG_BYTE_ORDER); 323 // 324 // os.write(markerBytes); 325 // os.write(segmentLengthBytes); 326 // os.write(piece.segmentData); 327 // } 328 329 public static class JpegSegmentOverflowException extends ImageWriteException { 330 private static final long serialVersionUID = -1062145751550646846L; 331 332 public JpegSegmentOverflowException(final String message) { 333 super(message); 334 } 335 } 336 337}