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

chaney1992 2021-08-08 20:39:50
记录 谷歌 浏览器 问题 题记


起因:

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

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

解决方案:

谷歌浏览器提供了将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

版权声明
本文为[chaney1992]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/cwsheng/p/15114972.html

  1. HTML + CSS + JavaScript to achieve cool Fireworks (cloud like particle text 3D opening)
  2. HTML + CSS + JavaScript realizes 520 advertising love tree (including music), which is necessary for programmers to express themselves
  3. Solve the problem of Web front-end deployment server (it can be deployed online without a server)
  4. HTML + CSS + JS make wedding countdown web page template (520 / Tanabata Valentine's Day / programmer advertisement)
  5. What else can driverless minibus do besides "Park connection"?
  6. Cloud native leads the era of all cloud development
  7. NRM mirror source management tool
  8. Bring it to you, flex Jiugong
  9. Lolstyle UI component development practice (II) -- button group component
  10. Deconstruction assignment in ES6
  11. Luo 2 peerless Tang clan was officially launched. The official gave a key point, and the broadcast time was implied
  12. 20初识前端HTML(1)
  13. 当新零售遇上 Serverless
  14. 20 initial knowledge of front-end HTML (1)
  15. When new retail meets serverless
  16. [golang] - go into go language lesson 5 type conversion
  17. [golang] - go into go language lesson 6 conditional expression
  18. HTML5(八)——SVG 之 path 详解
  19. HTML5 (8) -- detailed explanation of SVG path
  20. 需要开通VIP以后页面内容才能复制怎么办?控制台禁用javascript即可
  21. Web前端|CSS入门教程(超详细的CSS使用讲解,适合前端初学者)
  22. 实践积累 —— 用Vue3简单写一个单行横向滚动组件
  23. Serverless 全能选手,再下一城
  24. What if you need to open a VIP to copy the page content? Just disable JavaScript on the console
  25. Web front end | CSS introductory tutorial (super detailed CSS explanation, suitable for front-end beginners)
  26. Practice accumulation - write a single line horizontal scroll component simply with vue3
  27. Dili Reba is thin again. She looks elegant and high in a strapless hollow skirt, and her "palm waist" is beautiful to a new height
  28. Serverless all-round player, next city
  29. The difference between MySQL semi synchronous replication and lossless semi synchronous replication
  30. Vue表单设计器的终极解决方案
  31. The ultimate solution for Vue form designer
  32. Nginx从理论到实践超详细笔记
  33. Yu Shuxin's red backless swimsuit is split to the waist and tail, with a concave convex figure and excessive color matching, and his face is white to dazzling
  34. Nginx ultra detailed notes from theory to practice
  35. 【动画消消乐|CSS】086.炫酷水波浪Loading过渡动画
  36. typecho全站启用https
  37. CCTV has another popular employee. The off-site interpretation is very professional, and the appearance ability is no less than that of Wang Bingbing
  38. [animation Xiaole | CSS] 086. Cool water wave loading transition animation
  39. Enable HTTPS in Typecho
  40. 50天用JavaScript完成50个web项目,我学到了什么?
  41. 根据JavaScript中原生的XMLHttpRequest实现jQuery的Ajax
  42. What have I learned from completing 50 web projects with JavaScript in 50 days?
  43. "My neighbor doesn't grow up" has hit the whole network. There are countless horse music circles, and actor Zhou Xiaochuan has successfully made a circle
  44. 根据JavaScript中原生的XMLHttpRequest实现jQuery的Ajax
  45. Implement the Ajax of jQuery according to the native XMLHttpRequest in JavaScript
  46. Implement the Ajax of jQuery according to the native XMLHttpRequest in JavaScript
  47. 30 + women still wear less T-shirts and jeans. If they wear them like stars, they will lose weight
  48. 数栈技术分享前端篇:TS,看你哪里逃~
  49. Several stack technology sharing front end: TS, see where you escape~
  50. 舍弃Kong和Nginx,Apache APISIX 在趣链科技 BaaS 平台的落地实践
  51. Abandon the landing practice of Kong and nginx, Apache apisik on the baas platform of fun chain technology
  52. 浪迹天涯king教你用elementui做复杂的表格,去处理报表数据(合并表头,合并表体行和列)
  53. 前端HTML两万字图文大总结,快来看看你会多少!【️熬夜整理&建议收藏️】
  54. Wandering around the world king teaches you to use elementui to make complex tables and process report data (merge header, merge table body rows and columns)
  55. 路由刷新数据丢失 - vuex数据读取的问题
  56. Front end HTML 20000 word graphic summary, come and see how much you can【 Stay up late to sort out & suggestions]
  57. Route refresh data loss - vuex data reading problem
  58. Systemctl系统启动Nginx服务脚本
  59. Systemctl system startup nginx service script
  60. sleepless