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