Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

fix(icon): Allow using data URLs #7547

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/components/icon/demoLoadSvgIconsFromUrl/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,27 @@
<md-icon md-svg-src="{{ getAndroid() }}" class="s36" aria-label="Android "></md-icon>
<md-icon md-svg-src="img/icons/addShoppingCart.svg" class="s48" aria-label="Cart" ></md-icon>
</p>

<p>Use data URLs (base64 or un-encoded):</p>
<p>
<md-icon
md-svg-src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGcgaWQ9ImNha2UiPjxwYXRoIGQ9Ik0xMiA2YzEuMTEgMCAyLS45IDItMiAwLS4zOC0uMS0uNzMtLjI5LTEuMDNMMTIgMGwtMS43MSAyLjk3Yy0uMTkuMy0uMjkuNjUtLjI5IDEuMDMgMCAxLjEuOSAyIDIgMnptNC42IDkuOTlsLTEuMDctMS4wNy0xLjA4IDEuMDdjLTEuMyAxLjMtMy41OCAxLjMxLTQuODkgMGwtMS4wNy0xLjA3LTEuMDkgMS4wN0M2Ljc1IDE2LjY0IDUuODggMTcgNC45NiAxN2MtLjczIDAtMS40LS4yMy0xLjk2LS42MVYyMWMwIC41NS40NSAxIDEgMWgxNmMuNTUgMCAxLS40NSAxLTF2LTQuNjFjLS41Ni4zOC0xLjIzLjYxLTEuOTYuNjEtLjkyIDAtMS43OS0uMzYtMi40NC0xLjAxek0xOCA5aC01VjdoLTJ2Mkg2Yy0xLjY2IDAtMyAxLjM0LTMgM3YxLjU0YzAgMS4wOC44OCAxLjk2IDEuOTYgMS45Ni41MiAwIDEuMDItLjIgMS4zOC0uNTdsMi4xNC0yLjEzIDIuMTMgMi4xM2MuNzQuNzQgMi4wMy43NCAyLjc3IDBsMi4xNC0yLjEzIDIuMTMgMi4xM2MuMzcuMzcuODYuNTcgMS4zOC41NyAxLjA4IDAgMS45Ni0uODggMS45Ni0xLjk2VjEyQzIxIDEwLjM0IDE5LjY2IDkgMTggOXoiLz48L2c+PC9zdmc+"
class="s24"
aria-label="Cake">
</md-icon>

<md-icon
md-svg-src="data:image/svg+xml;base64,{{ getAndroidEncoded() }}"
class="s36"
aria-label="Android">
</md-icon>

<!-- un-encoded -->
<md-icon
md-svg-src='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g id="add-shopping-cart"><path d="M11 9h2V6h3V4h-3V1h-2v3H8v2h3v3zm-4 9c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zm10 0c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2zm-9.83-3.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.86-7.01L19.42 4h-.01l-1.1 2-2.76 5H8.53l-.13-.27L6.16 6l-.95-2-.94-2H1v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.13 0-.25-.11-.25-.25z"/></g></svg>'
class="s48"
aria-label="Cart">
</md-icon>
</p>
</div>

4 changes: 4 additions & 0 deletions src/components/icon/demoLoadSvgIconsFromUrl/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ angular.module('appDemoSvgIcons', ['ngMaterial'])
$scope.getAndroid = function() {
return 'img/icons/android.svg';
}
/* Returns base64 encoded SVG. */
$scope.getAndroidEncoded = function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment that this is base64 encoded

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need spec test for getAndroidEncoded type of solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGcgaWQ9ImFuZHJvaWQiPjxwYXRoIGQ9Ik02IDE4YzAgLjU1LjQ1IDEgMSAxaDF2My41YzAgLjgzLjY3IDEuNSAxLjUgMS41czEuNS0uNjcgMS41LTEuNVYxOWgydjMuNWMwIC44My42NyAxLjUgMS41IDEuNXMxLjUtLjY3IDEuNS0xLjVWMTloMWMuNTUgMCAxLS40NSAxLTFWOEg2djEwek0zLjUgOEMyLjY3IDggMiA4LjY3IDIgOS41djdjMCAuODMuNjcgMS41IDEuNSAxLjVTNSAxNy4zMyA1IDE2LjV2LTdDNSA4LjY3IDQuMzMgOCAzLjUgOHptMTcgMGMtLjgzIDAtMS41LjY3LTEuNSAxLjV2N2MwIC44My42NyAxLjUgMS41IDEuNXMxLjUtLjY3IDEuNS0xLjV2LTdjMC0uODMtLjY3LTEuNS0xLjUtMS41em0tNC45Ny01Ljg0bDEuMy0xLjNjLjItLjIuMi0uNTEgMC0uNzEtLjItLjItLjUxLS4yLS43MSAwbC0xLjQ4IDEuNDhDMTMuODUgMS4yMyAxMi45NSAxIDEyIDFjLS45NiAwLTEuODYuMjMtMi42Ni42M0w3Ljg1LjE1Yy0uMi0uMi0uNTEtLjItLjcxIDAtLjIuMi0uMi41MSAwIC43MWwxLjMxIDEuMzFDNi45NyAzLjI2IDYgNS4wMSA2IDdoMTJjMC0xLjk5LS45Ny0zLjc1LTIuNDctNC44NHpNMTAgNUg5VjRoMXYxem01IDBoLTFWNGgxdjF6Ii8+PC9nPjwvc3ZnPg==';
}
});
54 changes: 49 additions & 5 deletions src/components/icon/icon.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,20 @@ describe('mdIcon directive', function() {
return {
then: function(fn) {
switch(id) {
case 'android' : fn('<svg><g id="android"></g></svg>');
case 'cake' : fn('<svg><g id="cake"></g></svg>');
case 'android.svg' : fn('<svg><g id="android"></g></svg>');
case 'cake.svg' : fn('<svg><g id="cake"></g></svg>');
case 'image:android': fn('');
case 'android' : fn('<svg><g id="android"></g></svg>');
break;
case 'cake' : fn('<svg><g id="cake"></g></svg>');
break;
case 'android.svg' : fn('<svg><g id="android"></g></svg>');
break;
case 'cake.svg' : fn('<svg><g id="cake"></g></svg>');
break;
case 'image:android' : fn('');
break;
default :
if (/^data:/.test(id)) {
fn(window.atob(id.split(',')[1]));
}
}
}
}
Expand Down Expand Up @@ -240,6 +249,18 @@ describe('mdIcon directive', function() {
expect(el.html()).toEqual('');
}));

describe('with a data URL', function() {
it('should set mdSvgSrc from a function expression', inject(function() {
var svgData = '<svg><g><circle r="50" cx="100" cy="100"></circle></g></svg>';
$scope.getData = function() {
return 'data:image/svg+xml;base64,' + window.btoa(svgData);
}
console.log(svgData)
el = make('<md-icon md-svg-src="{{ getData() }}"></md-icon>');
$scope.$digest();
expect(el[0].innerHTML).toEqual(svgData);
}));
})
});

describe('with ARIA support', function() {
Expand Down Expand Up @@ -419,6 +440,29 @@ describe('mdIcon service', function() {
$scope.$digest();
});

describe('and the URL is a data URL', function() {
var svgData = '<svg><g><circle r="50" cx="100" cy="100"></circle></g></svg>';

describe('and the data is base64 encoded', function() {
it('should return correct SVG markup', function() {
var data = 'data:image/svg+xml;base64,' + btoa(svgData);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need another test for raw, base64 encoded (where we do not use btoa())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I follow. Are you saying you want a test that passes this pre-encoded data

var data = 'data:image/svg+xml;base64,PHN2Zz48Zz48Y2lyY2xlIHI9IjUwIiBjeD0iMTAwIiBjeT0iMTAwIj48L2NpcmNsZT48L2c+PC9zdmc+';

rather than encoding on-the-fly (as below)?

var data = 'data:image/svg+xml;base64,' + btoa(svgData);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In demoLoadSvgIconsFromUrl/script.js you have a function $scope.getAndroidEncoded(), I do not see a unit test for that type of solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a function expression test in my last round of updates (in response to your initial set of comments). You can see it here (line 253): https://github.com/angular/material/pull/7547/files#diff-c132fcb7e577f14138c70b13aff4ea4fR253

$mdIcon(data).then(function(el) {
expect(el.outerHTML).toEqual( updateDefaults(svgData) );
})
$scope.$digest();
});
});

describe('and the data is un-encoded', function() {
it('should return correct SVG markup', function() {
var data = 'data:image/svg+xml,' + svgData;
$mdIcon(data).then(function(el) {
expect(el.outerHTML).toEqual( updateDefaults(svgData) );
})
$scope.$digest();
});
});
});
});

describe('icon set URL is not found', function() {
Expand Down
32 changes: 24 additions & 8 deletions src/components/icon/js/iconService.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,8 @@
/* @ngInject */
function MdIconService(config, $http, $q, $log, $templateCache) {
var iconCache = {};
var urlRegex = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i;
var urlRegex = /[-\w@:%\+.~#?&//=]{2,}\.[a-z]{2,4}\b(\/[-\w@:%\+.~#?&//=]*)?/i;
var dataUrlRegex = /^data:image\/svg\+xml[\s*;\w\-\=]*?(base64)?,(.*)$/i;

Icon.prototype = { clone : cloneSVG, prepare: prepareAndStyle };
getIcon.fontSet = findRegisteredFontSet;
Expand All @@ -392,8 +393,8 @@
// If already loaded and cached, use a clone of the cached icon.
// Otherwise either load by URL, or lookup in the registry and then load by URL, and cache.

if ( iconCache[id] ) return $q.when( iconCache[id].clone() );
if ( urlRegex.test(id) ) return loadByURL(id).then( cacheIcon(id) );
if ( iconCache[id] ) return $q.when( iconCache[id].clone() );
if ( urlRegex.test(id) || dataUrlRegex.test(id) ) return loadByURL(id).then( cacheIcon(id) );
if ( id.indexOf(':') == -1 ) id = '$default:' + id;

var load = config[id] ? loadByID : loadFromIconSet;
Expand Down Expand Up @@ -481,11 +482,26 @@
* Extract the data for later conversion to Icon
*/
function loadByURL(url) {
return $http
.get(url, { cache: $templateCache })
.then(function(response) {
return angular.element('<div>').append(response.data).find('svg')[0];
}).catch(announceNotFound);
/* Load the icon from embedded data URL. */
function loadByDataUrl(url) {
var results = dataUrlRegex.exec(url);
var isBase64 = /base64/i.test(url);
var data = isBase64 ? window.atob(results[2]) : results[2];
return $q.when(angular.element(data)[0]);
}

/* Load the icon by URL using HTTP. */
function loadByHttpUrl(url) {
return $http
.get(url, { cache: $templateCache })
.then(function(response) {
return angular.element('<div>').append(response.data).find('svg')[0];
}).catch(announceNotFound);
}

return dataUrlRegex.test(url)
? loadByDataUrl(url)
: loadByHttpUrl(url);
}

/**
Expand Down