mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
fix opacity
This commit is contained in:
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"liveServer.settings.port": 0
|
||||
}
|
||||
@@ -45,8 +45,11 @@ import {overflowWrap} from './property-descriptors/overflow-wrap';
|
||||
import {paddingBottom, paddingLeft, paddingRight, paddingTop} from './property-descriptors/padding';
|
||||
import {textAlign} from './property-descriptors/text-align';
|
||||
import {position, POSITION} from './property-descriptors/position';
|
||||
import {textFillColor} from './property-descriptors/text-fill-color';
|
||||
import {textShadow} from './property-descriptors/text-shadow';
|
||||
import {textTransform} from './property-descriptors/text-transform';
|
||||
import {textStrokeColor} from './property-descriptors/text-stroke-color';
|
||||
import {textStrokeWidth} from './property-descriptors/text-stroke-width';
|
||||
import {transform} from './property-descriptors/transform';
|
||||
import {transformOrigin} from './property-descriptors/transform-origin';
|
||||
import {visibility, VISIBILITY} from './property-descriptors/visibility';
|
||||
@@ -73,6 +76,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>;
|
||||
@@ -101,6 +105,8 @@ export class CSSParsedDeclaration {
|
||||
boxShadow: ReturnType<typeof boxShadow.parse>;
|
||||
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;
|
||||
@@ -127,10 +133,13 @@ export class CSSParsedDeclaration {
|
||||
paddingLeft: LengthPercentage;
|
||||
position: ReturnType<typeof position.parse>;
|
||||
textAlign: ReturnType<typeof textAlign.parse>;
|
||||
textFillColor: Color;
|
||||
textDecorationColor: Color;
|
||||
textDecorationLine: ReturnType<typeof textDecorationLine.parse>;
|
||||
textShadow: ReturnType<typeof textShadow.parse>;
|
||||
textTransform: ReturnType<typeof textTransform.parse>;
|
||||
textStrokeColor: Color;
|
||||
textStrokeWidth: LengthPercentage;
|
||||
transform: ReturnType<typeof transform.parse>;
|
||||
transformOrigin: ReturnType<typeof transformOrigin.parse>;
|
||||
visibility: ReturnType<typeof visibility.parse>;
|
||||
@@ -164,6 +173,8 @@ 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.filterOriginal = declaration.filter;
|
||||
this.float = parse(float, declaration.cssFloat);
|
||||
this.fontFamily = parse(fontFamily, declaration.fontFamily);
|
||||
this.fontSize = parse(fontSize, declaration.fontSize);
|
||||
@@ -191,10 +202,13 @@ export class CSSParsedDeclaration {
|
||||
this.paddingLeft = parse(paddingLeft, declaration.paddingLeft);
|
||||
this.position = parse(position, declaration.position);
|
||||
this.textAlign = parse(textAlign, declaration.textAlign);
|
||||
this.textFillColor = parse(textFillColor, declaration.textFillColor);
|
||||
this.textDecorationColor = parse(textDecorationColor, declaration.textDecorationColor || declaration.color);
|
||||
this.textDecorationLine = parse(textDecorationLine, declaration.textDecorationLine);
|
||||
this.textShadow = parse(textShadow, declaration.textShadow);
|
||||
this.textTransform = parse(textTransform, declaration.textTransform);
|
||||
this.textStrokeColor = parse(textStrokeColor, declaration.webkitTextStrokeColor);
|
||||
this.textStrokeWidth = parse(textStrokeWidth, declaration.webkitTextStrokeWidth);
|
||||
this.transform = parse(transform, declaration.transform);
|
||||
this.transformOrigin = parse(transformOrigin, declaration.transformOrigin);
|
||||
this.visibility = parse(visibility, declaration.visibility);
|
||||
@@ -261,7 +275,8 @@ export class CSSParsedCounterDeclaration {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const parse = (descriptor: CSSPropertyDescriptor<any>, style?: string | null) => {
|
||||
const tokenizer = new Tokenizer();
|
||||
const value = style !== null && typeof style !== 'undefined' ? style.toString() : descriptor.initialValue;
|
||||
const value =
|
||||
style !== null && typeof style !== 'undefined' && style != '' ? style.toString() : descriptor.initialValue;
|
||||
tokenizer.write(value);
|
||||
const parser = new Parser(tokenizer.read());
|
||||
switch (descriptor.type) {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import {deepEqual} from 'assert';
|
||||
import {Parser} from '../../syntax/parser';
|
||||
import {fontFamily} from '../font-family';
|
||||
|
||||
const fontFamilyParse = (value: string) => fontFamily.parse(Parser.parseValues(value));
|
||||
|
||||
describe('property-descriptors', () => {
|
||||
describe('font-family', () => {
|
||||
it('sans-serif', () => deepEqual(fontFamilyParse('sans-serif'), ['sans-serif']));
|
||||
|
||||
it('great fonts 40 library', () =>
|
||||
deepEqual(fontFamilyParse('great fonts 40 library'), ["'great fonts 40 library'"]));
|
||||
|
||||
it('preferred font, "quoted fallback font", font', () =>
|
||||
deepEqual(fontFamilyParse('preferred font, "quoted fallback font", font'), [
|
||||
"'preferred font'",
|
||||
"'quoted fallback font'",
|
||||
'font'
|
||||
]));
|
||||
|
||||
it("'escaping test\\'s font'", () =>
|
||||
deepEqual(fontFamilyParse("'escaping test\\'s font'"), ["'escaping test's font'"]));
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,8 @@ import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
export enum BACKGROUND_CLIP {
|
||||
BORDER_BOX = 0,
|
||||
PADDING_BOX = 1,
|
||||
CONTENT_BOX = 2
|
||||
CONTENT_BOX = 2,
|
||||
TEXT = 3
|
||||
}
|
||||
|
||||
export type BackgroundClip = BACKGROUND_CLIP[];
|
||||
@@ -21,6 +22,8 @@ export const backgroundClip: IPropertyListDescriptor<BackgroundClip> = {
|
||||
return BACKGROUND_CLIP.PADDING_BOX;
|
||||
case 'content-box':
|
||||
return BACKGROUND_CLIP.CONTENT_BOX;
|
||||
case 'text':
|
||||
return BACKGROUND_CLIP.TEXT;
|
||||
}
|
||||
}
|
||||
return BACKGROUND_CLIP.BORDER_BOX;
|
||||
|
||||
53
src/css/property-descriptors/filter.ts
Normal file
53
src/css/property-descriptors/filter.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentWithValue, CSSFunction, isCSSFunction} from '../syntax/parser';
|
||||
import {isLength, Length} from '../types/length';
|
||||
|
||||
export type Filter = FilterItem[];
|
||||
export interface FilterItem {
|
||||
name: string;
|
||||
value: Length;
|
||||
}
|
||||
|
||||
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 = [];
|
||||
|
||||
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.push({name: token.name, value: value});
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasFilter) {
|
||||
return filter;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {StringValueToken, TokenType} from '../syntax/tokenizer';
|
||||
|
||||
export type FONT_FAMILY = string;
|
||||
|
||||
@@ -12,26 +12,9 @@ export const fontFamily: IPropertyListDescriptor<FontFamily> = {
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]) => {
|
||||
const accumulator: string[] = [];
|
||||
const results: string[] = [];
|
||||
tokens.forEach(token => {
|
||||
switch (token.type) {
|
||||
case TokenType.IDENT_TOKEN:
|
||||
case TokenType.STRING_TOKEN:
|
||||
accumulator.push(token.value);
|
||||
break;
|
||||
case TokenType.NUMBER_TOKEN:
|
||||
accumulator.push(token.number.toString());
|
||||
break;
|
||||
case TokenType.COMMA_TOKEN:
|
||||
results.push(accumulator.join(' '));
|
||||
accumulator.length = 0;
|
||||
break;
|
||||
}
|
||||
});
|
||||
if (accumulator.length) {
|
||||
results.push(accumulator.join(' '));
|
||||
}
|
||||
return results.map(result => (result.indexOf(' ') === -1 ? result : `'${result}'`));
|
||||
return tokens.filter(isStringToken).map(token => token.value);
|
||||
}
|
||||
};
|
||||
|
||||
const isStringToken = (token: CSSValue): token is StringValueToken =>
|
||||
token.type === TokenType.STRING_TOKEN || token.type === TokenType.IDENT_TOKEN;
|
||||
|
||||
9
src/css/property-descriptors/text-fill-color.ts
Normal file
9
src/css/property-descriptors/text-fill-color.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
|
||||
export const textFillColor: IPropertyTypeValueDescriptor = {
|
||||
name: `text-fill-color`,
|
||||
initialValue: 'transparent',
|
||||
prefix: true,
|
||||
type: PropertyDescriptorParsingType.TYPE_VALUE,
|
||||
format: 'color'
|
||||
};
|
||||
9
src/css/property-descriptors/text-stroke-color.ts
Normal file
9
src/css/property-descriptors/text-stroke-color.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
|
||||
export const textStrokeColor: IPropertyTypeValueDescriptor = {
|
||||
name: `text-stroke-color`,
|
||||
initialValue: 'transparent',
|
||||
prefix: true,
|
||||
type: PropertyDescriptorParsingType.TYPE_VALUE,
|
||||
format: 'color'
|
||||
};
|
||||
8
src/css/property-descriptors/text-stroke-width.ts
Normal file
8
src/css/property-descriptors/text-stroke-width.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
export const textStrokeWidth: IPropertyTypeValueDescriptor = {
|
||||
name: 'text-stroke-width',
|
||||
initialValue: '0',
|
||||
type: PropertyDescriptorParsingType.TYPE_VALUE,
|
||||
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/stack-blur.ts
Normal file
1057
src/css/types/functions/stack-blur.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import {
|
||||
isElementNode,
|
||||
isHTMLElementNode,
|
||||
isIFrameElement,
|
||||
isImageElement,
|
||||
isScriptElement,
|
||||
isSelectElement,
|
||||
isStyleElement,
|
||||
@@ -116,7 +115,7 @@ export class DocumentCloner {
|
||||
return iframeLoad;
|
||||
}
|
||||
|
||||
createElementClone<T extends HTMLElement | SVGElement>(node: T): HTMLElement | SVGElement {
|
||||
createElementClone(node: HTMLElement): HTMLElement {
|
||||
if (isCanvasElement(node)) {
|
||||
return this.createCanvasClone(node);
|
||||
}
|
||||
@@ -129,14 +128,7 @@ export class DocumentCloner {
|
||||
return this.createStyleClone(node);
|
||||
}
|
||||
|
||||
const clone = node.cloneNode(false) as T;
|
||||
// @ts-ignore
|
||||
if (isImageElement(clone) && clone.loading === 'lazy') {
|
||||
// @ts-ignore
|
||||
clone.loading = 'eager';
|
||||
}
|
||||
|
||||
return clone;
|
||||
return node.cloneNode(false) as HTMLElement;
|
||||
}
|
||||
|
||||
createStyleClone(node: HTMLStyleElement): HTMLStyleElement {
|
||||
@@ -265,14 +257,14 @@ export class DocumentCloner {
|
||||
|
||||
const window = node.ownerDocument.defaultView;
|
||||
|
||||
if (window && isElementNode(node) && (isHTMLElementNode(node) || isSVGElementNode(node))) {
|
||||
if (isHTMLElementNode(node) && window) {
|
||||
const clone = this.createElementClone(node);
|
||||
|
||||
const style = window.getComputedStyle(node);
|
||||
const styleBefore = window.getComputedStyle(node, ':before');
|
||||
const styleAfter = window.getComputedStyle(node, ':after');
|
||||
|
||||
if (this.referenceElement === node && isHTMLElementNode(clone)) {
|
||||
if (this.referenceElement === node) {
|
||||
this.clonedReferenceElement = clone;
|
||||
}
|
||||
if (isBodyElement(clone)) {
|
||||
@@ -306,7 +298,7 @@ export class DocumentCloner {
|
||||
|
||||
this.counters.pop(counters);
|
||||
|
||||
if (style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node)) {
|
||||
if (style && this.options.copyStyles && !isIFrameElement(node)) {
|
||||
copyCSSStyles(style, clone);
|
||||
}
|
||||
|
||||
@@ -486,7 +478,7 @@ const iframeLoader = (iframe: HTMLIFrameElement): Promise<HTMLIFrameElement> =>
|
||||
});
|
||||
};
|
||||
|
||||
export const copyCSSStyles = <T extends HTMLElement | SVGElement>(style: CSSStyleDeclaration, target: T): T => {
|
||||
export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): HTMLElement => {
|
||||
// Edge does not provide value for cssText
|
||||
for (let i = style.length - 1; i >= 0; i--) {
|
||||
const property = style.item(i);
|
||||
|
||||
@@ -102,7 +102,7 @@ const createsStackingContext = (styles: CSSParsedDeclaration): boolean => styles
|
||||
export const isTextNode = (node: Node): node is Text => node.nodeType === Node.TEXT_NODE;
|
||||
export const isElementNode = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE;
|
||||
export const isHTMLElementNode = (node: Node): node is HTMLElement =>
|
||||
isElementNode(node) && typeof (node as HTMLElement).style !== 'undefined' && !isSVGElementNode(node);
|
||||
typeof (node as HTMLElement).style !== 'undefined';
|
||||
export const isSVGElementNode = (element: Element): element is SVGElement =>
|
||||
typeof (element as SVGElement).className === 'object';
|
||||
export const isLIElement = (node: Element): node is HTMLLIElement => node.tagName === 'LI';
|
||||
|
||||
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
interface CSSStyleDeclaration {
|
||||
textDecorationColor: string | null;
|
||||
textDecorationLine: string | null;
|
||||
textFillColor: string | null;
|
||||
overflowWrap: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const html2canvas = (element: HTMLElement, options: Partial<Options> = {}): Prom
|
||||
|
||||
export default html2canvas;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
CacheStorage.setContext(window);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip';
|
||||
import {BoundCurves, calculateBorderBoxPath, calculateContentBoxPath, calculatePaddingBoxPath} from '../bound-curves';
|
||||
import {isBezierCurve} from '../bezier-curve';
|
||||
import {Vector} from '../vector';
|
||||
import {CSSImageType, CSSURLImage, isLinearGradient, isRadialGradient} from '../../css/types/image';
|
||||
import {CSSImageType, CSSLinearGradientImage, CSSRadialGradientImage, CSSURLImage, isLinearGradient, isRadialGradient} from '../../css/types/image';
|
||||
import {parsePathForBorder} from '../border';
|
||||
import {Cache} from '../../core/cache-storage';
|
||||
import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background';
|
||||
@@ -22,7 +22,7 @@ import {contentBox} from '../box-sizing';
|
||||
import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-element-container';
|
||||
import {SVGElementContainer} from '../../dom/replaced-elements/svg-element-container';
|
||||
import {ReplacedElementContainer} from '../../dom/replaced-elements/index';
|
||||
import {EffectTarget, IElementEffect, isClipEffect, isOpacityEffect, isTransformEffect} from '../effects';
|
||||
import {EffectTarget, IElementEffect, isClipEffect, isTransformEffect, isOpacityEffect} from '../effects';
|
||||
import {contains} from '../../core/bitwise';
|
||||
import {calculateGradientDirection, calculateRadius, processColorStops} from '../../css/types/functions/gradient';
|
||||
import {FIFTY_PERCENT, getAbsoluteValue} from '../../css/types/length-percentage';
|
||||
@@ -38,6 +38,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 { processImage, isSupportedFilter } from '../image-filter';
|
||||
import {isFontColorGradient } from '../font-color-gradient'
|
||||
|
||||
export type RenderConfigurations = RenderOptions & {
|
||||
backgroundColor: Color | null;
|
||||
@@ -99,10 +101,6 @@ export class CanvasRenderer {
|
||||
|
||||
applyEffect(effect: IElementEffect) {
|
||||
this.ctx.save();
|
||||
if (isOpacityEffect(effect)) {
|
||||
this.ctx.globalAlpha = effect.opacity;
|
||||
}
|
||||
|
||||
if (isTransformEffect(effect)) {
|
||||
this.ctx.translate(effect.offsetX, effect.offsetY);
|
||||
this.ctx.transform(
|
||||
@@ -120,7 +118,9 @@ export class CanvasRenderer {
|
||||
this.path(effect.path);
|
||||
this.ctx.clip();
|
||||
}
|
||||
|
||||
if (isOpacityEffect(effect)) {
|
||||
this.ctx.globalAlpha = effect.val;
|
||||
}
|
||||
this._activeEffects.push(effect);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ export class CanvasRenderer {
|
||||
async renderStack(stack: StackingContext) {
|
||||
const styles = stack.element.container.styles;
|
||||
if (styles.isVisible()) {
|
||||
this.ctx.globalAlpha = styles.opacity;
|
||||
await this.renderStackContent(stack);
|
||||
}
|
||||
}
|
||||
@@ -172,13 +173,56 @@ export class CanvasRenderer {
|
||||
];
|
||||
}
|
||||
|
||||
async renderTextNode(text: TextContainer, styles: CSSParsedDeclaration) {
|
||||
async renderTextNode(text: TextContainer, styles: CSSParsedDeclaration, container: ElementContainer) {
|
||||
const [font, fontFamily, fontSize] = this.createFontStyle(styles);
|
||||
|
||||
this.ctx.font = font;
|
||||
let fillStyle: CanvasGradient|string = asString(styles.color);
|
||||
if (isFontColorGradient(styles)) {
|
||||
if (isLinearGradient(styles.backgroundImage[0])) {
|
||||
const backgroundImage = styles.backgroundImage[0] as CSSLinearGradientImage
|
||||
const [, x, y, width, height] = calculateBackgroundRendering(container, 0, [null, null, null]);
|
||||
const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);
|
||||
if (Math.round(Math.abs(x0)) === Math.round(Math.abs(x1))) {
|
||||
const gradient = this.ctx.createLinearGradient(x, y0+y, x, y1+y);
|
||||
processColorStops(backgroundImage.stops, lineLength).forEach(colorStop =>
|
||||
gradient.addColorStop(colorStop.stop, asString(colorStop.color))
|
||||
);
|
||||
fillStyle = gradient;
|
||||
} else if (Math.round(Math.abs(y0)) === Math.round(Math.abs(y1))) {
|
||||
const gradient = this.ctx.createLinearGradient(x+x0, y, x+x1, y);
|
||||
processColorStops(backgroundImage.stops, lineLength).forEach(colorStop =>
|
||||
gradient.addColorStop(colorStop.stop, asString(colorStop.color))
|
||||
);
|
||||
fillStyle = gradient;
|
||||
}
|
||||
} else if (isRadialGradient(styles.backgroundImage[0])) {
|
||||
const backgroundImage = styles.backgroundImage[0] as CSSRadialGradientImage
|
||||
const [, left, top, width, height] = calculateBackgroundRendering(container, 0, [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]);
|
||||
const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
|
||||
const x = getAbsoluteValue(position[0], width);
|
||||
const y = getAbsoluteValue(position[position.length - 1], height);
|
||||
|
||||
const [rx,] = calculateRadius(backgroundImage, x, y, width, height);
|
||||
if (rx > 0 && rx > 0) {
|
||||
const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
|
||||
|
||||
processColorStops(backgroundImage.stops, rx * 2).forEach(colorStop =>
|
||||
radialGradient.addColorStop(colorStop.stop, asString(colorStop.color))
|
||||
);
|
||||
|
||||
fillStyle = radialGradient;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
text.textBounds.forEach(text => {
|
||||
this.ctx.fillStyle = asString(styles.color);
|
||||
this.ctx.fillStyle = fillStyle;
|
||||
this.renderTextWithLetterSpacing(text, styles.letterSpacing);
|
||||
const textShadows: TextShadow = styles.textShadow;
|
||||
|
||||
@@ -234,6 +278,20 @@ export class CanvasRenderer {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
styles.textStrokeWidth &&
|
||||
styles.textStrokeWidth.number &&
|
||||
styles.textStrokeColor &&
|
||||
text.text.trim().length
|
||||
) {
|
||||
this.ctx.strokeStyle = asString(styles.textStrokeColor);
|
||||
this.ctx.lineWidth = getAbsoluteValue(styles.textStrokeWidth, text.bounds.width);
|
||||
this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
|
||||
|
||||
this.ctx.strokeStyle = '';
|
||||
this.ctx.lineWidth = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -248,6 +306,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,
|
||||
@@ -269,12 +330,13 @@ export class CanvasRenderer {
|
||||
const curves = paint.curves;
|
||||
const styles = container.styles;
|
||||
for (const child of container.textNodes) {
|
||||
await this.renderTextNode(child, styles);
|
||||
await this.renderTextNode(child, styles, container);
|
||||
}
|
||||
|
||||
if (container instanceof ImageElementContainer) {
|
||||
try {
|
||||
const image = await this.options.cache.match(container.src);
|
||||
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}`);
|
||||
@@ -545,6 +607,7 @@ export class CanvasRenderer {
|
||||
|
||||
async renderBackgroundImage(container: ElementContainer) {
|
||||
let index = container.styles.backgroundImage.length - 1;
|
||||
if (isFontColorGradient(container.styles)) return false;
|
||||
for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) {
|
||||
if (backgroundImage.type === CSSImageType.URL) {
|
||||
let image;
|
||||
@@ -570,7 +633,6 @@ export class CanvasRenderer {
|
||||
} else if (isLinearGradient(backgroundImage)) {
|
||||
const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [null, null, null]);
|
||||
const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
@@ -18,37 +18,42 @@ export interface IElementEffect {
|
||||
}
|
||||
|
||||
export class TransformEffect implements IElementEffect {
|
||||
readonly type: EffectType = EffectType.TRANSFORM;
|
||||
readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
|
||||
readonly type: EffectType;
|
||||
readonly target: number;
|
||||
readonly offsetX: number;
|
||||
readonly offsetY: number;
|
||||
readonly matrix: Matrix;
|
||||
|
||||
constructor(offsetX: number, offsetY: number, matrix: Matrix) {
|
||||
this.type = EffectType.TRANSFORM;
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
this.matrix = matrix;
|
||||
this.target = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClipEffect implements IElementEffect {
|
||||
readonly type: EffectType = EffectType.CLIP;
|
||||
readonly type: EffectType;
|
||||
readonly target: number;
|
||||
readonly path: Path[];
|
||||
|
||||
constructor(path: Path[], target: EffectTarget) {
|
||||
this.type = EffectType.CLIP;
|
||||
this.target = target;
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
export class OpacityEffect implements IElementEffect {
|
||||
readonly type: EffectType = EffectType.OPACITY;
|
||||
readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
|
||||
readonly opacity: number;
|
||||
readonly type: EffectType;
|
||||
readonly target: number;
|
||||
readonly val: number;
|
||||
|
||||
constructor(opacity: number) {
|
||||
this.opacity = opacity;
|
||||
constructor(val: number, target: EffectTarget) {
|
||||
this.type = EffectType.OPACITY;
|
||||
this.target = target;
|
||||
this.val = val;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
src/render/font-color-gradient.ts
Normal file
13
src/render/font-color-gradient.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {CSSParsedDeclaration} from '../css/index';
|
||||
import {isLinearGradient, isRadialGradient} from '../css/types/image';
|
||||
import {BACKGROUND_CLIP} from '../css/property-descriptors/background-clip';
|
||||
|
||||
export const isFontColorGradient = (styles: CSSParsedDeclaration) => {
|
||||
return (
|
||||
styles.backgroundImage.length === 1 &&
|
||||
(isLinearGradient(styles.backgroundImage[0]) || isRadialGradient(styles.backgroundImage[0])) &&
|
||||
(styles.textFillColor === 0 || styles.color === 0) &&
|
||||
styles.backgroundClip.length === 1 &&
|
||||
styles.backgroundClip[0] === BACKGROUND_CLIP.TEXT
|
||||
);
|
||||
};
|
||||
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;
|
||||
@@ -1,7 +1,7 @@
|
||||
import {ElementContainer, FLAGS} from '../dom/element-container';
|
||||
import {contains} from '../core/bitwise';
|
||||
import {BoundCurves, calculateBorderBoxPath, calculatePaddingBoxPath} from './bound-curves';
|
||||
import {ClipEffect, EffectTarget, IElementEffect, OpacityEffect, TransformEffect} from './effects';
|
||||
import {ClipEffect, EffectTarget, IElementEffect, TransformEffect, OpacityEffect} from './effects';
|
||||
import {OVERFLOW} from '../css/property-descriptors/overflow';
|
||||
import {equalPath} from './path';
|
||||
import {DISPLAY} from '../css/property-descriptors/display';
|
||||
@@ -41,10 +41,6 @@ export class ElementPaint {
|
||||
this.container = element;
|
||||
this.effects = parentStack.slice(0);
|
||||
this.curves = new BoundCurves(element);
|
||||
if (element.styles.opacity < 1) {
|
||||
this.effects.push(new OpacityEffect(element.styles.opacity));
|
||||
}
|
||||
|
||||
if (element.styles.transform !== null) {
|
||||
const offsetX = element.bounds.left + element.styles.transformOrigin[0].number;
|
||||
const offsetY = element.bounds.top + element.styles.transformOrigin[1].number;
|
||||
@@ -63,6 +59,10 @@ export class ElementPaint {
|
||||
this.effects.push(new ClipEffect(paddingBox, EffectTarget.CONTENT));
|
||||
}
|
||||
}
|
||||
|
||||
if (element.styles.opacity < 1) {
|
||||
this.effects.push(new OpacityEffect(element.styles.opacity, EffectTarget.CONTENT));
|
||||
}
|
||||
}
|
||||
|
||||
getParentEffects(): IElementEffect[] {
|
||||
@@ -74,6 +74,9 @@ export class ElementPaint {
|
||||
effects.push(new ClipEffect(paddingBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT));
|
||||
}
|
||||
}
|
||||
if (this.container.styles.opacity < 1) {
|
||||
effects.push(new OpacityEffect(this.container.styles.opacity, EffectTarget.CONTENT));
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
}
|
||||
@@ -119,7 +122,7 @@ const parseStackTree = (
|
||||
} else if (order > 0) {
|
||||
let index = 0;
|
||||
parentStack.positiveZIndex.some((current, i) => {
|
||||
if (order >= current.element.container.styles.zIndex.order) {
|
||||
if (order > current.element.container.styles.zIndex.order) {
|
||||
index = i + 1;
|
||||
return false;
|
||||
} else if (index > 0) {
|
||||
|
||||
Reference in New Issue
Block a user