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