添加对css滤镜的支持

This commit is contained in:
ysk2014 2020-08-31 16:37:19 +08:00
parent 51bb0da805
commit 94e966d147
9 changed files with 1426 additions and 7 deletions

79
examples/demo4.html Normal file
View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<title>Nested transform tests</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style>
* {
margin: 0;
padding: 0;
}
#first {
background: indianred;
margin-top: 100px;
}
#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);
}
#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+ */
}
#fourth {
background: #bc8f8f;
}
div {
display: inline-block;
}
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>

View File

@ -75,6 +75,7 @@ import {counterIncrement} from './property-descriptors/counter-increment';
import {counterReset} from './property-descriptors/counter-reset'; import {counterReset} from './property-descriptors/counter-reset';
import {quotes} from './property-descriptors/quotes'; import {quotes} from './property-descriptors/quotes';
import {boxShadow} from './property-descriptors/box-shadow'; import {boxShadow} from './property-descriptors/box-shadow';
import {filter} from './property-descriptors/filter';
export class CSSParsedDeclaration { export class CSSParsedDeclaration {
backgroundClip: ReturnType<typeof backgroundClip.parse>; backgroundClip: ReturnType<typeof backgroundClip.parse>;
@ -103,6 +104,7 @@ export class CSSParsedDeclaration {
boxShadow: ReturnType<typeof boxShadow.parse>; boxShadow: ReturnType<typeof boxShadow.parse>;
color: Color; color: Color;
display: ReturnType<typeof display.parse>; display: ReturnType<typeof display.parse>;
filter: ReturnType<typeof filter.parse>;
float: ReturnType<typeof float.parse>; float: ReturnType<typeof float.parse>;
fontFamily: ReturnType<typeof fontFamily.parse>; fontFamily: ReturnType<typeof fontFamily.parse>;
fontSize: LengthPercentage; fontSize: LengthPercentage;
@ -168,6 +170,7 @@ export class CSSParsedDeclaration {
this.boxShadow = parse(boxShadow, declaration.boxShadow); this.boxShadow = parse(boxShadow, declaration.boxShadow);
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.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);

View File

@ -0,0 +1,68 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
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 const filter: IPropertyListDescriptor<Filter | null> = {
name: 'filter',
initialValue: 'none',
prefix: true,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
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
};
let hasFilter: boolean = false;
tokens.filter(isCSSFunction).forEach((token: CSSFunction) => {
switch (token.name) {
case 'contrast':
case 'hue-rotate':
case 'grayscale':
case 'brightness':
case 'blur':
case 'invert':
case 'saturate':
case 'sepia':
for (let index = 0; index < token.values.length; index++) {
const value: CSSValue = token.values[index];
if (isLength(value)) {
hasFilter = true;
filter[token.name] = value;
}
}
break;
default:
break;
}
});
if (hasFilter) {
return filter;
} else {
return null;
}
}
};

View File

@ -1,9 +1,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor'; import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const textStrokeColor: IPropertyTypeValueDescriptor = { export const textStrokeColor: IPropertyTypeValueDescriptor = {
name: `-webkit-text-stroke-color`, name: `text-stroke-color`,
initialValue: 'transparent', initialValue: 'transparent',
prefix: false, prefix: true,
type: PropertyDescriptorParsingType.TYPE_VALUE, type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color' format: 'color'
}; };

View File

@ -1,8 +1,8 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor'; import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const textStrokeWidth: IPropertyTypeValueDescriptor = { export const textStrokeWidth: IPropertyTypeValueDescriptor = {
name: '-webkit-text-stroke-width', name: 'text-stroke-width',
initialValue: '0', initialValue: '0',
type: PropertyDescriptorParsingType.TYPE_VALUE, type: PropertyDescriptorParsingType.TYPE_VALUE,
prefix: false, prefix: true,
format: 'length-percentage' format: 'length-percentage'
}; };

View File

@ -141,6 +141,7 @@ export class Parser {
} }
} }
export const isCSSFunction = (token: CSSValue): token is CSSFunction => token.type === TokenType.FUNCTION;
export const isDimensionToken = (token: CSSValue): token is DimensionToken => token.type === TokenType.DIMENSION_TOKEN; export const isDimensionToken = (token: CSSValue): token is DimensionToken => token.type === TokenType.DIMENSION_TOKEN;
export const isNumberToken = (token: CSSValue): token is NumberValueToken => token.type === TokenType.NUMBER_TOKEN; export const isNumberToken = (token: CSSValue): token is NumberValueToken => token.type === TokenType.NUMBER_TOKEN;
export const isIdentToken = (token: CSSValue): token is StringValueToken => token.type === TokenType.IDENT_TOKEN; export const isIdentToken = (token: CSSValue): token is StringValueToken => token.type === TokenType.IDENT_TOKEN;

View File

@ -302,3 +302,156 @@ export const COLORS: {[key: string]: Color} = {
YELLOW: 0xffff00ff, YELLOW: 0xffff00ff,
YELLOWGREEN: 0x9acd32ff YELLOWGREEN: 0x9acd32ff
}; };
export interface RGBColor {
r: number;
g: number;
b: number;
}
export const contrastRGB = (rgb: RGBColor, value: number): RGBColor => {
if (value < 0) value = 0;
else if (value > 1) value = 1;
return {
r: Math.max(0, Math.min(255, value * (rgb.r - 128) + 128)),
g: Math.max(0, Math.min(255, value * (rgb.g - 128) + 128)),
b: Math.max(0, Math.min(255, value * (rgb.b - 128) + 128))
};
};
export const grayscaleRGB = (rgb: RGBColor, value: number, mode?: string | null) => {
var gray = 0;
//different grayscale algorithms
switch (mode) {
case 'average':
gray = (rgb.r + rgb.g + rgb.b) / 3;
break;
case 'luma:BT601':
gray = rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114;
break;
case 'desaturation':
gray = (Math.max(rgb.r, rgb.g, rgb.b) + Math.max(rgb.r, rgb.g, rgb.b)) / 2;
break;
case 'decompsition:max':
gray = Math.max(rgb.r, rgb.g, rgb.b);
break;
case 'decompsition:min':
gray = Math.min(rgb.r, rgb.g, rgb.b);
break;
case 'luma:BT709':
default:
gray = rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722;
break;
}
rgb.r = value * (gray - rgb.r) + rgb.r;
rgb.g = value * (gray - rgb.g) + rgb.g;
rgb.b = value * (gray - rgb.b) + rgb.b;
return rgb;
};
export const brightnessRGB = (rgb: RGBColor, value: number): RGBColor => {
if (value < 0) value = 0;
return {
r: Math.max(0, Math.min(255, rgb.r * value)),
g: Math.max(0, Math.min(255, rgb.g * value)),
b: Math.max(0, Math.min(255, rgb.b * value))
};
};
export const invertRGB = (rgb: RGBColor, value: number): RGBColor => {
return {
r: value * (255 - 2 * rgb.r) + rgb.r,
g: value * (255 - 2 * rgb.g) + rgb.g,
b: value * (255 - 2 * rgb.b) + rgb.b
};
};
export const sepiaRGB = (rgb: RGBColor, value: number): RGBColor => {
if (value < 0) value = 0;
else if (value > 1) value = 1;
return {
r: value * Math.min(255, rgb.r * 0.393 + rgb.g * 0.769 + rgb.b * 0.189 - rgb.r) + rgb.r,
g: value * Math.min(255, rgb.r * 0.349 + rgb.g * 0.686 + rgb.b * 0.168 - rgb.g) + rgb.g,
b: value * Math.min(255, rgb.r * 0.272 + rgb.g * 0.534 + rgb.b * 0.131 - rgb.b) + rgb.b
};
};
export const hueRotateRGB = (rgb: RGBColor, value: number): RGBColor => {
while (value < 0) value += 360;
while (value > 360) value -= 360;
rgb2hsl(rgb);
rgb.r += value;
if (rgb.r < 0) rgb.r += 360;
if (rgb.r > 359) rgb.r -= 360;
hsl2rgb(rgb);
return rgb;
};
export const saturateRGB = (rgb: RGBColor, value: number): RGBColor => {
if (value < 0) value = 0;
rgb2hsl(rgb);
rgb.g *= value;
if (rgb.g > 100) rgb.g = 100;
hsl2rgb(rgb);
return rgb;
};
function rgb2hsl(rgb: RGBColor) {
rgb.r = Math.max(0, Math.min(255, rgb.r)) / 255;
rgb.g = Math.max(0, Math.min(255, rgb.g)) / 255;
rgb.b = Math.max(0, Math.min(255, rgb.b)) / 255;
let h, l;
let M = Math.max(rgb.r, rgb.g, rgb.b);
let m = Math.min(rgb.r, rgb.g, rgb.b);
let d = M - m;
if (d == 0) h = 0;
else if (M == rgb.r) h = ((rgb.g - rgb.b) / d) % 6;
else if (M == rgb.g) h = (rgb.b - rgb.r) / d + 2;
else h = (rgb.r - rgb.g) / d + 4;
h = Math.round(h * 60);
if (h < 0) h += 360;
rgb.r = h;
l = (M + m) / 2;
if (d == 0) rgb.g = 0;
else rgb.g = (d / (1 - Math.abs(2 * l - 1))) * 100;
rgb.b = l * 100;
}
function hsl2rgb(rgb: RGBColor) {
rgb.r = Math.max(0, Math.min(359, rgb.r));
rgb.g = Math.max(0, Math.min(100, rgb.g)) / 100;
rgb.b = Math.max(0, Math.min(100, rgb.b)) / 100;
let C = (1 - Math.abs(2 * rgb.b - 1)) * rgb.g;
let h = rgb.r / 60;
let X = C * (1 - Math.abs((h % 2) - 1));
let l = rgb.b;
rgb.r = 0;
rgb.g = 0;
rgb.b = 0;
if (h >= 0 && h < 1) {
rgb.r = C;
rgb.g = X;
} else if (h >= 1 && h < 2) {
rgb.r = X;
rgb.g = C;
} else if (h >= 2 && h < 3) {
rgb.g = C;
rgb.b = X;
} else if (h >= 3 && h < 4) {
rgb.g = X;
rgb.b = C;
} else if (h >= 4 && h < 5) {
rgb.r = X;
rgb.b = C;
} else {
rgb.r = C;
rgb.b = X;
}
let m = l - C / 2;
rgb.r += m;
rgb.g += m;
rgb.b += m;
rgb.r = Math.round(rgb.r * 255.0);
rgb.g = Math.round(rgb.g * 255.0);
rgb.b = Math.round(rgb.b * 255.0);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,17 @@
import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-context'; import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-context';
import {asString, Color, isTransparent} from '../../css/types/color'; import {
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';
@ -38,6 +50,8 @@ 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 {stackBlurImage} from '../../css/types/functions/stackBlur';
export type RenderConfigurations = RenderOptions & { export type RenderConfigurations = RenderOptions & {
backgroundColor: Color | null; backgroundColor: Color | null;
@ -248,7 +262,8 @@ 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);
@ -267,6 +282,49 @@ 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();
} }
} }
@ -283,7 +341,7 @@ 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); this.renderReplacedElement(container, curves, image, styles.filter);
} 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}`);
} }