Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cee62f55d1 | |||
| 1b8f3a31b8 | |||
|
|
a9a4d3a631 | ||
|
|
dd78487b84 | ||
|
|
cbc092987d | ||
|
|
a101d84e45 | ||
| e868c152e0 | |||
| 93b41bfa94 | |||
| 52e830b459 |
15 changed files with 325 additions and 186 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,4 +1,6 @@
|
|||
bin
|
||||
obj
|
||||
.vs
|
||||
*.png
|
||||
*.png
|
||||
.idea
|
||||
/.contest
|
||||
|
|
|
|||
13
.idea/.idea.lilo-stitcher-console/.idea/.gitignore
generated
vendored
13
.idea/.idea.lilo-stitcher-console/.idea/.gitignore
generated
vendored
|
|
@ -1,13 +0,0 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/modules.xml
|
||||
/contentModel.xml
|
||||
/projectSettingsUpdater.xml
|
||||
/.idea.lilo-stitcher-console.iml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
18
LiloController.cs
Normal file
18
LiloController.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using lilos_stitcher;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace LiloStitcher;
|
||||
|
||||
[ApiController]
|
||||
[Route( "api/image" )]
|
||||
public sealed class LiloController( lilos_stitcher.LiloStitcher stitcher ) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
[Route( "generate" )]
|
||||
[Produces( "image/png" )]
|
||||
public async Task<IActionResult> Generate( [FromBody] GenerateRequest payload, CancellationToken ct )
|
||||
{
|
||||
string path = await stitcher.CreateImageAsync( payload, ct );
|
||||
return PhysicalFile(path, "image/png", enableRangeProcessing: false);
|
||||
}
|
||||
}
|
||||
189
LiloStitcher.cs
189
LiloStitcher.cs
|
|
@ -1,136 +1,137 @@
|
|||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NetVips;
|
||||
|
||||
namespace lilo_stitcher_console;
|
||||
namespace lilos_stitcher;
|
||||
|
||||
public record GenerateRequest(
|
||||
string CanvasRect,
|
||||
double[] CropOffset,
|
||||
double[] CropSize,
|
||||
double OutputScale
|
||||
public sealed record GenerateRequest(
|
||||
string Canvas_Rect,
|
||||
double[] Crop_Offset,
|
||||
double[] Crop_Size,
|
||||
double Output_Scale
|
||||
);
|
||||
|
||||
public readonly record struct PlateCoordinate(int Row, int Col)
|
||||
public readonly record struct PlateCoordinate( int Row, int Col )
|
||||
{
|
||||
public static PlateCoordinate Parse(string token)
|
||||
{
|
||||
var rowPart = new string(token.TakeWhile(char.IsLetter).ToArray()).ToUpperInvariant();
|
||||
var colPart = new string(token.SkipWhile(char.IsLetter).ToArray());
|
||||
|
||||
int row = 0;
|
||||
foreach (var character in rowPart)
|
||||
row = row * 26 + (character - 'A' + 1);
|
||||
|
||||
int.TryParse(colPart, out int col);
|
||||
return new PlateCoordinate(row, col);
|
||||
}
|
||||
}
|
||||
|
||||
public class TileCache(IMemoryCache cache)
|
||||
{
|
||||
private const long TileBytes = 720L * 720 * 3;
|
||||
|
||||
public Image? Get(string key) => cache.TryGetValue(key, out Image? img) ? img : null;
|
||||
|
||||
public void Set(string key, Image img) =>
|
||||
cache.Set(key, img, new MemoryCacheEntryOptions
|
||||
public static PlateCoordinate Parse( string token )
|
||||
{
|
||||
Size = TileBytes,
|
||||
SlidingExpiration = TimeSpan.FromMinutes(20)
|
||||
});
|
||||
if( string.IsNullOrWhiteSpace( token ) )
|
||||
throw new ArgumentException( "Empty coordinate." );
|
||||
|
||||
var rowPart = new string( token.TakeWhile( char.IsLetter ).ToArray() ).ToUpperInvariant();
|
||||
var colPart = new string( token.SkipWhile( char.IsLetter ).ToArray() );
|
||||
|
||||
int row = 0;
|
||||
foreach( char c in rowPart )
|
||||
row = row * 26 + ( c - 'A' + 1 );
|
||||
|
||||
return new PlateCoordinate( row, int.Parse( colPart ) );
|
||||
}
|
||||
}
|
||||
|
||||
public class TileLoader(TileCache cache, string assetDir)
|
||||
{
|
||||
public async Task<Image> LoadAsync(string name, CancellationToken ct)
|
||||
{
|
||||
if (cache.Get(name) is { } hit)
|
||||
return hit;
|
||||
|
||||
var image = await Task.Run(() =>
|
||||
Image.NewFromFile(Path.Combine(assetDir, $"{name}.png"), access: Enums.Access.Sequential), ct);
|
||||
|
||||
cache.Set(name, image);
|
||||
return image;
|
||||
}
|
||||
public class TileCache( IMemoryCache cache )
|
||||
{
|
||||
private const long TileBytes = 720L * 720 * 3;
|
||||
|
||||
public Image? Get( string key ) => cache.TryGetValue( key, out Image? img ) ? img : null;
|
||||
|
||||
public void Set( string key, Image img ) =>
|
||||
cache.Set( key, img, new MemoryCacheEntryOptions
|
||||
{
|
||||
Size = TileBytes,
|
||||
SlidingExpiration = TimeSpan.FromMinutes( 20 )
|
||||
} );
|
||||
}
|
||||
|
||||
public class LiloStitcher(TileLoader loader)
|
||||
public class TileLoader( TileCache cache, string assetDir )
|
||||
{
|
||||
public async Task<string> CreateImageAsync(GenerateRequest req, CancellationToken ct)
|
||||
public async Task<Image> LoadAsync( string name, CancellationToken ct )
|
||||
{
|
||||
(int rowMin, int colMin, int rows, int cols) = ParseCanvas(req.CanvasRect);
|
||||
if( cache.Get( name ) is { } hit )
|
||||
return hit;
|
||||
|
||||
Validate(req.CropOffset, nameof(req.CropOffset));
|
||||
Validate(req.CropSize, nameof(req.CropSize));
|
||||
var image = await Task.Run( () =>
|
||||
Image.NewFromFile( Path.Combine( assetDir, $"{name}.png" ), access: Enums.Access.Sequential ), ct );
|
||||
|
||||
double scale = req.OutputScale;
|
||||
if (scale <= 0 || scale > 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(req.OutputScale));
|
||||
cache.Set( name, image );
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
var tiles = new List<Image>(rows * cols);
|
||||
public class LiloStitcher( TileLoader loader )
|
||||
{
|
||||
public async Task<string> CreateImageAsync( GenerateRequest req, CancellationToken ct )
|
||||
{
|
||||
(int rowMin, int colMin, int rows, int cols) = ParseCanvas( req.Canvas_Rect );
|
||||
|
||||
Validate(req.Crop_Offset, nameof(req.Crop_Offset));
|
||||
Validate(req.Crop_Size, nameof(req.Crop_Size));
|
||||
double scale = req.Output_Scale;
|
||||
if( scale <= 0 || scale > 1 )
|
||||
throw new ArgumentOutOfRangeException( nameof( req.Output_Scale ) );
|
||||
|
||||
var tiles = new List<Image>( rows * cols );
|
||||
for( int row = 0; row < rows; row++ )
|
||||
{
|
||||
for( int col = 0; col < cols; col++ )
|
||||
{
|
||||
string id = $"{RowName( rowMin + row )}{colMin + col}";
|
||||
var tile = await loader.LoadAsync( id, ct );
|
||||
if( scale < 1 ) tile = tile.Resize( scale );
|
||||
tiles.Add( tile );
|
||||
}
|
||||
for( int col = 0; col < cols; col++ )
|
||||
{
|
||||
string id = $"{RowName( rowMin + row )}{colMin + col}";
|
||||
var tile = await loader.LoadAsync( id, ct );
|
||||
if( scale < 1 ) tile = tile.Resize( scale );
|
||||
tiles.Add( tile );
|
||||
}
|
||||
}
|
||||
|
||||
var mosaic = Image.Arrayjoin(tiles.ToArray(), across: cols);
|
||||
var mosaic = Image.Arrayjoin( tiles.ToArray(), across: cols );
|
||||
|
||||
int offsetX = (int)Math.Truncate(req.CropOffset[0] * mosaic.Width);
|
||||
int offsetY = (int)Math.Truncate(req.CropOffset[1] * mosaic.Height);
|
||||
int offsetX = (int)Math.Truncate( req.Crop_Offset[0] * mosaic.Width );
|
||||
int offsetY = (int)Math.Truncate( req.Crop_Offset[1] * mosaic.Height );
|
||||
|
||||
int restWidth = mosaic.Width - offsetX;
|
||||
int restHeight = mosaic.Height - offsetY;
|
||||
|
||||
int cropWidth = Math.Max(1, (int)Math.Truncate(req.CropSize[0] * restWidth));
|
||||
int cropHeight = Math.Max(1, (int)Math.Truncate(req.CropSize[1] * restHeight));
|
||||
int cropWidth = Math.Max( 1, (int)Math.Truncate( req.Crop_Size[0] * restWidth ) );
|
||||
int cropHeight = Math.Max( 1, (int)Math.Truncate( req.Crop_Size[1] * restHeight ) );
|
||||
|
||||
int cropX = (int)Math.Truncate(offsetX / 2.0 + (restWidth - cropWidth) / 2.0);
|
||||
int cropY = (int)Math.Truncate(offsetY / 2.0 + (restHeight - cropHeight) / 2.0);
|
||||
int cropX = (int)Math.Truncate( offsetX / 2.0 + ( restWidth - cropWidth ) / 2.0 );
|
||||
int cropY = (int)Math.Truncate( offsetY / 2.0 + ( restHeight - cropHeight ) / 2.0 );
|
||||
|
||||
var cropRect = mosaic.Crop(cropX, cropY, cropWidth, cropHeight);
|
||||
var cropped = mosaic.Crop( cropX, cropY, cropWidth, cropHeight );
|
||||
|
||||
string tmpPath = Path.Combine(Path.GetTempPath(), $"mosaic-{Guid.NewGuid():N}.png");
|
||||
cropRect.WriteToFile(tmpPath);
|
||||
|
||||
return tmpPath;
|
||||
string path = Path.Combine(Path.GetTempPath(), $"mosaic-{Guid.NewGuid():N}.png");
|
||||
cropped.WriteToFile(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static (int rowMin, int colMin, int rows, int cols) ParseCanvas(string rect)
|
||||
private static (int rowMin, int colMin, int rows, int cols) ParseCanvas( string rect )
|
||||
{
|
||||
var parts = rect.ToUpperInvariant().Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
var part1 = PlateCoordinate.Parse(parts[0]);
|
||||
var part2 = PlateCoordinate.Parse(parts[1]);
|
||||
int rowMin = Math.Min(part1.Row, part2.Row);
|
||||
int rowMax = Math.Max(part1.Row, part2.Row);
|
||||
int colMin = Math.Min(part1.Col, part2.Col);
|
||||
int colMax = Math.Max(part1.Col, part2.Col);
|
||||
var parts = rect.ToUpperInvariant().Split( ':', StringSplitOptions.RemoveEmptyEntries );
|
||||
var part1 = PlateCoordinate.Parse( parts[0] );
|
||||
var part2 = PlateCoordinate.Parse( parts[1] );
|
||||
int rowMin = Math.Min( part1.Row, part2.Row );
|
||||
int rowMax = Math.Max( part1.Row, part2.Row );
|
||||
int colMin = Math.Min( part1.Col, part2.Col );
|
||||
int colMax = Math.Max( part1.Col, part2.Col );
|
||||
return (rowMin, colMin, rowMax - rowMin + 1, colMax - colMin + 1);
|
||||
}
|
||||
|
||||
private static void Validate(double[] arr, string name)
|
||||
private static void Validate( double[] arr, string name )
|
||||
{
|
||||
if (arr is null || arr.Length < 2)
|
||||
throw new ArgumentException($"{name} length");
|
||||
if (arr.Any(x => x < 0 || x > 1))
|
||||
throw new ArgumentOutOfRangeException(name);
|
||||
if( arr is null || arr.Length < 2 )
|
||||
throw new ArgumentException( $"{name} length" );
|
||||
if( arr.Any( x => x < 0 || x > 1 ) )
|
||||
throw new ArgumentOutOfRangeException( name );
|
||||
}
|
||||
|
||||
static string RowName(int row)
|
||||
static string RowName( int row )
|
||||
{
|
||||
var stringBuilder = new System.Text.StringBuilder();
|
||||
while (row > 0)
|
||||
{
|
||||
row--;
|
||||
stringBuilder.Insert(0, (char)('A' + row % 26));
|
||||
row /= 26;
|
||||
}
|
||||
return stringBuilder.ToString();
|
||||
var stringBuilder = new System.Text.StringBuilder();
|
||||
while( row > 0 )
|
||||
{
|
||||
row--;
|
||||
stringBuilder.Insert( 0, (char)( 'A' + row % 26 ) );
|
||||
row /= 26;
|
||||
}
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
74
Program.cs
74
Program.cs
|
|
@ -1,68 +1,20 @@
|
|||
using lilo_stitcher_console;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using lilos_stitcher;
|
||||
|
||||
namespace LiloStitcher;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static async Task<int> Main( string[] args )
|
||||
{
|
||||
try
|
||||
{
|
||||
NetVips.NetVips.Init();
|
||||
NetVips.NetVips.Concurrency = 3;
|
||||
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
var begin = sw.ElapsedMilliseconds;
|
||||
var opt = new Options
|
||||
{
|
||||
CanvasRect = "A1:AE55",
|
||||
CropOffset = new[] { 0.4, 0.4 },
|
||||
CropSize = new[] { 0.8, 0.8 },
|
||||
OutputScale = 0.5,
|
||||
OutputPath = "stitched.png"
|
||||
};
|
||||
NetVips.NetVips.Init();
|
||||
NetVips.NetVips.Concurrency = 3;
|
||||
|
||||
string tileFilePath = "../tiles1705"; // should later be directed to the read-only `ASSET_PATH_RO` environment variable
|
||||
string assetDir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), tileFilePath));
|
||||
builder.Services.AddControllers().AddJsonOptions(o => o.JsonSerializerOptions.PropertyNameCaseInsensitive = true);
|
||||
|
||||
using var memCache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 128L * 1024 * 1024 });
|
||||
var tileCache = new TileCache(memCache);
|
||||
var loader = new TileLoader(tileCache, assetDir);
|
||||
var stitcher = new lilo_stitcher_console.LiloStitcher(loader);
|
||||
builder.Services.AddMemoryCache(o => o.SizeLimit = 128L * 1024 * 1024);
|
||||
|
||||
var req = new GenerateRequest(
|
||||
opt.CanvasRect!,
|
||||
opt.CropOffset!,
|
||||
opt.CropSize!,
|
||||
opt.OutputScale
|
||||
);
|
||||
string assetDir = Environment.GetEnvironmentVariable("ASSET_PATH_RO") ?? throw new InvalidOperationException("dir not found");
|
||||
|
||||
Console.WriteLine("Stitching...");
|
||||
var png = await stitcher.CreateImageAsync(req, CancellationToken.None);
|
||||
File.Move( png, opt.OutputPath!, overwrite: true );
|
||||
|
||||
long bytes = new FileInfo( opt.OutputPath! ).Length;
|
||||
Console.WriteLine($"Done. Wrote {opt.OutputPath} ({bytes / 1024.0:F1} KB)");
|
||||
builder.Services.AddSingleton<TileCache>();
|
||||
builder.Services.AddSingleton(provider => new TileLoader(provider.GetRequiredService<TileCache>(), assetDir));
|
||||
builder.Services.AddSingleton<lilos_stitcher.LiloStitcher>();
|
||||
|
||||
Console.WriteLine(sw.ElapsedMilliseconds - begin);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
Console.Error.WriteLine( "ERROR: " + ex.Message );
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private struct Options
|
||||
{
|
||||
public string? CanvasRect { get; set; }
|
||||
public double[]? CropOffset { get; set; }
|
||||
public double[]? CropSize { get; set; }
|
||||
public double OutputScale { get; set; }
|
||||
public string? OutputPath { get; set; }
|
||||
}
|
||||
}
|
||||
var app = builder.Build();
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
23
Properties/launchSettings.json
Normal file
23
Properties/launchSettings.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5243",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7121;http://localhost:5243",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
README.md
Normal file
44
README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Lilo Stitcher API
|
||||
|
||||
An ASP.NET Core Web API for stitching, cropping, and scaling tiled images (720×720).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* .NET 9.0 SDK
|
||||
* `ASSET_PATH_RO` environment variable containing the path to tiles directory (1,705 PNGs named A1.png…AE55.png)
|
||||
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
cd lilo-stitcher
|
||||
dotnet clean
|
||||
dotnet run
|
||||
```
|
||||
|
||||
By default, the API listens on `http://localhost:5243` and `https://localhost:7121` (see [`launchSettings.json`](https://null.formulatrix.dev/fikribahru/lilo-stitcher/src/branch/main/Properties/launchSettings.json)).
|
||||
|
||||
## Usage
|
||||
|
||||
**Endpoint:** `POST /api/image/generate`
|
||||
|
||||
**Request Body (JSON):**
|
||||
|
||||
```json
|
||||
{
|
||||
"canvas_rect": "A1:H12",
|
||||
"crop_offset": [0.25, 0.25],
|
||||
"crop_size": [0.5, 0.5],
|
||||
"output_scale": 1.0
|
||||
}
|
||||
```
|
||||
|
||||
**Example (using `curl`):**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5243/api/image/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-o output.png \
|
||||
-d '{"canvas_rect":"A1:H12","crop_offset":[0.25,0.25],"crop_size":[0.5,0.5],"output_scale":1.0}'
|
||||
```
|
||||
|
||||
The API will return a `image/png` containing the stitched, cropped, and scaled result.
|
||||
8
appsettings.Development.json
Normal file
8
appsettings.Development.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
appsettings.json
Normal file
9
appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
@ -1,18 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>lilo_stitcher_console</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>lilos_stitcher</RootNamespace>
|
||||
<LangVersion>preview</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0-preview.6.25358.103" />
|
||||
<PackageReference Include="NetVips" Version="3.1.0" />
|
||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
|
||||
<PackageReference Include="NetVips.Native.linux-arm64" Version="8.17.1" />
|
||||
<!-- <PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />-->
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
6
lilos-stitcher.http
Normal file
6
lilos-stitcher.http
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
@lilos_stitcher_HostAddress = http://localhost:5243
|
||||
|
||||
GET {{lilos_stitcher_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
|
@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "lilo-stitcher-console", "lilo-stitcher-console.csproj", "{2A1F81C9-D10F-1AE9-CA5C-0714270F48C1}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "lilos-stitcher", "lilos-stitcher.csproj", "{8FDFFCBC-9C8E-94D5-A96D-606027D71B44}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
@ -10,15 +10,15 @@ Global
|
|||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2A1F81C9-D10F-1AE9-CA5C-0714270F48C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2A1F81C9-D10F-1AE9-CA5C-0714270F48C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2A1F81C9-D10F-1AE9-CA5C-0714270F48C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2A1F81C9-D10F-1AE9-CA5C-0714270F48C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8FDFFCBC-9C8E-94D5-A96D-606027D71B44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8FDFFCBC-9C8E-94D5-A96D-606027D71B44}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8FDFFCBC-9C8E-94D5-A96D-606027D71B44}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8FDFFCBC-9C8E-94D5-A96D-606027D71B44}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C7CD65D7-8035-4FC2-A584-06B21F732FF7}
|
||||
SolutionGuid = {8DD20971-C4B3-41BF-8263-D31D5D81631F}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
92
mise.toml
Normal file
92
mise.toml
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Quick Guide
|
||||
# - install mise
|
||||
# - download the asset and extract them to ASSET_PATH_RO
|
||||
# - mise trust mise.toml
|
||||
# - mise run verify-asset
|
||||
# - require hashdeep
|
||||
# `hashdeep` package in debian
|
||||
# or `md5deep` package in fedora
|
||||
# or uncomment `tools."http:hashdeep"` below in windows
|
||||
# - mise run serve
|
||||
# - mise run arrange
|
||||
# - mise run action
|
||||
# - mise run assert
|
||||
# - mise run bench
|
||||
|
||||
[env]
|
||||
ASSET_PATH_RO = "{{ [xdg_cache_home, 'stitch-a-ton', 'asset'] | join_path }}"
|
||||
CONTEST_HOST = "http://localhost:7007"
|
||||
CONTEST_API = "/api/image/generate"
|
||||
CONTEST_OUTPUT = "{{ [cwd, '.contest'] | join_path }}"
|
||||
DOTNET_ENVIRONMENT = "Production"
|
||||
ANSWER_COMMIT_HASH = "89a07b40bf0414212c96945671a012035d375a25"
|
||||
|
||||
[tools]
|
||||
dotnet = "9"
|
||||
xh = "latest"
|
||||
uv = "latest"
|
||||
k6 = "latest"
|
||||
|
||||
# uncomment these if you're on windows
|
||||
#[tools."http:hashdeep"]
|
||||
#version = "4.4"
|
||||
|
||||
#[tools."http:hashdeep".platforms]
|
||||
#windows-x64 = {url = "https://github.com/jessek/hashdeep/releases/download/v4.4/md5deep-4.4.zip"}
|
||||
|
||||
[tasks.setup]
|
||||
run = '''
|
||||
{% if env.CONTEST_OUTPUT is not exists %}
|
||||
mkdir .contest
|
||||
{% endif %}
|
||||
'''
|
||||
|
||||
[tasks.verify-asset]
|
||||
dir = "{{ env.ASSET_PATH_RO }}"
|
||||
run = '''
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/asset.txt -o ../asset.txt
|
||||
hashdeep -arbvk ../asset.txt .
|
||||
'''
|
||||
|
||||
[tasks.arrange]
|
||||
depends = ['setup']
|
||||
dir = "{{ env.CONTEST_OUTPUT }}"
|
||||
outputs = ['answer.json', 'action.py', 'assert.py', 'bench.js', 'fuzzy.json']
|
||||
run = '''
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/answer.json -o answer.json
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/action.py -o action.py
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/assert.py -o assert.py
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/bench.js -o bench.js
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/fuzzy.json -o fuzzy.json
|
||||
'''
|
||||
|
||||
[tasks.serve]
|
||||
run = "dotnet run -c Release --no-launch-profile --urls {{env.CONTEST_HOST}}"
|
||||
|
||||
[tasks.quick]
|
||||
depends = ['arrange']
|
||||
dir = "{{ env.CONTEST_OUTPUT }}"
|
||||
run = '''
|
||||
xh post {{env.CONTEST_HOST}}{{env.CONTEST_ENDPOINT}} canvas_rect=A1:H12 crop_offset:=[0,0] crop_size:=[1,1] output_scale:=0.25 -o quick.png
|
||||
'''
|
||||
|
||||
[tasks.action]
|
||||
depends = ['arrange']
|
||||
dir = "{{ env.CONTEST_OUTPUT }}"
|
||||
run = '''
|
||||
uv run --no-config --script {{ [env.CONTEST_OUTPUT, 'action.py'] | join_path }}
|
||||
'''
|
||||
|
||||
[tasks.assert]
|
||||
depends = ['arrange']
|
||||
dir = "{{ env.CONTEST_OUTPUT }}"
|
||||
run = '''
|
||||
uvx --no-config --with-requirements assert.py pytest assert.py
|
||||
'''
|
||||
|
||||
[tasks.bench]
|
||||
depends = ['arrange']
|
||||
dir = "{{ env.CONTEST_OUTPUT }}"
|
||||
run = '''
|
||||
k6 run -e TARGET_URL="{{ env.CONTEST_HOST }}{{ env.CONTEST_API }}" bench.js
|
||||
'''
|
||||
Loading…
Add table
Add a link
Reference in a new issue