0

We need to render SVG images on canvas with Firefox, but Firefox doesn't render SVG images without explicit values for the width and height attributes.

Our users upload SVG images like the ones below without width and height attributes.

How can we use Javascript to dynamically add width and height attributes? The approach below fails.

We read the SVG image as a data URL using FileReader and convert this data URL into a SVG doc with the function below.

Finally, we turn the modified SVG doc into a data URL for rendering on canvas. But the modified SVG doc has the same zero width/height issue as the original.

Codepen: https://codepen.io/Crashalot/pen/vYGJzdN

$('#file').on('change', function() {
    didPickFile($(this));
});

function didPickFile(fileElement) {
   // Get image file.
   let imageFile = fileElement[0].files[0];

   // Get image data.
     didImageUpload(imageFile);

    // Clear filepath so user can reupload same file.
    fileElement.val('');
}

function didImageUpload(file) {
        let reader = new FileReader();

        // Process image when picked.
        reader.onload = async function(e) {
            // Get data URL for image.
              let dataUrl = reader.result;
        let svgSize = {
          width: 512,
          height: 500
        }
        
        let svgDoc = dataURLToSVGDoc(dataUrl);
        let unit = 'px';
        $(svgDoc).attr('width', svgSize.width + unit);
        $(svgDoc).attr('height', svgSize.height + unit);
        let newDataURL = svgDocToDataURL(svgDoc);      
    }
  
    // Read image from hard disk.
        reader.readAsDataURL(file);
}

function dataURLToSVGDoc(dataURI) {
   // Set default result.
   let svgDoc = null;

   // Set RegEx vars.
   let svgURIRegEx = /data:image\/svg\+xml;(base64|charset=utf8),(.*)/;

   // Read data URI.
   let uriMatch = svgURIRegEx.exec(dataURI);
   if (uriMatch) {
      // Set default SVG string.
      let svgStr = '';

      // Base64? Convert Base64 -> SVG.
      if (uriMatch[1] === 'base64') {
         svgStr = atob(uriMatch[2]);

      // Not Base64, ensure no URL-encoded characters.
      } else {
         svgStr = decodeURI(uriMatch[2]);
      }

      // Convert SVG string -> SVG doc.
      let parser = new DOMParser();
      svgDoc = parser.parseFromString(svgStr, 'image/svg+xml');
   }

   // Return result.
   return svgDoc;
}

function svgDocToDataURL(svgDoc, base64) {
   // Set SVG prefix.
   const svgPrefix = "data:image/svg+xml;";

   // Serialize SVG doc.
   var svgData = new XMLSerializer().serializeToString(svgDoc);

   // Base64? Return Base64-encoding for data URL.
   if (base64) {
      var base64Data = btoa(svgData);
      return svgPrefix + "base64," + base64Data;

   // Nope, not Base64. Return URL-encoding for data URL.
   } else {
      var urlData = encodeURIComponent(svgData);
      return svgPrefix + "charset=utf8," + urlData;
   }
}

Sample SVG Icon

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     viewBox="0 0 512 502.2" style="enable-background:new 0 0 512 502.2;" xml:space="preserve">
<style type="text/css">
    .st0{fill:#7C7A7D;}
</style>
<path class="st0" d="M25.1,178.7L0.5,40.9C-2.8,22,9.8,3.9,28.7,0.5l0,0c18.9-3.4,37,9.2,40.4,28.2l24.5,137.8
    c3.4,18.9-9.2,37-28.2,40.4l0,0C46.5,210.2,28.5,197.6,25.1,178.7z"/>
<path class="st0" d="M55.2,137.9l138.9-17.3c19.1-2.4,36.5,11.2,38.8,30.2l0,0c2.4,19.1-11.2,36.5-30.2,38.8L63.8,206.9
    c-19.1,2.4-36.5-11.2-38.8-30.2l0,0C22.6,157.7,36.2,140.3,55.2,137.9z"/>
<path class="st0" d="M506.6,304.6c0-0.2,0.1-0.5,0.1-0.7c6-27.2,7.5-55.7,3.8-84.7c-2.1-16.3-5.8-32.1-10.8-47.2c0,0,0,0,0-0.1
    c-0.5-1.5-1-3-1.6-4.6c-0.3-0.8-0.5-1.5-0.8-2.3c-0.3-0.8-0.6-1.5-0.9-2.3c-0.6-1.5-1.1-3-1.7-4.5c0,0,0,0,0-0.1
    c-5.9-14.8-13.3-29.2-22.2-43C456.5,90.7,437,69.9,414.9,53c-0.2-0.1-0.4-0.3-0.6-0.4c-0.8-0.6-1.6-1.2-2.3-1.8
    C361.4,12.7,296.6-6.6,229.5,2C176.8,8.7,128,32,89.7,67.8c-1.7,1.6-3.2,3.3-4.4,5.1c-25.2,24.8-44.8,54.7-57.7,87.7
    c-7.7,19.8,4.3,41.8,25,46.3l0,0c16.7,3.6,33.3-5.6,39.6-21.5C124.1,104.2,211.9,55,299.7,73.8c62.4,13.4,110.8,58.1,132,114.2
    c0.1,0.3,0.2,0.6,0.4,1c0.1,0.3,0.2,0.6,0.3,1c20,56.5,12,121.9-27.1,172.4c-61,78.9-176.2,93.7-255.1,32.7
    c-30.5-23.6-52.5-56-63.2-92.5c-4.8-16.2-20.7-26.4-37.5-24.2h0c-21.2,2.7-34.9,23.7-28.8,44.2c10.3,34.5,27.8,65.7,50.6,92
    c1.2,2,2.5,3.8,4.1,5.6c81,89.3,218.5,109.8,322.7,42.4c56.8-36.8,93.8-93.3,107.9-155C506.1,306.5,506.3,305.5,506.6,304.6z"/>
</svg>
Crashalot
  • 33,605
  • 61
  • 269
  • 439
  • Could you add `width="100%"` and `height="100%"` in the svg file? – Zach Jensz Sep 01 '20 at 08:14
  • 1
    @zach you mustn't use percentages if you want it to work in Firefox. – Robert Longson Sep 01 '20 at 08:22
  • This might be relevant: https://bugzilla.mozilla.org/show_bug.cgi?id=614649 – Zach Jensz Sep 01 '20 at 08:29
  • @RobertLongson yes, firefox won't render SVG images on canvas with percentages for the width/height attributes. do you have any suggestions for this problem? thanks for your help! – Crashalot Sep 01 '20 at 08:32
  • @Crashalot set values with units that are not percentages on the root element. – Robert Longson Sep 01 '20 at 08:33
  • Not easy to *guess* what values should be used. In that exact case, I guess Chrome will use the values from the viewBox, but having float width and height seems weird, so not sure how exactly they would handle that case, and an even more complicated case would be an svg without width/height nor viewBox. (That's why this behavior is not covered by specs yet) Ps: you could save you a lot of IO by reading the file directly as text. – Kaiido Sep 01 '20 at 08:35
  • @Kaiido yes, we assume there are values from the viewBox. otherwise, it's not possible to know which values to use. do you see anything wrong with this code? it should work, right? – Crashalot Sep 01 '20 at 08:37
  • @RobertLongson we tried this in the code provided, but it's not working. – Crashalot Sep 01 '20 at 08:37
  • @RobertLongson tried to exclude code that wasn't essential to the core problem, but will update shortly with enough to reproduce. – Crashalot Sep 01 '20 at 08:44
  • @RobertLongson updated with codepen so you can reproduce the issue. thanks again! – Crashalot Sep 01 '20 at 08:54

1 Answers1

2

Your call to $(svgDoc) won't wrap the correct <svg> element.

To get it, you would need to do $("svg", svgDoc) so it uses the correct document context and search for the correct <svg> element.

const svg_file = new Blob([`<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     viewBox="0 0 512 502.2" style="enable-background:new 0 0 512 502.2;" xml:space="preserve">
<style type="text/css">
    .st0{fill:#7C7A7D;}
</style>
<path class="st0" d="M25.1,178.7L0.5,40.9C-2.8,22,9.8,3.9,28.7,0.5l0,0c18.9-3.4,37,9.2,40.4,28.2l24.5,137.8
    c3.4,18.9-9.2,37-28.2,40.4l0,0C46.5,210.2,28.5,197.6,25.1,178.7z"/>
<path class="st0" d="M55.2,137.9l138.9-17.3c19.1-2.4,36.5,11.2,38.8,30.2l0,0c2.4,19.1-11.2,36.5-30.2,38.8L63.8,206.9
    c-19.1,2.4-36.5-11.2-38.8-30.2l0,0C22.6,157.7,36.2,140.3,55.2,137.9z"/>
<path class="st0" d="M506.6,304.6c0-0.2,0.1-0.5,0.1-0.7c6-27.2,7.5-55.7,3.8-84.7c-2.1-16.3-5.8-32.1-10.8-47.2c0,0,0,0,0-0.1
    c-0.5-1.5-1-3-1.6-4.6c-0.3-0.8-0.5-1.5-0.8-2.3c-0.3-0.8-0.6-1.5-0.9-2.3c-0.6-1.5-1.1-3-1.7-4.5c0,0,0,0,0-0.1
    c-5.9-14.8-13.3-29.2-22.2-43C456.5,90.7,437,69.9,414.9,53c-0.2-0.1-0.4-0.3-0.6-0.4c-0.8-0.6-1.6-1.2-2.3-1.8
    C361.4,12.7,296.6-6.6,229.5,2C176.8,8.7,128,32,89.7,67.8c-1.7,1.6-3.2,3.3-4.4,5.1c-25.2,24.8-44.8,54.7-57.7,87.7
    c-7.7,19.8,4.3,41.8,25,46.3l0,0c16.7,3.6,33.3-5.6,39.6-21.5C124.1,104.2,211.9,55,299.7,73.8c62.4,13.4,110.8,58.1,132,114.2
    c0.1,0.3,0.2,0.6,0.4,1c0.1,0.3,0.2,0.6,0.3,1c20,56.5,12,121.9-27.1,172.4c-61,78.9-176.2,93.7-255.1,32.7
    c-30.5-23.6-52.5-56-63.2-92.5c-4.8-16.2-20.7-26.4-37.5-24.2h0c-21.2,2.7-34.9,23.7-28.8,44.2c10.3,34.5,27.8,65.7,50.6,92
    c1.2,2,2.5,3.8,4.1,5.6c81,89.3,218.5,109.8,322.7,42.4c56.8-36.8,93.8-93.3,107.9-155C506.1,306.5,506.3,305.5,506.6,304.6z"/>
</svg>`], {type: "image/svg+xml"});
const svgSize = { width: 512, height: 502 };
const reader = new FileReader();
reader.readAsDataURL( svg_file) ;
reader.onload = evt => {

const dataUrl = reader.result;
let svgDoc = dataURLToSVGDoc(dataUrl);
let unit = 'px';
$("svg", svgDoc).attr('width', svgSize.width + unit);
$("svg", svgDoc).attr('height', svgSize.height + unit);
let newDataURL = svgDocToDataURL(svgDoc);

const img = new Image();
img.src = newDataURL;
img.onload = e=>console.log('loaded', img.width);

function dataURLToSVGDoc(dataURI) {
   // Set default result.
   let svgDoc = null;

   // Set RegEx vars.
   let svgURIRegEx = /data:image\/svg\+xml;(base64|charset=utf8),(.*)/;

   // Read data URI.
   let uriMatch = svgURIRegEx.exec(dataURI);
   if (uriMatch) {

      // Set default SVG string.
      let svgStr = '';

      // Base64? Convert Base64 -> SVG.
      if (uriMatch[1] === 'base64') {
         svgStr = atob(uriMatch[2]);

      // Not Base64, ensure no URL-encoded characters.
      } else {
         svgStr = decodeURI(uriMatch[2]);
      }

      // Convert SVG string -> SVG doc.
      let parser = new DOMParser();
      svgDoc = parser.parseFromString(svgStr, 'image/svg+xml');
   }
   // Return result.
   return svgDoc;
}

function svgDocToDataURL(svgDoc, base64) {
   // Set SVG prefix.
   const svgPrefix = "data:image/svg+xml;";

   // Serialize SVG doc.
   var svgData = new XMLSerializer().serializeToString(svgDoc);

   // Base64? Return Base64-encoding for data URL.
   if (base64) {
      var base64Data = btoa(svgData);
      return svgPrefix + "base64," + base64Data;

   // Nope, not Base64. Return URL-encoding for data URL.
   } else {
      var urlData = encodeURIComponent(svgData);
      return svgPrefix + "charset=utf8," + urlData;
   }
}

};
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

Now, note that all this could be simplified by reading this File as text directly:

const svg_file = new Blob([`<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     viewBox="0 0 512 502.2" style="enable-background:new 0 0 512 502.2;" xml:space="preserve">
<style type="text/css">
    .st0{fill:#7C7A7D;}
</style>
<path class="st0" d="M25.1,178.7L0.5,40.9C-2.8,22,9.8,3.9,28.7,0.5l0,0c18.9-3.4,37,9.2,40.4,28.2l24.5,137.8
    c3.4,18.9-9.2,37-28.2,40.4l0,0C46.5,210.2,28.5,197.6,25.1,178.7z"/>
<path class="st0" d="M55.2,137.9l138.9-17.3c19.1-2.4,36.5,11.2,38.8,30.2l0,0c2.4,19.1-11.2,36.5-30.2,38.8L63.8,206.9
    c-19.1,2.4-36.5-11.2-38.8-30.2l0,0C22.6,157.7,36.2,140.3,55.2,137.9z"/>
<path class="st0" d="M506.6,304.6c0-0.2,0.1-0.5,0.1-0.7c6-27.2,7.5-55.7,3.8-84.7c-2.1-16.3-5.8-32.1-10.8-47.2c0,0,0,0,0-0.1
    c-0.5-1.5-1-3-1.6-4.6c-0.3-0.8-0.5-1.5-0.8-2.3c-0.3-0.8-0.6-1.5-0.9-2.3c-0.6-1.5-1.1-3-1.7-4.5c0,0,0,0,0-0.1
    c-5.9-14.8-13.3-29.2-22.2-43C456.5,90.7,437,69.9,414.9,53c-0.2-0.1-0.4-0.3-0.6-0.4c-0.8-0.6-1.6-1.2-2.3-1.8
    C361.4,12.7,296.6-6.6,229.5,2C176.8,8.7,128,32,89.7,67.8c-1.7,1.6-3.2,3.3-4.4,5.1c-25.2,24.8-44.8,54.7-57.7,87.7
    c-7.7,19.8,4.3,41.8,25,46.3l0,0c16.7,3.6,33.3-5.6,39.6-21.5C124.1,104.2,211.9,55,299.7,73.8c62.4,13.4,110.8,58.1,132,114.2
    c0.1,0.3,0.2,0.6,0.4,1c0.1,0.3,0.2,0.6,0.3,1c20,56.5,12,121.9-27.1,172.4c-61,78.9-176.2,93.7-255.1,32.7
    c-30.5-23.6-52.5-56-63.2-92.5c-4.8-16.2-20.7-26.4-37.5-24.2h0c-21.2,2.7-34.9,23.7-28.8,44.2c10.3,34.5,27.8,65.7,50.6,92
    c1.2,2,2.5,3.8,4.1,5.6c81,89.3,218.5,109.8,322.7,42.4c56.8-36.8,93.8-93.3,107.9-155C506.1,306.5,506.3,305.5,506.6,304.6z"/>
</svg>`], {type: "image/svg+xml"});
const svgSize = { width: 512, height: 502 };

const reader = new FileReader();
reader.readAsText( svg_file) ;
reader.onload = evt => {

const markup = reader.result;
let svgDoc = new DOMParser().parseFromString( markup, 'image/svg+xml' );
const unit = 'px';
$("svg", svgDoc).attr( {
  'width': svgSize.width + unit,
  'height': svgSize.height + unit
});
const newMarkup = new XMLSerializer().serializeToString( svgDoc );
const newBlob = new Blob( [ newMarkup ], { type: "image/svg+xml" } );
let newDataURL = URL.createObjectURL( newBlob );

const img = new Image();
img.src = newDataURL;
img.onload = e=>console.log('loaded', img.width);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Yes, we read as a data URL because it's part of a more general function for images so we can't assume SVG format all the time. Thanks so much! The `$("svg", svgDoc)` bit fixed it. – Crashalot Sep 01 '20 at 20:33