Skip to content

Instantly share code, notes, and snippets.

@mdchaney
Created July 17, 2025 18:29
Show Gist options
  • Save mdchaney/d7a178d3bf395216564401456d5404e6 to your computer and use it in GitHub Desktop.
Save mdchaney/d7a178d3bf395216564401456d5404e6 to your computer and use it in GitHub Desktop.
Take a picture of a web page from the command line
#!/usr/bin/env ruby
require 'ferrum'
require 'optparse'
require 'uri'
class WebScreenshotter
def initialize(options = {})
@options = {
width: 1920,
height: 1080,
timeout: 30,
output: 'screenshot.png',
username: nil,
password: nil
}.merge(options)
@browser = Ferrum::Browser.new(
headless: true,
window_size: [@options[:width], @options[:height]],
timeout: @options[:timeout],
browser_options: {
'disable-gpu' => nil,
'no-sandbox' => nil
}
)
end
def handle_login(original_url)
current_url = @browser.current_url
if current_url.include?('/admin/poobah_session/new')
username = @options[:username] || ENV['SCREENSHOT_USERNAME']
password = @options[:password] || ENV['SCREENSHOT_PASSWORD']
unless username && password
raise ArgumentError, "Login required but no username/password provided. Use --username and --password options or set SCREENSHOT_USERNAME and SCREENSHOT_PASSWORD environment variables."
end
puts "Login page detected. Attempting to authenticate..."
# Fill in username
username_field = @browser.at_css('input[type=email][name=email_address]')
if username_field
username_field.focus.type(username)
puts "Username entered"
else
raise "Could not find username field"
end
# Fill in password
password_field = @browser.at_css('input[type=password][name=password]')
if password_field
password_field.focus.type(password)
puts "Password entered"
else
raise "Could not find password field"
end
# Submit the form
submit_button = @browser.at_css('input[type=submit][name=commit]')
if submit_button
submit_button.click
puts "Login form submitted"
else
raise "Could not find submit button"
end
# Wait for redirect after login
@browser.network.wait_for_idle
# Check if we were redirected to the original URL or if login failed
current_url_after_login = @browser.current_url
if current_url_after_login.include?('/admin/poobah_session/new')
raise "Login failed - still on login page"
end
puts "Login successful, redirected to: #{current_url_after_login}"
# If we weren't redirected to the original URL, navigate there
unless current_url_after_login == original_url
puts "Navigating to original URL: #{original_url}"
@browser.goto(original_url)
@browser.network.wait_for_idle
end
end
end
def capture(url)
unless url =~ URI::DEFAULT_PARSER.make_regexp
raise ArgumentError, "Invalid URL format: #{url}"
end
begin
puts "Navigating to #{url}..."
@browser.goto(url)
# Wait for network requests to finish
@browser.network.wait_for_idle
# Check if we were redirected to login page
handle_login(url)
# Wait for page to be fully loaded
@browser.evaluate('document.readyState') == 'complete'
# Scroll to capture full page content
@browser.execute(<<~JS)
window.scrollTo(0, 0);
window.scrollTo(0, document.body.scrollHeight);
window.scrollTo(0, 0);
JS
puts "Taking screenshot..."
@browser.screenshot(
path: @options[:output],
full: true
)
puts "Screenshot saved to #{@options[:output]}"
rescue Ferrum::NodeNotFoundError => e
puts "Error: Could not find element on page: #{e.message}"
rescue Ferrum::TimeoutError => e
puts "Error: Page load timed out: #{e.message}"
rescue StandardError => e
puts "Error: #{e.message}"
ensure
@browser.quit
end
end
end
# Parse command line options
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: screenshot.rb [options] URL"
opts.on("-w", "--width WIDTH", Integer, "Window width (default: 1920)") do |w|
options[:width] = w
end
opts.on("-h", "--height HEIGHT", Integer, "Window height (default: 1080)") do |h|
options[:height] = h
end
opts.on("-t", "--timeout SECONDS", Integer, "Timeout in seconds (default: 30)") do |t|
options[:timeout] = t
end
opts.on("-o", "--output FILE", String, "Output file (default: screenshot.png)") do |o|
options[:output] = o
end
opts.on("-u", "--username USERNAME", String, "Username for login (required for admin pages)") do |u|
options[:username] = u
end
opts.on("-p", "--password PASSWORD", String, "Password for login (required for admin pages)") do |p|
options[:password] = p
end
opts.on("--help", "Show this help message") do
puts opts
exit
end
end.parse!
if ARGV.empty?
puts "Error: URL is required"
puts "Use --help for usage information"
exit 1
end
# Create screenshotter and capture the page
screenshotter = WebScreenshotter.new(options)
screenshotter.capture(ARGV[0])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment