src/org/terifan/imageio/jpeg/CompressionType.java
public enum CompressionType
{
	Huffman(SegmentMarker.SOF0, HuffmanDecoder.class),
	HuffmanOptimized(SegmentMarker.SOF0, HuffmanDecoder.class),
	HuffmanProgressive(SegmentMarker.SOF2, HuffmanDecoder.class),
	Arithmetic(SegmentMarker.SOF9, ArithmeticDecoder.class),
	ArithmeticProgressive(SegmentMarker.SOF10, ArithmeticDecoder.class);


	private final SegmentMarker mSegmentMarker;
	private final Class<? extends Decoder> mDecoderType;


	private CompressionType(SegmentMarker aSegmentMarker, Class<? extends Decoder> aDecoderType)
	{
		mSegmentMarker = aSegmentMarker;
		mDecoderType = aDecoderType;
	}


	public static CompressionType decode(SegmentMarker aMarker) throws IOException
	{
		switch (aMarker)
		{
			case SOF0:
				return CompressionType.Huffman;
			case SOF2:
				return CompressionType.HuffmanProgressive;
			case SOF9:
				return CompressionType.Arithmetic;
			case SOF10:
				return CompressionType.ArithmeticProgressive;
			default:
				throw new IOException("Image encoding not supported");
		}
	}


	SegmentMarker getSegmentMarker()
	{
		return mSegmentMarker;
	}


	public boolean isProgressive()
	{
		return this == HuffmanProgressive || this == ArithmeticProgressive;
	}


	public boolean isArithmetic()
	{
		return this == Arithmetic || this == ArithmeticProgressive;
	}


	public Decoder createDecoderInstance()
	{
		try
		{
			return mDecoderType.newInstance();
		}
		catch (IllegalAccessException | InstantiationException e)
		{
			throw new IllegalStateException(e);
		}
	}
}
src/org/terifan/imageio/jpeg/APP0Segment.java
public class APP0Segment extends Segment
{
	private JPEG mJPEG;
	private boolean mExtension;
	private int mThumbnailWidth;
	private int mThumbnailHeight;
	private byte[] mThumbnailData;
	private int mThumbnailFormat;


	public APP0Segment(JPEG aJPEG)
	{
		mJPEG = aJPEG;

		mThumbnailData = new byte[0];
	}


	@Override
	public APP0Segment decode(BitInputStream aBitStream) throws IOException
	{
		int length = aBitStream.readInt16();
		String type = aBitStream.readString();

		switch (type)
		{
			case "JFIF":
				int version = aBitStream.readInt16();

				if ((version >> 8) != 1)
				{
					throw new IOException("Error in JPEG stream; unsupported version: " + (version >> 8) + "." + (version & 255));
				}

				mJPEG.mDensitiesUnits = aBitStream.readInt8();
				mJPEG.mDensityX = aBitStream.readInt16();
				mJPEG.mDensityY = aBitStream.readInt16();

				mThumbnailWidth = aBitStream.readInt8();
				mThumbnailHeight = aBitStream.readInt8();
				mThumbnailData = new byte[mThumbnailWidth * mThumbnailHeight * 3];

				aBitStream.read(mThumbnailData);

				if (length != 16 + mThumbnailData.length)
				{
					throw new IOException("Error in JPEG stream; illegal APP0 segment size.");
				}

				mJPEG.mJFIFSegmentMarker = this;

				break;
			case "JFXX":
				mExtension = true;

				mThumbnailFormat = aBitStream.readInt8();

				mThumbnailData = new byte[length - 2 - 5 - 1];

				aBitStream.read(mThumbnailData);

				break;
			default:
				throw new IOException("Unsupported APP0 extension: " + type);
		}

		return this;
	}


	@Override
	public APP0Segment encode(BitOutputStream aBitStream) throws IOException
	{
		aBitStream.writeInt16(SegmentMarker.APP0.CODE);

		if (mExtension)
		{
			aBitStream.writeInt16(2 + 5 + 1 + mThumbnailData.length);
			aBitStream.writeString("JFXX");
			aBitStream.writeInt8(mThumbnailFormat);
			aBitStream.write(mThumbnailData);
		}
		else
		{
			aBitStream.writeInt16(2 + 5 + 2 + 1 + 2 + 2 + 1 + 1 + mThumbnailData.length);
			aBitStream.writeString("JFIF");
			aBitStream.writeInt16(0x0102); // version
			aBitStream.writeInt8(mJPEG.mDensitiesUnits);
			aBitStream.writeInt16(mJPEG.mDensityX);
			aBitStream.writeInt16(mJPEG.mDensityY);
			aBitStream.writeInt8(mThumbnailWidth); // thumbnail width
			aBitStream.writeInt8(mThumbnailHeight); // thumbnail height
			aBitStream.write(mThumbnailData);
		}

		return this;
	}


	@Override
	public APP0Segment print(Log aLog) throws IOException
	{
		if (mExtension)
		{
			aLog.println("APP0 segment");
			aLog.println("  JFXX");
			if (mThumbnailData.length > 0)
			{
				aLog.println("  thumbnail");
				aLog.println("  width=%d, height=%d, data=%d bytes", mThumbnailWidth, mThumbnailHeight, mThumbnailData.length);
			}
		}
		else
		{
			aLog.println("APP0 segment");
			aLog.println("  JFIF");
			if (mThumbnailData.length > 0)
			{
				aLog.println("  thumbnail");
				aLog.println("  format=%d, data=%d bytes", mThumbnailFormat, mThumbnailData.length);
			}
		}

		return this;
	}
}
src/org/terifan/imageio/jpeg/DACSegment.java
public class DACSegment extends Segment
{
	private JPEG mJPEG;
	private SOSSegment mSOSSegment;


	public DACSegment(JPEG aJPEG)
	{
		mJPEG = aJPEG;
	}


	public DACSegment(JPEG aJPEG, SOSSegment aSOSSegment)
	{
		mJPEG = aJPEG;
		mSOSSegment = aSOSSegment;
	}


	@Override
	public DACSegment decode(BitInputStream aBitStream) throws IOException
	{
		int length = aBitStream.readInt16() - 2;

		while (length > 0)
		{
			int index = aBitStream.readInt8();
			int val = aBitStream.readInt8();
			length -= 2;

			if (index < 0 || index >= (2 * NUM_ARITH_TBLS))
			{
				throw new IllegalArgumentException("Bad DAC index: " + index);
			}

			if (index >= NUM_ARITH_TBLS) // define AC table
			{
				mJPEG.mArithACK[index - NUM_ARITH_TBLS] = val;
			}
			else // define DC table
			{
				mJPEG.mArithDCU[index] = val >> 4;
				mJPEG.mArithDCL[index] = val & 0x0F;

				if (mJPEG.mArithDCL[index] > mJPEG.mArithDCU[index])
				{
					throw new IllegalArgumentException("Bad DAC value: " + val);
				}
			}
		}

		if (length != 0)
		{
			throw new IllegalArgumentException("Bad DAC segment: remaining: " + length);
		}

		return this;
	}


	@Override
	public DACSegment encode(BitOutputStream aBitStream) throws IOException
	{
		int ac = 0;
		for (int i = 0; i < mJPEG.mArithACK.length; i++)
		{
			boolean found = false;
			for (int scanComponentIndex = 0; !found && scanComponentIndex < mJPEG.mScanBlockCount; scanComponentIndex++)
			{
				found = mSOSSegment.getACTable(scanComponentIndex) == i;
			}

			if (found)
			{
				ac++;
			}
		}

		aBitStream.writeInt16(SegmentMarker.DAC.CODE);
		aBitStream.writeInt16(2 + 2 * (mJPEG.mArithDCU.length + ac));

		for (int i = 0; i < mJPEG.mArithDCU.length; i++)
		{
			aBitStream.writeInt8(i);
			aBitStream.writeInt8((mJPEG.mArithDCU[i] << 4) + mJPEG.mArithDCL[i]);
		}
		for (int i = 0; i < mJPEG.mArithACK.length; i++)
		{
			boolean found = false;
			for (int scanComponentIndex = 0; !found && scanComponentIndex < mJPEG.mScanBlockCount; scanComponentIndex++)
			{
				found = mSOSSegment.getACTable(scanComponentIndex) == i;
			}

			if (found)
			{
				aBitStream.writeInt8(NUM_ARITH_TBLS + i);
				aBitStream.writeInt8(mJPEG.mArithACK[i]);
			}
		}

		return this;
	}


	@Override
	public DACSegment print(Log aLog) throws IOException
	{
		aLog.println("DAC segment");

		aLog.println("  DC=%d, AC=%d", mJPEG.mArithDCU.length, mJPEG.mArithACK.length);

		if (aLog.isDetailed())
		{
			for (int i = 0; i < mJPEG.mArithDCU.length; i++)
			{
				aLog.println("    DC %d", i);
				aLog.println("      u=%d, l=%d", mJPEG.mArithDCU[i], mJPEG.mArithDCL[i]);
			}

			for (int i = 0; i < mJPEG.mArithACK.length; i++)
			{
				boolean found = false;
				for (int scanComponentIndex = 0; !found && scanComponentIndex < mJPEG.mScanBlockCount; scanComponentIndex++)
				{
					found = mSOSSegment.getACTable(scanComponentIndex) == i;
				}

				if (found)
				{
					aLog.println("    AC %d", i);
					aLog.println("      k=%d", mJPEG.mArithACK[i]);
				}
			}
		}

		return this;
	}
}
src/org/terifan/imageio/jpeg/APP2Segment.java
public class APP2Segment extends Segment
{
	public final static String ICC_PROFILE = "ICC_PROFILE";

	private JPEG mJPEG;
	private String mType;
	private int mVersion;


	public APP2Segment(JPEG aJPEG)
	{
		mJPEG = aJPEG;
		mVersion = 0x0101;
	}


	public String getType()
	{
		return mType;
	}


	public APP2Segment setType(String aType)
	{
		mType = aType;
		return this;
	}


	@Override
	public APP2Segment decode(BitInputStream aBitStream) throws IOException
	{
		int length = aBitStream.readInt16() - 2;

		mType = aBitStream.readString();

		switch (mType)
		{
			case ICC_PROFILE:
				mVersion = aBitStream.readInt16(); // version

				byte[] buffer = new byte[length - 2 - ICC_PROFILE.length() - 1];
				aBitStream.read(buffer);

				mJPEG.mICCProfile = ICC_Profile.getInstance(new ByteArrayInputStream(buffer));
				break;
			default:
				aBitStream.skipBytes(length - mType.length() - 1);
				break;
		}

		return this;
	}


	@Override
	public APP2Segment encode(BitOutputStream aBitStream) throws IOException
	{
		if (mJPEG.mICCProfile != null)
		{
			byte[] data = mJPEG.mICCProfile.getData();

			aBitStream.writeInt16(SegmentMarker.APP2.CODE);
			aBitStream.writeInt16(2 + 12 + 2 + data.length);
			aBitStream.writeString(ICC_PROFILE);
			aBitStream.writeInt16(mVersion);
			aBitStream.write(data);
		}

		return this;
	}


	@Override
	public APP2Segment print(Log aLog) throws IOException
	{
		aLog.println("APP2 segment");
		switch (mType)
		{
			case ICC_PROFILE:
				aLog.println("ICC_PROFILE");
				break;
			default:
				aLog.println("Unsupported APP2 extension - %s", mType);
				break;
		}

		return this;
	}
}
src/org/terifan/imageio/jpeg/DQTSegment.java
public class DQTSegment extends Segment
{
	private JPEG mJPEG;


	public DQTSegment(JPEG aJPEG)
	{
		mJPEG = aJPEG;
	}


	@Override
	public DQTSegment decode(BitInputStream aBitStream) throws IOException
	{
		int length = aBitStream.readInt16() - 2;

		do
		{
			QuantizationTable table = readTable(aBitStream);

			mJPEG.mQuantizationTables[table.getIdentity()] = table;

			length -= 1 + (table.getPrecision() == PRECISION_8_BITS ? 64 : 128);

			if (length < 0)
			{
				throw new IOException("Error in JPEG stream; illegal DQT segment size.");
			}
		}
		while (length > 0);

		return this;
	}


	@Override
	public DQTSegment encode(BitOutputStream aBitStream) throws IOException
	{
		for (QuantizationTable table : mJPEG.mQuantizationTables)
		{
			if (table == null)
			{
				continue;
			}

			int len = 2 + 1 + (table.getPrecision() == PRECISION_8_BITS ? 64 : 128);

			aBitStream.writeInt16(SegmentMarker.DQT.CODE);
			aBitStream.writeInt16(len);
			aBitStream.writeInt8(((table.getPrecision() == PRECISION_8_BITS ? 0 : 1) << 3) | table.getIdentity());

			double[] data = table.getDivisors();

			for (int i = 0; i < 64; i++)
			{
				double v = data[NATURAL_ORDER[i]];

				if (table.getPrecision() == PRECISION_16_BITS)
				{
					v *= 256.0;
				}

				aBitStream.write((int)v);
			}
		}

		return this;
	}


	private QuantizationTable readTable(BitInputStream aBitStream) throws IOException
	{
		int temp = aBitStream.readInt8();
		int identity = temp & 0x07;
		int precision = (temp >> 3) == 0 ? PRECISION_8_BITS : PRECISION_16_BITS;

		double[] data = new double[64];

		for (int i = 0; i < 64; i++)
		{
			if (precision == PRECISION_8_BITS)
			{
				data[NATURAL_ORDER[i]] = aBitStream.readInt8();
			}
			else
			{
				data[NATURAL_ORDER[i]] = aBitStream.readInt16() / 256.0;
			}
		}

		return new QuantizationTable(identity, precision, data);
	}


	@Override
	public DQTSegment print(Log aLog) throws IOException
	{
		aLog.println("DQT segment");
		for (QuantizationTable table : mJPEG.mQuantizationTables)
		{
			if (table != null)
			{
				aLog.println("  identity=%d, precision=%d bits", table.getIdentity(), table.getPrecision() == PRECISION_8_BITS ? 8 : 16);

				if (aLog.isDetailed())
				{
					table.print(aLog);
				}
			}
		}
		return this;
	}
}
src/org/terifan/imageio/jpeg/ComponentInfo.java
public class ComponentInfo
{
	public enum Type
	{
		Y, CB, CR, I, Q
	}

	private int mComponentIndex; // identifier for this component (0..255)
	private int mComponentId; // its index in SOF or cinfo->comp_info[]
	private int mQuantizationTableId;
	private int mHorSampleFactor; // horizontal sampling factor (1..4)
	private int mVerSampleFactor; // vertical sampling factor (1..4)
	private int mTableDC; // DC entropy table selector (0..3)
	private int mTableAC; // AC entropy table selector (0..3)
	private int mComponentBlockOffset;


	public ComponentInfo()
	{
	}


	public ComponentInfo(int aComponentIndex, int aComponentId, int aQuantizationTableId, int aHorSampleFactor, int aVerSampleFactor)
	{
		mComponentIndex = aComponentIndex;
		mComponentId = aComponentId;
		mQuantizationTableId = aQuantizationTableId;
		mHorSampleFactor = aHorSampleFactor;
		mVerSampleFactor = aVerSampleFactor;
	}


	public ComponentInfo decode(BitInputStream aInputStream, int aComponentIndex) throws IOException
	{
		mComponentIndex = aComponentIndex;
		mComponentId = aInputStream.readInt8();
		mHorSampleFactor = aInputStream.readBits(4);
		mVerSampleFactor = aInputStream.readBits(4);
		mQuantizationTableId = aInputStream.readInt8();

		return this;
	}


	public ComponentInfo encode(BitOutputStream aBitStream) throws IOException
	{
		aBitStream.writeInt8(mComponentId);
		aBitStream.writeInt8((mHorSampleFactor << 4) + mVerSampleFactor);
		aBitStream.writeInt8(mQuantizationTableId);

		return this;
	}


	public void print(Log aLog)
	{
		aLog.println("  component %s", ComponentInfo.Type.values()[mComponentId]);
		aLog.println("    id=%d, dc-table=%d, ac-table=%d, quantizationTableId=%d, sample-factor=%dx%d", mComponentIndex, mTableDC, mTableAC, mQuantizationTableId, mHorSampleFactor, mVerSampleFactor);
	}


	public int getComponentIndex()
	{
		return mComponentIndex;
	}


	public int getComponentId()
	{
		return mComponentId;
	}


	public int getQuantizationTableId()
	{
		return mQuantizationTableId;
	}


	public int getHorSampleFactor()
	{
		return mHorSampleFactor;
	}


	public int getVerSampleFactor()
	{
		return mVerSampleFactor;
	}


	public int getTableDC()
	{
		return mTableDC;
	}


	public void setTableDC(int aSOSTableDC)
	{
		mTableDC = aSOSTableDC;
	}


	public int getTableAC()
	{
		return mTableAC;
	}


	public void setTableAC(int aSOSTableAC)
	{
		mTableAC = aSOSTableAC;
	}


	public void setComponentBlockOffset(int aComponentBlockOffset)
	{
		mComponentBlockOffset = aComponentBlockOffset;
	}


	public int getComponentBlockOffset()
	{
		return mComponentBlockOffset;
	}


	@Override
	public String toString()
	{
		return "ComponentInfo{" + "mComponentIndex=" + mComponentIndex + ", mComponentId=" + mComponentId + ", mQuantizationTableId=" + mQuantizationTableId + ", mHorSampleFactor=" + mHorSampleFactor + ", mVerSampleFactor=" + mVerSampleFactor + ", mTableDC=" + mTableDC + ", mTableAC=" + mTableAC + ", mComponentBlockOffset=" + mComponentBlockOffset + '}';
	}
}
src/org/terifan/imageio/jpeg/SegmentMarker.java
public enum SegmentMarker
{
	SOF0(0xFFC0, "Baseline"),
	SOF1(0xFFC1, "Extended sequential, Huffman"),
	SOF2(0xFFC2, "Progressive, Huffman"),
	DHT(0xFFC4, "Define Huffman Table"),
	JPG(0xFFC8, "Reserved, fatal error"),
	SOF9(0xFFC9, "Sequential, arithmetic"),
	SOF10(0xFFCA, "Progressive, arithmetic"),
	DAC(0xFFCC, "Define Arithmetic Table"),
	SOF3(0xFFC3, "Unsupported; Lossless, Huffman"),
	SOF5(0xFFC5, "Unsupported; Differential sequential, Huffman"),
	SOF6(0xFFC6, "Unsupported; Differential progressive, Huffman"),
	SOF7(0xFFC7, "Unsupported; Differential lossless, Huffman"),
	SOF11(0xFFCB, "Unsupported; Lossless, arithmetic"),
	SOF13(0xFFCD, "Unsupported; Differential sequential, arithmetic"),
	SOF14(0xFFCE, "Unsupported; Differential progressive, arithmetic"),
	SOF15(0xFFCF, "Unsupported; Differential lossless, arithmetic"),

	RST0(0xFFD0), // RSTn are used for resync
	RST1(0xFFD1),
	RST2(0xFFD2),
	RST3(0xFFD3),
	RST4(0xFFD4),
	RST5(0xFFD5),
	RST6(0xFFD6),
	RST7(0xFFD7),
	SOI(0xFFD8, "Start Of Image"),
	EOI(0xFFD9, "End Of Image"),
	SOS(0xFFDA, "Start Of Scan"),
	DQT(0xFFDB, "Define Quantization Table"),
	DNL(0xFFDC, "Number Of Lines"),
	DRI(0xFFDD, "Define Restart Interval"),
	DHP(0xFFDE, "Define Hiearchical Progression"),
	EXP(0xFFDF, "Expands Reference Component(s)"),

	APP0(0xFFE0, "JFIF APP0 segment marker"),
	APP1(0xFFE1, "Exif metadata"),
	APP2(0xFFE2, "FlashPix data"),
	APP3(0xFFE3),
	APP4(0xFFE4),
	APP5(0xFFE5),
	APP6(0xFFE6),
	APP7(0xFFE7),
	APP8(0xFFE8),
	APP9(0xFFE9),
	APP10(0xFFEA),
	APP11(0xFFEB),
	APP12(0xFFEC),
	APP13(0xFFED, "XML metadata"),
	APP14(0xFFEE, "Adobe color profile"),
	APP15(0xFFEF),

	JPG0(0xFFF0),   // Reserved for JPEG extensions, fatal error
	JPG1(0xFFF1),   // Reserved for JPEG extensions, fatal error
	JPG2(0xFFF2),   // Reserved for JPEG extensions, fatal error
	JPG3(0xFFF3),   // Reserved for JPEG extensions, fatal error
	JPG4(0xFFF4),   // Reserved for JPEG extensions, fatal error
	JPG5(0xFFF5),   // Reserved for JPEG extensions, fatal error
	JPG6(0xFFF6),   // Reserved for JPEG extensions, fatal error
	JPG7(0xFFF7),   // Reserved for JPEG extensions, fatal error
	LSE(0xFFF8),    // LSE inverse color transform specification marker
	JPG9(0xFFF9),   // Reserved for JPEG extensions, fatal error
	JPG10(0xFFFA),  // Reserved for JPEG extensions, fatal error
	JPG11(0xFFFB),  // Reserved for JPEG extensions, fatal error
	JPG12(0xFFFC),  // Reserved for JPEG extensions, fatal error
	JPG13(0xFFFD),  // Reserved for JPEG extensions, fatal error
	COM(0xFFFE, "Comment");

	public final int CODE;
	public final String mDescription;

	private SegmentMarker(int aCode)
	{
		this(aCode, "");
	}

	private SegmentMarker(int aCode, String aDescription)
	{
		CODE = aCode;
		mDescription = aDescription;
	}

	public static SegmentMarker valueOf(int aCode)
	{
		for (SegmentMarker sm : values())
		{
			if (sm.CODE == aCode)
			{
				return sm;
			}
		}

		return null;
	}
}
src/org/terifan/imageio/jpeg/DHTSegment.java
public class DHTSegment extends Segment
{
	private JPEG mJPEG;
	private String mLog;


	public DHTSegment(JPEG aJPEG) throws IOException
	{
		mJPEG = aJPEG;
	}


	@Override
	public DHTSegment decode(BitInputStream aBitStream) throws IOException
	{
		int length = aBitStream.readInt16() - 2;

		mLog = "";

		while (length > 0)
		{
			HuffmanTable table = new HuffmanTable().decode(aBitStream);

			mJPEG.mHuffmanTables[table.getIndex()][table.getType()] = table;

			if (mLog.length() > 0)
			{
				mLog += "\n";
			}
			mLog += "    index=" + table.getIndex() + ", type=" + (table.getType() == HuffmanTable.TYPE_DC ? "DC" : "AC");

			length -= 17 + table.getNumSymbols();

			if (length < 0)
			{
				throw new IOException("Error in JPEG stream; illegal DHT segment size.");
			}
		}

		return this;
	}


	@Override
	public DHTSegment encode(BitOutputStream aBitStream) throws IOException
	{
		mLog = "";

		ByteArrayOutputStream baos = new ByteArrayOutputStream();

		for (HuffmanTable table : mJPEG.mHuffmanDCTables)
		{
			if (table != null && !table.isSent())
			{
				if (mLog.length() > 0)
				{
					mLog += "\n";
				}
				mLog += "    index=" + table.getIndex() + ", type=" + (table.getType() == HuffmanTable.TYPE_DC ? "DC" : "AC");

				table.encode(baos);
			}
		}

		for (HuffmanTable table : mJPEG.mHuffmanACTables)
		{
			if (table != null && !table.isSent())
			{
				if (mLog.length() > 0)
				{
					mLog += "\n";
				}
				mLog += "    index=" + table.getIndex() + ", type=" + (table.getType() == HuffmanTable.TYPE_DC ? "DC" : "AC");

				table.encode(baos);
			}
		}

		if (baos.size() > 0)
		{
			aBitStream.writeInt16(SegmentMarker.DHT.CODE);
			aBitStream.writeInt16(2 + baos.size());
			baos.writeTo(aBitStream);
		}

		return this;
	}


	@Override
	public DHTSegment print(Log aLog) throws IOException
	{
		aLog.println("DHT segment");

		if (mLog != null)
		{
			aLog.println(mLog);
		}

		if (aLog.isDetailed())
		{
			for (HuffmanTable[] tables : mJPEG.mHuffmanTables)
			{
				for (HuffmanTable table : tables)
				{
					if (table != null)
					{
						table.print(aLog);
					}
				}
			}
		}

		return this;
	}
}
src/org/terifan/imageio/jpeg/Log.java
public class Log
{
	protected PrintStream mPrintStream;
	protected boolean mDetailed;


	public Log()
	{
	}


	public Log(PrintStream aPrintStream, boolean aDetailed)
	{
		mPrintStream = aPrintStream;
		mDetailed = aDetailed;
	}


	public boolean isDetailed()
	{
		return mDetailed;
	}


	public Log setDetailed(boolean aDetailed)
	{
		mDetailed = aDetailed;
		return this;
	}


	public Log setPrintStream(PrintStream aPrintStream)
	{
		mPrintStream = aPrintStream;
		return this;
	}


	public void print(String aText, Object... aParams)
	{
		if (mPrintStream != null)
		{
			mPrintStream.printf(aText, aParams);
		}
	}


	public void println(String aText, Object... aParams)
	{
		if (mPrintStream != null)
		{
			mPrintStream.printf(aText, aParams);
			mPrintStream.println();
		}
	}
}
src/org/terifan/imageio/jpeg/SOSSegment.java
public class SOSSegment extends Segment
{
	private int[] mComponentIds;
	private int[] mTableAC;
	private int[] mTableDC;
	private JPEG mJPEG;


	public SOSSegment(JPEG aJPEG, int... aComponentIds)
	{
		mJPEG = aJPEG;
		mTableDC = new int[4];
		mTableAC = new int[4];
		mComponentIds = aComponentIds;
	}


	@Override
	public SOSSegment decode(BitInputStream aBitStream) throws IOException
	{
		int length = aBitStream.readInt16();

		mJPEG.mScanBlockCount = aBitStream.readInt8();

		if (6 + 2 * mJPEG.mScanBlockCount != length)
		{
			throw new IOException("Error in JPEG stream; illegal SOS segment size: " + length + ", offset " + aBitStream.getStreamOffset());
		}

		mComponentIds = new int[mJPEG.mScanBlockCount];

		for (int i = 0; i < mJPEG.mScanBlockCount; i++)
		{
			mComponentIds[i] = aBitStream.readInt8();
			mTableDC[i] = aBitStream.readBits(4);
			mTableAC[i] = aBitStream.readBits(4);
		}

		mJPEG.Ss = aBitStream.readInt8();
		mJPEG.Se = aBitStream.readInt8();
		mJPEG.Ah = aBitStream.readBits(4);
		mJPEG.Al = aBitStream.readBits(4);
		mJPEG.mScanBlockCount = getNumComponents();

		return this;
	}


	@Override
	public SOSSegment encode(BitOutputStream aBitStream) throws IOException
	{
		aBitStream.writeInt16(SegmentMarker.SOS.CODE);
		aBitStream.writeInt16(2 + 1 + mComponentIds.length * 2 + 3);

		aBitStream.writeInt8(mComponentIds.length);

		for (int i = 0; i < mComponentIds.length; i++)
		{
			aBitStream.writeInt8(mComponentIds[i]);
			aBitStream.writeInt8((mTableDC[i] << 4)  + mTableAC[i]);
		}

		aBitStream.writeInt8(mJPEG.Ss);
		aBitStream.writeInt8(mJPEG.Se);
		aBitStream.writeInt8((mJPEG.Ah << 4) + mJPEG.Al);

		return this;
	}


	@Override
	public SOSSegment print(Log aLog) throws IOException
	{
		aLog.println("SOS segment");
		aLog.println("  coefficient partitioning");
		aLog.println("    ss=%d, se=%d, ah=%d, al=%d", mJPEG.Ss, mJPEG.Se, mJPEG.Ah, mJPEG.Al);

		for (int i = 0; i < mJPEG.mScanBlockCount; i++)
		{
			aLog.println("  component %s", ComponentInfo.Type.values()[mComponentIds[i] - 0*1].name());
			aLog.println("    dc-table=%d, ac-table=%d", mTableDC[i], mTableAC[i]);
		}

		return this;
	}


	public int getComponentByIndex(int aIndex)
	{
		return mComponentIds[aIndex];
	}


	public int getNumComponents()
	{
		return mComponentIds.length;
	}


	public int getACTable(int aIndex)
	{
		return mTableAC[aIndex];
	}


	public int getDCTable(int aIndex)
	{
		return mTableDC[aIndex];
	}


	public SOSSegment setTableAC(int aIndex, int aTableAC)
	{
		mTableAC[aIndex] = aTableAC;
		return this;
	}


	public SOSSegment setTableDC(int aIndex, int aTableDC)
	{
		mTableDC[aIndex] = aTableDC;
		return this;
	}


	public void prepareMCU()
	{
		mJPEG.mScanBlockCount = mComponentIds.length;

		mJPEG.mMCUBlockCount = 0;
		for (int scanComponentIndex = 0; scanComponentIndex < mJPEG.mScanBlockCount; scanComponentIndex++)
		{
			ComponentInfo comp = mJPEG.mSOFSegment.getComponentById(getComponentByIndex(scanComponentIndex));
			mJPEG.mMCUBlockCount += comp.getHorSampleFactor() * comp.getVerSampleFactor();
		}

		mJPEG.mMCUComponentIndices = new int[mJPEG.mMCUBlockCount];
		mJPEG.mComponentInfo = new ComponentInfo[mJPEG.mScanBlockCount];

		for (int scanComponentIndex = 0, blockIndex = 0; scanComponentIndex < mJPEG.mScanBlockCount; scanComponentIndex++)
		{
			ComponentInfo comp = mJPEG.mSOFSegment.getComponentById(getComponentByIndex(scanComponentIndex));
			comp.setTableAC(getACTable(scanComponentIndex));
			comp.setTableDC(getDCTable(scanComponentIndex));

			mJPEG.mComponentInfo[scanComponentIndex] = comp;

			for (int i = 0; i < comp.getHorSampleFactor() * comp.getVerSampleFactor(); i++, blockIndex++)
			{
				mJPEG.mMCUComponentIndices[blockIndex] = scanComponentIndex;
			}
		}
	}
}
src/org/terifan/imageio/jpeg/JPEG.java
public class JPEG
{
	public SOFSegment mSOFSegment;

	public int[][][][] mCoefficients;

	public ComponentInfo[] mComponentInfo;
	public int[] mMCUComponentIndices;
	public int mMCUBlockCount;
	public int mScanBlockCount;

	public QuantizationTable[] mQuantizationTables;

	public HuffmanTable[][] mHuffmanTables;
	public HuffmanTable[] mHuffmanDCTables;
	public HuffmanTable[] mHuffmanACTables;

	public ColorSpace mColorSpace;
	public ICC_Profile mICCProfile;
	public APP14Segment mColorSpaceTransform;
	public APP0Segment mJFIFSegmentMarker;

	public int[] mArithDCL;
	public int[] mArithDCU;
	public int[] mArithACK;

	public int Ss;
	public int Se;
	public int Ah;
	public int Al;

	public int mDensitiesUnits;
	public int mDensityX;
	public int mDensityY;
	public int mRestartInterval;
	public int mRestartMarkerIndex;
	public int mBlockCount;


	public JPEG()
	{
		mDensitiesUnits = 1;
		mDensityX = 72;
		mDensityY = 72;

		mQuantizationTables = new QuantizationTable[8];

		mHuffmanTables = new HuffmanTable[NUM_HUFF_TBLS][2];
		mHuffmanDCTables = new HuffmanTable[NUM_HUFF_TBLS];
		mHuffmanACTables = new HuffmanTable[NUM_HUFF_TBLS];

		mArithACK = new int[NUM_ARITH_TBLS];
		mArithDCU = new int[NUM_ARITH_TBLS];
		mArithDCL = new int[NUM_ARITH_TBLS];

		for (int i = 0; i < NUM_ARITH_TBLS; i++)
		{
			mArithDCL[i] = 0;
			mArithDCU[i] = 1;
			mArithACK[i] = 5;
		}
	}


	public int[][][][] getCoefficients()
	{
		return mCoefficients;
	}


	public ColorSpace getColorSpace()
	{
		if (mColorSpaceTransform != null)
		{
			switch (mColorSpaceTransform.getTransform())
			{
				case 1:
					return JPEGImageIO.createColorSpaceInstance("ycbcr");
				case 2:
					return JPEGImageIO.createColorSpaceInstance("ycck");
				default:
					return JPEGImageIO.createColorSpaceInstance(mSOFSegment.getComponents().length == 3 ? "rgb" : "cmyk");
			}
		}

		return JPEGImageIO.createColorSpaceInstance(mSOFSegment.getColorSpaceName());
	}
}


Question: package org.terifan.imageio.jpeg.encoder;
import java.io.IOException;
import org.terifan.imageio.jpeg.APP0Segment;
import org.terifan.imageio.jpeg.APP2Segment;
import org.terifan.imageio.jpeg.ComponentInfo;
import org.terifan.imageio.jpeg.CompressionType;
import org.terifan.imageio.jpeg.DACSegment;
import org.terifan.imageio.jpeg.DHTSegment;
import org.terifan.imageio.jpeg.DQTSegment;
import org.terifan.imageio.jpeg.JPEG;
import org.terifan.imageio.jpeg.Log;
import org.terifan.imageio.jpeg.SOSSegment;
import org.terifan.imageio.jpeg.SegmentMarker;



public class JPEGImageWriterImpl
{
	public void create(JPEG aJPEG, BitOutputStream aOutput, Log aLog) throws IOException
	{
		aOutput.writeInt16(SegmentMarker.SOI.CODE);

		new APP0Segment(aJPEG).encode(aOutput).log(aLog);

		if (aJPEG.mICCProfile != null)
		{
Answer: 			new APP2Segment(aJPEG).setType(APP2Segment.ICC_PROFILE).encode(aOutput);