mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
添加对css滤镜的支持
This commit is contained in:
parent
51bb0da805
commit
94e966d147
79
examples/demo4.html
Normal file
79
examples/demo4.html
Normal 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>
|
@ -75,6 +75,7 @@ import {counterIncrement} from './property-descriptors/counter-increment';
|
||||
import {counterReset} from './property-descriptors/counter-reset';
|
||||
import {quotes} from './property-descriptors/quotes';
|
||||
import {boxShadow} from './property-descriptors/box-shadow';
|
||||
import {filter} from './property-descriptors/filter';
|
||||
|
||||
export class CSSParsedDeclaration {
|
||||
backgroundClip: ReturnType<typeof backgroundClip.parse>;
|
||||
@ -103,6 +104,7 @@ export class CSSParsedDeclaration {
|
||||
boxShadow: ReturnType<typeof boxShadow.parse>;
|
||||
color: Color;
|
||||
display: ReturnType<typeof display.parse>;
|
||||
filter: ReturnType<typeof filter.parse>;
|
||||
float: ReturnType<typeof float.parse>;
|
||||
fontFamily: ReturnType<typeof fontFamily.parse>;
|
||||
fontSize: LengthPercentage;
|
||||
@ -168,6 +170,7 @@ export class CSSParsedDeclaration {
|
||||
this.boxShadow = parse(boxShadow, declaration.boxShadow);
|
||||
this.color = parse(color, declaration.color);
|
||||
this.display = parse(display, declaration.display);
|
||||
this.filter = parse(filter, declaration.filter);
|
||||
this.float = parse(float, declaration.cssFloat);
|
||||
this.fontFamily = parse(fontFamily, declaration.fontFamily);
|
||||
this.fontSize = parse(fontSize, declaration.fontSize);
|
||||
|
68
src/css/property-descriptors/filter.ts
Normal file
68
src/css/property-descriptors/filter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
@ -1,9 +1,9 @@
|
||||
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
|
||||
export const textStrokeColor: IPropertyTypeValueDescriptor = {
|
||||
name: `-webkit-text-stroke-color`,
|
||||
name: `text-stroke-color`,
|
||||
initialValue: 'transparent',
|
||||
prefix: false,
|
||||
prefix: true,
|
||||
type: PropertyDescriptorParsingType.TYPE_VALUE,
|
||||
format: 'color'
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
export const textStrokeWidth: IPropertyTypeValueDescriptor = {
|
||||
name: '-webkit-text-stroke-width',
|
||||
name: 'text-stroke-width',
|
||||
initialValue: '0',
|
||||
type: PropertyDescriptorParsingType.TYPE_VALUE,
|
||||
prefix: false,
|
||||
prefix: true,
|
||||
format: 'length-percentage'
|
||||
};
|
||||
|
@ -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 isNumberToken = (token: CSSValue): token is NumberValueToken => token.type === TokenType.NUMBER_TOKEN;
|
||||
export const isIdentToken = (token: CSSValue): token is StringValueToken => token.type === TokenType.IDENT_TOKEN;
|
||||
|
@ -302,3 +302,156 @@ export const COLORS: {[key: string]: Color} = {
|
||||
YELLOW: 0xffff00ff,
|
||||
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);
|
||||
}
|
||||
|
1057
src/css/types/functions/stackBlur.ts
Normal file
1057
src/css/types/functions/stackBlur.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,17 @@
|
||||
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 {ElementContainer} from '../../dom/element-container';
|
||||
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 {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/stackBlur';
|
||||
|
||||
export type RenderConfigurations = RenderOptions & {
|
||||
backgroundColor: Color | null;
|
||||
@ -248,7 +262,8 @@ export class CanvasRenderer {
|
||||
renderReplacedElement(
|
||||
container: ReplacedElementContainer,
|
||||
curves: BoundCurves,
|
||||
image: HTMLImageElement | HTMLCanvasElement
|
||||
image: HTMLImageElement | HTMLCanvasElement,
|
||||
filter?: Filter | null | undefined
|
||||
) {
|
||||
if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) {
|
||||
const box = contentBox(container);
|
||||
@ -267,6 +282,49 @@ 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();
|
||||
}
|
||||
}
|
||||
@ -283,7 +341,7 @@ export class CanvasRenderer {
|
||||
if (container instanceof ImageElementContainer) {
|
||||
try {
|
||||
const image = await this.options.cache.match(container.src);
|
||||
this.renderReplacedElement(container, curves, image);
|
||||
this.renderReplacedElement(container, curves, image, styles.filter);
|
||||
} catch (e) {
|
||||
Logger.getInstance(this.options.id).error(`Error loading image ${container.src}`);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user