(此段文字摘自云风的《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 段.
首先,要能够读取二进制文件,因为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文件。以下只展示了部分的类定义,其余部分在后面章节展示
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();
......
}
接下来是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;
}
}
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");
}
}