html2canvas-pro 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html2canvas-pro.esm.js +21 -7
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +21 -7
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +3 -3
- package/dist/lib/core/cache-storage.js +2 -2
- package/dist/lib/core/features.js +2 -2
- package/dist/lib/render/canvas/background-renderer.js +6 -0
- package/dist/lib/render/canvas/canvas-renderer.js +5 -1
- package/dist/lib/render/canvas/foreignobject-renderer.js +5 -1
- package/package.json +3 -11
- package/dist/lib/invariant.js +0 -9
- package/dist/types/invariant.d.ts +0 -1
- package/src/__tests__/index.ts +0 -99
- package/src/config.ts +0 -107
- package/src/core/__mocks__/cache-storage.ts +0 -1
- package/src/core/__mocks__/context.ts +0 -19
- package/src/core/__mocks__/features.ts +0 -8
- package/src/core/__mocks__/logger.ts +0 -17
- package/src/core/__tests__/cache-storage.test.ts +0 -205
- package/src/core/__tests__/cache-storage.ts +0 -278
- package/src/core/__tests__/logger.ts +0 -29
- package/src/core/__tests__/validator.ts +0 -359
- package/src/core/bitwise.ts +0 -1
- package/src/core/cache-storage.ts +0 -315
- package/src/core/context.ts +0 -31
- package/src/core/debugger.ts +0 -32
- package/src/core/features.ts +0 -222
- package/src/core/logger.ts +0 -64
- package/src/core/origin-checker.ts +0 -57
- package/src/core/performance-monitor.ts +0 -241
- package/src/core/render-element.ts +0 -272
- package/src/core/util.ts +0 -1
- package/src/core/validator.ts +0 -593
- package/src/css/index.ts +0 -427
- package/src/css/layout/__mocks__/bounds.ts +0 -6
- package/src/css/layout/bounds.ts +0 -79
- package/src/css/layout/text.ts +0 -161
- package/src/css/property-descriptor.ts +0 -49
- package/src/css/property-descriptors/__tests__/background-tests.ts +0 -65
- package/src/css/property-descriptors/__tests__/clip-path.test.ts +0 -280
- package/src/css/property-descriptors/__tests__/font-family.ts +0 -25
- package/src/css/property-descriptors/__tests__/image-rendering-integration.test.ts +0 -153
- package/src/css/property-descriptors/__tests__/image-rendering-performance.test.ts +0 -175
- package/src/css/property-descriptors/__tests__/image-rendering.test.ts +0 -72
- package/src/css/property-descriptors/__tests__/paint-order.ts +0 -87
- package/src/css/property-descriptors/__tests__/text-shadow.ts +0 -94
- package/src/css/property-descriptors/__tests__/transform-tests.ts +0 -18
- package/src/css/property-descriptors/background-clip.ts +0 -30
- package/src/css/property-descriptors/background-color.ts +0 -9
- package/src/css/property-descriptors/background-image.ts +0 -27
- package/src/css/property-descriptors/background-origin.ts +0 -31
- package/src/css/property-descriptors/background-position.ts +0 -38
- package/src/css/property-descriptors/background-repeat.ts +0 -44
- package/src/css/property-descriptors/background-size.ts +0 -27
- package/src/css/property-descriptors/border-color.ts +0 -13
- package/src/css/property-descriptors/border-radius.ts +0 -19
- package/src/css/property-descriptors/border-style.ts +0 -34
- package/src/css/property-descriptors/border-width.ts +0 -20
- package/src/css/property-descriptors/box-shadow.ts +0 -60
- package/src/css/property-descriptors/clip-path.ts +0 -271
- package/src/css/property-descriptors/color.ts +0 -9
- package/src/css/property-descriptors/content.ts +0 -26
- package/src/css/property-descriptors/counter-increment.ts +0 -43
- package/src/css/property-descriptors/counter-reset.ts +0 -36
- package/src/css/property-descriptors/direction.ts +0 -23
- package/src/css/property-descriptors/display.ts +0 -117
- package/src/css/property-descriptors/duration.ts +0 -14
- package/src/css/property-descriptors/float.ts +0 -29
- package/src/css/property-descriptors/font-family.ts +0 -38
- package/src/css/property-descriptors/font-size.ts +0 -9
- package/src/css/property-descriptors/font-style.ts +0 -25
- package/src/css/property-descriptors/font-variant.ts +0 -12
- package/src/css/property-descriptors/font-weight.ts +0 -26
- package/src/css/property-descriptors/image-rendering.ts +0 -33
- package/src/css/property-descriptors/letter-spacing.ts +0 -25
- package/src/css/property-descriptors/line-break.ts +0 -22
- package/src/css/property-descriptors/line-height.ts +0 -22
- package/src/css/property-descriptors/list-style-image.ts +0 -19
- package/src/css/property-descriptors/list-style-position.ts +0 -22
- package/src/css/property-descriptors/list-style-type.ts +0 -179
- package/src/css/property-descriptors/margin.ts +0 -13
- package/src/css/property-descriptors/mix-blend-mode.ts +0 -35
- package/src/css/property-descriptors/object-fit.ts +0 -39
- package/src/css/property-descriptors/opacity.ts +0 -15
- package/src/css/property-descriptors/overflow-wrap.ts +0 -22
- package/src/css/property-descriptors/overflow.ts +0 -34
- package/src/css/property-descriptors/padding.ts +0 -14
- package/src/css/property-descriptors/paint-order.ts +0 -42
- package/src/css/property-descriptors/position.ts +0 -30
- package/src/css/property-descriptors/quotes.ts +0 -57
- package/src/css/property-descriptors/rotate.ts +0 -34
- package/src/css/property-descriptors/text-align.ts +0 -26
- package/src/css/property-descriptors/text-decoration-color.ts +0 -9
- package/src/css/property-descriptors/text-decoration-line.ts +0 -38
- package/src/css/property-descriptors/text-decoration-style.ts +0 -32
- package/src/css/property-descriptors/text-decoration-thickness.ts +0 -30
- package/src/css/property-descriptors/text-overflow.ts +0 -23
- package/src/css/property-descriptors/text-shadow.ts +0 -52
- package/src/css/property-descriptors/text-transform.ts +0 -27
- package/src/css/property-descriptors/text-underline-offset.ts +0 -27
- package/src/css/property-descriptors/transform-origin.ts +0 -29
- package/src/css/property-descriptors/transform.ts +0 -74
- package/src/css/property-descriptors/visibility.ts +0 -25
- package/src/css/property-descriptors/webkit-line-clamp.ts +0 -30
- package/src/css/property-descriptors/webkit-text-stroke-color.ts +0 -8
- package/src/css/property-descriptors/webkit-text-stroke-width.ts +0 -15
- package/src/css/property-descriptors/word-break.ts +0 -25
- package/src/css/property-descriptors/writing-mode.ts +0 -37
- package/src/css/property-descriptors/z-index.ts +0 -27
- package/src/css/syntax/__tests__/tokernizer-tests.ts +0 -29
- package/src/css/syntax/parser.ts +0 -188
- package/src/css/syntax/tokenizer.ts +0 -822
- package/src/css/type-descriptor.ts +0 -7
- package/src/css/types/__tests__/color-tests.ts +0 -147
- package/src/css/types/__tests__/image-tests.ts +0 -239
- package/src/css/types/angle.ts +0 -86
- package/src/css/types/color-math.ts +0 -22
- package/src/css/types/color-spaces/a98.ts +0 -86
- package/src/css/types/color-spaces/p3.ts +0 -92
- package/src/css/types/color-spaces/pro-photo.ts +0 -87
- package/src/css/types/color-spaces/rec2020.ts +0 -90
- package/src/css/types/color-spaces/srgb.ts +0 -87
- package/src/css/types/color-utilities.ts +0 -452
- package/src/css/types/color.ts +0 -485
- package/src/css/types/functions/-prefix-linear-gradient.ts +0 -35
- package/src/css/types/functions/-prefix-radial-gradient.ts +0 -106
- package/src/css/types/functions/-webkit-gradient.ts +0 -69
- package/src/css/types/functions/__tests__/radial-gradient.ts +0 -69
- package/src/css/types/functions/counter.ts +0 -511
- package/src/css/types/functions/gradient.ts +0 -206
- package/src/css/types/functions/linear-gradient.ts +0 -28
- package/src/css/types/functions/radial-gradient.ts +0 -101
- package/src/css/types/image.ts +0 -120
- package/src/css/types/index.ts +0 -1
- package/src/css/types/length-percentage.ts +0 -137
- package/src/css/types/length.ts +0 -7
- package/src/css/types/time.ts +0 -20
- package/src/dom/__mocks__/document-cloner.ts +0 -22
- package/src/dom/__tests__/dom-normalizer.test.ts +0 -133
- package/src/dom/__tests__/element-container.test.ts +0 -129
- package/src/dom/document-cloner.ts +0 -929
- package/src/dom/dom-normalizer.ts +0 -133
- package/src/dom/element-container.ts +0 -75
- package/src/dom/elements/li-element-container.ts +0 -10
- package/src/dom/elements/ol-element-container.ts +0 -12
- package/src/dom/elements/select-element-container.ts +0 -10
- package/src/dom/elements/textarea-element-container.ts +0 -9
- package/src/dom/node-parser.ts +0 -177
- package/src/dom/node-type-guards.ts +0 -70
- package/src/dom/replaced-elements/canvas-element-container.ts +0 -15
- package/src/dom/replaced-elements/iframe-element-container.ts +0 -55
- package/src/dom/replaced-elements/image-element-container.ts +0 -16
- package/src/dom/replaced-elements/index.ts +0 -5
- package/src/dom/replaced-elements/input-element-container.ts +0 -105
- package/src/dom/replaced-elements/pseudo-elements.ts +0 -0
- package/src/dom/replaced-elements/svg-element-container.ts +0 -23
- package/src/dom/text-container.ts +0 -42
- package/src/global.d.ts +0 -19
- package/src/index.ts +0 -82
- package/src/invariant.ts +0 -5
- package/src/options.ts +0 -55
- package/src/render/__tests__/object-fit.test.ts +0 -85
- package/src/render/background.ts +0 -298
- package/src/render/bezier-curve.ts +0 -47
- package/src/render/border.ts +0 -165
- package/src/render/bound-curves.ts +0 -388
- package/src/render/box-sizing.ts +0 -31
- package/src/render/canvas/__tests__/background-renderer.test.ts +0 -72
- package/src/render/canvas/__tests__/border-renderer.test.ts +0 -24
- package/src/render/canvas/__tests__/effects-renderer.test.ts +0 -32
- package/src/render/canvas/__tests__/text-renderer.test.ts +0 -471
- package/src/render/canvas/background-renderer.ts +0 -271
- package/src/render/canvas/border-renderer.ts +0 -224
- package/src/render/canvas/canvas-path.ts +0 -31
- package/src/render/canvas/canvas-renderer.ts +0 -641
- package/src/render/canvas/effects-renderer.ts +0 -130
- package/src/render/canvas/foreignobject-renderer.ts +0 -53
- package/src/render/canvas/text-renderer.ts +0 -700
- package/src/render/effects.ts +0 -75
- package/src/render/font-metrics.ts +0 -72
- package/src/render/object-fit.ts +0 -100
- package/src/render/path.ts +0 -37
- package/src/render/renderer-interface.ts +0 -28
- package/src/render/stacking-context.ts +0 -386
- package/src/render/vector.ts +0 -19
|
@@ -1,641 +0,0 @@
|
|
|
1
|
-
import { ElementPaint, parseStackingContexts, StackingContext } from '../stacking-context';
|
|
2
|
-
import { Color } from '../../css/types/color';
|
|
3
|
-
import { asString, isTransparent } from '../../css/types/color-utilities';
|
|
4
|
-
import { ElementContainer } from '../../dom/element-container';
|
|
5
|
-
import { BORDER_STYLE } from '../../css/property-descriptors/border-style';
|
|
6
|
-
import { Path, transformPath } from '../path';
|
|
7
|
-
import { BACKGROUND_CLIP } from '../../css/property-descriptors/background-clip';
|
|
8
|
-
import { BoundCurves, calculateBorderBoxPath, calculateContentBoxPath, calculatePaddingBoxPath } from '../bound-curves';
|
|
9
|
-
import { Vector } from '../vector';
|
|
10
|
-
import { CSSImageType, CSSURLImage } from '../../css/types/image';
|
|
11
|
-
import { getBackgroundValueForIndex } from '../background';
|
|
12
|
-
import { TextBounds } from '../../css/layout/text';
|
|
13
|
-
import { ImageElementContainer } from '../../dom/replaced-elements/image-element-container';
|
|
14
|
-
import { contentBox } from '../box-sizing';
|
|
15
|
-
import { CanvasElementContainer } from '../../dom/replaced-elements/canvas-element-container';
|
|
16
|
-
import { SVGElementContainer } from '../../dom/replaced-elements/svg-element-container';
|
|
17
|
-
import { ReplacedElementContainer } from '../../dom/replaced-elements';
|
|
18
|
-
import { EffectTarget } from '../effects';
|
|
19
|
-
import { contains } from '../../core/bitwise';
|
|
20
|
-
import { getAbsoluteValue } from '../../css/types/length-percentage';
|
|
21
|
-
import { FontMetrics } from '../font-metrics';
|
|
22
|
-
import { DISPLAY } from '../../css/property-descriptors/display';
|
|
23
|
-
import { Bounds } from '../../css/layout/bounds';
|
|
24
|
-
import { IMAGE_RENDERING } from '../../css/property-descriptors/image-rendering';
|
|
25
|
-
import { LIST_STYLE_TYPE } from '../../css/property-descriptors/list-style-type';
|
|
26
|
-
import { computeLineHeight } from '../../css/property-descriptors/line-height';
|
|
27
|
-
import {
|
|
28
|
-
CHECKBOX,
|
|
29
|
-
INPUT_COLOR,
|
|
30
|
-
PLACEHOLDER_COLOR,
|
|
31
|
-
InputElementContainer,
|
|
32
|
-
RADIO
|
|
33
|
-
} from '../../dom/replaced-elements/input-element-container';
|
|
34
|
-
import { TEXT_ALIGN } from '../../css/property-descriptors/text-align';
|
|
35
|
-
import { TextareaElementContainer } from '../../dom/elements/textarea-element-container';
|
|
36
|
-
import { SelectElementContainer } from '../../dom/elements/select-element-container';
|
|
37
|
-
import { IFrameElementContainer } from '../../dom/replaced-elements/iframe-element-container';
|
|
38
|
-
import { Context } from '../../core/context';
|
|
39
|
-
import { BackgroundRenderer } from './background-renderer';
|
|
40
|
-
import { BorderRenderer } from './border-renderer';
|
|
41
|
-
import { EffectsRenderer } from './effects-renderer';
|
|
42
|
-
import { TextRenderer } from './text-renderer';
|
|
43
|
-
import { createCanvasPath, formatCanvasPath } from './canvas-path';
|
|
44
|
-
import { calculateObjectFitRendering } from '../object-fit';
|
|
45
|
-
|
|
46
|
-
export type RenderConfigurations = RenderOptions & {
|
|
47
|
-
backgroundColor: Color | null;
|
|
48
|
-
signal?: AbortSignal;
|
|
49
|
-
/**
|
|
50
|
-
* Enable/disable image smoothing (anti-aliasing).
|
|
51
|
-
* When disabled, images are rendered with pixel-perfect sharpness (no interpolation).
|
|
52
|
-
* CSS `image-rendering` property on individual elements takes precedence.
|
|
53
|
-
* @default browser default (usually true)
|
|
54
|
-
*/
|
|
55
|
-
imageSmoothing?: boolean;
|
|
56
|
-
/**
|
|
57
|
-
* Image smoothing quality level when imageSmoothing is enabled.
|
|
58
|
-
* Higher quality may be slower for large images.
|
|
59
|
-
* Only supported in modern browsers (Chrome 54+, Firefox 94+, Safari 17+).
|
|
60
|
-
* Falls back gracefully in older browsers.
|
|
61
|
-
* @default browser default
|
|
62
|
-
*/
|
|
63
|
-
imageSmoothingQuality?: 'low' | 'medium' | 'high';
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export interface RenderOptions {
|
|
67
|
-
scale: number;
|
|
68
|
-
canvas?: HTMLCanvasElement;
|
|
69
|
-
x: number;
|
|
70
|
-
y: number;
|
|
71
|
-
width: number;
|
|
72
|
-
height: number;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const MASK_OFFSET = 10000;
|
|
76
|
-
|
|
77
|
-
export class CanvasRenderer {
|
|
78
|
-
canvas: HTMLCanvasElement;
|
|
79
|
-
ctx: CanvasRenderingContext2D;
|
|
80
|
-
private readonly context: Context;
|
|
81
|
-
private readonly options: RenderConfigurations;
|
|
82
|
-
private readonly fontMetrics: FontMetrics;
|
|
83
|
-
private readonly backgroundRenderer: BackgroundRenderer;
|
|
84
|
-
private readonly borderRenderer: BorderRenderer;
|
|
85
|
-
private readonly effectsRenderer: EffectsRenderer;
|
|
86
|
-
private readonly textRenderer: TextRenderer;
|
|
87
|
-
|
|
88
|
-
constructor(context: Context, options: RenderConfigurations) {
|
|
89
|
-
this.context = context;
|
|
90
|
-
this.options = options;
|
|
91
|
-
this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
|
|
92
|
-
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
|
|
93
|
-
if (!options.canvas) {
|
|
94
|
-
this.canvas.width = Math.floor(options.width * options.scale);
|
|
95
|
-
this.canvas.height = Math.floor(options.height * options.scale);
|
|
96
|
-
this.canvas.style.width = `${options.width}px`;
|
|
97
|
-
this.canvas.style.height = `${options.height}px`;
|
|
98
|
-
}
|
|
99
|
-
this.fontMetrics = new FontMetrics(document);
|
|
100
|
-
this.ctx.scale(this.options.scale, this.options.scale);
|
|
101
|
-
this.ctx.translate(-options.x, -options.y);
|
|
102
|
-
this.ctx.textBaseline = 'bottom';
|
|
103
|
-
|
|
104
|
-
// Set image smoothing options
|
|
105
|
-
if (options.imageSmoothing !== undefined) {
|
|
106
|
-
this.ctx.imageSmoothingEnabled = options.imageSmoothing;
|
|
107
|
-
}
|
|
108
|
-
if (options.imageSmoothingQuality) {
|
|
109
|
-
this.ctx.imageSmoothingQuality = options.imageSmoothingQuality;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Initialize specialized renderers
|
|
113
|
-
this.backgroundRenderer = new BackgroundRenderer({
|
|
114
|
-
ctx: this.ctx,
|
|
115
|
-
context: this.context,
|
|
116
|
-
canvas: this.canvas,
|
|
117
|
-
options: {
|
|
118
|
-
width: options.width,
|
|
119
|
-
height: options.height,
|
|
120
|
-
scale: options.scale
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
this.borderRenderer = new BorderRenderer(
|
|
125
|
-
{ ctx: this.ctx },
|
|
126
|
-
{
|
|
127
|
-
path: (paths) => this.path(paths),
|
|
128
|
-
formatPath: (paths) => this.formatPath(paths)
|
|
129
|
-
}
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
this.effectsRenderer = new EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
|
|
133
|
-
|
|
134
|
-
this.textRenderer = new TextRenderer({
|
|
135
|
-
ctx: this.ctx,
|
|
136
|
-
context: this.context,
|
|
137
|
-
options: { scale: options.scale }
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
this.context.logger.debug(
|
|
141
|
-
`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async renderStack(stack: StackingContext): Promise<void> {
|
|
146
|
-
const styles = stack.element.container.styles;
|
|
147
|
-
if (styles.isVisible()) {
|
|
148
|
-
await this.renderStackContent(stack);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async renderNode(paint: ElementPaint): Promise<void> {
|
|
153
|
-
if (paint.container.debugRender) {
|
|
154
|
-
debugger;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (paint.container.styles.isVisible()) {
|
|
158
|
-
await this.renderNodeBackgroundAndBorders(paint);
|
|
159
|
-
await this.renderNodeContent(paint);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Helper method to render text with paint order support
|
|
165
|
-
* Reduces code duplication in line-clamp and normal rendering
|
|
166
|
-
*/
|
|
167
|
-
|
|
168
|
-
// Helper method to truncate text and add ellipsis if needed
|
|
169
|
-
|
|
170
|
-
renderReplacedElement(
|
|
171
|
-
container: ReplacedElementContainer,
|
|
172
|
-
curves: BoundCurves,
|
|
173
|
-
image: HTMLImageElement | HTMLCanvasElement
|
|
174
|
-
): void {
|
|
175
|
-
const intrinsicWidth = (image as HTMLImageElement).naturalWidth || container.intrinsicWidth;
|
|
176
|
-
const intrinsicHeight = (image as HTMLImageElement).naturalHeight || container.intrinsicHeight;
|
|
177
|
-
if (image && intrinsicWidth > 0 && intrinsicHeight > 0) {
|
|
178
|
-
const box = contentBox(container);
|
|
179
|
-
const path = calculatePaddingBoxPath(curves);
|
|
180
|
-
this.path(path);
|
|
181
|
-
this.ctx.save();
|
|
182
|
-
this.ctx.clip();
|
|
183
|
-
const { sx, sy, sw, sh, dx, dy, dw, dh } = calculateObjectFitRendering(
|
|
184
|
-
intrinsicWidth,
|
|
185
|
-
intrinsicHeight,
|
|
186
|
-
box,
|
|
187
|
-
container.styles.objectFit
|
|
188
|
-
);
|
|
189
|
-
this.ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
190
|
-
this.ctx.restore();
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async renderNodeContent(paint: ElementPaint): Promise<void> {
|
|
195
|
-
this.effectsRenderer.applyEffects(paint.getEffects(EffectTarget.CONTENT));
|
|
196
|
-
const container = paint.container;
|
|
197
|
-
const curves = paint.curves;
|
|
198
|
-
const styles = container.styles;
|
|
199
|
-
// Use content box for text overflow calculation (excludes padding and border)
|
|
200
|
-
// This matches browser behavior where text-overflow uses the content width
|
|
201
|
-
const textBounds = contentBox(container);
|
|
202
|
-
for (const child of container.textNodes) {
|
|
203
|
-
await this.textRenderer.renderTextNode(child, styles, textBounds);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (container instanceof ImageElementContainer) {
|
|
207
|
-
try {
|
|
208
|
-
const image = await this.context.cache.match(container.src);
|
|
209
|
-
|
|
210
|
-
// Apply image smoothing based on CSS image-rendering property and global options
|
|
211
|
-
const prevSmoothing = this.ctx.imageSmoothingEnabled;
|
|
212
|
-
|
|
213
|
-
// CSS image-rendering property overrides global settings
|
|
214
|
-
if (
|
|
215
|
-
styles.imageRendering === IMAGE_RENDERING.PIXELATED ||
|
|
216
|
-
styles.imageRendering === IMAGE_RENDERING.CRISP_EDGES
|
|
217
|
-
) {
|
|
218
|
-
this.context.logger.debug(
|
|
219
|
-
`Disabling image smoothing for ${container.src} due to CSS image-rendering: ${styles.imageRendering === IMAGE_RENDERING.PIXELATED ? 'pixelated' : 'crisp-edges'}`
|
|
220
|
-
);
|
|
221
|
-
this.ctx.imageSmoothingEnabled = false;
|
|
222
|
-
} else if (styles.imageRendering === IMAGE_RENDERING.SMOOTH) {
|
|
223
|
-
this.context.logger.debug(
|
|
224
|
-
`Enabling image smoothing for ${container.src} due to CSS image-rendering: smooth`
|
|
225
|
-
);
|
|
226
|
-
this.ctx.imageSmoothingEnabled = true;
|
|
227
|
-
}
|
|
228
|
-
// IMAGE_RENDERING.AUTO: keep current global setting
|
|
229
|
-
|
|
230
|
-
this.renderReplacedElement(container, curves, image!);
|
|
231
|
-
|
|
232
|
-
// Restore previous smoothing state
|
|
233
|
-
this.ctx.imageSmoothingEnabled = prevSmoothing;
|
|
234
|
-
} catch (e) {
|
|
235
|
-
this.context.logger.error(`Error loading image ${container.src}`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (container instanceof CanvasElementContainer) {
|
|
240
|
-
this.renderReplacedElement(container, curves, container.canvas);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (container instanceof SVGElementContainer) {
|
|
244
|
-
try {
|
|
245
|
-
const image = await this.context.cache.match(container.svg);
|
|
246
|
-
this.renderReplacedElement(container, curves, image!);
|
|
247
|
-
} catch (e) {
|
|
248
|
-
this.context.logger.error(`Error loading svg ${container.svg.substring(0, 255)}`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (container instanceof IFrameElementContainer && container.tree) {
|
|
253
|
-
const iframeRenderer = new CanvasRenderer(this.context, {
|
|
254
|
-
scale: this.options.scale,
|
|
255
|
-
backgroundColor: container.backgroundColor,
|
|
256
|
-
x: 0,
|
|
257
|
-
y: 0,
|
|
258
|
-
width: container.width,
|
|
259
|
-
height: container.height
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
const canvas = await iframeRenderer.render(container.tree);
|
|
263
|
-
if (container.width && container.height) {
|
|
264
|
-
this.ctx.drawImage(
|
|
265
|
-
canvas,
|
|
266
|
-
0,
|
|
267
|
-
0,
|
|
268
|
-
container.width,
|
|
269
|
-
container.height,
|
|
270
|
-
container.bounds.left,
|
|
271
|
-
container.bounds.top,
|
|
272
|
-
container.bounds.width,
|
|
273
|
-
container.bounds.height
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (container instanceof InputElementContainer) {
|
|
279
|
-
const size = Math.min(container.bounds.width, container.bounds.height);
|
|
280
|
-
|
|
281
|
-
if (container.type === CHECKBOX) {
|
|
282
|
-
if (container.checked) {
|
|
283
|
-
this.ctx.save();
|
|
284
|
-
this.path([
|
|
285
|
-
new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79),
|
|
286
|
-
new Vector(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549),
|
|
287
|
-
new Vector(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071),
|
|
288
|
-
new Vector(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649),
|
|
289
|
-
new Vector(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23),
|
|
290
|
-
new Vector(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085),
|
|
291
|
-
new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79)
|
|
292
|
-
]);
|
|
293
|
-
|
|
294
|
-
this.ctx.fillStyle = asString(INPUT_COLOR);
|
|
295
|
-
this.ctx.fill();
|
|
296
|
-
this.ctx.restore();
|
|
297
|
-
}
|
|
298
|
-
} else if (container.type === RADIO) {
|
|
299
|
-
if (container.checked) {
|
|
300
|
-
this.ctx.save();
|
|
301
|
-
this.ctx.beginPath();
|
|
302
|
-
this.ctx.arc(
|
|
303
|
-
container.bounds.left + size / 2,
|
|
304
|
-
container.bounds.top + size / 2,
|
|
305
|
-
size / 4,
|
|
306
|
-
0,
|
|
307
|
-
Math.PI * 2,
|
|
308
|
-
true
|
|
309
|
-
);
|
|
310
|
-
this.ctx.fillStyle = asString(INPUT_COLOR);
|
|
311
|
-
this.ctx.fill();
|
|
312
|
-
this.ctx.restore();
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (isTextInputElement(container) && container.value.length) {
|
|
318
|
-
const [font, fontFamily, fontSize] = this.textRenderer.createFontStyle(styles);
|
|
319
|
-
const { baseline } = this.fontMetrics.getMetrics(fontFamily, fontSize);
|
|
320
|
-
|
|
321
|
-
this.ctx.font = font;
|
|
322
|
-
|
|
323
|
-
// Fix for Issue #92: Use placeholder color when rendering placeholder text
|
|
324
|
-
const isPlaceholder = container instanceof InputElementContainer && container.isPlaceholder;
|
|
325
|
-
this.ctx.fillStyle = isPlaceholder ? asString(PLACEHOLDER_COLOR) : asString(styles.color);
|
|
326
|
-
|
|
327
|
-
this.ctx.textBaseline = 'alphabetic';
|
|
328
|
-
this.ctx.textAlign = canvasTextAlign(container.styles.textAlign);
|
|
329
|
-
|
|
330
|
-
const bounds = contentBox(container);
|
|
331
|
-
|
|
332
|
-
let x = 0;
|
|
333
|
-
|
|
334
|
-
switch (container.styles.textAlign) {
|
|
335
|
-
case TEXT_ALIGN.CENTER:
|
|
336
|
-
x += bounds.width / 2;
|
|
337
|
-
break;
|
|
338
|
-
case TEXT_ALIGN.RIGHT:
|
|
339
|
-
x += bounds.width;
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Fix for Issue #92: Position text vertically centered in single-line input
|
|
344
|
-
// Only apply vertical centering for InputElementContainer, not for textarea or select
|
|
345
|
-
let verticalOffset = 0;
|
|
346
|
-
if (container instanceof InputElementContainer) {
|
|
347
|
-
const fontSizeValue = getAbsoluteValue(styles.fontSize, 0);
|
|
348
|
-
verticalOffset = (bounds.height - fontSizeValue) / 2;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Create text bounds with horizontal and vertical offsets
|
|
352
|
-
// Height is not modified as it doesn't affect text rendering position
|
|
353
|
-
const textBounds = bounds.add(x, verticalOffset, 0, 0);
|
|
354
|
-
|
|
355
|
-
this.ctx.save();
|
|
356
|
-
this.path([
|
|
357
|
-
new Vector(bounds.left, bounds.top),
|
|
358
|
-
new Vector(bounds.left + bounds.width, bounds.top),
|
|
359
|
-
new Vector(bounds.left + bounds.width, bounds.top + bounds.height),
|
|
360
|
-
new Vector(bounds.left, bounds.top + bounds.height)
|
|
361
|
-
]);
|
|
362
|
-
|
|
363
|
-
this.ctx.clip();
|
|
364
|
-
|
|
365
|
-
this.textRenderer.renderTextWithLetterSpacing(
|
|
366
|
-
new TextBounds(container.value, textBounds),
|
|
367
|
-
styles.letterSpacing,
|
|
368
|
-
baseline,
|
|
369
|
-
styles.writingMode
|
|
370
|
-
);
|
|
371
|
-
this.ctx.restore();
|
|
372
|
-
this.ctx.textBaseline = 'alphabetic';
|
|
373
|
-
this.ctx.textAlign = 'left';
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (contains(container.styles.display, DISPLAY.LIST_ITEM)) {
|
|
377
|
-
if (container.styles.listStyleImage !== null) {
|
|
378
|
-
const img = container.styles.listStyleImage;
|
|
379
|
-
if (img.type === CSSImageType.URL) {
|
|
380
|
-
let image;
|
|
381
|
-
const url = (img as CSSURLImage).url;
|
|
382
|
-
try {
|
|
383
|
-
image = await this.context.cache.match(url);
|
|
384
|
-
this.ctx.drawImage(image!, container.bounds.left - (image!.width + 10), container.bounds.top);
|
|
385
|
-
} catch (e) {
|
|
386
|
-
this.context.logger.error(`Error loading list-style-image ${url}`);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
} else if (paint.listValue && container.styles.listStyleType !== LIST_STYLE_TYPE.NONE) {
|
|
390
|
-
const [font] = this.textRenderer.createFontStyle(styles);
|
|
391
|
-
|
|
392
|
-
this.ctx.font = font;
|
|
393
|
-
this.ctx.fillStyle = asString(styles.color);
|
|
394
|
-
|
|
395
|
-
this.ctx.textBaseline = 'middle';
|
|
396
|
-
this.ctx.textAlign = 'right';
|
|
397
|
-
const bounds = new Bounds(
|
|
398
|
-
container.bounds.left,
|
|
399
|
-
container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width),
|
|
400
|
-
container.bounds.width,
|
|
401
|
-
computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
this.textRenderer.renderTextWithLetterSpacing(
|
|
405
|
-
new TextBounds(paint.listValue, bounds),
|
|
406
|
-
styles.letterSpacing,
|
|
407
|
-
computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2,
|
|
408
|
-
styles.writingMode
|
|
409
|
-
);
|
|
410
|
-
this.ctx.textBaseline = 'bottom';
|
|
411
|
-
this.ctx.textAlign = 'left';
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async renderStackContent(stack: StackingContext): Promise<void> {
|
|
417
|
-
if (stack.element.container.debugRender) {
|
|
418
|
-
debugger;
|
|
419
|
-
}
|
|
420
|
-
const signal = this.options.signal;
|
|
421
|
-
// https://www.w3.org/TR/css-position-3/#painting-order
|
|
422
|
-
// 1. the background and borders of the element forming the stacking context.
|
|
423
|
-
await this.renderNodeBackgroundAndBorders(stack.element);
|
|
424
|
-
// 2. the child stacking contexts with negative stack levels (most negative first).
|
|
425
|
-
for (const child of stack.negativeZIndex) {
|
|
426
|
-
if (signal?.aborted) throw new DOMException('The operation was aborted.', 'AbortError');
|
|
427
|
-
await this.renderStack(child);
|
|
428
|
-
}
|
|
429
|
-
// 3. For all its in-flow, non-positioned, block-level descendants in tree order:
|
|
430
|
-
if (signal?.aborted) throw new DOMException('The operation was aborted.', 'AbortError');
|
|
431
|
-
await this.renderNodeContent(stack.element);
|
|
432
|
-
|
|
433
|
-
for (const child of stack.nonInlineLevel) {
|
|
434
|
-
await this.renderNode(child);
|
|
435
|
-
}
|
|
436
|
-
// 4. All non-positioned floating descendants, in tree order. For each one of these,
|
|
437
|
-
// treat the element as if it created a new stacking context, but any positioned descendants and descendants
|
|
438
|
-
// which actually create a new stacking context should be considered part of the parent stacking context,
|
|
439
|
-
// not this new one.
|
|
440
|
-
for (const child of stack.nonPositionedFloats) {
|
|
441
|
-
if (signal?.aborted) throw new DOMException('The operation was aborted.', 'AbortError');
|
|
442
|
-
await this.renderStack(child);
|
|
443
|
-
}
|
|
444
|
-
// 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
|
|
445
|
-
for (const child of stack.nonPositionedInlineLevel) {
|
|
446
|
-
if (signal?.aborted) throw new DOMException('The operation was aborted.', 'AbortError');
|
|
447
|
-
await this.renderStack(child);
|
|
448
|
-
}
|
|
449
|
-
for (const child of stack.inlineLevel) {
|
|
450
|
-
await this.renderNode(child);
|
|
451
|
-
}
|
|
452
|
-
// 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
|
|
453
|
-
// All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
|
|
454
|
-
// For those with 'z-index: auto', treat the element as if it created a new stacking context,
|
|
455
|
-
// but any positioned descendants and descendants which actually create a new stacking context should be
|
|
456
|
-
// considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
|
|
457
|
-
// treat the stacking context generated atomically.
|
|
458
|
-
//
|
|
459
|
-
// All opacity descendants with opacity less than 1
|
|
460
|
-
//
|
|
461
|
-
// All transform descendants with transform other than none
|
|
462
|
-
for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {
|
|
463
|
-
if (signal?.aborted) throw new DOMException('The operation was aborted.', 'AbortError');
|
|
464
|
-
await this.renderStack(child);
|
|
465
|
-
}
|
|
466
|
-
// 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
|
|
467
|
-
// order (smallest first) then tree order.
|
|
468
|
-
for (const child of stack.positiveZIndex) {
|
|
469
|
-
if (signal?.aborted) throw new DOMException('The operation was aborted.', 'AbortError');
|
|
470
|
-
await this.renderStack(child);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
mask(paths: Path[]): void {
|
|
475
|
-
this.ctx.beginPath();
|
|
476
|
-
this.ctx.moveTo(0, 0);
|
|
477
|
-
// Use logical dimensions (options.width/height) instead of canvas pixel dimensions
|
|
478
|
-
// because context has already been scaled by this.options.scale
|
|
479
|
-
// Fix for Issue #126: Using canvas pixel dimensions causes broken output
|
|
480
|
-
this.ctx.lineTo(this.options.width, 0);
|
|
481
|
-
this.ctx.lineTo(this.options.width, this.options.height);
|
|
482
|
-
this.ctx.lineTo(0, this.options.height);
|
|
483
|
-
this.ctx.lineTo(0, 0);
|
|
484
|
-
this.formatPath(paths.slice(0).reverse());
|
|
485
|
-
this.ctx.closePath();
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
path(paths: Path[]): void {
|
|
489
|
-
createCanvasPath(this.ctx, paths);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
formatPath(paths: Path[]): void {
|
|
493
|
-
formatCanvasPath(this.ctx, paths);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
async renderNodeBackgroundAndBorders(paint: ElementPaint): Promise<void> {
|
|
497
|
-
this.effectsRenderer.applyEffects(paint.getEffects(EffectTarget.BACKGROUND_BORDERS));
|
|
498
|
-
const styles = paint.container.styles;
|
|
499
|
-
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
|
|
500
|
-
|
|
501
|
-
const borders = [
|
|
502
|
-
{ style: styles.borderTopStyle, color: styles.borderTopColor, width: styles.borderTopWidth },
|
|
503
|
-
{ style: styles.borderRightStyle, color: styles.borderRightColor, width: styles.borderRightWidth },
|
|
504
|
-
{ style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth },
|
|
505
|
-
{ style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth }
|
|
506
|
-
];
|
|
507
|
-
|
|
508
|
-
const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(
|
|
509
|
-
getBackgroundValueForIndex(styles.backgroundClip, 0),
|
|
510
|
-
paint.curves
|
|
511
|
-
);
|
|
512
|
-
|
|
513
|
-
if (hasBackground || styles.boxShadow.length) {
|
|
514
|
-
this.ctx.save();
|
|
515
|
-
this.path(backgroundPaintingArea);
|
|
516
|
-
this.ctx.clip();
|
|
517
|
-
|
|
518
|
-
if (!isTransparent(styles.backgroundColor)) {
|
|
519
|
-
this.ctx.fillStyle = asString(styles.backgroundColor);
|
|
520
|
-
this.ctx.fill();
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
524
|
-
|
|
525
|
-
this.ctx.restore();
|
|
526
|
-
|
|
527
|
-
styles.boxShadow
|
|
528
|
-
.slice(0)
|
|
529
|
-
.reverse()
|
|
530
|
-
.forEach((shadow) => {
|
|
531
|
-
this.ctx.save();
|
|
532
|
-
const borderBoxArea = calculateBorderBoxPath(paint.curves);
|
|
533
|
-
const maskOffset = shadow.inset ? 0 : MASK_OFFSET;
|
|
534
|
-
const shadowPaintingArea = transformPath(
|
|
535
|
-
borderBoxArea,
|
|
536
|
-
-maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number,
|
|
537
|
-
(shadow.inset ? 1 : -1) * shadow.spread.number,
|
|
538
|
-
shadow.spread.number * (shadow.inset ? -2 : 2),
|
|
539
|
-
shadow.spread.number * (shadow.inset ? -2 : 2)
|
|
540
|
-
);
|
|
541
|
-
|
|
542
|
-
if (shadow.inset) {
|
|
543
|
-
this.path(borderBoxArea);
|
|
544
|
-
this.ctx.clip();
|
|
545
|
-
this.mask(shadowPaintingArea);
|
|
546
|
-
} else {
|
|
547
|
-
this.mask(borderBoxArea);
|
|
548
|
-
this.ctx.clip();
|
|
549
|
-
this.path(shadowPaintingArea);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
|
|
553
|
-
this.ctx.shadowOffsetY = shadow.offsetY.number;
|
|
554
|
-
this.ctx.shadowColor = asString(shadow.color);
|
|
555
|
-
this.ctx.shadowBlur = shadow.blur.number;
|
|
556
|
-
this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)';
|
|
557
|
-
|
|
558
|
-
this.ctx.fill();
|
|
559
|
-
this.ctx.restore();
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
let side = 0;
|
|
564
|
-
for (const border of borders) {
|
|
565
|
-
if (border.style !== BORDER_STYLE.NONE && !isTransparent(border.color) && border.width > 0) {
|
|
566
|
-
if (border.style === BORDER_STYLE.DASHED) {
|
|
567
|
-
await this.borderRenderer.renderDashedDottedBorder(
|
|
568
|
-
border.color,
|
|
569
|
-
border.width,
|
|
570
|
-
side,
|
|
571
|
-
paint.curves,
|
|
572
|
-
BORDER_STYLE.DASHED
|
|
573
|
-
);
|
|
574
|
-
} else if (border.style === BORDER_STYLE.DOTTED) {
|
|
575
|
-
await this.borderRenderer.renderDashedDottedBorder(
|
|
576
|
-
border.color,
|
|
577
|
-
border.width,
|
|
578
|
-
side,
|
|
579
|
-
paint.curves,
|
|
580
|
-
BORDER_STYLE.DOTTED
|
|
581
|
-
);
|
|
582
|
-
} else if (border.style === BORDER_STYLE.DOUBLE) {
|
|
583
|
-
await this.borderRenderer.renderDoubleBorder(border.color, border.width, side, paint.curves);
|
|
584
|
-
} else {
|
|
585
|
-
await this.borderRenderer.renderSolidBorder(border.color, side, paint.curves);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
side++;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
async render(element: ElementContainer): Promise<HTMLCanvasElement> {
|
|
593
|
-
if (this.options.backgroundColor) {
|
|
594
|
-
this.ctx.fillStyle = asString(this.options.backgroundColor);
|
|
595
|
-
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const stack = parseStackingContexts(element);
|
|
599
|
-
|
|
600
|
-
await this.renderStack(stack);
|
|
601
|
-
this.effectsRenderer.applyEffects([]);
|
|
602
|
-
return this.canvas;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const isTextInputElement = (
|
|
607
|
-
container: ElementContainer
|
|
608
|
-
): container is InputElementContainer | TextareaElementContainer | SelectElementContainer => {
|
|
609
|
-
if (container instanceof TextareaElementContainer) {
|
|
610
|
-
return true;
|
|
611
|
-
} else if (container instanceof SelectElementContainer) {
|
|
612
|
-
return true;
|
|
613
|
-
} else if (container instanceof InputElementContainer && container.type !== RADIO && container.type !== CHECKBOX) {
|
|
614
|
-
return true;
|
|
615
|
-
}
|
|
616
|
-
return false;
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
const calculateBackgroundCurvedPaintingArea = (clip: BACKGROUND_CLIP, curves: BoundCurves): Path[] => {
|
|
620
|
-
switch (clip) {
|
|
621
|
-
case BACKGROUND_CLIP.BORDER_BOX:
|
|
622
|
-
return calculateBorderBoxPath(curves);
|
|
623
|
-
case BACKGROUND_CLIP.CONTENT_BOX:
|
|
624
|
-
return calculateContentBoxPath(curves);
|
|
625
|
-
case BACKGROUND_CLIP.PADDING_BOX:
|
|
626
|
-
default:
|
|
627
|
-
return calculatePaddingBoxPath(curves);
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
|
-
|
|
631
|
-
const canvasTextAlign = (textAlign: TEXT_ALIGN): CanvasTextAlign => {
|
|
632
|
-
switch (textAlign) {
|
|
633
|
-
case TEXT_ALIGN.CENTER:
|
|
634
|
-
return 'center';
|
|
635
|
-
case TEXT_ALIGN.RIGHT:
|
|
636
|
-
return 'right';
|
|
637
|
-
case TEXT_ALIGN.LEFT:
|
|
638
|
-
default:
|
|
639
|
-
return 'left';
|
|
640
|
-
}
|
|
641
|
-
};
|