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.
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;
}
}