first commit: push the working console project

This commit is contained in:
Bahru 2025-07-19 17:44:08 +07:00
commit efaaee3b17
5 changed files with 246 additions and 0 deletions

148
LiloStitcher.cs Normal file
View file

@ -0,0 +1,148 @@
using Microsoft.Extensions.Caching.Memory;
using SkiaSharp;
namespace LiloStitcher;
public record GenerateRequest(
string CanvasRect,
double[] CropOffset,
double[] CropSize,
double OutputScale
);
public 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 c in rowPart )
{
row = row * 26 + ( c - 'A' + 1 );
}
int.TryParse( colPart, out int col );
return new PlateCoordinate( row, col );
}
}
public class TileCache( IMemoryCache cache )
{
private const long TileBytes = 720L * 720 * 4;
public SKBitmap? Get( string key ) => cache.TryGetValue( key, out SKBitmap? bmp ) ? bmp : null;
public void Set( string key, SKBitmap bmp ) =>
cache.Set( key, bmp, new MemoryCacheEntryOptions
{
Size = TileBytes,
SlidingExpiration = TimeSpan.FromMinutes( 20 )
} );
}
public class TileLoader( TileCache cache, string assetDir )
{
public async Task<SKBitmap> LoadAsync( string name, CancellationToken ct )
{
if( cache.Get( name ) is { } hit ) return hit;
var path = Path.Combine( assetDir, $"{name}.png" );
var bytes = await File.ReadAllBytesAsync( path, ct ).ConfigureAwait( false );
using var data = SKData.CreateCopy( bytes );
var bmp = SKBitmap.Decode( data );
cache.Set( name, bmp );
return bmp;
}
}
public class LiloStitcher( TileLoader loader )
{
private const int TileSize = 720;
public async Task<byte[]> CreateImageAsync( GenerateRequest req, CancellationToken ct )
{
var parts = req.CanvasRect.ToUpperInvariant().Split( ':', StringSplitOptions.TrimEntries | 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 );
int rows = rowMax - rowMin + 1;
int cols = colMax - colMin + 1;
var names = Enumerable.Range( rowMin, rows )
.SelectMany( r => Enumerable.Range( colMin, cols ).Select( c => $"{RowName( r )}{c}" ) )
.ToArray();
var bitmaps = await Task.WhenAll( names.Select( n => loader.LoadAsync( n, ct ) ) ).ConfigureAwait( false );
var fullInfo = new SKImageInfo( cols * TileSize, rows * TileSize );
using var surface = SKSurface.Create( fullInfo );
var canvas = surface.Canvas;
int idx = 0;
for( int row = 0; row < rows; row++ )
for( int column = 0; column < cols; column++, idx++ )
canvas.DrawBitmap( bitmaps[idx], column * TileSize, row * TileSize );
using var full = new SKBitmap( fullInfo );
surface.ReadPixels( fullInfo, full.GetPixels(), full.RowBytes, 0, 0 );
ValidateFraction( req.CropOffset, 2, nameof( req.CropOffset ), inclusiveUpper: true );
ValidateFraction( req.CropSize, 2, nameof( req.CropSize ), inclusiveUpper: true );
int offsetX = (int)( req.CropOffset[0] * fullInfo.Width );
int offsetY = (int)( req.CropOffset[1] * fullInfo.Height );
int cropWidth = (int)( req.CropSize[0] * fullInfo.Width );
int cropHeight = (int)( req.CropSize[1] * fullInfo.Height );
if( offsetX + cropWidth > fullInfo.Width ) cropWidth = fullInfo.Width - offsetX;
if( offsetY + cropHeight > fullInfo.Height ) cropHeight = fullInfo.Height - offsetY;
var cropRect = new SKRectI( offsetX, offsetY, offsetX + cropWidth, offsetY + cropHeight );
using var crop = new SKBitmap( cropWidth, cropHeight );
full.ExtractSubset( crop, cropRect );
double scale = Math.Clamp( req.OutputScale, 0.0, 1.0 );
int truncatedWidth = Math.Max( 1, (int)Math.Round( cropWidth * scale ) );
int truncatedHeight = Math.Max( 1, (int)Math.Round( cropHeight * scale ) );
using var finalBmp = scale < 1.0
? crop.Resize( new SKSizeI( truncatedWidth, truncatedHeight ), new SKSamplingOptions( SKFilterMode.Linear ) )
: crop;
using var image = SKImage.FromBitmap( finalBmp );
using var data = image.Encode( SKEncodedImageFormat.Png, 100 );
return data.ToArray();
static void ValidateFraction( double[] arr, int len, string name, bool inclusiveUpper )
{
for( int i = 0; i < len; i++ )
{
double v = arr[i];
double upper = inclusiveUpper ? 1.0 : 1.0 - double.Epsilon;
}
}
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();
}
}
}