# Issue #008: Screenshot de Elementos Específicos **Tipo**: Enhancement **Prioridad**: Media **Estado**: Pendiente ## Descripción Implementar capacidad de tomar screenshots de elementos específicos de la página en lugar de solo página completa. ## Funcionalidad deseada ### Operaciones - Screenshot de elemento específico por selector CSS - Screenshot de región (coordenadas x, y, width, height) - Screenshot con padding/margin alrededor del elemento - Scroll automático al elemento antes de capturar - Esperar a que elemento sea visible antes de capturar - Captura de múltiples elementos en batch - Captura con o sin sombras CSS ## Implementación técnica ### Archivo sugerido Extender `pkg/browser/navigation.go` o crear `pkg/browser/screenshots.go` ### CDP Methods - **DOM.getBoxModel** - Obtener dimensiones del elemento - **Page.captureScreenshot** - Capturar con clip region ### API propuesta ```go // ScreenshotElementOptions opciones para screenshot de elemento type ScreenshotElementOptions struct { Format string // "png" o "jpeg" (default: png) Quality int // 0-100 para JPEG (default: 80) Padding int // Padding en pixels alrededor del elemento WaitVisible bool // Esperar a que sea visible (default: true) ScrollIntoView bool // Scroll al elemento antes (default: true) OmitBackground bool // Fondo transparente (default: false) } // DefaultScreenshotElementOptions retorna opciones por defecto func DefaultScreenshotElementOptions() *ScreenshotElementOptions // ScreenshotElement toma screenshot de un elemento específico func (b *Browser) ScreenshotElement(ctx context.Context, selector string, opts *ScreenshotElementOptions) ([]byte, error) // ScreenshotElementToFile guarda screenshot de elemento a archivo func (b *Browser) ScreenshotElementToFile(ctx context.Context, selector string, filepath string, opts *ScreenshotElementOptions) error // ScreenshotRegion toma screenshot de región específica func (b *Browser) ScreenshotRegion(ctx context.Context, x, y, width, height int) ([]byte, error) // ScreenshotElements toma screenshots de múltiples elementos func (b *Browser) ScreenshotElements(ctx context.Context, selectors []string, opts *ScreenshotElementOptions) (map[string][]byte, error) ``` ## Casos de uso ### Caso 1: Screenshot de botón específico ```go opts := browser.DefaultScreenshotElementOptions() opts.Padding = 10 // 10px de margen screenshot, _ := b.ScreenshotElement(ctx, "#submit-button", opts) os.WriteFile("button.png", screenshot, 0644) ``` ### Caso 2: Screenshot de cada producto ```go products := []string{ ".product:nth-child(1)", ".product:nth-child(2)", ".product:nth-child(3)", } screenshots, _ := b.ScreenshotElements(ctx, products, nil) for selector, data := range screenshots { filename := strings.ReplaceAll(selector, ":", "-") + ".png" os.WriteFile(filename, data, 0644) } ``` ### Caso 3: Screenshot con fondo transparente ```go opts := &browser.ScreenshotElementOptions{ Format: "png", OmitBackground: true, // PNG transparente } screenshot, _ := b.ScreenshotElement(ctx, ".icon", opts) ``` ### Caso 4: Screenshot de región específica ```go // Capturar área de 300x200 en posición (100, 150) screenshot, _ := b.ScreenshotRegion(ctx, 100, 150, 300, 200) ``` ## Implementación interna ```go func (b *Browser) ScreenshotElement(ctx context.Context, selector string, opts *ScreenshotElementOptions) ([]byte, error) { if opts == nil { opts = DefaultScreenshotElementOptions() } // 1. Esperar a que elemento sea visible si se especificó if opts.WaitVisible { if err := b.WaitForElement(ctx, selector, nil); err != nil { return nil, fmt.Errorf("element not visible: %w", err) } } // 2. Scroll al elemento si se especificó if opts.ScrollIntoView { script := fmt.Sprintf(` document.querySelector('%s').scrollIntoView({ behavior: 'instant', block: 'center' }) `, selector) b.Evaluate(ctx, script) } // 3. Obtener dimensiones del elemento var result struct { Model struct { Content []float64 `json:"content"` // [x1, y1, x2, y2, x3, y3, x4, y4] } `json:"model"` } // Primero obtener nodeId nodeID, err := b.querySelector(ctx, selector) if err != nil { return nil, err } // Obtener box model if err := b.cdpClient.Execute(ctx, "DOM.getBoxModel", map[string]interface{}{ "nodeId": nodeID, }, &result); err != nil { return nil, fmt.Errorf("failed to get box model: %w", err) } // Calcular clip region content := result.Model.Content x := content[0] y := content[1] width := content[4] - content[0] height := content[5] - content[1] // Aplicar padding if opts.Padding > 0 { x -= float64(opts.Padding) y -= float64(opts.Padding) width += float64(opts.Padding * 2) height += float64(opts.Padding * 2) } // 4. Capturar screenshot con clip params := map[string]interface{}{ "format": opts.Format, "clip": map[string]interface{}{ "x": x, "y": y, "width": width, "height": height, "scale": 1, }, } if opts.OmitBackground { params["captureBeyondViewport"] = true params["fromSurface"] = true } if opts.Format == "jpeg" && opts.Quality > 0 { params["quality"] = opts.Quality } var screenshotResult struct { Data string `json:"data"` } if err := b.cdpClient.Execute(ctx, "Page.captureScreenshot", params, &screenshotResult); err != nil { return nil, fmt.Errorf("failed to capture screenshot: %w", err) } // 5. Decodificar base64 data, err := base64.StdEncoding.DecodeString(screenshotResult.Data) if err != nil { return nil, fmt.Errorf("failed to decode screenshot: %w", err) } return data, nil } ``` ## Comandos CDP ### Obtener dimensiones del elemento ```json { "method": "DOM.getBoxModel", "params": { "nodeId": 123 } } // Response: { "model": { "content": [x1, y1, x2, y2, x3, y3, x4, y4], "padding": [...], "border": [...], "margin": [...], "width": 200, "height": 100 } } ``` ### Capturar con clip ```json { "method": "Page.captureScreenshot", "params": { "format": "png", "clip": { "x": 100, "y": 200, "width": 300, "height": 150, "scale": 1 }, "captureBeyondViewport": true } } ``` ## Casos de uso avanzados ### Comparación visual ```go // Capturar antes y después de una acción before, _ := b.ScreenshotElement(ctx, "#component", nil) b.Click(ctx, "#toggle-button") after, _ := b.ScreenshotElement(ctx, "#component", nil) // Comparar imágenes if !bytes.Equal(before, after) { log.Println("El componente cambió visualmente") } ``` ### Generación de thumbnails ```go opts := &browser.ScreenshotElementOptions{ Format: "jpeg", Quality: 60, // Compresión para thumbnails } // Capturar todos los artículos articles := []string{".article-1", ".article-2", ".article-3"} thumbnails, _ := b.ScreenshotElements(ctx, articles, opts) ``` ### Screenshot de elemento fuera de viewport ```go // Elemento muy abajo en la página opts := &browser.ScreenshotElementOptions{ ScrollIntoView: true, // Scroll automático WaitVisible: true, } screenshot, _ := b.ScreenshotElement(ctx, "#footer-logo", opts) ``` ## Mejoras adicionales ### Screenshot de elemento con sombra ```go // Incluir box-shadow en captura opts.IncludeShadow = true ``` ### Screenshot de elemento rotado ```go // Calcular bounding box considerando rotación CSS opts.ConsiderTransform = true ``` ### Screenshot de SVG específico ```go // Elementos SVG pueden necesitar manejo especial screenshot, _ := b.ScreenshotElement(ctx, "svg#chart", opts) ``` ## Referencias - CDP DOM.getBoxModel: https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getBoxModel - CDP Page.captureScreenshot: https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot - Playwright elementHandle.screenshot: https://playwright.dev/docs/api/class-elementhandle#element-handle-screenshot - Puppeteer element screenshots: https://pptr.dev/api/puppeteer.elementhandle.screenshot