Unable to use uploaded file for SVG external symbols

Trying to use an uploaded file with SVG’s <use> tag returns a browser security error. See this example: https://observablehq.com/d/3f86e34a6908896f

Interestingly this only happens once a notebook is published/link shared. It works as expected when a notebook is private. (To see this in action, fork the notebook and see a chess pawn appear).

I tested this in the latest Chrome and Firefox. Haven’t tried other browsers.

It seems like when you publish a notebook, file attachments are moved from you personal namespace to static,

https://harrislapiroff.static.observableusercontent.com/
https://static.observableusercontent.com/

When I access your shared notebook the worker is coming from your namespace

https://harrislapiroff.static.observableusercontent.com/worker/worker.249a7c03e7f0275d3429f05c18cde5b5276e994e3ce98e604cbe5bab159cc31b.html

and the file attachment from

https://static.observableusercontent.com/files/88f3a72f7964fb536ffeb7bdc725853cba38542e9e8db7f535e18989a766105464df4255236560845917720d8235f06aa6e99574dec6ea922e89ccba49e85a64

Which is causing the error! Weird, I don’t know why! is it a bug?

I guess when you’re on your private notebook file attachment is coming from ?

https://harrislapiroff.static.observableusercontent.com/files/88f3a72f7964fb536ffeb7bdc725853cba38542e9e8db7f535e18989a766105464df4255236560845917720d8235f06aa6e99574dec6ea922e89ccba49e85a64

I quick hack would be download the svg text and encode it as a base64 string

svgEncoded = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent( await FileAttachment("chesspieces.svg").text())))

than replace your spritesURL with svgEncoded

edit:
here is working example https://observablehq.com/d/857b70e4f458e01b

3 Likes

Very cool trick!

To add to @radames reply, SVG does not support crossorigin use elements:

When files are published, they move from your private origin to a shared public origin. This shared origin supports CORS, but SVG does not.

Another workaround is to embed the SVG file locally, and then use a local URL:

html`<svg viewbox="0 0 100 100" width="100" height="100">
  <defs>
    ${await FileAttachment("chesspieces.svg").text()}
  </defs>
  <use href="${new URL("#white-pawn", location)}" x="0" y="0" width="100" height="100">
</svg>`

(You could use #white-pawn here instead in Chrome and Firefox, but sadly Safari has another bug where anchor fragments are not resolved correctly when the page has a <base href> element.)

3 Likes

Thanks! Very helpful and thorough answer. It seems a shame that SVG doesn’t support CORS, but between these two workarounds, I think I’m good to go!

@mbostock This is maybe a separate question, but unfortunately that approach is causing me some trouble on the notebook I’m actually trying to do this in because I’m using hypertext literal which is auto-escaping that SVG code. Is there any way to disable autoescaping in htl or an elegant workaround?

There are a variety of ways to interpret text as markup (rather than text). One way would be to invoke html or svg as a function, passing in a dynamic string:

html`<svg viewbox="0 0 100 100" width="100" height="100">
  <defs>
    ${svg.fragment({raw: [await FileAttachment("chesspieces.svg").text()]})}
  </defs>
  <use href="${new URL("#white-pawn", location)}" x="0" y="0" width="100" height="100">
</svg>`

A more manual way of doing this would be to use DOMParser:

html`<svg viewbox="0 0 100 100" width="100" height="100">
  <defs>
    ${document.importNode(new DOMParser()
      .parseFromString(await FileAttachment("chesspieces.svg").text(), "image/svg+xml")
      .documentElement, true)}
  </defs>
  <use href="${new URL("#white-pawn", location)}" x="0" y="0" width="100" height="100">
</svg>`

For reasons that are not clear to me these seem to behave slightly differently with respect to the viewBox, and I’m not sure why.

1 Like

I suspect it’s because my SVG code is malformed. viewBox is case sensitive but in my file I have viewbox. Maybe the latter approach is using some sort of lenient parsing that corrects the capitalization? I spent a while wrestling with that last night before I figured it out. It was all the more confusing because I tried to edit it in the developer tools while sleuthing around, but the developer tools also silently corrected the capitalization when I edited it and I didn’t notice for quite a while :joy:

Ah, good catch! Yes, the DOMParser approach is strict because it’s SVG, whereas using Hypertext Literal will be lenient per HTML5 parsing rules.