Headless screenshots¶
Using a headless browser¶
OCP Viewer renders in a browser. There is no server-side rendering: save_screenshot asks the browser to read its canvas back as a PNG, the browser sends the image to the server, and the server writes the file. A screenshot therefore always needs a browser page connected to the server — on a machine without a display, or in a script that should run unattended, that page comes from a headless browser.
Two ways to get one. Both end with the same two Python calls, so pick by how you want the browser's lifetime managed.
from build123d import *
from ocp_viewer import Camera, show, save_screenshot
part = fillet(Box(30, 20, 10).edges().filter_by(Axis.Z), 3) - Cylinder(4, 10)
show(part, port=3999, reset_camera=Camera.RESET)
save_screenshot("part.png", port=3999)
save_screenshot returns once the file exists; it gives up after two seconds with a warning if no browser answered. The port= keyword addresses the server the headless page is attached to — see Addressing a viewer. The PNG's background is transparent — see Screenshots before flattening it with Pillow.
Camera
Pass reset_camera=Camera.RESET on the show() that precedes a screenshot. With the default Camera.KEEP, a show() issued right after the page has loaded can leave the model small and off-centre in the PNG, and the client prints a CameraKeepWarning.
Polling
Polling for the file to arrive currently waits for 2 sec only. This could be too short for large models. Try polling=True and if an error happens, set it to False and poll for the screenshot yourself.
-
Headless Chrome
A long-lived browser the scripts attach to again and again. Start the server, then point Chrome at its page:
python -m ocp_viewer --port 3999 --theme light --no_tools --no_glass"C:\Program Files\Google\Chrome\Application\chrome.exe" ^ --headless=new --window-size=1600,1000 --hide-scrollbars ^ --user-data-dir=%TEMP%\ocp-headless ^ http://127.0.0.1:3999/viewer"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ --headless=new --window-size=1600,1000 --hide-scrollbars \ --user-data-dir=/tmp/ocp-headless \ http://127.0.0.1:3999/viewer/opt/google/chrome/google-chrome \ --headless=new --window-size=1600,1000 --hide-scrollbars \ --user-data-dir=/tmp/ocp-headless \ http://127.0.0.1:3999/viewerHeadless Chrome renders WebGL on the machine's GPU without further flags.
--window-sizesets the size of the PNG. From then on everyshow()andsave_screenshot()against port 3999 lands in that page, until the Chrome process is stopped. -
Playwright
The browser starts and stops with the script, which suits tests and CI.
pip install playwrightinstalls the Python package and its driver only; no browser is downloaded.channel="chrome"in the script below drives the Google Chrome installed on the machine, which uses the GPU like the command above. Playwright looks for it in Chrome's standard location (/Applications/Google Chrome.appon macOS,/opt/google/chrome/google-chromeon Linux,C:\Program Files\Google\Chrome\Application\chrome.exeunder the program files folders on Windows).pip install playwrightRunning Chrome installed on the system through Playwright
from playwright.sync_api import sync_playwright from build123d import * from ocp_viewer import Camera, show, save_screenshot part = fillet(Box(30, 20, 10).edges().filter_by(Axis.Z), 3) - Cylinder(4, 10) with sync_playwright() as p: browser = p.chromium.launch(headless=True, channel="chrome") page = browser.new_page(viewport={"width": 1600, "height": 1000}) page.goto("http://127.0.0.1:3999/viewer") page.wait_for_selector("#cad_viewer canvas") show(part, port=3999, reset_camera=Camera.RESET) save_screenshot("part.png", polling=False, port=3999) browser.close()The server (
python -m ocp_viewer --port 3999) still has to be running; Playwright only replaces the Chrome command above.page.screenshot(path=...)works as well and captures the whole page including the axes gizmo, but the model still arrives throughshow(), so it adds nothing oversave_screenshot.Without a system Chrome, let Playwright download its own Chromium. Check out the Playwright docs how to do this.
No GPU available
On a machine without a GPU — a CI runner, a container, a bare server — add Chrome's software renderer:
--use-angle=swiftshader --enable-unsafe-swiftshader --ignore-gpu-blocklist
to the headless Chrome command, or as args=[...] to p.chromium.launch(...). Rendering is then a few times slower but produces the same picture. Measured on an Apple Silicon Mac, by asking the viewer page for its WebGL renderer and timing show() plus save_screenshot():
| Binary | Flags | Renderer | show + screenshot |
|---|---|---|---|
| installed Google Chrome | none | ANGLE Metal, Apple GPU | 0.11 s |
| installed Google Chrome | SwiftShader flags | SwiftShader | 0.53 s |
Playwright channel="chrome" |
none | ANGLE Metal, Apple GPU | 0.12 s |
Playwright channel="chrome" |
SwiftShader flags | SwiftShader | 0.64 s |
| Playwright's bundled Chromium | none | SwiftShader | 1.58 s |
| Playwright's bundled Chromium | --use-angle=metal --ignore-gpu-blocklist |
ANGLE Metal, Apple GPU | 0.22 s |
The GPU and SwiftShader screenshots differ in a few pixels, not in content. The table has not been reproduced on Linux or Windows, where the GPU backend name differs and a display or a Vulkan driver may be needed.
When the viewer is not needed¶
If the question is only whether some build123d or CadQuery code produces the shape you meant, tcv-screenshots renders a PNG with one command and no server:
pip install tcv-screenshots
playwright install chromium
python -m tcv_screenshots -f example.py -o screenshots
It bundles its own copy of three-cad-viewer and does not involve OCP Viewer, so it cannot show how a model looks in a running viewer with its workspace config applied.