Add DeepZoom static preview services and samples#379
Open
mattleibow wants to merge 39 commits intomainfrom
Open
Add DeepZoom static preview services and samples#379mattleibow wants to merge 39 commits intomainfrom
mattleibow wants to merge 39 commits intomainfrom
Conversation
Extract the core DeepZoom rendering system from PR #378: - DeepZoom collection/image source parsers (DZC/DZI XML) - Tile cache, scheduler, and HTTP/file fetchers - Controller (orchestrates loading), viewport (centered-fit geometry), renderer (draws tiles) - 436 unit tests (animation tests excluded — no dependency on gestures/animations) - Blazor sample page (Deep Zoom Preview) with testgrid DZI static asset - MAUI sample page (Deep Zoom Preview) with testgrid DZI app-package asset - Documentation: deep-zoom.md, deep-zoom-blazor.md, deep-zoom-maui.md - Solution file updated to include DeepZoom test project - DeepZoom added to MAUI ExtendedDemos and Blazor NavMenu No gestures, animations, or custom controls included. This is a minimal static preview system for gigapixel images. Images render centered-fit (aspect ratio preserved, no cropping/stretching). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
📖 Documentation Preview The documentation for this PR has been deployed and is available at: This preview will be updated automatically when you push new commits to this PR. This comment is automatically updated by the documentation staging workflow. |
… add README - Blazor: remove duplicated testgrid assets from wwwroot/deepzoom/ - Blazor: load DZI directly from GitHub raw URL (no local duplication) - Blazor: switch SKCanvasView → SKGLView for hardware-accelerated rendering - Controller: SetControlSize() now refits viewport when canvas size changes, ensuring centered-fit works correctly in any aspect ratio (portrait/landscape) - DeepZoom: add README.md with architecture overview and Mermaid diagrams showing component relationships, tile pipeline, and rendering flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SKDeepZoomTileRequest: correct fields are {TileId, Priority}, not IsFallback/FallbackParent
- SKDeepZoomCollectionSubImage.Source is string? (URI path), not SKDeepZoomImageSource
- Remove incorrect inheritance arrow from SubImage → ImageSource
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Blazor (DeepZoom.razor): - Canvas now fills available viewport height using flexbox + calc(100vh) - Added collapsible debug inspector sidebar showing: image source info (dimensions, tile size, levels, format, aspect ratio), viewport state (control size, origin, scale, zoom), tile cache stats (count/max/pending), and a live tile table with level/col/row/priority/cached status - Added PendingTileCount property to SKDeepZoomController for inspector - Inspector refreshes every 250ms during tile loading - Window resize triggers canvas invalidate via JS interop - Toggle button (fixed bottom-right) shows/hides the inspector MAUI (DeepZoomPage.xaml.cs): - Removed AppPackageFetcher and embedded testgrid assets - Now fetches DZI and tiles from GitHub raw URL (same as Blazor) - Removed TestGrid/ from Resources/Raw/ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add deepZoomUnregisterResize JS function to remove the resize handler; call it in DisposeAsync so listeners don't accumulate on page navigation - Switch Blazor page from IDisposable → IAsyncDisposable to cleanly await the JS interop call during disposal - Use parameterless SKDeepZoomHttpTileFetcher() in both Blazor and MAUI samples so the fetcher owns and disposes its HttpClient (_ownsClient=true) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Blazor: zoom slider (0.1–10×) in toolbar + mouse drag to pan - Reset view button returns to fit-to-view - Inspector zoom label auto-syncs with viewport state - MAUI: Slider in toolbar for zoom + PanGestureRecognizer for drag pan - Toolbar shows current zoom level label - Both samples call controller.Pan() and controller.SetZoom() APIs - Bare minimum interaction for testing tile loading at various zoom levels Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove pan direction inversion in both Blazor and MAUI samples - Fix MAUI pan to compute frame-to-frame delta from TotalX/TotalY Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove MaxViewportWidth = fitWidth from FitToView() so users can zoom out beyond the initial fit without snapping back - Track _userHasZoomed in controller; SetControlSize no longer resets the viewport to fit when the user has manually adjusted zoom/pan - ResetView() and Load() clear the flag to restore auto-refit behaviour - All pan/zoom methods set the flag Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
FitToView() no longer sets MaxViewportWidth to the fit width, so users can zoom out past the initial fit level. Update the test to assert double.MaxValue and update the comment accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Zoom range: - Blazor and MAUI slider max increased 10x → 50x (step 0.05) - Native 1:1 pixel zoom for 8192px image on 1500px canvas is ~5.46x, so 50x allows substantial upscaling to inspect tile quality Inspector - new 'Level Selection' section: - Current level and max level (e.g. '13 / 13⚠️ ') - Level dimensions (e.g. '8192 × 8192 px') - Tile actual size (e.g. '256 × 256 px' - the stored file dimensions) - Tile on screen size (pixels the tile occupies at current zoom) - Native 1:1 zoom indicator - Warning message when at max level: 'Max detail — native resolution reached, zooming further upscales tiles' Level selection explanation (why it stays at level 13 from zoom 2.73+): The testgrid is 8192×8192, max level = 13. GetOptimalLevel selects the lowest level where levelWidth > controlWidth/viewportWidth. For a 1500px canvas, level 13 (8192px) is selected at zoom ≥ 8192/1500/~2 ≈ 2.73x. Beyond that there are no higher levels, so it stays at 13 — correct. Testgrid DZI has Overlap=0 (tiles were generated without overlap pixels). The generate-dzi.cs script already defaults to overlap=1 for new images. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Inspector bug fix: - tilePxH was computed as (TileSize * Scale / levelH) * AspectRatio which is wrong for non-square images. Correct formula divides by AspectRatio: tilePxH = TileSize * Scale / levelH / AspectRatio Testgrid regeneration: - Recreated 8192x8192 source image as a colored grid pattern with cell labels (using PIL in a helper script) - Ran scripts/generate-dzi.cs with --overlap 1 to generate all 14 levels (0-13) with 1px tile overlap as the user requested - testgrid.dzi now has Overlap='1' instead of Overlap='0' - Also committed scripts/generate-dzi.cs to this branch for reproducibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the⚠️ appeared whenever level 13 was selected, even at zoom 2.73x where it is the correct level and no upscaling is happening. Now 'isUpscaling' is true only when zoom > nativeZoom (the 1:1 pixel mapping threshold, ≈ imageWidth / controlWidth). This means the warning appears only when the renderer would need a higher level than maxLevel but none exists — i.e. tiles are genuinely being upscaled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both Blazor and MAUI samples now bake the current git branch into the
assembly at build time via AssemblyMetadataAttribute('GitBranch', ...).
At runtime the URL is constructed from the embedded branch name, so:
- main builds fetch tiles from main
- PR builds fetch tiles from their own branch
Auto-detection: A 'DetectGitBranch' MSBuild target runs 'git rev-parse
--abbrev-ref HEAD' before the build. Falls back to 'main' if git is not
available or the property is explicitly overridden by CI via -p:GitBranch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previous approach added AssemblyAttribute items inside a target's
<ItemGroup> before GenerateAssemblyInfo — but GenerateAssemblyInfo
had already captured the item list at evaluation time, so GitBranch
was always 'main'.
New approach: a 'DetectAndWriteGitBranch' target (BeforeTargets=CoreCompile)
writes a small generated .cs file to the intermediate output directory and
adds it to the Compile item group at target execution time. This is picked
up by CoreCompile (the actual C# compiler), so the branch value is correct.
Also fixed:
- MAUI csproj had unclosed <PropertyGroup> tag (Bug 1)
- Detached HEAD fallback ('HEAD' → 'main') for CI shallow checkouts (Bug 3)
Verification on branch 'feature/deep-zoom-static-preview':
obj/.../GitBranchInfo.cs contains 'feature/deep-zoom-static-preview'
The DLL contains the correct branch name at offset 90216
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Reads GitHub Actions (GITHUB_HEAD_REF, GITHUB_REF_NAME) env vars - Reads Azure DevOps (BUILD_SOURCEBRANCH) env var with refs/heads/ stripping - Falls back to local git, then to 'main' - Config embedded as resource, read at runtime via GetManifestResourceStream - Removes AssemblyMetadataAttribute/GitBranchInfo.cs approach Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5.5ch was too narrow for the new 0.01-100 zoom range, still causing layout shift at 9.99->10.00 transitions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Gradient: diagonal warm (peach, 55α) → cool (blue, 55α) tint across full image
- Computed per-tile using image-space coordinates for seamless consistency at all zoom levels
- Color blocks: 1024×1024px (4×4 tiles at max zoom), no borders, pastel palette
- Level labels: 'L{level}' in top-left corner of each tile
- Close PR #381 (testgrid work consolidated here)
- Regenerate all 21,853 tiles
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Debug borders: SKColor(128,128,128,80) → SKColor(60,60,60,160) — darker gray at 63% alpha - Testgrid gradient: alpha 55 → 110 (~43%), peach/blue tones slightly deepened - Regenerate all 21,853 testgrid tiles with the stronger gradient Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add ISKDeepZoomTileCache interface with async PutAsync() for extensible caching - SKDeepZoomTileCache implements ISKDeepZoomTileCache (PutAsync calls Put synchronously) - SKDeepZoomController accepts ISKDeepZoomTileCache? in constructor (default: built-in LRU) - SKDeepZoomRenderer and SKDeepZoomTileScheduler use ISKDeepZoomTileCache throughout - Controller LoadTileAsync now awaits cache.PutAsync() enabling delay decorators - Add DelayTileCache in Blazor sample: wraps any ISKDeepZoomTileCache and adds configurable random delay (0-2500ms) to PutAsync to simulate slow tile delivery - Blazor inspector gets 'Cache Delay' section: enable checkbox + min/max sliders (0-2500ms range, 10ms step) - Convert SKDeepZoomTileId to readonly record struct (Level, Col, Row positional) - Convert SKDeepZoomViewportState to readonly record struct (ViewportWidth, OriginX, OriginY) - Both have custom GetHashCode for netstandard2.0 compatibility (no HashCode.Combine) - Add Polyfills.cs with IsExternalInit shim for netstandard2.0 record support - Set LangVersion=latest in SkiaSharp.Extended.csproj Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…city
- Add Put(id, bitmap) to ISKDeepZoomTileCache for sync operations (used by tests)
- DelayTileCache.Put() delegates to inner cache without delay (delay only on PutAsync)
- Override ToString() on SKDeepZoomTileId record struct to preserve compact format '(L,C,R)'
(record structs auto-generate verbose 'SKDeepZoomTileId { Level = L, ... }' format)
- Replace hardcoded 1024 in Blazor inspector with CacheCapacity constant
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cache support - Remove ShowDebugStats from renderer and controller (inspector replaces it) - Rename SKDeepZoomTileCache → SKDeepZoomMemoryTileCache for clarity - Add TryGetAsync to ISKDeepZoomTileCache for async tiered cache support - Controller LoadTileAsync checks TryGetAsync before network fetch - Add BrowserStorageTileCache: sessionStorage-backed L2 cache via JS interop - Redesign inspector cache section: cache mode (memory/browser), size slider, delay controls, clear button - Add RebuildControllerCache() to swap cache at runtime without page reload - Fix test compilation: rename all SKDeepZoomTileCache → SKDeepZoomMemoryTileCache - Fix Record.Exception() calls for xUnit v3 compatibility - All 436 tests pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Save TileSource BEFORE RebuildControllerCache so image is preserved after cache type/size changes - Fix double-dispose: _delayCache.Dispose() cascades to inner cache; _browserCache no longer disposed separately - Add _innerCache field to clearly track ownership in cache chain - Cap BrowserStorageTileCache._memIndex at 512 entries (tiles beyond the cap remain in sessionStorage, accessible via TryGetAsync) - PutAsync only populates memIndex on successful sessionStorage write - Remove ShowDebugStats reference from README (feature removed) - Rename SKDeepZoomTileCache → SKDeepZoomMemoryTileCache in README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Restructure overview (deep-zoom.md): architecture diagram, quick start, image source sections, programmatic navigation, events table - New deep-zoom-controller.md: full controller API, viewport coordinate system, zoom semantics, NativeZoom, SKDeepZoomImageSource/CollectionSource - New deep-zoom-caching.md: ISKDeepZoomTileCache interface, SKDeepZoomMemoryTileCache, tiered cache-aside pattern, custom cache examples, SKDeepZoomTileId docs - New deep-zoom-fetching.md: ISKDeepZoomTileFetcher, built-in fetchers, custom fetcher examples (app package, auth headers, pre-loaded) - Update deep-zoom-blazor.md: current API (IAsyncDisposable, DZC loading, custom cache, pan/zoom wiring) - Update deep-zoom-maui.md: current API, pan/zoom gestures, DZC loading - Add Deep Zoom section to toc.yml with all 6 sub-pages - Rewrite README.md: concise class reference table, minimal usage snippet, architecture notes, coordinate system explanation - Add XML doc comments to SKDeepZoomFileTileFetcher and SKDeepZoomTileRequest Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix cumulative TotalX/Y bug in MAUI pan gesture example: track delta with _lastPanX/_lastPanY instead of passing TotalX/Y directly - Add SKDeepZoomViewportState section to controller docs (with/record, save/restore, use for undo/redo and navigation history) - Add SKDeepZoomCollectionSubImage section (properties table) - Add SKDeepZoomDisplayRect section (sparse DZI regions) - Add SKDeepZoomTileRequest section (scheduler output, priority ordering) - Add EnableLodBlending to renderer options table in controller docs - Add constructor XML doc comment to SKDeepZoomCollectionSubImage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lending, reorganize docs
- Create ISKDeepZoomRenderer: pluggable renderer interface with Dispose()
- SKDeepZoomRenderer implements ISKDeepZoomRenderer; removes ShowTileBorders
(debug feature moves to sample layer via decorator pattern)
- SKDeepZoomController accepts ISKDeepZoomRenderer? in constructor
- SKDeepZoomController.Renderer type changed to ISKDeepZoomRenderer
- SKDeepZoomController.ShowTileBorders removed (use DebugBorderRenderer)
- Add SKDeepZoomRenderer.GetTileDestRect(public) for decorator use
- Create DebugBorderRenderer in Blazor sample: wraps any ISKDeepZoomRenderer,
adds ShowTileBorders + forwards EnableLodBlending to core renderer
- Add EnableLodBlending checkbox to inspector Debug section
- Move docs to docs/docs/deep-zoom/ subfolder:
deep-zoom.md → deep-zoom/index.md
deep-zoom-controller.md → deep-zoom/controller.md
deep-zoom-caching.md → deep-zoom/caching.md
deep-zoom-fetching.md → deep-zoom/fetching.md
deep-zoom-blazor.md → deep-zoom/blazor.md
deep-zoom-maui.md → deep-zoom/maui.md
- Fix all internal doc links to use new relative paths
- Rewrite toc.yml: Deep Zoom as nested items section (visible in nav)
- Update controller.md: document ISKDeepZoomRenderer, decorator pattern,
EnableLodBlending semantics (LOD blending explanation)
- Update tests: ShowTileBorders_* → EnableLodBlending_* on renderer
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove ShowTileBorders_DefaultIsFalse test (property removed from core) - Rename ShowTileBorders_DrawsBorders → Render_WithBordersEnabled_DrawsTiles - Remove renderer.ShowTileBorders = true assignments from 2 more tests - Remove duplicate Render_EmptyCache_DoesNotThrow (exact duplicate) - Rename controller Render_WithTileBorders_DoesNotThrow → Render_WithController_DoesNotThrow; remove ShowTileBorders = true - All 434 tests pass; library, Blazor sample, and MAUI sample all build Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The renderer had an else-if in Pass 2 that still drew fallback tiles when EnableLodBlending=false, making both states visually identical. Fix: remove the erroneous else-if block. When EnableLodBlending=false, Pass 1 (fallback pass) is skipped and Pass 2 leaves missing tiles blank. Visual difference is now observable by: 1. Open the Deep Zoom demo in Blazor 2. Enable Demo Cache, set delay min=500ms max=1500ms 3. Clear the cache, then zoom in to a high zoom level 4. LOD ON: blurry scaled-up parent tiles appear immediately as placeholders; sharpen tile-by-tile as hi-res loads 5. LOD OFF: white/blank rectangles where tiles are still loading; tiles pop in abruptly when loaded Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause: OnCacheDelayChanged was calling Cache.Clear() whenever the user enabled tile delay. This removed the lower-level tiles that LOD blending needs as fallbacks, making LOD on/off indistinguishable. Fix: - Remove Cache.Clear() from OnCacheDelayChanged — delay changes only affect future PutAsync calls; existing tiles must stay as potential LOD fallbacks - Add live debug info panel: shows LOD state on renderer, tiles cached, pending fetches (proves the toggle is wired through to the renderer) - Add 'Zoom to Max (LOD demo)' button: zooms to native 1:1 resolution so hi-res tiles must load — hold existing cache to preserve LOD fallbacks - Add dz-debug-info CSS class for the info panel How to see the LOD difference now: 1. Load an image, let fit-view tiles render (no delay needed at this stage) 2. Enable 'Simulate tile delay' and set to 1000-2000ms 3. Click 'Zoom to Max' (do NOT clear cache first) 4. While hi-res tiles load, toggle 'Enable LOD Blending': ON = blurry lower-res tiles fill in immediately, then sharpen OFF = white/blank rectangles until tiles pop in Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sample page changes: - Remove 'Zoom to Max (LOD demo)' button and OnLodDemo handler - Remove live debug info panel (.dz-debug-info) and CSS - Restore OnInvalidateRequired to simple canvas invalidate (no StateHasChanged) - Keep the OnCacheDelayChanged fix (no Cache.Clear when enabling delay) Docs (docs/docs/deep-zoom/controller.md): - Expand 'Renderer and LOD Blending' section with full explanation - Document the two-pass rendering strategy (Pass 1: fallbacks, Pass 2: exact) - Add 'when to use each mode' guidance with use-case table - Explain when LOD OFF is preferable: scientific/medical imaging, pixel-accurate rendering, load-performance testing, bandwidth-constrained apps - Add tip on how to see the difference interactively Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extracts the core DeepZoom static rendering system. This is the minimal set of services needed to load and render Deep Zoom collections and images without gestures, animations, or custom controls.
What's included
SKDeepZoomCollectionSource,SKDeepZoomImageSource)SKDeepZoomTileCache), scheduler (SKDeepZoomTileScheduler), and HTTP/file fetchersSKDeepZoomController) — orchestrates loading and delegates to tile servicesSKDeepZoomViewport) — centered-fit geometry (aspect ratio preserved, no cropping/stretching)SKDeepZoomRenderer) — draws best-resolution tiles ontoSKCanvasSkiaSharp.Extended.DeepZoom.TestsprojectPages/DeepZoom.razor) — minimal static preview usingSKCanvasViewDemos/DeepZoom/DeepZoomPage) — same concept, uses app-package assetsdeep-zoom.md,deep-zoom-blazor.md,deep-zoom-maui.mdWhat's NOT included (intentionally)
SKGestureTracker,SKGestureDetector, etc.)SKAnimationSpring,SKAnimationTimer, etc.)SKDeepZoomViewcontrolArchitecture
The sample page passes a URI to
SKDeepZoomController, which loads the DZI/DZC, manages tiles, and firesInvalidateRequiredwhen new tiles are ready. On each paint, the page callsSetControlSize()thenRender(). The only input is the canvas size — the image is always centered-fit.Tests
Related: #378