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