对canvas filter属性的支持

This commit is contained in:
ysk2014 2020-09-03 14:03:38 +08:00
parent ce9a89826c
commit 7c57338d2b
5 changed files with 178 additions and 152 deletions

View File

@ -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>

View File

@ -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);

View File

@ -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;

View File

@ -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}`);
}

View 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;