Vamos a programar #18 - Extraer una imagen embebida en un MP3.

Hola de nuevo a todos, el día de hoy vamos terminar con el programa encargado de extraer la imágenes que están embebidas en los archivos MP3.
El día de hoy vamos a implementar todo el programa y ademas haremos una pequeñas optimizaciones al código que ya teníamos previamente hecho.

Mejorando el código previo.

Primero vamos crear una función para calcular el tamaño de la etiqueta, en el código, lo que hacíamos primero, era buscar la etiqueta, luego leer la longitud, pero hay que recordar que dependiendo de la versión, se usan tres o cuatro bytes para esto.
Entoces basados en la informacion anterior, podemos hacer una función similar a la siguiente:



private int ByteArrToSize(byte[] Data)
{
 int Result = 0;
 if (Data.Length == 4)
 {
  Result = Data[0] * 256 * 256 * 256 + Data[1] * 256 * 256 + Data[2] * 256 + Data[3];
  return Result;
 }
 if (Data.Length == 3)
 {
  Result = Data[0] * 256 * 256 + Data[1] * 256 + Data[2];
  return Result;
 }
 else return 0;
}

Si recordamos en otro código, es exactamente lo mismo, solo que lo separamos para poder usarlo de manera rápida. Esta función recibe una matriz de bytes que contiene los 4 o 3 bytes después de la etiqueta "PIC" o "APIC". cómo resultado, devuelve un numero del tipo int que contiene la longitud de la etiqueta. Para usarla solo hay que sustituir la parte de los calculos por la llamada a la funcion.

Código anterior:

//Size
TotalLenght = TotalLenght + BinRead.ReadByte() * 256 * 256 * 256;
TotalLenght = TotalLenght + BinRead.ReadByte() * 256 * 256;
TotalLenght = TotalLenght + BinRead.ReadByte() * 256;
TotalLenght = TotalLenght + BinRead.ReadByte();
txtInfo.Text = txtInfo.Text + "\n La etiqueta APIC esta en la dirección: " + PicTagPos.ToString() + "\n";
txtInfo.Text = txtInfo.Text + "con longitud de" + TotalLenght.ToString() + " bytes\n";

Código nuevo implementando la función ByteArrToSize:


TotalLenght = ByteArrToSize(BinRead.ReadBytes(4));
txtInfo.Text = txtInfo.Text + "\n La etiqueta APIC esta en la dirección: " + PicTagPos.ToString() + "\n";
txtInfo.Text = txtInfo.Text + "con longitud de" + TotalLenght.ToString() + " bytes\n";


y cómo es un cálculo que lo vamos a hacer más de una vez, lo mejor es usar la función, originalmente la hice de la primer forma debido a que quería mostrar lo que estábamos haciendo a mano, pero para fines más prácticos, usamos la función, no hace falta ser experto para ver que en ambos casos se hace exactamente lo mismo, solo que la función determina; hasta cierto punto; que es lo que va a hacer, si usáramos el código original, debido a que es diferente como esta descrito el tamaño de la etiqueta. Primero había que comprobar para que caso íbamos a hacer el cálculo, y de leer un byte, multiplicar y sumar 3 o 4veces dependiendo del caso. En la función solo pedimos los datos y dependiendo de el numero de bytes que contenga el argumento, seguirá las condiciones pertinentes para devolver el valor correcto.

Otro de los procesos que decidí volver en una función; fue el de obtener la imagen, si recordamos, en el código original, una vez que ya teníamos definidos todos los datos de la imagen (tipo y longitud), en cada una de las versiones de las versiones, leíamos los datos, creábamos un MemoryStream y creábamos una imagen a partir de el.

El codigo es el siguiente:

using (MemoryStream MS = new MemoryStream(BinRead.ReadBytes(ImageLenght)))
{
 PicArt.Image = Image.FromStream(MS);
 using (FileStream file = new FileStream(SaveLocation + "." + ImageType(MType), FileMode.Create, FileAccess.Write))
 {
  MS.WriteTo(file);
  file.Flush();
  file.Close();
 }
 MS.Close();
}

Entonces para pasar todo el fragmento de código anterior, solo necesitamos crear una función que devuelva una imagen cómo resultado.

El siguiente código es la versión en función del primer código.

private Image StreamToImage(byte[] Data, string SaveLocation)
{
 using (MemoryStream MS = new MemoryStream(Data))
 {
  Image TempImage;
  using (FileStream file = new FileStream(SaveLocation, FileMode.Create, FileAccess.Write))
  {
   MS.WriteTo(file);
   file.Flush();
   file.Close();
   TempImage = Image.FromStream(MS);
  }
  MS.Close();
  return TempImage;
 }
}


Al igual que en el caso anterior, tomamos todo el código que ya teníamos y lo usamos tal cual estaba, solo que este caso, devolvemos un valor del tipo Image que contiene la imagen que extrajimos. La función recibe dos parámetros: uno del tipo byte[] que es el que contiene todos los datos leídos previamente desde el Stream donde se manda a llamar esta función y un valor del tipo string que contiene la ruta en donde se guardará la imagen (en adición a la imagen que devuelve la función).

El código nuevo.

Todos estos post comenzaron por el hecho de que cuando se realiza una conversión entre una versión de las etiquetas ID3 a otra, en muchos casos se pierde la imagen (o eso fue lo que me dijeron), entonces nuestro objetivo es crear un Back up de estás. Pero ahora que ya sabemos cómo extraerlas, sigue almacenarlas, pero creo que surgiría un pequeño problema.
Cuando guardábamos las imágenes, les poníamos como nombre de archivo el nombre de la canción, solo que en lugar de extensión MP3 le poníamos JPG o PNG (dependiendo el caso), pero imaginemos que tenemos el álbum completo, no tendría caso extraer la imagen para cada una, porque seria la misma para cada canción del mismo álbum, supongamos que tenemos un disco completo ya en MP3 y este contiene 20 canciones, si le extraemos la imagen a cada uno, tendríamos 20 archivos de imagen iguales con nombres distintos.
La solucion es un tanto sencilla, solo debemos de tomar cómo nombre de archivo algo que tengan en comun los archivos que no sea el artista. El album es un buen identificador, porque bastará que extraigamos una sola imagen para cubrir los 20 archivos.
Para poder usar el nombre del album cómo nombre de archivo para la imagen, primero debemos de saber cual es el nombre del album. Igual que con la imagen, dentro del tag ID3 hay un frame que describe la información para el album. En el caso de la version 2.3 es la siguiente:

Text information frames The text information frames are the most important frames, containing information like artist, album and more. There may only be one text information frame of its kind in an tag. If the textstring is followed by a termination ($00 (00)) all the following information should be ignored and not be displayed. All text frame identifiers begin with "T". Only text frame identifiers begin with "T", with the exception of the "TXXX" frame. All the text information frames have the following format:
<Header for 'Text information frame', ID: "T000" - "TZZZ", excluding "TXXX" described in 4.2.2.>
Text encoding $xx Information
text string according to encoding
Y para la version 2 es la siguiente:
The text information frames are the most important frames, containing information like artist, album and more. There may only be one text information frame of its kind in an tag. If the textstring is followed by a termination ($00 (00)) all the following information should be ignored and not be displayed. All the text information frames have the following format:
Text information identifier "T00" - "TZZ" , excluding "TXX", described in 4.2.2.
Frame size $xx xx xx
Text encoding $xx
Information <textstring>
Entonces sabiendo lo anterior podemos crear una código cómo el que sigue:


       private string DataToAlbum(byte[] Data)
      {
            if (Data[0] == 0)
                return Encoding.ASCII.GetString(Data, 1, Data.Length - 1);
            if (Data[0] == 1)
                return Encoding.Unicode.GetString(Data, 1, Data.Length - 1);
            else
                return null;
        }

El código anterior sirve para leer cualquier frame de texto (exceptuando los que dice la documentación), pero ademas incluye casos definidos para cuando el texto es ASCII o unicode. La función recibe como argumento una matriz de bytes que incluye todos los bytes que le siguen a la longitud del frame, es decir, el primer byte deberá de ser el que indica que tipo de codificación se va a usar; inmediatamente seguido de el string (que es el resto de la etiqueta)esto para el caso de la versión 2. Para la versión 2.3 se deben de pasar los bytes empezando desde el byte que indica la codificación del texto, omitimos los 2 bytes que se usan cómo flags.
La función se debe de mandar a llamar cuando ya tenemos separados los datos de este frame, es decir, primero hay que determinar que tipo de tag ID3 es, luego buscar "TALB" o "TAL" dependiendo de la versión y leer los datos de la misma forma que lo hicimos para el frame "APIC" o" PIC", esto se puede hacer dentro del mismo código que lee los datos para buscar la imagen y ya una vez que se encuentran, simplemente le pasamos la información a la función.

Es importante usar la codificación correcta. Si no lo hiciéramos, no podríamos leer "ドリドリ" que es el álbum de esta canción.


Es importante leer la codificación, porque para este caso, los caracteres del japones, no están disponibles en el estandar ASCII, Cuando el byte que nos indica que codificación se usa es 1, hay que recordar que la codificación es UNICODE, cuando es 0 es ASCII.


Y bien, por ahora solo queda pendiente la extracción por lotes, pero será en el siguiente post.
Los leo luego.

5 comentarios

  1. Cuando extraes la informacion de un archivo que usa la codificaion unicode, en la cadena de caracteres hay uno que no se ve, arreglalo

    ResponderBorrar
  2. En algunos archivos cuado extrae la imagen no les pone el nombre del album como si no lo tubiera pero las canciones que tengo si las tiene porqu pasa y como se arregla

    ResponderBorrar
  3. Respuestas
    1. Nunca lo mencioné, pero soy fan de esto desde hace casi 20 años.

      Borrar