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
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 == TIFF_HEADER_SIZE && onlyElement.offset + onlyElement.length + TIFF_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                BinaryOutputStream headerBinaryStream = BinaryOutputStream.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 (BinaryOutputStream bos = BinaryOutputStream.create(new BufferOutputStream(output, (int) outputItem.getOffset()), byteOrder)) {
304                outputItem.writeItem(bos);
305            }
306        }
307
308        os.write(output);
309    }
310
311}