136 lines
4.4 KiB
C#
136 lines
4.4 KiB
C#
using Microsoft.Extensions.Caching.Memory;
|
|
using NetVips;
|
|
|
|
namespace lilo_stitcher_console;
|
|
|
|
public record GenerateRequest(
|
|
string CanvasRect,
|
|
double[] CropOffset,
|
|
double[] CropSize,
|
|
double OutputScale
|
|
);
|
|
|
|
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
|
|
{
|
|
Size = TileBytes,
|
|
SlidingExpiration = TimeSpan.FromMinutes(20)
|
|
});
|
|
}
|
|
|
|
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 LiloStitcher(TileLoader loader)
|
|
{
|
|
public async Task<string> CreateImageAsync(GenerateRequest req, CancellationToken ct)
|
|
{
|
|
(int rowMin, int colMin, int rows, int cols) = ParseCanvas(req.CanvasRect);
|
|
|
|
Validate(req.CropOffset, nameof(req.CropOffset));
|
|
Validate(req.CropSize, nameof(req.CropSize));
|
|
|
|
double scale = req.OutputScale;
|
|
if (scale <= 0 || scale > 1)
|
|
throw new ArgumentOutOfRangeException(nameof(req.OutputScale));
|
|
|
|
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 );
|
|
}
|
|
}
|
|
|
|
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 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 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);
|
|
|
|
string tmpPath = Path.Combine(Path.GetTempPath(), $"mosaic-{Guid.NewGuid():N}.png");
|
|
cropRect.WriteToFile(tmpPath);
|
|
|
|
return tmpPath;
|
|
}
|
|
|
|
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);
|
|
return (rowMin, colMin, rowMax - rowMin + 1, colMax - colMin + 1);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|