Créer des captures de pages web ou de fichiers HTML en .NET (C#)
Utilisez Selenium WebDriver pour ouvrir une page dans Chrome sans interface graphique et enregistrer sa zone d’affichage visible au format PNG. L’application console C# ci-dessous accepte un chemin vers un fichier HTML local ou une URL HTTP, attend un élément qui signale que le contenu est prêt, puis enregistre la capture d’écran sans remplacer de fichier existant.
Prérequis
Ce guide utilise Bash sous Linux, le SDK .NET 10,
et un navigateur Chrome ou Chromium installé avec un ChromeDriver compatible. Il a été testé avec
le SDK 10.0.111 et Chromium/ChromeDriver 152.0.7977.82. Installez les mises à jour de maintenance
actuelles de .NET 10, qui est une version LTS activement prise en charge.
L’exemple HTTP local facultatif nécessite aussi une installation de Python 3 maintenue, disposant des
options http.server --bind et --directory ; il a été testé avec Python 3.14.7.
Ne rendez que des pages et des fichiers HTML auxquels vous faites confiance. Un navigateur peut exécuter leur JavaScript et effectuer des requêtes réseau. Cet exemple capture la zone d’affichage, y compris les zones vides ; il ne capture pas une page défilante entière, ne valide pas les codes d’état HTTP et ne permet pas d’établir qu’une application quelconque a fini de se charger.
Vérifiez les outils installés avant de créer le projet :
dotnet --version && chromium --version && chromedriver --version
Pour l’exemple HTTP, vérifiez Python et confirmez que son aide répertorie les deux options avant de créer le projet :
python3 --version && python3 -m http.server --help
Si vous utilisez Google Chrome, remplacez chromium par son exécutable.
Définissez ces chemins selon l’emplacement réel du navigateur et du pilote compatible sur votre
machine. Voici les chemins utilisés pour cet exemple sous Linux :
export CHROME_BINARY=/usr/bin/chromium &&
export CHROMEDRIVER_PATH=/usr/bin/chromedriver
Utiliser Selenium WebDriver
Commencez dans un répertoire accessible en écriture, en dehors d’un dépôt .NET existant. Les
configurations du SDK, de NuGet et de MSBuild dans les répertoires parents peuvent modifier le
comportement d’un nouveau projet. Le bloc de configuration refuse ces fichiers dans les répertoires
parents ainsi qu’une destination WebpageScreenshots existante avant de créer quoi que
ce soit. Exécutez-le depuis 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
)
Les versions des paquets sont fixées à celles de la version Selenium 4.28.0 testée. Si l’installation des paquets échoue, conservez le nouveau projet, corrigez le problème NuGet ou réseau signalé, puis réessayez les étapes d’installation des dépendances depuis le même répertoire parent :
dotnet add WebpageScreenshots package Selenium.WebDriver --version 4.28.0 &&
dotnet add WebpageScreenshots package Selenium.Support --version 4.28.0
Remplacez le fichier WebpageScreenshots/Program.cs généré par ce programme complet. Les arguments
sont input output.png width height ready-element-id timeout-seconds. La largeur et la hauteur décrivent la zone d’affichage en pixels
CSS ; le délai maximal s’applique séparément à la navigation et à l’attente du contenu prêt.
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;
}
Les blocs using libèrent le navigateur et le service du pilote à la fin
normale de l’exécution comme en cas d’exception. Le programme met le PNG en mémoire tampon avant
d’ouvrir la destination ; un échec de navigation ou d’attente du contenu prêt ne crée donc aucune
capture d’écran. FileMode.CreateNew refuse aussi un fichier de sortie créé depuis la
vérification initiale. Un échec d’écriture sur disque peut laisser un nouveau fichier partiel ;
inspectez et supprimez uniquement ce fichier avant de réessayer.
Compatibilité des navigateurs
Cette implémentation sélectionne explicitement Chrome et ChromeDriver. Veillez à ce que leurs versions soient compatibles ; le guide de sélection des versions de ChromeDriver explique comment obtenir une paire compatible. Selenium Manager peut détecter et télécharger des pilotes, mais ce programme utilise les chemins que vous avez fournis. Firefox et Edge nécessitent leurs propres classes de pilote et d’options ; ils ne sont pas couverts par ce guide.
Capturer des fichiers HTML locaux
Enregistrez cette page sous le nom WebpageScreenshots/sample.html. Elle comporte un fond bleu et
une carte blanche portant le texte LOCAL PAGE.
Le marqueur indiquant que le contenu est prêt est présent immédiatement :
<!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>
Compilez et exécutez depuis le répertoire parent. && empêche
l’exécution d’un ancien binaire si la compilation échoue, et --
transmet les arguments restants à l’application :
dotnet build WebpageScreenshots --disable-build-servers &&
dotnet run --project WebpageScreenshots --no-build -- \
WebpageScreenshots/sample.html screenshots/webshot-local.png 800 600 content-loaded 10
En cas de réussite, le terminal affiche Saved screenshots/webshot-local.png, suivi de données JSON
décrivant l’URL réelle, la zone d’affichage, la fenêtre externe et le rapport de pixels.
Ouvrez ce PNG : il devrait mesurer 800 × 600 pixels, avec la carte
LOCAL PAGE entière près du haut et un fond bleu
qui s’étend jusqu’en bas.
La zone d’affichage indiquée dans le JSON devrait mesurer 800 × 600 avec un rapport de pixels de un.
Un PNG non vide ne prouve pas à lui seul que vous avez capturé la page souhaitée.
Gérer le contenu dynamique
La navigation attend le document, mais JavaScript peut ensuite mettre la page à jour. Utilisez un élément visible indiquant que le contenu est prêt, que votre application n’affiche que lorsque le contenu souhaité est prêt. Un conteneur visible alors que ses données sont encore en cours de chargement n’est pas un signal approprié.
Enregistrez cette deuxième page sous le nom WebpageScreenshots/delayed.html. La carte est
initialement masquée et apparaît après 1,5 seconde, sur un fond vert qui permet de repérer
facilement une capture trop précoce :
<!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>
Pour obtenir une source HTTP reproductible, vérifiez python3 --version, puis exécutez
ce serveur au premier plan dans un deuxième terminal, depuis le même répertoire parent. Il
sert le répertoire sélectionné sur l’interface de bouclage :
python3 -m http.server 8765 --bind 127.0.0.1 --directory WebpageScreenshots
Si ce port est occupé, choisissez un port inutilisé et mettez à jour les deux URL ci-dessous. Laissez le serveur fonctionner pendant la capture des pages statique et différée :
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
Ouvrez les deux fichiers. L’image statique devrait correspondre à la page locale bleue ; l’image
différée devrait afficher DELAYED PAGE sur sa
carte blanche sur fond vert, avec des dimensions de 800 × 600 pixels.
Une image différée entièrement blanche signifie que vous avez effectué la capture trop tôt.
Arrêtez le serveur avec Ctrl+C dans son terminal lorsque vous avez terminé.
Un marqueur absent ou qui reste masqué entraîne la fermeture de l’outil CLI avec un code de sortie
égal à un et sans nouveau PNG à l’expiration du délai d’attente. Pour une application réelle,
remplacez content-loaded par l’ID réel de son élément indiquant que le contenu est prêt.
Gérer le stockage des captures d’écran
Enregistrez les captures dans un répertoire qui vous appartient et choisissez un nouveau nom de fichier pour chaque image conservée. Répéter une commande avec une destination déjà existante échoue et préserve ce fichier. La capture ne modifie pas les PNG voisins.
Pour une politique de conservation facultative, réservez à cette application les noms de fichiers
correspondant à webshot-[letters, digits, underscores, or hyphens].png. Depuis le répertoire parent, cette commande supprime
les fichiers correspondants dont la dernière écriture remonte à plus de sept jours, sans entrer
dans les sous-répertoires :
dotnet run --project WebpageScreenshots --no-build -- cleanup screenshots 7
Les autres noms de fichiers sont laissés intacts. Le préfixe est une convention ; n’utilisez donc le nettoyage que dans un répertoire que vous contrôlez, où chaque fichier correspondant appartient à cette application. Pour conserver toutes les captures d’écran, omettez le nettoyage. Une erreur de suppression renvoie un code de sortie égal à un ; les suppressions précédentes de cette exécution ne sont pas annulées.
Gérer différentes tailles d’écran
L’outil CLI utilise la substitution des métriques d’appareil de Chrome pour définir la zone d’affichage et un rapport de pixels de un. La fenêtre externe du navigateur peut être plus grande, surtout lorsque la largeur est faible. Cela permet de tester les points de rupture CSS sans modifier l’agent utilisateur ni activer l’émulation tactile mobile ; cela ne reproduit pas un téléphone particulier.
Pour une zone d’affichage étroite, conservez la même entrée et le même marqueur indiquant que le contenu est prêt, puis modifiez les dimensions :
dotnet run --project WebpageScreenshots --no-build -- \
WebpageScreenshots/sample.html screenshots/webshot-narrow.png 375 667 content-loaded 10
Vérifiez que le PNG mesure 375 × 667 pixels et que la carte reste lisible. Le programme accepte des largeurs de 200 à 3 840 et des hauteurs de 200 à 2 160 ; les captures plus grandes dépassent les limites de cet exemple.
Diagnostiquer l’échec d’une capture
Utilisez le code de sortie non nul et le message d’erreur de l’outil CLI pour trouver l’étape qui échoue. Un fichier HTML manquant est signalé avant le démarrage de Chrome. Un navigateur ou un pilote manquant ou incompatible empêche le démarrage d’une session. Si le délai d’attente du contenu prêt est dépassé, vérifiez l’ID et si l’élément devient visible ; augmenter le délai ne corrigera pas un marqueur incorrect. En cas d’erreur de sortie, vérifiez les permissions du répertoire et choisissez un nom de fichier inutilisé. Conservez les captures d’écran précédentes pendant que vous corrigez l’entrée ou la configuration, puis relancez la même commande avec une nouvelle destination.
