UNPKG

html2canvas-pro

Version:

Screenshots with JavaScript. Next generation!

222 lines 9.69 kB
"use strict"; /** * Background Renderer * * Handles rendering of element backgrounds including: * - Background colors * - Background images (URL) * - Linear gradients * - Radial gradients * - Background patterns and repeats */ Object.defineProperty(exports, "__esModule", { value: true }); exports.BackgroundRenderer = void 0; const image_1 = require("../../css/types/image"); const background_1 = require("../background"); const gradient_1 = require("../../css/types/functions/gradient"); const length_percentage_1 = require("../../css/types/length-percentage"); const color_utilities_1 = require("../../css/types/color-utilities"); const bezier_curve_1 = require("../bezier-curve"); const image_rendering_1 = require("../../css/property-descriptors/image-rendering"); /** * Background Renderer * * Specialized renderer for element backgrounds. * Extracted from CanvasRenderer to improve code organization and maintainability. */ class BackgroundRenderer { constructor(deps) { this.ctx = deps.ctx; this.context = deps.context; this.canvas = deps.canvas; // Options stored in deps but not needed as instance property } /** * Render background images for a container * Supports URL images, linear gradients, and radial gradients * * @param container - Element container with background styles */ async renderBackgroundImage(container) { let index = container.styles.backgroundImage.length - 1; for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) { if (backgroundImage.type === 0 /* CSSImageType.URL */) { await this.renderBackgroundURLImage(container, backgroundImage, index); } else if ((0, image_1.isLinearGradient)(backgroundImage)) { this.renderLinearGradient(container, backgroundImage, index); } else if ((0, image_1.isRadialGradient)(backgroundImage)) { this.renderRadialGradient(container, backgroundImage, index); } index--; } } /** * Render a URL-based background image */ async renderBackgroundURLImage(container, backgroundImage, index) { let image; const url = backgroundImage.url; try { image = await this.context.cache.match(url); } catch (e) { this.context.logger.error(`Error loading background-image ${url}`); } if (image) { const imageWidth = isNaN(image.width) || image.width === 0 ? 1 : image.width; const imageHeight = isNaN(image.height) || image.height === 0 ? 1 : image.height; const [path, x, y, width, height] = (0, background_1.calculateBackgroundRendering)(container, index, [ imageWidth, imageHeight, imageWidth / imageHeight ]); const pattern = this.ctx.createPattern(this.resizeImage(image, width, height, container.styles.imageRendering), 'repeat'); this.renderRepeat(path, pattern, x, y); } } /** * Render a linear gradient background */ renderLinearGradient(container, backgroundImage, index) { const [path, x, y, width, height] = (0, background_1.calculateBackgroundRendering)(container, index, [null, null, null]); const [lineLength, x0, x1, y0, y1] = (0, gradient_1.calculateGradientDirection)(backgroundImage.angle, width, height); const ownerDocument = this.canvas.ownerDocument ?? document; const canvas = ownerDocument.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); const gradient = ctx.createLinearGradient(x0, y0, x1, y1); (0, gradient_1.processColorStops)(backgroundImage.stops, lineLength || 1).forEach((colorStop) => gradient.addColorStop(colorStop.stop, (0, color_utilities_1.asString)(colorStop.color))); ctx.fillStyle = gradient; ctx.fillRect(0, 0, width, height); if (width > 0 && height > 0) { const pattern = this.ctx.createPattern(canvas, 'repeat'); this.renderRepeat(path, pattern, x, y); } } /** * Render a radial gradient background */ renderRadialGradient(container, backgroundImage, index) { const [path, left, top, width, height] = (0, background_1.calculateBackgroundRendering)(container, index, [null, null, null]); const position = backgroundImage.position.length === 0 ? [length_percentage_1.FIFTY_PERCENT] : backgroundImage.position; const x = (0, length_percentage_1.getAbsoluteValue)(position[0], width); const y = (0, length_percentage_1.getAbsoluteValue)(position[position.length - 1], height); let [rx, ry] = (0, gradient_1.calculateRadius)(backgroundImage, x, y, width, height); // Handle edge case where radial gradient size is 0 // Use a minimum value of 0.01 to ensure gradient is still rendered if (rx === 0 || ry === 0) { rx = Math.max(rx, 0.01); ry = Math.max(ry, 0.01); } if (rx > 0 && ry > 0) { const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx); (0, gradient_1.processColorStops)(backgroundImage.stops, rx * 2).forEach((colorStop) => radialGradient.addColorStop(colorStop.stop, (0, color_utilities_1.asString)(colorStop.color))); 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; 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(); } } } /** * Render a repeating pattern with offset * * @param path - Path to fill * @param pattern - Canvas pattern or gradient * @param offsetX - X offset for pattern * @param offsetY - Y offset for pattern */ renderRepeat(path, pattern, offsetX, offsetY) { this.path(path); this.ctx.fillStyle = pattern; this.ctx.translate(offsetX, offsetY); this.ctx.fill(); this.ctx.translate(-offsetX, -offsetY); } /** * Resize an image to target dimensions * * @param image - Source image * @param width - Target width * @param height - Target height * @param imageRendering - CSS image-rendering property value * @returns Resized canvas or original image */ resizeImage(image, width, height, imageRendering) { // https://github.com/niklasvh/html2canvas/pull/2911 // if (image.width === width && image.height === height) { // return image; // } const ownerDocument = this.canvas.ownerDocument ?? document; const canvas = ownerDocument.createElement('canvas'); canvas.width = Math.max(1, width); canvas.height = Math.max(1, height); const ctx = canvas.getContext('2d'); // Apply image smoothing based on CSS image-rendering property if (imageRendering === image_rendering_1.IMAGE_RENDERING.PIXELATED || imageRendering === image_rendering_1.IMAGE_RENDERING.CRISP_EDGES) { this.context.logger.debug(`Disabling image smoothing for background image due to CSS image-rendering`); ctx.imageSmoothingEnabled = false; } else if (imageRendering === image_rendering_1.IMAGE_RENDERING.SMOOTH) { this.context.logger.debug(`Enabling image smoothing for background image due to CSS image-rendering: smooth`); ctx.imageSmoothingEnabled = true; } else { // AUTO: inherit from main renderer context ctx.imageSmoothingEnabled = this.ctx.imageSmoothingEnabled; } // Inherit quality setting if (this.ctx.imageSmoothingQuality) { ctx.imageSmoothingQuality = this.ctx.imageSmoothingQuality; } ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height); return canvas; } /** * Create a canvas path from path array * * @param paths - Array of path points */ path(paths) { this.ctx.beginPath(); this.formatPath(paths); this.ctx.closePath(); } /** * Format path points into canvas path * * @param paths - Array of path points */ formatPath(paths) { paths.forEach((point, index) => { const start = (0, bezier_curve_1.isBezierCurve)(point) ? point.start : point; if (index === 0) { this.ctx.moveTo(start.x, start.y); } else { this.ctx.lineTo(start.x, start.y); } if ((0, bezier_curve_1.isBezierCurve)(point)) { this.ctx.bezierCurveTo(point.startControl.x, point.startControl.y, point.endControl.x, point.endControl.y, point.end.x, point.end.y); } }); } } exports.BackgroundRenderer = BackgroundRenderer; //# sourceMappingURL=background-renderer.js.map