Skip to main content

C# wrapper class wrapper around the WkPdfToHtml executable.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using MarkdownMonster.Annotations;

namespace MarkdownMonster
{

    /// <summary>
    /// Class wrapper around the WkPdfToHtml Engine
    /// https://wkhtmltopdf.org
    /// </summary>
    public class HtmlToPdfGeneration : INotifyPropertyChanged
    {
        /// <summary>
        /// The document title. If null or empty the first
        /// header is used which is the default..
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        ///  The path to the wkpdftohtml executable (optional)
        /// </summary>
        public string ExecutionPath { get; set; }

        /// <summary>
        /// Documents paper size Letter,
        /// </summary>
        public PdfPageSizes PageSize { get; set; }

        public PdfPageOrientation Orientation { get; set; }

        #region headers and footers

        public string FooterText { get; set; }
        public string FooterHtmlUrl { get; set; }
        public bool ShowFooterLine { get; set; }
        public int FooterFontSize { get; set; }
        public bool ShowFooterPageNumbers { get; set; }
        public string HeaderHtmlUrl { get; set; }
        public string HeaderLeft { get; set; }
        public string HeaderRight { get; set; }

        #endregion

        public int ImageDpi { get; set; } = 300;
        public bool GenerateTableOfContents { get; set; } = true;
        public bool DisplayPdfAfterGeneration { get; set; } = true;

        public string ExecutionOutputText
        {
            get { return _executionOutputText; }
            set
            {
                if (value == _executionOutputText) { return; }
                _executionOutputText = value;
                OnPropertyChanged();
            }
        }
        private string _executionOutputText;

        public string FullExecutionCommand
        {
            get { return _fullExecutionCommand; }
            set
            {
                if (value == _fullExecutionCommand) { return; }
                _fullExecutionCommand = value;
                OnPropertyChanged();
                OnPropertyChanged(nameof(HasExecutionCommandLine));
            }
        }
        private string _fullExecutionCommand;

        public bool HasExecutionCommandLine
        {
            get { return !string.IsNullOrEmpty(FullExecutionCommand); }
        }
        public bool HasExecutionOutput
        {
            get { return !string.IsNullOrEmpty(FullExecutionCommand); }
        }

        public PdfPageMargins Margins { get; set; } = new PdfPageMargins();

        public bool GeneratePdfFromHtml(string sourceHtmlFileOrUri, string outputPdfFile)
        {
            StringBuilder sb = new StringBuilder();

            // options
            sb.Append($"--image-dpi {ImageDpi} ");
            sb.Append($"--page-size {PageSize} ");
            sb.Append($"--orientation {Orientation} ");
            sb.Append("--enable-internal-links ");
            sb.Append("--keep-relative-links ");
            sb.Append("--print-media-type ");

            sb.Append($"--footer-font-size {FooterFontSize} ");
            if (ShowFooterLine)
            {
                sb.Append("--footer-line");
            }

            // precedence: Html, text, page numbers
            if (!string.IsNullOrEmpty(FooterHtmlUrl))
            {
                sb.Append($"--footer-html \"{FooterHtmlUrl}\" ");
            }
            else if (!string.IsNullOrEmpty(FooterText))
            {
                sb.Append($"--footer-right \"{FooterText}\" ");
            }
            else if (ShowFooterPageNumbers)
            {
                sb.Append("--footer-right \"[page] of [topage]\" ");
            }

            if (!string.IsNullOrEmpty(Title))
            {
                sb.Append($"--header-left \"{Title}\" ");
                //sb.Append($"--header-right \"[subsection]\" ");
                sb.Append("--header-spacing 3 ");
            }

            if (Margins.MarginLeft > 0)
            {
                sb.Append($"--margin-left {Margins.MarginLeft} ");
            }
            if (Margins.MarginRight > 0)
            {
                sb.Append($"--margin-right {Margins.MarginRight} ");
            }
            if (Margins.MarginTop > 0)
            {
                sb.Append($"--margin-top {Margins.MarginTop} ");
            }
            if (Margins.MarginBottom > 0)
            {
                sb.Append($"--margin-bottom {Margins.MarginBottom} ");
            }

            if (!GenerateTableOfContents)
            {
                sb.Append($"--no-outline ");
                sb.Append($"--outline-depth 3 ");
            }

            // in and out files
            sb.Append($"\"{sourceHtmlFileOrUri}\" \"{outputPdfFile}\" ");

            string exe = "wkhtmltopdf.exe";
            if (!string.IsNullOrEmpty(ExecutionPath))
            {
                exe = Path.Combine(ExecutionPath, exe);
            }

            FullExecutionCommand = exe + " " + sb;

            try
            {
                File.Delete(outputPdfFile);
            }
            catch
            {
                SetError("Please close the output file first:\r\n" + outputPdfFile);
                return false;
            }

            int exitCode = ExecuteWkProcess(sb.ToString());
            if (exitCode != 0)
            {
                SetError("Unable to create PDF document. Exit code: " + exitCode + "\r\n" + sb);
                return false;
            }

            if (exitCode == 0 && DisplayPdfAfterGeneration)
            {
                try
                {
                    Process.Start(outputPdfFile);
                }
                catch { }
            }

            return true;
        }

        public int ExecuteWkProcess(string parms, bool copyCommandLineToClipboard = false)
        {
            string exe = "wkhtmltopdf.exe";
            if (!string.IsNullOrEmpty(ExecutionPath))
            {
                exe = Path.Combine(ExecutionPath, exe);
            }

            StringBuilder output = new StringBuilder();

            FullExecutionCommand = "\"" + exe + "\" " + parms;
            if (copyCommandLineToClipboard)
            {
                ClipboardHelper.SetText(FullExecutionCommand);
            }

            using(Process process = new Process())
            {
                process.StartInfo.FileName = exe;
                process.StartInfo.Arguments = parms;
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.CreateNoWindow = true;
                process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.RedirectStandardInput = true;

                void OnProcessOnOutputDataReceived(object o, DataReceivedEventArgs e) => output.AppendLine(e.Data);

                process.OutputDataReceived += OnProcessOnOutputDataReceived;
                process.ErrorDataReceived += OnProcessOnOutputDataReceived;

                try
                {
                    process.Start();
                    process.BeginOutputReadLine();
                    process.BeginErrorReadLine();

                    bool result = process.WaitForExit(180000);
                    if (!result)
                    {
                        return 1473;
                    }
                }
                finally
                {
                    process.OutputDataReceived -= OnProcessOnOutputDataReceived;
                    process.ErrorDataReceived -= OnProcessOnOutputDataReceived;
                }

                ExecutionOutputText = output.ToString();

                return process.ExitCode;

            }
        }

        #region Error Message
        public string ErrorMessage { get; set; }

        protected void SetError()
        {
            SetError("CLEAR");
        }

        protected void SetError(string message)
        {
            if (message == null || message == "CLEAR")
            {
                ErrorMessage = string.Empty;
                return;
            }
            ErrorMessage += message;
        }

        protected void SetError(Exception ex, bool checkInner = false)
        {
            if (ex == null)
            {
                ErrorMessage = string.Empty;
            }

            Exception e = ex;
            if (checkInner)
            {
                e = e.GetBaseException();
            }

            ErrorMessage = e.Message;
        }
        #endregion

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class PdfPageMargins
    {
        public int MarginLeft { get; set; }
        public int MarginRight { get; set; }
        public int MarginTop { get; set; }
        public int MarginBottom { get; set; }
    }

    public enum PdfPageSizes
    {
        Letter,
        A4,
    }

    public enum PdfPageOrientation
    {
        Portrait,
        Landscape
    }

}