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.write; 018 019import java.awt.image.BufferedImage; 020import java.awt.image.ColorModel; 021import java.io.IOException; 022import java.io.OutputStream; 023import java.nio.ByteOrder; 024import java.nio.charset.StandardCharsets; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Map; 032 033import org.apache.commons.imaging.ImagingException; 034import org.apache.commons.imaging.PixelDensity; 035import org.apache.commons.imaging.common.AbstractBinaryOutputStream; 036import org.apache.commons.imaging.common.Allocator; 037import org.apache.commons.imaging.common.PackBits; 038import org.apache.commons.imaging.common.RationalNumber; 039import org.apache.commons.imaging.common.ZlibDeflate; 040import org.apache.commons.imaging.formats.tiff.AbstractTiffElement; 041import org.apache.commons.imaging.formats.tiff.AbstractTiffImageData; 042import org.apache.commons.imaging.formats.tiff.TiffImagingParameters; 043import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants; 044import org.apache.commons.imaging.formats.tiff.constants.TiffConstants; 045import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants; 046import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; 047import org.apache.commons.imaging.formats.tiff.itu_t4.T4AndT6Compression; 048import org.apache.commons.imaging.mylzw.MyLzwCompressor; 049 050public abstract class AbstractTiffImageWriter { 051 052 private static final int MAX_PIXELS_FOR_RGB = 1024 * 1024; 053 054 protected static int imageDataPaddingLength(final int dataLength) { 055 return (4 - dataLength % 4) % 4; 056 } 057 058 protected final ByteOrder byteOrder; 059 060 public AbstractTiffImageWriter() { 061 this.byteOrder = TiffConstants.DEFAULT_TIFF_BYTE_ORDER; 062 } 063 064 public AbstractTiffImageWriter(final ByteOrder byteOrder) { 065 this.byteOrder = byteOrder; 066 } 067 068 private void applyPredictor(final int width, final int bytesPerSample, final byte[] b) { 069 final int nBytesPerRow = bytesPerSample * width; 070 final int nRows = b.length / nBytesPerRow; 071 for (int iRow = 0; iRow < nRows; iRow++) { 072 final int offset = iRow * nBytesPerRow; 073 for (int i = nBytesPerRow - 1; i >= bytesPerSample; i--) { 074 b[offset + i] -= b[offset + i - bytesPerSample]; 075 } 076 } 077 } 078 079 /** 080 * Check an image to see if any of its pixels are non-opaque. 081 * 082 * @param src a valid image 083 * @return true if at least one non-opaque pixel is found. 084 */ 085 private boolean checkForActualAlpha(final BufferedImage src) { 086 // to conserve memory, very large images may be read 087 // in pieces. 088 final int width = src.getWidth(); 089 final int height = src.getHeight(); 090 int nRowsPerRead = MAX_PIXELS_FOR_RGB / width; 091 if (nRowsPerRead < 1) { 092 nRowsPerRead = 1; 093 } 094 final int nReads = (height + nRowsPerRead - 1) / nRowsPerRead; 095 final int[] argb = Allocator.intArray(nRowsPerRead * width); 096 for (int iRead = 0; iRead < nReads; iRead++) { 097 final int i0 = iRead * nRowsPerRead; 098 final int i1 = i0 + nRowsPerRead > height ? height : i0 + nRowsPerRead; 099 src.getRGB(0, i0, width, i1 - i0, argb, 0, width); 100 final int n = (i1 - i0) * width; 101 for (int i = 0; i < n; i++) { 102 if ((argb[i] & 0xff000000) != 0xff000000) { 103 return true; 104 } 105 } 106 } 107 return false; 108 } 109 110 private void combineUserExifIntoFinalExif(final TiffOutputSet userExif, final TiffOutputSet outputSet) throws ImagingException { 111 final List<TiffOutputDirectory> outputDirectories = outputSet.getDirectories(); 112 outputDirectories.sort(TiffOutputDirectory.COMPARATOR); 113 for (final TiffOutputDirectory userDirectory : userExif.getDirectories()) { 114 final int location = Collections.binarySearch(outputDirectories, userDirectory, TiffOutputDirectory.COMPARATOR); 115 if (location < 0) { 116 outputSet.addDirectory(userDirectory); 117 } else { 118 final TiffOutputDirectory outputDirectory = outputDirectories.get(location); 119 for (final TiffOutputField userField : userDirectory) { 120 if (outputDirectory.findField(userField.tagInfo) == null) { 121 outputDirectory.add(userField); 122 } 123 } 124 } 125 } 126 } 127 128 private byte[][] getStrips(final BufferedImage src, final int samplesPerPixel, final int bitsPerSample, final int rowsPerStrip) { 129 final int width = src.getWidth(); 130 final int height = src.getHeight(); 131 132 final int stripCount = (height + rowsPerStrip - 1) / rowsPerStrip; 133 134 // Write Strips 135 final byte[][] result = new byte[Allocator.check(stripCount)][]; 136 137 int remainingRows = height; 138 139 for (int i = 0; i < stripCount; i++) { 140 final int rowsInStrip = Math.min(rowsPerStrip, remainingRows); 141 remainingRows -= rowsInStrip; 142 143 final int bitsInRow = bitsPerSample * samplesPerPixel * width; 144 final int bytesPerRow = (bitsInRow + 7) / 8; 145 final int bytesInStrip = rowsInStrip * bytesPerRow; 146 147 final byte[] uncompressed = Allocator.byteArray(bytesInStrip); 148 149 int counter = 0; 150 int y = i * rowsPerStrip; 151 final int stop = i * rowsPerStrip + rowsPerStrip; 152 153 for (; y < height && y < stop; y++) { 154 int bitCache = 0; 155 int bitsInCache = 0; 156 for (int x = 0; x < width; x++) { 157 final int rgb = src.getRGB(x, y); 158 final int red = 0xff & rgb >> 16; 159 final int green = 0xff & rgb >> 8; 160 final int blue = 0xff & rgb >> 0; 161 162 if (bitsPerSample == 1) { 163 int sample = (red + green + blue) / 3; 164 if (sample > 127) { 165 sample = 0; 166 } else { 167 sample = 1; 168 } 169 bitCache <<= 1; 170 bitCache |= sample; 171 bitsInCache++; 172 if (bitsInCache == 8) { 173 uncompressed[counter++] = (byte) bitCache; 174 bitCache = 0; 175 bitsInCache = 0; 176 } 177 } else if (samplesPerPixel == 4) { 178 uncompressed[counter++] = (byte) red; 179 uncompressed[counter++] = (byte) green; 180 uncompressed[counter++] = (byte) blue; 181 uncompressed[counter++] = (byte) (rgb >> 24); 182 } else { 183 // samples per pixel is 3 184 uncompressed[counter++] = (byte) red; 185 uncompressed[counter++] = (byte) green; 186 uncompressed[counter++] = (byte) blue; 187 } 188 } 189 if (bitsInCache > 0) { 190 bitCache <<= 8 - bitsInCache; 191 uncompressed[counter++] = (byte) bitCache; 192 } 193 } 194 195 result[i] = uncompressed; 196 } 197 198 return result; 199 } 200 201 protected TiffOutputSummary validateDirectories(final TiffOutputSet outputSet) throws ImagingException { 202 if (outputSet.isEmpty()) { 203 throw new ImagingException("No directories."); 204 } 205 206 TiffOutputDirectory exifDirectory = null; 207 TiffOutputDirectory gpsDirectory = null; 208 TiffOutputDirectory interoperabilityDirectory = null; 209 TiffOutputField exifDirectoryOffsetField = null; 210 TiffOutputField gpsDirectoryOffsetField = null; 211 TiffOutputField interoperabilityDirectoryOffsetField = null; 212 213 final List<Integer> directoryIndices = new ArrayList<>(); 214 final Map<Integer, TiffOutputDirectory> directoryTypeMap = new HashMap<>(); 215 for (final TiffOutputDirectory directory : outputSet) { 216 final int dirType = directory.getType(); 217 directoryTypeMap.put(dirType, directory); 218 // Debug.debug("validating dirType", dirType + " (" 219 // + directory.getFields().size() + " fields)"); 220 221 if (dirType < 0) { 222 switch (dirType) { 223 case TiffDirectoryConstants.DIRECTORY_TYPE_EXIF: 224 if (exifDirectory != null) { 225 throw new ImagingException("More than one EXIF directory."); 226 } 227 exifDirectory = directory; 228 break; 229 230 case TiffDirectoryConstants.DIRECTORY_TYPE_GPS: 231 if (gpsDirectory != null) { 232 throw new ImagingException("More than one GPS directory."); 233 } 234 gpsDirectory = directory; 235 break; 236 237 case TiffDirectoryConstants.DIRECTORY_TYPE_INTEROPERABILITY: 238 if (interoperabilityDirectory != null) { 239 throw new ImagingException("More than one Interoperability directory."); 240 } 241 interoperabilityDirectory = directory; 242 break; 243 default: 244 throw new ImagingException("Unknown directory: " + dirType); 245 } 246 } else { 247 if (directoryIndices.contains(dirType)) { 248 throw new ImagingException("More than one directory with index: " + dirType + "."); 249 } 250 directoryIndices.add(dirType); 251 // dirMap.put(arg0, arg1) 252 } 253 254 final HashSet<Integer> fieldTags = new HashSet<>(); 255 for (final TiffOutputField field : directory) { 256 if (fieldTags.contains(field.tag)) { 257 throw new ImagingException("Tag (" + field.tagInfo.getDescription() + ") appears twice in directory."); 258 } 259 fieldTags.add(field.tag); 260 261 if (field.tag == ExifTagConstants.EXIF_TAG_EXIF_OFFSET.tag) { 262 if (exifDirectoryOffsetField != null) { 263 throw new ImagingException("More than one Exif directory offset field."); 264 } 265 exifDirectoryOffsetField = field; 266 } else if (field.tag == ExifTagConstants.EXIF_TAG_INTEROP_OFFSET.tag) { 267 if (interoperabilityDirectoryOffsetField != null) { 268 throw new ImagingException("More than one Interoperability directory offset field."); 269 } 270 interoperabilityDirectoryOffsetField = field; 271 } else if (field.tag == ExifTagConstants.EXIF_TAG_GPSINFO.tag) { 272 if (gpsDirectoryOffsetField != null) { 273 throw new ImagingException("More than one GPS directory offset field."); 274 } 275 gpsDirectoryOffsetField = field; 276 } 277 } 278 // directory. 279 } 280 281 if (directoryIndices.isEmpty()) { 282 throw new ImagingException("Missing root directory."); 283 } 284 285 // "normal" TIFF directories should have continous indices starting with 286 // 0, ie. 0, 1, 2... 287 Collections.sort(directoryIndices); 288 289 TiffOutputDirectory previousDirectory = null; 290 for (int i = 0; i < directoryIndices.size(); i++) { 291 final Integer index = directoryIndices.get(i); 292 if (index != i) { 293 throw new ImagingException("Missing directory: " + i + "."); 294 } 295 296 // set up chain of directory references for "normal" directories. 297 final TiffOutputDirectory directory = directoryTypeMap.get(index); 298 if (null != previousDirectory) { 299 previousDirectory.setNextDirectory(directory); 300 } 301 previousDirectory = directory; 302 } 303 304 final TiffOutputDirectory rootDirectory = directoryTypeMap.get(TiffDirectoryConstants.DIRECTORY_TYPE_ROOT); 305 306 // prepare results 307 final TiffOutputSummary result = new TiffOutputSummary(byteOrder, rootDirectory, directoryTypeMap); 308 309 if (interoperabilityDirectory == null && interoperabilityDirectoryOffsetField != null) { 310 // perhaps we should just discard field? 311 throw new ImagingException("Output set has Interoperability Directory Offset field, but no Interoperability Directory"); 312 } 313 if (interoperabilityDirectory != null) { 314 if (exifDirectory == null) { 315 exifDirectory = outputSet.addExifDirectory(); 316 } 317 318 if (interoperabilityDirectoryOffsetField == null) { 319 interoperabilityDirectoryOffsetField = TiffOutputField.createOffsetField(ExifTagConstants.EXIF_TAG_INTEROP_OFFSET, byteOrder); 320 exifDirectory.add(interoperabilityDirectoryOffsetField); 321 } 322 323 result.add(interoperabilityDirectory, interoperabilityDirectoryOffsetField); 324 } 325 326 // make sure offset fields and offset'd directories correspond. 327 if (exifDirectory == null && exifDirectoryOffsetField != null) { 328 // perhaps we should just discard field? 329 throw new ImagingException("Output set has Exif Directory Offset field, but no Exif Directory"); 330 } 331 if (exifDirectory != null) { 332 if (exifDirectoryOffsetField == null) { 333 exifDirectoryOffsetField = TiffOutputField.createOffsetField(ExifTagConstants.EXIF_TAG_EXIF_OFFSET, byteOrder); 334 rootDirectory.add(exifDirectoryOffsetField); 335 } 336 337 result.add(exifDirectory, exifDirectoryOffsetField); 338 } 339 340 if (gpsDirectory == null && gpsDirectoryOffsetField != null) { 341 // perhaps we should just discard field? 342 throw new ImagingException("Output set has GPS Directory Offset field, but no GPS Directory"); 343 } 344 if (gpsDirectory != null) { 345 if (gpsDirectoryOffsetField == null) { 346 gpsDirectoryOffsetField = TiffOutputField.createOffsetField(ExifTagConstants.EXIF_TAG_GPSINFO, byteOrder); 347 rootDirectory.add(gpsDirectoryOffsetField); 348 } 349 350 result.add(gpsDirectory, gpsDirectoryOffsetField); 351 } 352 353 return result; 354 355 // Debug.debug(); 356 } 357 358 public abstract void write(OutputStream os, TiffOutputSet outputSet) throws IOException, ImagingException; 359 360 public void writeImage(final BufferedImage src, final OutputStream os, final TiffImagingParameters params) throws ImagingException, IOException { 361 final TiffOutputSet userExif = params.getOutputSet(); 362 363 final String xmpXml = params.getXmpXml(); 364 365 PixelDensity pixelDensity = params.getPixelDensity(); 366 if (pixelDensity == null) { 367 pixelDensity = PixelDensity.createFromPixelsPerInch(72, 72); 368 } 369 370 final int width = src.getWidth(); 371 final int height = src.getHeight(); 372 373 // If the source image has a color model that supports alpha, 374 // this module performs a call to checkForActualAlpha() to see whether 375 // the image that was supplied to the API actually contains 376 // non-opaque data in its alpha channel. It is common for applications 377 // to create a BufferedImage using TYPE_INT_ARGB, and fill the entire 378 // image with opaque pixels. In such a case, the file size of the output 379 // can be reduced by 25 percent by storing the image in an 3-byte RGB 380 // format. This approach will also make a small reduction in the runtime 381 // to read the resulting file when it is accessed by an application. 382 final ColorModel cModel = src.getColorModel(); 383 final boolean hasAlpha = cModel.hasAlpha() && checkForActualAlpha(src); 384 385 // 10/2020: In the case of an image with pre-multiplied alpha 386 // (what the TIFF specification calls "associated alpha"), the 387 // Java getRGB method adjusts the value to a non-premultiplied 388 // alpha state. However, this class could access the pre-multiplied 389 // alpha data by obtaining the underlying raster. At this time, 390 // the value of such a little-used feature does not seem 391 // commensurate with the complexity of the extra code it would require. 392 393 int compression = TiffConstants.COMPRESSION_LZW; 394 short predictor = TiffTagConstants.PREDICTOR_VALUE_NONE; 395 396 int stripSizeInBits = 64000; // the default from legacy implementation 397 final Integer compressionParameter = params.getCompression(); 398 if (compressionParameter != null) { 399 compression = compressionParameter; 400 final Integer stripSizeInBytes = params.getLzwCompressionBlockSize(); 401 if (stripSizeInBytes != null) { 402 if (stripSizeInBytes < 8000) { 403 throw new ImagingException("Block size parameter " + stripSizeInBytes + " is less than 8000 minimum"); 404 } 405 stripSizeInBits = stripSizeInBytes * 8; 406 } 407 } 408 409 final int samplesPerPixel; 410 final int bitsPerSample; 411 final int photometricInterpretation; 412 if (compression == TiffConstants.COMPRESSION_CCITT_1D || compression == TiffConstants.COMPRESSION_CCITT_GROUP_3 413 || compression == TiffConstants.COMPRESSION_CCITT_GROUP_4) { 414 samplesPerPixel = 1; 415 bitsPerSample = 1; 416 photometricInterpretation = 0; 417 } else { 418 samplesPerPixel = hasAlpha ? 4 : 3; 419 bitsPerSample = 8; 420 photometricInterpretation = 2; 421 } 422 423 int rowsPerStrip = stripSizeInBits / (width * bitsPerSample * samplesPerPixel); 424 rowsPerStrip = Math.max(1, rowsPerStrip); // must have at least one. 425 426 final byte[][] strips = getStrips(src, samplesPerPixel, bitsPerSample, rowsPerStrip); 427 428 // System.out.println("width: " + width); 429 // System.out.println("height: " + height); 430 // System.out.println("fRowsPerStrip: " + fRowsPerStrip); 431 // System.out.println("fSamplesPerPixel: " + fSamplesPerPixel); 432 // System.out.println("stripCount: " + stripCount); 433 434 int t4Options = 0; 435 int t6Options = 0; 436 switch (compression) { 437 case TiffConstants.COMPRESSION_CCITT_1D: 438 for (int i = 0; i < strips.length; i++) { 439 strips[i] = T4AndT6Compression.compressModifiedHuffman(strips[i], width, strips[i].length / ((width + 7) / 8)); 440 } 441 break; 442 case TiffConstants.COMPRESSION_CCITT_GROUP_3: { 443 final Integer t4Parameter = params.getT4Options(); 444 if (t4Parameter != null) { 445 t4Options = t4Parameter.intValue(); 446 } 447 t4Options &= 0x7; 448 final boolean is2D = (t4Options & 1) != 0; 449 final boolean usesUncompressedMode = (t4Options & 2) != 0; 450 if (usesUncompressedMode) { 451 throw new ImagingException("T.4 compression with the uncompressed mode extension is not yet supported"); 452 } 453 final boolean hasFillBitsBeforeEOL = (t4Options & 4) != 0; 454 for (int i = 0; i < strips.length; i++) { 455 if (is2D) { 456 strips[i] = T4AndT6Compression.compressT4_2D(strips[i], width, strips[i].length / ((width + 7) / 8), hasFillBitsBeforeEOL, rowsPerStrip); 457 } else { 458 strips[i] = T4AndT6Compression.compressT4_1D(strips[i], width, strips[i].length / ((width + 7) / 8), hasFillBitsBeforeEOL); 459 } 460 } 461 break; 462 } 463 case TiffConstants.COMPRESSION_CCITT_GROUP_4: { 464 final Integer t6Parameter = params.getT6Options(); 465 if (t6Parameter != null) { 466 t6Options = t6Parameter.intValue(); 467 } 468 t6Options &= 0x4; 469 final boolean usesUncompressedMode = (t6Options & TiffConstants.FLAG_T6_OPTIONS_UNCOMPRESSED_MODE) != 0; 470 if (usesUncompressedMode) { 471 throw new ImagingException("T.6 compression with the uncompressed mode extension is not yet supported"); 472 } 473 for (int i = 0; i < strips.length; i++) { 474 strips[i] = T4AndT6Compression.compressT6(strips[i], width, strips[i].length / ((width + 7) / 8)); 475 } 476 break; 477 } 478 case TiffConstants.COMPRESSION_PACKBITS: 479 for (int i = 0; i < strips.length; i++) { 480 strips[i] = PackBits.compress(strips[i]); 481 } 482 break; 483 case TiffConstants.COMPRESSION_LZW: 484 predictor = TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING; 485 for (int i = 0; i < strips.length; i++) { 486 final byte[] uncompressed = strips[i]; 487 applyPredictor(width, samplesPerPixel, strips[i]); 488 489 final int LZW_MINIMUM_CODE_SIZE = 8; 490 final MyLzwCompressor compressor = new MyLzwCompressor(LZW_MINIMUM_CODE_SIZE, ByteOrder.BIG_ENDIAN, true); 491 final byte[] compressed = compressor.compress(uncompressed); 492 strips[i] = compressed; 493 } 494 break; 495 case TiffConstants.COMPRESSION_DEFLATE_ADOBE: 496 predictor = TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING; 497 for (int i = 0; i < strips.length; i++) { 498 applyPredictor(width, samplesPerPixel, strips[i]); 499 strips[i] = ZlibDeflate.compress(strips[i]); 500 } 501 break; 502 case TiffConstants.COMPRESSION_UNCOMPRESSED: 503 break; 504 default: 505 throw new ImagingException( 506 "Invalid compression parameter (Only CCITT 1D/Group 3/Group 4, LZW, Packbits, Zlib Deflate and uncompressed supported)."); 507 } 508 509 final AbstractTiffElement.DataElement[] imageData = new AbstractTiffElement.DataElement[strips.length]; 510 Arrays.setAll(imageData, i -> new AbstractTiffImageData.Data(0, strips[i].length, strips[i])); 511 512 final TiffOutputSet outputSet = new TiffOutputSet(byteOrder); 513 final TiffOutputDirectory directory = outputSet.addRootDirectory(); 514 515 // WriteField stripOffsetsField; 516 517 directory.add(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, width); 518 directory.add(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, height); 519 directory.add(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION, (short) photometricInterpretation); 520 directory.add(TiffTagConstants.TIFF_TAG_COMPRESSION, (short) compression); 521 directory.add(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL, (short) samplesPerPixel); 522 523 switch (samplesPerPixel) { 524 case 3: 525 directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample, (short) bitsPerSample, (short) bitsPerSample); 526 break; 527 case 4: 528 directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample, (short) bitsPerSample, (short) bitsPerSample, 529 (short) bitsPerSample); 530 directory.add(TiffTagConstants.TIFF_TAG_EXTRA_SAMPLES, (short) TiffTagConstants.EXTRA_SAMPLE_UNASSOCIATED_ALPHA); 531 break; 532 case 1: 533 directory.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample); 534 break; 535 default: 536 break; 537 } 538 // { 539 // stripOffsetsField = new WriteField(TIFF_TAG_STRIP_OFFSETS, 540 // FIELD_TYPE_LONG, stripOffsets.length, FIELD_TYPE_LONG 541 // .writeData(stripOffsets, byteOrder)); 542 // directory.add(stripOffsetsField); 543 // } 544 // { 545 // WriteField field = new WriteField(TIFF_TAG_STRIP_BYTE_COUNTS, 546 // FIELD_TYPE_LONG, stripByteCounts.length, 547 // FIELD_TYPE_LONG.writeData(stripByteCounts, 548 // WRITE_BYTE_ORDER)); 549 // directory.add(field); 550 // } 551 directory.add(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP, rowsPerStrip); 552 if (pixelDensity.isUnitless()) { 553 directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, (short) 0); 554 directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, RationalNumber.valueOf(pixelDensity.getRawHorizontalDensity())); 555 directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, RationalNumber.valueOf(pixelDensity.getRawVerticalDensity())); 556 } else if (pixelDensity.isInInches()) { 557 directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, (short) 2); 558 directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, RationalNumber.valueOf(pixelDensity.horizontalDensityInches())); 559 directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, RationalNumber.valueOf(pixelDensity.verticalDensityInches())); 560 } else { 561 directory.add(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT, (short) 1); 562 directory.add(TiffTagConstants.TIFF_TAG_XRESOLUTION, RationalNumber.valueOf(pixelDensity.horizontalDensityCentimetres())); 563 directory.add(TiffTagConstants.TIFF_TAG_YRESOLUTION, RationalNumber.valueOf(pixelDensity.verticalDensityCentimetres())); 564 } 565 if (t4Options != 0) { 566 directory.add(TiffTagConstants.TIFF_TAG_T4_OPTIONS, t4Options); 567 } 568 if (t6Options != 0) { 569 directory.add(TiffTagConstants.TIFF_TAG_T6_OPTIONS, t6Options); 570 } 571 572 if (null != xmpXml) { 573 final byte[] xmpXmlBytes = xmpXml.getBytes(StandardCharsets.UTF_8); 574 directory.add(TiffTagConstants.TIFF_TAG_XMP, xmpXmlBytes); 575 } 576 577 if (predictor == TiffTagConstants.PREDICTOR_VALUE_HORIZONTAL_DIFFERENCING) { 578 directory.add(TiffTagConstants.TIFF_TAG_PREDICTOR, predictor); 579 } 580 581 final AbstractTiffImageData abstractTiffImageData = new AbstractTiffImageData.Strips(imageData, rowsPerStrip); 582 directory.setTiffImageData(abstractTiffImageData); 583 584 if (userExif != null) { 585 combineUserExifIntoFinalExif(userExif, outputSet); 586 } 587 588 write(os, outputSet); 589 } 590 591 protected void writeImageFileHeader(final AbstractBinaryOutputStream bos) throws IOException { 592 writeImageFileHeader(bos, TiffConstants.HEADER_SIZE); 593 } 594 595 protected void writeImageFileHeader(final AbstractBinaryOutputStream bos, final long offsetToFirstIFD) throws IOException { 596 if (byteOrder == ByteOrder.LITTLE_ENDIAN) { 597 bos.write('I'); 598 bos.write('I'); 599 } else { 600 bos.write('M'); 601 bos.write('M'); 602 } 603 604 bos.write2Bytes(42); // tiffVersion 605 606 bos.write4Bytes((int) offsetToFirstIFD); 607 } 608 609}