lilo-stitcher/LiloStitcher.cs

140 lines
4.8 KiB
C#
Raw Normal View History

using Microsoft.Extensions.Caching.Memory;
using NetVips;
2025-07-31 23:56:15 +07:00
namespace LiloStitcher;
2025-07-31 23:56:15 +07:00
public sealed record GenerateRequest(
string Canvas_Rect,
double[] Crop_Offset,
double[] Crop_Size,
double Output_Scale
);
2025-07-31 23:56:15 +07:00
public readonly record struct PlateCoordinate( int Row, int Col )
{
2025-07-31 23:56:15 +07:00
public static PlateCoordinate Parse( string token )
{
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 ) );
}
}
2025-07-31 23:56:15 +07:00
public class TileCache( IMemoryCache cache )
{
2025-07-31 23:56:15 +07:00
private const long TileBytes = 720L * 720 * 3;
2025-07-31 23:56:15 +07:00
public Image? Get( string key ) => cache.TryGetValue( key, out Image? img ) ? img : null;
2025-07-31 23:56:15 +07:00
public void Set( string key, Image img ) =>
cache.Set( key, img, new MemoryCacheEntryOptions
{
Size = TileBytes,
SlidingExpiration = TimeSpan.FromMinutes( 20 )
} );
}
2025-07-31 23:56:15 +07:00
public class TileLoader( TileCache cache, string assetDir )
{
2025-07-31 23:56:15 +07:00
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;
}
}
2025-07-31 23:56:15 +07:00
public class LiloStitcher( TileLoader loader )
{
2025-07-31 23:56:15 +07:00
public async Task<byte[]> CreateImageAsync( GenerateRequest req, CancellationToken ct )
{
2025-07-31 23:56:15 +07:00
(int rowMin, int colMin, int rows, int cols) = ParseCanvas( req.Canvas_Rect );
2025-07-31 23:56:15 +07:00
double scale = req.Output_Scale;
if( scale <= 0 || scale > 1 )
throw new ArgumentOutOfRangeException( nameof( req.Output_Scale ) );
2025-07-31 23:56:15 +07:00
var tiles = new List<Image>( rows * cols );
for( int row = 0; row < rows; row++ )
{
2025-07-31 23:56:15 +07:00
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 );
}
}
2025-07-31 23:56:15 +07:00
var mosaic = Image.Arrayjoin( tiles.ToArray(), across: cols );
2025-07-31 23:56:15 +07:00
int offsetX = (int)Math.Truncate( req.Output_Scale * mosaic.Width );
int offsetY = (int)Math.Truncate( req.Output_Scale * mosaic.Height );
int restWidth = mosaic.Width - offsetX;
int restHeight = mosaic.Height - offsetY;
2025-07-31 23:56:15 +07:00
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 ) );
2025-07-31 23:56:15 +07:00
int cropX = (int)Math.Truncate( offsetX / 2.0 + ( restWidth - cropWidth ) / 2.0 );
int cropY = (int)Math.Truncate( offsetY / 2.0 + ( restHeight - cropHeight ) / 2.0 );
2025-07-31 23:56:15 +07:00
var cropped = mosaic.Crop( cropX, cropY, cropWidth, cropHeight );
2025-07-31 23:56:15 +07:00
var finalImg = scale < 1.0 ? cropped.Resize( scale ) : cropped;
2025-07-31 23:56:15 +07:00
string tmp = Path.GetTempFileName() + ".png";
finalImg.WriteToFile( tmp );
byte[] result = await File.ReadAllBytesAsync( tmp, ct );
File.Delete( tmp );
return result;
}
2025-07-31 23:56:15 +07:00
private static (int rowMin, int colMin, int rows, int cols) ParseCanvas( string rect )
{
2025-07-31 23:56:15 +07:00
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);
}
2025-07-31 23:56:15 +07:00
private static void Validate( double[] arr, string name )
{
2025-07-31 23:56:15 +07:00
if( arr is null || arr.Length < 2 )
throw new ArgumentException( $"{name} length" );
if( arr.Any( x => x < 0 || x > 1 ) )
throw new ArgumentOutOfRangeException( name );
}
2025-07-31 23:56:15 +07:00
static string RowName( int row )
{
2025-07-31 23:56:15 +07:00
var stringBuilder = new System.Text.StringBuilder();
while( row > 0 )
{
row--;
stringBuilder.Insert( 0, (char)( 'A' + row % 26 ) );
row /= 26;
}
return stringBuilder.ToString();
}
}