From dd59fc344acaec28c74840e004cea1a19a81612d Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Sun, 7 Nov 2021 18:31:46 -0800 Subject: [PATCH] Use path params for /api/image/[id].js (#1262) * move image api routes to pages/ * upgrade puppeteer * try and minimize * attempt to use URLSearchParams * use path params in api routes instead * api/image/[id].js * try it w/o builds config * use index.js --- api/image/[id].js | 117 ++++++++++++++++++++++++++++++++++++++++++++ api/image/index.js | 118 +-------------------------------------------- vercel.json | 27 ----------- 3 files changed, 118 insertions(+), 144 deletions(-) create mode 100755 api/image/[id].js mode change 100755 => 100644 api/image/index.js diff --git a/api/image/[id].js b/api/image/[id].js new file mode 100755 index 0000000..268a1d4 --- /dev/null +++ b/api/image/[id].js @@ -0,0 +1,117 @@ +/* global domtoimage */ +const qs = require('querystring') +const { json, send } = require('micro') +const chrome = require('chrome-aws-lambda') +const puppeteer = require('puppeteer-core') + +// TODO expose local version of dom-to-image +const DOM_TO_IMAGE_URL = 'https://unpkg.com/dom-to-image@2.6.0/dist/dom-to-image.min.js' +const NOTO_COLOR_EMOJI_URL = + 'https://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttf' + +module.exports = async (req, res) => { + // TODO proper auth + if (req.method === 'GET') { + if ( + req.referer || + (req.headers['user-agent'].indexOf('Twitterbot') < 0 && + // Slack does not honor robots.txt: https://api.slack.com/robots + req.headers['user-agent'].indexOf('Slackbot') < 0 && + req.headers['user-agent'].indexOf('Slack-ImgProxy') < 0) + ) { + return send(res, 401, 'Unauthorized') + } + } else { + if (!req.headers.origin && !req.headers.authorization) { + return send(res, 401, 'Unauthorized') + } + } + + const host = (req.headers && req.headers.host) || 'carbon.now.sh' + + try { + await chrome.font(`https://${host}/static/fonts/NotoSansSC-Regular.otf`) + await chrome.font(NOTO_COLOR_EMOJI_URL) + } catch (e) { + console.error(e) + } + + const browser = await puppeteer.launch({ + args: chrome.args, + executablePath: await chrome.executablePath, + headless: chrome.headless, + }) + + try { + const { state, id, ...params } = + req.method === 'GET' ? req.query : await json(req, { limit: '6mb' }) + + const page = await browser.newPage() + + const queryString = state ? `state=${state}` : qs.stringify(params) + + await page.goto(`https://${host}/${id ? id : `?${queryString}`}`) + await page.addScriptTag({ url: DOM_TO_IMAGE_URL }) + + await page.waitForSelector('.export-container', { visible: true, timeout: 9500 }) + + const targetElement = await page.$('.export-container') + + const dataUrl = await page.evaluate((target = document) => { + const query = new URLSearchParams(document.location.search) + + const EXPORT_SIZES_HASH = { + '1x': '1', + '2x': '2', + '4x': '4', + } + + const exportSize = EXPORT_SIZES_HASH[query.get('es')] || '2' + + target.querySelectorAll('span[role="presentation"]').forEach(node => { + if (node.innerText && node.innerText.match(/%[A-Fa-f0-9]{2}/)) { + node.innerText.match(/%[A-Fa-f0-9]{2}/g).forEach(t => { + node.innerHTML = node.innerHTML.replace(t, encodeURIComponent(t)) + }) + } + }) + + const width = target.offsetWidth * exportSize + const height = + query.get('si') === 'true' || query.get('si') === true + ? target.offsetWidth * exportSize + : target.offsetHeight * exportSize + + const config = { + style: { + transform: `scale(${exportSize})`, + 'transform-origin': 'center', + background: query.get('si') ? query.get('bg') : 'none', + }, + filter: n => { + if (n.className) { + return String(n.className).indexOf('eliminateOnRender') < 0 + } + return true + }, + width, + height, + } + + return domtoimage.toPng(target, config) + }, targetElement) + + if (req.method === 'GET') { + res.setHeader('Content-Type', 'image/png') + const data = new Buffer(dataUrl.split(',')[1], 'base64') + return send(res, 200, data) + } + return send(res, 200, dataUrl) + } catch (e) { + // eslint-disable-next-line + console.error(e) + return send(res, 500) + } finally { + await browser.close() + } +} diff --git a/api/image/index.js b/api/image/index.js old mode 100755 new mode 100644 index 268a1d4..d4f7b6a --- a/api/image/index.js +++ b/api/image/index.js @@ -1,117 +1 @@ -/* global domtoimage */ -const qs = require('querystring') -const { json, send } = require('micro') -const chrome = require('chrome-aws-lambda') -const puppeteer = require('puppeteer-core') - -// TODO expose local version of dom-to-image -const DOM_TO_IMAGE_URL = 'https://unpkg.com/dom-to-image@2.6.0/dist/dom-to-image.min.js' -const NOTO_COLOR_EMOJI_URL = - 'https://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttf' - -module.exports = async (req, res) => { - // TODO proper auth - if (req.method === 'GET') { - if ( - req.referer || - (req.headers['user-agent'].indexOf('Twitterbot') < 0 && - // Slack does not honor robots.txt: https://api.slack.com/robots - req.headers['user-agent'].indexOf('Slackbot') < 0 && - req.headers['user-agent'].indexOf('Slack-ImgProxy') < 0) - ) { - return send(res, 401, 'Unauthorized') - } - } else { - if (!req.headers.origin && !req.headers.authorization) { - return send(res, 401, 'Unauthorized') - } - } - - const host = (req.headers && req.headers.host) || 'carbon.now.sh' - - try { - await chrome.font(`https://${host}/static/fonts/NotoSansSC-Regular.otf`) - await chrome.font(NOTO_COLOR_EMOJI_URL) - } catch (e) { - console.error(e) - } - - const browser = await puppeteer.launch({ - args: chrome.args, - executablePath: await chrome.executablePath, - headless: chrome.headless, - }) - - try { - const { state, id, ...params } = - req.method === 'GET' ? req.query : await json(req, { limit: '6mb' }) - - const page = await browser.newPage() - - const queryString = state ? `state=${state}` : qs.stringify(params) - - await page.goto(`https://${host}/${id ? id : `?${queryString}`}`) - await page.addScriptTag({ url: DOM_TO_IMAGE_URL }) - - await page.waitForSelector('.export-container', { visible: true, timeout: 9500 }) - - const targetElement = await page.$('.export-container') - - const dataUrl = await page.evaluate((target = document) => { - const query = new URLSearchParams(document.location.search) - - const EXPORT_SIZES_HASH = { - '1x': '1', - '2x': '2', - '4x': '4', - } - - const exportSize = EXPORT_SIZES_HASH[query.get('es')] || '2' - - target.querySelectorAll('span[role="presentation"]').forEach(node => { - if (node.innerText && node.innerText.match(/%[A-Fa-f0-9]{2}/)) { - node.innerText.match(/%[A-Fa-f0-9]{2}/g).forEach(t => { - node.innerHTML = node.innerHTML.replace(t, encodeURIComponent(t)) - }) - } - }) - - const width = target.offsetWidth * exportSize - const height = - query.get('si') === 'true' || query.get('si') === true - ? target.offsetWidth * exportSize - : target.offsetHeight * exportSize - - const config = { - style: { - transform: `scale(${exportSize})`, - 'transform-origin': 'center', - background: query.get('si') ? query.get('bg') : 'none', - }, - filter: n => { - if (n.className) { - return String(n.className).indexOf('eliminateOnRender') < 0 - } - return true - }, - width, - height, - } - - return domtoimage.toPng(target, config) - }, targetElement) - - if (req.method === 'GET') { - res.setHeader('Content-Type', 'image/png') - const data = new Buffer(dataUrl.split(',')[1], 'base64') - return send(res, 200, data) - } - return send(res, 200, dataUrl) - } catch (e) { - // eslint-disable-next-line - console.error(e) - return send(res, 500) - } finally { - await browser.close() - } -} +module.exports = require('./[id]') diff --git a/vercel.json b/vercel.json index 9bed1ff..6ba3d98 100644 --- a/vercel.json +++ b/vercel.json @@ -2,33 +2,6 @@ "alias": "carbon.now.sh", "version": 2, "public": true, - "builds": [ - { - "src": "package.json", - "use": "@vercel/next" - }, - { - "src": "api/image/index.js", - "use": "@vercel/node", - "config": { - "maxLambdaSize": "40mb" - } - } - ], - "routes": [ - { - "src": "/api/image/?(?[^/]*)", - "dest": "api/image/index.js?id=$id", - "methods": [ - "POST", - "GET" - ] - }, - { - "src": "/api(/.*)?$", - "status": 404 - } - ], "github": { "autoAlias": false, "silent": true