screenshot

前陣子因為想把特定的youtube的對話訊息轉發到twitter上,但是只把文字轉出去又覺得哪裡感覺不對,所以就打算把訊息做成一張圖後再上傳,而且這樣也可以附上發言人的頭像。

這邊就先寫擷取網頁畫面的部分,流程大概是這樣:

  • 讀取聊天室訊息
  • 取得到需要轉發的訊息
  • 呼叫HTTP API,傳入網址視窗寬度視窗高度等待時間
  • 收到請求
  • 啟動瀏覽器
  • 開啟網頁
  • 等待網頁讀取完成
  • 截圖
  • 產生base64字串並回傳

這次是用nodejs開發:

// server.js
const express = require('express');
const bodyParser = require("body-parser");

const puppeteer = require('puppeteer');

const app = express();
const PORT = process.env.PORT || 8080;

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
  extended: true
}));

async function doNewPage(b) {
  return new Promise((resolve) => {
    let isWaiting = true;
    setTimeout(() => {
      if (isWaiting) {
        b.close();
        process.exit(4);
      }
    }, 5000);

    b.newPage().then((res) => {
      isWaiting = false;
      resolve(res);
    })
  })
}

// link = 需要截圖的網址
// w    = 瀏覽器寬度
// h    = 瀏覽器高度
// f    = 是否為全頁截圖 ; puppeteer
// ms   = 延遲毫秒數    ; 自訂等待時間
async function task(link, w, h, f, ms) {
  let browser = null;

  try {
    browser = await puppeteer.launch({
      defaultViewport: null,
      timeout: 5000,
    }).catch((e) => {
      process.exit(2);
    });
  } catch (e) {
    process.exit(3);
  }

  let page = await doNewPage(browser);

  let fullPage = f == 't' ? true : false;

  let b64 = '';

  if (w > 0 && h > 0) {
    await page.setViewport({ width: w, height: h, deviceScaleFactor: 1 });
  }

  await page.goto(link, {
    waitUntil: 'networkidle2',
  });

  if (ms > 0) {
    await page.waitForTimeout(ms);
  }

  b64 = await page.screenshot({ fullPage, encoding: "base64" });

  // to file
  //let rnd = new Date().getTime();
  //await page.screenshot({ path: `${rnd}.png`, fullPage });

  browser.close();

  return b64;
}

////

app.get('/', (req, res) => {
  res.status(200).send('not here');
});

app.post('/shot', async (req, res) => {
  let link = decodeURI(req.body.link);
  let w = parseInt(req.body.w, 10);
  let h = parseInt(req.body.h, 10);
  let f = req.body.f; // 't' or other
  let ms = parseInt(req.body.ms, 10);

  let pass = link != '' && w >= 0 && h >= 0 && ms >= 0 && ms <= 15000;

  let b64 = '';

  if (pass) {
    b64 = await task(link, w, h, f, ms);
  } else {
    console.log('input failed');
  }

  res.status(200).send({ b64 });
})

app.listen(PORT, () => {
  console.log(`App listening on port ${PORT}`);
});

這邊定義了POST方法/shot,會讀取下列參數:

  • link = 網址
  • w = 視窗寬度
  • h = 視窗高度
  • f = 是否全頁截圖
  • ms = 需要等待的毫秒數

puppeteer還可以檢查頁面的變數相關的值或操作,不過這邊還不需要所以沒仔細去試。

接下來弄個Dockerfile

FROM node:17-alpine

RUN apk add --no-cache \
    chromium \
    nss \
    freetype \
    harfbuzz \
    ca-certificates \
    ttf-freefont \
    nodejs \
    yarn \
    font-noto-cjk \
    font-noto-emoji

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

RUN yarn add [email protected]
RUN yarn add express

RUN addgroup -S pptruser && adduser -S -g pptruser pptruser \
    && mkdir -p /home/pptruser/Downloads /app \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /app

USER pptruser

WORKDIR /app

COPY ./server.js /app/server.js

RUN yarn

EXPOSE 8080

當時忘記產生package.json了,而是直接在dockerfile直個yarn add

接下來就是建置:

docker build -t express-puppeteer . --no-cache

然後啟動容器:

docker run -itd --cap-add=SYS_ADMIN -p 9999:8080 -v ${PWD}:/app --name exp-pup express-puppeteer node server.js

-v參數是用來測試用的。

最後就可以使用curl來測試:

curl -s -X POST -H "Content-Type: application/json" -d '{ "link" : "https://www.nijisanji.jp/works" , "w" : 1920 , "h" : 1080 , "f" : "t" , "ms" : 5000 }' "http://127.0.0.1:9999/shot" 

順利的話就會回傳個帶有b64屬性的json字串。

{ "b64" : ".........." }

當然測試的時候也可以改成直接存成圖檔:

// ...

// base64
b64 = await page.screenshot({ fullPage, encoding: "base64" });

// to file
let rnd = new Date().getTime();
await page.screenshot({ path: `${rnd}.png`, fullPage });

// ...

以後只好寫好html後就可以靠這API產生出圖檔了。