Vamos a programar #107 - Agregando imagenes a archivos FLAC usando C# (Parte 1).
Hola de nuevo a todos. El día de hoy vamos a continuar con mas de los archivos FLAC. En post anteriores vimos cómo insertar la imagen usando solo un editor hexadecimal, pero hoy, vamos a usar C# para crear un programa que lo haga por nosotros.
Antes de continuar te recomiendo que leas los post relacionados:
- Vamos a programar #102
- Vamos a programar #103
- Vamos a programar #104
- Vamos a programar #105
- Vamos a programar #106
Una vez leidos los post anteriores, vamos a ver el código que hace funcionar el programa. Algo importante que debo aclarar, es que por primera vez en la historia del blog, voy a usar visual studio 2022 y .Net 8.0, la mayor parte del código es retro-compatible, pero en c# 7.0 se agregaron algunas caracteristicas que hacen las cosas mas sencillas, por lo que si quieres probar el código deberías de instalar al menos esa version de visual studio.Además el código es mas que nada una prueba de concepto por lo que se puede optimizar, pero además, todo se hace para que sea lo mas similar al proceso que se hizo a mano (Vamos a programar #102) por lo que si quieres optimizarlo, puedes hacerlo sin ningun problema.
Una vez dicho eso, el código es el siguiente:
using System.Diagnostics.Eventing.Reader;
using System.Runtime.InteropServices;
using System.Text;
using System.Drawing.Imaging;
using System.Net.Http.Headers;
using System.ComponentModel;
namespace FlacPictureWriter
{
public partial class Form1 : Form
{
///Picture type:
/// $00 Other
/// $01 32x32 pixels 'file icon' (PNG only)
/// $02 Other file icon
/// $03 Cover (front)
/// $04 Cover (back)
/// $05 Leaflet page
/// $06 Media (e.g. lable side of CD)
/// $07 Lead artist/lead performer/soloist
/// $08 Artist/performer
/// $09 Conductor
/// $0A Band/Orchestra
/// $0B Composer
/// $0C Lyricist/text writer
/// $0D Recording Location
/// $0E During recording
/// $0F During performance
/// $10 Movie/video screen capture
/// $11 A bright coloured fish
/// $12 Illustration
/// $13 Band/artist logotype
/// $14 Publisher/Studio logotype
private enum APICType
{
/// <summary>
/// Imagen génerica
/// </summary>
APIC_TYPE_Other = 0,
/// <summary>
/// ícono (solo PNG)
/// </summary>
APIC_TYPE_FileIcon,
/// <summary>
/// Otro ícono
/// </summary>
APIC_TYPE_OtherFile_Icon,
/// <summary>
/// Cubierta frontal del disco
/// </summary>
APIC_TYPE_CoverFront,
/// <summary>
/// Cubierta trasera del disco
/// </summary>
APIC_TYPE_CoverBack,
/// <summary>
/// Folleto
/// </summary>
APIC_TYPE_LeafletPage,
/// <summary>
/// Otra ímagen
/// </summary>
APIC_TYPE_Media,
/// <summary>
/// Artista principal
/// </summary>
APIC_TYPE_LeadArtist,
/// <summary>
/// Artista
/// </summary>
APIC_TYPE_Artist,
/// <summary>
/// Conductor
/// </summary>
APIC_TYPE_Conductor,
/// <summary>
/// Banda
/// </summary>
APIC_TYPE_Band,
/// <summary>
/// Compositor
/// </summary>
APIC_TYPE_Composer,
/// <summary>
/// Letrista
/// </summary>
APIC_TYPE_Lyricist,
/// <summary>
/// Lugar de la grabacion
/// </summary>
APIC_TYPE_RecordingLocation,
/// <summary>
/// Captura durante la grabacion
/// </summary>
APIC_TYPE_DuringRecording,
/// <summary>
/// Captura durante concierto
/// </summary>
APIC_TYPE_DuringPerformance,
/// <summary>
/// Captura de videclip
/// </summary>
APIC_TYPE_VideoScreenCapture,
/// <summary>
/// ???
/// </summary>
APIC_TYPE_ABrightColouredFish,
/// <summary>
/// Ilustracion
/// </summary>
APIC_TYPE_Illustration,
/// <summary>
/// Logotipo de la banda/artista
/// </summary>
APIC_TYPE_ArtistLogotype,
/// <summary>
/// Logotipo de la discografica
/// </summary>
APIC_TYPE_PublisherLogotype,
APIC_TYPE_NoValid
}
// 0 : STREAMINFO 0000000
// 1 : PADDING 0000001
// 2 : APPLICATION 0000010
// 3 : SEEKTABLE 0000011
// 4 : VORBIS_COMMENT 0000100
// 5 : CUESHEET 0000101
// 6 : PICTURE 0000110
// 7-126 : reserved 1111000 - Invalid
// 127 : invalid, to avoid confusion with a frame sync code
/// <summary>
/// Enumeración con los posibles bloques contenidos en un archivo FLAC
/// </summary>
private enum BlocksTypes
{
/// <summary>
/// Bloque mandatorio con la información del Stream
/// </summary>
Block_Type_StreamInfo = 0,
/// <summary>
/// Bloque de Padding
/// </summary>
Block_Type_Padding = 1,
/// <summary>
/// Bloque con la informacion de la aplicación
/// </summary>
Block_Type_Application = 2,
/// <summary>
/// Bloque Seektable
/// </summary>
Block_Type_SeekTable = 3,
/// <summary>
/// Bloque con metadatos
/// </summary>
Block_Type_VorbisComment = 4,
/// <summary>
/// Bloque con cuesheet
/// </summary>
Block_Type_CueSheet = 5,
/// <summary>
/// Bloque con imagen
/// </summary>
Block_Type_Picture = 6,
/// <summary>
/// Bloque no válido
/// </summary>
Block_Type_NoValid = 7
}
public Form1()
{
InitializeComponent();
}
List<UInt32> Direcciones;
bool TieneImagen = false;
UInt32 ImageSize = 0;
UInt32 LastBLockPosition = 0;
UInt32 VorbisCommentPosition = 0;
UInt32 ImagePosition = 0;
private byte[] NormalizeToFourBytes(UInt32 Value, bool Reverse = false)
{
byte[] buff = BitConverter.GetBytes(Value);
if (Reverse)
Array.Reverse(buff);
return buff;
}
/// <summary>
/// Obtiene el "Mime Type" basado en la extension de un archivo
/// </summary>
/// <param name="FilePath">Ruta de la cual se va a extraer la información</param>
/// <returns></returns>
private string GetMIMEType(string FilePath)
{
FileInfo FI = new(FilePath);
if (string.Compare(FI.Extension, ".jpg", StringComparison.OrdinalIgnoreCase) == 0)
{
return @"image/jpeg";
}
else if (string.Compare(FI.Extension, ".png", StringComparison.OrdinalIgnoreCase) == 0)
{
return @"image/PNG";
}
else
{
return "No valido";
}
}
/// <summary>
/// Crea una imagen a partir de una secuencia de bytes
/// </summary>
/// <param name="bytesArr">Matriz de bytes que contiene los datos de la imagen</param>
/// <returns>Regresa una imagen</returns>
public Image ByteArrayToImage(byte[] bytesArr)
{
using (MemoryStream memstr = new(bytesArr))
{
Image img = Image.FromStream(memstr);
return img;
}
}
// 4 4 n*4 4 n*4 4 4 4 4 !4 !n*4
//<32>,<32>,<n*8>,<32>,<n*8>,<32>,<32>,<32>,<32>,<32>,<n*8>
// 1 2 3 4 5 6 7 8 9 10 11
/// <summary>
/// Lee todo los datos del bloque PICTURE y extrae la imagen contenida
/// </summary>
/// <param name="TheData">Arreglos bytes que contiene todo el bloque PICTURE</param>
/// <returns>Regresa una imagen</returns>
private Image ReadPictureData(byte[] TheData)
{
uint CurrentPos = 4;//1
uint CurrentSize = GetBlockSize(TheData, (int)CurrentPos, 4);
CurrentPos += 4;//2
string MimeType = Encoding.ASCII.GetString(TheData, (int)CurrentPos, (int)CurrentSize);
CurrentPos += CurrentSize;//3
CurrentSize = GetBlockSize(TheData, (int)CurrentPos, 4);
CurrentPos += (CurrentSize + (4 * 5));//4-9
CurrentSize = GetBlockSize(TheData, (int)CurrentPos, 4);
CurrentPos += 4;
byte[] TempImage = new byte[CurrentSize];
Array.Copy(TheData, CurrentPos, TempImage, 0, CurrentSize);
return ByteArrayToImage(TempImage);
}
/// <summary>
/// Actualiza la imagen de un archivo FLAC.
/// </summary>
/// <param name="ImagePath">Ruta de la imagen JPG</param>
private byte[] BuildAPICFrame(string ImagePath, int PictureType, string MimeType, UInt32 ImageSize)
{
List<byte> Data = new List<byte>();
byte[] Buff = new byte[ImageSize];
using (FileStream FS = new FileStream(ImagePath, FileMode.Open))
{
using (BinaryReader BR = new(FS))
{
Buff = BR.ReadBytes((int)ImageSize);
}
}
//Tipo de Imagen APIC
Data.AddRange(NormalizeToFourBytes((UInt32)PictureType, true));
//Tamaño del tipo de contenido
Data.AddRange(NormalizeToFourBytes((UInt32)MimeType.Length, true));
//Tipo de contenido
Data.AddRange(Encoding.ASCII.GetBytes(MimeType));
//Descripcion del tipo de contenido
Data.AddRange(NormalizeToFourBytes((UInt32)MimeType.Length, true));
//Tamaño de la descripcion del contenido
Data.AddRange(Encoding.ASCII.GetBytes(MimeType));
//Ancho de la imagen
Data.AddRange(NormalizeToFourBytes(0, true));
//Alto de la imagen
Data.AddRange(NormalizeToFourBytes(0, true));
//Profundidad del color
Data.AddRange(NormalizeToFourBytes(0, true));
//Indice de la paleta de colores
Data.AddRange(NormalizeToFourBytes(0, true));
//Tamaño de la imagen en bytes
Data.AddRange(NormalizeToFourBytes(ImageSize, true));
//Datos de la Imagen
Data.AddRange(Buff);
return Data.ToArray();
}
/// <summary>
/// Construye el bloque de imagen
/// </summary>
/// <param name="APICData"></param>
/// <returns></returns>
private byte[] BuildPictureBlock(byte[] APICData)
{
byte[] Buff = new byte[APICData.Length + 4];
byte[] PICSize = new byte[4];
PICSize = NormalizeToFourBytes((UInt32)APICData.Length, true);
PICSize.CopyTo(Buff, 0);
//siempre pondremos la imagen al final de los metadatos
//Establecemos el marcador de ultimo bloque y el tipo de bloque en 6 (Block_Type_Picture)
//Quedando 10000110b o 134
Buff[0] = 134;
APICData.CopyTo(Buff, 4);
return Buff;
}
private List<UInt32> GetOffsets(string FileName)
{
byte[] DataBuff;
List<UInt32> offsets = new List<UInt32>();
bool LastFrame = false;
using (FileStream FS = new(FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
offsets.Add((UInt32)FS.Position);
using (BinaryReader BR = new(FS, Encoding.ASCII))
{
DataBuff = BR.ReadBytes(4);
if (string.Equals(Encoding.ASCII.GetString(DataBuff), "fLaC", StringComparison.Ordinal))
{
UInt32 CurrentBlockSize;
UInt32 CurrentPosition = (UInt32)FS.Position;
BlocksTypes CurrentBlockType = NumberToBlockType(GetBlockType(DataBuff[0]));
while (!LastFrame)
{
//primer bloque siempre ser� Block_Type_StreamInfo
offsets.Add((UInt32)FS.Position);
DataBuff = BR.ReadBytes(4);
LastFrame = IsLastFrame(DataBuff[0]);
CurrentBlockType = NumberToBlockType(GetBlockType(DataBuff[0]));
CurrentBlockSize = GetBlockSize(DataBuff);
FS.Seek(FS.Position + CurrentBlockSize, SeekOrigin.Begin);
if (CurrentBlockType == BlocksTypes.Block_Type_VorbisComment)
{
VorbisCommentPosition = offsets[offsets.Count - 1];
}
if (CurrentBlockType == BlocksTypes.Block_Type_Picture)
{
TieneImagen = true;
ImagePosition = offsets[offsets.Count - 1];
}
if (LastFrame)
LastBLockPosition = offsets[offsets.Count - 1];
}
}
else
{
MessageBox.Show("Archivo Flac no v�lido");
}
}
}
return offsets;
}
/// <summary>
/// Construye el encabezado de un bloque de informacion
/// </summary>
/// <param name="Number">Valor que indica la longitud del bloque en bytes</param>
/// <param name="BlockType">Valor que indica el tipo de bloque de acuerdo a <c>BlockTypes</c></param>
/// <param name="IsLastFrame">Indica si es el ultimo bloque de informacion</param>
/// <returns>Regresa un arreglo de cuatro bytes que representan el encabezado del bloque</returns>
private byte[] BuildHeader(uint Number, BlocksTypes BlockType, bool IsLastFrame)
{
byte[] HeaderBuff = new byte[4];
HeaderBuff = BitConverter.GetBytes(Number);
HeaderBuff[3] = (byte)BlockType;
if (IsLastFrame)
{
HeaderBuff[3] |= 128;
}
Array.Reverse(HeaderBuff);
return HeaderBuff;
}
/// <summary>
/// Convierte un número en su valor equivalente al tipo de bloque
/// </summary>
/// <param name="Data">
/// Entero sin signo con la representacion del bloque</param>
/// <returns>Regresa un valor de la enumeración <typeparamref name="BlockTypes"/>BlockTypes</returns>
private BlocksTypes NumberToBlockType(uint Data)
{
switch (Data)
{
case 0:
return BlocksTypes.Block_Type_StreamInfo;
case 1:
return BlocksTypes.Block_Type_Padding;
case 2:
return BlocksTypes.Block_Type_Application;
case 3:
return BlocksTypes.Block_Type_SeekTable;
case 4:
return BlocksTypes.Block_Type_VorbisComment;
case 5:
return BlocksTypes.Block_Type_CueSheet;
case 6:
return BlocksTypes.Block_Type_Picture;
default:
return BlocksTypes.Block_Type_NoValid;
}
}
/// <summary>
/// Lee el bloque con la informacion estructurada
/// </summary>
/// <param name="TheData">Arreglo de bytes del cual se extraera la información</param>
/// <returns>Regresa un arreglo del tipo string con todos los campos que se encontraron</returns>
private string[] ReadVorbisData(byte[] TheData)
{
uint NumberOfFields = 0;
uint CurrentField = 1;
uint CurrentPosition = 0;
//Vendor
uint CurrentSize = BitConverter.ToUInt32(TheData, (int)CurrentPosition);
CurrentPosition += 4;
byte[] CurrentChunk = new byte[CurrentSize];
Array.Copy(TheData, CurrentPosition, CurrentChunk, 0, CurrentSize);
string Currenttext = Encoding.UTF8.GetString(CurrentChunk);
CurrentPosition += CurrentSize;
NumberOfFields = BitConverter.ToUInt32(TheData, (int)CurrentPosition);
CurrentPosition += 4;
string[] Fields = new string[NumberOfFields];
//CommentField
while (CurrentField <= NumberOfFields)
{
CurrentSize = BitConverter.ToUInt32(TheData, (int)CurrentPosition);
CurrentPosition += 4;
byte[] CurrenUserCommentList = new byte[CurrentSize];
Array.Copy(TheData, CurrentPosition, CurrenUserCommentList, 0, CurrentSize);
Currenttext = Encoding.UTF8.GetString(CurrenUserCommentList);
CurrentPosition += CurrentSize;
Fields[CurrentField - 1] = Currenttext;
CurrentField += 1;
}
return Fields;
}
/// <summary>
/// Obtiene el valor del bloque desde un byte
/// </summary>
/// <param name="Data">byte del cual se va a obtener la información</param>
/// <returns>Regresa un valor que es equivalente al tipo de bloque</returns>
private uint GetBlockType(byte Data)
{
Data <<= 3;
Data >>= 3;
return Data;
}
/// <summary>
/// Obtiene si el bloque es el último de la serie
/// </summary>
/// <param name="Data">byte del cual se obtendra la información</param>
/// <returns>true si el bloque es el último, false en caso contrario</returns>
private bool IsLastFrame(byte Data)
{
Data >>= 7;
if (Data == 1)
return true;
else
return false;
}
/// <summary>
/// Obtiene el tamaño de un bloque
/// </summary>
/// <param name="Data">Arreglo de bytes que contiene el tamaño del bloque</param>
/// <returns>Regresa el tamaño del bloque actual</returns>
private uint GetBlockSize(byte[] Data)
{
byte[] CurrentData = Data;
//Ponemos el primer byte en 0 porque se usa para identificar el bloque
//a la hora de convertir a UInt32 se esperan 4 bytes, pero este siempre será 0 u otro valor no relevante
//para el tamaño del bloque
CurrentData[0] = 0;
Array.Reverse(CurrentData);
return BitConverter.ToUInt32(CurrentData, 0);
}
/// <summary>
/// Obtiene el tamaño de un bloque
/// </summary>
/// <param name="Data">Arreglo de bytes que contiene el tamaño del bloque</param>
/// <param name="StartIndex">Indica la posicion en donde se empezará a leer</param>
/// <param name="Size">Indica el tamaño en bytes que se van a leer</param>
/// <returns>Regresa el tamaño del bloque actual</returns>
private uint GetBlockSize(byte[] Data, int StartIndex, int Size)
{
byte[] CurrentData = new byte[4];
Array.Copy(Data, StartIndex, CurrentData, 0, Size);
Array.Reverse(CurrentData);
return BitConverter.ToUInt32(CurrentData, 0);
}
/// <summary>
/// Obtiene una imagen desde un archivo
/// </summary>
private void GetImageFromFile()
{
using OpenFileDialog OpDiag = new()
{
Filter = "Imagenes|*.jpg;*.png|Archivos JPG|*.jpg|Imagenes PNG|*.png",
Multiselect = false
};
if (OpDiag.ShowDialog() == DialogResult.OK)
{
TxtInputPic.Text = OpDiag.FileName;
PicInput.ImageLocation = TxtInputPic.Text;
PicInput.SizeMode = PictureBoxSizeMode.StretchImage;
FileInfo FileSize = new FileInfo(TxtInputPic.Text);
ImageSize = (UInt32)FileSize.Length;
LblPicInfo.Text = "Tamaño de la imagen en bytes " + ImageSize.ToString() + " bytes";
}
}
/// <summary>
/// Obtiene un bloque de datos de un archivo a partir de una posicion definida
/// </summary>
/// <param name="FileName">Archivo del cual se va a extraer el bloque</param>
/// <param name="StartPosition">Posicion des la cual se va a obtener el bloque relativo al inicio del archivo</param>
/// <returns>Regresa un arreglo de bytes con el bloque completo</returns>
private byte[] GetBlockData(string FileName, int StartPosition)
{
byte[] DataBuff;
using (FileStream FS = new(FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (BinaryReader BR = new(FS, Encoding.ASCII))
{
UInt32 CurrentBlockSize;
UInt32 CurrentPosition = (UInt32)FS.Position;
FS.Seek(StartPosition, SeekOrigin.Begin);
DataBuff = BR.ReadBytes(4);
CurrentBlockSize = GetBlockSize(DataBuff);
FS.Seek(StartPosition, SeekOrigin.Begin);
return (BR.ReadBytes((int)(CurrentBlockSize + 4)));
}
}
}
/// <summary>
/// Obtiene los datos de un bloque
/// </summary>
/// <param name="FileName">Nombre del archivo del cual se obtendra</param>
/// <param name="TypeOfBlock">Tipo de bloque que se va a buscar</param>
private void GetBlock(string FileName, BlocksTypes TypeOfBlock, int StartPosition)
{
byte[] DataBuff;
using (FileStream FS = new(FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (BinaryReader BR = new(FS, Encoding.ASCII))
{
UInt32 CurrentBlockSize;
UInt32 CurrentPosition = (UInt32)FS.Position;
BlocksTypes CurrentBlockType = BlocksTypes.Block_Type_NoValid;
FS.Seek(StartPosition, SeekOrigin.Begin);
DataBuff = BR.ReadBytes(4);
CurrentBlockType = NumberToBlockType(GetBlockType(DataBuff[0]));
if (CurrentBlockType == TypeOfBlock)
{
if (TypeOfBlock == BlocksTypes.Block_Type_VorbisComment)
{
string[] Fields;
CurrentBlockSize = GetBlockSize(DataBuff);
Fields = ReadVorbisData(BR.ReadBytes((int)CurrentBlockSize));
TxtInfo.Clear();
for (int i = 0; i < Fields.Length; i++)
{
TxtInfo.Text += Fields[i] + " | ";
}
}
else if (TypeOfBlock == BlocksTypes.Block_Type_Picture)
{
CurrentBlockSize = GetBlockSize(DataBuff);
PicFlac.Image = ReadPictureData(BR.ReadBytes((int)CurrentBlockSize));
}
}
else
{
return;
}
}
}
}
private void BtnOpen_Click(object sender, EventArgs e)
{
TieneImagen = false;
VorbisCommentPosition = 0;
ImagePosition = 0;
OpenFileDialog OpDiag = new()
{
Filter = "Archivos Flac|*.flac|Todos los arvhivos|*.*",
Multiselect = false
};
if (OpDiag.ShowDialog() == DialogResult.OK)
{
txtInputFlac.Text = OpDiag.FileName;
PicFlac.SizeMode = PictureBoxSizeMode.StretchImage;
Direcciones = GetOffsets(txtInputFlac.Text);
GetBlock(txtInputFlac.Text, BlocksTypes.Block_Type_VorbisComment, (int)VorbisCommentPosition);
if (TieneImagen)
GetBlock(txtInputFlac.Text, BlocksTypes.Block_Type_Picture, (int)ImagePosition);
}
}
private void BtnOpenPic_Click(object sender, EventArgs e)
{
ImageSize = 0;
GetImageFromFile();
}
private void BtnUpdateImage_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(txtInputFlac.Text))
{
MessageBox.Show("No hay arhivo de entrada", "Flac Image Writer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}else if (string.IsNullOrEmpty(TxtInputPic.Text))
{
MessageBox.Show("No hay imagen de entrada", "Flac Image Writer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
string SaveFile;
bool HasImage = false;
List<UInt32> Direcciones = GetOffsets(txtInputFlac.Text);
int CurrentSize = (int)Direcciones[Direcciones.Count - 1];
int bufferSize = 1024 * 1024;
using (SaveFileDialog SavDiag = new SaveFileDialog())
{
SavDiag.Filter = "Archivos FLAC|*.flac";
SavDiag.AddExtension = true;
SavDiag.OverwritePrompt = true;
if (SavDiag.ShowDialog() == DialogResult.OK)
SaveFile = SavDiag.FileName;
else
return;
}
using (FileStream FSWrite = new FileStream(SaveFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
{
using (BinaryWriter BW = new(FSWrite))
{
BW.Write(Encoding.ASCII.GetBytes("fLaC"));
for (int x = 1; x < Direcciones.Count; x++)
{
byte[] FileBuff = GetBlockData(txtInputFlac.Text, (int)Direcciones[x]);
BlocksTypes CurrentBlockType = BlocksTypes.Block_Type_NoValid;
CurrentBlockType = NumberToBlockType(GetBlockType(FileBuff[0]));
if (CurrentBlockType == BlocksTypes.Block_Type_Picture)
HasImage = true;
if (x == Direcciones.Count - 1)
{
if (!HasImage)
{
CurrentSize += FileBuff.Length;
BW.Write(BuildHeader((uint)(FileBuff.Length - 4), CurrentBlockType, false));
}
else
{
CurrentSize += FileBuff.Length;
BW.Write(BuildHeader((uint)(FileBuff.Length - 4), CurrentBlockType, true));
}
}
else
{
BW.Write(BuildHeader((uint)(FileBuff.Length - 4), CurrentBlockType, false));
}
BW.Write(FileBuff, 4, FileBuff.Length - 4);
}
if (HasImage == false)
BW.Write(BuildPictureBlock(BuildAPICFrame(TxtInputPic.Text, CboPictureType.SelectedIndex, GetMIMEType(TxtInputPic.Text), ImageSize)));
FileStream fs = new FileStream(txtInputFlac.Text, FileMode.Open, FileAccess.ReadWrite);
fs.Position = CurrentSize;
int bytesRead = -1;
byte[] bytes = new byte[bufferSize];
while ((bytesRead = fs.Read(bytes, 0, bufferSize)) > 0)
{
BW.Write(bytes, 0, bytesRead);
}
fs.Close();
}
}
}
private void Form1_Load(object sender, EventArgs e)
{
CboPictureType.SelectedIndex = 3;
}
private void PicFlac_DoubleClick(object sender, EventArgs e)
{
try
{
if (TieneImagen)
{
using (SaveFileDialog SavDiag = new SaveFileDialog())
{
SavDiag.Filter = "Arhivo Jpeg|*.jpg";
if (SavDiag.ShowDialog() == DialogResult.OK)
PicFlac.Image.Save(SavDiag.FileName);
}
//PicFlac.Image.Save
}
}
catch
{
MessageBox.Show("Error al guardar la imagen","Error",MessageBoxButtons.OK,MessageBoxIcon.Error);
}
}
private void button1_Click(object sender, EventArgs e)
{
}
}
}
Ahora veamos las funciones.
Primero empezamos con la enumeración "APICType", en ella se definen los posibles valores para el tipo de imagen que se va incrustarde acuerdo a lo establecido en la documentacion. Esto solo se hace para poder identificar las cosas de la forma mas humana posible, no es del todo necesario, pero ayuda bastante.
La primera funcion es "NormalizeToFourBytes()". Esta función recibe dos parámetros, el primero, es un valor del tipo UInt32 llamado "Value" y el otro es un valor del tipo bool llamado "Reverse". Esta función se encarga de convertir un numero en su equivalente en una secuencia de bytes, pero además tiene la opción de invertir el orden de la secuencia de bytes, dependiendo de la maquina en la que se trabaje o del idioma, los números se pueden representar de forma distinta, por ejemplo, si tenemos el número hexadecimal 0x3259 , puede representarse cómo 0x32 0x59 o 0x59 0x32. La función devuelve un arreglo de bytes con la representación del número.
La siguiente función es "GetMIMEType()". Esta función recibe un valor del tipo string con una cadena de texto. Se espera solo la extensión de las imágenes compatibles, es decir solo .jpg o .png. Regresa el Mime Type basado en la cadena de texto.
La siguiente funcion es "ByteArrayToImage()" Esta funcion recibe un arreglo de bytes, se espera que la secuencia de bytes en si conformen una imagen. Regresa un valor del tipo Image que contiene la imagen de la secuencia de bytes.
Y bien, por ahora es todo, las siguientes funciones merecen una explicacion mas detallada para poder entender mejor cada una. el código del formulario principal lo puedes copiar y pegar para probarlo, pero al final cuando todo este listo lo publicaré en mi dropbox para que lo puedas descargar y probar.
Los leo luego
Vamos a programar #105 - Agregando imagenes a archivos FLAC parte 1.
Hola de nuevo a todos. Tiempo ha pasado desde la última vez que un post se publicó. Cómo no es bueno dejar cosas pendiente, el día de hoy vamos a continuar con mas programación.
En alguno de los post anterior, vimos cómo leer los metadatos de los archivos FLAC, vimos cómo recuperar información importante: álbum, artista, título de la canción (solo por mencionar algunos), pero además, vimos cómo leer la imagen del archivo; eso si contiene una. El día de hoy, nos concentraremos en cómo está estructurado solo esta parte y servirá de preámbulo para hacer un programa en c# que escriba la imagen en el archivo.
Antes que nada, recordemos cómo esta estructurada la imagen. Hay que recordar que sigue los lineamientos de la etiqueta APIC de las etiquetas ID3v2. Desde este momento, toda la notación estará en bytes (al menos que se indique lo contrario).
- <4> El tipo de imagen que va a representar.
- 0 - Other
- 1 - 32x32 pixels 'file icon' (PNG only)
- 2 - Other file icon
- 3 - Cover (front)
- 4 - Cover (back)
- 5 - Leaflet page
- 6 - Media (e.g. label side of CD)
- 7 - Lead artist/lead performer/soloist
- 8 - Artist/performer
- 9 - Conductor
- 10 - Band/Orchestra
- 11 - Composer
- 12 - Lyricist/text writer
- 13 - Recording Location
- 14 - During recording
- 15 - During performance
- 16 - Movie/video screen capture
- 17 - A bright coloured fish
- 18 - Illustration
- 19 - Band/artist logotype
- 20 - Publisher/Studio logotype
- <4> El tamaño de el tipo de archivo (MIME Type).
- <N> La cadena de texto con el tipo de archivo (Solo usaremos "image/jpeg" o "image/png" ).
- <4> El tamaño de la descripción de la imagen.
- <N> La cadena de texto con la descripción de la imagen.
- <4> El ancho de la imagen en píxeles.
- <4> El alto de la imagen en píxeles.
- <4> La profundidad de colo de la imagen.
- <4> Para imágenes con indice de color, el número de colores a usar.
- <4> El tamaño de la imagen en bytes
- <N> Los datos de la imagen.
![]() |
| En esta imagen se puede apreciar cada apartado (si no se ve bien hay que darle zoom). |
Lo relevante de todo esto, y si miras bien la imagen, es los apartados cuatro, cinco, seis, siete; son totalmente innecesarios y se pueden dejar en cero o 0x00, 0x00, 0x00, 0x00; aun sin se establecen estas propiedades, en la documentacion oficial, no se aconseja hacer uso de ellas al momento de mostrar la imagen.
Habiendo descrito lo anterior, solo queda escribir código en c# para agregar la imagen.
Y bien, por ahora es todo. cómo de costumbre, hago la promesa de que trataré de publicar mas seguido. realmente agradezco a todos los que se toman la molestia de darse una vuelta por acá.
Los sigo leyendo.
Vamos a programar #103 - Leyendo metadatos de archivos FLAC (pt 2. Ver c#)
Hola de nuevo a todos. El día de hoy vamos a continuar con mas de los archivos FLAC y sus metadatos.
En el post anterior vimos cómo es que está estructurado cada bloque de metadatos, incluso vimos cómo es que se hace la lectura de cada uno (y leímos el bloque "STREAMINFO"). Ahora que sabemos cómo, solo nos queda automatizarlo y para eso vamos a usar el lenguaje de programación C#.
using System; using System.Drawing; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace FLACTagReader { public partial class Form1 : Form { // 0 : STREAMINFO 0000000 // 1 : PADDING 0000001 // 2 : APPLICATION 0000010 // 3 : SEEKTABLE 0000011 // 4 : VORBIS_COMMENT 0000100 // 5 : CUESHEET 0000101 // 6 : PICTURE 0000110 // 7-126 : reserved 1111000 - Invalid // 127 : invalid, to avoid confusion with a frame sync code /// <summary> /// Enumeración con los posibles bloques contenidos en un archivo FLAC /// </summary> private enum BlocksTypes { /// <summary> /// Bloque mandatorio con la información del Stream /// </summary> Block_Type_StreamInfo = 0, /// <summary> /// Bloque de Padding /// </summary> Block_Type_Padding = 1, /// <summary> /// Bloque con la informacion de la aplicación /// </summary> Block_Type_Application = 2, /// <summary> /// Bloque Seektable /// </summary> Block_Type_SeekTable = 3, /// <summary> /// Bloque con metadatos /// </summary> Block_Type_VorbisComment = 4, /// <summary> /// Bloque con cuesheet /// </summary> Block_Type_CueSheet = 5, /// <summary> /// Bloque con imagen /// </summary> Block_Type_Picture = 6, /// <summary> /// Bloque no válido /// </summary> Block_Type_NoValid = 7 } public Form1() { InitializeComponent(); } /// <summary> /// Crea una imagen a partir de una secuencia de bytes /// </summary> /// <param name="bytesArr">Matriz de bytes que contiene los datos de la imagen</param> /// <returns>Regresa una imagen</returns> public Image ByteArrayToImage(byte[] bytesArr) { using (MemoryStream memstr = new MemoryStream(bytesArr)) { Image img = Image.FromStream(memstr); return img; } } // 4 4 n*4 4 n*4 4 4 4 4 !4 !n*4 //<32>,<32>,<n*8>,<32>,<n*8>,<32>,<32>,<32>,<32>,<32>,<n*8> // 1 2 3 4 5 6 7 8 9 10 11 /// <summary> /// Lee todo los datos del bloque PICTURE y extrae la imagen contenida /// </summary> /// <param name="TheData">Arreglos bytes que contiene todo el bloque PICTURE</param> /// <returns>Regresa una imagen</returns> private Image ReadPictureData(byte[] TheData) { uint CurrentPos = 4;//1 uint CurrentSize = GetBlockSize(TheData, (int)CurrentPos, 4); CurrentPos += 4;//2 string MimeType = Encoding.ASCII.GetString(TheData, (int)CurrentPos, (int)CurrentSize); CurrentPos += CurrentSize;//3 CurrentSize = GetBlockSize(TheData, (int)CurrentPos, 4); CurrentPos += (CurrentSize + (4 * 5));//4-9 CurrentSize = GetBlockSize(TheData, (int)CurrentPos, 4); CurrentPos += 4; byte[] TempImage = new byte[CurrentSize]; Array.Copy(TheData, CurrentPos, TempImage, 0, CurrentSize); return ByteArrayToImage(TempImage); } /// <summary> /// Convierte un número en su valor equivalente al tipo de bloque /// </summary> /// <param name="Data"> /// Entero sin signo con la representacion del bloque</param> /// <returns>Regresa un valor de la enumeración <typeparamref name="BlockTypes"/>BlockTypes</returns> private BlocksTypes NumberToBlockType(uint Data) { switch (Data) { case 0: return BlocksTypes.Block_Type_StreamInfo; case 1: return BlocksTypes.Block_Type_Padding; case 2: return BlocksTypes.Block_Type_Application; case 3: return BlocksTypes.Block_Type_SeekTable; case 4: return BlocksTypes.Block_Type_VorbisComment; case 5: return BlocksTypes.Block_Type_CueSheet; case 6: return BlocksTypes.Block_Type_Picture; default: return BlocksTypes.Block_Type_NoValid; } } /// <summary> /// Lee el bloque con la informacion estructurada /// </summary> /// <param name="TheData">Arreglo de bytes del cual se extraera la información</param> /// <returns>Regresa un arreglo del tipo string con todos los campos que se encontraron</returns> private string[] ReadVorbisData(byte[] TheData) { uint NumberOfFields = 0; uint CurrentField = 1; uint CurrentPosition = 0; //Vendor uint CurrentSize = BitConverter.ToUInt32(TheData, (int)CurrentPosition); CurrentPosition += 4; byte[] CurrentChunk = new byte[CurrentSize]; Array.Copy(TheData, CurrentPosition, CurrentChunk, 0, CurrentSize); string Currenttext = Encoding.UTF8.GetString(CurrentChunk); CurrentPosition += CurrentSize; NumberOfFields = BitConverter.ToUInt32(TheData, (int)CurrentPosition); CurrentPosition += 4; string[] Fields = new string[NumberOfFields]; //CommentField while (CurrentField <= NumberOfFields) { CurrentSize = BitConverter.ToUInt32(TheData, (int)CurrentPosition); CurrentPosition += 4; byte[] CurrenUserCommentList = new byte[CurrentSize]; Array.Copy(TheData, CurrentPosition, CurrenUserCommentList, 0, CurrentSize); Currenttext = Encoding.UTF8.GetString(CurrenUserCommentList); CurrentPosition += CurrentSize; Fields[CurrentField - 1] = Currenttext; CurrentField += 1; } return Fields; } /// <summary> /// Obtiene el valor del bloque desde un byte /// </summary> /// <param name="Data">byte del cual se va a obtener la información</param> /// <returns>Regresa un valor que es equivalente al tipo de bloque</returns> private UInt32 GetBlockType(byte Data) { Data <<= 3; Data >>= 3; return Data; } /// <summary> /// Obtiene si el bloque es el último de la serie /// </summary> /// <param name="Data">byte del cual se obtendra la información</param> /// <returns>true si el bloque es el último, false en caso contrario</returns> private bool IsLastFrame(byte Data) { Data >>= 7; if (Data == 1) return true; else return false; } /// <summary> /// Obtiene el tamaño de un bloque /// </summary> /// <param name="Data">Arreglo de bytes que contiene el tamaño del bloque</param> /// <param name="StartIndex">Indica la posicion en donde se empezará a leer</param> /// <param name="Size">Indica el tamaño en bytes que se van a leer</param> /// <returns>Regresa el tamaño del bloque actual</returns> private UInt32 GetBlockSize(byte[] Data, int StartIndex, int Size) { byte[] CurrentData = new byte[4]; Array.Copy(Data, StartIndex, CurrentData, 0, Size); Array.Reverse(CurrentData); return BitConverter.ToUInt32(CurrentData, 0); } /// <summary> /// Obtiene el tamaño de un bloque /// </summary> /// <param name="Data">Arreglo de bytes que contiene el tamaño del bloque</param> /// <returns>Regresa el tamaño del bloque actual</returns> private UInt32 GetBlockSize(byte[] Data) { byte[] CurrentData = Data; //Ponemos el primer byte en 0 porque se usa para identificar el bloque //a la hora de convertir a UInt32 se esperan 4 bytes, pero este siempre será 0 u otro valor no relevante //para el tamaño del bloque CurrentData[0] = 0; Array.Reverse(CurrentData); return BitConverter.ToUInt32(CurrentData, 0); } /// <summary> /// Obtiene los datos de un bloque /// </summary> /// <param name="FileName">Nombre del archivo del cual se obtendra</param> /// <param name="TypeOfBlock">Tipo de bloque que se va a buscar</param> private void GetBlock(string FileName, BlocksTypes TypeOfBlock) { byte[] DataBuff; bool LastFrame = false; FileStream FS = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.Read); using (BinaryReader BR = new BinaryReader(FS, Encoding.ASCII)) { DataBuff = BR.ReadBytes(4); if (string.Equals(Encoding.ASCII.GetString(DataBuff), "fLaC", StringComparison.Ordinal)) { UInt32 CurrentBlockSize; UInt32 CurrentPosition = (UInt32)FS.Position; BlocksTypes CurrentBlockType = BlocksTypes.Block_Type_NoValid; while (!LastFrame) { //primer bloque siempre será Block_Type_StreamInfo DataBuff = BR.ReadBytes(4); LastFrame = IsLastFrame(DataBuff[0]); CurrentBlockType = NumberToBlockType(GetBlockType(DataBuff[0])); if (CurrentBlockType == TypeOfBlock) { if (TypeOfBlock == BlocksTypes.Block_Type_VorbisComment) { string[] Fields; CurrentBlockSize = GetBlockSize(DataBuff); Fields = ReadVorbisData(BR.ReadBytes((int)CurrentBlockSize)); textBox1.Clear(); for (int i = 0; i < Fields.Length; i++) { textBox1.Text += Fields[i] + " | "; } } else if (TypeOfBlock == BlocksTypes.Block_Type_Picture) { CurrentBlockSize = GetBlockSize(DataBuff); pictureBox1.Image = ReadPictureData(BR.ReadBytes((int)CurrentBlockSize)); } } else { CurrentBlockSize = GetBlockSize(DataBuff); FS.Seek(FS.Position + CurrentBlockSize, SeekOrigin.Begin); } } } else { MessageBox.Show("Archivo Flac no válido"); } } } private void button1_Click(object sender, EventArgs e) { using (OpenFileDialog OpDiag = new OpenFileDialog()) { OpDiag.Filter = "Archivos FLAC|*.flac|Todos los arhivos|*.*"; if (OpDiag.ShowDialog() == DialogResult.OK) { GetBlock(OpDiag.FileName, BlocksTypes.Block_Type_VorbisComment); GetBlock(OpDiag.FileName, BlocksTypes.Block_Type_Picture); pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage; } } } } }
- <32> El tipo de imagen de acuerdo a la descripción de la etiqueta ID3 "APIC".
- <32> El tamaño de la descripción del tipo de archivo o MIME type.
- <n*8> Cadena de texto con la descripción del tipo de archivo.
- <32> El tamaño de la descripción de la imagen.
- <n*8> La descripción de la imagen en UTF8
- <32> El ancho de la imagen en píxeles
- <32> El alto de la imagen en pixeles.
- <32> La profundidad del color de la imagen en bits por pixel.
- <32> Para las imágenes con índice de color, el número de colores usados, para el resto 0.
- <32> El tamaño de los datos de la imagen.
- <n*8> Los datos de la imagen.
Vamos a programar #98 - Ver el rendimiento del PC usando arduino y C#.
Hola de nuevo a todos, el día de hoy vamos a ver cómo ver el rendimiento del PC usando C# y arduino.
En el post anterior vimos una versión beta de un programa que sirve para monitorear cual es el rendimiento del PC, y si bien ya era algo funcional, le he hecho algunas mejoras para que resulte más fácil de leer.
El código está conformado por dos partes, la primera, un programa en C# para windows que se encarga de obtener la información de cual es la carga del CPU en porcentaje, pero además de la RAM también en porcentaje, luego esa información la envía a un puerto al cual conectamos un arduino. La segunda parte consiste en un arduino que toma la información que recibió del programa y la muestra.
El programa en C#
using System;
using System.Windows.Forms;
using System.IO.Ports;
namespace CPUMeterToArduino
{
public partial class Form1 : Form
{
System.Diagnostics.PerformanceCounter CPULoad;
System.Diagnostics.PerformanceCounter RAMInUse;
System.Diagnostics.PerformanceCounter AvailableRAM;
private SerialPort Port = new SerialPort();
private void ArduinoMessage(string Message)
{
try
{
Port.Write(Message);
}
catch (Exception e)
{
MessageBox.Show(e.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
string DATA = string.Concat(((int)CPULoad.NextValue()).ToString("D3") , "," , PercentRAM(RAMInUse.NextValue() , AvailableRAM.NextValue()).ToString("D3") , "\0");
SerialPort sp = (SerialPort)sender;
string indata = sp.ReadExisting();
if (indata.Contains("\r\n"))
{
//ArduinoMessage("100,050\0");
ArduinoMessage(DATA);
}
}
Timer TimerMain;
private int PercentRAM(float InUse, float Available)
{
if (Available == 0)
return 100;
else
{
float RamMBInUse = (InUse / 1024 / 1024);
float TotalRam = RamMBInUse + Available;
float Percent = ((RamMBInUse / TotalRam) * 100);
return (int)Percent;
}
}
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, System.EventArgs e)
{
RAMInUse = new System.Diagnostics.PerformanceCounter("Memory", "Committed Bytes");
AvailableRAM = new System.Diagnostics.PerformanceCounter("Memory", "Available MBytes");
CPULoad = new System.Diagnostics.PerformanceCounter("Processor", "% Processor Time", "_Total");
TimerMain = new Timer();
TimerMain.Interval = 1000;
TimerMain.Enabled = true;
TimerMain.Tick += new System.EventHandler(TimerMain_Tick);
string[] Ports = SerialPort.GetPortNames();
cboportname.Items.AddRange(Ports);
cboportname.Text = "COM3";
Port.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
TSSLbl.Text = Port.IsOpen ? "Conectado" : "No conectado";
}
private void TimerMain_Tick(object sender, EventArgs e)
{
PBCPUUsage.Value = (int)CPULoad.NextValue();
int PR = PercentRAM(RAMInUse.NextValue(), AvailableRAM.NextValue());
PBRAMUsage.Value = PR;
this.Text = "Uso de RAM " + PR + "%" + " | CPU: " + PBCPUUsage.Value; ;
NTFYMain.Text = "Uso de RAM " + PR + "%";
TSSLbl.Text = Port.IsOpen ? "Conectado con " + Port.PortName : "No conectado";
}
private void Form1_Resize(object sender, EventArgs e)
{
if (this.WindowState == FormWindowState.Minimized)
{
Hide();
NTFYMain.Visible = true;
}
}
private void NTFYMain_MouseDoubleClick(object sender, MouseEventArgs e)
{
Show();
this.WindowState = FormWindowState.Normal;
NTFYMain.Visible = false;
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (Port.IsOpen == true)
{
Port.Close();
}
NTFYMain.Visible = false;
}
private void BtnConnect_Click(object sender, EventArgs e)
{
try
{
if (Port.IsOpen == false)
{
Port.BaudRate = 9600;
Port.PortName = cboportname.Text;
Port.Open();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void BtnDis_Click(object sender, EventArgs e)
{
if (Port.IsOpen == true)
{
Port.Write("Se ha desconectado el cliente\0");
Port.Close();
}
}
private void TSMIRestore_Click(object sender, EventArgs e)
{
NTFYMain_MouseDoubleClick(null, null);
}
private void TSMIExit_Click(object sender, EventArgs e)
{
Application.Exit();
}
}
}
El código para arduino.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
//SCL - A5
//SDA - A4
LiquidCrystal_I2C lcd(0x27,20,4);
char Texto[80];
char BuffCPU[20];
char BuffRAM[20];
byte BarBeginEmpty[] = {
B11111,
B10000,
B10000,
B10000,
B10000,
B10000,
B10000,
B11111
};
byte BarBeginFull[] = {
B11111,
B10000,
B10111,
B10111,
B10111,
B10111,
B10000,
B11111
};
byte BarSegmentFull[] = {
B11111,
B00000,
B11111,
B11111,
B11111,
B11111,
B00000,
B11111
};
byte BarSegmentEmpty[] = {
B11111,
B00000,
B00000,
B00000,
B00000,
B00000,
B00000,
B11111
};
byte BarEndEmpty[] = {
B11111,
B00001,
B00001,
B00001,
B00001,
B00001,
B00001,
B11111
};
byte BarEndFull[] = {
B11111,
B00001,
B11101,
B11101,
B11101,
B11101,
B00001,
B11111
};
void setup() {
lcd.init();
lcd.createChar(0 , BarBeginEmpty);
lcd.createChar(1 , BarBeginFull);
lcd.createChar(2 , BarEndEmpty);
lcd.createChar(3 , BarEndFull);
lcd.createChar(4 , BarSegmentEmpty);
lcd.createChar(5 , BarSegmentFull);
lcd.backlight();
Serial.begin(9600);
}
void DrawProgressBar(uint8_t Value, uint8_t XLocation, uint8_t YLocation){
uint8_t Progress = map(Value , 1 , 100 , 0 , 20);
lcd.setCursor(XLocation , YLocation);
for (uint8_t i = 0; i < 20; i++)
{
if (i == 0)
{
if (Progress == 0)
lcd.write(0);
else
lcd.write(1);
}
else if (i == 19)
{
if (Progress == 20)
lcd.write(3);
else
lcd.write(2);
}
else
{
if (Progress <= i)
lcd.write(4);
else
lcd.write(5);
}
}
}
void loop() {
int i = 0;
if (Serial.available()) {
while (Serial.available() > 0) {
Texto[i] = Serial.read();
i++;
}
Texto[i] = '\0';
}
String Text(Texto);
int X = Text.substring(0 , 3).toInt();
int Y = Text.substring(5 , 7).toInt();
lcd.setCursor(0 , 0);
//lcd.print(Texto);
sprintf(BuffCPU , "CPU: %03d", X);
sprintf(BuffRAM , "RAM: %03d", Y);
lcd.print(BuffCPU);
lcd.setCursor(0,2);
lcd.print(BuffRAM);
DrawProgressBar(X , 0 , 1);
DrawProgressBar(Y , 0 , 3);
Serial.println("\r\n");
delay(500);
}Para empezar, creamos seis "sprites" que corresponden a los segmentos de la barra de progreso "BarBeginEmpty", "BarBeginFull", "BarSegmentFull", "BarSegmentEmpty", "BarEndEmpty" y "BarEndEmpty", aunque sus nombres pueden resultar auto-explanatorios para algunos, cada uno define cada estado para las diferentes partes de la barra de progreso. Cada uno de los segmentos de la pantalla están conformados por una matriz de 5x8, entonces (y por ahora no entrare en detalles) creamos un array de esa dimensión y para simplificarlo, podemos aprovechar que arduino acepta numero en binario siempre y cuando vayan precedidos por el prefijo "B", ahora observemos la siguiente imagen:
![]() |
| Está es la definición de "BarBeginFull" |
Podemos observar que para formar el sprite, simplemente debemos de decidir si queremos o no usar cada cuadrado de la matriz. Si nos fijamos bien en la imagen, "BarBeginFull" es: "B11111,B10000,B10111,B10111,B10111,B10111,B10000,B11111"
Por lo tanto podemos usar una tabla e iluminar las celdas para formar el sprite que queramos.
![]() |
| Definición de "BarSegmentFull" |
![]() |
| Definición de "BarSegmentEmpty" |


















