Share via


Help with printing to PDF

Question

Friday, June 14, 2019 2:12 PM

I have an automated process that generates various reports for security audits in various file formats (depending on the source of the data).  I'm trying to automate printing these reports to PDF as this is the format required by the security team and the auditors. 

Here's my ConvertTo-PDF function, which I adopted from a blog post from a few years ago.  Both the unmodified code and my updated function just print a blank 1KB PDF file.  I feel like there is a missing step in here somewhere.  Any help would be appreciated.

Function ConvertTo-PDF{
    <#
        .NOTES
            Created by Adam Gloyd on 6/12/2019
            Adapted from a TechNet blog post here: https://social.technet.microsoft.com/Forums/ie/en-US/04ddfe8c-a07f-4d9b-afd6-04b147f59e28/automating-printing-to-pdf

        .SYNOPSIS
            Converts a specified file to a PDF using the Microsoft Print to PDF file printer.
            
        .DESCRIPTION
            Uses the built-in Microsoft Print to PDF file printer to convert virtually any file to a PDF.
            Be aware that if the source file is wider than the standard portrait document, the resulting PDF may not be properly formatted.

            Currently, the function automatically saves the PDF in the same folder as the source document with the same name, simply updating the file extension to 'PDF.'
        
        .Parameter File
            The source file that needs to be converted to a PDF.

        .Example
            ConvertTo-PDF -File "C:\Test\MyWordDoc.docx"

            This command will print the specified word document to a PDF called MyWordDoc.pdf stored in the 'C:\Test' directory.

        .LINK
            https://social.technet.microsoft.com/Forums/ie/en-US/04ddfe8c-a07f-4d9b-afd6-04b147f59e28/automating-printing-to-pdf

        .INPUTS
            System.String

        .OUTPUTS
            None
    #>
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0)]
        [String]$File
    )

    # Import System.Drawing namespace; testing whether the module definition can load this assembly instead of placing it inside the function.
    Add-Type -AssemblyName System.Drawing

    # Create a new PrintDocument object
    $ObjDoc = New-Object System.Drawing.Printing.PrintDocument
    
    # Set the document display name to the specified document path
    $ObjDoc.DocumentName = $File
    
    # Add a PrinterSettings object to the existing print document object
    $ObjDoc.PrinterSettings = new-Object System.Drawing.Printing.PrinterSettings
    
    # Specify the name of the printer (in this case, the 'Microsoft Print to PDF' file printer)
    $ObjDoc.PrinterSettings.PrinterName = 'Microsoft Print to PDF'
    
    # Specify that we are printing to a file and not to a physical printer
    $ObjDoc.PrinterSettings.PrintToFile = $true
    
    # We need to set the name and path of the destination pdf file, which we will base on the name and location of the source document.
    $source = Get-Item -Path $File # Create an object containing information about the source file
    $pdf = Join-Path -Path ($source.DirectoryName) -ChildPath ($source.BaseName + '.pdf') # Create a new object with an updated path
    # The pdf will be stored in the same location as the base file
    
    # Update the destination PDF file name/path based on previous section
    $ObjDoc.PrinterSettings.PrintFileName = $pdf
    $ObjDoc.Print() # Print the document (creating the PDF file)
    $ObjDoc.Dispose() # Release all related resources so the garbage collector can release associated memory.
}

All replies (4)

Wednesday, June 19, 2019 12:57 AM ✅Answered

Sorry for the slow reply.  I rethought my approach based on a blog post that I found (btw, I couldn't get your first post to run).  I now have a function that works (depending on source file type), which is part of a module that should be on GitHub soon.

Function ConvertTo-PDF{
    <#
        .NOTES
            2019/06/17: Created by Adam Gloyd; inspired by code found on the Idera community website. Link listed in the links section of help.
            2019/06/18: Added debug output
            2019/06/18: Used debug output to fix the last couple of symantic errors
            2019/06/18: Added help documentation

        .SYNOPSIS
            Converts the specified file to a PDF file.

        .DESCRIPTION
            Converts the specified file to a PDF file by printing it using the Microsoft Print to PDF driver.  The new file will be in same path as the original and keep the 
            original name, just with a new .pdf extension.  Only certain file types are supported at this time as each file type needs to be handled differently, usually by 
            an application designed to manage the file type.
            
            The following file types are currently supported and are treated as raw text:
            .txt
            .log
            .csv
            .xml

            The following file types will have added support in the future by using the appropriate application to handle the content and send it to the printer:
            .html
            .xhtml
            .doc
            .docx
            .xls
            .xlsx

            Csv files are currently handled via Import-Csv, so they are not printed in a tabular format.  
            In the future, an option will be added to treat CSVs as an Excel document and print them using Excel.

            Another option in the future may be added to take PowerShell output via the pipeline and print it to a PDF.

        .EXAMPLE
            ConnvertTo-PDF -File C:\Test\Test.txt

            Prints the text contained within Test.txt to a PDF file: C:\Test\Test.pdf

        .EXAMPLE
            Get-ADGroupMember -Identity 'Domain Admins' | Out-File C:\Test\DomainAdmins.txt
            ConvertTo-PDF -File C:\Test\DomainAdmins.txt

            Exports a list of all of the accounts in the domain admins group and saves it to a text file.
            It then imports the data and prints it to a new PDF file called: C:\Test\DomainAdmins.pdf
        
        .LINK
            Get-Content
            Get-Item
            Import-Csv
            Import-CliXml
            New-PDFPrinterPort
            New-PDFUnattendPrinter
            Out-Printer
            Remove-PDFPrinterPort
            Remove-Printer
            https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/sending-powershell-results-to-pdf-part-2

        .INPUTS
            System.String[]
        
        .OUTPUTS
            PDF File
    #>
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,Position=0)]
        [String[]]$File
    )

    Begin{
        # Get source file information
        Write-Debug "ConvertTo-PDF: Starting 'Begin' block."
        $Item = Get-Item $File
        Write-Debug "ConvertTo-PDF: Setting variable `$Item to the file located at: '$File'"
        Write-Debug ("ConvertTo-PDF: `$Item parent directory: "+ $Item.DirectoryName)
        Write-Debug ("ConvertTo-PDF: `$Item file name: " + $Item.Name)
        Write-Debug ("ConvertTo-PDF: `$Item full name: " + $Item.FullName)
        Write-Debug ("ConvertTo-PDF: `$Item base name: " + $Item.BaseName)
        Write-Debug ("ConvertTo-PDF: `$Item file extension: " + $Item.Extension)

        # Create the necessary printer that will be used to print the specified file unattended.
        Write-Verbose "Creting a temporary printer and printer port for unattended printing."
        Write-Debug "ConvertTo-PDF: Setting variable `$PDFPrinter to string value 'UnattendPDFPrinter'"
        $PDFPrinter = "UnattendPDFPrinter"

        $PDFPath = $Item.DirectoryName + '\' + $Item.BaseName + '.pdf'
        Write-Debug "ConvertTo-PDF: Set variable `$PDFPath to '$PDFPath'"
        Write-Debug "ConvertTo-PDF: The destination PDF file will be '$PDFPath'"

        Write-Debug "ConvertTo-PDF: Creating new printer port called '$PDFPath'"
        New-PDFPrinterPort -FilePath $PDFPath

        Write-Debug "ConvertTo-PDF: Creating new printer called '$PDFPrinter'; it will use the port called '$PDFPath'"
        New-PDFUnattendPrinter -PrinterName $PDFPrinter -PortName $PDFPath

        # Rationalize file information and extensions
        Write-Verbose "Checking file type to determine method for conversion."
        Write-Debug "ConvertTo-PDF: Creating variable `$FileType and setting to an empty string."
        [String]$FileType = ""
        Write-Debug ("ConvertTo-PDF: Processing switch statement for `$Item.Extension; current value: " + $Item.Extension)
        Switch($Item.Extension){
            '.txt' {$FileType = "Text"; Write-Debug "File type was .txt; set `$FileType to '$FileType'"}
            '.log' {$FileType = "Text"; Write-Debug "File type was .log; set `$FileType to '$FileType'"}
            '.html' {$FileType = "HTML"; Write-Debug "File type was .html; set `$FileType to '$FileType'"}
            '.xhtml' {$FileType = "HTML"; Write-Debug "File type was .xhtml; set `$FileType to '$FileType'"}
            '.csv' {$FileType = "CSV"; Write-Debug "File type was .csv; set `$FileType to '$FileType'"}
            '.xml' {$FileType = "XML"; Write-Debug "File type was .xml; set `$FileType to '$FileType'"}
            '.xls' {$FileType = "Excel"; Write-Debug "File type was .xls; set `$FileType to '$FileType'"}
            '.xlsx' {$FileType = "Excel"; Write-Debug "File type was .xlsx; set `$FileType to '$FileType'"}
            '.doc' {$FileType = "Word"; Write-Debug "File type was .doc; set `$FileType to '$FileType'"}
            '.docx' {$FileType = "Word"; Write-Debug "File type was .docx; set `$FileType to '$FileType'"}
        }
        Write-Debug "ConvertTo-PDF: Ending 'Begin' block."
    }

    Process{
        Write-Debug "ConvertTo-PDF: Starting 'Process' block."
        # Based on file extension, import the data and send it to the temporary printer
        Write-Debug "ConvertTo-PDF: Using If statements to import content based on the value of `$FileType: $FileType."
        If($FileType -eq "Text"){
            # The file should be treated as raw text; use Get-Content
            Write-Verbose "The file is a type of text file. Importing data with Get-Content and sending to PDF printer."
            Write-Debug "ConvertTo-PDF: `$FileType matched string 'Text'; importing source file with Get-Content."
            Get-Content $File | Out-Printer -Name $PDFPrinter
        }ElseIf($FileType -eq "HTML"){
            # The file is an HTML document; use Edge to open and print the file
            Write-Debug "ConvertTo-PDF: `$FileType matched string 'HTML'; file type not supported at this time."
            Write-Warning "Unsupported file type.  Support for the file type of the provided file will be added soon."
        }ElseIf($FileType -eq "CSV"){
            # The file contains comma delimited values; use Import-Csv
            Write-Verbose "File is a CSV.  Using Import-Csv to retrieve the data and print raw CSV entries into target PDF file."
            Write-Verbose "Support will be added in the future to treat a Csv as an Excel document.  If Csv should printed in a table, convert to Excel and try again."
            Write-Debug "ConvertTo-PDF: `$FileType matched string 'CSV'; Using Import-Csv to import content.  This means the data will be exported in raw text."
            Import-Csv $File | Out-Printer -Name $PDFPrinter
        }ElseIf($FileType -eq "XML"){
            # The file contains XML data; use Import-CliXml
            Write-Verbose "File is an XML document; attempting to retrieve data with Import-CliXml and printing raw data to target PDF file."
            Try{
                Write-Debug "ConvertTo-PDF: `$FileType matched string 'XML'; Using Import-CliXml to import content.  This means the data will be exported in raw text."
                Import-Clixml $File -ErrorAction SilentlyContinue | Out-Printer -Name $PDFPrinter
            }Catch{
                Write-Verbose "Failed to import the XML document.  It may not be in a supported schema."
                Write-Debug "ConvertTo-PDF: failed to import the XML document; it may not be in the correct schema for use with Import-CliXml; processing the XML document as text using Get-Content"
                Get-Content $File -ErrorAction SilentlyContinue | Out-Printer -Name $PDFPrinter
            }
        }ElseIf($FileType -eq "Excel"){
            # The file is an Excel workbook; use the Excel application to open and print the workbook
            Write-Debug "ConvertTo-PDF: `$FileType matched string 'Excel'; file type not supported at this time."
            Write-Warning "Unsupported file type.  Support for the file type of the provided file will be added soon."
        }ElseIf($FileType -eq "Word"){
            # The file is a Word document; use the Word appliction to open and print the the document
            Write-Debug "ConvertTo-PDF: `$FileType matched string 'Word'; file type not supported at this time."
            Write-Warning "Unsupported file type.  Support for the file type of the provided file will be added soon."
        }Else{
            # The file type is currently unsupported
            Write-Debug "ConvertTo-PDF: `$FileType value unmatched; file type not supported at this time."
            Write-Error "Unsupported file type.  The file type of the provided file is not surrently supported by this converter."
        }
        Write-Debug "ConvertTo-PDF: Ending 'Process' block."
    }

    End{
        Write-Debug "ConvertTo-PDF: Starting 'End' block."
        # Do any post-run cleanup processes
        Write-verbose "Removing temporary PDF printer."
        Write-Debug "ConvertTo-PDF: Running 'Remove-Printer -Name $PDFPrinter -Confirm:`$false' to delete the temporary printer."
        Remove-Printer -Name $PDFPrinter -Confirm:$false

        Write-Verbose "Removing temporary PDF printer port."
        Write-Debug "ConvertTo-PDF: Running 'Remove-PDFPrinterPort -FilePath $PDFPath' to delete the temporary printer port."
        Remove-PDFPrinterPort -FilePath $PDFPath

        Write-Debug "ConvertTo-PDF: Closing 'End' block."
    }
}

Friday, June 14, 2019 2:54 PM

Simple.  You have to write the text into the document.  What you have is a blank document with a name but no contents. Get the graphics object and use the System.Drawing API to write the text into the document.

Here is a full example of how to render a text file to a document being printed. THe output is sent one page at a time until there is no more to print.

Here is the C# example that can be run under PowerShell as a quick demo. CHange the two file names in the code to your file names and test. You will see how it works:

$code = @'
using System;
using System.IO;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;


public partial class Form1 : System.Windows.Forms.Form{
    private System.ComponentModel.Container components;
    private System.Windows.Forms.Button printButton;
    private Font printFont;
    private StreamReader streamToPrint;

    public Form1(){
        // The Windows Forms Designer requires the following call.
        InitializeComponent();
    }

    // The Click event is raised when the user clicks the Print button.
    private void printButton_Click(object sender, EventArgs e){
        try{
            streamToPrint = new StreamReader("d:\\scripts\\test.csv");
            try{
                printFont = new Font("Arial", 10);
                PrintDocument pd = new PrintDocument();
                pd.PrinterSettings.PrinterName = "Microsoft Print to PDF";
                pd.PrinterSettings.PrintToFile = true;
                pd.PrinterSettings.PrintFileName = "d:\\scripts\\test.pdf";
                pd.PrintPage += new PrintPageEventHandler(this.pd_PrintPage);
                pd.Print();
            }
            finally{
                streamToPrint.Close();
            }
        }
        catch (Exception ex){
            MessageBox.Show(ex.Message);
        }
    }

    // The PrintPage event is raised for each page to be printed.
    private void pd_PrintPage(object sender, PrintPageEventArgs ev){
        float linesPerPage = 0;
        float yPos = 0;
        int count = 0;
        float leftMargin = ev.MarginBounds.Left;
        float topMargin = ev.MarginBounds.Top;
        string line = null;

        // Calculate the number of lines per page.
        linesPerPage = ev.MarginBounds.Height /
           printFont.GetHeight(ev.Graphics);

        // Print each line of the file.
        while (count < linesPerPage && ((line = streamToPrint.ReadLine()) != null)){
            yPos = topMargin + (count *
               printFont.GetHeight(ev.Graphics));
            ev.Graphics.DrawString(line, printFont, Brushes.Black,
               leftMargin, yPos, new StringFormat());
            count++;
        }

        // If more lines exist, print another page.
        if (line != null)
            ev.HasMorePages = true;
        else
            ev.HasMorePages = false;
    }


    // The Windows Forms Designer requires the following procedure.
    private void InitializeComponent(){
        this.components = new System.ComponentModel.Container();
        this.printButton = new System.Windows.Forms.Button();

        this.ClientSize = new System.Drawing.Size(504, 381);
        this.Text = "Print Example";

        printButton.ImageAlign = System.Drawing.ContentAlignment.MiddleLeft;
        printButton.Location = new System.Drawing.Point(32, 110);
        printButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
        printButton.TabIndex = 0;
        printButton.Text = "Print the file.";
        printButton.Size = new System.Drawing.Size(136, 40);
        printButton.Click += new System.EventHandler(printButton_Click);

        this.Controls.Add(printButton);
    }

}
'@
Add-Type $code -ReferencedAssemblies System.Windows.Forms, System.Drawing
$f = [form1]::new()
$f.ShowDialog()

\(ツ)_/


Friday, June 14, 2019 6:10 PM

Just for fun - here is a more automated version that takes a source file and a pdf file name and prints it.  THe Window closes automatically and there is no need to click a button.

$code = @'
using System;
using System.IO;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;


public class PrintToPDF : System.Windows.Forms.Form{
    private System.ComponentModel.Container components;
    private System.Windows.Forms.Button printButton;
    private Font printFont;
    private StreamReader streamToPrint;
    private string _filePathname;
    private string _pdfPathname;

    public PrintToPDF(string filePathname, string pdfPathname){
        _pdfPathname = pdfPathname;
        _filePathname = filePathname;
        InitializeComponent();
    }

    private void PrintToPDF_Activated(object sender, EventArgs e){
        Console.Write("Form loaded!");
        printButton.PerformClick();
    }

    // The Click event is raised when the user clicks the Print button.
    private void printButton_Click(object sender, EventArgs e){
        try{
            streamToPrint = new StreamReader(_filePathname);
            try{
                printFont = new Font("Arial", 10);
                PrintDocument pd = new PrintDocument();
                pd.PrinterSettings.PrinterName = "Microsoft Print to PDF";
                pd.PrinterSettings.PrintToFile = true;
                pd.PrinterSettings.PrintFileName = _pdfPathname;
                pd.PrintPage += new PrintPageEventHandler(this.pd_PrintPage);
                pd.Print();
            }
            finally{
                streamToPrint.Close();
                Form f = this.FindForm();
                f.Close();
            }
        }
        catch (Exception ex){
            MessageBox.Show(ex.Message);
        }
    }

    // The PrintPage event is raised for each page to be printed.
    private void pd_PrintPage(object sender, PrintPageEventArgs ev){
        float linesPerPage = 0;
        float yPos = 0;
        int count = 0;
        float leftMargin = ev.MarginBounds.Left;
        float topMargin = ev.MarginBounds.Top;
        string line = null;

        // Calculate the number of lines per page.
        linesPerPage = ev.MarginBounds.Height / printFont.GetHeight(ev.Graphics);

        // Print each line of the file.
        while (count < linesPerPage && ((line = streamToPrint.ReadLine()) != null)){
            yPos = topMargin + (count * printFont.GetHeight(ev.Graphics));
            ev.Graphics.DrawString(line, printFont, Brushes.Black, leftMargin, yPos, new StringFormat());
            count++;
        }

        // If more lines exist, print another page.
        if (line != null) ev.HasMorePages = true;
        else ev.HasMorePages = false;
    }

    // The Windows Forms Designer requires the following procedure.
    private void InitializeComponent(){
        this.components = new System.ComponentModel.Container();
        this.printButton = new System.Windows.Forms.Button();

        this.ClientSize = new System.Drawing.Size(10, 10);
        this.Text = "Print 2 PDF";
        this.Visible = false;

        printButton.ImageAlign = System.Drawing.ContentAlignment.MiddleLeft;
        printButton.Location = new System.Drawing.Point(1, 1);
        printButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
        printButton.TabIndex = 0;
        printButton.Text = "Print the file.";
        printButton.Size = new System.Drawing.Size(1,1);
        printButton.Click += new System.EventHandler(printButton_Click);
        this.Activated += new System.EventHandler(PrintToPDF_Activated);

        this.Controls.Add(printButton);
    }

}
'@
Add-Type $code -ReferencedAssemblies System.Windows.Forms, System.Drawing
$f = [PrintToPDF]::new('d:\scripts\test.csv', 'd:\scripts\test.pdf')
$f.ShowDialog()

\(ツ)_/


Wednesday, June 19, 2019 12:59 AM

It only handles text-type sources well currently (.txt, .log, etc), but I'm going to add support for Office documents and better HTML support at some point.