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}