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

make the screenshot actions act the same as Chrome commands #863

Merged
merged 8 commits into from Jul 12, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -8,5 +8,6 @@ cdp-*.txt
/chromedp.test
/chromedp.test.exe

/*.jpeg
/*.png
/*.pdf
54 changes: 0 additions & 54 deletions emulate.go
@@ -1,11 +1,7 @@
package chromedp

import (
"context"
"math"

"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp/device"
)

Expand Down Expand Up @@ -125,53 +121,3 @@ func Emulate(device Device) EmulateAction {
func EmulateReset() EmulateAction {
return Emulate(device.Reset)
}

// FullScreenshot takes a full screenshot with the specified image quality of
// the entire browser viewport. Calls emulation.SetDeviceMetricsOverride (see
// note below).
//
// Implementation liberally sourced from puppeteer.
//
// Note: after calling this action, reset the browser's viewport using
// ResetViewport, EmulateReset, or page.SetDeviceMetricsOverride.
func FullScreenshot(res *[]byte, quality int) EmulateAction {
if res == nil {
panic("res cannot be nil")
}
return ActionFunc(func(ctx context.Context) error {
// get layout metrics
_, _, contentSize, _, _, cssContentSize, err := page.GetLayoutMetrics().Do(ctx)
if err != nil {
return err
}
// protocol v90 changed the return parameter name (contentSize -> cssContentSize)
if cssContentSize != nil {
contentSize = cssContentSize
}
width, height := int64(math.Ceil(contentSize.Width)), int64(math.Ceil(contentSize.Height))
// force viewport emulation
err = emulation.SetDeviceMetricsOverride(width, height, 1, false).
WithScreenOrientation(&emulation.ScreenOrientation{
Type: emulation.OrientationTypePortraitPrimary,
Angle: 0,
}).
Do(ctx)
if err != nil {
return err
}
// capture screenshot
*res, err = page.CaptureScreenshot().
WithQuality(int64(quality)).
WithClip(&page.Viewport{
X: contentSize.X,
Y: contentSize.Y,
Width: contentSize.Width,
Height: contentSize.Height,
Scale: 1,
}).Do(ctx)
if err != nil {
return err
}
return nil
})
}
6 changes: 3 additions & 3 deletions example_test.go
Expand Up @@ -582,10 +582,10 @@ func ExampleFullScreenshot() {
log.Fatal(err)
}

if err := ioutil.WriteFile("fullScreenshot.png", buf, 0644); err != nil {
if err := ioutil.WriteFile("fullScreenshot.jpeg", buf, 0644); err != nil {
log.Fatal(err)
}
fmt.Println("wrote fullScreenshot.png")
fmt.Println("wrote fullScreenshot.jpeg")
// Output:
// wrote fullScreenshot.png
// wrote fullScreenshot.jpeg
}
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -6,5 +6,6 @@ require (
github.com/chromedp/cdproto v0.0.0-20210526005521-9e51b9051fd0
github.com/gobwas/ws v1.1.0
github.com/mailru/easyjson v0.7.7
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -12,6 +12,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13 changes: 13 additions & 0 deletions js.go
Expand Up @@ -72,6 +72,19 @@ const (
return Boolean( this.offsetWidth || this.offsetHeight || this.getClientRects().length );
}`

// getClientRectJS is a javascript snippet that returns the information about the
// size of the specified node and its position relative to its owner document.
getClientRectJS = `function getClientRect() {
const e = this.getBoundingClientRect(),
t = this.ownerDocument.documentElement.getBoundingClientRect();
return {
x: e.left - t.left,
y: e.top - t.top,
width: e.width,
height: e.height,
};
}`

// waitForPredicatePageFunction is a javascript snippet that runs the polling in the
// browser. It's copied from puppeteer. See
// https://github.com/puppeteer/puppeteer/blob/669f04a7a6e96cc8353a8cb152898edbc25e7c15/src/common/DOMWorld.ts#L870-L944
Expand Down
19 changes: 0 additions & 19 deletions nav.go
Expand Up @@ -95,25 +95,6 @@ func Stop() Action {
return page.StopLoading()
}

// CaptureScreenshot is an action that captures/takes a screenshot of the
// current browser viewport.
//
// See the Screenshot action to take a screenshot of a specific element.
//
// See the 'screenshot' example in the https://github.com/chromedp/examples
// project for an example of taking a screenshot of the entire page.
func CaptureScreenshot(res *[]byte) Action {
if res == nil {
panic("res cannot be nil")
}

return ActionFunc(func(ctx context.Context) error {
var err error
*res, err = page.CaptureScreenshot().Do(ctx)
return err
})
}

// Location is an action that retrieves the document location.
func Location(urlstr *string) Action {
if urlstr == nil {
Expand Down
32 changes: 0 additions & 32 deletions nav_test.go
@@ -1,11 +1,9 @@
package chromedp

import (
"bytes"
"context"
"errors"
"fmt"
"image"
_ "image/png"
"io"
"net/http"
Expand Down Expand Up @@ -207,36 +205,6 @@ func TestReload(t *testing.T) {
}
}

func TestCaptureScreenshot(t *testing.T) {
t.Parallel()

ctx, cancel := testAllocate(t, "image.html")
defer cancel()

const width, height = 650, 450

// set the viewport size, to know what screenshot size to expect
var buf []byte
if err := Run(ctx,
EmulateViewport(width, height),
CaptureScreenshot(&buf),
); err != nil {
t.Fatal(err)
}

config, format, err := image.DecodeConfig(bytes.NewReader(buf))
if err != nil {
t.Fatal(err)
}
if want := "png"; format != want {
t.Fatalf("expected format to be %q, got %q", want, format)
}
if config.Width != width || config.Height != height {
t.Fatalf("expected dimensions to be %d*%d, got %d*%d",
width, height, config.Width, config.Height)
}
}

func TestLocation(t *testing.T) {
t.Parallel()

Expand Down
51 changes: 0 additions & 51 deletions query.go
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"math"
"strconv"
"strings"
"sync"
Expand All @@ -13,7 +12,6 @@ import (
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/css"
"github.com/chromedp/cdproto/dom"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/cdproto/runtime"
)

Expand Down Expand Up @@ -1061,55 +1059,6 @@ func SetUploadFiles(sel interface{}, files []string, opts ...QueryOption) QueryA
}, opts...)
}

// Screenshot is an element query action that takes a screenshot of the first element
// node matching the selector.
//
// See CaptureScreenshot for capturing a screenshot of the browser viewport.
//
// See the 'screenshot' example in the https://github.com/chromedp/examples
// project for an example of taking a screenshot of the entire page.
func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) QueryAction {
if picbuf == nil {
panic("picbuf cannot be nil")
}

return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
if len(nodes) < 1 {
return fmt.Errorf("selector %q did not return any nodes", sel)
}

// get box model
box, err := dom.GetBoxModel().WithNodeID(nodes[0].NodeID).Do(ctx)
if err != nil {
return err
}
if len(box.Margin) != 8 {
return ErrInvalidBoxModel
}

// take screenshot of the box
buf, err := page.CaptureScreenshot().
WithFormat(page.CaptureScreenshotFormatPng).
WithClip(&page.Viewport{
// Round the dimensions, as otherwise we might
// lose one pixel in either dimension.
X: math.Round(box.Margin[0]),
Y: math.Round(box.Margin[1]),
Width: math.Round(box.Margin[4] - box.Margin[0]),
Height: math.Round(box.Margin[5] - box.Margin[1]),
// This seems to be necessary? Seems to do the
// right thing regardless of DPI.
Scale: 1.0,
}).Do(ctx)
if err != nil {
return err
}

*picbuf = buf
return nil
}, append(opts, NodeVisible)...)
}

// Submit is an element query action that submits the parent form of the first element
// node matching the selector.
func Submit(sel interface{}, opts ...QueryOption) QueryAction {
Expand Down
90 changes: 0 additions & 90 deletions query_test.go
@@ -1,11 +1,9 @@
package chromedp

import (
"bytes"
"context"
"errors"
"fmt"
"image/png"
"io/ioutil"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -1036,94 +1034,6 @@ func TestSendKeys(t *testing.T) {
}
}

func TestScreenshot(t *testing.T) {
t.Parallel()

ctx, cancel := testAllocate(t, "image2.html")
defer cancel()

tests := []struct {
sel string
by QueryOption
size int
}{
{`/html/body/img`, BySearch, 239},
{`img`, ByQueryAll, 239},
{`#icon-github`, ByID, 120},
{`document.querySelector('#imagething').shadowRoot.querySelector('.container')`, ByJSPath, 190},
}

// a smaller viewport speeds up this test
if err := Run(ctx, EmulateViewport(600, 400)); err != nil {
t.Fatal(err)
}

for i, test := range tests {
var buf []byte
if err := Run(ctx, Screenshot(test.sel, &buf, test.by)); err != nil {
t.Fatalf("test %d got error: %v", i, err)
}

if len(buf) == 0 {
t.Fatalf("test %d failed to capture screenshot", i)
}
img, err := png.Decode(bytes.NewReader(buf))
if err != nil {
t.Fatal(err)
}
size := img.Bounds().Size()
if size.X != test.size || size.Y != test.size {
t.Errorf("expected dimensions to be %d*%d, got %d*%d",
test.size, test.size, size.X, size.Y)
}
}
}

func TestScreenshotHighDPI(t *testing.T) {
t.Parallel()

ctx, cancel := testAllocate(t, "image.html")
defer cancel()

// Use a weird screen dimension with a 1.5 scale factor, so that
// cropping the screenshot is forced to use floating point arithmetic
// and keep the high DPI in mind.
// We also want the dimensions to be large enough to see the element we
// want, since we're not scrolling to ensure it's in view.
if err := Run(ctx, EmulateViewport(905, 705, EmulateScale(1.5))); err != nil {
t.Fatal(err)
}

var buf []byte
if err := Run(ctx, Screenshot("#half-color", &buf, ByID)); err != nil {
t.Fatal(err)
}
img, err := png.Decode(bytes.NewReader(buf))
if err != nil {
t.Fatal(err)
}
size := img.Bounds().Size()
wantSize := 300 // 200px at 1.5 scaling factor
if size.X != wantSize || size.Y != wantSize {
t.Fatalf("expected dimensions to be %d*%d, got %d*%d",
wantSize, wantSize, size.X, size.Y)
}
wantColor := func(x, y int, r, g, b, a uint32) {
color := img.At(x, y)
r_, g_, b_, a_ := color.RGBA()
if r_ != r || g_ != g || b_ != b || a_ != a {
t.Errorf("got 0x%04x%04x%04x%04x at (%d,%d), want 0x%04x%04x%04x%04x",
r_, g_, b_, a_, x, y, r, g, b, a)
}
}
// The left half is blue.
wantColor(5, 5, 0x0, 0x0, 0xffff, 0xffff)
wantColor(5, 295, 0x0, 0x0, 0xffff, 0xffff)
// The right half is red.
wantColor(295, 5, 0xffff, 0x0, 0x0, 0xffff)
wantColor(295, 295, 0xffff, 0x0, 0x0, 0xffff)
}

func TestSubmit(t *testing.T) {
t.Parallel()

Expand Down