ASP.NET Tips #007 - WebP対応に合わせてimgタグの拡張子を置き換えるタグヘルパーを実装する

モダンなブラウザーではJPEG形式やPNG形式のファイルより軽量なWebP形式の画像を表示する事が可能です。

ただ、 Safari 14.0 より古いバージョンのブラウザー等を使っている場合などは、WebPだけを指定してしまうと画像が表示できなくなる為、<picture> タグなどで併記したり、スクリプトで制御したりが必要となります。

WebP 形式に対応したブラウザーで同名の WebP ファイルがサーバー上に存在する場合に、拡張子を置き換えて HTML を出力させ、 WebP ファイルが表示できない場合は src で指定されたファイルをそのまま表示させるタグヘルパーを作成します。

まず、プロジェクトに TagHelpers フォルダーを作り、 ReplaceWebPTagHelper.cs ファイルを作成し、リクエストヘッダーにアクセスが必要になるため、 UrlResolutionTagHelper を継承させます。

タグヘルパーは img タグを対象とし、 src 属性と replace-webp 属性を必要とさせます。

~/TagHelpers/ReplaceWebPTagHelper.cs
[HtmlTargetElement(TagName, Attributes = AttributesName, TagStructure = TagStructure.WithoutEndTag)]
public class ReplaceWebPTagHelper : UrlResolutionTagHelper
{
	private const string TagName = "img";
	private const string AttributesName = SrcAttributeName + "," + ReplaceWebpAttributeName;
	private const string SrcAttributeName = "src";
	private const string ReplaceWebpAttributeName = "replace-webp";

また、オーバーライドした Order を -1001 にする事で asp-append-version より前に実行させる様に設定しています。

タグヘルパーの実行順序
public override int Order => -1001;

次にブラウザーがWebP形式に対応しているか判断する為、リクエストヘッダーの "accept" に "image/webp" が含まれているか、 "user-agent" に "AppleWebKit" と "Version/14." が含まれているのかをチェックします。

"accept" に "image/webp" が含まれているかどうか
var f1 = false;
if (ViewContext.HttpContext.Request.Headers.TryGetValue("accept", out var sv1))
{
	f1 = sv1.ToString().Contains("image/webp");
}
"user-agent" に "AppleWebKit" と "Version/14." が含まれているかどうか
var f2 = false;
if (ViewContext.HttpContext.Request.Headers.TryGetValue("user-agent", out var sv2))
{
	var ua = sv2.ToString();
	f2 = ua.Contains("AppleWebKit") && ua.Contains("Version/14.");
}

このどちらかの条件に一致したら、 src で指定されたファイルの拡張子を .webp に置き換え、 wwwroot 配下の物理パスに変換し、同名の WebP 形式のファイルがサーバー上に存在するかどうかをチェックします。

同名のWebP形式のファイルがサーバー上に存在するかどうか
if (f1 | f2)
{
	var virtualPath = Path.ChangeExtension(Src, "webp");

	var physicalPath = _env.MapWebRootPath(virtualPath);

	if (File.Exists(physicalPath))
		Src = virtualPath;
}

※仮想パスを物理パスに変換する _env.MapWebRootPath() は独自に実装した拡張メソッドになります。 HostEnvironmentExtension に関しては 以前の記事 を参照ください。

これらの条件に一致した場合に拡張子を書き換えて出力します。

以下が完成したタグヘルパーのコードになります。

~/TagHelpers/ReplaceWebPTagHelper.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.IO;
using System.Text.Encodings.Web;

namespace AspNetTips.MvcSite.TagHelpers
{
	[HtmlTargetElement(TagName, Attributes = AttributesName, TagStructure = TagStructure.WithoutEndTag)]
	public class ReplaceWebPTagHelper : UrlResolutionTagHelper
	{
		private const string TagName = "img";
		private const string AttributesName = SrcAttributeName + "," + ReplaceWebpAttributeName;
		private const string SrcAttributeName = "src";
		private const string ReplaceWebpAttributeName = "replace-webp";

		private readonly IWebHostEnvironment _env;

		public ReplaceWebPTagHelper(
			IWebHostEnvironment env,
			IUrlHelperFactory urlHelperFactory,
			HtmlEncoder htmlEncoder
		) : base(urlHelperFactory, htmlEncoder)
		{
			_env = env;
		}

		/// <inheritdoc />
		public override int Order => -1001;

		[HtmlAttributeName(SrcAttributeName)]
		public string Src { get; set; }

		[HtmlAttributeName(ReplaceWebpAttributeName)]
		public bool ReplaceWebp { get; set; }

		public override void Process(TagHelperContext context, TagHelperOutput output)
		{
			if (context == null)
				throw new ArgumentNullException(nameof(context));

			if (output == null)
				throw new ArgumentNullException(nameof(output));

			output.CopyHtmlAttribute(SrcAttributeName, context);

			if (ReplaceWebp)
			{
				Src = output.Attributes[SrcAttributeName].Value as string;

				var f1 = false;
				if (ViewContext.HttpContext.Request.Headers.TryGetValue("accept", out var sv1))
				{
					f1 = sv1.ToString().Contains("image/webp");
				}

				var f2 = false;
				if (ViewContext.HttpContext.Request.Headers.TryGetValue("user-agent", out var sv2))
				{
					var ua = sv2.ToString();
					f2 = ua.Contains("AppleWebKit") && ua.Contains("Version/14.");
				}

				if (f1 | f2)
				{
					var virtualPath = Path.ChangeExtension(Src, "webp");

					var physicalPath = _env.MapWebRootPath(virtualPath);

					if (File.Exists(physicalPath))
						Src = virtualPath;
				}

				output.Attributes.SetAttribute(SrcAttributeName, Src);
			}
		}
	}
}

タグヘルパーができたらビューで使用できる様に設定が必要です。
Views フォルダーにある _ViewImports.cshtml@addTagHelper ディレクティブ を追加します。プロジェクトのアセンブリ名を指定して、 * で全てのタグヘルパーを使用可能にしています。

ソリューション エクスプローラー
~/Views/_ViewImports.cshtml

準備が整ったら、ビューで img タグに replace-webp="true" を付けるだけで使用でき、 asp-append-version と併用する事でブラウザーキャッシュ対策にも対応してハッシュを付与する事も可能です。

タグヘルパーの使い方

HTML出力時には、 "~/images/test1.png" → "/images/test1.webp?v=xxxxxxxxxx" と変換されます。

ページのソースを表示
試作環境
  • Windows 10 (20H2)
  • Visual Studio 2019 (v16.8.5)
  • .NET SDK 5.0.103
  • ASP.NET Core 5.0 / MVC