Compare commits

..

9 Commits

18 changed files with 180 additions and 60 deletions

View File

@ -113,9 +113,10 @@ jobs:
name: iOS Simulator Safari 14 name: iOS Simulator Safari 14
targetBrowser: Safari_IOS_14 targetBrowser: Safari_IOS_14
xcode: /Applications/Xcode_12_beta.app xcode: /Applications/Xcode_12_beta.app
- os: windows-latest - os: macos-11
name: Windows Internet Explorer 9 (Emulated) name: iOS Simulator Safari 15
targetBrowser: IE_9 targetBrowser: Safari_IOS_15
xcode: /Applications/Xcode_13.0.app
- os: windows-latest - os: windows-latest
name: Windows Internet Explorer 10 (Emulated) name: Windows Internet Explorer 10 (Emulated)
targetBrowser: IE_10 targetBrowser: IE_10

View File

@ -2,6 +2,16 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.2.1](https://github.com/niklasvh/html2canvas/compare/v1.2.0...v1.2.1) (2021-08-05)
### fix
* none image (#2627) ([6651fc6](https://github.com/niklasvh/html2canvas/commit/6651fc6789d5902d171dc53b4094887870433018)), closes [#2627](https://github.com/niklasvh/html2canvas/issues/2627)
* type import that is only available ts 3.8 or higher (#2629) ([c5c6fa0](https://github.com/niklasvh/html2canvas/commit/c5c6fa00d71f36ef963ba5170ebc7b668d39c407)), closes [#2629](https://github.com/niklasvh/html2canvas/issues/2629)
# [1.2.0](https://github.com/niklasvh/html2canvas/compare/v1.1.5...v1.2.0) (2021-08-04) # [1.2.0](https://github.com/niklasvh/html2canvas/compare/v1.1.5...v1.2.0) (2021-08-04)

View File

@ -3,7 +3,7 @@ html2canvas
[Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](https://github.com/niklasvh/html2canvas/discussions/categories/q-a) [Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](https://github.com/niklasvh/html2canvas/discussions/categories/q-a)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/niklasvh/html2canvas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/niklasvh/html2canvas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
![CI](https://github.com/niklasvh/html2canvas/workflows/CI/badge.svg?branch=master) ![CI](https://github.com/niklasvh/html2canvas/workflows/CI/badge.svg?branch=master)
[![NPM Downloads](https://img.shields.io/npm/dm/html2canvas.svg)](https://www.npmjs.org/package/html2canvas) [![NPM Downloads](https://img.shields.io/npm/dm/html2canvas.svg)](https://www.npmjs.org/package/html2canvas)
[![NPM Version](https://img.shields.io/npm/v/html2canvas.svg)](https://www.npmjs.org/package/html2canvas) [![NPM Version](https://img.shields.io/npm/v/html2canvas.svg)](https://www.npmjs.org/package/html2canvas)
@ -28,7 +28,8 @@ The library should work fine on the following browsers (with `Promise` polyfill)
* Firefox 3.5+ * Firefox 3.5+
* Google Chrome * Google Chrome
* Opera 12+ * Opera 12+
* IE9+ * IE10+
* Edge
* Safari 6+ * Safari 6+
As each CSS property needs to be manually built to be supported, there are a number of properties that are not yet supported. As each CSS property needs to be manually built to be supported, there are a number of properties that are not yet supported.

View File

@ -5,28 +5,28 @@ nextUrl: "/getting-started"
nextTitle: "Getting Started" nextTitle: "Getting Started"
--- ---
Before you get started with the script, there are a few things that are good to know regarding the Before you get started with the script, there are a few things that are good to know regarding the
script and some of its limitations. script and some of its limitations.
## Introduction ## Introduction
The script allows you to take "screenshots" of webpages or parts of it, directly on the users browser. The script allows you to take "screenshots" of webpages or parts of it, directly on the users browser.
The screenshot is based on the DOM and as such may not be 100% accurate to the real representation The screenshot is based on the DOM and as such may not be 100% accurate to the real representation
as it does not make an actual screenshot, but builds the screenshot based on the information as it does not make an actual screenshot, but builds the screenshot based on the information
available on the page. available on the page.
## How it works ## How it works
The script traverses through the DOM of the page it is loaded on. It gathers information on all the elements The script traverses through the DOM of the page it is loaded on. It gathers information on all the elements
there, which it then uses to build a representation of the page. In other words, it does not actually take a there, which it then uses to build a representation of the page. In other words, it does not actually take a
screenshot of the page, but builds a representation of it based on the properties it reads from the DOM. screenshot of the page, but builds a representation of it based on the properties it reads from the DOM.
As a result, it is only able to render correctly properties that it understands, meaning there are many As a result, it is only able to render correctly properties that it understands, meaning there are many
CSS properties which do not work. For a full list of supported CSS properties, check out the CSS properties which do not work. For a full list of supported CSS properties, check out the
[supported features](/features/) page. [supported features](/features/) page.
## Limitations ## Limitations
All the images that the script uses need to reside under the [same origin](http://en.wikipedia.org/wiki/Same_origin_policy) All the images that the script uses need to reside under the [same origin](http://en.wikipedia.org/wiki/Same_origin_policy)
for it to be able to read them without the assistance of a [proxy](/proxy/). Similarly, if you have other `canvas` for it to be able to read them without the assistance of a [proxy](/proxy/). Similarly, if you have other `canvas`
elements on the page, which have been tainted with cross-origin content, they will become dirty and no longer readable by html2canvas. elements on the page, which have been tainted with cross-origin content, they will become dirty and no longer readable by html2canvas.
The script doesn't render plugin content such as Flash or Java applets. The script doesn't render plugin content such as Flash or Java applets.
@ -37,6 +37,6 @@ The library should work fine on the following browsers (with `Promise` polyfill)
- Firefox 3.5+ - Firefox 3.5+
- Google Chrome - Google Chrome
- Opera 12+ - Opera 12+
- IE9+ - IE10+
- Edge - Edge
- Safari 6+ - Safari 6+

View File

@ -42,11 +42,11 @@ module.exports = function(config) {
platform: 'iOS', platform: 'iOS',
sdk: '14.0' sdk: '14.0'
}, },
SauceLabs_IE9: { Safari_IOS_15: {
base: 'SauceLabs', base: 'MobileSafari',
browserName: 'internet explorer', name: 'iPhone 8',
version: '9.0', platform: 'iOS',
platform: 'Windows 7' sdk: '15.0'
}, },
SauceLabs_IE10: { SauceLabs_IE10: {
base: 'SauceLabs', base: 'SauceLabs',
@ -87,11 +87,6 @@ module.exports = function(config) {
version: '9.3', version: '9.3',
device: 'iPhone 6 Plus Simulator' device: 'iPhone 6 Plus Simulator'
}, },
IE_9: {
base: 'IE',
'x-ua-compatible': 'IE=EmulateIE9',
flags: ['-extoff']
},
IE_10: { IE_10: {
base: 'IE', base: 'IE',
'x-ua-compatible': 'IE=EmulateIE10', 'x-ua-compatible': 'IE=EmulateIE10',

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "html2canvas", "name": "html2canvas",
"version": "1.2.0", "version": "1.2.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@ -6,7 +6,7 @@
"module": "dist/html2canvas.esm.js", "module": "dist/html2canvas.esm.js",
"typings": "dist/types/index.d.ts", "typings": "dist/types/index.d.ts",
"browser": "dist/html2canvas.js", "browser": "dist/html2canvas.js",
"version": "1.2.0", "version": "1.2.1",
"author": { "author": {
"name": "Niklas von Hertzen", "name": "Niklas von Hertzen",
"email": "niklasvh@gmail.com", "email": "niklasvh@gmail.com",

View File

@ -1,5 +1,5 @@
import {FEATURES} from './features'; import {FEATURES} from './features';
import type {Context} from './context'; import {Context} from './context';
export class CacheStorage { export class CacheStorage {
private static _link?: HTMLAnchorElement; private static _link?: HTMLAnchorElement;
@ -12,7 +12,6 @@ export class CacheStorage {
} }
link.href = url; link.href = url;
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
return link.protocol + link.hostname + link.port; return link.protocol + link.hostname + link.port;
} }

View File

@ -1,3 +1,5 @@
import {fromCodePoint, toCodePoints} from 'css-line-break';
const testRangeBounds = (document: Document) => { const testRangeBounds = (document: Document) => {
const TEST_HEIGHT = 123; const TEST_HEIGHT = 123;
@ -22,6 +24,45 @@ const testRangeBounds = (document: Document) => {
return false; return false;
}; };
const testIOSLineBreak = (document: Document) => {
const testElement = document.createElement('boundtest');
testElement.style.width = '50px';
testElement.style.display = 'block';
testElement.style.fontSize = '12px';
testElement.style.letterSpacing = '0px';
testElement.style.wordSpacing = '0px';
document.body.appendChild(testElement);
const range = document.createRange();
testElement.innerHTML = typeof ''.repeat === 'function' ? '👨'.repeat(10) : '';
const node = testElement.firstChild as Text;
const textList = toCodePoints(node.data).map((i) => fromCodePoint(i));
let offset = 0;
let prev: DOMRect = {} as DOMRect;
// ios 13 does not handle range getBoundingClientRect line changes correctly #2177
const supports = textList.every((text, i) => {
range.setStart(node, offset);
range.setEnd(node, offset + text.length);
const rect = range.getBoundingClientRect();
offset += text.length;
const boundAhead = rect.x > prev.x || rect.y > prev.y;
prev = rect;
if (i === 0) {
return true;
}
return boundAhead;
});
document.body.removeChild(testElement);
return supports;
};
const testCORS = (): boolean => typeof new Image().crossOrigin !== 'undefined'; const testCORS = (): boolean => typeof new Image().crossOrigin !== 'undefined';
const testResponseType = (): boolean => typeof new XMLHttpRequest().responseType === 'string'; const testResponseType = (): boolean => typeof new XMLHttpRequest().responseType === 'string';
@ -132,6 +173,12 @@ export const FEATURES = {
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value}); Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
return value; return value;
}, },
get SUPPORT_WORD_BREAKING(): boolean {
'use strict';
const value = FEATURES.SUPPORT_RANGE_BOUNDS && testIOSLineBreak(document);
Object.defineProperty(FEATURES, 'SUPPORT_WORD_BREAKING', {value});
return value;
},
get SUPPORT_SVG_DRAWING(): boolean { get SUPPORT_SVG_DRAWING(): boolean {
'use strict'; 'use strict';
const value = testSVG(document); const value = testSVG(document);

View File

@ -15,6 +15,20 @@ export class Bounds {
clientRect.height clientRect.height
); );
} }
static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds {
const domRect = domRectList[0];
return domRect
? new Bounds(
domRect.x + context.windowBounds.left,
domRect.y + context.windowBounds.top,
domRect.width,
domRect.height
)
: Bounds.EMPTY;
}
static EMPTY = new Bounds(0, 0, 0, 0);
} }
export const parseBounds = (context: Context, node: Element): Bounds => { export const parseBounds = (context: Context, node: Element): Bounds => {

View File

@ -27,7 +27,16 @@ export const parseTextBounds = (
textList.forEach((text) => { textList.forEach((text) => {
if (styles.textDecorationLine.length || text.trim().length > 0) { if (styles.textDecorationLine.length || text.trim().length > 0) {
if (FEATURES.SUPPORT_RANGE_BOUNDS) { if (FEATURES.SUPPORT_RANGE_BOUNDS) {
textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length))); if (!FEATURES.SUPPORT_WORD_BREAKING) {
textBounds.push(
new TextBounds(
text,
Bounds.fromDOMRectList(context, createRange(node, offset, text.length).getClientRects())
)
);
} else {
textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length)));
}
} else { } else {
const replacementNode = node.splitText(text.length); const replacementNode = node.splitText(text.length);
textBounds.push(new TextBounds(text, getWrapperBounds(context, node))); textBounds.push(new TextBounds(text, getWrapperBounds(context, node)));
@ -58,10 +67,10 @@ const getWrapperBounds = (context: Context, node: Text): Bounds => {
} }
} }
return new Bounds(0, 0, 0, 0); return Bounds.EMPTY;
}; };
const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => { const createRange = (node: Text, offset: number, length: number): Range => {
const ownerDocument = node.ownerDocument; const ownerDocument = node.ownerDocument;
if (!ownerDocument) { if (!ownerDocument) {
throw new Error('Node has no owner document'); throw new Error('Node has no owner document');
@ -69,7 +78,11 @@ const getRangeBounds = (context: Context, node: Text, offset: number, length: nu
const range = ownerDocument.createRange(); const range = ownerDocument.createRange();
range.setStart(node, offset); range.setStart(node, offset);
range.setEnd(node, offset + length); range.setEnd(node, offset + length);
return Bounds.fromClientRect(context, range.getBoundingClientRect()); return range;
};
const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => {
return Bounds.fromClientRect(context, createRange(node, offset, length).getBoundingClientRect());
}; };
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => { const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {

View File

@ -94,12 +94,15 @@ export const image: ITypeDescriptor<ICSSImage> = {
return imageFunction(context, value.values); return imageFunction(context, value.values);
} }
throw new Error(`Unsupported image type`); throw new Error(`Unsupported image type ${value.type}`);
} }
}; };
export function isSupportedImage(value: CSSValue): boolean { export function isSupportedImage(value: CSSValue): boolean {
return value.type !== TokenType.FUNCTION || !!SUPPORTED_IMAGE_FUNCTIONS[value.name]; return (
!(value.type === TokenType.IDENT_TOKEN && value.value === 'none') &&
(value.type !== TokenType.FUNCTION || !!SUPPORTED_IMAGE_FUNCTIONS[value.name])
);
} }
const SUPPORTED_IMAGE_FUNCTIONS: Record<string, (context: Context, args: CSSValue[]) => ICSSImage> = { const SUPPORTED_IMAGE_FUNCTIONS: Record<string, (context: Context, args: CSSValue[]) => ICSSImage> = {

View File

@ -628,7 +628,7 @@ export class CanvasRenderer extends Renderer {
const y = getAbsoluteValue(position[position.length - 1], height); const y = getAbsoluteValue(position[position.length - 1], height);
const [rx, ry] = calculateRadius(backgroundImage, x, y, width, height); const [rx, ry] = calculateRadius(backgroundImage, x, y, width, height);
if (rx > 0 && rx > 0) { if (rx > 0 && ry > 0) {
const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx); const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) => processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) =>

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,7 @@
<title>Background attribute tests</title> <title>Background attribute tests</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script> <script type="text/javascript" src="../../test.js"></script>
<link href="base64.css" rel="stylesheet">
<style> <style>
html { html {
background-color: red; background-color: red;
@ -39,6 +40,9 @@
background:url("data:image/jpeg;base64,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"); background:url("data:image/jpeg;base64,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");
} }
.encoded2 {
background-size: cover;
}
</style> </style>
</head> </head>
@ -46,6 +50,7 @@
<div class="medium"> <div class="medium">
<div class="encoded"></div> <div class="encoded"></div>
<div class="base64 encoded2"></div>
</div> </div>
</body> </body>

View File

@ -46,6 +46,7 @@
<div style="background: linear-gradient(60deg, hsla(120,80%,50%,0.8) 0%, transparent 50%, rgba(255,100,100,0.5) 100%);"></div> <div style="background: linear-gradient(60deg, hsla(120,80%,50%,0.8) 0%, transparent 50%, rgba(255,100,100,0.5) 100%);"></div>
<div style="background: linear-gradient(to right, red 20%, orange 20% 40%, yellow 40% 60%, green 60% 80%, blue 80%);"></div> <div style="background: linear-gradient(to right, red 20%, orange 20% 40%, yellow 40% 60%, green 60% 80%, blue 80%);"></div>
<div style="background: linear-gradient(-45deg, #FF0000 40%, #00FF00 50%);"></div> <div style="background: linear-gradient(-45deg, #FF0000 40%, #00FF00 50%);"></div>
<div style="background:linear-gradient(217deg, rgba(255, 0, 0, 0.8) 10%, rgba(255, 0, 0, 0) 70.71%), linear-gradient(127deg, rgba(0, 255, 0, 0.8) 20%, rgba(0, 255, 0, 0) 70.71%), linear-gradient(336deg, rgba(0, 0, 255, 0.8) 40%, rgba(0, 0, 255, 0) 70.71%), rgb(255, 255, 255);"></div>
<div class="linearGradient"></div> <div class="linearGradient"></div>
</body> </body>
</html> </html>

View File

@ -8,15 +8,14 @@ import {ScreenshotRequest} from './types';
// @ts-ignore // @ts-ignore
window.Promise = Promise; window.Promise = Promise;
const testRunnerUrl = location.href; const testRunnerUrl = location.href;
const hasHistoryApi = typeof window.history !== 'undefined' && typeof window.history.replaceState !== 'undefined';
const uploadResults = (canvas: HTMLCanvasElement, url: string) => { const uploadResults = (canvas: HTMLCanvasElement, url: string) => {
return new Promise((resolve: () => void, reject: (error: string) => void) => { // @ts-ignore
// @ts-ignore return new Promise((resolve: () => void, reject: (error: any) => void) => {
const xhr = 'withCredentials' in new XMLHttpRequest() ? new XMLHttpRequest() : new XDomainRequest(); const xhr = new XMLHttpRequest();
xhr.onload = () => { xhr.onload = () => {
if (typeof xhr.status !== 'number' || xhr.status === 200) { if (xhr.status === 200) {
resolve(); resolve();
} else { } else {
reject(`Failed to send screenshot with status ${xhr.status}`); reject(`Failed to send screenshot with status ${xhr.status}`);
@ -62,21 +61,17 @@ testList
testContainer.onload = () => done(); testContainer.onload = () => done();
testContainer.src = url + '?selenium&run=false&reftest&' + Math.random(); testContainer.src = url + '?selenium&run=false&reftest&' + Math.random();
if (hasHistoryApi) { // Chrome does not resolve relative background urls correctly inside of a nested iframe
// Chrome does not resolve relative background urls correctly inside of a nested iframe try {
try { history.replaceState(null, '', url);
history.replaceState(null, '', url); } catch (e) {}
} catch (e) {}
}
document.body.appendChild(testContainer); document.body.appendChild(testContainer);
}); });
after(() => { after(() => {
if (hasHistoryApi) { try {
try { history.replaceState(null, '', testRunnerUrl);
history.replaceState(null, '', testRunnerUrl); } catch (e) {}
} catch (e) {}
}
document.body.removeChild(testContainer); document.body.removeChild(testContainer);
}); });

View File

@ -22,18 +22,30 @@ type TestList = {[key: string]: Test[]};
function onTestChange(browserTests: Test[]) { function onTestChange(browserTests: Test[]) {
if (browserSelector) { if (browserSelector) {
const currentSelection = browserSelector.value;
while (browserSelector.firstChild) { while (browserSelector.firstChild) {
browserSelector.firstChild.remove(); browserSelector.firstChild.remove();
} }
let newSelection;
browserTests.forEach((browser, i) => { browserTests.forEach((browser, i) => {
if (i === 0) { if (i === 0) {
onBrowserChange(browser); newSelection = browser;
} }
const option = document.createElement('option'); const option = document.createElement('option');
option.value = browser.id; option.value = browser.id;
if (browser.id === currentSelection) {
option.selected = true;
newSelection = browser;
}
option.textContent = browser.id.replace(/_/g, ' '); option.textContent = browser.id.replace(/_/g, ' ');
browserSelector.appendChild(option); browserSelector.appendChild(option);
}); });
if (newSelection) {
onBrowserChange(newSelection);
}
} }
} }
@ -48,13 +60,20 @@ function onBrowserChange(browserTest: Test) {
previewImage.style.transformOrigin = ''; previewImage.style.transformOrigin = '';
} }
} }
if (history) {
history.replaceState(null, document.title, `?browser=${browserSelector?.value}&test=${testSelector?.value}`);
}
} }
function selectTest(testName: string) { function selectTest(testName: string) {
if (testLink) { const foundTest = testList[testName];
testLink.textContent = testLink.href = testName; if (foundTest) {
if (testLink) {
testLink.textContent = testLink.href = testName;
}
onTestChange(foundTest);
} }
onTestChange(testList[testName]);
} }
const UP_ARROW = 38; const UP_ARROW = 38;
@ -116,15 +135,29 @@ if (testSelector && browserSelector) {
false false
); );
const tests: string[] = Object.keys(testList); let testFromUrl: string | null = null;
tests.forEach((testName, i) => {
if (i === 0) { if (URLSearchParams) {
selectTest(testName); const url = new URLSearchParams(location.search);
testFromUrl = url.get('test');
if (browserSelector) {
const option = document.createElement('option');
browserSelector.appendChild(option);
browserSelector.value = option.value = url.get('browser') ?? '';
} }
}
const tests: string[] = Object.keys(testList);
tests.forEach((testName) => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = testName; option.value = testName;
option.textContent = testName; option.textContent = testName;
if (option.value === testFromUrl) {
option.selected = true;
}
testSelector.appendChild(option); testSelector.appendChild(option);
}); });
selectTest(testSelector.value ?? testSelector.firstChild?.textContent ?? '');
} }