2019年07月17日(星期三)  农历:己亥年六月十五

作者:三年。分类: JAVA

 一、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 03: QT 号(03, 否则错误)

bit 47: 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 03: HT 号 (03, 否则错误)

bit 4 : HT 类型, 0 = DC table, 1 = AC table

bit 57: 必须是 0

- 16 bytes: 长度是 116 代码的符号数。 这 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 03: AC table (03)

- bit 47: DC table (03)

- 忽略 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<INTEGER, SectionType> m_mapSectionType = new HashMap<INTEGER, SectionType>();

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<JCOMPONENT> m_arrComponents = new ArrayList<JCOMPONENT>();

}

JSOF0 m_oSOF0 = new JSOF0();

//DQT: Define Quantization Table:

class JDQT {

byte m_byQTInfo;

class JQT {

byte [] m_arrByte = new byte[64];

}

ArrayList<JQT> m_arrQT = new ArrayList<JQT>();

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<JDQT> m_arrDQT = new ArrayList<JDQT>();

……

}

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

四、Java的JPEG类读取数据

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

class JPEGFile

{

……

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的各个数据段和具体图像数据。

温馨提示如有转载或引用以上内容之必要,敬请将本文链接作为出处标注,谢谢合作!

已有 0/1303 人参与

发表评论:



手Q扫描加入Java初学者群