Toma capturas de páginas web o archivos HTML en .NET (C#)
Usa Selenium WebDriver para abrir una página en Chrome sin interfaz gráfica y guardar su área de visualización visible como PNG. La aplicación de consola en C# que aparece a continuación acepta una ruta HTML local o una URL HTTP, espera un elemento que indica que el contenido está listo y guarda la captura sin reemplazar un archivo existente.
Requisitos previos
Este tutorial usa Bash en Linux, el SDK de .NET 10 y un navegador
Chrome o Chromium instalado con una versión compatible de ChromeDriver. Se probó con el SDK
10.0.111 y Chromium/ChromeDriver 152.0.7977.82. Instala las actualizaciones de mantenimiento vigentes
para .NET 10, que es una versión LTS con soporte activo.
El ejemplo HTTP local opcional también necesita una instalación de Python 3 que siga recibiendo
mantenimiento y tenga las opciones http.server --bind y
--directory; se probó con Python 3.14.7.
Renderiza solo páginas y archivos HTML en los que confíes. Un navegador puede ejecutar su JavaScript y realizar solicitudes de red. Este ejemplo captura el área de visualización, incluidas las zonas en blanco; no captura una página completa con desplazamiento, no valida los códigos de estado HTTP ni determina si una aplicación arbitraria terminó de cargarse.
Comprueba las herramientas instaladas antes de crear el proyecto:
dotnet --version && chromium --version && chromedriver --version
Para el ejemplo HTTP, comprueba Python y confirma que su ayuda incluya ambas opciones antes de crear el proyecto:
python3 --version && python3 -m http.server --help
Si usas Google Chrome, sustituye chromium por su ejecutable. Configura estas
rutas para que apunten al navegador y al controlador compatible que tienes en tu equipo. Estas son
las rutas usadas en este ejemplo de Linux:
export CHROME_BINARY=/usr/bin/chromium &&
export CHROMEDRIVER_PATH=/usr/bin/chromedriver
Usa Selenium WebDriver
Comienza en un directorio con permisos de escritura fuera de un repositorio .NET existente. La
configuración del SDK, NuGet y MSBuild en directorios superiores puede cambiar el comportamiento de
un proyecto nuevo. El bloque de configuración rechaza esos archivos en directorios superiores y un
destino WebpageScreenshots existente antes de crear cualquier cosa. Ejecútalo desde Bash:
(
cd -P . || exit 1
parent="$PWD"
while :; do
for config in global.json NuGet.Config NuGet.config nuget.config \
Directory.Build.props Directory.Build.targets Directory.Packages.props; do
if [ -e "$parent/$config" ]; then
printf 'Choose a directory outside this .NET configuration: %s\n' "$parent/$config" >&2
exit 1
fi
done
[ "$parent" = / ] && break
parent=${parent%/*}
[ -n "$parent" ] || parent=/
done
if [ -e WebpageScreenshots ]; then
printf 'WebpageScreenshots already exists; keep it and choose a new directory.\n' >&2
exit 1
fi
dotnet new console -n WebpageScreenshots --framework net10.0 --no-restore &&
dotnet add WebpageScreenshots package Selenium.WebDriver --version 4.28.0 &&
dotnet add WebpageScreenshots package Selenium.Support --version 4.28.0
)
Los paquetes están fijados a la versión probada de Selenium 4.28.0. Si la instalación de paquetes falla, conserva el proyecto nuevo, resuelve el problema de NuGet o de red indicado y vuelve a intentar los pasos de instalación de dependencias desde el mismo directorio padre:
dotnet add WebpageScreenshots package Selenium.WebDriver --version 4.28.0 &&
dotnet add WebpageScreenshots package Selenium.Support --version 4.28.0
Reemplaza el archivo WebpageScreenshots/Program.cs generado por este programa completo. Los argumentos
son input output.png width height ready-element-id timeout-seconds. El ancho y el alto describen el área de visualización en píxeles
CSS; el tiempo de espera se aplica por separado a la navegación y a la espera de contenido listo.
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
try
{
if (args.Length == 3 && args[0] == "cleanup")
{
int days = int.Parse(args[2], CultureInfo.InvariantCulture);
if (days < 1) throw new ArgumentException("Retention must be at least one day.");
string directory = Path.GetFullPath(args[1]);
DateTime cutoff = DateTime.UtcNow.AddDays(-days);
foreach (string file in Directory.EnumerateFiles(directory, "webshot-*.png"))
{
if (Regex.IsMatch(Path.GetFileName(file), @"\Awebshot-[A-Za-z0-9_-]+\.png\z")
&& File.GetLastWriteTimeUtc(file) < cutoff)
{
File.Delete(file);
Console.WriteLine($"Deleted {file}");
}
}
return 0;
}
if (args.Length != 6)
throw new ArgumentException("Usage: input output.png width height ready-element-id timeout-seconds; or cleanup directory days");
int width = int.Parse(args[2], CultureInfo.InvariantCulture);
int height = int.Parse(args[3], CultureInfo.InvariantCulture);
int seconds = int.Parse(args[5], CultureInfo.InvariantCulture);
if (width < 200 || width > 3840 || height < 200 || height > 2160)
throw new ArgumentException("Choose a viewport from 200x200 to 3840x2160.");
if (seconds < 1 || seconds > 60 || string.IsNullOrWhiteSpace(args[4]))
throw new ArgumentException("Supply a readiness element ID and a timeout from 1 to 60 seconds.");
string url;
if (Uri.TryCreate(args[0], UriKind.Absolute, out Uri? remote)
&& (remote.Scheme == "http" || remote.Scheme == "https"))
{
url = remote.AbsoluteUri;
}
else
{
string path = Path.GetFullPath(args[0]);
if (!File.Exists(path)) throw new FileNotFoundException("HTML file not found.", path);
// Escape each Linux path segment so literal %, #, and ? keep their filename identity.
url = "file://" + string.Join("/", path.Split('/').Select(Uri.EscapeDataString));
}
string outputPath = Path.GetFullPath(args[1]);
if (!outputPath.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("The output filename must end in .png.");
if (File.Exists(outputPath) || Directory.Exists(outputPath))
throw new IOException("Output already exists; choose a new filename.");
string browser = Environment.GetEnvironmentVariable("CHROME_BINARY")
?? throw new ArgumentException("Set CHROME_BINARY to your browser executable.");
string driverPath = Environment.GetEnvironmentVariable("CHROMEDRIVER_PATH")
?? throw new ArgumentException("Set CHROMEDRIVER_PATH to your driver executable.");
if (!File.Exists(browser) || !File.Exists(driverPath))
throw new FileNotFoundException("Browser or ChromeDriver executable not found.");
var options = new ChromeOptions { BinaryLocation = browser };
options.AddArgument("--headless=new");
using var service = ChromeDriverService.CreateDefaultService(
Path.GetDirectoryName(driverPath), Path.GetFileName(driverPath));
byte[] png;
object result;
using (var driver = new ChromeDriver(service, options, TimeSpan.FromSeconds(30)))
{
driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(seconds);
driver.Manage().Window.Size = new System.Drawing.Size(Math.Max(800, width), Math.Max(600, height));
driver.ExecuteCdpCommand("Emulation.setDeviceMetricsOverride", new Dictionary<string, object>
{
["width"] = width,
["height"] = height,
["deviceScaleFactor"] = 1,
["mobile"] = false
});
driver.Navigate().GoToUrl(url);
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(seconds));
wait.Until(d => d.FindElement(By.Id(args[4])).Displayed);
png = driver.TakeScreenshot().AsByteArray;
var window = driver.Manage().Window.Size;
result = new
{
url = driver.Url,
viewportWidth = driver.ExecuteScript("return window.innerWidth"),
viewportHeight = driver.ExecuteScript("return window.innerHeight"),
windowWidth = window.Width,
windowHeight = window.Height,
devicePixelRatio = driver.ExecuteScript("return window.devicePixelRatio")
};
}
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
using (var output = new FileStream(outputPath, FileMode.CreateNew, FileAccess.Write))
output.Write(png);
Console.WriteLine($"Saved {args[1]}");
Console.WriteLine(JsonSerializer.Serialize(result));
return 0;
}
catch (Exception error)
{
Console.Error.WriteLine($"Screenshot failed: {error.Message}");
return 1;
}
Los bloques using liberan los recursos del navegador y del servicio del
controlador tanto al finalizar normalmente como cuando se producen excepciones. El programa almacena
el PNG en un búfer antes de abrir el destino, por lo que un fallo de navegación o de espera de
contenido listo no crea ninguna captura. FileMode.CreateNew también rechaza un archivo de
salida creado después de la comprobación inicial. Un fallo de escritura en disco puede dejar un
archivo nuevo incompleto; inspecciona y elimina solo ese archivo antes de volver a intentarlo.
Compatibilidad con navegadores
Esta implementación selecciona Chrome y ChromeDriver explícitamente. Mantén sus versiones compatibles; la guía de selección de versiones de ChromeDriver explica cómo obtener un par compatible. Selenium Manager puede detectar y descargar controladores, pero este programa usa las rutas que proporcionaste. Firefox y Edge necesitan sus respectivas clases de controlador y opciones; quedan fuera de este tutorial.
Captura archivos HTML locales
Guarda esta página como WebpageScreenshots/sample.html. Tiene un fondo azul y una tarjeta blanca con
el texto LOCAL PAGE. El marcador de contenido listo está presente de inmediato:
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Local screenshot example</title>
<style>
html, body { margin: 0; background: #0c5393; }
#content-loaded { margin: 32px; padding: 24px; background: white; color: black;
font: bold 32px sans-serif; }
</style>
<h1 id="content-loaded">LOCAL PAGE</h1>
</html>
Compila y ejecuta desde el directorio padre. && impide que una compilación
fallida ejecute un binario anterior, y -- pasa los argumentos restantes a
la aplicación:
dotnet build WebpageScreenshots --disable-build-servers &&
dotnet run --project WebpageScreenshots --no-build -- \
WebpageScreenshots/sample.html screenshots/webshot-local.png 800 600 content-loaded 10
Si la operación tiene éxito, la terminal imprime Saved screenshots/webshot-local.png seguido de un JSON
que describe la URL real, el área de visualización, la ventana exterior y la relación de píxeles.
Abre ese PNG: debería medir 800 × 600 píxeles, con la tarjeta
LOCAL PAGE completa cerca de la parte superior y un fondo azul
que se extienda hasta la parte inferior.
El área de visualización indicada en el JSON debería ser de 800 × 600, con una relación de píxeles
de uno. Un PNG que no esté vacío no demuestra por sí solo que hayas capturado la página prevista.
Maneja contenido dinámico
La navegación espera a que se cargue el documento, pero JavaScript puede actualizar la página después. Usa un elemento visible que indique que el contenido está listo y que tu aplicación muestre solo cuando el contenido deseado esté preparado. Un contenedor visible que exista mientras sus datos siguen cargándose no es una señal adecuada.
Guarda esta segunda página como WebpageScreenshots/delayed.html. La tarjeta comienza oculta y aparece
tras 1,5 segundos, con un fondo verde que permite detectar fácilmente una captura prematura:
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Delayed screenshot example</title>
<style>
html, body { margin: 0; background: white; }
#content-loaded { margin: 32px; padding: 24px; background: white; color: black;
font: bold 32px sans-serif; }
</style>
<h1 id="content-loaded" hidden>DELAYED PAGE</h1>
<script>
setTimeout(() => {
document.documentElement.style.background = '#198754';
document.body.style.background = '#198754';
document.getElementById('content-loaded').hidden = false;
}, 1500);
</script>
</html>
Para tener una fuente HTTP reproducible, comprueba python3 --version y ejecuta este
servidor en primer plano en una segunda terminal desde el mismo directorio padre. El servidor
sirve el directorio seleccionado a través de la interfaz de bucle local:
python3 -m http.server 8765 --bind 127.0.0.1 --directory WebpageScreenshots
Si ese puerto está ocupado, elige uno libre y actualiza ambas URL que aparecen a continuación. Mantén el servidor en ejecución mientras capturas la página estática y la de contenido retrasado:
dotnet run --project WebpageScreenshots --no-build -- \
http://127.0.0.1:8765/sample.html screenshots/webshot-http.png 800 600 content-loaded 10 &&
dotnet run --project WebpageScreenshots --no-build -- \
http://127.0.0.1:8765/delayed.html screenshots/webshot-delayed.png 800 600 content-loaded 10
Abre ambos archivos. La imagen estática debería coincidir con la página local azul; la imagen de
contenido retrasado debería mostrar
DELAYED PAGE en su tarjeta blanca sobre fondo verde, a 800 × 600 píxeles.
Si la imagen de contenido retrasado es completamente blanca, significa que hiciste la captura
demasiado pronto. Al terminar, detén el servidor con Ctrl+C en su terminal.
Un marcador ausente o permanentemente oculto hace que la CLI termine con el código de salida uno y
sin un PNG nuevo cuando vence el tiempo de espera. Para una aplicación real, reemplaza
content-loaded por el ID real de su marcador de contenido listo.
Gestiona el almacenamiento de capturas
Guarda las capturas en un directorio que te pertenezca y elige un nombre de archivo nuevo para cada imagen que conserves. Repetir un comando cuyo destino ya existe falla y conserva ese archivo. La captura no modifica los otros PNG del directorio.
Si quieres aplicar una política de conservación opcional, reserva para esta aplicación los nombres
de archivo que coincidan con webshot-[letters, digits, underscores, or hyphens].png.
Desde el directorio padre, este comando elimina los archivos coincidentes cuya última escritura
ocurrió hace más de siete días, sin entrar en subdirectorios:
dotnet run --project WebpageScreenshots --no-build -- cleanup screenshots 7
Los archivos con otros nombres no se modifican. El prefijo es una convención, así que usa la limpieza solo en un directorio que controles y donde todos los archivos coincidentes pertenezcan a esta aplicación. Para conservar todas las capturas, omite la limpieza. Un error de eliminación devuelve el código de salida uno; las eliminaciones anteriores de esa ejecución no se revierten.
Maneja distintos tamaños de pantalla
La CLI usa la anulación de métricas de dispositivo de Chrome para establecer el área de visualización y una relación de píxeles de uno. La ventana exterior del navegador puede ser más grande, especialmente con anchos pequeños. Esto permite probar los puntos de ruptura de CSS sin cambiar el agente de usuario ni habilitar la emulación táctil móvil; no reproduce un teléfono específico.
Para un área de visualización estrecha, conserva la misma entrada y el mismo marcador de contenido listo, y cambia las dimensiones:
dotnet run --project WebpageScreenshots --no-build -- \
WebpageScreenshots/sample.html screenshots/webshot-narrow.png 375 667 content-loaded 10
Comprueba que el PNG mida 375 × 667 píxeles y que la tarjeta siga siendo legible. El programa acepta anchos de 200 a 3.840 y altos de 200 a 2.160; las capturas más grandes quedan fuera de los límites de este ejemplo.
Diagnostica una captura fallida
Usa el código de salida distinto de cero y el mensaje de error de la CLI para encontrar el paso que falla. Si falta un archivo HTML, se informa antes de que Chrome se inicie. Un navegador o controlador ausente o incompatible impide iniciar una sesión. Si vence el tiempo de espera de contenido listo, comprueba el ID y si el elemento llega a ser visible; aumentar el tiempo de espera no corregirá un marcador incorrecto. Si hay un error de salida, comprueba los permisos del directorio y elige un nombre de archivo que no esté en uso. Conserva las capturas anteriores mientras corriges la entrada o la configuración y luego vuelve a ejecutar el mismo comando con un destino nuevo.
