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