Imprimir en PDF con aplicaciones web AVR siempre ha sido una molestia. Hacerlo requería costosos controladores de impresión de terceros y difíciles de configurar; los controladores de impresión de gama baja tampoco funcionan porque son de proceso único (no son multi hilo) o requieren acceso de registro para asignar un nombre de archivo PDF en tiempo de ejecución. En los comienzos de Windows, el acceso al registro para tales tareas ad hoc era fácil, pero en estos tiempos de concienciación en cuanto a seguridad no es la práctica más aceptada (incluso debido a sus políticas de seguridad, podría no poder llevarse a cabo).

Microsoft finalmente ha llegado al rescate añadiendo unidades de impresión PDF nativas tanto a Windows 10 como a Windows Server 2016 (¡quizás señalando que MS se dio cuenta de que su escritor de documentos XPS no es un asesino de PDF!). Esas son las buenas noticias, las malas noticias son que este controlador de impresión PDF no está presente, ni está disponible, para versiones anteriores de Windows o Windows Server.

Necesitará Windows 10 o Windows Server 2016 para obtener el controlador PDF nativo de Microsoft. Si aún no se ha actualizado, para aquellos con necesidades de impresión web, el coste de la actualización se ve parcialmente compensado por no tener que pagar el alto coste de los controladores PDF de terceros. Como comentario, Windows Server 2012 R2 se queda fuera del soporte principal en octubre de 2018. ¡Planifíquese!

Hemos probado este controlador de Microsoft en Windows 10 y Windows Server 2016 y con ASNA Visual RPG 15.0. No es compatible con AVR 14 (la versión que funciona con Visual Studio 15), pero el código de este artículo funcionó perfectamente con esa versión.

Viendo el código

La clase (a continuación en la Figura 2ª) proporciona la lógica para imprimir un informe en un archivo PDF o una impresora. Si compara este código con otro código que hemos proporcionado para imprimir PDFapreciará la poca fricción que hay al imprimir en PDF con el controlador PDF de Microsoft.

Dependiendo de cómo se instale el CustomerReport la salida de la impresora se dirige a una impresora o a un archivo PDF. Imprimir en una impresora generalmente no es una buena idea en una aplicación web. La impresión se llevaría a cabo en una impresora de la red, que probablemente no esté donde está el usuario: ese es el atractivo de imprimir en PDF. Permite al usuario imprimir el PDF local o guardarlo para usarlo más adelante. La clase permite imprimir en una impresora principalmente para mostrar cómo una sola clase no solo puede hacer las dos cosas, sino que debe.

Vea los comentarios en el código para tener más detalles sobre esta clase. El código es bastante simple, pero si las expresiones regulares en la subrutinaCheckFileAndPathSeparators le provocan dolores de cabeza, eche un vistazo a este artículo sobre regular expressions.

Figura 1. La aplicación de ejemplo para imprimir un simple informe con AVR Web.

Using System
Using System.Text.RegularExpressions
Using System.IO 

/*
 | This class shows how to print to Microsoft's native PDF
 | print driver (which produces a PDF file) or to a real printer.
 |
 | When printing to the MS native PDF driver, the printer name
 | must be:
 |      "Microsoft Print to PDF"
 |
 | Use MS Word to save a sample document with this driver to ensure
 | it is available. It should be present for Windows 10 or Windows 
 | Server 2016. It is not present, not is it available, for previous
 | Windows or Windows Server versions.
 */

BegClass CustomerReport Access(*Public)

    DclDB Name(pgmDB) DBName("*Public/DG NET Local")

    DclDiskFile Cust +
        Type(*Input) +
        Org(*Indexed) +
        Prefix(Cust_) +
        File("Examples/CMastNewL2") +
        DB(pgmDB) +
        ImpOpen(*No) 

    DclPrintFile MyPrint +
        DB (pgmDB) + 
        File ("Examples/CustList") + 
        ImpOpen (*No) 
    
    DclProp DocumentName Type (*String) 
    DclProp IsPdf Type (*Boolean) 
    DclProp OutputDirectory Type (*String) 
    DclProp OutputFileFullName Type(*String) Access(*Public) 
    DclProp PrinterName Type (*String) 
    DclProp VirtualPathToPdf Type(*String) Access(*Public) 

    BegSr OpenData Access (*Private) 
        Connect PgmDB
        Open Cust

        MyPrint.Printer = *This.PrinterName
        If (IsPdf) 
            MyPrint.PrintToFileName = *This.OutputFileFullName
        EndIf             
        Open MyPrint
    EndSr

    BegSr CloseData Access (*Private) 
        Close Cust
        Close MyPrint
        Disconnect PgmDB
    EndSr

    BegSr Print Access(*Public) 
        OpenData()
        WriteReportFormats()
        CloseData()
        // If printing to PDF, pause just a bit
        // to ensure the PDF file is closed before
        // returning.
        If (*This.IsPdf) 
            Sleep(5000)
        EndIf 
    EndSr

    BegSr WriteReportformats Access (*Private) 
        DclFld StartingfooterSize Type (*Integer4) 
        DclFld NeedHeader Type (*Boolean) 

        // There are 254 print units in an inch. 
        DclConst ONE_INCH Value(254)

        StartingfooterSize = MyPrint.FooterSize
        NeedHeader = *True

        DclFld Counter Type(*Integer4) 

        Read Cust
        DoWhile (NOT Cust.IsEof)
            Counter += 1
            CustomerName = Cust_CMName

            If (NeedHeader) 
                Write Heading
                NeedHeader = *False
            EndIf

            Write Detail
            
            // This results in a 1.25 inch footer. 
            If (MyPrint.FooterSize <= ONE_INCH * 1.25)
                Write Footer
                NeedHeader = *True
                StartingFooterSize = MyPrint.FooterSize
            Endif 

            // An arbitrary value to limit pages printed
            // for testing.
            If Counter = 75 
                Leave 
            EndIf

            Read Cust
        EndDo

        If (StartingfooterSize <> MyPrint.FooterSize) 
            Write Footer
        EndIf
    EndSr

    BegConstructor Access(*Public) 
        //
        // Constructor for printing to printer.
        //
        DclSrParm PrinterName Type(*String) 

        *This.PrinterName =	PrinterName
        *This.IsPdf = *False
    EndConstructor

    BegConstructor Access(*Public) 
        //
        // Constructor for printing to PDF.
        //
        DclSrParm WebRoot Type(*String) 
        DclSrParm OutputDirectory Type(*String) 
        DclSrParm DocumentName Type(*String)
        DclSrParm PrinterName Type(*String)
        
        DclConst ERROR_MESSAGE Value('There isn''t a [{0}] directory in the app root.') 

        *This.PrinterName = PrinterName
        
        CheckFileAndPathSeparators(OutputDirectory, DocumentName)

        // Throw exception if output directory provided doesn't exist.             
        If NOT Directory.Exists(WebRoot + *This.OutputDirectory )
            Throw *New System.ArgumentException(String.Format(ERROR_MESSAGE, +
                                                *This.OutputDirectory))
        EndIf

        // Create PDF file name. 
        *This.OutputFileFullName = String.Format('{0}{1}\{2}', +              
                                       WebRoot, +
                                       *This.OutputDirectory, +
                                       *This.DocumentName)
        
        // Create relative output file name for response.redirect.
        *This.VirtualPathToPdf = String.Format('/{0}/{1}', + 
                                     *This.OutputDirectory, +
                                     *This.DocumentName)
        *This.IsPdf = *True
    EndConstructor

    BegSr CheckFileAndPathSeparators
        DclSrParm OutputDirectory Type(*String) 
        DclSrParm DocumentName Type(*String) 

        DclConst BACK_SLASH Value('\') 
        DclConst FORWARD_SLASH Value('/')
        DclConst LEADING_BACK_SLASH Value('^\\')
        DclConst TRAILING_BACK_SLASH Value('\\$')

        // Don't assume the slashes or backslashes provided 
        // are correct! 
        // Remove leading backslash if present.
        *This.OutputDirectory = RegEx.Replace(OutputDirectory, + 
                                   LEADING_BACK_SLASH, String.Empty)
        // Remove trailing backslash if present.
        *This.OutputDirectory = RegEx.Replace(OutputDirectory, + 
                                   TRAILING_BACK_SLASH, String.Empty) 
        // Swap / slashes for \ slashes if present.
        *This.OutputDirectory = RegEx.Replace(OutputDirectory, + 
                                   FORWARD_SLASH, BACK_SLASH)
        // Remove leading backslash if present.
        *This.DocumentName = RegEx.Replace(DocumentName, +
                                LEADING_BACK_SLASH, String.Empty)
        // Remove trailing backslash if present.
        *This.DocumentName = RegEx.Replace(DocumentName, +
                                TRAILING_BACK_SLASH, String.Empty)
    EndSr
    
EndClass

Figura 2a. Una clase ejemplo para imprimir un informe con AVR

A continuación, se muestra un ejemplo de aplicación para imprimir en la Web.

Su código fuente asociado se muestra a continuación en la Figura 2b.

BegClass PrintFromWeb Partial(*Yes) Access(*Public) Extends(System.Web.UI.Page)
    //
    // Print to PDF.
    //
    BegSr Button1_Click Access(*Private) Event(*This.Button1.Click)
        DclSrParm sender Type(*Object)
        DclSrParm e Type(System.EventArgs)

        DclFld report Type(CustomerReport) 

        DclFld WebRoot Type(*String) 
        DclFld PDFFolder Type(*String) 
        DclFld PDFFileName Type(*String) 
        DclFld PrinterName Type(*String) 

        /*
         | When printing to the MS native PDF driver, the printer name
         | must be:
         |      "Microsoft Print to PDF"
         | Use MS Word to save a sample document with this driver to ensure
         | it is available. It should be present for Windows 10 or Windows 
         | Server 2016. It is not present, not is it available, for previous
         | Windows or Windows Server versions.
         */
        DclConst MS_PDF_DRIVER Value('Microsoft Print to PDF')

        WebRoot = Server.MapPath('\')
        // The PDFFolder is relative to the root.
        PDFFolder = 'pdf/files'
        PDFFileName = textboxPDFFileName.Text.Trim()
        PrinterName =  MS_PDF_DRIVER
        report = *New CustomerReport(WebRoot, PDFFolder, PDFFileName, PrinterName) 
        report.Print()                         

        Response.Redirect(report.VirtualPathToPDF)		
    EndSr	

    //
    // Print to printer.
    //
    BegSr Button2_Click Access(*Private) Event(*This.Button2.Click)
        DclSrParm sender Type(*Object)
        DclSrParm e Type(System.EventArgs)

        DclFld report Type(CustomerReport) 
        DclFld PrinterName Type(*String) 

        PrinterName = textboxPrinterName.Text.Trim() 
        report = *New CustomerReport(PrinterName)
        
        report.Print()                                		
    EndSr
EndClass

Figura 2b. Código fuente asociado AVR usando la clase de la Figura 1a.