repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

5.4 KB · 140 lines · markdown Raw History
  1---
  2title: Capturing screenshots with Chromium using Python
  3slug: capturing-screenshots-with-chromium-using-python
  4date: 2022-08-06
  5publish_date: 2022-08-06
  6tags: coding
  7description: Sometimes you need to take screenshots of the web and Chromium provides an easy way to do that.
  8cover_image: blog-screenshot.webp
  9---
 10
 11Chromium for a long time has provided a CLI for capturing web screenshots. I've found myself recently needing a to do a lot of this.
 12
 13To start my script I import my deps, find Chromium, and setup my base command. I've found that Chromium can be under two different names, `chromium` and `chromium-browser`, depending on your container OS so the path check helps with that.
 14
 15This example also makes use of Django's `default_storage` functionality to store files in the proper location making this work with a variety of different storage options.
 16
 17```python
 18import distutils
 19import os
 20import subprocess
 21import uuid
 22
 23from django.core.files.storage import default_storage
 24
 25
 26# Get chromium path, it's sometimes chromium and sometimes chromium-browser
 27chromium = None
 28if distutils.spawn.find_executable("chromium"):
 29    chromium = "chromium"
 30elif distutils.spawn.find_executable("chromium-browser"):
 31    chromium = "chromium-browser"
 32else:
 33    raise Exception("Could not find chromium")
 34
 35
 36base_command = [
 37    chromium,
 38    "--headless",
 39    "--no-sandbox",
 40    "--use-gl=swiftshader",
 41    "--disable-gpu",
 42    "--disable-software-rasterizer",
 43    "--disable-dev-shm-usage",
 44    "--disable-crash-reporter",
 45    "--disable-extensions",
 46    "--disable-in-process-stack-traces",
 47    "--disable-logging",
 48    "--window-size=1280x720",
 49    "--hide-scrollbars",
 50]
 51```
 52
 53Note that I do use Chromium in a Docker container for this so I have a flag that disables Chromium sandboxing since that's the current recommended way of running Chromium inside Docker. You should absolutely remove this flag if you aren't running Chromium in a container.
 54
 55I then make two helper functions for saving images to storage and running our Chromium command, you can modify this to save to the OS directly if you don't want to use Django's storage system.
 56
 57```python
 58def save_tempfile_to_storage(tempfilename, filename):
 59    """
 60    Saves the given tempfile to django default_storage.
 61    :param tempfilename: The tempfile we want to save
 62    :param filename: The storage location to save the file to
 63    """
 64    if default_storage.exists(filename):
 65        default_storage.delete(filename)
 66    default_storage.save(filename, open(tempfilename, "rb"))
 67    os.remove(tempfilename)
 68
 69
 70def run_chromium_command(command):
 71    """
 72    Runs the given chromium command and returns the stdout.
 73    :param command: The command to run
 74    """
 75    command = command.split()
 76    command = base_command + command
 77    subprocess.run(
 78        command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
 79    )
 80```
 81
 82Then create our two main functions for generating the actual screenshots, one for generating from a URL and one from generating from HTML directly. You'll also need to modify these slightly if you don't want to use Django's storage system.
 83
 84```python
 85def generate_screenshot_from_url(url, filename):
 86    """
 87    Generates a screenshot of the given url and saves it to the given output
 88    file.
 89    :param url: The url to screenshot
 90    :param filename: The output file to save the screenshot to
 91    """
 92    tempfilename = f"{uuid.uuid4()}.png"
 93    run_chromium_command(f"--screenshot={tempfilename} {url}")
 94    save_tempfile_to_storage(tempfilename, filename)
 95    return default_storage.url(filename)
 96
 97
 98def generate_screenshot_from_html(html, filename):
 99    """
100    Generates a screenshot of the given html and saves it to the given output
101    file.
102    :param html: The html to screenshot
103    :param filename: The output file to save the screenshot to
104    """
105    tempfilename = f"{uuid.uuid4()}.html"
106    with open(tempfilename, "w") as f:
107        f.write(html)
108    tempfilename_path = "file://" + os.path.join(os.getcwd(), tempfilename)
109    run_chromium_command(f"--screenshot={tempfilename} {tempfilename_path}")
110    save_tempfile_to_storage(tempfilename, filename)
111    return default_storage.url(filename)
112```
113
114You can now import these two functions anywhere you want to create a screenshot. As a quick example if you wanted to take a screenshot of my blog you'd run:
115
116```python
117from chromium import generate_screenshot_from_url
118
119generate_screenshot_from_url("https://blog.bythewood.me/", "screenshots/blog-bythewood-me.png")
120```
121
122As a bonus if you wanted to generate a PDF you can add another function to do this very easily since Chromium supports CLI PDF generation.
123
124```python
125def generate_pdf_from_url(url, filename):
126    """
127    Generates a pdf of the given url and saves it to the given output file.
128    :param url: The url to screenshot
129    :param filename: The output file to save the screenshot to
130    """
131    tempfilename = f"{uuid.uuid4()}.pdf"
132    run_chromium_command(f"--print-to-pdf-no-header --print-to-pdf={tempfilename} {url}")
133    save_tempfile_to_storage(tempfilename, filename)
134    return default_storage.url(filename)
135```
136
137You'd run this the exact same way as the `generate_screenshot_from_url` function.
138
139That's all you need to generate screenshots and PDFs! I've found this to be much more consistent than using the various screenshot and PDF libraries available for Python, you also have a lot of control over Chromium with its [many CLI switches](https://peter.sh/experiments/chromium-command-line-switches/).