mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
feat: use native text segmenter where available (#2782)
This commit is contained in:
parent
0476d06515
commit
6521a487d7
@ -211,5 +211,12 @@ export const FEATURES = {
|
|||||||
const value = 'withCredentials' in new XMLHttpRequest();
|
const value = 'withCredentials' in new XMLHttpRequest();
|
||||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
|
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
|
||||||
return value;
|
return value;
|
||||||
|
},
|
||||||
|
get SUPPORT_NATIVE_TEXT_SEGMENTATION(): boolean {
|
||||||
|
'use strict';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const value = !!(typeof Intl !== 'undefined' && (Intl as any).Segmenter);
|
||||||
|
Object.defineProperty(FEATURES, 'SUPPORT_NATIVE_TEXT_SEGMENTATION', {value});
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ export class Bounds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds {
|
static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds {
|
||||||
const domRect = Array.from(domRectList).find(rect => rect.width !== 0);
|
const domRect = Array.from(domRectList).find((rect) => rect.width !== 0);
|
||||||
return domRect
|
return domRect
|
||||||
? new Bounds(
|
? new Bounds(
|
||||||
domRect.x + context.windowBounds.left,
|
domRect.x + context.windowBounds.left,
|
||||||
|
@ -28,15 +28,24 @@ export const parseTextBounds = (
|
|||||||
textList.forEach((text) => {
|
textList.forEach((text) => {
|
||||||
if (styles.textDecorationLine.length || text.trim().length > 0) {
|
if (styles.textDecorationLine.length || text.trim().length > 0) {
|
||||||
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
|
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
|
||||||
if (!FEATURES.SUPPORT_WORD_BREAKING) {
|
const clientRects = createRange(node, offset, text.length).getClientRects();
|
||||||
textBounds.push(
|
if (clientRects.length > 1) {
|
||||||
new TextBounds(
|
const subSegments = segmentGraphemes(text);
|
||||||
text,
|
let subOffset = 0;
|
||||||
Bounds.fromDOMRectList(context, createRange(node, offset, text.length).getClientRects())
|
subSegments.forEach((subSegment) => {
|
||||||
)
|
textBounds.push(
|
||||||
);
|
new TextBounds(
|
||||||
|
subSegment,
|
||||||
|
Bounds.fromDOMRectList(
|
||||||
|
context,
|
||||||
|
createRange(node, subOffset + offset, subSegment.length).getClientRects()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
subOffset += subSegment.length;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length)));
|
textBounds.push(new TextBounds(text, Bounds.fromDOMRectList(context, clientRects)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const replacementNode = node.splitText(text.length);
|
const replacementNode = node.splitText(text.length);
|
||||||
@ -82,12 +91,32 @@ const createRange = (node: Text, offset: number, length: number): Range => {
|
|||||||
return range;
|
return range;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => {
|
export const segmentGraphemes = (value: string): string[] => {
|
||||||
return Bounds.fromClientRect(context, createRange(node, offset, length).getBoundingClientRect());
|
if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const segmenter = new (Intl as any).Segmenter(void 0, {granularity: 'grapheme'});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return Array.from(segmenter.segment(value)).map((segment: any) => segment.segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return splitGraphemes(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const segmentWords = (value: string, styles: CSSParsedDeclaration): string[] => {
|
||||||
|
if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const segmenter = new (Intl as any).Segmenter(void 0, {
|
||||||
|
granularity: 'word'
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return Array.from(segmenter.segment(value)).map((segment: any) => segment.segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return breakWords(value, styles);
|
||||||
};
|
};
|
||||||
|
|
||||||
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
|
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
|
||||||
return styles.letterSpacing !== 0 ? splitGraphemes(value) : breakWords(value, styles);
|
return styles.letterSpacing !== 0 ? segmentGraphemes(value) : segmentWords(value, styles);
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://drafts.csswg.org/css-text/#word-separator
|
// https://drafts.csswg.org/css-text/#word-separator
|
||||||
|
@ -2,7 +2,7 @@ import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-
|
|||||||
import {asString, Color, isTransparent} from '../../css/types/color';
|
import {asString, Color, isTransparent} from '../../css/types/color';
|
||||||
import {ElementContainer, FLAGS} from '../../dom/element-container';
|
import {ElementContainer, FLAGS} from '../../dom/element-container';
|
||||||
import {BORDER_STYLE} from '../../css/property-descriptors/border-style';
|
import {BORDER_STYLE} from '../../css/property-descriptors/border-style';
|
||||||
import {CSSParsedDeclaration} from '../../css/index';
|
import {CSSParsedDeclaration} from '../../css';
|
||||||
import {TextContainer} from '../../dom/text-container';
|
import {TextContainer} from '../../dom/text-container';
|
||||||
import {Path, transformPath} from '../path';
|
import {Path, transformPath} from '../path';
|
||||||
import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip';
|
import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip';
|
||||||
@ -18,12 +18,12 @@ import {
|
|||||||
} from '../border';
|
} from '../border';
|
||||||
import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background';
|
import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background';
|
||||||
import {isDimensionToken} from '../../css/syntax/parser';
|
import {isDimensionToken} from '../../css/syntax/parser';
|
||||||
import {TextBounds} from '../../css/layout/text';
|
import {segmentGraphemes, TextBounds} from '../../css/layout/text';
|
||||||
import {ImageElementContainer} from '../../dom/replaced-elements/image-element-container';
|
import {ImageElementContainer} from '../../dom/replaced-elements/image-element-container';
|
||||||
import {contentBox} from '../box-sizing';
|
import {contentBox} from '../box-sizing';
|
||||||
import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-element-container';
|
import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-element-container';
|
||||||
import {SVGElementContainer} from '../../dom/replaced-elements/svg-element-container';
|
import {SVGElementContainer} from '../../dom/replaced-elements/svg-element-container';
|
||||||
import {ReplacedElementContainer} from '../../dom/replaced-elements/index';
|
import {ReplacedElementContainer} from '../../dom/replaced-elements';
|
||||||
import {EffectTarget, IElementEffect, isClipEffect, isOpacityEffect, isTransformEffect} from '../effects';
|
import {EffectTarget, IElementEffect, isClipEffect, isOpacityEffect, isTransformEffect} from '../effects';
|
||||||
import {contains} from '../../core/bitwise';
|
import {contains} from '../../core/bitwise';
|
||||||
import {calculateGradientDirection, calculateRadius, processColorStops} from '../../css/types/functions/gradient';
|
import {calculateGradientDirection, calculateRadius, processColorStops} from '../../css/types/functions/gradient';
|
||||||
@ -44,7 +44,6 @@ import {PAINT_ORDER_LAYER} from '../../css/property-descriptors/paint-order';
|
|||||||
import {Renderer} from '../renderer';
|
import {Renderer} from '../renderer';
|
||||||
import {Context} from '../../core/context';
|
import {Context} from '../../core/context';
|
||||||
import {DIRECTION} from '../../css/property-descriptors/direction';
|
import {DIRECTION} from '../../css/property-descriptors/direction';
|
||||||
import {splitGraphemes} from 'text-segmentation';
|
|
||||||
|
|
||||||
export type RenderConfigurations = RenderOptions & {
|
export type RenderConfigurations = RenderOptions & {
|
||||||
backgroundColor: Color | null;
|
backgroundColor: Color | null;
|
||||||
@ -149,7 +148,7 @@ export class CanvasRenderer extends Renderer {
|
|||||||
if (letterSpacing === 0) {
|
if (letterSpacing === 0) {
|
||||||
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
|
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
|
||||||
} else {
|
} else {
|
||||||
const letters = splitGraphemes(text.text);
|
const letters = segmentGraphemes(text.text);
|
||||||
letters.reduce((left, letter) => {
|
letters.reduce((left, letter) => {
|
||||||
this.ctx.fillText(letter, left, text.bounds.top + baseline);
|
this.ctx.fillText(letter, left, text.bounds.top + baseline);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user