- Overview
- Key Features
- Technical Details
- Supported Image Formats
- Color Management
- PSD / PSB Decoding Model
- DDS & KTX Texture Decoding Model
- Keyboard Shortcuts & Controls
- Dependencies
- Native Libraries & Bundling
- Installation
- Configuration & Data Files
Lyra is a high-performance, minimalist image viewer designed for speed, fluid navigation, and precision. It handles modern and professional image formats without the overhead of full editing suites or Electron-based tools. Built for anyone who relies on images as a core resource in their workflow:
- 2D/3D artists and game developers browsing texture maps and baked assets
- Photographers reviewing large batches of exports
- Developers inspecting UI assets, icons, and generated output
- And ordinary advanced users
- Lyra is solely a viewer - nothing more. It opens and displays your files; it never writes to them, moves them, or deletes them. Your files are always safe and untouched.
- Lyra is not an Electron application. It is a native application built on SDL3 and Skia, with no embedded web browser and no JavaScript runtime. It runs on .NET 9 and renders directly through your GPU, keeping performance at the forefront of every decision.
- Lyra does not connect to the internet. It has no telemetry, no update pings, no cloud sync, no nag screens, and no AI features. Everything runs locally, offline, on your machine. Updates are manual - check for new releases and install them through your package manager (Homebrew, APT, or Scoop) when you're ready. If there's a format, workflow, or feature you'd like to see, the right place to say so is the GitHub issue tracker.
Lyra is designed for capable, modern hardware - a dedicated GPU and SSD storage will get the best out of it. Not every limitation is Lyra's to solve: network shares over SMB are constrained to a single stream by the protocol itself and cannot be parallelised, so performance over a NAS or remote share will always be bounded by that ceiling.
- Fast navigation through large directories of images or texture assets.
- Zoom-to-cursor and panning for intuitive inspection at any scale.
- Directory tree sidebar for browsing the filesystem without leaving the viewer.
- SVG support for previewing scalable vector assets.
- Adjustable background modes to improve visibility of transparent images.
- Duplicate finder that locates exact and visually similar images across a directory tree using perceptual hashing.
- EXIF metadata and format-specific information panel.
- PSD layer hierarchy panel showing group structure, layer names, and visibility state.
- Reasonable support for modern image formats, with limited support for older formats that refuse to die.
Lyra is built on .NET 9 with SDL3 for windowing and input, and SkiaSharp for hardware-accelerated rendering via OpenGL or Metal. It is not an Electron app - there is no embedded browser, no web runtime, and no hidden resource overhead (and definitely no AI client). The architecture is designed around fast, non-blocking image loading:
- Decoded images are cached and adjacent files are preloaded in the background, so navigation feels instant even in large directories.
- Large PSD/PSB files use streaming and tiled decoding to avoid loading entire documents into memory - tested with files exceeding 3 GB.
Decoding is split into two layers. Lyra.ManagedCodecs is a pure-managed, dependency-free codec library that owns the formats Lyra decodes itself - TGA, Radiance HDR, and the GPU texture containers (DDS, KTX, KTX2) together with their block formats (BC1–BC7, BC6H, ETC2 / EAC, ASTC). These readers parse the container structure in C#, slice each subresource as a zero-copy view into the source file, treat all input as hostile (every byte range and surface size is bounds-checked against overflow), and decode only the surface actually needed - so a thumbnail or a perceptual hash never pays to decode a full-resolution mip. Because nothing here links a native library, it behaves identically on every platform .NET targets.
For the remaining formats Lyra integrates lightweight native interop wrappers for EXR, JPEG 2000, JPEG XL, and TIFF decoding, delegating format-specific work to focused libraries. The one native exception inside the managed codec layer is Basis Universal (ETC1S / UASTC) supercompression carried in KTX2: rather than reimplement its intricate transcoder, Lyra wraps Binomial's open-source reference transcoder (Apache-2.0) in a small native wrapper.
How these native libraries are shipped differs by platform - see Native Libraries & Bundling.
Developer note: Lyra is designed and written simultaneously. As a result, parts of the code reflect iterative exploration rather than a fully pre-planned architecture. Refactoring is ongoing wherever it improves clarity or maintainability.
| Format | Description | Extensions |
|---|---|---|
| PNG | Lossless raster image format with optional alpha | .png |
| JPEG / JFIF | Lossy raster image format (JPEG family) | .jpg .jpeg .jif .jfif |
| TIFF | High-precision raster image container | .tif .tiff |
| Targa | Raster image format with optional alpha | .tga |
| BMP | Uncompressed bitmap image format | .bmp |
| Format | Description | Extensions | Notes |
|---|---|---|---|
| AVIF | High-efficiency image format based on AV1 | .avif |
|
| HEIF / HEIC | High-efficiency image container format (HEVC-based) | .heif .heic |
|
| JPEG XL | JPEG XL Image Coding System | .jxl |
Lyra displays static JPEG XL images. Animated JXL is decoded to its first frame only (same policy as JPEG 2000). HDR (floating-point) JXL is tone-mapped for display. |
| WebP | Compressed raster image format with optional alpha | .webp |
| Format | Description | Extensions | Notes |
|---|---|---|---|
| SVG | Scalable Vector Graphics | .svg |
|
| Photoshop | Adobe Photoshop document | .psd .psb |
See PSD / PSB Decoding Model section below |
| Format | Description | Extensions |
|---|---|---|
| OpenEXR | High-dynamic range, multi-channel raster format | .exr |
| Radiance HDR | High-dynamic range RGBE format | .hdr |
Note: EXR and HDR images are tone-mapped for display using the ACES filmic curve, so high-dynamic-range highlights roll off smoothly instead of clipping harshly to white.
| Format | Description | Extensions | Notes |
|---|---|---|---|
| DDS | DirectDraw Surface | .dds |
See DDS & KTX Texture Decoding Model section below |
| KTX | Khronos GPU texture container | .ktx .ktx2 |
KTX 1.x and KTX 2.0; see section below |
| Format | Description | Extensions | Notes |
|---|---|---|---|
| ICO | Icon container format | .ico |
|
.icns |
|||
| JPEG 2000 | Wavelet-based image format | .jp2 .jpg2.j2k .j2c .jpc |
Lyra supports single-image JPEG 2000 files. Multi-image, animated, or compound JPEG 2000 formats (JPX, JPM, MJ2, JPIP) are intentionally NOT supported. |
Note: Crossed-out formats are not implemented yet.
Lyra is color-managed from decode to screen. Wide-gamut images are never clamped to sRGB at decode time - the conversion happens once, at draw time, against the gamut of the display being drawn to.
- Decode tags every image with the gamut it was authored in. That means the embedded ICC profile wherever one exists (PNG, JPEG, TIFF, JPEG 2000, HEIF / AVIF, PSD), the NCLX primaries when a HEIF / AVIF file carries those instead, and Display P3 for JPEG XL, which Lyra asks libjxl to decode into P3 rather than fold down to sRGB. An image carrying no color information at all is interpreted as sRGB - the only defensible assumption.
- Display tags the render surface with the gamut of the screen. On macOS that is Display P3. On Windows and Linux, Lyra asks the windowing system for the monitor's ICC profile and uses it; if the system publishes no profile, or publishes one that cannot be expressed as a matrix/transfer-function color space, the surface falls back to sRGB.
- Draw is where the transform happens, per frame, on the GPU. Because both ends carry real profiles, a Display-P3 photograph on a Display-P3 screen keeps its saturated colors instead of being flattened on the way in.
Colors outside the display's physical gamut are folded into the ones it can reproduce. This is correct behavior rather than a defect, and it is documented here because it is easy to mistake for one.
The clearest illustration is the well-known WebKit Display-P3 logo test image, which circulates in PNG, JPEG XL and other formats. It is constructed so the logo and its background are different colors in Display P3 but clamp to the same color in sRGB. The intended outcome is:
| Display | Color-managed viewer | Non-color-managed viewer |
|---|---|---|
| Display P3 / wide gamut | Logo visible | Logo visible |
| sRGB / standard gamut | Flat rectangle - logo invisible | Logo visible |
So if Lyra renders a flat rectangle on a standard-gamut monitor, the pipeline is working exactly as intended.
Lyra converts using relative colorimetric intent with clipping, the same choice web browsers make. Colors inside the display's gamut are reproduced exactly; colors outside it are clipped to the gamut boundary.
The alternative - perceptual intent - compresses the whole gamut inward so that out-of-gamut relationships survive, at the cost of desaturating colors that were perfectly reproducible to begin with. That trades fidelity for the preservation of differences, and Lyra does not make that trade: accuracy for the colors a display can show takes precedence over a hint of the ones it cannot.
Note: Gamut and dynamic range are separate concerns. High-dynamic-range sources (EXR, Radiance HDR, BC6H, float textures, HDR JPEG XL) are additionally tone-mapped for display as described in their sections above; that handles brightness range, not color gamut.
Lyra currently focuses on decoding the flattened Image Data section of Photoshop files, rather than individual layers. This design choice prioritizes performance and fast previewing.
For PSD / PSB files, Lyra also surfaces the layer hierarchy in the sidebar - showing group structure, layer names, and visibility state - independently of the flattened composite decode.
This is explicitly documented because the Image Data section is not strictly mandatory in the PSD specification and, in some edge cases, may be missing or may not fully represent the document as it appears when opened in Photoshop.
Adobe Photoshop File Format Specification
| Color Mode | Channels | Lyra Support |
|---|---|---|
| Bitmap | 1 (1-bit) | Planned |
| Grayscale | 1 | Full |
| Duotone / Tritone / Quadtone | 1 + inks | In progress (clean-room) |
| Indexed | 1 + palette | Full |
| RGB | 3 | Full |
| CMYK | 4 | Full |
| Lab | 3 | MVP |
| Multichannel | N | In progress (clean-room) |
Legal Note: Duotone and Multichannel support is an independent, clean-room implementation. It was derived by observing the documented PSD/PSB file structure, publicly available format references, and the contents of sample files - not by decompiling, disassembling, or otherwise reverse-engineering Adobe software, and not from any Adobe source code. Spot/named colors are rendered using the color values stored within each document; no proprietary color libraries (e.g. PANTONE) are bundled.
Lyra fully supports PSB (Photoshop Big Document Format) files.
- Successfully tested with ~3 GB PSB files
- Uses streaming / tiled decoding internally where possible to avoid loading entire images eagerly
See Color Management for how profiles are handled across all formats; this section covers what is specific to PSD / PSB.
Lyra honors embedded ICC color profiles whenever they are present. If a PSD / PSB document does not contain an embedded profile - most notably in CMYK color modes - Lyra falls back to the system’s default color profile to produce a usable result.
Without an explicit ICC profile, CMYK data has no well-defined color meaning. In such cases, different viewers may interpret the same document very differently, sometimes resulting in severely distorted or inverted-looking colors.
Lyra’s fallback behavior is intended to be predictable and standards-compliant rather than attempting heuristic or hard-coded CMYK assumptions.
Developer note: During development, Lyra was tested against several large CMYK PSB files from the NASA public image archive. These documents did not contain embedded ICC profiles and produced drastically different results across common image viewers - ranging from heavily shifted colors to near-inverted appearances.
This behavior is not a defect of the files themselves, but a direct consequence of CMYK data being interpreted without a defined color profile.
When viewing a PSD or PSB file, Lyra surfaces document-level metadata and the full layer hierarchy through dedicated sidebar sections. This information is extracted directly from the binary file structure during decoding.
PSD Layers
The PSD Layers section presents the full layer hierarchy as a tree view, reconstructed from the flat layer record list stored in the file. Groups are displayed with their child count and can be expanded or collapsed.
This display is read-only and independent of the flattened composite decode - Lyra does not render individual layer contents, but provides the structural overview that is otherwise only visible inside Photoshop.
The PSD decoder is intentionally structured to allow future expansion.
DDS and KTX are GPU texture containers - one file can hold a full mip chain, cube-map faces, array layers, or
volume slices, usually in a block-compressed GPU format. Lyra reads all three (.dds, .ktx, .ktx2) with a
single pure-managed codec (no native dependencies, save the Basis transcoder noted below): for display it
decodes the base surface (mip 0, first face / layer); for thumbnails and perceptual hashing it decodes the
smallest stored mip that still covers the target size. The container readers differ - each owns its own header
and format mapping - but they all feed one shared set of block decoders.
| Family | Formats | Notes |
|---|---|---|
| Block-compressed (BCn) | BC1–BC3 (DXT1/3/5), BC4 / BC5 (unorm + snorm), BC7 | The mainstream desktop formats |
| HDR block | BC6H (signed + unsigned) | Decoded to float, then tone-mapped |
| Mobile block | ETC2 / EAC - RGB, RGB+A1, RGBA8, R11 / RG11 (unorm + snorm) | Typically carried in KTX / KTX2 |
| Adaptive block (ASTC) | All LDR footprints - 2D (4×4 … 12×12) and 3D (3×3×3 … 6×6×6) | sRGB + linear; HDR ASTC not decoded |
| Uncompressed 8-bit | R8, RG8, RGB8, RGBA8 / BGRA8 (+ sRGB), snorm, packed (4/4/4/4, 5/6/5, 5/5/5/1, 10/10/10/2) |
snorm remapped for display |
| Uncompressed float | R16F / R32F, RGB16F, RGBA16F / RGBA32F, RG11B10, RGB9E5 | Decoded to float, then tone-mapped |
The decoders are validated against independent reference decoders - BC1 / BC3 / BC7 against Pillow, BC6H against
imagecodecs, and ASTC against the official astcenc reference decoder - across fuzzed inputs covering every
block mode and partition.
- DDS - both the legacy
DDS_PIXELFORMATheader and theDX10extended header, including four-character codes (DXT1,ATI2,BC5S, …), the numericD3DFORMATcodes some older D3D9 exporters store in the FourCC field, andDXGI_FORMATidentifiers. - KTX 1.x - mapped from its OpenGL
glInternalFormat. Rare big-endian files are byte-swapped on read, and the OpenGL bottom-left row order is flipped to top-left for display. - KTX 2.0 - mapped from its Vulkan
VkFormat. Per-level Zstandard and ZLIB supercompression is inflated on read. Basis Universal (ETC1S / UASTC) payloads are transcoded to RGBA by a small native wrapper - the one native dependency in this path - since their block stream is proprietary.
Decoding is faithful - no color transform is applied, so an sRGB source decodes to sRGB-tagged bytes and the display path linearizes.
- HDR formats (BC6H, RGBA16F / RGBA32F, and the packed float formats) are scene-referred float and are tone-mapped with the same ACES filmic curve used for EXR and Radiance HDR, so highlights roll off smoothly instead of clipping.
- Signed (
snorm) formats - common in bump / normal maps - are remapped from[-1, 1]to[0, 1], which avoids the "shifted color" look some viewers produce by rendering the raw signed bytes as unsigned.
When a DDS or KTX file is open, two sidebar sections surface its internals:
- Format Specific lists the headline facts: the source-native format name (e.g.
BC7_UNORM,DXT4, a VulkanVK_FORMAT_…for KTX2, orR16G16B16A16_FLOAT (FourCC 'q')when a numericD3DFORMATis decoded), Has Alpha, Is Cubemap, Is Volume, Depth (volumes only), Mipmap Count, and Bits/Pixel. - Structure is a scrollable, collapsible view of the file's binary layout - the container header and its sub-structures, and every mip level. Each part shows its name, a short description and its byte size, and expands to the raw key-value fields it holds.
Lyra treats texture input as hostile. It parses the full subresource layout (mips, faces, array layers, volume
depth slices) and validates every subresource's byte range against the file length before exposing it;
dimensions, mip counts and surface sizes are bounds-checked against overflow, and each parsed level is cross-checked
against Lyra's own independent sizing math. A malformed or truncated header is rejected cleanly rather than read out
of bounds, and an unrecognized format fails with a descriptive message naming the exact DXGI_FORMAT, VkFormat,
glInternalFormat, or FourCC rather than failing silently.
- PVRTC - the PowerVR block formats are not decoded.
- HDR ASTC - LDR ASTC is fully supported; the HDR ASTC profiles are not yet decoded.
- KTX files that declare zero stored levels (deferred runtime mip generation) are rejected rather than guessed.
| Key | Action |
|---|---|
← → |
Previous / Next image |
Home End |
First / Last image |
+ - |
Zoom in / Zoom out |
Mouse Wheel |
Zoom at cursor position |
Middle Mouse Button |
Customizable (see app-settings.toml) |
0 |
Toggle Fit to Screen / Original Size |
S |
Toggle sampling mode |
F |
Toggle fullscreen |
B |
Toggle background mode |
I |
Toggle image information overlay |
H |
Toggle help bar |
Return |
Reveal image or directory in native file explorer |
Esc |
Cancel an operation, or exit application |
| Key | Action |
|---|---|
⌘ ← ⌘ → |
First / Last image |
⌥ ← ⌥ → |
First / Last image within the directory |
| Key | Action |
|---|---|
Ctrl ← Ctrl → |
First / Last image within the directory |
| Context | How Lyra interprets it | Make a collection from files around | Recursion |
|---|---|---|---|
| Single file | Anchor (Open / Open With / Double-click) | Yes | No |
| Multiple files (same directory) | Selection | No | No |
| Single directory | Directory collection | No | Yes |
| Multiple directories | Multi-directory selection | No | Yes |
| Mixed files from different directories | Multi-directory selection | No | No |
Recursion applies only when directories are explicitly dropped. Opening or dropping files never implicitly expands into subdirectories.
Developer note: Lyra intentionally favors context-aware navigation. Opening a single image always implies “show me this image in relation to its neighbors”, not isolation.
| Library | Purpose | License | Repository |
|---|---|---|---|
| SDL3-CS | Core graphics, input, and windowing | zlib | github |
| SkiaSharp | Hardware-accelerated 2D rendering | BSD-3-Clause | github |
| Svg.Skia | SVG parsing and rendering | MIT | github |
| LibHeifSharp | HEIF / HEIC image decoding | LGPL-3.0 | github |
| OpenEXR | High-dynamic-range OpenEXR (.exr) decoding | BSD-3-Clause | github |
| OpenJPEG | JPEG 2000 still-image decoding | BSD-2-Clause | github |
| libjxl | JPEG XL decoding (native wrapper) | BSD-3-Clause | github |
| libtiff | TIFF decoding | BSD-like | gitlab |
| Basis Universal | KTX2 ETC1S / UASTC transcoding (native wrapper) | Apache-2.0 | github |
| ZstdSharp.Port | Zstandard decompression for KTX2 supercompression | MIT | github |
| Unicolour | Color space conversions & perceptual color math (transitive, via the in-house PSD decoder) | MIT | github |
| MetadataExtractor | EXIF metadata extraction | Apache-2.0 | github |
| Tomlyn | TOML parsing for configuration files | BSD-2-Clause | github |
| System.IO.Hashing | Fast non-cryptographic hashing (duplicate detection) | MIT | github |
A handful of formats are decoded through native libraries (libheif, OpenJPEG, libjxl, OpenEXR, libtiff, plus the Basis Universal transcoder). How those libraries are delivered depends on the platform:
- macOS - expected from the package manager (Homebrew).
- Linux - resolved as APT dependencies of the
.deb, with the exception of libjxl, which is vendored inside the package for now. This is a temporary measure until JPEG XL support is more widely available across Ubuntu releases; it will be dropped in favor of the system package once that lands. - Windows - bundled with the application. The wrapper DLLs are self-contained and ship inside the distribution, so no separate installation is required.
Lyra Viewer is distributed via Homebrew on macOS, an APT repository (or a
direct .deb) on Debian/Ubuntu, and Scoop on Windows.
brew tap lyra-viewer/lyra
brew trust --cask lyra-viewer/lyra/lyra-viewer
brew install --cask lyra-viewerscoop bucket add lyra-viewer https://github.com/lyra-viewer/scoop-lyra
scoop install lyra-viewerUpdates then arrive through scoop update lyra-viewer.
Add the signed repository once, then install and receive updates through apt:
# 1. Trust the repository signing key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://lyra-viewer.github.io/apt-lyra/lyra-archive-keyring.asc \
| sudo gpg --dearmor -o /etc/apt/keyrings/lyra.gpg
# 2. Add the repository
echo "deb [signed-by=/etc/apt/keyrings/lyra.gpg] https://lyra-viewer.github.io/apt-lyra stable main" \
| sudo tee /etc/apt/sources.list.d/lyra.list
# 3. Install
sudo apt update
sudo apt install lyra-viewerUpdates then arrive through the usual sudo apt update && sudo apt upgrade.
To remove the repository:
sudo rm /etc/apt/sources.list.d/lyra.list /etc/apt/keyrings/lyra.gpg
sudo apt updatePrefer not to add a repository? Download lyra-viewer_<version>_amd64.deb from the
latest release and install it
directly (apt resolves the system dependencies):
sudo apt install ./lyra-viewer_0.5.2_amd64.debNote: Linux builds are amd64 (x86-64) only for now.
On macOS and Linux, Lyra stores configuration and runtime data in standard XDG-compliant locations. On Windows, both the
configuration and data files live together under %LOCALAPPDATA%\lyra-viewer (the file names below are unchanged).
~/.config/lyra-viewer/
| File | Description |
|---|---|
app-settings.toml |
Application settings: renderer, window state, middle mouse button function, text sizes... |
ui-settings.toml |
UI state - saved automatically on exit |
~/.local/share/lyra-viewer/
| File | Description |
|---|---|
log.txt |
Application log output |
load-time-data.toml |
Recorded decode times per format, used to estimate loading progress |
If any configuration file is missing or malformed, Lyra falls back to built-in defaults and recreates the file on next save. Deleting everything under these directories is always safe - Lyra will start fresh with default settings.



