View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.tiff.write;
18  
19  import static org.apache.commons.imaging.formats.tiff.constants.TiffConstants.TIFF_HEADER_SIZE;
20  
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.nio.ByteOrder;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collections;
27  import java.util.Comparator;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Map;
31  
32  import org.apache.commons.imaging.FormatCompliance;
33  import org.apache.commons.imaging.ImagingException;
34  import org.apache.commons.imaging.bytesource.ByteSource;
35  import org.apache.commons.imaging.common.Allocator;
36  import org.apache.commons.imaging.common.BinaryOutputStream;
37  import org.apache.commons.imaging.formats.tiff.AbstractTiffElement;
38  import org.apache.commons.imaging.formats.tiff.AbstractTiffElement.DataElement;
39  import org.apache.commons.imaging.formats.tiff.AbstractTiffImageData;
40  import org.apache.commons.imaging.formats.tiff.JpegImageData;
41  import org.apache.commons.imaging.formats.tiff.TiffContents;
42  import org.apache.commons.imaging.formats.tiff.TiffDirectory;
43  import org.apache.commons.imaging.formats.tiff.TiffField;
44  import org.apache.commons.imaging.formats.tiff.TiffImagingParameters;
45  import org.apache.commons.imaging.formats.tiff.TiffReader;
46  import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
47  
48  /**
49   * TIFF lossless image writer.
50   */
51  public class TiffImageWriterLossless extends AbstractTiffImageWriter {
52      private static final class BufferOutputStream extends OutputStream {
53          private final byte[] buffer;
54          private int index;
55  
56          BufferOutputStream(final byte[] buffer, final int index) {
57              this.buffer = buffer;
58              this.index = index;
59          }
60  
61          @Override
62          public void write(final byte[] b, final int off, final int len) throws IOException {
63              if (index + len > buffer.length) {
64                  throw new ImagingException("Buffer overflow.");
65              }
66              System.arraycopy(b, off, buffer, index, len);
67              index += len;
68          }
69  
70          @Override
71          public void write(final int b) throws IOException {
72              if (index >= buffer.length) {
73                  throw new ImagingException("Buffer overflow.");
74              }
75  
76              buffer[index++] = (byte) b;
77          }
78      }
79  
80      private static final Comparator<AbstractTiffElement> ELEMENT_SIZE_COMPARATOR = Comparator.comparingInt(e -> e.length);
81      private static final Comparator<AbstractTiffOutputItem> ITEM_SIZE_COMPARATOR = Comparator.comparingInt(AbstractTiffOutputItem::getItemLength);
82  
83      private final byte[] exifBytes;
84  
85      public TiffImageWriterLossless(final byte[] exifBytes) {
86          this.exifBytes = exifBytes;
87      }
88  
89      public TiffImageWriterLossless(final ByteOrder byteOrder, final byte[] exifBytes) {
90          super(byteOrder);
91          this.exifBytes = exifBytes;
92      }
93  
94      private List<AbstractTiffElement> analyzeOldTiff(final Map<Integer, TiffOutputField> frozenFields) throws ImagingException, IOException {
95          try {
96              final ByteSource byteSource = ByteSource.array(exifBytes);
97              final FormatCompliance formatCompliance = FormatCompliance.getDefault();
98              final TiffContents contents = new TiffReader(false).readContents(byteSource, new TiffImagingParameters(), formatCompliance);
99  
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 }