Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle dereferencing errors more gracefully. #803

Merged
merged 2 commits into from
Apr 10, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 8 additions & 1 deletion src/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -1711,7 +1711,14 @@ var map = function (arg) {
defer = defer.then(function () {
var canvas = result;
if (type !== 'canvas') {
result = result.toDataURL(type, encoderOptions);
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,
Expand Down
93 changes: 80 additions & 13 deletions src/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -1191,33 +1248,43 @@ 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);
$('head', container).append(styleElem);
});

$.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();
img.onload = function () {
if (!util.htmlToImageSupported()) {
defer.resolve(img);
};
img.onerror = function () {
defer.reject();
};
img.src = 'data:image/svg+xml;base64,' +
btoa(util.escapeUnicodeHTML(
new XMLSerializer().serializeToString(svg[0])));
} else {
img.onload = function () {
defer.resolve(img);
};
img.onerror = function () {
console.warn('Failed to render html to image');
defer.reject();
};
// 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;
},
Expand Down
48 changes: 44 additions & 4 deletions tests/cases/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)');
Expand All @@ -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');
Expand All @@ -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
Expand Down