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