Optimizar archivo jpeg

Los archivos Jpeg además de la propia imagen contienen información extra añadida por algunos programas (comentarios, tipo de cámara, thumbnails, etc) que engordan el archivo pero que no son necesarios para ver la imagen.

En este truco vamos a eliminar toda esa información extra y dejar el archivo lo mas pequeño posible sin modificar en nada la calidad de la imagen.

procedure Limpiar(Origen, Destino: TStream);
var
  Buffer: array[0..$FFFF] of Char;
  Header: array[1..4] of byte;
  Size: Integer;
begin
  Origen.Seek(0,soFromBeginning);
  Origen.ReadBuffer(Header,2);
  if (Header[1] <> $FF) or (Header[2] <> $D8) then
    raise Exception.Create('Identificador incorrecto');
  Destino.WriteBuffer(Header,2);
  repeat
    Origen.ReadBuffer(Header,4);
    Size:= ((Header[3] shl 8) + Header[4]) - 2;
    case Header[2] of
      // $FE: Origen.Seek(Size,soFromCurrent);
      $FE, $E0..$EF: Origen.Seek(Size,soFromCurrent);
      $01,$D0..$D9: begin
                      Destino.WriteBuffer(Header,2);
                      Origen.Seek(-2,soFromCurrent)
                    end
      else
      begin
        Origen.ReadBuffer(Buffer,Size);
        Destino.WriteBuffer(Header,4);
        Destino.WriteBuffer(Buffer,Size);
      end;
    end;
  until Header[2] = $DA;
  Destino.CopyFrom(Origen,Origen.Size - Origen.Position);
end;
 
// Ejemplo de uso
var
  Origen, Destino: TFileStream;
begin
  if OpenDialog1.Execute then
  begin
    Origen:= TFileStream.Create(OpenDialog1.FileName, fmOpenRead);
    try
      Destino:= TFileStream.Create(ChangeFileExt(OpenDialog1.FileName,'_b.jpg'),fmCreate);
      try
        Limpiar(Origen,Destino);
      finally
        Destino.Free;
      end;
    finally
      Origen.Free;
    end;
  end;
end;

Esta limpieza además de ajustar el tamaño del archivo, ayuda a mantener nuestra intimidad, ya que borra también las imágenes en miniatura, conocidas como thumbnails, que algunas aplicaciones guardan dentro del archivo jpeg. Se han dado casos de programas, que al recortar la imagen, siguen manteniendo la imagen en miniatura de la imagen completa, revelando así partes de la imagen que queríamos ocultar. Una buena limpieza y problema resuelto ...

Ahora poniéndonos del otro lado, del lado de los curiosos, podemos pensar una manera de extraer las imágenes en miniatura de los archivos jpeg y así comprobar si alguien a cometido una indiscreción. Podríamos aplicar un método mas ortodoxo, siguiendo el formato Exif por el que se rigen este tipo de miniaturas, pero lo que vamos a hacer es extraer el segmento que contiene la información Exif, una vez lo tenemos buscamos la marca $FFD8 que indica el inicio de una imagen jpeg. Esto no es la forma mas correcta de hacerlo, ya que nos saltamos toda la estructura de datos Exif, pero el caso es que funciona y para jugar un rato nos sirve.

function ReadThumb(Source, Dest: TStream): Boolean;
var
  Buffer: array[0..$FFFF] of Char;
  Header: array[1..4] of byte;
  Size: Integer;
  i: Integer;
begin
  Result:= FALSE;
  Source.Seek(0,soFromBeginning);
  Source.ReadBuffer(Header,2);
  if (Header[1] <> $FF) or (Header[2] <> $D8) then
    raise Exception.Create('Identificador incorrecto');
  repeat
    Source.ReadBuffer(Header,4);
    Size:= ((Header[3] shl 8) + Header[4]) - 2;
    case Header[2] of
      $E1: begin
        Source.ReadBuffer(Buffer,Size);
        i:= 0;
        while i < Size - 1 do
        begin
          if CompareMem(@Buffer[i],PChar(#$FF#$D8),2) then
          begin
            Dest.WriteBuffer((@Buffer[i])^, Size - i);
            Result:= TRUE;
            break;
          end;
          inc(i);
        end;
        break;
      end;
      $01,$D0..$D9: Source.Seek(-2,soFromCurrent);
      else Source.Seek(Size,soFromCurrent);
    end;
  until (Header[2] = $DA);
end;
 
procedure ExtractThumb(Filename: string);
var
  Source: TFileStream;
  Thumb: TMemoryStream;
begin
  Source:= TFileStream.Create(Filename, fmOpenRead);
  try
    Thumb:= TMemoryStream.Create;
    try
      if ReadThumb(Source,Thumb) then
        Thumb.SaveToFile(ChangeFileExt(Filename,'_thumb.jpg'));
    finally
      Thumb.Free;
    end;
  finally
    Source.Free;
  end;
end;
 
// Por ejemplo
  ExtractThumb('D:\1.jpg');

En el ejemplo anterior si "d:\1.jpg" contiene un imagen en miniatura, se guardara en el archivo "d:\1_thumb.jpg", y si no contienen ninguna no se crea ningún fichero. Ahora solo tenemos que buscar un directorio con un buen numero de jpegs y hacerlo con todas las imágenes para ver si descubrimos algo ...

Comentarios

¿y otros números mágicos para ficheros .png, .gif, jpeg2000, .bmp, etc ?

Thx

Los números FFD8 no son tan "mágicos" simplemente es la marca con lo que empiezan todos los ficheros jpeg según las especificaciones de ese formato. Para otro tipo de ficheros habría que encontrar las especificaciones de ese formato, ver si existe algún tipo de marca reconocible (por ejemplo los bmp empiezan con los caracteres "BM"), y luego encontrar la forma de leer los datos sin conocer de antemano el tamaño del fichero.

Para empezar puedes buscar las especificaciones en esta pagina:
http://www.wotsit.org/