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