Fix issue #2739 allow for multiple bounding boxes in an element.

This commit is contained in:
Chad Petersen 2022-09-06 15:17:51 -06:00
parent 6020386bbe
commit 8d5a45ad40
14 changed files with 462 additions and 347 deletions

3
package-lock.json generated
View File

@ -5,7 +5,8 @@
"requires": true,
"packages": {
"": {
"version": "1.4.0",
"name": "html2canvas",
"version": "1.4.1",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",

View File

@ -1,4 +1,4 @@
export const {Bounds} = jest.requireActual('../bounds');
export const parseBounds = (): typeof Bounds => {
return new Bounds(0, 0, 200, 50);
export const parseBounds = (): typeof Bounds[] => {
return [new Bounds(0, 0, 200, 50)];
};

View File

@ -7,17 +7,19 @@ export class Bounds {
return new Bounds(this.left + x, this.top + y, this.width + w, this.height + h);
}
static fromClientRect(context: Context, clientRect: ClientRect): Bounds {
return new Bounds(
clientRect.left + context.windowBounds.left,
clientRect.top + context.windowBounds.top,
clientRect.width,
clientRect.height
);
static fromDomRect(context: Context, domRect: DOMRect): Bounds {
return domRect.width !== 0 && domRect.height !== 0
? new Bounds(
domRect.left + context.windowBounds.left,
domRect.top + context.windowBounds.top,
domRect.width,
domRect.height
)
: Bounds.EMPTY;
}
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 && rect.height !== 0);
return domRect
? new Bounds(
domRect.left + context.windowBounds.left,
@ -30,9 +32,12 @@ export class Bounds {
static EMPTY = new Bounds(0, 0, 0, 0);
}
export const parseBound = (context: Context, node: Element): Bounds => {
return Bounds.fromDomRect(context, node.getBoundingClientRect());
};
export const parseBounds = (context: Context, node: Element): Bounds => {
return Bounds.fromClientRect(context, node.getBoundingClientRect());
export const parseBounds = (context: Context, node: Element): Bounds[] => {
return Array.from(node.getClientRects()).map((b) => Bounds.fromDomRect(context, b));
};
export const parseDocumentSize = (document: Document): Bounds => {

View File

@ -49,7 +49,9 @@ export const parseTextBounds = (
}
} else {
const replacementNode = node.splitText(text.length);
textBounds.push(new TextBounds(text, getWrapperBounds(context, node)));
getWrapperBounds(context, node).forEach((wrapperBound) => {
textBounds.push(new TextBounds(text, wrapperBound));
});
node = replacementNode;
}
} else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
@ -61,7 +63,7 @@ export const parseTextBounds = (
return textBounds;
};
const getWrapperBounds = (context: Context, node: Text): Bounds => {
const getWrapperBounds = (context: Context, node: Text): Bounds[] => {
const ownerDocument = node.ownerDocument;
if (ownerDocument) {
const wrapper = ownerDocument.createElement('html2canvaswrapper');
@ -77,7 +79,7 @@ const getWrapperBounds = (context: Context, node: Text): Bounds => {
}
}
return Bounds.EMPTY;
return [Bounds.EMPTY];
};
const createRange = (node: Text, offset: number, length: number): Range => {

View File

@ -16,7 +16,7 @@ export class ElementContainer {
readonly styles: CSSParsedDeclaration;
readonly textNodes: TextContainer[] = [];
readonly elements: ElementContainer[] = [];
bounds: Bounds;
bounds: Bounds[];
flags = 0;
constructor(protected readonly context: Context, element: Element) {

View File

@ -74,7 +74,7 @@ export class InputElementContainer extends ElementContainer {
BORDER_STYLE.SOLID;
this.styles.backgroundClip = [BACKGROUND_CLIP.BORDER_BOX];
this.styles.backgroundOrigin = [BACKGROUND_ORIGIN.BORDER_BOX];
this.bounds = reformatInputBounds(this.bounds);
this.bounds = this.bounds.map(reformatInputBounds);
}
switch (this.type) {

View File

@ -1,5 +1,5 @@
import {ElementContainer} from '../element-container';
import {parseBounds} from '../../css/layout/bounds';
import {parseBound} from '../../css/layout/bounds';
import {Context} from '../../core/context';
export class SVGElementContainer extends ElementContainer {
@ -10,7 +10,7 @@ export class SVGElementContainer extends ElementContainer {
constructor(context: Context, img: SVGSVGElement) {
super(context, img);
const s = new XMLSerializer();
const bounds = parseBounds(context, img);
const bounds = parseBound(context, img);
img.setAttribute('width', `${bounds.width}px`);
img.setAttribute('height', `${bounds.height}px`);

View File

@ -1,4 +1,4 @@
import {Bounds, parseBounds, parseDocumentSize} from './css/layout/bounds';
import {Bounds, parseBound, parseDocumentSize} from './css/layout/bounds';
import {COLORS, isTransparent, parseColor} from './css/types/color';
import {CloneConfigurations, CloneOptions, DocumentCloner, WindowOptions} from './dom/document-cloner';
import {isBodyElement, isHTMLElement, parseTree} from './dom/node-parser';
@ -98,7 +98,7 @@ const renderElement = async (element: HTMLElement, opts: Partial<Options>): Prom
const {width, height, left, top} =
isBodyElement(clonedElement) || isHTMLElement(clonedElement)
? parseDocumentSize(clonedElement.ownerDocument)
: parseBounds(context, clonedElement);
: parseBound(context, clonedElement);
const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);

View File

@ -13,7 +13,7 @@ import {BACKGROUND_CLIP} from '../css/property-descriptors/background-clip';
export const calculateBackgroundPositioningArea = (
backgroundOrigin: BACKGROUND_ORIGIN,
element: ElementContainer
): Bounds => {
): Bounds[] => {
if (backgroundOrigin === BACKGROUND_ORIGIN.BORDER_BOX) {
return element.bounds;
}
@ -25,7 +25,10 @@ export const calculateBackgroundPositioningArea = (
return paddingBox(element);
};
export const calculateBackgroundPaintingArea = (backgroundClip: BACKGROUND_CLIP, element: ElementContainer): Bounds => {
export const calculateBackgroundPaintingArea = (
backgroundClip: BACKGROUND_CLIP,
element: ElementContainer
): Bounds[] => {
if (backgroundClip === BACKGROUND_CLIP.BORDER_BOX) {
return element.bounds;
}
@ -41,43 +44,46 @@ export const calculateBackgroundRendering = (
container: ElementContainer,
index: number,
intrinsicSize: [number | null, number | null, number | null]
): [Path[], number, number, number, number] => {
const backgroundPositioningArea = calculateBackgroundPositioningArea(
): [Path[], number, number, number, number][] => {
const backgroundPositioningAreas = calculateBackgroundPositioningArea(
getBackgroundValueForIndex(container.styles.backgroundOrigin, index),
container
);
const backgroundPaintingArea = calculateBackgroundPaintingArea(
const backgroundPaintingAreas = calculateBackgroundPaintingArea(
getBackgroundValueForIndex(container.styles.backgroundClip, index),
container
);
const backgroundImageSize = calculateBackgroundSize(
const backgroundImageSizes = calculateBackgroundSize(
getBackgroundValueForIndex(container.styles.backgroundSize, index),
intrinsicSize,
backgroundPositioningArea
backgroundPositioningAreas
);
const [sizeWidth, sizeHeight] = backgroundImageSize;
return backgroundPositioningAreas.map((backgroundPositioningArea, positionIndex) => {
const backgroundImageSize = backgroundImageSizes[positionIndex];
const [sizeWidth, sizeHeight] = backgroundImageSize;
const position = getAbsoluteValueForTuple(
getBackgroundValueForIndex(container.styles.backgroundPosition, index),
backgroundPositioningArea.width - sizeWidth,
backgroundPositioningArea.height - sizeHeight
);
const position = getAbsoluteValueForTuple(
getBackgroundValueForIndex(container.styles.backgroundPosition, index),
backgroundPositioningArea.width - sizeWidth,
backgroundPositioningArea.height - sizeHeight
);
const path = calculateBackgroundRepeatPath(
getBackgroundValueForIndex(container.styles.backgroundRepeat, index),
position,
backgroundImageSize,
backgroundPositioningArea,
backgroundPaintingArea
);
const path = calculateBackgroundRepeatPath(
getBackgroundValueForIndex(container.styles.backgroundRepeat, index),
position,
backgroundImageSize,
backgroundPositioningArea,
backgroundPaintingAreas[positionIndex]
);
const offsetX = Math.round(backgroundPositioningArea.left + position[0]);
const offsetY = Math.round(backgroundPositioningArea.top + position[1]);
const offsetX = Math.round(backgroundPositioningArea.left + position[0]);
const offsetY = Math.round(backgroundPositioningArea.top + position[1]);
return [path, offsetX, offsetY, sizeWidth, sizeHeight];
return [path, offsetX, offsetY, sizeWidth, sizeHeight];
});
};
export const isAuto = (token: CSSValue): boolean => isIdentToken(token) && token.value === BACKGROUND_SIZE.AUTO;
@ -87,30 +93,34 @@ const hasIntrinsicValue = (value: number | null): value is number => typeof valu
export const calculateBackgroundSize = (
size: BackgroundSizeInfo[],
[intrinsicWidth, intrinsicHeight, intrinsicProportion]: [number | null, number | null, number | null],
bounds: Bounds
): [number, number] => {
bounds: Bounds[]
): [number, number][] => {
const [first, second] = size;
if (!first) {
return [0, 0];
return bounds.map(() => [0, 0]);
}
if (isLengthPercentage(first) && second && isLengthPercentage(second)) {
return [getAbsoluteValue(first, bounds.width), getAbsoluteValue(second, bounds.height)];
return bounds.map((bound) => {
return [getAbsoluteValue(first, bound.width), getAbsoluteValue(second, bound.height)];
});
}
const hasIntrinsicProportion = hasIntrinsicValue(intrinsicProportion);
if (isIdentToken(first) && (first.value === BACKGROUND_SIZE.CONTAIN || first.value === BACKGROUND_SIZE.COVER)) {
if (hasIntrinsicValue(intrinsicProportion)) {
const targetRatio = bounds.width / bounds.height;
return bounds.map((bound) => {
const targetRatio = bound.width / bound.height;
return targetRatio < intrinsicProportion !== (first.value === BACKGROUND_SIZE.COVER)
? [bounds.width, bounds.width / intrinsicProportion]
: [bounds.height * intrinsicProportion, bounds.height];
return targetRatio < intrinsicProportion !== (first.value === BACKGROUND_SIZE.COVER)
? [bound.width, bound.width / intrinsicProportion]
: [bound.height * intrinsicProportion, bound.height];
});
}
return [bounds.width, bounds.height];
return bounds.map((bound) => [bound.width, bound.height]);
}
const hasIntrinsicWidth = hasIntrinsicValue(intrinsicWidth);
@ -121,14 +131,14 @@ export const calculateBackgroundSize = (
if (isAuto(first) && (!second || isAuto(second))) {
// If the image has both horizontal and vertical intrinsic dimensions, it's rendered at that size.
if (hasIntrinsicWidth && hasIntrinsicHeight) {
return [intrinsicWidth as number, intrinsicHeight as number];
return bounds.map(() => [intrinsicWidth as number, intrinsicHeight as number]);
}
// If the image has no intrinsic dimensions and has no intrinsic proportions,
// it's rendered at the size of the background positioning area.
if (!hasIntrinsicProportion && !hasIntrinsicDimensions) {
return [bounds.width, bounds.height];
return bounds.map((bound) => [bound.width, bound.height]);
}
// TODO If the image has no intrinsic dimensions but has intrinsic proportions, it's rendered as if contain had been specified instead.
@ -142,69 +152,73 @@ export const calculateBackgroundSize = (
const height = hasIntrinsicHeight
? (intrinsicHeight as number)
: (intrinsicWidth as number) / (intrinsicProportion as number);
return [width, height];
return bounds.map(() => [width, height]);
}
// If the image has only one intrinsic dimension but has no intrinsic proportions,
// it's rendered using the specified dimension and the other dimension of the background positioning area.
const width = hasIntrinsicWidth ? (intrinsicWidth as number) : bounds.width;
const height = hasIntrinsicHeight ? (intrinsicHeight as number) : bounds.height;
return [width, height];
return bounds.map((bound) => {
const width = hasIntrinsicWidth ? (intrinsicWidth as number) : bound.width;
const height = hasIntrinsicHeight ? (intrinsicHeight as number) : bound.height;
return [width, height];
});
}
// If the image has intrinsic proportions, it's stretched to the specified dimension.
// The unspecified dimension is computed using the specified dimension and the intrinsic proportions.
if (hasIntrinsicProportion) {
let width = 0;
let height = 0;
if (isLengthPercentage(first)) {
width = getAbsoluteValue(first, bounds.width);
} else if (isLengthPercentage(second)) {
height = getAbsoluteValue(second, bounds.height);
}
return bounds.map((bound) => {
let width = 0;
let height = 0;
if (isLengthPercentage(first)) {
width = getAbsoluteValue(first, bound.width);
} else if (isLengthPercentage(second)) {
height = getAbsoluteValue(second, bound.height);
}
if (isAuto(first)) {
width = height * (intrinsicProportion as number);
} else if (!second || isAuto(second)) {
height = width / (intrinsicProportion as number);
}
if (isAuto(first)) {
width = height * (intrinsicProportion as number);
} else if (!second || isAuto(second)) {
height = width / (intrinsicProportion as number);
}
return [width, height];
return [width, height];
});
}
// If the image has no intrinsic proportions, it's stretched to the specified dimension.
// The unspecified dimension is computed using the image's corresponding intrinsic dimension,
// if there is one. If there is no such intrinsic dimension,
// it becomes the corresponding dimension of the background positioning area.
return bounds.map((bound) => {
let width = null;
let height = null;
let width = null;
let height = null;
if (isLengthPercentage(first)) {
width = getAbsoluteValue(first, bound.width);
} else if (second && isLengthPercentage(second)) {
height = getAbsoluteValue(second, bound.height);
}
if (isLengthPercentage(first)) {
width = getAbsoluteValue(first, bounds.width);
} else if (second && isLengthPercentage(second)) {
height = getAbsoluteValue(second, bounds.height);
}
if (width !== null && (!second || isAuto(second))) {
height =
hasIntrinsicWidth && hasIntrinsicHeight
? (width / (intrinsicWidth as number)) * (intrinsicHeight as number)
: bound.height;
}
if (width !== null && (!second || isAuto(second))) {
height =
hasIntrinsicWidth && hasIntrinsicHeight
? (width / (intrinsicWidth as number)) * (intrinsicHeight as number)
: bounds.height;
}
if (height !== null && isAuto(first)) {
width =
hasIntrinsicWidth && hasIntrinsicHeight
? (height / (intrinsicHeight as number)) * (intrinsicWidth as number)
: bound.width;
}
if (height !== null && isAuto(first)) {
width =
hasIntrinsicWidth && hasIntrinsicHeight
? (height / (intrinsicHeight as number)) * (intrinsicWidth as number)
: bounds.width;
}
if (width !== null && height !== null) {
if (width === null || height === null) {
throw new Error(`Unable to calculate background-size for element`);
}
return [width, height];
}
throw new Error(`Unable to calculate background-size for element`);
});
};
export const getBackgroundValueForIndex = <T>(values: T[], index: number): T => {

View File

@ -2,30 +2,36 @@ import {Path} from './path';
import {BoundCurves} from './bound-curves';
import {isBezierCurve} from './bezier-curve';
export const parsePathForBorder = (curves: BoundCurves, borderSide: number): Path[] => {
export enum BORDER_SIDE {
TOP = 0,
RIGHT = 1,
BOTTOM = 2,
LEFT = 3
}
export const parsePathForBorder = (curves: BoundCurves, borderSide: BORDER_SIDE): Path[] => {
switch (borderSide) {
case 0:
case BORDER_SIDE.TOP:
return createPathFromCurves(
curves.topLeftBorderBox,
curves.topLeftPaddingBox,
curves.topRightBorderBox,
curves.topRightPaddingBox
);
case 1:
case BORDER_SIDE.RIGHT:
return createPathFromCurves(
curves.topRightBorderBox,
curves.topRightPaddingBox,
curves.bottomRightBorderBox,
curves.bottomRightPaddingBox
);
case 2:
case BORDER_SIDE.BOTTOM:
return createPathFromCurves(
curves.bottomRightBorderBox,
curves.bottomRightPaddingBox,
curves.bottomLeftBorderBox,
curves.bottomLeftPaddingBox
);
case 3:
case BORDER_SIDE.LEFT:
default:
return createPathFromCurves(
curves.bottomLeftBorderBox,
@ -36,30 +42,30 @@ export const parsePathForBorder = (curves: BoundCurves, borderSide: number): Pat
}
};
export const parsePathForBorderDoubleOuter = (curves: BoundCurves, borderSide: number): Path[] => {
export const parsePathForBorderDoubleOuter = (curves: BoundCurves, borderSide: BORDER_SIDE): Path[] => {
switch (borderSide) {
case 0:
case BORDER_SIDE.TOP:
return createPathFromCurves(
curves.topLeftBorderBox,
curves.topLeftBorderDoubleOuterBox,
curves.topRightBorderBox,
curves.topRightBorderDoubleOuterBox
);
case 1:
case BORDER_SIDE.RIGHT:
return createPathFromCurves(
curves.topRightBorderBox,
curves.topRightBorderDoubleOuterBox,
curves.bottomRightBorderBox,
curves.bottomRightBorderDoubleOuterBox
);
case 2:
case BORDER_SIDE.BOTTOM:
return createPathFromCurves(
curves.bottomRightBorderBox,
curves.bottomRightBorderDoubleOuterBox,
curves.bottomLeftBorderBox,
curves.bottomLeftBorderDoubleOuterBox
);
case 3:
case BORDER_SIDE.LEFT:
default:
return createPathFromCurves(
curves.bottomLeftBorderBox,
@ -70,30 +76,30 @@ export const parsePathForBorderDoubleOuter = (curves: BoundCurves, borderSide: n
}
};
export const parsePathForBorderDoubleInner = (curves: BoundCurves, borderSide: number): Path[] => {
export const parsePathForBorderDoubleInner = (curves: BoundCurves, borderSide: BORDER_SIDE): Path[] => {
switch (borderSide) {
case 0:
case BORDER_SIDE.TOP:
return createPathFromCurves(
curves.topLeftBorderDoubleInnerBox,
curves.topLeftPaddingBox,
curves.topRightBorderDoubleInnerBox,
curves.topRightPaddingBox
);
case 1:
case BORDER_SIDE.RIGHT:
return createPathFromCurves(
curves.topRightBorderDoubleInnerBox,
curves.topRightPaddingBox,
curves.bottomRightBorderDoubleInnerBox,
curves.bottomRightPaddingBox
);
case 2:
case BORDER_SIDE.BOTTOM:
return createPathFromCurves(
curves.bottomRightBorderDoubleInnerBox,
curves.bottomRightPaddingBox,
curves.bottomLeftBorderDoubleInnerBox,
curves.bottomLeftPaddingBox
);
case 3:
case BORDER_SIDE.LEFT:
default:
return createPathFromCurves(
curves.bottomLeftBorderDoubleInnerBox,
@ -104,15 +110,15 @@ export const parsePathForBorderDoubleInner = (curves: BoundCurves, borderSide: n
}
};
export const parsePathForBorderStroke = (curves: BoundCurves, borderSide: number): Path[] => {
export const parsePathForBorderStroke = (curves: BoundCurves, borderSide: BORDER_SIDE): Path[] => {
switch (borderSide) {
case 0:
case BORDER_SIDE.TOP:
return createStrokePathFromCurves(curves.topLeftBorderStroke, curves.topRightBorderStroke);
case 1:
case BORDER_SIDE.RIGHT:
return createStrokePathFromCurves(curves.topRightBorderStroke, curves.bottomRightBorderStroke);
case 2:
case BORDER_SIDE.BOTTOM:
return createStrokePathFromCurves(curves.bottomRightBorderStroke, curves.bottomLeftBorderStroke);
case 3:
case BORDER_SIDE.LEFT:
default:
return createStrokePathFromCurves(curves.bottomLeftBorderStroke, curves.topLeftBorderStroke);
}

View File

@ -1,8 +1,11 @@
import {ElementContainer} from '../dom/element-container';
import {getAbsoluteValue, getAbsoluteValueForTuple} from '../css/types/length-percentage';
import {getAbsoluteValue, getAbsoluteValueForTuple, parseLengthPercentageTuple} from '../css/types/length-percentage';
import {Vector} from './vector';
import {BezierCurve} from './bezier-curve';
import {Path} from './path';
import {Bounds} from '../css/layout/bounds';
import {CSSParsedDeclaration} from '../css';
import {TokenType} from '../css/syntax/tokenizer';
export class BoundCurves {
readonly topLeftBorderDoubleOuterBox: Path;
@ -29,15 +32,53 @@ export class BoundCurves {
readonly topRightContentBox: Path;
readonly bottomRightContentBox: Path;
readonly bottomLeftContentBox: Path;
readonly isFirstBoundOfElement: boolean;
readonly isLastBoundOfElement: boolean;
constructor(element: ElementContainer) {
const styles = element.styles;
const bounds = element.bounds;
static fromElementContainer(element: ElementContainer): BoundCurves[] {
const lastIndex = element.bounds.length - 1;
return element.bounds.map((bound, index) => {
return new BoundCurves(bound, element.styles, index === 0, index === lastIndex);
});
}
let [tlh, tlv] = getAbsoluteValueForTuple(styles.borderTopLeftRadius, bounds.width, bounds.height);
let [trh, trv] = getAbsoluteValueForTuple(styles.borderTopRightRadius, bounds.width, bounds.height);
let [brh, brv] = getAbsoluteValueForTuple(styles.borderBottomRightRadius, bounds.width, bounds.height);
let [blh, blv] = getAbsoluteValueForTuple(styles.borderBottomLeftRadius, bounds.width, bounds.height);
private static NoRadiusPercentage = parseLengthPercentageTuple([
{
type: TokenType.DIMENSION_TOKEN,
flags: 0,
unit: 'px',
number: 0
}
]);
constructor(
bounds: Bounds,
styles: CSSParsedDeclaration,
isFirstBoundOfElement: boolean,
isLastBoundOfElement: boolean
) {
this.isFirstBoundOfElement = isFirstBoundOfElement;
this.isLastBoundOfElement = isLastBoundOfElement;
let [tlh, tlv] = getAbsoluteValueForTuple(
this.isFirstBoundOfElement ? styles.borderTopLeftRadius : BoundCurves.NoRadiusPercentage,
bounds.width,
bounds.height
);
let [trh, trv] = getAbsoluteValueForTuple(
this.isLastBoundOfElement ? styles.borderTopRightRadius : BoundCurves.NoRadiusPercentage,
bounds.width,
bounds.height
);
let [brh, brv] = getAbsoluteValueForTuple(
this.isLastBoundOfElement ? styles.borderBottomRightRadius : BoundCurves.NoRadiusPercentage,
bounds.width,
bounds.height
);
let [blh, blv] = getAbsoluteValueForTuple(
this.isFirstBoundOfElement ? styles.borderBottomLeftRadius : BoundCurves.NoRadiusPercentage,
bounds.width,
bounds.height
);
const factors = [];
factors.push((tlh + trh) / bounds.width);
@ -67,10 +108,10 @@ export class BoundCurves {
const borderBottomWidth = styles.borderBottomWidth;
const borderLeftWidth = styles.borderLeftWidth;
const paddingTop = getAbsoluteValue(styles.paddingTop, element.bounds.width);
const paddingRight = getAbsoluteValue(styles.paddingRight, element.bounds.width);
const paddingBottom = getAbsoluteValue(styles.paddingBottom, element.bounds.width);
const paddingLeft = getAbsoluteValue(styles.paddingLeft, element.bounds.width);
const paddingTop = getAbsoluteValue(styles.paddingTop, bounds.width);
const paddingRight = getAbsoluteValue(styles.paddingRight, bounds.width);
const paddingBottom = getAbsoluteValue(styles.paddingBottom, bounds.width);
const paddingLeft = getAbsoluteValue(styles.paddingLeft, bounds.width);
this.topLeftBorderDoubleOuterBox =
tlh > 0 || tlv > 0

View File

@ -2,30 +2,30 @@ import {getAbsoluteValue} from '../css/types/length-percentage';
import {Bounds} from '../css/layout/bounds';
import {ElementContainer} from '../dom/element-container';
export const paddingBox = (element: ElementContainer): Bounds => {
const bounds = element.bounds;
export const paddingBox = (element: ElementContainer): Bounds[] => {
const styles = element.styles;
return bounds.add(
styles.borderLeftWidth,
styles.borderTopWidth,
-(styles.borderRightWidth + styles.borderLeftWidth),
-(styles.borderTopWidth + styles.borderBottomWidth)
);
return element.bounds.map((bound) => {
return bound.add(
styles.borderLeftWidth,
styles.borderTopWidth,
-(styles.borderRightWidth + styles.borderLeftWidth),
-(styles.borderTopWidth + styles.borderBottomWidth)
);
});
};
export const contentBox = (element: ElementContainer): Bounds => {
export const contentBox = (element: ElementContainer): Bounds[] => {
const styles = element.styles;
const bounds = element.bounds;
const paddingLeft = getAbsoluteValue(styles.paddingLeft, bounds.width);
const paddingRight = getAbsoluteValue(styles.paddingRight, bounds.width);
const paddingTop = getAbsoluteValue(styles.paddingTop, bounds.width);
const paddingBottom = getAbsoluteValue(styles.paddingBottom, bounds.width);
return bounds.add(
paddingLeft + styles.borderLeftWidth,
paddingTop + styles.borderTopWidth,
-(styles.borderRightWidth + styles.borderLeftWidth + paddingLeft + paddingRight),
-(styles.borderTopWidth + styles.borderBottomWidth + paddingTop + paddingBottom)
);
return element.bounds.map((bound) => {
const paddingLeft = getAbsoluteValue(styles.paddingLeft, bound.width);
const paddingRight = getAbsoluteValue(styles.paddingRight, bound.width);
const paddingTop = getAbsoluteValue(styles.paddingTop, bound.width);
const paddingBottom = getAbsoluteValue(styles.paddingBottom, bound.width);
return bound.add(
paddingLeft + styles.borderLeftWidth,
paddingTop + styles.borderTopWidth,
-(styles.borderRightWidth + styles.borderLeftWidth + paddingLeft + paddingRight),
-(styles.borderTopWidth + styles.borderBottomWidth + paddingTop + paddingBottom)
);
});
};

View File

@ -14,7 +14,8 @@ import {
parsePathForBorder,
parsePathForBorderDoubleInner,
parsePathForBorderDoubleOuter,
parsePathForBorderStroke
parsePathForBorderStroke,
BORDER_SIDE
} from '../border';
import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background';
import {isDimensionToken} from '../../css/syntax/parser';
@ -267,12 +268,19 @@ export class CanvasRenderer extends Renderer {
renderReplacedElement(
container: ReplacedElementContainer,
curves: BoundCurves,
curves: BoundCurves[],
image: HTMLImageElement | HTMLCanvasElement
): void {
if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) {
const box = contentBox(container);
const path = calculatePaddingBoxPath(curves);
const boxes = contentBox(container);
if (boxes.length !== 1) {
throw new Error(`Expecting a single bounding box but got ${boxes.length} for image replacement.`);
}
if (curves.length !== 1) {
throw new Error(`Expecting a single bounding box but got ${boxes.length} for image replacement.`);
}
const box = boxes[0];
const path = calculatePaddingBoxPath(curves[0]);
this.path(path);
this.ctx.save();
this.ctx.clip();
@ -334,55 +342,56 @@ export class CanvasRenderer extends Renderer {
const canvas = await iframeRenderer.render(container.tree);
if (container.width && container.height) {
const bound = container.bounds[0];
this.ctx.drawImage(
canvas,
0,
0,
container.width,
container.height,
container.bounds.left,
container.bounds.top,
container.bounds.width,
container.bounds.height
bound.left,
bound.top,
bound.width,
bound.height
);
}
}
if (container instanceof InputElementContainer) {
const size = Math.min(container.bounds.width, container.bounds.height);
//Should use flat map if target is updated.
const size = Math.min(
...container.bounds.map((b) => b.width).concat(container.bounds.map((b) => b.height))
);
if (container.type === CHECKBOX) {
if (container.checked) {
this.ctx.save();
this.path([
new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79),
new Vector(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549),
new Vector(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071),
new Vector(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649),
new Vector(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23),
new Vector(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085),
new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79)
]);
container.bounds.forEach((bound) => {
this.ctx.save();
this.path([
new Vector(bound.left + size * 0.39363, bound.top + size * 0.79),
new Vector(bound.left + size * 0.16, bound.top + size * 0.5549),
new Vector(bound.left + size * 0.27347, bound.top + size * 0.44071),
new Vector(bound.left + size * 0.39694, bound.top + size * 0.5649),
new Vector(bound.left + size * 0.72983, bound.top + size * 0.23),
new Vector(bound.left + size * 0.84, bound.top + size * 0.34085),
new Vector(bound.left + size * 0.39363, bound.top + size * 0.79)
]);
this.ctx.fillStyle = asString(INPUT_COLOR);
this.ctx.fill();
this.ctx.restore();
this.ctx.fillStyle = asString(INPUT_COLOR);
this.ctx.fill();
this.ctx.restore();
});
}
} else if (container.type === RADIO) {
if (container.checked) {
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(
container.bounds.left + size / 2,
container.bounds.top + size / 2,
size / 4,
0,
Math.PI * 2,
true
);
this.ctx.fillStyle = asString(INPUT_COLOR);
this.ctx.fill();
this.ctx.restore();
container.bounds.forEach((bound) => {
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(bound.left + size / 2, bound.top + size / 2, size / 4, 0, Math.PI * 2, true);
this.ctx.fillStyle = asString(INPUT_COLOR);
this.ctx.fill();
this.ctx.restore();
});
}
}
}
@ -397,7 +406,7 @@ export class CanvasRenderer extends Renderer {
this.ctx.textBaseline = 'alphabetic';
this.ctx.textAlign = canvasTextAlign(container.styles.textAlign);
const bounds = contentBox(container);
const bounds = contentBox(container)[0];
let x = 0;
@ -439,7 +448,11 @@ export class CanvasRenderer extends Renderer {
const url = (img as CSSURLImage).url;
try {
image = await this.context.cache.match(url);
this.ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
this.ctx.drawImage(
image,
Math.min(...container.bounds.map((b) => b.left)) - (image.width + 10),
Math.min(...container.bounds.map((b) => b.top))
);
} catch (e) {
this.context.logger.error(`Error loading list-style-image ${url}`);
}
@ -452,10 +465,12 @@ export class CanvasRenderer extends Renderer {
this.ctx.textBaseline = 'middle';
this.ctx.textAlign = 'right';
const width = Math.max(...container.bounds.map((b) => b.width));
const bounds = new Bounds(
container.bounds.left,
container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width),
container.bounds.width,
Math.min(...container.bounds.map((b) => b.left)),
Math.min(...container.bounds.map((b) => b.top)) +
getAbsoluteValue(container.styles.paddingTop, width),
width,
computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1
);
@ -586,7 +601,7 @@ export class CanvasRenderer extends Renderer {
let index = container.styles.backgroundImage.length - 1;
for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) {
if (backgroundImage.type === CSSImageType.URL) {
let image;
let image: HTMLImageElement | null = null;
const url = (backgroundImage as CSSURLImage).url;
try {
image = await this.context.cache.match(url);
@ -595,75 +610,91 @@ export class CanvasRenderer extends Renderer {
}
if (image) {
const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [
const areas = calculateBackgroundRendering(container, index, [
image.width,
image.height,
image.width / image.height
]);
const pattern = this.ctx.createPattern(
this.resizeImage(image, width, height),
'repeat'
) as CanvasPattern;
this.renderRepeat(path, pattern, x, y);
areas.forEach((area) => {
const [path, x, y, width, height] = area;
const pattern = this.ctx.createPattern(
this.resizeImage(image!, width, height),
'repeat'
) as CanvasPattern;
this.renderRepeat(path, pattern, x, y);
});
}
} else if (isLinearGradient(backgroundImage)) {
const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [null, null, null]);
const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
processColorStops(backgroundImage.stops, lineLength).forEach((colorStop) =>
gradient.addColorStop(colorStop.stop, asString(colorStop.color))
);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
if (width > 0 && height > 0) {
const pattern = this.ctx.createPattern(canvas, 'repeat') as CanvasPattern;
this.renderRepeat(path, pattern, x, y);
}
} else if (isRadialGradient(backgroundImage)) {
const [path, left, top, width, height] = calculateBackgroundRendering(container, index, [
null,
null,
null
]);
const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
const x = getAbsoluteValue(position[0], width);
const y = getAbsoluteValue(position[position.length - 1], height);
const [rx, ry] = calculateRadius(backgroundImage, x, y, width, height);
if (rx > 0 && ry > 0) {
const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) =>
radialGradient.addColorStop(colorStop.stop, asString(colorStop.color))
const areas = calculateBackgroundRendering(container, index, [null, null, null]);
areas.forEach((area) => {
const [path, x, y, width, height] = area;
const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(
backgroundImage.angle,
width,
height
);
this.path(path);
this.ctx.fillStyle = radialGradient;
if (rx !== ry) {
// transforms for elliptical radial gradient
const midX = container.bounds.left + 0.5 * container.bounds.width;
const midY = container.bounds.top + 0.5 * container.bounds.height;
const f = ry / rx;
const invF = 1 / f;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
this.ctx.save();
this.ctx.translate(midX, midY);
this.ctx.transform(1, 0, 0, f, 0, 0);
this.ctx.translate(-midX, -midY);
processColorStops(backgroundImage.stops, lineLength).forEach((colorStop) =>
gradient.addColorStop(colorStop.stop, asString(colorStop.color))
);
this.ctx.fillRect(left, invF * (top - midY) + midY, width, height * invF);
this.ctx.restore();
} else {
this.ctx.fill();
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
if (width > 0 && height > 0) {
const pattern = this.ctx.createPattern(canvas, 'repeat') as CanvasPattern;
this.renderRepeat(path, pattern, x, y);
}
}
});
} else if (isRadialGradient(backgroundImage)) {
const areas = calculateBackgroundRendering(container, index, [null, null, null]);
const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
areas.forEach((area, areaindex) => {
const [path, left, top, width, height] = area;
const x = getAbsoluteValue(position[0], width);
const y = getAbsoluteValue(position[position.length - 1], height);
const [rx, ry] = calculateRadius(backgroundImage, x, y, width, height);
if (rx > 0 && ry > 0) {
const radialGradient = this.ctx.createRadialGradient(
left + x,
top + y,
0,
left + x,
top + y,
rx
);
processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) =>
radialGradient.addColorStop(colorStop.stop, asString(colorStop.color))
);
this.path(path);
this.ctx.fillStyle = radialGradient;
if (rx !== ry) {
// transforms for elliptical radial gradient
const midX = container.bounds[areaindex].left + 0.5 * container.bounds[areaindex].width;
const midY = container.bounds[areaindex].top + 0.5 * container.bounds[areaindex].height;
const f = ry / rx;
const invF = 1 / f;
this.ctx.save();
this.ctx.translate(midX, midY);
this.ctx.transform(1, 0, 0, f, 0, 0);
this.ctx.translate(-midX, -midY);
this.ctx.fillRect(left, invF * (top - midY) + midY, width, height * invF);
this.ctx.restore();
} else {
this.ctx.fill();
}
}
});
}
index--;
}
@ -701,88 +732,95 @@ export class CanvasRenderer extends Renderer {
{style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth},
{style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth}
];
for (const curve of paint.curves) {
const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(
getBackgroundValueForIndex(styles.backgroundClip, 0),
curve
);
const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(
getBackgroundValueForIndex(styles.backgroundClip, 0),
paint.curves
);
if (hasBackground || styles.boxShadow.length) {
this.ctx.save();
this.path(backgroundPaintingArea);
this.ctx.clip();
if (!isTransparent(styles.backgroundColor)) {
this.ctx.fillStyle = asString(styles.backgroundColor);
this.ctx.fill();
}
await this.renderBackgroundImage(paint.container);
this.ctx.restore();
styles.boxShadow
.slice(0)
.reverse()
.forEach((shadow) => {
this.ctx.save();
const borderBoxArea = calculateBorderBoxPath(paint.curves);
const maskOffset = shadow.inset ? 0 : MASK_OFFSET;
const shadowPaintingArea = transformPath(
borderBoxArea,
-maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number,
(shadow.inset ? 1 : -1) * shadow.spread.number,
shadow.spread.number * (shadow.inset ? -2 : 2),
shadow.spread.number * (shadow.inset ? -2 : 2)
);
if (shadow.inset) {
this.path(borderBoxArea);
this.ctx.clip();
this.mask(shadowPaintingArea);
} else {
this.mask(borderBoxArea);
this.ctx.clip();
this.path(shadowPaintingArea);
}
this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
this.ctx.shadowOffsetY = shadow.offsetY.number;
this.ctx.shadowColor = asString(shadow.color);
this.ctx.shadowBlur = shadow.blur.number;
this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)';
if (hasBackground || styles.boxShadow.length) {
this.ctx.save();
this.path(backgroundPaintingArea);
this.ctx.clip();
if (!isTransparent(styles.backgroundColor)) {
this.ctx.fillStyle = asString(styles.backgroundColor);
this.ctx.fill();
this.ctx.restore();
});
}
let side = 0;
for (const border of borders) {
if (border.style !== BORDER_STYLE.NONE && !isTransparent(border.color) && border.width > 0) {
if (border.style === BORDER_STYLE.DASHED) {
await this.renderDashedDottedBorder(
border.color,
border.width,
side,
paint.curves,
BORDER_STYLE.DASHED
);
} else if (border.style === BORDER_STYLE.DOTTED) {
await this.renderDashedDottedBorder(
border.color,
border.width,
side,
paint.curves,
BORDER_STYLE.DOTTED
);
} else if (border.style === BORDER_STYLE.DOUBLE) {
await this.renderDoubleBorder(border.color, border.width, side, paint.curves);
} else {
await this.renderSolidBorder(border.color, side, paint.curves);
}
await this.renderBackgroundImage(paint.container);
this.ctx.restore();
styles.boxShadow
.slice(0)
.reverse()
.forEach((shadow) => {
this.ctx.save();
const borderBoxArea = calculateBorderBoxPath(curve);
const maskOffset = shadow.inset ? 0 : MASK_OFFSET;
const shadowPaintingArea = transformPath(
borderBoxArea,
-maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number,
(shadow.inset ? 1 : -1) * shadow.spread.number,
shadow.spread.number * (shadow.inset ? -2 : 2),
shadow.spread.number * (shadow.inset ? -2 : 2)
);
if (shadow.inset) {
this.path(borderBoxArea);
this.ctx.clip();
this.mask(shadowPaintingArea);
} else {
this.mask(borderBoxArea);
this.ctx.clip();
this.path(shadowPaintingArea);
}
this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
this.ctx.shadowOffsetY = shadow.offsetY.number;
this.ctx.shadowColor = asString(shadow.color);
this.ctx.shadowBlur = shadow.blur.number;
this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)';
this.ctx.fill();
this.ctx.restore();
});
}
let side = 0;
for (const border of borders) {
if (
border.style !== BORDER_STYLE.NONE &&
!isTransparent(border.color) &&
border.width > 0 &&
(curve.isFirstBoundOfElement || side !== BORDER_SIDE.LEFT) &&
(curve.isLastBoundOfElement || side !== BORDER_SIDE.RIGHT)
) {
if (border.style === BORDER_STYLE.DASHED) {
await this.renderDashedDottedBorder(
border.color,
border.width,
side,
curve,
BORDER_STYLE.DASHED
);
} else if (border.style === BORDER_STYLE.DOTTED) {
await this.renderDashedDottedBorder(
border.color,
border.width,
side,
curve,
BORDER_STYLE.DOTTED
);
} else if (border.style === BORDER_STYLE.DOUBLE) {
await this.renderDoubleBorder(border.color, border.width, side, curve);
} else {
await this.renderSolidBorder(border.color, side, curve);
}
}
side++;
}
side++;
}
}

View File

@ -34,32 +34,38 @@ export class StackingContext {
export class ElementPaint {
readonly effects: IElementEffect[] = [];
readonly curves: BoundCurves;
readonly curves: BoundCurves[];
listValue?: string;
constructor(readonly container: ElementContainer, readonly parent: ElementPaint | null) {
this.curves = new BoundCurves(this.container);
this.curves = BoundCurves.fromElementContainer(this.container);
if (this.container.styles.opacity < 1) {
this.effects.push(new OpacityEffect(this.container.styles.opacity));
}
if (this.container.styles.transform !== null) {
const offsetX = this.container.bounds.left + this.container.styles.transformOrigin[0].number;
const offsetY = this.container.bounds.top + this.container.styles.transformOrigin[1].number;
const matrix = this.container.styles.transform;
const matrix = this.container.styles.transform,
originZero = this.container.styles.transformOrigin[0].number,
originOne = this.container.styles.transformOrigin[1].number,
offsetX = Math.min(...this.container.bounds.map((b) => b.left)) + originZero,
offsetY = Math.min(...this.container.bounds.map((b) => b.top)) + originOne;
this.effects.push(new TransformEffect(offsetX, offsetY, matrix));
}
if (this.container.styles.overflowX !== OVERFLOW.VISIBLE) {
const borderBox = calculateBorderBoxPath(this.curves);
const paddingBox = calculatePaddingBoxPath(this.curves);
this.curves.forEach((curve) => {
const borderBox = calculateBorderBoxPath(curve);
const paddingBox = calculatePaddingBoxPath(curve);
if (equalPath(borderBox, paddingBox)) {
this.effects.push(new ClipEffect(borderBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT));
} else {
this.effects.push(new ClipEffect(borderBox, EffectTarget.BACKGROUND_BORDERS));
this.effects.push(new ClipEffect(paddingBox, EffectTarget.CONTENT));
}
if (equalPath(borderBox, paddingBox)) {
this.effects.push(
new ClipEffect(borderBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT)
);
} else {
this.effects.push(new ClipEffect(borderBox, EffectTarget.BACKGROUND_BORDERS));
this.effects.push(new ClipEffect(paddingBox, EffectTarget.CONTENT));
}
});
}
}
@ -73,13 +79,15 @@ export class ElementPaint {
effects.unshift(...croplessEffects);
inFlow = [POSITION.ABSOLUTE, POSITION.FIXED].indexOf(parent.container.styles.position) === -1;
if (parent.container.styles.overflowX !== OVERFLOW.VISIBLE) {
const borderBox = calculateBorderBoxPath(parent.curves);
const paddingBox = calculatePaddingBoxPath(parent.curves);
if (!equalPath(borderBox, paddingBox)) {
effects.unshift(
new ClipEffect(paddingBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT)
);
}
this.curves.forEach((curve) => {
const borderBox = calculateBorderBoxPath(curve);
const paddingBox = calculatePaddingBoxPath(curve);
if (!equalPath(borderBox, paddingBox)) {
effects.unshift(
new ClipEffect(paddingBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT)
);
}
});
}
} else {
effects.unshift(...croplessEffects);