Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Supports capture area screenshot #410

Merged
merged 1 commit into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,12 @@ Saves screenshot on a disk or returns it as base64.
* :format `String` "jpeg" | "png"
* :quality `Integer` 0-100 works for jpeg only
* :full `Boolean` whether you need full page screenshot or a viewport
* :selector `String` css selector for given element
* :selector `String` css selector for given element, optional
* :area `Hash` area for screenshot, optional
* :x `Integer`
* :y `Integer`
* :width `Integer`
* :height `Integer`
* :scale `Float` zoom in/out
* :background_color `Ferrum::RGBA.new(0, 0, 0, 0.0)` to have specific background color

Expand All @@ -370,7 +375,11 @@ browser.screenshot(path: "google.png") # => 134660
# Save on the disk in JPG
browser.screenshot(path: "google.jpg") # => 30902
# Save to Base64 the whole page not only viewport and reduce quality
browser.screenshot(full: true, quality: 60) # "iVBORw0KGgoAAAANSUhEUgAABAAAAAMACAYAAAC6uhUNAAAAAXNSR0IArs4c6Q...
browser.screenshot(full: true, quality: 60, encoding: :base64) # "iVBORw0KGgoAAAANSUhEUgAABAAAAAMACAYAAAC6uhUNAAAAAXNSR0IArs4c6Q...
# Save on the disk with the selected element in PNG
browser.screenshot(path: "google.png", selector: 'textarea') # => 11340
# Save to Base64 with an area of the page in PNG
browser.screenshot(path: "google.png", area: { x: 0, y: 0, width: 400, height: 300 }) # => 54239
# Save with specific background color
browser.screenshot(background_color: Ferrum::RGBA.new(0, 0, 0, 0.0))
```
Expand Down
42 changes: 27 additions & 15 deletions lib/ferrum/page/screenshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
module Ferrum
class Page
module Screenshot
PARTIAL_SCREENSHOT_ARGUMENTS_MESSAGE = "Ignoring :selector or :area in #screenshot since full: true was given at "
AREA_SCREENSHOT_ARGUMENT_MESSAGE = "Ignoring :area in #screenshot since selector: was given at "

DEFAULT_PDF_OPTIONS = {
landscape: false,
paper_width: 8.5,
Expand Down Expand Up @@ -50,6 +53,9 @@ module Screenshot
# @option opts [String] :selector
# CSS selector for the given element.
#
# @option opts [Hash] :area
# x, y, width, height to screenshot an area.
#
# @option opts [Float] :scale
# Zoom in/out.
#
Expand Down Expand Up @@ -198,7 +204,7 @@ def screenshot_options(path = nil, format: nil, scale: 1.0, **options)
screenshot_options.merge!(quality: quality) if quality
screenshot_options.merge!(format: format)

clip = area_options(options[:full], options[:selector], scale)
clip = area_options(options[:full], options[:selector], scale, options[:area])
screenshot_options.merge!(clip: clip) if clip

screenshot_options
Expand All @@ -214,29 +220,35 @@ def format_options(format, path, quality)
[format, quality]
end

def area_options(full, selector, scale)
message = "Ignoring :selector in #screenshot since full: true was given at #{caller(1..1).first}"
warn(message) if full && selector
def area_options(full, selector, scale, area = nil)
warn("#{PARTIAL_SCREENSHOT_ARGUMENTS_MESSAGE}#{caller(1..1).first}") if full && (selector || area)
warn("#{AREA_SCREENSHOT_ARGUMENT_MESSAGE}#{caller(1..1).first}") if selector && area

clip = if full
width, height = document_size
{ x: 0, y: 0, width: width, height: height, scale: scale } if width.positive? && height.positive?
full_window_area || viewport_area
elsif selector
bounding_rect(selector).merge(scale: scale)
bounding_rect(selector)
elsif area
area
else
viewport_area
end

if scale != 1
unless clip
width, height = viewport_size
clip = { x: 0, y: 0, width: width, height: height }
end

clip.merge!(scale: scale)
end
clip.merge!(scale: scale)

clip
end

def full_window_area
width, height = document_size
{ x: 0, y: 0, width: width, height: height } if width.positive? && height.positive?
end

def viewport_area
width, height = viewport_size
{ x: 0, y: 0, width: width, height: height }
end

def bounding_rect(selector)
rect = evaluate_async(%(
const rect = document
Expand Down
50 changes: 46 additions & 4 deletions spec/page/screenshot_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@
end
end

it "ignores :selector in #save_screenshot if full: true" do
it "ignores :selector and :area in #save_screenshot if full: true" do
browser.go_to("/ferrum/long_page")
expect(browser.page).to receive(:warn).with(/Ignoring :selector/)
expect(browser.page).to receive(:warn).with(/Ignoring :selector or :area/)

create_screenshot(path: file, full: true, selector: "#penultimate")

Expand All @@ -63,6 +63,24 @@
end
end

it "ignores :area in #save_screenshot if selector is set" do
browser.go_to("/ferrum/long_page")
expect(browser.page).to receive(:warn).with(/Ignoring :area/)

create_screenshot(path: file, selector: "#penultimate", area: { x: 0, y: 0, width: 200, height: 100 })

File.open(file, "rb") do |f|
size = browser.evaluate <<-JS
function() {
var ele = document.getElementById("penultimate");
var rect = ele.getBoundingClientRect();
return [rect.width, rect.height];
}();
JS
expect(ImageSize.new(f.read).size).to eq(size.map { |s| s * device_pixel_ratio })
end
end

it "resets element positions after" do
browser.go_to("ferrum/long_page")
el = browser.at_css("#middleish")
Expand All @@ -84,10 +102,10 @@
img.pixels.inject(0) { |i, p| p > 255 ? i + 1 : i }
}

browser.screenshot(path: file)
create_screenshot(path: file)
before = black_pixels_count[file]

browser.screenshot(path: file, scale: scale)
create_screenshot(path: file, scale: scale)
after = black_pixels_count[file]

expect(after.to_f / before).to eq(scale**2)
Expand Down Expand Up @@ -199,6 +217,30 @@ def create_screenshot(**options)
end
end

context "with area screenshot" do
it "supports screenshotting of an area" do
browser.go_to("/ferrum/custom_html_size")
expect(browser.viewport_size).to eq([1024, 768])

browser.screenshot(path: file, area: { x: 0, y: 0, width: 300, height: 200 })

File.open(file, "rb") do |f|
expect(ImageSize.new(f.read).size).to eq([300, 200].map { |s| s * device_pixel_ratio })
end
expect(browser.viewport_size).to eq([1024, 768])
end

it "keeps current viewport" do
browser.go_to
browser.set_viewport(width: 800, height: 200)

browser.screenshot(path: file, area: { x: 0, y: 0, width: 300, height: 200 })

expect(File.exist?(file)).to be(true)
expect(browser.viewport_size).to eq([800, 200])
end
end

context "with :background_color option" do
it "supports screenshotting page with the specific background color" do
file = "#{PROJECT_ROOT}/spec/tmp/screenshot.jpeg"
Expand Down