mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
对canvas filter属性的支持
This commit is contained in:
parent
ce9a89826c
commit
7c57338d2b
@ -1,79 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Nested transform tests</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<style>
|
||||
#first {
|
||||
background: indianred;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
<head>
|
||||
<title>Nested transform tests</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#second {
|
||||
border: 15px solid red;
|
||||
background: darkseagreen;
|
||||
-webkit-transform: rotate(7.5deg);
|
||||
/* Chrome, Safari 3.1+ */
|
||||
-moz-transform: rotate(7.5deg);
|
||||
/* Firefox 3.5-15 */
|
||||
-ms-transform: rotate(7.5deg);
|
||||
/* IE 9 */
|
||||
-o-transform: rotate(7.5deg);
|
||||
/* Opera 10.50-12.00 */
|
||||
transform: rotate(7.5deg);
|
||||
}
|
||||
|
||||
#first {
|
||||
background: indianred;
|
||||
margin-top: 100px;
|
||||
}
|
||||
#third {
|
||||
background: cadetblue;
|
||||
-webkit-transform: rotate(-70.5deg);
|
||||
/* Chrome, Safari 3.1+ */
|
||||
-moz-transform: rotate(-70.5deg);
|
||||
/* Firefox 3.5-15 */
|
||||
-ms-transform: rotate(-70.5deg);
|
||||
/* IE 9 */
|
||||
-o-transform: rotate(-70.5deg);
|
||||
/* Opera 10.50-12.00 */
|
||||
transform: rotate(-70.5deg);
|
||||
/* Firefox 16+, IE 10+, Opera 12.10+ */
|
||||
}
|
||||
|
||||
#second {
|
||||
border: 15px solid red;
|
||||
background: darkseagreen;
|
||||
-webkit-transform: rotate(7.5deg);
|
||||
/* Chrome, Safari 3.1+ */
|
||||
-moz-transform: rotate(7.5deg);
|
||||
/* Firefox 3.5-15 */
|
||||
-ms-transform: rotate(7.5deg);
|
||||
/* IE 9 */
|
||||
-o-transform: rotate(7.5deg);
|
||||
/* Opera 10.50-12.00 */
|
||||
transform: rotate(7.5deg);
|
||||
}
|
||||
#fourth {
|
||||
margin-bottom: 100px;
|
||||
{% comment %} transform: scale(2); {% endcomment %}
|
||||
}
|
||||
|
||||
#third {
|
||||
background: cadetblue;
|
||||
-webkit-transform: rotate(-70.5deg);
|
||||
/* Chrome, Safari 3.1+ */
|
||||
-moz-transform: rotate(-70.5deg);
|
||||
/* Firefox 3.5-15 */
|
||||
-ms-transform: rotate(-70.5deg);
|
||||
/* IE 9 */
|
||||
-o-transform: rotate(-70.5deg);
|
||||
/* Opera 10.50-12.00 */
|
||||
transform: rotate(-70.5deg);
|
||||
/* Firefox 16+, IE 10+, Opera 12.10+ */
|
||||
}
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#fourth {
|
||||
background: #bc8f8f;
|
||||
}
|
||||
body {
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
img.test {
|
||||
width: 100px;
|
||||
/* Chrome, Safari, Opera */
|
||||
filter: contrast(110%) sepia(100%) grayscale(100%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
body {
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
img.test {
|
||||
width: 100px;
|
||||
-webkit-filter: saturate(1600%);
|
||||
/* Chrome, Safari, Opera */
|
||||
filter: saturate(1600%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="fourth"><img class="test" src="../tests/assets/image.jpg" crossOrigin="anonymous" alt=""></div>
|
||||
<div id="fourth2"><img src="../tests/assets/image.jpg" crossOrigin="anonymous" alt=""></div>
|
||||
<script type="text/javascript" src="../dist/html2canvas.js"></script>
|
||||
<script type="text/javascript">
|
||||
html2canvas(document.body, { allowTaint: true, scale: 1 }).then(function (canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<body>
|
||||
<div id="fourth">
|
||||
<img class="test" src="../tests/assets/image.jpg" crossorigin="anonymous" alt="" />
|
||||
</div>
|
||||
<script type="text/javascript" src="../dist/html2canvas.js"></script>
|
||||
<script type="text/javascript">
|
||||
html2canvas(document.getElementById('fourth'), {allowTaint: true, scale: 1}).then(function(canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -105,6 +105,7 @@ export class CSSParsedDeclaration {
|
||||
color: Color;
|
||||
display: ReturnType<typeof display.parse>;
|
||||
filter: ReturnType<typeof filter.parse>;
|
||||
filterOriginal: string | null;
|
||||
float: ReturnType<typeof float.parse>;
|
||||
fontFamily: ReturnType<typeof fontFamily.parse>;
|
||||
fontSize: LengthPercentage;
|
||||
@ -171,6 +172,7 @@ export class CSSParsedDeclaration {
|
||||
this.color = parse(color, declaration.color);
|
||||
this.display = parse(display, declaration.display);
|
||||
this.filter = parse(filter, declaration.filter);
|
||||
this.filterOriginal = declaration.filter;
|
||||
this.float = parse(float, declaration.cssFloat);
|
||||
this.fontFamily = parse(fontFamily, declaration.fontFamily);
|
||||
this.fontSize = parse(fontSize, declaration.fontSize);
|
||||
|
@ -2,15 +2,10 @@ import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IProper
|
||||
import {CSSValue, isIdentWithValue, CSSFunction, isCSSFunction} from '../syntax/parser';
|
||||
import {isLength, Length} from '../types/length';
|
||||
|
||||
export interface Filter {
|
||||
contrast: Length | null;
|
||||
'hue-rotate': Length | null;
|
||||
grayscale: Length | null;
|
||||
brightness: Length | null;
|
||||
blur: Length | null;
|
||||
invert: Length | null;
|
||||
saturate: Length | null;
|
||||
sepia: Length | null;
|
||||
export type Filter = FilterItem[];
|
||||
export interface FilterItem {
|
||||
name: string;
|
||||
value: Length;
|
||||
}
|
||||
|
||||
export const filter: IPropertyListDescriptor<Filter | null> = {
|
||||
@ -22,17 +17,7 @@ export const filter: IPropertyListDescriptor<Filter | null> = {
|
||||
if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filter: Filter = {
|
||||
contrast: null,
|
||||
'hue-rotate': null,
|
||||
grayscale: null,
|
||||
brightness: null,
|
||||
blur: null,
|
||||
invert: null,
|
||||
saturate: null,
|
||||
sepia: null
|
||||
};
|
||||
const filter: Filter = [];
|
||||
|
||||
let hasFilter: boolean = false;
|
||||
|
||||
@ -50,7 +35,7 @@ export const filter: IPropertyListDescriptor<Filter | null> = {
|
||||
const value: CSSValue = token.values[index];
|
||||
if (isLength(value)) {
|
||||
hasFilter = true;
|
||||
filter[token.name] = value;
|
||||
filter.push({name: token.name, value: value});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -1,17 +1,5 @@
|
||||
import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-context';
|
||||
import {
|
||||
asString,
|
||||
Color,
|
||||
isTransparent,
|
||||
RGBColor,
|
||||
contrastRGB,
|
||||
hueRotateRGB,
|
||||
grayscaleRGB,
|
||||
brightnessRGB,
|
||||
invertRGB,
|
||||
saturateRGB,
|
||||
sepiaRGB
|
||||
} from '../../css/types/color';
|
||||
import {asString, Color, isTransparent} from '../../css/types/color';
|
||||
import {Logger} from '../../core/logger';
|
||||
import {ElementContainer} from '../../dom/element-container';
|
||||
import {BORDER_STYLE} from '../../css/property-descriptors/border-style';
|
||||
@ -50,8 +38,7 @@ import {TextareaElementContainer} from '../../dom/elements/textarea-element-cont
|
||||
import {SelectElementContainer} from '../../dom/elements/select-element-container';
|
||||
import {IFrameElementContainer} from '../../dom/replaced-elements/iframe-element-container';
|
||||
import {TextShadow} from '../../css/property-descriptors/text-shadow';
|
||||
import {Filter} from '../../css/property-descriptors/filter';
|
||||
import {stackBlurImage} from '../../css/types/functions/stack-blur';
|
||||
import {processImage, isSupportedFilter} from '../image-filter';
|
||||
|
||||
export type RenderConfigurations = RenderOptions & {
|
||||
backgroundColor: Color | null;
|
||||
@ -262,8 +249,7 @@ export class CanvasRenderer {
|
||||
renderReplacedElement(
|
||||
container: ReplacedElementContainer,
|
||||
curves: BoundCurves,
|
||||
image: HTMLImageElement | HTMLCanvasElement,
|
||||
filter?: Filter | null | undefined
|
||||
image: HTMLImageElement | HTMLCanvasElement
|
||||
) {
|
||||
if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) {
|
||||
const box = contentBox(container);
|
||||
@ -271,6 +257,9 @@ export class CanvasRenderer {
|
||||
this.path(path);
|
||||
this.ctx.save();
|
||||
this.ctx.clip();
|
||||
if (isSupportedFilter(this.ctx) && container.styles.filterOriginal) {
|
||||
this.ctx.filter = container.styles.filterOriginal;
|
||||
}
|
||||
this.ctx.drawImage(
|
||||
image,
|
||||
0,
|
||||
@ -282,49 +271,6 @@ export class CanvasRenderer {
|
||||
box.width,
|
||||
box.height
|
||||
);
|
||||
if (filter) {
|
||||
try {
|
||||
let imageData = this.ctx.getImageData(box.left, box.top, box.width, box.height);
|
||||
for (let _j = 0; _j < imageData.height; _j++) {
|
||||
for (let _i = 0; _i < imageData.width; _i++) {
|
||||
let index = _j * 4 * imageData.width + _i * 4;
|
||||
let rgb: RGBColor = {
|
||||
r: imageData.data[index],
|
||||
g: imageData.data[index + 1],
|
||||
b: imageData.data[index + 2]
|
||||
};
|
||||
if (filter.contrast) {
|
||||
rgb = contrastRGB(rgb, filter.contrast.number);
|
||||
}
|
||||
if (filter['hue-rotate']) {
|
||||
rgb = hueRotateRGB(rgb, filter['hue-rotate'].number);
|
||||
}
|
||||
if (filter.grayscale) {
|
||||
rgb = grayscaleRGB(rgb, filter['grayscale'].number, 'luma:BT709');
|
||||
}
|
||||
if (filter.brightness) {
|
||||
rgb = brightnessRGB(rgb, filter['brightness'].number);
|
||||
}
|
||||
if (filter.invert) {
|
||||
rgb = invertRGB(rgb, filter['invert'].number);
|
||||
}
|
||||
if (filter.saturate) {
|
||||
rgb = saturateRGB(rgb, filter.saturate.number);
|
||||
}
|
||||
if (filter.sepia) {
|
||||
rgb = sepiaRGB(rgb, filter.sepia.number);
|
||||
}
|
||||
imageData.data[index] = rgb.r;
|
||||
imageData.data[index + 1] = rgb.g;
|
||||
imageData.data[index + 2] = rgb.b;
|
||||
}
|
||||
}
|
||||
if (filter.blur) {
|
||||
imageData = stackBlurImage(imageData, box.width, box.height, filter.blur.number * 2.2, 1);
|
||||
}
|
||||
this.ctx.putImageData(imageData, box.left, box.top);
|
||||
} catch (error) {}
|
||||
}
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
||||
@ -341,7 +287,8 @@ export class CanvasRenderer {
|
||||
if (container instanceof ImageElementContainer) {
|
||||
try {
|
||||
const image = await this.options.cache.match(container.src);
|
||||
this.renderReplacedElement(container, curves, image, styles.filter);
|
||||
if (styles.filter && !isSupportedFilter(this.ctx)) await processImage(image, styles.filter);
|
||||
this.renderReplacedElement(container, curves, image);
|
||||
} catch (e) {
|
||||
Logger.getInstance(this.options.id).error(`Error loading image ${container.src}`);
|
||||
}
|
||||
|
99
src/render/image-filter.ts
Normal file
99
src/render/image-filter.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import {
|
||||
RGBColor,
|
||||
contrastRGB,
|
||||
hueRotateRGB,
|
||||
grayscaleRGB,
|
||||
brightnessRGB,
|
||||
invertRGB,
|
||||
saturateRGB,
|
||||
sepiaRGB
|
||||
} from '../css/types/color';
|
||||
import {Filter, FilterItem} from '../css/property-descriptors/filter';
|
||||
import {stackBlurImage} from '../css/types/functions/stack-blur';
|
||||
|
||||
export const processImage = (img: HTMLImageElement, filter: Filter) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!img || !('naturalWidth' in img)) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
const w = img['naturalWidth'];
|
||||
const h = img['naturalHeight'];
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.style.width = w + 'px';
|
||||
canvas.style.height = h + 'px';
|
||||
canvas.width = w * 2;
|
||||
canvas.height = h * 2;
|
||||
const context = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
|
||||
context.clearRect(0, 0, w, h);
|
||||
context.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, w, h);
|
||||
|
||||
let imageData: ImageData = context.getImageData(0, 0, w, h);
|
||||
|
||||
handlePerPixel(imageData, filter);
|
||||
|
||||
let blurFilter = filter.find((item: FilterItem) => item.name === 'blur');
|
||||
|
||||
if (blurFilter) {
|
||||
imageData = stackBlurImage(imageData, w, h, blurFilter.value.number * 2, 1);
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.src = canvas.toDataURL();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
|
||||
if (img.complete === true) {
|
||||
// Inline XML images may fail to parse, throwing an Error later on
|
||||
setTimeout(() => resolve(img), 500);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function handlePerPixel(imageData: ImageData, filter: Filter) {
|
||||
for (let _j = 0; _j < imageData.height; _j++) {
|
||||
for (let _i = 0; _i < imageData.width; _i++) {
|
||||
let index = _j * 4 * imageData.width + _i * 4;
|
||||
let rgb: RGBColor = {
|
||||
r: imageData.data[index],
|
||||
g: imageData.data[index + 1],
|
||||
b: imageData.data[index + 2]
|
||||
};
|
||||
filter.forEach((item: FilterItem) => {
|
||||
switch (item.name) {
|
||||
case 'contrast':
|
||||
rgb = contrastRGB(rgb, item.value.number);
|
||||
break;
|
||||
case 'hue-rotate':
|
||||
rgb = hueRotateRGB(rgb, item.value.number);
|
||||
break;
|
||||
case 'grayscale':
|
||||
rgb = grayscaleRGB(rgb, item.value.number);
|
||||
break;
|
||||
case 'brightness':
|
||||
rgb = brightnessRGB(rgb, item.value.number);
|
||||
break;
|
||||
case 'invert':
|
||||
rgb = invertRGB(rgb, item.value.number);
|
||||
break;
|
||||
case 'saturate':
|
||||
rgb = saturateRGB(rgb, item.value.number);
|
||||
break;
|
||||
case 'sepia':
|
||||
rgb = sepiaRGB(rgb, item.value.number);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
imageData.data[index] = rgb.r;
|
||||
imageData.data[index + 1] = rgb.g;
|
||||
imageData.data[index + 2] = rgb.b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isSupportedFilter = (ctx: CanvasRenderingContext2D) => 'filter' in ctx;
|
Loading…
Reference in New Issue
Block a user