All notable changes to lucidVIEW are documented here. Format loosely based on Keep a Changelog, versions follow SemVer.
- HTTP-aware image cache with proper LRU and per-image metadata sidecars.
ImageCacheServicenow persistsETag/Last-Modified/Cache-Control: max-age/Expires/ fetch and access timestamps to{hash}.meta.jsonfiles alongside each cached image. Reopening a document within the freshness window skips the network entirely; stale entries send conditionalIf-None-Match/If-Modified-SinceGETs and use 304 responses to refresh metadata without re-downloading the body. Disk eviction now also unlinks the sidecar. In-memory cache uses aLinkedList<ImageCacheEntry>+DictionaryLRU with O(1) access and bounded capacity (256 entries by default). Verified end-to-end against shields.io: first load = 200 + sidecar write; second load =STALE→ conditionalFETCH→304→ cache hit (no body re-downloaded). ux-scripts/verify-{ruler-visible,render-mermaid,export-pdf}.yaml— three new harness scripts that exercise the bug-fixed code paths end-to-end.
- "Render Diagram..." → "Export Diagram..." in the side panel. The
menu was a misnomer — it opens a save file picker, not a render
preview dialog. The handler is now
async voidso any exception surfaces on the UI thread instead of disappearing into a fire-and-forget Task. The status bar now showsOpening export dialog (N diagram)…immediately on click so the user gets confirmation that the click registered, even if the native picker takes a moment to render or appears as a sheet on a different screen. Cancel feedback showsDiagram export canceled.
- Ruler regression: a previously-saved
contentMaxWidthlarger than the current window width stranded the right ruler handle off-screen.ApplyContentMaxWidthnow clamps toEffectiveMaxContentWidth = min(2000, Width − 60)and recovers automatically on launch (resetting to 900 if the saved value is out of range). The clamp is also applied on every drag/click and re-applied onSizeChangedso shrinking the window pulls the right handle back into view.
The bundled SVG renderer + svg2png CLI both jumped to v0.2.0 with significant feature and performance gains:
Features added
fill-opacity/stroke-opacityare now inheritable through the cascade and applied separately from elementopacity. This was the missing piece for the shields.io drop-shadow text trick (fill="#010101" fill-opacity=".3").- Real
LinearGradientBrush/RadialGradientBrushwith fullgradientUnits=objectBoundingBox|userSpaceOnUse, x1/y1/x2/y2 or cx/cy/r in % or units, and href stop-inheritance chains. <marker>support (arrowheads) — every SVG path command (M, L, H, V, C, S, Q, T, A, Z) is parsed by a hand-rolled tangent extractor (SvgPathEndpoints) somarker-start/marker-endrotate to match the path direction. Mermaid edge arrows now render correctly.clip-path="url(#id)"viaClipPathExtensions.Clip— supports rect/circle/ellipse/polygon/polyline/path inside<clipPath>. Shields' rounded corners no longer rely on the rect's own rx/ry.rgb()/rgba()/hsl()/hsla()color parsers added toSvgValueParser.ParseColor. Mermaid pie slice fills, flowchart edge label backgrounds, and any SVG with non-hex colors now render correctly.- Embedded DejaVu Sans font (739 KB) shipped inside the
library.
SvgRenderOptions.ForceBundledFont = true(default) ensures byte-identical text rendering across Windows / macOS / Linux instead of falling back to whatever the host has installed. SvgRenderOptions.Configuration— pass a custom ImageSharpConfiguration(e.g. PNG-only) to let the AOT trimmer drop unused encoder modules.
Performance
| Bench | Before | After | Δ |
|---|---|---|---|
| Shield (~1 KB SVG) | 911 µs / 1627 KB | 820 µs / 1211 KB | −10% time, −26% alloc |
| Mermaid flowchart | 13525 µs / 2651 KB | 13352 µs / 2172 KB | −18% alloc |
| C4 container | 36111 µs / 6113 KB | 34999 µs / 4475 KB | −27% alloc |
| Render-only (shield) | 342 µs / 1208 KB | 276 µs / 855 KB | −19% time, −29% alloc |
Hot-path optimizations:
InheritedStyle.Merge— fast path that skips dict creation entirely when the element has nostyle=""and noclass="".SvgValueParser.ParseStyle/ParseNumberList— return shared empty singletons when input is null/empty.FillAndStroke— solid-paint fast path collapses toctx.Fill(Color, IPath)instead of allocating aSolidBrush.DrawText— converts text to glyph paths viaTextBuilder.GenerateGlyphsand fills them as regular shapes, bypassing ImageSharp's heavy text-rendering pipeline. This was the single biggest win —ctx.DrawTextwas allocating ~250 KB per call internally; the path route shares the existing fill machinery and is dramatically lighter.MeasureSizeskipped whentext-anchorisstart(default).
Tooling
Mostlylucid.ImageSharp.Svg.Benchmarks— BenchmarkDotNet harness with 3-tier benchmarks (shield / mermaid / C4) plus anallocmode for per-stage allocation breakdowns.Mostlylucid.ImageSharp.Svg.Conformance— resvg-comparison harness with multi-metric reporting (MSE, % pixels close, % exact, max channel diff). Forces both renderers to use the bundled DejaVu Sans for fair text-fidelity comparison.
svg2png v0.2.0 inherits all of the above through its
ProjectReference to the library. The single AOT-published native
binary stays at ~10 MB and remains the smallest cross-platform SVG
rasterizer in the .NET ecosystem.
Mostlylucid.ImageSharp.Svg— a small, low-allocation, AOT-clean SVG rasterizer built on SixLabors.ImageSharp. Lives at the repo root as a standalone library (~1100 lines, 7 files). No SkiaSharp, no native binaries, no reflection. Hand-rolled XmlReader-based parser → AST → ImageSharp draw calls. Honours the cascade for inheritable presentation attributes (<g>parents,style="", CSS class selectors from<style>blocks), parses every CSS color form (hex, named,rgb(),rgba(),hsl(),hsla()), supportsviewBox, transforms (translate/scale/rotate/skew/ matrix), and the full SVG element subset that shields.io and Naiad/mermaid emit (rect/circle/ellipse/line/path/polygon/polyline/text/g). Publishes clean underdotnet publish -p:PublishAot=true— verified at 1 MB native dylib, zero trim warnings.svg2pngAOT command-line tool (released separately, see the svg2png-v0.1.0 release). Single 9.7 MB native binary, no .NET runtime required. Reads SVG from disk or stdin, writes PNG to disk or stdout, supports--scale,--background,--quiet. Built from the newMostlylucid.ImageSharp.Svg.Cliproject. Cross-platform: win-x64, linux-x64, osx-x64, osx-arm64.- Local SVG markdown images are now rendered through the new managed
rasterizer.
MarkdownService.ProcessImagePathsdetects.svgpaths and routes them toImageCacheService.CacheLocalSvg, which converts to PNG viaMostlylucid.ImageSharp.Svgand rewrites the markdown to point at the cached PNG. Cache key includes the source file's mtime so editing the SVG on disk re-renders automatically on the next document open. - Animated GIF / WebP / animated PNG playback in the markdown renderer.
LiveMarkdown.Avalonia uses Avalonia's static
Bitmapfor images which only decodes the first frame, so animated formats appeared as one static frame. AddedAnimatedImage.Avaloniaand a post-render visual-tree visitor that findsImagecontrols in the document and promotes the ones whose markdown source URL is*.gif|*.webp|*.apngto the animated source viaImageBehavior.SetAnimatedSource. Loop count is respected viaRepeatBehavior.Default(the file's embedded loop count). Verified end-to-end viaux-scripts/verify-gif-playback.yaml— captures four frames spaced 600ms apart and confirms the md5s differ. - Restart-animation overlay button on each animated image. 28×28
circular button in the top-right corner of the image, attached via the
visual tree alongside the image. Click → clears
AnimatedSourceand immediately re-sets it, restarting the animation from frame 0. Pause/resume isn't possible becauseAnimatedImage.Avaloniadoesn't expose a public play/pause API; clearingAnimatedSourcewould blank the image, which is worse than no pause at all.
- Five SKSvg sites in lucidview replaced with the new managed
rasterizer:
ImageCacheService.ConvertSvgToPng(shields/badges)MarkdownService.RenderMermaidToPng(in-app mermaid PNG fallback)MarkdownService.ExportMermaidToPngBytes(PDF export pipeline)FlowchartLayoutBenchmark.RenderFullPipeline(test harness)- Local
.svgmarkdown image loading (was previously handled byLiveMarkdown.Avalonia.Svg.Skia)
Svg.Skiaand theLiveMarkdown.Avalonia.Svg.Skiaplugin removed fromMarkdownViewer.csproj. The entireSvg.Skia/Svg.Custom/Svg.Model/ShimSkiaSharpfamily is gone from the resolved dependency graph (dotnet list package --include-transitiveconfirms this for bothMarkdownViewerandMarkdownViewer.Tests). Trims a meaningful chunk of the Release single-file payload — the newMostlylucid.ImageSharp.Svg.dllis 31 KB, replacing several MB of Svg.Skia + ShimSkiaSharp + Svg.Custom + Svg.Model.
- Shields/badges no longer render at column width. Two distinct
bugs were collapsing into the same visible symptom. (1) The cache
rewriter was emitting
file://URIs for resolved cache paths, which LiveMarkdown'sLocalFileAsyncImageLoaderHandlersilently rejects — fixed by emitting plain absolute paths. (2) PNGs cached at 2× their intrinsic size for hi-DPI crispness were being displayed pixel-for-DIP by Avalonia, doubling on-screen size — fixed byImageCacheServicerecording the SVG's natural 1× dimensions and a post-render visual-tree walker (MainWindow.ScheduleConstrainCachedImages) settingWidth/Heighton each cachedImagecontrol to the natural size, so the bitmap downscales at composite time and looks crisp. - Shields re-fetch on every document open. Added
ImageCacheService.InvalidateInMemoryCache(), called fromLoadFile/LoadFromUrlso dynamic shields (build status, latest version, downloads) reflect the current state instead of whatever was cached when the app started.
- Open File / Save File / Export PDF / Export Diagram dialogs no longer
stack on top of each other. macOS was firing the click path AND the
IActivatableLifetime.Activatedevent in the same flow, opening two pickers — the second one underneath the first and unclickable. Added a_filePickerOpenre-entry guard around everyStorageProvidercall so the dialog is one-at-a-time regardless of which path triggered it. Applies to:OpenFile,ExportPdf,ExportMermaidDiagram,ExportAllMermaidDiagrams,SaveDiagramAs.
- Ruler architecture rewritten — alignment is now layout-driven, not math.
Ripped out the entire
UpdateRulerHandlesFromWidth/TransformToVisual/RulerCanvas/ scrollbar / gutter / scale / fallback math. The ruler bar is now aGridinside theLayoutTransformControl, in aStackPanelabove the documentBorder. The bar'sWidthis bound to the Border'sWidthvia XAMLElementNamebinding, so they are always the same logical width. Handles useHorizontalAlignment=Left/Rightwith negative margins to extend slightly past the column edges. Avalonia's layout engine handles every alignment concern: scrollbar, centering, scale transform, gutter, padding — all gone, all replaced by one ElementName binding. - Code-behind shrank from ~230 lines to ~85 lines. The remaining handlers
do exactly three things: change
Border.Widthon drag, changeBorder.Widthon click, update the readout text. - Zoom slider, font size, fit modes — they all "just work" because the
ruler is inside the same
LayoutTransformControlas the document, so it scales with the column automatically. NoRefreshRulerForScaleChangeplumbing required.
- Release build does NOT include
Mostlylucid.Avalonia.UITesting. The Debug-onlyCondition="'$(Configuration)' == 'Debug'"on the<PackageReference>is honoured by MSBuild andobj/Release/contains no UITesting traces. The recent ~5 MB Release size growth (~73 MB → ~78 MB) is fromFluentAvaloniaUIwhich IS a Release dependency, added in v2.1.1 for the polished button styling.
- Ruler handles align with the actual card outline at every scale. The
Border had
HorizontalAlignment="Center"which made it shrink to fit the content's natural width rather than honourMaxWidth. So the visible card was sitting at the content width (e.g. 616px), not the configured 900px column, and the handles were correctly placed on the configured edges — which were nowhere near the visible card edges. Switched toBorder.Width = ContentMaxWidth(an explicit width, not a cap) so the Border is forced to the column width regardless of content. - Width persists immediately on drag.
OnRulerHandleDragDeltanow calls_settings.Save()after each delta. The settings file is tiny so the write cost is negligible. Survives unexpected app exit, not just a clean window close.
Mostlylucid.Avalonia.UITestingis now consumed via NuGet (1.1.0) instead of a<ProjectReference>to the local lucidRESUME working tree. Decouples the lucidVIEW Debug build from in-progress lucidRESUME edits. The package is still gated to Debug-only so Release builds stay tiny and AOT-friendly.
- Added a
LUCIDVIEW_RULER_DEBUG=1environment variable that prints the computed ruler alignment values toConsole.WriteLineon every layout update. Avoids having to record large GIFs to diagnose alignment drift. - All ruler logic now reads/writes
MarkdownContentBorder.Widthinstead of.MaxWidth(drag handler, click handler, scale refresh, layout subscription, width readout).
- Ruler handles now sit exactly on the visible Border outline at all
zoom levels. Replaced the hand-rolled position math with
TransformToVisual(MarkdownContentBorder → RulerCanvas)so the handles use the actual rendered geometry of the card edge. No more drift from scrollbar widths, content alignment, ScaleTransform, gutter padding, or inner Padding offsets — the ruler asks Avalonia where the Border is. - Removed the dotted ghost guides entirely. The card border itself is the only vertical reference now. The user complaint was that the dotted guides and the visible border were two different lines that drifted out of alignment; now there is exactly one line.
- Document no longer jumps to the right when zooming out.
RenderedScrollergotHorizontalContentAlignment="Center"andMarkdownLayoutTransformgotHorizontalAlignment="Center"so the centred Border stays centred regardless of how the LayoutTransform scales it. Previously the ScrollViewer aligned the shrunken content to top-left. - Ruler is now zoom-aware.
GetMarkdownScale()reads the liveScaleTransformfromMarkdownLayoutTransformand the ruler math multiplies by it when positioning handles. The handles follow the visible text edges as the user changes font size or zoom slider, not the unscaled logical width. - Click-anywhere-on-the-ruler snaps the column width to that point. Both edges move symmetrically since the column is centered. Lets the user set the width with one click instead of fiddling with two handle drags.
- Built-in MainWindow ruler now updates whenever the Border bounds
change via a
BoundsPropertysubscription onMarkdownContentBorder. Window resize, font-size change, zoom slider, manual MaxWidth — all trigger an automatic re-position.
OnRulerHandleDragDeltanow divides the drag vector by the current scale, so dragging in window pixels translates to the right delta on the underlying logicalMaxWidth.- Padding bumped from 40→48 to give the new card border breathing room around the text, with the math constant updated in lock-step.
- Two upstream errors fixed in
Mostlylucid.Avalonia.UITesting:Pointerambiguity inPointerSimulator.cs(qualified toglobal::Avalonia.Input.Pointer) and unimplemented MCP wheel/pinch/ rotate/swipe/touch handlers (stubbed as “not implemented yet” instead of unresolved methods).
- README rewrite — actually showcases lucidVIEW now. Hero screenshot, 6-theme gallery from the user manual, copy-paste install commands per platform, full keyboard-shortcut table, link to the in-app user manual. The Naiad fork section is still there but moved below the lucidVIEW feature pitch.
- Subtle document border —
MarkdownContentBordernow has a 1pxAppBorderSubtleoutline +CornerRadius="4"+ 16px vertical margin. Document looks like a card now instead of text floating in space. The border colour is per-theme so it's barely-visible brightening on dark themes and barely-visible darkening on light themes. ux-scripts/capture-ruler.yaml— UI test that toggles the ruler on/off and captures the two states for the user manual.- New section 11. Word-style ruler in the in-app User Manual, with the ruler-off and ruler-on screenshots. Subsequent sections renumbered 12–19.
- Ruler handles now sit at the actual text edges, not the Border edges.
Previously the
MarkdownContentBorderhadPadding="40,32"so the handles appeared with a ~40px gap on each side of the column they were supposed to resize. Ruler math now subtracts the horizontal padding when placing the handles, the highlighted track, the width readout, and the dotted side guides. Padding was also bumped from 40→48 to match the new card border. - Ruler bar now spans the full window width. Was being truncated by the 18px left gutter; reordered the DockPanel so the ruler is docked Top before the gutter claims its space. The gutter still appears under the ruler in the document area.
- Image scaling regression — re-added
StretchDirection="DownOnly"alongsideStretch="Uniform". Large images shrink to fit the column, small badges stay at natural size (no more bloat).
- Document border —
MarkdownContentBordergot a 1pxBorderBrush="{DynamicResource AppBorderSubtle}"outline,CornerRadius="4", and a 16px vertical margin. Gives the document a card feel so the text doesn't look like it's floating in space. TheAppBorderSubtlebrush is per-theme so the outline is barely-visible brightening on dark themes and barely-visible darkening on light themes. - Border padding bumped from
40,32to48,40to give the new outline a little breathing room around the text.
.gitignorenow ignores.idea/(and**/.idea/) and.DS_Storefolders properly. The 10 stale.idea/*.xmlfiles inlucid.viewer/that had been tracked by mistake are now untracked (kept on disk).
- FluentAvaloniaUI adopted as the base theme. Buttons get proper hover/press feedback, the side panel and header look notably more polished. ContentDialog available for future Settings dialog upgrade.
- FluentIcons (Microsoft Fluent UI System Icons) replace the hand-curated
boxicons set. 1,800+ icons available via
<ic:SymbolIcon Symbol="..."/>— themed automatically, scale viaFontSize, no inline path data. - Word-style ruler above the document with two draggable margin handles
and dotted vertical column guides. Toggle via the ruler button in the
header. Drag a handle to live-resize the content column; the new width
persists to
AppSettings.ContentMaxWidth. Default off.
- macOS Open With — double-clicking a
.mdfile in Finder launched lucidVIEW but never loaded the file. Wired upIActivatableLifetime.Activatedso file paths delivered via Apple Events reachLoadFile(). Same hook handles iOS / Android / Linux MIME activation. - Image cropping —
<Style Selector="Image">was settingStretch=Nonewhich clipped any image wider than the column. Now usesStretch=Uniformso images scale to fit the column width without cropping, preserving aspect ratio. ContentMaxWidthsetting is now actually honoured. Previously the XAML hard-codedMaxWidth=1200and ignored the persisted value.
-
Open URLHTTP client User-Agent bumped fromlucidVIEW/1.0tolucidVIEW/2.1. TheAccept: text/markdownheader is preserved (priority q=1) so Cloudflare URL→markdown conversion, Jina Reader, and similar services return markdown instead of HTML. -
Font selector in Settings dialog stops showing the raw
avares://lucidVIEW/Assets/Raleway-Regular.ttf#Raleway, Segoe UI, ...URI as a dropdown entry. The bundled Raleway is now listed as "Raleway (bundled)" at the top, the URI is parsed back when saving. Each font name in the dropdown is rendered in its own typeface (Word/Office style live preview) viaFontFamily="{Binding}"on the ItemTemplateTextBlocks. Same fix applied to the code font dropdown.
- New
Styles/Icons.axamlremoved — replaced wholesale by FluentIcons. Borderwrapping the markdown is now namedMarkdownContentBorderso the ruler can manipulate itsMaxWidth.- New
MainWindowregions: File Activation (Open With handler) and Word-style Ruler (drag math + persistence). MainWindow.axaml.csgot anAvalonia.Controls.ApplicationLifetimesusing.SettingsDialog.ExtractDisplayName()parses FontFamily strings (avares URIs, comma-separated lists, plain names) into clean dropdown labels.
- Self-documenting in-app User Manual (
F1) — bundled atmanual/user-manual.mdnext to the binary, with 17 screenshots automatically captured by the UI testing harness viaux-scripts/capture-manual.yaml. The manual covers every feature with real screenshots that always match the current build. Re-generate by running the capture script.Shift+F1still opens the README. - Pride theme — a celebration palette using the Pride flag colors as borders, accents, and section dividers. Tasteful enough to read code in, bright enough to show off.
- Configurable custom theme — define your own palette in
settings.jsonundercustomThemeand select Custom from the side panel. Seedefault-settings.jsonfor the schema and a Solarized-ish example. The Custom theme card only appears when a custom theme is configured. - UI polish — header bar got a 2px accent-coloured bottom border, font A buttons are now grouped in a rounded pill with proper spacing instead of cramping against the macOS window controls.
- Real Print function (
Ctrl+P) — sends the current document to the OS default printer. Generates a temp PDF via QuestPDF then hands it to ShellExecute (printverb) on Windows orlp(CUPS) on macOS/Linux. - macOS
.appbundle —pwsh ./publish.ps1 -Platform osxnow produceslucidVIEW.appwith properInfo.plist,Resources/lucidVIEW.icns, ad-hoc codesigning, and file-type associations for.md/.markdown/.mdown/.mkd. Double-clicking the bundle launches the GUI directly with no Terminal window and a Dock icon. Bothosx-x64andosx-arm64are produced. The release CI workflow assembles the same bundle. - Windows Store (MSIX) prep —
pack-msix.ps1produces a signablepublish/store/lucidVIEW.msixready for Partner Center upload. ThePackage.appxmanifestnow uses build-time identity tokens, theMarkdownViewer/Assets/Store/folder ships eight tile/logo PNGs, and a manual-dispatchstore-publish.ymlGitHub Actions workflow builds the MSIX onwindows-latest. Full setup walkthrough indocs/windows-store.md. - UI testing harness (Debug only) — Wired up
Mostlylucid.Avalonia.UITestingvia--ux-test,--ux-repl,--ux-mcpstartup flags. Three YAML scripts inux-scripts/exercise every non-dialog function, every dialog, and the hand-driven REPL/MCP modes. The harness is excluded from Release builds via a Debug-only<ProjectReference>and#if DEBUGguards — Release binaries stay tiny and AOT-capable. NavigateCommandonMainWindow(Debug-only consumer; routes toLoadFile). Lets the YAML test scripts self-bootstrap by loading a fixture markdown file via the standardNavigateaction.docs/macos-bundle.md,docs/windows-store.md, andux-scripts/README.md.
Ctrl+Pis now real Print, not Export PDF. Export PDF moved toCtrl+Shift+P(also still available via the side-panel menu). The side panel now lists "Print (Ctrl+P)" above "Export PDF... (Ctrl+Shift+P)".publish.ps1addsosx-arm64and reworks the platform list —osxshorthand now builds both Intel and Apple Silicon variants.release.ymlworkflow assembles macOS.appbundles before zipping the artifacts so downloads launch as GUI apps on the receiving Mac.- Bumped
MaxVersionTestedin the Store manifest from10.0.22621.0to10.0.26100.0(Windows 11 24H2).
- Pre-existing build break in
Naiad/QuadrantParser.cs:c is not '-' or ' 'parses as(c is not '-') or (c is ' ')which is always true. Corrected toc is not ('-' or ' '). Fixes CS9336 redundant-pattern errors that were blocking all builds on the .NET 10 SDK. - Pre-existing security warning NU1903 —
Tmds.DBus.Protocol0.21.2 (transitive via Avalonia) had a high-severity vulnerability (GHSA-xrw6-gwf8-vvr9). Pinned to 0.92.0. - Wrong
ExecutableinPackage.appxmanifest— wasMarkdownViewer.exe, the real assembly islucidVIEW.exe. The MSIX would have failed at launch. - xUnit2013 warning in
AvaloniaNativeDiagramRendererPluginTests— replacedAssert.Equal(0, count)withAssert.Empty(...).
- Refactored
PdfExportService— addedExportToTempAsyncso the new Print path can hand a temp PDF to the print service without duplicating the PDF-build logic. - Added
MarkdownViewer/Services/PrintService.cs. - All MSBuild changes that reference UITesting are conditional on
'$(Configuration)' == 'Debug'so the Release pipeline doesn't even see the testing harness. - Fixed
UseUITesting()upstream bug inMostlylucid.Avalonia.UITesting: the extension hookedAfterSetupwhich fires beforeApp.OnFrameworkInitializationCompletedsetsMainWindow, soUITestingStartup.AttachToApplicationwould silently bail. Re-routed through theIClassicDesktopStyleApplicationLifetime.Startupevent so the harness only attaches onceMainWindowactually exists. Without this fix every--ux-test/--ux-repl/--ux-mcpinvocation was a no-op. ThemeServicelearned to render a runtimeThemeDefinition(not just the static built-ins), used for bothAppTheme.Customand any future config-driven themes.MainWindow.NavigateCommand(Debug-only consumer) routes the YAMLNavigateaction toLoadFile.
Earlier releases (v0.0.x → v1.0.1) predate this changelog. See the GitHub Releases page.