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

El día de hoy vamos a terminar (ahora si no es broma) el código para extraer las imágenes que se encuentran embebidas en los archivos MP3.


En el post anterior implementamos algunas partes del código y lo pasamos a funciones, antes de avanzar hay algo que quiero decir de una de las funciones anteriores. Una persona que visitó el blog me dijo que la función DataToAlbum no funcionaba bien, a pesar de que yo mostré imágenes en la que parecía que todo funcionaba bien, pero no era asi, al analizar todos los bytes que componían al string (solo la versión unicode), había un carácter no visible. El carácter es 0xFF, eso se debia a que también le pasábamos a la función GetString los bytes adicionales que se usan para indicar que se trata de un string unicote, para agregarlo, solo hay que movernos hasta el tercer byte. Recordemos que la función que hicimos recibía cómo parámetro una matriz de bytes donde el primero, era el byte que nos indicaba cual era el tipo de codificación; uno para unicode y cero para ASCII. En el caso de la codificacion ASCII no existía mayor problema porque seguido de ese byte, inmediatamente va lo que vendría siendo el string en sí, pero en el caso del string unicode, inmediatamente después del primer byte, siguen dos bytes que sirven para indicar que tipo de codificación se usa (en lectores de texto por ejemplo,0xFF 0xFE para unicode).

private string DataToAlbum(byte[] Data)
{
 if (Data[0] == 0)
  return Encoding.ASCII.GetString(Data,1,Data.Length-1).TrimEnd((char)0).TrimStart((char)0);
 if (Data[0] == 1)
  return Encoding.Unicode.GetString(Data,3,Data.Length-3).TrimEnd((char)0).TrimStart((char)0);
 else
  return "Desconocido";
}

Procesando por lotes.

El chiste de todo esto era extraer las imágenes debido a que cuando se convertían entre una versión a otra, por lo que me dijeron, algunos programas omiten la imagen, entonces lo ideal es extraer primero la imagen y después insertarla de nuevo, eso si, solo después de haber realizado la conversión a la versión de destino.

Para eso creamos la función BatchProcess cuyo código es el siguiente:

private void BatchProccess(List<string> Files, string SaveFolder)
{
 int Current = 0;
 PBProgress.Maximum = Files.Count;
 Thread DoWork = new Thread (new ThreadStart(() =>
 {
  foreach (string Item in Files)
  {
   ImageContent(Item,SaveFolder);
   Current = Current + 1;
   UpdateProgress(Current);
   if (Current == Files.Count) {
    IsWorking =false;
   }
  }
 }
 ));
 DoWork.Start();
}


Está función recibe cómo parámetros una lista del tipo string que contiene todos los archivo de los cuales queremos extraerles la imágenes, el otro parámetro es un string que contiene la ruta (fólder) en donde guardáremos las imágenes. Después creamos un thread para no bloquear la interfaz principal. De hecho cuando pruebes el código, al agregar los archivos, si agregas muchos, notarás que la aplicación se congela un poco (dependiendo de cuantos agregues y de que tan potente sea tu equipo), esto lo deje así para que notes la diferenta entre usar un thread aparte y cuando no.
El uso del thread (hilo a partir de este momento). Para esta aplicación es realmente sencillo, ya que no hacemos uso de las funciones para controlarla como es debido, de hecho para saber si aun sigue ocupado ese hilo usamos el flag IsWorking. Para poder saber el progreso del proceso usamos un delegado que se encargara de recibir el valor actual del progreso, después llamará a la funcion UpdateProgress que se encargará de actualizar la propiedad Value del ProgressBar.

El código de la funcion UpdateProgress (incluido el delegado) es el siguiente:

private delegate void UpdateProgressDelegate(int Value);

private void UpdateProgress(int Value)
{
 if (PBProgress.InvokeRequired)
 {
  PBProgress.Invoke(new UpdateProgressDelegate(UpdateProgress), Value);
 }
 else
 {
  PBProgress.Value = Value;
 }
}

El uso un poco más avanzado de los hilos lo veremos en el siguiente Post.
Y ya solo que da el código que se encargará de hacer el trabajo. El uso de un flag para determinar si hay o no un proceso es importante, ya que podemos cerrar la ventana principal y algun proceso todavía se puede estar ejecutando, suponiendo que eres cómo yo y rara vez le pide a Windows que te dé permiso para sacar tu dispositivo, si lo extraes en medio de un proceso de lectura/escritura, puede ser que se dañe tu USB.

El código completo lo puedes descargar aquí, Dudas o comentarios no dudes en decirlo. Antes de terminar quiero agradecer al que reportó el error, preferiría que no usaran perfiles anónimos para poder dar el reconocimiento que se debe.

Por ahora es todo, los leo luego.
P.D, “Arceus existe ;)”

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.

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

Hola a todos de nuevo. El día de hoy vamos a terminar con el temas de las imágenes embebidas en los archivos MP3.
En el post anterior, hicimos una parte del código de C# encargado de hacer la extracción. Había quedado pendiente el trabajo para la versión 2 de las etiquetas ID3.

Hay que recordar que la esctructura del la etiqueta "PIC" es la siguiente:

Attached picture "PIC"
Frame size $xx xx xx
Text encoding $xx
Image format $xx xx xx
Picture type $xx
Description <textstring> $00 (00)
Picture data <binary data>
Basados en la información anterior crearemos el código necesario.

El código para la versión 2.

Retomando el codigo anterior, solo hay que agregar lo que nos hace falta. La función ImageContent constaba de 3 condiciones if importantes (ademas de algunos secundarios), el primero y principal consiste en verficar si hay una etiqueta ID3 valida, si la había, el siguiente if comprobaba que la version fuera la 2.3, el siguiente comprobaba para la version 2. Está fue la que quedo pendiente, el siguiente codigo va inmediatamente despues del if que comprobaba para la version 2.3.


 if (MyTagType == TagVersion.TagVer22)
                    {
                        {
                            //PIC
                            BinRead.BaseStream.Position = PicTagPos + 3;
                            //Tamaño
                            TotalLenght = TotalLenght + BinRead.ReadByte() * 256 * 256;
                            TotalLenght = TotalLenght + BinRead.ReadByte() * 256;
                            TotalLenght = TotalLenght + BinRead.ReadByte();
                            txtInfo.Text = txtInfo.Text + "\n La etiqueta PIC esta en la dirección: " + PicTagPos.ToString() + "\n";
                            txtInfo.Text = txtInfo.Text + "con longitud de" + TotalLenght.ToString() + " bytes\n";
                            //TextEncoding
                            BinRead.ReadByte();
                            //Type JPG o PNG
                            string MType = "";
                            MType = MType + string.Concat((char)BinRead.ReadByte());
                            MType = MType + string.Concat((char)BinRead.ReadByte());
                            MType = MType + string.Concat((char)BinRead.ReadByte());
                            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 - 6));
                            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();
                            }
                        }
                    }
}

Si revisamos el código de forma detenida veremos que es mu similar al código anterior. Entonces te preguntarás ¿No es un desperdicio? Si y no, el código está pensado para hacer una demostración de lo que hicimos en el primer post? Y la respuesta es: NO, lo que trate de hacer con estos post es demostrar como es que se puede portar una situación al código así "tal cual", porque de hecho lo que hicimos aquí, fue lo mismo que hicimos con el programa XVI32. Es decir lo que primero hicimos a mano, luego lo plasmamos en el código.

Información para una canción con etiqueta PIC (Ver 2)

Información para una canción con etiqueta APIC (Ver 2.3 {Lo admito soy un Pokefan!!!})
 En el próximo post crearemos una función para procesar por lotes y liberaré la aplicación completa, por ahora es todo.

Los leo luego.

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.