Add support for loading cross origin images using proxy

This commit is contained in:
MoyuScript 2017-09-04 23:36:19 +08:00
parent 286be1f6f0
commit 5ad8639bc6
10 changed files with 122 additions and 17 deletions

View File

@ -1,6 +1,7 @@
const Server = require('karma').Server; const Server = require('karma').Server;
const cfg = require('karma').config; const cfg = require('karma').config;
const path = require('path'); const path = require('path');
const proxy = require('html2canvas-proxy');
const karmaConfig = cfg.parseConfig(path.resolve('./karma.conf.js')); const karmaConfig = cfg.parseConfig(path.resolve('./karma.conf.js'));
const server = new Server(karmaConfig, (exitCode) => { const server = new Server(karmaConfig, (exitCode) => {
console.log('Karma has exited with ' + exitCode); console.log('Karma has exited with ' + exitCode);
@ -15,8 +16,9 @@ const filenamifyUrl = require('filenamify-url');
const CORS_PORT = 8081; const CORS_PORT = 8081;
const corsApp = express(); const corsApp = express();
corsApp.use(cors()); corsApp.use('/proxy', proxy());
corsApp.use('/', express.static(path.resolve(__dirname))); corsApp.use('/cors', cors(), express.static(path.resolve(__dirname)));
corsApp.use('/', express.static(path.resolve(__dirname, '/tests')));
corsApp.use((error, req, res, next) => { corsApp.use((error, req, res, next) => {
console.error(error); console.error(error);
next(); next();

View File

@ -39,6 +39,7 @@
"filenamify-url": "1.0.0", "filenamify-url": "1.0.0",
"flow-bin": "0.50.0", "flow-bin": "0.50.0",
"glob": "7.1.2", "glob": "7.1.2",
"html2canvas-proxy": "1.0.0",
"jquery": "3.2.1", "jquery": "3.2.1",
"karma": "1.7.0", "karma": "1.7.0",
"karma-chrome-launcher": "2.2.0", "karma-chrome-launcher": "2.2.0",

View File

@ -59,9 +59,9 @@ const testBase64 = (document: Document, src: string): Promise<boolean> => {
}); });
}; };
const testCORS = () => { const testCORS = () => typeof new Image().crossOrigin !== 'undefined';
return typeof new Image().crossOrigin !== 'undefined';
}; const testResponseType = () => typeof new XMLHttpRequest().responseType === 'string';
const testSVG = document => { const testSVG = document => {
const img = new Image(); const img = new Image();
@ -156,6 +156,20 @@ const FEATURES = {
const value = testCORS(); const value = testCORS();
Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value}); Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value});
return value; return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_RESPONSE_TYPE() {
'use strict';
const value = testResponseType();
Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_CORS_XHR() {
'use strict';
const value = 'withCredentials' in new XMLHttpRequest();
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
return value;
} }
}; };

View File

@ -8,6 +8,7 @@ export type ImageElement = Image | HTMLCanvasElement;
type ImageCache<T> = {[string]: Promise<T>}; type ImageCache<T> = {[string]: Promise<T>};
import FEATURES from './Feature'; import FEATURES from './Feature';
import {Proxy} from './Proxy';
// $FlowFixMe // $FlowFixMe
export default class ImageLoader<T> { export default class ImageLoader<T> {
@ -46,7 +47,10 @@ export default class ImageLoader<T> {
return this.addImage(src, src, false); return this.addImage(src, src, false);
} else if (!this.isSameOrigin(src)) { } else if (!this.isSameOrigin(src)) {
if (typeof this.options.proxy === 'string') { if (typeof this.options.proxy === 'string') {
// TODO proxy this.cache[src] = Proxy(src, this.options).then(src =>
loadImage(src, this.options.imageTimeout || 0)
);
return src;
} else if (this.options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES) { } else if (this.options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES) {
return this.addImage(src, src, true); return this.addImage(src, src, true);
} }
@ -61,7 +65,12 @@ export default class ImageLoader<T> {
if (this.hasImageInCache(src)) { if (this.hasImageInCache(src)) {
return this.cache[src]; return this.cache[src];
} }
// TODO proxy if (!this.isSameOrigin(src) && typeof this.options.proxy === 'string') {
return (this.cache[src] = Proxy(src, this.options).then(src =>
loadImage(src, this.options.imageTimeout || 0)
));
}
return this.xhrImage(src); return this.xhrImage(src);
} }
@ -71,7 +80,12 @@ export default class ImageLoader<T> {
xhr.onreadystatechange = () => { xhr.onreadystatechange = () => {
if (xhr.readyState === 4) { if (xhr.readyState === 4) {
if (xhr.status !== 200) { if (xhr.status !== 200) {
reject(`Failed to fetch image ${src} with status code ${xhr.status}`); reject(
`Failed to fetch image ${src.substring(
0,
256
)} with status code ${xhr.status}`
);
} else { } else {
const reader = new FileReader(); const reader = new FileReader();
// $FlowFixMe // $FlowFixMe
@ -87,7 +101,9 @@ export default class ImageLoader<T> {
const timeout = this.options.imageTimeout; const timeout = this.options.imageTimeout;
xhr.timeout = timeout; xhr.timeout = timeout;
xhr.ontimeout = () => xhr.ontimeout = () =>
reject(__DEV__ ? `Timed out (${timeout}ms) fetching ${src}` : ''); reject(
__DEV__ ? `Timed out (${timeout}ms) fetching ${src.substring(0, 256)}` : ''
);
} }
xhr.open('GET', src, true); xhr.open('GET', src, true);
xhr.send(); xhr.send();

62
src/Proxy.js Normal file
View File

@ -0,0 +1,62 @@
/* @flow */
'use strict';
import type Options from './index';
import FEATURES from './Feature';
export const Proxy = (src: string, options: Options): Promise<string> => {
if (!options.proxy) {
return Promise.reject(__DEV__ ? 'No proxy defined' : null);
}
const proxy = options.proxy;
return new Promise((resolve, reject) => {
const responseType =
FEATURES.SUPPORT_CORS_XHR && FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
const xhr = FEATURES.SUPPORT_CORS_XHR ? new XMLHttpRequest() : new XDomainRequest();
xhr.onload = () => {
if (xhr instanceof XMLHttpRequest) {
if (xhr.status === 200) {
if (responseType === 'text') {
resolve(xhr.response);
} else {
const reader = new FileReader();
// $FlowFixMe
reader.addEventListener('load', () => resolve(reader.result), false);
// $FlowFixMe
reader.addEventListener('error', e => reject(e), false);
reader.readAsDataURL(xhr.response);
}
} else {
reject(
__DEV__
? `Failed to proxy image ${src.substring(
0,
256
)} with status code ${xhr.status}`
: ''
);
}
} else {
resolve(xhr.responseText);
}
};
xhr.onerror = reject;
xhr.open('GET', `${proxy}?url=${encodeURIComponent(src)}&responseType=${responseType}`);
if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
xhr.responseType = responseType;
}
if (options.imageTimeout) {
const timeout = options.imageTimeout;
xhr.timeout = timeout;
xhr.ontimeout = () =>
reject(__DEV__ ? `Timed out (${timeout}ms) proxying ${src.substring(0, 256)}` : '');
}
xhr.send();
});
};

View File

@ -9,13 +9,13 @@
font-family: Arial; font-family: Arial;
} }
</style> </style>
<base href="http://www.google.com/" /> <base href="http://localhost:8081/" />
</head> </head>
<body> <body>
<h1>External image</h1> <h1>External image</h1>
<img src="http://www.google.com/logos/2011/gregormendel11-hp.jpg" style="border:5px solid black;" /> <img src="http://localhost:8081/assets/image2.jpg" style="border:5px solid black;" />
<h1>External image (using &lt;base&gt; href)</h1> <h1>External image (using &lt;base&gt; href)</h1>
<img src="/logos/2011/gregormendel11-res.jpg" /> <img src="/assets/image_1.jpg" />
</body> </body>
</html> </html>

View File

@ -15,6 +15,6 @@
</head> </head>
<body> <body>
<h1>External image (CORS)</h1> <h1>External image (CORS)</h1>
<img src="http://localhost:8081/tests/assets/image2.jpg" /> <img src="http://localhost:8081/cors/tests/assets/image2.jpg" />
</body> </body>
</html> </html>

View File

@ -5,6 +5,7 @@ const fs = require('fs');
const webpack = require('webpack'); const webpack = require('webpack');
const config = require('../webpack.config'); const config = require('../webpack.config');
const serveIndex = require('serve-index'); const serveIndex = require('serve-index');
const proxy = require('html2canvas-proxy');
const PORT = 8080; const PORT = 8080;
const CORS_PORT = 8081; const CORS_PORT = 8081;
@ -17,8 +18,9 @@ app.listen(PORT, () => {
}); });
const corsApp = express(); const corsApp = express();
corsApp.use(cors()); corsApp.use('/proxy', proxy());
corsApp.use('/', express.static(path.resolve(__dirname, '../'))); corsApp.use('/cors', cors(), express.static(path.resolve(__dirname, '../')));
corsApp.use('/', express.static(path.resolve(__dirname, '.')));
corsApp.listen(CORS_PORT, () => { corsApp.listen(CORS_PORT, () => {
console.log(`CORS server running on port ${CORS_PORT}`); console.log(`CORS server running on port ${CORS_PORT}`);
}); });

View File

@ -6,7 +6,14 @@ var REFTEST = window.location.search.indexOf('reftest') !== -1;
(function(document, window) { (function(document, window) {
function appendScript(src) { function appendScript(src) {
document.write( document.write(
'<script type="text/javascript" src="' + src + '.js?' + Math.random() + '"></script>' '<script type="text/javascript" src="' +
window.location.protocol +
'//' +
window.location.host +
src +
'.js?' +
Math.random() +
'"></script>'
); );
} }
@ -147,7 +154,7 @@ var REFTEST = window.location.search.indexOf('reftest') !== -1;
$.extend( $.extend(
{ {
logging: true, logging: true,
proxy: 'http://localhost:8082', proxy: 'http://localhost:8081/proxy',
useCORS: false, useCORS: false,
removeContainer: false, removeContainer: false,
target: targets target: targets

View File

@ -118,6 +118,7 @@ const assertPath = (result, expected, desc) => {
.html2canvas(testContainer.contentWindow.document.documentElement, { .html2canvas(testContainer.contentWindow.document.documentElement, {
removeContainer: true, removeContainer: true,
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
proxy: 'http://localhost:8081/proxy',
...(testContainer.contentWindow.h2cOptions || {}) ...(testContainer.contentWindow.h2cOptions || {})
}) })
.then(canvas => { .then(canvas => {