Adding images to a Power BI table looks trivial — until you try to publish your visual through AppSource, or notice your reports are making unexpected HTTP requests. The problem isn't the data itself; it's how the image actually reaches the browser.
There are three distinct systems for doing this, and each has a different risk profile. This guide covers them from highest to lowest risk, with copy-ready code for each.
The Underlying Problem
When a Power BI visual assigns a data value to img.src, the browser does exactly what it would do with any other image on the web: it fires an outbound HTTP request. That has three consequences that tend to catch people off guard:
- CSP (Content Security Policy) — certified AppSource visuals ship with a strict content security policy. Any domain that isn't explicitly declared simply won't load.
- Microsoft certification rejection — the AppSource review team actively looks for this pattern and flags it as a Content Requirements policy failure.
- Privacy — your report is leaking which data it's displaying to the outside world, because the server hosting the image receives (and can log) that request.
The Three Systems
1. Direct External URLs — most common, most problematic
Blocked on AppSource · live network requests · requires public access
Your table column holds a plain URL: https://mycompany.com/photos/product.png. The visual assigns it straight to img.src and the browser fires the request.
This works for local use in Power BI Desktop as long as the domain is reachable. It does not work in certified AppSource visuals, in environments with restricted internet access, or with images that require authentication.
// ❌ Fails AppSource review — unvalidated external URL
img.src = node.imageUrl; // could be anythingThis exact pattern was the reason for the certification rejection that prompted this article. The fix is switching methods, not patching around it.
2. Base64 Data URI — the correct approach for certified visuals
AppSource-compatible · zero network requests · data embedded in the model
Instead of a URL, the column holds the entire image encoded as Base64, embedded directly in the data value itself. The format looks like this:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0...
data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABI...The browser makes zero network requests — the file already lives inside the data value. For Microsoft's certification reviewers, this eliminates the CSP and undeclared-domain problem entirely.
The trade-off is model size: a 10KB PNG becomes roughly 13.5KB in Base64. For large catalogs with heavy images, the .pbix model can grow substantially. The fix is using small images, or dynamically generated SVGs (see Method 3).
Validation inside the visual
Your visual's code must validate the format before assigning to img.src. Without validation, an attacker could inject an arbitrary URL into the data and the visual would happily fetch it:
// Only allow data: URIs with an image MIME type — block everything else
function isSafeImageUrl(url: string): boolean {
return /^data:image\/[a-z+.\-]+;base64,/i.test(url);
}
// When reading from the dataView
const raw = String(imgVal);
imageUrl = isSafeImageUrl(raw) ? raw : null;
// When rendering
if (node.imageUrl && isSafeImageUrl(node.imageUrl)) {
img.src = node.imageUrl; // safe: validated as data:image/*
}3. Dynamically Generated SVG — most efficient, no external files
AppSource-compatible · lightweight model · no external images needed
When what you need is an icon, a colored initial, a status indicator, or any simple graphic, you can generate it entirely in Power Query or DAX as a Base64-encoded SVG. No external file required at all.
This is especially useful for category hierarchies where you want to give each level its own visual identity without managing image files.
let
name = [Category],
color = [Color],
initial = Text.Start(name, 1),
svg = "<svg xmlns='http://www.w3.org/2000/svg' width='48' height='48'>"
& "<circle cx='24' cy='24' r='24' fill='" & color & "'/>"
& "<text x='50%' y='54%' dominant-baseline='middle' "
& "text-anchor='middle' font-size='22' font-weight='bold' fill='#fff'>"
& initial & "</text></svg>",
encoded = "data:image/svg+xml;base64,"
& Binary.ToText(Text.ToBinary(svg), BinaryEncoding.Base64)
in
encodedThe result is a column holding the full data URI. The SVG weighs just 200–400 bytes in Base64, versus 5–15KB for an equivalent PNG.
How to Generate Base64 Images
If you already have PNG or JPG images and need to convert them to Base64 to load into the model, these are the three most practical methods:
Power Query (fetched on refresh)
Add a custom column in Power Query. The image downloads once during refresh and stays embedded permanently in the model — no requests are made while the report is open:
"data:image/png;base64," &
Binary.ToText(
Web.Contents("https://your-server.com/image.png"),
BinaryEncoding.Base64
)When to use it: corporate or catalog images that already live on a server you control. The URL is only ever touched during refresh, never at view time.
Python script (batch conversion)
To convert a whole folder of local images and generate a CSV ready to import and merge with your data:
import base64, csv, os
folder = r"C:\my-images"
rows = []
for fname in os.listdir(folder):
if fname.lower().endswith((".png", ".jpg", ".jpeg", ".svg")):
ext = fname.rsplit(".", 1)[1].lower()
mime = "svg+xml" if ext == "svg" else ext
with open(os.path.join(folder, fname), "rb") as f:
b64 = base64.b64encode(f.read()).decode()
rows.append({
"name": fname,
"imageUrl": f"data:image/{mime};base64,{b64}"
})
with open("images.csv", "w", newline="") as f:
w = csv.DictWriter(f, ["name", "imageUrl"])
w.writeheader(); w.writerows(rows)Import images.csv into Power BI and merge it by filename with your data table. A single imageUrl column then holds the complete image.
Method Comparison
| Method | AppSource | Live network | Model size | Complexity |
|---|---|---|---|---|
| Direct external URL | ✗ | Yes | Minimal | Low |
| Base64 PNG/JPG | ✓ | No | High | Medium |
| Base64 generated SVG | ✓ | No | Minimal | Low |
| Power Query + Web.Contents | ✓ | Refresh only | High | Medium |
Recommendation by Use Case
- Brand logos or product photos already on a server you control: use Power Query with Web.Contents. Download once, embed forever.
- A batch of local images: convert with the Python script, import the CSV, merge.
- Icons, initials, colors per category: generate the SVG directly in Power Query. No files, no external dependencies, lightweight model.
- A visual headed to AppSource: any of the above — never a direct external URL. And always add the isSafeImageUrl() validation inside the visual's code.
Practical summary: if the image comes from outside your control, embed it at refresh time with Power Query. If the image is generated or decorative, build it as an SVG in M. Either way, validate the format inside the visual before assigning to img.src.