V18 / 8d896b8a86e5
WallpaperSupport.cs
Lilith/Core/Addons/WallpaperSupport.cs
Changed in this versionCS
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
namespace Lilith.Core.Addons;
public sealed record AddonContext(string DataDirectory);
public interface IAddonContextAware { void Configure(AddonContext context); }
/// <summary>Small documented host primitives for crafted Windows wallpaper tools.</summary>
public static class WallpaperSupport
{
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string,DateTimeOffset> Evidence=new(StringComparer.Ordinal);
[DllImport("user32.dll",CharSet=CharSet.Unicode,SetLastError=true,EntryPoint="SystemParametersInfoW")]
private static extern bool Read(uint action,uint parameter,StringBuilder value,uint flags);
[DllImport("user32.dll",CharSet=CharSet.Unicode,SetLastError=true,EntryPoint="SystemParametersInfoW")]
private static extern bool Write(uint action,uint parameter,string value,uint flags);
public static string GetCurrent()
{
if(!OperatingSystem.IsWindows())throw new PlatformNotSupportedException("Windows wallpaper only.");
var value=new StringBuilder(32768);if(!Read(0x73,(uint)value.Capacity,value,0))throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());return value.ToString();
}
public static void SetCurrent(string path)
{
if(!OperatingSystem.IsWindows())throw new PlatformNotSupportedException("Windows wallpaper only.");
if(path.Length!=0&&!File.Exists(path))throw new FileNotFoundException("Wallpaper file missing.",path);
if(!Write(0x14,0,path,3))throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
if(!string.Equals(GetCurrent(),path,StringComparison.OrdinalIgnoreCase))throw new IOException("Windows did not confirm the requested wallpaper path.");
}
public static Task<string> SearchAsync(string query,int count=5,CancellationToken cancellationToken=default)=>
Environment.GetEnvironmentVariable("LILITH_IMAGE_SEARCH_SOURCE")=="commons"?SearchCommonsAsync(query,count,cancellationToken):SearchNasaAsync(query,count,cancellationToken);
private static async Task<string> SearchNasaAsync(string query,int count,CancellationToken ct)
{
if(query.Length is <2 or >200||count is <2 or >10)throw new ArgumentException("Use a short image query and 2–10 candidates.");
using var http=Client();
string searchQuery=System.Text.RegularExpressions.Regex.Replace(query,@"\b(wallpapers?|desktop|background|images?|pictures?)\b","",System.Text.RegularExpressions.RegexOptions.IgnoreCase).Trim();
searchQuery=System.Text.RegularExpressions.Regex.Replace(searchQuery,@"\s+"," ");
if(searchQuery.Length<2)throw new ArgumentException("Specify the image subject, such as a landscape or location.");
async Task<JsonDocument> GetJson(string url)
{
var uri=new Uri(url);if(uri.Scheme!="https"||uri.Host is not("images-api.nasa.gov" or "images-assets.nasa.gov")||!uri.IsDefaultPort)throw new InvalidDataException("Unexpected NASA metadata address.");
using var response=await http.GetAsync(uri,HttpCompletionOption.ResponseHeadersRead,ct);response.EnsureSuccessStatusCode();
using var stream=await response.Content.ReadAsStreamAsync(ct);using var body=new MemoryStream();byte[] block=new byte[8192];int read;
while((read=await stream.ReadAsync(block,ct))>0){if(body.Length+read>2000000)throw new InvalidDataException("Image metadata exceeds its bound.");body.Write(block,0,read);}
var data=JsonDocument.Parse(body.ToArray());if(data.RootElement.ValueKind==JsonValueKind.String){string nested=data.RootElement.GetString()!;data.Dispose();return JsonDocument.Parse(nested);}return data;
}
using var search=await GetJson("https://images-api.nasa.gov/search?media_type=image&page_size=30&q="+Uri.EscapeDataString(searchQuery));
List<object> candidates=[];List<string> failures=[];
foreach(var item in search.RootElement.GetProperty("collection").GetProperty("items").EnumerateArray().Take(20))
{
ct.ThrowIfCancellationRequested();
try
{
var data=item.GetProperty("data")[0];string id=data.GetProperty("nasa_id").GetString()!;
using var manifest=await GetJson("https://images-api.nasa.gov/asset/"+Uri.EscapeDataString(id));
var assets=manifest.RootElement.GetProperty("collection").GetProperty("items").EnumerateArray().Select(a=>a.GetProperty("href").GetString()!).Select(u=>u.StartsWith("http://images-assets.nasa.gov/",StringComparison.Ordinal)?"https://"+u[7..]:u).ToArray();
string? image=assets.FirstOrDefault(u=>u.Contains("~orig.",StringComparison.OrdinalIgnoreCase)&&Path.GetExtension(new Uri(u).AbsolutePath).ToLowerInvariant() is ".jpg" or ".jpeg" or ".png");
string? meta=assets.FirstOrDefault(u=>u.EndsWith("/metadata.json",StringComparison.Ordinal));if(image is null||meta is null)continue;
using var metadata=await GetJson(meta);
if(!metadata.RootElement.TryGetProperty("File:ImageWidth",out var w)||!metadata.RootElement.TryGetProperty("File:ImageHeight",out var h))continue;
int width=int.Parse(w.ToString()),height=int.Parse(h.ToString());if(width<1920||height<1080||width<=height)continue;
if(!ValidImageUrl(image))continue;
using var headRequest=new HttpRequestMessage(HttpMethod.Head,image);using var head=await http.SendAsync(headRequest,HttpCompletionOption.ResponseHeadersRead,ct);head.EnsureSuccessStatusCode();
if(head.Content.Headers.ContentLength>20000000||head.Content.Headers.ContentType?.MediaType is not("image/jpeg" or "image/png"))continue;
Evidence[image]=DateTimeOffset.UtcNow.AddMinutes(10);
candidates.Add(new{title=data.GetProperty("title").GetString(),download_url=image,source_url="https://images.nasa.gov/details/"+Uri.EscapeDataString(id),width,height,bytes=head.Content.Headers.ContentLength,provider="NASA"});if(candidates.Count>=count)break;
}
catch(Exception ex) when(ex is HttpRequestException or JsonException or InvalidDataException or FormatException or KeyNotFoundException){if(failures.Count<5)failures.Add(ex.Message);}
}
return JsonSerializer.Serialize(new{status=candidates.Count>0?"completed":"unavailable",query,search_query=searchQuery,requested=count,candidates,failures,provider="NASA"});
}
private static async Task<string> SearchCommonsAsync(string query,int count,CancellationToken cancellationToken)
{
if(query.Length is <2 or >200||count is <2 or >10)throw new ArgumentException("Use a short image query and 2–10 candidates.");
using var http=Client();
string uri="https://commons.wikimedia.org/w/api.php?action=query&format=json&generator=search&gsrnamespace=6&gsrlimit=30&gsrsearch="+Uri.EscapeDataString(query+" filetype:bitmap")+"&prop=imageinfo&iiprop=url|size|mime";
using var response=await http.GetAsync(uri,HttpCompletionOption.ResponseHeadersRead,cancellationToken);response.EnsureSuccessStatusCode();
if(response.Content.Headers.ContentLength>2000000)throw new InvalidDataException("Image search response exceeds its bound.");
using var stream=await response.Content.ReadAsStreamAsync(cancellationToken);using var body=new MemoryStream();byte[] block=new byte[8192];int amount;
while((amount=await stream.ReadAsync(block,cancellationToken))>0){if(body.Length+amount>2000000)throw new InvalidDataException("Image search response exceeds its bound.");body.Write(block,0,amount);}
using var json=JsonDocument.Parse(body.ToArray());
foreach(var old in Evidence.Where(e=>e.Value<DateTimeOffset.UtcNow))Evidence.TryRemove(old.Key,out _);
List<object> candidates=[];
if(json.RootElement.TryGetProperty("query",out var data)&&data.TryGetProperty("pages",out var pages))foreach(var page in pages.EnumerateObject())
{
if(!page.Value.TryGetProperty("imageinfo",out var infos))continue;
var i=infos[0];int width=i.GetProperty("width").GetInt32(),height=i.GetProperty("height").GetInt32();string mime=i.GetProperty("mime").GetString()??"";
string url=i.GetProperty("url").GetString()??"";
if(width<1920||height<1080||width<=height||mime is not("image/jpeg" or "image/png")||i.GetProperty("size").GetInt64()>20000000||!ValidImageUrl(url))continue;
Evidence[url]=DateTimeOffset.UtcNow.AddMinutes(10);
candidates.Add(new{title=page.Value.GetProperty("title").GetString(),download_url=url,source_url=i.GetProperty("descriptionurl").GetString(),width,height,mime});if(candidates.Count>=count)break;
}
return JsonSerializer.Serialize(new{status=candidates.Count>0?"completed":"unavailable",query,requested=count,candidates});
}
public static async Task<string> DownloadAsync(string url,string directory,CancellationToken cancellationToken=default)
{
if(!ValidImageUrl(url))throw new ArgumentException("Only verified HTTPS image catalog hosts are supported.");
if(!Evidence.TryGetValue(url,out var expires)||expires<DateTimeOffset.UtcNow)throw new InvalidOperationException("The image URL was not returned by a recent actual image search.");
Directory.CreateDirectory(directory);
using var http=Client();using var response=await http.GetAsync(url,HttpCompletionOption.ResponseHeadersRead,cancellationToken);response.EnsureSuccessStatusCode();
string? mime=response.Content.Headers.ContentType?.MediaType;
if(mime is not("image/jpeg" or "image/png")||response.Content.Headers.ContentLength>20000000)throw new InvalidDataException("Unsupported or oversized image.");
using var input=await response.Content.ReadAsStreamAsync(cancellationToken);using var bytes=new MemoryStream();byte[] buffer=new byte[8192];int read;
while((read=await input.ReadAsync(buffer,cancellationToken))>0){if(bytes.Length+read>20000000)throw new InvalidDataException("Image exceeds 20 MB.");await bytes.WriteAsync(buffer.AsMemory(0,read),cancellationToken);}
byte[] content=bytes.ToArray();bool png=content.Length>8&&content.AsSpan(0,8).SequenceEqual(new byte[]{137,80,78,71,13,10,26,10});bool jpg=content.Length>3&&content[0]==255&&content[1]==216&&content[2]==255;
if(!(mime=="image/png"&&png||mime=="image/jpeg"&&jpg))throw new InvalidDataException("Image signature mismatches content type.");
var dimensions=Dimensions(content);if(dimensions.Width<1920||dimensions.Height<1080||dimensions.Width<=dimensions.Height)throw new InvalidDataException("Downloaded image is not a high-resolution landscape.");
string file=Path.Combine(directory,Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(content))+(png?".png":".jpg"));await File.WriteAllBytesAsync(file,content,cancellationToken);return file;
}
private static bool ValidImageUrl(string url)=>Uri.TryCreate(url,UriKind.Absolute,out var uri)&&uri.Scheme=="https"&&uri.Host is "upload.wikimedia.org" or "images-assets.nasa.gov"&&uri.IsDefaultPort&&uri.UserInfo.Length==0;
internal static (int Width,int Height) Dimensions(byte[] bytes)
{
if(bytes.Length>=24&&bytes[0]==137&&bytes[1]==80)return(System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(16,4)),System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(20,4)));
if(bytes.Length>4&&bytes[0]==255&&bytes[1]==216)
{
int offset=2;
while(offset+4<bytes.Length)
{
if(bytes[offset++]!=255)continue;while(offset<bytes.Length&&bytes[offset]==255)offset++;if(offset>=bytes.Length)break;int marker=bytes[offset++];
if(marker is 216 or 217 or 1||marker is >=208 and <=215)continue;
if(offset+2>bytes.Length)break;int length=(bytes[offset]<<8)|bytes[offset+1];if(length<2||offset+length>bytes.Length)break;
if(marker is >=192 and <=207&&marker is not(196 or 200 or 204)&&length>=7)return((bytes[offset+5]<<8)|bytes[offset+6],(bytes[offset+3]<<8)|bytes[offset+4]);
offset+=length;
}
}
throw new InvalidDataException("Image dimensions could not be verified.");
}
private static HttpClient Client(){var client=new HttpClient(new HttpClientHandler{AllowAutoRedirect=false}){Timeout=TimeSpan.FromSeconds(45),MaxResponseContentBufferSize=20000000};client.DefaultRequestHeaders.UserAgent.ParseAdd("LilithAddonDemo/18.0 (local desktop research)");return client;}
}