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.tiff; 018 019import java.awt.Dimension; 020import java.awt.Rectangle; 021import java.awt.image.BufferedImage; 022import java.io.IOException; 023import java.io.OutputStream; 024import java.io.PrintWriter; 025import java.nio.ByteOrder; 026import java.nio.charset.StandardCharsets; 027import java.util.ArrayList; 028import java.util.List; 029 030import org.apache.commons.imaging.AbstractImageParser; 031import org.apache.commons.imaging.FormatCompliance; 032import org.apache.commons.imaging.ImageFormat; 033import org.apache.commons.imaging.ImageFormats; 034import org.apache.commons.imaging.ImageInfo; 035import org.apache.commons.imaging.ImagingException; 036import org.apache.commons.imaging.bytesource.ByteSource; 037import org.apache.commons.imaging.common.Allocator; 038import org.apache.commons.imaging.common.ImageBuilder; 039import org.apache.commons.imaging.common.ImageMetadata; 040import org.apache.commons.imaging.common.XmpEmbeddable; 041import org.apache.commons.imaging.common.XmpImagingParameters; 042import org.apache.commons.imaging.formats.tiff.TiffDirectory.ImageDataElement; 043import org.apache.commons.imaging.formats.tiff.constants.TiffConstants; 044import org.apache.commons.imaging.formats.tiff.constants.TiffEpTagConstants; 045import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration; 046import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; 047import org.apache.commons.imaging.formats.tiff.datareaders.AbstractImageDataReader; 048import org.apache.commons.imaging.formats.tiff.photometricinterpreters.AbstractPhotometricInterpreter; 049import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterBiLevel; 050import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterCieLab; 051import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterCmyk; 052import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterLogLuv; 053import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterPalette; 054import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterRgb; 055import org.apache.commons.imaging.formats.tiff.photometricinterpreters.PhotometricInterpreterYCbCr; 056import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy; 057 058/** 059 * Implements methods for reading and writing TIFF files. Instances of this class are invoked from the general Imaging class. Applications that require the use 060 * of TIFF-specific features may instantiate and access this class directly. 061 */ 062public class TiffImageParser extends AbstractImageParser<TiffImagingParameters> implements XmpEmbeddable<TiffImagingParameters> { 063 064 private static final String DEFAULT_EXTENSION = ImageFormats.TIFF.getDefaultExtension(); 065 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.TIFF.getExtensions(); 066 067 /** 068 * Constructs a new instance with the big-endian byte order. 069 */ 070 public TiffImageParser() { 071 // empty 072 } 073 074 private Rectangle checkForSubImage(final TiffImagingParameters params) { 075 // the params class enforces a correct specification for the 076 // sub-image, but does not have knowledge of the actual 077 // dimensions of the image that is being read. This method 078 // returns the sub-image specification, if any, and leaves 079 // further tests to the calling module. 080 if (params != null && params.isSubImageSet()) { 081 final int ix0 = params.getSubImageX(); 082 final int iy0 = params.getSubImageY(); 083 final int iwidth = params.getSubImageWidth(); 084 final int iheight = params.getSubImageHeight(); 085 return new Rectangle(ix0, iy0, iwidth, iheight); 086 } 087 return null; 088 } 089 090 public List<byte[]> collectRawImageData(final ByteSource byteSource, final TiffImagingParameters params) throws ImagingException, IOException { 091 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 092 final TiffContents contents = new TiffReader(params != null && params.isStrict()).readDirectories(byteSource, true, formatCompliance); 093 094 final List<byte[]> result = new ArrayList<>(); 095 for (int i = 0; i < contents.directories.size(); i++) { 096 final TiffDirectory directory = contents.directories.get(i); 097 final List<ImageDataElement> dataElements = directory.getTiffRawImageDataElements(); 098 for (final ImageDataElement element : dataElements) { 099 final byte[] bytes = byteSource.getByteArray(element.offset, element.length); 100 result.add(bytes); 101 } 102 } 103 return result; 104 } 105 106 @Override 107 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException { 108 try { 109 pw.println("tiff.dumpImageFile"); 110 111 { 112 final ImageInfo imageData = getImageInfo(byteSource); 113 if (imageData == null) { 114 return false; 115 } 116 117 imageData.toString(pw, ""); 118 } 119 120 pw.println(""); 121 122 // try 123 { 124 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 125 final TiffImagingParameters params = new TiffImagingParameters(); 126 final TiffContents contents = new TiffReader(true).readContents(byteSource, params, formatCompliance); 127 128 final List<TiffDirectory> directories = contents.directories; 129 if (directories == null) { 130 return false; 131 } 132 133 for (int d = 0; d < directories.size(); d++) { 134 final TiffDirectory directory = directories.get(d); 135 136 // Debug.debug("directory offset", directory.offset); 137 138 for (final TiffField field : directory) { 139 field.dump(pw, Integer.toString(d)); 140 } 141 } 142 143 pw.println(""); 144 } 145 // catch (Exception e) 146 // { 147 // Debug.debug(e); 148 // pw.println(""); 149 // return false; 150 // } 151 152 return true; 153 } finally { 154 pw.println(""); 155 } 156 } 157 158 @Override 159 protected String[] getAcceptedExtensions() { 160 return ACCEPTED_EXTENSIONS; 161 } 162 163 @Override 164 protected ImageFormat[] getAcceptedTypes() { 165 return new ImageFormat[] { ImageFormats.TIFF, // 166 }; 167 } 168 169 @Override 170 public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource) throws ImagingException, IOException { 171 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 172 final TiffReader tiffReader = new TiffReader(true); 173 final TiffContents contents = tiffReader.readDirectories(byteSource, true, formatCompliance); 174 final List<BufferedImage> results = new ArrayList<>(); 175 for (int i = 0; i < contents.directories.size(); i++) { 176 final TiffDirectory directory = contents.directories.get(i); 177 final BufferedImage result = directory.getTiffImage(tiffReader.getByteOrder(), null); 178 if (result != null) { 179 results.add(result); 180 } 181 } 182 return results; 183 } 184 185 /** 186 * <p> 187 * Gets a buffered image specified by the byte source. The TiffImageParser class features support for a number of options that are unique to the TIFF 188 * format. These options can be specified by supplying the appropriate parameters using the keys from the TiffConstants class and the params argument for 189 * this method. 190 * </p> 191 * 192 * <p> 193 * <strong>Loading Partial Images</strong> 194 * </p> 195 * 196 * <p> 197 * The TIFF parser includes support for loading partial images without committing significantly more memory resources than are necessary to store the image. 198 * This feature is useful for conserving memory in applications that require a relatively small sub image from a very large TIFF file. The specifications 199 * for partial images are as follows: 200 * </p> 201 * 202 * <pre> 203 * TiffImagingParameters params = new TiffImagingParameters(); 204 * params.setSubImageX(x); 205 * params.setSubImageY(y); 206 * params.setSubImageWidth(width); 207 * params.setSubImageHeight(height); 208 * </pre> 209 * 210 * <p> 211 * Note that the arguments x, y, width, and height must specify a valid rectangular region that is fully contained within the source TIFF image. 212 * </p> 213 * 214 * @param byteSource A valid instance of ByteSource 215 * @param params Optional instructions for special-handling or interpretation of the input data (null objects are permitted and must be supported by 216 * implementations). 217 * @return A valid instance of BufferedImage. 218 * @throws ImagingException In the event that the specified content does not conform to the format of the specific parser implementation. 219 * @throws IOException In the event of unsuccessful read or access operation. 220 */ 221 @Override 222 public BufferedImage getBufferedImage(final ByteSource byteSource, TiffImagingParameters params) throws ImagingException, IOException { 223 if (params == null) { 224 params = new TiffImagingParameters(); 225 } 226 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 227 final TiffReader reader = new TiffReader(params.isStrict()); 228 final TiffContents contents = reader.readFirstDirectory(byteSource, true, formatCompliance); 229 final ByteOrder byteOrder = reader.getByteOrder(); 230 final TiffDirectory directory = contents.directories.get(0); 231 final BufferedImage result = directory.getTiffImage(byteOrder, params); 232 if (null == result) { 233 throw new ImagingException("TIFF does not contain an image."); 234 } 235 return result; 236 } 237 238 protected BufferedImage getBufferedImage(final TiffDirectory directory, final ByteOrder byteOrder, final TiffImagingParameters params) 239 throws ImagingException, IOException { 240 final short compressionFieldValue; 241 if (directory.findField(TiffTagConstants.TIFF_TAG_COMPRESSION) != null) { 242 compressionFieldValue = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION); 243 } else { 244 compressionFieldValue = TiffConstants.COMPRESSION_UNCOMPRESSED_1; 245 } 246 final int compression = 0xffff & compressionFieldValue; 247 final int width = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH); 248 final int height = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH); 249 250 final Rectangle subImage = checkForSubImage(params); 251 if (subImage != null) { 252 // Check for valid subimage specification. The following checks 253 // are consistent with BufferedImage.getSubimage() 254 if (subImage.width <= 0) { 255 throw new ImagingException("Negative or zero subimage width."); 256 } 257 if (subImage.height <= 0) { 258 throw new ImagingException("Negative or zero subimage height."); 259 } 260 if (subImage.x < 0 || subImage.x >= width) { 261 throw new ImagingException("Subimage x is outside raster."); 262 } 263 if (subImage.x + subImage.width > width) { 264 throw new ImagingException("Subimage (x+width) is outside raster."); 265 } 266 if (subImage.y < 0 || subImage.y >= height) { 267 throw new ImagingException("Subimage y is outside raster."); 268 } 269 if (subImage.y + subImage.height > height) { 270 throw new ImagingException("Subimage (y+height) is outside raster."); 271 } 272 } 273 274 int samplesPerPixel = 1; 275 final TiffField samplesPerPixelField = directory.findField(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL); 276 if (samplesPerPixelField != null) { 277 samplesPerPixel = samplesPerPixelField.getIntValue(); 278 } 279 int[] bitsPerSample = { 1 }; 280 int bitsPerPixel = samplesPerPixel; 281 final TiffField bitsPerSampleField = directory.findField(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE); 282 if (bitsPerSampleField != null) { 283 bitsPerSample = bitsPerSampleField.getIntArrayValue(); 284 bitsPerPixel = bitsPerSampleField.getIntValueOrArraySum(); 285 } 286 287 // int bitsPerPixel = getTagAsValueOrArraySum(entries, 288 // TIFF_TAG_BITS_PER_SAMPLE); 289 290 int predictor = -1; 291 { 292 // dumpOptionalNumberTag(entries, TIFF_TAG_FILL_ORDER); 293 // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_BYTE_COUNTS); 294 // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_OFFSETS); 295 // dumpOptionalNumberTag(entries, TIFF_TAG_ORIENTATION); 296 // dumpOptionalNumberTag(entries, TIFF_TAG_PLANAR_CONFIGURATION); 297 final TiffField predictorField = directory.findField(TiffTagConstants.TIFF_TAG_PREDICTOR); 298 if (null != predictorField) { 299 predictor = predictorField.getIntValueOrArraySum(); 300 } 301 } 302 303 if (samplesPerPixel != bitsPerSample.length) { 304 throw new ImagingException("Tiff: samplesPerPixel (" + samplesPerPixel + ")!=fBitsPerSample.length (" + bitsPerSample.length + ")"); 305 } 306 307 final int photometricInterpretation = 0xffff & directory.getFieldValue(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION); 308 309 boolean hasAlpha = false; 310 boolean isAlphaPremultiplied = false; 311 if (photometricInterpretation == TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_RGB && samplesPerPixel == 4) { 312 final TiffField extraSamplesField = directory.findField(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES); 313 if (extraSamplesField == null) { 314 // this state is not defined in the TIFF specification 315 // and so this code will interpret it as meaning that the 316 // proper handling would be ARGB. 317 hasAlpha = true; 318 isAlphaPremultiplied = false; 319 } else { 320 final int extraSamplesValue = extraSamplesField.getIntValue(); 321 switch (extraSamplesValue) { 322 case TiffTagConstants.EXTRA_SAMPLE_UNASSOCIATED_ALPHA: 323 hasAlpha = true; 324 isAlphaPremultiplied = false; 325 break; 326 case TiffTagConstants.EXTRA_SAMPLE_ASSOCIATED_ALPHA: 327 hasAlpha = true; 328 isAlphaPremultiplied = true; 329 break; 330 case 0: 331 default: 332 hasAlpha = false; 333 isAlphaPremultiplied = false; 334 break; 335 } 336 } 337 } 338 339 AbstractPhotometricInterpreter photometricInterpreter = params == null ? null : params.getCustomPhotometricInterpreter(); 340 if (photometricInterpreter == null) { 341 photometricInterpreter = getPhotometricInterpreter(directory, photometricInterpretation, bitsPerPixel, bitsPerSample, predictor, samplesPerPixel, 342 width, height); 343 } 344 345 // Obtain the planar configuration 346 final TiffField pcField = directory.findField(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION); 347 final TiffPlanarConfiguration planarConfiguration = pcField == null ? TiffPlanarConfiguration.CHUNKY 348 : TiffPlanarConfiguration.lenientValueOf(pcField.getIntValue()); 349 350 if (planarConfiguration == TiffPlanarConfiguration.PLANAR) { 351 // currently, we support the non-interleaved (non-chunky) 352 // option only in the case of a 24-bit RBG photometric interpreter 353 // and for strips (not for tiles). 354 if (photometricInterpretation != TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_RGB || bitsPerPixel != 24) { 355 throw new ImagingException("For planar configuration 2, only 24 bit RGB is currently supported"); 356 } 357 if (null == directory.findField(TiffTagConstants.TIFF_TAG_STRIP_OFFSETS)) { 358 throw new ImagingException("For planar configuration 2, only strips-organization is supported"); 359 } 360 } 361 362 final AbstractTiffImageData imageData = directory.getTiffImageData(); 363 364 final AbstractImageDataReader dataReader = imageData.getDataReader(directory, photometricInterpreter, bitsPerPixel, bitsPerSample, predictor, 365 samplesPerPixel, width, height, compression, planarConfiguration, byteOrder); 366 final ImageBuilder iBuilder = dataReader.readImageData(subImage, hasAlpha, isAlphaPremultiplied); 367 return iBuilder.getBufferedImage(); 368 } 369 370 @Override 371 public String getDefaultExtension() { 372 return DEFAULT_EXTENSION; 373 } 374 375 @Override 376 public TiffImagingParameters getDefaultParameters() { 377 return new TiffImagingParameters(); 378 } 379 380 @Override 381 public FormatCompliance getFormatCompliance(final ByteSource byteSource) throws ImagingException, IOException { 382 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 383 final TiffImagingParameters params = new TiffImagingParameters(); 384 new TiffReader(params.isStrict()).readContents(byteSource, params, formatCompliance); 385 return formatCompliance; 386 } 387 388 @Override 389 public byte[] getIccProfileBytes(final ByteSource byteSource, final TiffImagingParameters params) throws ImagingException, IOException { 390 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 391 final TiffContents contents = new TiffReader(params != null && params.isStrict()).readFirstDirectory(byteSource, false, formatCompliance); 392 final TiffDirectory directory = contents.directories.get(0); 393 394 return directory.getFieldValue(TiffEpTagConstants.EXIF_TAG_INTER_COLOR_PROFILE, false); 395 } 396 397 @Override 398 public ImageInfo getImageInfo(final ByteSource byteSource, final TiffImagingParameters params) throws ImagingException, IOException { 399 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 400 final TiffContents contents = new TiffReader(params != null && params.isStrict()).readDirectories(byteSource, false, formatCompliance); 401 final TiffDirectory directory = contents.directories.get(0); 402 403 final TiffField widthField = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, true); 404 final TiffField heightField = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, true); 405 406 if (widthField == null || heightField == null) { 407 throw new ImagingException("TIFF image missing size info."); 408 } 409 410 final int height = heightField.getIntValue(); 411 final int width = widthField.getIntValue(); 412 413 final TiffField resolutionUnitField = directory.findField(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT); 414 int resolutionUnit = 2; // Inch 415 if (resolutionUnitField != null && resolutionUnitField.getValue() != null) { 416 resolutionUnit = resolutionUnitField.getIntValue(); 417 } 418 419 double unitsPerInch = -1; 420 switch (resolutionUnit) { 421 case 1: 422 break; 423 case 2: // Inch 424 unitsPerInch = 1.0; 425 break; 426 case 3: // Centimeter 427 unitsPerInch = 2.54; 428 break; 429 default: 430 break; 431 432 } 433 434 int physicalWidthDpi = -1; 435 float physicalWidthInch = -1; 436 int physicalHeightDpi = -1; 437 float physicalHeightInch = -1; 438 439 if (unitsPerInch > 0) { 440 final TiffField xResolutionField = directory.findField(TiffTagConstants.TIFF_TAG_XRESOLUTION); 441 final TiffField yResolutionField = directory.findField(TiffTagConstants.TIFF_TAG_YRESOLUTION); 442 443 if (xResolutionField != null && xResolutionField.getValue() != null) { 444 final double xResolutionPixelsPerUnit = xResolutionField.getDoubleValue(); 445 physicalWidthDpi = (int) Math.round(xResolutionPixelsPerUnit * unitsPerInch); 446 physicalWidthInch = (float) (width / (xResolutionPixelsPerUnit * unitsPerInch)); 447 } 448 if (yResolutionField != null && yResolutionField.getValue() != null) { 449 final double yResolutionPixelsPerUnit = yResolutionField.getDoubleValue(); 450 physicalHeightDpi = (int) Math.round(yResolutionPixelsPerUnit * unitsPerInch); 451 physicalHeightInch = (float) (height / (yResolutionPixelsPerUnit * unitsPerInch)); 452 } 453 } 454 455 final TiffField bitsPerSampleField = directory.findField(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE); 456 457 int bitsPerSample = 1; 458 if (bitsPerSampleField != null && bitsPerSampleField.getValue() != null) { 459 bitsPerSample = bitsPerSampleField.getIntValueOrArraySum(); 460 } 461 462 final int bitsPerPixel = bitsPerSample; // assume grayscale; 463 // dunno if this handles colormapped images correctly. 464 465 final List<String> comments = Allocator.arrayList(directory.size()); 466 for (final TiffField field : directory) { 467 comments.add(field.toString()); 468 } 469 470 final ImageFormat format = ImageFormats.TIFF; 471 final String formatName = "TIFF Tag-based Image File Format"; 472 final String mimeType = "image/tiff"; 473 final int numberOfImages = contents.directories.size(); 474 // not accurate ... only reflects first 475 final boolean progressive = false; 476 // is TIFF ever interlaced/progressive? 477 478 final String formatDetails = "TIFF v." + contents.header.tiffVersion; 479 480 boolean transparent = false; // TODO: wrong 481 boolean usesPalette = false; 482 final TiffField colorMapField = directory.findField(TiffTagConstants.TIFF_TAG_COLOR_MAP); 483 if (colorMapField != null) { 484 usesPalette = true; 485 } 486 487 final int photoInterp = 0xffff & directory.getFieldValue(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION); 488 final TiffField extraSamplesField = directory.findField(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES); 489 final int extraSamples; 490 if (extraSamplesField == null) { 491 extraSamples = 0; // no extra samples value 492 } else { 493 extraSamples = extraSamplesField.getIntValue(); 494 } 495 final TiffField samplesPerPixelField = directory.findField(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL); 496 final int samplesPerPixel; 497 if (samplesPerPixelField == null) { 498 samplesPerPixel = 1; 499 } else { 500 samplesPerPixel = samplesPerPixelField.getIntValue(); 501 } 502 503 final ImageInfo.ColorType colorType; 504 switch (photoInterp) { 505 case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_BLACK_IS_ZERO: 506 case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_WHITE_IS_ZERO: 507 // the ImageInfo.ColorType enumeration does not distinguish 508 // between monotone white is zero or black is zero 509 colorType = ImageInfo.ColorType.BW; 510 break; 511 case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_RGB: 512 colorType = ImageInfo.ColorType.RGB; 513 // even if 4 samples per pixel are included, TIFF 514 // doesn't specify transparent unless the optional "extra samples" 515 // field is supplied with a non-zero value 516 transparent = samplesPerPixel == 4 && extraSamples != 0; 517 break; 518 case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_RGB_PALETTE: 519 colorType = ImageInfo.ColorType.RGB; 520 usesPalette = true; 521 break; 522 case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_CMYK: 523 colorType = ImageInfo.ColorType.CMYK; 524 break; 525 case TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_YCB_CR: 526 colorType = ImageInfo.ColorType.YCbCr; 527 break; 528 default: 529 colorType = ImageInfo.ColorType.UNKNOWN; 530 } 531 532 final short compressionFieldValue; 533 if (directory.findField(TiffTagConstants.TIFF_TAG_COMPRESSION) != null) { 534 compressionFieldValue = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION); 535 } else { 536 compressionFieldValue = TiffConstants.COMPRESSION_UNCOMPRESSED_1; 537 } 538 final int compression = 0xffff & compressionFieldValue; 539 final ImageInfo.CompressionAlgorithm compressionAlgorithm; 540 541 switch (compression) { 542 case TiffConstants.COMPRESSION_UNCOMPRESSED_1: 543 compressionAlgorithm = ImageInfo.CompressionAlgorithm.NONE; 544 break; 545 case TiffConstants.COMPRESSION_CCITT_1D: 546 compressionAlgorithm = ImageInfo.CompressionAlgorithm.CCITT_1D; 547 break; 548 case TiffConstants.COMPRESSION_CCITT_GROUP_3: 549 compressionAlgorithm = ImageInfo.CompressionAlgorithm.CCITT_GROUP_3; 550 break; 551 case TiffConstants.COMPRESSION_CCITT_GROUP_4: 552 compressionAlgorithm = ImageInfo.CompressionAlgorithm.CCITT_GROUP_4; 553 break; 554 case TiffConstants.COMPRESSION_LZW: 555 compressionAlgorithm = ImageInfo.CompressionAlgorithm.LZW; 556 break; 557 case TiffConstants.COMPRESSION_JPEG_OBSOLETE: 558 compressionAlgorithm = ImageInfo.CompressionAlgorithm.JPEG_TIFF_OBSOLETE; 559 break; 560 case TiffConstants.COMPRESSION_JPEG: 561 compressionAlgorithm = ImageInfo.CompressionAlgorithm.JPEG; 562 break; 563 case TiffConstants.COMPRESSION_UNCOMPRESSED_2: 564 compressionAlgorithm = ImageInfo.CompressionAlgorithm.NONE; 565 break; 566 case TiffConstants.COMPRESSION_PACKBITS: 567 compressionAlgorithm = ImageInfo.CompressionAlgorithm.PACKBITS; 568 break; 569 case TiffConstants.COMPRESSION_DEFLATE_PKZIP: 570 case TiffConstants.COMPRESSION_DEFLATE_ADOBE: 571 compressionAlgorithm = ImageInfo.CompressionAlgorithm.DEFLATE; 572 break; 573 default: 574 compressionAlgorithm = ImageInfo.CompressionAlgorithm.UNKNOWN; 575 break; 576 } 577 return new ImageInfo(formatDetails, bitsPerPixel, comments, format, formatName, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch, 578 physicalWidthDpi, physicalWidthInch, width, progressive, transparent, usesPalette, colorType, compressionAlgorithm); 579 } 580 581 @Override 582 public Dimension getImageSize(final ByteSource byteSource, final TiffImagingParameters params) throws ImagingException, IOException { 583 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 584 final TiffContents contents = new TiffReader(params != null && params.isStrict()).readFirstDirectory(byteSource, false, formatCompliance); 585 final TiffDirectory directory = contents.directories.get(0); 586 587 final TiffField widthField = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, true); 588 final TiffField heightField = directory.findField(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, true); 589 590 if (widthField == null || heightField == null) { 591 throw new ImagingException("TIFF image missing size info."); 592 } 593 594 final int height = heightField.getIntValue(); 595 final int width = widthField.getIntValue(); 596 597 return new Dimension(width, height); 598 } 599 600 @Override 601 public ImageMetadata getMetadata(final ByteSource byteSource, TiffImagingParameters params) throws ImagingException, IOException { 602 if (params == null) { 603 params = getDefaultParameters(); 604 } 605 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 606 final TiffReader tiffReader = new TiffReader(params.isStrict()); 607 final TiffContents contents = tiffReader.readContents(byteSource, params, formatCompliance); 608 609 final List<TiffDirectory> directories = contents.directories; 610 611 final TiffImageMetadata result = new TiffImageMetadata(contents); 612 613 for (final TiffDirectory dir : directories) { 614 final TiffImageMetadata.Directory metadataDirectory = new TiffImageMetadata.Directory(tiffReader.getByteOrder(), dir); 615 616 final List<TiffField> entries = dir.getDirectoryEntries(); 617 618 entries.forEach(metadataDirectory::add); 619 620 result.add(metadataDirectory); 621 } 622 623 return result; 624 } 625 626 @Override 627 public String getName() { 628 return "Tiff-Custom"; 629 } 630 631 private AbstractPhotometricInterpreter getPhotometricInterpreter(final TiffDirectory directory, final int photometricInterpretation, final int bitsPerPixel, 632 final int[] bitsPerSample, final int predictor, final int samplesPerPixel, final int width, final int height) throws ImagingException { 633 switch (photometricInterpretation) { 634 case 0: 635 case 1: 636 final boolean invert = photometricInterpretation == 0; 637 return new PhotometricInterpreterBiLevel(samplesPerPixel, bitsPerSample, predictor, width, height, invert); 638 case 3: { 639 // Palette 640 final int[] colorMap = directory.findField(TiffTagConstants.TIFF_TAG_COLOR_MAP, true).getIntArrayValue(); 641 final int expectedColormapSize = 3 * (1 << bitsPerPixel); 642 if (colorMap.length != expectedColormapSize) { 643 throw new ImagingException("Tiff: fColorMap.length (" + colorMap.length + ") != expectedColormapSize (" + expectedColormapSize + ")"); 644 } 645 return new PhotometricInterpreterPalette(samplesPerPixel, bitsPerSample, predictor, width, height, colorMap); 646 } 647 case 2: // RGB 648 return new PhotometricInterpreterRgb(samplesPerPixel, bitsPerSample, predictor, width, height); 649 case 5: // CMYK 650 return new PhotometricInterpreterCmyk(samplesPerPixel, bitsPerSample, predictor, width, height); 651 case 6: { 652// final double[] yCbCrCoefficients = directory.findField( 653// TiffTagConstants.TIFF_TAG_YCBCR_COEFFICIENTS, true) 654// .getDoubleArrayValue(); 655// 656// final int[] yCbCrPositioning = directory.findField( 657// TiffTagConstants.TIFF_TAG_YCBCR_POSITIONING, true) 658// .getIntArrayValue(); 659// final int[] yCbCrSubSampling = directory.findField( 660// TiffTagConstants.TIFF_TAG_YCBCR_SUB_SAMPLING, true) 661// .getIntArrayValue(); 662// 663// final double[] referenceBlackWhite = directory.findField( 664// TiffTagConstants.TIFF_TAG_REFERENCE_BLACK_WHITE, true) 665// .getDoubleArrayValue(); 666 return new PhotometricInterpreterYCbCr(samplesPerPixel, bitsPerSample, predictor, width, height); 667 } 668 case 8: 669 return new PhotometricInterpreterCieLab(samplesPerPixel, bitsPerSample, predictor, width, height); 670 case 32844: 671 case 32845: { 672// final boolean yonly = (photometricInterpretation == 32844); 673 return new PhotometricInterpreterLogLuv(samplesPerPixel, bitsPerSample, predictor, width, height); 674 } 675 default: 676 throw new ImagingException("TIFF: Unknown fPhotometricInterpretation: " + photometricInterpretation); 677 } 678 } 679 680 /** 681 * Reads the content of a TIFF file that contains numerical data samples rather than image-related pixels. 682 * <p> 683 * If desired, sub-image data can be read from the file by using a Java {@code TiffImagingParameters} instance to specify the subsection of the image that 684 * is required. The following code illustrates the approach: 685 * 686 * <pre> 687 * int x; // coordinate (column) of corner of sub-image 688 * int y; // coordinate (row) of corner of sub-image 689 * int width; // width of sub-image 690 * int height; // height of sub-image 691 * 692 * TiffImagingParameters params = new TiffImagingParameters(); 693 * params.setSubImageX(x); 694 * params.setSubImageY(y); 695 * params.setSubImageWidth(width); 696 * params.setSubImageHeight(height); 697 * TiffRasterData raster = readFloatingPointRasterData(directory, byteOrder, params); 698 * </pre> 699 * 700 * @param directory the TIFF directory pointing to the data to be extracted (TIFF files may contain multiple directories) 701 * @param byteOrder the byte order of the data to be extracted 702 * @param params an optional parameter object instance 703 * @return a valid instance 704 * @throws ImagingException in the event of incompatible or malformed data 705 * @throws IOException in the event of an I/O error 706 */ 707 AbstractTiffRasterData getRasterData(final TiffDirectory directory, final ByteOrder byteOrder, TiffImagingParameters params) 708 throws ImagingException, IOException { 709 if (params == null) { 710 params = getDefaultParameters(); 711 } 712 final short[] sSampleFmt = directory.getFieldValue(TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, true); 713 if (sSampleFmt == null || sSampleFmt.length < 1) { 714 throw new ImagingException("Directory does not specify numeric raster data"); 715 } 716 int samplesPerPixel = 1; 717 final TiffField samplesPerPixelField = directory.findField(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL); 718 if (samplesPerPixelField != null) { 719 samplesPerPixel = samplesPerPixelField.getIntValue(); 720 } 721 int[] bitsPerSample = { 1 }; 722 int bitsPerPixel = samplesPerPixel; 723 final TiffField bitsPerSampleField = directory.findField(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE); 724 if (bitsPerSampleField != null) { 725 bitsPerSample = bitsPerSampleField.getIntArrayValue(); 726 bitsPerPixel = bitsPerSampleField.getIntValueOrArraySum(); 727 } 728 final short compressionFieldValue; 729 if (directory.findField(TiffTagConstants.TIFF_TAG_COMPRESSION) != null) { 730 compressionFieldValue = directory.getFieldValue(TiffTagConstants.TIFF_TAG_COMPRESSION); 731 } else { 732 compressionFieldValue = TiffConstants.COMPRESSION_UNCOMPRESSED_1; 733 } 734 final int compression = 0xffff & compressionFieldValue; 735 final int width = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH); 736 final int height = directory.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH); 737 Rectangle subImage = checkForSubImage(params); 738 if (subImage != null) { 739 // Check for valid subimage specification. The following checks 740 // are consistent with BufferedImage.getSubimage() 741 if (subImage.width <= 0) { 742 throw new ImagingException("Negative or zero subimage width."); 743 } 744 if (subImage.height <= 0) { 745 throw new ImagingException("Negative or zero subimage height."); 746 } 747 if (subImage.x < 0 || subImage.x >= width) { 748 throw new ImagingException("Subimage x is outside raster."); 749 } 750 if (subImage.x + subImage.width > width) { 751 throw new ImagingException("Subimage (x+width) is outside raster."); 752 } 753 if (subImage.y < 0 || subImage.y >= height) { 754 throw new ImagingException("Subimage y is outside raster."); 755 } 756 if (subImage.y + subImage.height > height) { 757 throw new ImagingException("Subimage (y+height) is outside raster."); 758 } 759 // if the subimage is just the same thing as the whole 760 // image, suppress the subimage processing 761 if (subImage.x == 0 && subImage.y == 0 && subImage.width == width && subImage.height == height) { 762 subImage = null; 763 } 764 } 765 // int bitsPerPixel = getTagAsValueOrArraySum(entries, 766 // TIFF_TAG_BITS_PER_SAMPLE); 767 int predictor = -1; 768 { 769 // dumpOptionalNumberTag(entries, TIFF_TAG_FILL_ORDER); 770 // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_BYTE_COUNTS); 771 // dumpOptionalNumberTag(entries, TIFF_TAG_FREE_OFFSETS); 772 // dumpOptionalNumberTag(entries, TIFF_TAG_ORIENTATION); 773 // dumpOptionalNumberTag(entries, TIFF_TAG_PLANAR_CONFIGURATION); 774 final TiffField predictorField = directory.findField(TiffTagConstants.TIFF_TAG_PREDICTOR); 775 if (null != predictorField) { 776 predictor = predictorField.getIntValueOrArraySum(); 777 } 778 } 779 // Obtain the planar configuration 780 final TiffField pcField = directory.findField(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION); 781 final TiffPlanarConfiguration planarConfiguration = pcField == null ? TiffPlanarConfiguration.CHUNKY 782 : TiffPlanarConfiguration.lenientValueOf(pcField.getIntValue()); 783 if (sSampleFmt[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT) { 784 if (bitsPerSample[0] != 32 && bitsPerSample[0] != 64) { 785 throw new ImagingException("TIFF floating-point data uses unsupported bits-per-sample: " + bitsPerSample[0]); 786 } 787 if (predictor != -1 && predictor != TiffTagConstants.PREDICTOR_VALUE_NONE 788 && predictor != TiffTagConstants.PREDICTOR_VALUE_FLOATING_POINT_DIFFERENCING) { 789 throw new ImagingException("TIFF floating-point data uses unsupported horizontal-differencing predictor"); 790 } 791 } else if (sSampleFmt[0] == TiffTagConstants.SAMPLE_FORMAT_VALUE_TWOS_COMPLEMENT_SIGNED_INTEGER) { 792 if (samplesPerPixel != 1) { 793 throw new ImagingException("TIFF integer data uses unsupported samples per pixel: " + samplesPerPixel); 794 } 795 if (bitsPerPixel != 16 && bitsPerPixel != 32) { 796 throw new ImagingException("TIFF integer data uses unsupported bits-per-pixel: " + bitsPerPixel); 797 } 798 if (predictor != -1 && predictor != TiffTagConstants.PREDICTOR_VALUE_NONE 799 && predictor != TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING) { 800 throw new ImagingException("TIFF integer data uses unsupported horizontal-differencing predictor"); 801 } 802 } else { 803 throw new ImagingException("TIFF does not provide a supported raster-data format"); 804 } 805 // The photometric interpreter is not used, but the image-based 806 // data reader classes require one. So we create a dummy interpreter. 807 final AbstractPhotometricInterpreter photometricInterpreter = new PhotometricInterpreterBiLevel(samplesPerPixel, bitsPerSample, predictor, width, 808 height, false); 809 final AbstractTiffImageData imageData = directory.getTiffImageData(); 810 final AbstractImageDataReader dataReader = imageData.getDataReader(directory, photometricInterpreter, bitsPerPixel, bitsPerSample, predictor, 811 samplesPerPixel, width, height, compression, planarConfiguration, byteOrder); 812 return dataReader.readRasterData(subImage); 813 } 814 815 @Override 816 public String getXmpXml(final ByteSource byteSource, XmpImagingParameters<TiffImagingParameters> params) throws ImagingException, IOException { 817 if (params == null) { 818 params = new XmpImagingParameters<>(); 819 } 820 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 821 final TiffContents contents = new TiffReader(params.isStrict()).readDirectories(byteSource, false, formatCompliance); 822 final TiffDirectory directory = contents.directories.get(0); 823 824 final byte[] bytes = directory.getFieldValue(TiffTagConstants.TIFF_TAG_XMP, false); 825 if (bytes == null) { 826 return null; 827 } 828 829 // segment data is UTF-8 encoded xml. 830 return new String(bytes, StandardCharsets.UTF_8); 831 } 832 833 @Override 834 public void writeImage(final BufferedImage src, final OutputStream os, TiffImagingParameters params) throws ImagingException, IOException { 835 if (params == null) { 836 params = new TiffImagingParameters(); 837 } 838 new TiffImageWriterLossy().writeImage(src, os, params); 839 } 840 841}