Created
November 18, 2018 14:42
-
-
Save badrazizi/70436808c706615e88c4377d8954e0ca to your computer and use it in GitHub Desktop.
HTML5 Android Vector Drawable To Svg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Android Vector Drawable To Svg</title> | |
</head> | |
<body> | |
<!-- STYLE --> | |
<style> | |
body { | |
width: calc(100vw - 25px); | |
height: calc(100vh - 16px); | |
overflow: hidden; | |
display: -webkit-box; | |
display: -webkit-flex; | |
display: -ms-flexbox; | |
display: flex; | |
-webkit-box-orient: vertical; | |
-webkit-box-direction: normal; | |
-webkit-flex-direction: column; | |
-ms-flex-direction: column; | |
flex-direction: column; | |
-webkit-box-pack: center; | |
-webkit-justify-content: center; | |
-ms-flex-pack: center; | |
justify-content: center; | |
-webkit-box-align: center; | |
-webkit-align-items: center; | |
-ms-flex-align: center; | |
align-items: center; | |
background-color: #33393E; | |
} | |
#infoContainer { | |
width: 100%; | |
height: auto; | |
display: -webkit-box; | |
display: -webkit-flex; | |
display: -ms-flexbox; | |
display: flex; | |
-webkit-box-orient: horizontal; | |
-webkit-box-direction: reverse; | |
-webkit-flex-direction: row-reverse; | |
-ms-flex-direction: row-reverse; | |
flex-direction: row-reverse; | |
-webkit-box-pack: center; | |
-webkit-justify-content: center; | |
-ms-flex-pack: center; | |
justify-content: center; | |
-webkit-box-align: center; | |
-webkit-align-items: center; | |
-ms-flex-align: center; | |
align-items: center; | |
} | |
.FileBtn { | |
-webkit-transition: all 0.3s ease-in-out; | |
-o-transition: all 0.3s ease-in-out; | |
transition: all 0.3s ease-in-out; | |
width: 192px; | |
height: 64px; | |
margin: 10px; | |
display: -webkit-box; | |
display: -webkit-flex; | |
display: -ms-flexbox; | |
display: flex; | |
-webkit-box-pack: center; | |
-webkit-justify-content: center; | |
-ms-flex-pack: center; | |
justify-content: center; | |
-webkit-box-align: center; | |
-webkit-align-items: center; | |
-ms-flex-align: center; | |
align-items: center; | |
background-color: #0078d2; | |
color: #fff; | |
-webkit-border-radius: 5px; | |
border-radius: 5px; | |
cursor: pointer; | |
} | |
.FileBtn:hover { | |
background-color: #fff; | |
color: #0078d2; | |
} | |
#infoView { | |
width: calc(100% - 192px); | |
height: auto; | |
margin: 0 20px; | |
color: #fff; | |
} | |
#ImgContainer { | |
height: calc(100% - 212px); | |
width: calc(100% - 60px); | |
overflow-y: auto; | |
overflow-x: hidden; | |
display: -ms-grid; | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(192px, 1fr)); | |
grid-row-gap: 10px; | |
padding: 10px; | |
margin: 20px; | |
background-color: transparent; | |
color: #fff; | |
-webkit-border-radius: 5px; | |
border-radius: 5px; | |
border: solid 1px #0078d2; | |
line-height: 300px; | |
text-align: center; | |
font-size: 4vh; | |
} | |
.imgView { | |
width: 185px; | |
height: 185px; | |
display: -webkit-box; | |
display: -webkit-flex; | |
display: -ms-flexbox; | |
display: flex; | |
-webkit-box-pack: center; | |
-webkit-justify-content: center; | |
-ms-flex-pack: center; | |
justify-content: center; | |
-webkit-box-align: center; | |
-webkit-align-items: center; | |
-ms-flex-align: center; | |
align-items: center; | |
-webkit-border-radius: 5px; | |
border-radius: 5px; | |
border: dashed 2px #0078d2; | |
cursor: pointer; | |
} | |
.imgViewSelected { | |
border: dashed 2px #00FF00; | |
} | |
svg { | |
width:100%; | |
height: auto; | |
} | |
</style> | |
<!-- HTML --> | |
<div id="ImgContainer"> | |
</div> | |
<div id="infoContainer"> | |
<input type="file" id="selectfile" hidden="hidden" multiple="true" > | |
<div class="FileBtn" id="SelectFileBtn"><p>Select File</p></div> | |
<div class="FileBtn" id="SaveFileBtn" style="display:none;"><p>Save</p></div> | |
<div id="infoView"> | |
<p>AndroidVectorDrawable2Svg: This script convert your Android Vector Drawable to a Svg</p> | |
<p>Rewriten to JS by <b>Badr Azizi</b> from <b>Alessandro Lucchet</b> python script and using <b>eligrey</b> Filesaver.js</p> | |
<p>Usage: click select file or drop one or more vector drawable onto the box to convert them to svg format</p> | |
</div> | |
</div> | |
<!-- SCRIPT --> | |
<script> | |
window.onload = function(e) { | |
let ImgContainer = document.getElementById('ImgContainer'); | |
let selectfile = document.getElementById('selectfile'); | |
let SelectFileBtn = document.getElementById('SelectFileBtn'); | |
addListenerMulti(ImgContainer, "dragenter dragover dragleave drop", function(e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
if(e.type === 'drop') { | |
files(e.dataTransfer.files); | |
e.dataTransfer.items.clear(); | |
} | |
}); | |
ImgContainer.addEventListener('click', function (e) { | |
if (e.target.parentNode.className == 'imgView' || e.target.className == 'imgView') { | |
let list = document.querySelectorAll('.imgViewSelected'); | |
Object.keys(list).forEach(l => { | |
list[l].className = 'imgView'; | |
}); | |
if(e.target.className == 'imgView') | |
e.target.className = 'imgView imgViewSelected'; | |
else if (e.target.parentNode.className == 'imgView') | |
e.target.parentNode.className = 'imgView imgViewSelected'; | |
else if (e.target.parentNode.parentNode.className == 'imgView') | |
e.target.parentNode.parentNode.className = 'imgView imgViewSelected'; | |
else if (e.target.parentNode.parentNode.parentNode.className == 'imgView') | |
e.target.parentNode.parentNode.parentNode.className = 'imgView imgViewSelected'; | |
} | |
}); | |
selectfile.onchange = function(e) { | |
if (this.files) { | |
files(this.files); | |
} | |
}; | |
SelectFileBtn.onclick = function(e) { | |
e.preventDefault(); | |
selectfile.click(); | |
} | |
} | |
function addListenerMulti(el, s, fn) { | |
s.split(' ').forEach(e => el.addEventListener(e, fn, false)); | |
} | |
function files(files) { | |
if(typeof f === 'string') { | |
convertVd(files); | |
} | |
else if(typeof files === 'object') { | |
Object.keys(files).forEach(f => { | |
if(files[f].type === 'text/xml') | |
convertVd(files[f].name); | |
else | |
alert(`File type not supported, type: ${files[f].type}, Require: text/xml`); | |
}); | |
} | |
else { | |
alert('No file selected.'); | |
} | |
} | |
// extracts all paths inside vdContainer and add them into svgContainer | |
function convertPaths(vdContainer, svgContainer, svgXml) { | |
let vdPaths = vdContainer.getElementsByTagName('path'); | |
Object.keys(vdPaths).forEach(v => { | |
// only iterate in the first level | |
if(vdPaths[v].parentNode === vdContainer) { | |
let svgPath = svgXml.createElement('path'); | |
svgPath.setAttribute('d', vdPaths[v].attributes['android:pathData'].value); | |
if (vdPaths[v].hasAttribute('android:fillColor')) | |
svgPath.setAttribute('fill', vdPaths[v].attributes['android:fillColor'].value); | |
else | |
svgPath.setAttribute('fill', 'none'); | |
if (vdPaths[v].hasAttribute('android:strokeLineJoin')) | |
svgPath.setAttribute('stroke-linejoin', vdPaths[v].attributes['android:strokeLineJoin'].value); | |
if (vdPaths[v].hasAttribute('android:strokeLineCap')) | |
svgPath.setAttribute('stroke-linecap', vdPaths[v].attributes['android:strokeLineCap'].value); | |
if (vdPaths[v].hasAttribute('android:strokeMiterLimit')) | |
svgPath.setAttribute('stroke-miterlimit', vdPaths[v].attributes['android:strokeMiterLimit'].value); | |
if (vdPaths[v].hasAttribute('android:strokeWidth')) | |
svgPath.setAttribute('stroke-width', vdPaths[v].attributes['android:strokeWidth'].value); | |
if (vdPaths[v].hasAttribute('android:strokeColor')) | |
svgPath.setAttribute('stroke', vdPaths[v].attributes['android:strokeColor'].value); | |
svgContainer.appendChild(svgPath); | |
} | |
}); | |
} | |
// define the function which converts a vector drawable to a svg | |
function convertVd(vdFilePath) { | |
// open vector drawable | |
let xhr = new XMLHttpRequest(); | |
xhr.onreadystatechange = function() { | |
if (xhr.readyState == 4) { | |
if (xhr.status == 200) { | |
let vdXml = (new DOMParser()).parseFromString(xhr.responseText, 'text/xml'); | |
let vdNode = vdXml.getElementsByTagName('vector')[0]; | |
if(!vdNode) { | |
console.log(`This not Android Vector Drawable File '${vdFilePath}'`) | |
return; | |
} | |
// create svg xml | |
let svgXml = document.implementation.createDocument("", "", null); | |
let svgNode = svgXml.createElement('svg'); | |
// setup basic svg info | |
svgNode.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); | |
svgNode.setAttribute('width', vdNode.attributes['android:viewportWidth'].value); | |
svgNode.setAttribute('height', vdNode.attributes['android:viewportHeight'].value); | |
svgNode.setAttribute('viewBox', `0 0 ${vdNode.attributes['android:viewportWidth'].value} ${vdNode.attributes['android:viewportHeight'].value}`); | |
svgXml.appendChild(svgNode); | |
// iterate through all groups | |
vdGroups = vdXml.getElementsByTagName('group'); | |
Object.keys(vdGroups).forEach(v => { | |
// create the group | |
let svgGroup = svgXml.createElement('g'); | |
// setup attributes of the group | |
if (vdGroups[v].hasAttribute('android:translateX') && vdGroups[v].hasAttribute('android:translateY')) | |
svgGroup.setAttribute('transform', `translate(${vdGroups[v].attributes['android:translateX'].value}, ${vdGroups[v].attributes['android:translateY'].value})`); | |
else if (vdGroups[v].hasAttribute('android:translateX')) | |
svgGroup.setAttribute('transform', `translate(${vdGroups[v].attributes['android:translateX'].value}, 0)`); | |
else if (vdGroups[v].hasAttribute('android:translateY')) | |
svgGroup.setAttribute('transform', `translate(0, ${vdGroups[v].attributes['android:translateY'].value})`); | |
// iterate through all paths inside the group | |
convertPaths(vdGroups[v],svgGroup,svgXml); | |
// append the group to the svg node | |
svgNode.appendChild(svgGroup); | |
}); | |
// iterate through all svg-level paths | |
convertPaths(vdNode,svgNode,svgXml); | |
// write xml to file | |
let result = (new XMLSerializer()).serializeToString(svgXml); | |
let i = document.createElement('div'); | |
let blob = new Blob([result], {type: 'image/svg+xml'}); | |
i.className = 'imgView'; | |
i.innerHTML = result; | |
i.id = `image-${ImgContainer.children.length}`; | |
ImgContainer.appendChild(i); | |
saveAs(blob, `${vdFilePath}.svg`) | |
} | |
} | |
} | |
xhr.open('GET', vdFilePath); | |
xhr.send(); | |
} | |
var _global = typeof window === 'object' && window.window === window | |
? window : typeof self === 'object' && self.self === self | |
? self : typeof global === 'object' && global.global === global | |
? global | |
: this | |
function bom (blob, opts) { | |
if (typeof opts === 'undefined') opts = { autoBom: false } | |
else if (typeof opts !== 'object') { | |
console.warn('Depricated: Expected third argument to be a object') | |
opts = { autoBom: !opts } | |
} | |
// prepend BOM for UTF-8 XML and text/* types (including HTML) | |
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF | |
if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { | |
return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type }) | |
} | |
return blob | |
} | |
function download (url, name, opts) { | |
var xhr = new XMLHttpRequest() | |
xhr.open('GET', url) | |
xhr.responseType = 'blob' | |
xhr.onload = function () { | |
saveAs(xhr.response, name, opts) | |
} | |
xhr.onerror = function () { | |
console.error('could not download file') | |
} | |
xhr.send() | |
} | |
function corsEnabled (url) { | |
var xhr = new XMLHttpRequest() | |
// use sync to avoid popup blocker | |
xhr.open('HEAD', url, false) | |
xhr.send() | |
return xhr.status >= 200 && xhr.status <= 299 | |
} | |
// `a.click()` doesn't work for all browsers (#465) | |
function click(node) { | |
try { | |
node.dispatchEvent(new MouseEvent('click')) | |
} catch (e) { | |
var evt = document.createEvent('MouseEvents') | |
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, | |
20, false, false, false, false, 0, null) | |
node.dispatchEvent(evt) | |
} | |
} | |
var saveAs = _global.saveAs || | |
// probably in some web worker | |
(typeof window !== 'object' || window !== _global) | |
? function saveAs () { /* noop */ } | |
// Use download attribute first if possible (#193 Lumia mobile) | |
: 'download' in HTMLAnchorElement.prototype | |
? function saveAs (blob, name, opts) { | |
var URL = _global.URL || _global.webkitURL | |
var a = document.createElement('a') | |
name = name || blob.name || 'download' | |
a.download = name | |
a.rel = 'noopener' // tabnabbing | |
// TODO: detect chrome extensions & packaged apps | |
// a.target = '_blank' | |
if (typeof blob === 'string') { | |
// Support regular links | |
a.href = blob | |
if (a.origin !== location.origin) { | |
corsEnabled(a.href) | |
? download(blob, name, opts) | |
: click(a, a.target = '_blank') | |
} else { | |
click(a) | |
} | |
} else { | |
// Support blobs | |
a.href = URL.createObjectURL(blob) | |
setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s | |
setTimeout(function () { click(a) }, 0) | |
} | |
} | |
// Use msSaveOrOpenBlob as a second approach | |
: 'msSaveOrOpenBlob' in navigator | |
? function saveAs (blob, name, opts) { | |
name = name || blob.name || 'download' | |
if (typeof blob === 'string') { | |
if (corsEnabled(blob)) { | |
download(blob, name, opts) | |
} else { | |
var a = document.createElement('a') | |
a.href = blob | |
a.target = '_blank' | |
setTimeout(function () { click(a) }) | |
} | |
} else { | |
navigator.msSaveOrOpenBlob(bom(blob, opts), name) | |
} | |
} | |
// Fallback to using FileReader and a popup | |
: function saveAs (blob, name, opts, popup) { | |
// Open a popup immediately do go around popup blocker | |
// Mostly only avalible on user interaction and the fileReader is async so... | |
popup = popup || open('', '_blank') | |
if (popup) { | |
popup.document.title = | |
popup.document.body.innerText = 'downloading...' | |
} | |
if (typeof blob === 'string') return download(blob, name, opts) | |
var force = blob.type === 'application/octet-stream' | |
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari | |
var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent) | |
if ((isChromeIOS || (force && isSafari)) && typeof FileReader === 'object') { | |
// Safari doesn't allow downloading of blob urls | |
var reader = new FileReader() | |
reader.onloadend = function () { | |
var url = reader.result | |
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;') | |
if (popup) popup.location.href = url | |
else location = url | |
popup = null // reverse-tabnabbing #460 | |
} | |
reader.readAsDataURL(blob) | |
} else { | |
var URL = _global.URL || _global.webkitURL | |
var url = URL.createObjectURL(blob) | |
if (popup) popup.location = url | |
else location.href = url | |
popup = null // reverse-tabnabbing #460 | |
setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s | |
} | |
} | |
_global.saveAs = saveAs.saveAs = saveAs | |
if (typeof module !== 'undefined') { | |
module.exports = saveAs; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment