From c390415b6ed06fb0d5a5f5b1fd363d4aa0586859 Mon Sep 17 00:00:00 2001 From: gelaws-hub Date: Fri, 1 Aug 2025 23:04:42 +0700 Subject: [PATCH] feat : rework with the logic again, use skiasharp instead --- Services/ImageService.cs | 274 +++++++++++++++++++++---- Services/Utilities/CoordinateHelper.cs | 60 ++++++ StitcherApi.csproj | 2 +- 3 files changed, 290 insertions(+), 46 deletions(-) create mode 100644 Services/Utilities/CoordinateHelper.cs diff --git a/Services/ImageService.cs b/Services/ImageService.cs index 6692d75..9480c1e 100644 --- a/Services/ImageService.cs +++ b/Services/ImageService.cs @@ -1,3 +1,4 @@ +using SkiaSharp; using StitcherApi.Models; using StitcherApi.Services.Utilities; @@ -5,64 +6,247 @@ namespace StitcherApi.Services; public class ImageService : IImageService { + private const int TileDimension = 720; + private const long HighQualityMemoryThreshold = 512 * 1024 * 1024 * 3; + private readonly string _assetPath; private readonly ILogger _logger; - private readonly StitchProcessor _stitchProcessor; - public ImageService(IConfiguration config, ILoggerFactory loggerFactory) + public ImageService(IConfiguration configuration, ILogger logger) { - _logger = loggerFactory.CreateLogger(); - string assetPath = - config["AssetPath"] ?? throw new InvalidOperationException("AssetPath not configured."); - _stitchProcessor = new StitchProcessor( - assetPath, - loggerFactory.CreateLogger() - ); + _assetPath = + configuration["AssetPath"] + ?? throw new InvalidOperationException("AssetPath is not configured."); + _logger = logger; } - public async Task GenerateImageAsync(GenerateImageRequest request) + public Task GenerateImageAsync(GenerateImageRequest request) { - _logger.LogInformation("Processing request with the fast processor..."); - (int minRow, int minCol, int maxRow, int maxCol) = CoordinateParser.ParseCanvasRect( - request.CanvasRect - ); + return Task.Run(() => + { + try + { + _logger.LogInformation( + "Starting image generation for canvas_rect: {CanvasRect}", + request.CanvasRect + ); - StitchRequest stitchRequest = CreateStitchRequest(request, minRow, minCol, maxRow, maxCol); + var (startRow, endRow, startCol, endCol) = CoordinateHelper.ParseCanvasRect( + request.CanvasRect + ); + int canvasWidth = (endCol - startCol + 1) * TileDimension; + int canvasHeight = (endRow - startRow + 1) * TileDimension; + int cropX = (int)(request.CropOffset[0] * canvasWidth); + int cropY = (int)(request.CropOffset[1] * canvasHeight); + int cropW = (int)(request.CropSize[0] * canvasWidth); + int cropH = (int)(request.CropSize[1] * canvasHeight); - return await _stitchProcessor.ProcessAsync(stitchRequest); + int outputW = (int)(cropW * request.OutputScale); + int outputH = (int)(cropH * request.OutputScale); + if (outputW <= 0 || outputH <= 0) + { + _logger.LogWarning( + "Output dimensions are zero or negative ({Width}x{Height}). Returning empty byte array.", + outputW, + outputH + ); + return Array.Empty(); + } + + _logger.LogDebug("Calculated final dimensions: {Width}x{Height}", outputW, outputH); + + long requiredMemory = (long)cropW * cropH * 4; + if (requiredMemory <= HighQualityMemoryThreshold) + { + _logger.LogInformation( + "Using high-quality rendering path (required memory: {Memory}MB)", + requiredMemory / (1024 * 1024) + ); + return GenerateWithHighQuality( + cropX, + cropY, + cropW, + cropH, + outputW, + outputH, + startRow, + startCol + ); + } + else + { + _logger.LogWarning( + "Required memory ({Memory}MB) exceeds threshold. Using low-memory fallback path. Image quality may be reduced.", + requiredMemory / (1024 * 1024) + ); + return GenerateWithLowMemory( + cropX, + cropY, + cropW, + cropH, + outputW, + outputH, + startRow, + startCol, + request.OutputScale + ); + } + } + catch (Exception ex) + { + _logger.LogError( + ex, + "An unhandled exception occurred during image generation for request: {@Request}", + request + ); + throw; + } + }); } - private StitchRequest CreateStitchRequest( - GenerateImageRequest request, - int minRow, - int minCol, - int maxRow, - int maxCol + private byte[] GenerateWithHighQuality( + int cropX, + int cropY, + int cropW, + int cropH, + int outputW, + int outputH, + int startRow, + int startCol ) { - const int TILE_SIZE = 720; - int stitchedCanvasWidth = (maxCol - minCol + 1) * TILE_SIZE; - int stitchedCanvasHeight = (maxRow - minRow + 1) * TILE_SIZE; - int cropX = (int)(request.CropOffset[0] * stitchedCanvasWidth); - int cropY = (int)(request.CropOffset[1] * stitchedCanvasHeight); - int cropW = (int)(request.CropSize[0] * stitchedCanvasWidth); - int cropH = (int)(request.CropSize[1] * stitchedCanvasHeight); - int startTileCol = minCol + cropX / TILE_SIZE; - int endTileCol = minCol + (cropX + cropW - 1) / TILE_SIZE; - int startTileRow = minRow + cropY / TILE_SIZE; - int endTileRow = minRow + (cropY + cropH - 1) / TILE_SIZE; + using var cropBufferBitmap = new SKBitmap(cropW, cropH); + using var cropBufferCanvas = new SKCanvas(cropBufferBitmap); - return new StitchRequest( - minRow, - minCol, - startTileRow, - startTileCol, - endTileRow, - endTileCol, - cropX, - cropY, - cropW, - cropH, - request.OutputScale + DrawTilesToCanvas(cropBufferCanvas, cropX, cropY, cropW, cropH, startRow, startCol); + + using var finalBitmap = new SKBitmap(outputW, outputH); + + var sampling = new SKSamplingOptions(SKCubicResampler.Mitchell); + cropBufferBitmap.ScalePixels(finalBitmap, sampling); + + return EncodeBitmap(finalBitmap); + } + + private byte[] GenerateWithLowMemory( + int cropX, + int cropY, + int cropW, + int cropH, + int outputW, + int outputH, + int startRow, + int startCol, + float scale + ) + { + using var finalBitmap = new SKBitmap(outputW, outputH); + using var finalCanvas = new SKCanvas(finalBitmap); + var sampling = new SKSamplingOptions(SKCubicResampler.Mitchell); + + int firstTileCol = cropX / TileDimension; + int lastTileCol = (cropX + cropW - 1) / TileDimension; + int firstTileRow = cropY / TileDimension; + int lastTileRow = (cropY + cropH - 1) / TileDimension; + + for (int r = firstTileRow; r <= lastTileRow; r++) + { + for (int c = firstTileCol; c <= lastTileCol; c++) + { + var tilePath = Path.Combine( + _assetPath, + $"{CoordinateHelper.IndexToRow(startRow + r)}{startCol + c + 1}.png" + ); + using var tileBitmap = SKBitmap.Decode(tilePath); + if (tileBitmap == null) + continue; + using var tileImage = SKImage.FromBitmap(tileBitmap); + + int tileCanvasX = c * TileDimension; + int tileCanvasY = r * TileDimension; + int intersectX = Math.Max(cropX, tileCanvasX); + int intersectY = Math.Max(cropY, tileCanvasY); + int intersectEndX = Math.Min(cropX + cropW, tileCanvasX + TileDimension); + int intersectEndY = Math.Min(cropY + cropH, tileCanvasY + TileDimension); + + var sourceRect = SKRect.Create( + intersectX - tileCanvasX, + intersectY - tileCanvasY, + intersectEndX - intersectX, + intersectEndY - intersectY + ); + var destRect = SKRect.Create( + (intersectX - cropX) * scale, + (intersectY - cropY) * scale, + (intersectEndX - intersectX) * scale, + (intersectEndY - intersectY) * scale + ); + + finalCanvas.DrawImage(tileImage, sourceRect, destRect, sampling); + } + } + return EncodeBitmap(finalBitmap); + } + + private void DrawTilesToCanvas( + SKCanvas canvas, + int cropX, + int cropY, + int cropW, + int cropH, + int startRow, + int startCol + ) + { + int firstTileCol = cropX / TileDimension; + int lastTileCol = (cropX + cropW - 1) / TileDimension; + int firstTileRow = cropY / TileDimension; + int lastTileRow = (cropY + cropH - 1) / TileDimension; + + for (int r = firstTileRow; r <= lastTileRow; r++) + { + for (int c = firstTileCol; c <= lastTileCol; c++) + { + var tilePath = Path.Combine( + _assetPath, + $"{CoordinateHelper.IndexToRow(startRow + r)}{startCol + c + 1}.png" + ); + using var tileBitmap = SKBitmap.Decode(tilePath); + if (tileBitmap == null) + continue; + + int tileCanvasX = c * TileDimension; + int tileCanvasY = r * TileDimension; + int intersectX = Math.Max(cropX, tileCanvasX); + int intersectY = Math.Max(cropY, tileCanvasY); + int intersectEndX = Math.Min(cropX + cropW, tileCanvasX + TileDimension); + int intersectEndY = Math.Min(cropY + cropH, tileCanvasY + TileDimension); + + var sourceRect = SKRect.Create( + intersectX - tileCanvasX, + intersectY - tileCanvasY, + intersectEndX - intersectX, + intersectEndY - intersectY + ); + var destRect = SKRect.Create( + intersectX - cropX, + intersectY - cropY, + intersectEndX - intersectX, + intersectEndY - intersectY + ); + + canvas.DrawBitmap(tileBitmap, sourceRect, destRect); + } + } + } + + private byte[] EncodeBitmap(SKBitmap bitmap) + { + using var image = SKImage.FromBitmap(bitmap); + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + _logger.LogInformation( + "Image generation successful. Returning {ByteCount} bytes.", + data.Size ); + return data.ToArray(); } } diff --git a/Services/Utilities/CoordinateHelper.cs b/Services/Utilities/CoordinateHelper.cs new file mode 100644 index 0000000..f2ba8e3 --- /dev/null +++ b/Services/Utilities/CoordinateHelper.cs @@ -0,0 +1,60 @@ +using System.Text.RegularExpressions; + +namespace StitcherApi.Services.Utilities; + +public static class CoordinateHelper +{ + private static readonly Regex CoordRegex = new(@"([A-Z]+)(\d+)", RegexOptions.Compiled); + + public static (int startRow, int endRow, int startCol, int endCol) ParseCanvasRect( + string rectStr + ) + { + var parts = rectStr.Split(':'); + if (parts.Length != 2) + { + throw new ArgumentException("Invalid canvas_rect format. Expected format 'A1:H12'."); + } + + var (r1, c1) = ParseSingleCoordinate(parts[0]); + var (r2, c2) = ParseSingleCoordinate(parts[1]); + + return (Math.Min(r1, r2), Math.Max(r1, r2), Math.Min(c1, c2), Math.Max(c1, c2)); + } + + public static string IndexToRow(int index) + { + index++; + var result = ""; + while (index > 0) + { + int remainder = (index - 1) % 26; + result = (char)('A' + remainder) + result; + index = (index - 1) / 26; + } + return result; + } + + private static (int row, int col) ParseSingleCoordinate(string coord) + { + var match = CoordRegex.Match(coord); + if (!match.Success) + { + throw new ArgumentException($"Invalid coordinate format: '{coord}'."); + } + + string rowStr = match.Groups[1].Value; + int col = int.Parse(match.Groups[2].Value) - 1; + return (RowToIndex(rowStr), col); + } + + private static int RowToIndex(string rowStr) + { + int index = 0; + foreach (char c in rowStr) + { + index = index * 26 + (c - 'A' + 1); + } + return index - 1; + } +} diff --git a/StitcherApi.csproj b/StitcherApi.csproj index a2c1ab7..1748954 100644 --- a/StitcherApi.csproj +++ b/StitcherApi.csproj @@ -6,6 +6,6 @@ - +