diff --git a/.gitignore b/.gitignore index 0db01f3..e286330 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules +examples run.sh +screenshot* diff --git a/index.js b/index.js index a0eea3f..de49a90 100644 --- a/index.js +++ b/index.js @@ -3,10 +3,43 @@ const fs = require('fs').promises; const process = require('process'); const puppeteer = require('puppeteer'); +const quality = require('./quality.js'); // Size of the rendering of the web page const size = { width: 1280, height: 720 }; +// Logging helper +function write(stream, chunk, encoding='utf8') { + return new Promise((resolve, reject) => { + const errListener = (err) => { + stream.removeListener('error', errListener); + reject(err); + }; + stream.addListener('error', errListener); + const callback = () => { + stream.removeListener('error', errListener); + resolve(undefined); + }; + stream.write(chunk, encoding, callback); + }); +} + +async function print(data) { + await write(process.stdout, data); +} + +async function eprint(data) { + await write(process.stderr, data); +} + +async function println(data) { + await write(process.stdout, data + '\n'); +} + +async function eprintln(data) { + await write(process.stderr, data + '\n'); +} + async function main() { if (process.argv[2] === undefined) { @@ -57,9 +90,17 @@ async function main() { let file2 = await fs.readFile(__dirname + '/' + 'screenshot2.png'); let filesAreSame = file1.map((x, i) => x === file2[i]).reduce((a, b) => a && b, true); - // Crash if they're different if (!filesAreSame) { - throw new Error("Page edit changed the layout"); + // Check psnr + let psnr = await quality.psnr(__dirname + '/' + 'screenshot1.png', __dirname + '/' + 'screenshot2.png'); + + // Crash if they're different + if (psnr > 70) { + eprintln("\x1b[33mWarning: " + process.argv[2] + " produced slight diff: psnr = " + psnr + '\x1b[0m'); + } else { + await eprintln("\x1b[31mError: age edit changed the layout: psnr = " + psnr + '\x1b[0m'); + process.exit(1); + } } // Analyse the root and output the result @@ -82,14 +123,19 @@ async function addSpan(element) { for (let elt of elts) { let value = await elt.evaluate(el => el.textContent, element); if (value !== "") { - await elt.evaluate(el => { - if (el.innerHTML.indexOf('
  • ') === -1) { + let html = await elt.evaluate(el => { + if (el.innerHTML.indexOf('
  • ') === -1 && el.innerHTML.indexOf('

    ') === -1) { + let tmp = el.innerHTML; el.innerHTML = '' + el.innerHTML + ''; - return true; + return [tmp, el.innerHTML]; } else { - return false; + return null; } }); + + if (html !== null) { + await eprintln("\x1b[36mReplaced " + JSON.stringify(html[0]) + " by " + JSON.stringify(html[1]) + '\x1b[0m'); + } } } diff --git a/package-lock.json b/package-lock.json index 6ccb661..0024732 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "pngjs": "^7.0.0", "puppeteer": "^19.7.1" }, "bin": { @@ -48,9 +49,9 @@ } }, "node_modules/@types/node": { - "version": "18.13.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", - "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==", + "version": "18.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.0.tgz", + "integrity": "sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==", "optional": true }, "node_modules/@types/yauzl": { @@ -540,6 +541,14 @@ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", diff --git a/package.json b/package.json index 06e6325..7489186 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "author": "", "license": "ISC", "dependencies": { + "pngjs": "^7.0.0", "puppeteer": "^19.7.1" } } diff --git a/quality.js b/quality.js new file mode 100644 index 0000000..53a7b54 --- /dev/null +++ b/quality.js @@ -0,0 +1,65 @@ +const { PNG } = require('pngjs'); +const fs = require('fs'); + +// Fixed from https://www.npmjs.com/package/png-quality + +async function loadPngFile(pathOrBuffer) { + // Load buffer of path + if (!(pathOrBuffer instanceof Buffer)) { + pathOrBuffer = await new Promise((resolve, reject) => { + fs.readFile(pathOrBuffer, (err, data) => { + if (err) return reject(err) + resolve(data) + }); + }) + } + + // Load PNG from buffer + return await new Promise((resolve, reject) => { + const png = new PNG() + png.parse(pathOrBuffer, err => { + if (err) return reject(err) + resolve(png) + }) + }) +} + +async function mse(png1, png2) { + const pngs = [png1, png2]; + for (let i in pngs) { + if (!(pngs[i] instanceof PNG)) pngs[i] = await loadPngFile(pngs[i]) + } + + if (pngs[0].width !== pngs[1].width || pngs[0].height !== pngs[1].height) { + throw new Error('Width or height does not equal') + } + + if (pngs[0].data.length !== pngs[1].data.length) { + throw new Error('Data buffer length does not equal') + } + + const square = (a) => a * a, + channelIndex = [0, 1, 2], + channelMax = 255 * 255, + area = pngs[0].width * pngs[1].height + + let mse = 0 + for (let i = 0; i < pngs[0].data.length; i += 4) { + const rgbas = pngs.map(png => png.data.slice(i, i + 4)) + const rgbs = rgbas.map(rgba => channelIndex.map(i => rgba[i] * rgba[3])) + channelIndex.forEach(i => mse += square(rgbs[0][i] - rgbs[1][i])) + } + + return mse / 3.0 / (channelMax * channelMax) / area +} + +async function psnr(png1, png2) { + const m = await mse(png1, png2) + return 10 * Math.log10(1 / m) +} + +module.exports = { + loadPngFile, + mse, + psnr, +};