CharacterKit/FacePhotoBridge.cs

UI helper that embeds a hidden WebPanel to send an image URL to an external face-analysis web page and read back analysis results encoded in the page title and URL query. It validates the input URL, opens a small offscreen web view, polls the page title for a JSON status blob, and parses the final data into a FaceMeasurement.

NetworkingExternal Download
🌐 https://adamnizol.github.io/sbox-face-bridge/
using System;
using System.Text.Json;
using Sandbox.UI;

namespace CharacterEditor;
/// <summary>Hidden, short-lived analyzer; all player controls remain native UI.</summary>
public sealed class FacePhotoBridge : IDisposable
{
	public const string Page = "https://adamnizol.github.io/sbox-face-bridge/";
	WebPanel panel;
	readonly Uri analyzerPage;
	string request;
	DateTime started;
	public string Status { get; private set; } = "Connecting to photo analyzer…";
	public string Error { get; private set; }
	public string Build { get; private set; }
	public FaceMeasurement Result { get; private set; }
	public bool Complete => Error is not null || Result is not null;

	public static bool ValidUrl(string value) => value?.Length <= 2048 && Uri.TryCreate(value, UriKind.Absolute, out var uri) && uri.Scheme == "https" && string.IsNullOrEmpty(uri.UserInfo) && !uri.IsLoopback && Uri.CheckHostName(uri.Host) == UriHostNameType.Dns;
	public FacePhotoBridge(Panel owner, string imageUrl, string page = Page)
	{
		if (!ValidUrl(imageUrl))
			throw new Exception("Use a direct HTTPS image URL without a login or password.");
		if (!Uri.TryCreate(page, UriKind.Absolute, out analyzerPage) || (analyzerPage.Scheme != "https" && !(analyzerPage.Scheme == "http" && analyzerPage.IsLoopback)) || !string.IsNullOrEmpty(analyzerPage.UserInfo) || !string.IsNullOrEmpty(analyzerPage.Fragment))
			throw new Exception("Use an HTTPS analyzer page, or HTTP on localhost for development, without credentials or a fragment.");
		request = Guid.NewGuid().ToString("N");
		started = DateTime.UtcNow;
		panel = owner.AddChild<WebPanel>();
		panel.Style.Position = PositionMode.Absolute;
		panel.Style.Width = 256;
		panel.Style.Height = 256;
		panel.Style.Left = -4096;
		panel.Style.Top = -4096;
		panel.Style.Opacity = 0;
		panel.Style.PointerEvents = PointerEvents.None;
		// Fragment avoids sending photo URLs to the GitHub Pages HTTP server.
		panel.Surface.Url = page + (page.Contains('?') ? "&" : "?") + "client=profiles-2#request=" + request + "&image=" + Uri.EscapeDataString(imageUrl);
	}

	public void Tick()
	{
		if (Complete)
			return;
		if ((DateTime.UtcNow - started).TotalSeconds > 120)
		{
			Fail("Photo analysis timed out. Check your connection and try again.");
			return;
		}

		try
		{
			var title = panel.Surface.PageTitle;
			if (string.IsNullOrEmpty(title) || !title.StartsWith('{'))
				return;
			using var doc = JsonDocument.Parse(title);
			var root = doc.RootElement;
			if (!root.TryGetProperty("bridge", out var b) || b.GetString() != "sbox-face")
				return;
			if (root.TryGetProperty("build", out var build))
				Build = build.GetString();
			if (!root.TryGetProperty("request", out var r) || r.GetString() != request)
			{
				if ((DateTime.UtcNow - started).TotalSeconds > 12)
					Fail("The photo analyzer needs its latest website update. Please try again after it is published.");
				return;
			}

			var status = root.GetProperty("status").GetString();
			Status = status switch
			{
				"reading" => "Loading photo…",
				"photo-ready" => "Photo loaded…",
				"loading" => "Loading face analysis model…",
				"analyzing" => "Measuring face…",
				"done" => "Photo analyzed",
				_ => "Connecting to photo analyzer…"
			};
			if (status == "error")
			{
				Fail(root.GetProperty("error").GetString() ?? "Photo analysis failed.");
				return;
			}

			if (status != "done")
				return;
			var uri = new Uri(panel.Surface.Url);
			if (!SameOrigin(analyzerPage, uri))
				throw new Exception("Unexpected analyzer response.");
			var raw = uri.Query.TrimStart('?').Split('&').FirstOrDefault(x => x.StartsWith("data="));
			if (raw is null)
				return;
			var json = Uri.UnescapeDataString(raw[5..].Replace("+", " "));
			if (json.Length != root.GetProperty("length").GetInt32())
				return; // Title and URL arrive independently.
			Result = FaceMeasurement.Parse(json);
			Status = "Ready to generate";
			Dispose();
		}
		catch (Exception e)
		{
			Fail("Could not use this photo: " + e.Message);
		}
	}

	public static bool SameOrigin(Uri expected, Uri actual) => expected is not null && actual is not null && expected.IsAbsoluteUri && actual.IsAbsoluteUri && expected.Scheme == actual.Scheme && expected.Host == actual.Host && expected.Port == actual.Port;
	void Fail(string message)
	{
		Error = message;
		Status = message;
		Dispose();
	}

	public void Dispose()
	{
		panel?.Delete();
		panel = null;
	}
}