zoukankan      html  css  js  c++  java
  • 【问题记录】- 谷歌浏览器 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

  • 相关阅读:
    软件体系架构复习要点
    Operating System on Raspberry Pi 3b
    2019-2020 ICPC North-Western Russia Regional Contest
    2019 ICPC ShenYang Regional Online Contest
    2019 ICPC XuZhou Regional Online Contest
    2019 ICPC NanChang Regional Online Contest
    2019 ICPC NanJing Regional Online Contest
    Codeforces Edu Round 72 (Rated for Div. 2)
    Codeforces Round #583 (Div.1+Div.2)
    AtCoder Beginning Contest 139
  • 原文地址:https://www.cnblogs.com/cwsheng/p/15114972.html
Copyright © 2011-2022 走看看