first commit: push the working console project
This commit is contained in:
commit
efaaee3b17
5 changed files with 246 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
bin
|
||||||
|
obj
|
||||||
148
LiloStitcher.cs
Normal file
148
LiloStitcher.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
Program.cs
Normal file
55
Program.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace LiloStitcher;
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static async Task<int> Main( string[] args )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var opt = new Options();
|
||||||
|
opt.CanvasRect = "A1:H12";
|
||||||
|
opt.CropOffset = [0.25, 0.25];
|
||||||
|
opt.CropSize = [0.75, 0.75];
|
||||||
|
opt.OutputScale = 0.5;
|
||||||
|
opt.OutputPath = "stitched.png";
|
||||||
|
|
||||||
|
string tileFilePath = "../stitch-a-ton/tiles1705";
|
||||||
|
string assetDir = Path.GetFullPath( Path.Combine( Directory.GetCurrentDirectory(), tileFilePath ) );
|
||||||
|
|
||||||
|
using var memCache = new MemoryCache( new MemoryCacheOptions { SizeLimit = 256L * 1024 * 1024 } );
|
||||||
|
var tileCache = new TileCache( memCache );
|
||||||
|
var loader = new TileLoader( tileCache, assetDir );
|
||||||
|
var stitcher = new LiloStitcher( loader );
|
||||||
|
|
||||||
|
var req = new GenerateRequest(
|
||||||
|
opt.CanvasRect!,
|
||||||
|
opt.CropOffset!,
|
||||||
|
opt.CropSize!,
|
||||||
|
opt.OutputScale
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine( "Stitching..." );
|
||||||
|
var png = await stitcher.CreateImageAsync( req, CancellationToken.None );
|
||||||
|
|
||||||
|
File.WriteAllBytes( opt.OutputPath!, png );
|
||||||
|
Console.WriteLine( $"Done. Wrote {opt.OutputPath} ({png.Length / 1024.0:F1} KB)" );
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lilo-stitcher-console.csproj
Normal file
17
lilo-stitcher-console.csproj
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<RootNamespace>lilo_stitcher_console</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
|
||||||
|
<PackageReference Include="SkiaSharp" Version="3.119.0" />
|
||||||
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
24
lilo-stitcher-console.sln
Normal file
24
lilo-stitcher-console.sln
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
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}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
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
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {C7CD65D7-8035-4FC2-A584-06B21F732FF7}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
Loading…
Add table
Add a link
Reference in a new issue