mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
对canvas filter属性的支持
This commit is contained in:
@@ -1,79 +1,72 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<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>
|
#second {
|
||||||
<title>Nested transform tests</title>
|
border: 15px solid red;
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
background: darkseagreen;
|
||||||
<style>
|
-webkit-transform: rotate(7.5deg);
|
||||||
* {
|
/* Chrome, Safari 3.1+ */
|
||||||
margin: 0;
|
-moz-transform: rotate(7.5deg);
|
||||||
padding: 0;
|
/* 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 {
|
#third {
|
||||||
background: indianred;
|
background: cadetblue;
|
||||||
margin-top: 100px;
|
-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 {
|
#fourth {
|
||||||
border: 15px solid red;
|
margin-bottom: 100px;
|
||||||
background: darkseagreen;
|
{% comment %} transform: scale(2); {% endcomment %}
|
||||||
-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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#third {
|
div {
|
||||||
background: cadetblue;
|
display: inline-block;
|
||||||
-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+ */
|
|
||||||
}
|
|
||||||
|
|
||||||
#fourth {
|
body {
|
||||||
background: #bc8f8f;
|
font-family: Arial;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
img.test {
|
||||||
display: inline-block;
|
width: 100px;
|
||||||
}
|
/* Chrome, Safari, Opera */
|
||||||
|
filter: contrast(110%) sepia(100%) grayscale(100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
body {
|
<body>
|
||||||
font-family: Arial;
|
<div id="fourth">
|
||||||
}
|
<img class="test" src="../tests/assets/image.jpg" crossorigin="anonymous" alt="" />
|
||||||
|
</div>
|
||||||
img.test {
|
<script type="text/javascript" src="../dist/html2canvas.js"></script>
|
||||||
width: 100px;
|
<script type="text/javascript">
|
||||||
-webkit-filter: saturate(1600%);
|
html2canvas(document.getElementById('fourth'), {allowTaint: true, scale: 1}).then(function(canvas) {
|
||||||
/* Chrome, Safari, Opera */
|
document.body.appendChild(canvas);
|
||||||
filter: saturate(1600%);
|
});
|
||||||
}
|
</script>
|
||||||
</style>
|
</body>
|
||||||
</head>
|
</html>
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export class CSSParsedDeclaration {
|
|||||||
color: Color;
|
color: Color;
|
||||||
display: ReturnType<typeof display.parse>;
|
display: ReturnType<typeof display.parse>;
|
||||||
filter: ReturnType<typeof filter.parse>;
|
filter: ReturnType<typeof filter.parse>;
|
||||||
|
filterOriginal: string | null;
|
||||||
float: ReturnType<typeof float.parse>;
|
float: ReturnType<typeof float.parse>;
|
||||||
fontFamily: ReturnType<typeof fontFamily.parse>;
|
fontFamily: ReturnType<typeof fontFamily.parse>;
|
||||||
fontSize: LengthPercentage;
|
fontSize: LengthPercentage;
|
||||||
@@ -171,6 +172,7 @@ export class CSSParsedDeclaration {
|
|||||||
this.color = parse(color, declaration.color);
|
this.color = parse(color, declaration.color);
|
||||||
this.display = parse(display, declaration.display);
|
this.display = parse(display, declaration.display);
|
||||||
this.filter = parse(filter, declaration.filter);
|
this.filter = parse(filter, declaration.filter);
|
||||||
|
this.filterOriginal = declaration.filter;
|
||||||
this.float = parse(float, declaration.cssFloat);
|
this.float = parse(float, declaration.cssFloat);
|
||||||
this.fontFamily = parse(fontFamily, declaration.fontFamily);
|
this.fontFamily = parse(fontFamily, declaration.fontFamily);
|
||||||
this.fontSize = parse(fontSize, declaration.fontSize);
|
this.fontSize = parse(fontSize, declaration.fontSize);
|
||||||
|
|||||||
@@ -2,15 +2,10 @@ import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IProper
|
|||||||
import {CSSValue, isIdentWithValue, CSSFunction, isCSSFunction} from '../syntax/parser';
|
import {CSSValue, isIdentWithValue, CSSFunction, isCSSFunction} from '../syntax/parser';
|
||||||
import {isLength, Length} from '../types/length';
|
import {isLength, Length} from '../types/length';
|
||||||
|
|
||||||
export interface Filter {
|
export type Filter = FilterItem[];
|
||||||
contrast: Length | null;
|
export interface FilterItem {
|
||||||
'hue-rotate': Length | null;
|
name: string;
|
||||||
grayscale: Length | null;
|
value: Length;
|
||||||
brightness: Length | null;
|
|
||||||
blur: Length | null;
|
|
||||||
invert: Length | null;
|
|
||||||
saturate: Length | null;
|
|
||||||
sepia: Length | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filter: IPropertyListDescriptor<Filter | null> = {
|
export const filter: IPropertyListDescriptor<Filter | null> = {
|
||||||
@@ -22,17 +17,7 @@ export const filter: IPropertyListDescriptor<Filter | null> = {
|
|||||||
if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
|
if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const filter: Filter = [];
|
||||||
const filter: Filter = {
|
|
||||||
contrast: null,
|
|
||||||
'hue-rotate': null,
|
|
||||||
grayscale: null,
|
|
||||||
brightness: null,
|
|
||||||
blur: null,
|
|
||||||
invert: null,
|
|
||||||
saturate: null,
|
|
||||||
sepia: null
|
|
||||||
};
|
|
||||||
|
|
||||||
let hasFilter: boolean = false;
|
let hasFilter: boolean = false;
|
||||||
|
|
||||||
@@ -50,7 +35,7 @@ export const filter: IPropertyListDescriptor<Filter | null> = {
|
|||||||
const value: CSSValue = token.values[index];
|
const value: CSSValue = token.values[index];
|
||||||
if (isLength(value)) {
|
if (isLength(value)) {
|
||||||
hasFilter = true;
|
hasFilter = true;
|
||||||
filter[token.name] = value;
|
filter.push({name: token.name, value: value});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-context';
|
import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-context';
|
||||||
import {
|
import {asString, Color, isTransparent} from '../../css/types/color';
|
||||||
asString,
|
|
||||||
Color,
|
|
||||||
isTransparent,
|
|
||||||
RGBColor,
|
|
||||||
contrastRGB,
|
|
||||||
hueRotateRGB,
|
|
||||||
grayscaleRGB,
|
|
||||||
brightnessRGB,
|
|
||||||
invertRGB,
|
|
||||||
saturateRGB,
|
|
||||||
sepiaRGB
|
|
||||||
} from '../../css/types/color';
|
|
||||||
import {Logger} from '../../core/logger';
|
import {Logger} from '../../core/logger';
|
||||||
import {ElementContainer} from '../../dom/element-container';
|
import {ElementContainer} from '../../dom/element-container';
|
||||||
import {BORDER_STYLE} from '../../css/property-descriptors/border-style';
|
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 {SelectElementContainer} from '../../dom/elements/select-element-container';
|
||||||
import {IFrameElementContainer} from '../../dom/replaced-elements/iframe-element-container';
|
import {IFrameElementContainer} from '../../dom/replaced-elements/iframe-element-container';
|
||||||
import {TextShadow} from '../../css/property-descriptors/text-shadow';
|
import {TextShadow} from '../../css/property-descriptors/text-shadow';
|
||||||
import {Filter} from '../../css/property-descriptors/filter';
|
import {processImage, isSupportedFilter} from '../image-filter';
|
||||||
import {stackBlurImage} from '../../css/types/functions/stack-blur';
|
|
||||||
|
|
||||||
export type RenderConfigurations = RenderOptions & {
|
export type RenderConfigurations = RenderOptions & {
|
||||||
backgroundColor: Color | null;
|
backgroundColor: Color | null;
|
||||||
@@ -262,8 +249,7 @@ export class CanvasRenderer {
|
|||||||
renderReplacedElement(
|
renderReplacedElement(
|
||||||
container: ReplacedElementContainer,
|
container: ReplacedElementContainer,
|
||||||
curves: BoundCurves,
|
curves: BoundCurves,
|
||||||
image: HTMLImageElement | HTMLCanvasElement,
|
image: HTMLImageElement | HTMLCanvasElement
|
||||||
filter?: Filter | null | undefined
|
|
||||||
) {
|
) {
|
||||||
if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) {
|
if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) {
|
||||||
const box = contentBox(container);
|
const box = contentBox(container);
|
||||||
@@ -271,6 +257,9 @@ export class CanvasRenderer {
|
|||||||
this.path(path);
|
this.path(path);
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
this.ctx.clip();
|
this.ctx.clip();
|
||||||
|
if (isSupportedFilter(this.ctx) && container.styles.filterOriginal) {
|
||||||
|
this.ctx.filter = container.styles.filterOriginal;
|
||||||
|
}
|
||||||
this.ctx.drawImage(
|
this.ctx.drawImage(
|
||||||
image,
|
image,
|
||||||
0,
|
0,
|
||||||
@@ -282,49 +271,6 @@ export class CanvasRenderer {
|
|||||||
box.width,
|
box.width,
|
||||||
box.height
|
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();
|
this.ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,7 +287,8 @@ export class CanvasRenderer {
|
|||||||
if (container instanceof ImageElementContainer) {
|
if (container instanceof ImageElementContainer) {
|
||||||
try {
|
try {
|
||||||
const image = await this.options.cache.match(container.src);
|
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) {
|
} catch (e) {
|
||||||
Logger.getInstance(this.options.id).error(`Error loading image ${container.src}`);
|
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;
|
||||||
Reference in New Issue
Block a user