package browser import ( "encoding/base64" "fmt" "os" "path/filepath" ) // CdpScreenshotOpts configura el screenshot. type CdpScreenshotOpts struct { // FullPage indica si capturar la pagina completa (scroll height) o solo el viewport. FullPage bool // Quality es la calidad JPEG (0-100). Solo aplica si Format es "jpeg". Por defecto 80. Quality int // Format es el formato de imagen: "png" o "jpeg". Por defecto "png". Format string } // fullPageClip es el rectangulo de recorte (en CSS pixels) que cubre la pagina // completa. scale=1 mantiene la resolucion nativa. type fullPageClip struct { X, Y, Width, Height, Scale float64 } // buildFullPageClip construye el clip de pagina completa a partir de la respuesta // de Page.getLayoutMetrics. Es una funcion pura: no toca red, recibe el mapa ya // deserializado por CDP y decide el rectangulo. // // Prefiere cssContentSize (dimensiones en CSS pixels, ya divididas por el DPR), // que es lo que espera el campo "clip" de Page.captureScreenshot. Cae a // contentSize (device pixels, protocolo antiguo) si cssContentSize no esta // presente. Devuelve ok=false cuando no hay un tamano valido (>0 en ambos ejes), // para que el caller capture solo el viewport en vez de un clip degenerado. func buildFullPageClip(metrics map[string]any) (fullPageClip, bool) { asFloat := func(v any) float64 { f, _ := v.(float64) return f } for _, key := range []string{"cssContentSize", "contentSize"} { size, ok := metrics[key].(map[string]any) if !ok { continue } w := asFloat(size["width"]) h := asFloat(size["height"]) if w > 0 && h > 0 { return fullPageClip{X: 0, Y: 0, Width: w, Height: h, Scale: 1}, true } } return fullPageClip{}, false } // CdpScreenshotBytes captura un screenshot de la pagina actual y devuelve los // bytes de imagen ya decodificados junto con su mimeType, sin tocar el disco. // Usa Page.captureScreenshot del protocolo CDP. // // El mimeType es "image/jpeg" cuando opts pide JPEG y "image/png" en cualquier // otro caso (incluido el default cuando opts.Format esta vacio). // // Si opts.FullPage es true, consulta Page.getLayoutMetrics para construir un clip // que cubra la altura completa del documento (no solo el viewport) y mantiene // captureBeyondViewport=true para que Chrome renderice mas alla del area visible. // // Es la primitiva reutilizable de captura: util para devolver la imagen al LLM // como image content (bytes) sin pasar por archivo. CdpScreenshot compone sobre // ella para persistir a disco. func CdpScreenshotBytes(c *CDPConn, opts CdpScreenshotOpts) ([]byte, string, error) { if c == nil { return nil, "", fmt.Errorf("cdp screenshot: conexion nula") } if opts.Format == "" { opts.Format = "png" } if opts.Quality == 0 && opts.Format == "jpeg" { opts.Quality = 80 } mimeType := "image/png" if opts.Format == "jpeg" { mimeType = "image/jpeg" } params := map[string]any{ "format": opts.Format, "captureBeyondViewport": opts.FullPage, } if opts.Format == "jpeg" { params["quality"] = opts.Quality } if opts.FullPage { // Page.getLayoutMetrics da el tamano real del documento. Construimos el // clip con la funcion pura buildFullPageClip. Si la consulta falla o no // hay dimensiones validas, omitimos el clip y caemos a captura normal // (con captureBeyondViewport=true Chrome aun captura algo razonable). if metrics, err := c.sendCDP("Page.getLayoutMetrics", nil); err == nil { if clip, ok := buildFullPageClip(metrics); ok { params["clip"] = map[string]any{ "x": clip.X, "y": clip.Y, "width": clip.Width, "height": clip.Height, "scale": clip.Scale, } } } } result, err := c.sendCDP("Page.captureScreenshot", params) if err != nil { return nil, "", fmt.Errorf("cdp screenshot: %w", err) } dataStr, ok := result["data"].(string) if !ok { return nil, "", fmt.Errorf("cdp screenshot: campo data ausente en respuesta") } imgData, err := base64.StdEncoding.DecodeString(dataStr) if err != nil { return nil, "", fmt.Errorf("cdp screenshot: decodificar base64: %w", err) } return imgData, mimeType, nil } // CdpScreenshot captura un screenshot de la pagina actual y lo guarda en outputPath. // outputPath debe tener extension .png o .jpg/.jpeg segun el formato elegido. // // Compone sobre CdpScreenshotBytes para obtener los bytes de imagen y luego crea // el directorio destino si no existe y escribe el archivo. Mismo comportamiento // observable que antes: mismos parametros, mismos efectos en disco, mismos // errores de captura. func CdpScreenshot(c *CDPConn, outputPath string, opts CdpScreenshotOpts) error { imgData, _, err := CdpScreenshotBytes(c, opts) if err != nil { return err } // Crear directorio si no existe if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil { return fmt.Errorf("cdp screenshot: crear directorio: %w", err) } if err := os.WriteFile(outputPath, imgData, 0644); err != nil { return fmt.Errorf("cdp screenshot: guardar archivo: %w", err) } return nil }