cause :
Because the project needs to realize the silent printing effect of web pages , Then the silent printing effect cannot be achieved by directly using the browser printing function .
The preview interface will pop up when the browser prints ( Here's the picture ), Unable to achieve silent printing .
Solution :
Google browser provides a way to html Print directly to pdf And save it as a file , And then pdf Print silently .
Before invoking Google commands , Need to get the current Google installation location :
public static class ChromeFinder { #region Get application directory 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 Get the current program directory private static string GetAppPath() { var appPath = AppDomain.CurrentDomain.BaseDirectory; if (appPath.EndsWith(Path.DirectorySeparatorChar.ToString())) return appPath; return appPath + Path.DirectorySeparatorChar; } #endregion #region lookup /// <summary> /// Try to find Google Apps /// </summary> /// <returns></returns> public static string Find() { // about Windows, Let's first check the registry . This is the safest way , Non default installation locations are also considered . Please note that ,Chrome x64 At present (2019 year 2 month ) Also installed in the program file (x86) in , And use the same registry key ! 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; } } // Collect common executable file names 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"); } // Check the running Directory var currentPath = GetAppPath(); foreach (var exeName in exeNames) { var path = Path.Combine(currentPath, exeName); if (File.Exists(path)) return path; } // Find Google program files in the general software installation directory 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、 Command mode :
Start the Google process by command , Incoming web address 、pdf Save location and other information , take html convert to pdf:
/// <summary> /// function cmd command /// </summary> /// <param name="command"></param> private void RunCMD(string command) { Process p = new Process(); p.StartInfo.FileName = "cmd.exe"; p.StartInfo.UseShellExecute = false; // Whether to use the operating system shell start-up p.StartInfo.RedirectStandardInput = true;// Accept input from the calling program p.StartInfo.RedirectStandardOutput = true;// Get the output information from the calling program p.StartInfo.RedirectStandardError = true;// Redirect standard error output p.StartInfo.CreateNoWindow = true;// Don't show program window p.Start();// Start the program // towards cmd Window sends input information p.StandardInput.WriteLine(command + "&exit"); p.StandardInput.AutoFlush = true; //p.StandardInput.WriteLine("exit"); // Write the command to be executed to the standard input . Use here & Is the symbol of a batch command , Whether the previous command is executed successfully or not (exit) command , If not implemented exit command , Call back ReadToEnd() The method will fake death // Similar symbols and && and || The former means that the following command will not be executed until the previous command is executed successfully , The latter means that the following command will not be executed until the previous command fails // obtain cmd The output of the window p.StandardOutput.ReadToEnd(); p.WaitForExit();// Wait for the program to finish executing and exit the process p.Close(); } public void GetPdf(string url, List<string> args = null) { var chromeExePath = ChromeFinder.Find(); if (string.IsNullOrEmpty(chromeExePath)) { MessageBox.Show(" Failed to get Google browser address "); 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");// Stealth mode args.Add("--headless");// No interface mode args.Add("--disable-gpu");// Ban gpu Speed up args.Add("--print-to-pdf-no-header");// Print generation pdf No header, no footers args.Add($"--print-to-pdf=\"{outpath}\" \"{url}\"");// Print generation pdf To the specified directory } 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"); }
The main command parameters include :
a) --headless: No interface
b) --print-to-pdf-no-header : Print generation pdf Do not include headers and footers
c) --print-to-pdf: Print the page as pdf, The parameter value is the output address
Existing problems :
-
- In this way, multiple Google processes will be generated ( As many as 5 individual ), And frequently create processes when performance is poor , Will lead to pdf slower
- In some cases , The process created by Google : Failed to exit completely , Cause subsequent generation pdf unexecuted .
The exception process parameters are similar :--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
that , There is no way to reuse Google processes , And can generate pdf Operation? ? Then you need to use the second way .
2、Chrome DevTools Protocol The way
The main steps of this method are :
- Create a Google process without interface
#region Start the Google browser process /// <summary> /// Start the Google process , If it has been started, it will not be started /// </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($" exceed {_conversionTimeout.Value}ms, Can't connect to Chrome development tool "); } _chromeWaitEvent.WaitOne(); _chromeProcess.ErrorDataReceived -= _chromeProcess_ErrorDataReceived; _chromeProcess.Exited -= _chromeProcess_Exited; } /// <summary> /// Exit event /// </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 Unexpected exit , {exception}"); } catch (Exception exception) { _chromeEventException = exception; _chromeWaitEvent.Set(); } }/// <summary> /// When Chrome Raised when data is sent to the error output /// </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
- Get the browser from the process output information ws Connection address , And create ws Connect ; Send... To the Google browser process ws news : Open a TAB
WebSocket4Net.WebSocket _browserSocket = null; /// <summary> /// Create connection /// </summary> /// <param name="uri"></param> private void ConnectToDevProtocol(Uri uri) { // establish socket Connect // Browser connection :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()); // Create page cards Socket Connect // Page card connection :ws://127.0.0.1:50160/devtools/browser/53add595-f351-4622-ab0a-5a4a100b3eae var pageUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}/devtools/page/ Page card id"; }
- according to devtools The agreement is created to the current page card ws Connect
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; } }
- Send a command to the page card , Jump to the need to generate pdf The page of
// Send refresh command JObject jObject = new JObject(); jObject["method"] = "Page.navigate"; // Method jObject["id"] = "2"; //id jObject["params"] = new JObject(); // Parameters jObject["params"]["url"] = "http://www.baidu.com"; _pageSocket.Send(jObject.ToString());
- Finally, the page card sends a command to generate pdf
// Send refresh command jObject = new JObject(); jObject["method"] = "Page.printToPDF"; // Method jObject["id"] = "3"; //id jObject["params"] = new JObject(); // Parameter print parameter settings jObject["params"]["landscape"] = false; jObject["params"]["displayHeaderFooter"] = false; jObject["params"]["printBackground"] = false; _pageSocket.Send(jObject.ToString());
Details of command support , Detailed view DevTools Content of agreement
Reference resources :
DevTools agreement : Chrome DevTools Protocol - Page domain
Google parameter description :List of Chromium Command Line Switches « Peter Beverloo