Created
July 17, 2025 18:29
-
-
Save mdchaney/d7a178d3bf395216564401456d5404e6 to your computer and use it in GitHub Desktop.
Take a picture of a web page from the command line
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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