svgopenlayers

Different results based on browser when drawing an SVG Icon


When running this code in Safari (Version 18.0.1), the image generated is:

Safari Version 18.0.1

However, when running the code in Chrome (Version 131.0.6778.139), the image generated is:

Chrome Version 131.0.6778.139

I can get these two to match if I define the width & height in the svg itself...

<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"...

But, modifying the raw SVG will be impractical for the real situation for reasons that don't matter.

Is there a way to use OpenLayers with the SVG so these cases will match regardless of browser?

const osm = new ol.source.OSM();

osm.setTileGridForProjection(
  "EPSG:4326",
  ol.tilegrid.createXYZ({ extent: [-180, -90, 180, 90] })
);

const tileLayer = new ol.layer.Tile({ source: osm });
const coordinate = [-97, 38];

const svg =
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 0c17.7 0 32 14.3 32 32l0 10.4c93.7 13.9 167.7 88 181.6 181.6l10.4 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-10.4 0c-13.9 93.7-88 167.7-181.6 181.6l0 10.4c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-10.4C130.3 455.7 56.3 381.7 42.4 288L32 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l10.4 0C56.3 130.3 130.3 56.3 224 42.4L224 32c0-17.7 14.3-32 32-32zM107.4 288c12.5 58.3 58.4 104.1 116.6 116.6l0-20.6c0-17.7 14.3-32 32-32s32 14.3 32 32l0 20.6c58.3-12.5 104.1-58.4 116.6-116.6L384 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l20.6 0C392.1 165.7 346.3 119.9 288 107.4l0 20.6c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-20.6C165.7 119.9 119.9 165.7 107.4 224l20.6 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-20.6 0zM256 224a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>';
console.log("svg", svg);

const svgPoint = new ol.Feature({
  geometry: new ol.geom.Point(coordinate),
  properties: {
    name: "svgPoint"
  }
});

const layer = new ol.layer.Vector({
  source: new ol.source.Vector({
    features: [svgPoint]
  }),

  style: (feature) => {
    const properties = feature.getProperties();
    let style;

    if (properties.properties.name === "svgPoint") {
      style = new ol.style.Style({
        image: new ol.style.Icon({
          opacity: 1,
          src: "data:image/svg+xml;base64," + btoa(svg),
          width: 20,
          height: 20
        })
      });
    }

    return style;
  }
});

const map = new ol.Map({
  target: "map",
  layers: [tileLayer, layer],
  view: new ol.View({
    projection: "EPSG:4326",
    center: coordinate,
    zoom: 5
  }),
  controls: ol.control.defaults.defaults({ attribution: false, zoom: false })
});

ol.proj.get("EPSG:4326").setExtent([-180, -85, 180, 85]);
#map {
  height: 256px;
  width: 256px;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/10.2.1/ol.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/10.2.1/dist/ol.min.js"></script>

<div id="map"></div>


Solution

  • The size of an unsized SVG drawn to canvas will depend on the size of the (map) canvas. In Firefox it will not work at all. If you do not need to support Firefox you could add color: 'transparent,` to the icon options to get a more consistent result, but it would be better to inject true default values into the SVG at run time - the SVG can be fetched, parsed, attributes added or changed, then serialized back to an SVG text (you will need to make your code asynchronous)

      const svgDimensions = (text) => {
    const xml = new DOMParser().parseFromString(text, 'application/xml');
    const svg = xml.getElementsByTagName('svg')[0];
    const defaultWidth = 300;
    const defaultHeight = 150;
    // extract any width or height values with 2 character units
    const defaultUnit = '';
    const ww = svg?.getAttribute('width');
    const wUnit = ww?.match(/[a-z]{2}$/)?.[0] || defaultUnit;
    let w = Number(ww?.replace(new RegExp(wUnit + '$'), ''));
    const hh = svg?.getAttribute('height');
    const hUnit = hh?.match(/[a-z]{2}$/)?.[0] || defaultUnit;
    let h = Number(hh?.replace(new RegExp(hUnit + '$'), ''));
    // disregard NaN percentage values
    w = w > 0 ? w : 0;
    h = h > 0 ? h : 0;
    const absent = !(w > 0 && h > 0);
    const viewBox = svg
      ?.getAttribute('viewBox')
      ?.split(/[\s,]+?/)
      .map(Number);
    if (viewBox || absent) {
      const wView = viewBox?.[2];
      const hView = viewBox?.[3];
      if (absent && wView > 0 && hView > 0) {
        // calculate missing dimensions based on viewBox aspect ratio
        // and default height
        if (w > 0) {
          h = (w * hView) / wView;
          svg.setAttribute('height', h + wUnit);
        } else if (h > 0) {
          w = (h * wView) / hView;
          svg.setAttribute('width', w + hUnit);
        } else {
          h = defaultHeight;
          w = (h * wView) / hView;
          svg.setAttribute('width', w + defaultUnit);
          svg.setAttribute('height', h + defaultUnit);
        }
      } else {
        // if no valid viewBox use defaults for missing dimensions
        if (w === 0) {
          w = defaultWidth;
          svg.setAttribute('width', w + defaultUnit);
        }
        if (h === 0) {
          h = defaultHeight;
          svg.setAttribute('height', h + defaultUnit);
        }
      }
      svg.setAttribute('preserveAspectRatio', 'none');
      text = new XMLSerializer().serializeToString(xml) || text;
    }
    return text;
      }
    
      const injectDimensions = async (src) => {
    const blob = await fetch(src)
      .then((response) => {
        if (response.headers.get('content-type').includes('image/svg+xml')) {
          return response.text().then((text) => {
            return new Blob([svgDimensions(text)], {type: 'image/svg+xml'});
          });
        }
        return response.blob();
      });
    return URL.createObjectURL(blob);
      }
    
      (async () => {
    
    const osm = new ol.source.OSM();
    
    osm.setTileGridForProjection(
      "EPSG:4326",
      ol.tilegrid.createXYZ({ extent: [-180, -90, 180, 90] })
    );
    
    const tileLayer = new ol.layer.Tile({ source: osm });
    const coordinate = [-97, 38];
    
    const svg =
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 0c17.7 0 32 14.3 32 32l0 10.4c93.7 13.9 167.7 88 181.6 181.6l10.4 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-10.4 0c-13.9 93.7-88 167.7-181.6 181.6l0 10.4c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-10.4C130.3 455.7 56.3 381.7 42.4 288L32 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l10.4 0C56.3 130.3 130.3 56.3 224 42.4L224 32c0-17.7 14.3-32 32-32zM107.4 288c12.5 58.3 58.4 104.1 116.6 116.6l0-20.6c0-17.7 14.3-32 32-32s32 14.3 32 32l0 20.6c58.3-12.5 104.1-58.4 116.6-116.6L384 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l20.6 0C392.1 165.7 346.3 119.9 288 107.4l0 20.6c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-20.6C165.7 119.9 119.9 165.7 107.4 224l20.6 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-20.6 0zM256 224a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>';
    //console.log("svg", svg);
    
    const svgPoint = new ol.Feature({
      geometry: new ol.geom.Point(coordinate),
      properties: {
        name: "svgPoint"
      }
    });
    
    const src = await injectDimensions("data:image/svg+xml;base64," + btoa(svg));
    //const src = await injectDimensions('https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg');
    
    const layer = new ol.layer.Vector({
      source: new ol.source.Vector({
        features: [svgPoint]
      }),
    
      style: (feature) => {
        const properties = feature.getProperties();
        let style;
    
        if (properties.properties.name === "svgPoint") {
          style = new ol.style.Style({
            image: new ol.style.Icon({
              opacity: 1,
              src: src,
              width: 75,
              height: 50
            })
          });
        }
    
        return style;
      }
    });
    
    const map = new ol.Map({
      target: "map",
      layers: [tileLayer, layer],
      view: new ol.View({
        projection: "EPSG:4326",
        center: coordinate,
        zoom: 5
      }),
      controls: ol.control.defaults.defaults({ attribution: false, zoom: false })
    });
    
    ol.proj.get("EPSG:4326").setExtent([-180, -85, 180, 85]);
    
      }) ();
    #map {
      height: 256px;
      width: 256px;
    }
    <link href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/10.2.1/ol.min.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/10.2.1/dist/ol.min.js"></script>
    
    <div id="map"></div>