Skip to content

Repository files navigation

Lyra Viewer

Ko-fi


Contents

Overview

Screenshot

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

What Lyra is and what it isn't

  • 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.

Recommended hardware & known limitations

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.


Key Features

  • 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.

Technical Details

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.


Supported Image Formats

Common Raster Formats (Essential)

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

Modern / Web-Friendly Formats

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

Document / Vector Formats

Format Description Extensions Notes
SVG Scalable Vector Graphics .svg
Photoshop Adobe Photoshop document .psd .psb See PSD / PSB Decoding Model section below

High Dynamic Range Formats

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.

GPU Formats

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

Minor Formats

Format Description Extensions Notes
ICO Icon container format .ico
ICNS Apple icon container format .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.


Color Management

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.

What this means on a standard-gamut display

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.

Rendering intent

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.


PSD / PSB Decoding Model

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.

Photoshop file structure

Adobe Photoshop File Format Specification

PSD Color Mode Support

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.

PSB Support

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

PSB Large

ICC Color Profiles

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.

Displayed PSD Information

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.

Future Direction

The PSD decoder is intentionally structured to allow future expansion.


DDS & KTX Texture Decoding Model

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.

Supported Texture Formats

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.

Containers

  • DDS - both the legacy DDS_PIXELFORMAT header and the DX10 extended header, including four-character codes (DXT1, ATI2, BC5S, …), the numeric D3DFORMAT codes some older D3D9 exporters store in the FourCC field, and DXGI_FORMAT identifiers.
  • 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.

Color & Signedness

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.

File Inspector

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 Vulkan VK_FORMAT_… for KTX2, or R16G16B16A16_FLOAT (FourCC 'q') when a numeric D3DFORMAT is 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.

Safety

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.

Not Yet Supported

  • 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.

Keyboard Shortcuts & Controls

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

macOS Specific

Key Action
⌘ ← ⌘ → First / Last image
⌥ ← ⌥ → First / Last image within the directory

Windows / Linux Specific

Key Action
Ctrl ← Ctrl → First / Last image within the directory

Open With / Drag & Drop

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.


Dependencies

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

Native Libraries & Bundling

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.

Installation

Lyra Viewer is distributed via Homebrew on macOS, an APT repository (or a direct .deb) on Debian/Ubuntu, and Scoop on Windows.

macOS (Homebrew)

brew tap lyra-viewer/lyra
brew trust --cask lyra-viewer/lyra/lyra-viewer
brew install --cask lyra-viewer

Windows (Scoop)

scoop bucket add lyra-viewer https://github.com/lyra-viewer/scoop-lyra
scoop install lyra-viewer

Updates then arrive through scoop update lyra-viewer.

Linux (APT)

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-viewer

Updates 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 update

Linux (direct .deb)

Prefer 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.deb

Note: Linux builds are amd64 (x86-64) only for now.


Configuration & Data Files

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).

Configuration

~/.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

Data

~/.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.


About

GPU-accelerated minimalist image viewer for creative professionals and advanced users who treat images as assets and graphical resources. Built on SDL3 and Skia.

Topics

Resources

Stars

23 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages