/* Copyright (c) 2001-2025, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */

package com.pixelmed.convert;

import com.pixelmed.dicom.Attribute;
import com.pixelmed.dicom.AttributeList;
import com.pixelmed.dicom.AttributeTag;
import com.pixelmed.dicom.BinaryOutputStream;
import com.pixelmed.dicom.DicomException;
import com.pixelmed.dicom.FileMetaInformation;
import com.pixelmed.dicom.OtherByteAttribute;
import com.pixelmed.dicom.OtherByteAttributeMultipleCompressedFrames;
import com.pixelmed.dicom.OtherByteAttributeMultipleFilesOnDisk;
import com.pixelmed.dicom.OtherByteAttributeCompressedSeparateFramesOnDisk;
import com.pixelmed.dicom.OtherVeryLongAttribute;
import com.pixelmed.dicom.OtherWordAttribute;
import com.pixelmed.dicom.OtherWordAttributeMultipleFilesOnDisk;
import com.pixelmed.dicom.SequenceAttribute;
import com.pixelmed.dicom.SequenceItem;
import com.pixelmed.dicom.TagFromName;
import com.pixelmed.dicom.TransferSyntax;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;

import java.util.Iterator;

import com.pixelmed.slf4j.Logger;
import com.pixelmed.slf4j.LoggerFactory;

/**
 * <p>A class for to add make a dual-personality DICOM-TIFF file and/or add Basic or Extended Offset Tables.</p>
 *
 * @see	com.pixelmed.convert.TIFFToDicom
 *
 * @author	dclunie
 */

public class AddTIFFOrOffsetTables {
	private static final String identString = "@(#) $Header: /userland/cvs/pixelmed/imgbook/com/pixelmed/convert/AddTIFFOrOffsetTables.java,v 1.9 2025/01/29 10:58:06 dclunie Exp $";

	private static final Logger slf4jlogger = LoggerFactory.getLogger(AddTIFFOrOffsetTables.class);

	private int getTIFFPhotometricFromDICOMPhotometricInterpretation(String photometricInterpretation) {
		int photometric = -1;
		switch (photometricInterpretation) {
			case "MONOCHROME1":		photometric = 0; break;
			case "MONOCHROME2":		photometric = 1; break;
			case "RGB":				photometric = 2; break;
			case "PALETTE COLOR":	photometric = 3; break;
			case "TRANSPARENCY":	photometric = 4; break;		// not standard DICOM
			case "CMYK":			photometric = 5; break;		// retired in DICOM
			case "YBR_ICT":			photometric = 6; break;
			case "YBR_RCT":			photometric = 6; break;
			case "YBR_FULL_422":	photometric = 6; break;
			case "CIELAB":			photometric = 8; break;		// not standard DICOM
		}
		return photometric;
	}
	
	private int getTIFFCompressionFromTransferSyntax(String transferSyntax) {
		int compression = 0;
		switch (transferSyntax) {
			case TransferSyntax.ImplicitVRLittleEndian:		compression = 1; break;
			case TransferSyntax.ExplicitVRLittleEndian:		compression = 1; break;
			case TransferSyntax.ExplicitVRBigEndian:		compression = 1; break;			// ? big endian actually handled properly in TIFF
			case TransferSyntax.JPEGBaseline:				compression = 7; break;
			case TransferSyntax.JPEG2000:					compression = 33005; break;		// ? should check if RGB
			case TransferSyntax.JPEG2000Lossless:			compression = 33005; break;		// ? should check if RGB
		}
		return compression;
	}


	// reuse same private group and creator as for com.pixelmed.dicom.PrivatePixelData
	private static final String pixelmedPrivateCreatorForPyramidData = "PixelMed Publishing";
	private static final int pixelmedPrivatePyramidDataGroup = 0x7FDF;	// Must be BEFORE (7FE0,0010) because we assume elsewhere that DataSetTrailingPadding will immediately follow (7FE0,0010)
	private static final AttributeTag pixelmedPrivatePyramidDataBlockReservation = new AttributeTag(pixelmedPrivatePyramidDataGroup,0x0010);
	private static final AttributeTag pixelmedPrivatePyramidData = new AttributeTag(pixelmedPrivatePyramidDataGroup,0x1001);

	private static final long UNSIGNED32_MAX_VALUE = 0xffffffffl;

	private static long getByteOffsetsAndLengthsOfTileDataFromStartOfFileForPixelDataAttribute(long byteoffset,Attribute a,int numberOfFrames,int numberOfTiles,boolean encodeAsExplicit,boolean encodeAsLittleEndian,long[] tileDataByteOffsets,long[] tileDataLengths,boolean mergeSamplesPerPixelTiles,long samplesPerPixel,boolean addBasicOffsetTable) throws DicomException {
		if (slf4jlogger.isDebugEnabled()) slf4jlogger.debug("0x{} ({} dec): {} {}",Long.toHexString(byteoffset),byteoffset,a.toString(),a.getClass());
		long vl = a.getVL();
		if (vl == 0xffffffffl) {
			if (mergeSamplesPerPixelTiles) {
				throw new DicomException("Planar Configuration with separate planes not supported for compressed pixel data");
			}
			// depending on the class of the OB attribute, get the offsets and lengths
			if (a instanceof OtherByteAttributeMultipleCompressedFrames) {
				OtherByteAttributeMultipleCompressedFrames ob = (OtherByteAttributeMultipleCompressedFrames)a;
				// simulate OtherByteAttributeMultipleCompressedFrames.write()
				byteoffset += a.getLengthOfBaseOfEncodedAttribute(encodeAsExplicit,encodeAsLittleEndian);
				byteoffset += 8;	// Item tag for basic offset table
				if (addBasicOffsetTable) {
					byteoffset += numberOfFrames * 4;
				}
				
				byte[][] frames = ob.getFrames();
				File[] files = ob.getFiles();
				
				int nFrames = 0;
				if (files != null) {
					nFrames = files.length;
				}
				else if (frames != null) {
					nFrames = frames.length;
				}
				else {
					throw new DicomException("Not yet implemented - calculating offsets and lengths from "+a.getClass()+" with all frames in one fragment");
				}
				
				if (nFrames != numberOfFrames) {
					throw new DicomException("Not yet implemented - calculating offsets and lengths from "+a.getClass()+" when number of files not the same as number of frames");
				}
				
				if (nFrames > 0) {
					for (int f=0; f<nFrames; ++f) {
						File file = null;
						byte[] frame = null;
						long frameLength = 0;
						if (files != null) {
							file = files[f];
							frameLength = file.length();
						}
						else {
							frame = frames[f];
							frameLength = frame.length;
						}
						tileDataLengths[f] = frameLength;		// does NOT include padding
						long padding = frameLength % 2;
						long paddedLength = frameLength + padding;
						byteoffset += 8;							// Item tag for one fragment per frame
						tileDataByteOffsets[f] = byteoffset;		// points to start of compressed data, not start of item
						if (slf4jlogger.isTraceEnabled()) slf4jlogger.trace("Frame {} 0x{} ({} dec): length = 0x{} ({} dec)",f,Long.toHexString(byteoffset),byteoffset,Long.toHexString(frameLength),frameLength);
						byteoffset += paddedLength;
					}
				}
				byteoffset += 8;	// SequenceDelimitationItem
			}
			else if (a instanceof OtherByteAttributeCompressedSeparateFramesOnDisk) {
				OtherByteAttributeCompressedSeparateFramesOnDisk ob = (OtherByteAttributeCompressedSeparateFramesOnDisk)a;
				byteoffset += a.getLengthOfBaseOfEncodedAttribute(encodeAsExplicit,encodeAsLittleEndian);
				byteoffset += 8;	// Item tag for basic offset table
				if (addBasicOffsetTable) {
					byteoffset += numberOfFrames * 4;
				}
				
				int nFrames = ob.getNumberOfFrames();
				
				if (nFrames != numberOfFrames) {
					throw new DicomException("Not yet implemented - calculating offsets and lengths from "+a.getClass()+" but expected number of frames "+numberOfFrames+"does not match actual number of frames"+nFrames);
				}

				long[][] frameItemLengths = ob.getFrameItemLengths();
				if (frameItemLengths.length != nFrames) {
					throw new DicomException("Not yet implemented - calculating offsets and lengths from "+a.getClass()+" when number of frames "+nFrames+"does not match length of frameItemLengths"+frameItemLengths.length);
				}
				
				if (nFrames > 0) {
					for (int f=0; f<nFrames; ++f) {
						long[] fragmentLengths = frameItemLengths[f];
						if (fragmentLengths == null) {
							throw new DicomException("Not yet implemented - calculating offsets and lengths from "+a.getClass()+" but no fragment lengths for frame "+f);
						}
						else if (fragmentLengths.length == 0) {
							throw new DicomException("Not yet implemented - calculating offsets and lengths from "+a.getClass()+" but missing fragment length for frame "+f);
						}
						else if (fragmentLengths.length > 1) {
							throw new DicomException("Not yet implemented - calculating offsets and lengths from "+a.getClass()+" when more than one fragment per frame for frame "+f);
						}

						long frameLength = fragmentLengths[0];

						tileDataLengths[f] = frameLength;		// does NOT include padding
						long padding = frameLength % 2;
						long paddedLength = frameLength + padding;
						byteoffset += 8;							// Item tag for one fragment per frame
						tileDataByteOffsets[f] = byteoffset;		// points to start of compressed data, not start of item
						if (slf4jlogger.isTraceEnabled()) slf4jlogger.trace("Frame {} 0x{} ({} dec): length = 0x{} ({} dec)",f,Long.toHexString(byteoffset),byteoffset,Long.toHexString(frameLength),frameLength);
						byteoffset += paddedLength;
					}
				}
				byteoffset += 8;	// SequenceDelimitationItem
			}
			else {
				throw new DicomException("Not yet implemented - calculating offsets and lengths from "+a.getClass());
			}
		}
		else {
			byteoffset += a.getLengthOfBaseOfEncodedAttribute(encodeAsExplicit,encodeAsLittleEndian);
			long frameLength = vl / numberOfFrames;
			if (numberOfFrames > 0) {
				if (mergeSamplesPerPixelTiles) {	// (001351)
					long tileLength = frameLength / samplesPerPixel;
					// DICOM encodes separate components successively within each tile (frame)
					for (int f=0; f<numberOfFrames; ++f) {
						for (int c=0; c<samplesPerPixel; ++c) {
							// TIFF encodes all tiles of one component before the next component
							int t = c*numberOfFrames + f;
							tileDataLengths[t] = tileLength;
							tileDataByteOffsets[t] = byteoffset;
							if (slf4jlogger.isTraceEnabled()) slf4jlogger.trace("DICOM Frame {} TIFF Tile {} 0x{} ({} dec): length = 0x{} ({} dec)",f,t,Long.toHexString(byteoffset),byteoffset,Long.toHexString(tileLength),tileLength);
							byteoffset += tileLength;
						}
					}
				}
				else {
					for (int f=0; f<numberOfFrames; ++f) {
						tileDataLengths[f] = frameLength;		// we need to return the lengths by populating the array that we were supplied with
						tileDataByteOffsets[f] = byteoffset;
						if (slf4jlogger.isTraceEnabled()) slf4jlogger.trace("Frame {} 0x{} ({} dec): length = 0x{} ({} dec)",f,Long.toHexString(byteoffset),byteoffset,Long.toHexString(frameLength),frameLength);
						byteoffset += frameLength;
					}
				}
				if (a.getVL() != a.getPaddedVL()) ++byteoffset;		// even padding at end
			}
		}
		if (slf4jlogger.isDebugEnabled()) slf4jlogger.debug("getByteOffsetsAndLengthsOfTileDataFromStartOfFileForPixelDataAttribute(): returns 0x{} ({} dec): {} {}",Long.toHexString(byteoffset),byteoffset);
		return byteoffset;
	}
	
	// package private scope so that TIFFToDICOM can use it
	static long getByteOffsetsAndLengthsOfTileDataFromStartOfFile(AttributeList list,String transferSyntaxUID,long[][] tileDataByteOffsets,long[][] tileDataLengths,long[] imageWidths,long[] imageLengths,boolean mergeSamplesPerPixelTiles,long samplesPerPixel,boolean addBasicOffsetTable) throws DicomException {
		long byteoffset = 132;						// assume preamble and magic number, else no reason to call this method
		boolean inDataSetPastMetaHeader = false;	// starting in metaheader
		boolean encodeAsExplicit = true;			// always for metaheader
		boolean encodeAsLittleEndian = true;		// always for metaheader
		TransferSyntax transferSyntax = new TransferSyntax(transferSyntaxUID);
		boolean dataSetIsExplicitVR = transferSyntax.isExplicitVR();
		boolean dataSetIsLittleEndian = transferSyntax.isLittleEndian();
		Iterator<Attribute> i = list.values().iterator();
		while (i.hasNext()) {
			Attribute a = ((Attribute)i.next());
			AttributeTag t = a.getTag();
			if (!inDataSetPastMetaHeader && !t.isFileMetaInformationGroup()) {
				// once set, stay set
				inDataSetPastMetaHeader = true;
				encodeAsExplicit = dataSetIsExplicitVR;
				encodeAsLittleEndian = dataSetIsLittleEndian;
			}
			if (t.equals(TagFromName.PixelData) || t.equals(TagFromName.FloatPixelData) || t.equals(TagFromName.DoubleFloatPixelData)) {	// (001323)
				slf4jlogger.debug("getByteOffsetsAndLengthsOfTileDataFromStartOfFile(): Have PixelData or FloatPixelData or DoubleFloatPixelData");
				imageWidths[0] = Attribute.getSingleIntegerValueOrDefault(list,TagFromName.TotalPixelMatrixColumns,0);
				imageLengths[0] = Attribute.getSingleIntegerValueOrDefault(list,TagFromName.TotalPixelMatrixRows,0);
				int numberOfFrames = Attribute.getSingleIntegerValueOrDefault(list,TagFromName.NumberOfFrames,1);
				slf4jlogger.debug("getByteOffsetsAndLengthsOfTileDataFromStartOfFile(): numberOfFrames = {}",numberOfFrames);
				int numberOfTiles = numberOfFrames * (mergeSamplesPerPixelTiles ? (int)samplesPerPixel : 1);	// (001351)
				slf4jlogger.debug("getByteOffsetsAndLengthsOfTileDataFromStartOfFile(): numberOfTiles = {}",numberOfTiles);
				tileDataByteOffsets[0] = new long[numberOfTiles];
				tileDataLengths[0] = new long[numberOfTiles];
				byteoffset = getByteOffsetsAndLengthsOfTileDataFromStartOfFileForPixelDataAttribute(byteoffset,a,numberOfFrames,numberOfTiles,encodeAsExplicit,encodeAsLittleEndian,tileDataByteOffsets[0],tileDataLengths[0],mergeSamplesPerPixelTiles,samplesPerPixel,addBasicOffsetTable);
			}
			else if (t.equals(pixelmedPrivatePyramidData)) {	// don't need to check creator since could not be here any other way
				int pyramidLevel = 1;
				SequenceAttribute pyramidData = (SequenceAttribute)a;
				byteoffset += pyramidData.getLengthOfBaseOfEncodedAttribute(encodeAsExplicit,encodeAsLittleEndian);
				Iterator<SequenceItem> iti = pyramidData.iterator();
				while (iti.hasNext()) {
					byteoffset += 8;		// length of Item
					SequenceItem it = iti.next();
					AttributeList ilist = it.getAttributeList();
					if (ilist != null) {
						// PlanarConfiguration of our own downsampled layers should always be 0 (DICOM, color-by-pixel) but check just in case ... (001351)
						int pyramidSamplesPerPixel = Attribute.getSingleIntegerValueOrDefault(ilist,TagFromName.SamplesPerPixel,0);
						int pyramidPlanarConfiguration = Attribute.getSingleIntegerValueOrDefault(ilist,TagFromName.PlanarConfiguration,0);
						boolean pyramidMergeSamplesPerPixelTiles = pyramidPlanarConfiguration == 1 && pyramidSamplesPerPixel > 1;  // DICOM PlanarConfiguration 1 is TIFF PlanarConfig 2
						slf4jlogger.debug("getByteOffsetsAndLengthsOfTileDataFromStartOfFile(): for PrivatePyramidData, mergeSamplesPerPixelTiles = {}",pyramidMergeSamplesPerPixelTiles);
						Iterator<Attribute> ili = ilist.values().iterator();
						while (ili.hasNext()) {
							Attribute aa = ili.next();
							AttributeTag tt = aa.getTag();
							if (tt.equals(TagFromName.PixelData)) {
								imageWidths[pyramidLevel] = Attribute.getSingleIntegerValueOrDefault(ilist,TagFromName.TotalPixelMatrixColumns,0);
								imageLengths[pyramidLevel] = Attribute.getSingleIntegerValueOrDefault(ilist,TagFromName.TotalPixelMatrixRows,0);
								int numberOfFrames = Attribute.getSingleIntegerValueOrDefault(ilist,TagFromName.NumberOfFrames,1);
								slf4jlogger.debug("getByteOffsetsAndLengthsOfTileDataFromStartOfFile(): numberOfFrames = {}",numberOfFrames);
								int numberOfTiles = numberOfFrames * (mergeSamplesPerPixelTiles ? (int)samplesPerPixel : 1);	// (001351);
								slf4jlogger.debug("getByteOffsetsAndLengthsOfTileDataFromStartOfFile(): numberOfTiles = {}",numberOfTiles);
								tileDataByteOffsets[pyramidLevel] = new long[numberOfTiles];
								tileDataLengths[pyramidLevel] = new long[numberOfTiles];
								byteoffset = getByteOffsetsAndLengthsOfTileDataFromStartOfFileForPixelDataAttribute(byteoffset,aa,numberOfFrames,numberOfTiles,encodeAsExplicit,encodeAsLittleEndian,tileDataByteOffsets[pyramidLevel],tileDataLengths[pyramidLevel],pyramidMergeSamplesPerPixelTiles,pyramidSamplesPerPixel,false/*addBasicOffsetTable*/);	// not adding BOT for private pyramid pixel data
							}
							else {
								byteoffset += aa.getLengthOfEntireEncodedAttribute(encodeAsExplicit,encodeAsLittleEndian);
							}
						}
					}
					++pyramidLevel;
					byteoffset += 8;		// length of Item Delimiter
				}
				byteoffset += 8;			// length of Sequence Delimiter
			}
			else {
				long l = a.getLengthOfEntireEncodedAttribute(encodeAsExplicit,encodeAsLittleEndian);
				if (slf4jlogger.isTraceEnabled()) slf4jlogger.trace("0x{} ({} dec): {} encoded length = 0x{} ({} dec)",Long.toHexString(byteoffset),byteoffset,a.toString(),Long.toHexString(l),l);
				byteoffset += l;
			}
		}
		long byteOffsetFromFileStartOfNextAttributeAfterPixelData = byteoffset;
		return byteOffsetFromFileStartOfNextAttributeAfterPixelData;
	}
	
	private static void writeUnsigned32Or64Bits(BinaryOutputStream o,boolean use64,long value) throws IOException {
		if (use64) { o.writeUnsigned64(value); } else { o.writeUnsigned32(value); }
	}
	
	// package private scope so that TIFFToDICOM can use it
	static byte[] makeTIFFInPreambleAndAddDataSetTrailingPadding(long byteOffsetFromFileStartOfNextAttributeAfterPixelData,int numberOfPyramidLevels,long[][] tileDataByteOffsets,long[][] tileDataLengths,long[] imageWidths,long[] imageLengths,
			AttributeList list,long tileWidth,long tileLength,long bitsPerSample,long compression,long basePhotometric,long lowerPhotometric,long samplesPerPixel,long planarConfig,long sampleFormat,
			byte[] iccProfile,double mmPerPixelXBaseLayer,double mmPerPixelYBaseLayer,boolean useBigTIFF) throws DicomException, IOException {
		
		//boolean useBigTIFF = true;
		slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): useBigTIFF = {}",useBigTIFF);
		
		boolean useResolution = true;
		slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): useResolution = {}",useResolution);
		
		boolean useSampleFormat = samplesPerPixel == 1 && bitsPerSample > 8;
		
		boolean includeYCbCrSubSampling = false;	// if present, causes Sedeen to fail to view the TIFF file
		slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): includeYCbCrSubSampling = {}",includeYCbCrSubSampling);

		boolean haveICCProfile = iccProfile != null && iccProfile.length > 0;

		byte[] preamble = null;
		slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): byteOffsetFromFileStartOfNextAttributeAfterPixelData = {}",byteOffsetFromFileStartOfNextAttributeAfterPixelData);
		// offset of IFD is start of value part of DataSetTrailingPadding
		long offsetofIFD = byteOffsetFromFileStartOfNextAttributeAfterPixelData + 12 /* group and element, reserved bytes + VR,  32 bit value length */;
		slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): offsetofIFD = {}",offsetofIFD);
		{
		
			ByteArrayOutputStream pbaos = new ByteArrayOutputStream(8);
			BinaryOutputStream pos = new BinaryOutputStream(pbaos,false/* not big endian*/);
			pos.write((byte)'I');	// Intel (little endian) byte order
			pos.write((byte)'I');

			if (useBigTIFF) {
				// "http://www.awaresystems.be/imaging/tiff/bigtiff.html"
				pos.writeUnsigned16(0x002B);			// TIFF Version number
				pos.writeUnsigned16(8);					// Bytesize of offsets - Always 8 in BigTIFF
				pos.writeUnsigned16(0);					// Always 0
				pos.writeUnsigned64(offsetofIFD);
			}
			else {
				pos.writeUnsigned16(0x002A);			// TIFF Version number
				if (offsetofIFD > UNSIGNED32_MAX_VALUE) {
					throw new DicomException("Offset of first IFD is too large to fit in 32 bits 0x"+Long.toHexString(offsetofIFD)+" ("+offsetofIFD+" dec)");
				}
				pos.writeUnsigned32(offsetofIFD);
			}
			pos.flush();
			pos.close();
			
			byte[] populated = pbaos.toByteArray();
			
			preamble = new byte[128];
			System.arraycopy(populated,0,preamble,0,populated.length);
		}

		{
			ByteArrayOutputStream ifdaos = new ByteArrayOutputStream(8);
			BinaryOutputStream ifdos = new BinaryOutputStream(ifdaos,false/* not big endian*/);
			
			double mmPerPixelX = mmPerPixelXBaseLayer;
			double mmPerPixelY = mmPerPixelYBaseLayer;
			slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): numberOfPyramidLevels = {}",numberOfPyramidLevels);
			for (int pyramidLevel=0; pyramidLevel<numberOfPyramidLevels; ++pyramidLevel) {
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): pyramidLevel = {}",pyramidLevel);

				int numberOfTiles = tileDataByteOffsets[pyramidLevel].length;
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): numberOfTiles = {}",numberOfTiles);

				int numberOfIFDEntries = pyramidLevel == 0 ? 14: 15;		// Entry count
				
				if (useSampleFormat) {
					++numberOfIFDEntries;
				}
				
				if (includeYCbCrSubSampling) {
					++numberOfIFDEntries;
				}

				if (haveICCProfile) {
					++numberOfIFDEntries;
				}
				
				if (useBigTIFF) {
					ifdos.writeUnsigned64(numberOfIFDEntries);
				}
				else {
					ifdos.writeUnsigned16(numberOfIFDEntries);
				}
				
				long lengthOfIFD =
					  (useBigTIFF ? 8 : 2)							// numberOfIFDEntries is 2 or 8
					+ numberOfIFDEntries * (useBigTIFF ? 20 : 12)	// each entry is 12 or 20 bytes fixed length regardless of type
					+ (useBigTIFF ? 8 : 4);							// offset of next IFD is 4 or 8 bytes
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): lengthOfIFD = {}",lengthOfIFD);

				long bitsPerSampleOffset = offsetofIFD + lengthOfIFD;
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): bitsPerSampleOffset = {}",bitsPerSampleOffset);
				
				boolean useBitsPerSampleOffset = (useBigTIFF && samplesPerPixel > 4) || (!useBigTIFF && samplesPerPixel > 2);
				
				long xResolutionOffset = bitsPerSampleOffset + (useBitsPerSampleOffset ? 2*samplesPerPixel : 0);	// bitsPerSample is samplesPerPixel two byte (short) entries
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): xResolutionOffset = {}",xResolutionOffset);
				
				long yResolutionOffset = xResolutionOffset + (useBigTIFF ? 0 : 8);	// xResolution was two four byte (long) entries if not BigTIFF
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): yResolutionOffset = {}",yResolutionOffset);
				
				long iccProfileOffset = yResolutionOffset + (useBigTIFF ? 0 : 8);	// yResolution was two four byte (long) entries if not BigTIFF
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): iccProfileOffset = {}",iccProfileOffset);

				boolean useTileOffsetAndByteCountsOffset = numberOfTiles > 1;	//otherwise will fit in place

				long tileOffsetsOffset = iccProfileOffset + (haveICCProfile ? iccProfile.length : 0);
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): tileOffsetsOffset = {}",tileOffsetsOffset);
				
				long tileByteCountsOffset = tileOffsetsOffset + (useTileOffsetAndByteCountsOffset ? (numberOfTiles * (useBigTIFF ? 8 : 4)) : 0);	// tileOffsets was LONG8 or LONG
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): tileByteCountsOffset = {}",tileByteCountsOffset);

				long offsetOfNextIFD = (pyramidLevel+1<numberOfPyramidLevels) ? (tileByteCountsOffset + (useTileOffsetAndByteCountsOffset ? (numberOfTiles * (useBigTIFF ? 8 : 4)) : 0)) : 0;	// tileByteCounts was LONG8 or LONG
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): offsetOfNextIFD = {}",offsetOfNextIFD);
				if (!useBigTIFF) {
					if (offsetOfNextIFD > UNSIGNED32_MAX_VALUE) {
						throw new DicomException("Offset for IFD is too large to fit in 32 bits 0x"+Long.toHexString(offsetOfNextIFD)+" ("+offsetOfNextIFD+" dec)");
					}
				}

				if (pyramidLevel > 0) {
					ifdos.writeUnsigned16(0x00fe);	// NewSubfileType
					ifdos.writeUnsigned16(0x0004);	// - type   LONG
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - reduced resolution version
				}
				{
					ifdos.writeUnsigned16(TIFFTags.IMAGEWIDTH);
					ifdos.writeUnsigned16(0x0004);	// - type   LONG
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,imageWidths[pyramidLevel]);
				}
				{
					ifdos.writeUnsigned16(TIFFTags.IMAGELENGTH);
					ifdos.writeUnsigned16(0x0004);	// - type   LONG
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,imageLengths[pyramidLevel]);
				}
				{
					ifdos.writeUnsigned16(TIFFTags.BITSPERSAMPLE);
					ifdos.writeUnsigned16(0x0003);	// - type   SHORT
					if (useBigTIFF) {	// up to 4 values fit in 8 byte value field
						ifdos.writeUnsigned64(samplesPerPixel);		// - length usually 1 or 3
						if (samplesPerPixel <= 4) {
							ifdos.writeUnsigned16((int)bitsPerSample);
							ifdos.writeUnsigned16(samplesPerPixel > 1 ? (int)bitsPerSample : 0);
							ifdos.writeUnsigned16(samplesPerPixel > 2 ? (int)bitsPerSample : 0);
							ifdos.writeUnsigned16(samplesPerPixel > 3 ? (int)bitsPerSample : 0);
						}
						else {
							ifdos.writeUnsigned64(bitsPerSampleOffset);
						}
					}
					else {	// three values (for RGB) do not fit in value field
						ifdos.writeUnsigned32(samplesPerPixel);		// - length usually 1 or 3
						if (samplesPerPixel <= 2) {
							ifdos.writeUnsigned16((int)bitsPerSample);
							ifdos.writeUnsigned16(samplesPerPixel > 1 ? (int)bitsPerSample : 0);
						}
						else {
							ifdos.writeUnsigned32(bitsPerSampleOffset);
						}
					}
				}
				{
					ifdos.writeUnsigned16(TIFFTags.COMPRESSION);
					ifdos.writeUnsigned16(0x0003);	// - type   SHORT
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,compression);
				}
				{
					ifdos.writeUnsigned16(TIFFTags.PHOTOMETRIC);
					ifdos.writeUnsigned16(0x0003);	// - type   SHORT
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,pyramidLevel == 0 ? basePhotometric : lowerPhotometric);	// the original for the first layer and then the default for the compression for later levels
				}
				{
					ifdos.writeUnsigned16(TIFFTags.SAMPLESPERPIXEL);
					ifdos.writeUnsigned16(0x0003);	// - type   SHORT
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,samplesPerPixel);
				}
				
				//XResolution (282) RATIONAL (5) 1<10>	The number of pixels per ResolutionUnit in the ImageWidth (typically, horizontal - see Orientation) direction
				//YResolution (283) RATIONAL (5) 1<10>

				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): mmPerPixelX = {}",mmPerPixelX);
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): mmPerPixelY = {}",mmPerPixelY);
				long resolutionNumeratorX = (!useResolution || mmPerPixelX == 0) ? 1 : Math.round(10.0d/mmPerPixelX);	// 10.0, because units are cm, not mm
				long resolutionNumeratorY = (!useResolution || mmPerPixelY == 0) ? 1 : Math.round(10.0d/mmPerPixelY);	// 10.0, because units are cm, not mm
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): resolutionNumeratorX = {}",resolutionNumeratorX);
				slf4jlogger.debug("makeTIFFInPreambleAndAddDataSetTrailingPadding(): resolutionNumeratorY = {}",resolutionNumeratorY);
				{
					ifdos.writeUnsigned16(TIFFTags.XRESOLUTION);
					ifdos.writeUnsigned16(0x0005);			// - type RATIONAL
					if (useBigTIFF) {						// two LONG values do fit in 8 byte value field
						ifdos.writeUnsigned64(0x0001);		// - length 1
						ifdos.writeUnsigned32(resolutionNumeratorX);
						ifdos.writeUnsigned32(0x0001);		// denominator
					}
					else {									// two LONG values do fit in 4 byte value field
						ifdos.writeUnsigned32(0x0001);		// - length 1
						ifdos.writeUnsigned32(xResolutionOffset);
					}
				}
				{
					ifdos.writeUnsigned16(TIFFTags.YRESOLUTION);
					ifdos.writeUnsigned16(0x0005);			// - type RATIONAL
					if (useBigTIFF) {						// two LONG values do fit in 8 byte value field
						ifdos.writeUnsigned64(0x0001);		// - length 1
						ifdos.writeUnsigned32(resolutionNumeratorY);
						ifdos.writeUnsigned32(0x0001);		// denominator
					}
					else {									// two LONG values do fit in 4 byte value field
						ifdos.writeUnsigned32(0x0001);		// - length 1
						ifdos.writeUnsigned32(yResolutionOffset);
					}
				}

				{
					ifdos.writeUnsigned16(TIFFTags.PLANARCONFIG);
					ifdos.writeUnsigned16(0x0003);	// - type   SHORT
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,pyramidLevel == 0 ? planarConfig : 1);	// downsampled pyramid always color-by-pixel, but top level may vary though should have been changed to color-by-pixel (at least for WSI) by this point (001351); could check this by passing in from parent or extracting from list :(
				}
				
				{
					ifdos.writeUnsigned16(TIFFTags.RESOLUTIONUNIT);
					ifdos.writeUnsigned16(0x0003);	// - type   SHORT
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,useResolution ? 0x0003 : 0x0001);	// 1 is none, 3 is cm (!)
				}
				
				{
					ifdos.writeUnsigned16(TIFFTags.TILEWIDTH);
					ifdos.writeUnsigned16(0x0003);	// - type   SHORT
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,tileWidth);
				}
				{
					ifdos.writeUnsigned16(TIFFTags.TILELENGTH);
					ifdos.writeUnsigned16(0x0003);	// - type   SHORT
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,tileLength);
				}
				{
					ifdos.writeUnsigned16(TIFFTags.TILEOFFSETS);
					ifdos.writeUnsigned16(useBigTIFF ? 0x0010 : 0x0004);		// - type   LONG8  or LONG
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,numberOfTiles);	// - length
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,useTileOffsetAndByteCountsOffset ? tileOffsetsOffset : tileDataByteOffsets[pyramidLevel][0]);
				}
				{
					ifdos.writeUnsigned16(TIFFTags.TILEBYTECOUNTS);
					ifdos.writeUnsigned16(useBigTIFF ? 0x0010 : 0x0004);		// - type   LONG8  or LONG
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,numberOfTiles);	// - length
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,useTileOffsetAndByteCountsOffset ? tileByteCountsOffset : tileDataLengths[pyramidLevel][0]);
				}
				if (useSampleFormat) {
					ifdos.writeUnsigned16(TIFFTags.SAMPLEFORMAT);
					ifdos.writeUnsigned16(0x0003);	// - type   SHORT
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0001);	// - length 1
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,sampleFormat);
				}
				if (includeYCbCrSubSampling) {
					ifdos.writeUnsigned16(TIFFTags.YCBCRSUBSAMPLING);
					ifdos.writeUnsigned16(0x0003);	// - type   SHORT
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,0x0002);	// - length 2
					ifdos.writeUnsigned16(pyramidLevel > 0 ? 0x0001 : 0x0001);	// horizontal subsampling - original is 1 (SVS file lies), default from Java codec is 1; override TIFF default of 2
					ifdos.writeUnsigned16(pyramidLevel > 0 ? 0x0001 : 0x0001);	// vertical   subsampling - original is 1 (SVS file lies), default from Java codec is 1; override TIFF default of 2
					if (useBigTIFF) {
						ifdos.writeUnsigned32(0x0000);		// value field is 64 not 32
					}
				}
				if (haveICCProfile) {
					ifdos.writeUnsigned16(TIFFTags.ICCPROFILE);		// This is a private tag - see icc1v42.pdf "B.4 Embedding ICC profiles in TIFF files"
					ifdos.writeUnsigned16(0x0007);		// - type   UNDEFINED
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,iccProfile.length);	// - length
					writeUnsigned32Or64Bits(ifdos,useBigTIFF,iccProfileOffset);
				}

				writeUnsigned32Or64Bits(ifdos,useBigTIFF,offsetOfNextIFD);
				
				// should now be positioned at bitsPerSampleOffset - write SHORT

				if (useBitsPerSampleOffset) {
					for (int i=0; i<samplesPerPixel; ++i) {
						ifdos.writeUnsigned16((int)bitsPerSample);
					}
				}
				// else wrote them in the value field since they fit
				
				// should now be positioned at xResolutionOffset

				if (!useBigTIFF) {
					ifdos.writeUnsigned32(resolutionNumeratorX);
					ifdos.writeUnsigned32(0x0001);	// denominator
				}
				// else wrote them in the longer value field since they fit

				// should now be positioned at yResolutionOffset

				if (!useBigTIFF) {
					ifdos.writeUnsigned32(resolutionNumeratorY);
					ifdos.writeUnsigned32(0x0001);	// denominator
				}
				// else wrote them in the longer value field since they fit
				
				// should now be positioned at iccProfileOffset

				if (iccProfile != null && iccProfile.length > 0) {
					ifdos.write(iccProfile);
				}
				
				// should now be positioned at tileOffsetsOffset
				
				for (int i=0; i<numberOfTiles; ++i) {
					long offset = tileDataByteOffsets[pyramidLevel][i];
					slf4jlogger.trace("makeTIFFInPreambleAndAddDataSetTrailingPadding(): pyramid level {} tile {} offset = {}",pyramidLevel,i,offset);
					if (useBigTIFF) {
						ifdos.writeUnsigned64(offset);
					}
					else {
						if (offset > UNSIGNED32_MAX_VALUE) {
							throw new DicomException("Offset for frame "+i+" is too large to fit in 32 bits 0x"+Long.toHexString(offset)+" ("+offset+" dec)");
						}
						ifdos.writeUnsigned32(offset);
					}
				}

				// should now be positioned at tileByteCountsOffset

				for (int i=0; i<numberOfTiles; ++i) {
					long length = tileDataLengths[pyramidLevel][i];
					slf4jlogger.trace("makeTIFFInPreambleAndAddDataSetTrailingPadding(): pyramid level {} tile {} length = {}",pyramidLevel,i,length);
					if (useBigTIFF) {
						ifdos.writeUnsigned64(length);
					}
					else {
						if (length > UNSIGNED32_MAX_VALUE) {
							throw new DicomException("Length for frame "+i+" is too large to fit in 32 bits 0x"+Long.toHexString(length)+" ("+length+" dec)");
						}
						ifdos.writeUnsigned32(length);
					}
				}

				offsetofIFD = offsetOfNextIFD;
				mmPerPixelX*=2;
				mmPerPixelY*=2;
			}
			
			ifdos.flush();
			ifdos.close();
			
			{
				Attribute dataSetTrailingPadding = new OtherByteAttribute(TagFromName.DataSetTrailingPadding);
				dataSetTrailingPadding.setValues(ifdaos.toByteArray());
				list.put(dataSetTrailingPadding);
			}
		}
		
		return preamble;
	}

	/**
	 * <p>Read a DICOM image input format file and create a dual-personality DICOM-TIFF file.</p>
	 *
	 * <p>Does not change the Transfer Syntax.</p>
	 *
	 * @param	inputFileName
	 * @param	outputFileName
	 * @param	addTIFF		whether or not to add a TIFF IFD in the DICOM preamble to make a dual-personality DICOM-TIFF file sharing the same pixel data
	 * @param	useBigTIFF	whether or not to create a BigTIFF rather than Classic TIFF file
	 * @exception			IOException
	 * @exception			DicomException
	 * @exception			NumberFormatException
	 */
	public AddTIFFOrOffsetTables(String inputFileName,String outputFileName,boolean addTIFF,boolean useBigTIFF)
			throws IOException, DicomException, TIFFException, NumberFormatException {
		this(inputFileName,outputFileName,addTIFF,useBigTIFF,false/*addExtendedOffsetTable*/,false/*addBasicOffsetTable*/);
	}
	
	/**
	 * <p>Read a DICOM image input format file and create a dual-personality DICOM-TIFF file and/or add Basic or Extended Offset Tables.</p>
	 *
	 * <p>Does not change the Transfer Syntax.</p>
	 *
	 * @param	inputFileName
	 * @param	outputFileName
	 * @param	addTIFF		whether or not to add a TIFF IFD in the DICOM preamble to make a dual-personality DICOM-TIFF file sharing the same pixel data
	 * @param	useBigTIFF	whether or not to create a BigTIFF rather than Classic TIFF file
	 * @param	addExtendedOffsetTable		whether or not to add an Extended Offset Table (EOT)
	 * @param	addBasicOffsetTable			whether or not to add a Basic Offset Table (EOT)
	 * @exception			IOException
	 * @exception			DicomException
	 * @exception			NumberFormatException
	 */
	public AddTIFFOrOffsetTables(String inputFileName,String outputFileName,boolean addTIFF,boolean useBigTIFF,boolean addExtendedOffsetTable,boolean addBasicOffsetTable)
			throws IOException, DicomException, TIFFException, NumberFormatException {
		AttributeList list = new AttributeList();
		list.setDecompressPixelData(false);
		list.read(inputFileName);
		
		String transferSyntax = Attribute.getSingleStringValueOrEmptyString(list,TagFromName.TransferSyntaxUID);
		if (transferSyntax == null || transferSyntax.length() == 0) {
			throw new DicomException("Missing or empty TransferSyntaxUID in "+inputFileName);
		}
		
		if (!new TransferSyntax(transferSyntax).isEncapsulated()) {
			if (addExtendedOffsetTable) {
				throw new DicomException("Cannot add Extended Offset Table for non-encapsulated Transfer Syntax "+transferSyntax);
			}
			if (addBasicOffsetTable) {
				throw new DicomException("Cannot add Basic Offset Table for non-encapsulated Transfer Syntax "+transferSyntax);
			}
		}

		list.removeGroupLengthAttributes();
		list.removeMetaInformationHeaderAttributes();
		list.remove(TagFromName.DataSetTrailingPadding);	// may be existing if already TIFF or some other reason, and needs to be removed first else screws up lengths

		FileMetaInformation.addFileMetaInformation(list,transferSyntax,"OURAETITLE");	// need to do this before addTIFF

		byte[] preamble = null;
		
		if (addTIFF || addExtendedOffsetTable || addBasicOffsetTable) {
			long samplesPerPixel = Attribute.getSingleIntegerValueOrDefault(list,TagFromName.SamplesPerPixel,0);
			long planarConfig = Attribute.getSingleIntegerValueOrDefault(list,TagFromName.PlanarConfiguration,0) + 1;	// TIFF is 1 or 2 but sometimes absent (0), DICOM is 0 or 1

			long outputPhotometric = getTIFFPhotometricFromDICOMPhotometricInterpretation(Attribute.getSingleStringValueOrEmptyString(list,TagFromName.PhotometricInterpretation));
			long lowerPhotometric =	outputPhotometric; // what to use for lower pyramidal levels, since may be compressed differently than base layer

			int numberOfPyramidLevels = 1;
			if (Attribute.getSingleStringValueOrEmptyString(list,pixelmedPrivatePyramidDataBlockReservation).equals(pixelmedPrivateCreatorForPyramidData)) {
				SequenceAttribute pyramidData = (SequenceAttribute)(list.get(pixelmedPrivatePyramidData));
				if (pyramidData != null) {
					numberOfPyramidLevels = pyramidData.getNumberOfItems() + 1;	// i.e., include base layer in PixelData
					if (numberOfPyramidLevels >= 2) {
						Attribute a = SequenceAttribute.getNamedAttributeFromWithinSelectedItemWithinSequence(pyramidData,0,TagFromName.PhotometricInterpretation);
						if (a != null) {
							String lowerPhotometricInterpretation = a.getSingleStringValueOrDefault("");
							lowerPhotometric = getTIFFPhotometricFromDICOMPhotometricInterpretation(lowerPhotometricInterpretation);
						}
					}
				}
			}
			slf4jlogger.debug("AddTIFFOrOffsetTables(): numberOfPyramidLevels = {}",numberOfPyramidLevels);
			long[][] tileDataByteOffsets = new long[numberOfPyramidLevels][];
			long[][] tileDataLengths = new long[numberOfPyramidLevels][];
			long[] imageWidths = new long[numberOfPyramidLevels];
			long[] imageLengths = new long[numberOfPyramidLevels];
			
			// numberOfSourceTiles will == number of DICOM tiles for planarConfig 1 (color-by-pixel) and be samplesPerPixel * number of DICOM tiles for planarConfig 2 (color-by-plane) (TIFF6 p 68) (001351)
			boolean mergeSamplesPerPixelTiles = planarConfig == 2 && samplesPerPixel > 1;
			slf4jlogger.debug("AddTIFFOrOffsetTables(): mergeSamplesPerPixelTiles = {}",mergeSamplesPerPixelTiles);

			int numberOfFrames = Attribute.getSingleIntegerValueOrDefault(list,TagFromName.NumberOfFrames,1);

			int basicOffsetTable[] = null;

			if (addBasicOffsetTable) {
				if (addExtendedOffsetTable) {
					slf4jlogger.warn("AddTIFFOrOffsetTables(): Basic Offset Table Item Value is not permitted if Extended Offset Table is present - inserting anyway assuming creating test invalid object"); // PS3.5 A.4
				}
				// create BOT with dummy values to be set later, so that length of dataset can be calculated before BOT entries are set ...
				basicOffsetTable = new int[numberOfFrames];
				Attribute aPixelData = list.get(TagFromName.PixelData);
				if (aPixelData != null) {
					if (aPixelData instanceof OtherByteAttributeMultipleCompressedFrames) {
						((OtherByteAttributeMultipleCompressedFrames)aPixelData).setBasicOffsetTable(basicOffsetTable);
					}
					else if (aPixelData instanceof OtherByteAttributeCompressedSeparateFramesOnDisk) {
						((OtherByteAttributeCompressedSeparateFramesOnDisk)aPixelData).setBasicOffsetTable(basicOffsetTable);
					}
					else {
						throw new DicomException("Not yet implemented - inserting BOT into "+aPixelData.getClass());
					}
				}
				else {
					throw new DicomException("Unable to find PixelData attribute into which to insert BOT");
				}
			}

			long extendedOffsetTable[] = null;
			long extendedOffsetTableLengths[] = null;
			if (addExtendedOffsetTable) {
				// create attributes with dummy values to be set later, so that length of dataset can be calculated before they are set ...
				extendedOffsetTable = new long[numberOfFrames];
				extendedOffsetTableLengths = new long[numberOfFrames];
				{ Attribute a = new OtherVeryLongAttribute(TagFromName.ExtendedOffsetTable); a.setValues(extendedOffsetTable); list.put(a); }
				{ Attribute a = new OtherVeryLongAttribute(TagFromName.ExtendedOffsetTableLengths); a.setValues(extendedOffsetTableLengths); list.put(a); }
			}
			
			long byteOffsetFromFileStartOfNextAttributeAfterPixelData = getByteOffsetsAndLengthsOfTileDataFromStartOfFile(list,transferSyntax,tileDataByteOffsets,tileDataLengths,imageWidths,imageLengths,mergeSamplesPerPixelTiles,samplesPerPixel,addBasicOffsetTable);
			
			if (addBasicOffsetTable) {
				long firstOffsetFromStartOfFile = tileDataByteOffsets[0][0];
				for (int f=0; f<numberOfFrames; ++f) {
					// PS3.5 A.4 "byte offsets to the first byte of the Item Tag of the first fragment for each frame ... measured from the first byte of the first Item Tag following the Basic Offset Table item"
					long offset = tileDataByteOffsets[0][f] - firstOffsetFromStartOfFile;	// 1st frame offset is always zero; successive offsets account for 8 bits of Item Tag + length
					if (offset > UNSIGNED32_MAX_VALUE) {
						throw new DicomException("Offset for frame is too large to fit in 32 bits 0x"+Long.toHexString(offset)+" ("+offset+" dec) - cannot create Basic Offset Table");
					}
					basicOffsetTable[f] = (int)offset;
					if (slf4jlogger.isTraceEnabled() && (f<10 || f>(numberOfFrames-10))){
						slf4jlogger.trace("AddTIFFOrOffsetTables(): frame[{}] BOT offset {}",f,basicOffsetTable[f]);
					}
				}
				// do not assume that setting the array previously re-used the array (may have copied), so update ...
				Attribute aPixelData = list.get(TagFromName.PixelData);
				if (aPixelData != null) {
					if (aPixelData instanceof OtherByteAttributeMultipleCompressedFrames) {
						((OtherByteAttributeMultipleCompressedFrames)aPixelData).setBasicOffsetTable(basicOffsetTable);
					}
					else if (aPixelData instanceof OtherByteAttributeCompressedSeparateFramesOnDisk) {
						((OtherByteAttributeCompressedSeparateFramesOnDisk)aPixelData).setBasicOffsetTable(basicOffsetTable);
					}
				}
			}
			
			if (addExtendedOffsetTable) {
				long firstOffsetFromStartOfFile = tileDataByteOffsets[0][0];
				for (int f=0; f<numberOfFrames; ++f) {
					// PS3.3 C.7.6.3.1.8 "byte offsets to the first byte of the Item Tag of the first fragment for every frame ... measured from the first byte of the first Item Tag of the first fragment of the first frame"
					extendedOffsetTable[f] = tileDataByteOffsets[0][f] - firstOffsetFromStartOfFile;	// 1st frame offset is always zero; successive offsets account for 8 bits of Item Tag + length
					extendedOffsetTableLengths[f] = tileDataLengths[0][f];								// These might be odd lengths - the standard is silent on whether these should be the padded even length or not :(
					if (slf4jlogger.isTraceEnabled() && (f<10 || f>(numberOfFrames-10))){
						slf4jlogger.trace("AddTIFFOrOffsetTables(): frame[{}] EOT offset {} length {}",f,extendedOffsetTable[f],extendedOffsetTableLengths[f]);
					}
				}
				// do not assume that setting the arrays previously re-used the arrays (may have copied), so update ...
				list.get(TagFromName.ExtendedOffsetTable).setValues(extendedOffsetTable);
				list.get(TagFromName.ExtendedOffsetTableLengths).setValues(extendedOffsetTableLengths);
				if (slf4jlogger.isTraceEnabled()) { slf4jlogger.trace("AddTIFFOrOffsetTables(): {}",list.toString()); }
			}

			if (addTIFF) {
				long tileWidth = Attribute.getSingleIntegerValueOrDefault(list,TagFromName.Columns,0);
				long tileLength = Attribute.getSingleIntegerValueOrDefault(list,TagFromName.Rows,0);
				long bitsPerSample = Attribute.getSingleIntegerValueOrDefault(list,TagFromName.BitsAllocated,0);
				long outputCompression = getTIFFCompressionFromTransferSyntax(transferSyntax);
				long sampleFormat = 0x0001;	// if used, always unsigned integer data
				byte[] iccProfile = null;
				{
					Attribute aICCProfile = SequenceAttribute.getNamedAttributeFromWithinSelectedItemWithinSequence(list,TagFromName.OpticalPathSequence,0,TagFromName.ICCProfile);
					if (aICCProfile != null) {
						iccProfile = aICCProfile.getByteValues();
					}
				}
				double mmPerPixelXBaseLayer = 0;
				double mmPerPixelYBaseLayer = 0;
				{
					Attribute aPixelSpacing = list.get(TagFromName.PixelSpacing);
					if (aPixelSpacing == null) {
						SequenceAttribute sharedFunctionalGroupsSequence = (SequenceAttribute)(list.get(TagFromName.SharedFunctionalGroupsSequence));
						if (sharedFunctionalGroupsSequence != null) {
							SequenceAttribute sharedPixelMeasuresSequence = (SequenceAttribute)(SequenceAttribute.getNamedAttributeFromWithinSequenceWithSingleItem(
								sharedFunctionalGroupsSequence,TagFromName.PixelMeasuresSequence));
							if (sharedPixelMeasuresSequence != null) {
								aPixelSpacing = SequenceAttribute.getNamedAttributeFromWithinSequenceWithSingleItem(sharedPixelMeasuresSequence,TagFromName.PixelSpacing);
							}
						}
					}
					if (aPixelSpacing != null) {
						// note that order in DICOM in PixelSpacing is "adjacent row spacing" (Y), then "adjacent column spacing" (X) ...
						double[] pixelSpacing = aPixelSpacing.getDoubleValues();
						if (pixelSpacing != null && pixelSpacing.length == 2) {		// (001327)
							mmPerPixelYBaseLayer = pixelSpacing[0];	// between rows
							mmPerPixelXBaseLayer = pixelSpacing[1];	// between cols
						}
					}
				}

				preamble = makeTIFFInPreambleAndAddDataSetTrailingPadding(byteOffsetFromFileStartOfNextAttributeAfterPixelData,numberOfPyramidLevels,tileDataByteOffsets,tileDataLengths,imageWidths,imageLengths,list,
					tileWidth,tileLength,bitsPerSample,outputCompression,outputPhotometric,lowerPhotometric,samplesPerPixel,planarConfig,sampleFormat,iccProfile,mmPerPixelXBaseLayer,mmPerPixelYBaseLayer,useBigTIFF);
			}
		}
		list.write(outputFileName,transferSyntax,true,true,preamble);
	}
	
	/**
	 * <p>Read a DICOM image input format file and create a dual-personality DICOM-TIFF file and/or add Basic or Extended Offset Tables.</p>
	 *
	 * <p>Options are:</p>
	 * <p>ADDTIFF | DONOTADDTIFF (default)</p>
	 * <p>USEBIGTIFF (default) | DONOTUSEBIGTIFF</p>
	 * <p>ADDEXTENDEDOFFSETTABLE | DONOTADDEXTENDEDOFFSETTABLE (default)</p>
	 * <p>ADDBASICOFFSETTABLE | DONOTADDBASICOFFSETTABLE (default)</p>
	 *
	 * @param	arg	two parameters plus options, the DICOM inputFile, the DICOM outputFile, then various options controlling conversion
	 */
	public static void main(String arg[]) {
		try {
			boolean addTIFF = false;
			boolean useBigTIFF = true;
			boolean addExtendedOffsetTable = false;
			boolean addBasicOffsetTable = false;
	
			int numberOfFixedArguments = 2;
			int numberOfFixedAndOptionalArguments = 2;
			int endOptionsPosition = arg.length;
			boolean bad = false;
			
			if (endOptionsPosition < numberOfFixedArguments) {
				bad = true;
			}
			boolean keepLooking = true;
			while (keepLooking && endOptionsPosition > numberOfFixedArguments) {
				String option = arg[endOptionsPosition-1].trim().toUpperCase();
				switch (option) {
					case "ADDTIFF":				addTIFF = true; --endOptionsPosition; break;
					case "DONOTADDTIFF":		addTIFF = false; --endOptionsPosition; break;
					
					case "USEBIGTIFF":			useBigTIFF = true; --endOptionsPosition; break;
					case "DONOTUSEBIGTIFF":		useBigTIFF = false; --endOptionsPosition; break;
					
					case "ADDEXTENDEDOFFSETTABLE":			addExtendedOffsetTable = true; --endOptionsPosition; break;
					case "DONOTADDEXTENDEDOFFSETTABLE":		addExtendedOffsetTable = false; --endOptionsPosition; break;
					
					case "ADDBASICOFFSETTABLE":				addBasicOffsetTable = true; --endOptionsPosition; break;
					case "DONOTADDBASICOFFSETTABLE":		addBasicOffsetTable = false; --endOptionsPosition; break;

					default:	if (endOptionsPosition > numberOfFixedAndOptionalArguments) {
									slf4jlogger.error("Unrecognized argument {}",option);
									bad = true;
								}
								keepLooking = false;
								break;
				}
			}
			
			if (!bad) {
				String inputFile = arg[0];
				String outputFile = arg[1];
				
				new AddTIFFOrOffsetTables(inputFile,outputFile,addTIFF,useBigTIFF,addExtendedOffsetTable,addBasicOffsetTable);
			}
			else {
				System.err.println("Error: Incorrect number of arguments or bad arguments");
				System.err.println("Usage: AddTIFFOrOffsetTables inputFile outputFile"
					+" [ADDTIFF|DONOTADDTIFF]"
					+" [USEBIGTIFF|DONOTUSEBIGTIFF]"
					+" [ADDEXTENDEDOFFSETTABLE|DONOTADDEXTENDEDOFFSETTABLE]"
					+" [ADDBASICOFFSETTABLE|DONOTADDBASICOFFSETTABLE]"
				);
				System.exit(1);
			}
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}
}



