可靠地识别JPG?

时间:2019-01-04 14:21:52

标签: parsing delphi jpeg md5

为了识别和比较从相机拍摄的JPG图像,我想计算JPG内图像扫描部分的MD5哈希值。我的想法是获取SOS和EOI标记之间的字节,并基于以下假设对这些字节执行哈希操作:这些字节除非处理和更改实际的图像,否则永远不会更改。

显然,这个问题已经123出现过几次。有人提出了较为复杂的解决方案,这一事实令我恼怒的是,我看似相当简单但显然有效的方法。 (或者说真的太简单了吗?)

我知道JPG文件中可以有多对SOS($ FFDA)和EOI($ FFD9),在我目前的文件中有3:缩略图,实际图像和附加的1920x1080图像(Sony)。我目前的方法是解析流并找到下一个SOS,然后查找EOI,计算大小并假设实际大小(如果大小超过文件大小的50%)。

这种方法适用于我目前的文件。我用exiftool -all= image.jpg从JPG文件中剥离了所有元数据,并发现MD5哈希相同。但是对于我来说,该算法似乎相当粗糙。 所以这是我的问题:

仅检查SOS和EOI之间的空间是否有可能失败?我已经读过this,但仍然不确定。

从实际图像的SOS解析每个字节需要很多时间。我从here得出结论,找到压缩数据的结尾没有捷径可走。但是我可能会从第二个SOS标记向前跃升80%左右。我说的是相机的图像-我可以在多大程度上依靠一个事实,即首先出现缩略图,然后才是实际图像?

我应该在SOS(here之后的6个字节处开始吗?)

有更好的方法吗?

2 个答案:

答案 0 :(得分:0)

  

仅仅检查SOS和EOI之间的空间是否有可能失败?

是的,如果您仅对扫描数据进行校验和,则可达到目的。它们之间可能有多个SOS标记和其他标记。

答案 1 :(得分:0)

在进行了一些研究并进行了一系列测试之后,我介绍了我的问题的解决方案。

首先,我想澄清一下,我们 不是 正在谈论法医调查。可能存在一些方法来操纵JPG图像,以使标记出现在不应出现的位置,而不会出现在根据规范需要出现的位置。

我们 不是 都在谈论图像的身份或相似性。如果无损地旋转JPG,则您仍然具有完全相同的图像信息,但不再具有相同的图像。我们也不是在谈论以任何其他方式调整大小,优化或更改过的图像。

我们正在谈论的

是识别已重命名或元数据已被修改或删除但图像本身从未被处理过的简单重复项或JPG。或以任何方式篡改。

  

SOS和EOI标记之间的字节哈希是否是唯一标识图像的可靠方法?

是的。在合理的范围内,图像扫描数据的两个具有相同MD5校验和的文件不可能包含不相同的图像,反之亦然。
我检查了使用12家不同制造商的相机拍摄的样本照片,并编辑/剥离了元数据。实际上,这并不是真正必要的,因为您从规范和代码中知道所有元数据都位于单独的块中(这就是为什么您可以在JPG中隐藏所有内容)和扫描数据的原因元数据操作将永远不会碰到它,但是是的,到处都是相同的MD5校验和。

  

有什么方法可以快速找到(正确的)SOS标记?

当然。 JPG规范是一个烂摊子。在尝试了许多代码之后,我发现NativeJPGNils Haeck是最简单的。 改编自sdJpegImage

function FindSOSPos(S: TStream): Cardinal;
var
  B, MarkerTag, BytesRead: byte;
  Size,W: word;
const
  mkNone = 0; mkSOF0 = $c0; mkSOF1 = $c1; mkSOF2 = $c2; mkSOF3 = $c3; mkSOF5 = $c5; 
  mkSOF6 = $c6; mkSOF7 = $c7; mkSOF9 = $c9; mkSOF10 = $ca; mkSOF11 = $cb; mkSOF13 = $cd; 
  mkSOF14 = $ce; mkSOF15 = $cf; mkDHT = $c4; mkDAC = $cc; mkSOI = $d8; mkEOI = $d9; mkSOS = $da; 
  mkDQT = $db; mkDNL = $dc; mkDRI = $dd; mkDHP = $de; mkEXP = $df; mkAPP0 = $e0; mkAPP15 = $ef; mkCOM = $fe; 
begin
  Repeat
    Result := 0;
    // Read markers from the stream, until a non $FF is encountered
    If S.Read(B, 1) = 0 then
      exit;
    // Do we have a marker?
    if B = $FF then
    begin
      BytesRead := S.Read(MarkerTag, 1);
      while (BytesRead > 0) and (MarkerTag = $FF) do
      begin
        MarkerTag := mkNone;
        BytesRead := S.Read(MarkerTag, 1);
      end;
      Size := 0;
      if MarkerTag in [mkAPP0..mkAPP15, mkDHT, mkDQT, mkDRI,
        mkSOF0, mkSOF1, mkSOF2, mkSOF3, mkSOF5, mkSOF6, mkSOF7, mkSOF9, mkSOF10, mkSOF11, mkSOF13, mkSOF14, mkSOF15,
        mkCOM, mkDNL] then
      begin
        // Read length of marker
        If S.Read(W, 2) = 2 then
          Size := Swap(W) - 2
        else exit;
      end else
        If MarkerTag = mkSOS
          then break;
      S.Position := S.Position + Size;
    end else
    begin
      // B <> $FF is an error, we try to be flexible
      repeat
        BytesRead := S.Read(B, 1);
      until (BytesRead = 0) or (B = $FF);
      if BytesRead = 0 then
        exit;
      S.Seek(-1, soFromCurrent);
    end;
  Until (MarkerTag = mkSOS) or (MarkerTag = mkNone);
  Result := S.Position;
end; 
  

忽略SOS标记后的前6个字节?

我决定对SOS和EOI之间的所有内容进行散列,不包括标记本身。

  

是否可以快速找到尾随的EOI标记?

不。但这是无关紧要的,因为要执行散列,您必须始终读取每个字节。

  

此方法的可靠性如何?

正如我所说,我相信在合理范围内这种方法不会产生误报的可能性实际上是100%。关于找到正确的图像:NativeJPG已经存在了10多年了,您发现抱怨的很少,即使有,他们也致力于解码图像而不丢失它。

在我的应用程序中,我提供了在UserComment字段中存储原始文件名,EXIF DateTimeDigitized,相机型号,GPS坐标和扫描数据(完整和前16 kB)的MD5哈希的选项。我非常有信心,这将允许以后在大多数情况下(如果UserComment保持完好无损)识别文件。