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.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.AbstractBinaryOutputStream; 036import org.apache.commons.imaging.common.Allocator; 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 048/** 049 * TIFF lossless image writer. 050 */ 051public class TiffImageWriterLossless extends AbstractTiffImageWriter { 052 private static final class BufferOutputStream extends OutputStream { 053 private final byte[] buffer; 054 private int index; 055 056 BufferOutputStream(final byte[] buffer, final int index) { 057 this.buffer = buffer; 058 this.index = index; 059 } 060 061 @Override 062 public void write(final byte[] b, final int off, final int len) throws IOException { 063 if (index + len > buffer.length) { 064 throw new ImagingException("Buffer overflow."); 065 } 066 System.arraycopy(b, off, buffer, index, len); 067 index += len; 068 } 069 070 @Override 071 public void write(final int b) throws IOException { 072 if (index >= buffer.length) { 073 throw new ImagingException("Buffer overflow."); 074 } 075 076 buffer[index++] = (byte) b; 077 } 078 } 079 080 private static final Comparator<AbstractTiffElement> ELEMENT_SIZE_COMPARATOR = Comparator.comparingInt(e -> e.length); 081 private static final Comparator<AbstractTiffOutputItem> ITEM_SIZE_COMPARATOR = Comparator.comparingInt(AbstractTiffOutputItem::getItemLength); 082 083 private final byte[] exifBytes; 084 085 public TiffImageWriterLossless(final byte[] exifBytes) { 086 this.exifBytes = exifBytes; 087 } 088 089 public TiffImageWriterLossless(final ByteOrder byteOrder, final byte[] exifBytes) { 090 super(byteOrder); 091 this.exifBytes = exifBytes; 092 } 093 094 private List<AbstractTiffElement> analyzeOldTiff(final Map<Integer, TiffOutputField> frozenFields) throws ImagingException, IOException { 095 try { 096 final ByteSource byteSource = ByteSource.array(exifBytes); 097 final FormatCompliance formatCompliance = FormatCompliance.getDefault(); 098 final TiffContents contents = new TiffReader(false).readContents(byteSource, new TiffImagingParameters(), formatCompliance); 099 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 == HEADER_SIZE && onlyElement.offset + onlyElement.length + 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 AbstractBinaryOutputStream headerBinaryStream = AbstractBinaryOutputStream.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 (AbstractBinaryOutputStream bos = AbstractBinaryOutputStream.create(new BufferOutputStream(output, (int) outputItem.getOffset()), byteOrder)) { 304 outputItem.writeItem(bos); 305 } 306 } 307 308 os.write(output); 309 } 310 311}