前言

昨天接到一個新需求,需要用 Url 連結到其他網頁將該頁面截圖後,轉換成pdf檔供使用者下載。
今天研究了一下覺得好玩紀錄一下。

本文

接到需求後一開始是用 .NET Webpage to pdf 當關鍵字搜尋,無奈找了幾個套件不是要收費就是已經年代久遠,最後用中文搜尋到了黑大去年寫的一篇文章。
C# 整合 Headless Chrome 的好工具 - Puppeteer Sharp

此篇是用 Puppeteer Sharp 這個套件將網頁截圖後儲存,套件資訊可以自行參閱。
Puppeteer Sharp 是使用 Chromium 幫你截圖網站頁面,所以其實後背後運作 .NET 還是有幫你開啟網頁的。
無奈今天嘗試後遇到 Failed to launch chrome 錯誤。
Google後發現很多人都有遇到類似問題,有很多人發issue給作者。

最後決定回頭繼續搜尋其他方式,這時候發現黑大今年又寫了另一篇文章。
C# 網頁轉圖檔 WebAPI - 青春版
這方法使用的是原生 System.Web.Forms.WebBrowser 方式擷取網頁圖片,一樣的 .NET 有幫你開啟網頁進行截圖的動作,只是這個原生的方式就是使用IE。

測試的此方式可行後我就開始尋找第二步動作,該怎麼把圖檔轉換成 pdf,於是找到了另一個套件iTextSharp
可以套過套件將圖檔直接轉換成pdf的強大方法。
將黑大的方法結合 iTextSharp 就能得到想到的結果如下…

	private static void Main(string[] args)
        {
            string url = null;
            var png = WebSnapTool.Snapshot(url ?? "https://sunnyday0932.github.io/2020/object-oriented%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E8%A8%AD%E8%A8%88%E5%8E%9F%E5%89%87solid-1_single-responsibility-principlesrp-%E5%96%AE%E4%B8%80%E8%81%B7%E8%B2%AC/", delaySecs: 0);
            using (var stream = File.Create(@"D:\test\test.pdf"))
            using (var doc = new Document())
            using (var pdfWriter = PdfWriter.GetInstance(doc, stream))
            {
                doc.Open();
                var image = iTextSharp.text.Image.GetInstance(png);
                image.ScalePercent(50); //調比例方式
                doc.Add(image);
                doc.Close();
            }

        }


	public class WebSnapTool
        {
            static WebSnapTool()
            {
                var appName = Process.GetCurrentProcess().MainModule.ModuleName;
                long webBrowserEmuVer = 11001; //指定 IE 相容模式 - IE11 Edge Mode
                Registry.SetValue(@"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Internet Explorer\MAIN\FeatureControl\FEATURE_BROWSER_EMULATION",
                    appName, webBrowserEmuVer, RegistryValueKind.DWord);
                Registry.SetValue(@"HKEY_CURRENT_USER\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\MAIN\FeatureControl\FEATURE_BROWSER_EMULATION",
                    appName, webBrowserEmuVer, RegistryValueKind.DWord);
            }

            public static byte[] Snapshot(string url, int width = 1024, int height = 768, int delaySecs = 0, bool autoSize = true)
            {
                var sync = new AutoResetEvent(false);
                byte[] png = null;
                //STA
                var thread = new Thread(() =>
                {
                    var threadSync = new AutoResetEvent(false);
                    using (var browser = new WebBrowser()
                    {
                        Width = width,
                        Height = height
                    })
                    {
                        Func<string, string> Eval = (script) =>
                        {
                            return browser.Document.InvokeScript("eval", new string[] {
                            "(function() { return " + script + "; })()" }).ToString();
                        };
                        browser.DocumentCompleted += delegate
                        {
                            Thread.Sleep(delaySecs * 1000);
                            if (autoSize)
                            {
                                browser.Width = int.Parse(Eval("document.body.clientWidth"));
                                browser.Height = int.Parse(Eval("document.body.clientHeight"));
                            }
                            using (var pic = new Bitmap(browser.Width, browser.Height))
                            {
                                browser.DrawToBitmap(pic, new System.Drawing.Rectangle(0, 0, pic.Width, pic.Height));
                                using (var ms = new MemoryStream())
                                {
                                    pic.Save(ms, ImageFormat.Png);
                                    png = ms.ToArray();
                                    Application.ExitThread();
                                }
                            }
                        };
                        browser.ScrollBarsEnabled = false;
                        browser.Navigate(url);
                        Application.Run();
                        sync.Set();
                    }
                });
                thread.SetApartmentState(ApartmentState.STA);
                thread.Start();
                //20秒無法產生即放棄
                var succ = sync.WaitOne(20 * 1000);
                if (thread.IsAlive)
                {
                    thread.Abort();
                }
                if (succ) return png;
                else throw new ApplicationException("Web snapshot failed");
            }
        }

後記

如果本篇文章主題-純屬好玩,實際做過之後發現幾個問題,跟主管討論後決定不採用這個方式製作,因此只是覺得好玩紀錄一下,也把發現的問題提供出來給大家參考。

  • 1、因為 .NET 實際背後運作是有幫你打開瀏覽器做截圖,若使用者過多是很吃資源的。
  • 2、當目標網頁若有語法錯誤的時候這個方法就會跳出警示Ex:前端一些 js 錯誤等。
  • 3、如上圖所示 pdf 出來大小會有點奇怪,需要透過自己調整 iTextSharp 的方法,才能符合最佳大小,這也意味著不同網站,或是同網站不同結果都需要客製化參數調整。
  • 4、回到該需求本身,若目標網頁修改了任意參數,那這個方式就會直接死亡。

參考連結