La batallita del agüelo:
No me voy a enrollar demasiado explicando qué hace este código, pero te
pongo en situación:
En mis foros, preguntaron cómo "Leer
secuencia de Bytes en un archivo binario" y se refería a extraer una
imagen de un fichero con extensión PBP.
Por lo que he visto en la "red", ese tipo de fichero es el usado por las
PSP de Sony (Play Station) y entre otras cosas tiene una imagen en formato
PNG, el código que te muestro aquí sirve para extraer esa imagen y guardarla
como un fichero independiente.
El formato de los ficheros PBP y cómo extraer el contenido
Los ficheros PBP tienen una cabecera con la información que contiene. Esa
cabecera es de 40 bytes y tiene el siguiente formato/información:
Offset |
Contents |
Remarks |
0-3 |
File type/descriptor |
Always a null byte followed by "PSP". |
4-7 |
PBP Version (?) |
Always 00 00 01 00. |
8-11 |
Offset of PARAM.SFO |
|
12-15 |
Offset of ICON0.PNG |
|
16-19 |
Offset of ICON1.PMF |
|
20-23 |
Offset of PIC0.PNG |
|
24-27 |
Offset of PIC1.PNG |
|
28-31 |
Offset of SND0.AT3 |
|
32-35 |
Offset of DATA.PSP |
|
36-39 |
Offset of DATA.PSAR |
|
Según indica esta página:
http://www.pdc.me.uk/pbpdocs/ los "offset" son valores enteros sin signo
de 32 bits que están almacenados como "little endian", que para que nos
entendamos, es como si los valores estuvieran al revés. Por ejemplo, si el
valor del offset de ICON0.PNG fuese 448 se almacenaría como 4 bytes que en
valor hexadecimal tendrían esta secuencia dentro del fichero: C0 01 00 00
(pero sin espacios). Ese valor convertido en formato "normal" sería: 00 00
01 C0 que corresponde al valor decimal 448.
Ese offset representa la posición en que se inicia la imagen que
corresponde a ICON0.PNG.
La pregunta es ¿y cuánto mide esa imagen?
Antes de responderte y para que lo entiendas (y te sirva para extraer el
resto del contenido), ese offset indica dónde empieza y la longitud que
tiene viene dada por la posición del siguiente "fichero" menos ese offset.
En el fichero PBP lo siguiente a ICON0.PNG es ICON1.PMF (que no se lo que
es, no tengo una PSP) y en el fichero EBOOT.PBP que he usado para probar
(bajado de una página, ya te digo que no tengo una PSP), la posición de
inicio (el offset) de ICON1.PMF es: 5366 (en hexadecimal: 00 00 14 F6), por
tanto, la longitud de ICON0.PNG es: 5366 - 448 = 4918.
Con el resto de cosas habría que hacer lo mismo, hasta el último
(DATA.PSAR), que al no haber nada después, habría que considerar que el
offset siguiente es la longitud total del fichero.
Por lo que se ve, no todas las "cosas" que tiene un fichero
PSP está dentro, por tanto, es posible que varios de esos
offsets tengan el mismo valor, por tanto la longitud será cero,
es decir, que no hay nada. Esto ocurre con el fichero ese que me
he bajado.
Una clase para "casi" manipular los ficheros PBP
Para simplificar el acceso al contenido de los ficheros PBP
he creado una clase que lee la cabecera y asigna los valores de
unos campos públicos (que representan a cada uno de los valores
mostrados en la tabla anterior).
Esa clase también define unos métodos estáticos (compartidos
que lo llamamos los que usamos Visual Basic) que sirve para
convertir un offset en un valor hexadecimal y en un valor
entero. Recuerda que esos valores (los offset) están guardados
como 4 bytes y en little endian, por tanto esos métodos tienen
en cuenta ese "detalle" y hacen bien su trabajo.
La función Offset2Hex convierte el offset indicado en un valor
hexadecimal que después se puede convertir en un valor entero de
32 bits. En el código comentado puedes ver una forma más fácil
de saber qué es lo que hace ese código, que en el fondo es bien
simple: toma los valores de cada uno de los bytes del offset, lo
convierte en una cadena hexadecimal, y después le da la vuelta (little
endian lo almacena de atrás hacia adelante) y eso es lo que
devuelve.
Este es el código para Visual Basic:
Public Shared Function Offset2Hex(ByVal offset As String) As String
' Si el valor es C0010000
' El valor devuelto será 000001C0
If Len(offset) <> 4 Then
Return "00000000"
End If
' De forma simple para que se entienda mejor
'Dim h1, h2, h3, h4 As String
'h1 = Asc(Mid(offset, 1, 1)).ToString("x2")
'h2 = Asc(Mid(offset, 2, 1)).ToString("x2")
'h3 = Asc(Mid(offset, 3, 1)).ToString("x2")
'h4 = Asc(Mid(offset, 4, 1)).ToString("x2")
'Return h4 & h3 & h2 & h1
' De forma no tan simple, pero igual de efectiva
Dim hx As String = ""
For i As Integer = 0 To offset.Length - 1
hx = Asc(Mid(offset, i + 1, 1)).ToString("x2") & hx
Next
Return hx
End Function
Una vez que tenemos el valor en hexadecimal, lo convertimos a un entero
de 32 bits (Integer en VB, int en C#), esto lo hacemos con la función
Offset2Integer de
la que te muestro el código de Visual Basic:
Public Shared Function Offset2Integer(ByVal offset As String) As Integer
Dim sHex As String = Offset2Hex(offset)
Return CInt(Val("&H" & sHex))
End Function
Y para el que quiera tenerlo todo como una sola función, he definido el
método LittleEndianToInt32 que recibe un offset como parámetro y devuelve directamente el
valor entero:
''' <summary>
''' Para los valores guardados como little endian
''' (32-bit unsigned little endian)
''' </summary>
Public Shared Function LittleEndianToInt32(ByVal offset As String) As Integer
Return Offset2Integer(Offset2Hex(offset))
End Function
Para leer la cabecera del fichero PBP y asignar los campos de la clase lo
hago desde un método al que se llama desde el constructor de la clase y al
que se le pasa el nombre del fichero PBP que la clase usará (esta clase solo
permite crear nuevos objetos si se indica el fichero PBP a usar).
Este método va leyendo de 4 en 4 bytes y va asignando los
campos de la clase:
''' <summary>
''' Leer la cabecera del fichero PBP
''' </summary>
Private Sub pLeerInfoPBP(ByVal fic As String)
m_Fic = fic
Dim by() As Byte
Dim fst As New FileStream(fic, _
FileMode.Open, _
FileAccess.Read, _
FileShare.Read)
Using br As New BinaryReader(fst, Encoding.Default)
With Me
by = br.ReadBytes(4)
.file_PBP = Encoding.Default.GetString(by)
by = br.ReadBytes(4)
.PBPversion = Encoding.Default.GetString(by)
by = br.ReadBytes(4)
.PARAM_SFO = Encoding.Default.GetString(by)
by = br.ReadBytes(4)
.ICON0_PNG = Encoding.Default.GetString(by)
by = br.ReadBytes(4)
.ICON1_PMF = Encoding.Default.GetString(by)
by = br.ReadBytes(4)
.PIC0_PNG = Encoding.Default.GetString(by)
by = br.ReadBytes(4)
.PIC1_PNG = Encoding.Default.GetString(by)
' idem con el resto
End With
End Using
fst.Close()
End Sub
No he asignado todos los valores, ya que lo he hecho para probar lo de
extraer ICON0.PNG, por tanto, si quieres tener toda la información debes seguir
añadiendo el resto de lectura/asignaciones. Pero no debería ser complicado, al
menos al ver cómo está hecho el resto de asignaciones.
Extraer el fichero ICON0.PNG
Para extraer el fichero, he creado un método que se apoya en
otros dos que indican en qué posición empieza y qué longitud
tiene.
Public Function PosICON0_PNG() As Integer
Return Offset2Integer(ICON0_PNG)
End Function
Public Function LenICON0_PNG() As Integer
Dim pic0 As Integer = Offset2Integer(ICON0_PNG)
Dim pic1 As Integer = Offset2Integer(ICON1_PMF)
Return pic1 - pic0
End Function
Como puedes ver en LenICON0_PNG para calcular la
longitud se mira en qué offset empieza el siguiente dato y se le resta el
valor en el que empieza el que queremos extraer (como ya te comenté antes).
Y ahora solo queda extraer esa información del fichero y
guardarlo como un fichero .PNG, y eso es lo que hace la función
GuardarICON0_PNG:
Public Sub GuardarICON0_PNG()
Dim pos As Integer = PosICON0_PNG()
Dim longitud As Integer = LenICON0_PNG()
Dim fic As String = m_Fic
Dim img As String = Path.Combine(Path.GetDirectoryName(fic), _
"ICON0.PNG")
Dim by() As Byte
Using fst As New FileStream(fic, _
FileMode.Open, _
FileAccess.Read, _
FileShare.Read)
fst.Seek(pos, SeekOrigin.Begin)
ReDim by(0 To longitud - 1)
fst.Read(by, 0, longitud)
Using bw As New FileStream(img, _
FileMode.OpenOrCreate, _
FileAccess.Write, _
FileShare.Read)
bw.Write(by, 0, by.Length)
bw.Close()
End Using
fst.Close()
End Using
End Sub
Ejemplo completo para leer la info y extraer la imagen de un
fichero PBP
Eso es lo que contiene el proyecto que está en el ZIP,
ese proyecto está hecho en Visual Basic 2008, pero es totalmente compatible
con Visual Basic 2005 (solo tendrías que crear un nuevo proyecto de VB2005 y
añadir el formulario y la clase CabeceraPBP).
No sé si lo convertiré a C#, ya que uso funciones propias de
Visual Basic y ahora con el calor que hace me da pereza
convertirlo, además de que la funcionalidad está en la clase CabeceraPBP y esa clase la puedes compilar como
un ensamblado DLL y usarlo desde un proyecto de C#.
Espero que te sea de utilidad.
Nos vemos.
Guillermo
Espacios de nombres usados en el código de este artículo:
System.IO
System.Text