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