Skip to content

Commit

Permalink
Merge pull request #803 from OpenGeoscience/screenshot-adjustments
Browse files Browse the repository at this point in the history
Handle dereferencing errors more gracefully.
  • Loading branch information
manthey authored Apr 10, 2018
2 parents a4e9530 + 08c99bb commit a37fa8d
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 18 deletions.
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

0 comments on commit a37fa8d

Please sign in to comment.