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