起因:

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

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

  

解决方案:

 谷歌浏览器提供了将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_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Google 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

【问题记录】- 谷歌浏览器 Html生成PDF的更多相关文章

  1. 生成 PDF 全攻略【2】在已有PDF上添加内容

    项目在变,需求在变,不变的永远是敲击键盘的程序员..... PDF 生成后,有时候需要在PDF上面添加一些其他的内容,比如文字,图片.... 经历几次失败的尝试,终于获取到了正确的代码书写方式. 在此 ...

  2. PHP 生成PDF

    一个项目中需要用到网页生成PDF,就是将整个网页生成一个PDF文件, 以前也用过HTML2PDF,只能生成一些简单的HTML代码,复杂的HTML + css 生成的效果惨不忍睹, 百度了一下,发现有个 ...

  3. 生成 PDF 全攻略【1】初体验

    经历过多少踩坑,翻看过多少类似博客,下载过多少版本的Jar,才能摸索出正确的代码书写方式,才能实现项目经理需求分析书中的功能点. 本文借一次 JavaEE 生成PDF的颠簸的实现过程,描述中小公司程序 ...

  4. Django中生成PDF(一)

    Django中生成PDF(一) 需求描述:     某网站与其用户达成一致的协议,每份协议中都有用户相关的独特信息,且还需要生成PDF并存档.PDF文件中需要有企业LOGO.文字描述等信息.其展现形式 ...

  5. Javascript 将 HTML 页面生成 PDF 并下载

    最近碰到个需求,需要把当前页面生成 pdf,并下载.弄了几天,自己整理整理,记录下来,我觉得应该会有人需要 :) html2canvas 简介 我们可以直接在浏览器端使用html2canvas,对整个 ...

  6. java生成PDF,各种格式、样式、水印都有

    代码中有两处需要图片,请自行替换. 一个是水印.一个是手指. 需要的JAR包链接:http://download.csdn.net/detail/justinytsoft/9688893 下面是预览: ...

  7. js将 HTML 页面生成 PDF 并下载

    最近碰到个需求,需要把当前页面生成 pdf,并下载.弄了几天,自己整理整理,记录下来,我觉得应该会有人需要 :) 先来科普两个插件: html2Canvas 简介 我们可以直接在浏览器端使用html2 ...

  8. 【原创】CRM 2015/2016,SSRS 生成PDF文件,幷以附件的形式发送邮件

    主要步骤如下: 生成一条邮件记录 生成一条ActivityParty记录 生成PDF文件,并以Base64添加到ActivityMimeAttachment 中去 打开发送邮件窗口,以便编辑及发送邮件 ...

  9. itext 生成pdf文件添加页眉页脚

    原文来自:https://www.cnblogs.com/joann/p/5511905.html 我只是记录所有jar版本,由于版本冲突及不兼容很让人头疼的,一共需要5个jar, 其中itextpd ...

随机推荐

  1. 【spring源码系列】之【Bean的生命周期】

    为源码付出的每一分努力都不会白费. 1. Bean的实例化概述 前一篇分析了BeanDefinition的封装过程,最终将beanName与BeanDefinition以一对一映射关系放到beanDe ...

  2. react 工程目录简介

    创建一个 todolist 项目,下图是其工程目录. node_modules文件夹 里面存放的是我们所建项目放所依赖的第三方的包 public文件夹 favicon.ico 图标文件,网页标题左上角 ...

  3. ES6 数组的方法

     数组的类 数组的类是Array 数组的定义 var arr=[元素] var arr=new Array(3) 数字3,代表有三个元素或者三个空位 如果数组定义采用 new 实例,类中跟的是一个数字 ...

  4. k8s 1.12 环境部署及学习笔记

    1.K8S概述 1.Kubernetes是什么 2.Kubernetes特性 3.Kubernetes集群架构与组件 4.Kubernetes核心概念 1.1 Kubernetes是什么 • Kube ...

  5. Gerrit+replication 同步Gitlab

    配置环境:gerrit 192.168.1.100gitlab 192.168.1.1011.创建秘钥 [root@gerrit ~]# ssh-keygen -m PEM -t rsa 2.添加ho ...

  6. 2300+字!在不同系统上安装Docker!看这一篇文章就够了

    辰哥准备出一期在Docker跑Python项目的技术文,比如在Docker跑Django或者Flask的网站.跑爬虫程序等等. 在Docker跑Python程序的时候不会太过于细去讲解Docker的基 ...

  7. C++智能指针之shared_ptr与右值引用(详细)

    1. 介绍 在 C++ 中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露.解决这个问题最有效的方法是使用智能指针(smart pointer).智能指针是存储指向动态分配(堆)对象指针 ...

  8. Docker:docker部署Tomcat运行web项目

    查找Docker Hub上的tomcat镜像 docker search tomcat 拉取官方的镜像 docker pull tomcat 启动Tomcat服务 方式1: 通过dockerfile生 ...

  9. Bootstrap-table 显示行号

    趁热打铁,使用bootstrap-table时,想要显示每行的行号,再网上查了查,网上给的显示行号的大部分方法,只要一翻页,行号就会又从1开始计算, 也许没有碰到想要的,自己试着解决了这个问题,本人初 ...

  10. SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!

    大家好,我是飘渺. 今天我们来聊一聊在基于SpringBoot前后端分离开发模式下,如何友好的返回统一的标准格式以及如何优雅的处理全局异常. 首先我们来看看为什么要返回统一的标准格式? 为什么要对Sp ...