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