Skip to content

Instantly share code, notes, and snippets.

@badrazizi
Created November 18, 2018 14:42
Show Gist options
  • Save badrazizi/70436808c706615e88c4377d8954e0ca to your computer and use it in GitHub Desktop.
Save badrazizi/70436808c706615e88c4377d8954e0ca to your computer and use it in GitHub Desktop.
HTML5 Android Vector Drawable To Svg
<!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