mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
feat: add support for webkit-text-stroke and paint-order (#2591)
This commit is contained in:
parent
dd6d8856ec
commit
522e5aac5f
@ -12,9 +12,9 @@ Below is a list of all the supported CSS properties and values.
|
|||||||
- url()
|
- url()
|
||||||
- linear-gradient()
|
- linear-gradient()
|
||||||
- radial-gradient()
|
- radial-gradient()
|
||||||
- background-origin
|
- background-origin
|
||||||
- background-position
|
- background-position
|
||||||
- background-size
|
- background-size
|
||||||
- border
|
- border
|
||||||
- border-color
|
- border-color
|
||||||
- border-radius
|
- border-radius
|
||||||
@ -50,6 +50,7 @@ Below is a list of all the supported CSS properties and values.
|
|||||||
- overflow
|
- overflow
|
||||||
- overflow-wrap
|
- overflow-wrap
|
||||||
- padding
|
- padding
|
||||||
|
- paint-order
|
||||||
- position
|
- position
|
||||||
- right
|
- right
|
||||||
- text-align
|
- text-align
|
||||||
@ -58,17 +59,18 @@ Below is a list of all the supported CSS properties and values.
|
|||||||
- text-decoration-line
|
- text-decoration-line
|
||||||
- text-decoration-style (**Only supports `solid`**)
|
- text-decoration-style (**Only supports `solid`**)
|
||||||
- text-shadow
|
- text-shadow
|
||||||
- text-transform
|
- text-transform
|
||||||
- top
|
- top
|
||||||
- transform (**Limited support**)
|
- transform (**Limited support**)
|
||||||
- visibility
|
- visibility
|
||||||
- white-space
|
- white-space
|
||||||
- width
|
- width
|
||||||
|
- webkit-text-stroke
|
||||||
- word-break
|
- word-break
|
||||||
- word-spacing
|
- word-spacing
|
||||||
- word-wrap
|
- word-wrap
|
||||||
- z-index
|
- z-index
|
||||||
|
|
||||||
## Unsupported CSS properties
|
## Unsupported CSS properties
|
||||||
These CSS properties are **NOT** currently supported
|
These CSS properties are **NOT** currently supported
|
||||||
- [background-blend-mode](https://github.com/niklasvh/html2canvas/issues/966)
|
- [background-blend-mode](https://github.com/niklasvh/html2canvas/issues/966)
|
||||||
|
@ -73,6 +73,9 @@ 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 {paintOrder} from './property-descriptors/paint-order';
|
||||||
|
import {webkitTextStrokeColor} from './property-descriptors/webkit-text-stroke-color';
|
||||||
|
import {webkitTextStrokeWidth} from './property-descriptors/webkit-text-stroke-width';
|
||||||
|
|
||||||
export class CSSParsedDeclaration {
|
export class CSSParsedDeclaration {
|
||||||
backgroundClip: ReturnType<typeof backgroundClip.parse>;
|
backgroundClip: ReturnType<typeof backgroundClip.parse>;
|
||||||
@ -125,6 +128,7 @@ export class CSSParsedDeclaration {
|
|||||||
paddingRight: LengthPercentage;
|
paddingRight: LengthPercentage;
|
||||||
paddingBottom: LengthPercentage;
|
paddingBottom: LengthPercentage;
|
||||||
paddingLeft: LengthPercentage;
|
paddingLeft: LengthPercentage;
|
||||||
|
paintOrder: ReturnType<typeof paintOrder.parse>;
|
||||||
position: ReturnType<typeof position.parse>;
|
position: ReturnType<typeof position.parse>;
|
||||||
textAlign: ReturnType<typeof textAlign.parse>;
|
textAlign: ReturnType<typeof textAlign.parse>;
|
||||||
textDecorationColor: Color;
|
textDecorationColor: Color;
|
||||||
@ -134,6 +138,8 @@ export class CSSParsedDeclaration {
|
|||||||
transform: ReturnType<typeof transform.parse>;
|
transform: ReturnType<typeof transform.parse>;
|
||||||
transformOrigin: ReturnType<typeof transformOrigin.parse>;
|
transformOrigin: ReturnType<typeof transformOrigin.parse>;
|
||||||
visibility: ReturnType<typeof visibility.parse>;
|
visibility: ReturnType<typeof visibility.parse>;
|
||||||
|
webkitTextStrokeColor: Color;
|
||||||
|
webkitTextStrokeWidth: ReturnType<typeof webkitTextStrokeWidth.parse>;
|
||||||
wordBreak: ReturnType<typeof wordBreak.parse>;
|
wordBreak: ReturnType<typeof wordBreak.parse>;
|
||||||
zIndex: ReturnType<typeof zIndex.parse>;
|
zIndex: ReturnType<typeof zIndex.parse>;
|
||||||
|
|
||||||
@ -189,6 +195,7 @@ export class CSSParsedDeclaration {
|
|||||||
this.paddingRight = parse(paddingRight, declaration.paddingRight);
|
this.paddingRight = parse(paddingRight, declaration.paddingRight);
|
||||||
this.paddingBottom = parse(paddingBottom, declaration.paddingBottom);
|
this.paddingBottom = parse(paddingBottom, declaration.paddingBottom);
|
||||||
this.paddingLeft = parse(paddingLeft, declaration.paddingLeft);
|
this.paddingLeft = parse(paddingLeft, declaration.paddingLeft);
|
||||||
|
this.paintOrder = parse(paintOrder, declaration.paintOrder);
|
||||||
this.position = parse(position, declaration.position);
|
this.position = parse(position, declaration.position);
|
||||||
this.textAlign = parse(textAlign, declaration.textAlign);
|
this.textAlign = parse(textAlign, declaration.textAlign);
|
||||||
this.textDecorationColor = parse(textDecorationColor, declaration.textDecorationColor ?? declaration.color);
|
this.textDecorationColor = parse(textDecorationColor, declaration.textDecorationColor ?? declaration.color);
|
||||||
@ -201,6 +208,8 @@ export class CSSParsedDeclaration {
|
|||||||
this.transform = parse(transform, declaration.transform);
|
this.transform = parse(transform, declaration.transform);
|
||||||
this.transformOrigin = parse(transformOrigin, declaration.transformOrigin);
|
this.transformOrigin = parse(transformOrigin, declaration.transformOrigin);
|
||||||
this.visibility = parse(visibility, declaration.visibility);
|
this.visibility = parse(visibility, declaration.visibility);
|
||||||
|
this.webkitTextStrokeColor = parse(webkitTextStrokeColor, declaration.webkitTextStrokeColor);
|
||||||
|
this.webkitTextStrokeWidth = parse(webkitTextStrokeWidth, declaration.webkitTextStrokeWidth);
|
||||||
this.wordBreak = parse(wordBreak, declaration.wordBreak);
|
this.wordBreak = parse(wordBreak, declaration.wordBreak);
|
||||||
this.zIndex = parse(zIndex, declaration.zIndex);
|
this.zIndex = parse(zIndex, declaration.zIndex);
|
||||||
}
|
}
|
||||||
|
86
src/css/property-descriptors/__tests__/paint-order.ts
Normal file
86
src/css/property-descriptors/__tests__/paint-order.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import {deepStrictEqual} from 'assert';
|
||||||
|
import {Parser} from '../../syntax/parser';
|
||||||
|
import {paintOrder, PAINT_ORDER_LAYER} from '../paint-order';
|
||||||
|
|
||||||
|
const paintOrderParse = (value: string) => paintOrder.parse(Parser.parseValues(value));
|
||||||
|
|
||||||
|
describe('property-descriptors', () => {
|
||||||
|
describe('paint-order', () => {
|
||||||
|
it('none', () =>
|
||||||
|
deepStrictEqual(paintOrderParse('none'), [
|
||||||
|
PAINT_ORDER_LAYER.FILL,
|
||||||
|
PAINT_ORDER_LAYER.STROKE,
|
||||||
|
PAINT_ORDER_LAYER.MARKERS
|
||||||
|
]));
|
||||||
|
|
||||||
|
it('EMPTY', () =>
|
||||||
|
deepStrictEqual(paintOrderParse(''), [
|
||||||
|
PAINT_ORDER_LAYER.FILL,
|
||||||
|
PAINT_ORDER_LAYER.STROKE,
|
||||||
|
PAINT_ORDER_LAYER.MARKERS
|
||||||
|
]));
|
||||||
|
|
||||||
|
it('other values', () =>
|
||||||
|
deepStrictEqual(paintOrderParse('other values'), [
|
||||||
|
PAINT_ORDER_LAYER.FILL,
|
||||||
|
PAINT_ORDER_LAYER.STROKE,
|
||||||
|
PAINT_ORDER_LAYER.MARKERS
|
||||||
|
]));
|
||||||
|
|
||||||
|
it('normal', () =>
|
||||||
|
deepStrictEqual(paintOrderParse('normal'), [
|
||||||
|
PAINT_ORDER_LAYER.FILL,
|
||||||
|
PAINT_ORDER_LAYER.STROKE,
|
||||||
|
PAINT_ORDER_LAYER.MARKERS
|
||||||
|
]));
|
||||||
|
|
||||||
|
it('stroke', () =>
|
||||||
|
deepStrictEqual(paintOrderParse('stroke'), [
|
||||||
|
PAINT_ORDER_LAYER.STROKE,
|
||||||
|
PAINT_ORDER_LAYER.FILL,
|
||||||
|
PAINT_ORDER_LAYER.MARKERS
|
||||||
|
]));
|
||||||
|
|
||||||
|
it('fill', () =>
|
||||||
|
deepStrictEqual(paintOrderParse('fill'), [
|
||||||
|
PAINT_ORDER_LAYER.FILL,
|
||||||
|
PAINT_ORDER_LAYER.STROKE,
|
||||||
|
PAINT_ORDER_LAYER.MARKERS
|
||||||
|
]));
|
||||||
|
|
||||||
|
it('markers', () =>
|
||||||
|
deepStrictEqual(paintOrderParse('markers'), [
|
||||||
|
PAINT_ORDER_LAYER.MARKERS,
|
||||||
|
PAINT_ORDER_LAYER.FILL,
|
||||||
|
PAINT_ORDER_LAYER.STROKE
|
||||||
|
]));
|
||||||
|
|
||||||
|
it('stroke fill', () =>
|
||||||
|
deepStrictEqual(paintOrderParse('stroke fill'), [
|
||||||
|
PAINT_ORDER_LAYER.STROKE,
|
||||||
|
PAINT_ORDER_LAYER.FILL,
|
||||||
|
PAINT_ORDER_LAYER.MARKERS
|
||||||
|
]));
|
||||||
|
|
||||||
|
it('markers stroke', () =>
|
||||||
|
deepStrictEqual(paintOrderParse('markers stroke'), [
|
||||||
|
PAINT_ORDER_LAYER.MARKERS,
|
||||||
|
PAINT_ORDER_LAYER.STROKE,
|
||||||
|
PAINT_ORDER_LAYER.FILL
|
||||||
|
]));
|
||||||
|
|
||||||
|
it('markers stroke fill', () =>
|
||||||
|
deepStrictEqual(paintOrderParse('markers stroke fill'), [
|
||||||
|
PAINT_ORDER_LAYER.MARKERS,
|
||||||
|
PAINT_ORDER_LAYER.STROKE,
|
||||||
|
PAINT_ORDER_LAYER.FILL
|
||||||
|
]));
|
||||||
|
|
||||||
|
it('stroke fill markers', () =>
|
||||||
|
deepStrictEqual(paintOrderParse('stroke fill markers'), [
|
||||||
|
PAINT_ORDER_LAYER.STROKE,
|
||||||
|
PAINT_ORDER_LAYER.FILL,
|
||||||
|
PAINT_ORDER_LAYER.MARKERS
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
});
|
41
src/css/property-descriptors/paint-order.ts
Normal file
41
src/css/property-descriptors/paint-order.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||||
|
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||||
|
export enum PAINT_ORDER_LAYER {
|
||||||
|
FILL,
|
||||||
|
STROKE,
|
||||||
|
MARKERS
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaintOrder = PAINT_ORDER_LAYER[];
|
||||||
|
|
||||||
|
export const paintOrder: IPropertyListDescriptor<PaintOrder> = {
|
||||||
|
name: 'paint-order',
|
||||||
|
initialValue: 'normal',
|
||||||
|
prefix: false,
|
||||||
|
type: PropertyDescriptorParsingType.LIST,
|
||||||
|
parse: (tokens: CSSValue[]): PaintOrder => {
|
||||||
|
const DEFAULT_VALUE = [PAINT_ORDER_LAYER.FILL, PAINT_ORDER_LAYER.STROKE, PAINT_ORDER_LAYER.MARKERS];
|
||||||
|
let layers: PaintOrder = [];
|
||||||
|
|
||||||
|
tokens.filter(isIdentToken).forEach((token) => {
|
||||||
|
switch (token.value) {
|
||||||
|
case 'stroke':
|
||||||
|
layers.push(PAINT_ORDER_LAYER.STROKE);
|
||||||
|
break;
|
||||||
|
case 'fill':
|
||||||
|
layers.push(PAINT_ORDER_LAYER.FILL);
|
||||||
|
break;
|
||||||
|
case 'markers':
|
||||||
|
layers.push(PAINT_ORDER_LAYER.MARKERS);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
DEFAULT_VALUE.forEach((value) => {
|
||||||
|
if (layers.indexOf(value) === -1) {
|
||||||
|
layers.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return layers;
|
||||||
|
}
|
||||||
|
};
|
8
src/css/property-descriptors/webkit-text-stroke-color.ts
Normal file
8
src/css/property-descriptors/webkit-text-stroke-color.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||||
|
export const webkitTextStrokeColor: IPropertyTypeValueDescriptor = {
|
||||||
|
name: `-webkit-text-stroke-color`,
|
||||||
|
initialValue: 'currentcolor',
|
||||||
|
prefix: false,
|
||||||
|
type: PropertyDescriptorParsingType.TYPE_VALUE,
|
||||||
|
format: 'color'
|
||||||
|
};
|
14
src/css/property-descriptors/webkit-text-stroke-width.ts
Normal file
14
src/css/property-descriptors/webkit-text-stroke-width.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import {CSSValue, isDimensionToken} from '../syntax/parser';
|
||||||
|
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||||
|
export const webkitTextStrokeWidth: IPropertyValueDescriptor<number> = {
|
||||||
|
name: `-webkit-text-stroke-width`,
|
||||||
|
initialValue: '0',
|
||||||
|
type: PropertyDescriptorParsingType.VALUE,
|
||||||
|
prefix: false,
|
||||||
|
parse: (token: CSSValue): number => {
|
||||||
|
if (isDimensionToken(token)) {
|
||||||
|
return token.number;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
@ -43,6 +43,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 {PAINT_ORDER_LAYER} from '../../css/property-descriptors/paint-order';
|
||||||
|
|
||||||
export type RenderConfigurations = RenderOptions & {
|
export type RenderConfigurations = RenderOptions & {
|
||||||
backgroundColor: Color | null;
|
backgroundColor: Color | null;
|
||||||
@ -179,65 +180,88 @@ export class CanvasRenderer {
|
|||||||
const [font, fontFamily, fontSize] = this.createFontStyle(styles);
|
const [font, fontFamily, fontSize] = this.createFontStyle(styles);
|
||||||
|
|
||||||
this.ctx.font = font;
|
this.ctx.font = font;
|
||||||
this.ctx.textBaseline = 'alphabetic';
|
|
||||||
|
|
||||||
|
this.ctx.textBaseline = 'alphabetic';
|
||||||
const {baseline, middle} = this.fontMetrics.getMetrics(fontFamily, fontSize);
|
const {baseline, middle} = this.fontMetrics.getMetrics(fontFamily, fontSize);
|
||||||
|
const paintOrder = styles.paintOrder;
|
||||||
|
|
||||||
text.textBounds.forEach((text) => {
|
text.textBounds.forEach((text) => {
|
||||||
this.ctx.fillStyle = asString(styles.color);
|
paintOrder.forEach((paintOrderLayer) => {
|
||||||
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
|
switch (paintOrderLayer) {
|
||||||
const textShadows: TextShadow = styles.textShadow;
|
case PAINT_ORDER_LAYER.FILL:
|
||||||
|
this.ctx.fillStyle = asString(styles.color);
|
||||||
if (textShadows.length && text.text.trim().length) {
|
|
||||||
textShadows
|
|
||||||
.slice(0)
|
|
||||||
.reverse()
|
|
||||||
.forEach((textShadow) => {
|
|
||||||
this.ctx.shadowColor = asString(textShadow.color);
|
|
||||||
this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
|
|
||||||
this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
|
|
||||||
this.ctx.shadowBlur = textShadow.blur.number;
|
|
||||||
|
|
||||||
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
|
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
|
||||||
});
|
const textShadows: TextShadow = styles.textShadow;
|
||||||
|
|
||||||
this.ctx.shadowColor = '';
|
if (textShadows.length && text.text.trim().length) {
|
||||||
this.ctx.shadowOffsetX = 0;
|
textShadows
|
||||||
this.ctx.shadowOffsetY = 0;
|
.slice(0)
|
||||||
this.ctx.shadowBlur = 0;
|
.reverse()
|
||||||
}
|
.forEach((textShadow) => {
|
||||||
|
this.ctx.shadowColor = asString(textShadow.color);
|
||||||
|
this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
|
||||||
|
this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
|
||||||
|
this.ctx.shadowBlur = textShadow.blur.number;
|
||||||
|
|
||||||
if (styles.textDecorationLine.length) {
|
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
|
||||||
this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
|
});
|
||||||
styles.textDecorationLine.forEach((textDecorationLine) => {
|
|
||||||
switch (textDecorationLine) {
|
|
||||||
case TEXT_DECORATION_LINE.UNDERLINE:
|
|
||||||
// Draws a line at the baseline of the font
|
|
||||||
// TODO As some browsers display the line as more than 1px if the font-size is big,
|
|
||||||
// need to take that into account both in position and size
|
|
||||||
this.ctx.fillRect(
|
|
||||||
text.bounds.left,
|
|
||||||
Math.round(text.bounds.top + baseline),
|
|
||||||
text.bounds.width,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
this.ctx.shadowColor = '';
|
||||||
case TEXT_DECORATION_LINE.OVERLINE:
|
this.ctx.shadowOffsetX = 0;
|
||||||
this.ctx.fillRect(text.bounds.left, Math.round(text.bounds.top), text.bounds.width, 1);
|
this.ctx.shadowOffsetY = 0;
|
||||||
break;
|
this.ctx.shadowBlur = 0;
|
||||||
case TEXT_DECORATION_LINE.LINE_THROUGH:
|
}
|
||||||
// TODO try and find exact position for line-through
|
|
||||||
this.ctx.fillRect(
|
if (styles.textDecorationLine.length) {
|
||||||
text.bounds.left,
|
this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
|
||||||
Math.ceil(text.bounds.top + middle),
|
styles.textDecorationLine.forEach((textDecorationLine) => {
|
||||||
text.bounds.width,
|
switch (textDecorationLine) {
|
||||||
1
|
case TEXT_DECORATION_LINE.UNDERLINE:
|
||||||
);
|
// Draws a line at the baseline of the font
|
||||||
break;
|
// TODO As some browsers display the line as more than 1px if the font-size is big,
|
||||||
}
|
// need to take that into account both in position and size
|
||||||
});
|
this.ctx.fillRect(
|
||||||
}
|
text.bounds.left,
|
||||||
|
Math.round(text.bounds.top + baseline),
|
||||||
|
text.bounds.width,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case TEXT_DECORATION_LINE.OVERLINE:
|
||||||
|
this.ctx.fillRect(
|
||||||
|
text.bounds.left,
|
||||||
|
Math.round(text.bounds.top),
|
||||||
|
text.bounds.width,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case TEXT_DECORATION_LINE.LINE_THROUGH:
|
||||||
|
// TODO try and find exact position for line-through
|
||||||
|
this.ctx.fillRect(
|
||||||
|
text.bounds.left,
|
||||||
|
Math.ceil(text.bounds.top + middle),
|
||||||
|
text.bounds.width,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PAINT_ORDER_LAYER.STROKE:
|
||||||
|
if (styles.webkitTextStrokeWidth && text.text.trim().length) {
|
||||||
|
this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
|
||||||
|
this.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
||||||
|
this.ctx.lineJoin = !!(window as any).chrome ? 'miter' : 'round';
|
||||||
|
this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + baseline);
|
||||||
|
}
|
||||||
|
this.ctx.strokeStyle = '';
|
||||||
|
this.ctx.lineWidth = 0;
|
||||||
|
this.ctx.lineJoin = 'miter';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user