diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..e12bae7 Binary files /dev/null and b/.DS_Store differ diff --git a/LiloStitcher.cs b/LiloStitcher.cs index 7ae74cc..7ac174f 100644 --- a/LiloStitcher.cs +++ b/LiloStitcher.cs @@ -21,8 +21,8 @@ public readonly record struct PlateCoordinate(int Row, int Col) var colPart = new string(token.SkipWhile(char.IsLetter).ToArray()); int row = 0; - foreach (var c in rowPart) - row = row * 26 + (c - 'A' + 1); + foreach (var character in rowPart) + row = row * 26 + (character - 'A' + 1); int.TryParse(colPart, out int col); return new PlateCoordinate(row, col); @@ -33,9 +33,9 @@ public class TileCache(IMemoryCache cache) { private const long TileBytes = 720L * 720 * 3; - public Image? Get(string key) => cache.TryGetValue(key, out Image? img) ? img : null; + public Image? Get(string key) => cache.TryGetValue(key, out Image? img) ? img : null; - public void Set(string key, Image img) => + public void Set(string key, Image img) => cache.Set(key, img, new MemoryCacheEntryOptions { Size = TileBytes, @@ -45,14 +45,14 @@ public class TileCache(IMemoryCache cache) public class TileLoader(TileCache cache, string assetDir) { - public async Task> LoadAsync(string name, CancellationToken ct) + public async Task> LoadAsync(string name, CancellationToken ct) { if (cache.Get(name) is { } hit) return hit; var path = Path.Combine(assetDir, $"{name}.png"); await using var fs = File.OpenRead(path); - var image = await Image.LoadAsync(fs, ct).ConfigureAwait(false); + var image = await Image.LoadAsync(fs, ct).ConfigureAwait(false); cache.Set(name, image); return image; } @@ -62,10 +62,17 @@ public class LiloStitcher(TileLoader loader) { private const int TileSize = 720; + private static readonly GraphicsOptions _copy = new() + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + BlendPercentage = 1f + }; + public async Task CreateImageAsync(GenerateRequest req, CancellationToken ct) { - var parts = req.CanvasRect.ToUpperInvariant() - .Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + var parts = req.CanvasRect.ToUpperInvariant().Split(':', StringSplitOptions.TrimEntries | + StringSplitOptions.RemoveEmptyEntries); var part1 = PlateCoordinate.Parse(parts[0]); var part2 = PlateCoordinate.Parse(parts[1]); @@ -82,34 +89,43 @@ public class LiloStitcher(TileLoader loader) .ToArray(); var bitmaps = await Task.WhenAll(names.Select(n => loader.LoadAsync(n, ct))).ConfigureAwait(false); - var stitched = new Image(cols * TileSize, rows * TileSize); + var stitched = new Image(cols * TileSize, rows * TileSize); - int idx = 0; - for (int r = 0; r < rows; r++) + stitched.Mutate(context => { - for (int c = 0; c < cols; c++, idx++) + int idx = 0; + for (int row = 0; row < rows; row++) { - stitched.Mutate(ctx => ctx.DrawImage(bitmaps[idx], new Point(c * TileSize, r * TileSize), 1f)); + for (int col = 0; col < cols; col++, idx++) + { + context.DrawImage(bitmaps[idx], new Point(col * TileSize, row * TileSize), 1f); + } } - } + }); Validate(req.CropOffset, 2, nameof(req.CropOffset), inclusiveUpper: true); Validate(req.CropSize, 2, nameof(req.CropSize), inclusiveUpper: true); - int offsetX = (int)(req.CropOffset[0] * stitched.Width); - int offsetY = (int)(req.CropOffset[1] * stitched.Height); - int cropWidth = (int)(req.CropSize[0] * stitched.Width); - int cropHeight = (int)(req.CropSize[1] * stitched.Height); + int offsetX = (int)Math.Truncate(req.CropOffset[0] * stitched.Width); + int offsetY = (int)Math.Truncate(req.CropOffset[1] * stitched.Height); - if (offsetX + cropWidth > stitched.Width) cropWidth = stitched.Width - offsetX; - if (offsetY + cropHeight > stitched.Height) cropHeight = stitched.Height - offsetY; + int restWidth = stitched.Width - offsetX; + int restHeight = stitched.Height - offsetY; - var cropRect = new Rectangle(offsetX, offsetY, cropWidth, cropHeight); + int cropWidth = Math.Max(1, (int)Math.Truncate(req.CropSize[0] * restWidth )); + int cropHeight = Math.Max(1, (int)Math.Truncate(req.CropSize[1] * restHeight)); - using var cropped = stitched.Clone(ctx => ctx.Crop(cropRect)); + int cropX = (int)Math.Truncate(offsetX / 2.0 + (restWidth - cropWidth) / 2.0); + int cropY = (int)Math.Truncate(offsetY / 2.0 + (restHeight - cropHeight) / 2.0); + + var cropRect = new Rectangle(cropX, cropY, cropWidth, cropHeight); + using var cropped = stitched.Clone(context => context.Crop(cropRect)); double scale = Math.Clamp(req.OutputScale, 0.0, 1.0); - Image output; + if (scale <= 0 || scale > 1) + throw new ArgumentOutOfRangeException(nameof(req.OutputScale), "OutputScale must be > 0 and ≤ 1.0"); + + Image output; if (scale < 1.0) { int width = Math.Max(1, (int)Math.Truncate(cropWidth * scale)); @@ -124,32 +140,33 @@ public class LiloStitcher(TileLoader loader) using var memStream = new MemoryStream(); await output.SaveAsync(memStream, new PngEncoder { + CompressionLevel = PngCompressionLevel.Level1 }, ct).ConfigureAwait(false); return memStream.ToArray(); + } - static void Validate(double[] arr, int len, string name, bool inclusiveUpper) + static void Validate(double[] arr, int len, string name, bool inclusiveUpper) + { + if (arr is null || arr.Length < len) + throw new ArgumentException($"{name} must have length {len}"); + double upper = inclusiveUpper ? 1.0 : 1.0 - double.Epsilon; + for (int i = 0; i < len; i++) { - if (arr is null || arr.Length < len) - throw new ArgumentException($"{name} must have length {len}"); - double upper = inclusiveUpper ? 1.0 : 1.0 - double.Epsilon; - for (int i = 0; i < len; i++) - { - var v = arr[i]; - if (v < 0 || v > upper) - throw new ArgumentOutOfRangeException($"{name}[{i}]= {v} outside [0,{upper}]"); - } - } - - 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(); + var v = arr[i]; + if (v < 0 || v > upper) + throw new ArgumentOutOfRangeException($"{name}[{i}]= {v} outside [0,{upper}]"); } } + + 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(); + } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index 3f0c08d..253020f 100644 --- a/Program.cs +++ b/Program.cs @@ -13,9 +13,9 @@ public static class Program var opt = new Options { CanvasRect = "A1:AE55", - CropOffset = [0.0, 0.0], - CropSize = [1.0, 1.0], - OutputScale = 1.0, + CropOffset = [0.4, 0.4], + CropSize = [0.8, 0.8], + OutputScale = 0.5, OutputPath = "stitched.png" }; @@ -40,7 +40,7 @@ public static class Program File.WriteAllBytes(opt.OutputPath!, pngBytes); Console.WriteLine($"Done. Wrote {opt.OutputPath} ({pngBytes.Length / 1024.0:F1} KB)"); - Console.WriteLine(begin - sw.ElapsedMilliseconds); + Console.WriteLine(sw.ElapsedMilliseconds - begin); return 0; } catch (Exception ex) diff --git a/lilo-stitcher-console.csproj b/lilo-stitcher-console.csproj index eb79597..05566b9 100644 --- a/lilo-stitcher-console.csproj +++ b/lilo-stitcher-console.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net8.0 lilo_stitcher_console enable enable