svs 1.2.0
svs: ^1.2.0 copied to clipboard
Streams and renders Aperio SVS whole-slide pathology images: pan/zoom, LOD tile streaming, JPEG & JPEG2000, no full-image load.
1.2.0 #
- Web support.
svsnow runs on Flutter Web, with the same API surface as native platforms — see the README's new "Platform support" section for the (small) list of things that differ.openjpeg_ffibumped to^0.3.0, whose WebAssembly build supplies JPEG2000 decode/encode on the web; everydart:io/dart:isolateusage in this package is now behind a conditional export, mirroring the patternopenjpeg_ffiitself already uses (export 'x_stub.dart' if (dart.library.io) 'x_io.dart';). SvsFile.openBytes(Uint8List bytes): opens a slide already fully in memory — the entry point for the web (no filesystem path to giveSvsFile.open), and also usable natively for bytes that came from somewhere other than a local file (e.g. a network fetch).SvsFile.path(andSvsFileInfo.path) is nowString?, null for a file opened this way — the one source-breaking change in this release, and only reachable if calling code stored.pathin a non-nullableString.DiskTileCacheand the isolate-backedTileWorkerPooltile-fetch path are native-only (dart:io/dart:isolatehave no web equivalent). Neither needs any code change to accommodate the web:SvsImageView'sLodControlleralready fell back to fetching/decoding tiles on the calling isolate whenever the worker pool wasn't available, which is exactly what happens on the web now (matchingopenjpeg_ffi's own web behavior — its JPEG2000 decode has no background-isolate path there either).DiskTileCache.openstill exists as a type on the web (soSvsImageView(diskCache: ...)keeps compiling) but throws if actually called; omit it (the default) and the in-memoryTileCachestill applies.exportSvsRegionToFile,exportAssociatedImageToFile,exportSvsLevelToFile,exportSvsRegionAsSvsToFile, andexportSvsRegionAsSvsPreservingLevelsToFile— every export function that writes to a filesystem path and returns aFile— are native-only and simply aren't declared on the web (aFile-typed return can't exist there). Use their byte-returning siblings (exportSvsRegion,exportAssociatedImage,exportSvsLevel,exportSvsRegionAsSvs,exportSvsRegionAsSvsPreservingLevels, all unchanged) plus your own browser-download mechanism instead.exportSvsRegionAsSvs/exportSvsRegionAsSvsPreservingLevels(the byte-returning pyramid-export functions) now build their output pyramid in an in-memory buffer throughout, instead of streaming through a temporary file on disk and reading it back at the end. Both already fully materialized the result into memory before returning it, so the observable output is unchanged — but peak memory during construction is now closer to the final output size for the whole export, not just at the very end. For an export large enough that this matters, prefer the (still disk-streamed, native-only)*ToFilevariants.- Deflate/AdobeDeflate-compressed associated images (TIFF
Compression8/- now decode via
package:archive'sZLibDecoderinstead ofdart:io's — pure Dart, works on every platform including the web, and decodes identically (only reachable for label/macro/thumbnail strips; pyramid tile levels never use Deflate).
- now decode via
example/now picks a file viapackage:file_pickerand opens it withSvsFile.openByteswhen running on the web (kIsWeb), instead of the path-TextFieldit already had for native.
1.1.0 #
openjpeg_ffibumped to^0.2.0, andexportSvsRegionAsSvs/exportSvsRegionAsSvsToFilegain acompressionparameter (SvsExportCompression.jpeg, the previous/default behavior, or.jpeg2000, via that version's newencodeJ2k) to encode the exported pyramid's tiles as JPEG2000 instead of JPEG — mathematically lossless by default, or lossy at a chosenjp2kCompressionRatio, typically a meaningfully smaller file than JPEG at comparable visual quality.exportSvsRegionAsSvs/exportSvsRegionAsSvsToFilegain amatchSourceCompressionparameter (defaultfalse): whentrue, the crop's compression/quality is derived from the source level being cropped instead of this function's own fixed defaults (JPEG quality 90, or mathematically lossless JPEG2000) — the same compression scheme the source already uses, plus either the source's own JPEG quality (parsed from itsImageDescription, e.g.Q=70) or an equivalent JPEG2000 ratio (estimated by sampling the source's actual on-disk tile sizes, since Aperio doesn't record that ratio). Without it, a small crop re-encoded at this function's own defaults could end up larger than the corresponding region of the source file, if the source itself was actually encoded leaner (real slides are commonly scanned atQ=70-80, notQ=90, or a lossy JP2K ratio, not lossless).tileSizeis now optional (int?, previously a non-nullable256default): left unset, it still resolves to 256 as before — unlessmatchSourceCompressionis alsotrue, in which case it resolves to the source level's own tile edge length instead, so a source scanned on a non-256 tile grid keeps that grid in the crop too.exportSvsRegionAsSvsPreservingLevels/exportSvsRegionAsSvsPreservingLevelsToFile: crops the same way asexportSvsRegionAsSvs, but builds each output pyramid level by cropping directly from the matching source level (level,level + 1, ... up to the source's coarsest) instead of decoding justleveland re-deriving every coarser level by halving it down 2x at a time. Real Aperio pyramids often don't step by a clean 2x between levels (e.g. a downsample sequence of 1x, 4x, 16x);exportSvsRegionAsSvsalways produces a 2x-stepped pyramid regardless, while this new function reproduces the source's real level count and downsample steps exactly (scaled to the crop's own extent). Accepts every parameterexportSvsRegionAsSvsdoes, includingmatchSourceCompression.
1.0.3 #
- Fixed: a right/bottom-edge tile whose JPEG decode legally comes back
smaller than its pyramid level's nominal tile size (unpadded — some
encoders don't pad boundary tiles up to a full block) used to get stretched
to fill the full nominal-size destination rect anyway, distorting slide
content near the level's true edge; since each level's width/height leaves
a different remainder past its last full tile, the stretch factor — and so
the visible distortion — differed level to level.
SvsImageViewnow sizes each tile's destination rect from that tile's own decoded dimensions instead of assuming every tile is nominal-sized. - Fixed:
exportSvsRegionAsSvs/exportSvsRegionAsSvsToFilenever wrote a thumbnail into the cropped.svsfile they produced, so reopening one withSvsImageViewnever showed a minimap (no associated image ofAssociatedImageKind.thumbnailfor it to find). The exported file now carries a thumbnail — the coarsest generated pyramid level's own image, reused rather than re-decoded — same as a real Aperio file's own thumbnail. The same export also now carries over the source file's label/macro associated images (copied byte-for-byte, no decode/re-encode needed — they describe the whole physical slide, unaffected by the crop) and every otherImageDescriptionfield the source carried (Filename, Date, Time, User, ScanScope ID, etc. — previously onlyAppMag/MPPsurvived), excluding the handful of fields (Left/Top/OriginalWidth/OriginalHeight) that describe the source image's position within the original slide and would be wrong once carried into a crop. Both are on by default but optional — newincludeLabelAndMacroImages/includeSourceMetadataparameters onexportSvsRegionAsSvs/exportSvsRegionAsSvsToFile(defaulttruefor both) let a caller opt out of either, e.g. before sharing a crop outside the context that made the original slide's label or scanner details meaningful. - The zoom-percentage HUD chip's tap-to-explain dialog (added in 1.0.2) is
removed — it added interaction surface for a detail most integrators don't
need explained in-app. In its place, a new chip shows how much of the
whole slide the viewport currently covers (100% at the initial/minimum
zoom, shrinking as the view zooms in) — a more broadly useful piece of
context than the removed dialog. The magnification chip (when the slide's
AppMagis known) is unchanged aside from also losing its tap dialog.
1.0.2 #
- Fixed: a JPEG-compressed slide tile/associated image whose TIFF
PhotometricInterpretationsaysRGB(literal RGB samples, not YCbCr-encoded — some Aperio files) used to come back with a visible color cast, most noticeable as a gray/lavender tint across the slide's background —dart:ui's JPEG codec has no visibility into that TIFF-level tag, so it always applied a YCbCr->RGB transform the samples never needed, and the previous fix (inverting that transform on the decoded pixels) couldn't recover channels the wrong transform had already clipped at 0 or 255, which near-white background pixels routinely are. Now fixed at the source: an Adobetransform=0marker is inserted into the JPEG bytes before decode, so the codec skips the color transform entirely and decodes the true samples losslessly. - Fixed: a JPEG tile at a pyramid level's right or bottom edge is padded up
to the full nominal tile size by the encoder (JPEG requires whole-block
data); that padding was being drawn at full size along with the rest of
the tile, bleeding a strip of stretched/duplicate-looking content past the
level's true edge into what should be empty space beyond the slide.
SvsImageViewnow clips each level's tiles to that level's real extent. SvsImageView.fit(SvsImageFit.contain/cover) andbackgroundColor: control how the initial view fits a slide whose aspect ratio doesn't match the viewport's —contain(the default, previous/only behavior) letterboxes;coverfills the viewport completely, cropping the slide's edges instead.backgroundColor(default unchanged) sets the fill for any letterboxed or not-yet-decoded area, so it can be made to match the surrounding UI. Also now documented on the class itself as expected behavior, not a rendering bug.- The zoom-percentage HUD chip is now tappable, showing a short explanation
of what the percentage means; when the slide's scan magnification
(
AppMag) is known, a second, also-tappable magnification chip (e.g. "20x") sits next to it.
1.0.1 #
exportSvsRegionAsSvs/exportSvsRegionAsSvsToFile: no pixel-count limit by default anymore — crop any size region, including a whole slide's full extent. The oldmaxPixelssafety cap existed because the previous implementation decoded the entire requested region into one full-resolution in-memory buffer before doing anything else; the export is now built by streaming the source band-by-band straight to the output file, so memory use stays bounded bytileSize * widthrather than the full crop area.maxPixelsis still accepted (now optional, defaultnull) for a caller that wants to opt back into a fail-fast size budget.exportSvsLevel'smaxPixelsdefault changes the same way, for consistency, though its underlying flat-raster output still needs the whole level in memory regardless — preferexportSvsRegionAsSvsToFilefor a very large export.exportSvsRegionAsSvs/exportSvsRegionAsSvsToFile/exportSvsRegion/exportSvsRegionToFile/exportSvsLevel/exportSvsLevelToFile/readSvsRegion: newonProgressparameter (0.0-1.0), invoked as the export/decode progresses.
1.0.0 #
- Fixed: with an
annotationControllerattached, a two-finger pinch could occasionally fail to zoom at all (ScaleGestureRecognizercomputingscale == 1.0despite a finger clearly moving), even indrawMode.nonewhere pan/zoom is supposed to work normally.GestureDetector's ownonTapUp(aTapGestureRecognizer) and the pan/zoomonScaleStart/Update/End(aScaleGestureRecognizer) were both live for the same pointer, and having the two recognizers compete in the same gesture arena made the scale recognizer's own math unreliable. Tap detection no longer goes through a separateTapGestureRecognizerat all — it's synthesized from rawListenerpointer events instead (which don't participate in the gesture arena), so there's nothing left forScaleGestureRecognizerto compete with. SvsFile.readInfo,SvsLevel.readAllTags/readTags,SvsAssociatedImage.readAllTags/readTags: read every TIFF tag on any level/associated-image IFD (or just a chosen subset), decoded regardless of type (ints, ASCII, RATIONAL/SRATIONAL, FLOAT/DOUBLE, raw bytes) — a full structured dump of the file's own metadata, alongside the existing narrower accessors (metadata.raw, level/associated-image geometry) for callers that only need specific fields.SvsFileInfo/SvsIfdInfocarry the result, with anamedTagsgetter (via the newtiffTagName) for presenting tag IDs as human-readable names.SvsImageAdjustments: brightness/contrast/shadow/highlight adjustment, applied identically live (SvsImageView.adjustments, GPU-accelerated — cheap to change every frame) and on export (everyexportSvs*/encodeSvsImagefunction's newadjustmentsparameter) — both derive from the same affine transform, so the two are always in sync.exportSvsRegionAsSvs/exportSvsRegionAsSvsToFile: crop a region and re-encode it as a brand new, valid multi-level pyramidal.svsfile (a tiled BigTIFF with JPEG-compressed tiles and an Aperio-styleImageDescription) instead of a single flat raster image — the result can be reopened withSvsFile.open, by this package or any other tiled-TIFF/ OpenSlide-aware tool, and panned/zoomed like any other slide.SvsImageView.showMinimap/showZoomLevel/showScaleBar: independently toggle the minimap, the zoom-percentage HUD text, and the physical scale bar — all default totrue(unchanged behavior).showMinimap: falseskips decoding the slide's thumbnail entirely, not just hiding it.- Fixed:
SvsImageView's zoom-percent HUD could jump to a garbage value while drawing a point/polyline/polygon annotation. Only rectangle mode used to suppress pan/zoom while drawing; point/polyline/polygon mode placed vertices via tap but left the underlying pan/zoom gesture live underneath, so a quick run of taps could occasionally be misread as a pinch. Pan/zoom is now fully suppressed (GestureDetector's scale callbacks are nulled out entirely, not just short-circuited) for the whole time any annotation shape is being drawn, and takes effect on the very next gesture even ifdrawModechanges with no other rebuild in between. - Fixed:
SvsFile.readInfo()/readAllTags()/readTags()could throw (or, for a corruptedcountfield, attempt a multi-gigabyte read) if any tag anywhere in the file had an unrecognized TIFF field type — aborting the entire "best-effort full dump" over one bad tag. Both now surface as a short<unreadable: ...>placeholder for that one tag instead. - Fixed: concurrent
DiskTileCache.put()calls for different tiles (routine during fast pan/zoom, since decoded tiles persist to disk unawaited) could race on the shared byte-budget accounting and eviction, letting the cache temporarily grow pastmaxBytesby more than the documented single-oversized-tile allowance. The accounting/eviction half ofputis now serialized against itself. exportSvsRegionAsSvsnow builds the pyramid (downsampling, JPEG encoding, the BigTIFF write) on a background isolate instead of blocking the calling isolate — previously a large crop could freeze the UI for the whole encode.LodControllerno longer persists prefetch-margin tiles (only ever fetched speculatively, not yet on screen) to the disk cache — only tiles actually visible when decoded are, roughly halving disk I/O during fast panning.- Internal:
TileCacheandDiskTileCachenow share their LRU eviction policy (pickEvictions) instead of maintaining two independent copies of the same algorithm.
0.3.0 #
SvsAnnotationController,SvsAnnotation: draw and manage point, rectangle, polyline, and polygon annotations over anSvsImageView, anchored in level-0 pixel space so they stay put across pan/zoom. Pass a controller toSvsImageView.annotationControllerto render its annotations and route pointer gestures to it whiledrawModeisn'tSvsAnnotationDrawMode.none— point mode commits on tap, rectangle mode draws on drag, polyline/polygon mode adds a vertex per tap (callfinishPath()to commit). Tapping in view mode (drawMode.none) hit-tests and auto-selects an existing annotation, and fires the newSvsImageView.onAnnotationTapcallback. Annotations round-trip to JSON viaSvsAnnotationController.toJsonList/loadFromJsonList.measureAnnotation: physical length (and, for rectangles/polygons, area) of anSvsAnnotation, computed from the slide's microns-per-pixel.SvsImageViewshows this as a live label on line/rectangle/polygon annotations — including the one being drawn, so drawing doubles as a ruler — toggle with the newSvsImageView.showMeasurements.DiskTileCache: an opt-in persistent tile cache. Pass one to the newSvsImageView.diskCacheand decoded tiles are read from disk first (and written back after a fresh decode), so re-viewing the same region of a slide — even across app restarts — skips the tile fetch and, for JPEG2000 slides, the wavelet decode. Byte-budgeted and LRU-evicted like the existing in-memoryTileCache.
0.2.0 #
readSvsRegion: crops an arbitrary rectangle of any pyramid level to a single composited image, stitching together only the tiles it overlaps. The rectangle may hang off the level's edges; the out-of-bounds part decodes transparent.encodeSvsImage,exportSvsRegion,exportAssociatedImage,exportSvsLevel: encode a decoded image (a crop, an associated image, or a whole pyramid level) to PNG, JPEG, BMP, TIFF, or WebP bytes, via the newimagedependency.exportSvsLevelguards against accidentally compositing/encoding a gigapixel level whole. Each has a...ToFilecounterpart (exportSvsRegionToFile,exportAssociatedImageToFile,exportSvsLevelToFile) that writes straight to a path instead of returning bytes.
0.1.0 #
- Initial release.
SvsFile: opens Aperio SVS (and generic tiled TIFF) files — resolution pyramid levels, associated images (thumbnail/label/macro), and parsed Aperio metadata (magnification, microns-per-pixel).SvsImageView: a pan/zoom widget streaming only the tiles the current viewport needs, at the resolution level matching the current zoom. Includes a minimap, zoom percentage, and a physical (µm/mm) scale bar.- JPEG (
Compression=7) and JPEG2000 (Compression=33005) tile decoding — JPEG2000 via theopenjpeg_ffipackage. - Tile fetch and JPEG2000 decode run on background isolates; a memory-budgeted LRU tile cache responds to OS memory-pressure signals and actively cancels in-flight requests for tiles scrolled out of view.