Recuperar imagenes jpeg 2

En un artículo anterior hablaba de como recuperar las imágenes borradas de un disco, o tarjeta de memoria, examinando cluster a cluster en busca de las imágenes. Ese programa estaba pensado para utilizarlo directamente sobre un disco o sobre la imagen de un disco, no sirve para examinar otro tipo de archivos, pero usando un sistema parecido podríamos extraer imágenes de cualquier archivo. La única diferencia es que en vez de ir buscando cluster a cluster, tendríamos que ir buscando byte a byte. Este proceso es mucho mas lento así que no es el mas adecuado para examinar discos enteros, sin embargo es perfecto para explorar archivo de un tamaño normal.

El principio es el mismo de antes, buscamos la marca que indica el comienzo de un archivo jpeg, es decir, los bytes FFD8FF. Una vez encontrada, intentamos obtener, de los bytes que siguen a esos tres, una imagen jpeg, para luego continuar la búsqueda en el byte siguiente.

El programa necesita que se le pasen dos parámetros, el archivo de entrada y la plantilla que sirve para dar nombre a las imágenes recuperadas. La plantilla tiene que incluir "%d", esta variable sera sustituida por un valor que se va incrementando. Por ejemplo, si queremos guardar las imágenes en C:\Temp utilizaremos C:\Temp\%d.jpg

El código es el siguiente (es una aplicación de consola):

program JpegRecover;
 
{$APPTYPE CONSOLE}
 
uses
  SysUtils,
  Classes;
 
procedure Recover(Origen: TStream; FormatStr: String; var Index: Integer);
var
  Destino: TMemoryStream;
  Buffer: array[0..$FFFF] of Byte;
  Header: array[1..4] of Byte;
  Size: Integer;
begin
  Destino:= TMemoryStream.Create;
  try
    // Escribimos los 2 primeros bytes del archivo
    Header[1]:= $FF;  Header[2]:= $D8;
    Destino.WriteBuffer(Header,2);
    Origen.Seek(-1,soFromCurrent);
    // En este bucle vamos copiando segmento a segmento
    while TRUE do
    begin
      FillChar(Header,Sizeof(Header),#0);
      Origen.Read(Header,4);
      Size:= ((Header[3] shl 8) + Header[4]) - 2;
      case Header[2] of
        $01,$D0..$D8: begin
                        // Estos segmentos solo miden dos bytes,
                        // asi que escribimos 2 bytes y
                        Destino.WriteBuffer(Header,2);
                        // retrocedemos otros dos hacia atras.
                        Origen.Seek(-2,soFromCurrent);
                      end;
        $D9: begin
               // Este indica que llegamos al final del archivo jpeg
               Destino.WriteBuffer(Header,2);
               // Guardamos el archivo usando la plantilla
               Destino.SaveToFile(Format(FormatStr,[Index]));
               Writeln('Recuperada: ' + Format(FormatStr,[Index]));
               // Incrementamos el indice
               inc(Index);
               // Salimos del procedure
               Exit;
             end;
        $DA: begin
               // Este segmento no tiene tamaño
               Destino.WriteBuffer(Header,2);
               Origen.Seek(-2,soFromCurrent);
               // Buscamos el final byte a byte
               while TRUE do
               begin
                 Origen.ReadBuffer(Buffer,1);
                 // Posible comienzo de otro segmento
                 if Buffer[0] = $FF then
                 begin
                   Origen.ReadBuffer(Buffer,1);
                   // $FF00 = Secuencia de escape
                   // $FFD0 .. $FFD7 = Se deben de ignorar
                   if (Buffer[0] <> 0) and not (Buffer[0] in [$D0..$D7]) then
                   begin
                     // Si encontramos el comienzo de otro segmento,
                     // retrocedemos dos bytes, y volvemos al bucle principal
                     Origen.Seek(-2,soFromCurrent);
                     break;
                   end else
                   begin
                     // Si no es el comienzo de otro segmento, escribimos
                     // y continuamos
                     Header[1]:= $FF;
                     Header[2]:= Buffer[0];
                     Destino.WriteBuffer(Header,2);
                   end;
                 end else
                   Destino.WriteBuffer(Buffer,1);
               end;
             end
        else
        begin
          // Cualquier otro segmento lo copiamos directamente
          Origen.ReadBuffer(Buffer,Size);
          Destino.WriteBuffer(Header,4);
          Destino.WriteBuffer(Buffer,Size);
        end;
      end;
    end;
  finally
    Destino.Free;
  end;
end;
 
procedure Scanfile(Stream: TStream; FormatStr: String; var Index: Integer);
var
  B: Byte;
  Leidos: Integer;
  Posicion: int64;
begin
  // Vamos leyendo byte a byte el archivo
  Leidos:= Stream.Read(B,1);
  while Leidos = 1 do
    if B = $FF then
    begin
      Leidos:= Stream.Read(B,1);
      if Leidos = 1 then
        if B = $D8 then
        begin
          Leidos:= Stream.Read(B,1);
          if Leidos = 1 then
            if B = $FF then
            begin
              // Hasta encontrar la combinacion $FFD8FF
              Writeln('Encontrada una posible imagen.');
              // Guardamos la posicion
              Posicion:= Stream.Position;
              try
                // Intentamos recuperar la imagen
                Recover(Stream,FormatStr,Index);
              except
                // Si se produce un error lo mostramos, pero no salimos del bucle
                On E: Exception do
                   Writeln('Error: ' + E.Message);
              end;
              // Volvemos a la posicion en la que estabamos
              Stream.Position:= Posicion;
            end;
        end;
    end else
      Leidos:= Stream.Read(B,1);
end;
 
var
  i: Integer;
  Stream: TFileStream;
begin
  Writeln('JpegRecover 0.1 - Copyright (c) 2007 Domingo Seoane');
  Writeln('<a href="http://delphi.jmrds.com'">http://delphi.jmrds.com'</a>);
  Writeln;
  if ParamCount = 2 then
  try
    // Comprobamos que se pueden crear archivos en la ruta indicada
    TFileStream.Create(Format(ParamStr(2),[0]),fmCreate).Free;
    // Borramos el archivo de prueba
    DeleteFile(Format(ParamStr(2),[0]));
    i:= 0;
    // Creamos el Stream del archivo
    Stream:= TFileStream.Create(Paramstr(1),fmOpenRead);
    try
      Writeln('Explorando el archivo: ' + ParamStr(1));
      Scanfile(Stream,ParamStr(2),i);
    finally
      Stream.Free;
    end;
  except
    // Si se produce un error, lo mostramos
    On E: Exception do
      Writeln('Error: ' + E.Message);
  end;
end.

Por ejemplo, para buscar imágenes en el archivo "ejemplo.dat" y guardarlas en C:\Temp, usaríamos el siguiente comando:

JpegRecover ejemplo.dat C:\Temp\%d.jpg

El código completo y programa, los puedes bajar de aquí

Ahora le podemos buscar un uso mas "ludico" a este codigo, y por ejemplo fisgar un poco en los archivos "thumb.db". Estos archivos "thumb.db" (al parecer en Vista se llaman thumbcache_xxxx.db) son creados por el explorer cuando vemos una carpeta con imágenes utilizando las "Vistas en miniatura". Lo "divertido" es que tiene el mal habito de no borrar las miniaturas almacenadas en la cache cuando se borra la imagen original, así que podemos encontrarnos con alguna sorpresa.

Es una aplicación de consola:

program Fisgon;
 
{$APPTYPE CONSOLE}
 
uses
  SysUtils,
  Classes;
 
procedure Recover(Origen: TStream; FormatStr: String; var Index: Integer);
var
  Destino: TMemoryStream;
  Buffer: array[0..$FFFF] of Byte;
  Header: array[1..4] of Byte;
  Size: Integer;
begin
  Destino:= TMemoryStream.Create;
  try
    // Escribimos los 2 primeros bytes del archivo
    Header[1]:= $FF;  Header[2]:= $D8;
    Destino.WriteBuffer(Header,2);
    Origen.Seek(-1,soFromCurrent);
    // En este bucle vamos copiando segmento a segmento
    while TRUE do
    begin
      FillChar(Header,Sizeof(Header),#0);
      Origen.Read(Header,4);
      Size:= ((Header[3] shl 8) + Header[4]) - 2;
      case Header[2] of
        $01,$D0..$D8: begin
                        // Estos segmentos solo miden dos bytes,
                        // asi que escribimos 2 bytes y
                        Destino.WriteBuffer(Header,2);
                        // retrocedemos otros dos hacia atras.
                        Origen.Seek(-2,soFromCurrent);
                      end;
        $D9: begin
               // Este indica que llegamos al final del archivo jpeg
               Destino.WriteBuffer(Header,2);
               // Guardamos el archivo usando la plantilla
               Destino.SaveToFile(Format(FormatStr,[Index]));
               Writeln('Recuperada: ' + Format(FormatStr,[Index]));
               // Incrementamos el indice
               inc(Index);
               // Salimos del procedure
               Exit;
             end;
        $DA: begin
               // Este segmento no tiene tamaño
               Destino.WriteBuffer(Header,2);
               Origen.Seek(-2,soFromCurrent);
               // Buscamos el final byte a byte
               while TRUE do
               begin
                 Origen.ReadBuffer(Buffer,1);
                 // Posible comienzo de otro segmento
                 if Buffer[0] = $FF then
                 begin
                   Origen.ReadBuffer(Buffer,1);
                   // $FF00 = Secuencia de escape
                   // $FFD0 .. $FFD7 = Se deben de ignorar
                   if (Buffer[0] <> 0) and not (Buffer[0] in [$D0..$D7]) then
                   begin
                     // Si encontramos el comienzo de otro segmento,
                     // retrocedemos dos bytes, y volvemos al bucle principal
                     Origen.Seek(-2,soFromCurrent);
                     break;
                   end else
                   begin
                     // Si no es el comienzo de otro segmento, escribimos
                     // y continuamos
                     Header[1]:= $FF;
                     Header[2]:= Buffer[0];
                     Destino.WriteBuffer(Header,2);
                   end;  
                 end else
                   Destino.WriteBuffer(Buffer,1);
               end;
             end
        else
        begin
          // Cualquier otro segmento lo copiamos directamente
          Origen.ReadBuffer(Buffer,Size);
          Destino.WriteBuffer(Header,4);
          Destino.WriteBuffer(Buffer,Size);
        end;
      end;
    end;
  finally
    Destino.Free;
  end;
end;
 
procedure Scanfile(Stream: TStream; FormatStr: String; var Index: Integer);
var
  B: Byte;
  Leidos: Integer;
  Posicion: int64;
begin
  // Vamos leyendo byte a byte el archivo
  Leidos:= Stream.Read(B,1);
  while Leidos = 1 do
    if B = $FF then
    begin
      Leidos:= Stream.Read(B,1);
      if Leidos = 1 then
        if B = $D8 then
        begin
          Leidos:= Stream.Read(B,1);
          if Leidos = 1 then
            if B = $FF then
            begin
              // Hasta encontrar la combinacion $FFD8FF
              Writeln('Encontrada una posible imagen.');
              // Guardamos la posicion
              Posicion:= Stream.Position;
              try
                // Intentamos recuperar la imagen
                Recover(Stream,FormatStr,Index);
              except
                // Si se produce un error lo mostramos, pero no salimos del bucle
                On E: Exception do
                   Writeln('Error: ' + E.Message);
              end;
              // Volvemos a la posicion en la que estabamos
              Stream.Position:= Posicion;
            end;
        end;
    end else
      Leidos:= Stream.Read(B,1);
end;
 
procedure Buscar(Path: string; var Index: Integer);
var
  SR: TSearchRec;
  Stream: TFileStream;
begin
  if Path = '' then exit;
  if copy(Path, Length(Path), 1) <> '\' then Path := Path + '\';
  if FindFirst(Path + '*', faDirectory, SR) = 0 then
  repeat
    if (SR.Name <> '.') and (SR.Name <> '..') then
      Buscar(Path + SR.Name, Index);
  until FindNext(SR) <> 0;
  FindClose(SR);
  if FindFirst(Path + 'thumb*.db',faAnyfile, SR) = 0 then
  repeat
    Stream:= TFileStream.Create(Path + SR.Name,fmOpenRead);
    try
      Writeln('Explorando el archivo: ' + Path + SR.Name);
      Scanfile(Stream,ParamStr(2),Index);
    finally
      Stream.Free;
    end;
  until FindNext(SR) <> 0;
  FindClose(SR);
end;
 
var
  i: Integer;
begin
  if ParamCount = 2 then
  try
    // Comprobamos que se pueden crear archivos en la ruta indicada
    TFileStream.Create(Format(ParamStr(2),[0]),fmCreate).Free;
    // Borramos el archivo de prueba
    DeleteFile(Format(ParamStr(2),[0]));
    // Buscamos en la ruta indicada
    i:= 0;
    Buscar(ParamStr(1),i);
  except
    // Si se produce un error, lo mostramos
    On E: Exception do
      Writeln('Error: ' + E.Message);
  end;
end.

Por ejemplo:

Fisgon C:\ %d.jpg

Bueno, creo que con esto ya hay bastante para entretenerse durante un tiempo. Pero si con esto no es suficiente, también puedes usar este mismo código para mirar dentro de los propios archivos jpeg. Alguno de ellos además de la imagen que muestran, incluyen una miniatura que se utiliza a modo de "vista previa". Y lo curioso en este caso, es que algunos programas al modificar la imagen no cambian la miniatura. Así nos podemos encontrar con fotos recortadas o con partes borradas, y sin embargo una miniatura de la imagen sin manipular.