    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.TIFF_HEADER_SIZE;
    020
    021import java.io.IOException;
    022import java.io.OutputStream;
    023import java.nio.ByteOrder;
    024import java.util.ArrayList;
    025import java.util.Arrays;
    026import java.util.Collections;
    027import java.util.Comparator;
    028import java.util.HashMap;
    029import java.util.List;
    030import java.util.Map;
    031
    032import org.apache.commons.imaging.FormatCompliance;
    033import org.apache.commons.imaging.ImageReadException;
    034import org.apache.commons.imaging.ImageWriteException;
    035import org.apache.commons.imaging.common.BinaryOutputStream;
    036import org.apache.commons.imaging.common.bytesource.ByteSource;
    037import org.apache.commons.imaging.common.bytesource.ByteSourceArray;
    038import org.apache.commons.imaging.formats.tiff.JpegImageData;
    039import org.apache.commons.imaging.formats.tiff.TiffContents;
    040import org.apache.commons.imaging.formats.tiff.TiffDirectory;
    041import org.apache.commons.imaging.formats.tiff.TiffElement;
    042import org.apache.commons.imaging.formats.tiff.TiffElement.DataElement;
    043import org.apache.commons.imaging.formats.tiff.TiffField;
    044import org.apache.commons.imaging.formats.tiff.TiffImageData;
    045import org.apache.commons.imaging.formats.tiff.TiffReader;
    046import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
    047
    048public class TiffImageWriterLossless extends TiffImageWriterBase {
    049    private final byte[] exifBytes;
    050    private static final Comparator<TiffElement> ELEMENT_SIZE_COMPARATOR = (e1, e2) -> e1.length - e2.length;
    051    private static final Comparator<TiffOutputItem> ITEM_SIZE_COMPARATOR = (e1, e2) -> e1.getItemLength() - e2.getItemLength();
    052
    053    public TiffImageWriterLossless(final byte[] exifBytes) {
    054        this.exifBytes = exifBytes;
    055    }
    056
    057    public TiffImageWriterLossless(final ByteOrder byteOrder, final byte[] exifBytes) {
    058        super(byteOrder);
    059        this.exifBytes = exifBytes;
    060    }
    061
    062    private List<TiffElement> analyzeOldTiff(final Map<Integer, TiffOutputField> frozenFields) throws ImageWriteException,
    063            IOException {
    064        try {
    065            final ByteSource byteSource = new ByteSourceArray(exifBytes);
    066            final Map<String, Object> params = null;
    067            final FormatCompliance formatCompliance = FormatCompliance.getDefault();
    068            final TiffContents contents = new TiffReader(false).readContents(
    069                    byteSource, params, formatCompliance);
    070
    071            final List<TiffElement> elements = new ArrayList<>();
    072
    073            final List<TiffDirectory> directories = contents.directories;
    074            for (final TiffDirectory directory : directories) {
    075                elements.add(directory);
    076
    077                for (final TiffField field : directory.getDirectoryEntries()) {
    078                    final TiffElement oversizeValue = field.getOversizeValueElement();
    079                    if (oversizeValue != null) {
    080                        final TiffOutputField frozenField = frozenFields.get(field.getTag());
    081                        if (frozenField != null
    082                                && frozenField.getSeperateValue() != null
    083                                && frozenField.bytesEqual(field.getByteArrayValue())) {
    084                            frozenField.getSeperateValue().setOffset(field.getOffset());
    085                        } else {
    086                            elements.add(oversizeValue);
    087                        }
    088                    }
    089                }
    090
    091                final JpegImageData jpegImageData = directory.getJpegImageData();
    092                if (jpegImageData != null) {
    093                    elements.add(jpegImageData);
    094                }
    095
    096                final TiffImageData tiffImageData = directory.getTiffImageData();
    097                if (tiffImageData != null) {
    098                    final DataElement[] data = tiffImageData.getImageData();
    099                    Collections.addAll(elements, data);
    100                }
    101            }
    102
    103            Collections.sort(elements, TiffElement.COMPARATOR);
    104
    105            final List<TiffElement> rewritableElements = new ArrayList<>();
    106            final int TOLERANCE = 3;
    107            TiffElement start = null;
    108            long index = -1;
    109            for (final TiffElement element : elements) {
    110                final long lastElementByte = element.offset + element.length;
    111                if (start == null) {
    112                    start = element;
    113                    index = lastElementByte;
    114                } else if (element.offset - index > TOLERANCE) {
    115                    rewritableElements.add(new TiffElement.Stub(start.offset,
    116                            (int) (index - start.offset)));
    117                    start = element;
    118                    index = lastElementByte;
    119                } else {
    120                    index = lastElementByte;
    121                }
    122            }
    123            if (null != start) {
    124                rewritableElements.add(new TiffElement.Stub(start.offset,
    125                        (int) (index - start.offset)));
    126            }
    127
    128            return rewritableElements;
    129        } catch (final ImageReadException e) {
    130            throw new ImageWriteException(e.getMessage(), e);
    131        }
    132    }
    133
    134    @Override
    135    public void write(final OutputStream os, final TiffOutputSet outputSet)
    136            throws IOException, ImageWriteException {
    137        // There are some fields whose address in the file must not change,
    138        // unless of course their value is changed.
    139        final Map<Integer, TiffOutputField> frozenFields = new HashMap<>();
    140        final TiffOutputField makerNoteField = outputSet.findField(ExifTagConstants.EXIF_TAG_MAKER_NOTE);
    141        if (makerNoteField != null && makerNoteField.getSeperateValue() != null) {
    142            frozenFields.put(ExifTagConstants.EXIF_TAG_MAKER_NOTE.tag, makerNoteField);
    143        }
    144        final List<TiffElement> analysis = analyzeOldTiff(frozenFields);
    145        final int oldLength = exifBytes.length;
    146        if (analysis.isEmpty()) {
    147            throw new ImageWriteException("Couldn't analyze old tiff data.");
    148        } else if (analysis.size() == 1) {
    149            final TiffElement onlyElement = analysis.get(0);
    150            if (onlyElement.offset == TIFF_HEADER_SIZE
    151                    && onlyElement.offset + onlyElement.length
    152                            + TIFF_HEADER_SIZE == oldLength) {
    153                // no gaps in old data, safe to complete overwrite.
    154                new TiffImageWriterLossy(byteOrder).write(os, outputSet);
    155                return;
    156            }
    157        }
    158        final Map<Long, TiffOutputField> frozenFieldOffsets = new HashMap<>();
    159        for (final Map.Entry<Integer, TiffOutputField> entry : frozenFields.entrySet()) {
    160            final TiffOutputField frozenField = entry.getValue();
    161            if (frozenField.getSeperateValue().getOffset() != TiffOutputItem.UNDEFINED_VALUE) {
    162                frozenFieldOffsets.put(frozenField.getSeperateValue().getOffset(), frozenField);
    163            }
    164        }
    165
    166        final TiffOutputSummary outputSummary = validateDirectories(outputSet);
    167
    168        final List<TiffOutputItem> allOutputItems = outputSet.getOutputItems(outputSummary);
    169        final List<TiffOutputItem> outputItems = new ArrayList<>();
    170        for (final TiffOutputItem outputItem : allOutputItems) {
    171            if (!frozenFieldOffsets.containsKey(outputItem.getOffset())) {
    172                outputItems.add(outputItem);
    173            }
    174        }
    175
    176        final long outputLength = updateOffsetsStep(analysis, outputItems);
    177
    178        outputSummary.updateOffsets(byteOrder);
    179
    180        writeStep(os, outputSet, analysis, outputItems, outputLength);
    181
    182    }
    183
    184    private long updateOffsetsStep(final List<TiffElement> analysis,
    185            final List<TiffOutputItem> outputItems) {
    186        // items we cannot fit into a gap, we shall append to tail.
    187        long overflowIndex = exifBytes.length;
    188
    189        // make copy.
    190        final List<TiffElement> unusedElements = new ArrayList<>(analysis);
    191
    192        // should already be in order of offset, but make sure.
    193        Collections.sort(unusedElements, TiffElement.COMPARATOR);
    194        Collections.reverse(unusedElements);
    195        // any items that represent a gap at the end of the exif segment, can be
    196        // discarded.
    197        while (!unusedElements.isEmpty()) {
    198            final TiffElement element = unusedElements.get(0);
    199            final long elementEnd = element.offset + element.length;
    200            if (elementEnd == overflowIndex) {
    201                // discarding a tail element. should only happen once.
    202                overflowIndex -= element.length;
    203                unusedElements.remove(0);
    204            } else {
    205                break;
    206            }
    207        }
    208
    209        Collections.sort(unusedElements, ELEMENT_SIZE_COMPARATOR);
    210        Collections.reverse(unusedElements);
    211
    212        // make copy.
    213        final List<TiffOutputItem> unplacedItems = new ArrayList<>(
    214                outputItems);
    215        Collections.sort(unplacedItems, ITEM_SIZE_COMPARATOR);
    216        Collections.reverse(unplacedItems);
    217
    218        while (!unplacedItems.isEmpty()) {
    219            // pop off largest unplaced item.
    220            final TiffOutputItem outputItem = unplacedItems.remove(0);
    221            final int outputItemLength = outputItem.getItemLength();
    222            // search for the smallest possible element large enough to hold the
    223            // item.
    224            TiffElement bestFit = null;
    225            for (final TiffElement element : unusedElements) {
    226                if (element.length >= outputItemLength) {
    227                    bestFit = element;
    228                } else {
    229                    break;
    230                }
    231            }
    232            if (null == bestFit) {
    233                // we couldn't place this item. overflow.
    234                if ((overflowIndex & 1L) != 0) {
    235                    overflowIndex += 1;
    236                }
    237                outputItem.setOffset(overflowIndex);
    238                overflowIndex += outputItemLength;
    239            } else {
    240                long offset = bestFit.offset;
    241                if ((offset & 1L) != 0) {
    242                    offset += 1;
    243                }
    244                outputItem.setOffset(offset);
    245                unusedElements.remove(bestFit);
    246
    247                if (bestFit.length > outputItemLength) {
    248                    // not a perfect fit.
    249                    final long excessOffset = bestFit.offset + outputItemLength;
    250                    final int excessLength = bestFit.length - outputItemLength;
    251                    unusedElements.add(new TiffElement.Stub(excessOffset,
    252                            excessLength));
    253                    // make sure the new element is in the correct order.
    254                    Collections.sort(unusedElements, ELEMENT_SIZE_COMPARATOR);
    255                    Collections.reverse(unusedElements);
    256                }
    257            }
    258        }
    259
    260        return overflowIndex;
    261    }
    262
    263    private static class BufferOutputStream extends OutputStream {
    264        private final byte[] buffer;
    265        private int index;
    266
    267        BufferOutputStream(final byte[] buffer, final int index) {
    268            this.buffer = buffer;
    269            this.index = index;
    270        }
    271
    272        @Override
    273        public void write(final int b) throws IOException {
    274            if (index >= buffer.length) {
    275                throw new IOException("Buffer overflow.");
    276            }
    277
    278            buffer[index++] = (byte) b;
    279        }
    280
    281        @Override
    282        public void write(final byte[] b, final int off, final int len) throws IOException {
    283            if (index + len > buffer.length) {
    284                throw new IOException("Buffer overflow.");
    285            }
    286            System.arraycopy(b, off, buffer, index, len);
    287            index += len;
    288        }
    289    }
    290
    291    private void writeStep(final OutputStream os, final TiffOutputSet outputSet,
    292            final List<TiffElement> analysis, final List<TiffOutputItem> outputItems,
    293            final long outputLength) throws IOException, ImageWriteException {
    294        final TiffOutputDirectory rootDirectory = outputSet.getRootDirectory();
    295
    296        final byte[] output = new byte[(int) outputLength];
    297
    298        // copy old data (including maker notes, etc.)
    299        System.arraycopy(exifBytes, 0, output, 0, Math.min(exifBytes.length, output.length));
    300
    301        final BufferOutputStream headerStream = new BufferOutputStream(output, 0);
    302        final BinaryOutputStream headerBinaryStream = new BinaryOutputStream(headerStream, byteOrder);
    303        writeImageFileHeader(headerBinaryStream, rootDirectory.getOffset());
    304
    305        // zero out the parsed pieces of old exif segment, in case we don't
    306        // overwrite them.
    307        for (final TiffElement element : analysis) {
    308            Arrays.fill(output, (int) element.offset, (int) Math.min(element.offset + element.length, output.length),
    309                    (byte) 0);
    310        }
    311
    312        // write in the new items
    313        for (final TiffOutputItem outputItem : outputItems) {
    314            try (BinaryOutputStream bos = new BinaryOutputStream(
    315                    new BufferOutputStream(output, (int) outputItem.getOffset()), byteOrder)) {
    316                outputItem.writeItem(bos);
    317            }
    318        }
    319
    320        os.write(output);
    321    }
    322
    323}