diff --git a/src/Oh.My.Stitcher/Oh.My.Stitcher.csproj b/src/Oh.My.Stitcher/Oh.My.Stitcher.csproj
index ca91574..b82d246 100644
--- a/src/Oh.My.Stitcher/Oh.My.Stitcher.csproj
+++ b/src/Oh.My.Stitcher/Oh.My.Stitcher.csproj
@@ -6,13 +6,21 @@
enable
true
true
+ true
-
+
+
+
+
+
+
+
+
diff --git a/src/Oh.My.Stitcher/Tile.cs b/src/Oh.My.Stitcher/Tile.cs
index 7d613c7..72ba464 100644
--- a/src/Oh.My.Stitcher/Tile.cs
+++ b/src/Oh.My.Stitcher/Tile.cs
@@ -1,8 +1,11 @@
// ReSharper disable ReplaceSliceWithRangeIndexer
+using System.Buffers.Text;
using System.Runtime.InteropServices;
+using System.Text;
using Microsoft.Extensions.Caching.Memory;
using NetVips;
+using Oh.My.Stitcher.NetVips;
using ZLogger;
namespace Oh.My.Stitcher;
@@ -73,6 +76,71 @@ public static class Tile
return true;
}
+ public static bool TryCreateFast(in Stitch request, string tilesDirectory, List images, ILogger logger,
+ IMemoryCache cache, out Image? image, out string? cacheKey, out string? cacheFile)
+ {
+ if( !TryParseRect(request.CanvasRect, out int minRow, out int maxRow, out int minCol, out int maxCol) )
+ throw new ArgumentException($"Invalid canvas_rect: '{request.CanvasRect}'");
+ if( maxRow > byte.MaxValue || maxCol > byte.MaxValue )
+ throw new NotSupportedException($"Unsupported rect row: {maxRow}, col: {maxCol}");
+
+ logger.ZLogDebug(
+ $"rect: {request.CanvasRect}, minRow: {minRow}, maxRow: {maxRow}, minCol: {minCol}, maxCol: {maxCol}");
+ int width = maxCol - minCol + 1;
+ int height = maxRow - minRow + 1;
+
+ float cropOffsetX = request.CropOffset.X;
+ float cropOffsetY = request.CropOffset.Y;
+ float cropSizeW = request.CropSize.Width;
+ float cropSizeH = request.CropSize.Height;
+ float outputScale = request.OutputScale;
+
+ image = null;
+ cacheKey = cacheFile = null;
+
+ // predicted size
+ int predictedW = (int)( width * TILE_SIZE * cropSizeW * outputScale );
+ int predictedH = (int)( height * TILE_SIZE * cropSizeH * outputScale );
+ bool canBeCached = ( predictedW * predictedH ) <= MAX_CACHED_TILE_SIZE;
+ if( canBeCached )
+ {
+ Span buffer = stackalloc byte[24];
+ byte minRowByte = (byte)minRow;
+ byte maxRowByte = (byte)maxRow;
+ byte minColByte = (byte)minCol;
+ byte maxColByte = (byte)maxCol;
+ MemoryMarshal.Write(buffer, in minRowByte);
+ MemoryMarshal.Write(buffer[1..], in maxRowByte);
+ MemoryMarshal.Write(buffer[2..], in minColByte);
+ MemoryMarshal.Write(buffer[3..], in maxColByte);
+ MemoryMarshal.Write(buffer[4..], in cropOffsetX);
+ MemoryMarshal.Write(buffer[8..], in cropOffsetY);
+ MemoryMarshal.Write(buffer[12..], in cropSizeW);
+ MemoryMarshal.Write(buffer[16..], in cropSizeH);
+ MemoryMarshal.Write(buffer[20..], in outputScale);
+ cacheKey = Convert.ToHexString(buffer).ToLowerInvariant();
+ if( cache.TryGetValue(cacheKey, out cacheFile) && File.Exists(cacheFile) )
+ return false;
+ }
+
+ Span pathBuffer = stackalloc byte[512];
+ for( int row = minRow; row <= maxRow; row++ )
+ for( int col = minCol; col <= maxCol; col++ )
+ {
+ int length = FullPathFast(tilesDirectory, row, col, pathBuffer);
+ images.Add(( OperationHacks.Call("pngload", pathBuffer.Slice(0, length + 1)) as Image )!);
+ }
+
+ using var canvasImage = Image.Arrayjoin(images.ToArray(), width);
+ int cropLeft = (int)( canvasImage.Width * cropOffsetX );
+ int cropTop = (int)( canvasImage.Height * cropOffsetY );
+ int cropWidth = (int)( canvasImage.Width * cropSizeW );
+ int cropHeight = (int)( canvasImage.Height * cropSizeH );
+
+ image = canvasImage.Crop(cropLeft, cropTop, cropWidth, cropHeight).Resize(outputScale);
+ return true;
+ }
+
public static Image CreateError(Exception e)
{
const int padding = 20;
@@ -80,60 +148,84 @@ public static class Tile
return text.Embed(padding, padding, text.Width + ( 2 * padding ), text.Height + ( 2 * padding ));
}
- private static string FullPath(string directory, int row, int col)
+ internal static string FullPath(string directory, int row, int col)
{
string letterPart = "";
- while (row > 0)
+ while( row > 0 )
{
- int remainder = (row - 1) % 26;
- letterPart = (char)('A' + remainder) + letterPart;
- row = (row - 1) / 26;
+ int remainder = ( row - 1 ) % 26;
+ letterPart = (char)( 'A' + remainder ) + letterPart;
+ row = ( row - 1 ) / 26;
}
+
string fileName = $"{letterPart}{col}.png";
return Path.Combine(directory, fileName);
}
- private static bool TryParseRect(string rect, out int minRow, out int maxRow, out int minCol, out int maxCol)
+ internal static int FullPathFast(string directory, int row, int col, Span buffer)
{
- minRow = maxRow = minCol = maxCol = 0;
- string[] corners = rect.Split(':');
- switch( corners.Length )
+ int p = 0;
+ p += Encoding.UTF8.GetBytes(directory, buffer);
+ buffer[p++] = (byte)Path.DirectorySeparatorChar;
+ int letterStart = p;
+ if( row > 0 )
{
- case 1:
- {
- if( !TryParseName(corners[0], out int r, out int c) )
- return false;
+ while( row > 0 )
+ {
+ buffer[p++] = (byte)( 'A' + ( row - 1 ) % 26 );
+ row = ( row - 1 ) / 26;
+ }
- minRow = maxRow = r;
- minCol = maxCol = c;
- return true;
- }
-
- case 2:
- {
- if( !TryParseName(corners[0], out int r1, out int c1) || !TryParseName(corners[1], out int r2, out int c2) )
- return false;
-
- minRow = Math.Min(r1, r2);
- maxRow = Math.Max(r1, r2);
- minCol = Math.Min(c1, c2);
- maxCol = Math.Max(c1, c2);
- return true;
- }
-
- default:
- return false;
+ buffer.Slice(letterStart, p - letterStart).Reverse();
}
+
+ Utf8Formatter.TryFormat(col, buffer.Slice(p), out int numBytesWritten);
+ p += numBytesWritten;
+
+ ".png"u8.CopyTo(buffer.Slice(p));
+ p += 4;
+
+ buffer[p] = 0; // null terminator
+ return p;
}
- private static bool TryParseName(string name, out int row, out int col)
+ internal static bool TryParseRect(ReadOnlySpan rect, out int minRow, out int maxRow, out int minCol,
+ out int maxCol)
+ {
+ minRow = maxRow = minCol = maxCol = 0;
+ int colonIndex = rect.IndexOf(':');
+
+ if( colonIndex == -1 )
+ {
+ if( !TryParseName(rect, out int r, out int c) )
+ return false;
+
+ minRow = maxRow = r;
+ minCol = maxCol = c;
+ return true;
+ }
+
+ ReadOnlySpan left = rect.Slice(0, colonIndex);
+ ReadOnlySpan right = rect.Slice(colonIndex + 1);
+
+ if( !TryParseName(left, out int r1, out int c1) || !TryParseName(right, out int r2, out int c2) )
+ return false;
+
+ minRow = Math.Min(r1, r2);
+ maxRow = Math.Max(r1, r2);
+ minCol = Math.Min(c1, c2);
+ maxCol = Math.Max(c1, c2);
+ return true;
+ }
+
+ private static bool TryParseName(ReadOnlySpan name, out int row, out int col)
{
row = col = 0;
- ReadOnlySpan span = name.AsSpan().Trim();
+ ReadOnlySpan span = name.Trim();
int splitIndex = span.IndexOfAnyInRange('0', '9');
if( splitIndex <= 0 )
return false;
- if( !int.TryParse(span.Slice(splitIndex), out col))
+ if( !int.TryParse(span.Slice(splitIndex), out col) )
return false;
int letter = 0;
foreach( char c in span.Slice(0, splitIndex) )