Loupe is GNOME’s new Core app for viewing images. Starting with the GNOME 45 release, you might find it as Image Viewer on your system. It replaces the previous image-viewing app Eye of GNOME. In honor of this historic occasion, I wanted to give a bit of insight into the making and technology of Loupe.
Why Start from Scratch
The first documented commits to Eye of GNOME (EOG) are from September 1999 by Federico. Some of this code from back then withstands the test of time until today. Likewise, the image loading was already powered by GdkPixbuf, which is still GNOME’s image loading library today. So why start replacing such a well-tried set of software now?
Original experiments for writing a new image viewer go back to 2020. There wasn’t a complete technical plan for Loupe, but rather a gut instinct with everyone involved that for the goals of Loupe, a rewrite would be the better solution. In retrospect, this was the right decision. With the port to GTK 4, a new interface design, gestures and animations, sandboxed image loading, and using more upstream projects for decoding tasks, the code overlap wouldn’t have been too large to begin with. On top, the Rust language is a good fit for handling media data since it provides memory safety and performance at the same time.
Hidden Features
On the surface, Loupe looks simple. And that’s what we want it to feel like. Behind this, however, are a lot of features that make this possible. Here are a few examples that highlight the differences to EOG. The image rendering is now happening on the graphics card. That allows viewing quite large images while retaining a responsive interface, including animations and direct visual feedback for touchpad and touchscreen gestures. It also makes Loupe much more snappy on high-resolution screens, especially noticeable with higher refresh rates. To keep image navigation for SVGs smooth as well, Loupe uses “tiled rendering,” only rendering relevant parts of the SVG and providing those tiles to the graphics card for combining them. This allows Loupe to render large SVGs while keeping the interface responsive. Loupe also supports High DPI screens better in the way that a 100 % image size is oriented on the physical amount of pixels. For transparent images, Loupe analyzes the image’s content and chooses a different background color to not end up drawing black content on a black background and alike.
Last but not least, I wrote a new wrapper around the image decoding process. The project is named Glycin after a photographic developing agent. Glycin sandboxes the image loading, which potentially¹ adds security and allows a flawed image loader to crash without pulling Loupe down with it.² Additionally, most decoders Glycin uses are written in Rust, adding an additional line of defense via memory safety.
Rust Doesn’t Solve Everything
Duh! I know. But sometimes I feel like there is a tendency that one might forget that besides all the struggle with programming languages and build systems, programming in itself also remains hard. No matter the tools one uses. While the codebase at hand is still relatively small – about 8000 lines of Rust code – there were a bunch of logic and design challenges that required a day without brain fog³. One example is the combination of many input methods and animations and their potential interactions and always getting them represented correctly on the screen.
Another big part of this project was, as so often, working with upstream projects that make all the magic possible in the first place. The GTK maintainers took on the battle with OpenGL to add mipmapping support for better texture rendering. I even dipped my toes into GTK’s texture code myself to add support for gray-scale textures, apart from some smaller additions. We also discussed future GTK APIs for HDR images and ICC profiles in a call. Libadwaita also got some features needed for Loupe. The image decoding crates got a bunch of commits to add support for ICC color management profiles. For the libheif bindings, the maintainer took care of most of the needed changes.
I learned a bunch of new things during the development. About GTK internals, custom widgets, the dreaded OpenGL, image formats, color spaces, memfd, and more.
What will Break
Now, with replacing and changing so many bits an pieces, what will break? First of all, there are some limitations when using Loupe via Flatpak. This, however, was already the case with EOG. Using hardware acceleration also means that your graphics drivers have to work. In some rare occasions, like with the PinePhone (non-pro), this seems to be an issue. Switching to software rendering in those cases is possible, however. The tiled rendering code is not working as well as I would like it to. This can, for example, result in SVGs being rendered slightly blurry while using a pinch gesture to zoom. There can also be a short delay in animations for very large images or on older hardware. I did not have the time to properly address the issue for this release.
We are also replacing many of the image decoders with new ones. There are cases where the new image loading has better support for formats, and there are images that can’t be opened at all but work in EOG. The reason is mostly that many image standards have parts that have rarely been used in practice, and therefore, almost no image decoder implementation is 100 % complete. I’m sure that we will find some formats that are used in practice, and support has to be added in future versions. Maybe the fax encoding standards for TIFF?
We are shipping Loupe in its current state as a Core component since we are convinced that the benefits will outweigh the minor regressions we might hit. We also had the chance to confirm this through the feedback we got over the complete development and incubation period.
Closing Thoughts
Most of Loupe has been written over a timespan of nine months, with me also having to move flats during this time. None of the people who directly contributed to Loupe were paid for their work. On one hand, it is promising that we can pull such a project off as a community effort. On the other hand, considering how many companies will rely on such GNOME Core components, the balance does look off to me.
Big thanks go out to everyone who helped make this project happen: Chris, who started the whole project. Allan, for all the design work. Alice, Ivan, Zander, for a huge amount of help in all areas, especially touch gestures, and lower-level plumbing, and … even more gestures! The contributors to all the countless projects Loupe depends on. And everyone who helps with testing and is reporting issues.
The development of Loupe hasn’t been without friction. With the release of GNOME 45, I will be stepping down from maintaining Loupe to not stand in the way of the original ideas of how to run this project.
¹ While decoder processes don’t have write access to files and no network connection, I still consider the hardening of the sandbox in its infancy.
² For those who are interested in the technical details: Glycin spawns one sandboxed process per image file via bwrap
or flatpak-spawn
. The communication happens via peer-to-peer D-Bus over UNIX socket. The file data is read from a GFile
and sent to the sandbox via a separate UNIX socket. The texture data is provided from the sandbox via a memfd that is sealed afterward and given as an mmap to GTK. For animations and SVGs the sandboxed process is kept alive for new frames or tiles as long as needed. For an introduction to this structure, see Christian Hergert, “Designing Multi-Process Application Security,” GUADEC 2019.
³ Developing software with chronic illness and disability would probably be worth a blog post on its own.
Amazing work 👏
Inspiring work, thank you!
Thanks Sophie for your hard work on Loupe, it’s really an important core component and so it can’t be honoured enough. I wish it had some optional productivity hacks like opening in fullscreen by default, but I’m aware of the design goals in terms of standard compliant behaviour.