252 lines
8.9 KiB
C#
252 lines
8.9 KiB
C#
using SkiaSharp;
|
|
using StitcherApi.Models;
|
|
using StitcherApi.Services.Utilities;
|
|
|
|
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;
|
|
|
|
public ImageService(IConfiguration configuration, ILogger<ImageService> logger)
|
|
{
|
|
_assetPath =
|
|
configuration["AssetPath"]
|
|
?? throw new InvalidOperationException("AssetPath is not configured.");
|
|
_logger = logger;
|
|
}
|
|
|
|
public Task<byte[]> GenerateImageAsync(GenerateImageRequest request)
|
|
{
|
|
return Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
_logger.LogInformation(
|
|
"Starting image generation for canvas_rect: {CanvasRect}",
|
|
request.CanvasRect
|
|
);
|
|
|
|
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);
|
|
|
|
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 byte[] GenerateWithHighQuality(
|
|
int cropX,
|
|
int cropY,
|
|
int cropW,
|
|
int cropH,
|
|
int outputW,
|
|
int outputH,
|
|
int startRow,
|
|
int startCol
|
|
)
|
|
{
|
|
using var cropBufferBitmap = new SKBitmap(cropW, cropH);
|
|
using var cropBufferCanvas = new SKCanvas(cropBufferBitmap);
|
|
|
|
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();
|
|
}
|
|
}
|