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