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