Skip to content

Instantly share code, notes, and snippets.

Created October 27, 2016 08:36
Show Gist options
  • Save anonymous/a1f2d6f56db7d8bbf0f8616ab5018346 to your computer and use it in GitHub Desktop.
Save anonymous/a1f2d6f56db7d8bbf0f8616ab5018346 to your computer and use it in GitHub Desktop.
import phantom from 'phantom'
import { PassThrough } from 'stream'
import createError from 'http-errors'
import reemit from 're-emitter'
/* eslint-disable max-len */
const mobileUA = 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_0 like Mac OS X; en-us) AppleWebKit/532.9 (KHTML, like Gecko) Version/4.0.5 Mobile/8A293 Safari/6531.22.7'
const desktopUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'
const getShotSize = (width, height) => ({
...(Array.isArray(width) && width.length > 1 && { width: width[1] }),
...(Array.isArray(height) && height.length > 1 && { height: height[1] }),
})
const getScreenSize = (width, height) => ({
width,
height,
...(Array.isArray(width) && { width: width[0] }),
...(Array.isArray(height) && { height: height[0] }),
})
// returns a phantom "singleton" instance.
// we re-use the same phantom process for all screenshots
// await instance.exit()
const initGetPhantom = () => {
let instance
return async () => {
if (instance) return instance
instance = await phantom.create()
return instance
}
}
// TODO: the getPhantom function should be passed as an
// argument, not created here.
// TODO: getPhantom() could return a new phantom instance every
// X requests, to avoid resource leaks, etc.
const getPhantom = initGetPhantom()
const takeScreenshot = async (instance, url, {
screenSize,
shotSize,
} = {}, {
userAgent,
} = {}) => {
const page = await instance.createPage()
// close asynchronously
const close = () => {
// TODO: should receive a logger as argument
page.close().catch(err => {
/* eslint-disable no-console */
console.error(err)
})
}
try {
// TODO: set user-agent in settings.headees?
const status = await page.open(url, {
operation: 'GET',
headers: {
'User-Agent': userAgent,
},
})
if (status !== 'success') {
// TODO: get status code
throw createError(400, 'Error opening URL', {
detail: { status },
})
}
// TODO: could do that in parallel with page.open?
await page.property('viewportSize', screenSize)
await page.property('clipRect', shotSize)
// page.propert('paperSize', shotSize)
// wait for onLoadFinished event
// TODO: is this *always* fired?
/*
console.log('waiting onloadfinished')
// TODO: add a timeout?
await new Promise(resolve => page.on('loadFinished', () => {
console.log('onloadfinished')
resolve()
}))
*/
/*
IDEA: create a unix named pipe and write there. return a stream
that reads from the pipe. delete the pipe when done
await page.render('google_home.jpeg', {
// TODO: configurable?
format: 'jpeg',
quality: '100',
})
*/
// TODO: configurable format/quality?
// const buf = await page.renderBuffer('png', -1)
const buf = Buffer.from(await page.renderBase64('PNG'), 'base64')
const through = new PassThrough()
through.end(buf)
close()
return through
} catch (err) {
close() // ensure page is closed
throw err
}
}
// If width is an array, first ele used for screen width and second is used for shot width
// Idem for height
export default ({
width = 1024, // width of browser window
height = 768, // height of browser window
agent = 'desktop',
crop = false,
selector, // optional css selector. todo: decode base64?
error = false,
}, { source: { url } }) => {
// TODO: offload this to worker on other machine using task queue?
const screenSize = getScreenSize(width, height)
const shotSize = getShotSize(width, height)
const userAgent = agent === 'mobile'
? mobileUA
: desktopUA
const through = new PassThrough()
getPhantom()
.then(instance => (
takeScreenshot(instance, url, { screenSize, shotSize }, { userAgent })
))
.then(rs => {
reemit(rs, through, ['error'])
rs.pipe(through)
})
.catch(err => {
through.emit('error', err)
})
return through
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment