确定JPEG(JFIF)图像的大小

35

我需要找到JPEG(JFIF)图像的大小。该图像未保存为独立文件,因此我无法使用 GetFileSize 或任何其他 API(图像位于流中,除JPEG/JFIF头之外没有其他标头)。

我进行了一些研究,发现JPEG图像由不同部分组成,每个部分都以帧标记(0xFF 0xXX)开头,以及该帧的大小。使用这些信息,我能够从文件中解析出很多信息。

问题在于,我找不到压缩数据的大小,因为似乎没有压缩数据的帧标记。此外,压缩数据似乎紧随SOS(FFDA)标记,并且图像以图像结束(EOI)(FFD9)标记结束。

一种解决方法是逐字节搜索EOI标记,但我认为压缩数据可能包含这些字节的组合,对吗?

有没有一种简单而正确的方法来找到图像的总大小?(我更喜欢没有任何外部库的代码/想法)

基本上,我需要起始图像(SOI-FFE0)和结束图像(EOI-FFD9)之间的距离(以字节为单位)。


哦...JFIF文件中的SOS标记?..我感觉在规格说明中漏掉了什么... - jayarjo
原帖说“没有文件”。他说有一个SOS和一个EOI。不知何故,他嵌入了一个没有外部包装的JFIF流。 - Jesse Chisholm
5个回答

41

压缩数据不包括SOI或EOI字节,因此您是安全的。但是,评论、应用程序数据或其他标头可能会包含这些字节。幸运的是,您可以根据给定的长度标识和跳过这些部分。

JPEG规范告诉您所需的信息:
http://www.w3.org/Graphics/JPEG/itu-t81.pdf

请查看第32页的B.1表格。带有 * 的符号后面没有长度字段 (RST、SOI、EOI、TEM),其他都有。

您需要跳过各种字段,但这并不太困难。

如何进行:

  1. 开始读取SOI (FFD8),这是起点。它应该是流中的第一件事。

    • 然后,通过文件,查找更多标记并跳过标头:

    • SOI标记 (FFD8): 损坏的图像。您应该已经找到了EOI!

    • TEM (FF01): 独立标记,请继续。

    • RST (FFD0FFD7): 独立标记,请继续。您可以验证重启标记从 FFD0FFD7 计数并重复,但这对于测量长度不是必需的。

    • EOI标记 (FFD9): 完成!

    • 任何不是RST、SOI、EOI、TEM (FF01FFFE,减去上述例外) 的标记: 在标记之后,读取下2个字节,这是该帧头的16位大端长度(不包括2个字节的标记,但包括长度字段)。跳过给定的数量 (通常是长度减2,因为您已经获取了这些字节)。

  • 如果在 EOI 之前得到一个文件结尾标记,那么你就得到了一个损坏的图像。

  • 一旦你得到了 EOI,你就通过了 JPEG 并且应该有长度信息。如果你期望数据流中有多个 JPEG ,则可以通过读取另一个 SOI 来重新开始。


  • 5
    这对我很有帮助,但我发现另一个参考资料说,在找到SOS标记后,你需要开始阅读数据,寻找EOI标记,那就是结尾。http://gvsoft.homedns.org/exif/Exif-explanation.html这与我目前正在处理的图像看到的相符。 - Tom Ritter
    你的意思是所有这些标记都存在于JFIF中吗? 我认为它们是EXIF规范的一部分,而EXIF规范又通常不兼容JFIF?..我错过了什么地方吗? - jayarjo
    9
    这里缺少的是,当你找到一个SOS(扫描开始)标记时,你不仅必须跳过标记段本身,还要跳过紧随其后的熵编码段。标记不能出现在熵编码段内,因此请继续扫描直到读取到FF,后面紧接着的任何字节都不等于0。(参见B.1.1.5“熵编码数据段”,注2。) - devconsole
    这是关于编程的内容,请将以下文本从英语翻译成中文。仅返回已翻译的文本:它是正确的,有关更多信息,请参见此链接:http://www.media.mit.edu/pia/Research/deepview/exif.html - Trong Vu
    @devconsole 它是如何丢失的?如果你在文件中找到标记并继续前进,那么你将会跳过 FFDA 之后的所有数据字节。 - IS4

    3
    也许是这样的:

    可能是这个样子的

    int GetJpgSize(unsigned char *pData, DWORD FileSizeLow, unsigned short *pWidth, unsigned short *pHeight)
    {
      unsigned int i = 0;
    
    
      if ((pData[i] == 0xFF) && (pData[i + 1] == 0xD8) && (pData[i + 2] == 0xFF) && (pData[i + 3] == 0xE0)) {
        i += 4;
    
        // Check for valid JPEG header (null terminated JFIF)
        if ((pData[i + 2] == 'J') && (pData[i + 3] == 'F') && (pData[i + 4] == 'I') && (pData[i + 5] == 'F')
            && (pData[i + 6] == 0x00)) {
    
          //Retrieve the block length of the first block since the first block will not contain the size of file
          unsigned short block_length = pData[i] * 256 + pData[i + 1];
    
          while (i < FileSizeLow) {
            //Increase the file index to get to the next block
            i += block_length; 
    
            if (i >= FileSizeLow) {
              //Check to protect against segmentation faults
              return -1;
            }
    
            if (pData[i] != 0xFF) {
              return -2;
            } 
    
            if (pData[i + 1] == 0xC0) {
              //0xFFC0 is the "Start of frame" marker which contains the file size
              //The structure of the 0xFFC0 block is quite simple [0xFFC0][ushort length][uchar precision][ushort x][ushort y]
              *pHeight = pData[i + 5] * 256 + pData[i + 6];
              *pWidth = pData[i + 7] * 256 + pData[i + 8];
    
              return 0;
            }
            else {
              i += 2; //Skip the block marker
    
              //Go to the next block
              block_length = pData[i] * 256 + pData[i + 1];
            }
          }
    
          //If this point is reached then no size was found
          return -3;
        }
        else {
          return -4;
        } //Not a valid JFIF string
      }
      else {
        return -5;
      } //Not a valid SOI header
    
      return -6;
    }  // GetJpgSize
    

    2

    由于您没有发布任何语言,我不确定这是否有效,但:

    您可以Stream.Seek(0,StreamOffset.End); ,然后获取流的位置吗?

    请明确您正在使用的框架。

    事实上,如果文件头未指定预期大小,则必须查找(或读取)图像的末尾。

    编辑

    由于您要流式传输多个文件,因此需要使用适合流式传输的容器格式。

    OGG应该很适合这个需求。

    JPEG实际上已经适合流式传输,但是您必须保证每个文件在发送到流之前都有一个有效的终止符,否则会因意外输入而导致应用程序崩溃。


    我可以使用C或Perl,两者都可以。但请注意,我不能使用任何形式的GetFileSize / GetStreamSize,因为我的流可能包含多张图片或图片之后的任何其他信息。 - botismarius
    问题在于如果你传输一个不完整的JPEG文件,你将永远看不到终止符。JPEG文件期望成为流中唯一的内容。请参见我的答案的最后一句话。 - John Gietzen
    所以,基本上你的意见是如果没有添加额外的头文件,就不能将两个JPEG图像放入一个流中?换句话说,你的意思是JPEG头文件无法告诉JPEG图像的大小? - botismarius
    基本上是这样的。虽然我不能确定JFIF文件格式是否不包含这些信息,但如果你要一次发送两个文件流,你绝对需要自己进行分帧处理。或许你可以考虑使用Ogg容器格式。 - John Gietzen
    不是我选择改变流格式。它就是这样,我必须尽力处理它 :) - botismarius
    好的,只需交叉手指,希望您能正确接收结束块。 - John Gietzen

    0
    在Python中,您可以将整个文件读入字符串对象中,并查找FF E0的第一次出现和FF D9的最后一次出现。假定这些是您正在寻找的开头和结尾?
    f = open("filename.jpg", "r")
    s = f.read()
    start = s.find("\xff\xe0")
    end = s.rfind("\xff\xd9")
    imagesize = end - start
    

    3
    在 JPEG 图像中间出现 \xff\xd9 是完全有可能的。任何两个字节匹配该模式的概率为 1/65536。 - Mark Ransom
    是的,这是正确的。然而,假设您有一个有效的JPEG文件,find和rfind将分别返回字符串的第一次和最后一次出现。我认为可以相当安全地假设第一次出现是图像数据的开始,最后一次出现是结束? - Chinmay Kanchi
    2
    问题在于找到最后一个,在不知道图像结尾在哪里的情况下。OP似乎打算通过同一流传输多个JPEG文件。 - Greg Hewgill
    另一个问题是 OP 没有一个“文件”,而是一个在 JFIF/JPEG 图像之后有东西的流。 - Jesse Chisholm
    @MarkRansom "在jpeg图像中间出现\xff\xd9是完全可能的。任何两个字节匹配该模式的概率为1/65536" - 是和不是:根据JPEG/JFIF规范,B.1.1.5节要求初始熵编码输出中的任何0xFF字节都必须写入文件/流作为0xFF 0x00,因此,虽然0xFF 0xD9可以出现在JPEG文件的任何位置,只要它存在于SOS标记(0xFF 0xDA)之后(并且在任何其他EOI标记之前),它将始终指示EOI标记,而永远不会是实际的巧合JPEG图像数据。 - Dai
    @Dai 很高兴知道这个,我不知道JPEG有这样的规则。我一直以为它和随机字节流无法区分。 - Mark Ransom

    0
    在C#和.NET的情况下,有一个简单的解决方案。没有必要手动解析任何内容。它会读取整个集群,但不会读取完整的文件内容。
    using (var fileStream = new FileStream(imagePath, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        using (var image = Image.FromStream(fileStream, false, false))
        {       
             var height = image.Height;
             var width = image.Width;
        }
    }
    

    来源:GitHub参考资料

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接