ゲーム翻訳ツール

<iframe width="560" height="400" src="https://www.youtube.com/embed/V10MgLpjpBc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
　
<iframe width="560" height="400" src="https://www.youtube.com/embed/LidIzK3iPqQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
　
サンプルコード

```js:GAS
function doPost(e) {
	var a = [e.postData.getDataAsString(),'en','ja']
	return ContentService.createTextOutput(LanguageApp.translate(...a))
}
```

```c#:MainWindow.xaml.cs
namespace Konjac{
	public partial class MainWindow : Window{
		public MainWindow(){
			InitializeComponent();
			webView.NavigationCompleted += (s, e) => {
				webView.CoreWebView2.AddHostObjectToScript("bridge", new Bridge(webView));
			};
			webView.Source = new Uri(GetPath() + "\\Konjac.html");
		}
		private static string GetPath(){
			var p = System.Diagnostics.Process.GetCurrentProcess();
			return System.IO.Path.GetDirectoryName(p.MainModule.FileName);
		}
	}
}
```


```c#:Bridge.cs
public class Native{
	// DPI (PerMonitorV2)
	//https://msdn.microsoft.com/en-us/library/windows/desktop/dd145062(v=vs.85).aspx
	//https://msdn.microsoft.com/en-us/library/windows/desktop/dn280510(v=vs.85).aspx
	public const int WM_DPICHANGED = 0x02E0;
	[DllImport("User32.dll")]
	public static extern IntPtr MonitorFromPoint([In] Point pt, [In] uint dwFlags);
	[DllImport("Shcore.dll")]
	public static extern IntPtr GetDpiForMonitor([In] IntPtr hmonitor, [In] DpiType dpiType, [Out] out uint dpiX, [Out] out uint dpiY);
	public enum DpiType{
		Effective = 0,
		Angular = 1,
		Raw = 2,
	}
}
public class ScreenData{
	public uint dpi { get; set; }
	public Screen screen { get; set; }
	public ScreenData(Screen s){
		var p = new Point(s.Bounds.Left + 1, s.Bounds.Top + 1);
		var m = Native.MonitorFromPoint(p, 2/*MONITOR_DEFAULTTONEAREST*/);
		uint x, y;
		Native.GetDpiForMonitor(m, Native.DpiType.Effective, out x, out y);
		screen = s;
		dpi = x;
	}
}
public class BridgeEvent{
	public string type { get; set; }
	public string value { get; set; }
	public BridgeEvent(string t, string v){
		type = t;
		value = v;
	}
	public string ToJson(){
		return JsonSerializer.Serialize(this);
	}
}

[ComVisible(true)]
public class Bridge{
	Bitmap capture;
	WebView2 view;
	public Bridge(WebView2 w){
		view = w;
	}
	public async void Scan(int x, int y, int w, int h){
		Bitmap textBmp = new Bitmap(w, h);
		Graphics g = Graphics.FromImage(textBmp);
		g.DrawImage(capture, new Point(-x, -y));
		g.Dispose();
		string file = "Konjac.png";
		string path = $"{GetPath()}\\{file}";
		textBmp.Save(path);
		string s = await OCR(file);
		Console.WriteLine(s);
		var e = new BridgeEvent("SCAN_DONE", s);

		//jsからの非同期呼び出しでnullが返る
		//https://github.com/MicrosoftEdge/WebView2Feedback/issues/822
		view.CoreWebView2.PostWebMessageAsString(e.ToJson());
	}
	public async Task<string> OCR(string file){
		//https://water2litter.net/rye/post/c_ocr_hello/

		Stream input_stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
		var source = new WriteableBitmap(System.Windows.Media.Imaging.BitmapFrame.Create(input_stream));
		input_stream.Close();

		PngBitmapEncoder encoder = new PngBitmapEncoder();
		encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(source));
		MemoryStream temp_stream = new MemoryStream();
		encoder.Save(temp_stream);

		var converted_stream = WindowsRuntimeStreamExtensions.AsRandomAccessStream(temp_stream);

		var decorder = await Windows.Graphics.Imaging.BitmapDecoder.CreateAsync(converted_stream);
		SoftwareBitmap bitmap = await decorder.GetSoftwareBitmapAsync();
		converted_stream.Dispose();
		temp_stream.Close();

		OcrEngine engine = OcrEngine.TryCreateFromLanguage(new Windows.Globalization.Language("en"));
		var result = await engine.RecognizeAsync(bitmap);
		return result.Text;
	}
	public string Post(string url, string data){
		//https://docs.microsoft.com/ja-jp/dotnet/framework/network-programming/how-to-send-data-using-the-webrequest-class

		Console.WriteLine(url);
		byte[] byteArray = Encoding.UTF8.GetBytes(data);
		var request = (HttpWebRequest)WebRequest.Create(url);
		request.Method = "POST";
		request.ContentType = "application/x-www-form-urlencoded";
		request.ContentLength = byteArray.Length;
		Stream dataStream = request.GetRequestStream();
		dataStream.Write(byteArray, 0, byteArray.Length);
		dataStream.Close();

		var response = (HttpWebResponse)request.GetResponse();
		string responseContent = string.Empty;
		using (dataStream = response.GetResponseStream())
		{
			StreamReader reader = new StreamReader(dataStream);
			string responseFromServer = reader.ReadToEnd();
			Console.WriteLine(responseFromServer);
			response.Close();
			return responseFromServer;
		}
	}
	public string CaptureScreen(int i){
		Screen s = Screen.AllScreens[i];
		capture = new Bitmap(s.Bounds.Width, s.Bounds.Height);
		Graphics g = Graphics.FromImage(capture);
		g.CopyFromScreen(new Point(s.Bounds.X, s.Bounds.Y), new Point(0, 0), capture.Size);
		g.Dispose();
		string file = $"Screen{i}.png";
		string path = $"{GetPath()}\\{file}";
		Console.WriteLine(path);
		capture.Save(path);
		return file;
	}
	private static string GetPath(){
		var p = System.Diagnostics.Process.GetCurrentProcess();
		return Path.GetDirectoryName(p.MainModule.FileName);
	}
	public string getAllScreens(){
		List<ScreenData> screens = new List<ScreenData>();
		foreach (Screen s in Screen.AllScreens){
			screens.Add(new ScreenData(s));
		}
		return JsonSerializer.Serialize(screens);
	}
}
```

```html:Konjac.html
<!DOCTYPE html><html lang="ja"><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script>

DEPLOY_ID = '***************************' //GAS

$(e=>{$('body').append(new Konjac().view)})

class Konjac{
	constructor(){
		Konjac.native = new Konjac.Bridge().ready(v=>{
			v.getScreens(r=>{
				this.item = r.map((v,i)=>new Konjac.Screen(v,i))
				this.init()
			})
		})
		this.view = _html('.container')
	}
	init(){
		this.currentItem = this.item[0]
		var screens = this.item.sort((a,b)=>{return a.x-b.x})
		screens.forEach(v=>v.menuItem.click(e=>this.select(v)))
		
		this.view.append(
			_html('.content',screens.map(v=>v.view)),
			this.overlay = _html('.overlay').hide().click(e=>this.overlay.hide()),
			_html('.header',
				_html('ul',screens.map(v=>v.menuItem)),
				_html('.button,翻訳する').click(e=>{
					this.overlay.empty().show()
					this.ocr()
				})
			),
		)
		Konjac.native.hook.SCAN_DONE = e=>{
			this.overlay.append(_html('.en',e.value))
			this.translate(e.value,ja=>{
				this.overlay.prepend(_html('.ja',ja))
			})
		}
		var g = new Konjac.Gamepad({
			GAMEPAD_A:e=>this.rescan(),
			GAMEPAD_B:e=>{this.overlay.hide()}
		})
		this.select(this.currentItem)
	}
	rescan(){
		this.currentItem.capture(r=>{
			this.overlay.empty().show()
			this.ocr()
		})
	}
	select(item){
		this.overlay.hide()
		this.currentItem = item
		this.item.forEach(v=>{
			v.menuItem.removeClass('selected')
			v.view.hide()
		})
		item.menuItem.addClass('selected')
		item.view.show()
		item.capture()
	}
	ocr(){
		var dpr = this.currentItem.dpr
		var w = $(window)
		var rect = [	
			Math.round(w.scrollLeft()*dpr),
			Math.round(w.scrollTop()*dpr),
			Math.round(w.width()*dpr),
			Math.round((w.height()-$('.header').height())*dpr),
		]
		Konjac.native.ocr(...rect)
		return this
	}
	translate(data,success){
		var url = `https://script.google.com/macros/s/${DEPLOY_ID}/exec`;
		Konjac.native.post(url,data,v=>success(v))
		return this
	}
}

Konjac.Gamepad = class KonjacGamepad{
	constructor(hook){
		var s = 'A,B,X,Y,LB,RB,LT,RT,BACK,START,LS,RS,UP,DOWN,LEFT,RIGHT,HOME'
		this.types	= s.split(',').map(s=>'GAMEPAD_'+s)
		this.hook	= hook
		this.stack	= []
		this.length	= 0
		window.addEventListener("gamepadconnected",e=>{
			console.log(e)
			this.stack[e.gamepad.index] = [e.gamepad]
			this.length++
		});
		window.addEventListener("gamepaddisconnected",e=>{
			console.log(e)
			this.stack[e.gamepad.index] = []
			this.length--
		});
		setInterval(e=>this.loop(),1000/30)
	}
	loop(){
		if(this.length==0) return
		var items = [...navigator.getGamepads()].filter(Boolean)
		if(items.length==0) return
		var a = this.stack[items[0].index]
		a.unshift(items[0])
		a[0].buttons.forEach((b,i)=>{
			if(b.pressed==true) return
			if(a[1].buttons[i].pressed==false) return
			this.dispatch(this.types[i])
		})
		this.stack[items[0].index] = a.slice(0,1)
	}
	dispatch(type){
		console.log(type)
		if(this.hook[type]) this.hook[type]()
	}
}

Konjac.Screen = class KonjacScreen{
	constructor(data,i){
		var b = data.screen.Bounds
		var dpr = data.dpi/96
		this.x	= b.Left
		this.y	= b.Top
		this.w	= b.Width
		this.h	= b.Height
		this.width	= Math.round(b.Width/dpr)
		this.height	= Math.round(b.Height/dpr)
		this.dpr 	= dpr//devicePixelRatio
		this.name	= data.screen.DeviceName.replace(/[^\w\s]/g,'')
		this.index	= i
		Object.assign(this,data)
		var s = b.Width+' x '+b.Height
		this.sizeText = Konjac.Screen.type[s]||s
		this.menuItem = _html('li',this.name,' ',_html('span',this.sizeText))
		this.view = _html('img').css({width:this.width})
	}
	capture(f){
		Konjac.native.capture(this.index,r=>{
			this.view.attr('src',r+'?'+Math.random())
			if(f) f(r)
		})
		return this
	}
}
Konjac.Screen.type = {
	'1920 x 1080':'FHD',
	'1920 x 1200':'WUXGA',
	'2560 x 1440':'WQHD',
	'3840 x 2160':'4K',
}

Konjac.Bridge = class KonjacBridge{
	constructor(){
		this.hook = {}
		this.on(()=>{}).ready(()=>{})
		this.hostObjects = {bridge:{}}
		if(chrome.webview){
			this.hostObjects = chrome.webview.hostObjects
			chrome.webview.addEventListener('message',e=>{
				var data = JSON.parse(e.data)
				var hook = this.hook[data.type]||this.hook.on
				hook(data)
			})
		}
		setTimeout(e=>this.hook.ready(this),100)
	}
	on(f){this.hook.on = f
		return this
	}
	ready(f){this.hook.ready = f
		return this
	}
	getScreens(f){
		this.hostObjects.bridge.getAllScreens().then(r=>{
			f(JSON.parse(r))
		}).catch(console.log)
		return this
	}
	capture(i,f){
		this.hostObjects.bridge.CaptureScreen(i).then(r=>f(r))
		return this
	}
	ocr(...a){this.hostObjects.bridge.Scan(...a)
		return this
	}
	async post(url,data,success) {
    	var result = await this.hostObjects.bridge.Post(url,data);
    	success(result)
	}
}

function _html(...a){
	var b = a.shift().split(',')
	var c = b.shift().split('.')
	var d = c.shift()||'div'
	return $(`<${d}>`).addClass(...c).append(...b,...a)
}

</script>
<style>
/* 略 */
</style></head></html>
```