使用Java程序读取JPEG文件

一、JPEG文件格式简介

(此段文字摘自云风的《JPEG简易文档》,我觉得写得简单明了,有兴趣的同学可以直接度娘)

附:JPEG 文件格式

~~~~~~~~~~~~~~~~


  - 文件头 (2 bytes):  $ff, $d8 (SOI) (JPEG 文件标识)
  - 任意数量的段 , 见后面
  - 文件结束 (2 bytes): $ff, $d9 (EOI)


段的格式:
~~~~~~~~~


  - header (4 bytes):
       $ff     段标识
        n      段的类型 (1 byte)
       sh, sl  该段长度, 包括这两个字节, 但是不包括前面的 $ff 和 n.
               注意: 长度不是 intel 次序, 而是 Motorola 的, 高字节在前,
      低字节在后!
  - 该段的内容, 最多 65533 字节


 注意:
  - 有一些无参数的段 (下面那些前面注明星号的)
    这些段没有长度描述 (而且没有内容), 只有 $ff 和类型字节.
  - 段之间无论有多少 $ff 都是合法的, 必须被忽略掉.


段的类型:
~~~~~~~~~


   *TEM   = $01   可以忽略掉


    SOF0  = $c0   帧开始 (baseline JPEG), 细节附后
    SOF1  = $c1   dito
    SOF2  = $c2   通常不支持
    SOF3  = $c3   通常不支持


    SOF5  = $c5   通常不支持
    SOF6  = $c6   通常不支持
    SOF7  = $c7   通常不支持


    SOF9  = $c9   arithmetic 编码(Huffman 的一种扩展算法), 通常不支持
    SOF10 = $ca   通常不支持
    SOF11 = $cb   通常不支持


    SOF13 = $cd   通常不支持
    SOF14 = $ce   通常不支持
    SOF14 = $ce   通常不支持
    SOF15 = $cf   通常不支持


    DHT   = $c4   定义 Huffman Table,  细节附后
    JPG   = $c8   未定义/保留 (引起解码错误)
    DAC   = $cc   定义 Arithmetic Table, 通常不支持


   *RST0  = $d0   RSTn 用于 resync, 通常被忽略
   *RST1  = $d1
   *RST2  = $d2
   *RST3  = $d3
   *RST4  = $d4
   *RST5  = $d5
   *RST6  = $d6
   *RST7  = $d7


    SOI   = $d8   图片开始
    EOI   = $d9   图片结束
    SOS   = $da   扫描行开始, 细节附后
    DQT   = $db   定义 Quantization Table, 细节附后
    DNL   = $dc   通常不支持, 忽略
    DRI   = $dd   定义重新开始间隔, 细节附后
    DHP   = $de   忽略 (跳过)
    EXP   = $df   忽略 (跳过)


    APP0  = $e0   JFIF APP0 segment marker (细节略)
    APP15 = $ef   忽略


    JPG0  = $f0   忽略 (跳过)
    JPG13 = $fd   忽略 (跳过)
    COM   = $fe   注释, 细节附后


 其它的段类型都保留必须跳过


SOF0: Start Of Frame 0:
~~~~~~~~~~~~~~~~~~~~~~~


  - $ff, $c0 (SOF0)
  - 长度 (高字节, 低字节), 8+components*3
  - 数据精度 (1 byte) 每个样本位数, 通常是 8 (大多数软件不支持 12 和 16)
  - 图片高度 (高字节, 低字节), 如果不支持 DNL 就必须 >0
  - 图片宽度 (高字节, 低字节), 如果不支持 DNL 就必须 >0
  - components 数量(1 byte), 灰度图是 1, YCbCr/YIQ 彩色图是 3, CMYK 彩色图
    是 4
  - 每个 component: 3 bytes
     - component id (1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q)
     - 采样系数 (bit 0-3 vert., 4-7 hor.)
     - quantization table 号


DRI: Define Restart Interval:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


  - $ff, $dd (DRI)
  - 长度 (高字节, 低字节), 必须是 4
  - MCU 块的单元中的重新开始间隔 (高字节, 低字节),
    意思是说, 每 n 个 MCU 块就有一个 RSTn 标记.
    第一个标记是 RST0, 然后是 RST1 等, RST7 后再从 RST0 重复 


DQT: Define Quantization Table:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


  - $ff, $db (DQT)
  - 长度 (高字节, 低字节)
  - QT 信息 (1 byte):
     bit 0..3: QT 号(0..3, 否则错误)
     bit 4..7: QT 精度, 0 = 8 bit, 否则 16 bit
  - n 字节的 QT, n = 64*(精度+1)


 备注:
  - 一个单独的 DQT 段可以包含多个 QT, 每个都有自己的信息字节
  - 当精度=1 (16 bit), 每个字都是高位在前低位在后


DAC: Define Arithmetic Table:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 法律原因, 现在的软件不支持 arithmetic 编码.
 不能生产使用 arithmetic 编码的 JPEG 文件


DHT: Define Huffman Table:
~~~~~~~~~~~~~~~~~~~~~~~~~~


  - $ff, $c4 (DHT)
  - 长度 (高字节, 低字节)
  - HT 信息 (1 byte):
     bit 0..3: HT 号 (0..3, 否则错误)
     bit 4   : HT 类型, 0 = DC table, 1 = AC table
     bit 5..7: 必须是 0
  - 16 bytes: 长度是 1..16 代码的符号数. 这 16 个数的和应该 <=256
  - n bytes: 一个包含了按递增次序代码长度排列的符号表
    (n = 代码总数)


 备注:
  - 一个单独的 DHT 段可以包含多个 HT, 每个都有自己的信息字节


COM: 注释:
~~~~~~~~~~


  - $ff, $fe (COM)
  - 注释长度 (高字节, 低字节) = L+2
  - 注释为长度为 L 的字符流


SOS: Start Of Scan:
~~~~~~~~~~~~~~~~~~~


  - $ff, $da (SOS)
  - 长度 (高字节, 低字节), 必须是 6+2*(扫描行内组件的数量)
  - 扫描行内组件的数量 (1 byte), 必须 >= 1 , <=4 (否则是错的) 通常是 3
  - 每个组件: 2 bytes
     - component id (1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q), 见 SOF0
     - 使用的 Huffman 表:
- bit 0..3: AC table (0..3)
- bit 4..7: DC table (0..3)
  - 忽略 3 bytes (???)


 备注:
  - 图片数据 (一个个扫描行) 紧接着 SOS 段.


二、Java读取二进制文件

首先,要能够读取二进制文件,因为JPEG文件其实上也就是二进制编码的格式文件,只是有它特定的编码方式而已。

class BinaryFile {

	static byte[] read(File bFile) throws IOException {

		BufferedInputStream bf = new BufferedInputStream(
			new FileInputStream(bFile));

		try {
			byte[] data = new byte[bf.available()];
			bf.read(data);
			return data;
		} finally {
			bf.close();
		}
	}

	static byte[] read(String bFile) throws IOException {
		return read(new File(bFile).getAbsoluteFile());
	}

}

此类将文件以二进制形式读取到内存中,保存为字节数组。


三、使用Java定义JPEG格式的类

定义一个Java类,来表示JPEG文件。以下只展示了部分的类定义,其余部分在后面章节展示

class JPEGFile {

	enum SectionType
	{
		TEM,
		SOF0, SOF1, SOF2, SOF3, SOF5, SOF6, SOF7, SOF9, SOF10, SOF11, SOF13, SOF14, SOF15, 
		DHT, JPG, DAC, 
		RST0, RST1, RST2, RST3, RST4, RST5, RST6, RST7,
		SOI, EOI, SOS, DQT, DNL, DRI, DHP, EXP, 
		APP0, APP15, JPG0, JPG13, COM, NOP,
	};

	static HashMap m_mapSectionType = new HashMap();

	static {
		m_mapSectionType.put(0x01, SectionType.TEM);
		m_mapSectionType.put(0xc0, SectionType.SOF0);
		m_mapSectionType.put(0xc1, SectionType.SOF1);
		m_mapSectionType.put(0xc2, SectionType.SOF2);
		m_mapSectionType.put(0xc3, SectionType.SOF3);

		m_mapSectionType.put(0xc5, SectionType.SOF5);
		m_mapSectionType.put(0xc6, SectionType.SOF6);
		m_mapSectionType.put(0xc7, SectionType.SOF7);

		m_mapSectionType.put(0xc9, SectionType.SOF9);
		m_mapSectionType.put(0xca, SectionType.SOF10);
		m_mapSectionType.put(0xcb, SectionType.SOF11);

		m_mapSectionType.put(0xcd, SectionType.SOF13);
		m_mapSectionType.put(0xce, SectionType.SOF14);
		m_mapSectionType.put(0xcf, SectionType.SOF15);

		m_mapSectionType.put(0xc4, SectionType.DHT);
		m_mapSectionType.put(0xc8, SectionType.JPG);
		m_mapSectionType.put(0xcc, SectionType.DAC);

		m_mapSectionType.put(0xd0, SectionType.RST0);
		m_mapSectionType.put(0xd1, SectionType.RST1);
		m_mapSectionType.put(0xd2, SectionType.RST2);
		m_mapSectionType.put(0xd3, SectionType.RST3);
		m_mapSectionType.put(0xd4, SectionType.RST4);
		m_mapSectionType.put(0xd5, SectionType.RST5);
		m_mapSectionType.put(0xd6, SectionType.RST6);
		m_mapSectionType.put(0xd7, SectionType.RST7);

		m_mapSectionType.put(0xd8, SectionType.SOI);
		m_mapSectionType.put(0xd9, SectionType.EOI);
		m_mapSectionType.put(0xda, SectionType.SOS);
		m_mapSectionType.put(0xdb, SectionType.DQT);
		m_mapSectionType.put(0xdc, SectionType.DNL);
		m_mapSectionType.put(0xdd, SectionType.DRI);
		m_mapSectionType.put(0xde, SectionType.DHP);
		m_mapSectionType.put(0xdf, SectionType.EXP);

		m_mapSectionType.put(0xe0, SectionType.APP0);
		m_mapSectionType.put(0xef, SectionType.APP15);

		m_mapSectionType.put(0xf0, SectionType.JPG0);
		m_mapSectionType.put(0xfd, SectionType.JPG13);
		m_mapSectionType.put(0xfe, SectionType.COM);

		m_mapSectionType.put(0xff, SectionType.NOP);
	};

	enum ERROR_CODE
	{
		ERR_OK,
		ERR_NOT_JEPG_FILE,
	};

	//SOF0: Start Of Frame 0:
	class JSOF0 {
		byte m_byPrecision;
		byte m_byHeight;
		byte m_byWidth;
		byte m_byComponentNum;

		class JComponent {
			byte m_byId;
			byte m_byFactor;
			byte m_byQTId;
		}
		ArrayList m_arrComponents = new ArrayList();
	}
	JSOF0 m_oSOF0 = new JSOF0();

	//DQT: Define Quantization Table:
	class JDQT {
		byte m_byQTInfo;

		class JQT {
			byte [] m_arrByte = new byte[64];
		}
		ArrayList m_arrQT = new ArrayList();

		void ReadQTArray(ByteArrayInputStream bais, int n) throws IOException{
			for (int i = 0; i < n + 1; ++i) {
				JQT qt = new JQT();
				bais.read(qt.m_arrByte);
				m_arrQT.add(qt);
			}
		}
	}
	ArrayList m_arrDQT = new ArrayList();

......
}

类的开始部分定义了各个段的标示符,接下来对SOF0和DQT段分别定义了内部类,这两个段是我们需要读取数据的段,其他的段我们暂时不分析,直接跳过,当然,也可以增加其他段的内部类,来读取段数据。


四、Java的JPEG类读取数据

  接下来是JPEGFile类的读取JPEG数据的部分。


class JPEG
{
  ......
  SectionType GetSectionType(int iFirstByte, int iSecondByte) {
		iFirstByte = iFirstByte & 0xff;
		iSecondByte = iSecondByte & 0xff;

		System.out.println("GetSectionType(" + iFirstByte + ", " + iSecondByte + ")");
		if (0xff != iFirstByte) return SectionType.NOP;

		if (!m_mapSectionType.containsKey(iSecondByte)) return SectionType.NOP;

		return m_mapSectionType.get(iSecondByte);
	}

	int GetSectionLen(int iByteHigh, int iByteLow) {

		System.out.println("GetSectionLen(" + iByteHigh + ", " + iByteLow + ")");

		int iBlockLen = ((iByteHigh << 8) + iByteLow) & 0xffff;

		System.out.println("Section length: " + iBlockLen + " bytes");

		return iBlockLen;
	}

	void JumpOverSection(ByteArrayInputStream bais, SectionType eSectionType) {

		System.out.println("Section[" + eSectionType + "] jump over");

		int iBlockLen = GetSectionLen(bais.read(), bais.read());

		bais.skip(iBlockLen - 2);

	}

	void ReadDQT(ByteArrayInputStream bais) throws IOException{
		
		System.out.println("----------Section[DQT]----------");

		System.out.println("Section[DQT] reading ...");

		int iBlockLen = GetSectionLen(bais.read(), bais.read());

		JDQT dqt = new JDQT();
		dqt.m_byQTInfo = (byte)bais.read();

		//check validation

		//QT
		int n = (dqt.m_byQTInfo >> 4) & 0xf;
		dqt.ReadQTArray(bais, n);

		m_arrDQT.add(dqt);

		System.out.println("Section[DQT] read " + (64 * (n + 1) + 1) + " bytes");

		System.out.println("");
	}

	//
	ERROR_CODE readFromFile(String sFilename) {

		try {
			byte[] byteArr = BinaryFile.read(sFilename);

			ByteArrayInputStream bais = new ByteArrayInputStream(byteArr);

			if (bais.available() < 2) {
				System.out.println("File length is too small");
				return ERROR_CODE.ERR_OK;
			}

			//JPEG header
			SectionType eSectionType = GetSectionType(bais.read(), bais.read());
			if (eSectionType != SectionType.SOI) {
				System.out.println("File is not JPEG File");
				return ERROR_CODE.ERR_NOT_JEPG_FILE;
			}

			//Iterate Section
			System.out.println("File is JPEGFile");
			System.out.println("Start iterating sections ...");

			Boolean bStartScanning = false;
			while (bais.available() > 0) {
				int iByteHigh = bais.read();
				int iByteLow = bais.read();

				if (bStartScanning) {
					continue;
				}

				eSectionType = GetSectionType(iByteHigh, iByteLow);
				switch (eSectionType) {
					case NOP:
						continue;
					case SOF0:
						JumpOverSection(bais, eSectionType);
						break;
					case SOF2:
						JumpOverSection(bais, eSectionType);
						break;
					case DRI:
						JumpOverSection(bais, eSectionType);
						break;
					case DQT:
						ReadDQT(bais);
						break;
					case DHT:
						JumpOverSection(bais, eSectionType);
						break;
					case COM:
						JumpOverSection(bais, eSectionType);
						break;
					case SOS:
						bStartScanning = true;
						JumpOverSection(bais, eSectionType);
						break;
					default:
						JumpOverSection(bais, eSectionType);
						break;
				}

				
			}


		} catch (IOException e) {
			System.out.println("IOException: " + e);
		}

		return ERROR_CODE.ERR_OK;
	}
}



GetSectionType获取段的类型,GetSectionLen获取段的长度,JumpOverSection跳过当前段的数据,ReadDQT读取DQT段的数据。

readFromFile利用BinaryFile读取文件到内存的字节数组,判断JPEG头。如果是JPEG文件,那么逐段的读入数据。我们这里只处理了DQT段,其他的段我们都跳过了;在SOS段我们设置了bStartScanning标示,这个SOS段标示之后就是具体的图像数据,可以开始扫描了。

五、类的使用方法

如下使用此类
public class Hello {
	public static void main(String[] args) {
		
		JPEGFile jpg = new JPEGFile();

		jpg.readFromFile("JUC801.jpg");
	
	}
}

六、总结

本文提供了最简单的读取JPEG各段的方法,在此基础上可以继续分析JPEG的各个数据段和具体图像数据。


你可能感兴趣的:(Java)