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}