Our Old Friend, the Data URL

Example with Gemtext link syntax:

=> data:text/plain,Hello%20world.%20I'm%20an%20attachment. Data URL example

I first looked into data URLs seriously after encountering them in Sean Conner's (now defunct) Gemini client torture test, shortly after I started working on Lagrange. I've since seen these being played with here and there.

When you think about it, data URLs have untapped potential for Gemini. Due to the stipulation that each user action results in no more than a single network request, the usual way of accessing documents composed of heterogeneous content is unavailable. With data URLs, though, it is possible to embed images, CSV-tables, and whatever else into a perfectly normal, spec-compliant Gemtext file — without any ambiguity or new semantics. And the best part is that since clients normally hide raw URLs for basic readability, this doesn't cause (well written) clients to misbehave when encountering such a URL.

While the Gemini protocol specifies a maximum length of 1024 bytes for a Gemini URL, there is no restriction on how long a link line can be in a Gemtext file. And because it's not a Gemini scheme, it never gets sent to a Gemini server that would enforce a length limit. Therefore, one can embed content of arbitrary length. Of course, manually dealing with a .gmi file that has a, say, 100 KB link line can get a bit unwieldy in practice. Long data URLs seem more appropriate when the Gemtext is being generated by a program, e.g., a CGI script or a static generator.

While embedding multi-megabyte base64-encoded JPEGs becomes arduous for everyone involved, one can compress a surprising amount of image data into a few kilobytes, which is quite suitable for embedding. For example:

The URL above is about 1½ screenfuls of base64 in my text editor (5.8 KB). It doesn't hinder editing the text of the file in any significant way. Also, this image is a PNG; there are formats that can represent specific types of images even more compactly, if those happen to be supported.

When it comes to (hyper)links, the Gemini specification states:

Clients can present links to users in whatever fashion the client author wishes

Solderpunk probably didn't have embedded images in mind here, but the language of the specification is clear. Unlike links to resources over the network, these embedded data attachments can be displayed immediately and inline on the same page, since they already contain the data ready to go as part of the main document.

The fallback behavior is graceful. A client can just treat data URLs like any other link, and a) open it as a separate page if the scheme is supported in-app, or b) open it externally in, say, a web browser.

While images are perhaps the most obvious use case, any MIME type can be used when embedding content. This opens the door to arbitrary metadata that gets neatly hidden by clients. One might even go as far as to abuse a link line by using an invisible character as the link label, or slightly less eggregiously:

Why would one use this?

Self-containment: archiving, preventing link rot

Geminispace and the internet in general have been managing quite well linking out to resources. Especially if all the linked-to parts are in your control, it's easier to manage and update them on serverside if they're separate and unencoded. However, published content is accessed more often by others and not by the author. Therefore, packaging the content into one easily downloadable (and archivable) file has its upsides, particularly if the linked resources are integral to the overall message.

Metadata benefits perhaps the most from self-containment. There is no ambiguity in how to find the metadata, if it's too old or too new, or if it exists in the first place, since it's right there in the same file. Obviously people aren't going to agree on a common metadata format, but an elegant way to include metadata has value even if it's only for the author's own needs.

Link rot is a real problem on the internet. When archiving content, to ensure that links remain valid, one could embed the content inside the archived page. In practice, when making a local copy of a page, instead of downloading linked resources and saving them to separate files and then having to deal with a directory tree instead of a single file, a client could insert a data URL after each link.

This might also make sense when making reply posts on a gemlog, in case the linked content disappears or is modified. Of course, when it comes to replies, nothing stops one from putting the entire original post in a block quote `>`, but it's certainly neater if the referenced content has its full original formatting and is hidden by default.

Attachments

The concept of attaching files to a document is age-old in the computing world, e.g., in email messages. Data URLs provide a clean and simple way to attach whatever you want to a .gmi file, and have the data be available directly in the same document. If these attachments are placed out of the way in the end of the document, they won't even hinder reading and editing the file in the normal ways.

In addition to images, one useful type of attachment could be a detached PGP signature, although one would have to omit the data URL when verifying it.

"Rich" image pages

Instead of serving a regular image, one could serve a page that is mostly comprised of the embedded image data, but also has a bit of Gemtext for a rich caption and action links relevant for the task at hand. For example, a graphical game, or an image gallery where you can also make small edits to images or export them in different formats. One could achieve the same by linking out to an image file, but the user experience isn't nearly as nice: if the purpose of the page is to see an image, why would it require another click (and an extra network request) to actually view it?

Image thumbnails

One can use two links: an embedded small thumbnail of an image in a data URL, followed by a link to the original full-sized image. This could even be considered common courtesy, so a reader can decide if a particular image is interesting enough to load fully.

Incentivizing use of small resources

Embedding images and whatnot into a single file creates an inherent incentive to try to compress the embedded data as much as possible, since large files are unwieldy to edit manually and transfer over the network.

Favicons

Instead of the web-like approach of having a /favicon.txt at the site root, we could put a data URL on any index page, whether it be a capsule root page or a gemlog index:

While a client would still have to load the index page to discover the icon, e.g., when subscribing to a gemlog, at least finding it doesn't involve making additional network requests. Also, there is no problem if the favicon changes, as the correct icon is always included. An external file has to be rechecked (at some unspecified frequency) to see if it has changed.

Downsides

Bloat

Encoding binary data in base64 increases the size of the data by 35%. Percent-encoding text also has overhead, depending on how many reserved/international characters are used.

Gemtext pages are typically small, in the order of a kilobyte or two. This makes them well-suited for slow internet connections. If data URLs become prevalent, page sizes may increase by an order of magnitude, if not even more. Embedding one image of any reasonable complexity increases the file size by tens of kilobytes at a minimum.

Client support

On the plus side, data URLs are exactly like regular URLs, so clients can parse them just fine without any additional work. Decoding the contained data is also pretty easy, but of course requires a client to handle the "data:" scheme. The worst case is that you just get a nonfunctional link, showing only the label. However, the data is there in the page source so the user can manually access it.

Very long URLs may expose some inefficiencies or unexpected behavior in clients. For example, I've noticed two problems in Lagrange at the moment: the History sidebar has a visual glitch due to newline characters appearing in decoded data URLs, and navigation history (visited URLs and tab history) may get quite large as they store full data URLs with the base64 encoded content included.

I had to fix a small bug in Lagrange to make data URLs with images work correctly. With the recently added detection of image media types eligible for inlining, there's a mechanism to convert an ongoing page request into an inline media request. However, if the newly inlined media request was already finished at this time, its completion was never signalled to the document and the image in a data URL wouldn't show up. This was also a problem with regular inlined Gemini requests that completed very quickly.

There was a problem with link hover popups, too. There was no limit on how large they could be, so hovering on a data URL could cover the entire window! Although, this is not specific to data URLs as other URLs can potentially be long as well.

Lack of direct referencing

One cannot reference (i.e., link to) a specific piece of embedded data from outside or even from inside the Gemtext document. In theory one could use URL fragments to solve this, but such semantics haven't been defined in Gemini.

Difficulty of generating the link

One has to either percent-encode or base64 encode the content. While you can sort of do the former manually, the latter requires a tool. Command line tools exist, of course, but without a GUI this would be prohibitively complicated for a regular person.

Editing the embedded data

Once encoded, the embedded data is difficult or impossible to modify. In practice, one would need to have a different "authoring format" for all the individual pieces of content. However, automation can help: have a main .gmi file with placeholders for the data URLs, and a script that encodes the attachments and replaces the placeholders to generate the final .gmi.

Humans

People in general won't be mindful to create Gemtext files that are small in size, work in visual and audio contexts, and behave well in all kinds of clients. You will get your 5 MB .gmi files with an embedded JPEG photo that could've been 100 KB without any appreciable loss of quality.

It could help if clients on constrained devices set a maximum size for a page, and require manual confirmation to continue loading long pages.

Breaking constraints

It can be argued that allowing data URLs to be used steps over the intended constraints of Gemini.

Data URLs basically turn Gemtext into a container format that can have arbitrary other data types inside it. This also takes away the user's ability to completely choose which links' content is fetched, giving a bit more power to the server who is effectively able to "preload" content of some links, forcing them on the client whether it wants the data or not.

A compromise would be to add a requirement in the specification that clients must disregard links that are longer than N bytes, where N is a reasonably low number like 4096 or even 1024 bytes. This would allow some of the nice use cases of data URLs (like small metadata attachments), but would protect against inappropriate image attachments, for example.

Conclusion

The takeaway here isn't that we should all start embedding images all over the place (capsule banners and "Under Construction" GIFs, anyone?) or go on a metadata frenzy. I'm just pointing out that this "technology" exists and has valid use cases that could ease certain pain points in Geminispace. Most images and media are better left as separate resources, for several reasons including being able to link to them individually.

I'm writing this not only because data URLs are cool, but because Lagrange v1.11 will have a few related tweaks and fixes, including displaying images in data URLs inline automatically. Minor changes, really, but they make handling data URLs more streamlined.

I'm hoping this has gotten you thinking about the potential use cases for embedded data in Gemtext.

📅 2022-02-13

🏷 Gemini

CC-BY-SA 4.0