CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
screenshot.ts243 linesDownload Raw Back to utils
1import html2canvas from 'html2canvas';2import { logger } from '@/lib/utils';3 4/**5 * Waits for document resources (fonts, images, idle) to finish loading.6 * All resource promises race against a timeout to prevent indefinite blocking.7 * @param doc The document to wait on8 * @param minDelay Minimum delay in ms regardless of resource readiness (default: 2000)9 * @param timeout Maximum time to wait for resources in ms (default: 8000)10 */11export async function waitForResources(doc: Document, minDelay = 2000, timeout = 8000): Promise<void> {12  const win = doc.defaultView;13 14  const resourcePromises: Promise<unknown>[] = [15    // Minimum buffer delay16    new Promise(resolve => setTimeout(resolve, minDelay)),17  ];18 19  // Wait for fonts20  if (doc.fonts?.ready) {21    resourcePromises.push(doc.fonts.ready.catch(() => {}));22  }23 24  // Wait for all <img> elements to load25  const images = doc.querySelectorAll('img');26  images.forEach((img) => {27    if (!img.complete) {28      resourcePromises.push(29        new Promise<void>(resolve => {30          img.addEventListener('load', () => resolve(), { once: true });31          img.addEventListener('error', () => resolve(), { once: true });32        })33      );34    }35  });36 37  // Wait for idle callback (indicates browser has finished layout/paint work)38  if (win) {39    resourcePromises.push(40      new Promise<void>(resolve => {41        if ('requestIdleCallback' in win) {42          (win as Window & { requestIdleCallback: (cb: () => void, opts?: { timeout: number }) => void })43            .requestIdleCallback(() => resolve(), { timeout: 500 });44        } else {45          setTimeout(resolve, 500);46        }47      })48    );49  }50 51  // Race all resource promises against timeout52  await Promise.race([53    Promise.all(resourcePromises),54    new Promise(resolve => setTimeout(resolve, timeout)),55  ]);56}57 58/**59 * Internal function to attempt screenshot capture60 */61async function attemptCapture(62  iframeDoc: Document,63  captureWidth: number,64  captureHeight: number,65  fullPage: boolean66): Promise<HTMLCanvasElement> {67  // Determine capture height based on mode68  let effectiveHeight: number;69 70  if (fullPage) {71    // Get actual document height for full-page capture72    effectiveHeight = Math.max(73      iframeDoc.body.scrollHeight,74      iframeDoc.body.offsetHeight,75      iframeDoc.documentElement.clientHeight,76      iframeDoc.documentElement.scrollHeight,77      iframeDoc.documentElement.offsetHeight78    );79    logger.debug('[Screenshot] Full-page mode: document height =', effectiveHeight);80  } else {81    // Use viewport height for initial view capture82    effectiveHeight = captureHeight;83    logger.debug('[Screenshot] Viewport-only mode: using height =', effectiveHeight);84  }85 86  logger.debug('[Screenshot] Capture dimensions:', captureWidth, 'x', effectiveHeight);87 88  return Promise.race([89    html2canvas(iframeDoc.body, {90        width: captureWidth,91        height: effectiveHeight,92        scale: 1,93        useCORS: true,94        allowTaint: true,95        logging: false,96        windowWidth: captureWidth,97        windowHeight: effectiveHeight,98        scrollX: 0,99        scrollY: 0,100        imageTimeout: 3000,101        backgroundColor: '#ffffff',102        removeContainer: true,103        // Clean up problematic elements in the cloned document104        onclone: (clonedDoc) => {105          // Remove external stylesheets that cause CORS errors106          const externalLinks = clonedDoc.querySelectorAll('link[rel="stylesheet"]');107          externalLinks.forEach((link) => {108            const href = link.getAttribute('href');109            if (href && (href.startsWith('http://') || href.startsWith('https://'))) {110              link.remove();111            }112          });113 114          // Remove ALL gradient backgrounds (not just ones with "gradient" in class name)115          // Tailwind gradients can cause "non-finite" errors in html2canvas116          const allElements = clonedDoc.querySelectorAll('*');117 118          // CRITICAL: Use cloned document's window for getComputedStyle, not parent window119          const clonedWindow = clonedDoc.defaultView;120          if (!clonedWindow) {121            return;122          }123 124          allElements.forEach((el: Element) => {125            const htmlEl = el as HTMLElement;126            // Read styles from CLONED document's context127            const computedStyle = clonedWindow.getComputedStyle(htmlEl);128            const bg = computedStyle.backgroundImage;129 130            // Check if element has a gradient background131            if (bg && (bg.includes('gradient') || bg.includes('linear-gradient') || bg.includes('radial-gradient'))) {132              // Replace gradient with solid color from gradient's first color if possible133              // or use a neutral fallback134              const bgColor = computedStyle.backgroundColor;135              htmlEl.style.backgroundImage = 'none';136              if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {137                htmlEl.style.backgroundColor = bgColor;138              } else {139                htmlEl.style.backgroundColor = '#64748b'; // slate-500 as neutral fallback140              }141            }142          });143        }144      }),145    new Promise<never>((_, reject) =>146      setTimeout(() => reject(new Error('html2canvas timeout after 4 seconds')), 4000)147    )148  ]);149}150 151export async function captureIframeScreenshot(152  iframe: HTMLIFrameElement,153  captureWidth: number = 1280,154  captureHeight: number = 720,155  outputWidth: number = 640,156  outputHeight: number = 360,157  quality: number = 0.8,158  fullPage: boolean = true,159  waitForContent: boolean = false,160  minWaitDelay: number = 1500161): Promise<string | null> {162  try {163    // Get the iframe's document164    const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;165 166    if (!iframeDoc || !iframeDoc.body) {167      logger.warn('Cannot access iframe document');168      return null;169    }170 171    // Wait for resources if requested172    if (waitForContent) {173      try {174        await waitForResources(iframeDoc, minWaitDelay);175      } catch {176        // Fall back to simple delay if resource waiting fails177        await new Promise(resolve => setTimeout(resolve, minWaitDelay));178      }179    }180 181    // Attempt capture with automatic retry on gradient errors182    let canvas: HTMLCanvasElement;183    try {184      canvas = await attemptCapture(iframeDoc, captureWidth, captureHeight, fullPage);185    } catch (firstError) {186      // Check if this is a gradient-related error187      const errorMsg = String(firstError);188      if (errorMsg.includes('non-finite') || errorMsg.includes('addColorStop') || errorMsg.includes('CanvasGradient')) {189        // Wait a bit for styles to stabilize further190        await new Promise(resolve => setTimeout(resolve, 500));191        canvas = await attemptCapture(iframeDoc, captureWidth, captureHeight, fullPage);192      } else {193        // Not a gradient error, rethrow194        throw firstError;195      }196    }197 198    // Scale down the captured image maintaining aspect ratio199    const aspectRatio = canvas.height / canvas.width;200    const scaledHeight = Math.round(outputWidth * aspectRatio);201 202    const scaledCanvas = document.createElement('canvas');203    scaledCanvas.width = outputWidth;204    scaledCanvas.height = scaledHeight;205    const ctx = scaledCanvas.getContext('2d');206 207    if (!ctx) {208      logger.error('Failed to get canvas context');209      return null;210    }211 212    // Draw the captured image scaled down maintaining aspect ratio213    ctx.drawImage(canvas, 0, 0, outputWidth, scaledHeight);214 215    // Convert scaled canvas to base64 JPEG216    const dataUrl = scaledCanvas.toDataURL('image/jpeg', quality);217 218    // Validate size (max 250KB)219    const sizeInBytes = Math.ceil((dataUrl.length * 3) / 4);220    const sizeInKB = sizeInBytes / 1024;221 222    if (sizeInKB > 250) {223      logger.warn(`Screenshot too large: ${sizeInKB.toFixed(0)}KB, trying with lower quality`);224      // Retry with lower quality using scaled canvas225      const retryDataUrl = scaledCanvas.toDataURL('image/jpeg', 0.6);226      const retrySizeInKB = Math.ceil((retryDataUrl.length * 3) / 4) / 1024;227 228      if (retrySizeInKB > 250) {229        logger.warn(`Screenshot still too large: ${retrySizeInKB.toFixed(0)}KB`);230        return retryDataUrl; // Return anyway, let VFS handle size limit231      }232 233      return retryDataUrl;234    }235 236    return dataUrl;237 238  } catch (error) {239    logger.error('Failed to capture screenshot:', error);240    return null;241  }242}243