Search code examples
javascriptsvgforeignobject

SVG with foreignObject has a different height when used as an image src attribute


I'm experimenting with taking an image of an existing element. First I copy the image to a foreignObject inside SVG element, then serialize it and use it as a "data" image source. But for some reason in Chrome I see the element rendered a bit differently. Does anybody know why this is the case and what can be done to make it right?

My HTML is this:

<!DOCTYPE html>
<html>

<head>
  <style>
    html,
    body {
      margin: 0
    }

    .x {
      border: 1px solid black;
      float: left;
      width: 100.3px;
    }

  </style>
  <script>
    function test() {
      let x = document.getElementById('xx')
      let i = document.getElementById('if')
      i.contentDocument.body.appendChild(document.getElementById('sv').cloneNode(true))
      let c = i.contentDocument
      let s = c.getElementById('sv')
      let t = c.getElementById('st')
      let f = c.getElementById('fo')
      i.width = x.offsetWidth * 2
      i.height = x.offsetHeight
      s.viewBox.baseVal.width = x.offsetWidth
      s.viewBox.baseVal.height = x.offsetHeight
      s.width.baseVal.value = x.offsetWidth
      s.height.baseVal.value = x.offsetHeight
      f.width.baseVal.value = x.offsetWidth
      f.height.baseVal.value = x.offsetHeight
      t.textContent = document.getElementsByTagName('STYLE')[0].textContent
      f.appendChild(x.cloneNode(true))
      d = new XMLSerializer().serializeToString(s)
      g = c.createElement('IMG')
      g.width = x.offsetWidth
      g.height = x.offsetHeight
      g.src = 'data:image/svg+xml,' + encodeURIComponent(d)
      c.body.appendChild(g)
    }
  </script>
</head>

<body onclick="test()">
  <div class="x" id="xx">
    <ul>
      <li>a</li>
      <li>b</li>
      <li>c</li>
      <li>d</li>
      <li>e</li>
    </ul>
  </div>
  <svg xmlns="http://www.w3.org/2000/svg" id="sv" viewBox="0 0 100 100" width="100px" height="100px">
    <style id="st"></style>
    <foreignObject id="fo" width="100px" height="100px" style="background:lightgray"></foreignObject>
  </svg>
  <iframe id="if"></iframe>
</body>

</html>

Here is the result (after clicking somewhere in the body): 1

The list on the left is the original, then there is the blueprint I use for SVG, then the iframe with two elements in it: one is an SVG element with a foreignObject inside, showing the same list, and another is the IMG element that has the very same SVG, serialized, as its source. And for some reason they render a little bit differently in Chrome (it is fine in Firefox).


Solution

  • OK, so, it seems there is indeed a workaround. It throws things into disarray in Safari, so, I don't have a nice cross-platform code, but the issue is that HTML is rendered a bit differently depending on window.devicePixelRatio. For images, it is always 1. But for the main content it was 2 (retina display).

    The workaround is to first copy the content in SVG as it is here, but wrap that SVG in a DIV with zoom = 1/devicePixelRatio; then adjust all the dimensions based on the cloned content's actual offsetHeight and offsetWidth, and only then do XML serialization and the rest.