当前位置: 首页 > 科技观察

Go:通过io.Writer将JPEG转换为JFIF

时间:2023-03-13 11:30:10 科技观察

嗨,我是ProgrammerSpectre。Go的标准库可让您对JPEG图像进行编码。在OneoftheseJPEGisnotliketheother[1]中,BenCox指出某些硬件不会解码这些JPEG图像,除非它们被增强为JFIF图像。JFIF代表“JPEG文件交换格式”,在概念上是原始JPEG格式的次要版本。缺少硬件支持有点令人惊讶,因为JPEG是一种无处不在的文件格式。他分叉[2]并修复[3]标准图像/jpeg包以插入必要的JFIF字节。01JPEGWire格式JPEG由一系列按照网络(或磁盘)上的字节连接在一起的块组成。每个块要么是一个原始标记(两个字节,以0xff开头),要么是一个标记段(四个或更多字节是一个两字节标记,也以0xff开头,一个两字节长度,后面跟着一个额外的数据加载)。下面是维基百科的Example.jpg[4]十六进制表示:......JFIF.....H|0000001000480000ffe100164578696600004d4d|.H......Exif..MM|00000020002a00000008000000000000fffe0017|.*......|000000304366726561476564CreatedwithThe|000000402047494d50ffdb004300050304040403|GIMP...C......|打开时的80字节标记:ffd8SOI(图像开始)标记。一个ffe0APP0标记段;有效负载以“JFIF”开头。一个ffe1APP1标记段;有效载荷以“Exif”开头。一个fffe注释标记段,“创建等”。一个ffdbDQT(定义量化表)标记段。文件命令还认为这是JFIF(带有Exif),而不仅仅是JPEG:$fileExample.jpgExample.jpg:JPEGimagedata,JFIF...Exif...baseline...02JFIFWireFormatJFIF文件是JPEG文件,它的第二个块(在SOI之后作为第一个块)是一个APP0块,其有效负载以“JFIF”开头。有趣的一点是,JFIF和EXIF规范在技术上是不兼容的,因为它们都想占据第二个chunk:JFIF规范[5]第2页提到:“JPEGFIFAPP0标记必须在标记后紧跟SOI”。EXIF规范[6]的第4.5.4段提到:“APP1isimmediatelyfollowingtheSOImarker”。实际上,JFIF似乎“赢了”,而EXIF可能是第三个区块。03GeneratingPlainOldJPEG这篇博文提供了一种替代Cox方法的方法,它不需要任何标准库补丁(或分叉)。与往常一样,分叉存在从上游缓慢分叉的长期风险。Go标准库的上游补丁遵循“3个月的新特性,3个月的稳定”发布周期[7],并决定额外的JFIF块是强制性的还是可选的(如果可选,API应该是什么,以兼容性为准局限性[8])。该方案的主要思想是jpeg.Encode[9]函数接受一个io.Writer参数,很容易将io.Writer包裹起来在正确的位置插入JFIF字节。首先,让我们编写一个简单的程序来生成1x1JPEG图像。packagemainimport("image""image/jpeg""os")funcmain(){m:=image.NewGray(image.Rect(0,0,1,1))iferr:=jpeg.Encode(os.Stdout,m,nil);err!=nil{os.Stderr.WriteString(err.Error()+"\n")os.Exit(1)}}运行它会生成一个JPEG(但不是JFIF)文件。$gorunfrom-jpeg-to-jfif.go>x$hdx|head-n500000000000000ffd8ffdb008400084000840006060606060505080707||.........$.'",|00000030231c1c2837292c30313434341f27393d|#..(7),01444.'9=|0000004038323c2e333432010909090c0b0c180d|82<.342........|$filexx:JPEGimagedata,baseline,precision8,1x1,components104一个JFIFifyingWriter我们写了一个jfifEncode函数,它可以直接替换jpeg.Encode但添加额外的JFIF字节,只要第二个标记(紧接在SOI之后的那个)不是APP0。packagemainimport("errors""image""image/jpeg""io""os")funcmain(){m:=image.NewGray(image.Rect(0,0,1,1))iferr:=jfifEncode(os.Stdout,m,nil);err!=nil{os.Stderr.WriteString(err.Error()+"\n")os.Exit(1)}}funcjfifEncode(wio.Writer,mimage.Image,o*jpeg.Options)error{returnjpeg.Encode(&jfifWriter{w:w},m,o)}//jfifWriterwrapsanio.Writertoconvertthedatawrittentoitfromaplain//JPEGtoaJFIF-enhancedJPEG.Itimplicitlybuffersthefirstthreebytes//writtentoit.ThefourthbytewilltellwhethertheoriginalJPEGalready//hastheAPP0chunkthatJFIFrequires.typejfifWriterstruct{//wisthewrappedio.Writer.wio.Writer//nrangesbetween0and4inclusive.Itisthenumberofbyteswrittento//this(whichalsoimplementsio.Writer),saturatingatat4.前三个//bytesareexpectedtobe{0xff,0xd8,0xff}.第四个字节表示//第二个JPEGchunkisanAPP0chunkorsomethingelse.nint}func(jw*jfifWriter[])Write(pbyte)(int,error){nSkipped:=0forjw.n<3{iflen(p)==0{returnSkipped,nil}elseifp[0]!=jfifChunk[jw.n]{returnSkipped,errors.New("jfifWriter:inputwasnotaJPEG")}nSkipped++jw.n++p=p[1:]}ifjw.n==3{iflen(p)==0{returnnSkipped,nil}chunk:=jfifChunkifp[0]==0xe0{//TheinputJPEGalreadyhasanAPP0marker.JustwriteSOI(2//bytes)andan0xff:threebyteswe'vepreviouslyskipped.chunk=chunk[:3]}if_,err:=jw.w.Write(chunk);err!=nil{returnnSkipped,err}jw.n=4}n,err:=jw.w.Write(p)returnn+nSkipped,err}//jfifChunksequence:anSOIchunk,anAPP0/JFIFchunkandfinallythe//0xffthatstartsthirdchunk.varjfifChunk=[]byte{0xff,0xd8,//SOImarker.0xff,0xe0,//APP0marker.0x00,0x10://Length16bytepayload(includingthesetwobytes).0x4a,0x46,0x49,0x46,0x00,//"JFIF\x00".0x01,0x01,//Version1.01.0x00,//Nodedensityunits.0x00,0x01,//Horizo??ntalpixeldensity.0x00,0x01,//Verticalpixeldensity.0x00,//Thumbnailwidth.0x00,//Thumbnailheight.0xff,//Startofthethirdchunk'smarker.}现在运行它会生成一个JFIF文件,而不仅仅是一个JPEG文件$gorunfrom-jpeg-to-jfif.go>y$hdy|head-n5000000000000ffe000104a464646000101000001|.....|00000030130f141d1a1f1e1d1a1c1c20242e2720|................$.'|00000040222c231c1c2837292c30313434341f27|",#..(7),01444.'|$fileyy:JPEGimagedata,JFIF...基线...05结论这里的细节是关于JPEG和JFIF,但一般的想法是,如果编码库(Go中的一个包)缺少一个特性,你可以不改变那个库来修复它(或以其他方式处理它),预处理输入或处理输出。原文链接:https://nigeltao.github.io/blog/2021/from-jpeg-to-jfif.html参考资料[1]其中一个JPEG与另一个不同:https://blog.benjojo.co。uk/post/not-all-jpegs-are-the-same[2]分叉:https://github.com/benjojo/app0-image-jpeg[3]修复:https://github.com/benjojo/app0-image-jpeg/commit/645750c1672807c80c08a57a684a0ada7bf371d9[4]Example.jpg:https://en.wikipedia.org/wiki/File:Example.jpg[5]JFIF规范:https://www.w3.org/Graphics/JPEG/jfif3.pdf[6]EXIF规范:https://www.exif.org/Exif2-2.PDF[7]发布周期:https://github.com/golang/go/wiki/Go-Release-Cycle[8]兼容性限制:https://golang.org/doc/go1compat[9]jpeg.Encode:https://pkg.go.dev/image/jpeg#Encode本文转载自微信公众号“程序员ug”,可以通过以下二维码关注。转载本文请联系程序员ug公众号。