• 【问题记录】- 谷歌浏览器 Html生成PDF


    起因:

     由于项目需要实现将网页静默打印效果,那么直接使用浏览器打印功能无法达到静默打印效果。

     浏览器打印都会弹出预览界面(如下图),无法达到静默打印。

      

    解决方案:

     谷歌浏览器提供了将html直接打印成pdf并保存成文件方法,然后再将pdf进行静默打印。

     在调用谷歌命令前,需要获取当前谷歌安装位置:

    public static class ChromeFinder
    {
        #region 获取应用程序目录
        private static void GetApplicationDirectories(ICollection<string> directories)
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                const string subDirectory = "Google\Chrome\Application";
                directories.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), subDirectory));
                directories.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), subDirectory));
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                directories.Add("/usr/local/sbin");
                directories.Add("/usr/local/bin");
                directories.Add("/usr/sbin");
                directories.Add("/usr/bin");
                directories.Add("/sbin");
                directories.Add("/bin");
                directories.Add("/opt/google/chrome");
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
                throw new Exception("Finding Chrome on MacOS is currently not supported, please contact the programmer.");
        }
        #endregion
        #region 获取当前程序目录
        private static string GetAppPath()
        {
            var appPath = AppDomain.CurrentDomain.BaseDirectory;
            if (appPath.EndsWith(Path.DirectorySeparatorChar.ToString()))
                return appPath;
            return appPath + Path.DirectorySeparatorChar;
        }
        #endregion
        #region 查找
        /// <summary>
        /// 尝试查找谷歌程序
        /// </summary>
        /// <returns></returns>
        public static string Find()
        {
            // 对于Windows,我们首先检查注册表。这是最安全的方法,也考虑了非默认安装位置。请注意,Chrome x64当前(2019年2月)也安装在程序文件(x86)中,并使用相同的注册表项!
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                var key = Registry.GetValue(@"HKEY_LOCAL_MACHINESOFTWAREWOW6432NodeMicrosoftWindowsCurrentVersionUninstallGoogle Chrome","InstallLocation", string.Empty);
                if (key != null)
                {
                    var path = Path.Combine(key.ToString(), "chrome.exe");
                    if (File.Exists(path)) return path;
                }
            }
            // 收集常用的可执行文件名
            var exeNames = new List<string>();
    
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                exeNames.Add("chrome.exe");
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                exeNames.Add("google-chrome");
                exeNames.Add("chrome");
                exeNames.Add("chromium");
                exeNames.Add("chromium-browser");
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                exeNames.Add("Google Chrome.app/Contents/MacOS/Google Chrome");
                exeNames.Add("Chromium.app/Contents/MacOS/Chromium");
            }
            //检查运行目录
            var currentPath = GetAppPath();
            foreach (var exeName in exeNames)
            {
                var path = Path.Combine(currentPath, exeName);
                if (File.Exists(path)) return path;
            }
            //在通用软件安装目录中查找谷歌程序文件
            var directories = new List<string>();
            GetApplicationDirectories(directories);
            foreach (var exeName in exeNames)
            {
                foreach (var directory in directories)
                {
                    var path = Path.Combine(directory, exeName);
                    if (File.Exists(path)) return path;
                }
            }
            return null;
        }
        #endregion
    }

     1、命令方式: 

      通过命令方式启动谷歌进程,传入网页地址、pdf保存位置等信息,将html转换成pdf:

    /// <summary>
    /// 运行cmd命令
    /// </summary>
    /// <param name="command"></param>
    private void RunCMD(string command)
    {
        Process p = new Process();
        p.StartInfo.FileName = "cmd.exe";
        p.StartInfo.UseShellExecute = false;    //是否使用操作系统shell启动
        p.StartInfo.RedirectStandardInput = true;//接受来自调用程序的输入信息
        p.StartInfo.RedirectStandardOutput = true;//由调用程序获取输出信息
        p.StartInfo.RedirectStandardError = true;//重定向标准错误输出
        p.StartInfo.CreateNoWindow = true;//不显示程序窗口
        p.Start();//启动程序
        //向cmd窗口发送输入信息
        p.StandardInput.WriteLine(command + "&exit");
        p.StandardInput.AutoFlush = true;
        //p.StandardInput.WriteLine("exit");
        //向标准输入写入要执行的命令。这里使用&是批处理命令的符号,表示前面一个命令不管是否执行成功都执行后面(exit)命令,如果不执行exit命令,后面调用ReadToEnd()方法会假死
        //同类的符号还有&&和||前者表示必须前一个命令执行成功才会执行后面的命令,后者表示必须前一个命令执行失败才会执行后面的命令
        //获取cmd窗口的输出信息
        p.StandardOutput.ReadToEnd();
        p.WaitForExit();//等待程序执行完退出进程
        p.Close();
    }
    
    public void GetPdf(string url, List<string> args = null)
    {
        var chromeExePath = ChromeFinder.Find();
        if (string.IsNullOrEmpty(chromeExePath))
        {
            MessageBox.Show("获取谷歌浏览器地址失败");
            return;
        }
        var outpath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tmppdf");
        if (!Directory.Exists(outpath))
        {
            Directory.CreateDirectory(outpath);
        }
        outpath = Path.Combine(outpath, DateTime.Now.Ticks + ".pdf");
        if (args == null)
        {
            args = new List<string>();
            args.Add("--start-in-incognito");//隐身模式
            args.Add("--headless");//无界面模式
            args.Add("--disable-gpu");//禁用gpu加速
            args.Add("--print-to-pdf-no-header");//打印生成pdf无页眉页脚
            args.Add($"--print-to-pdf="{outpath}" "{url}"");//打印生成pdf到指定目录
        }
        string command = $""{chromeExePath}"";
        if (args != null && args.Count > 0)
        {
            foreach (var item in args)
            {
                command += $" {item} ";
            }
        }
        Stopwatch sw = new Stopwatch();
        sw.Start();
        RunCMD(command);
        sw.Stop();
        MessageBox.Show(sw.ElapsedMilliseconds + "ms");
    }

      其中最主要的命令参数包含:

      a)  --headless:无界面

      b) --print-to-pdf-no-header :打印生成pdf不包含页眉页脚

      c) --print-to-pdf:将页面打印成pdf,参数值为输出地址

      存在问题:

      • 通过该方式会生成多个谷歌进程(多达5个),并且频繁的创建进程在性能较差时,会导致生成pdf较慢
      • 在某些情况下,谷歌创建的进程:未能完全退出,导致后续生成pdf未执行。

          异常进程参数类似:--type=crashpad-handler "--user-data-dir=xxx" /prefetch:7 --monitor-self-annotation=ptype=crashpad-handler "--database=xx" "--metrics-dir=xx" --url=https://clients2.google.com/cr/report --annotation=channel= --annotation=plat=Win64 --annotation=prod=Chrome

      那么,有没有方式能达到重用谷歌进程,并且能生成pdf操作呢? 那就需要使用第二种方式。

     2、Chrome DevTools Protocol 方式

      该方式主要步骤:

    • 创建一个无界面谷歌进程
    #region 启动谷歌浏览器进程
    /// <summary>
    /// 启动谷歌进程,如已启动则不启动
    /// </summary>
    /// <exception cref="ChromeException"></exception>
    private void StartChromeHeadless()
    {
        if (IsChromeRunning)
        {
            return;
        }
    
        var workingDirectory = Path.GetDirectoryName(_chromeExeFileName);
        _chromeProcess = new Process();
        var processStartInfo = new ProcessStartInfo
        {
            FileName = _chromeExeFileName,
            Arguments = string.Join(" ", DefaultChromeArguments),
            CreateNoWindow = true,
        };
        _chromeProcess.ErrorDataReceived += _chromeProcess_ErrorDataReceived;
        _chromeProcess.EnableRaisingEvents = true;
        processStartInfo.UseShellExecute = false;
        processStartInfo.RedirectStandardError = true;
        _chromeProcess.StartInfo = processStartInfo;
        _chromeProcess.Exited += _chromeProcess_Exited;
        try
        {
            _chromeProcess.Start();
        }
        catch (Exception exception)
        {
            throw;
        }
        _chromeWaitEvent = new ManualResetEvent(false);
        _chromeProcess.BeginErrorReadLine();
        if (_conversionTimeout.HasValue)
        {
            if (!_chromeWaitEvent.WaitOne(_conversionTimeout.Value))
                throw new Exception($"超过{_conversionTimeout.Value}ms,无法连接到Chrome开发工具");
        }
        _chromeWaitEvent.WaitOne();
        _chromeProcess.ErrorDataReceived -= _chromeProcess_ErrorDataReceived;
        _chromeProcess.Exited -= _chromeProcess_Exited;
    }
    /// <summary>
    /// 退出事件
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void _chromeProcess_Exited(object sender, EventArgs e)
    {
        try
        {
            if (_chromeProcess == null) return;
            var exception = Marshal.GetExceptionForHR(_chromeProcess.ExitCode);
            throw new Exception($"Chrome意外退出, {exception}");
        }
        catch (Exception exception)
        {
            _chromeEventException = exception;
            _chromeWaitEvent.Set();
        }
    }/// <summary>
    /// 当Chrome将数据发送到错误输出时引发
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    private void _chromeProcess_ErrorDataReceived(object sender, DataReceivedEventArgs args)
    {
        try
        {
            if (args.Data == null || string.IsNullOrEmpty(args.Data) || args.Data.StartsWith("[")) return;
            if (!args.Data.StartsWith("DevTools listening on")) return;
            // DevTools listening on ws://127.0.0.1:50160/devtools/browser/53add595-f351-4622-ab0a-5a4a100b3eae
            var uri = new Uri(args.Data.Replace("DevTools listening on ", string.Empty));
            ConnectToDevProtocol(uri);
            _chromeProcess.ErrorDataReceived -= _chromeProcess_ErrorDataReceived;
            _chromeWaitEvent.Set();
        }
        catch (Exception exception)
        {
            _chromeEventException = exception;
            _chromeWaitEvent.Set();
        }
    }
    #endregion
    • 从进程输出信息中获取浏览器ws连接地址,并创建ws连接;向谷歌浏览器进程发送ws消息:打开一个选项卡
    WebSocket4Net.WebSocket _browserSocket = null;
    /// <summary>
    /// 创建连接
    /// </summary>
    /// <param name="uri"></param>
    private void ConnectToDevProtocol(Uri uri)
    {
        //创建socket连接
        //浏览器连接:ws://127.0.0.1:50160/devtools/browser/53add595-f351-4622-ab0a-5a4a100b3eae
        _browserSocket = new WebSocket4Net.WebSocket(uri.ToString());
        _browserSocket.MessageReceived += WebSocket_MessageReceived;
        JObject jObject = new JObject();
       jObject["id"] =
    1;
       jObject[
    "method"] = "Target.createTarget"; jObject["params"] = new JObject(); jObject["params"]["url"] = "about:blank"; _browserSocket.Send(jObject.ToString()); //创建页卡Socket连接 //页卡连接:ws://127.0.0.1:50160/devtools/browser/53add595-f351-4622-ab0a-5a4a100b3eae var pageUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}/devtools/page/页卡id"; }
    • 根据devtools协议向当前页卡创建ws连接
      WebSocket4Net.WebSocket _pageSocket = null;
      private void WebSocket_MessageReceived(object sender, WebSocket4Net.MessageReceivedEventArgs e)
      {
          string msg = e.Message;
          var pars = JObject.Parse(msg);
          string id = pars["id"].ToString();
          switch (id)
          {
              case "1":
                  var pageUrl = $"{_browserUrl.Scheme}://{_browserUrl.Host}:{_browserUrl.Port}/devtools/page/{pars["result"]["targetId"].ToString()}";
                  _pageSocket = new WebSocket4Net.WebSocket(pageUrl);
                  _pageSocket.MessageReceived += _pageSocket_MessageReceived;
                  _pageSocket.Open();
                  break;
          }
      }
    • 向页卡发送命令,跳转到需要生成pdf的页面
    //发送刷新命令
    JObject jObject = new JObject();
    jObject["method"] = "Page.navigate"; //方法
    jObject["id"] = "2"; //id
    jObject["params"] = new JObject(); //参数
    jObject["params"]["url"] = "http://www.baidu.com";
    _pageSocket.Send(jObject.ToString());
    • 最后项该页卡发送命令生成pdf  
      //发送刷新命令
      jObject = new JObject();
      jObject["method"] = "Page.printToPDF"; //方法
      jObject["id"] = "3"; //id
      jObject["params"] = new JObject(); //参数打印参数设置
      jObject["params"]["landscape"] = false;
      jObject["params"]["displayHeaderFooter"] = false;
      jObject["params"]["printBackground"] = false;
      _pageSocket.Send(jObject.ToString());

        

      命令支持的详细内容,详细查看DevTools协议内容

    参考:

     DevTools协议: Chrome DevTools Protocol - Page domain

       谷歌参数说明:List of Chromium Command Line Switches « Peter Beverloo

  • 相关阅读:
    != 比 & 的优先级高
    VC++ 报错:Heap corruption detected
    sqlite 查询数据库中所有的表名,判断某表是否存在,将某列所有数值去重后获得数量
    Unicode与UTF-8,UTF-16
    各种格式的压缩包解压,7zip 命令行
    bat批处理文件运行时隐藏cmd窗口
    标准库中 vector list等排序
    duilib 实现列表头任意拖动
    duilib 实现 XML重用(item完全重合的CList)
    C++-POJ2503-Babelfish[hash]
  • 原文地址:https://www.cnblogs.com/cwsheng/p/15114972.html
Copyright © 2020-2023  润新知