Skip to content

Instantly share code, notes, and snippets.

@Konfekt
Created February 24, 2026 10:58
Show Gist options
  • Select an option

  • Save Konfekt/dc7313ca6c8bc15cbba75172fb4fd031 to your computer and use it in GitHub Desktop.

Select an option

Save Konfekt/dc7313ca6c8bc15cbba75172fb4fd031 to your computer and use it in GitHub Desktop.
Convert file to PNG using Firefox's headless screenshot feature
#!/usr/bin/env bash
# Render a local file (or URL) to PNG using Firefox headless screenshot.
# This is often a high-fidelity rasterizer for SVG/HTML/CSS compared to some converters.
set -o errtrace -o errexit -o pipefail
[[ "${TRACE:-0}" == "1" ]] && set -o xtrace
if [[ "${BASH_VERSINFO:-0}" -gt 4 || ( "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 ) ]]; then
shopt -s inherit_errexit
fi
PS4='+\t '
[[ ! -t 0 ]] && [[ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]] && command -v notify-send >/dev/null 2>&1 && notify=1
error_handler() {
local line="$1" bash_lineno="$2" cmd="$3" status="$4"
local start=$(( line > 3 ? line - 3 : 1 ))
local summary="Error: In ${BASH_SOURCE[0]}, Line ${line}, Command ${cmd} exited with Status ${status}"
local body
body="$(pr -tn "${BASH_SOURCE[0]}" | tail -n+"$start" | head -n7 | sed '4s/^[[:space:]]*/>> /')"
echo >&2 -e "${summary}\n${body}"
[[ -z "${notify:+x}" ]] || notify-send --urgency=critical "$summary" "$body"
exit "$status"
}
trap 'error_handler "$LINENO" "${BASH_LINENO[0]:-?}" "$BASH_COMMAND" "$?"' ERR
usage() {
cat <<'TXT'
firefox-screenshot.sh: Render an input in Firefox headless mode and save a PNG screenshot.
Usage:
firefox-screenshot.sh [-o output.png] [-s width,height] <input>
Arguments:
<input> Local path or URL.
Options:
-o, --output Output PNG path.
-s, --size Window size as "width,height", e.g. "1920,1080".
-h, --help Show help.
Rules:
If <input> is a local path and -o is omitted, derive output as "<input-without-last-extension>.png".
If <input> is a URL, require -o.
TXT
}
abs_path() {
local p="$1"
if command -v realpath >/dev/null 2>&1; then
realpath -- "$p"
elif command -v python3 >/dev/null 2>&1; then
python3 - <<'PY' "$p"
import os,sys
print(os.path.realpath(sys.argv[1]))
PY
else
(cd -- "$(dirname -- "$p")" && printf '%s/%s\n' "$PWD" "$(basename -- "$p")")
fi
}
default_output_for_file() {
local in="$1" base
base="$(basename -- "$in")"
if [[ "$base" == *.* && "$base" != .* ]]; then
printf '%s.png\n' "${in%.*}"
else
printf '%s.png\n' "$in"
fi
}
main() {
command -v firefox >/dev/null 2>&1 || { echo "Firefox is required but not found." >&2; return 1; }
local output="" size="" input=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; return 0 ;;
-o|--output) output="${2-}"; shift 2 ;;
-s|--size) size="${2-}"; shift 2 ;;
--) shift; break ;;
-*) echo "Unknown option: $1" >&2; usage >&2; return 2 ;;
*) input="$1"; shift; break ;;
esac
done
[[ -n "$input" ]] || { usage >&2; return 2; }
local uri
if [[ "$input" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*:// ]]; then
uri="$input"
[[ -n "$output" ]] || { echo "Output path is required for URL input." >&2; return 2; }
else
[[ -f "$input" ]] || { echo "Input path not found: $input" >&2; return 1; }
uri="file://$(abs_path "$input")"
[[ -n "$output" ]] || output="$(default_output_for_file "$input")"
fi
local dir="${XDG_CACHE_HOME:-$HOME/.cache}"
mkdir -p -- "$dir"
local temp
temp="$(mktemp -d -- "$dir/firefox-XXXXXX")"
trap 'rm -rf -- "$temp"' INT TERM EXIT
local args=(--profile "$temp" --headless --new-instance --screenshot "$output")
[[ -n "$size" ]] && args+=(--window-size "$size")
firefox "${args[@]}" "$uri"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment