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; 018 019import static org.apache.commons.imaging.common.BinaryFunctions.remainingBytes; 020 021import java.awt.Dimension; 022import java.awt.image.BufferedImage; 023import java.io.IOException; 024import java.io.PrintWriter; 025import java.nio.charset.StandardCharsets; 026import java.text.NumberFormat; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collections; 030import java.util.List; 031import java.util.logging.Level; 032import java.util.logging.Logger; 033 034import org.apache.commons.imaging.AbstractImageParser; 035import org.apache.commons.imaging.ImageFormat; 036import org.apache.commons.imaging.ImageFormats; 037import org.apache.commons.imaging.ImageInfo; 038import org.apache.commons.imaging.ImagingException; 039import org.apache.commons.imaging.bytesource.ByteSource; 040import org.apache.commons.imaging.common.Allocator; 041import org.apache.commons.imaging.common.ImageMetadata; 042import org.apache.commons.imaging.common.XmpEmbeddable; 043import org.apache.commons.imaging.common.XmpImagingParameters; 044import org.apache.commons.imaging.formats.jpeg.decoder.JpegDecoder; 045import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser; 046import org.apache.commons.imaging.formats.jpeg.iptc.PhotoshopApp13Data; 047import org.apache.commons.imaging.formats.jpeg.segments.AbstractGenericSegment; 048import org.apache.commons.imaging.formats.jpeg.segments.AbstractSegment; 049import org.apache.commons.imaging.formats.jpeg.segments.App13Segment; 050import org.apache.commons.imaging.formats.jpeg.segments.App14Segment; 051import org.apache.commons.imaging.formats.jpeg.segments.App2Segment; 052import org.apache.commons.imaging.formats.jpeg.segments.ComSegment; 053import org.apache.commons.imaging.formats.jpeg.segments.DqtSegment; 054import org.apache.commons.imaging.formats.jpeg.segments.JfifSegment; 055import org.apache.commons.imaging.formats.jpeg.segments.SofnSegment; 056import org.apache.commons.imaging.formats.jpeg.segments.UnknownSegment; 057import org.apache.commons.imaging.formats.jpeg.xmp.JpegXmpParser; 058import org.apache.commons.imaging.formats.tiff.TiffField; 059import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; 060import org.apache.commons.imaging.formats.tiff.TiffImageParser; 061import org.apache.commons.imaging.formats.tiff.TiffImagingParameters; 062import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; 063import org.apache.commons.imaging.internal.Debug; 064import org.apache.commons.lang3.ArrayUtils; 065 066public class JpegImageParser extends AbstractImageParser<JpegImagingParameters> implements XmpEmbeddable<JpegImagingParameters> { 067 068 private static final Logger LOGGER = Logger.getLogger(JpegImageParser.class.getName()); 069 070 private static final String DEFAULT_EXTENSION = ImageFormats.JPEG.getDefaultExtension(); 071 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.JPEG.getExtensions(); 072 073 public static boolean isExifApp1Segment(final AbstractGenericSegment segment) { 074 return JpegConstants.EXIF_IDENTIFIER_CODE.isStartOf(segment.getSegmentData()); 075 } 076 077 /** 078 * Constructs a new instance with the big-endian byte order. 079 */ 080 public JpegImageParser() { 081 // empty 082 } 083 084 private byte[] assembleSegments(final List<App2Segment> segments) throws ImagingException { 085 try { 086 return assembleSegments(segments, false); 087 } catch (final ImagingException e) { 088 return assembleSegments(segments, true); 089 } 090 } 091 092 private byte[] assembleSegments(final List<App2Segment> segments, final boolean startWithZero) throws ImagingException { 093 if (segments.isEmpty()) { 094 throw new ImagingException("No App2 Segments Found."); 095 } 096 097 final int markerCount = segments.get(0).numMarkers; 098 099 if (segments.size() != markerCount) { 100 throw new ImagingException("App2 Segments Missing. Found: " + segments.size() + ", Expected: " + markerCount + "."); 101 } 102 103 Collections.sort(segments); 104 105 final int offset = startWithZero ? 0 : 1; 106 107 int total = 0; 108 for (int i = 0; i < segments.size(); i++) { 109 final App2Segment segment = segments.get(i); 110 111 if (i + offset != segment.curMarker) { 112 dumpSegments(segments); 113 throw new ImagingException("Incoherent App2 Segment Ordering. i: " + i + ", segment[" + i + "].curMarker: " + segment.curMarker + "."); 114 } 115 116 if (markerCount != segment.numMarkers) { 117 dumpSegments(segments); 118 throw new ImagingException( 119 "Inconsistent App2 Segment Count info. markerCount: " + markerCount + ", segment[" + i + "].numMarkers: " + segment.numMarkers + "."); 120 } 121 122 if (segment.getIccBytes() != null) { 123 total += segment.getIccBytes().length; 124 } 125 } 126 127 final byte[] result = Allocator.byteArray(total); 128 int progress = 0; 129 130 for (final App2Segment segment : segments) { 131 System.arraycopy(segment.getIccBytes(), 0, result, progress, segment.getIccBytes().length); 132 progress += segment.getIccBytes().length; 133 } 134 135 return result; 136 } 137 138 @Override 139 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException { 140 pw.println("jpeg.dumpImageFile"); 141 142 { 143 final ImageInfo imageInfo = getImageInfo(byteSource); 144 if (imageInfo == null) { 145 return false; 146 } 147 148 imageInfo.toString(pw, ""); 149 } 150 151 pw.println(""); 152 153 { 154 final List<AbstractSegment> abstractSegments = readSegments(byteSource, null, false); 155 156 if (abstractSegments == null) { 157 throw new ImagingException("No Segments Found."); 158 } 159 160 for (int d = 0; d < abstractSegments.size(); d++) { 161 162 final AbstractSegment abstractSegment = abstractSegments.get(d); 163 164 final NumberFormat nf = NumberFormat.getIntegerInstance(); 165 // this.debugNumber("found, marker: ", marker, 4); 166 pw.println(d + ": marker: " + Integer.toHexString(abstractSegment.marker) + ", " + abstractSegment.getDescription() + " (length: " 167 + nf.format(abstractSegment.length) + ")"); 168 abstractSegment.dump(pw); 169 } 170 171 pw.println(""); 172 } 173 174 return true; 175 } 176 177 private void dumpSegments(final List<? extends AbstractSegment> v) { 178 Debug.debug(); 179 Debug.debug("dumpSegments: " + v.size()); 180 181 for (int i = 0; i < v.size(); i++) { 182 final App2Segment segment = (App2Segment) v.get(i); 183 184 Debug.debug(i + ": " + segment.curMarker + " / " + segment.numMarkers); 185 } 186 Debug.debug(); 187 } 188 189 private List<AbstractSegment> filterApp1Segments(final List<AbstractSegment> abstractSegments) { 190 final List<AbstractSegment> result = new ArrayList<>(); 191 192 for (final AbstractSegment s : abstractSegments) { 193 final AbstractGenericSegment segment = (AbstractGenericSegment) s; 194 if (isExifApp1Segment(segment)) { 195 result.add(segment); 196 } 197 } 198 199 return result; 200 } 201 202 @Override 203 protected String[] getAcceptedExtensions() { 204 return ACCEPTED_EXTENSIONS; 205 } 206 207 @Override 208 protected ImageFormat[] getAcceptedTypes() { 209 return new ImageFormat[] { ImageFormats.JPEG, // 210 }; 211 } 212 213 @Override 214 public final BufferedImage getBufferedImage(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException { 215 final JpegDecoder jpegDecoder = new JpegDecoder(); 216 return jpegDecoder.decode(byteSource); 217 } 218 219 @Override 220 public String getDefaultExtension() { 221 return DEFAULT_EXTENSION; 222 } 223 224 @Override 225 public JpegImagingParameters getDefaultParameters() { 226 return new JpegImagingParameters(); 227 } 228 229 public TiffImageMetadata getExifMetadata(final ByteSource byteSource, TiffImagingParameters params) throws ImagingException, IOException { 230 final byte[] bytes = getExifRawData(byteSource); 231 if (null == bytes) { 232 return null; 233 } 234 235 if (params == null) { 236 params = new TiffImagingParameters(); 237 } 238 params.setReadThumbnails(Boolean.TRUE); 239 240 return (TiffImageMetadata) new TiffImageParser().getMetadata(bytes, params); 241 } 242 243 public byte[] getExifRawData(final ByteSource byteSource) throws ImagingException, IOException { 244 final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP1_MARKER, }, false); 245 246 if (abstractSegments == null || abstractSegments.isEmpty()) { 247 return null; 248 } 249 250 final List<AbstractSegment> exifSegments = filterApp1Segments(abstractSegments); 251 if (LOGGER.isLoggable(Level.FINEST)) { 252 LOGGER.finest("exifSegments.size()" + ": " + exifSegments.size()); 253 } 254 255 // Debug.debug("segments", segments); 256 // Debug.debug("exifSegments", exifSegments); 257 258 // TODO: concatenate if multiple segments, need example. 259 if (exifSegments.isEmpty()) { 260 return null; 261 } 262 if (exifSegments.size() > 1) { 263 throw new ImagingException( 264 "Imaging currently can't parse EXIF metadata split across multiple APP1 segments. " + "Please send this image to the Imaging project."); 265 } 266 267 final AbstractGenericSegment segment = (AbstractGenericSegment) exifSegments.get(0); 268 final byte[] bytes = segment.getSegmentData(); 269 270 // byte[] head = readBytearray("exif head", bytes, 0, 6); 271 // 272 // Debug.debug("head", head); 273 274 return remainingBytes("trimmed exif bytes", bytes, 6); 275 } 276 277 @Override 278 public byte[] getIccProfileBytes(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException { 279 final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP2_MARKER, }, false); 280 281 final List<App2Segment> filtered = new ArrayList<>(); 282 if (abstractSegments != null) { 283 // throw away non-icc profile app2 segments. 284 for (final AbstractSegment s : abstractSegments) { 285 final App2Segment segment = (App2Segment) s; 286 if (segment.getIccBytes() != null) { 287 filtered.add(segment); 288 } 289 } 290 } 291 292 if (filtered.isEmpty()) { 293 return null; 294 } 295 296 final byte[] bytes = assembleSegments(filtered); 297 298 if (LOGGER.isLoggable(Level.FINEST)) { 299 LOGGER.finest("bytes" + ": " + bytes.length); 300 } 301 302 return bytes; 303 } 304 305 @Override 306 public ImageInfo getImageInfo(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException { 307 // List allSegments = readSegments(byteSource, null, false); 308 309 final List<AbstractSegment> SOF_segments = readSegments(byteSource, new int[] { 310 // kJFIFMarker, 311 312 JpegConstants.SOF0_MARKER, JpegConstants.SOF1_MARKER, JpegConstants.SOF2_MARKER, JpegConstants.SOF3_MARKER, JpegConstants.SOF5_MARKER, 313 JpegConstants.SOF6_MARKER, JpegConstants.SOF7_MARKER, JpegConstants.SOF9_MARKER, JpegConstants.SOF10_MARKER, JpegConstants.SOF11_MARKER, 314 JpegConstants.SOF13_MARKER, JpegConstants.SOF14_MARKER, JpegConstants.SOF15_MARKER, 315 316 }, false); 317 318 if (SOF_segments == null) { 319 throw new ImagingException("No SOFN Data Found."); 320 } 321 322 // if (SOF_segments.size() != 1) 323 // System.out.println("Incoherent SOFN Data Found: " 324 // + SOF_segments.size()); 325 326 final List<AbstractSegment> jfifSegments = readSegments(byteSource, new int[] { JpegConstants.JFIF_MARKER, }, true); 327 328 final SofnSegment fSOFNSegment = (SofnSegment) SOF_segments.get(0); 329 // SofnSegment fSOFNSegment = (SofnSegment) findSegment(segments, 330 // SOFNmarkers); 331 332 if (fSOFNSegment == null) { 333 throw new ImagingException("No SOFN Data Found."); 334 } 335 336 final int width = fSOFNSegment.width; 337 final int height = fSOFNSegment.height; 338 339 JfifSegment jfifSegment = null; 340 341 if (jfifSegments != null && !jfifSegments.isEmpty()) { 342 jfifSegment = (JfifSegment) jfifSegments.get(0); 343 } 344 345 final List<AbstractSegment> app14Segments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP14_MARKER }, true); 346 App14Segment app14Segment = null; 347 if (app14Segments != null && !app14Segments.isEmpty()) { 348 app14Segment = (App14Segment) app14Segments.get(0); 349 } 350 351 // JfifSegment fTheJFIFSegment = (JfifSegment) findSegment(segments, 352 // kJFIFMarker); 353 354 double xDensity = -1.0; 355 double yDensity = -1.0; 356 double unitsPerInch = -1.0; 357 // int JFIF_major_version; 358 // int JFIF_minor_version; 359 final String formatDetails; 360 361 if (jfifSegment != null) { 362 xDensity = jfifSegment.xDensity; 363 yDensity = jfifSegment.yDensity; 364 final int densityUnits = jfifSegment.densityUnits; 365 // JFIF_major_version = fTheJFIFSegment.JFIF_major_version; 366 // JFIF_minor_version = fTheJFIFSegment.JFIF_minor_version; 367 368 formatDetails = "Jpeg/JFIF v." + jfifSegment.jfifMajorVersion + "." + jfifSegment.jfifMinorVersion; 369 370 switch (densityUnits) { 371 case 0: 372 break; 373 case 1: // inches 374 unitsPerInch = 1.0; 375 break; 376 case 2: // cms 377 unitsPerInch = 2.54; 378 break; 379 default: 380 break; 381 } 382 } else { 383 final JpegImageMetadata metadata = (JpegImageMetadata) getMetadata(byteSource, params); 384 385 if (metadata != null) { 386 { 387 final TiffField field = metadata.findExifValue(TiffTagConstants.TIFF_TAG_XRESOLUTION); 388 if (field != null) { 389 xDensity = ((Number) field.getValue()).doubleValue(); 390 } 391 } 392 { 393 final TiffField field = metadata.findExifValue(TiffTagConstants.TIFF_TAG_YRESOLUTION); 394 if (field != null) { 395 yDensity = ((Number) field.getValue()).doubleValue(); 396 } 397 } 398 { 399 final TiffField field = metadata.findExifValue(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT); 400 if (field != null) { 401 final int densityUnits = ((Number) field.getValue()).intValue(); 402 403 switch (densityUnits) { 404 case 1: 405 break; 406 case 2: // inches 407 unitsPerInch = 1.0; 408 break; 409 case 3: // cms 410 unitsPerInch = 2.54; 411 break; 412 default: 413 break; 414 } 415 } 416 417 } 418 } 419 420 formatDetails = "Jpeg/DCM"; 421 422 } 423 424 int physicalHeightDpi = -1; 425 float physicalHeightInch = -1; 426 int physicalWidthDpi = -1; 427 float physicalWidthInch = -1; 428 429 if (unitsPerInch > 0) { 430 physicalWidthDpi = (int) Math.round(xDensity * unitsPerInch); 431 physicalWidthInch = (float) (width / (xDensity * unitsPerInch)); 432 physicalHeightDpi = (int) Math.round(yDensity * unitsPerInch); 433 physicalHeightInch = (float) (height / (yDensity * unitsPerInch)); 434 } 435 436 final List<AbstractSegment> commentSegments = readSegments(byteSource, new int[] { JpegConstants.COM_MARKER }, false); 437 final List<String> comments = Allocator.arrayList(commentSegments.size()); 438 for (final AbstractSegment commentSegment : commentSegments) { 439 final ComSegment comSegment = (ComSegment) commentSegment; 440 comments.add(new String(comSegment.getComment(), StandardCharsets.UTF_8)); 441 } 442 443 final int numberOfComponents = fSOFNSegment.numberOfComponents; 444 final int precision = fSOFNSegment.precision; 445 446 final int bitsPerPixel = numberOfComponents * precision; 447 final ImageFormat format = ImageFormats.JPEG; 448 final String formatName = "JPEG (Joint Photographic Experts Group) Format"; 449 final String mimeType = "image/jpeg"; 450 // TODO: we ought to count images, but don't yet. 451 final int numberOfImages = 1; 452 // not accurate ... only reflects first 453 final boolean progressive = fSOFNSegment.marker == JpegConstants.SOF2_MARKER; 454 455 boolean transparent = false; 456 final boolean usesPalette = false; // TODO: inaccurate. 457 458 // See https://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#color 459 ImageInfo.ColorType colorType = ImageInfo.ColorType.UNKNOWN; 460 // Some images have both JFIF/APP0 and APP14. 461 // JFIF is meant to win but in them APP14 is clearly right, so make it win. 462 if (app14Segment != null && app14Segment.isAdobeJpegSegment()) { 463 final int colorTransform = app14Segment.getAdobeColorTransform(); 464 switch (colorTransform) { 465 case App14Segment.ADOBE_COLOR_TRANSFORM_UNKNOWN: 466 if (numberOfComponents == 3) { 467 colorType = ImageInfo.ColorType.RGB; 468 } else if (numberOfComponents == 4) { 469 colorType = ImageInfo.ColorType.CMYK; 470 } 471 break; 472 case App14Segment.ADOBE_COLOR_TRANSFORM_YCbCr: 473 colorType = ImageInfo.ColorType.YCbCr; 474 break; 475 case App14Segment.ADOBE_COLOR_TRANSFORM_YCCK: 476 colorType = ImageInfo.ColorType.YCCK; 477 break; 478 default: 479 break; 480 } 481 } else if (jfifSegment != null) { 482 if (numberOfComponents == 1) { 483 colorType = ImageInfo.ColorType.GRAYSCALE; 484 } else if (numberOfComponents == 3) { 485 colorType = ImageInfo.ColorType.YCbCr; 486 } 487 } else { 488 switch (numberOfComponents) { 489 case 1: 490 colorType = ImageInfo.ColorType.GRAYSCALE; 491 break; 492 case 2: 493 colorType = ImageInfo.ColorType.GRAYSCALE; 494 transparent = true; 495 break; 496 case 3: 497 case 4: 498 boolean have1 = false; 499 boolean have2 = false; 500 boolean have3 = false; 501 boolean have4 = false; 502 boolean haveOther = false; 503 for (final SofnSegment.Component component : fSOFNSegment.getComponents()) { 504 final int id = component.componentIdentifier; 505 switch (id) { 506 case 1: 507 have1 = true; 508 break; 509 case 2: 510 have2 = true; 511 break; 512 case 3: 513 have3 = true; 514 break; 515 case 4: 516 have4 = true; 517 break; 518 default: 519 haveOther = true; 520 break; 521 } 522 } 523 if (numberOfComponents == 3 && have1 && have2 && have3 && !have4 && !haveOther) { 524 colorType = ImageInfo.ColorType.YCbCr; 525 } else if (numberOfComponents == 4 && have1 && have2 && have3 && have4 && !haveOther) { 526 colorType = ImageInfo.ColorType.YCbCr; 527 transparent = true; 528 } else { 529 boolean haveR = false; 530 boolean haveG = false; 531 boolean haveB = false; 532 boolean haveA = false; 533 boolean haveC = false; 534 boolean havec = false; 535 boolean haveY = false; 536 for (final SofnSegment.Component component : fSOFNSegment.getComponents()) { 537 final int id = component.componentIdentifier; 538 switch (id) { 539 case 'R': 540 haveR = true; 541 break; 542 case 'G': 543 haveG = true; 544 break; 545 case 'B': 546 haveB = true; 547 break; 548 case 'A': 549 haveA = true; 550 break; 551 case 'C': 552 haveC = true; 553 break; 554 case 'c': 555 havec = true; 556 break; 557 case 'Y': 558 haveY = true; 559 break; 560 default: 561 break; 562 } 563 } 564 if (haveR && haveG && haveB && !haveA && !haveC && !havec && !haveY) { 565 colorType = ImageInfo.ColorType.RGB; 566 } else if (haveR && haveG && haveB && haveA && !haveC && !havec && !haveY) { 567 colorType = ImageInfo.ColorType.RGB; 568 transparent = true; 569 } else if (haveY && haveC && havec && !haveR && !haveG && !haveB && !haveA) { 570 colorType = ImageInfo.ColorType.YCC; 571 } else if (haveY && haveC && havec && haveA && !haveR && !haveG && !haveB) { 572 colorType = ImageInfo.ColorType.YCC; 573 transparent = true; 574 } else { 575 int minHorizontalSamplingFactor = Integer.MAX_VALUE; 576 int maxHorizontalSmaplingFactor = Integer.MIN_VALUE; 577 int minVerticalSamplingFactor = Integer.MAX_VALUE; 578 int maxVerticalSamplingFactor = Integer.MIN_VALUE; 579 for (final SofnSegment.Component component : fSOFNSegment.getComponents()) { 580 if (minHorizontalSamplingFactor > component.horizontalSamplingFactor) { 581 minHorizontalSamplingFactor = component.horizontalSamplingFactor; 582 } 583 if (maxHorizontalSmaplingFactor < component.horizontalSamplingFactor) { 584 maxHorizontalSmaplingFactor = component.horizontalSamplingFactor; 585 } 586 if (minVerticalSamplingFactor > component.verticalSamplingFactor) { 587 minVerticalSamplingFactor = component.verticalSamplingFactor; 588 } 589 if (maxVerticalSamplingFactor < component.verticalSamplingFactor) { 590 maxVerticalSamplingFactor = component.verticalSamplingFactor; 591 } 592 } 593 final boolean isSubsampled = minHorizontalSamplingFactor != maxHorizontalSmaplingFactor 594 || minVerticalSamplingFactor != maxVerticalSamplingFactor; 595 if (numberOfComponents == 3) { 596 if (isSubsampled) { 597 colorType = ImageInfo.ColorType.YCbCr; 598 } else { 599 colorType = ImageInfo.ColorType.RGB; 600 } 601 } else if (numberOfComponents == 4) { 602 if (isSubsampled) { 603 colorType = ImageInfo.ColorType.YCCK; 604 } else { 605 colorType = ImageInfo.ColorType.CMYK; 606 } 607 } 608 } 609 } 610 break; 611 default: 612 break; 613 } 614 } 615 616 final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.JPEG; 617 618 return new ImageInfo(formatDetails, bitsPerPixel, comments, format, formatName, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch, 619 physicalWidthDpi, physicalWidthInch, width, progressive, transparent, usesPalette, colorType, compressionAlgorithm); 620 } 621 622 @Override 623 public Dimension getImageSize(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException { 624 final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] { 625 // kJFIFMarker, 626 JpegConstants.SOF0_MARKER, JpegConstants.SOF1_MARKER, JpegConstants.SOF2_MARKER, JpegConstants.SOF3_MARKER, JpegConstants.SOF5_MARKER, 627 JpegConstants.SOF6_MARKER, JpegConstants.SOF7_MARKER, JpegConstants.SOF9_MARKER, JpegConstants.SOF10_MARKER, JpegConstants.SOF11_MARKER, 628 JpegConstants.SOF13_MARKER, JpegConstants.SOF14_MARKER, JpegConstants.SOF15_MARKER, 629 630 }, true); 631 632 if (abstractSegments == null || abstractSegments.isEmpty()) { 633 throw new ImagingException("No JFIF Data Found."); 634 } 635 636 if (abstractSegments.size() > 1) { 637 throw new ImagingException("Redundant JFIF Data Found."); 638 } 639 640 final SofnSegment fSOFNSegment = (SofnSegment) abstractSegments.get(0); 641 642 return new Dimension(fSOFNSegment.width, fSOFNSegment.height); 643 } 644 645 @Override 646 public ImageMetadata getMetadata(final ByteSource byteSource, JpegImagingParameters params) throws ImagingException, IOException { 647 if (params == null) { 648 params = new JpegImagingParameters(); 649 } 650 final TiffImageMetadata exif = getExifMetadata(byteSource, new TiffImagingParameters()); 651 652 final JpegPhotoshopMetadata photoshop = getPhotoshopMetadata(byteSource, params); 653 654 if (null == exif && null == photoshop) { 655 return null; 656 } 657 658 return new JpegImageMetadata(photoshop, exif); 659 } 660 661 @Override 662 public String getName() { 663 return "Jpeg-Custom"; 664 } 665 666 public JpegPhotoshopMetadata getPhotoshopMetadata(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException { 667 final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP13_MARKER, }, false); 668 669 if (abstractSegments == null || abstractSegments.isEmpty()) { 670 return null; 671 } 672 673 PhotoshopApp13Data photoshopApp13Data = null; 674 675 for (final AbstractSegment s : abstractSegments) { 676 final App13Segment segment = (App13Segment) s; 677 678 final PhotoshopApp13Data data = segment.parsePhotoshopSegment(params); 679 if (data != null) { 680 if (photoshopApp13Data != null) { 681 throw new ImagingException("JPEG contains more than one Photoshop App13 segment."); 682 } 683 photoshopApp13Data = data; 684 } 685 } 686 687 if (null == photoshopApp13Data) { 688 return null; 689 } 690 return new JpegPhotoshopMetadata(photoshopApp13Data); 691 } 692 693 /** 694 * Extracts embedded XML metadata as XML string. 695 * <p> 696 * 697 * @param byteSource File containing image data. 698 * @param params Map of optional parameters, defined in ImagingConstants. 699 * @return Xmp Xml as String, if present. Otherwise, returns null. 700 */ 701 @Override 702 public String getXmpXml(final ByteSource byteSource, final XmpImagingParameters<JpegImagingParameters> params) throws ImagingException, IOException { 703 704 final List<String> result = new ArrayList<>(); 705 706 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { 707 // return false to exit before reading image data. 708 @Override 709 public boolean beginSos() { 710 return false; 711 } 712 713 // return false to exit traversal. 714 @Override 715 public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes, 716 final byte[] segmentData) throws ImagingException { 717 if (marker == 0xffd9) { 718 return false; 719 } 720 721 if (marker == JpegConstants.JPEG_APP1_MARKER && new JpegXmpParser().isXmpJpegSegment(segmentData)) { 722 result.add(new JpegXmpParser().parseXmpJpegSegment(segmentData)); 723 return false; 724 } 725 726 return true; 727 } 728 729 @Override 730 public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) { 731 // don't need image data 732 } 733 }; 734 new JpegUtils().traverseJfif(byteSource, visitor); 735 736 if (result.isEmpty()) { 737 return null; 738 } 739 if (result.size() > 1) { 740 throw new ImagingException("JPEG file contains more than one XMP segment."); 741 } 742 return result.get(0); 743 } 744 745 public boolean hasExifSegment(final ByteSource byteSource) throws ImagingException, IOException { 746 final boolean[] result = { false, }; 747 748 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { 749 // return false to exit before reading image data. 750 @Override 751 public boolean beginSos() { 752 return false; 753 } 754 755 // return false to exit traversal. 756 @Override 757 public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes, 758 final byte[] segmentData) { 759 if (marker == 0xffd9) { 760 return false; 761 } 762 763 if (marker == JpegConstants.JPEG_APP1_MARKER && JpegConstants.EXIF_IDENTIFIER_CODE.isStartOf(segmentData)) { 764 result[0] = true; 765 return false; 766 } 767 768 return true; 769 } 770 771 @Override 772 public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) { 773 // don't need image data 774 } 775 }; 776 777 new JpegUtils().traverseJfif(byteSource, visitor); 778 779 return result[0]; 780 } 781 782 public boolean hasIptcSegment(final ByteSource byteSource) throws ImagingException, IOException { 783 final boolean[] result = { false, }; 784 785 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { 786 // return false to exit before reading image data. 787 @Override 788 public boolean beginSos() { 789 return false; 790 } 791 792 // return false to exit traversal. 793 @Override 794 public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes, 795 final byte[] segmentData) { 796 if (marker == 0xffd9) { 797 return false; 798 } 799 800 if (marker == JpegConstants.JPEG_APP13_MARKER && new IptcParser().isPhotoshopJpegSegment(segmentData)) { 801 result[0] = true; 802 return false; 803 } 804 805 return true; 806 } 807 808 @Override 809 public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) { 810 // don't need image data 811 } 812 }; 813 814 new JpegUtils().traverseJfif(byteSource, visitor); 815 816 return result[0]; 817 } 818 819 public boolean hasXmpSegment(final ByteSource byteSource) throws ImagingException, IOException { 820 final boolean[] result = { false, }; 821 822 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { 823 // return false to exit before reading image data. 824 @Override 825 public boolean beginSos() { 826 return false; 827 } 828 829 // return false to exit traversal. 830 @Override 831 public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes, 832 final byte[] segmentData) { 833 if (marker == 0xffd9) { 834 return false; 835 } 836 837 if (marker == JpegConstants.JPEG_APP1_MARKER && new JpegXmpParser().isXmpJpegSegment(segmentData)) { 838 result[0] = true; 839 return false; 840 } 841 842 return true; 843 } 844 845 @Override 846 public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) { 847 // don't need image data 848 } 849 }; 850 new JpegUtils().traverseJfif(byteSource, visitor); 851 852 return result[0]; 853 } 854 855 private boolean keepMarker(final int marker, final int[] markers) { 856 return ArrayUtils.contains(markers, marker); 857 } 858 859 public List<AbstractSegment> readSegments(final ByteSource byteSource, final int[] markers, final boolean returnAfterFirst) 860 throws ImagingException, IOException { 861 final List<AbstractSegment> result = new ArrayList<>(); 862 final int[] sofnSegments = { 863 // kJFIFMarker, 864 JpegConstants.SOF0_MARKER, JpegConstants.SOF1_MARKER, JpegConstants.SOF2_MARKER, JpegConstants.SOF3_MARKER, JpegConstants.SOF5_MARKER, 865 JpegConstants.SOF6_MARKER, JpegConstants.SOF7_MARKER, JpegConstants.SOF9_MARKER, JpegConstants.SOF10_MARKER, JpegConstants.SOF11_MARKER, 866 JpegConstants.SOF13_MARKER, JpegConstants.SOF14_MARKER, JpegConstants.SOF15_MARKER, }; 867 868 final JpegUtils.Visitor visitor = new JpegUtils.Visitor() { 869 // return false to exit before reading image data. 870 @Override 871 public boolean beginSos() { 872 return false; 873 } 874 875 // return false to exit traversal. 876 @Override 877 public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes, 878 final byte[] segmentData) throws ImagingException, IOException { 879 if (marker == JpegConstants.EOI_MARKER) { 880 return false; 881 } 882 883 // Debug.debug("visitSegment marker", marker); 884 // // Debug.debug("visitSegment keepMarker(marker, markers)", 885 // keepMarker(marker, markers)); 886 // Debug.debug("visitSegment keepMarker(marker, markers)", 887 // keepMarker(marker, markers)); 888 889 if (!keepMarker(marker, markers)) { 890 return true; 891 } 892 893 switch (marker) { 894 case JpegConstants.JPEG_APP13_MARKER: 895 // Debug.debug("app 13 segment data", segmentData.length); 896 result.add(new App13Segment(marker, segmentData)); 897 break; 898 case JpegConstants.JPEG_APP14_MARKER: 899 result.add(new App14Segment(marker, segmentData)); 900 break; 901 case JpegConstants.JPEG_APP2_MARKER: 902 result.add(new App2Segment(marker, segmentData)); 903 break; 904 case JpegConstants.JFIF_MARKER: 905 result.add(new JfifSegment(marker, segmentData)); 906 break; 907 default: 908 if (Arrays.binarySearch(sofnSegments, marker) >= 0) { 909 result.add(new SofnSegment(marker, segmentData)); 910 } else if (marker == JpegConstants.DQT_MARKER) { 911 result.add(new DqtSegment(marker, segmentData)); 912 } else if (marker >= JpegConstants.JPEG_APP1_MARKER && marker <= JpegConstants.JPEG_APP15_MARKER) { 913 result.add(new UnknownSegment(marker, segmentData)); 914 } else if (marker == JpegConstants.COM_MARKER) { 915 result.add(new ComSegment(marker, segmentData)); 916 } 917 break; 918 } 919 920 return !returnAfterFirst; 921 } 922 923 @Override 924 public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) { 925 // don't need image data 926 } 927 }; 928 929 new JpegUtils().traverseJfif(byteSource, visitor); 930 931 return result; 932 } 933}