5 October 2024
I was avoiding getting out of bed when I came across a Mastodon post by @Foone that got me thinking. She had expressed a very reasonable thought, characteristic of her posts✪Ok, for the most part. You should consider buying her a coffee, her feed is great., to include descriptions in the metadata of image so you wouldn’t have to write and rewrite alt texts every time you upload a file.
Being half awake, eyes recently unglued, mind acclimatizing to the horrors of consciousness, I misread Foone post and my interest was piqued:
Naturally, I had to try this.
About Images and Metadata
For those not in the know, image files can carry extra data about the image, such as information about the camera that took it, GPS coordinates, and all manner of sundry bits and bobs.
Most of the time, this is done using an almost 30 year old standard called Exif (“Exchangeable image file format”). It’s derived from the TIFF file format, and mostly uses pointer offsets to refer to the data.
More recently, the Extensible Metadata Platform (XMP) standard was put forward by Adobe. XMP stores metadata using the RDF/XML syntax.
Goals
So I’m going to try to:
- Hide an image in within the metadata of an image.
- Without reversing/exploiting anything✪For the sole reason that I’m too dumb to do that..
This is already kind of a thing
Both Exif and XMP can store thumbnails, which technically is an image stored in the metadata.
For XMP, you can find some (scarce) details in the official documentation. For Exif, Section 4.4 covers this in the spec (watch out, big PDF ahead).
I thought that this would sort of fail the “hide” criteria. Obviously, we’d expect to find the thumbnails specified in the metadata to be shown in all manner of places where thumbnails are usually shown. From my exceptionally limited testing, that’s not really the case. My operating system’s file manager (Nemo), nor a image metadata viewer I tried (Geeqie) show thumbnails from the XMP metadata.
Doing the thing
Here’s how I used python-xmp-toolkit
, a python wrapper of Exempi, to add our “inner” image as the “thumbnail” of the “outer” image. This is a very naughty thing to do, like labelling the salt as sugar or jaywalking. Do so at your own peril.
from libxmp import XMPFiles
from base64 import b64encode
import shutil
# Encode the "inner" image using Base64
with open(inner_image_path, "rb") as file:
inner_image_encoded = b64encode(file.read())
# Back-up the old image
shutil.copyfile(outer_image_path, new_image_path)
# Load the existing XMP metadata
xmpfile_new = XMPFiles(file_path=new_image_path, open_forupdate=True)
xmp_new = xmpfile_new.get_xmp()
THUMBNAIL_NS = "http://ns.adobe.com/xap/1.0/g/img/"
# Save the "inner" image in the thumbnail metadata of "outer" image
xmp_new.set_property(THUMBNAIL_NS,
'xmpGImg:image',
inner_image_encoded.decode('utf8'))
xmp_new.set_property(THUMBNAIL_NS,
'xmpGImg:height',
"150")
xmp_new.set_property(THUMBNAIL_NS,
'xmpGImg:width',
"100")
# Write the XMP metadata to the file
xmpfile_new.put_xmp(xmp_new)
xmpfile_new.close_file()
That results in the following image:
You can then check in on the XMP metadata and see our Base64-encoded binary blob in the xmpGImage:image
property.
Now I know your heart is likely still racing, but I’m about to take it up a notch. We just used the thumbnail property, which is meant for images, but we can also just use any arbitrary XMP property meant for text by simply changing the namespace and property:
xmp_new.set_property(consts.XMP_NS_DC,
'format',
inner_image_encoded.decode('utf8'))
To restore the image from the metadata, simply read the XMP property, decode Base64 to bytes, and write to a file:
from base64 import b64decode
xmpfile = XMPFiles(file_path=outer_image_path, open_forupdate=False)
xmp = xmpfile.get_xmp()
THUMBNAIL_NS = "http://ns.adobe.com/xap/1.0/g/img/"
inner_image_encoded = xmp.get_property(THUMBNAIL_NS, 'xmpGImg:image')
# alternatively,
# inner_image_encoded = xmp.get_property(consts.XMP_NS_DC, 'format')
inner_image = b64decode(inner_image_encoded)
with open(new_image_path, 'wb') as file:
file.write(inner_image)
When you restore the image, it looks like this:
Conclusion
I suspect this isn’t particularly useful, but it was fun to explore. I made a quick little CLI tool so you too can pointelessly stuff images inside images, à la Xzibit.
Until next time!