Skip to content

Commit

Permalink
feat: add support for webkit-text-stroke and paint-order (#2591)
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasvh authored Jul 15, 2021
1 parent dd6d885 commit 522e5aa
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 58 deletions.
10 changes: 6 additions & 4 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Below is a list of all the supported CSS properties and values.
- url()
- linear-gradient()
- radial-gradient()
- background-origin
- background-origin
- background-position
- background-size
- background-size
- border
- border-color
- border-radius
Expand Down Expand Up @@ -50,6 +50,7 @@ Below is a list of all the supported CSS properties and values.
- overflow
- overflow-wrap
- padding
- paint-order
- position
- right
- text-align
Expand All @@ -58,17 +59,18 @@ Below is a list of all the supported CSS properties and values.
- text-decoration-line
- text-decoration-style (**Only supports `solid`**)
- text-shadow
- text-transform
- text-transform
- top
- transform (**Limited support**)
- visibility
- white-space
- width
- webkit-text-stroke
- word-break
- word-spacing
- word-wrap
- z-index

## Unsupported CSS properties
These CSS properties are **NOT** currently supported
- [background-blend-mode](https://github.com/niklasvh/html2canvas/issues/966)
Expand Down
9 changes: 9 additions & 0 deletions src/css/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ 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 {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 {
backgroundClip: ReturnType<typeof backgroundClip.parse>;
Expand Down Expand Up @@ -125,6 +128,7 @@ export class CSSParsedDeclaration {
paddingRight: LengthPercentage;
paddingBottom: LengthPercentage;
paddingLeft: LengthPercentage;
paintOrder: ReturnType<typeof paintOrder.parse>;
position: ReturnType<typeof position.parse>;
textAlign: ReturnType<typeof textAlign.parse>;
textDecorationColor: Color;
Expand All @@ -134,6 +138,8 @@ export class CSSParsedDeclaration {
transform: ReturnType<typeof transform.parse>;
transformOrigin: ReturnType<typeof transformOrigin.parse>;
visibility: ReturnType<typeof visibility.parse>;
webkitTextStrokeColor: Color;
webkitTextStrokeWidth: ReturnType<typeof webkitTextStrokeWidth.parse>;
wordBreak: ReturnType<typeof wordBreak.parse>;
zIndex: ReturnType<typeof zIndex.parse>;

Expand Down Expand Up @@ -189,6 +195,7 @@ export class CSSParsedDeclaration {
this.paddingRight = parse(paddingRight, declaration.paddingRight);
this.paddingBottom = parse(paddingBottom, declaration.paddingBottom);
this.paddingLeft = parse(paddingLeft, declaration.paddingLeft);
this.paintOrder = parse(paintOrder, declaration.paintOrder);
this.position = parse(position, declaration.position);
this.textAlign = parse(textAlign, declaration.textAlign);
this.textDecorationColor = parse(textDecorationColor, declaration.textDecorationColor ?? declaration.color);
Expand All @@ -201,6 +208,8 @@ export class CSSParsedDeclaration {
this.transform = parse(transform, declaration.transform);
this.transformOrigin = parse(transformOrigin, declaration.transformOrigin);
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.zIndex = parse(zIndex, declaration.zIndex);
}
Expand Down
86 changes: 86 additions & 0 deletions src/css/property-descriptors/__tests__/paint-order.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 41 additions & 0 deletions src/css/property-descriptors/paint-order.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions src/css/property-descriptors/webkit-text-stroke-color.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions src/css/property-descriptors/webkit-text-stroke-width.ts
Original file line number Diff line number Diff line change
@@ -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;
}
};
132 changes: 78 additions & 54 deletions src/render/canvas/canvas-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,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 {PAINT_ORDER_LAYER} from '../../css/property-descriptors/paint-order';

export type RenderConfigurations = RenderOptions & {
backgroundColor: Color | null;
Expand Down Expand Up @@ -179,65 +180,88 @@ export class CanvasRenderer {
const [font, fontFamily, fontSize] = this.createFontStyle(styles);

this.ctx.font = font;
this.ctx.textBaseline = 'alphabetic';

this.ctx.textBaseline = 'alphabetic';
const {baseline, middle} = this.fontMetrics.getMetrics(fontFamily, fontSize);
const paintOrder = styles.paintOrder;

text.textBounds.forEach((text) => {
this.ctx.fillStyle = asString(styles.color);
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
const textShadows: TextShadow = styles.textShadow;

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;

paintOrder.forEach((paintOrderLayer) => {
switch (paintOrderLayer) {
case PAINT_ORDER_LAYER.FILL:
this.ctx.fillStyle = asString(styles.color);
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
});

this.ctx.shadowColor = '';
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
this.ctx.shadowBlur = 0;
}

if (styles.textDecorationLine.length) {
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;
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;
}
});
}
const textShadows: TextShadow = styles.textShadow;

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.ctx.shadowColor = '';
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
this.ctx.shadowBlur = 0;
}

if (styles.textDecorationLine.length) {
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;
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;
}
});
});
}

Expand Down

0 comments on commit 522e5aa

Please sign in to comment.