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.TiffImagingParameters; 046import org.apache.commons.imaging.formats.tiff.TiffReader; 047import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants; 048 049public class TiffImageWriterLossless extends TiffImageWriterBase { 050 private final byte[] exifBytes; 051 private static final Comparator<TiffElement> ELEMENT_SIZE_COMPARATOR = Comparator.comparingInt(e -> e.length); 052 private static final Comparator<TiffOutputItem> ITEM_SIZE_COMPARATOR = Comparator.comparingInt(TiffOutputItem::getItemLength); 053 054 public TiffImageWriterLossless(final byte[] exifBytes) { 055 this.exifBytes = exifBytes; 056 } 057 058 public TiffImageWriterLossless(final ByteOrder byteOrder, final byte[] exifBytes) { 059 super(byteOrder); 060 this.exifBytes = exifBytes; 061 } 062 063 private List<TiffElement> analyzeOldTiff(final Map<Integer, TiffOutputField> frozenFields) throws ImageWriteException, 064 IOException { 065 try { 066 final ByteSource byteSource = new ByteSourceArray(exifBytes); 067 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 068 final TiffContents contents = new TiffReader(false).readContents( 069 byteSource, new TiffImagingParameters(), 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 elements.sort(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 } 149 if (analysis.size() == 1) { 150 final TiffElement onlyElement = analysis.get(0); 151 if (onlyElement.offset == TIFF_HEADER_SIZE 152 && onlyElement.offset + onlyElement.length 153 + TIFF_HEADER_SIZE == oldLength) { 154 // no gaps in old data, safe to complete overwrite. 155 new TiffImageWriterLossy(byteOrder).write(os, outputSet); 156 return; 157 } 158 } 159 final Map<Long, TiffOutputField> frozenFieldOffsets = new HashMap<>(); 160 for (final Map.Entry<Integer, TiffOutputField> entry : frozenFields.entrySet()) { 161 final TiffOutputField frozenField = entry.getValue(); 162 if (frozenField.getSeperateValue().getOffset() != TiffOutputItem.UNDEFINED_VALUE) { 163 frozenFieldOffsets.put(frozenField.getSeperateValue().getOffset(), frozenField); 164 } 165 } 166 167 final TiffOutputSummary outputSummary = validateDirectories(outputSet); 168 169 final List<TiffOutputItem> allOutputItems = outputSet.getOutputItems(outputSummary); 170 final List<TiffOutputItem> outputItems = new ArrayList<>(); 171 for (final TiffOutputItem outputItem : allOutputItems) { 172 if (!frozenFieldOffsets.containsKey(outputItem.getOffset())) { 173 outputItems.add(outputItem); 174 } 175 } 176 177 final long outputLength = updateOffsetsStep(analysis, outputItems); 178 179 outputSummary.updateOffsets(byteOrder); 180 181 writeStep(os, outputSet, analysis, outputItems, outputLength); 182 183 } 184 185 private long updateOffsetsStep(final List<TiffElement> analysis, 186 final List<TiffOutputItem> outputItems) { 187 // items we cannot fit into a gap, we shall append to tail. 188 long overflowIndex = exifBytes.length; 189 190 // make copy. 191 final List<TiffElement> unusedElements = new ArrayList<>(analysis); 192 193 // should already be in order of offset, but make sure. 194 unusedElements.sort(TiffElement.COMPARATOR); 195 Collections.reverse(unusedElements); 196 // any items that represent a gap at the end of the exif segment, can be 197 // discarded. 198 while (!unusedElements.isEmpty()) { 199 final TiffElement element = unusedElements.get(0); 200 final long elementEnd = element.offset + element.length; 201 if (elementEnd != overflowIndex) { 202 break; 203 } 204 // discarding a tail element. should only happen once. 205 overflowIndex -= element.length; 206 unusedElements.remove(0); 207 } 208 209 unusedElements.sort(ELEMENT_SIZE_COMPARATOR); 210 Collections.reverse(unusedElements); 211 212 // make copy. 213 final List<TiffOutputItem> unplacedItems = new ArrayList<>( 214 outputItems); 215 unplacedItems.sort(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 break; 228 } 229 bestFit = element; 230 } 231 if (null == bestFit) { 232 // we couldn't place this item. overflow. 233 if ((overflowIndex & 1L) != 0) { 234 overflowIndex += 1; 235 } 236 outputItem.setOffset(overflowIndex); 237 overflowIndex += outputItemLength; 238 } else { 239 long offset = bestFit.offset; 240 if ((offset & 1L) != 0) { 241 offset += 1; 242 } 243 outputItem.setOffset(offset); 244 unusedElements.remove(bestFit); 245 246 if (bestFit.length > outputItemLength) { 247 // not a perfect fit. 248 final long excessOffset = bestFit.offset + outputItemLength; 249 final int excessLength = bestFit.length - outputItemLength; 250 unusedElements.add(new TiffElement.Stub(excessOffset, 251 excessLength)); 252 // make sure the new element is in the correct order. 253 unusedElements.sort(ELEMENT_SIZE_COMPARATOR); 254 Collections.reverse(unusedElements); 255 } 256 } 257 } 258 259 return overflowIndex; 260 } 261 262 private static class BufferOutputStream extends OutputStream { 263 private final byte[] buffer; 264 private int index; 265 266 BufferOutputStream(final byte[] buffer, final int index) { 267 this.buffer = buffer; 268 this.index = index; 269 } 270 271 @Override 272 public void write(final int b) throws IOException { 273 if (index >= buffer.length) { 274 throw new IOException("Buffer overflow."); 275 } 276 277 buffer[index++] = (byte) b; 278 } 279 280 @Override 281 public void write(final byte[] b, final int off, final int len) throws IOException { 282 if (index + len > buffer.length) { 283 throw new IOException("Buffer overflow."); 284 } 285 System.arraycopy(b, off, buffer, index, len); 286 index += len; 287 } 288 } 289 290 private void writeStep(final OutputStream os, final TiffOutputSet outputSet, 291 final List<TiffElement> analysis, final List<TiffOutputItem> outputItems, 292 final long outputLength) throws IOException, ImageWriteException { 293 final TiffOutputDirectory rootDirectory = outputSet.getRootDirectory(); 294 295 final byte[] output = new byte[(int) outputLength]; 296 297 // copy old data (including maker notes, etc.) 298 System.arraycopy(exifBytes, 0, output, 0, Math.min(exifBytes.length, output.length)); 299 300 final BufferOutputStream headerStream = new BufferOutputStream(output, 0); 301 final BinaryOutputStream headerBinaryStream = new BinaryOutputStream(headerStream, byteOrder); 302 writeImageFileHeader(headerBinaryStream, rootDirectory.getOffset()); 303 304 // zero out the parsed pieces of old exif segment, in case we don't 305 // overwrite them. 306 for (final TiffElement element : analysis) { 307 Arrays.fill(output, (int) element.offset, (int) Math.min(element.offset + element.length, output.length), 308 (byte) 0); 309 } 310 311 // write in the new items 312 for (final TiffOutputItem outputItem : outputItems) { 313 try (BinaryOutputStream bos = new BinaryOutputStream( 314 new BufferOutputStream(output, (int) outputItem.getOffset()), byteOrder)) { 315 outputItem.writeItem(bos); 316 } 317 } 318 319 os.write(output); 320 } 321 322}