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
Cumplimos 10 Años!!!
Un día cómo hoy pero de hace diez años surgió el blog de XWork. Ha pasado tanto tiempo desde que todo esto empezó, si bien ha venido en declive la cantidad de entradas que se crean al mes (eso por decirlo de alguna manera ya que el blog ha estado totalmente muerto en los últimos meses). creo que es importante seguir.
Es un camino complicado, ya que actualmente mucha gente ya no recurre a lugares como este para poder encontrar informacion, eso debido a que la inteligencia artificial ha ayudado a resolver muchos de los dilemas de los programadores, no por eso digo que sea malo usarla, cómo cualquier herramienta, es buena mientras no se vuelva la encargada de hacer todo por uno.
Confío en que los post seguiran fluyendo y la gennte venga a leer un poco.
Cómo de costumbre, los sigo leyendo a todos y sé que sitios cómo este siempre perduraran.
Vamos a platicar #13 - It´s no over (yet)
Hola de nuevo a todos. Ha pasado el tiempo desde la última vez que algo se publico por acá. Los tiempos han cambiado y cada vez es menos común que la gente recurra a este tipo de sitios para buscar información.
En los próximos días este sitio cumplirá diez años; si bien es cierto que desde hace un par, todo ha venido en declive, es importante mencionar que trataré de continuar con el, mas que nada por todo lo que representa para mi.
Por ahora es todo y cómo de costumbre le agradezco a todos los que se toman el tiempo de pasar a leer.
Los sigo leyendo.
Bienvenido el 2026
Hola de nuevo a todos. El día de hoy solo escribo para hacer lo que usualmente hago cada fin de año. Es cierto que el blog ha disminuido en la cantidad de post que se publican, pero si de algo estoy seguro, es que este es mi lugar. Ahora mas que nunca, debemos de crear conocimiento y que sea la mente humana la que predomine sobre la máquina.
Por el momento es todo y al igual que cada año, nos seguiremos leyendo por acá. Que el 2026 sea un año bueno para todas las personas que se toman la molestia de leer.
Los leo luego
Vamos a programar #106 - Agregando imagenes a archivos FLAC parte 2.
Hola de nuevo a todos, el día de hoy vamos a continuar con mas de los archivos FLAC.
En el post anterior vimos cómo es que esta estructurado la parte que conforma la imagen en un archivo FLAC. Hoy, al igual que en otras ocasiones, antes de escribir el programa en c#, vamos a insertar la imagen "a mano" solo usando un editor Hexadecimal; el que siempre uso es XVI32 que puedes descargar de su sitio oficial. Cómo archivo de prueba voy a usar una canción en formato FLAC, para que sea sencillo seguir los pasos, te recomiendo usar el mismo archivo que puedes descargar aquí. Pero al igual que las otras canciones, si no posees el original, debes de eliminarlo al terminar de hacer las pruebas. La caratula del álbum también la puedes descargar de aquí.Una vez todo listo, procederemos a abrir el archivo FLAC con XVI32. Hay que tomar en cuenta que a partir de este momento, cuando escribamos un numero en decimal simplemente será el numero, cuando hagamos referencia a un numero en binario, será bxxxxxxxx y en hexadecimal sera 0xXX
Una vez abierto, vamos a desplazarnos por el archivo. Si hacemos memoria, los primeros cuatro bytes, corresponden al identificador del archivo que es "fLaC" inmediatamente después de este, viene el bloque con la información del stream. este tiene un tamaño fijo de 34 bytes (en bits son: 16+16+24+24+20+3+5+36+128 = 272 bits = 34 bytes), que es lo que nos indica el byte en la dirección 0x05 hasta 0x07 que es la secuencia 0x00, 0x00, 0x22 que al convertirlo a decimal nos da treinta y cuatro, hay que recordar que en el tamaño no se incluye el encabezado. En XVI32, nos podemos desplazar cierto numero de bytes. Al terminar de leer el encabezado del bloque con la información del stream, deberíamos de estar en la dirección 0x07 posicionamos el cursor en el byte siguiente y presionamos la tecla de "control" y la tecla "b", eso le indicará a XVI32 que estamos marcando el inicio de lo que queremos seleccionar, luego vamos al menú "edit>>block <n> chars" e introducimos tantos bytes cómo nos dijo el encabezado del bloque, en este caso fueron treinta y cuatro.
Con esto estará resaltado todo el bloque. Esto solo lo hacemos con el fin de que todo resulte mas claro, pero este bloque no lo vamos a modificar, asi que avanzamos al siguiente.
![]() | ||
| Se puede apreciar todo el bloque. |
Inmediatamente después de la selección, viene el siguiente bloque. Por ahora los que nos interesan son o el bloque de la imagen (aunque en este ejemplo no existe) o el bloque de "padding". De cualquier forma vamos a leer los bloques hasta que encontremos alguno. Para eso simplemente repetimos el paso anterior.
En nuestra secuencia, leemos 0x04, 0x00, 0x01 y 0x58 (el tamaño es 0x0158) que al convertirlo a decimal nos da 344, el primer byte nos dice que este es el bloque "Vorbis Comment", nuevamente marcamos esa cantidad de bytes después del encabezado.
Algo importante que debemos de recordar, es que debemos de repetir todo hasta que encontremos el marcador del ultimo bloque. Si continuamos leyendo, tenemos la secuencia 0x81, 0x00, 0x1E y 0xD0, esta es relevante porque al convertir el primer byte a binario, nos da b10000001 y justamente el primer bit (de izquierda a derecha) nos dice que este es el último bloque y además que es del tipo "padding" y que tiene una longitud de 7,888 bytes, procedemos a marcarlos.
Con esto ya tenemos toda la información que necesitamos. antes que nada, necesitamos ver si el tamaño reservado (padding) es suficiente para nuestra imagen, si descargaste la imagen al inicio, estas deben de ser sus propiedades.
Para poder inscrustar la imagen necesitamos un espacio 180,677 bytes mas el encabezado, el espacio que tenemos es de 7,888 y claramente es insuficiente, por lo que vamos a reemplazar el bloque en su totalidad.
Preparando la imagen.
Vamos a usar Xvi32 para crear el bloque completo. Primero preparamos el bit que describe el bloque. EN este caso, va a ser el ultimo bloque, pero ademas sera del tipo "PICTURE", el primer bit es b1, seguido de la descripción que es b110, al unirlos para formar el byte tenemos b10000110 que es 134 en decimal o 0x86. En XVI32 podemos insertar directamente los dígitos hexadecimales en el panel de lado izquierdo, si queremos insertar 134 decimal simplemente lo convertimos a hexadecimal y lo escribimos tal cual. Antes de continuar, guardamos lo que llevamos con el nombre de "Picture block.bin".
para calcular el tamaño total, primero vamos a construir al bloque APIC.
- Tipo de imagen por ahora usaremos 0x03 (cubierta frontal del disco) usa 4 bytes.
- 0x00, 0x00, 0x00, 0x03, en XVI32 vamos al menú "Edit>>Inser string"
- El tamaño del tipo MIME , usaremos "image/jpeg" que tiene una longitud de 10 bytes.
- 0x00, 0x00, 0x00, 0x0A.
- La cadena de texto "image/jpeg".
- Usando el comando insertar escribimos literal image/jpeg.
- El tamaño de la descripción , usaremos la misma secuencia "image/jpeg" que tiene una longitud de 10 bytes.
- 0x00, 0x00, 0x00, 0x0A.
- La cadena de texto "image/jpeg".
- Usando el comando insertar escribimos literal image/jpeg.
- El ancho de la imagen
- 0x00, 0x00, 0x00, 0x00
- El alto de la imagen
- 0x00, 0x00, 0x00, 0x00
- La profundidad de color
- 0x00, 0x00, 0x00, 0x00
- El indice de colores
- 0x00, 0x00, 0x00, 0x00
- El tamaño de la imagen en bytes de la imagen
- la imagen pesa 180,677 bytes que es 0x02C1C5 que queda para insertarlo cómo 0x00, 0x02, 0xC1, 0xC5
- Los datos de la imagen.
- Abrimos la imagen con XVI32, seleccionamos todos los bytes, los copiamos y los pegamos inmediatamente después de nuestro último byte.
![]() | |
| Así debería de lucir nuestra secuencia antes de insertar los datos de la imagen. |
Guardamos el archivo como APIC.bin e inspeccionamos sus propiedades para obtener el tamaño.
Volvemos a abrir "Picture block.bin" e insertamos el tamaño de "APIC.bin", en este caso 180,729 es 0x02C1F9 y en la secuencia es: 0x02, 0xC1, 0xF9. Una vez escrito, Abrimos "APIC.bin" y seleccionamos todos los datos, los copiamos y pegamos en "picture block.bin" inmediatamente después de la secuencia actual.
Con esto ya tenemos todo listo para unir las partes.Para eso, seleccionaremos y copiaremos todos los datos previos al bloque que vamos a sustituir (en nuestro archivo FLAC). Los copiamos y pegamos, luego el bloque que creamos y finalmente el resto del archivo FLAC.
Para simplifica un tanto las cosas, y si quieres ir directo al grano, de nuestro archivo original, primero seleccionaremos los primeros 390 bytes, los copiamos y los pegamos en un nuevo archivo, luego abrimos "picture block.bin", seleccionamos todo y lo insertamos en el archivo que creamos, luego en nuestro archivo original, nos desplazamos a la posición 8,282, usando el comando "go to" en el menú "address" y de ahí seleccionamos todo el resto del archivo, lo copiamos y lo insertamos en el nuevo, si todo fue bien deberíamos de tener nuestro archivo FLAC con su caratula.
Al inspeccionarlo tenemos que todo fue como es debido.
Y bien, por ahora es todo, el objetivo de hacer los pasos a mano, es para ayudar a comprender que es lo que el programa va a hacer. En el siguiente post escribiremos el código en C# que nos ayudara.
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.
Cumplimos 9 años.
Hola de nuevo a todos. Un día cómo hoy pero de hace nueve años nació el blog de Xwork.
Si bien la actividad del blog ha bajado a un nivel de cero. Eso no implica que me haya olvidado de el, afortunadamente para mi, he llegado a una etapa nueva de mi vida y por ahora es a lo que le dedico mi atención. Eso no implica que el blog haya muerto, simplemente estoy disfrutando el momento.
Eso no quiere decir que me haya olvidado que este sitio lo cree en primer lugar para pasar las situaciones complicadas, por eso, ahora con una nueva compañera, es cuestión de retomarlo (ahora con compañía).
Sin mas que decir, espero que los artículos ya disponibles, sean de utilidad sin importar que el mas viejo tenga nueve años (el conocimiento es atemporal). En algún momento continuaremos, pero por ahora solo me queda agradecer a aquellos que expectantes se dan una vuelta de vez en vez.
Sin mas que decir. Gracias y cómo siempre los sigo leyendo.
Att: XWork (y ahora con EV).
Vamos a programar #105 - Enmascaramiento y secuencias de bits - Camino a ClockView 3.0
Hola de nuevo a todos, el día de hoy vamos a ver como hacer uso del enmascaramiento y cómo ahorrar un poco de memoria para algunos tipos de variables.
Desde hace tiempo actualicé ClockView, si no sabes de lo que hablo, puedes darte una vuelta por el post de la versión dos.
Desde el principio planee poner una serie de alarmas muy similares a las de un reloj casio que tengo, este tiene la particularidad de que se puede establecer por fecha: es decir, si queremos que suene el tres de julio, eso hará; por mes: es decir, si establecemos solo el mes, sonará todos los días de octubre (por solo decir una fecha); por año: la alarma sonará todos los días del año. Además se podía establecer para que la alarma sonara ciertos días de la semana, está función me es realmente útil, ya que normalmente solo algunos días de la semana requiero de la alarma (aunque ya no tanto, creo que llegué a la edad en que le gano al despertador).
Si recordamos la versión anterior de clock view, este, carecía de una función de alarma, pero el modulo de reloj en tiempo real DS1302, además de llevar la cuenta del tiempo, posee la capacidad de llevar los días de la semana. Para poder guardar los datos de los días de la semana en los cuales queremos que la alarma suene, vamos a usar un solo byte.
Para eso, podemos estructurar los días de la semana cómo se muestra en la siguiente imagen:
Oportunamente para nosotros, la semana consta de siete días, y cada byte consta de ocho bits, con lo que podemos asignar un valor cómo se muestra en la imagen anterior. Pero mas que su valor numérico, debemos de ver cómo es su representación en binario.
Pero ahora que hemos asignado un valor a cada día de la semana ¿cómo debemos de usarlos? Antes de entrar esos detalles, supongamos que queremos que la alarma suene los días: domingo, jueves y sábado, deberíamos de hacer algo cómo lo que sigue:
- Tomar el valor del primer día deseado.
- Tomar el valor del segundo día deseado
- Aplicar el operador "or" a ambos números y asignarlos a una variable.
- Si se desean agregar mas días, al valor anterior se le aplica de nuevo el operador "or" junto con el día a agregar
Si lo convertimos a código en c, quedaría algo cómo lo que sigue:
uint8_t BuildTheDaysOfAlarms(uint8_t TheDay1, uint8_t TheDay2)
{
return TheDay1|TheDay2;
}
Entonces los días que queremos usar en la alarma quedarían cómo:
- 00000001 or 00100000 or 01000000 = 01010001.
- que es igual en decimal a: 1 or 16 or 64 = 81.
- Lunes a viernes = 00111110 = 62.
- Sábado = 01000000 = 64
- 62 and 64 = 0.
- El día no está en el rango.
- Lunes, miércoles y viernes = 00101010 = 42.
- Miércoles = 00001000 = 8
- 42 and 8 = 8
- El día está en el rango.
Cumplimos 8 años.
Hola de nuevo a todos. El día de hoy solo escribo para recordar que un día cómo hoy pero de hace ocho años, nació el Blog de Xwork. Siempre le voy a agradecer a todos los que se toman la molestia de venir a leer aunque sea un poco. Si bien la cantidad de publicaciones ha disminuido de forma drástica, tengan por seguro que va a seguir habiendo, a inicio de año planifique todo y de hecho se iba a ir publicando un post al mes, pero por razones que son difíciles de comprender aun para mi, algo salio mal en mayo y todos los post que se iban a publicar, simplemente desaparecieron. Estoy arreglando todo y en cuanto este listo, se subirán de uno por uno.
Sin más que decir, solo quiero agradecer una vez mas a todos los que me leen.
Nos seguimos leyendo.
Att. Xwork.
El Video Correcto #10 - VEncoder y subtitulos.
Hola de nuevo a todos, el día de hoy vamos a ver cómo insertar subtitulos en los videos que se codifiquen con VEncoder.
Hay que recordar que VEncoder es una interfaz de usuario para FFMPEG; si bien la idea es mantener los mas simple posible todo, para lograr resultados complejos, es necesario hacer ajustes complejos. Antes de continuar, me gustaría recapitular cual fue la idea de VEncoder. El programa inicialmente nació cómo una herramienta para convertir los videos para que el Play Station Portable pudiera reproducirlos, incluso era parte de un programa llamado PSP Manager, y cómo su nombre lo pudiera sugerir, servía para manejar el contenido multimedia de la consola. Poco a poco cada una de las partes se separó y dieron origen a VEncoder y AEncoder que a diferencia de PSP Manager, no tuvieron tanta relevancia.
He usado mi PSP desde el 2010 y de las pocas cosas que no he logrado entender, es cómo funciona la opción de los subtítulos en el reproductor de video.
Recientemente me he encontrado con gente para la cual la consola es algo totalmente nuevo y he compartido algunos videos con esas personas, pero cómo los tiempos cambian, ahora resulta más sencillo encontrar contenido que no está en nuestro idioma por lo que resulta necesario encontrar una forma de incluirlos. Cómo mencioné hace poco, a pesar de que la PSP tiene (o eso según el menú) la capacidad de reproducir subtítulos, nunca supe cómo usarlos.
FFMPEG tiene la capacidad de reproducir subtítulos, pero antes de detallar cómo es que funcionan, hay que resaltar algo importante. Hay dos formas de incrustar los subtítulos; la primera es sencilla, se dota de un archivo que tiene la transcripción de los diálogos con los tiempos para cada uno, el reproductor los lee y los muestra sobre el video, a esto le podemos llamar "subtítulos por software". La otra forma es similar, tenemos un archivo con las transcripciones pero solo las usamos al momento de codificar el video y justo en la codificación, vamos a modificar la imagen correspondiente al tiempo del fotograma, de tal forma que el texto quede cómo parte de la misma, con eso bastará para que los subtítulos queden de forma permanente en el video.
Para lograr esto, usaremos FFMPEG junto con VEncoder (el cual actualizare pronto para que la opción "Si hay subtítulos 'quemarlos'" funcione). En VEncoder, hay una ficha llamada "Parámetros adicionales" dentro, hay una ficha llamada "video". FFMPEG soporta varios tipos de subtítulos, archivos SRT o archivos ASS, los primeros son mas sencillos, pues solo incluyen el dialogo y el tiempo en que deben de aparecer; los segundos incluyen "estilos" y efectos (transiciones, diferentes ubicaciones para el texto).
Primero veamos un ejemplo de cómo insertar un archivo SRT.
-vf "subtitles='I\:\\\VIDEOS\\\Track3.srt'"
-vf "ass='I\:\\\VIDEOS\\\Track3.srt'"
![]() |
| No es precisamente el mismo cuadro, pero se ven lo subtítulos "quemados" |





























