UNPKG

rn-color-thief

Version:

A powerful React Native library for extracting prominent colors from images and SVGs using Skia rendering engine

614 lines (613 loc) 24.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getPackageVersion = exports.compareVersions = exports.parseVersion = exports.supportsVideoFeatures = exports.getRecommendedSkiaVersion = exports.logCompatibilityStatus = exports.checkCompatibility = exports.getProminentColors = exports.defaultColorThief = exports.createColorThief = exports.ReactNativeColorThief = void 0; const react_native_skia_1 = require("@shopify/react-native-skia"); const quantize_1 = __importDefault(require("quantize")); const color_converters_1 = require("./color-converters"); const version_compatibility_1 = require("./version-compatibility"); /** * React Native Color Thief - OOP implementation for extracting prominent colors from images * * This class provides methods to extract color palettes from images and SVGs using * React Native Skia for rendering and quantize.js for color quantization. * * @example * ```typescript * // Basic usage * const colorThief = new ReactNativeColorThief(); * const colors = await colorThief.getProminentColors('https://example.com/image.jpg'); * * // Custom configuration * const customColorThief = new ReactNativeColorThief({ * quality: 8, * colorCount: 6, * excludeWhite: false * }); * const palette = await customColorThief.getPalette('https://example.com/image.jpg'); * ``` * * @class ReactNativeColorThief */ class ReactNativeColorThief { /** * Creates an instance of ReactNativeColorThief * * @param {ColorThiefConfig} [config={}] - Configuration options for color extraction * @example * ```typescript * // Default configuration * const colorThief = new ReactNativeColorThief(); * * // Custom configuration * const colorThief = new ReactNativeColorThief({ * quality: 5, * colorCount: 8, * minAlpha: 100, * excludeWhite: false, * whiteThreshold: 200, * canvasSize: 512 * }); * ``` */ constructor(config = {}) { /** Offscreen surface for image processing */ this.offScreen = null; /** Canvas for drawing operations */ this.canvas = null; this.config = { quality: config.quality ?? 10, colorCount: config.colorCount ?? 5, minAlpha: config.minAlpha ?? 125, excludeWhite: config.excludeWhite ?? true, whiteThreshold: config.whiteThreshold ?? 250, canvasSize: config.canvasSize ?? 256, suppressCompatibilityWarnings: config.suppressCompatibilityWarnings ?? false, }; // Check version compatibility on initialization if (!this.config.suppressCompatibilityWarnings) { const isCompatible = (0, version_compatibility_1.logCompatibilityStatus)(); if (!isCompatible) { console.warn('🎨 react-native-color-thief may not work correctly with incompatible versions. ' + 'Please update your dependencies or set suppressCompatibilityWarnings: true to hide this warning.'); } } } /** * Initialize the canvas and offscreen surface for image processing * * @private * @throws {Error} When offscreen surface creation fails * @returns {void} */ initializeCanvas() { this.offScreen = react_native_skia_1.Skia.Surface.MakeOffscreen(this.config.canvasSize, this.config.canvasSize); if (!this.offScreen) { throw new Error("Failed to create offscreen surface"); } this.canvas = this.offScreen.getCanvas(); } /** * Clean up resources and reset internal state * * @private * @returns {void} */ cleanup() { this.offScreen = null; this.canvas = null; } /** * Check if the provided URL ends with SVG extension * * @private * @param {string} url - URL to check * @returns {boolean} True if URL ends with .svg (case insensitive) */ isURLEndsWithSVG(url) { return url.toLowerCase().endsWith(".svg"); } /** * Create pixel array from image data with filtering applied * * @private * @param {Uint8Array<ArrayBufferLike> | Float32Array<ArrayBufferLike>} imgData - Raw image pixel data * @param {number} pixelCount - Total number of pixels in the image * @returns {number[][]} Array of RGB pixel values after filtering */ createPixelArray(imgData, pixelCount) { const pixelArray = []; for (let i = 0; i < pixelCount; i += this.config.quality) { const offset = i * 4; const [r, g, b, a] = imgData.slice(offset, offset + 4); // Check alpha threshold if (typeof a === "undefined" || a >= this.config.minAlpha) { // Check white exclusion if (!this.config.excludeWhite || !(r > this.config.whiteThreshold && g > this.config.whiteThreshold && b > this.config.whiteThreshold)) { pixelArray.push([r, g, b]); } } } return pixelArray; } /** * Process SVG image and extract pixel data * * @private * @param {string} sourceURI - URI of the SVG file * @returns {Promise<number[][]>} Array of RGB pixel values * @throws {Error} When canvas is not initialized or SVG processing fails */ async processSVG(sourceURI) { if (!this.canvas || !this.offScreen) { throw new Error("Canvas not initialized"); } const svgURI = await react_native_skia_1.Skia.Data.fromURI(sourceURI); const svgFactory = react_native_skia_1.Skia.SVG.MakeFromData.bind(react_native_skia_1.Skia.SVG); const svg = svgFactory(svgURI); if (!svg) { throw new Error("Failed to load SVG"); } this.canvas.drawSvg(svg); this.offScreen.flush(); const image = this.offScreen.makeImageSnapshot(); const { width, height } = image.getImageInfo(); const pixels = image.readPixels(); const pixelCount = width * height; if (!pixels) { throw new Error("Failed to read pixels from SVG"); } return this.createPixelArray(pixels, pixelCount); } /** * Process regular image (JPG, PNG, etc.) and extract pixel data * * @private * @param {string} sourceURI - URI of the image file * @returns {Promise<number[][]>} Array of RGB pixel values * @throws {Error} When canvas is not initialized or image processing fails */ async processImage(sourceURI) { if (!this.canvas || !this.offScreen) { throw new Error("Canvas not initialized"); } const imageURI = await react_native_skia_1.Skia.Data.fromURI(sourceURI); const imgFactory = react_native_skia_1.Skia.Image.MakeImageFromEncoded.bind(react_native_skia_1.Skia.Image); const image = imgFactory(imageURI); if (!image) { throw new Error("Failed to load image"); } this.canvas.drawImage(image, 0, 0); this.offScreen.flush(); const { width, height } = image.getImageInfo(); const pixels = image.readPixels(); const pixelCount = width * height; if (!pixels) { throw new Error("Failed to read pixels from image"); } return this.createPixelArray(pixels, pixelCount); } /** * Quantize colors using the quantize library to reduce color palette * * @private * @param {number[][]} pixelArray - Array of RGB pixel values * @returns {quantize.RgbPixel[] | null} Quantized color palette or null if empty */ quantizeColors(pixelArray) { if (pixelArray.length === 0) { return null; } const cmap = (0, quantize_1.default)(pixelArray, this.config.colorCount); return cmap ? cmap.palette() : null; } /** * Convert RGB array to ColorResult with formatted color strings * * @private * @param {ArrayRGB} rgb - RGB color array [red, green, blue] * @param {number} [weight] - Optional weight/frequency of the color * @returns {ColorResult} Complete color result with formatted strings */ createColorResult(rgb, weight) { return { rgb, formats: { hex: (0, color_converters_1.formatRGB)(rgb, "hex"), rgb: (0, color_converters_1.formatRGB)(rgb, "rgbString"), hsl: (0, color_converters_1.formatRGB)(rgb, "hslString"), keyword: (0, color_converters_1.formatRGB)(rgb, "keyword"), }, weight, }; } /** * Get prominent colors from an image or SVG * * Extracts the most prominent colors from the provided image URI using the current configuration. * Supports both regular images (JPG, PNG, GIF, BMP, WebP) and SVG files. * * @param {string} sourceURI - URI of the image or SVG file * @returns {Promise<ColorResult[]>} Array of prominent colors sorted by prominence * @throws {Error} When image processing fails or no colors are found * * @example * ```typescript * const colorThief = new ReactNativeColorThief(); * const colors = await colorThief.getProminentColors('https://example.com/image.jpg'); * console.log(colors[0].formats.hex); // "#FF5733" * ``` */ async getProminentColors(sourceURI) { try { this.initializeCanvas(); const sourceType = this.isURLEndsWithSVG(sourceURI) ? "svg" : "image"; let pixelArray; if (sourceType === "svg") { pixelArray = await this.processSVG(sourceURI); } else { pixelArray = await this.processImage(sourceURI); } const palette = this.quantizeColors(pixelArray); if (!palette || palette.length === 0) { return []; } return palette.map((color) => this.createColorResult(color)); } catch (error) { throw new Error(`Failed to extract colors: ${error instanceof Error ? error.message : "Unknown error"}`); } finally { this.cleanup(); } } /** * Get a complete palette with dominant and secondary colors * * Extracts all prominent colors and organizes them into a structured palette result * with dominant color, secondary colors, and metadata. * * @param {string} sourceURI - URI of the image or SVG file * @returns {Promise<PaletteResult>} Complete palette analysis with dominant and secondary colors * @throws {Error} When image processing fails or no colors are found * * @example * ```typescript * const colorThief = new ReactNativeColorThief(); * const palette = await colorThief.getPalette('https://example.com/image.jpg'); * console.log('Dominant color:', palette.dominant.formats.hex); * console.log('Secondary colors:', palette.secondary.length); * ``` */ async getPalette(sourceURI) { try { const colors = await this.getProminentColors(sourceURI); if (colors.length === 0) { throw new Error("No colors found in image"); } const dominant = colors[0]; const secondary = colors.slice(1); return { colors, dominant, secondary, pixelCount: colors.length * this.config.quality, }; } catch (error) { throw new Error(`Failed to get palette: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Get only the dominant (most prominent) color from an image * * A convenience method that returns only the most prominent color from the image. * This is useful when you only need the primary color for UI theming. * * @param {string} sourceURI - URI of the image or SVG file * @returns {Promise<ColorResult>} The most prominent color in the image * @throws {Error} When image processing fails or no colors are found * * @example * ```typescript * const colorThief = new ReactNativeColorThief(); * const dominant = await colorThief.getDominantColor('https://example.com/image.jpg'); * console.log('Primary color:', dominant.formats.hex); // "#FF5733" * ``` */ async getDominantColor(sourceURI) { try { const colors = await this.getProminentColors(sourceURI); if (colors.length === 0) { throw new Error("No colors found in image"); } return colors[0]; } catch (error) { throw new Error(`Failed to get dominant color: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Get colors in a specific string format * * Extracts prominent colors and returns them as an array of strings in the specified format. * Useful when you need colors in a specific format for styling or display purposes. * * @param {string} sourceURI - URI of the image or SVG file * @param {ColorFormats} format - Desired color format (hex, rgbString, hslString, keyword) * @returns {Promise<string[]>} Array of colors in the specified format * @throws {Error} When image processing fails or format is invalid * * @example * ```typescript * const colorThief = new ReactNativeColorThief(); * const hexColors = await colorThief.getColorsInFormat('https://example.com/image.jpg', 'hex'); * console.log(hexColors); // ["#FF5733", "#33FF57", "#5733FF"] * * const rgbColors = await colorThief.getColorsInFormat('https://example.com/image.jpg', 'rgbString'); * console.log(rgbColors); // ["rgb(255, 87, 51)", "rgb(51, 255, 87)", "rgb(87, 51, 255)"] * ``` */ async getColorsInFormat(sourceURI, format) { try { const colors = await this.getProminentColors(sourceURI); return colors.map((color) => (0, color_converters_1.formatRGB)(color.rgb, format)); } catch (error) { throw new Error(`Failed to get colors in format: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Update configuration with new settings * * Allows dynamic updating of configuration options without creating a new instance. * Only the provided options will be updated, others will remain unchanged. * * @param {Partial<ColorThiefConfig>} newConfig - Partial configuration object with new settings * @returns {void} * * @example * ```typescript * const colorThief = new ReactNativeColorThief(); * colorThief.updateConfig({ quality: 5, colorCount: 8 }); * ``` */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig, }; } /** * Get current configuration settings * * Returns a copy of the current configuration object. Useful for debugging * or when you need to check the current settings. * * @returns {ColorThiefConfig} Copy of current configuration * * @example * ```typescript * const colorThief = new ReactNativeColorThief(); * const config = colorThief.getConfig(); * console.log('Current quality:', config.quality); * ``` */ getConfig() { return { ...this.config }; } /** * Reset configuration to default values * * Restores all configuration options to their default values. * Useful when you want to start fresh with default settings. * * @returns {void} * * @example * ```typescript * const colorThief = new ReactNativeColorThief({ quality: 3 }); * colorThief.resetConfig(); // Back to quality: 10 * ``` */ resetConfig() { this.config = { quality: 10, colorCount: 5, minAlpha: 125, excludeWhite: true, whiteThreshold: 250, canvasSize: 256, suppressCompatibilityWarnings: false, }; } /** * Validate if the source URI is supported * * Checks if the provided URI points to a supported image format. * Supported formats include: JPG, JPEG, PNG, GIF, BMP, WebP, and SVG. * * @param {string} sourceURI - URI to validate * @returns {boolean} True if the URI points to a supported format * * @example * ```typescript * const colorThief = new ReactNativeColorThief(); * if (colorThief.isSupportedFormat('https://example.com/image.jpg')) { * const colors = await colorThief.getProminentColors('https://example.com/image.jpg'); * } * ``` */ isSupportedFormat(sourceURI) { const supportedExtensions = [ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg", ]; const lowerURI = sourceURI.toLowerCase(); return supportedExtensions.some((ext) => lowerURI.endsWith(ext)); } /** * Check version compatibility for React, React Native, and Skia * * Validates that the current environment has compatible versions of all required dependencies. * Returns detailed compatibility information including warnings and errors. * * @returns {object} Compatibility check results * @returns {boolean} isCompatible - Whether all dependencies are compatible * @returns {boolean} hasWarnings - Whether there are any compatibility warnings * @returns {object} react - React compatibility details * @returns {object} reactNative - React Native compatibility details * @returns {object} skia - React Native Skia compatibility details * @returns {string[]} platformRequirements - Platform version requirements * * @example * ```typescript * const colorThief = new ReactNativeColorThief(); * const compatibility = colorThief.checkVersionCompatibility(); * * if (!compatibility.isCompatible) { * console.error('Incompatible versions detected:', compatibility); * } * * if (compatibility.hasWarnings) { * console.warn('Version warnings:', compatibility); * } * ``` */ checkVersionCompatibility() { return (0, version_compatibility_1.checkCompatibility)(); } /** * Get detailed color statistics from an image * * Analyzes the extracted colors and provides statistical information including * total color count, average brightness, and color distribution weights. * * @param {string} sourceURI - URI of the image or SVG file * @returns {Promise<object>} Object containing color statistics * @returns {number} totalColors - Total number of colors found * @returns {number} averageBrightness - Average brightness across all colors (0-255) * @returns {Object} colorDistribution - Weighted distribution of colors by hex value * @throws {Error} When image processing fails * * @example * ```typescript * const colorThief = new ReactNativeColorThief(); * const stats = await colorThief.getColorStatistics('https://example.com/image.jpg'); * console.log('Total colors:', stats.totalColors); * console.log('Average brightness:', stats.averageBrightness); * console.log('Color distribution:', stats.colorDistribution); * ``` */ async getColorStatistics(sourceURI) { try { const palette = await this.getPalette(sourceURI); const averageBrightness = palette.colors.reduce((sum, color) => { const [r, g, b] = color.rgb; return sum + (r + g + b) / 3; }, 0) / palette.colors.length; const colorDistribution = {}; palette.colors.forEach((color, index) => { colorDistribution[color.formats.hex] = (palette.colors.length - index) / palette.colors.length; }); return { totalColors: palette.colors.length, averageBrightness, colorDistribution, }; } catch (error) { throw new Error(`Failed to get color statistics: ${error instanceof Error ? error.message : "Unknown error"}`); } } } exports.ReactNativeColorThief = ReactNativeColorThief; /** * Factory function to create a ColorThief instance * * A convenience function that creates a new ReactNativeColorThief instance with optional configuration. * This is useful when you prefer functional programming style or want to create instances dynamically. * * @param {ColorThiefConfig} [config] - Optional configuration options * @returns {ReactNativeColorThief} New ColorThief instance * * @example * ```typescript * // Create with default configuration * const colorThief = createColorThief(); * * // Create with custom configuration * const colorThief = createColorThief({ * quality: 8, * colorCount: 6, * excludeWhite: false * }); * ``` */ const createColorThief = (config) => { return new ReactNativeColorThief(config); }; exports.createColorThief = createColorThief; /** * Default ColorThief instance with default configuration * * A pre-configured instance that can be used immediately without creating a new instance. * Useful for simple use cases or when you want to share a single instance across your application. * * @constant {ReactNativeColorThief} * * @example * ```typescript * // Use the default instance directly * const colors = await defaultColorThief.getProminentColors('https://example.com/image.jpg'); * ``` */ exports.defaultColorThief = new ReactNativeColorThief(); /** * Legacy function for backward compatibility * * This function maintains backward compatibility with the original color-thief implementation. * It returns colors as hex strings only, unlike the new class-based approach. * * @deprecated Use ReactNativeColorThief class instead for better functionality and type safety * @param {string} sourceURI - URI of the image or SVG file * @returns {Promise<string[]>} Array of hex color strings * * @example * ```typescript * // Legacy usage (deprecated) * const hexColors = await getProminentColors('https://example.com/image.jpg'); * * // Recommended usage * const colorThief = new ReactNativeColorThief(); * const colors = await colorThief.getProminentColors('https://example.com/image.jpg'); * const hexColors = colors.map(color => color.formats.hex); * ``` */ const getProminentColors = async (sourceURI) => { const colorThief = new ReactNativeColorThief(); const colors = await colorThief.getProminentColors(sourceURI); return colors.map((color) => color.formats.hex); }; exports.getProminentColors = getProminentColors; // Export version compatibility utilities var version_compatibility_2 = require("./version-compatibility"); Object.defineProperty(exports, "checkCompatibility", { enumerable: true, get: function () { return version_compatibility_2.checkCompatibility; } }); Object.defineProperty(exports, "logCompatibilityStatus", { enumerable: true, get: function () { return version_compatibility_2.logCompatibilityStatus; } }); Object.defineProperty(exports, "getRecommendedSkiaVersion", { enumerable: true, get: function () { return version_compatibility_2.getRecommendedSkiaVersion; } }); Object.defineProperty(exports, "supportsVideoFeatures", { enumerable: true, get: function () { return version_compatibility_2.supportsVideoFeatures; } }); Object.defineProperty(exports, "parseVersion", { enumerable: true, get: function () { return version_compatibility_2.parseVersion; } }); Object.defineProperty(exports, "compareVersions", { enumerable: true, get: function () { return version_compatibility_2.compareVersions; } }); Object.defineProperty(exports, "getPackageVersion", { enumerable: true, get: function () { return version_compatibility_2.getPackageVersion; } });