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}