    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.TiffImagingParameters;
    046import org.apache.commons.imaging.formats.tiff.TiffReader;
    047import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
    048
    049public class TiffImageWriterLossless extends TiffImageWriterBase {
    050    private final byte[] exifBytes;
    051    private static final Comparator<TiffElement> ELEMENT_SIZE_COMPARATOR = Comparator.comparingInt(e -> e.length);
    052    private static final Comparator<TiffOutputItem> ITEM_SIZE_COMPARATOR = Comparator.comparingInt(TiffOutputItem::getItemLength);
    053
    054    public TiffImageWriterLossless(final byte[] exifBytes) {
    055        this.exifBytes = exifBytes;
    056    }
    057
    058    public TiffImageWriterLossless(final ByteOrder byteOrder, final byte[] exifBytes) {
    059        super(byteOrder);
    060        this.exifBytes = exifBytes;
    061    }
    062
    063    private List<TiffElement> analyzeOldTiff(final Map<Integer, TiffOutputField> frozenFields) throws ImageWriteException,
    064            IOException {
    065        try {
    066            final ByteSource byteSource = new ByteSourceArray(exifBytes);
    067            final FormatCompliance formatCompliance = FormatCompliance.getDefault();
    068            final TiffContents contents = new TiffReader(false).readContents(
    069                    byteSource, new TiffImagingParameters(), 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            elements.sort(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        }
    149        if (analysis.size() == 1) {
    150            final TiffElement onlyElement = analysis.get(0);
    151            if (onlyElement.offset == TIFF_HEADER_SIZE
    152                    && onlyElement.offset + onlyElement.length
    153                            + TIFF_HEADER_SIZE == oldLength) {
    154                // no gaps in old data, safe to complete overwrite.
    155                new TiffImageWriterLossy(byteOrder).write(os, outputSet);
    156                return;
    157            }
    158        }
    159        final Map<Long, TiffOutputField> frozenFieldOffsets = new HashMap<>();
    160        for (final Map.Entry<Integer, TiffOutputField> entry : frozenFields.entrySet()) {
    161            final TiffOutputField frozenField = entry.getValue();
    162            if (frozenField.getSeperateValue().getOffset() != TiffOutputItem.UNDEFINED_VALUE) {
    163                frozenFieldOffsets.put(frozenField.getSeperateValue().getOffset(), frozenField);
    164            }
    165        }
    166
    167        final TiffOutputSummary outputSummary = validateDirectories(outputSet);
    168
    169        final List<TiffOutputItem> allOutputItems = outputSet.getOutputItems(outputSummary);
    170        final List<TiffOutputItem> outputItems = new ArrayList<>();
    171        for (final TiffOutputItem outputItem : allOutputItems) {
    172            if (!frozenFieldOffsets.containsKey(outputItem.getOffset())) {
    173                outputItems.add(outputItem);
    174            }
    175        }
    176
    177        final long outputLength = updateOffsetsStep(analysis, outputItems);
    178
    179        outputSummary.updateOffsets(byteOrder);
    180
    181        writeStep(os, outputSet, analysis, outputItems, outputLength);
    182
    183    }
    184
    185    private long updateOffsetsStep(final List<TiffElement> analysis,
    186            final List<TiffOutputItem> outputItems) {
    187        // items we cannot fit into a gap, we shall append to tail.
    188        long overflowIndex = exifBytes.length;
    189
    190        // make copy.
    191        final List<TiffElement> unusedElements = new ArrayList<>(analysis);
    192
    193        // should already be in order of offset, but make sure.
    194        unusedElements.sort(TiffElement.COMPARATOR);
    195        Collections.reverse(unusedElements);
    196        // any items that represent a gap at the end of the exif segment, can be
    197        // discarded.
    198        while (!unusedElements.isEmpty()) {
    199            final TiffElement element = unusedElements.get(0);
    200            final long elementEnd = element.offset + element.length;
    201            if (elementEnd != overflowIndex) {
    202                break;
    203            }
    204            // discarding a tail element. should only happen once.
    205            overflowIndex -= element.length;
    206            unusedElements.remove(0);
    207        }
    208
    209        unusedElements.sort(ELEMENT_SIZE_COMPARATOR);
    210        Collections.reverse(unusedElements);
    211
    212        // make copy.
    213        final List<TiffOutputItem> unplacedItems = new ArrayList<>(
    214                outputItems);
    215        unplacedItems.sort(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                    break;
    228                }
    229                bestFit = element;
    230            }
    231            if (null == bestFit) {
    232                // we couldn't place this item. overflow.
    233                if ((overflowIndex & 1L) != 0) {
    234                    overflowIndex += 1;
    235                }
    236                outputItem.setOffset(overflowIndex);
    237                overflowIndex += outputItemLength;
    238            } else {
    239                long offset = bestFit.offset;
    240                if ((offset & 1L) != 0) {
    241                    offset += 1;
    242                }
    243                outputItem.setOffset(offset);
    244                unusedElements.remove(bestFit);
    245
    246                if (bestFit.length > outputItemLength) {
    247                    // not a perfect fit.
    248                    final long excessOffset = bestFit.offset + outputItemLength;
    249                    final int excessLength = bestFit.length - outputItemLength;
    250                    unusedElements.add(new TiffElement.Stub(excessOffset,
    251                            excessLength));
    252                    // make sure the new element is in the correct order.
    253                    unusedElements.sort(ELEMENT_SIZE_COMPARATOR);
    254                    Collections.reverse(unusedElements);
    255                }
    256            }
    257        }
    258
    259        return overflowIndex;
    260    }
    261
    262    private static class BufferOutputStream extends OutputStream {
    263        private final byte[] buffer;
    264        private int index;
    265
    266        BufferOutputStream(final byte[] buffer, final int index) {
    267            this.buffer = buffer;
    268            this.index = index;
    269        }
    270
    271        @Override
    272        public void write(final int b) throws IOException {
    273            if (index >= buffer.length) {
    274                throw new IOException("Buffer overflow.");
    275            }
    276
    277            buffer[index++] = (byte) b;
    278        }
    279
    280        @Override
    281        public void write(final byte[] b, final int off, final int len) throws IOException {
    282            if (index + len > buffer.length) {
    283                throw new IOException("Buffer overflow.");
    284            }
    285            System.arraycopy(b, off, buffer, index, len);
    286            index += len;
    287        }
    288    }
    289
    290    private void writeStep(final OutputStream os, final TiffOutputSet outputSet,
    291            final List<TiffElement> analysis, final List<TiffOutputItem> outputItems,
    292            final long outputLength) throws IOException, ImageWriteException {
    293        final TiffOutputDirectory rootDirectory = outputSet.getRootDirectory();
    294
    295        final byte[] output = new byte[(int) outputLength];
    296
    297        // copy old data (including maker notes, etc.)
    298        System.arraycopy(exifBytes, 0, output, 0, Math.min(exifBytes.length, output.length));
    299
    300        final BufferOutputStream headerStream = new BufferOutputStream(output, 0);
    301        final BinaryOutputStream headerBinaryStream = new BinaryOutputStream(headerStream, byteOrder);
    302        writeImageFileHeader(headerBinaryStream, rootDirectory.getOffset());
    303
    304        // zero out the parsed pieces of old exif segment, in case we don't
    305        // overwrite them.
    306        for (final TiffElement element : analysis) {
    307            Arrays.fill(output, (int) element.offset, (int) Math.min(element.offset + element.length, output.length),
    308                    (byte) 0);
    309        }
    310
    311        // write in the new items
    312        for (final TiffOutputItem outputItem : outputItems) {
    313            try (BinaryOutputStream bos = new BinaryOutputStream(
    314                    new BufferOutputStream(output, (int) outputItem.getOffset()), byteOrder)) {
    315                outputItem.writeItem(bos);
    316            }
    317        }
    318
    319        os.write(output);
    320    }
    321
    322}