Repository: niklasvh/html2canvas (ts)

Issue:
text-stroke is not support;I can not find any code about ‘text-stroke’
Please make sure you are testing with the latest [release of html2canvas](https://github.com/niklasvh/html2canvas/releases). 
Old versions are not supported and issues reported for them will be closed.

# Please follow the general troubleshooting steps first:

- [ ] You are using the latest [version](https://github.com/niklasvh/html2canvas/releases)
- [ ] You are testing using the non-minified version of html2canvas and checked any potential issues reported in the console

<!-- You can erase any parts of this template not applicable to your Issue. -->

### Bug reports:

Please replace this line with a brief summary of your issue **AND** if possible an example on [jsfiddle](https://jsfiddle.net/).

### Specifications:

 * html2canvas version tested with:
 * Browser & version:
 * Operating system:


Pull request description:
feat: add support for webkit-text-stroke and paint-order
Based on original work in #1540

Fixes #1540
Fixes #2479
Fixes #2009


Patch:
diff --git a/docs/features.md b/docs/features.md
index e14c33d..d1e1c83 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -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
@@ -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
@@ -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)
diff --git a/src/css/index.ts b/src/css/index.ts
index af794d6..09d22d3 100644
--- a/src/css/index.ts
+++ b/src/css/index.ts
@@ -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>;
@@ -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;
@@ -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>;
 
@@ -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);
@@ -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);
     }
diff --git a/src/css/property-descriptors/paint-order.ts b/src/css/property-descriptors/paint-order.ts
new file mode 100644
index 0000000..eb2a901
--- /dev/null
+++ b/src/css/property-descriptors/paint-order.ts
@@ -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;
+    }
+};
diff --git a/src/css/property-descriptors/webkit-text-stroke-color.ts b/src/css/property-descriptors/webkit-text-stroke-color.ts
new file mode 100644
index 0000000..b7fb031
--- /dev/null
+++ b/src/css/property-descriptors/webkit-text-stroke-color.ts
@@ -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'
+};
diff --git a/src/css/property-descriptors/webkit-text-stroke-width.ts b/src/css/property-descriptors/webkit-text-stroke-width.ts
new file mode 100644
index 0000000..4d0acef
--- /dev/null
+++ b/src/css/property-descriptors/webkit-text-stroke-width.ts
@@ -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;
+    }
+};
diff --git a/src/render/canvas/canvas-renderer.ts b/src/render/canvas/canvas-renderer.ts
index fb4d1ab..08625bd 100644
--- a/src/render/canvas/canvas-renderer.ts
+++ b/src/render/canvas/canvas-renderer.ts
@@ -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;
@@ -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;
+                }
+            });
         });
     }
 


Test patch:
diff --git a/src/css/property-descriptors/__tests__/paint-order.ts b/src/css/property-descriptors/__tests__/paint-order.ts
new file mode 100644
index 0000000..19aa7e3
--- /dev/null
+++ b/src/css/property-descriptors/__tests__/paint-order.ts
@@ -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
+            ]));
+    });
+});