feat : rework with the logic again, use skiasharp instead

This commit is contained in:
gelaws-hub 2025-08-01 23:04:42 +07:00
parent dd9f0d9cbe
commit c390415b6e
3 changed files with 290 additions and 46 deletions

View file

@ -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<ImageService> _logger;
private readonly StitchProcessor _stitchProcessor;
public ImageService(IConfiguration config, ILoggerFactory loggerFactory)
public ImageService(IConfiguration configuration, ILogger<ImageService> logger)
{
_logger = loggerFactory.CreateLogger<ImageService>();
string assetPath =
config["AssetPath"] ?? throw new InvalidOperationException("AssetPath not configured.");
_stitchProcessor = new StitchProcessor(
assetPath,
loggerFactory.CreateLogger<StitchProcessor>()
);
_assetPath =
configuration["AssetPath"]
?? throw new InvalidOperationException("AssetPath is not configured.");
_logger = logger;
}
public async Task<byte[]> GenerateImageAsync(GenerateImageRequest request)
public Task<byte[]> 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<byte>();
}
_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();
}
}