<header class="container">
  <h1><a href="https://codepen.io/shshaw/full/XbxvNj" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -0.5 61 11.5" shape-rendering="crispEdges" aria-labelledby="title">
    <title>Pixels to SVG</title>
    <g stroke="#f05555"><path stroke="currentColor" d="M0 0h61M0 1h61M0 2h2M5 2h2M8 2h1M10 2h3M14 2h1M19 2h1M21 2h5M29 2h2M41 2h3M47 2h1M49 2h3M53 2h2M58 2h3M0 3h2M3 3h2M6 3h1M8 3h1M10 3h3M14 3h1M16 3h4M21 3h4M26 3h5M33 3h1M41 3h2M44 3h4M49 3h3M53 3h1M55 3h6M0 4h2M3 4h2M6 4h1M8 4h2M11 4h1M13 4h2M18 4h2M21 4h4M26 4h5M32 4h3M37 4h2M41 4h2M44 4h4M49 4h3M53 4h1M55 4h6M0 5h2M5 5h2M8 5h3M12 5h3M16 5h4M21 5h5M28 5h3M33 5h1M36 5h1M39 5h1M41 5h3M46 5h2M49 5h3M53 5h1M55 5h1M58 5h3M0 6h2M3 6h4M8 6h2M11 6h1M13 6h2M16 6h4M21 6h7M29 6h2M33 6h1M36 6h1M39 6h1M41 6h5M47 6h2M50 6h1M52 6h2M55 6h3M59 6h2M0 7h2M3 7h4M8 7h1M10 7h3M14 7h1M16 7h4M21 7h7M29 7h2M34 7h1M37 7h2M41 7h5M47 7h2M50 7h1M52 7h2M55 7h3M59 7h2M0 8h2M3 8h4M8 8h1M10 8h3M14 8h1M19 8h1M24 8h1M28 8h3M41 8h2M46 8h4M51 8h4M58 8h3M0 9h61M0 10h61" /></g>
    <path stroke="#ffffff" d="M2 2h3M7 2h1M9 2h1M13 2h1M15 2h4M20 2h1M26 2h3M31 2h10M44 2h3M48 2h1M52 2h1M55 2h3M2 3h1M5 3h1M7 3h1M9 3h1M13 3h1M15 3h1M20 3h1M25 3h1M31 3h2M34 3h7M43 3h1M48 3h1M52 3h1M54 3h1M2 4h1M5 4h1M7 4h1M10 4h1M12 4h1M15 4h3M20 4h1M25 4h1M31 4h1M35 4h2M39 4h2M43 4h1M48 4h1M52 4h1M54 4h1M2 5h3M7 5h1M11 5h1M15 5h1M20 5h1M26 5h2M31 5h2M34 5h2M37 5h2M40 5h1M44 5h2M48 5h1M52 5h1M54 5h1M56 5h2M2 6h1M7 6h1M10 6h1M12 6h1M15 6h1M20 6h1M28 6h1M31 6h2M34 6h2M37 6h2M40 6h1M46 6h1M49 6h1M51 6h1M54 6h1M58 6h1M2 7h1M7 7h1M9 7h1M13 7h1M15 7h1M20 7h1M28 7h1M31 7h3M35 7h2M39 7h2M46 7h1M49 7h1M51 7h1M54 7h1M58 7h1M2 8h1M7 8h1M9 8h1M13 8h1M15 8h4M20 8h4M25 8h3M31 8h10M43 8h3M50 8h1M55 8h3" />
    </svg></a></h1>

  <p>Need a pixel-perfect scalable image, but all you have is a low-res GIF? You could use <a href="https://css-tricks.com/almanac/properties/i/image-rendering/" target="_blank"><code>image-rendering: pixelated</code></a> and hope the browser will scale it right, or you could use this tool to convert a raster image to SVG.</p>
  
  <p><em>Need more SVG? Try <a href="https://codepen.io/shshaw/pen/doLavv" target="_top">HalfTone.svg</a> and <a href="https://codepen.io/shshaw/pen/BNeBPq">LinePrint.svg</a>.</p>
  
  <p class="note">Now featuring drag & drop uploads, multiple file output, and Web Workers for faster image processing!</p>

  <div class="choices">
    <div class="file">
      Drag & Drop files or 
      <input type="file" id="upload" multiple="multiple" accept=".jpg,.jpeg,.png,.gif,.svg" />
    </div>

    <button id="test">Try a test image: <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAMAAABcgh8DAAAADFBMVEUAAAD/MRjGYwD/lFpY2Im6AAAAAXRSTlMAQObYZgAAAExJREFUeNptzgkKwCAQQ1Enuf+dm8wYbIsfF56CuC6VO0pDACS4tij1DN8WLLUmTltA+Spq5gSmwphEsTRANgXHphXnz7Pk5e929l8Pl0sBHu8kwiYAAAAASUVORK5CYII=" id="testImage" /></button>
    
    <button id="clear">Clear Output</button>
  </div>
</header>

<div id="output"></div>

<div id="filedrag">Drop images to convert here.</div>

<div class="container">
  <h2><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -0.5 17 7" shape-rendering="crispEdges" aria-labelledby="title"><title>Tips</title><path stroke="#000" d="M0 0h5m1 0h1m1 0h3m3 0h3M2 1h1m3 0h1m1 0h1m2 0h1m1 0h1M2 2h1m3 0h1m1 0h1m2 0h1m1 0h1M2 3h1m3 0h1m1 0h3m3 0h2M2 4h1m3 0h1m1 0h1m7 0h1M2 5h1m3 0h1m1 0h1m7 0h1M2 6h1m3 0h1m1 0h1m4 0h3"/></svg></h2>
  <p>Works best with 8-bit images, or graphics where colors are limited and the dimensions are relatively small. After conversion, run the output through <a href="https://jakearchibald.github.io/svgomg/">SVGOMG</a> for even smaller file sizes.</p>

  <h2><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -0.5 20 7" shape-rendering="crispEdges" aria-labelledby="title"><title>How?</title><path stroke="#000" d="M0 0h1m2 0h1m2 0h2m2 0h1m3 0h1m1 0h3M0 1h1m2 0h1m1 0h1m2 0h1m1 0h1m3 0h1m4 0h1M0 2h1m2 0h1m1 0h1m2 0h1m1 0h1m3 0h1m4 0h1M0 3h4m1 0h1m2 0h1m1 0h1m3 0h1m3 0h1M0 4h1m2 0h1m1 0h1m2 0h1m1 0h1m1 0h1m1 0h1m2 0h1M0 5h1m2 0h1m1 0h1m2 0h1m1 0h1m1 0h1m1 0h1M0 6h1m2 0h1m2 0h2m3 0h1m1 0h1m3 0h1"/></svg></h2>
  <p>The image data is extracted from a canvas, and separated by color. The coordinates for each color are combined into single runs where possible to keep the path data syntax short, then merged into one <code>path</code> with a <code>stroke</code> of the color.

  <p>The script has been updated to use Web Workers where available, so larger images should now be processed in a fraction of the time. A 540x540 gif takes only 300 ms versus 30 seconds or a timeout before.</p>

  <h2><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -0.5 32 7" shape-rendering="crispEdges" aria-labelledby="title"><title>Credits</title><g stroke="#000"><path stroke="currentColor" d="M1 0h3m1 0h3m2 0h4m1 0h3m2 0h1m1 0h5m2 0h3M0 1h1m4 0h1m2 0h1m1 0h1m4 0h1m2 0h1m1 0h1m3 0h1m3 0h1M0 2h1m4 0h1m2 0h1m1 0h3m2 0h1m2 0h1m1 0h1m3 0h1m3 0h1M0 3h1m4 0h3m2 0h1m4 0h1m2 0h1m1 0h1m3 0h1m4 0h2M0 4h1m4 0h1m2 0h1m1 0h1m4 0h1m2 0h1m1 0h1m3 0h1m6 0h1M0 5h1m4 0h1m2 0h1m1 0h1m4 0h1m2 0h1m1 0h1m3 0h1m6 0h1M1 6h3m1 0h1m2 0h1m1 0h4m1 0h3m2 0h1m3 0h1m3 0h3"/></g></svg></h2>
  <p>Inspired by <a href="https://github.com/meyerweb/px2svg" target="_blank">px2svg</a>. WebWorker support through <a href="http://catilinejs.com/" target="_blank">Catiline.js</a>.</p>
</div>
@accent: #f05555;
@accent-light: #FF7777;

@gray-dark: #535050;
@gray: #ddd;
@gray-light: #eaeaea;
@gray-lighter: #f5f5f5;

body { 
  background: @gray-lighter;
  padding: 1em;
  padding-bottom: 2em;
  margin: auto;
  font-size: 14px;
  line-height: 1.4;
}

.container { margin: 1em auto; max-width: 40em; }
.container:first-child { margin-top: 0; }

.note { background: #ffee77; font-size: 0.8em; text-align: center; padding: 0.5em; }

h1, h2 { display: block; margin: 0 auto 0.5em; }

h2 { font-size: 1.4em; color: @accent; }
h2 svg { height: 21px; width: auto; margin: 0; }
h2 svg path { stroke: currentColor; }

p { margin: 0 auto 1em; }

a { 
  color: @accent;
  transition: color 250ms ease-in-out;
  &:hover,
  &:focus { color: @accent-light; }
}
img { vertical-align: middle; }
button { 
  background: #ddd;
  border: none;
  appearance: none; 
  cursor: pointer;
}
button, .file { 
  padding: 0.75em;
}


svg, label, input[type=file] { 
  display: block; 
  margin: auto;
  text-align: center;
}
label small { display: block; }

svg { max-width: 100%; }

.choices { 
  background: #eaeaea;
  display: flex;
  align-items: stretch;
}

.choices button {
  background: transparent;
  font-size: 0.8em; 
  border-left: solid 3px #aaa;
}

.choices > * { flex: 1 1 auto; }
.file { overflow: hidden; flex-basis: 40%; }

#output { margin: auto auto 2em; text-align: center; }

.output { 
  display: flex;
  width: 100%;
  margin: 0;
  justify-content: space-between;
  align-items: stretch;
  padding: 1em 0;
  border-bottom: solid 3px @gray;
}
.output > a { 
  display: block; 
  width: 50%;
}
.output svg { 
  width: 100%;
  height: auto;
  margin: 1em auto;
}
.output__details { 
  width: 50%;
  display: flex;
  flex-direction: column;
}
.output__raw {
  display: block;
  background: #eaeaea;
  white-space: pre-wrap;
  padding: 0.5em;
  font-size: 0.75em;
  max-height: 20em;
  overflow: auto;
  text-align: left;
}
.download,
.output__size { 
  display: inline-block;
  text-align: center;
  margin: 1em auto 0;
  padding: 0.25em 0.5em;
  font-size: 0.8em;
}

.download { background-color: #f05555; color: #FFF; }

#filedrag { display: none; }
#filedrag.dragenter { 
  position: fixed;
  top: 0; right: 0; bottom: 0; left: 0;
  margin: auto;
  background: #ff5555;
  background: rgba(255,200,200,0.8);
  text-align: center;
  display: block;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  z-index: 999;
}
View Compiled
console.clear();
console.time = console.time || function(){};
console.timeEnd = console.timeEnd || function(){};

(function(){

  "use strict";

  function each(obj,fn) {
    var length = obj.length,
        likeArray = ( length === 0 || ( length > 0 && (length - 1) in obj ) ),
        i = 0;

    if ( likeArray ) {
      for ( ; i < length; i++ ) { if ( fn.call( obj[ i ], i, obj[ i ] ) === false ) { break; } }
    } else {
      for (i in obj) { if ( fn.call( obj[ i ], i, obj[ i ] ) === false ) { break; } }
    }
  }

  function convertImage(img){
    "use strict";

    function each(obj,fn) {
      var length = obj.length,
          likeArray = ( length === 0 || ( length > 0 && (length - 1) in obj ) ),
          i = 0;

      if ( likeArray ) {
        for ( ; i < length; i++ ) { if ( fn.call( obj[ i ], i, obj[ i ] ) === false ) { break; } }
      } else {
        for (i in obj) { if ( fn.call( obj[ i ], i, obj[ i ] ) === false ) { break; } }
      }
    }

    function componentToHex(c) {
      var hex = parseInt(c).toString(16);
      return hex.length == 1 ? "0" + hex : hex;
    }

    function getColor(r,g,b,a){
      a = parseInt(a);
      if ( a === undefined || a === 255 ) { return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); }
      if ( a === 0 ) { return false; }
      return 'rgba('+r+','+g+','+b+','+(a/255)+')';
    }

    // Optimized for horizontal lines
    function makePathData(x,y,w) { return ('M'+x+' '+y+'h'+w+''); }
    function makePath(color,data) { return '<path stroke="'+color+'" d="'+data+'" />\n'; }

    function colorsToPaths(colors){

      var output = ""; 

      // Loop through each color to build paths
      each(colors,function(color,values){
        var orig = color;
        color = getColor.apply(null,color.split(','));

        if ( color === false ) { return; }

        var paths = [];
        var curPath;
        var w = 1;

        // Loops through each color's pixels to optimize paths
        each(values,function(){

          if ( curPath && this[1] === curPath[1] && this[0] === (curPath[0] + w) ) {
            w++;
          } else {
            if ( curPath ) {
              paths.push(makePathData(curPath[0],curPath[1],w));
              w = 1;
            }
            curPath = this;
          }

        });

        paths.push(makePathData(curPath[0],curPath[1],w)); // Finish last path
        output += makePath(color,paths.join(''));
      });

      return output;
    }

    var getColors = function(img) {
      var colors = {},
          data = img.data,
          len = data.length,
          w = img.width,
          h = img.height,
          x = 0,
          y = 0,
          i = 0,
          color;

      for (; i < len; i+= 4) {
        if ( data[i+3] > 0 ) {
          color = data[i]+','+data[i+1]+','+data[i+2]+','+data[i+3];
          colors[color] = colors[color] || [];
          x = (i / 4) % w;
          y = Math.floor((i / 4) / w);
          colors[color].push([x,y]);
        }                      
      }

      return colors;
    }

    var window = window || {};
    window.CP = { 
      shouldStopExecution: function(){ return false; },
      exitedLoop: function(){}
    };

    var colors = getColors(img),
        paths = colorsToPaths(colors),
        output = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -0.5 '+img.width+' '+img.height+'" shape-rendering="crispEdges">\n<metadata>Made with Pixels to Svg https://codepen.io/shshaw/pen/XbxvNj</metadata>\n' + paths + '</svg>';

    // Send message back to the main script
    return output;

  };
  
  
  // File Output
  var outputDiv = document.getElementById('output');
  
  function fileSize(str) {
    var bytes = encodeURI(str).split(/%..|./).length - 1;
    if ( bytes === 0 ) return 0;
    var sizes = ['bytes', 'kb', 'mb', 'gb', 'tb'],
        i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))),
        size = bytes / Math.pow(1024, i);
    return (Math.round(size * 100) / 100) + ' ' + sizes[i];
  };
  
  function downloadLink(output,fileName,linkContent) {
    return '<a href="data:Application/octet-stream,'+ encodeURIComponent(output) +'" download="'+fileName+'.svg">' + (linkContent || output ) + '</a>';
  }
  
  function showOutput(output,fileName) {
    
    outputDiv.innerHTML = '<figure class="output">' + downloadLink(output,fileName) + '<figcaption class="output__details"><em class="output__size">Output size: ' + fileSize(output) + '</em>' + downloadLink(output,fileName,'<span class="download">Download SVG</span>') + '<pre contentEditable="true"  class="output__raw">' + output.replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</pre></figcaption></figure>'  + outputDiv.innerHTML;

    console.timeEnd('conversion');
  }

  // Convert image to canvas ImageData
  function imageToData(img) {

    var canvas = document.createElement("canvas"),
        ctx = canvas.getContext("2d"),
        width = img.width,
        height = img.height;

    canvas.width = width;
    canvas.height = height;
    ctx.drawImage(img,0,0);
    
    return ctx.getImageData(0,0,width,height);
  }
  
  var imageWorker = cw(convertImage);
  function convert(img,fileName){

    img = (img.type ? this : img ); // use `this` if `img` is event
    if ( !img || img === window ) { return false; }
    
    console.time('conversion');
    fileName = fileName || 'pixels';
    
    var imgData = imageToData(img);

    if ( !Modernizr.webworkers || !Modernizr.blobworkers ) {
      console.log('No workers or blog support. Larger images may timeout.');
      var converted = convertImage(imgData);
      showOutput(converted,fileName);
    } else {
      imageWorker.data(imgData).then(function(converted){
        showOutput(converted,fileName);
      },function(e){ 
        outputDiv.innerHTML = outputRaw.innerHTML = "";
        console.error(e);
        console.timeEnd('conversion');
      });
    }
  }

  function makeImage(src,callback){
    var img = new Image();
    img.onload = callback;
    img.src = ( src.target ? src.target.result : src );
  }

  function loadFiles(e){
    var files = (e.target.files || e.dataTransfer.files || uploader.files),
        len = files.length,
        i = 0;

    each(files,function(i,file){
      var reader = new FileReader();
      var fileName = file.name;
      fileName = fileName.slice(0,fileName.lastIndexOf('.')) || fileName + "";
      reader.onload = function(e){
        console.log(e,arguments);
        makeImage(e,function(img){ convert(this,fileName); });
      }
      reader.readAsDataURL(files[i]);
    });
   
  }

  // File Uploader
  var uploader = document.getElementById('upload');
  uploader.onchange = loadFiles;

  // Test Image Conversion
  var test = document.getElementById('test');
  var testImage = document.getElementById('testImage');
  test.onclick = function(){ convert(testImage,'test'); }
  
  // Clear Output
  var clear = document.getElementById('clear');
  clear.onclick = function(){ output.innerHTML = ""; };
  
  // Drag & Drop
  var fileDrag = document.getElementById('filedrag');
  
  function FileDragReset(e){
    e.preventDefault();
    fileDrag.className = '';
  }
  
  function FileDragDrop(e){
    e = e || window.event;
		FileDragReset(e);    
    loadFiles(e);
  }
	
  fileDrag.addEventListener("dragleave", FileDragReset);
  document.addEventListener("dragenter", function(){ fileDrag.className = 'dragenter'; });
  document.addEventListener('dragover',function(e){ e.preventDefault(); /* Essential! */ });
  document.addEventListener("drop", FileDragDrop);

}());
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://codepen.io/shshaw/pen/PqvYBB.js
  2. https://codepen.io/shshaw/pen/epmrgO.js