Initial commit

This commit is contained in:
dennisarfan 2025-07-30 07:30:00 +07:00
commit ef3b7d68fb
30 changed files with 1568 additions and 0 deletions

View file

@ -0,0 +1,53 @@
using System.Text.Json;
using StitchATon2.App.Models;
using StitchATon2.Domain;
namespace StitchATon2.App.Controllers;
public static class ImageController
{
public static async Task GenerateImage(HttpResponse response, GenerateImageDto dto, TileManager tileManager)
{
if (dto.GetErrors() is { Count: > 0 } errors)
{
response.StatusCode = 422;
response.ContentType = "text/json";
var errorBody = JsonSerializer.Serialize(errors, AppJsonSerializerContext.Default.DictionaryStringListString);
response.ContentLength = errorBody.Length;
await response.WriteAsync(errorBody);
await response.CompleteAsync();
return;
}
response.StatusCode = 200;
response.ContentType = "image/png";
await tileManager
.CreateSection(dto)
.WriteToStream(response.Body, dto.OutputScale);
await response.CompleteAsync();
}
public static async Task GenerateRandomImage(HttpResponse response, TileManager tileManager)
{
response.StatusCode = 200;
response.ContentType = "image/png";
var maxId = tileManager.Configuration.Rows * tileManager.Configuration.Columns;
var id0 = Random.Shared.Next(maxId);
var id1 = Random.Shared.Next(maxId);
var tile0 = tileManager.GetTile(id0);
var tile1 = tileManager.GetTile(id1);
var coordinatePair = $"{tile0.Coordinate}:{tile1.Coordinate}";
var section = tileManager.CreateSection(coordinatePair, 0, 0, 1, 1);
var scale = float.Clamp(480f / int.Max(section.Width, section.Height), 0.01f, 1f);
Console.WriteLine($"Generate random image for {coordinatePair} scale: {scale}");
await section.WriteToStream(response.Body, scale);
await response.CompleteAsync();
}
}

View file

@ -0,0 +1,72 @@
using System.Text.Json.Serialization;
namespace StitchATon2.App.Models;
public class GenerateImageDto
{
[JsonPropertyName("canvas_rect")]
public string? CanvasRect { get; set; }
[JsonPropertyName("crop_offset")]
public float[]? CropOffset { get; set; }
[JsonPropertyName("crop_size")]
public float[]? CropSize { get; set; }
[JsonPropertyName("output_scale")]
public float? OutputScale { get; set; }
public Dictionary<string, List<string>> GetErrors()
{
return ValidateCanvasRect()
.Concat(ValidateNumberPair(CropOffset, "crop_offset"))
.Concat(ValidateNumberPair(CropSize, "crop_size"))
.Concat(ValidateNumber(OutputScale, "output_scale"))
.GroupBy(item => item.Item1)
.ToDictionary(item => item.Key, item => item.Select(p => p.Item2).ToList());
}
private IEnumerable<(string, string)> ValidateCanvasRect()
{
if (string.IsNullOrEmpty(CanvasRect))
{
yield return ("canvas_rect", "canvas_rect is required.");
}
}
private IEnumerable<(string, string)> ValidateNumberPair(float[]? numberPair, string fieldName)
{
if (numberPair is null)
{
yield return (fieldName, $"{fieldName} is required.");
}
else if (numberPair.Length != 2)
{
yield return (fieldName, $"{fieldName} must have exactly 2 elements.");
}
else
{
foreach (var item in ValidateNumber(numberPair[0], $"{fieldName}[0]"))
yield return item;
foreach (var item in ValidateNumber(numberPair[1], $"{fieldName}[1]"))
yield return item;
}
}
private IEnumerable<(string, string)> ValidateNumber(float? number, string fieldName, double min = 0.0, double max = 1.0)
{
if (number is null)
{
yield return (fieldName, $"{fieldName} is required.");
}
else if (number < min)
{
yield return (fieldName, $"{fieldName} must be greater than or equal to {min}.");
}
else if (number > max)
{
yield return (fieldName, $"{fieldName} must be less than or equal to {max}.");
}
}
}

28
App/Program.cs Normal file
View file

@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
using StitchATon2.App.Controllers;
using StitchATon2.App.Models;
using StitchATon2.Domain;
var builder = WebApplication.CreateSlimBuilder(args);
using var tileManager = new TileManager(Configuration.Default);
builder.Services.AddSingleton(tileManager);
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});
var app = builder.Build();
app.MapPost("/api/image/generate", ImageController.GenerateImage);
app.MapGet("/api/image/generate/random", ImageController.GenerateRandomImage);
app.Run();
[JsonSourceGenerationOptions(WriteIndented = false)]
[JsonSerializable(typeof(GenerateImageDto))]
[JsonSerializable(typeof(Dictionary<string,List<string>>))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

View file

@ -0,0 +1,16 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "todos",
"applicationUrl": "http://localhost:5088",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASSET_PATH_RO": "C:\\Storage\\tiles1705"
}
}
}
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<PublishAot>true</PublishAot>
<RootNamespace>StitchATon2.App</RootNamespace>
<!-- <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>-->
<!-- <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>-->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\StitchATon2.Domain.csproj" />
</ItemGroup>
</Project>

22
App/Utils.cs Normal file
View file

@ -0,0 +1,22 @@
using StitchATon2.App.Models;
using StitchATon2.Domain;
using StitchATon2.Domain.ImageCreators;
namespace StitchATon2.App;
public static class Utils
{
public static GridSection CreateSection(this TileManager manager, GenerateImageDto dto)
=> manager.CreateSection(
dto.CanvasRect!,
dto.CropOffset![0],
dto.CropOffset![1],
dto.CropSize![0],
dto.CropSize![1]);
public static async Task WriteToStream(this GridSection section, Stream stream, float? scale)
{
var imageCreator = new ImageCreator(section);
await imageCreator.WriteToStream(stream, scale!.Value);
}
}

View file

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
App/appsettings.json Normal file
View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}