JPEG文件分析与无损压缩实践

 分类: 杂谈

JPEG格式的图片是最常见的图片格式,它采用有损压缩算法,很好的处理了图片体积和质量之间的关系,更有利于传播,所以JPEG也是最常见的网络标准文件格式。使用JPEG格式压缩的图片文件一般也被称为JPEG Files,最普遍被使用的扩展名格式为.jpg。但JEPG标准只是一些压缩压法的描述,它并没有指定一个文件格式,为了能够保存和传输文件,一些文件格式标准被发明出来,最常见的文件格式是JPEG/Exif和JPEG/JFIF。JPEG和JIFF/Exif的关系类似于unicode和utf-8,一个用来编码一个用来存储。


JPEG文件一般来说采用有损压缩,所谓有损压缩,是指JPEG通过破坏性数据压缩达到减少图片尺寸,便于传输的目的。我们最常见的用photoshop对图片进行质量选择,质量越低,图片体积越小,质量越高,图片体积越大。 在web应用中,我们经常会被要求对图片进行压缩处理,以达到减少宽带占用,加快页面展示的目的。一些测试软件也经常会对web页面的图片大小给出建议,例如thinkwithgoogle。我们经常被告知最简单的方式是用photoshtop把图片调整成相应大小,“另存为web”格式,然后选择合适的质量即可。那么通过Photoshop压缩之后,真的可以高枕无忧,图片完全没有其它优化的余地了吗,要弄清这个问题,我们需要分析一下JPEG文件的存储格式。

JPEG元数据结构

JPEG文件包含多个数据段(segment),每个段包括不同类型的数据,数据段之间通过两个字节被称之为marker的标记分开,marker以0xff开头,后面一个接一个字节来表述不同类型的marker。一些marker只有单独两个字节,另外一些会包括数据的长度和数据。具体的结构如下:

JEPG的元数据存放在APPn段中,注释数据存放在com段中,一些厂商可能会在APPn段中添加他们自己的数据,例如相机型号,GPS信息等。

哪些信息可以被压缩

com是图片的注释信息,而APPn是应用程序存放的相关信息,这些信息对图片本身来说都没有作用,都是可以移出的,我们拿来一张PS处理之后的图片,用二进制打开,发现以下内容:

很显然,这些信息对于图片来说都是无用的内容,是可以删掉的,而且删掉之后,并不会影响图片质量。

如何压缩

PHP

<?php
class JPEG{
    //JPEG Metadata Structure
    private $markerkey=array("soi"=>0xd8,"sof0"=>0xc0,"sof2"=>0xc2,"dht"=>0xc4,"dqt"=>0xdb,"dri"=>0xdd,"sos"=>0xda,"rst"=>array(0xd0,0xd1,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7),"app"=>array(0xe0,0xe1,0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xeb,0xec,0xed,0xee,0xef),"com"=>0xfe,"eoi"=>0xd9);
    private $markerdata=array();
    private $ImageScan;
    private $Image;
    private $PosPointer;
    private $ImageSize;
    public function __construct($argument){
        if(file_exists($argument)){
            $this->Image = fopen($argument,"rb+");
            $this->ImageSize= filesize($argument);
        }else{
            die("File Not Exists");
        }
        if($this->GetByte(2)!==0xd8ff){
            die("Not a JPEG Image");
        }
        $this->Parse();
    }

    public function GetByte($len){
        if(feof($this->Image)) return NULL;
        $tmp =  fread($this->Image,$len);
        $tmp2=array();
        $this->PosPointer=ftell($this->Image);
        if($len==1){
            $tmp2=unpack("C",$tmp);
        }else if($len==2){
            $tmp2=unpack("S",$tmp);
        }else if($len==4){
            $tmp2=unpack("L",$tmp);
        }else{
            $tmp2[1]=$tmp;
        }
        return $tmp2[1];
    }
    public function GetMarkerData(){
        $buffer="";
        foreach($this->markerdata as $d){
            foreach($d as $c){
                $buffer.=$c;
            }
        }
        return $buffer;
    }

    public function get_marker($byte){
        foreach($this->markerkey as $meta=>$bit){
            if(is_array($bit)){
                foreach($bit as $b){
                    if($byte == $b) return $meta;
                }
            }
            if($byte == $bit) return $meta;
        }
        return NULL;
    }

    public function compress(){
        //Remove Com Marker
        $this->markerdata['com']=array();
        //Remove App Marker
        $this->markerdata['app']=array();
    }

    public function Parse(){
        while(($byte=$this->GetByte(1))!==NULL){
            $nxtbyte = @$this->GetByte(1);
            if($byte == 0xff){
                $marker = $this->get_marker($nxtbyte);
                //Variable Size Marker
                if($marker == "sof" || $marker == "sof2" || $marker == "dht" || $marker == "dqt" || $marker == "sof0" || $marker == "app" || $marker == "com"){
                    $offset1= $this->GetByte(1);
                    $offset2 = $this->GetByte(1);
                    $len = $offset1*256+$offset2;
                    $data = $this->GetByte($len-2);
                    //full data include marker and length
                    $fulldata = pack("C",0xff).pack("C",$nxtbyte).pack("C",$offset1).pack("C",$offset2).$data;
                    $this->markerdata[$marker][]= $fulldata;
                }else if($marker=="dri"){
                    //pass
                }else if($marker=="rst"){
                    //pass
                }else if($marker=="sos"){
                    //SOS Segment
                    $len = $this->ImageSize-$this->PosPointer-1;
                    $this->ImageScan =  pack("C",0xff). pack("C",0xda).$this->GetByte($len);
                }
            }
        }
    }
    public function GetImageBin(){
        return pack("S",0xd8ff).$this->GetMarkerData().$this->ImageScan.pack("S",0xd9ff);
    }
    public function Storage(){
        fseek($this->Image,0);
        fwrite($this->Image,$this->GetImageBin());
    }
    public function __destruct(){
        fclose($this->Image);
    }
}
$image = new JPEG("test.jpg");
$image->compress();
$image->Storage();

参考资料

  1. The Metadata in JPEG files
  2. Description of Exif file format
  3. The File Layout

发表回复

评论列表:

绿软库
绿软库
感谢分享
回复此留言
知道91博客
知道91博客
不错哦,如果博主这是原创,说明博客还是有点功底的
回复此留言