CFDI.BuildPdf
4.0.0
dotnet add package CFDI.BuildPdf --version 4.0.0
NuGet\Install-Package CFDI.BuildPdf -Version 4.0.0
<PackageReference Include="CFDI.BuildPdf" Version="4.0.0" />
<PackageVersion Include="CFDI.BuildPdf" Version="4.0.0" />
<PackageReference Include="CFDI.BuildPdf" />
paket add CFDI.BuildPdf --version 4.0.0
#r "nuget: CFDI.BuildPdf, 4.0.0"
#:package CFDI.BuildPdf@4.0.0
#addin nuget:?package=CFDI.BuildPdf&version=4.0.0
#tool nuget:?package=CFDI.BuildPdf&version=4.0.0
📦 CFDI.BuildPdf
Descripción general
CFDI.BuildPdf es una librería .NET 8+ que genera una representación impresa en PDF a partir de un XML CFDI 4.0. Detecta automáticamente el tipo de comprobante/complemento y soporta actualmente:
- Factura base (Ingreso/Egreso) sin complemento
- Complemento Carta Porte 3.1
- Complemento Nómina 1.2
Es cross-platform (Windows, Linux, macOS, contenedores) y no tiene dependencias nativas: usa QuestPDF como motor de renderizado y QRCoder para el código QR del timbre fiscal.
v3.0.0 cambia namespaces y requiere .NET 8. Si migras desde v2.x, consulta MIGRATION.md.
📥 Instalación
dotnet add package CFDI.BuildPdf
🚀 Características
- ✔️ Soporte para CFDI 4.0 con complementos Carta Porte 3.1 y Nómina 1.2.
- ✔️ Detección automática del tipo de complemento.
- ✔️ Traducción automática de claves SAT (
c_FormaPago,c_RegimenFiscal,c_UsoCFDI,c_TipoDeComprobante,c_TipoRelacion, etc.): el PDF muestraclave - descripciónlegible en lugar de códigos crudos. Ver CATALOGOS_SAT_MAPEADOS.md. - ✔️ Catálogos del Complemento Nómina 1.2 traducidos:
c_TipoContrato,c_TipoRegimen,c_PeriodicidadPago,c_RiesgoPuesto,c_Estado,c_TipoPercepcion,c_TipoDeduccion,c_TipoOtroPago,c_TipoIncapacidad,c_TipoHoras. - ✔️ Soporte para
<cfdi:CfdiRelacionados>: render condicional de los UUIDs relacionados con suTipoRelaciondescrito. - ✔️ PAC que timbró: RFC del timbre y, opcionalmente, su nombre comercial configurado por el consumidor (
NombresPac). - ✔️ Múltiples formatos de entrada: ruta de archivo,
string,byte[]yStream. - ✔️ Escritura directa a archivo o
Streamde salida (ideal para respuestas HTTP). - ✔️ Opciones configurables: mostrar/ocultar mercancías, condiciones del contrato, addenda; logotipo en Base64; orientación portrait/landscape; texto libre en el pie de página (folio, leyenda, código).
- ✔️ Inyección de dependencias con
Microsoft.Extensions.DependencyInjection. - ✔️ Integración opcional con
ILoggerpara diagnóstico. - ✔️ Excepciones de dominio claras (
CfdiXmlInvalidoException,CfdiComplementoNoSoportadoException).
📁 Estructura del PDF generado
Paginación: los títulos de sección nunca quedan solos al pie de una hoja; cuando una tabla continúa en la hoja siguiente se repiten su título y sus encabezados, las filas no se parten entre hojas y los bloques chicos (datos clave-valor, totales, QR + sellos) se mantienen completos.
- Datos del emisor y receptor (con
Régimen Fiscaltraducido). - Datos de Emisión: fecha, serie/folio, moneda, tipo de cambio, lugar de expedición,
Exportación(traducida) y sub-bloque CFDI Relacionados cuando el XML lo incluye. - Forma / Método de Pago: con
FormaPago,MetodoPagoyTipoDeComprobantetraducidos. - Conceptos facturados: clave producto/servicio,
ClaveUnidadtraducida, descripción, importes formateados (N2es-MX), descuentos yObjetoImpdescrito. - Totales e impuestos (
c_Impuesto: 001 ISR, 002 IVA, 003 IEPS). - Addenda genérica (opcional).
- Complemento Carta Porte: ubicaciones, mercancías (detalle o resumen), autotransporte (
c_TipoPermiso,c_ConfigAutotransportedescritos), seguros, remolques (c_SubTipoRemdescrito), figuras de transporte (c_FiguraTransportedescrito), página de condiciones del contrato. - Complemento Nómina: datos del empleado, percepciones, deducciones, otros pagos, incapacidades, totales.
- Bloque fiscal: UUID, fechas, certificados, PAC que timbró identificado por RFC.
- QR y sellos digitales (sello CFDI, sello SAT, cadena original del complemento de certificación).
⚠️ Licencia QuestPDF (léeme antes de producción)
Esta librería usa QuestPDF, que tiene licencia dual:
- Community (gratuita) — apta para la mayoría de proyectos open-source y empresas que cumplan los criterios de elegibilidad vigentes del proveedor.
- Professional / Enterprise (de pago) — requerida por QuestPDF para el resto de organizaciones.
El consumidor de esta librería es responsable de elegir y adquirir la licencia correcta. Consulta los términos vigentes en https://www.questpdf.com/license/.
Por defecto, CFDI.BuildPdf declara Community. Para cambiarlo:
Con la fachada estática
using CFDI.BuildPdf;
// Llamar una sola vez al iniciar el proceso, antes de generar cualquier PDF
CfdiPdf.ConfigureQuestPdfLicense(CfdiPdfLicenseType.Professional);
Con inyección de dependencias
builder.Services.AddCfdiPdfServices(
configure: opts => opts.MostrarMercancias = true,
licenseType: CfdiPdfLicenseType.Professional);
📚 Requisitos
- .NET 8.0 o superior.
- Sin dependencias nativas (no requiere
wkhtmltopdfni binarios de sistema).
🔵 Uso básico (fachada estática)
Desde una ruta de archivo
using CFDI.BuildPdf;
var pdfBytes = await CfdiPdf.DesdeRutaAsync(@"C:\facturas\cfdi.xml");
await File.WriteAllBytesAsync("cfdi_output.pdf", pdfBytes);
Desde un string XML
var pdfBytes = await CfdiPdf.DesdeXmlStringAsync(xmlString);
Desde bytes
var pdfBytes = await CfdiPdf.DesdeXmlBytesAsync(xmlBytes);
Desde un Stream (por ejemplo, IFormFile en ASP.NET Core)
await using var stream = archivoXml.OpenReadStream();
var pdfBytes = await CfdiPdf.DesdeStreamAsync(stream);
Guardar directamente a archivo
await CfdiPdf.GuardarDesdeRutaAsync(
rutaXml: @"C:\facturas\cfdi.xml",
rutaPdfDestino: @"C:\facturas\cfdi.pdf");
Escribir en un Stream de salida (respuesta HTTP)
[HttpPost("generar-pdf")]
public async Task<IActionResult> GenerarPdf(IFormFile xml)
{
Response.ContentType = "application/pdf";
await using var xmlStream = xml.OpenReadStream();
await CfdiPdf.EscribirEnStreamAsync(xmlStream, Response.Body);
return new EmptyResult();
}
🗂️ Traducción automática de catálogos SAT
El PDF traduce las claves del SAT a su descripción legible en tiempo de render — no necesitas pre-procesar el XML. Ejemplos del output:
| Campo | Valor en el XML | Valor en el PDF |
|---|---|---|
| Régimen Fiscal | 624 |
624 - Coordinados |
| Forma de Pago | 99 |
99 - Por definir |
| Método de Pago | PPD |
PPD - Pago en parcialidades o diferido |
| Uso del CFDI | G03 |
G03 - Gastos en general |
| Tipo de Comprobante | I |
I - Ingreso |
| Exportación | 01 |
01 - No aplica |
| Tipo Relación | 04 |
04 - Sustitución de los CFDI previos |
| Objeto Imp. | 02 |
Sí objeto de impuesto |
| Clave Unidad | E48 |
Unidad de Servicio |
| PAC que timbró | SST060807KU0 |
SST060807KU0, o Buzón E (SST060807KU0) si se configura en NombresPac |
| Tipo Nómina (contrato) | 01 |
01 - Contrato de trabajo por tiempo indeterminado |
| Periodicidad Pago (Nómina) | 04 |
04 - Quincenal |
| Tipo Percepción (Nómina) | 001 |
001 - Sueldos, Salarios Rayas y Jornales |
| Clave Entidad Federativa | SIN |
SIN - Sinaloa |
Si una clave no está en el catálogo embebido se renderiza tal cual (fallback seguro, sin excepción). Catálogos soportados (23 helpers / 36 campos): c_ClaveUnidad, c_Impuesto, c_ObjetoImp, c_UsoCFDI, c_RegimenFiscal, c_FormaPago, c_MetodoPago, c_Exportacion, c_TipoDeComprobante, c_TipoRelacion, c_CveTransporte, c_TipoPermiso, c_ConfigAutotransporte, c_SubTipoRem, c_FiguraTransporte, c_TipoContrato, c_TipoRegimen, c_PeriodicidadPago, c_RiesgoPuesto, c_Estado, c_TipoPercepcion, c_TipoDeduccion, c_TipoOtroPago, c_TipoIncapacidad, c_TipoHoras. Inventario completo en CATALOGOS_SAT_MAPEADOS.md.
Nombre del PAC que timbró (configurable)
El campo PAC QUE TIMBRÓ imprime siempre el RFC del PAC, que viene del timbre (RfcProvCertif) y es el dato fiscal. El nombre comercial es opcional y lo aporta el consumidor con NombresPac (RFC → nombre; las claves no distinguen mayúsculas): con nombre se imprime Nombre (RFC), sin nombre solo el RFC.
La librería no trae nombres de PAC: los PACs cambian de RFC, de nombre o pierden la autorización fuera del ciclo de versiones de una librería, así que ese dato vive en la configuración de quien la usa. Cambiar de PAC es editar configuración, no publicar otra versión.
options.NombresPac["SST060807KU0"] = "Buzón E";
options.NombresPac["PPD101129EA3"] = "Mi PAC nuevo";
O desde appsettings.json (ver inyección de dependencias):
"CfdiPdf": {
"NombresPac": { "PPD101129EA3": "Mi PAC nuevo" }
}
⚙️ Opciones de generación
var options = new CfdiPdfOptions
{
MostrarMercancias = true, // Carta Porte: mostrar detalle de mercancías
MostrarCondicionesContrato = true, // Carta Porte: incluir página de condiciones
MostrarAddenda = true, // Incluir sección de addenda
LogoBase64 = logoBase64, // Logo de la empresa (opcional)
Orientacion = PdfOrientation.Portrait,
TextoPiePagina = "FOLIO INTERNO: HG-123456", // Texto libre en el pie de todas las hojas (opcional)
NombresPac = { ["PPD101129EA3"] = "Mi PAC" } // RFC → nombre del PAC (opcional; sin nombre se imprime el RFC)
};
var pdfBytes = await CfdiPdf.DesdeRutaAsync(rutaXml, options);
Texto libre en el pie de página
TextoPiePagina imprime un texto propio (folio interno, leyenda, código de control, etc.) centrado en el pie de todas las hojas, arriba de la leyenda y el paginado. En Carta Porte también aparece en la hoja de condiciones del contrato.
- Admite saltos de línea (
\n); se muestran a lo más 2 renglones y el excedente se corta con…, para que un texto largo nunca rompa el layout. - Los renglones vacíos se descartan y los caracteres de control (tabuladores, etc.) se cambian por espacio.
nullo vacío (default): no se imprime nada y el PDF queda igual que sin la opción.
FOLIO INTERNO: HG-123456
ESTE DOCUMENTO ES UNA REPRESENTACIÓN IMPRESA DE UN CFDI Página 1 de 2
Para probarlo sin integrar la librería, el demo de consola acepta --pie:
dotnet run --project CFDI.BuildPdf.ConsoleDemo -- ./cfdi.xml ./salida.pdf --pie "FOLIO INTERNO: HG-123456"
💉 Uso con inyección de dependencias
Registro de servicios
using CFDI.BuildPdf;
using Microsoft.Extensions.DependencyInjection;
builder.Services.AddCfdiPdfServices(
configure: opts =>
{
opts.TextoPiePagina = "HG-F-CXC-02 REV.:02 24/09/2026";
opts.NombresPac["SST060807KU0"] = "Buzón E";
},
licenseType: CfdiPdfLicenseType.Community);
Las opciones de configure son las opciones por defecto: se aplican en cada llamada a ICfdiPdfGenerator que no traiga las suyas. Si una llamada pasa su propio CfdiPdfOptions, se usa ése completo, con una excepción: NombresPac es un catálogo acumulable y se combina con el de DI (si un RFC está en ambos, gana el de la llamada). Así los nombres de PAC configurados una vez no se pierden cuando una llamada trae sus propias opciones.
También se pueden cargar desde appsettings.json, así un cambio de leyenda o de PAC es solo de configuración:
builder.Services.Configure<CfdiPdfOptions>(builder.Configuration.GetSection("CfdiPdf"));
builder.Services.AddCfdiPdfServices();
"CfdiPdf": {
"TextoPiePagina": "HG-F-CXC-02 REV.:02 24/09/2026",
"NombresPac": { "SST060807KU0": "Buzón E" }
}
Antes de 4.0.0 las opciones de
configurese registraban pero no se aplicaban.
Consumo desde un servicio
using CFDI.BuildPdf;
public class FacturaService
{
private readonly ICfdiPdfGenerator _generator;
public FacturaService(ICfdiPdfGenerator generator)
{
_generator = generator;
}
public Task<byte[]> GenerarPdfAsync(string rutaXml)
=> _generator.GenerarDesdeRutaAsync(rutaXml);
}
🛑 Manejo de errores
Los métodos públicos lanzan excepciones de dominio específicas para facilitar el diagnóstico:
| Excepción | Cuándo ocurre |
|---|---|
ArgumentNullException / ArgumentException |
Inputs nulos, vacíos o inválidos. |
FileNotFoundException |
La ruta del XML no existe. |
CfdiXmlInvalidoException |
El contenido no es un XML bien formado. |
CfdiComplementoNoSoportadoException |
El CFDI no contiene Carta Porte 3.1 ni Nómina 1.2. |
CfdiPdfException |
Clase base para cualquier error de dominio de la librería. |
try
{
var pdf = await CfdiPdf.DesdeRutaAsync(ruta);
}
catch (CfdiXmlInvalidoException ex)
{
logger.LogWarning(ex, "XML no válido: {Mensaje}", ex.Message);
}
catch (CfdiComplementoNoSoportadoException ex)
{
logger.LogWarning(ex, "Complemento no soportado: {Mensaje}", ex.Message);
}
📝 Licencia
Este proyecto está bajo licencia MIT. Recuerda que QuestPDF (dependencia interna) tiene su propia licencia dual — ver sección arriba.
👤 Autor
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
- QRCoder (>= 1.6.0)
- QuestPDF (>= 2024.3.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.0.0 | 91 | 10/6/2026 |
| 3.2.0 | 92 | 10/5/2026 |
| 3.1.0 | 162 | 6/18/2026 |
| 3.0.0 | 140 | 6/17/2026 |
| 2.0.8 | 134 | 4/29/2026 |
| 2.0.7 | 121 | 4/22/2026 |
| 2.0.6 | 125 | 4/19/2026 |
| 2.0.5 | 121 | 4/16/2026 |
| 2.0.4 | 138 | 4/12/2026 |
| 2.0.3 | 118 | 4/12/2026 |
| 2.0.2 | 118 | 4/12/2026 |
| 2.0.1 | 131 | 4/12/2026 |
| 2.0.0 | 130 | 4/12/2026 |
| 1.0.5 | 301 | 6/16/2025 |
| 1.0.4 | 282 | 5/1/2025 |
| 1.0.3 | 261 | 4/30/2025 |
| 1.0.2 | 265 | 4/30/2025 |
| 1.0.1 | 253 | 4/30/2025 |
| 1.0.0 | 244 | 4/30/2025 |
v4.0.0 (BREAKING: se elimina el catálogo interno de PACs; para conservar los nombres configúralos en NombresPac, ver MIGRATION.md): CfdiPdfOptions.NombresPac (nombre del PAC lo aporta el consumidor, también desde appsettings; se elimina el catálogo interno de PACs y sin nombre se imprime solo el RFC); las opciones de AddCfdiPdfServices(configure) ahora sí se aplican; paginación sin títulos/encabezados huérfanos, encabezados repetidos y filas sin partir. Sin cambios en la API pública.