1function generateId() { 2 return Math.random().toString(36).substring(2, 10); 3} 4 5function truncateName(name, maxLength = 12) { 6 if (name.length <= maxLength) return name; 7 const ext = name.lastIndexOf('.'); 8 if (ext > 0 && name.length - ext <= 5) { 9 const base = name.substring(0, ext); 10 const extension = name.substring(ext); 11 const available = maxLength - extension.length - 1; 12 if (available > 3) { 13 return base.substring(0, available) + 'â¦' + extension; 14 } 15 } 16 return name.substring(0, maxLength - 1) + 'â¦'; 17} 18 19function formatFileSize(bytes) { 20 if (bytes == null || isNaN(bytes)) return '0 B'; 21 if (bytes < 1024) return bytes + ' B'; 22 if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; 23 return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; 24} 25 26function getFileExtension(filename) { 27 const ext = filename.split('.').pop().toLowerCase(); 28 if (ext === 'jpeg') return 'jpg'; 29 if (ext === 'heif') return 'heic'; 30 return ext; 31} 32 33function calculateSavings(original, compressed) { 34 if (!original || original === 0) return 0; 35 return Math.round((1 - compressed / original) * 100); 36} 37 38function shimmerHint(element, shimmeredElements) { 39 if (!element || shimmeredElements.has(element)) return; 40 shimmeredElements.add(element); 41 element.classList.remove('hint-shimmer'); 42 void element.offsetWidth; 43 element.classList.add('hint-shimmer'); 44 element.addEventListener('animationend', () => { 45 element.classList.remove('hint-shimmer'); 46 }, { once: true }); 47} 48 49function createDragDropHandlers(dropZone, onFiles) { 50 function handleDragOver(e) { 51 e.preventDefault(); 52 e.stopPropagation(); 53 dropZone.classList.add('files__list-outer_dragover'); 54 } 55 56 function handleDragLeave(e) { 57 e.preventDefault(); 58 e.stopPropagation(); 59 dropZone.classList.remove('files__list-outer_dragover'); 60 } 61 62 function handleDrop(e) { 63 e.preventDefault(); 64 e.stopPropagation(); 65 dropZone.classList.remove('files__list-outer_dragover'); 66 const droppedFiles = e.dataTransfer.files; 67 if (droppedFiles && droppedFiles.length > 0) { 68 onFiles(droppedFiles); 69 } 70 } 71 72 return { handleDragOver, handleDragLeave, handleDrop }; 73} 74 75function createScrollControls(scrollContainer, prevBtn, nextBtn, scrollAmount = 200) { 76 function updateScrollButtons() { 77 if (!scrollContainer) return; 78 const { scrollLeft, scrollWidth, clientWidth } = scrollContainer; 79 const canScrollLeft = scrollLeft > 0; 80 const canScrollRight = scrollLeft + clientWidth < scrollWidth - 1; 81 if (prevBtn) prevBtn.disabled = !canScrollLeft; 82 if (nextBtn) nextBtn.disabled = !canScrollRight; 83 } 84 85 function scrollToEnd() { 86 if (!scrollContainer) return; 87 setTimeout(() => { 88 scrollContainer.scrollTo({
89 left: scrollContainer.scrollWidth, 90 behavior: 'smooth' 91 }); 92 }, 100); 93 } 94 95 function scrollBy(amount) { 96 if (!scrollContainer) return; 97 98 const maxScroll = scrollContainer.scrollWidth - scrollContainer.clientWidth; 99 100 if (amount < 0 && scrollContainer.scrollLeft < Math.abs(amount)) { 101 scrollContainer.scrollTo({ left: 0, behavior: 'smooth' }); 102 return; 103 } 104 105 if (amount > 0 && scrollContainer.scrollLeft + amount > maxScroll - 10) { 106 scrollContainer.scrollTo({ left: maxScroll, behavior: 'smooth' }); 107 return; 108 } 109 110 scrollContainer.scrollBy({ 111 left: amount, 112 behavior: 'smooth' 113 }); 114 } 115 116 if (prevBtn) prevBtn.addEventListener('click', () => scrollBy(-scrollAmount)); 117 if (nextBtn) nextBtn.addEventListener('click', () => scrollBy(scrollAmount)); 118 if (scrollContainer) scrollContainer.addEventListener('scroll', updateScrollButtons); 119 120 return { updateScrollButtons, scrollToEnd, scrollBy }; 121} 122 123function downloadBlob(url, filename) { 124 const a = document.createElement('a'); 125 a.href = url; 126 a.download = filename; 127 document.body.appendChild(a); 128 a.click(); 129 document.body.removeChild(a); 130} 131 132function downloadMultiple(files, delay = 100) { 133 return new Promise((resolve) => {
134 let completed = 0; 135 files.forEach((file, index) => { 136 setTimeout(() => { 137 downloadBlob(file.url, file.filename); 138 completed++; 139 if (completed >= files.length) { 140 resolve(); 141 } 142 }, index * delay); 143 }); 144 }); 145} 146 147function imageDims(source) { 148 if (!source) return null; 149 const w = source.imageWidth; 150 const h = source.imageHeight; 151 if (typeof w === 'number' && w > 0 && typeof h === 'number' && h > 0) { 152 return { width: w, height: h }; 153 } 154 return null; 155} 156 157function reportError(error, context = {}) { 158 try { 159 const err = error instanceof Error ? error : new Error(String(error)); 160 const config = window.APP_CONFIG || {}; 161 162 const errDims = imageDims(err); 163 const ctx = (errDims && context.width === undefined) ? { ...context, ...errDims } : context; 164 165 const payload = JSON.stringify({ 166 error: err.message, 167 stack: err.stack || '', 168 context: ctx, 169 site: config.siteKey || '', 170 ua: navigator.userAgent, 171 url: location.pathname 172 }); 173 174 if (navigator.sendBeacon) { 175 navigator.sendBeacon('/api/error', new Blob([payload], { type: 'application/json' })); 176 } else { 177 fetch('/api/error', { 178 method: 'POST', 179 headers: { 'Content-Type': 'application/json' }, 180 body: payload, 181 keepalive: true 182 }).catch(() => {}); 183 } 184 } catch (_) {} 185} 186 187function successRate(setting) { 188 if (setting === true) return 1; 189 if (typeof setting === 'number' && setting > 0) return Math.min(setting, 1); 190 return 0; 191} 192 193function reportSuccess(context = {}) { 194 try { 195 const config = window.APP_CONFIG || {}; 196 const rate = successRate(config.trackSuccess); 197 if (rate <= 0) return; 198 if (rate < 1 && Math.random() >= rate) return; 199 200 const payload = JSON.stringify({ 201 site: config.siteKey || '', 202 context: rate < 1 ? { ...context, rate } : context, 203 url: location.pathname 204 }); 205 206 if (navigator.sendBeacon) { 207 navigator.sendBeacon('/api/success', new Blob([payload], { type: 'application/json' })); 208 } else { 209 fetch('/api/success', { 210 method: 'POST', 211 headers: { 'Content-Type': 'application/json' }, 212 body: payload, 213 keepalive: true 214 }).catch(() => {}); 215 } 216 } catch (_) {} 217} 218 219async function detectFormat(file) { 220 try { 221 const buf = await file.slice(0, 16).arrayBuffer(); 222 const b = new Uint8Array(buf); 223 if (b.length < 4) return null; 224 225 if (b[0] === 0xFF && b[1] === 0xD8 && b[2] === 0xFF) return 'jpg'; 226 227 if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4E && b[3] === 0x47) return 'png'; 228 229 if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x38) return 'gif'; 230 231 if (b[0] === 0x42 && b[1] === 0x4D) return 'bmp'; 232 233 if ((b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2A && b[3] === 0x00) || 234 (b[0] === 0x4D && b[1] === 0x4D && b[2] === 0x00 && b[3] === 0x2A)) return 'tiff'; 235 236 if (b[0] === 0x25 && b[1] === 0x50 && b[2] === 0x44 && b[3] === 0x46) return 'pdf'; 237 238 if (b[0] === 0x1F && b[1] === 0x8B) return 'svgz'; 239 240 if (b[0] === 0x38 && b[1] === 0x42 && b[2] === 0x50 && b[3] === 0x53) return 'psd'; 241 242 if (b.length >= 12 && b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 && 243 b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50) return 'webp'; 244 245 if (b.length >= 12 && b[4] === 0x66 && b[5] === 0x74 && b[6] === 0x79 && b[7] === 0x70) { 246 const brand = String.fromCharCode(b[8], b[9], b[10], b[11]); 247 if (brand === 'avif' || brand === 'avis') return 'avif'; 248 return 'heic'; 249 } 250 251 const textBuf = await file.slice(0, 256).arrayBuffer(); 252 const text = new TextDecoder().decode(new Uint8Array(textBuf)); 253 const trimmed = text.trimStart(); 254 if (trimmed.startsWith('<svg') || (trimmed.startsWith('<?xml') && text.includes('<svg'))) return 'svg'; 255 256 return null; 257 } catch { 258 return null; 259 } 260} 261 262function isWasmCrashError(errorMessage) { 263 if (!errorMessage) return false; 264 const msg = String(errorMessage).toLowerCase(); 265 return msg.includes('memory access out of bounds') || 266 msg.includes('out of bounds memory access') || 267 msg.includes('exit(1)') || 268 msg.includes('abort(') || 269 msg.includes('unreachable') || 270 msg.includes('runtimeerror'); 271} 272 273function classifyError(msg, errors) { 274 if (!errors) errors = {}; 275 if (!msg) return errors.processing || ''; 276 const m = msg.toLowerCase(); 277 if (m.includes('could not be decoded') || m.includes('cannot decode') || m.includes('size of offscreencanvas is zero') || m.includes('readback') || m.includes('createimagebitmap') || m.includes('encoding') || m.includes('invalid image') || m.includes('not a valid') || m.includes('invalid state')) return errors.corrupt || ''; 278 if (m.includes('memory access') || m.includes('out of memory') || m.includes('abort(') || m.includes('exit(1)') || m.includes('unreachable') || m.includes('wasm worker crashed')) return errors.tooLarge || ''; 279 if (m.includes('failed to fetch') || m.includes('dynamically imported module') || m.includes('load failed')) return errors.loading || ''; 280 if (m.includes('max buffer length') || m.includes('non-whitespace before first tag') || m.includes('invalid svg') || m.includes('entity reference')) return errors.svg || ''; 281 return errors.processing || ''; 282} 283 284window.SharedUtils = { 285 generateId, 286 truncateName, 287 formatFileSize, 288 getFileExtension, 289 calculateSavings, 290 shimmerHint, 291 createDragDropHandlers, 292 createScrollControls, 293 downloadBlob, 294 downloadMultiple, 295 reportError, 296 reportSuccess, 297 imageDims, 298 detectFormat, 299 isWasmCrashError, 300 classifyError 301}; 302 303window.generateId = generateId; 304window.truncateName = truncateName; 305window.formatFileSize = formatFileSize; 306window.getFileExtension = getFileExtension; 307window.calculateSavings = calculateSavings; 308window.reportError = reportError; 309window.reportSuccess = reportSuccess; 310window.imageDims = imageDims; 311window.detectFormat = detectFormat; 312window.isWasmCrashError = isWasmCrashError; 313window.classifyError = classifyError;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.