Why a security error when downloading PNG?

I’m unable to download the PNG of my SVG, though “Download as PNG” is visible after clicking the three dots.

In the console, I’m told the action is insecure:

Unhandled Promise Rejection: SecurityError: The operation is insecure.
(anonymous function) — worker-7ff35fe9.js:2754
asyncFunctionResume
(anonymous function)
promiseReactionJobWithoutPromise
promiseReactionJob

When I got to line 2754 of worker-7ff35fe9.js, I’m told

Unhandled Promise Rejection: SecurityError: The operation is insecure.

I can download SVGs without a problem. I thought the issue may be that the notebook isn’t published, but I imported someone else’s chart and had no problem downloading the PNG.

I also attempted to use Mike Bostock’s precursor to the built-in PNG download, but again ran into security problems.

This time, the console reads

SecurityError: The operation is insecure.
toBlob — saving-svg.js:67
(anonymous function) — saving-svg.js:67

Line 67 is

    context.canvas.toBlob(resolve);

which is flagged with “SecurityError: The operation is insecure.”

I’m using Safari but have also tested Chrome.

Is there typically something you need to do in order to make PNGs downloadable?

My code is below. Tomorrow I can reduce it to something minimal and provide fake data for it.

function heatmap(data, country) { // forEach method (non-standard d3)
  // Set the svg size; this can be made responsive later (if even wanted)
  const width = 600;
  const height = 600;

  // Row height (legend has a half height for now)
  let rowHeight = height/6
  let rowGap = 6;

  let margin = {"top": rowHeight/3, "bottom": rowHeight/3}
  
  // Initiate svg
  const svg = d3.select(DOM.svg(width, height));

  svg.append("text")
    .text(country + ": Crisis Preparedness Assessment")
    .attr("class", "chartTitle")
    .attr("y", margin.top - rowGap/2)
  
  // Color Ramp
  const colorRamp = d3.scaleLinear()
    .domain([0, 1, 2, 4])
    .range(["#D22027", "#FDB52A", "#589643", "#027546"])

  data.forEach( function (comp, i) {
    svg.append("line")
      .attr("x1", 0)
      .attr("x2", width)
      .attr("y1", margin.top + i * rowHeight)
      .attr("y2", margin.top + i * rowHeight)
      .attr("stroke-width", 0.75)
      .attr("stroke", "#999999")

    svg.append("foreignObject")
      .attr("width", width/3 - rowHeight/5)
      .attr("height", rowHeight)
      .attr("x", rowHeight/5)
      .attr("y", margin.top + rowHeight * (i))
      .append("xhtml:body")
      .html("<div class='components' style='height:" + rowHeight + "px'><text>" + comp.component + "</text></div>")

    svg.append("rect")
      .attr("y", margin.top + rowHeight * i + rowGap/2)
      .attr("height", rowHeight - rowGap)
      .attr("width", rowHeight/5)
      .attr("fill", colorRamp(Math.floor(comp.maturity)))

    let littleRow = (rowHeight - rowGap)/comp.dimensions.length

    comp.dimensions.forEach( (dim, j) => {
      svg.append("foreignObject")
        .attr("width", width/3)
        .attr("height", littleRow)
        .attr("y", margin.top + (rowHeight * i) + rowGap/2 + littleRow * j)
        .attr("x", width / 3)
        .append("xhtml:body")
        .html("<div class='dimensions' style='height:" + littleRow + "px'><text>" + dim.dimension + "</text></div>")
      
      svg.append("rect")
        .attr("y", margin.top + (rowHeight * i) + rowGap/2 + littleRow * j)
        .attr("x", 2 * width / 3)
        .attr("width", width / 3)
        .attr("height", littleRow)
        .attr("fill", colorRamp(Math.floor(dim.maturity)))

      svg.append("text")
        .attr("y", margin.top + (rowHeight * i) + rowGap/2 + littleRow * (j + 0.5))
        .attr("x", 2 * width / 3 + width/6)
        .attr("width", width / 3)
        .attr("height", littleRow)
        .text(Math.round(10 * dim.maturity) / 10)
        .attr("text-anchor", "middle")
        .attr("alignment-baseline", "central")
        .attr("fill", "white")
        .attr("font-weight", "bold")
      
    })
  })

  svg.append("line")
    .attr("x1", 0)
    .attr("x2", width)
    .attr("y1", margin.top + 5 * rowHeight)
    .attr("y2", margin.top + 5 * rowHeight)
    .attr("stroke-width", 0.75)
    .attr("stroke", "#999999")

svg.append("foreignObject")
      .attr("width", width/3 - rowHeight/5)
      .attr("height", rowHeight)
      .attr("y", margin.top + rowHeight * (data.length + 1/3))
      .attr("x", rowHeight/5)
      .append("xhtml:body")
      .html("<div class='components' style='height:" + rowHeight/3 + "px'><text>Maturity Level</text></div>")

  for (let i = 0; i < 5; ++i) {
    svg.append("rect")
      .attr("x", width / 3 + (width / 3 * 2 / 5) * i)
      .attr("y", margin.top + rowHeight * 16/3)
      .attr("width", (width / 3 * 2 / 5))
      .attr("height", rowHeight / 3)
      .attr("fill", colorRamp(i))

    svg.append("text")
      .text(["0 Unmet", "1 Nascent", "2 Basic", "3 Good", "4 Advanced"][i])
      .attr("text-anchor", "middle")
      .attr("alignment-baseline", "central")
      .attr("x", width / 3 + (width / 3 * 2 / 5) * (i + .5))
      .attr("y", margin.top + rowHeight * (16/3) + rowHeight / 6)
      .attr("height", rowHeight / 3)      
      .attr("fill", "white")
      .attr("font-weight", "bold")
  }

    svg.append("style")
    .text(css)
  
  return svg.node()
}  

We will look into it. It would be great if you could link share a notebook (aka unlisted publish) with some test data so we could look at why this particular download is failing.

Thanks, @Cobus. In classic form, I found the problem when attempting to reduce my code to a minimal example: my SVGs included foreignObjects. When I removed them, the PNG would successfully download.

The question, then, becomes how to have multi-line text in a chart. I was using foreignObjects but that seems to break the download function.

Still sharing my notebook, even if the original problem is identified.

1 Like

Good to see you figured out the problem! Text formatting in svg is so painful! I don’t have a good answer for you, unfortunately. I wish you were able to embed html into an svg…
If anyone else here has some good tips, I would love to learn about how to do that.

1 Like

Note that you’re using <text> inside an HTML foreignObject. <text> is an SVG element, and while the browser will still display its contents, you could just as well write <foo> (although you shouldn’t :slight_smile:).