早先介紹過用 Puppeteer Sharp + Chromium 寫出 C# 網頁擷圖服務。同事反映會遇到 Chrom 無故卡住無回應的現象,依過去經驗,這類 Chrome 層級的疑難排除非我能力所及,解決只能靠運氣,只能祝福原力與他同在。。

回家沒事亂爬文找替代方案,發現一個有趣解法 - 使用 System.Web.Forms.WebBrowser 抓圖!

聲明在先:程式碼是由 Stackoverflow + Google 爬文拼湊改寫,對理論未深入理解,也存在不少已知問題,與其說是解決方案,不如說是實驗。如要拿來用於正式環境,請自行調整評估。

核心程式如下,置於 ASP.NET Web 內,專案參照 System.Web.Forms,故可建立 System.Windows.Forms.WebBrowser 物件,Navigate() 連上指定網址,在 DocumentCompleted 事件呼叫 DrawToBitmap() 方法匯出圖檔 參考,非同步轉同步是靠 AutoResetEvent 搞定。但 WebBrowser 畢竟是桌面程式專用元件,存在特殊限制如必須在 Single-Threaded Apartment 模式下執行。我在 Stackoverflow 找到自開 Thread 設定 SetApartmentState(ApartmentState.STA) 配合 Application.Start()、Application.ExitThread() 的奧妙解法,感覺可能有副作用,但初步實測可行。另外,程式透過 Registry HKEY_CURRENT_USER\SOFTWARE\Microsoft\Internet Explorer\MAIN\FeatureControl\FEATURE_BROWSER_EMULATION 參考 指定 WebBrowser 的 IE 相容模式為 IE11 Edge。 圖檔長寬部分則使用 WebBrowser.Document.InvokeScript() 技巧自動偵測 document.body.clientWidth/ clientHeight。

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading;
using System.Web;
using System.Windows.Forms;

namespace MyWeb.Models
{
    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 System.Windows.Forms.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 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");
        }
    }
}

完成後,我在 ASP.NET MVC Home/Index 傳入 URL 取得截圖:(警告:實際上不應任由使用者指定網址,以免被當成跳板)

// 注意:此為示範用途,接受使用者輸入任意網址,實務上應避免以防被當成跳板
public ActionResult Index(string url = null)
{
    try
    {
        var png = WebSnapTool.Snapshot(url ?? "https://www.twitter.com/", delaySecs: 0);
        return File(png, "image/png");
    }
    catch (Exception ex)
    {
        return Content(ex.Message);
    }
}

隨手實測幾個現成網站,得到幾個還能看的樣本,如下圖有 NuGet、Google News、Facebook 登入頁、Twitter 登入頁:

但更多的狀況會得到出現一片空白、部分內容遺失、排版錯亂... 等各式問題,有時 WebBrowser 遇到 HTTPS 安全疑慮、JavaScript 錯誤,Visual Studio + IIS Express 偵錯模式還會跳出對話框要求確認,若是在 IIS 執行不可能互動操作便會卡住。稱不上是一種通用各種網頁的解決方案,但是它存在一些優點:

  1. 完全靠 .NET 內建元件實現網頁截圖,.NET Framework 3.5 都能跑,不限定 ASP.NET MVC / .NET Core。
  2. 可直接放在 IIS 上執行,不需要為了 Chromium 特性費神寫 Windows Servcie
  3. 基於 .NET 技術,在 .NET Process 內執行,較易掌控與偵錯

至於網頁輸出異常率過高,若應用情境擷圖對象有限,內容單純(實務上很多只是數據、圖表而已),也願意積極配合調整(盡可能用純 HTML,簡化 CSS,少用 JavaScript),再加上擷圖頻率有限,則這個出奇簡單的寫法仍很值得一試。

Example of using System.Windows.Forms.WebBrowser in ASP.NET web site to capture image of specified URL.


Comments

Be the first to post a comment

Post a comment