[med-svn] [pixelmed-codec] 03/05: New upstream version 20170512

Andreas Tille tille at debian.org
Thu Sep 14 16:56:17 UTC 2017


This is an automated email from the git hooks/post-receive script.

tille pushed a commit to branch master
in repository pixelmed-codec.

commit 1541fc9c7412e2e96ae1d045b895c0ef2076b264
Author: Andreas Tille <tille at debian.org>
Date:   Thu Sep 14 18:54:56 2017 +0200

    New upstream version 20170512
---
 Makefile                                           |  48 ++-
 com/pixelmed/codec/jpeg/EntropyCodedSegment.java   | 259 ++++++++++++++--
 com/pixelmed/codec/jpeg/Makefile                   |  63 +++-
 com/pixelmed/codec/jpeg/MarkerSegmentSOF.java      |  15 +-
 com/pixelmed/codec/jpeg/MarkerSegmentSOS.java      |   7 +-
 com/pixelmed/codec/jpeg/Markers.java               |  79 ++++-
 com/pixelmed/codec/jpeg/OutputArrayOrStream.java   | 172 +++++++++++
 com/pixelmed/codec/jpeg/Parse.java                 | 298 ++++++++++++++-----
 com/pixelmed/codec/jpeg/package.html               |   2 +-
 com/pixelmed/imageio/JPEGLosslessImageReader.java  | 329 +++++++++++++++++++++
 .../imageio/JPEGLosslessImageReaderSpi.java        | 130 ++++++++
 com/pixelmed/imageio/JPEGLosslessMetadata.java     | 105 +++++++
 .../imageio/JPEGLosslessMetadataFormat.java        |  48 +++
 com/pixelmed/imageio/Makefile                      |  32 ++
 com/pixelmed/imageio/TestImageIO.java              | 106 +++++++
 com/pixelmed/{codec/jpeg => imageio}/package.html  |   7 +-
 16 files changed, 1572 insertions(+), 128 deletions(-)

diff --git a/Makefile b/Makefile
index e6e395f..d9b3587 100755
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-all:	pixelmed_codec.jar
+all:	pixelmed_codec.jar pixelmed_imageio.jar
 
 TAR = gnutar
 #TAR = tar
@@ -6,10 +6,12 @@ COMPRESS = bzip2
 COMPRESSEXT = bz2
 
 SUBDIRS = \
-	com/pixelmed/codec/jpeg
+	com/pixelmed/codec/jpeg \
+	com/pixelmed/imageio
 	
 SUBPACKAGES = \
-	com.pixelmed.codec.jpeg
+	com.pixelmed.codec.jpeg \
+	com.pixelmed.imageio
 
 ADDITIONALFILES = \
 	COPYRIGHT \
@@ -22,6 +24,7 @@ ADDITIONALSOURCEFILES = \
 
 BINARYRELEASEFILES = \
 	pixelmed_codec.jar \
+	pixelmed_imageio.jar \
 	BUILDDATE \
 	${ADDITIONALFILES}
 
@@ -50,27 +53,44 @@ JAVADOCFILES = \
 DOXYGENFILES = \
         docs/doxygen
 
-OTHERDOCRELEASEFILES = \
+#OTHERDOCRELEASEFILES = \
+#
 
 PATHTOROOT = .
 
 include ${PATHTOROOT}/Makefile.common.mk
 
+metainf:
+	mkdir -p META-INF/services
+	rm -f META-INF/services/javax.imageio.spi.ImageReaderSpi
+	echo >META-INF/services/javax.imageio.spi.ImageReaderSpi "com.pixelmed.imageio.JPEGLosslessImageReaderSpi"
+
 pixelmed_codec.jar:
 	(cd com/pixelmed/codec/jpeg; make all)
 	date >BUILDDATE
 	jar -cvf $@ BUILDDATE COPYRIGHT \
 		com/pixelmed/codec/jpeg/*.class
+
+pixelmed_imageio.jar:	metainf
+	(cd com/pixelmed/codec/jpeg; make all)
+	(cd com/pixelmed/imageio; make all)
+	date >BUILDDATE
+	jar -cvf $@ BUILDDATE COPYRIGHT \
+		META-INF/services/javax.imageio.spi.ImageReaderSpi \
+		com/pixelmed/codec/jpeg/*.class \
+		com/pixelmed/imageio/*.class \
+
 changelog:
 	rm -f CHANGES
 	cvsps -u -q | egrep -v '^(PatchSet|Author:|Branch:|Tag:|Members:|Log:)' | fgrep -v '*** empty log message ***' | grep -v '^[ ]*$$' | sed -e 's/:[0-9.]*->[0-9.]*//' -e 's/:INITIAL->[0-9.]*//' -e 's/^Date: \([0-9][0-9][0-9][0-9]\/[0-9][0-9]\/[0-9][0-9]\) [0-9:]*$$/\1/' >CHANGES
 	bzip2 <CHANGES >CHANGES.bz2
 	
 clean:	cleanallexceptjar
-	rm -f pixelmed_codec.jar
+	rm -f pixelmed_codec.jar pixelmed_imageio.jar
 
 cleanallexceptjar:	cleansubdirs
 	rm -f *~ *.class .exclude.list
+	rm -rf META-INF
 
 cleansubdirs:
 	for d in ${SUBDIRS}; \
@@ -95,17 +115,18 @@ archivedoxygen: .exclude.list #doxygen
 	export COPY_EXTENDED_ATTRIBUTES_DISABLE=true; \
 	${TAR} -cv -X .exclude.list -f - ${DOXYGENFILES} | ${COMPRESS} > pixelmedjavacodec_javadoc_archive.`date '+%Y%m%d'`.tar.${COMPRESSEXT}
 
-releaseall:	changelog sourcerelease javadocrelease doxygenrelease binaryrelease dependencyrelease otherdocrelease
+#releaseall:	changelog sourcerelease javadocrelease doxygenrelease binaryrelease dependencyrelease otherdocrelease
+releaseall:	changelog sourcerelease javadocrelease doxygenrelease binaryrelease dependencyrelease
 
-binaryrelease: cleanallexceptjar .exclude.list pixelmed_codec.jar #javadoc doxygen
+binaryrelease: cleanallexceptjar .exclude.list pixelmed_codec.jar pixelmed_imageio.jar #javadoc doxygen
 	export COPYFILE_DISABLE=true; \
 	export COPY_EXTENDED_ATTRIBUTES_DISABLE=true; \
 	${TAR} -cv -X .exclude.list -f - ${BINARYRELEASEFILES} | ${COMPRESS} > pixelmedjavacodec_binaryrelease.`date '+%Y%m%d'`.tar.${COMPRESSEXT}
 
-otherdocrelease:
-	export COPYFILE_DISABLE=true; \
-	export COPY_EXTENDED_ATTRIBUTES_DISABLE=true; \
-	${TAR} -cv -f - ${OTHERDOCRELEASEFILES} | ${COMPRESS} > pixelmedjavacodec_otherdocsrelease.`date '+%Y%m%d'`.tar.${COMPRESSEXT}
+#otherdocrelease:
+#	export COPYFILE_DISABLE=true; \
+#	export COPY_EXTENDED_ATTRIBUTES_DISABLE=true; \
+#	${TAR} -cv -f - ${OTHERDOCRELEASEFILES} | ${COMPRESS} > pixelmedjavacodec_otherdocsrelease.`date '+%Y%m%d'`.tar.${COMPRESSEXT}
 
 dependencyrelease:	.exclude.list
 	export COPYFILE_DISABLE=true; \
@@ -161,6 +182,7 @@ doxygen:
 	rm -rf docs/doxygen
 	doxygen Doxyfile
 
-installinpixelmed:	pixelmed_codec.jar
-	cp $< ../pixelmed/imgbook/lib/additional/
+installinpixelmed:	pixelmed_codec.jar pixelmed_imageio.jar
+	cp pixelmed_codec.jar ../pixelmed/imgbook/lib/additional/
+	cp pixelmed_imageio.jar ../pixelmed/imgbook/lib/additional/
 
diff --git a/com/pixelmed/codec/jpeg/EntropyCodedSegment.java b/com/pixelmed/codec/jpeg/EntropyCodedSegment.java
index c084b79..f42f22a 100644
--- a/com/pixelmed/codec/jpeg/EntropyCodedSegment.java
+++ b/com/pixelmed/codec/jpeg/EntropyCodedSegment.java
@@ -1,4 +1,4 @@
-/* Copyright (c) 2014, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+/* Copyright (c) 2014-2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
 
 package com.pixelmed.codec.jpeg;
 
@@ -22,18 +22,51 @@ import java.util.Vector;
  */
 public class EntropyCodedSegment {
 
-	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/EntropyCodedSegment.java,v 1.13 2014/03/29 21:58:47 dclunie Exp $";
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/EntropyCodedSegment.java,v 1.24 2016/01/16 13:30:09 dclunie Exp $";
 
 	private boolean copying;
+	private boolean decompressing;
+
+	private OutputArrayOrStream[] decompressedOutputPerComponent;
+
+	private boolean isHuffman;
+	private boolean isDCT;
+	private boolean isLossless;
 
 	private ByteArrayOutputStream copiedBytes;
 		
-	private final int restartinterval;
  	private final MarkerSegmentSOS sos;
  	private final MarkerSegmentSOF sof;
  	private final Map<String,HuffmanTable> htByClassAndIdentifer;
  	private final Map<String,QuantizationTable> qtByIdentifer;
+
+ 	private final int nComponents;
+ 	private final int[] DCEntropyCodingTableSelector;
+ 	private final int[] ACEntropyCodingTableSelector;
+ 	private final int[] HorizontalSamplingFactor;
+ 	private final int[] VerticalSamplingFactor;
+		
+ 	private final int maxHorizontalSamplingFactor;
+ 	private final int maxVerticalSamplingFactor;
+	
+	private final int nMCUHorizontally;
 	
+	private final Vector<Shape> redactionShapes;
+
+	// stuff for lossless decompression ...
+	private final int predictorForFirstSample;
+ 	private final int[] predictorForComponent;
+	private final int predictorSelectionValue;
+
+	// these are class level and used by getOneLosslessValue() to maintain state (updates them) and initialized by constructor
+ 	private int[] rowNumberAtBeginningOfRestartInterval;	// indexed by component number, not final since set at beginning of each
+ 	private final int[] rowLength;							// indexed by component number
+ 	private final int[] currentRowNumber;					// indexed by component number
+ 	private final int[] positionWithinRow;					// indexed by component number
+ 	private final int[][] previousReconstructedRow;			// indexed by component number, positionWithinRow
+ 	private final int[][] currentReconstructedRow;			// indexed by component number, positionWithinRow
+
+	// stuff for bit extraction ...
 	// copied from com.pixelmed.scpecg.HuffmanDecoder ...
 	private byte[] bytesToDecompress;
 	private int availableBytes;
@@ -180,6 +213,7 @@ public class EntropyCodedSegment {
 
 	// values above index 11 only occur for 12 bit process ...
 	private int[] dcSignBitMask = { 0x00/*na*/,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x100,0x200,0x400,0x800,0x1000,0x2000,0x4000 /*no entry for 16*/};
+	private int[] maxAmplitude  = { 0/*na*/,0x02-1,0x04-1,0x08-1,0x10-1,0x20-1,0x40-1,0x80-1,0x100-1,0x200-1,0x400-1,0x800-1,0x1000-1,0x2000-1,0x4000-1,0x8000-1 /*no entry for 16*/};
 
 	private final int convertSignAndAmplitudeBitsToValue(int value,int length) throws Exception {
 		// see P&M Table 11-1 page 190 and Table 11-4 page 193 (same for DC and AC)
@@ -187,7 +221,7 @@ public class EntropyCodedSegment {
 //System.err.println("dcSignBitMask = "+Integer.toHexString(dcSignBitMask[length]));
 			if ((value & dcSignBitMask[length]) == 0) {
 //System.err.println("Have sign bit");
-				value = - value - 1;
+				value = value - maxAmplitude[length];
 			}
 		}
 		return value;
@@ -198,18 +232,174 @@ public class EntropyCodedSegment {
 		writeBits(usingTable.getEOBCode(),usingTable.getEOBCodeLength());
 	}
 	
-	
-	public EntropyCodedSegment(int restartinterval,MarkerSegmentSOS sos,MarkerSegmentSOF sof,Map<String,HuffmanTable> htByClassAndIdentifer,Map<String,QuantizationTable> qtByIdentifer,boolean copying,boolean dumping) {
- 		this.restartinterval = restartinterval;
+
+	/**
+	 * <p>Set up the environment to decode an EntropyCodedSeqment to dump, redact or copy as required.</p>
+	 *
+	 * @param	sos								SOS marker segment contents
+	 * @param	sof								SOF marker segment contents
+	 * @param	htByClassAndIdentifer			Huffman tables
+	 * @param	qtByIdentifer					quantization tables
+	 * @param	nMCUHorizontally				the number of MCUs in a single row
+	 * @param	redactionShapes					a Vector of Shape that are Rectangle
+	 * @param	copying							true if copying
+	 * @param	dumping							true if dumping
+	 * @param	decompressing					true if decompressing
+	 * @param	decompressedOutput				the decompressed output (with specified or default endianness if precision > 8)
+	 * @throws Exception						if JPEG process not supported
+	 */
+	public EntropyCodedSegment(MarkerSegmentSOS sos,MarkerSegmentSOF sof,Map<String,HuffmanTable> htByClassAndIdentifer,Map<String,QuantizationTable> qtByIdentifer,int nMCUHorizontally,Vector<Shape> redactionShapes,boolean copying,boolean dumping,boolean decompressing,Parse.DecompressedOutput decompressedOutput) throws Exception {
  		this.sos = sos;
  		this.sof = sof;
  		this.htByClassAndIdentifer = htByClassAndIdentifer;
  		this.qtByIdentifer = qtByIdentifer;
+		this.nMCUHorizontally = nMCUHorizontally;
+		this.redactionShapes = redactionShapes;
 		this.copying = copying;
+		// dumping is not used other than in this constructor
+		this.decompressing = decompressing;
+		this.decompressedOutputPerComponent = decompressedOutput == null ? null : decompressedOutput.getDecompressedOutputPerComponent();
+		
+		this.isHuffman = Markers.isHuffman(sof.getMarker());
+		if (!isHuffman) {
+			throw new Exception("Only Huffman processes supported (not "+Markers.getAbbreviation(sof.getMarker())+" "+Markers.getDescription(sof.getMarker())+")");
+		}
+		this.isDCT = Markers.isDCT(sof.getMarker());
+		this.isLossless = Markers.isLossless(sof.getMarker());
+
+		nComponents = sos.getNComponentsPerScan();
+		DCEntropyCodingTableSelector = sos.getDCEntropyCodingTableSelector();
+		ACEntropyCodingTableSelector = sos.getACEntropyCodingTableSelector();
+		HorizontalSamplingFactor = sof.getHorizontalSamplingFactor();
+		VerticalSamplingFactor   = sof.getVerticalSamplingFactor();
+		
+		maxHorizontalSamplingFactor = max(HorizontalSamplingFactor);
+//System.err.println("maxHorizontalSamplingFactor "+maxHorizontalSamplingFactor);
+		maxVerticalSamplingFactor   = max(VerticalSamplingFactor);
+//System.err.println("maxVerticalSamplingFactor "+maxVerticalSamplingFactor);
+
+		if (isLossless && decompressing) {
+//System.err.println("SamplePrecision "+sof.getSamplePrecision());
+//System.err.println("SuccessiveApproximationBitPositionLowOrPointTransform "+sos.getSuccessiveApproximationBitPositionLowOrPointTransform());
+			predictorForFirstSample = 1 << (sof.getSamplePrecision() - sos.getSuccessiveApproximationBitPositionLowOrPointTransform() - 1);
+//System.err.println("predictorForFirstSample "+predictorForFirstSample+" dec");
+			predictorForComponent = new int[nComponents];
+			predictorSelectionValue = sos.getStartOfSpectralOrPredictorSelection();
+//System.err.println("predictorSelectionValue "+predictorSelectionValue);
+
+			rowLength = new int[nComponents];
+			currentRowNumber = new int[nComponents];
+			positionWithinRow = new int[nComponents];
+			rowNumberAtBeginningOfRestartInterval = new int[nComponents];
+			previousReconstructedRow = new int[nComponents][];
+			currentReconstructedRow = new int[nComponents][];
+			for (int c=0; c<nComponents; ++c) {
+				//rowLength[c] = sof.getNSamplesPerLine()/sof.getHorizontalSamplingFactor()[c];
+				rowLength[c] = (sof.getNSamplesPerLine()-1)/sof.getHorizontalSamplingFactor()[c]+1;		// account for sampling of row lengths not an exact multiple of sampling factor ... hmmm :(
+//System.err.println("rowLength["+c+"] "+rowLength[c]);
+				currentRowNumber[c] = 0;
+				positionWithinRow[c] = 0;
+				rowNumberAtBeginningOfRestartInterval[c] = 0;
+				previousReconstructedRow[c] = new int[rowLength[c]];
+				currentReconstructedRow[c] = new int[rowLength[c]];
+			}
+		}
+		else {
+			predictorForFirstSample = 0;	// silence uninitialized warnings
+			predictorForComponent = null;
+			predictorSelectionValue = 0;
+			rowLength = null;
+			currentRowNumber = null;
+			positionWithinRow = null;
+			rowNumberAtBeginningOfRestartInterval = null;
+			previousReconstructedRow = null;
+			currentReconstructedRow = null;
+		}
 		
 		if (dumping) dumpHuffmanTables();
 		//dumpQuantizationTables();
 	}
+
+	private final int getOneLosslessValue(int c,int dcEntropyCodingTableSelector,int colMCU,int rowMCU) throws Exception {
+		// per P&M page 492 (DIS H-2)
+		int prediction = 0;
+		if (decompressing) {
+			if (currentRowNumber[c] == rowNumberAtBeginningOfRestartInterval[c]) {		// will be true for first row since all rowNumberAtBeginningOfRestartInterval entries are initialized to zero
+				if (positionWithinRow[c] == 0)	{	// first sample of first row
+//System.err.println("Component "+c+" first sample of first row or first row after beginning of restart interval ... use predictorForFirstSample");
+					prediction = predictorForFirstSample;
+				}
+				else {
+//System.err.println("Component "+c+" other than first sample of first row or first row after beginning of restart interval ... use Ra (previous sample in row)");
+					prediction = currentReconstructedRow[c][positionWithinRow[c]-1];	// Ra
+				}
+			}
+			else if (positionWithinRow[c] == 0) {						// first sample of subsequent rows
+//System.err.println("Component "+c+" first sample of subsequent rows");
+				prediction = previousReconstructedRow[c][0];			// Rb for position 0
+			}
+			else {
+				switch(predictorSelectionValue) {
+					case 1:	prediction = currentReconstructedRow[c][positionWithinRow[c]-1];	// Ra
+							break;
+					case 2:	prediction = previousReconstructedRow[c][positionWithinRow[c]];		// Rb
+							break;
+					case 3:	prediction = previousReconstructedRow[c][positionWithinRow[c]-1];	// Rc
+							break;
+					case 4:	prediction = currentReconstructedRow[c][positionWithinRow[c]-1] + previousReconstructedRow[c][positionWithinRow[c]] - previousReconstructedRow[c][positionWithinRow[c]-1];		// Ra + Rb - Rc
+							break;
+					case 5:	prediction = currentReconstructedRow[c][positionWithinRow[c]-1] + ((previousReconstructedRow[c][positionWithinRow[c]] - previousReconstructedRow[c][positionWithinRow[c]-1])>>1);	// Ra + (Rb - Rc)/2
+							break;
+					case 6:	prediction = previousReconstructedRow[c][positionWithinRow[c]] + ((currentReconstructedRow[c][positionWithinRow[c]-1] - previousReconstructedRow[c][positionWithinRow[c]-1])>>1);	// Rb + (Ra - Rc)/2
+							break;
+					case 7: prediction = (currentReconstructedRow[c][positionWithinRow[c]-1] + previousReconstructedRow[c][positionWithinRow[c]])>>1;	// (Ra+Rb)/2
+							break;
+					default:
+						throw new Exception("Unrecognized predictor selection value "+predictorSelectionValue);
+				}
+			}
+//System.err.println("prediction ["+currentRowNumber[c]+","+positionWithinRow[c]+"] = "+prediction+" dec (0x"+Integer.toHexString(prediction)+")");
+		}
+			
+		usingTable = htByClassAndIdentifer.get("0+"+Integer.toString(dcEntropyCodingTableSelector));
+
+		final int ssss = decode();	// number of DC bits encoded next
+		// see P&M Table 11-1 page 190
+		int dcValue = 0;
+		if (ssss == 0) {
+			dcValue = 0;
+		}
+		else if (ssss == 16) {	// only occurs for lossless
+			dcValue = 32768;
+		}
+		else {
+			final int dcBits = getValueOfRequestedLength(ssss);
+			dcValue = convertSignAndAmplitudeBitsToValue(dcBits,ssss);
+		}
+//System.err.println("encoded difference value ["+currentRowNumber[c]+","+positionWithinRow[c]+"] = "+dcValue+" dec (0x"+Integer.toHexString(dcValue)+")");
+		
+		int reconstructedValue = 0;
+		
+		if (decompressing) {
+			reconstructedValue = (dcValue + prediction) & 0x0000ffff;
+		
+//System.err.println("reconstructedValue value ["+currentRowNumber[c]+","+positionWithinRow[c]+"] = "+reconstructedValue+" dec (0x"+Integer.toHexString(reconstructedValue)+")");
+		
+			currentReconstructedRow[c][positionWithinRow[c]] = reconstructedValue;
+		
+			++positionWithinRow[c];
+			if (positionWithinRow[c] >= rowLength[c]) {
+//System.err.println("Component "+c+" starting next row");
+				positionWithinRow[c] = 0;
+				++currentRowNumber[c];
+				int[] holdRow = previousReconstructedRow[c];
+				previousReconstructedRow[c] = currentReconstructedRow[c];
+				currentReconstructedRow[c] = holdRow;	// values do not matter, will be overwritten, saves deallocating and reallocating
+			}
+		}
+		
+		return reconstructedValue;	// meaingless unless decompressing, but still need to have absorbed bits from input to stay in sync
+	}
 	
 	// A "data unit" is the "smallest logical unit that can be processed", which in the case of DCT-based processes is one 8x8 block of coefficients (P&M page 101)
 	private final void getOneDCTDataUnit(int dcEntropyCodingTableSelector,int acEntropyCodingTableSelector,boolean redact) throws Exception {
@@ -268,6 +458,7 @@ public class EntropyCodedSegment {
 	}
 	
 	private final boolean redactionDecision(int colMCU,int rowMCU,int thisHorizontalSamplingFactor,int thisVerticalSamplingFactor,int maxHorizontalSamplingFactor,int maxVerticalSamplingFactor,int h,int v,Vector<Shape> redactionShapes) {
+		// only invoked for DCT so block size is always 8
 		final int vMCUSize = 8 * maxVerticalSamplingFactor;
 		final int hMCUSize = 8 * maxHorizontalSamplingFactor;
 //System.err.println("MCUSize in pixels = "+hMCUSize+" * "+vMCUSize);
@@ -298,14 +489,35 @@ public class EntropyCodedSegment {
 		return redact;
 	}
 	
-	private final void getOneMinimumCodedUnit(int nComponents,int[] DCEntropyCodingTableSelector,int[] ACEntropyCodingTableSelector,int[] HorizontalSamplingFactor,int[] VerticalSamplingFactor,int maxHorizontalSamplingFactor,int maxVerticalSamplingFactor,int colMCU,int rowMCU,Vector<Shape> redactionShapes) throws Exception {
+	private final void writeDecompressedPixel(int c,int decompressedPixel) throws IOException {
+		if (sof.getSamplePrecision() <= 8) {
+			decompressedOutputPerComponent[c].writeByte(decompressedPixel);
+		}
+		else {
+			// endianness handled by OutputArrayOrStream
+			decompressedOutputPerComponent[c].writeShort(decompressedPixel);
+		}
+	}
+	
+	private final void getOneMinimumCodedUnit(int nComponents,int[] DCEntropyCodingTableSelector,int[] ACEntropyCodingTableSelector,int[] HorizontalSamplingFactor,int[] VerticalSamplingFactor,int maxHorizontalSamplingFactor,int maxVerticalSamplingFactor,int colMCU,int rowMCU,Vector<Shape> redactionShapes) throws Exception, IOException {
 		for (int c=0; c<nComponents; ++c) {
 			// See discussion of interleaving of data units within MCUs in P&M section 7.3.5 pages 101-105; always interleaved in sequential mode
 			for (int v=0; v<VerticalSamplingFactor[c]; ++v) {
 				for (int h=0; h<HorizontalSamplingFactor[c]; ++h) {
 //System.err.println("Component "+c+" v "+v+" h "+h);
 					boolean redact = redactionDecision(colMCU,rowMCU,HorizontalSamplingFactor[c],VerticalSamplingFactor[c],maxHorizontalSamplingFactor,maxVerticalSamplingFactor,h,v,redactionShapes);
-					getOneDCTDataUnit(DCEntropyCodingTableSelector[c],ACEntropyCodingTableSelector[c],redact);
+					if (isDCT) {
+						getOneDCTDataUnit(DCEntropyCodingTableSelector[c],ACEntropyCodingTableSelector[c],redact);
+					}
+					else if (isLossless) {
+						int decompressedPixel = getOneLosslessValue(c,DCEntropyCodingTableSelector[c],colMCU,rowMCU);
+						if (decompressing) {
+							writeDecompressedPixel(c,decompressedPixel);
+						}
+					}
+					else {
+						throw new Exception("Only DCT or Lossless processes supported (not "+Markers.getAbbreviation(sof.getMarker())+" "+Markers.getDescription(sof.getMarker())+")");
+					}
 				}
 			}
 		}
@@ -324,14 +536,12 @@ public class EntropyCodedSegment {
 	 *
 	 * @param	bytesToDecompress	the bytes in the EntropyCodedSeqment
 	 * @param	mcuCount			the number of MCUs encoded by this EntropyCodedSeqment
-	 * @param	nMCUHorizontally	the number of MCUs in a single row
 	 * @param	mcuOffset			the number of MCUs that have previously been read for the frame containing this EntropyCodedSeqment
-	 * @param	redactionShapes		a Vector of Shape that are Rectangle
 	 * @return						the bytes in a copy of the EntropyCodedSeqment appropriately redacted
-	 * @exception Exception			if bad things happen parsing the EntropyCodedSeqment, like running out of bits, caused by malformed input
-	 * @exception IOException		if bad things happen reading or writing the bytes
+	 * @throws Exception			if bad things happen parsing the EntropyCodedSeqment, like running out of bits, caused by malformed input
+	 * @throws IOException		if bad things happen reading or writing the bytes
 	 */
-	public final byte[] finish(byte[] bytesToDecompress,int mcuCount,int nMCUHorizontally,int mcuOffset,Vector<Shape> redactionShapes) throws Exception, IOException {
+	public final byte[] finish(byte[] bytesToDecompress,int mcuCount,int mcuOffset) throws Exception, IOException {
 		this.bytesToDecompress = bytesToDecompress;
 		availableBytes = this.bytesToDecompress.length;
 		byteIndex = 0;
@@ -341,24 +551,19 @@ public class EntropyCodedSegment {
 		if (copying) {
 			initializeWriteBits();		// will create a new ByteArrayOutputStream
 		}
-		
+
+		if (rowNumberAtBeginningOfRestartInterval != null) {	// do not need to do this unless decompressiong lossless
+			for (int c=0; c<nComponents; ++c) {
+//System.err.println("Setting rowNumberAtBeginningOfRestartInterval["+c+"] to "+currentRowNumber[c]);
+				rowNumberAtBeginningOfRestartInterval[c] = currentRowNumber[c];	// for lossless decompression predictor selection
+			}
+		}
 		//try {
 		
-		final int nComponents = sos.getNComponentsPerScan();
-		final int[] DCEntropyCodingTableSelector = sos.getDCEntropyCodingTableSelector();
-		final int[] ACEntropyCodingTableSelector = sos.getACEntropyCodingTableSelector();
-		final int[] HorizontalSamplingFactor = sof.getHorizontalSamplingFactor();
-		final int[] VerticalSamplingFactor   = sof.getVerticalSamplingFactor();
-		
-		final int maxHorizontalSamplingFactor = max(HorizontalSamplingFactor);
-//System.err.println("maxHorizontalSamplingFactor "+maxHorizontalSamplingFactor);
-		final int maxVerticalSamplingFactor   = max(VerticalSamplingFactor);
-//System.err.println("maxVerticalSamplingFactor "+maxVerticalSamplingFactor);
-		
 		for (int mcu=0; mcu<mcuCount; ++mcu) {
 			int rowMCU = mcuOffset / nMCUHorizontally;
 			int colMCU = mcuOffset % nMCUHorizontally;
-//System.err.println("MCU ("+colMCU+","+rowMCU+")");
+//System.err.println("MCU ("+rowMCU+","+colMCU+")");
 			getOneMinimumCodedUnit(nComponents,DCEntropyCodingTableSelector,ACEntropyCodingTableSelector,HorizontalSamplingFactor,VerticalSamplingFactor,maxHorizontalSamplingFactor,maxVerticalSamplingFactor,colMCU,rowMCU,redactionShapes);
 			++mcuOffset;
 		}
diff --git a/com/pixelmed/codec/jpeg/Makefile b/com/pixelmed/codec/jpeg/Makefile
index ff9a849..4de7af3 100755
--- a/com/pixelmed/codec/jpeg/Makefile
+++ b/com/pixelmed/codec/jpeg/Makefile
@@ -9,7 +9,8 @@ OBJS = \
 	MarkerSegmentSOS.class \
 	Parse.class \
 	QuantizationTable.class \
-	Utilities.class
+	Utilities.class \
+	OutputArrayOrStream.class
 
 all:	${OBJS}
 
@@ -24,12 +25,13 @@ testparse:	${OBJS}
 	rm -f /tmp/crap_copied.jpg
 	rm -f /tmp/crap_source.jpg
 	cp -v \
-		"$${HOME}/Documents/Clients/MDDX/Experiment20130905/crap.jpg" \
+		"/Volumes/Toshiba5TEnc/MDDX/20170320_Assembla2719_MissingJPEGEOI/corruptedfile147652_IM001_35.jpg" \
 		/tmp/crap_source.jpg
-	java -cp ${PATHTOROOT} com.pixelmed.codec.jpeg.Parse \
+	java -Djava.awt.headless=true  -cp ${PATHTOROOT} com.pixelmed.codec.jpeg.Parse \
 		/tmp/crap_source.jpg \
 		/tmp/crap_copied.jpg
-	@echo "Comparing source and copied ... may fail with EOF if padding after EOI marker that is not copied, which is OK"
+	# use make -i to continue to dump
+	@echo "Comparing source and copied ... may fail with EOF if padding after EOI marker that is not copied, or missing EOI marker is added, both of which are OK"
 	cmp /tmp/crap_source.jpg /tmp/crap_copied.jpg
 	@echo "Finished comparing"
 	hexdump -C /tmp/crap_source.jpg | tail -3
@@ -45,9 +47,58 @@ testparse:	${OBJS}
 # without restart and not working
 
 # with restart and working
-		#"$${HOME}/Pictures/Interesting/clunie_737_cropped_close.jpg"
+		#"$${HOME}/Pictures/Interesting/Me/clunie_737_cropped_close.jpg"
 		#"${PATHTOROOT}/${PATHTOTESTFILESFROMROOT}/smpte_8_cjpeg_rst1.jpg"
-		#"/tmp/crap.jpg"
+		#"$${HOME}/Documents/Medical/compression/JPEG/10918-2/ITU T83/T83_process1/A1.JPG"
 
 # with restart and not working
 
+# premature EOF without EOI marker working (Heartlab)
+		# dctoraw "/Volumes/Toshiba5TEnc/MDDX/20160928_CorruptionDuringMasking/dicom File.dcm" "/Volumes/Toshiba5TEnc/MDDX/20160928_CorruptionDuringMasking/dicom File.jpg"
+		# "/Volumes/Toshiba5TEnc/MDDX/20160928_CorruptionDuringMasking/dicom File.jpg"
+
+		# dctoraw "/Volumes/Toshiba5TEnc/MDDX/20170320_Assembla2719_MissingJPEGEOI/corruptedfile147652/corruptedfile147652/IM001(35)" "/Volumes/Toshiba5TEnc/MDDX/20170320_Assembla2719_MissingJPEGEOI/corruptedfile147652_IM001_35.jpg"
+		# "/Volumes/Toshiba5TEnc/MDDX/20170320_Assembla2719_MissingJPEGEOI/corruptedfile147652_IM001_35.jpg"
+
+
+testdecompress:	${OBJS}
+	rm -f /tmp/crap_source.jpg
+	rm -f /tmp/crap_decompressed*.raw
+	cp -v \
+		"$${HOME}/Documents/Medical/compression/JPEG/10918-2/ITU T83/T83_process14/O1.JPG" \
+		/tmp/crap_source.jpg
+	java -Djava.awt.headless=true  -cp ${PATHTOROOT} com.pixelmed.codec.jpeg.Parse \
+		/tmp/crap_source.jpg \
+		"" \
+		/tmp/crap_decompressed.raw
+	ls -l /tmp/crap_decompressed*.raw
+
+		#"$${HOME}/Documents/Medical/compression/JPEG/10918-2/ITU T83/T83_process14/O1.JPG" \
+		#"$${HOME}/Documents/Medical/compression/JPEG/10918-2/ITU T83/T83_process14/O2.JPG" \
+
+testdecompressfromdicom:	${OBJS}
+	rm -f /tmp/crap_source.dcm
+	rm -f /tmp/crap_source.jpg
+	rm -f /tmp/crap_decompressed*.raw
+	cp -v \
+		"$${HOME}/Pictures/Medical/JPEGVarious/z18" \
+		/tmp/crap_source.dcm
+	dctoraw \
+		/tmp/crap_source.dcm \
+		/tmp/crap_source.jpg
+	java -Djava.awt.headless=true  -cp ${PATHTOROOT} com.pixelmed.codec.jpeg.Parse \
+		/tmp/crap_source.jpg \
+		"" \
+		/tmp/crap_decompressed.raw
+	ls -l /tmp/crap_decompressed*.raw
+	rm -f /tmp/crap_source_dcunjpeg.*
+	dcunjpeg /tmp/crap_source.dcm /tmp/crap_source_dcunjpeg_little.dcm
+	dccp -endian big -vr explicit /tmp/crap_source_dcunjpeg_little.dcm /tmp/crap_source_dcunjpeg_big.dcm
+	dctoraw /tmp/crap_source_dcunjpeg_big.dcm /tmp/crap_source_dcunjpeg.raw
+	@echo "Comparing decompressed with pixelmed codec and decompressed with whatever codec dcunjpeg uses"
+	cmp /tmp/crap_decompressed.raw /tmp/crap_source_dcunjpeg.raw
+	@echo "Finished comparing"
+	hexdump -C /tmp/crap_decompressed.raw | head -3
+	hexdump -C /tmp/crap_source_dcunjpeg.raw | head -3
+
+		# "$${HOME}/Pictures/Medical/JPEGVarious/z18"
diff --git a/com/pixelmed/codec/jpeg/MarkerSegmentSOF.java b/com/pixelmed/codec/jpeg/MarkerSegmentSOF.java
index 067f668..57bff62 100644
--- a/com/pixelmed/codec/jpeg/MarkerSegmentSOF.java
+++ b/com/pixelmed/codec/jpeg/MarkerSegmentSOF.java
@@ -1,4 +1,4 @@
-/* Copyright (c) 2014, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+/* Copyright (c) 2014-2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
 
 package com.pixelmed.codec.jpeg;
 
@@ -9,8 +9,9 @@ package com.pixelmed.codec.jpeg;
  */
 public class MarkerSegmentSOF {
 
-	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/MarkerSegmentSOF.java,v 1.2 2014/03/22 09:06:13 dclunie Exp $";
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/MarkerSegmentSOF.java,v 1.4 2015/10/17 21:20:52 dclunie Exp $";
 	
+	private int  marker;
 	private int  SamplePrecision;
 	private int  nLines;
 	private int  nSamplesPerLine;
@@ -20,13 +21,19 @@ public class MarkerSegmentSOF {
 	private int[] VerticalSamplingFactor;
 	private int[] QuantizationTableDestinationSelector;
 
+	public int  getMarker() { return marker; }
+
+	public int  getSamplePrecision() { return SamplePrecision; }
 	public int  getNLines() { return nLines; }
 	public int  getNSamplesPerLine() { return nSamplesPerLine; }
+	public int  getNComponentsInFrame() { return nComponentsInFrame; }
 
 	public int[] getHorizontalSamplingFactor() { return HorizontalSamplingFactor; }
 	public int[] getVerticalSamplingFactor()   { return VerticalSamplingFactor; }
 
-	public MarkerSegmentSOF(byte[] b,int length) throws Exception {
+	public MarkerSegmentSOF(int marker,byte[] b,int length) throws Exception {
+		this.marker = marker;
+		
 		SamplePrecision    = Utilities.extract8(b,0);
 		nLines             = Utilities.extract16be(b,1);
 		nSamplesPerLine    = Utilities.extract16be(b,3);
@@ -52,7 +59,7 @@ public class MarkerSegmentSOF {
 
 	public String toString() {
 		StringBuffer buf = new StringBuffer();
-		buf.append("\n\tSOF:\n");
+		buf.append("\n\t"+Markers.getAbbreviation(marker)+":\n");
 		buf.append("\t\t SamplePrecision = "	+SamplePrecision+"\n");
 		buf.append("\t\t nLines = "             +nLines+"\n");
 		buf.append("\t\t nSamplesPerLine = "    +nSamplesPerLine+"\n");
diff --git a/com/pixelmed/codec/jpeg/MarkerSegmentSOS.java b/com/pixelmed/codec/jpeg/MarkerSegmentSOS.java
index 188a916..a8961af 100644
--- a/com/pixelmed/codec/jpeg/MarkerSegmentSOS.java
+++ b/com/pixelmed/codec/jpeg/MarkerSegmentSOS.java
@@ -1,4 +1,4 @@
-/* Copyright (c) 2014, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+/* Copyright (c) 2014-2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
 
 package com.pixelmed.codec.jpeg;
 
@@ -9,7 +9,7 @@ package com.pixelmed.codec.jpeg;
  */
 public class MarkerSegmentSOS {
 
-	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/MarkerSegmentSOS.java,v 1.2 2014/03/22 09:06:13 dclunie Exp $";
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/MarkerSegmentSOS.java,v 1.3 2015/10/17 21:20:52 dclunie Exp $";
 	
 	private int nComponentsPerScan;
 	private int[] ScanComponentSelector;
@@ -24,6 +24,8 @@ public class MarkerSegmentSOS {
 	public int   getNComponentsPerScan() { return nComponentsPerScan; }
 	public int[] getDCEntropyCodingTableSelector() { return DCEntropyCodingTableSelector; }
 	public int[] getACEntropyCodingTableSelector() { return ACEntropyCodingTableSelector; }
+	public int   getStartOfSpectralOrPredictorSelection() { return StartOfSpectralOrPredictorSelection; }
+	public int   getSuccessiveApproximationBitPositionLowOrPointTransform() { return SuccessiveApproximationBitPositionLowOrPointTransform; }
 
 	public MarkerSegmentSOS(byte[] b,int length) throws Exception {
 		nComponentsPerScan=Utilities.extract8(b,0);
@@ -61,6 +63,7 @@ public class MarkerSegmentSOS {
 		buf.append("\t\t StartOfSpectralOrPredictorSelection/NearLosslessDifferenceBound(LS) = "+StartOfSpectralOrPredictorSelection+"\n");
 		buf.append("\t\t EndOfSpectralSelection/InterleaveMode(LS) = "+EndOfSpectralSelection+"\n");
 		buf.append("\t\t SuccessiveApproximationBitPositionHigh = "+SuccessiveApproximationBitPositionHigh+"\n");
+		buf.append("\t\t SuccessiveApproximationBitPositionLowOrPointTransform = "+SuccessiveApproximationBitPositionLowOrPointTransform+"\n");
 		return buf.toString();
 	}
 
diff --git a/com/pixelmed/codec/jpeg/Markers.java b/com/pixelmed/codec/jpeg/Markers.java
index ab29947..d0826d3 100644
--- a/com/pixelmed/codec/jpeg/Markers.java
+++ b/com/pixelmed/codec/jpeg/Markers.java
@@ -1,4 +1,4 @@
-/* Copyright (c) 2014, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+/* Copyright (c) 2014-2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
 
 package com.pixelmed.codec.jpeg;
 
@@ -12,7 +12,7 @@ import java.util.Map;
  */
 public class Markers {
 
-	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/Markers.java,v 1.1 2014/03/21 15:28:07 dclunie Exp $";
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/Markers.java,v 1.4 2016/01/16 15:07:52 dclunie Exp $";
 	
 	// modified from dicom3tools appsrc/misc/jpegdump.cc ...
 
@@ -183,6 +183,80 @@ public class Markers {
 	public static final boolean isVariableLengthJPEGSegment(int marker) {
 		return !isNoLengthJPEGSegment(marker) && isFixedLengthJPEGSegment(marker) == 0;
 	}
+
+	public static final boolean isSOF(int marker) {
+		boolean isSOF;
+		switch (marker) {
+			case SOF0:
+			case SOF1:
+			case SOF2:
+			case SOF3:
+			case SOF5:
+			case SOF6:
+			case SOF7:
+			case SOF9:
+			case SOFA:
+			case SOFB:
+			case SOFD:
+			case SOFE:
+			case SOFF:
+			case SOF55:
+				isSOF=true; break;
+			default:
+				isSOF=false; break;
+		}
+		return isSOF;
+	}
+
+	public static final boolean isHuffman(int marker) {
+		boolean isHuffman;
+		switch (marker) {
+			case SOF0:
+			case SOF1:
+			case SOF2:
+			case SOF3:
+			case SOF5:
+			case SOF6:
+			case SOF7:
+				isHuffman=true; break;
+			default:
+				isHuffman=false; break;
+		}
+		return isHuffman;
+	}
+	
+	public static final boolean isDCT(int marker) {
+		boolean isDCT;
+		switch (marker) {
+			case SOF0:
+			case SOF1:
+			case SOF2:
+			case SOF5:
+			case SOF6:
+			case SOF9:
+			case SOFA:
+			case SOFD:
+			case SOFE:
+				isDCT=true; break;
+			default:
+				isDCT=false; break;
+		}
+		return isDCT;
+	}
+	
+	public static final boolean isLossless(int marker) {
+		boolean isLossless;
+		switch (marker) {
+			case SOF3:
+			case SOF7:
+			case SOFB:
+			case SOFF:
+				isLossless=true; break;
+			default:
+				isLossless=false; break;
+		}
+		return isLossless;
+	}
 	
 	private static class MarkerDictionaryEntry {
 		int markercode;
@@ -307,7 +381,6 @@ public class Markers {
 		return e == null ? "" : e.abbreviation;
 	}
 
-	
 	public static final String getDescription(int marker) {
 		MarkerDictionaryEntry e = mapOfMarkerToDictionaryEntry.get(new Integer(marker));
 		return e == null ? "" : e.description;
diff --git a/com/pixelmed/codec/jpeg/OutputArrayOrStream.java b/com/pixelmed/codec/jpeg/OutputArrayOrStream.java
new file mode 100644
index 0000000..8bcabc5
--- /dev/null
+++ b/com/pixelmed/codec/jpeg/OutputArrayOrStream.java
@@ -0,0 +1,172 @@
+/* Copyright (c) 2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+
+package com.pixelmed.codec.jpeg;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import java.nio.ByteOrder;
+
+/**
+ * <p>A class that allows writing to either an {@link java.io.OutputStream OutputStream}
+ * or a byte[] or short[] of preallocated size.</p>
+ *
+ * <p>An unallocated instance may be constructed but any attempt to write to it will
+ * fail until either an OutputStream is assigned or an array of the appropriate type
+ * is allocated. This allows, for example, the instance to be created and later
+ * allocated based on size information, e.g., as header information is encountered
+ * while decompressing before decompressed pixel values need to be written.</p>
+ *
+ * @author	dclunie
+ */
+public class OutputArrayOrStream {
+
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/OutputArrayOrStream.java,v 1.5 2016/01/16 13:30:09 dclunie Exp $";
+	
+	protected OutputStream out = null;
+	protected ByteOrder order = null;
+	protected byte[] byteValues = null;
+	protected short[] shortValues = null;
+	protected int byteOffset = 0;
+	protected int shortOffset = 0;
+	
+	public OutputArrayOrStream() {
+		// lazy allocation
+	}
+	
+	public OutputArrayOrStream(OutputStream out,ByteOrder order) {
+		this.out = out;
+		this.order = order;
+	}
+	
+	public OutputArrayOrStream(byte[] byteValues) {
+		this.byteValues = byteValues;
+		byteOffset = 0;
+	}
+	
+	public OutputArrayOrStream(short[] shortValues) {
+		this.shortValues = shortValues;
+		shortOffset = 0;
+	}
+	
+	public void setOutputStream(OutputStream out,ByteOrder order) throws IOException {
+		if (this.out != null || this.byteValues != null || this.shortValues != null) {
+			throw new IOException("Destination already allocated");
+		}
+		this.out = out;
+		this.order = order;
+	}
+	
+	/**
+	 * <p>Retrieves the OutputStream's byte order used when writing short values.</p>
+	 *
+	 * @return	The OutputStream's byte order, or null if no OutputStream
+	 */
+	public ByteOrder order() {
+		return out == null ? null : order;
+	}
+	
+	/**
+	 * <p>Modifes the OutputStream's byte order used when writing short values.</p>
+	 *
+	 * @param	order		the new byte order, either BIG_ENDIAN or LITTLE_ENDIAN
+	 * @throws	IOException	if no OutputStream assigned
+	 */
+	public void order(ByteOrder order) throws IOException {
+		if (out == null) {
+			throw new IOException("Cannot assign byte order if no OutputStream");
+		}
+		this.order = order;
+	}
+	
+	public void allocateByteArray(int length) throws IOException {
+		if (this.out != null || this.byteValues != null || this.shortValues != null) {
+			throw new IOException("Destination already allocated");
+		}
+		this.byteValues = new byte[length];
+		byteOffset = 0;
+	}
+	
+	public void allocateShortArray(int length) throws IOException {
+		if (this.out != null || this.byteValues != null || this.shortValues != null) {
+			throw new IOException("Destination already allocated");
+		}
+		this.shortValues = new short[length];
+		shortOffset = 0;
+	}
+	
+	public OutputStream getOutputStream() {
+		return out;
+	}
+	
+	public byte[] getByteArray() {
+		return byteValues;
+	}
+	
+	public short[] getShortArray() {
+		return shortValues;
+	}
+
+    /**
+     * Writes the specified <code>byte</code> to this output.
+     *
+     * @param      b   the <code>byte</code>.
+     * @throws  IOException  if an I/O error occurs.
+     */
+    public void writeByte(int b) throws IOException {
+		if (out != null) {
+			out.write(b);
+		}
+		else if (byteValues != null) {
+			byteValues[byteOffset++] = (byte)b;
+		}
+		else if (shortValues != null) {
+			throw new IOException("Cannot write byte value to short array");
+		}
+		else {
+			throw new IOException("Byte array not allocated yet");
+		}
+    }
+
+    /**
+     * Writes the specified <code>short</code> to this output.
+     *
+     * @param      s   the <code>short</code>.
+     * @throws  IOException  if an I/O error occurs.
+     */
+    public void writeShort(int s) throws IOException {
+		if (out != null) {
+			if (order == ByteOrder.LITTLE_ENDIAN) {
+				out.write(s);
+				out.write(s>>8);
+			}
+			else {
+				out.write(s>>8);
+				out.write(s);
+			}
+		}
+		else if (shortValues != null) {
+			shortValues[shortOffset++] = (short)s;
+		}
+		else if (byteValues != null) {
+			throw new IOException("Cannot write short value to byte array");
+		}
+		else {
+			throw new IOException("Short array not allocated yet");
+		}
+    }
+	
+    /**
+     * <p>Closes any assigned OutputStream.</p>
+     *
+     * <p>Does nothing if arrays allocated instead of an OutputStream (i.e., does NOT release them).</p>
+     *
+     * @throws  IOException  if an I/O error occurs.
+     */
+	public void close() throws IOException {
+		if (out != null) {
+			out.close();
+		}
+	}
+
+}
diff --git a/com/pixelmed/codec/jpeg/Parse.java b/com/pixelmed/codec/jpeg/Parse.java
index 54e0171..8267b98 100644
--- a/com/pixelmed/codec/jpeg/Parse.java
+++ b/com/pixelmed/codec/jpeg/Parse.java
@@ -1,4 +1,4 @@
-/* Copyright (c) 2014, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+/* Copyright (c) 2014-2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
 
 package com.pixelmed.codec.jpeg;
 
@@ -6,12 +6,15 @@ import java.awt.Rectangle;
 import java.awt.Shape;
 
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 
+import java.nio.ByteOrder;
+
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Vector;
@@ -21,13 +24,15 @@ import java.util.Vector;
  *
  * <p>Includes the ability to selectively redact blocks and leave other blocks alone, to permit "lossless" redaction.</p>
  *
+ * <p>Includes the ability to decompress lossless JPEG.</p>
+ *
  * <p>Development of this class was supported by funding from MDDX Research and Informatics.</p>
  *
  * @author	dclunie
  */
 public class Parse {
 
-	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/Parse.java,v 1.6 2014/03/29 21:58:58 dclunie Exp $";
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/codec/jpeg/Parse.java,v 1.18 2017/03/21 17:42:24 dclunie Exp $";
 	
 	private static int getLargestSamplingFactor(int[] factors) {
 		int largest = 0;
@@ -50,21 +55,122 @@ public class Parse {
 		writeMarkerAndLength(out,marker,length);
 		out.write(b,0,length-2);
 	}
+
+	public static class DecompressedOutput {
+		private int nComponents;
+		private OutputArrayOrStream[] decompressedOutputPerComponent;
+		private File fileBasis;
+		private ByteOrder order;
+		
+		public DecompressedOutput() {
+		}
+		
+		/*
+		 * @param	fileBasis	will be used literally if one component, with an appended suffix _n before the file extension (if any), where n is the component number from 0
+		 */
+		public DecompressedOutput(File fileBasis,ByteOrder order) {
+			this.fileBasis = fileBasis;
+			this.order = order;
+		}
+		
+		public OutputArrayOrStream[] getDecompressedOutputPerComponent() { return decompressedOutputPerComponent; }
+		
+		public void configureDecompressedOutput(MarkerSegmentSOF sof) throws IOException {
+			nComponents = sof.getNComponentsInFrame();
+			decompressedOutputPerComponent = new OutputArrayOrStream[nComponents];
+			if (fileBasis == null) {
+				int length = sof.getNSamplesPerLine() * sof.getNLines();
+				for (int c=0; c<nComponents; ++c) {
+					decompressedOutputPerComponent[c] = new OutputArrayOrStream();
+					if (sof.getSamplePrecision() <= 8) {
+						decompressedOutputPerComponent[c].allocateByteArray(length);
+					}
+					else {
+						decompressedOutputPerComponent[c].allocateShortArray(length);
+					}
+				}
+			}
+			else {
+				if (nComponents == 1) {
+					decompressedOutputPerComponent[0] = new OutputArrayOrStream(new FileOutputStream(fileBasis),order);
+				}
+				else {
+					File parent = fileBasis.getParentFile();	// may be null
+					String baseFileName = fileBasis.getName();
+					String prefix;
+					String suffix;
+					int periodPosition = baseFileName.lastIndexOf('.');
+					if (periodPosition > -1) {
+						if (periodPosition > 0) {
+							prefix = baseFileName.substring(0,periodPosition);		// copies from 0 to periodPosition-1
+						}
+						else {
+							prefix = "";
+						}
+						suffix = baseFileName.substring(periodPosition);			// copies the period to the end
+					}
+					else {
+						prefix = baseFileName;
+						suffix = "";
+					}
+					for (int c=0; c<nComponents; ++c) {
+						String componentFileName = prefix + c + suffix;
+//System.err.println("Parse.DecompressedOutput.configureDecompressedOutput(): componentFileName["+c+"] = "+componentFileName);
+						decompressedOutputPerComponent[c] = new OutputArrayOrStream(new FileOutputStream(new File(parent,componentFileName)),order);	// OK if parent is null
+					}
+				}
+			}
+		}
+		
+		public void close() throws IOException {
+			for (int c=0; c<nComponents; ++c) {
+				decompressedOutputPerComponent[c].close();
+			}
+		}
+
+	}
+
+	public static class MarkerSegmentsFoundDuringParse {
+		private MarkerSegmentSOS sos;
+		private MarkerSegmentSOF sof;
+		private Map<String,HuffmanTable> htByClassAndIdentifer;
+		private Map<String,QuantizationTable> qtByIdentifer;
+		
+		public MarkerSegmentSOS getSOS() { return sos; }
+		public MarkerSegmentSOF getSOF() { return sof; }
+		public Map<String,HuffmanTable>  getHuffmanTableByClassAndIdentifer() { return htByClassAndIdentifer; }
+		public Map<String,QuantizationTable>  getQuantizationTableByIdentifer() { return qtByIdentifer; }
+		
+		public MarkerSegmentsFoundDuringParse(MarkerSegmentSOS sos,MarkerSegmentSOF sof,Map<String,HuffmanTable> htByClassAndIdentifer,Map<String,QuantizationTable> qtByIdentifer) {
+			this.sos = sos;
+			this.sof = sof;
+			this.htByClassAndIdentifer = htByClassAndIdentifer;
+			this.qtByIdentifer = qtByIdentifer;
+		}
+	}
 	
 	// follows pattern of dicom3tools appsrc/misc/jpegdump.cc
 	
 	/**
-	 * <p>Parse a JPEG bitstream and copying to the output redacting any blocks that intersect with the specified locations.</p>
+	 * <p>Parse a JPEG bitstream and either copy to the output redacting any blocks that intersect with the specified locations, or decompress.</p>
+	 *
+	 * <p>Parsing and redaction is implemented only for baseline (8 bit DCT Huffman).</p>
+	 *
+	 * <p>Parsing and decompression is implemented only for lossless sequential Huffman.</p>
 	 *
-	 * @param	in				the input JPEG bitstream
-	 * @param	out				the output JPEG bitsream, redacted as specified
-	 * @param	redactionShapes	a Vector of Shape that are Rectangle
+	 * @param	in							the input JPEG bitstream
+	 * @param	copiedRedactedOutputStream	the output JPEG bitstream, redacted as specified
+	 * @param	redactionShapes				a Vector of Shape that are Rectangle
+	 * @param	decompressedOutput			the decompressed output (with specified or default endianness if precision > 8)
+	 * @return								the marker segments found during parsing
 	 * @exception Exception			if bad things happen parsing the JPEG bit stream, caused by malformed input
 	 * @exception IOException		if bad things happen reading or writing
 	 */
-	public static void parse(InputStream in,OutputStream out,Vector<Shape> redactionShapes) throws Exception, IOException {
-		boolean dumping = false;
-		boolean copying = out != null;
+	public static MarkerSegmentsFoundDuringParse parse(InputStream in,OutputStream copiedRedactedOutputStream,Vector<Shape> redactionShapes,DecompressedOutput decompressedOutput) throws Exception, IOException {
+		boolean dumping = copiedRedactedOutputStream == null && decompressedOutput == null;
+		//boolean dumping = true;
+		boolean copying = copiedRedactedOutputStream != null;
+		boolean decompressing = decompressedOutput != null;
 	
 		EntropyCodedSegment ecs = null;					// lazy instantiation of EntropyCodedSegment ... wait until we have relevant marker segments for its constructor
 		
@@ -78,81 +184,108 @@ public class Parse {
 		
 		int mcuOffset = 0;
 		int nMCUHorizontally = 0;
+		int nMCUVertically = 0;
 		int mcuCountPerEntropyCodedSegment = 0;
 
 		int offset=0;
 		int markerprefix = in.read();
 		while (true) {
+			int marker=0;	// will be overwritten by what we read, unless we have premature EOF, in which case this will not be used
+			boolean sawEOF = false;
 			if (markerprefix == -1) {
 				if (dumping) System.err.print("End of file\n");
-				break;
+				sawEOF=true;
 			}
-			if (markerprefix != 0xff) {		// byte of entropy-coded segment
-				if (byteAccumulator == null) {
-					if (dumping) System.err.print("Offset "+Utilities.toPaddedHexString(offset,4)+" Starting new Entropy Coded Segment\n");
-					byteAccumulator = new ByteArrayOutputStream();
+			else {
+				if (markerprefix != 0xff) {		// byte of entropy-coded segment
+					if (byteAccumulator == null) {
+						if (dumping) System.err.print("Offset "+Utilities.toPaddedHexString(offset,4)+" Starting new Entropy Coded Segment\n");
+						byteAccumulator = new ByteArrayOutputStream();
+					}
+					byteAccumulator.write(markerprefix);
+					++offset;
+					markerprefix=in.read();
+					continue;
 				}
-				byteAccumulator.write(markerprefix);
-				++offset;
-				markerprefix=in.read();
-				continue;
-			}
-			int marker=in.read();
-			if (marker == -1) {
-				if (dumping) System.err.print("End of file immediately after marker flag 0xff ... presumably was padding\n");
-				break;
-			}
-			else if (marker == 0xff) {		// 0xff byte of padding
-				if (dumping) System.err.print("Offset "+Utilities.toPaddedHexString(offset,4)+" Fill byte 0xff\n");
-				++offset;
-				markerprefix=marker;		// the first 0xff is padding, the 2nd may be the start of a marker
-				continue;
-			}
-			// ignore doing_jpeg2k_tilepart for now :(
-			else if (marker == 0) {			// 0xff byte of entropy-coded segment ... ignore following zero byte
-				if (dumping) System.err.print("Offset "+Utilities.toPaddedHexString(offset,4)+" Encoded 0xff in entropy-coded segment followed by stuffed zero byte\n");
-				if (byteAccumulator == null) {
-					if (dumping) System.err.print("Offset "+Utilities.toPaddedHexString(offset,4)+" Starting new Entropy Coded Segment\n");
-					byteAccumulator = new ByteArrayOutputStream();
+				marker=in.read();
+				if (marker == -1) {
+					if (dumping) System.err.print("End of file immediately after marker flag 0xff ... presumably was padding\n");
+					sawEOF=true;
+				}
+				else if (marker == 0xff) {		// 0xff byte of padding
+					if (dumping) System.err.print("Offset "+Utilities.toPaddedHexString(offset,4)+" Fill byte 0xff\n");
+					++offset;
+					markerprefix=marker;		// the first 0xff is padding, the 2nd may be the start of a marker
+					continue;
 				}
-				byteAccumulator.write(markerprefix);
-				markerprefix=in.read();
-				offset+=2;
-				continue;
+				// ignore doing_jpeg2k_tilepart for now :(
+				else if (marker == 0) {			// 0xff byte of entropy-coded segment ... ignore following zero byte
+					if (dumping) System.err.print("Offset "+Utilities.toPaddedHexString(offset,4)+" Encoded 0xff in entropy-coded segment followed by stuffed zero byte\n");
+					if (byteAccumulator == null) {
+						if (dumping) System.err.print("Offset "+Utilities.toPaddedHexString(offset,4)+" Starting new Entropy Coded Segment\n");
+						byteAccumulator = new ByteArrayOutputStream();
+					}
+					byteAccumulator.write(markerprefix);
+					markerprefix=in.read();
+					offset+=2;
+					continue;
+				}
+				// ignore doing_jpegls and zero stuffed bit instead of byte for now :(
 			}
-			// ignore doing_jpegls and zero stuffed bit instead of byte for now :(
-
-			// Definitely have a marker ...
+			
+			// Definitely have a marker or EOF ...
 			
 			if (byteAccumulator != null) {
 				// process any Entropy Coded Segment bytes accumulated so far ...
 				if (ecs == null) {
-					ecs = new EntropyCodedSegment(restartinterval,sos,sof,htByClassAndIdentifer,qtByIdentifer,copying,dumping);
-					
 					// need to figure out the sampling factors if this is the first Entropy Coded Segment, so that EntropyCodedSegment.finish() knows how many to process and where it is at
 					
-					int horizontalSamplesPerMCU = 8 * getLargestSamplingFactor(sof.getHorizontalSamplingFactor());
+					if (sof == null) {
+						throw new Exception("Error - compressed data without preceding SOF marker segment");
+					}
+					
+					int blockSize = Markers.isDCT(sof.getMarker()) ? 8 : 1;
+					
+					int horizontalSamplesPerMCU = blockSize * getLargestSamplingFactor(sof.getHorizontalSamplingFactor());
 //System.err.println("horizontalSamplesPerMCU "+horizontalSamplesPerMCU);
 					nMCUHorizontally = (sof.getNSamplesPerLine()-1)/horizontalSamplesPerMCU + 1;
 //System.err.println("nMCUHorizontally "+nMCUHorizontally);
 		
-					int verticalSamplesPerMCU = 8 * getLargestSamplingFactor(sof.getVerticalSamplingFactor());
+					int verticalSamplesPerMCU = blockSize * getLargestSamplingFactor(sof.getVerticalSamplingFactor());
 //System.err.println("verticalSamplesPerMCU "+verticalSamplesPerMCU);
-					int nMCUVertically = (sof.getNLines()-1)/verticalSamplesPerMCU + 1;					// may need to update this from DNL marker :(
+					nMCUVertically = (sof.getNLines()-1)/verticalSamplesPerMCU + 1;					// may need to update this from DNL marker :(
 //System.err.println("nMCUVertically "+nMCUVertically);
 		
+//System.err.println("restartinterval "+restartinterval);
 					mcuCountPerEntropyCodedSegment = (restartinterval == 0) ? nMCUHorizontally * nMCUVertically : restartinterval;
+//System.err.println("mcuCountPerEntropyCodedSegment "+mcuCountPerEntropyCodedSegment);
 					mcuOffset = 0;
+
+					ecs = new EntropyCodedSegment(sos,sof,htByClassAndIdentifer,qtByIdentifer,nMCUHorizontally,redactionShapes,copying,dumping,decompressing,decompressedOutput);
 				}
 				byte[] bytesToDecompress = byteAccumulator.toByteArray();
-//System.err.println("bytesToDecompress length="+bytesToDecompress.length);
-				byte[] bytesToCopy = ecs.finish(bytesToDecompress,mcuCountPerEntropyCodedSegment,nMCUHorizontally,mcuOffset,redactionShapes);
+//System.err.println("bytesToDecompress length "+bytesToDecompress.length);
+//System.err.println("mcuOffset "+mcuOffset);
+				int mcuStillNeeded = (nMCUHorizontally * nMCUVertically) - mcuOffset;
+//System.err.println("mcuStillNeeded "+mcuStillNeeded);
+				int mcuNeededThisInterval = mcuCountPerEntropyCodedSegment > mcuStillNeeded ? mcuStillNeeded : mcuCountPerEntropyCodedSegment;		// Do NOT attempt to read beyond what is needed
+//System.err.println("mcuNeededThisInterval "+mcuNeededThisInterval);
+				byte[] bytesToCopy = ecs.finish(bytesToDecompress,mcuNeededThisInterval,mcuOffset);
 				if (copying) {
-					out.write(bytesToCopy);		// NB. EntropyCodedSegment.finish() has already done the zero byte stuffing after 0xff values
+					copiedRedactedOutputStream.write(bytesToCopy);		// NB. EntropyCodedSegment.finish() has already done the zero byte stuffing after 0xff values
 				}
 				byteAccumulator = null;
 				mcuOffset += mcuCountPerEntropyCodedSegment;
 			}
+			
+			if (sawEOF) {
+				// would have stopped already if we saw an EOI, so can assume it was missing
+				if (copying) {
+//System.err.println("inserting missing EOI because premature EOF");
+					copiedRedactedOutputStream.write(0xff); copiedRedactedOutputStream.write(Markers.EOI);
+				}
+				break;
+			}
 
 			marker|=0xff00;			// convention is to express them with the leading ff, so that is what we look up
 			
@@ -181,7 +314,7 @@ public class Parse {
 							case Markers.SOS:
 								sos = new MarkerSegmentSOS(b,length-2);
 								if (dumping) System.err.print(sos);
-								if (copying) writeVariableLengthMarkerSegment(out,marker,length,b);
+								if (copying) writeVariableLengthMarkerSegment(copiedRedactedOutputStream,marker,length,b);
 								break;
 							case Markers.SOF0:
 							case Markers.SOF1:
@@ -197,21 +330,22 @@ public class Parse {
 							case Markers.SOFE:
 							case Markers.SOFF:
 							case Markers.SOF55:
-								sof = new MarkerSegmentSOF(b,length-2);
+								sof = new MarkerSegmentSOF(marker,b,length-2);
 								if (dumping) System.err.print(sof);
-								if (copying) writeVariableLengthMarkerSegment(out,marker,length,b);
+								if (copying) writeVariableLengthMarkerSegment(copiedRedactedOutputStream,marker,length,b);
+								if (decompressing) decompressedOutput.configureDecompressedOutput(sof);
 								break;
 							case Markers.DHT:
 								MarkerSegmentDHT dht = new MarkerSegmentDHT(b,length-2);
 								dht.addToMapByClassAndIdentifier(htByClassAndIdentifer);	// hokey, but sometimes multiple tables in one segment, sometimes multiple segments
 								if (dumping) System.err.print(dht);
-								if (copying) writeVariableLengthMarkerSegment(out,marker,length,b);
+								if (copying) writeVariableLengthMarkerSegment(copiedRedactedOutputStream,marker,length,b);
 								break;
 							case Markers.DQT:
 								MarkerSegmentDQT dqt = new MarkerSegmentDQT(b,length-2);
 								dqt.addToMapByIdentifier(qtByIdentifer);					// hokey, but sometimes multiple tables in one segment, sometimes multiple segments
 								if (dumping) System.err.print(dqt);
-								if (copying) writeVariableLengthMarkerSegment(out,marker,length,b);
+								if (copying) writeVariableLengthMarkerSegment(copiedRedactedOutputStream,marker,length,b);
 								break;
 							//case Markers.LSE
 							//	break;
@@ -229,7 +363,7 @@ public class Parse {
 									throw new Exception("Illegal length "+length+" of restart interval at Offset "+Utilities.toPaddedHexString(offset,4));
 								}
 								if (dumping) System.err.print("\n\tDRI - Define Restart Interval = "+Utilities.toPaddedHexString(restartinterval,4)+"\n");
-								if (copying) writeVariableLengthMarkerSegment(out,marker,length,b);
+								if (copying) writeVariableLengthMarkerSegment(copiedRedactedOutputStream,marker,length,b);
 								break;
 							case Markers.DNL:
 								long numberoflines;
@@ -246,7 +380,7 @@ public class Parse {
 									throw new Exception("Illegal length "+length+" of number of lines at Offset "+Utilities.toPaddedHexString(offset,4));
 								}
 								if (dumping) System.err.print("\n\tDNL - Define Number of Lines = "+Utilities.toPaddedHexString(numberoflines,4)+"\n");
-								if (copying) writeVariableLengthMarkerSegment(out,marker,length,b);
+								if (copying) writeVariableLengthMarkerSegment(copiedRedactedOutputStream,marker,length,b);
 								break;
 							//case Markers.COD:
 							//	break;
@@ -267,14 +401,14 @@ public class Parse {
 								if (dumping) System.err.print(magic);
 								if (marker == Markers.APP0 && magic.equals("JFIF")) {
 									if (dumping) System.err.print(new MarkerSegmentAPP0JFIF(b,length-2));
-									//if (copying) writeVariableLengthMarkerSegment(out,marker,length,b);
+									//if (copying) writeVariableLengthMarkerSegment(copiedRedactedOutputStream,marker,length,b);
 								}
 								// may want to consider not copying unrecognized APPn segments ... may leak identity ... copy everything for now :(
-								if (copying) writeVariableLengthMarkerSegment(out,marker,length,b);
+								if (copying) writeVariableLengthMarkerSegment(copiedRedactedOutputStream,marker,length,b);
 								break;
 							default:
 								// may want to consider not copying unrecognized segments ... may leak identity ... copy everything for now :(
-								if (copying) writeVariableLengthMarkerSegment(out,marker,length,b);
+								if (copying) writeVariableLengthMarkerSegment(copiedRedactedOutputStream,marker,length,b);
 								break;
 						}
 					}
@@ -285,7 +419,7 @@ public class Parse {
 				offset+=(length-2);
 			}
 			else if (Markers.isNoLengthJPEGSegment(marker)) {
-				if (copying) { out.write(0xff); out.write(marker&0xff);}
+				if (copying) { copiedRedactedOutputStream.write(0xff); copiedRedactedOutputStream.write(marker&0xff);}
 				if (marker == Markers.EOI) {
 					// stop rather than process padding to end of file, so as not to create spurious empty EntropyCodedSegment
 					if (dumping) System.err.print("\n");
@@ -303,7 +437,7 @@ public class Parse {
 							if (value != -1) {
 								offset+=1;
 								if (dumping) System.err.print("length fixed 3 value "+Utilities.toPaddedHexString(value,2)+" ");
-								if (copying) { writeMarkerAndLength(out,marker,length); out.write(value&0xff); }
+								if (copying) { writeMarkerAndLength(copiedRedactedOutputStream,marker,length); copiedRedactedOutputStream.write(value&0xff); }
 							}
 							else {
 								throw new Exception("Error - fixed length 3 marker without value at Offset "+Utilities.toPaddedHexString(offset,4));
@@ -316,7 +450,7 @@ public class Parse {
 							if (value != -1) {
 								offset+=2;
 								if (dumping) System.err.print("length fixed 4 value "+Utilities.toPaddedHexString(value,2)+" ");
-								if (copying) { writeMarkerAndLength(out,marker,length); out.write((value>>>8)&0xff); out.write(value&0xff); }
+								if (copying) { writeMarkerAndLength(copiedRedactedOutputStream,marker,length); copiedRedactedOutputStream.write((value>>>8)&0xff); copiedRedactedOutputStream.write(value&0xff); }
 							}
 							else {
 								throw new Exception("Error - fixed length 4 marker without value at Offset "+Utilities.toPaddedHexString(offset,4));
@@ -332,19 +466,47 @@ public class Parse {
 			if (dumping) System.err.print("\n");
 			markerprefix=in.read();
 		}
+		
+		if (copying) {
+			copiedRedactedOutputStream.close();
+		}
+		if (decompressing) {
+			decompressedOutput.close();
+		}
+		return new MarkerSegmentsFoundDuringParse(sos,sof,htByClassAndIdentifer,qtByIdentifer);
+	}
+
+	/**
+	 * <p>Parse a JPEG bitstream and copying to the output redacting any blocks that intersect with the specified locations.</p>
+	 *
+	 * <p>Parsing and redaction is implemented only for baseline (8 bit DCT Huffman).</p>
+	 *
+	 * @param	in							the input JPEG bitstream
+	 * @param	copiedRedactedOutputStream	the output JPEG bitstream, redacted as specified
+	 * @param	redactionShapes				a Vector of Shape that are Rectangle
+	 * @return								the marker segments found during parsing
+	 * @exception Exception			if bad things happen parsing the JPEG bit stream, caused by malformed input
+	 * @exception IOException		if bad things happen reading or writing
+	 */
+	public static MarkerSegmentsFoundDuringParse parse(InputStream in,OutputStream copiedRedactedOutputStream,Vector<Shape> redactionShapes) throws Exception, IOException {
+		return parse(in,copiedRedactedOutputStream,redactionShapes,null/*decompressedOutput*/);
 	}
-	
 	/**
 	 * <p>Test utility to read and write a JPEG file to check parsing is sound.</p>
 	 *
-	 * @param	arg	two parameters, the input file and the output file
+	 * <p>If only an input file is supplied, will dump rather than copy.</p>
+	 *
+	 * <p>If a decompressed output file is supplied, will write in big endian if precision greater than 8, and will appended component number before file extension iff more than one component.</p>
+	 *
+	 * @param	arg	two or three parameters, the input file, the copied compressed output file, and the decompressed output file
 	 */
 	public static void main(String arg[]) {
 		try {
 			InputStream in = new FileInputStream(arg[0]);
-			OutputStream out = arg.length > 1 ? new FileOutputStream(arg[1]) : null;
+			OutputStream copiedCompressedOutput = arg.length > 1 && arg[1].length() > 0 ? new FileOutputStream(arg[1]) : null;
+			DecompressedOutput decompressedOutput = arg.length > 2 && arg[2].length() > 0 ? new DecompressedOutput(new File(arg[2]),ByteOrder.BIG_ENDIAN) : null;
 			long startTime = System.currentTimeMillis();
-			parse(in,out,null);
+			parse(in,copiedCompressedOutput,null,decompressedOutput);
 			long currentTime = System.currentTimeMillis();
 			long runTime = currentTime-startTime;
 System.err.println("Took = "+runTime+" ms");
diff --git a/com/pixelmed/codec/jpeg/package.html b/com/pixelmed/codec/jpeg/package.html
index 3572859..9a8c168 100755
--- a/com/pixelmed/codec/jpeg/package.html
+++ b/com/pixelmed/codec/jpeg/package.html
@@ -5,7 +5,7 @@
 
   @(#)package.html	1.60 98/01/27
 
-  Copyright (c) 2001-2014, David A. Clunie DBA Pixelmed Publishing. All rights reserved.
+  Copyright (c) 2001-2016, David A. Clunie DBA Pixelmed Publishing. All rights reserved.
 
 -->
 </head>
diff --git a/com/pixelmed/imageio/JPEGLosslessImageReader.java b/com/pixelmed/imageio/JPEGLosslessImageReader.java
new file mode 100644
index 0000000..2ea9837
--- /dev/null
+++ b/com/pixelmed/imageio/JPEGLosslessImageReader.java
@@ -0,0 +1,329 @@
+/* Copyright (c) 2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+
+package com.pixelmed.imageio;
+
+// follow the pattern described in "http://docs.oracle.com/javase/1.5.0/docs/guide/imageio/spec/extending.fm3.html"
+
+import com.pixelmed.codec.jpeg.MarkerSegmentSOF;
+import com.pixelmed.codec.jpeg.OutputArrayOrStream;
+import com.pixelmed.codec.jpeg.Parse;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+import java.nio.ByteOrder;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import java.awt.Point;
+import java.awt.Transparency;
+
+import java.awt.color.ColorSpace;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.ComponentColorModel;
+import java.awt.image.ComponentSampleModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.awt.image.DataBufferUShort;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+
+import javax.imageio.ImageReader;
+import javax.imageio.IIOException;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.stream.ImageInputStream;
+
+public class JPEGLosslessImageReader extends ImageReader {
+
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/imageio/JPEGLosslessImageReader.java,v 1.8 2015/10/19 15:34:42 dclunie Exp $";
+
+	ImageInputStream stream = null;
+	
+	int width;
+	int height;
+	int bitDepth;
+	
+	Parse.DecompressedOutput decompressedOutput = null;
+	
+	boolean gotEverything = false;
+
+	public JPEGLosslessImageReader(ImageReaderSpi originatingProvider) {
+		super(originatingProvider);
+	}
+	
+	public void reset() {
+System.err.println("reset()");
+		super.reset();
+		stream = null;
+		gotEverything = false;
+		decompressedOutput = null;
+	}
+
+	public void setInput(Object input, boolean isStreamable,boolean ignoreMetadata) {	// contrary to docs, need to override three argument method
+//System.err.println("JPEGLosslessImageReader.setInput("+input+","+isStreamable+"/*isStreamable*/,"+ignoreMetadata+"/*ignoreMetadata*/)");
+		super.setInput(input,isStreamable,ignoreMetadata);
+		if (input == null) {
+			this.stream = null;
+			return;
+		}
+		if (input instanceof ImageInputStream) {
+			this.stream = (ImageInputStream)input;
+		}
+		else {
+			throw new IllegalArgumentException("bad input");
+		}
+		// just in case we don't call reset() before reusing reader ...
+		gotEverything = false;
+		decompressedOutput = null;
+	}
+
+	public int getNumImages(boolean allowSearch) throws IIOException {
+		return 1; // format can only encode a single image
+	}
+
+	private void checkIndex(int imageIndex) {
+		// format can only encode a single image
+		if (imageIndex != 0) {
+			throw new IndexOutOfBoundsException("bad index");
+		}
+	}
+
+	public int getWidth(int imageIndex) throws IIOException {
+		checkIndex(imageIndex); // will throw an exception if != 0
+		readEverything();
+		return width;
+	}
+
+	public int getHeight(int imageIndex) throws IIOException {
+		checkIndex(imageIndex); // will throw an exception if != 0
+		readEverything();
+		return height;
+	}
+
+	public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IIOException {
+		checkIndex(imageIndex);
+		readEverything();
+		
+		ImageTypeSpecifier imageType = null;
+		List l = new ArrayList<ImageTypeSpecifier>();
+		imageType = ImageTypeSpecifier.createGrayscale(
+			bitDepth,
+			bitDepth <= 8 ? DataBuffer.TYPE_BYTE : DataBuffer.TYPE_USHORT,
+			false/*isSigned*/);	// have no way to determine from the JPEG lossless bitstream if signed or not
+		l.add(imageType);
+		return l.iterator();
+	}
+	
+	private final class WrapImageInputStreamAsInputStream extends InputStream {
+		private final ImageInputStream iis;
+		
+		private WrapImageInputStreamAsInputStream() {
+			iis = null;
+		}
+		
+		public WrapImageInputStreamAsInputStream(ImageInputStream iis) {
+			this.iis = iis;
+		}
+		
+		public final int available() { return 0; }	// no such method in ImageInputStream
+		
+		public final void close() throws IOException { iis.close(); }
+		
+		public final void mark(int readlimit) { iis.mark(); }		// ImageInputStream has no readlimit
+		
+		public final boolean markSupported() { return true; }		// always supported
+		
+		public final int read() throws IOException { return iis.read(); }
+		
+		public final int read(byte[] b) throws IOException { return iis.read(b); }
+		
+		public final int read(byte[] b, int off, int len) throws IOException { return iis.read(b,off,len); }
+		
+		public final void reset() throws IOException { iis.reset(); }
+		
+		public final long skip(long n) throws IOException { return iis.skipBytes(n); }
+	}
+	
+	public void readEverything() throws IIOException {
+		if (gotEverything) {
+			return;
+		}
+		gotEverything = true;
+		
+		if (stream == null) {
+			throw new IllegalStateException("No input stream");
+		}
+		decompressedOutput = new Parse.DecompressedOutput();		// allocation to byte or short, and setting of correct size, will be done by com.pixelmed.codec.jpeg.Parse
+		try {
+			Parse.MarkerSegmentsFoundDuringParse markerSegments = Parse.parse(new WrapImageInputStreamAsInputStream(stream),null,null,decompressedOutput);
+			MarkerSegmentSOF sof = markerSegments != null ? markerSegments.getSOF() : null;
+			if (sof != null) {
+				if (sof.getNComponentsInFrame() != 1 && sof.getNComponentsInFrame() != 3) {
+					throw new IIOException("Error reading JPEG stream - only single component (grayscale) or three component supported)");
+				}
+				width = sof.getNSamplesPerLine();
+				height = sof.getNLines();
+				bitDepth = sof.getSamplePrecision();
+			}
+			else {
+				throw new IIOException("Error reading JPEG stream - no SOS or SOF marker segment parsed");
+			}
+		}
+		catch (Exception e) {
+			throw new IIOException("Error reading JPEG stream",e);
+		}
+	}
+
+	public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
+		checkIndex(imageIndex);
+		readEverything();
+		
+		BufferedImage image = null;
+		
+		OutputArrayOrStream[] decompressedOutputPerComponent = decompressedOutput.getDecompressedOutputPerComponent();
+		
+		ComponentColorModel cm = null;
+		ComponentSampleModel sm = null;
+		DataBuffer buf = null;
+		if (decompressedOutputPerComponent.length == 1) {
+			if (bitDepth <= 8) {
+				// copied from com.pixelmed.display.SourceImage.createByteGrayscaleImage() ...
+				cm=new ComponentColorModel(
+										   ColorSpace.getInstance(ColorSpace.CS_GRAY),
+										   new int[] {8},
+										   false,		// has alpha
+										   false,		// alpha premultipled
+										   Transparency.OPAQUE,
+										   DataBuffer.TYPE_BYTE
+										   );
+				sm = new ComponentSampleModel(
+											  DataBuffer.TYPE_BYTE,
+											  width,
+											  height,
+											  1,
+											  width,
+											  new int[] {0}
+											  );
+				buf = new DataBufferByte(decompressedOutputPerComponent[0].getByteArray(),width,0);
+			}
+			else {
+				// copied from com.pixelmed.display.SourceImage.createUnsignedShortGrayscaleImage() ...
+				cm=new ComponentColorModel(
+										   ColorSpace.getInstance(ColorSpace.CS_GRAY),
+										   new int[] {16},
+										   false,		// has alpha
+										   false,		// alpha premultipled
+										   Transparency.OPAQUE,
+										   DataBuffer.TYPE_USHORT
+										   );
+				sm = new ComponentSampleModel(
+											  DataBuffer.TYPE_USHORT,
+											  width,
+											  height,
+											  1,
+											  width,
+											  new int[] {0}
+											  );
+				buf = new DataBufferUShort(decompressedOutputPerComponent[0].getShortArray(),width,0);
+			}
+		}
+		else if (decompressedOutputPerComponent.length == 3) {
+			// the decompressedOutput has separated the input into separate arrays, each of which we can use as a bank and use a band interleaved model
+			if (bitDepth <= 8) {
+				// copied from com.pixelmed.display.SourceImage.createBandInterleavedByteRGBImage(), except that we have three rather than one banks ...
+				cm=new ComponentColorModel(
+										   ColorSpace.getInstance(ColorSpace.CS_sRGB),	// lie if YCbCr (we don't know at this point) :(
+										   new int[] {8,8,8},
+										   false,		// has alpha
+										   false,		// alpha premultipled
+										   Transparency.OPAQUE,
+										   DataBuffer.TYPE_BYTE
+										   );
+				sm = new ComponentSampleModel(
+											  DataBuffer.TYPE_BYTE,
+											  width,
+											  height,
+											  1/*pixelStride*/,
+											  width/*scanlineStride*/,
+											  new int[] {0,1,2}/*bankIndices*/,
+											  new int[] {0,0,0}/*bandOffsets*/
+											  );
+				buf = new DataBufferByte(
+					new byte[][] {
+						decompressedOutputPerComponent[0].getByteArray(),
+						decompressedOutputPerComponent[1].getByteArray(),
+						decompressedOutputPerComponent[2].getByteArray(),
+					},
+					width*height);
+			}
+			else {
+				// not really expecting to see > 8 bit color per channel, but no reason not to build it ... probably not tested yet though :(
+				cm=new ComponentColorModel(
+										   ColorSpace.getInstance(ColorSpace.CS_sRGB),	// lie if YCbCr (we don't know at this point) :(
+										   new int[] {16,16,16},
+										   false,		// has alpha
+										   false,		// alpha premultipled
+										   Transparency.OPAQUE,
+										   DataBuffer.TYPE_USHORT
+										   );
+				sm = new ComponentSampleModel(
+											  DataBuffer.TYPE_USHORT,
+											  width,
+											  height,
+											  1/*pixelStride*/,
+											  width/*scanlineStride*/,
+											  new int[] {0,1,2}/*bankIndices*/,
+											  new int[] {0,0,0}/*bandOffsets*/
+											  );
+				buf = new DataBufferUShort(
+					new short[][] {
+						decompressedOutputPerComponent[0].getShortArray(),
+						decompressedOutputPerComponent[1].getShortArray(),
+						decompressedOutputPerComponent[2].getShortArray(),
+					},
+					width*height);
+			}
+		}
+		
+		if (buf != null) {
+			WritableRaster wr = Raster.createWritableRaster(sm,buf,new Point(0,0));
+			image = new BufferedImage(cm,wr,true,null);	// no properties hash table
+		}
+		
+		return image;
+	}
+
+	JPEGLosslessMetadata metadata = null;
+
+	public IIOMetadata getStreamMetadata() throws IIOException {
+		return null;
+	}
+
+	public IIOMetadata getImageMetadata(int imageIndex) throws IIOException {
+		if (imageIndex != 0) {
+			throw new IndexOutOfBoundsException("imageIndex != 0!");
+		}
+		readMetadata();
+		return metadata;
+	}
+	
+	public void readMetadata() throws IIOException {
+		if (metadata != null) {
+			return;
+		}
+		readEverything();
+		this.metadata = new JPEGLosslessMetadata();
+		//try {
+		//}
+		//catch (IOException e) {
+		//	throw new IIOException("Exception reading metadata", e);
+		//}
+	}
+}
\ No newline at end of file
diff --git a/com/pixelmed/imageio/JPEGLosslessImageReaderSpi.java b/com/pixelmed/imageio/JPEGLosslessImageReaderSpi.java
new file mode 100644
index 0000000..7623990
--- /dev/null
+++ b/com/pixelmed/imageio/JPEGLosslessImageReaderSpi.java
@@ -0,0 +1,130 @@
+/* Copyright (c) 2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+package com.pixelmed.imageio;
+
+// follow the pattern described in "http://docs.oracle.com/javase/1.5.0/docs/guide/imageio/spec/extending.fm3.html"
+
+import com.pixelmed.codec.jpeg.Markers;
+import com.pixelmed.codec.jpeg.Utilities;
+
+import java.io.IOException;
+import java.util.Locale;
+import javax.imageio.ImageReader;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.stream.ImageInputStream;
+
+public class JPEGLosslessImageReaderSpi extends ImageReaderSpi {
+
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/imageio/JPEGLosslessImageReaderSpi.java,v 1.5 2016/01/16 15:07:52 dclunie Exp $";
+
+	static final String vendorName = "PixelMed Publishing, LLC.";
+	static final String version = "0.01";
+	static final String readerClassName = "com.pixelmed.imageio.JPEGLosslessImageReader";
+	static final String description = "PixelMed JPEG Lossless Image Reader";
+	
+	public static final Class<?>[] inputTypes = { ImageInputStream.class };	// current JavaDoc says STANDARD_INPUT_TYPE is deprecated
+	
+	static final String[] names = { "jpeg-lossless" };			// this is what Sun JIIO JAI codecs use to recognize JPEG lossless
+	static final String[] suffixes = { "ljpeg", "jpl" };		// not "jls", which is JPEG-LS; "ljpeg" was used by USF Mammo
+	static final String[] MIMETypes = null;						// current JavaDoc says null or empty array OK
+	
+	static final String nativeImageMetadataFormatName = "com.pixelmed.imageio.JPEGLosslessMetadata_0.1";
+	static final String nativeImageMetadataFormatClassName = "com.pixelmed.imageio.JPEGLosslessMetadata";
+	
+	public JPEGLosslessImageReaderSpi() {
+		super(
+			vendorName,
+			version,
+			names,
+			suffixes,
+			MIMETypes,
+			readerClassName,
+			inputTypes,
+			null/*writerSpiNames*/,
+			false/*supportsStandardStreamMetadataFormat*/,
+			null/*nativeStreamMetadataFormatName*/,
+			null/*nativeStreamMetadataFormatClassName*/,
+			null/*extraStreamMetadataFormatNames*/,
+			null/*extraStreamMetadataFormatClassNames*/,
+			false/*supportsStandardImageMetadataFormat*/,
+			nativeImageMetadataFormatName,
+			nativeImageMetadataFormatClassName,
+			null/*extraImageMetadataFormatNames*/,
+			null/*extraImageMetadataFormatClassNames*/);
+	}
+	
+	public boolean canDecodeInput(Object input) throws IOException {
+		// Need SOI Start of Image
+		// May be intervening table/misc segments
+		// Need SOF3 Huffman Lossless Sequential length variable 0x0b
+		boolean canDecode = false;
+		try {
+			if (input instanceof ImageInputStream) {
+				ImageInputStream stream = (ImageInputStream)input;
+				byte[] b = new byte[4];
+				stream.mark();
+				stream.readFully(b,0,2);
+				if (b[0] == (byte)0xff && b[1] == (byte)0xd8) {		// have SOI
+					int markerprefix = stream.read();
+					while (markerprefix == 0xff) {			// keep reading until we have an SOF or until not a marker segment
+						int marker = stream.read();
+						marker|=0xff00;						// convention is to express them with the leading ff, so that is what we look up
+//System.err.println("JPEGLosslessImageReaderSpi.canDecodeInput(): have marker "+Utilities.toPaddedHexString(marker,4)+" "+Markers.getAbbreviation(marker));
+						// should not have to worry about stuffed bytes in ECS or padding because we never get that far in the stream
+						if (Markers.isSOF(marker)) {
+//System.err.println("JPEGLosslessImageReaderSpi.canDecodeInput(): have some type of SOF marker");
+							if (marker == Markers.SOF3) {
+//System.err.println("JPEGLosslessImageReaderSpi.canDecodeInput(): have SOF3");
+								canDecode = true;
+							}
+							break;		// stop reading after any SOF
+						}
+						else if (marker == Markers.SOS) {
+							break;		// stop reading at SOS since too late to get SOF3
+						}
+						else if (Markers.isVariableLengthJPEGSegment(marker)) {
+							stream.readFully(b,0,2);
+							int length=((b[0]&0xff)<<8) + (b[1]&0xff);	// big endian
+							if (length > 2) {
+//System.err.println("JPEGLosslessImageReaderSpi.canDecodeInput(): skipping variable length marker segment length="+length);
+								stream.skipBytes(length-2);
+							}
+							else {
+//System.err.println("JPEGLosslessImageReaderSpi.canDecodeInput(): variable length marker segment with invalid length="+length);
+								break;
+							}
+						}
+						else if (Markers.isNoLengthJPEGSegment(marker)) {
+						}
+						else {
+							int length=Markers.isFixedLengthJPEGSegment(marker);
+							if (length == 0) {
+//System.err.println("JPEGLosslessImageReaderSpi.canDecodeInput(): stopping on unrecognized marker segment");
+								break;
+							}
+							else {
+//System.err.println("JPEGLosslessImageReaderSpi.canDecodeInput(): skipping fixed length marker segment length="+length);
+								stream.skipBytes(length-2);
+							}
+						}
+						markerprefix=stream.read();
+					}
+				}
+				// else no SOI so not JPEG
+				stream.reset();
+System.err.println("JPEGLosslessImageReaderSpi.canDecodeInput() = "+canDecode);
+			}
+		}
+		catch (IOException e) {
+		}
+		return canDecode;
+	}
+	
+	public String getDescription(Locale locale) {
+		return description;
+	}
+	
+	public ImageReader createReaderInstance(Object extension) {
+		return new JPEGLosslessImageReader(this);
+	}
+
+}
diff --git a/com/pixelmed/imageio/JPEGLosslessMetadata.java b/com/pixelmed/imageio/JPEGLosslessMetadata.java
new file mode 100644
index 0000000..f189b79
--- /dev/null
+++ b/com/pixelmed/imageio/JPEGLosslessMetadata.java
@@ -0,0 +1,105 @@
+/* Copyright (c) 2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+
+package com.pixelmed.imageio;
+
+// follow the pattern described in "http://docs.oracle.com/javase/1.5.0/docs/guide/imageio/spec/extending.fm3.html"
+
+import org.w3c.dom.*;
+import javax.xml.parsers.*; // Package name may change in J2SDK 1.4
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import javax.imageio.metadata.IIOInvalidTreeException;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataFormat;
+import javax.imageio.metadata.IIOMetadataNode;
+
+public class JPEGLosslessMetadata extends IIOMetadata {
+
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/imageio/JPEGLosslessMetadata.java,v 1.2 2015/10/19 15:34:42 dclunie Exp $";
+
+	static final String nativeMetadataFormatName = "com.pixelmed.imageio.JPEGLosslessMetadata_0.1";
+	static final String nativeMetadataFormatClassName = "com.pixelmed.imageio.JPEGLosslessMetadata";
+	
+	// Keyword/value pairs
+	List keywords = new ArrayList();
+	List values = new ArrayList();
+
+	public JPEGLosslessMetadata() {
+		super(
+			  false/*standardMetadataFormatSupported*/,
+			  nativeMetadataFormatName,
+			  nativeMetadataFormatClassName,
+			  null/*extraMetadataFormatNames*/,
+			  null/*extraMetadataFormatClassNames*/);
+	}
+	
+	public IIOMetadataFormat getMetadataFormat(String formatName) {
+		if (!formatName.equals(nativeMetadataFormatName)) {
+			throw new IllegalArgumentException("Bad format name!");
+		}
+		return JPEGLosslessMetadataFormat.getDefaultInstance();
+	}
+	
+	public Node getAsTree(String formatName) {
+		if (!formatName.equals(nativeMetadataFormatName)) {
+			throw new IllegalArgumentException("Bad format name!");
+		}
+		
+		// Create a root node
+		IIOMetadataNode root = new IIOMetadataNode(nativeMetadataFormatName);
+		
+		// Add a child to the root node for each keyword/value pair
+		Iterator keywordIter = keywords.iterator();
+		Iterator valueIter = values.iterator();
+		while (keywordIter.hasNext()) {
+			IIOMetadataNode node = new IIOMetadataNode("KeywordValuePair");
+			node.setAttribute("keyword", (String)keywordIter.next());
+			node.setAttribute("value", (String)valueIter.next());
+			root.appendChild(node);
+		}
+		
+		return root;
+	}
+	
+	public boolean isReadOnly() {
+		return true;	// since no writer plug-in
+	}
+	
+	public void reset() {
+		this.keywords = new ArrayList();
+		this.values = new ArrayList();
+	}
+	
+	public void mergeTree(String formatName, Node root) throws IIOInvalidTreeException {
+		if (!formatName.equals(nativeMetadataFormatName)) {
+			throw new IllegalArgumentException("Bad format name!");
+		}
+		
+		Node node = root;
+		if (!node.getNodeName().equals(nativeMetadataFormatName)) {
+			throw new IIOInvalidTreeException("Root must be " + nativeMetadataFormatName,node);
+		}
+		node = node.getFirstChild();
+		while (node != null) {
+			if (!node.getNodeName().equals("KeywordValuePair")) {
+				throw new IIOInvalidTreeException("Node name not KeywordValuePair!",node);
+			}
+			NamedNodeMap attributes = node.getAttributes();
+			Node keywordNode = attributes.getNamedItem("keyword");
+			Node valueNode = attributes.getNamedItem("value");
+			if (keywordNode == null || valueNode == null) {
+				throw new IIOInvalidTreeException("Keyword or value missing!",node);
+			}
+			
+			// Store keyword and value
+			keywords.add((String)keywordNode.getNodeValue());
+			values.add((String)valueNode.getNodeValue());
+			
+			// Move to the next sibling
+			node = node.getNextSibling();
+		}
+	}
+}
+
diff --git a/com/pixelmed/imageio/JPEGLosslessMetadataFormat.java b/com/pixelmed/imageio/JPEGLosslessMetadataFormat.java
new file mode 100644
index 0000000..bf8a6da
--- /dev/null
+++ b/com/pixelmed/imageio/JPEGLosslessMetadataFormat.java
@@ -0,0 +1,48 @@
+/* Copyright (c) 2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+
+package com.pixelmed.imageio;
+
+// follow the pattern described in "http://docs.oracle.com/javase/1.5.0/docs/guide/imageio/spec/extending.fm3.html"
+
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.metadata.IIOMetadataFormatImpl;
+
+public class JPEGLosslessMetadataFormat extends IIOMetadataFormatImpl {
+
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/imageio/JPEGLosslessMetadataFormat.java,v 1.2 2015/10/19 15:34:42 dclunie Exp $";
+	
+	// Create a single instance of this class (singleton pattern)
+	private static JPEGLosslessMetadataFormat defaultInstance = new JPEGLosslessMetadataFormat();
+	
+	// Make constructor private to enforce the singleton pattern
+	private JPEGLosslessMetadataFormat() {
+		// Set the name of the root node
+		// The root node has a single child node type that may repeat
+		super("com.pixelmed.imageio.JPEGLosslessMetadata_0.1",
+			  CHILD_POLICY_REPEAT);
+		
+		// Set up the "KeywordValuePair" node, which has no children
+		addElement("KeywordValuePair",
+				   "com.pixelmed.imageio.JPEGLosslessMetadata_0.1",
+				   CHILD_POLICY_EMPTY);
+		
+		// Set up attribute "keyword" which is a String that is required
+		// and has no default value
+		addAttribute("KeywordValuePair", "keyword", DATATYPE_STRING,
+					 true, null);
+		// Set up attribute "value" which is a String that is required
+		// and has no default value
+		addAttribute("KeywordValuePair", "value", DATATYPE_STRING,
+					 true, null);
+	}
+	
+	// Check for legal element name
+	public boolean canNodeAppear(String elementName,ImageTypeSpecifier imageType) {
+		return elementName.equals("KeywordValuePair");
+	}
+	
+	// Return the singleton instance
+	public static JPEGLosslessMetadataFormat getDefaultInstance() {
+		return defaultInstance;
+	}
+}
diff --git a/com/pixelmed/imageio/Makefile b/com/pixelmed/imageio/Makefile
new file mode 100755
index 0000000..0b7ccb7
--- /dev/null
+++ b/com/pixelmed/imageio/Makefile
@@ -0,0 +1,32 @@
+OBJS = \
+	JPEGLosslessImageReaderSpi.class \
+	JPEGLosslessImageReader.class \
+	JPEGLosslessMetadata.class \
+	JPEGLosslessMetadataFormat.class \
+	TestImageIO.class
+
+all:	${OBJS}
+
+PATHTOROOT = ../../..
+
+include ${PATHTOROOT}/Makefile.common.mk
+
+clean:
+	rm -f *~ *.class core *.bak
+
+testlosslessjpeg:
+	# need to use jar file, since won't find new SPI in classpath without META-INF
+	#java -cp ${PATHTOROOT}/pixelmed_imageio.jar com.pixelmed.imageio.TestImageIO /tmp/crap6.jpg jpeg-lossless 0
+	#java -cp ${PATHTOROOT}/pixelmed_imageio.jar com.pixelmed.imageio.TestImageIO /tmp/crap6.jpg		# tests JPEGLosslessImageReaderSpi.canDecodeInput() (works irrespective of extension)
+	#java -cp ${PATHTOROOT}/pixelmed_imageio.jar com.pixelmed.imageio.TestImageIO "$${HOME}/Documents/Medical/compression/JPEG/10918-2/ITU T83/T83_process14/O1.JPG" jpeg-lossless 0	# 4 components, so will fail
+	#java -cp ${PATHTOROOT}/pixelmed_imageio.jar com.pixelmed.imageio.TestImageIO "$${HOME}/Documents/Medical/compression/JPEG/10918-2/ITU T83/T83_process14/O2.JPG" jpeg-lossless 0	# 4 components, so will fail
+	#gunzip < "$${HOME}/Pictures/Medical/USFDigitalMammography/USFmammo_cases_partial_fromsite/DDSM/cases/benigns/benign_01/case0029/C_0029_1.LEFT_CC.LJPEG.gz" > /tmp/C_0029_1.LEFT_CC.LJPEG
+	#java -cp ${PATHTOROOT}/pixelmed_imageio.jar com.pixelmed.imageio.TestImageIO /tmp/C_0029_1.LEFT_CC.LJPEG jpeg-lossless 0	# uses SV7
+	#java -cp ${PATHTOROOT}/pixelmed_imageio.jar com.pixelmed.imageio.TestImageIO /tmp/C_0029_1.LEFT_CC.LJPEG	# tests JPEGLosslessImageReaderSpi.canDecodeInput()
+	rm -rf  /tmp/crap.jpg
+	#dctoraw "$${HOME}/Pictures/Medical/JPEGLossless/BadWithPVRGCodecButOKWithOther/000caecb.dcm" /tmp/crap.jpg
+	#dctoraw "$${HOME}/Pictures/Medical/JPEGLossless/BadWithPVRGCodecButOKWithOther/000cb12e.dcm" /tmp/crap.jpg
+	dctoraw "$${HOME}/Pictures/Medical/JPEGLossless/EightBitJIIOCodecProblem/seq0" /tmp/crap.jpg
+	#dctoraw "$${HOME}/Pictures/Medical/JPEGLossless/eightbitrgbsingleframe.dcm" /tmp/crap.jpg	# DHT segment between SOI and SOF
+	#dctoraw "$${HOME}/Pictures/Medical/JPEGLossless/ivus_thousandsofframes_losslessjpeg.dcm" /tmp/crap.jpg	# DHT segment between SOI and SOF
+	java -cp ${PATHTOROOT}/pixelmed_imageio.jar com.pixelmed.imageio.TestImageIO /tmp/crap.jpg
diff --git a/com/pixelmed/imageio/TestImageIO.java b/com/pixelmed/imageio/TestImageIO.java
new file mode 100644
index 0000000..4fee225
--- /dev/null
+++ b/com/pixelmed/imageio/TestImageIO.java
@@ -0,0 +1,106 @@
+/* Copyright (c) 2004-2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */
+package com.pixelmed.imageio;
+
+import java.io.*;
+import java.util.*;
+import java.awt.*; 
+import java.awt.event.*; 
+import java.awt.image.*;
+import javax.imageio.*;
+import javax.imageio.spi.*;
+import javax.swing.*; 
+import javax.swing.event.*; 
+
+public class TestImageIO extends JFrame {
+
+	private static final String identString = "@(#) $Header: /userland/cvs/codec/com/pixelmed/imageio/TestImageIO.java,v 1.4 2016/01/16 15:01:48 dclunie Exp $";
+
+	class SingleImagePanel extends JComponent {
+	
+		BufferedImage image;
+	
+		SingleImagePanel(String args[]) throws Exception {
+			File f = new File(args[0]);
+			try {
+				if (args.length == 1) {
+					image = ImageIO.read(f);
+				}
+				else {
+					Iterator readers = ImageIO.getImageReadersByFormatName(args[1]);
+					int whichReader = Integer.valueOf(args[2]).intValue();
+					int i=0;
+					while (readers.hasNext()) {
+						ImageReader reader = (ImageReader)readers.next();
+						if (i == whichReader) {
+							ImageReaderSpi spi = reader.getOriginatingProvider();
+System.out.println("Using reader "+i+" from "+spi.getDescription(Locale.US)+" "+spi.getVendorName()+" "+spi.getVersion());
+//while (true) {
+							reader.setInput(ImageIO.createImageInputStream(f));
+							image = reader.read(0);
+//}
+							break;
+						}
+					}
+				}
+				if (image == null) {
+					throw new Exception("Couldn't find a reader");
+				}
+System.out.println("Image width="+image.getWidth()+" height="+image.getHeight());
+				setSize(image.getWidth(),image.getHeight());
+			}
+			catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+		
+		public void paintComponent(Graphics g) {
+//System.out.println("SingleImagePanel.paintComponent()");
+			Graphics2D g2d=(Graphics2D)g;
+			g2d.drawImage(image,0,0,this);
+		}
+	}
+
+	TestImageIO(String args[]) throws Exception {
+
+		addWindowListener(new WindowAdapter() {
+			public void windowClosing(WindowEvent e) {
+				dispose();
+			}
+		});
+
+		Container content = getContentPane();
+		//content.setLayout(new GridLayout(1,1));
+		SingleImagePanel panel = new SingleImagePanel(args);
+		content.add(panel);
+		setSize(panel.getWidth(),panel.getHeight());
+		//pack();
+	}
+
+	public static void main(String args[]) {
+	
+		//javax.imageio.spi.IIORegistry.getDefaultInstance().registerApplicationClasspathSpis();
+		javax.imageio.ImageIO.scanForPlugins();
+		
+		javax.imageio.ImageIO.setUseCache(false);		// no slow disk caches for us !
+	
+		String[] formats=ImageIO.getReaderFormatNames();
+		for (int i=0; formats != null && i<formats.length; ++i) {
+System.out.println(formats[i]);
+			Iterator readers = ImageIO.getImageReadersByFormatName(formats[i]);
+			while (readers.hasNext()) {
+				ImageReader reader = (ImageReader)readers.next();
+				ImageReaderSpi spi = reader.getOriginatingProvider();
+System.out.println("\t"+spi.getDescription(Locale.US)+" "+spi.getVendorName()+" "+spi.getVersion());
+			}
+		}
+		try {
+			TestImageIO f = new TestImageIO(args);
+			f.setVisible(true);
+		}
+		catch (Exception e) {
+			e.printStackTrace();
+			System.exit(0);
+		}
+	}
+
+}
diff --git a/com/pixelmed/codec/jpeg/package.html b/com/pixelmed/imageio/package.html
similarity index 68%
copy from com/pixelmed/codec/jpeg/package.html
copy to com/pixelmed/imageio/package.html
index 3572859..36d5150 100755
--- a/com/pixelmed/codec/jpeg/package.html
+++ b/com/pixelmed/imageio/package.html
@@ -5,17 +5,16 @@
 
   @(#)package.html	1.60 98/01/27
 
-  Copyright (c) 2001-2014, David A. Clunie DBA Pixelmed Publishing. All rights reserved.
+  Copyright (c) 2001-2016, David A. Clunie DBA Pixelmed Publishing. All rights reserved.
 
 -->
 </head>
 <body bgcolor="white">
-<p>JPEG selective block redaction codec.</p>
+<p>JPEG lossless decoder.</p>
 
 <h2>Package Specification</h2>
 
-<p>This package contains a pure Java codec for selective block redaction of
-baseline process (8 bit, DCT, Huffman coded) JPEG images.</p>
+<p>This package contains a pure Java decoder for Huffman Lossless Sequential JPEG encoded images (SOF3).</p>
 
 <p>Development of this package was supported by funding from MDDX Research and Informatics.</p>
 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/pixelmed-codec.git



More information about the debian-med-commit mailing list