Documentation
¶
Overview ¶
Package imaging reads, decodes, EXIF-orients, and caches the image files PicFetch displays: JPEG, PNG, GIF (including animated), WebP, BMP, TIFF, ICO, XPM, HEIC, AVIF, SVG, and camera RAW (embedded JPEG preview only — see raw.go).
SVG is the one vector format here and the only one whose pixels are not fixed at load: LoadedImage carries the parsed Vector alongside its first raster, so internal/ui can rasterize it again whenever the display scale changes. See svg.go and vector.go.
Index ¶
- Constants
- Variables
- func ApplyOrientation(img image.Image, orientation int) image.Image
- func CanEncode(u fyne.URI) bool
- func CanEncodeExt(ext string) bool
- func CanStripJPEGMetadata(data []byte) bool
- func CaptureDate(u fyne.URI) (time.Time, bool)
- func ClampVectorRaster(w, h int) (int, int)
- func DifferenceHash(img image.Image) uint64
- func DuplicateGroups(hashes []uint64, maxDist int) [][]int
- func EstimateDecodedBytes(b image.Rectangle) int64
- func Export(dest fyne.URI, img image.Image, src fyne.URI) error
- func Hamming(a, b uint64) int
- func IsSupportedImage(u fyne.URI) bool
- func LoadThumbnail(u fyne.URI) (image.Image, error)
- func LoadThumbnailAndBounds(u fyne.URI) (image.Image, image.Rectangle, error)
- func MaxEncodedBytes() int64
- func MaxVectorRasterPixels() int64
- func ReadAndProbe(ctx context.Context, u fyne.URI) (data []byte, bounds image.Rectangle, err error)
- func RotateSteps(img image.Image, steps int) image.Image
- func SaveRotated(u fyne.URI, img image.Image) error
- func SetMaxEncodedBytes(n int64)
- func SetMaxVectorRasterPixels(n int64)
- func StripJPEGMetadata(u fyne.URI) error
- func SupportedExtensions() []string
- type ByteCache
- func (c *ByteCache[V]) Add(key string, v V)
- func (c *ByteCache[V]) AddIfFits(key string, v V) bool
- func (c *ByteCache[V]) Budget() int64
- func (c *ByteCache[V]) Bytes() int64
- func (c *ByteCache[V]) Contains(key string) bool
- func (c *ByteCache[V]) Get(key string) (V, bool)
- func (c *ByteCache[V]) Len() int
- func (c *ByteCache[V]) Purge()
- func (c *ByteCache[V]) Remove(key string)
- func (c *ByteCache[V]) SetBudget(n int64)
- type InputTooLargeError
- type InvalidDimensionsError
- type LoadedImage
- type Metadata
- type UnsupportedSaveFormatError
- type Vector
Constants ¶
const ( MinVectorWidth = 520 MinVectorHeight = 340 )
MinVectorWidth and MinVectorHeight are the floor a vector's logical size is raised to when its own is smaller. Deliberately equal to internal/ui's startW/startH - the app's smallest window - so an icon-sized SVG opens filling that window rather than as a 24-pixel stamp in the corner of it. They cannot be imported from there (internal/imaging must not depend on any UI package), so internal/ui carries a test pinning the two together.
const DefaultImgCacheBytes = 512 << 20
DefaultImgCacheBytes is the shipped byte budget for NewImgCache, until the settings window (internal/ui/settingswin) changes it. Bounded in bytes rather than entries because a decoded image ranges over four orders of magnitude in size: 16 entries could mean 2 MB or 12 GB, which is no bound at all. 512 MB holds a good run of ordinary photos while staying well inside what a desktop can spare.
const DefaultMaxEncodedBytes = 512 << 20
DefaultMaxEncodedBytes is the shipped ceiling on a file's *encoded* size, until the settings window (internal/ui/settingswin) changes it. Sized off what real cameras actually produce rather than off an arbitrary small number: a 100-megapixel uncompressed TIFF lands near 600 MB, so 512 MB is deliberately generous while still stopping a file that would blow out memory before a single pixel is decoded. maxImagePixels above bounds the *decoded* side; this bounds the read that precedes it.
const DefaultMaxVectorRasterPixels = 32_000_000
DefaultMaxVectorRasterPixels caps a single rasterization until the settings window's image-cache budget derives a different cap (see internal/ui's SetMaxImageCacheMB). zoom's maxScale is 16, so an unclamped 1600x1200 logical image would ask for 492 megapixels - about 2 GB as RGBA - at full zoom. 32 million still covers the common case completely: a 340x340 icon at 16x is 5440x5440, or 29.6 million. Past the cap a vector goes soft exactly as every raster format already does, just very much later.
const DefaultThumbCacheBytes = 256 << 20
DefaultThumbCacheBytes is the shipped byte budget for NewThumbCache, until the settings window (internal/ui/settingswin) changes it. Each entry is capped to ThumbnailSize on its long edge, so 256 MB covers on the order of 1600 thumbnails - enough for a large drop's whole file set to stay warm while scrolling the grid, without the 625 MB an entry-bounded 4096 could quietly reach.
const DuplicateMaxDistance = 6
DuplicateMaxDistance is the default Hamming threshold at or below which two dHashes count as the same shot. The settings slider may pass a different maxDist into DuplicateGroups.
6, not the 10 usually quoted for dHash. That folklore figure assumes a hash that drifts under re-encoding, which this one no longer does: over a 13k-image library, a JPEG re-export or a downscale to a quarter size moves the hash by at most 7 bits and by 4 at the 99th percentile, so the extra slack in 10 buys almost no genuine matches while admitting a great many wrong ones. Measured on that library, moving 10 → 6 dropped false pairs from 1150 to 66 and the largest wrong group from 26 files to 4, at a cost of 17 correct pairs out of 5930.
const ThumbnailSize = 200
ThumbnailSize is the maximum length, in pixels, of a generated thumbnail's longer edge; the shorter edge is scaled to preserve the source image's aspect ratio.
Variables ¶
var ( ErrNotSVG = errors.New("not an SVG document") ErrNoSVGSize = errors.New("SVG declares no usable size") )
ErrNotSVG and ErrNoSVGSize distinguish the two ways a document can fail to be a usable vector, because they say different things: the first is not an SVG at all, the second is one that declares no size this app can work out - neither a usable viewBox nor absolute width and height.
Functions ¶
func ApplyOrientation ¶
ApplyOrientation returns img corrected for the given Exif orientation tag value (1-8, per the Exif spec). Orientation 1, and any value outside that range, means no correction is needed.
func CanEncode ¶
CanEncode reports whether SaveRotated has an encoder for u's format, so a caller (internal/ui's canSaveRotation) can decide whether to offer saving at all instead of finding out only after attempting it. It resolves a symlink first, matching SaveRotated's own behavior: what governs there is the format of the file that will actually be written.
func CanEncodeExt ¶
CanEncodeExt reports whether ext (a leading-dot file extension, as filepath.Ext and fyne.URI.Extension both produce, in any case) has an encoder. It is the check the export path wants - internal/ui asks it about a destination the user just named, which may not exist yet and so has no symlink for CanEncode above to resolve.
func CanStripJPEGMetadata ¶ added in v0.2.1
CanStripJPEGMetadata reports whether StripJPEGMetadata would rewrite data. False for non-JPEG. True when there is a removable COM/APPn segment, bytes after the primary EOI (a concatenated second JPEG or motion-photo video), or when Exif Orientation is 2–8 (those files must be re-encoded so they stay upright).
func CaptureDate ¶
CaptureDate reads u's raw bytes and returns its Exif capture date (see Metadata.DateTakenTime), without decoding pixels or building the rest of Metadata - the one field internal/filesort's capture-date sort mode actually needs. ok is false if u can't be read or carries no recognizable capture date, mirroring ReadMetadata's tolerant-failure style; callers are expected to fall back to the file's mtime in that case. Uses context.Background() rather than taking a ctx of its own: filesort.Order already checks its own ctx once per file before calling this (see its own doc comment), which is the granularity that sort needs; the read itself is small enough not to need a second, finer-grained cancellation point on top of that.
func ClampVectorRaster ¶ added in v0.1.7
ClampVectorRaster reduces a requested raster size to MaxVectorRasterPixels, scaling both axes together so the aspect ratio survives. Exported because internal/ui applies it to its own re-render target before comparing that target against the raster already on screen - without it, a request the cap would shrink anyway would look like a permanently unmet demand for a sharper image and re-render on every scale change forever.
func DifferenceHash ¶ added in v0.2.3
DifferenceHash is a 64-bit dHash of img: luma reduced to a 9×8 grid, then one bit per adjacent horizontal pair (8 rows × 8 comparisons). Uniform images have no horizontal gradient and hash to 0, so two different solid colors collide — callers that need “not a duplicate” fixtures must use patterned pixels, not solid JPEGs.
func DuplicateGroups ¶ added in v0.2.3
DuplicateGroups partitions indices into groups of near-duplicates. Each group has a representative at the lowest hashes-slice index (grp[0]), not the grid's visible stand-in. A later file joins only if it is within maxDist of *every* current member (complete linkage), so neighbors of the first file that are far from each other do not become one giant group. Hash 0 (uniform images) is omitted; groups of size 1 are omitted.
func EstimateDecodedBytes ¶
EstimateDecodedBytes is the worst-case decoded size of an image whose header declares these bounds - for callers deciding whether a decode is worth starting at all, before there is any concrete image type to measure. Deliberately the four-bytes-per-pixel ceiling: guessing low here would let exactly the images this budget exists to bound slip through the check.
func Export ¶
Export writes img to dest, encoded in dest's format. src is the file the pixels came from and may be nil. When dest is JPEG and src is a readable JPEG, dest receives a normalized copy of src's metadata segments (same rules as SaveRotated). A read failure on src does not fail the export: pixels are written without metadata.
The destination's extension alone picks the encoder: unlike SaveRotated, no symlink is resolved first, since dest is a destination the user just named rather than a file already open in the viewer, and the format they typed is the format they asked for. An existing destination is replaced (keeping its own permission bits), atomically, by the same temp-file-then-rename writeEncoded gives SaveRotated - so an export over a previous copy cannot damage it if the encode fails partway.
func IsSupportedImage ¶
func LoadThumbnail ¶
LoadThumbnail reads and decodes u exactly like LoadImage - full EXIF orientation correction included - then downsamples the first frame (animated GIFs show only their first frame here, same as every other still context in this app) to fit within ThumbnailSize on its longer edge. An SVG skips the decode-then-downsample round trip entirely and rasterizes straight at the thumbnail's size.
The zero animation budget is what makes that "first frame only" literal: without it a long animation composited every one of its frames to a full RGBA canvas so this could keep one and discard the rest, which for a large GIF meant gigabytes of allocation per grid cell.
func LoadThumbnailAndBounds ¶ added in v0.2.8
LoadThumbnailAndBounds is LoadThumbnail plus the file's EXIF-oriented display size from ReadAndProbe (SVG logical size, RAW preview size). Callers that need a representative by resolution use native, not thumb.Bounds - generated thumbs are capped at ThumbnailSize.
func MaxEncodedBytes ¶
func MaxEncodedBytes() int64
MaxEncodedBytes reports the current encoded-size ceiling. Zero means "never set", falling back to the shipped default - the same zero-means-unset sentinel internal/preferences uses for every numeric preference.
func MaxVectorRasterPixels ¶ added in v0.1.7
func MaxVectorRasterPixels() int64
MaxVectorRasterPixels reports the current cap on one rasterization. Zero means "never set", falling back to the shipped default - the same sentinel MaxEncodedBytes uses.
func ReadAndProbe ¶
ReadAndProbe reads u's raw bytes and decodes just its header - via image.DecodeConfig, so no pixel data is touched - to learn its final display size and reject a zero or absurdly large one instantly, without paying for a full decode that was only going to be thrown away. bounds already accounts for any Exif orientation swap (a 90/270 degree rotation exchanges width and height), so a caller can resize the window to it ahead of the full pixel decode in DecodeLoaded. This is also the natural hook for a future downsampling pass on huge-but-valid images.
ctx is threaded through to readRawBytes, which is where the actual I/O happens - see its own comment. A caller (internal/ui's attemptLoad/ preloadOne) whose generation has been superseded by a newer navigation or drop cancels ctx instead of just discarding the result once it comes back, so an abandoned load stops doing I/O instead of finishing unseen.
func RotateSteps ¶
RotateSteps rotates img clockwise by steps quarter turns, wrapping any value outside 0-3 into that range first (so -1 and 3 both mean "one turn counter-clockwise"). It's the primitive behind the app's view-only R/ Shift+R rotation: composed on top of an image ApplyOrientation has already corrected, it never touches the EXIF tag or file data, and being a plain 90-degree-multiple permutation (no resampling), applying it repeatedly never degrades the pixels the way a resampled rotation would.
func SaveRotated ¶
SaveRotated writes img - a caller's already-rotated, already-oriented frame, typically internal/ui's v.img.Image - back to u, re-encoded in the target file's format, replacing the file's previous contents. For JPEG, SaveRotated copies the original metadata segments onto the re-encoded file with Exif Orientation reset to 1. Other formats still do not carry metadata.
It resolves a symlink before writing, so saving an image opened through a link updates the target instead of replacing the link itself, and the replacement keeps the original file's permission bits. See writeEncoded for the atomic write both this and Export go through.
func SetMaxEncodedBytes ¶
func SetMaxEncodedBytes(n int64)
SetMaxEncodedBytes changes the encoded-size ceiling - the settings window's binding, via internal/ui's SetMaxFileSizeMB. Applies to the next read; one already in flight finishes under the limit it started with.
func SetMaxVectorRasterPixels ¶ added in v0.1.7
func SetMaxVectorRasterPixels(n int64)
SetMaxVectorRasterPixels changes that cap - internal/ui's SetMaxImageCacheMB derives it from the image-cache budget (a quarter of the budget's bytes, 4 of them per RGBA pixel), because the re-render raster is deliberately not charged to that cache and this is how the user's memory setting still reaches it. The clamp owns the domain rule the way SetMaxWindowWidth owns its floor: never below the usability floor, never above the shipped default's known-good ceiling.
func StripJPEGMetadata ¶ added in v0.2.1
StripJPEGMetadata removes identifying metadata from the JPEG at u (Exif, XMP, IPTC, COM, MPF) in place, keeping JFIF APP0, Adobe APP14, and ICC. Bytes after the primary EOI (a concatenated second JPEG, motion-photo video) are discarded. CanStripJPEGMetadata is true when those bytes are the only thing left to remove. When the file's Exif Orientation is 2–8, the pixels are decoded with that orientation applied and re-encoded at jpegSaveQuality so the photo does not appear sideways after the tag is gone; ICC APP2 from the original is spliced back (Adobe APP14 is not: it would misdeclare image/jpeg.Encode's color transform). On orientation 1 the lossless header walk keeps APP14 as well.
A non-JPEG returns errNotJPEG and does not write. A JPEG with nothing removable returns nil without rewriting the file. The write is the same temp-file-then-rename as SaveRotated, through a symlink to the target, preserving permission bits.
func SupportedExtensions ¶ added in v0.2.10
func SupportedExtensions() []string
SupportedExtensions returns every filename extension IsSupportedImage recognizes, lowercase with a leading dot, in declared order. The result is a defensive copy - the caller mutating it can't affect supportedExtensions itself. scripts/plistdoctypes is the one caller outside this package, using it to keep the packaged macOS app's CFBundleTypeExtensions list from drifting out of sync with the decoders actually registered above.
Types ¶
type ByteCache ¶
type ByteCache[V any] struct { // contains filtered or unexported fields }
ByteCache is an LRU cache bounded by the estimated byte weight of what it holds, rather than by a count of entries. Decoded image memory varies by four orders of magnitude between a 16x16 icon and a 200-megapixel panorama, so an entry count says nothing useful about how much memory the cache is actually using - which is the whole reason this type exists instead of a plain count-bounded LRU.
It is safe for concurrent use on its own: internal/ui's attemptLoad decode goroutine and its preloadOne background goroutines both populate the image cache without going through fyne.Do, and internal/ui/grid's worker pool does the same for thumbnails.
func NewByteCache ¶
NewByteCache returns a cache holding at most budget bytes, as measured by weigh. A budget below 1 is raised to 1 rather than rejected: the eviction rule below keeps the most recently added entry regardless of budget, so even an absurdly small one still behaves correctly - it just degenerates to holding a single entry.
func NewImgCache ¶
func NewImgCache(budget int64) *ByteCache[*LoadedImage]
NewImgCache builds the byte-bounded cache callers use to hold recently decoded images, weighing each entry by the pixel memory all of its frames retain - see loadedImageBytes, and ByteCache for the eviction rule that keeps the image currently on screen resident even when it alone exceeds budget.
func NewThumbCache ¶
NewThumbCache builds the byte-bounded cache callers use to hold generated thumbnails, keyed by URI string - a separate cache and budget from NewImgCache's full-size decodes, so populating it (e.g. from the grid overview) can never evict a full decode the normal viewing path still needs, or vice versa.
func (*ByteCache[V]) Add ¶
Add stores v under key as the most recently used entry, evicting older ones until the budget is met. An entry that on its own exceeds the whole budget is still stored: see evict for why that's deliberate.
func (*ByteCache[V]) AddIfFits ¶
AddIfFits stores v only if it fits in the budget by itself, reporting whether it did. This is what speculative writers use (internal/ui's preloadOne): Add's never-evict-the-newest rule exists to protect the image being displayed, and a preloaded neighbor big enough to trigger it would evict that image instead - the opposite of the point.
func (*ByteCache[V]) Budget ¶
Budget reports the current byte budget - what a caller deciding whether a decode is worth starting compares an estimate against.
func (*ByteCache[V]) Contains ¶
Contains reports whether key is cached, without promoting it. This is the right call for a "have we already got this?" check on a speculative path (internal/ui's preloadOne, internal/ui/grid's Cached): promoting a neighbor the user isn't looking at can make it outlive the image that's actually on screen once the budget is tight.
func (*ByteCache[V]) Get ¶
Get returns the value stored under key and promotes it to most-recently used. Use Contains instead for a presence test that shouldn't reorder anything - see its own comment.
func (*ByteCache[V]) Len ¶
Len reports how many entries are held. Unlike the count-bounded cache this replaced, it is a diagnostic rather than the thing being bounded.
func (*ByteCache[V]) Purge ¶
func (c *ByteCache[V]) Purge()
Purge drops every entry, releasing the memory they held. internal/ui's clearToDropzone calls this: once the file set is closed, holding decodes of files no longer open just spends the budget on nothing.
type InputTooLargeError ¶
type InputTooLargeError struct {
// contains filtered or unexported fields
}
InputTooLargeError reports that a file's encoded bytes exceed MaxEncodedBytes, so it was never read into memory in full. Distinct from InvalidDimensionsError because the two say different things to the user: that one means the file claims a size no real image has, this one means the file is real but larger than the limit they set.
func (*InputTooLargeError) Error ¶
func (e *InputTooLargeError) Error() string
type InvalidDimensionsError ¶
type InvalidDimensionsError struct {
// contains filtered or unexported fields
}
InvalidDimensionsError reports that an image's header declared dimensions ReadAndProbe rejects: zero, negative, or large enough to be a decompression-bomb risk.
func (*InvalidDimensionsError) Error ¶
func (e *InvalidDimensionsError) Error() string
type LoadedImage ¶
type LoadedImage struct {
Frames []image.Image
Delays []time.Duration // parallel to Frames; unused when len(Frames) == 1
FileSize int64 // raw byte count read by ReadAndProbe, for the info overlay
// HasEXIF reports whether ReadMetadata found anything in the raw bytes
// this was decoded from - what the info overlay uses to decide whether
// offering its "Show EXIF data" link means anything. Filled in by the
// caller alongside FileSize rather than by DecodeLoaded itself, since
// the thumbnail path decodes through here too and has no use for it.
HasEXIF bool
// AnimationTruncated reports that this was a multi-frame GIF whose
// composited frames would have exceeded the animation budget, so only
// its first frame was decoded. The image still displays - it just
// doesn't move - which is a far better outcome for a valid file than
// refusing it outright; internal/ui says so with a toast.
AnimationTruncated bool
// Vector is the parsed source of an SVG, retained so the app can
// rasterize it again at a different size as the zoom level or window
// size changes. Nil for every raster format, which is what internal/ui
// branches on to decide whether re-rendering means anything.
Vector *Vector
// Preview reports that Frames came from an embedded JPEG inside a camera
// RAW container (CR2, NEF, ARW, DNG, CR3, …) rather than from decoding
// the file's own pixels. The info overlay and window title mark those
// with "(preview)"; Save Changes stays off because this module does not
// write RAW. False for every format that DecodeLoaded already handled.
Preview bool
}
LoadedImage holds one or more display-ready frames. Static images (JPEG, PNG, WebP, single-frame GIF) carry exactly one frame; animated GIFs carry every frame, each already composited to the GIF's full canvas per its disposal method, paired with that frame's display delay.
func DecodeLoaded ¶
DecodeLoaded finishes decoding data - already read and header-validated by ReadAndProbe - applying EXIF orientation correction where present. The shared reader handles JPEG APP1, PNG eXIf, WebP EXIF, and TIFF-container RAW metadata. Animated GIFs are decoded to every frame instead of just the first.
ctx is checked once, up front, rather than threaded into the decode itself: unlike ReadAndProbe's file read, decoding already-in-memory bytes doesn't block on external I/O, so there's no slow operation to interrupt mid-flight - only a possibly-wasted one to skip entirely if ctx is already done by the time this runs (e.g. a generation that went stale while queued behind preloadOne's semaphore).
func LoadImage ¶
func LoadImage(u fyne.URI, maxAnimBytes int64) (*LoadedImage, error)
LoadImage reads and decodes an image file of any format registered with the image package (JPEG, PNG, GIF, WebP, BMP, TIFF, ICO, XPM, HEIC, AVIF) or a camera RAW container whose embedded JPEG preview raw.go can extract - see ReadAndProbe and DecodeLoaded, which callers wanting to resize a window ahead of the full pixel decode call separately instead. Uses context.Background() rather than taking a ctx of its own: its only caller, LoadThumbnail, is read by internal/ui/grid's own bounded worker pool, which has its own staleness guard (a generation the caller checks against once a thumbnail comes back) rather than the cancellable-context one internal/ui's main decode path (ShowImage/attemptLoad/preloadOne) uses.
func (*LoadedImage) DecodedBytes ¶ added in v1.0.1
func (l *LoadedImage) DecodedBytes() int64
DecodedBytes estimates the retained pixel and vector memory of a loaded image, using the same accounting as the viewer's byte-budgeted cache.
type Metadata ¶
type Metadata struct {
Make string
Model string
LensModel string
ExposureTime string
FNumber string
ISO string
FocalLength string
DateTaken string
// Latitude and Longitude are the capture position in signed decimal
// degrees (north and east positive), read from the GPS sub-IFD that
// IFD0's pointer tag 0x8825 locates. Only meaningful when HasGPS is
// set: a photo without location tags leaves all three zero, which is
// what keeps the EXIF window's map collapsed and hidden.
Latitude float64
Longitude float64
HasGPS bool
// DateTakenTime is DateTaken's underlying value, parsed from the same
// raw Exif tag - for callers that need to compare or sort capture
// dates (see CaptureDate in loader.go and internal/filesort's
// captureOrModTime) rather than just display DateTaken's
// already-formatted string. Zero when DateTaken is empty, or set from a
// raw value that didn't parse.
DateTakenTime time.Time
}
Metadata is the subset of a photo's Exif tags the EXIF window (see internal/ui/exifwin) displays: camera make/model, lens, exposure, aperture, ISO, focal length, capture date, and - only where the photo carries one - the GPS position its map view centers on. A zero Metadata (every field "", no position) means either the file has no Exif data or none of these particular tags.
func ReadMetadata ¶
ReadMetadata scans data (a whole image file's raw bytes) for an Exif APP1 segment and extracts Metadata from it. Like readEXIFOrientation, it is deliberately failure-tolerant throughout: a malformed tag, a truncated value, or a file with no Exif data at all just leaves the corresponding field (or all of them) blank rather than returning an error - there is no error to report, only "nothing to show".
type UnsupportedSaveFormatError ¶
type UnsupportedSaveFormatError struct {
// contains filtered or unexported fields
}
UnsupportedSaveFormatError reports that SaveRotated has no encoder for a file's extension.
func (*UnsupportedSaveFormatError) Error ¶
func (e *UnsupportedSaveFormatError) Error() string
type Vector ¶ added in v0.1.7
type Vector struct {
// contains filtered or unexported fields
}
Vector is a parsed SVG kept alive so the app can rasterize it again as the zoom level or window size changes. Held on LoadedImage, and therefore shared through the image cache - see RasterAt for what that costs.
func ParseVector ¶ added in v0.1.7
ParseVector parses an SVG document. It does not rasterize - DecodeLoaded asks for the first raster separately, at Logical's size.
func (*Vector) Logical ¶ added in v0.1.7
Logical is the size the app treats this image as being, whatever size its current raster happens to be - see vectorLogical.
func (*Vector) RasterAt ¶ added in v0.1.7
RasterAt draws the vector at w by h pixels, clamped to the pixel ceiling.
The lock is not optional: SetTarget writes icon.Transform and Draw reads it, and two of internal/ui/vector.go's rasterizeVector goroutines can be inside this method on the same *Vector at once - goroutine A can pass its own staleness check, and only then does a fresher scale change spawn goroutine B and bump the generation, so A is already past the guard and still rasterizing when B starts. TestRasterAtIsSafeForConcurrentUse covers exactly this. The recover sits inside the lock because oksvg panics outright on some inputs (a 60000-unit viewBox raises a slice-bounds panic) - letting that escape would both crash the app and leave the transform half written under a held mutex.