screenshot.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package chromedp
  2. import (
  3. "context"
  4. "fmt"
  5. "math"
  6. "github.com/chromedp/cdproto/cdp"
  7. "github.com/chromedp/cdproto/page"
  8. "github.com/chromedp/cdproto/runtime"
  9. )
  10. // Screenshot is an element query action that takes a screenshot of the first element
  11. // node matching the selector.
  12. //
  13. // It's supposed to act the same as the command "Capture node screenshot" in Chrome.
  14. //
  15. // Behavior notes: the Protocol Monitor shows that the command sends the following
  16. // CDP commands too:
  17. // - Emulation.clearDeviceMetricsOverride
  18. // - Network.setUserAgentOverride with {"userAgent": ""}
  19. // - Overlay.setShowViewportSizeOnResize with {"show": false}
  20. //
  21. // These CDP commands are not sent by chromedp. If it does not work as expected,
  22. // you can try to send those commands yourself.
  23. //
  24. // See [CaptureScreenshot] for capturing a screenshot of the browser viewport.
  25. //
  26. // See [screenshot] for an example of taking a screenshot of the entire page.
  27. //
  28. // [screenshot]: https://github.com/chromedp/examples/tree/master/screenshot
  29. func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) QueryAction {
  30. return ScreenshotScale(sel, 1, picbuf, opts...)
  31. }
  32. // ScreenshotScale is like [Screenshot] but accepts a scale parameter that
  33. // specifies the page scale factor.
  34. func ScreenshotScale(sel interface{}, scale float64, picbuf *[]byte, opts ...QueryOption) QueryAction {
  35. if picbuf == nil {
  36. panic("picbuf cannot be nil")
  37. }
  38. return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
  39. if len(nodes) < 1 {
  40. return fmt.Errorf("selector %q did not return any nodes", sel)
  41. }
  42. return ScreenshotNodes(nodes, scale, picbuf).Do(ctx)
  43. }, append(opts, NodeVisible)...)
  44. }
  45. // ScreenshotNodes is an action that captures/takes a screenshot of the
  46. // specified nodes, by calculating the extents of the top most left node and
  47. // bottom most right node.
  48. func ScreenshotNodes(nodes []*cdp.Node, scale float64, picbuf *[]byte) Action {
  49. if len(nodes) == 0 {
  50. panic("nodes must be non-empty")
  51. }
  52. if picbuf == nil {
  53. panic("picbuf cannot be nil")
  54. }
  55. return ActionFunc(func(ctx context.Context) error {
  56. var clip page.Viewport
  57. // get box model of first node
  58. if err := callFunctionOnNode(ctx, nodes[0], getClientRectJS, &clip); err != nil {
  59. return err
  60. }
  61. // remainder
  62. for _, node := range nodes[1:] {
  63. var v page.Viewport
  64. // get box model of first node
  65. if err := callFunctionOnNode(ctx, node, getClientRectJS, &v); err != nil {
  66. return err
  67. }
  68. clip.X, clip.Width = extents(clip.X, clip.Width, v.X, v.Width)
  69. clip.Y, clip.Height = extents(clip.Y, clip.Height, v.Y, v.Height)
  70. }
  71. // The "Capture node screenshot" command does not handle fractional dimensions properly.
  72. // Let's align with puppeteer:
  73. // https://github.com/puppeteer/puppeteer/blob/bba3f41286908ced8f03faf98242d4c3359a5efc/src/common/Page.ts#L2002-L2011
  74. x, y := math.Round(clip.X), math.Round(clip.Y)
  75. clip.Width, clip.Height = math.Round(clip.Width+clip.X-x), math.Round(clip.Height+clip.Y-y)
  76. clip.X, clip.Y = x, y
  77. clip.Scale = scale
  78. // take screenshot of the box
  79. buf, err := page.CaptureScreenshot().
  80. WithFormat(page.CaptureScreenshotFormatPng).
  81. WithCaptureBeyondViewport(true).
  82. WithFromSurface(true).
  83. WithClip(&clip).
  84. Do(ctx)
  85. if err != nil {
  86. return err
  87. }
  88. *picbuf = buf
  89. return nil
  90. })
  91. }
  92. // CaptureScreenshot is an action that captures/takes a screenshot of the
  93. // current browser viewport.
  94. //
  95. // It's supposed to act the same as the command "Capture screenshot" in
  96. // Chrome. See the behavior notes of Screenshot for more information.
  97. //
  98. // See the [Screenshot] action to take a screenshot of a specific element.
  99. //
  100. // See [screenshot] for an example of taking a screenshot of the entire page.
  101. //
  102. // [screenshot]: https://github.com/chromedp/examples/tree/master/screenshot
  103. func CaptureScreenshot(res *[]byte) Action {
  104. if res == nil {
  105. panic("res cannot be nil")
  106. }
  107. return ActionFunc(func(ctx context.Context) error {
  108. var err error
  109. *res, err = page.CaptureScreenshot().
  110. WithFromSurface(true).
  111. Do(ctx)
  112. return err
  113. })
  114. }
  115. // FullScreenshot takes a full screenshot with the specified image quality of
  116. // the entire browser viewport.
  117. //
  118. // It's supposed to act the same as the command "Capture full size screenshot"
  119. // in Chrome. See the behavior notes of Screenshot for more information.
  120. //
  121. // The valid range of the compression quality is [0..100]. When this value is
  122. // 100, the image format is png; otherwise, the image format is jpeg.
  123. func FullScreenshot(res *[]byte, quality int) EmulateAction {
  124. if res == nil {
  125. panic("res cannot be nil")
  126. }
  127. return ActionFunc(func(ctx context.Context) error {
  128. format := page.CaptureScreenshotFormatPng
  129. if quality != 100 {
  130. format = page.CaptureScreenshotFormatJpeg
  131. }
  132. // capture screenshot
  133. var err error
  134. *res, err = page.CaptureScreenshot().
  135. WithCaptureBeyondViewport(true).
  136. WithFromSurface(true).
  137. WithFormat(format).
  138. WithQuality(int64(quality)).
  139. Do(ctx)
  140. if err != nil {
  141. return err
  142. }
  143. return nil
  144. })
  145. }
  146. func extents(m, n, o, p float64) (float64, float64) {
  147. a := min(m, o)
  148. b := max(m+n, o+p)
  149. return a, b - a
  150. }
  151. func min(a, b float64) float64 {
  152. if a < b {
  153. return a
  154. }
  155. return b
  156. }
  157. func max(a, b float64) float64 {
  158. if a > b {
  159. return a
  160. }
  161. return b
  162. }