mirror of
https://github.com/gnh1201/welsonjs.git
synced 2025-10-29 12:01:43 +00:00
Add ImageColorPicker tool and async resource serving
Introduces the ImageColorPicker resource tool for extracting color information from images via a POST API. Refactors ResourceServer and all resource tools to use async ServeResource methods, improving scalability and consistency. Updates JsSerializer with an engine-backed document store for efficient repeated JSON extraction.
This commit is contained in:
parent
b1b36744cf
commit
676f791fab
|
|
@ -16,6 +16,10 @@ namespace WelsonJS.Launcher
|
|||
private readonly JsCore _core;
|
||||
private readonly bool _ownsCore;
|
||||
|
||||
// In-engine parsed document store management
|
||||
private bool _storeReady;
|
||||
private int _nextId = 1; // 0 is reserved (unused)
|
||||
|
||||
public JsSerializer() : this(new JsCore(), true) { }
|
||||
|
||||
public JsSerializer(JsCore core, bool ownsCore)
|
||||
|
|
@ -25,6 +29,100 @@ namespace WelsonJS.Launcher
|
|||
_ownsCore = ownsCore;
|
||||
}
|
||||
|
||||
// ---------------- Engine-backed document store ----------------
|
||||
|
||||
/// <summary>
|
||||
/// Parses JSON once and stores it in the engine under a numeric id.
|
||||
/// Returns the id which can be used for fast repeated extraction.
|
||||
/// </summary>
|
||||
public int Load(string json)
|
||||
{
|
||||
if (json == null) throw new ArgumentNullException("json");
|
||||
EnsureStore();
|
||||
|
||||
int id = _nextId++;
|
||||
// Create slot and parse
|
||||
// Using Object.create(null) for a clean dictionary without prototype.
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("(function(){var S=globalThis.__WJ_STORE;");
|
||||
sb.Append("S[").Append(id.ToString(CultureInfo.InvariantCulture)).Append("]=JSON.parse(").Append(Q(json)).Append(");");
|
||||
sb.Append("return '1';})()");
|
||||
string r = _core.EvaluateToString(sb.ToString());
|
||||
if (r != "1") throw new InvalidOperationException("Failed to load JSON into the engine store.");
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously loaded document from the engine store.
|
||||
/// After this, the id becomes invalid.
|
||||
/// </summary>
|
||||
public void Unload(int id)
|
||||
{
|
||||
EnsureStore();
|
||||
string script = "(function(){var S=globalThis.__WJ_STORE; delete S[" + id.ToString(CultureInfo.InvariantCulture) + "]; return '1';})()";
|
||||
_core.EvaluateToString(script);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the stored JSON at a given id (parse once, reuse later).
|
||||
/// </summary>
|
||||
public void Reload(int id, string json)
|
||||
{
|
||||
if (json == null) throw new ArgumentNullException("json");
|
||||
EnsureStore();
|
||||
string script = "(function(){var S=globalThis.__WJ_STORE; S[" + id.ToString(CultureInfo.InvariantCulture) + "]=JSON.parse(" + Q(json) + "); return '1';})()";
|
||||
_core.EvaluateToString(script);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stringifies the stored value identified by id (no reparse).
|
||||
/// </summary>
|
||||
public string ToJson(int id, int space)
|
||||
{
|
||||
EnsureStore();
|
||||
space = Clamp(space, 0, 10);
|
||||
string script = "(function(){var v=globalThis.__WJ_STORE[" + id.ToString(CultureInfo.InvariantCulture) + "]; return JSON.stringify(v,null," + space.ToString(CultureInfo.InvariantCulture) + ");})()";
|
||||
return _core.EvaluateToString(script);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts from a stored document identified by id (no reparse).
|
||||
/// Returns JSON of the selected value.
|
||||
/// </summary>
|
||||
public string ExtractFrom(int id, params object[] path)
|
||||
{
|
||||
EnsureStore();
|
||||
if (path == null) path = new object[0];
|
||||
string jsPath = BuildJsPath(path);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("(function(){var v=globalThis.__WJ_STORE[")
|
||||
.Append(id.ToString(CultureInfo.InvariantCulture))
|
||||
.Append("];var p=").Append(jsPath).Append(";");
|
||||
sb.Append("for(var i=0;i<p.length;i++){var k=p[i];");
|
||||
sb.Append("if(Array.isArray(v) && typeof k==='number'){ v=v[k]; }");
|
||||
sb.Append("else { v=(v==null?null:v[k]); }}");
|
||||
sb.Append("return JSON.stringify(v);})()");
|
||||
return _core.EvaluateToString(sb.ToString());
|
||||
}
|
||||
|
||||
// Initialize the global store only once per JsSerializer instance/context
|
||||
private void EnsureStore()
|
||||
{
|
||||
if (_storeReady) return;
|
||||
// Create a single global dictionary: globalThis.__WJ_STORE
|
||||
// Object.create(null) prevents prototype pollution and accidental hits on built-ins.
|
||||
string script =
|
||||
"(function(){var g=globalThis||this;" +
|
||||
"if(!g.__WJ_STORE){Object.defineProperty(g,'__WJ_STORE',{value:Object.create(null),writable:false,enumerable:false,configurable:false});}" +
|
||||
"return '1';})()";
|
||||
string r = _core.EvaluateToString(script);
|
||||
_storeReady = (r == "1");
|
||||
if (!_storeReady) throw new InvalidOperationException("Failed to initialize the engine-backed JSON store.");
|
||||
}
|
||||
|
||||
// ---------------- Existing API (kept for compatibility) ----------------
|
||||
|
||||
public bool IsValid(string json)
|
||||
{
|
||||
if (json == null) throw new ArgumentNullException("json");
|
||||
|
|
@ -55,8 +153,8 @@ namespace WelsonJS.Launcher
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a value by a simple path of property names (numeric segment as string = array index).
|
||||
/// Returns the selected value as a JSON string.
|
||||
/// Extracts by string-only path (kept for backward compatibility).
|
||||
/// Internally forwards to the mixed-path overload.
|
||||
/// </summary>
|
||||
public string Extract(string json, params string[] path)
|
||||
{
|
||||
|
|
@ -67,9 +165,8 @@ namespace WelsonJS.Launcher
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts by a mixed path. Segments can be strings (object keys) or integers (array indices).
|
||||
/// Returns the selected value as a JSON string (e.g., a JS string returns with quotes).
|
||||
/// Usage: Extract(json, "items", 0, "name")
|
||||
/// Extracts by a mixed path directly from a JSON string (parses every call).
|
||||
/// Prefer Load(...) + ExtractFrom(...) to avoid repeated parsing.
|
||||
/// </summary>
|
||||
public string Extract(string json, params object[] path)
|
||||
{
|
||||
|
|
@ -96,6 +193,8 @@ namespace WelsonJS.Launcher
|
|||
return _core.EvaluateToString(script);
|
||||
}
|
||||
|
||||
// ---------------- Helpers ----------------
|
||||
|
||||
private static int Clamp(int v, int min, int max)
|
||||
{
|
||||
if (v < min) return min;
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ namespace WelsonJS.Launcher
|
|||
_tools.Add(new ResourceTools.IpQuery(this, _httpClient, _logger));
|
||||
_tools.Add(new ResourceTools.TwoFactorAuth(this, _httpClient, _logger));
|
||||
_tools.Add(new ResourceTools.Whois(this, _httpClient, _logger));
|
||||
_tools.Add(new ResourceTools.ImageColorPicker(this, _httpClient, _logger));
|
||||
|
||||
// Register the prefix
|
||||
_listener.Prefixes.Add(prefix);
|
||||
|
|
@ -132,14 +133,14 @@ namespace WelsonJS.Launcher
|
|||
// Serve from a resource name
|
||||
if (String.IsNullOrEmpty(path))
|
||||
{
|
||||
ServeResource(context, GetResource(_resourceName), "text/html");
|
||||
await ServeResource(context, GetResource(_resourceName), "text/html");
|
||||
return;
|
||||
}
|
||||
|
||||
// Serve the favicon.ico file
|
||||
if ("favicon.ico".Equals(path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ServeResource(context, GetResource("favicon"), "image/x-icon");
|
||||
await ServeResource(context, GetResource("favicon"), "image/x-icon");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +158,7 @@ namespace WelsonJS.Launcher
|
|||
if (await ServeBlob(context, path)) return;
|
||||
|
||||
// Fallback to serve from a resource name
|
||||
ServeResource(context, GetResource(_resourceName), "text/html");
|
||||
await ServeResource(context, GetResource(_resourceName), "text/html");
|
||||
}
|
||||
|
||||
private async Task<bool> ServeBlob(HttpListenerContext context, string path, string prefix = null)
|
||||
|
|
@ -188,7 +189,7 @@ namespace WelsonJS.Launcher
|
|||
data = await response.Content.ReadAsByteArrayAsync();
|
||||
mimeType = response.Content.Headers.ContentType?.MediaType ?? _defaultMimeType;
|
||||
|
||||
ServeResource(context, data, mimeType);
|
||||
await ServeResource(context, data, mimeType);
|
||||
_ = TrySaveCachedBlob(path, data, mimeType);
|
||||
|
||||
return true;
|
||||
|
|
@ -211,7 +212,7 @@ namespace WelsonJS.Launcher
|
|||
mimeType = _defaultMimeType;
|
||||
}
|
||||
|
||||
ServeResource(context, data, mimeType);
|
||||
await ServeResource(context, data, mimeType);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -345,12 +346,12 @@ namespace WelsonJS.Launcher
|
|||
}
|
||||
}
|
||||
|
||||
public void ServeResource(HttpListenerContext context)
|
||||
public async Task ServeResource(HttpListenerContext context)
|
||||
{
|
||||
ServeResource(context, "<error>Not Found</error>", "application/xml", 404);
|
||||
await ServeResource(context, "<error>Not Found</error>", "application/xml", 404);
|
||||
}
|
||||
|
||||
public void ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html", int statusCode = 200)
|
||||
public async Task ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html", int statusCode = 200)
|
||||
{
|
||||
string xmlHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
|
||||
|
||||
|
|
@ -364,13 +365,10 @@ namespace WelsonJS.Launcher
|
|||
context.Response.StatusCode = statusCode;
|
||||
context.Response.ContentType = mimeType;
|
||||
context.Response.ContentLength64 = data.Length;
|
||||
using (Stream outputStream = context.Response.OutputStream)
|
||||
{
|
||||
outputStream.Write(data, 0, data.Length);
|
||||
}
|
||||
await context.Response.OutputStream.WriteAsync(data, 0, data.Length);
|
||||
}
|
||||
|
||||
public void ServeResource(HttpListenerContext context, string data, string mimeType = "text/html", int statusCode = 200)
|
||||
public async Task ServeResource(HttpListenerContext context, string data, string mimeType = "text/html", int statusCode = 200)
|
||||
{
|
||||
string xmlHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
|
||||
|
||||
|
|
@ -385,7 +383,7 @@ namespace WelsonJS.Launcher
|
|||
data = xmlHeader + "\r\n" + data;
|
||||
}
|
||||
|
||||
ServeResource(context, Encoding.UTF8.GetBytes(data), mimeType, statusCode);
|
||||
await ServeResource(context, Encoding.UTF8.GetBytes(data), mimeType, statusCode);
|
||||
}
|
||||
|
||||
private byte[] GetResource(string resourceName)
|
||||
|
|
|
|||
|
|
@ -47,11 +47,11 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
string baseUrl = Program.GetAppConfig("ChromiumDevToolsPrefix"); // e.g., http://localhost:9222/
|
||||
string url = baseUrl.TrimEnd('/') + "/" + endpoint;
|
||||
string data = await _httpClient.GetStringAsync(url);
|
||||
Server.ServeResource(context, data, "application/json");
|
||||
await Server.ServeResource(context, data, "application/json");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Server.ServeResource(context, $"<error>Failed to process DevTools request. {EscapeXml(ex.Message)}</error>", "application/xml", 500);
|
||||
await Server.ServeResource(context, $"<error>Failed to process DevTools request. {EscapeXml(ex.Message)}</error>", "application/xml", 500);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
string baseUrl = Program.GetAppConfig("ChromiumDevToolsPrefix");
|
||||
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out Uri uri))
|
||||
{
|
||||
Server.ServeResource(context, "<error>Invalid ChromiumDevToolsPrefix</error>", "application/xml", 500);
|
||||
await Server.ServeResource(context, "<error>Invalid ChromiumDevToolsPrefix</error>", "application/xml", 500);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -97,17 +97,17 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
try
|
||||
{
|
||||
string response = await _wsManager.SendAndReceiveAsync(hostname, port, wsPath, postBody, timeout);
|
||||
Server.ServeResource(context, response, "application/json", 200);
|
||||
await Server.ServeResource(context, response, "application/json", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_wsManager.Remove(hostname, port, wsPath);
|
||||
Server.ServeResource(context, $"<error>WebSocket communication error: {EscapeXml(ex.Message)}</error>", "application/xml", 500);
|
||||
await Server.ServeResource(context, $"<error>WebSocket communication error: {EscapeXml(ex.Message)}</error>", "application/xml", 500);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Server.ServeResource(context, "<error>Invalid DevTools endpoint</error>", "application/xml", 404);
|
||||
await Server.ServeResource(context, "<error>Invalid DevTools endpoint</error>", "application/xml", 404);
|
||||
}
|
||||
|
||||
private string EscapeXml(string text)
|
||||
|
|
|
|||
|
|
@ -72,11 +72,11 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
))
|
||||
);
|
||||
|
||||
Server.ServeResource(context, response.ToString(), "application/xml");
|
||||
await Server.ServeResource(context, response.ToString(), "application/xml");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Server.ServeResource(context, $"<error>Failed to try completion. {ex.Message}</error>", "application/xml", 500);
|
||||
await Server.ServeResource(context, $"<error>Failed to try completion. {ex.Message}</error>", "application/xml", 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,13 +42,11 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
|
||||
public async Task HandleAsync(HttpListenerContext context, string path)
|
||||
{
|
||||
await Task.Delay(0);
|
||||
|
||||
string query = path.Substring(Prefix.Length);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query) || query.Length > 255)
|
||||
{
|
||||
Server.ServeResource(context, "<error>Invalid query parameter</error>", "application/xml", 400);
|
||||
await Server.ServeResource(context, "<error>Invalid query parameter</error>", "application/xml", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -67,11 +65,11 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
}
|
||||
|
||||
string data = result.ToString();
|
||||
Server.ServeResource(context, data, "text/plain", 200);
|
||||
await Server.ServeResource(context, data, "text/plain", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Server.ServeResource(context, $"<error>Failed to process DNS query. {ex.Message}</error>", "application/xml", 500);
|
||||
await Server.ServeResource(context, $"<error>Failed to process DNS query. {ex.Message}</error>", "application/xml", 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,343 @@
|
|||
// ImageColorPicker.cs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
|
||||
// https://github.com/gnh1201/welsonjs
|
||||
//
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WelsonJS.Launcher.ResourceTools
|
||||
{
|
||||
/// <summary>
|
||||
/// POST image-color-picker/ with a unified JSON body:
|
||||
/// { "image": "<B64>", "point": { "x": 10, "y": 20 } }
|
||||
///
|
||||
/// Response (success):
|
||||
/// { "ok": true, "hex": "#RRGGBB", "name": "white", "rgb": { "r": 255, "g": 255, "b": 255 } }
|
||||
///
|
||||
/// Response (error):
|
||||
/// { "ok": false, "error": "..." }
|
||||
///
|
||||
/// JSON handling:
|
||||
/// - Request parse: JsSerializer.Load/ExtractFrom (engine) + tiny local decoders for literals.
|
||||
/// - Response encode: Dictionary<string, object> → JsSerializer.Serialize(...).
|
||||
/// </summary>
|
||||
public class ImageColorPicker : IResourceTool
|
||||
{
|
||||
private readonly ResourceServer Server;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ICompatibleLogger _logger;
|
||||
private const string Prefix = "image-color-picker";
|
||||
|
||||
public ImageColorPicker(ResourceServer server, HttpClient httpClient, ICompatibleLogger logger)
|
||||
{
|
||||
Server = server;
|
||||
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CanHandle(string path)
|
||||
{
|
||||
return path.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public async Task HandleAsync(HttpListenerContext context, string path)
|
||||
{
|
||||
context.Response.ContentType = "application/json; charset=utf-8";
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.Equals(context.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
|
||||
await WriteJsonAsync(context, new Dictionary<string, object>
|
||||
{
|
||||
["ok"] = false,
|
||||
["error"] = "Only POST is supported."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Read request body ---------------------------------------------------------
|
||||
string body;
|
||||
using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding ?? Encoding.UTF8))
|
||||
body = await reader.ReadToEndAsync();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
await WriteJsonAsync(context, new Dictionary<string, object>
|
||||
{
|
||||
["ok"] = false,
|
||||
["error"] = "Empty request body."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Extract strictly in unified format ---------------------------------------
|
||||
string imageJson, xJson, yJson;
|
||||
using (var ser = new JsSerializer())
|
||||
{
|
||||
int id = ser.Load(body);
|
||||
|
||||
// Required: "image" as JSON string
|
||||
imageJson = ser.ExtractFrom(id, "image");
|
||||
|
||||
// Required: "point": { "x": <num>, "y": <num> }
|
||||
xJson = ser.ExtractFrom(id, "point", "x");
|
||||
yJson = ser.ExtractFrom(id, "point", "y");
|
||||
|
||||
ser.Unload(id);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(imageJson))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
await WriteJsonAsync(context, new Dictionary<string, object>
|
||||
{
|
||||
["ok"] = false,
|
||||
["error"] = "Missing required field: 'image' (base64 string)."
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(xJson) || string.IsNullOrEmpty(yJson))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
await WriteJsonAsync(context, new Dictionary<string, object>
|
||||
{
|
||||
["ok"] = false,
|
||||
["error"] = "Missing required field: 'point' with numeric 'x' and 'y'."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Convert JSON literals → CLR ----------------------------------------------
|
||||
string imageB64;
|
||||
if (!TryParseJsonString(imageJson, out imageB64) || string.IsNullOrWhiteSpace(imageB64))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
await WriteJsonAsync(context, new Dictionary<string, object>
|
||||
{
|
||||
["ok"] = false,
|
||||
["error"] = "'image' must be a non-empty JSON string."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryParseJsonInt(xJson, out var x) || !TryParseJsonInt(yJson, out var y))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
await WriteJsonAsync(context, new Dictionary<string, object>
|
||||
{
|
||||
["ok"] = false,
|
||||
["error"] = "'point.x' and 'point.y' must be JSON numbers."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow data URL prefix (data:*;base64,....)
|
||||
imageB64 = StripDataUrlPrefix(imageB64);
|
||||
|
||||
byte[] bytes;
|
||||
try { bytes = Convert.FromBase64String(imageB64); }
|
||||
catch
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
await WriteJsonAsync(context, new Dictionary<string, object>
|
||||
{
|
||||
["ok"] = false,
|
||||
["error"] = "Invalid base64 image data."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Decode image and sample pixel --------------------------------------------
|
||||
using (var ms = new MemoryStream(bytes, writable: false))
|
||||
using (var bmp = new Bitmap(ms))
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= bmp.Width || y >= bmp.Height)
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
await WriteJsonAsync(context, new Dictionary<string, object>
|
||||
{
|
||||
["ok"] = false,
|
||||
["error"] = $"Point out of bounds. Image size: {bmp.Width}x{bmp.Height}, requested: ({x},{y})."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var color = bmp.GetPixel(x, y);
|
||||
var hex = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
|
||||
var name = ToCommonColorName(color); // null if not close to a known color
|
||||
|
||||
context.Response.StatusCode = (int)HttpStatusCode.OK;
|
||||
await WriteJsonAsync(context, new Dictionary<string, object>
|
||||
{
|
||||
["ok"] = true,
|
||||
["hex"] = hex,
|
||||
["name"] = name, // may be null → serializer will emit "name": null
|
||||
["rgb"] = new Dictionary<string, object>
|
||||
{
|
||||
["r"] = color.R,
|
||||
["g"] = color.G,
|
||||
["b"] = color.B
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Error($"ImageColorPicker error: {ex}");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
await WriteJsonAsync(context, new Dictionary<string, object>
|
||||
{
|
||||
["ok"] = false,
|
||||
["error"] = "Internal error while processing image."
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { context.Response.OutputStream.Flush(); } catch { }
|
||||
try { context.Response.OutputStream.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------- Helpers -----------------------------------------------
|
||||
|
||||
private static bool TryParseJsonInt(string jsonNumberLiteral, out int value)
|
||||
{
|
||||
// Accept JSON numbers like: 12, -3, 10.0, 1e2, -4E+1
|
||||
// Strategy: parse as double using invariant culture, then cast if within int range.
|
||||
value = default;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(jsonNumberLiteral))
|
||||
return false;
|
||||
|
||||
var s = jsonNumberLiteral.Trim();
|
||||
|
||||
// Reject if wrapped in quotes (should be a number literal, not a string)
|
||||
if (s.Length >= 2 && s[0] == '"' && s[s.Length - 1] == '"')
|
||||
return false;
|
||||
|
||||
if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d))
|
||||
{
|
||||
if (d >= int.MinValue && d <= int.MaxValue)
|
||||
{
|
||||
value = (int)d; // truncate toward zero (JSON has no ints; this is reasonable)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseJsonString(string jsonStringLiteral, out string value)
|
||||
{
|
||||
value = null;
|
||||
if (string.IsNullOrWhiteSpace(jsonStringLiteral))
|
||||
return false;
|
||||
|
||||
var s = jsonStringLiteral.Trim();
|
||||
if (s.Length < 2 || s[0] != '"' || s[s.Length - 1] != '"')
|
||||
return false;
|
||||
|
||||
// Remove surrounding quotes and unescape JSON sequences
|
||||
value = JsonUnescape(s.Substring(1, s.Length - 2));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string JsonUnescape(string s)
|
||||
{
|
||||
var sb = new StringBuilder(s.Length);
|
||||
for (int i = 0; i < s.Length; i++)
|
||||
{
|
||||
char c = s[i];
|
||||
if (c != '\\') { sb.Append(c); continue; }
|
||||
|
||||
if (i + 1 >= s.Length) { sb.Append('\\'); break; }
|
||||
char n = s[++i];
|
||||
switch (n)
|
||||
{
|
||||
case '\"': sb.Append('\"'); break;
|
||||
case '\\': sb.Append('\\'); break;
|
||||
case '/': sb.Append('/'); break;
|
||||
case 'b': sb.Append('\b'); break;
|
||||
case 'f': sb.Append('\f'); break;
|
||||
case 'n': sb.Append('\n'); break;
|
||||
case 'r': sb.Append('\r'); break;
|
||||
case 't': sb.Append('\t'); break;
|
||||
case 'u':
|
||||
if (i + 4 < s.Length)
|
||||
{
|
||||
string hex = s.Substring(i + 1, 4);
|
||||
if (ushort.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cp))
|
||||
{
|
||||
sb.Append((char)cp);
|
||||
i += 4;
|
||||
}
|
||||
else sb.Append('u');
|
||||
}
|
||||
else sb.Append('u');
|
||||
break;
|
||||
default:
|
||||
sb.Append(n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string StripDataUrlPrefix(string b64)
|
||||
{
|
||||
var idx = b64.IndexOf("base64,", StringComparison.OrdinalIgnoreCase);
|
||||
return idx >= 0 ? b64.Substring(idx + "base64,".Length) : b64;
|
||||
}
|
||||
|
||||
private static string ToCommonColorName(Color c)
|
||||
{
|
||||
(string name, Color color)[] palette =
|
||||
{
|
||||
("white", Color.FromArgb(255,255,255)),
|
||||
("black", Color.FromArgb(0,0,0)),
|
||||
("red", Color.FromArgb(255,0,0)),
|
||||
("green", Color.FromArgb(0,128,0)),
|
||||
("blue", Color.FromArgb(0,0,255)),
|
||||
("yellow", Color.FromArgb(255,255,0)),
|
||||
("cyan", Color.FromArgb(0,255,255)),
|
||||
("magenta", Color.FromArgb(255,0,255)),
|
||||
("gray", Color.FromArgb(128,128,128))
|
||||
};
|
||||
|
||||
const int tolerance = 20; // distance threshold
|
||||
string best = null;
|
||||
int bestDist = int.MaxValue;
|
||||
|
||||
foreach (var (name, col) in palette)
|
||||
{
|
||||
int dr = c.R - col.R, dg = c.G - col.G, db = c.B - col.B;
|
||||
int dist = dr * dr + dg * dg + db * db;
|
||||
if (dist < bestDist) { bestDist = dist; best = name; }
|
||||
}
|
||||
|
||||
return bestDist <= tolerance * tolerance ? best : null;
|
||||
}
|
||||
|
||||
private async Task WriteJsonAsync(HttpListenerContext ctx, Dictionary<string, object> doc, int space = 0)
|
||||
{
|
||||
using (var ser = new JsSerializer())
|
||||
{
|
||||
string json = ser.Serialize(doc, space);
|
||||
byte[] payload = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
await Server.ServeResource(ctx, payload, "application/json", 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
string apiKey = Program.GetAppConfig("CriminalIpApiKey");
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
Server.ServeResource(context, "<error>Missing API key</error>", "application/xml", 500);
|
||||
await Server.ServeResource(context, "<error>Missing API key</error>", "application/xml", 500);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -54,11 +54,11 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
string content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
context.Response.StatusCode = (int)response.StatusCode;
|
||||
Server.ServeResource(context, content, "application/json", (int)response.StatusCode);
|
||||
await Server.ServeResource(context, content, "application/json", (int)response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Server.ServeResource(context, $"<error>{ex.Message}</error>", "application/xml", 500);
|
||||
await Server.ServeResource(context, $"<error>{ex.Message}</error>", "application/xml", 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
finalConfig.Select(kv => new XElement(kv.Key, kv.Value))
|
||||
);
|
||||
|
||||
Server.ServeResource(context, xml.ToString(), "application/xml");
|
||||
await Server.ServeResource(context, xml.ToString(), "application/xml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
length = parsed;
|
||||
}
|
||||
|
||||
Server.ServeResource(context, GetPubKey(length), "text/plain", 200);
|
||||
await Server.ServeResource(context, GetPubKey(length), "text/plain", 200);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
var parsed = ParseFormEncoded(body);
|
||||
if (!parsed.TryGetValue("secret", out string secret) || string.IsNullOrWhiteSpace(secret))
|
||||
{
|
||||
Server.ServeResource(context, "missing 'secret' parameter", "text/plain", 400);
|
||||
await Server.ServeResource(context, "missing 'secret' parameter", "text/plain", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -79,17 +79,17 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
{
|
||||
int otp = GetOtp(secret.Trim());
|
||||
string otp6 = otp.ToString("D6"); // always 6 digits
|
||||
Server.ServeResource(context, otp6, "text/plain", 200);
|
||||
await Server.ServeResource(context, otp6, "text/plain", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Server.ServeResource(context, $"invalid secret: {ex.Message}", "text/plain", 400);
|
||||
await Server.ServeResource(context, $"invalid secret: {ex.Message}", "text/plain", 400);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: not found
|
||||
Server.ServeResource(context, "not found", "text/plain", 404);
|
||||
await Server.ServeResource(context, "not found", "text/plain", 404);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
|
||||
if (string.IsNullOrWhiteSpace(query) || query.Length > 255)
|
||||
{
|
||||
Server.ServeResource(context, "<error>Invalid query parameter</error>", "application/xml", 400);
|
||||
await Server.ServeResource(context, "<error>Invalid query parameter</error>", "application/xml", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -57,11 +57,11 @@ namespace WelsonJS.Launcher.ResourceTools
|
|||
HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
string responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Server.ServeResource(context, responseBody, "text/plain", (int)response.StatusCode);
|
||||
await Server.ServeResource(context, responseBody, "text/plain", (int)response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Server.ServeResource(context, $"<error>Failed to process WHOIS request. {ex.Message}</error>", "application/xml", 500);
|
||||
await Server.ServeResource(context, $"<error>Failed to process WHOIS request. {ex.Message}</error>", "application/xml", 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@
|
|||
<Compile Include="JsNative.cs" />
|
||||
<Compile Include="JsSerializer.cs" />
|
||||
<Compile Include="NativeBootstrap.cs" />
|
||||
<Compile Include="ResourceTools\ImageColorPicker.cs" />
|
||||
<Compile Include="ResourceTools\IpQuery.cs" />
|
||||
<Compile Include="ResourceTools\Settings.cs" />
|
||||
<Compile Include="ResourceTools\Completion.cs" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user