PATH:
home
/
centosnipponia
/
public_html
/
nipponiacar
/
wp-content
/
plugins
/
optimole-wp
/
assets
/
js
/
modules
/** * Optimole Srcset Detection Module * Detects images without data-opt-src (lazyload) and calculates missing srcset variations * for different device types and viewport widths */ import { optmlLogger } from './logger.js'; import { optmlDevice } from './device.js'; /** * Srcset detection and calculation utilities */ export const optmlSrcsetDetector = { /** * Common device breakpoints for responsive images * Based on popular CSS frameworks and real-world usage patterns */ DEVICE_BREAKPOINTS: { // Mobile breakpoints MOBILE_SMALL: 320, // Small mobile devices MOBILE_MEDIUM: 375, // Medium mobile devices MOBILE_LARGE: 425, // Large mobile devices // Tablet breakpoints TABLET_SMALL: 768, // Small tablets TABLET_LARGE: 1024, // Large tablets // Desktop breakpoints DESKTOP_SMALL: 1200, // Small desktop DESKTOP_MEDIUM: 1440, // Medium desktop DESKTOP_LARGE: 1920, // Large desktop DESKTOP_XL: 2560 // Extra large desktop }, /** * Device pixel ratio multipliers for high-DPI displays */ DPR_MULTIPLIERS: [1, 2], /** * Configuration for srcset generation */ CONFIG: { // Minimum image size to consider (in pixels) MIN_SIZE: 200, // Maximum number of srcset variations per image MAX_VARIATIONS: 8, // Tolerance for existing srcset sizes (in pixels) SIZE_TOLERANCE: 50 }, /** * Configure srcset generation settings * @param {Object} config - Configuration options * @param {number} config.minSize - Minimum image size to consider (default: 200) * @param {number} config.maxVariations - Maximum srcset variations per image (default: 8) * @param {number} config.sizeTolerance - Tolerance for existing sizes (default: 50) */ configure: function(config) { if (config.minSize) this.CONFIG.MIN_SIZE = config.minSize; if (config.maxVariations) this.CONFIG.MAX_VARIATIONS = config.maxVariations; if (config.sizeTolerance) this.CONFIG.SIZE_TOLERANCE = config.sizeTolerance; optmlLogger.info('Srcset detector configured:', this.CONFIG); }, /** * Wait for images to load and get their natural dimensions * @param {NodeList} images - Collection of image elements * @returns {Promise} Promise that resolves when all images are loaded */ _waitForImagesToLoad: function(images) { const imagePromises = Array.from(images).map(img => { return new Promise((resolve) => { // If image is already loaded if (img.complete && img.naturalWidth > 0) { resolve(img); return; } // Wait for image to load const onLoad = () => { img.removeEventListener('load', onLoad); img.removeEventListener('error', onError); resolve(img); }; const onError = () => { img.removeEventListener('load', onLoad); img.removeEventListener('error', onError); optmlLogger.warn('Image failed to load:', img.src); resolve(img); // Still resolve to continue processing }; img.addEventListener('load', onLoad); img.addEventListener('error', onError); // Timeout after 5 seconds setTimeout(() => { img.removeEventListener('load', onLoad); img.removeEventListener('error', onError); optmlLogger.warn('Image load timeout:', img.src); resolve(img); }, 5000); }); }); return Promise.all(imagePromises); }, /** * Detect all Optimole images that are NOT using lazyload (no data-opt-src) * and calculate missing srcset variations * @returns {Promise<Object>} Promise that resolves to object with srcset data and crop status */ detectMissingSrcsets: async function() { const missingSrcsetData = {}; const cropStatusData = {}; // Find all Optimole images const optimoleImages = document.querySelectorAll('img[data-opt-id]'); if (optimoleImages.length === 0) { optmlLogger.info('No Optimole images found for srcset analysis'); return { srcset: missingSrcsetData, crop: cropStatusData }; } optmlLogger.info(`Found ${optimoleImages.length} Optimole images, waiting for them to load...`); // Wait for all images to load to get accurate natural dimensions const loadedImages = await this._waitForImagesToLoad(optimoleImages); optmlLogger.info(`Loaded ${loadedImages.length} images, analyzing srcset requirements...`); loadedImages.forEach(img => { try { const imageId = parseInt(img.getAttribute('data-opt-id'), 10); if (isNaN(imageId)) return; // Improved skip logic: // - Include if image doesn't have data-opt-src // - Include if image has data-opt-src but also has data-opt-lazy-loaded // - Skip otherwise const hasOptSrc = img.hasAttribute('data-opt-src'); const hasLazyLoaded = img.hasAttribute('data-opt-lazy-loaded'); const shouldInclude = !hasOptSrc || (hasOptSrc && hasLazyLoaded); if (shouldInclude) { const reason = !hasOptSrc ? 'not using lazyload' : 'lazyload completed (has data-opt-lazy-loaded)'; optmlLogger.info(`Image ${imageId} ${reason}, analyzing srcset requirements`); // Analyze the image and calculate required srcset variations const analysisResult = this._analyzeSrcsetRequirements(img, imageId); if (analysisResult && analysisResult.srcset && analysisResult.srcset.length > 0) { missingSrcsetData[imageId] = analysisResult.srcset; cropStatusData[imageId] = analysisResult.requiresCropping; } } } catch (error) { optmlLogger.error('Error analyzing image for srcset:', img, error); } }); optmlLogger.info('Images requiring srcset variations:', Object.keys(missingSrcsetData).length); optmlLogger.info('Images with crop status:', Object.keys(cropStatusData).length); return { srcset: missingSrcsetData, crop: cropStatusData }; }, /** * Analyze an image and determine what srcset variations are needed * @private * @param {HTMLImageElement} img - Image element to analyze * @param {number} imageId - Optimole image ID * @returns {Object|null} Srcset requirements data or null */ _analyzeSrcsetRequirements: function(img, imageId) { // Get current image dimensions const currentWidth = img.offsetWidth || img.clientWidth; const currentHeight = img.offsetHeight || img.clientHeight; const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; // Skip if we can't determine dimensions if (!currentWidth || !currentHeight || !naturalWidth || !naturalHeight) { optmlLogger.warn(`Skipping image ${imageId}: insufficient dimension data`); return null; } optmlLogger.info(`Analyzing image ${imageId}:`, { current: `${currentWidth}x${currentHeight}`, natural: `${naturalWidth}x${naturalHeight}` }); // Calculate aspect ratios const naturalAspectRatio = naturalWidth / naturalHeight; const currentAspectRatio = currentWidth / currentHeight; // Determine if image requires cropping based on object-fit and aspect ratio difference const aspectRatioDifference = Math.abs(naturalAspectRatio - currentAspectRatio); const requiresCropping = this._requiresCropping(img, aspectRatioDifference, naturalAspectRatio, currentAspectRatio); // Get current device type const currentDeviceType = optmlDevice.getDeviceType(); // Calculate required sizes for different breakpoints // Use current aspect ratio for srcset generation to match the rendered dimensions const aspectRatioForSizing = requiresCropping ? currentAspectRatio : naturalAspectRatio; const requiredSizes = this._calculateRequiredSizes( currentWidth, currentHeight, aspectRatioForSizing, currentDeviceType, naturalWidth, naturalHeight ); // Check if current image has existing srcset const existingSrcset = img.getAttribute('srcset'); const existingSizes = this._parseExistingSrcset(existingSrcset); // Determine missing sizes const missingSizes = this._findMissingSizes(requiredSizes, existingSizes); if (missingSizes.length === 0) { optmlLogger.info(`Image ${imageId} already has adequate srcset coverage`); return null; } // Log full analysis for debugging optmlLogger.info(`Image ${imageId} srcset analysis:`, { currentSize: { w: currentWidth, h: currentHeight }, naturalSize: { w: naturalWidth, h: naturalHeight }, requiredSizes: requiredSizes, naturalAspectRatio: Math.round(naturalAspectRatio * 1000) / 1000, currentAspectRatio: Math.round(currentAspectRatio * 1000) / 1000, aspectRatioDifference: Math.round(aspectRatioDifference * 1000) / 1000, requiresCropping: requiresCropping, aspectRatioForSizing: Math.round(aspectRatioForSizing * 1000) / 1000, deviceType: currentDeviceType, missingSizes: missingSizes, existingSrcset: existingSrcset || null }); // Additional debug logging for aspect ratio analysis optmlLogger.info(`[Optimole Debug] Image ${imageId} aspect ratio analysis:`, { natural: `${naturalWidth}x${naturalHeight} (${Math.round(naturalAspectRatio * 1000) / 1000}:1)`, current: `${currentWidth}x${currentHeight} (${Math.round(currentAspectRatio * 1000) / 1000}:1)`, difference: Math.round(aspectRatioDifference * 1000) / 1000, requiresCropping: requiresCropping, aspectRatioForSizing: Math.round(aspectRatioForSizing * 1000) / 1000, reason: requiresCropping ? 'Aspect ratio significantly different' : 'Aspect ratios match within tolerance' }); // Return both srcset data and crop status separately return { srcset: missingSizes.map(size => ({ w: size.w, h: size.h, d: size.dpr, // dpr -> d s: size.descriptor, // descriptor -> s (srcset) b: size.breakpoint // breakpoint -> b })), requiresCropping: requiresCropping }; }, /** * Determine if an image requires cropping based on object-fit and aspect ratio differences * Requires BOTH object-fit: cover AND significant aspect ratio difference to return true * @private * @param {HTMLImageElement} img - Image element to analyze * @param {number} aspectRatioDifference - Absolute difference between natural and current aspect ratios * @param {number} naturalAspectRatio - Natural image aspect ratio * @param {number} currentAspectRatio - Current displayed aspect ratio * @returns {boolean} True if the image requires cropping (both conditions must be true) */ _requiresCropping: function(img, aspectRatioDifference, naturalAspectRatio, currentAspectRatio) { // First check if the image has object-fit: cover let hasObjectFitCover = false; try { const computedStyle = window.getComputedStyle(img); hasObjectFitCover = computedStyle.objectFit === 'cover'; if (hasObjectFitCover) { optmlLogger.info(`Image has object-fit: cover`); } } catch (error) { optmlLogger.warn('Could not get computed style for object-fit check:', error); } // Define thresholds for determining when cropping is needed const ASPECT_RATIO_TOLERANCE = 0.05; // 5% tolerance for minor differences const SIGNIFICANT_DIFFERENCE_THRESHOLD = 0.15; // 15% for significant differences // If the difference is very small, no cropping needed if (aspectRatioDifference <= ASPECT_RATIO_TOLERANCE) { return false; } // Check aspect ratio conditions let aspectRatioRequiresCropping = false; // If the difference is significant, definitely needs cropping if (aspectRatioDifference >= SIGNIFICANT_DIFFERENCE_THRESHOLD) { aspectRatioRequiresCropping = true; } else { // For moderate differences, check if the current aspect ratio is significantly different // from the natural one (indicating intentional resizing that would require cropping) const ratioChange = Math.abs(currentAspectRatio - naturalAspectRatio) / naturalAspectRatio; // If the current aspect ratio is more than 10% different from natural, likely needs cropping aspectRatioRequiresCropping = ratioChange > 0.1; } // Both object-fit: cover AND aspect ratio difference must be true to require cropping const requiresCropping = hasObjectFitCover && aspectRatioRequiresCropping; if (requiresCropping) { optmlLogger.info(`Image requires cropping: object-fit cover=${hasObjectFitCover}, aspect ratio=${aspectRatioRequiresCropping}`); } return requiresCropping; }, /** * Calculate required image sizes for different breakpoints and DPR * @private * @param {number} currentWidth - Current displayed width * @param {number} currentHeight - Current displayed height * @param {number} aspectRatio - Image aspect ratio * @param {number} currentDeviceType - Current device type * @param {number} naturalWidth - Natural image width * @param {number} naturalHeight - Natural image height * @returns {Array} Array of required size objects */ _calculateRequiredSizes: function(currentWidth, currentHeight, aspectRatio, currentDeviceType, naturalWidth, naturalHeight) { const requiredSizes = []; // Generate responsive sizes based on common viewport widths and typical image usage const responsiveSizes = this._generateResponsiveSizes(currentWidth, currentHeight, aspectRatio, naturalWidth, naturalHeight); // Add all responsive sizes responsiveSizes.forEach(size => { requiredSizes.push(size); }); // Remove duplicates and sort by width const uniqueSizes = this._removeDuplicateSizes(requiredSizes); const sortedSizes = uniqueSizes.sort((a, b) => a.w - b.w); // Apply MAX_VARIATIONS limit to the final result with smart selection if (sortedSizes.length > this.CONFIG.MAX_VARIATIONS) { optmlLogger.info(`Limiting srcset variations from ${sortedSizes.length} to ${this.CONFIG.MAX_VARIATIONS}`); return this._selectBestVariations(sortedSizes, this.CONFIG.MAX_VARIATIONS); } return sortedSizes; }, /** * Parse existing srcset attribute to understand what sizes are already available * @private * @param {string|null} srcset - Existing srcset attribute value * @returns {Array} Array of existing size descriptors */ _parseExistingSrcset: function(srcset) { if (!srcset) return []; const existingSizes = []; const srcsetEntries = srcset.split(',').map(entry => entry.trim()); srcsetEntries.forEach(entry => { const parts = entry.split(/\s+/); if (parts.length >= 2) { const descriptor = parts[parts.length - 1]; // Parse width descriptor (e.g., "800w") if (descriptor.endsWith('w')) { const width = parseInt(descriptor.slice(0, -1), 10); if (!isNaN(width)) { existingSizes.push({ w: width, descriptor: descriptor, url: parts.slice(0, -1).join(' ') }); } } } }); return existingSizes; }, /** * Generate responsive sizes based on the current image ratio * Device-aware approach: generates sizes for different viewports while maintaining the current ratio * @private * @param {number} currentWidth - Current displayed width * @param {number} currentHeight - Current displayed height * @param {number} aspectRatio - Image aspect ratio * @param {number} naturalWidth - Natural image width * @param {number} naturalHeight - Natural image height * @returns {Array} Array of responsive size objects */ _generateResponsiveSizes: function(currentWidth, currentHeight, aspectRatio, naturalWidth, naturalHeight) { const sizes = []; // Calculate the current ratio: what percentage of viewport width does this image occupy? const viewportWidth = window.innerWidth; const currentRatio = Math.min(currentWidth / viewportWidth, 1.0); optmlLogger.info(`Generating srcset for current ratio: ${Math.round(currentRatio * 100)}% of viewport`); // Use configured device breakpoints const breakpoints = Object.entries(this.DEVICE_BREAKPOINTS).map(([key, viewport]) => ({ viewport, label: key.toLowerCase().replace('_', '-'), dpr: this.DPR_MULTIPLIERS })); // Generate sizes for each viewport using the current ratio breakpoints.forEach(bp => { const baseWidth = Math.round(bp.viewport * currentRatio); // Skip if the calculated width is too small or too large if (baseWidth < this.CONFIG.MIN_SIZE || baseWidth > naturalWidth * 1.2) { return; } // Generate for each DPR bp.dpr.forEach(dprValue => { const targetWidth = Math.round(baseWidth * dprValue); const targetHeight = Math.round(targetWidth / aspectRatio); // Don't generate 1x DPR variations larger than natural image size // But allow 2x DPR variations for retina displays // Fixes: srcset capped at profiling container width (issue #1030) if (dprValue === 1 && targetWidth > naturalWidth) { return; } // Only include if within reasonable bounds if (this._isValidSize(targetWidth, targetHeight, naturalWidth, naturalHeight) && targetWidth >= this.CONFIG.MIN_SIZE) { sizes.push({ w: targetWidth, h: targetHeight, dpr: dprValue, breakpoint: bp.viewport, descriptor: `${targetWidth}w`, source: 'responsive', category: `${bp.label}-${Math.round(currentRatio * 100)}`, label: `${bp.label}-${targetWidth}w${dprValue > 1 ? `-${dprValue}x` : ''}` }); } }); }); // Add current size if not already covered const currentExists = sizes.some(size => Math.abs(size.w - currentWidth) <= this.CONFIG.SIZE_TOLERANCE ); if (!currentExists && this._isValidSize(currentWidth, currentHeight, naturalWidth, naturalHeight)) { sizes.push({ w: currentWidth, h: currentHeight, dpr: 1, breakpoint: window.innerWidth, descriptor: `${currentWidth}w`, source: 'current', category: 'current', label: 'current-size' }); } return sizes; }, /** * Check if a size is valid for srcset generation * @private * @param {number} width - Target width * @param {number} height - Target height * @param {number} naturalWidth - Natural image width * @param {number} naturalHeight - Natural image height * @returns {boolean} True if size is valid */ _isValidSize: function(width, height, naturalWidth, naturalHeight) { return width >= this.CONFIG.MIN_SIZE && height >= this.CONFIG.MIN_SIZE && width <= naturalWidth && height <= naturalHeight; }, /** * Find missing sizes by comparing required vs existing * @private * @param {Array} requiredSizes - Required size variations * @param {Array} existingSizes - Existing size variations * @returns {Array} Array of missing size variations */ _findMissingSizes: function(requiredSizes, existingSizes) { const existingWidths = new Set(existingSizes.map(size => size.w)); const tolerance = this.CONFIG.SIZE_TOLERANCE; return requiredSizes.filter(required => { // Check if we already have a size within tolerance const hasCloseMatch = Array.from(existingWidths).some(existingWidth => Math.abs(existingWidth - required.w) <= tolerance ); return !hasCloseMatch; }); }, /** * Select the best variations when limiting the number of srcset sizes * Prioritizes 1x variants and ensures good coverage across the size range * @private * @param {Array} sortedSizes - Array of size objects sorted by width * @param {number} maxVariations - Maximum number of variations to select * @returns {Array} Array of selected size variations */ _selectBestVariations: function(sortedSizes, maxVariations) { if (sortedSizes.length <= maxVariations) { return sortedSizes; } // Separate 1x and 2x variants const dpr1Sizes = sortedSizes.filter(size => size.dpr === 1); const dpr2Sizes = sortedSizes.filter(size => size.dpr === 2); const selected = []; // Strategy: Prioritize 1x variants, then add strategic 2x variants const target1xCount = Math.min(dpr1Sizes.length, Math.ceil(maxVariations * 0.75)); // 75% should be 1x const target2xCount = maxVariations - target1xCount; // Select 1x variants with even distribution if (dpr1Sizes.length > 0) { if (dpr1Sizes.length <= target1xCount) { // Include all 1x variants selected.push(...dpr1Sizes); } else { // Distribute 1x variants evenly const step = (dpr1Sizes.length - 1) / (target1xCount - 1); for (let i = 0; i < target1xCount; i++) { const index = Math.round(i * step); selected.push(dpr1Sizes[index]); } } } // Select 2x variants strategically (prefer middle sizes for 2x) if (target2xCount > 0 && dpr2Sizes.length > 0) { if (dpr2Sizes.length <= target2xCount) { // Include all 2x variants selected.push(...dpr2Sizes); } else { // Select middle-range 2x variants (most useful for retina displays) const sortedDpr2 = dpr2Sizes.sort((a, b) => a.w - b.w); const middleStart = Math.floor(sortedDpr2.length / 4); const middleEnd = Math.ceil(sortedDpr2.length * 3 / 4); const middleRange = sortedDpr2.slice(middleStart, middleEnd); const step = Math.max(1, Math.floor(middleRange.length / target2xCount)); for (let i = 0; i < target2xCount && i * step < middleRange.length; i++) { selected.push(middleRange[i * step]); } } } // Remove duplicates and sort by width const uniqueSelected = selected.filter((size, index, arr) => arr.findIndex(s => s.w === size.w) === index ); return uniqueSelected.sort((a, b) => a.w - b.w); }, /** * Remove duplicate sizes from the required sizes array * @private * @param {Array} sizes - Array of size objects * @returns {Array} Array with duplicates removed */ _removeDuplicateSizes: function(sizes) { const seen = new Set(); return sizes.filter(size => { const key = `${size.w}x${size.h}`; if (seen.has(key)) { return false; } seen.add(key); return true; }); }, /** * Get a summary of srcset analysis for all images * @returns {Object} Summary statistics */ getSrcsetAnalysisSummary: function() { const allImages = document.querySelectorAll('img[data-opt-id]'); // Count images based on new logic let includedImages = 0; let skippedImages = 0; let lazyLoadedImages = 0; let nonLazyloadImages = 0; allImages.forEach(img => { const hasOptSrc = img.hasAttribute('data-opt-src'); const hasLazyLoaded = img.hasAttribute('data-opt-lazy-loaded'); const shouldInclude = !hasOptSrc || (hasOptSrc && hasLazyLoaded); if (shouldInclude) { includedImages++; if (!hasOptSrc) { nonLazyloadImages++; } else if (hasLazyLoaded) { lazyLoadedImages++; } } else { skippedImages++; } }); return { totalOptimoleImages: allImages.length, includedImages: includedImages, skippedImages: skippedImages, nonLazyloadImages: nonLazyloadImages, lazyLoadedImages: lazyLoadedImages, currentDeviceType: optmlDevice.getDeviceType(), viewportSize: { w: window.innerWidth, h: window.innerHeight } }; } };
[+]
..
[-] api.js
[edit]
[-] background.js
[edit]
[-] device.js
[edit]
[-] dom-utils.js
[edit]
[-] image-detector.js
[edit]
[-] lcp.js
[edit]
[-] logger.js
[edit]
[-] main.js
[edit]
[-] srcset-detector.js
[edit]
[-] storage.js
[edit]