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

Hola de nuevo a todos, han pasado algunos días desde la ultima vez que hice un post y finalmente le traigo uno nuevo. Cómo recordaran, en el post anterior vimos como extraer una imagen embebida en un mp3, pero en esa ocasión lo hicimos manualmente. En este post vamos a hacer un programa en c# que se encargue de hacer de forma automática ese proceso.

El código en c#.

Primero que nada haremos un pequeño repaso a que es lo que vamos a hacer.
  1. Abrimos un archivo mp3.
  2. Leemos una fracción del archivo para determinar si existe o no la etiqueta "APIC" o "PIC".
  3. Si existe Busacmos su posicion.
  4. Leemos el tamaño total de la etiqueta.
  5. Leemos los datos adicionales (tipo, codificacion, etc).
  6. Leemos el archivo de la imagen.
  7. Guardamos el archivo de imagen.
  8. Listo.
Primero crearemos una nueva aplicación de windows forms, yo en este caso usare #Develop
Ahora agregaremos solo 5 controles:
  • TextBox
    • (name) = TxtPath
  • Button
    • (name) = BtnOpen
    • Text = "Abrir"
  • Button
    • (name) = BtnExtract
    • Text = Extraer
  • TextBox
    • (name) = TxtInfo
    • ScrollBars = Vertical
    • Multiline = True
  • PictureBox
    • (name) = PicArt

Ya con los controles vamos a agregar código para hacer funcionar las cosas. En primer lugar, hay que recordar que hay más de una version de las etiquetas ID3, hasta el momento en que se creo este post existian las versiones: 1, 2, 2.3 y 2.4, nosotros solo nos ocuparemos para los casos de las versiones 2 y 2.3 (y 2.4 ya que para el caso de las etiquetas no hay cambios tan grandes con respecto a la versión anterior).
Basados en lo anterior, lo primero que debemos de hacer es identificar en que versión estamos trabajando, para eso crearemos una función que se encargue de devolver la versión. Para evitar que se usen valores desconocidos, en primer lugar crearemos una enumeración que contenga solo los valores posibles para nuestro proyecto.

private enum TagVersion
{
 TagVer22,
 TagVer23,
 TagVerUnknow
}

En la enumeración anterior se definen situaciones para la versión 2 y 2.3, cualquier otra versión devolverá el valor  "TagVerUnknow". Teniendo una definición para los valores que podemos esperar, ahora si creamos una función que devuelva uno de los valores de la enumeración.


private TagVersion GetId3Ver(byte[] Data)
{//49 44 33
	if (Data[0] == 0x49 && Data[1] == 0x44 && Data[2] == 0x33)
	{
		if (Data[3] == 2) return TagVersion.TagVer22;
		if (Data[3] == 3) return TagVersion.TagVer23;
		else return TagVersion.TagVerUnknow;
	}
	else
	{
		return TagVersion.TagVerUnknow;
	}
}

La funcion GetId3Ver recibe cómo parámetro una matriz de bytes, después lee los primeros 3 valores y compra si corresponden a "ID3", si si lo son, después leen el cuarto byte y regresan el la versión de acuerdo al valor de este. hay que recordar que cuando hay una etiqueta ID3, los primero bytes describen la etiqueta en si y el cuarto byte nos dice que versión es; 2 para la versión 2, 3 para la versión 2.3 y 4 para la versión 2.4.

Ahora vamos a crear la función que se encargará de leer toda la información y guardarla en un archivo a parte.

private void ImageContent(string FilePath,string SaveLocation)
{
	Byte[] BinSearchData;
	Stream FS = new FileStream(FilePath, FileMode.Open, FileAccess.Read,FileShare.Read);
	using(BinaryReader BinRead = new BinaryReader(FS,Encoding.Default))
	{
		BinSearchData = BinRead.ReadBytes(1024);
		TagVersion MyTagType;
		string MyTag;
		switch (GetId3Ver(BinSearchData))
		{
			case TagVersion.TagVer22:
				MyTag = "PIC";
				MyTagType = TagVersion.TagVer22;
				break;
			case TagVersion.TagVer23:
				MyTag = "APIC";
				MyTagType = TagVersion.TagVer23;
				break;
			default:
				MyTag = "Error";
				MyTagType = TagVersion.TagVerUnknow;
				break;
		}
		Match FindTag = Regex.Match(Encoding.ASCII.GetString(BinSearchData), MyTag);
		if (FindTag.Success)
		{
			int TotalLenght = 0;
			int PicTagPos = FindTag.Index;
			if (MyTagType == TagVersion.TagVer23)
			{
				bool IsNotZero = false;
				BinRead.BaseStream.Position = PicTagPos + 4;
				//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";
				//Flags, por ahora no nos interesan
				BinRead.ReadByte();
				BinRead.ReadByte();
				//TextEncoding
				BinRead.ReadByte();
				//Type
				string MType= "";
				while (IsNotZero == false)
				{
					byte Val = BinRead.ReadByte();
					if (Val != 0x00)
					{
						IsNotZero = false;
						MType = MType + string.Concat((char)Val);
					}
					else
					{
						IsNotZero = true;
					}
				}
				txtInfo.Text = txtInfo.Text + "\nDel tipo " + MType;
				//Tipo de imagen
				BinRead.ReadByte();
				//Reciclamos el flag anterior
				IsNotZero = true;
				string ImgDescription = "";
				//Descripcion de la imagen
				while (IsNotZero == false)
				{
					byte Val = BinRead.ReadByte();
					if (Val != 0x00)
					{
						ImgDescription = ImgDescription + string.Concat((char)Val);
						IsNotZero = false;
					}
					else
					{
						IsNotZero = true;
					}
				}
				long ImageFileBegin = BinRead.BaseStream.Position;
				int ImageLenght = (TotalLenght- 1 - ((int)ImageFileBegin - PicTagPos - 10));
				txtInfo.Text = txtInfo.Text + " La imagen empieza en " + ImageFileBegin.ToString();
				txtInfo.Text = txtInfo.Text + "Y mide " + ImageLenght.ToString();
				//Creamos una imagen desde el stream y la asignamos al picbox
				//Ponemos el puntero al inicio de la imagen
				BinRead.BaseStream.Position = ImageFileBegin + 1;
				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();
				}
			}
			if (MyTagType == TagVersion.TagVer22)
			{
				//Lo mismo que para la anterior, solo ajustando los datos, queda de tarea
			}
		}
		else
		{
			ErrPr.SetError(BtnOpen, "No hay etiqueta APIC");
		}
	}
	FS.Close();
}

Hacemos exactamente lo que hicimos cuando extrajimos la imagen a mano, primero leeremos el archivo, buscamos la etiqueta y luego leemos el archivo, determinamos el tipo de imagen que es, para eso usaremos la siguiente funcion:
private string ImageType(string MIMEType)
{
	//PNG or JPG
	//image/png or image/jpeg
	if (MIMEType == "PNG" || MIMEType == "image/png")
		return "png";
	if (MIMEType == "JPG" || MIMEType == "image/jpeg")
		return "jpg";
	else
		return "bin";
}

Está función solo devuelve "PNG" o "JPG" y se usa al momento de guardar el resultado; es decir al momento de construir el nombre de archivo.

Finalmente solo queda mandar a llamar a la función "ImageContent" desde el botón BtnExtract, la función guardara la imagen con el mismo nombre de archivo que el del MP3 que usemos y también mostrará la imagen en el picture box "PicArt"

Ahora solo queda hacer todo el procedimiento para la versión 2, pero eso lo haremos en el siguiente Post, ademas crearemos una función para agregar muchos archivos y realizar la extracción por lotes. Te recomiendo que le eches un vistazo a la documentación parta que sea más fácil entender cómo es que funciona.

Por el momento es todo, los leo luego.

2 comentarios

  1. Hola buen dia excelente trabajo , trabajo ahora mismo en un proyeto propio en lenguaje Harbour, pero me tope con una duda ojala puedas ayudarme en aprender a calcular el completo de todas las partes que conforman el id3v2.3 se que se extraen los 4 bytes de la posicion 7,8,9 y 10 pero no doy como convertirlo a valor decimal de bytes.. Saludos

    ResponderBorrar
    Respuestas
    1. Claro imagina que una etiqueta empieza con los siguientes valores hexadecimales: 0x49 0x44 0x33 0x03 0x00 0x00 0x00 0x0F 0x79 0x68. Entonces el tamaño de la etiqueta es 0x00 0x0F 0x79 0x68, pero estos valores están "codificados" (por decirlo de alguna manera). Para obtener el valor, lo primero que debes de hacer es convertir los cuatro valores a binario. Allgo asi:
      0x00 > 0000 0000
      0x0F > 0000 1111
      0x79 > 0111 1001
      0x68 > 0110 1000
      Entonces tomas todos los valores menos el primero (de izquierda a derecha) y los separas y tienes algo así:
      000 0000 | 000 1111 | 111 1001 | 110 1000
      que nos da el número binario 111111110011101000 que al convertirlo a decimal resulta
      261352. Espero que sea de utilidad, pero si no estate al pendiente que se vienen más post sobre esto. Saludos.

      Borrar