物流プロジェクトにおける在庫経路の天気監視要件を満たすために、HTMLメールに組み込む天気情報画像生成プロセスの実装経験を記録する。
-
要件仕様 特定都市の5日間天気予報をテーブル形式で表示し、PDF添付メールとして配送する必要がある。中国気象局のAPI利用よりも視覚的な表現を重視したため、ウェブページキャプチャを採用。
-
ソリューション設計
- オプション1: 公開API利用(高速だが画像変換が必要)
- オプション2: ウェブページスクリーンショット取得(デザイン維持可能だがリソース消費が大きい)
本実装では後者を選択し、以下のようなアーキテクチャを構築した。
- コア実装コード
- メイン処理ロジック
for (int i = 0; i < dt.Rows.Count; i++)
{
DataRow dr = dt.Rows[i];
string pref = HanziToPinyin.Convert(dr["prefecture"].ToString()),
city = HanziToPinyin.Convert(dr["city"].ToString());
string path = (pref == city) ? $"{pref}/" : $"{pref}/{city}.htm";
WebCapture wc = new WebCapture(weatherBaseUrl + path,
Path.Combine(Application.StartupPath, $"{i}.jpeg"));
attachments.Add(wc.CaptureImage());
dr["weatherImage"] = $"<img src='cid:{i}.jpeg' width='420' height='140'/>";
}
// メモリ最適化処理
Process[] runningProcesses = Process.GetProcessesByName("WebCaptureWorker");
foreach (var p in runningProcesses) p.Dispose();
Main2) ウェブページキャプチャクラス
class WebCapture
{
private Bitmap _screenshot;
private string _url, _outputPath;
public WebCapture(string url, string outputPath)
{
_url = url;
_outputPath = outputPath;
}
public Bitmap CaptureImage()
{
Thread thread = new Thread(DoCapture)
{
Name = "WebCaptureWorker",
Priority = ThreadPriority.Highest,
ApartmentState = ApartmentState.STA
};
thread.Start();
thread.Join();
return _screenshot;
}
private void DoCapture()
{
using (WebBrowser browser = new WebBrowser())
{
browser.ScriptErrorsSuppressed = true;
browser.Navigate(_url);
while (browser.ReadyState != WebBrowserReadyState.Complete)
Application.DoEvents();
// 特定領域切り抜き
Rectangle targetArea = new Rectangle(10, 355, 770, 210);
_screenshot = new Bitmap(targetArea.Width, targetArea.Height);
browser.DrawToBitmap(_screenshot, targetArea);
_screenshot.SaveJpeg(_outputPath, 100L);
}
}
}
WebCapture3) 特定要素解析クラス
class ImageUrlParser
{
private WebBrowser _browser;
private string _targetUrl, _searchKeyword;
public ImageUrlParser(string url, string keyword)
{
_targetUrl = url;
_searchKeyword = keyword;
}
public string ExtractImageSrc()
{
Thread thread = new Thread(DoParse)
{
ApartmentState = ApartmentState.STA
};
thread.Start();
thread.Join();
return GetImageSource();
}
private void DoParse()
{
_browser = new WebBrowser();
_browser.Navigate(_targetUrl);
while (_browser.ReadyState != WebBrowserReadyState.Complete)
Application.DoEvents();
foreach (HtmlElement img in _browser.Document.Images)
{
string src = img.GetAttribute("src");
if (src.Contains(_searchKeyword))
{
// 要素位置調整
img.Style = "position:absolute;top:0;left:0;z-index:9999";
break;
}
}
}
private string GetImageSource()
{
foreach (HtmlElement img in _browser.Document.Images)
{
string src = img.GetAttribute("src");
if (src.StartsWith(_searchKeyword))
return src;
}
return string.Empty;
}
}
ImageUrlParser4) 漢字-ピニン変換ユーティリティ
public static class HanziToPinyin
{
private static readonly int[] PinyinCodeMap = {
-20319,-20317,-20304,-20295,-20292,-20283,-20265,-20257,-20242,-20230, // A
// ... (中略) ...
-10322,-10315,-10309,-10307,-10296,-10281,-10274,-10270,-10262,-10260,
-10256,-10254
};
private static readonly string[] PinyinTable = {
"A","Ai","An","Ang","Ao","Ba","Bai","Ban","Bang","Bao", // A
// ... (中略) ...
"Yun","Za","Zai","Zan","Zang","Zao","Ze","Zei","Zen","Zeng"
};
public static string Convert(string input)
{
StringBuilder result = new StringBuilder();
foreach (char c in input)
{
if (IsChineseCharacter(c))
{
byte[] bytes = Encoding.GetEncoding("GB2312").GetBytes(new char[] { c });
int code = (bytes[0] << 8) | bytes[1] - 65536;
for (int i = 0; i < PinyinCodeMap.Length; i++)
{
if (PinyinCodeMap[i] <= code)
{
result.Append(PinyinTable[i]);
break;
}
}
}
else
{
result.Append(c);
}
}
return result.ToString();
}
private static bool IsChineseCharacter(char c)
{
return c >= '\u4e00' && c <= '\u9fff';
}
}
HanziToPinyin