diff --git a/Svg.Tests.Win/DeepZoom/Cache/TileCacheTests.cs b/Svg.Tests.Win/DeepZoom/Cache/TileCacheTests.cs
index b22dd080e..4535e6da5 100644
--- a/Svg.Tests.Win/DeepZoom/Cache/TileCacheTests.cs
+++ b/Svg.Tests.Win/DeepZoom/Cache/TileCacheTests.cs
@@ -311,6 +311,67 @@ public void CleanUp_ShouldNotRemoveNonExpiredItems()
Assert.AreEqual(bitmap, cacheItem.Tile);
}
+ [Test]
+ public void DrainPendingDisposals_AfterEviction_DisposesEvictedBitmap()
+ {
+ var opts = new TileCacheOptions { CleanupInterval = TimeSpan.FromHours(1), MaximalTiles = 2 };
+ using var cache = new TileCache(opts);
+
+ var evicted = new SKBitmap(1, 1);
+ cache.GetOrCreate("k0", () => evicted);
+ cache.GetOrCreate("k1", () => new SKBitmap(1, 1));
+
+ // Exceed capacity → k0 (LRU) is evicted but disposal is deferred.
+ cache.GetOrCreate("k2", () => new SKBitmap(1, 1));
+ Assert.AreNotEqual(IntPtr.Zero, evicted.Handle, "evicted bitmap must not be disposed before drain");
+
+ cache.DrainPendingDisposals();
+ Assert.AreEqual(IntPtr.Zero, evicted.Handle, "evicted bitmap must be disposed after drain");
+ }
+
+ [Test]
+ public void DrainPendingDisposals_AfterRemove_DisposesRemovedBitmap()
+ {
+ var removed = new SKBitmap(1, 1);
+ _cache.GetOrCreate("k", () => removed);
+
+ _cache.Remove("k");
+ Assert.AreNotEqual(IntPtr.Zero, removed.Handle, "removed bitmap must not be disposed before drain");
+
+ _cache.DrainPendingDisposals();
+ Assert.AreEqual(IntPtr.Zero, removed.Handle, "removed bitmap must be disposed after drain");
+ }
+
+ [Test]
+ public void DrainPendingDisposals_DoesNotDisposeLiveItems()
+ {
+ var live = new SKBitmap(1, 1);
+ _cache.GetOrCreate("live", () => live);
+
+ _cache.DrainPendingDisposals();
+
+ Assert.AreNotEqual(IntPtr.Zero, live.Handle, "a cached (live) bitmap must not be disposed");
+ Assert.IsTrue(_cache.TryGetValue("live", out var item));
+ Assert.AreEqual(live, item.Tile);
+ }
+
+ [Test]
+ public void DrainPendingDisposals_AfterExpiryCleanup_DisposesExpiredBitmap()
+ {
+ var expired = new SKBitmap(1, 1);
+ var options = new TileCacheOptions { CleanupInterval = TimeSpan.FromMilliseconds(300) };
+ using var cache = new TileCache(options);
+
+ cache.GetOrCreate("k", () => expired);
+
+ // Wait for the cleanup timer to remove the expired entry (still not disposed yet).
+ Thread.Sleep(800);
+ Assert.AreNotEqual(IntPtr.Zero, expired.Handle, "expired bitmap must not be disposed before drain");
+
+ cache.DrainPendingDisposals();
+ Assert.AreEqual(IntPtr.Zero, expired.Handle, "expired bitmap must be disposed after drain");
+ }
+
[Test]
public void CleanUp_ShouldFreeCapacityForNewItems()
{
diff --git a/Svg.Tests.Win/DeepZoom/Renderer/TileRendererManagerTests.cs b/Svg.Tests.Win/DeepZoom/Renderer/TileRendererManagerTests.cs
new file mode 100644
index 000000000..49d0eeb29
--- /dev/null
+++ b/Svg.Tests.Win/DeepZoom/Renderer/TileRendererManagerTests.cs
@@ -0,0 +1,88 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using NUnit.Framework;
+using SkiaSharp;
+using Svg;
+using Svg.DeepZoom;
+
+namespace Svg.Tests.Win.DeepZoom.Renderer;
+
+///
+/// Verifies that caches the open zip archive per path
+/// (opened/enumerated once per plan, not per render) and disposes it on teardown.
+///
+public class TileRendererManagerTests
+{
+ private static string WriteTempZipWithTile(string entryName)
+ {
+ var path = Path.Combine(Path.GetTempPath(), $"tiles_{Guid.NewGuid():N}.zip");
+ using var fs = File.Create(path);
+ using var zip = new ZipArchive(fs, ZipArchiveMode.Create);
+
+ using var bmp = new SKBitmap(1, 1);
+ using var png = new MemoryStream();
+ using (var w = new SKManagedWStream(png))
+ bmp.Encode(w, SKEncodedImageFormat.Png, 100);
+
+ var entry = zip.CreateEntry(entryName);
+ using var es = entry.Open();
+ png.WriteTo(es);
+ return path;
+ }
+
+ [Test]
+ public void GetOrCreateZipTileProvider_ReusesOpenArchive_AndDisposesOnTeardown()
+ {
+ SvgPlatform.Init();
+ var zipPath = WriteTempZipWithTile("z0/y0_x0.png");
+ var manager = new TileRendererManager();
+
+ try
+ {
+ var provider1 = manager.GetOrCreateZipTileProvider(zipPath);
+ var provider2 = manager.GetOrCreateZipTileProvider(zipPath);
+
+ // Both providers read from the same cached, still-open archive across calls.
+ using (var s1 = provider1("z0", "y0_x0.png"))
+ Assert.IsNotNull(s1, "first provider should read an existing tile");
+ using (var s2 = provider2("z0", "y0_x0.png"))
+ Assert.IsNotNull(s2, "second provider should read from the same cached archive");
+
+ // Tearing the manager down disposes the cached archive: a previously handed-out
+ // provider now fails, proving it shared one persistent open archive (not a reopen-per-call).
+ manager.DisposeTileRenderer();
+ Assert.Catch(() => provider1("z0", "y0_x0.png"),
+ "after teardown the cached archive must be disposed");
+
+ // A fresh request reopens the archive and works again.
+ var provider3 = manager.GetOrCreateZipTileProvider(zipPath);
+ using var s3 = provider3("z0", "y0_x0.png");
+ Assert.IsNotNull(s3, "a fresh request should reopen the archive");
+ }
+ finally
+ {
+ manager.DisposeTileRenderer();
+ File.Delete(zipPath);
+ }
+ }
+
+ [Test]
+ public void GetOrCreateZipTileProvider_ReturnsNullForMissingEntry()
+ {
+ SvgPlatform.Init();
+ var zipPath = WriteTempZipWithTile("z0/y0_x0.png");
+ var manager = new TileRendererManager();
+
+ try
+ {
+ var provider = manager.GetOrCreateZipTileProvider(zipPath);
+ Assert.IsNull(provider("z0", "y99_x99.png"), "missing tile must yield null");
+ }
+ finally
+ {
+ manager.DisposeTileRenderer();
+ File.Delete(zipPath);
+ }
+ }
+}
diff --git a/Svg.Tests.Win/DeepZoom/Renderer/TileRendererTests.cs b/Svg.Tests.Win/DeepZoom/Renderer/TileRendererTests.cs
index ba34a99ae..b65e554d9 100644
--- a/Svg.Tests.Win/DeepZoom/Renderer/TileRendererTests.cs
+++ b/Svg.Tests.Win/DeepZoom/Renderer/TileRendererTests.cs
@@ -270,6 +270,48 @@ Stream CountingProvider(string folder, string file)
$"Expected only visible tiles to be loaded (12); got {loadCalls} provider calls.");
}
+ [Test]
+ public void RenderBitmap_WithDifferentSourceKeys_DoesNotReuseCachedTilesAcrossSources()
+ {
+ // Regression for #11850: a single (singleton) TileRenderer with an active cache is reused
+ // across plans. Tiles must be keyed by their source — otherwise plan B's "z0/y0_x0.png"
+ // lookup returns plan A's cached bitmap and every plan after the first renders the first
+ // plan's image.
+ SvgPlatform.Init();
+
+ // One renderer + one cache, reused for both "sources" (as the reporting singleton would be).
+ using var rndr = new TileRenderer(10, 10, new TileCacheOptions(TimeSpan.FromHours(1)));
+
+ Stream RedProvider(string folder, string file) => SolidTilePng(SKColors.Red);
+ Stream BlueProvider(string folder, string file) => SolidTilePng(SKColors.Blue);
+
+ // Sample an interior pixel: RenderBitmap strokes a red debug-border rectangle around each
+ // tile under #if DEBUG, so the (0,0) corner is unreliable — read the tile fill instead.
+ const int sx = 5, sy = 5;
+
+ // Source "A" caches a red tile at z0/y0_x0.
+ using (var first = rndr.RenderBitmap(RedProvider, 0, 0, 1f, "planA.zip"))
+ first.GetPixel(sx, sy).ShouldBe(SKColors.Red, "first source should render its own (red) tile");
+
+ // Source "B" provides a blue tile for the same coordinate. With source-scoped keys this is a
+ // cache miss and renders blue; with the old coordinate-only key it would return A's red tile.
+ using var second = rndr.RenderBitmap(BlueProvider, 0, 0, 1f, "planB.zip");
+
+ second.GetPixel(sx, sy).ShouldBe(SKColors.Blue,
+ "second source must render its own tile, not the first source's cached tile");
+ }
+
+ private static MemoryStream SolidTilePng(SKColor color)
+ {
+ using var bmp = new SKBitmap(TileConstants.TileSize, TileConstants.TileSize);
+ bmp.Erase(color);
+ var ms = new MemoryStream();
+ using (var w = new SKManagedWStream(ms))
+ bmp.Encode(w, SKEncodedImageFormat.Png, 100);
+ ms.Position = 0;
+ return ms;
+ }
+
private static void AssertRenderedFile(string path, int expectedWidth, int expectedHeight)
{
File.Exists(path).ShouldBeTrue($"renderer should have written output file: {path}");
@@ -328,6 +370,7 @@ public async Task GetOrCreateAsync(string key, Func _inner.TryGetValue(key, out item);
public void Remove(string key) => _inner.Remove(key);
+ public void DrainPendingDisposals() => _inner.DrainPendingDisposals();
public void Dispose() => _inner.Dispose();
}
}
diff --git a/Svg/Basic Shapes/SvgImage.cs b/Svg/Basic Shapes/SvgImage.cs
index 4f376d259..a8b1ad09e 100644
--- a/Svg/Basic Shapes/SvgImage.cs
+++ b/Svg/Basic Shapes/SvgImage.cs
@@ -208,21 +208,21 @@ protected override void Render(ISvgRenderer renderer)
{
PushTransforms(renderer);
- var fileSystem = SvgEngine.Resolve();
var scaleX = renderer.Transform.ScaleX;
var xOffset = renderer.Transform.OffsetX;
var yOffset = renderer.Transform.OffsetY;
- var tileRenderer = SvgEngine.Resolve().GetOrCreateTileRenderer();
+ var tileRendererManager = SvgEngine.Resolve();
+ var tileRenderer = tileRendererManager.GetOrCreateTileRenderer();
tileRenderer.SetDimensions(renderer.ScreenWidth, renderer.ScreenHeight);
- using var zipFileStream = fileSystem.OpenRead(Href);
- using var zipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Read);
- var tileProvider = CreateZipTileProvider(zipArchive, (a, b) => fileSystem.PathCombine(a, b));
+ // Reuse the open archive + entry lookup across renders instead of reopening
+ // and re-enumerating the zip on every render.
+ var tileProvider = tileRendererManager.GetOrCreateZipTileProvider(Href);
using var skImage = tileRenderer.RenderBitmap(tileProvider, xOffset, yOffset,
- scaleX);
+ scaleX, Href);
var image = new SkiaBitmap(skImage);
diff --git a/Svg/DeepZoom/Cache/ITileCache.cs b/Svg/DeepZoom/Cache/ITileCache.cs
index acbf14153..03ab9d9e1 100644
--- a/Svg/DeepZoom/Cache/ITileCache.cs
+++ b/Svg/DeepZoom/Cache/ITileCache.cs
@@ -10,5 +10,11 @@ public interface ITileCache : IDisposable
bool TryGetValue(string key, out TileCacheItem item);
void Remove(string key);
+ ///
+ /// Disposes the bitmaps of items that have left the cache (eviction, expiry, removal).
+ /// Must be called at a point where no tile is being drawn (e.g. the start of a render),
+ /// so an in-flight draw can never reference a just-disposed bitmap.
+ ///
+ void DrainPendingDisposals();
}
}
\ No newline at end of file
diff --git a/Svg/DeepZoom/Cache/TileCache.cs b/Svg/DeepZoom/Cache/TileCache.cs
index 1fdcd5934..e63b1458b 100644
--- a/Svg/DeepZoom/Cache/TileCache.cs
+++ b/Svg/DeepZoom/Cache/TileCache.cs
@@ -16,6 +16,11 @@ public class TileCache : ITileCache
private readonly object _evictionLock = new();
private long _tickCounter = 0;
+ // Items that have left the cache but whose SKBitmap is not yet disposed. Disposal is
+ // deferred to DrainPendingDisposals (called at render start) so the background cleanup
+ // timer can never free a bitmap that a concurrent draw still references.
+ private readonly ConcurrentQueue _pendingDispose = new();
+
public TileCache(TileCacheOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
@@ -61,7 +66,8 @@ public bool TryGetValue(string key, out TileCacheItem item)
return true;
}
- _cache.TryRemove(key, out _);
+ if (_cache.TryRemove(key, out var expired))
+ _pendingDispose.Enqueue(expired);
}
item = default;
@@ -91,27 +97,36 @@ private void EvictLru()
}
}
- if (lruKey != null)
- _cache.TryRemove(lruKey, out _);
+ if (lruKey != null && _cache.TryRemove(lruKey, out var evicted))
+ _pendingDispose.Enqueue(evicted);
}
public void Remove(string key)
{
if (key == null) throw new ArgumentNullException(nameof(key));
- _cache.TryRemove(key, out _);
+ if (_cache.TryRemove(key, out var removed))
+ _pendingDispose.Enqueue(removed);
}
private void CleanUp(object state)
{
foreach (var key in _cache.Keys)
{
- if (_cache.TryGetValue(key, out var cacheItem) && cacheItem.IsExpired)
- _cache.TryRemove(key, out _);
+ if (_cache.TryGetValue(key, out var cacheItem) && cacheItem.IsExpired
+ && _cache.TryRemove(key, out var expired))
+ _pendingDispose.Enqueue(expired);
}
}
+ public void DrainPendingDisposals()
+ {
+ while (_pendingDispose.TryDequeue(out var item))
+ item?.Dispose();
+ }
+
public void Dispose()
{
+ DrainPendingDisposals();
foreach (var tileCacheItem in _cache)
{
tileCacheItem.Value?.Dispose();
diff --git a/Svg/DeepZoom/Renderer/ITileRenderer.cs b/Svg/DeepZoom/Renderer/ITileRenderer.cs
index 9e4f72a1d..df2ce7a94 100644
--- a/Svg/DeepZoom/Renderer/ITileRenderer.cs
+++ b/Svg/DeepZoom/Renderer/ITileRenderer.cs
@@ -11,12 +11,12 @@ public interface ITileRenderer : IDisposable
void RenderBitmap(string tileFolderPath, string outputPath, float x, float y, float zoomFactor = 1);
- SKBitmap RenderBitmap(Func tileProvider, float offsetX, float offsetY, float zoomFactor = 1);
+ SKBitmap RenderBitmap(Func tileProvider, float offsetX, float offsetY, float zoomFactor = 1, string sourceKey = null);
Task RenderBitmapAsync(string tileFolderPath, string outputPath, float x, float y, float zoomFactor = 1);
Task RenderBitmapAsync(Func> tileProvider, float offsetX,
- float offsetY, float zoomFactor = 1);
+ float offsetY, float zoomFactor = 1, string sourceKey = null);
}
}
\ No newline at end of file
diff --git a/Svg/DeepZoom/Renderer/ITileRendererManager.cs b/Svg/DeepZoom/Renderer/ITileRendererManager.cs
index abf3eee66..d2966e6ac 100644
--- a/Svg/DeepZoom/Renderer/ITileRendererManager.cs
+++ b/Svg/DeepZoom/Renderer/ITileRendererManager.cs
@@ -1,8 +1,17 @@
-namespace Svg.DeepZoom;
+using System;
+using System.IO;
+
+namespace Svg.DeepZoom;
public interface ITileRendererManager
{
public ITileRenderer GetOrCreateTileRenderer();
+ ///
+ /// Returns a tile provider backed by a cached, already-open zip archive for the given path,
+ /// so the archive is opened and enumerated once per plan rather than on every render.
+ ///
+ Func GetOrCreateZipTileProvider(string zipPath);
+
public void DisposeTileRenderer();
}
\ No newline at end of file
diff --git a/Svg/DeepZoom/Renderer/TileRenderer.cs b/Svg/DeepZoom/Renderer/TileRenderer.cs
index 5bb2ef198..47a207149 100644
--- a/Svg/DeepZoom/Renderer/TileRenderer.cs
+++ b/Svg/DeepZoom/Renderer/TileRenderer.cs
@@ -68,7 +68,7 @@ public void RenderBitmap(string tileFolderPath, string outputPath, float x, floa
var tileProvider = new Func((folder, fileName) =>
LoadTileStream(_fileSystem.PathCombine(tileFolderPath, folder), fileName));
- using var tileBitmap = RenderBitmap(tileProvider, x, y, zoomFactor);
+ using var tileBitmap = RenderBitmap(tileProvider, x, y, zoomFactor, tileFolderPath);
// Save the tile as a PNG file
using var image = SKImage.FromBitmap(tileBitmap);
@@ -86,8 +86,11 @@ public void RenderBitmap(string tileFolderPath, string outputPath, float x, floa
/// Example zoom factor: 1.0 means fully zoomed in at highest detail (z0), 0.5 means one level zoomed out (z1), etc.
///
public SKBitmap RenderBitmap(Func tileProvider, float offsetX, float offsetY,
- float zoomFactor = 1)
+ float zoomFactor = 1, string sourceKey = null)
{
+ // Free bitmaps evicted/expired since the last render now, while no tile is being drawn.
+ _cache?.DrainPendingDisposals();
+
var tileSize = TileConstants.TileSize;
SKBitmap bitmap = new SKBitmap(Width, Height);
using var canvas = new SKCanvas(bitmap);
@@ -132,7 +135,7 @@ public SKBitmap RenderBitmap(Func tileProvider, float of
continue;
// Load the tile bitmap only for visible tiles; not disposing bitmap because its cached
- var tileBitmap = LoadTile($"z{zoomLevel}", $"y{tileY}_x{tileX}.png", tileProvider);
+ var tileBitmap = LoadTile($"z{zoomLevel}", $"y{tileY}_x{tileX}.png", tileProvider, sourceKey);
if (tileBitmap != null)
{
@@ -172,7 +175,7 @@ public async Task RenderBitmapAsync(string tileFolderPath, string outputPath, fl
var tileProvider = new Func>((folder, fileName) =>
LoadTileStreamAsync(_fileSystem.PathCombine(tileFolderPath, folder), fileName));
- using var tileBitmap = await RenderBitmapAsync(tileProvider, x, y, zoomFactor);
+ using var tileBitmap = await RenderBitmapAsync(tileProvider, x, y, zoomFactor, tileFolderPath);
// Save the tile as a PNG file
@@ -183,8 +186,11 @@ public async Task RenderBitmapAsync(string tileFolderPath, string outputPath, fl
}
public async Task RenderBitmapAsync(Func> tileProvider, float offsetX,
- float offsetY, float zoomFactor = 1)
+ float offsetY, float zoomFactor = 1, string sourceKey = null)
{
+ // Free bitmaps evicted/expired since the last render now, while no tile is being drawn.
+ _cache?.DrainPendingDisposals();
+
var tileSize = TileConstants.TileSize;
SKBitmap bitmap = new SKBitmap(Width, Height);
using var canvas = new SKCanvas(bitmap);
@@ -227,7 +233,7 @@ public async Task RenderBitmapAsync(Func>
tileLoadTasks.Add(Task.Run(async () =>
{
var bmp = await LoadTileAsync($"z{zoomLevel}", $"y{localTileY}_x{localTileX}.png",
- tileProvider);
+ tileProvider, sourceKey);
// Calculate the position to draw the tile on the canvas
float drawX = offsetX + localTileX * tileSizeAtZoom;
float drawY = offsetY + localTileY * tileSizeAtZoom;
@@ -289,11 +295,11 @@ internal Stream LoadTileStream(string zoomFolderName, string tileFileName)
// Asynchronous method to load a tile from disk
private async Task LoadTileAsync(string zoomFolderName, string tileFileName,
- Func> tileProvider)
+ Func> tileProvider, string sourceKey = null)
{
if (_cache is { } cache)
{
- var item = await cache.GetOrCreateAsync(Path.Combine(zoomFolderName, tileFileName),
+ var item = await cache.GetOrCreateAsync(Path.Combine(sourceKey ?? string.Empty, zoomFolderName, tileFileName),
async () =>
{
using var stream = await tileProvider.Invoke(zoomFolderName, tileFileName);
@@ -313,9 +319,9 @@ private async Task LoadTileAsync(string zoomFolderName, string tileFil
}
private SKBitmap LoadTile(string zoomFolderName, string tileFileName,
- Func tileProvider)
+ Func tileProvider, string sourceKey = null)
{
- var tilePath = Path.Combine(zoomFolderName, tileFileName);
+ var tilePath = Path.Combine(sourceKey ?? string.Empty, zoomFolderName, tileFileName);
if (_cache is { } cache)
{
diff --git a/Svg/DeepZoom/Renderer/TileRendererManager.cs b/Svg/DeepZoom/Renderer/TileRendererManager.cs
index 2b17b0635..9be6c0e75 100644
--- a/Svg/DeepZoom/Renderer/TileRendererManager.cs
+++ b/Svg/DeepZoom/Renderer/TileRendererManager.cs
@@ -1,9 +1,20 @@
-namespace Svg.DeepZoom;
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.IO.Compression;
+using Svg.Interfaces;
+
+namespace Svg.DeepZoom;
public class TileRendererManager : ITileRendererManager
{
private ITileRenderer _tileRendererInstance;
+
+ // Open zip archives + prebuilt tile lookups, keyed by zip path, so the archive is opened
+ // and its entry list enumerated once per plan instead of on every render.
+ private readonly ConcurrentDictionary> _zipArchives = new();
+
public ITileRenderer GetOrCreateTileRenderer()
{
if (_tileRendererInstance == null)
@@ -14,6 +25,22 @@ public ITileRenderer GetOrCreateTileRenderer()
return _tileRendererInstance;
}
+ ///
+ /// Returns a tile provider backed by a cached, already-open zip archive for
+ /// . The archive is opened (and its entries enumerated) only on
+ /// the first request for a given path; subsequent renders reuse it. Disposed in
+ /// .
+ ///
+ public Func GetOrCreateZipTileProvider(string zipPath)
+ {
+ if (zipPath == null) throw new ArgumentNullException(nameof(zipPath));
+
+ var archive = _zipArchives.GetOrAdd(zipPath,
+ p => new Lazy(() => new ZipTileArchive(p))).Value;
+
+ return archive.GetEntryStream;
+ }
+
public void DisposeTileRenderer()
{
if (_tileRendererInstance != null)
@@ -21,5 +48,43 @@ public void DisposeTileRenderer()
_tileRendererInstance.Dispose();
_tileRendererInstance = null;
}
+
+ foreach (var key in _zipArchives.Keys)
+ {
+ if (_zipArchives.TryRemove(key, out var lazy) && lazy.IsValueCreated)
+ lazy.Value.Dispose();
+ }
+ }
+
+ ///
+ /// Holds an open zip archive and its prebuilt entry lookup. Entry reads are serialised
+ /// because does not support concurrent access.
+ ///
+ private sealed class ZipTileArchive : IDisposable
+ {
+ private readonly Stream _stream;
+ private readonly ZipArchive _archive;
+ private readonly Func _provider;
+ private readonly object _gate = new();
+
+ public ZipTileArchive(string zipPath)
+ {
+ var fileSystem = SvgEngine.Resolve();
+ _stream = fileSystem.OpenRead(zipPath);
+ _archive = new ZipArchive(_stream, ZipArchiveMode.Read);
+ _provider = SvgImage.CreateZipTileProvider(_archive, (a, b) => fileSystem.PathCombine(a, b));
+ }
+
+ public Stream GetEntryStream(string folderName, string fileName)
+ {
+ lock (_gate)
+ return _provider(folderName, fileName);
+ }
+
+ public void Dispose()
+ {
+ _archive.Dispose();
+ _stream.Dispose();
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Svg/Svg.csproj b/Svg/Svg.csproj
index c030dc414..edaf6b0a9 100644
--- a/Svg/Svg.csproj
+++ b/Svg/Svg.csproj
@@ -3,10 +3,14 @@
netstandard2.0;net48;net10.0
PackageReference
- 3.2.0-optiq02
+ 3.2.0-optiq03
gentledpp,zepr
Opti-Q GmbH
+ #3.2.0-optiq03
+ Fixed: tile cache key now includes the tile source (zip Href / tile folder) so the shared
+ singleton TileRenderer no longer returns one source's tiles for another — reports rendering
+ multiple file-tiled plans previously all showed the first plan's image.
#3.2.0-optiq02
TileGenerator stores scale factor in dimensions.json (relative to svg dimensions)
#3.2.0-optiq01