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