Base64 to image: what it is, how to do it, and when you should

In 2018 I spent some time working out how to move medical images, TIFF files among them, between services and into web pages without a separate request for every picture. Base64 turned up everywhere: in data URIs, in JSON payloads, in the FHIR healthcare standard. This is the article I wish I had read first: what Base64 actually is, a worked example small enough to check by hand, working code in Python, Java and JavaScript, and an honest account of when inlining an image as text is a good idea and when it is not.

What Base64 is

Computers store everything as bytes, but a surprising number of the channels we push data through were designed for text: email, HTML, JSON, URLs, XML, configuration files. Hand them raw image bytes and something along the way may treat a byte as a line break, a control character, or the end of the message. Base64 solves this by rewriting any sequence of bytes using only 64 safe characters: A to Z, a to z, 0 to 9, + and /, with = as padding at the end.

The mechanism is simple. Eight bits per byte, six bits per Base64 character, so every three bytes become four characters. Here is the classic example, the word Man:

Input bytesMan
As numbers7797110
As 8-bit binary010011010110000101101110
Regrouped into 6 bits010011   010110   000101   101110
As numbers 0 to 6319   22   5   46
Looked up in the alphabetT   W   F   u

Man becomes TWFu. When the input is not a multiple of three bytes, the last group is padded with zero bits and one or two = signs mark how much padding there was: Ma becomes TWE= and M becomes TQ==. The cost is that four characters carry three bytes of information, so Base64 output is always about a third larger than the input. A 1.9 KB image becomes 2.5 KB of text.

Two things Base64 is not. It is not compression: the output is bigger, never smaller. And it is not encryption, even though it looks like gibberish and often appears next to cryptographic material. Anyone can turn a Base64 string back into the original bytes with one function call, so it protects nothing. It is a transport format, and that is all.

Image to text and back, in Python

The Python standard library has done this since the beginning. Open the file in binary mode, encode, and turn the resulting bytes into a string.

import base64

with open("brain.png", "rb") as f:          # "rb": read the raw bytes, not text
    raw = f.read()

text = base64.b64encode(raw).decode("ascii")   # bytes in, bytes out; .decode turns them into a str
print(len(raw), "bytes of PNG ->", len(text), "characters of Base64")
print(text[:60] + "...")
# 1904 bytes of PNG -> 2540 characters of Base64
# iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAADAFBMVEU7PT4g...

Every PNG file starts with the same eight bytes, so every Base64-encoded PNG starts with iVBORw0KGgo. Once you have seen that prefix a few times you will recognise an inlined PNG anywhere. JPEGs start with /9j/.

To put the image in a web page, prefix the text with a small header that says what it is. This is a data URI, and browsers accept it anywhere they accept a normal image address.

data_uri = "data:image/png;base64," + text
html = f'<img src="{data_uri}" alt="a brain">'
print(html[:80] + "...")
# <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAD...
The site icon, delivered as Base64 text inside this article
This image is not a file on the server. It is 2,540 characters of Base64 text sitting in the HTML of this article, and your browser decoded it.

Going the other way is one call to b64decode. The decoded bytes are a PNG file in memory; BytesIO lets Pillow open them as if they were on disk, and from there they are a NumPy array, which is where every image analysis on this site begins.

import io
import numpy as np
from PIL import Image

raw_again = base64.b64decode(text)
print("round trip identical:", raw_again == raw)

img = Image.open(io.BytesIO(raw_again))        # BytesIO makes bytes in memory look like a file
pixels = np.array(img.convert("RGB"))          # a NumPy array: rows x columns x (red, green, blue)
print(img.format, img.size, pixels.shape, pixels.dtype)
# round trip identical: True
# PNG (32, 32) (32, 32, 3) uint8

The same thing in Java and JavaScript

The original version of this article used the Apache Commons codec and got the last line wrong: Base64.encodeBase64(bytes).toString() returns the memory address of a byte array, not the encoded text, which is an easy mistake to make and a hard one to notice until something downstream fails to decode. Since Java 8 the standard library has a correct and simpler answer.

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

// Java 8 or later: no third-party library needed
byte[] raw = Files.readAllBytes(Paths.get("brain.png"));
String text = Base64.getEncoder().encodeToString(raw);
byte[] rawAgain = Base64.getDecoder().decode(text);

In the browser the job is usually the reverse: a user picks a file and you want to show it, or send it to an API, without uploading it first. FileReader.readAsDataURL produces the complete data URI directly.

// In the browser: read a file the user picked, straight to a data URI
const file = document.querySelector("input[type=file]").files[0];
const reader = new FileReader();
reader.onload = () => { document.querySelector("img").src = reader.result; };
reader.readAsDataURL(file);   // result looks like "data:image/png;base64,iVBOR..."

When to inline an image, and when not to

The reason I went looking at this in the first place was web performance. Every image on a page used to cost a separate HTTP request, and in the HTTP/1.1 era, where a browser would open only a handful of connections to a server, a page with fifty small icons spent most of its load time waiting in line. Steve Souders built a career on documenting this, and the standard fixes were sprites (pack all the icons into one image and show a slice of it with CSS) and inlining (put the image in the page as a data URI, so it arrives with the HTML). Sprites work but are painful to maintain, and for large or unusual formats such as TIFF they do not work at all. Inlining looked like the way out.

It is, for some images. The honest trade-off in 2026:

  • Inline tiny images. Icons, logos, a one-pixel placeholder, anything under a couple of kilobytes. The 33 percent size penalty is a few hundred bytes, and saving a round trip is worth more than that.
  • Serve everything else as a file. A large inlined image bloats the HTML, cannot be cached separately from the page, cannot be lazy-loaded, and blocks the page from rendering until it has fully arrived. A file is fetched in parallel, cached once, and reused across pages.
  • HTTP/2 changed the maths. Requests are cheap now; many can share one connection. The case for sprites has mostly gone, and the case for inlining has shrunk to the very small.
  • APIs are the exception. When an image has to travel inside JSON or XML, Base64 is the only option, and the size penalty is the price of using a text format. This is why the FHIR standard for health data carries images and documents as Base64 in its Attachment and Binary resources, and why a great deal of medical imaging code is really Base64 plumbing.

Mistakes to watch for

  • Bytes versus strings. In Python 3, b64encode takes bytes and returns bytes. Forgetting the .decode() puts b'...' into your JSON. Forgetting to open the file with "rb" gives you a decoding error before you even start.
  • There is more than one alphabet. The URL-safe variant swaps + and / for - and _, because the standard characters have meanings inside a URL. Python has urlsafe_b64encode for it. Decoding one variant with the other fails.
  • Line breaks. Email-style encoders wrap the output every 76 characters. Most decoders tolerate the newlines; some do not. Strip them if in doubt.
  • Missing padding. Some systems drop the trailing = signs to save space. If a decoder complains about “incorrect padding”, append = until the length is a multiple of four.
  • Treating it as security. See above. If a system hides a password behind Base64, it is not hidden.

A note on image search

The title of the original article promised image search, and it is worth being clear about what Base64 contributes to that: nothing. The encoded text is a faithful copy of the bytes and carries no more meaning than they do. Searching for similar images means computing something from the pixels, a hash for exact duplicates or an embedding from a neural network for visual similarity, and comparing those. Base64 is just how the picture gets to the service that does the computing. That work, turning pixels into vectors you can compare, is a topic for the machine learning track of this site.