@applitools/image
Advanced tools
| "use strict"; | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.toPngSharp = exports.fromPngBufferSharp = void 0; | ||
| const sharp_1 = __importDefault(require("sharp")); | ||
| const buffer_1 = require("buffer"); | ||
| async function fromPngBufferSharp(buffer) { | ||
| // Ensure we always get RGBA (4 channels) output for consistency | ||
| const { data, info } = await (0, sharp_1.default)(buffer_1.Buffer.from(buffer)) | ||
| .ensureAlpha() // Force RGBA output | ||
| .raw() | ||
| .toBuffer({ resolveWithObject: true }); | ||
| return { | ||
| width: info.width, | ||
| height: info.height, | ||
| data: data, | ||
| }; | ||
| } | ||
| exports.fromPngBufferSharp = fromPngBufferSharp; | ||
| async function toPngSharp(image) { | ||
| // Calculate channels from data size (only support RGB=3 and RGBA=4) | ||
| const calculatedChannels = image.data.length / (image.width * image.height); | ||
| const channels = Math.round(calculatedChannels); | ||
| // Validate channels and data size | ||
| if (![3, 4].includes(channels) || Math.abs(channels - calculatedChannels) > 0.01) { | ||
| throw new Error(`Invalid image data: expected ${image.width}x${image.height} with RGB (3) or RGBA (4) channels, ` + | ||
| `got ${image.data.length} bytes (${calculatedChannels.toFixed(2)} channels)`); | ||
| } | ||
| return await (0, sharp_1.default)(buffer_1.Buffer.from(image.data), { | ||
| raw: { | ||
| width: image.width, | ||
| height: image.height, | ||
| channels, | ||
| }, | ||
| }) | ||
| .png({ | ||
| compressionLevel: 9, | ||
| adaptiveFiltering: false, | ||
| }) | ||
| .toBuffer(); | ||
| } | ||
| exports.toPngSharp = toPngSharp; |
| import type { ImageBuffer, ImageRaw } from '../types'; | ||
| export declare function fromPngBufferSharp(buffer: ImageBuffer): Promise<ImageRaw>; | ||
| export declare function toPngSharp(image: ImageRaw): Promise<ImageBuffer>; |
+7
-0
| # Changelog | ||
| ## [1.2.7](https://github.com/Applitools-Dev/sdk/compare/js/image@1.2.6...js/image@1.2.7) (2026-03-01) | ||
| ### Performance Improvements | ||
| * use sharp to accelerate png performance in all js packages ([#3547](https://github.com/Applitools-Dev/sdk/issues/3547)) ([6e3c7b9](https://github.com/Applitools-Dev/sdk/commit/6e3c7b9e34815f470032ece52a161001592ad1b1)) | ||
| ## [1.2.6](https://github.com/Applitools-Dev/sdk/compare/js/image@1.2.5...js/image@1.2.6) (2025-12-01) | ||
@@ -4,0 +11,0 @@ |
+63
-12
@@ -39,18 +39,69 @@ "use strict"; | ||
| exports.extractPngSize = extractPngSize; | ||
| async function fromPngBuffer(buffer) { | ||
| return new Promise((resolve, reject) => { | ||
| const image = new png.Image(); | ||
| image.parse(buffer_1.Buffer.from(buffer), (err, image) => { | ||
| if (err) | ||
| return reject(err); | ||
| resolve(image); | ||
| /** | ||
| * Dynamically import png-sharp module | ||
| * Tries .js first (production), then .ts (ts-node/dev) | ||
| */ | ||
| async function importPngSharp() { | ||
| try { | ||
| return await import('./png-sharp.js'); | ||
| } | ||
| catch (error) { | ||
| // In ts-node, .js imports fail, try .ts instead | ||
| // Use dynamic string to bypass TypeScript's static import checking | ||
| const tsPath = './png-sharp' + '.ts'; | ||
| return await import(tsPath); | ||
| } | ||
| } | ||
| /** | ||
| * Decode PNG buffer to ImageRaw | ||
| * Tries sharp first (faster), falls back to png-async if sharp fails | ||
| */ | ||
| async function fromPngBuffer(buffer, defaultLogger) { | ||
| const logger = defaultLogger === null || defaultLogger === void 0 ? void 0 : defaultLogger.extend({ label: 'image/png' }); | ||
| try { | ||
| const pngSharp = await importPngSharp(); | ||
| const result = await pngSharp.fromPngBufferSharp(buffer); | ||
| logger === null || logger === void 0 ? void 0 : logger.log('Used Sharp for PNG decoding'); | ||
| return result; | ||
| } | ||
| catch (error) { | ||
| // Fallback to png-async if sharp fails (SEA, not installed, or runtime error) | ||
| logger === null || logger === void 0 ? void 0 : logger.log('Falling back to png-async for PNG decoding:', error instanceof Error ? error.message : String(error)); | ||
| return new Promise((resolve, reject) => { | ||
| const image = new png.Image(); | ||
| image.parse(buffer_1.Buffer.from(buffer), (err, image) => { | ||
| if (err) | ||
| return reject(err); | ||
| resolve(image); | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
| } | ||
| exports.fromPngBuffer = fromPngBuffer; | ||
| async function toPng(image) { | ||
| const wrapper = new png.Image({ width: image.width, height: image.height }); | ||
| wrapper.data = buffer_1.Buffer.from(image.data); | ||
| return utils.streams.toBuffer(wrapper.pack()); | ||
| /** | ||
| * Encode ImageRaw to PNG buffer | ||
| * Tries sharp first (22× faster), falls back to png-async if sharp fails | ||
| */ | ||
| async function toPng(image, defaultLogger) { | ||
| const logger = defaultLogger === null || defaultLogger === void 0 ? void 0 : defaultLogger.extend({ label: 'image/png' }); | ||
| try { | ||
| const pngSharp = await importPngSharp(); | ||
| const result = await pngSharp.toPngSharp(image); | ||
| logger === null || logger === void 0 ? void 0 : logger.log('Used Sharp for PNG encoding'); | ||
| return result; | ||
| } | ||
| catch (error) { | ||
| // Fallback to png-async if sharp fails (SEA, not installed, or runtime error) | ||
| // Uses RLE strategy (default) for optimal performance (~35ms) | ||
| logger === null || logger === void 0 ? void 0 : logger.log('Falling back to png-async for PNG encoding:', error instanceof Error ? error.message : String(error)); | ||
| const expectedSize = image.width * image.height * 4; | ||
| const actualSize = image.data.length; | ||
| if (actualSize !== expectedSize) { | ||
| throw new Error(`Image data size mismatch: expected ${expectedSize} bytes for ${image.width}x${image.height}, got ${actualSize} bytes`); | ||
| } | ||
| const wrapper = new png.Image({ width: image.width, height: image.height }); | ||
| wrapper.data = buffer_1.Buffer.from(image.data); | ||
| return utils.streams.toBuffer(wrapper.pack()); | ||
| } | ||
| } | ||
| exports.toPng = toPng; |
+8
-7
@@ -38,9 +38,10 @@ "use strict"; | ||
| const utils = __importStar(require("@applitools/utils")); | ||
| function makeImage(data) { | ||
| function makeImage(data, defaultLogger) { | ||
| let image, size; | ||
| let transforms = { rotate: 0, scale: 1, crop: undefined, modifiers: [] }; | ||
| const logger = defaultLogger === null || defaultLogger === void 0 ? void 0 : defaultLogger.extend({ label: 'image' }); | ||
| if (utils.types.isUint8Array(data) || utils.types.isAnyArrayBuffer(data)) { | ||
| const buffer = utils.types.isAnyArrayBuffer(data) ? new Uint8Array(data) : data; | ||
| if ((0, png_1.isPngBuffer)(buffer)) { | ||
| image = (0, png_1.fromPngBuffer)(buffer); | ||
| image = (0, png_1.fromPngBuffer)(buffer, logger); | ||
| size = (0, png_1.extractPngSize)(buffer); | ||
@@ -188,6 +189,6 @@ } | ||
| async toPng() { | ||
| return (0, png_1.toPng)(await this.toObject()); | ||
| return (0, png_1.toPng)(await this.toObject(), logger); | ||
| }, | ||
| async toFile(path) { | ||
| return toFile(await image, path); | ||
| return toFile(await image, path, logger); | ||
| }, | ||
@@ -206,3 +207,3 @@ async toObject() { | ||
| const transformedImage = await transform(image ? await image : size, transforms); | ||
| return toFile(transformedImage, path_1.default.join(debug.path, filename)).catch(() => undefined); | ||
| return toFile(transformedImage, path_1.default.join(debug.path, filename), logger).catch(() => undefined); | ||
| }, | ||
@@ -215,4 +216,4 @@ }; | ||
| } | ||
| async function toFile(image, filepath) { | ||
| const buffer = await (0, png_1.toPng)(image); | ||
| async function toFile(image, filepath, logger) { | ||
| const buffer = await (0, png_1.toPng)(image, logger); | ||
| return new Promise((resolve, reject) => { | ||
@@ -219,0 +220,0 @@ fs_1.default.mkdirSync(path_1.default.dirname(filepath), { recursive: true }); |
+9
-2
| { | ||
| "name": "@applitools/image", | ||
| "version": "1.2.6", | ||
| "version": "1.2.7", | ||
| "description": "Image primitive used for capturing and manipulating screenshots", | ||
@@ -39,2 +39,3 @@ "homepage": "https://applitools.com", | ||
| "dependencies": { | ||
| "@applitools/logger": "2.2.8", | ||
| "@applitools/utils": "1.14.1", | ||
@@ -44,3 +45,4 @@ "bmpimagejs": "1.0.4", | ||
| "omggif": "1.0.10", | ||
| "png-async": "0.9.4" | ||
| "png-async": "0.9.4", | ||
| "sharp": "^0.34.5" | ||
| }, | ||
@@ -59,3 +61,8 @@ "devDependencies": { | ||
| "access": "public" | ||
| }, | ||
| "lavamoat": { | ||
| "allowScripts": { | ||
| "sharp": true | ||
| } | ||
| } | ||
| } |
| import type { ImageBuffer, ImageRaw } from '../types'; | ||
| import type { Size } from '@applitools/utils'; | ||
| import type { Logger } from '@applitools/logger'; | ||
| export declare function isPngBuffer(buffer: ImageBuffer): boolean; | ||
| export declare function extractPngSize(buffer: ImageBuffer): Size; | ||
| export declare function fromPngBuffer(buffer: ImageBuffer): Promise<ImageRaw>; | ||
| export declare function toPng(image: ImageRaw): Promise<ImageBuffer>; | ||
| /** | ||
| * Decode PNG buffer to ImageRaw | ||
| * Tries sharp first (faster), falls back to png-async if sharp fails | ||
| */ | ||
| export declare function fromPngBuffer(buffer: ImageBuffer, defaultLogger?: Logger): Promise<ImageRaw>; | ||
| /** | ||
| * Encode ImageRaw to PNG buffer | ||
| * Tries sharp first (22× faster), falls back to png-async if sharp fails | ||
| */ | ||
| export declare function toPng(image: ImageRaw, defaultLogger?: Logger): Promise<ImageBuffer>; |
+2
-1
| import type { ImageBuffer, ImageRaw } from './types'; | ||
| import type { Location, Size, Region } from '@applitools/utils'; | ||
| import type { Logger } from '@applitools/logger'; | ||
| export interface Image { | ||
@@ -44,3 +45,3 @@ isImage: true; | ||
| }; | ||
| export declare function makeImage(data: ImageSource): Image; | ||
| export declare function makeImage(data: ImageSource, defaultLogger?: Logger): Image; | ||
| export {}; |
73658
8.02%19
11.76%1007
12.14%7
40%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added