@browserless/screenshot 10.10.0 → 10.10.2
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 +92 -54
- package/src/is-white-screenshot.js +59 -24
- package/src/wait-for-dom.js +51 -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` | `1000` | DOM stability window in ms (idle is `waitForDom / 10`) |
|
|
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.2",
|
|
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": "ec6a614923a1a692bd717ecc8e6f1b09417801d9"
|
|
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,16 +99,28 @@ 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 = [
|
|
120
|
+
{
|
|
121
|
+
fn: () => page.evaluate(waitForDomStability, waitForDomOpts),
|
|
122
|
+
debug: 'beforeScreenshot:waitForDomStability'
|
|
123
|
+
},
|
|
128
124
|
{
|
|
129
125
|
fn: () => page.evaluate('document.fonts.ready'),
|
|
130
126
|
debug: 'beforeScreenshot:fontsReady'
|
|
@@ -170,38 +166,80 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
170
166
|
}
|
|
171
167
|
|
|
172
168
|
const takeScreenshot = async opts => {
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
const timeout = goto.timeouts.action(opts.timeout)
|
|
170
|
+
const elapsed = createElapsed()
|
|
171
|
+
let retry = 0
|
|
172
|
+
let isWhite = false
|
|
173
|
+
let isReady = false
|
|
174
|
+
|
|
175
|
+
do {
|
|
177
176
|
screenshot = await page.screenshot(opts)
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
isWhite = await isWhiteScreenshot(screenshot)
|
|
178
|
+
const snapshotResult = await pReflect(getPageSnapshot(page))
|
|
179
|
+
const pageSnapshot = snapshotResult.isRejected ? {} : snapshotResult.value
|
|
180
|
+
const pageReadyResult = await pReflect(
|
|
181
|
+
opts.isPageReady({
|
|
182
|
+
page,
|
|
183
|
+
response: opts.response,
|
|
184
|
+
screenshot,
|
|
185
|
+
isWhite,
|
|
186
|
+
isWhiteScreenshot,
|
|
187
|
+
...pageSnapshot
|
|
188
|
+
})
|
|
189
|
+
)
|
|
190
|
+
isReady = !pageReadyResult.isRejected && !!pageReadyResult.value
|
|
191
|
+
|
|
192
|
+
if (isReady || elapsed() >= timeout) break
|
|
193
|
+
|
|
194
|
+
retry += 1
|
|
195
|
+
await goto.waitUntilAuto(page, { timeout })
|
|
196
|
+
} while (!isReady)
|
|
197
|
+
|
|
198
|
+
return { isWhite, isReady, retry }
|
|
180
199
|
}
|
|
181
200
|
|
|
182
|
-
|
|
201
|
+
const onDialog = dialog => pReflect(dialog.dismiss())
|
|
202
|
+
page.on('dialog', onDialog)
|
|
183
203
|
|
|
184
|
-
|
|
204
|
+
try {
|
|
205
|
+
const timeScreenshot = timeSpan()
|
|
185
206
|
|
|
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 }) {
|
|
207
|
+
if (waitUntil !== 'auto') {
|
|
208
|
+
;({ response } = await goto(page, { ...opts, url, waitUntil }))
|
|
194
209
|
const screenshotOpts = await beforeScreenshot(page, response, opts)
|
|
195
|
-
|
|
196
|
-
debug('screenshot', { waitUntil,
|
|
210
|
+
screenshot = await page.screenshot({ ...opts, ...screenshotOpts })
|
|
211
|
+
debug('screenshot', { waitUntil, duration: timeScreenshot() })
|
|
212
|
+
} else {
|
|
213
|
+
;({ response } = await goto(page, { ...opts, url, waitUntil, waitUntilAuto }))
|
|
214
|
+
async function waitUntilAuto (page, { response }) {
|
|
215
|
+
const screenshotOpts = await beforeScreenshot(page, response, opts)
|
|
216
|
+
const { isWhite, isReady, retry } = await takeScreenshot({
|
|
217
|
+
...opts,
|
|
218
|
+
...screenshotOpts,
|
|
219
|
+
isPageReady,
|
|
220
|
+
response
|
|
221
|
+
})
|
|
222
|
+
debug('screenshot', {
|
|
223
|
+
waitUntil,
|
|
224
|
+
isReady,
|
|
225
|
+
isWhite,
|
|
226
|
+
retry,
|
|
227
|
+
duration: timeScreenshot()
|
|
228
|
+
})
|
|
229
|
+
}
|
|
197
230
|
}
|
|
198
|
-
}
|
|
199
231
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
232
|
+
return Object.keys(overlayOpts).length === 0
|
|
233
|
+
? screenshot
|
|
234
|
+
: overlay(screenshot, { ...opts, ...overlayOpts, viewport: page.viewport() })
|
|
235
|
+
} finally {
|
|
236
|
+
page.off('dialog', onDialog)
|
|
237
|
+
}
|
|
203
238
|
}
|
|
204
239
|
}
|
|
205
240
|
}
|
|
206
241
|
|
|
207
242
|
module.exports.isWhiteScreenshot = isWhiteScreenshot
|
|
243
|
+
module.exports.waitForDomStability = waitForDomStability
|
|
244
|
+
module.exports.resolveWaitForDom = resolveWaitForDom
|
|
245
|
+
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,51 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_WAIT_FOR_DOM = 1000
|
|
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
|
+
return {
|
|
10
|
+
timeout,
|
|
11
|
+
idle: timeout / WAIT_FOR_DOM_IDLE_RATIO
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const waitForDomStability = ({ idle, timeout } = {}) =>
|
|
16
|
+
new Promise(resolve => {
|
|
17
|
+
if (!document.body) return resolve({ status: 'no-body' })
|
|
18
|
+
|
|
19
|
+
let lastChange = performance.now()
|
|
20
|
+
const observer = new window.MutationObserver(() => {
|
|
21
|
+
lastChange = performance.now()
|
|
22
|
+
})
|
|
23
|
+
observer.observe(document.body, {
|
|
24
|
+
childList: true,
|
|
25
|
+
subtree: true,
|
|
26
|
+
attributes: false,
|
|
27
|
+
characterData: false
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const deadline = performance.now() + timeout
|
|
31
|
+
|
|
32
|
+
;(function check () {
|
|
33
|
+
const now = performance.now()
|
|
34
|
+
if (now - lastChange >= idle) {
|
|
35
|
+
observer.disconnect()
|
|
36
|
+
return resolve({ status: 'idle' })
|
|
37
|
+
}
|
|
38
|
+
if (now >= deadline) {
|
|
39
|
+
observer.disconnect()
|
|
40
|
+
return resolve({ status: 'timeout' })
|
|
41
|
+
}
|
|
42
|
+
window.requestAnimationFrame(check)
|
|
43
|
+
})()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
DEFAULT_WAIT_FOR_DOM,
|
|
48
|
+
WAIT_FOR_DOM_IDLE_RATIO,
|
|
49
|
+
resolveWaitForDom,
|
|
50
|
+
waitForDomStability
|
|
51
|
+
}
|