Handle dereferencing errors more gracefully.
This will log relevant warnings and not fail if a css file is missing.
Check if html to image via svg canvas is supported.
Add some more tests and comments.
MODIFY
src/map.js
src/map.js
@@ -1711,7 +1711,14 @@ var map = function (arg) {
defer = defer.then(function () {
var canvas = result;
if (type !== 'canvas') {
+ try {
result = result.toDataURL(type, encoderOptions);
+ } catch (err) {
+ console.warn('Failed to convert screenshot to output', err);
+ var failure = $.Deferred();
+ failure.reject();
+ return failure;
+ }
}
m_this.geoTrigger(geo_event.screenshot.ready, {
canvas: canvas,

MODIFY
src/util/index.js
src/util/index.js
@@ -8,10 +8,16 @@ var ClusterGroup = require('./clustering.js');
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+var svgForeignObject = '<svg xmlns="http://www.w3.org/2000/svg">' +
+ '<foreignObject width="100%" height="100%">' +
+ '</foreignObject>' +
+ '</svg>';
+
var m_timingData = {},
m_timingKeepRecent = 200,
m_threshold = 15,
- m_originalRequestAnimationFrame;
+ m_originalRequestAnimationFrame,
+ m_htmlToImageSupport;
/**
* @typedef {object} geo.util.cssColorConversionRecord
@@ -1060,6 +1066,12 @@ var util = module.exports = {
var deferList = [],
results = [];
+ /* Remove comments to avoid dereferencing commented out sections.
+ * To match across lines, use [^\0] rather than . */
+ css = css.replace(/\/\*[^\0]*?\*\//g, '');
+ /* reduce whitespace to make the css shorter */
+ css = css.replace(/\r/g, '\n').replace(/\s+\n/g, '\n')
+ .replace(/\n\s+/g, '\n').replace(/\n\n+/g, '\n');
if (baseUrl) {
var match = /(^[^?#]*)\/[^?#/]*([?#]|$)/g.exec(baseUrl);
baseUrl = match && match[1] ? match[1] + '/' : null;
@@ -1109,6 +1121,49 @@ var util = module.exports = {
});
},
+ /**
+ * Check if the current browser supports covnerting html to an image via an
+ * svg foreignObject and canvas. If this has not been checked before, it
+ * returns a Deferred that resolves to a boolean (never rejects). If the
+ * check has been done before, it returns a boolean.
+ *
+ * @returns {boolean|jQuery.Deferred}
+ */
+ htmlToImageSupported: function () {
+ if (m_htmlToImageSupport === undefined) {
+ var defer = $.Deferred();
+ var svg = $(svgForeignObject);
+ svg.attr({
+ width: '10px',
+ height: '10px',
+ 'text-rendering': 'optimizeLegibility'
+ });
+ $('foreignObject', svg).append('<div/>');
+ var img = new Image();
+ img.onload = img.onerror = function () {
+ var canvas = document.createElement('canvas');
+ canvas.width = 10;
+ canvas.height = 10;
+ var context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ try {
+ canvas.toDataURL();
+ m_htmlToImageSupport = true;
+ } catch (err) {
+ console.warn(
+ 'This browser does not support converting HTML to an image via ' +
+ 'SVG foreignObject. Some functionality will be limited.', err);
+ m_htmlToImageSupport = false;
+ }
+ defer.resolve(m_htmlToImageSupport);
+ };
+ img.src = 'data:image/svg+xml;base64,' + btoa(util.escapeUnicodeHTML(
+ new XMLSerializer().serializeToString(svg[0])));
+ return defer;
+ }
+ return m_htmlToImageSupport;
+ },
+
/**
* Convert an html element to an image. This attempts to localize any
* images within the element. If there are other external references, the
@@ -1124,7 +1179,9 @@ var util = module.exports = {
* @memberof geo.util
*/
htmlToImage: function (elem, parents) {
- var defer = $.Deferred(), container, deferList = [];
+ var defer = $.Deferred(),
+ deferList = [util.htmlToImageSupported()],
+ container;
var parent = $(elem);
elem = $(elem).clone();
@@ -1191,6 +1248,10 @@ var util = module.exports = {
var href = $(this).attr('href');
$.get(href).done(function (css) {
util.dereferenceCssUrls(css, styleElem, styleDefer, undefined, href);
+ }).fail(function (xhr, status, err) {
+ console.warn('Failed to dereference ' + href, status, err);
+ styleElem.remove();
+ styleDefer.resolve();
});
}
deferList.push(styleDefer);
@@ -1198,26 +1259,32 @@ var util = module.exports = {
});
$.when.apply($, deferList).then(function () {
- var svg = $('<svg xmlns="http://www.w3.org/2000/svg">' +
- '<foreignObject width="100%" height="100%">' +
- '</foreignObject></svg>');
+ var svg = $(svgForeignObject);
svg.attr({
width: parent.width() + 'px',
height: parent.height() + 'px',
+ // Adding this via the attr call works in Firefox headless, whereas if
+ // it is part of the svgForeignObject string, it does not.
'text-rendering': 'optimizeLegibility'
});
$('foreignObject', svg).append(container);
var img = new Image();
+ if (!util.htmlToImageSupported()) {
+ defer.resolve(img);
+ } else {
img.onload = function () {
defer.resolve(img);
};
img.onerror = function () {
+ console.warn('Failed to render html to image');
defer.reject();
};
- img.src = 'data:image/svg+xml;base64,' +
- btoa(util.escapeUnicodeHTML(
+ // Firefox requires the HTML to be base64 encoded. Chrome doesn't, but
+ // doing so does no harm.
+ img.src = 'data:image/svg+xml;base64,' + btoa(util.escapeUnicodeHTML(
new XMLSerializer().serializeToString(svg[0])));
+ }
});
return defer;
},

MODIFY
tests/cases/map.js
tests/cases/map.js
@@ -822,8 +822,9 @@ describe('geo.core.map', function () {
done();
});
});
+ // These tests won't work in PhantomJS. See
+ // https://bugs.webkit.org/show_bug.cgi?id=17352, also 29305 and 129172.
if (!isPhantomJS()) {
- // this test won't work in PhantomJS.
it('layer background', function (done) {
var layer3 = m.createLayer('ui');
layer3.node().css('background-image', 'url(/data/tilefancy.png)');
@@ -834,9 +835,6 @@ describe('geo.core.map', function () {
done();
});
}, 10000);
- }
- if (!isPhantomJS()) {
- // this test won't work in PhantomJS.
it('layer css background', function (done) {
geo.jQuery('head').append('<link rel="stylesheet" href="/testdata/test.css" type="text/css"/>');
var layer3 = m.createLayer('ui');
@@ -849,7 +847,49 @@ describe('geo.core.map', function () {
done();
});
}, 10000);
+ it('layer missing css background', function (done) {
+ geo.jQuery('head').append('<link rel="stylesheet" href="/testdata/nosuchfile.css" type="text/css"/>');
+ var layer3 = m.createLayer('ui');
+ layer3.node().addClass('image-background');
+ layer3.opacity(0.5);
+ m.screenshot().then(function (result) {
+ expect(result).not.toEqual(ss.basic);
+ expect(result).not.toEqual(ss.nobackground);
+ m.deleteLayer(layer3);
+ done();
+ });
+ }, 10000);
+ }
+ // end of non-PhantomJS tests
+ if (isPhantomJS()) {
+ it('no html to image warning', function (done) {
+ var layer3 = m.createLayer('ui');
+ layer3.node().css('background-image', 'url(/data/tilefancy.png)');
+ var warn = sinon.stub(console, 'warn', function () {});
+ m.screenshot().then(function (result) {
+ expect(warn.calledOnce).toBe(true);
+ expect(result).toEqual(ss.basic);
+ m.deleteLayer(layer3);
+ console.warn.restore();
+ done();
+ });
+ }, 10000);
+ it('warnings on html to image failures', function (done) {
+ var layer3 = m.createLayer('ui');
+ layer3.node().css('background-image', 'url(/data/tilefancy.png)');
+ var warn = sinon.stub(console, 'warn', function () {});
+ sinon.stub(geo.util, 'htmlToImageSupported', function () { return true; });
+ m.screenshot().fail(function () {
+ expect(warn.calledOnce).toBe(true);
+ expect(warn.calledWith('Failed to convert screenshot to output')).toBe(true);
+ m.deleteLayer(layer3);
+ geo.util.htmlToImageSupported.restore();
+ console.warn.restore();
+ done();
+ });
+ }, 10000);
}
+ // end of PhantomJS tests
it('layers in a different order', function (done) {
m.screenshot([layer2, layer1]).then(function (result) {
// the order doesn't matter

Handle dereferencing errors more gracefully.

This will log relevant warnings and not fail if a css file is missing.

Check if html to image via svg canvas is supported.

Add some more tests and comments.