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; @@ -125,6 +128,7 @@ export class CSSParsedDeclaration { paddingRight: LengthPercentage; paddingBottom: LengthPercentage; paddingLeft: LengthPercentage; + paintOrder: ReturnType; position: ReturnType; textAlign: ReturnType; textDecorationColor: Color; @@ -134,6 +138,8 @@ export class CSSParsedDeclaration { transform: ReturnType; transformOrigin: ReturnType; visibility: ReturnType; + webkitTextStrokeColor: Color; + webkitTextStrokeWidth: ReturnType; wordBreak: ReturnType; zIndex: ReturnType; @@ -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/__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 + ])); + }); +}); 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 = { + 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 = { + 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); - }); + const textShadows: TextShadow = styles.textShadow; - this.ctx.shadowColor = ''; - this.ctx.shadowOffsetX = 0; - this.ctx.shadowOffsetY = 0; - this.ctx.shadowBlur = 0; - } + 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; - 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 - ); + this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline); + }); - 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; - } - }); - } + 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; + } + }); }); }