Whether GTK Can Render Its Interface To A Vector Image Format

Occasionally, I want to screenshot my applications in a way that is scalable. Because current compositors appear to exclusively render pixmaps, this isn’t possible at that level. Consequently, I am hoping that GTK 4 may be able to, not least because it is CSS-based.

To summarise, is one theoretically able to implement a Button in a standard GtkApplication, which captures the current interface as a static vectoral image (of which SVG is probably all that I care about), or, better yet, instruct a GTK application to generate such an image from another toolkit?

Placement

I’ve submitted this to Development, but if Mutter is somehow able to accomplish this in a toolkit-inspecific manner, I don’t mind it being transferred elsewhere.

The short answer is no, and the longer answer is even more “no” than a simple “no”.

Compositors do not take SVGs or rendering operations: they hand out memory in the form of windowing system surfaces, and toolkits render inside those surfaces. What’s inside that memory is completely opaque from a compositor’s perspective, so even if GTK would render a bunch of SVG operations, the end result would still be a bunch of pixel data.

Applications only have access to the windowing system surfaces that they receive from the compositor, and nothing else; they cannot ask for other surfaces, owned by other applications.

The fact that GTK uses CSS has nothing to do with anything; it’s just a grammar for describing drawing properties and matching them to specific parts of the UI scene graph.

From this, I’ll conclude that vectoral screenshots would require a very monolithic graphical stack, to be possible from the compositor’s perspective, and that the pixmap stage would need to be immediately derived from the vectoral intermediate, to be consistent.

Indeed. @ebassi, that’s rather why I’m interested in whether I can add such a button to an application, say, when re-compiling it, such that I can capture a screenshot. If the toolkit of the application/window is able to access the source representation of these surfaces, surely generating a static output should be theoretically possible, like the toolkit essentially does each frame, when it generates a bitmap?

You’d need an entire graphics stack that operated by sending SVG around, which is like asking that the entire desktop works like a web rendering engine.

For all intents and purposes, even if you limited yourself to only your application and to GTK, you’d need to re-render the entirety of your UI into an SVG, which is currently not possible. Widgets create rendering operations as a tree of GskRenderNode instances, so you’d need to create a whole new GskRenderer implementation inside GTK that turned render nodes into SVG.

It is somewhat possible by using GSK with Cairo, and Cairo’s SVG backend. Here’s a simple demo, using only public API even:

#! /usr/bin/python3

import cairo

import gi
gi.require_version('Gtk', '4.0')

from gi.repository import Gtk, Gsk, Gdk, Gio, GLib

def make_svg(window, file_path):
    paintable = Gtk.WidgetPaintable.new(window)
    width, height = window.get_default_size()
    width, height = paintable.compute_concrete_size(0, 0, width, height)
    snapshot = Gtk.Snapshot.new()
    paintable.snapshot(snapshot, width, height)
    node = snapshot.to_node()

    with cairo.SVGSurface(file_path, width, height) as surface:
        context = cairo.Context(surface)
        node.draw(context)
        surface.flush()
        surface.finish()

def pick_output_file(button):
    window = button.get_root()

    dialog = Gtk.FileDialog.new()
    dialog.set_accept_label('Render SVG')
    dialog.set_initial_name('screenshot.svg')

    filter = Gtk.FileFilter.new()
    filter.set_name('SVG images')
    filter.add_suffix('svg')
    filter_list = Gio.ListStore.new(Gtk.FileFilter)
    filter_list.append(filter)
    dialog.set_filters(filter_list)

    def picked_file_cb(dialog, async_result):
        file = dialog.save_finish(async_result)
        make_svg(window, file.get_path())
        launcher = Gtk.FileLauncher.new(file)
        launcher.launch(window)

    dialog.save(window, None, picked_file_cb)

window = Gtk.Window()
window.set_title('Render to SVG')
button = Gtk.Button.new_with_label('Make SVG')

button.set_margin_top(6)
button.set_margin_bottom(6)
button.set_margin_start(6)
button.set_margin_end(6)

button.connect('clicked', pick_output_file)
window.set_child(button)
window.present()

while True:
    GLib.MainContext.default().iteration(True)

To make something production-ready out of it, there is of course a bunch of work that needs to happen. Our designers want this, so perhaps one day?