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