@browserless/screenshot 10.10.1 → 10.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +3 -4
- package/src/index.js +95 -54
- package/src/is-white-screenshot.js +59 -24
- package/src/wait-for-dom.js +53 -0
package/README.md
CHANGED
|
@@ -69,6 +69,8 @@ const buffer = await browserless.screenshot('https://example.com', {
|
|
|
69
69
|
| `element` | `string` | — | CSS selector for element screenshot |
|
|
70
70
|
| `codeScheme` | `string` | `'atom-dark'` | Prism.js theme for code highlighting |
|
|
71
71
|
| `waitUntil` | `string` | `'auto'` | When to consider navigation done |
|
|
72
|
+
| `waitForDom` | `number` | `0` | DOM stability window in ms (idle is `waitForDom / 10`, `0` disables DOM wait) |
|
|
73
|
+
| `isPageReady` | `function` | `({ isWhite }) => !isWhite` | Custom readiness predicate for retry loop |
|
|
72
74
|
| `overlay` | `object` | `{}` | Browser overlay options |
|
|
73
75
|
|
|
74
76
|
All [Puppeteer page.screenshot() options](https://pptr.dev/api/puppeteer.screenshotoptions) are supported.
|
|
@@ -231,8 +233,7 @@ This is a **core functionality package** for screenshot capture:
|
|
|
231
233
|
| Package | Purpose |
|
|
232
234
|
|---------|---------|
|
|
233
235
|
| `@browserless/goto` | Page navigation with ad blocking |
|
|
234
|
-
| `sharp` | Image composition for overlays |
|
|
235
|
-
| `jimp` | White/blank screenshot detection |
|
|
236
|
+
| `sharp` | Image composition for overlays; White/blank screenshot detection |
|
|
236
237
|
| `prism-themes` | Syntax highlighting themes |
|
|
237
238
|
| `svg-gradient` | Generate gradient backgrounds |
|
|
238
239
|
| `got` | Fetch remote background images |
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@browserless/screenshot",
|
|
3
3
|
"description": "Capture high-quality screenshots of websites with overlay support, device emulation, and automated image optimization.",
|
|
4
4
|
"homepage": "https://browserless.js.org/#/?id=screenshoturl-options",
|
|
5
|
-
"version": "10.10.
|
|
5
|
+
"version": "10.10.3",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"author": {
|
|
8
8
|
"email": "hello@microlink.io",
|
|
@@ -32,14 +32,13 @@
|
|
|
32
32
|
"jpeg"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@browserless/goto": "^10.10.
|
|
35
|
+
"@browserless/goto": "^10.10.2",
|
|
36
36
|
"@kikobeats/content-type": "~1.0.3",
|
|
37
37
|
"@kikobeats/time-span": "~1.0.11",
|
|
38
38
|
"debug-logfmt": "~1.4.7",
|
|
39
39
|
"got": "~11.8.6",
|
|
40
40
|
"is-html-content": "~1.0.0",
|
|
41
41
|
"is-url-http": "~2.3.13",
|
|
42
|
-
"jimp": "~1.6.0",
|
|
43
42
|
"lodash": "~4.17.23",
|
|
44
43
|
"map-values-deep": "~1.0.2",
|
|
45
44
|
"mime": "~3.0.0",
|
|
@@ -72,5 +71,5 @@
|
|
|
72
71
|
"timeout": "2m",
|
|
73
72
|
"workerThreads": false
|
|
74
73
|
},
|
|
75
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "4d2b893e8215a91d830f4471f51c567a53cd6bbb"
|
|
76
75
|
}
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,21 @@ const isWhiteScreenshot = require('./is-white-screenshot')
|
|
|
8
8
|
const waitForPrism = require('./pretty')
|
|
9
9
|
const timeSpan = require('./time-span')
|
|
10
10
|
const overlay = require('./overlay')
|
|
11
|
+
const { waitForDomStability, resolveWaitForDom, DEFAULT_WAIT_FOR_DOM } = require('./wait-for-dom')
|
|
12
|
+
|
|
13
|
+
const createElapsed = () => {
|
|
14
|
+
const start = Date.now()
|
|
15
|
+
return () => Date.now() - start
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const getPageSnapshot = page =>
|
|
19
|
+
page.evaluate(() => ({
|
|
20
|
+
title: document.title || '',
|
|
21
|
+
bodyText: document.body ? document.body.innerText || '' : '',
|
|
22
|
+
url: window.location.href || ''
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
const defaultIsPageReady = ({ isWhite }) => !isWhite
|
|
11
26
|
|
|
12
27
|
const getBoundingClientRect = element => {
|
|
13
28
|
const { top, left, height, width, x, y } = element.getBoundingClientRect()
|
|
@@ -32,37 +47,6 @@ const waitForImagesOnViewport = page =>
|
|
|
32
47
|
)
|
|
33
48
|
)
|
|
34
49
|
|
|
35
|
-
const waitForDomStability = ({ idle, timeout } = {}) =>
|
|
36
|
-
new Promise(resolve => {
|
|
37
|
-
if (!document.body) return resolve({ status: 'no-body' })
|
|
38
|
-
|
|
39
|
-
let lastChange = performance.now()
|
|
40
|
-
const observer = new window.MutationObserver(() => {
|
|
41
|
-
lastChange = performance.now()
|
|
42
|
-
})
|
|
43
|
-
observer.observe(document.body, {
|
|
44
|
-
childList: true,
|
|
45
|
-
subtree: true,
|
|
46
|
-
attributes: false,
|
|
47
|
-
characterData: false
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const deadline = performance.now() + timeout
|
|
51
|
-
|
|
52
|
-
;(function check () {
|
|
53
|
-
const now = performance.now()
|
|
54
|
-
if (now - lastChange >= idle) {
|
|
55
|
-
observer.disconnect()
|
|
56
|
-
return resolve({ status: 'idle' })
|
|
57
|
-
}
|
|
58
|
-
if (now >= deadline) {
|
|
59
|
-
observer.disconnect()
|
|
60
|
-
return resolve({ status: 'timeout' })
|
|
61
|
-
}
|
|
62
|
-
window.requestAnimationFrame(check)
|
|
63
|
-
})()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
50
|
const scrollFullPageToLoadContent = async (page, timeout) => {
|
|
67
51
|
const debug = require('debug-logfmt')('browserless:goto')
|
|
68
52
|
|
|
@@ -115,13 +99,21 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
115
99
|
return function screenshot (page) {
|
|
116
100
|
return async (
|
|
117
101
|
url,
|
|
118
|
-
{
|
|
102
|
+
{
|
|
103
|
+
codeScheme = 'atom-dark',
|
|
104
|
+
overlay: overlayOpts = {},
|
|
105
|
+
waitUntil = 'auto',
|
|
106
|
+
waitForDom = DEFAULT_WAIT_FOR_DOM,
|
|
107
|
+
isPageReady = defaultIsPageReady,
|
|
108
|
+
...opts
|
|
109
|
+
} = {}
|
|
119
110
|
) => {
|
|
120
111
|
let screenshot
|
|
121
112
|
let response
|
|
122
113
|
|
|
123
114
|
const beforeScreenshot = async (page, response, { element, fullPage = false } = {}) => {
|
|
124
115
|
const timeout = goto.timeouts.action(opts.timeout)
|
|
116
|
+
const waitForDomOpts = resolveWaitForDom(waitForDom)
|
|
125
117
|
|
|
126
118
|
let screenshotOpts = {}
|
|
127
119
|
const tasks = [
|
|
@@ -135,6 +127,13 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
135
127
|
}
|
|
136
128
|
]
|
|
137
129
|
|
|
130
|
+
if (waitForDomOpts) {
|
|
131
|
+
tasks.push({
|
|
132
|
+
fn: () => page.evaluate(waitForDomStability, waitForDomOpts),
|
|
133
|
+
debug: 'beforeScreenshot:waitForDomStability'
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
138
137
|
if (codeScheme && response) {
|
|
139
138
|
tasks.push({
|
|
140
139
|
fn: () => waitForPrism(page, response, { codeScheme, ...opts }),
|
|
@@ -170,38 +169,80 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
170
169
|
}
|
|
171
170
|
|
|
172
171
|
const takeScreenshot = async opts => {
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
const timeout = goto.timeouts.action(opts.timeout)
|
|
173
|
+
const elapsed = createElapsed()
|
|
174
|
+
let retry = 0
|
|
175
|
+
let isWhite = false
|
|
176
|
+
let isReady = false
|
|
177
|
+
|
|
178
|
+
do {
|
|
177
179
|
screenshot = await page.screenshot(opts)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
isWhite = await isWhiteScreenshot(screenshot)
|
|
181
|
+
const snapshotResult = await pReflect(getPageSnapshot(page))
|
|
182
|
+
const pageSnapshot = snapshotResult.isRejected ? {} : snapshotResult.value
|
|
183
|
+
const pageReadyResult = await pReflect(
|
|
184
|
+
opts.isPageReady({
|
|
185
|
+
page,
|
|
186
|
+
response: opts.response,
|
|
187
|
+
screenshot,
|
|
188
|
+
isWhite,
|
|
189
|
+
isWhiteScreenshot,
|
|
190
|
+
...pageSnapshot
|
|
191
|
+
})
|
|
192
|
+
)
|
|
193
|
+
isReady = !pageReadyResult.isRejected && !!pageReadyResult.value
|
|
194
|
+
|
|
195
|
+
if (isReady || elapsed() >= timeout) break
|
|
196
|
+
|
|
197
|
+
retry += 1
|
|
198
|
+
await goto.waitUntilAuto(page, { timeout })
|
|
199
|
+
} while (!isReady)
|
|
200
|
+
|
|
201
|
+
return { isWhite, isReady, retry }
|
|
180
202
|
}
|
|
181
203
|
|
|
182
|
-
|
|
204
|
+
const onDialog = dialog => pReflect(dialog.dismiss())
|
|
205
|
+
page.on('dialog', onDialog)
|
|
183
206
|
|
|
184
|
-
|
|
207
|
+
try {
|
|
208
|
+
const timeScreenshot = timeSpan()
|
|
185
209
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const screenshotOpts = await beforeScreenshot(page, response, opts)
|
|
189
|
-
screenshot = await page.screenshot({ ...opts, ...screenshotOpts })
|
|
190
|
-
debug('screenshot', { waitUntil, duration: timeScreenshot() })
|
|
191
|
-
} else {
|
|
192
|
-
;({ response } = await goto(page, { ...opts, url, waitUntil, waitUntilAuto }))
|
|
193
|
-
async function waitUntilAuto (page, { response }) {
|
|
210
|
+
if (waitUntil !== 'auto') {
|
|
211
|
+
;({ response } = await goto(page, { ...opts, url, waitUntil }))
|
|
194
212
|
const screenshotOpts = await beforeScreenshot(page, response, opts)
|
|
195
|
-
|
|
196
|
-
debug('screenshot', { waitUntil,
|
|
213
|
+
screenshot = await page.screenshot({ ...opts, ...screenshotOpts })
|
|
214
|
+
debug('screenshot', { waitUntil, duration: timeScreenshot() })
|
|
215
|
+
} else {
|
|
216
|
+
;({ response } = await goto(page, { ...opts, url, waitUntil, waitUntilAuto }))
|
|
217
|
+
async function waitUntilAuto (page, { response }) {
|
|
218
|
+
const screenshotOpts = await beforeScreenshot(page, response, opts)
|
|
219
|
+
const { isWhite, isReady, retry } = await takeScreenshot({
|
|
220
|
+
...opts,
|
|
221
|
+
...screenshotOpts,
|
|
222
|
+
isPageReady,
|
|
223
|
+
response
|
|
224
|
+
})
|
|
225
|
+
debug('screenshot', {
|
|
226
|
+
waitUntil,
|
|
227
|
+
isReady,
|
|
228
|
+
isWhite,
|
|
229
|
+
retry,
|
|
230
|
+
duration: timeScreenshot()
|
|
231
|
+
})
|
|
232
|
+
}
|
|
197
233
|
}
|
|
198
|
-
}
|
|
199
234
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
235
|
+
return Object.keys(overlayOpts).length === 0
|
|
236
|
+
? screenshot
|
|
237
|
+
: overlay(screenshot, { ...opts, ...overlayOpts, viewport: page.viewport() })
|
|
238
|
+
} finally {
|
|
239
|
+
page.off('dialog', onDialog)
|
|
240
|
+
}
|
|
203
241
|
}
|
|
204
242
|
}
|
|
205
243
|
}
|
|
206
244
|
|
|
207
245
|
module.exports.isWhiteScreenshot = isWhiteScreenshot
|
|
246
|
+
module.exports.waitForDomStability = waitForDomStability
|
|
247
|
+
module.exports.resolveWaitForDom = resolveWaitForDom
|
|
248
|
+
module.exports.DEFAULT_WAIT_FOR_DOM = DEFAULT_WAIT_FOR_DOM
|
|
@@ -1,32 +1,67 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const sharp = require('sharp')
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
const SAMPLE_PERCENTAGE = 0.25
|
|
6
|
+
const SAMPLE_STEP_SIZE = Math.max(1, Math.ceil(Math.sqrt(1 / SAMPLE_PERCENTAGE)))
|
|
7
|
+
const WHITE_PIXEL_THRESHOLD = 245
|
|
8
|
+
const WHITE_COLOR_VARIANCE_TOLERANCE = 8
|
|
9
|
+
|
|
10
|
+
const blendOnWhite = (channel, alpha) => (channel * alpha + 255 * (255 - alpha)) / 255
|
|
11
|
+
|
|
12
|
+
const isWhiteSampledImage = (data, { width, height, channels }) => {
|
|
13
|
+
if (!width || !height || !channels || data.length < channels) return false
|
|
14
|
+
|
|
15
|
+
let minR = 255
|
|
16
|
+
let minG = 255
|
|
17
|
+
let minB = 255
|
|
18
|
+
let maxR = 0
|
|
19
|
+
let maxG = 0
|
|
20
|
+
let maxB = 0
|
|
21
|
+
|
|
22
|
+
for (let y = 0; y < height; y += SAMPLE_STEP_SIZE) {
|
|
23
|
+
const rowOffset = y * width * channels
|
|
24
|
+
|
|
25
|
+
for (let x = 0; x < width; x += SAMPLE_STEP_SIZE) {
|
|
26
|
+
const pixelOffset = rowOffset + x * channels
|
|
27
|
+
const a = channels > 3 ? data[pixelOffset + 3] : 255
|
|
28
|
+
const r = blendOnWhite(data[pixelOffset], a)
|
|
29
|
+
const g = blendOnWhite(data[pixelOffset + 1], a)
|
|
30
|
+
const b = blendOnWhite(data[pixelOffset + 2], a)
|
|
31
|
+
|
|
32
|
+
if (r < WHITE_PIXEL_THRESHOLD || g < WHITE_PIXEL_THRESHOLD || b < WHITE_PIXEL_THRESHOLD) {
|
|
33
|
+
return false
|
|
22
34
|
}
|
|
23
|
-
}
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
if (r < minR) minR = r
|
|
37
|
+
if (g < minG) minG = g
|
|
38
|
+
if (b < minB) minB = b
|
|
39
|
+
if (r > maxR) maxR = r
|
|
40
|
+
if (g > maxG) maxG = g
|
|
41
|
+
if (b > maxB) maxB = b
|
|
29
42
|
}
|
|
30
|
-
throw error
|
|
31
43
|
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
maxR - minR <= WHITE_COLOR_VARIANCE_TOLERANCE &&
|
|
47
|
+
maxG - minG <= WHITE_COLOR_VARIANCE_TOLERANCE &&
|
|
48
|
+
maxB - minB <= WHITE_COLOR_VARIANCE_TOLERANCE
|
|
49
|
+
)
|
|
32
50
|
}
|
|
51
|
+
|
|
52
|
+
module.exports = async uint8array => {
|
|
53
|
+
const input = Buffer.isBuffer(uint8array) ? uint8array : Buffer.from(uint8array)
|
|
54
|
+
const { data, info } = await sharp(input)
|
|
55
|
+
.ensureAlpha()
|
|
56
|
+
.raw()
|
|
57
|
+
.toBuffer({ resolveWithObject: true })
|
|
58
|
+
|
|
59
|
+
return isWhiteSampledImage(data, info)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports.SAMPLE_PERCENTAGE = SAMPLE_PERCENTAGE
|
|
63
|
+
module.exports.SAMPLE_STEP_SIZE = SAMPLE_STEP_SIZE
|
|
64
|
+
module.exports.WHITE_PIXEL_THRESHOLD = WHITE_PIXEL_THRESHOLD
|
|
65
|
+
module.exports.WHITE_COLOR_VARIANCE_TOLERANCE = WHITE_COLOR_VARIANCE_TOLERANCE
|
|
66
|
+
module.exports.blendOnWhite = blendOnWhite
|
|
67
|
+
module.exports.isWhiteSampledImage = isWhiteSampledImage
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_WAIT_FOR_DOM = 0
|
|
4
|
+
const WAIT_FOR_DOM_IDLE_RATIO = 10
|
|
5
|
+
|
|
6
|
+
const resolveWaitForDom = waitForDom => {
|
|
7
|
+
const timeout = Number.isFinite(waitForDom) && waitForDom >= 0 ? waitForDom : DEFAULT_WAIT_FOR_DOM
|
|
8
|
+
|
|
9
|
+
if (timeout === 0) return undefined
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
timeout,
|
|
13
|
+
idle: timeout / WAIT_FOR_DOM_IDLE_RATIO
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const waitForDomStability = ({ idle, timeout } = {}) =>
|
|
18
|
+
new Promise(resolve => {
|
|
19
|
+
if (!document.body) return resolve({ status: 'no-body' })
|
|
20
|
+
|
|
21
|
+
let lastChange = performance.now()
|
|
22
|
+
const observer = new window.MutationObserver(() => {
|
|
23
|
+
lastChange = performance.now()
|
|
24
|
+
})
|
|
25
|
+
observer.observe(document.body, {
|
|
26
|
+
childList: true,
|
|
27
|
+
subtree: true,
|
|
28
|
+
attributes: false,
|
|
29
|
+
characterData: false
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const deadline = performance.now() + timeout
|
|
33
|
+
|
|
34
|
+
;(function check () {
|
|
35
|
+
const now = performance.now()
|
|
36
|
+
if (now - lastChange >= idle) {
|
|
37
|
+
observer.disconnect()
|
|
38
|
+
return resolve({ status: 'idle' })
|
|
39
|
+
}
|
|
40
|
+
if (now >= deadline) {
|
|
41
|
+
observer.disconnect()
|
|
42
|
+
return resolve({ status: 'timeout' })
|
|
43
|
+
}
|
|
44
|
+
window.requestAnimationFrame(check)
|
|
45
|
+
})()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
DEFAULT_WAIT_FOR_DOM,
|
|
50
|
+
WAIT_FOR_DOM_IDLE_RATIO,
|
|
51
|
+
resolveWaitForDom,
|
|
52
|
+
waitForDomStability
|
|
53
|
+
}
|