← Blog
GuidesAug 21, 2026

How to Insert Images in Power BI Tables and Visuals

By Tino Callarisa

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.
This applies equally to visuals that aren't headed to AppSource. If your report opens in an environment with restricted egress — Power BI Embedded on a corporate network, a sovereign cloud, etc. — external images can silently stop loading with no warning.

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 anything

This 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
  encoded

The 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

MethodAppSourceLive networkModel sizeComplexity
Direct external URLYesMinimalLow
Base64 PNG/JPGNoHighMedium
Base64 generated SVGNoMinimalLow
Power Query + Web.ContentsRefresh onlyHighMedium

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.

FAQ

Can I use external image URLs in a Power BI custom visual?
Not if you want to publish through AppSource certification. Microsoft's review team flags external image URLs as a content-policy violation, and certified visuals ship with a strict CSP that blocks any undeclared domain anyway.
What's the best way to embed images in a Power BI data model without live network requests?
Base64-encode them as data URIs (data:image/png;base64,...). The image lives inside the data value itself, so the browser never makes an outbound request to render it.
How do I add category icons or avatars without managing image files?
Generate them as SVG directly in Power Query M, Base64-encoded. A colored circle with an initial, for example, costs only 200–400 bytes versus 5–15KB for an equivalent PNG — and there's no external file to manage.